diff --git a/CMakeLists.txt b/CMakeLists.txt index 418a5d9c2..e19d3147b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,6 +43,14 @@ set(SLIC3R_GTK "2" CACHE STRING "GTK version to use with wxWidgets on Linux") set(IS_CROSS_COMPILE FALSE) +if (SLIC3R_STATIC) + # Prefer config scripts over find modules. This is helpful when building with + # the static dependencies. Many libraries have their own export scripts + # while having a Find module in standard cmake installation. + # (e.g. CURL) + set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON) +endif () + if (APPLE) set(CMAKE_FIND_FRAMEWORK LAST) set(CMAKE_FIND_APPBUNDLE LAST) @@ -437,6 +445,14 @@ include_directories(BEFORE SYSTEM ${EIGEN3_INCLUDE_DIR}) # no matter what. find_package(EXPAT REQUIRED) +add_library(libexpat INTERFACE) + +if (TARGET EXPAT::EXPAT ) + target_link_libraries(libexpat INTERFACE EXPAT::EXPAT) +elseif(TARGET expat::expat) + target_link_libraries(libexpat INTERFACE expat::expat) +endif () + find_package(PNG REQUIRED) set(OpenGL_GL_PREFERENCE "LEGACY") diff --git a/deps/wxWidgets/wxWidgets.cmake b/deps/wxWidgets/wxWidgets.cmake index 29374974b..95f54c1a2 100644 --- a/deps/wxWidgets/wxWidgets.cmake +++ b/deps/wxWidgets/wxWidgets.cmake @@ -13,8 +13,8 @@ if (UNIX AND NOT APPLE) # wxWidgets will not use char as the underlying type for endif() prusaslicer_add_cmake_project(wxWidgets - URL https://github.com/prusa3d/wxWidgets/archive/2a0b365df947138c513a888d707d46248d78a341.zip - URL_HASH SHA256=9ab05cd5179196fad4ae702c78eaae9418e73a402cfd390f7438e469b13eb735 + URL https://github.com/prusa3d/wxWidgets/archive/34b524f8d5134a40a90d93a16360d533af2676ae.zip + URL_HASH SHA256=e76ca0dd998905c4dbb86f41f264e6e0468504dc2398f7e7e3bba8dc37de2f45 DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} dep_TIFF dep_JPEG dep_NanoSVG CMAKE_ARGS -DwxBUILD_PRECOMP=ON @@ -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/collapse_btn.svg b/resources/icons/collapse_btn.svg new file mode 100644 index 000000000..4ee221a44 --- /dev/null +++ b/resources/icons/collapse_btn.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/icons/cut_.svg b/resources/icons/cut_.svg new file mode 100644 index 000000000..a7f462bb9 --- /dev/null +++ b/resources/icons/cut_.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/icons/cut_connectors.svg b/resources/icons/cut_connectors.svg new file mode 100644 index 000000000..8cd03aa06 --- /dev/null +++ b/resources/icons/cut_connectors.svg @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/resources/icons/expand_btn.svg b/resources/icons/expand_btn.svg new file mode 100644 index 000000000..32d7f9959 --- /dev/null +++ b/resources/icons/expand_btn.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/resources/icons/measure.svg b/resources/icons/measure.svg new file mode 100644 index 000000000..3ea137a1e --- /dev/null +++ b/resources/icons/measure.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + diff --git a/resources/localization/list.txt b/resources/localization/list.txt index bc8c9f272..dc30cdcc8 100644 --- a/resources/localization/list.txt +++ b/resources/localization/list.txt @@ -32,6 +32,7 @@ src/slic3r/GUI/Gizmos/GLGizmoSeam.hpp src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp src/slic3r/GUI/Gizmos/GLGizmosManager.cpp src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp src/slic3r/GUI/GUI.cpp diff --git a/resources/profiles/Anker.idx b/resources/profiles/Anker.idx new file mode 100644 index 000000000..4a988bbdf --- /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 000000000..c7408f6e4 --- /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 000000000..72e12a23c 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 000000000..8b04342e3 --- /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 000000000..e13731920 Binary files /dev/null and b/resources/profiles/Anker/M5_thumbnail.png differ diff --git a/src/PrusaSlicer.cpp b/src/PrusaSlicer.cpp index ecb157218..500028525 100644 --- a/src/PrusaSlicer.cpp +++ b/src/PrusaSlicer.cpp @@ -426,7 +426,9 @@ int CLI::run(int argc, char **argv) o->cut(Z, m_config.opt_float("cut"), &out); } #else - model.objects.front()->cut(0, m_config.opt_float("cut"), ModelObjectCutAttribute::KeepLower | ModelObjectCutAttribute::KeepUpper | ModelObjectCutAttribute::FlipLower); +// model.objects.front()->cut(0, m_config.opt_float("cut"), ModelObjectCutAttribute::KeepLower | ModelObjectCutAttribute::KeepUpper | ModelObjectCutAttribute::FlipLower); + model.objects.front()->cut(0, Geometry::assemble_transform(m_config.opt_float("cut")* Vec3d::UnitZ()), + ModelObjectCutAttribute::KeepLower | ModelObjectCutAttribute::KeepUpper | ModelObjectCutAttribute::PlaceOnCutUpper); #endif model.delete_object(size_t(0)); } diff --git a/src/imgui/imconfig.h b/src/imgui/imconfig.h index f2c3ef083..6dabace5b 100644 --- a/src/imgui/imconfig.h +++ b/src/imgui/imconfig.h @@ -140,6 +140,7 @@ namespace ImGui const wchar_t CancelButton = 0x14; const wchar_t CancelHoverButton = 0x15; // const wchar_t VarLayerHeightMarker = 0x16; + const wchar_t RevertButton = 0x16; const wchar_t RightArrowButton = 0x18; const wchar_t RightArrowHoverButton = 0x19; @@ -156,19 +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 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 a118f6ef3..79e0e6355 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -180,6 +180,9 @@ set(SLIC3R_SOURCES MultiMaterialSegmentation.hpp MeshNormals.hpp MeshNormals.cpp + Measure.hpp + Measure.cpp + MeasureUtils.hpp CustomGCode.cpp CustomGCode.hpp Arrange.hpp @@ -253,6 +256,7 @@ set(SLIC3R_SOURCES Surface.hpp SurfaceCollection.cpp SurfaceCollection.hpp + SurfaceMesh.hpp SVG.cpp SVG.hpp Technologies.hpp diff --git a/src/libslic3r/ExPolygon.cpp b/src/libslic3r/ExPolygon.cpp index 651be062a..6d9e21f28 100644 --- a/src/libslic3r/ExPolygon.cpp +++ b/src/libslic3r/ExPolygon.cpp @@ -19,6 +19,13 @@ void ExPolygon::scale(double factor) hole.scale(factor); } +void ExPolygon::scale(double factor_x, double factor_y) +{ + contour.scale(factor_x, factor_y); + for (Polygon &hole : holes) + hole.scale(factor_x, factor_y); +} + void ExPolygon::translate(const Point &p) { contour.translate(p); diff --git a/src/libslic3r/ExPolygon.hpp b/src/libslic3r/ExPolygon.hpp index a319d003c..d21b4db12 100644 --- a/src/libslic3r/ExPolygon.hpp +++ b/src/libslic3r/ExPolygon.hpp @@ -37,6 +37,7 @@ public: void clear() { contour.points.clear(); holes.clear(); } void scale(double factor); + void scale(double factor_x, double factor_y); void translate(double x, double y) { this->translate(Point(coord_t(x), coord_t(y))); } void translate(const Point &vector); void rotate(double angle); diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index 5caff6d3a..f2056b8bd 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -77,6 +77,7 @@ const std::string LAYER_CONFIG_RANGES_FILE = "Metadata/Prusa_Slicer_layer_config const std::string SLA_SUPPORT_POINTS_FILE = "Metadata/Slic3r_PE_sla_support_points.txt"; const std::string SLA_DRAIN_HOLES_FILE = "Metadata/Slic3r_PE_sla_drain_holes.txt"; const std::string CUSTOM_GCODE_PER_PRINT_Z_FILE = "Metadata/Prusa_Slicer_custom_gcode_per_print_z.xml"; +const std::string CUT_INFORMATION_FILE = "Metadata/Prusa_Slicer_cut_information.xml"; static constexpr const char* MODEL_TAG = "model"; static constexpr const char* RESOURCES_TAG = "resources"; @@ -408,6 +409,19 @@ namespace Slic3r { VolumeMetadataList volumes; }; + struct CutObjectInfo + { + struct Connector + { + int volume_id; + int type; + float r_tolerance; + float h_tolerance; + }; + CutObjectBase id; + std::vector connectors; + }; + // Map from a 1 based 3MF object ID to a 0 based ModelObject index inside m_model->objects. typedef std::map IdToModelObjectMap; typedef std::map IdToAliasesMap; @@ -416,6 +430,7 @@ namespace Slic3r { typedef std::map IdToGeometryMap; typedef std::map> IdToLayerHeightsProfileMap; typedef std::map IdToLayerConfigRangesMap; + typedef std::map IdToCutObjectInfoMap; typedef std::map> IdToSlaSupportPointsMap; typedef std::map> IdToSlaDrainHolesMap; @@ -443,6 +458,7 @@ namespace Slic3r { IdToGeometryMap m_geometries; CurrentConfig m_curr_config; IdToMetadataMap m_objects_metadata; + IdToCutObjectInfoMap m_cut_object_infos; IdToLayerHeightsProfileMap m_layer_heights_profiles; IdToLayerConfigRangesMap m_layer_config_ranges; IdToSlaSupportPointsMap m_sla_support_points; @@ -474,6 +490,7 @@ namespace Slic3r { bool _load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions); bool _extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); + void _extract_cut_information_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions); void _extract_layer_heights_profile_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); void _extract_layer_config_ranges_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions); void _extract_sla_support_points_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); @@ -676,6 +693,10 @@ namespace Slic3r { // extract slic3r layer heights profile file _extract_layer_heights_profile_config_from_archive(archive, stat); } + else if (boost::algorithm::iequals(name, CUT_INFORMATION_FILE)) { + // extract slic3r layer config ranges file + _extract_cut_information_from_archive(archive, stat, config_substitutions); + } else if (boost::algorithm::iequals(name, LAYER_CONFIG_RANGES_FILE)) { // extract slic3r layer config ranges file _extract_layer_config_ranges_from_archive(archive, stat, config_substitutions); @@ -818,6 +839,19 @@ namespace Slic3r { if (!_generate_volumes(*model_object, obj_geometry->second, *volumes_ptr, config_substitutions)) return false; + + // Apply cut information for object if any was loaded + // m_cut_object_ids are indexed by a 1 based model object index. + IdToCutObjectInfoMap::iterator cut_object_info = m_cut_object_infos.find(object.second + 1); + if (cut_object_info != m_cut_object_infos.end()) { + model_object->cut_id = cut_object_info->second.id; + + for (auto connector : cut_object_info->second.connectors) { + assert(0 <= connector.volume_id && connector.volume_id <= int(model_object->volumes.size())); + model_object->volumes[connector.volume_id]->cut_info = + ModelVolume::CutInfo(CutConnectorType(connector.type), connector.r_tolerance, connector.h_tolerance, true); + } + } } // If instances contain a single volume, the volume offset should be 0,0,0 @@ -944,6 +978,65 @@ namespace Slic3r { return true; } + void _3MF_Importer::_extract_cut_information_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions) + { + if (stat.m_uncomp_size > 0) { + std::string buffer((size_t)stat.m_uncomp_size, 0); + mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); + if (res == 0) { + add_error("Error while reading cut information data to buffer"); + return; + } + + std::istringstream iss(buffer); // wrap returned xml to istringstream + pt::ptree objects_tree; + pt::read_xml(iss, objects_tree); + + for (const auto& object : objects_tree.get_child("objects")) { + pt::ptree object_tree = object.second; + int obj_idx = object_tree.get(".id", -1); + if (obj_idx <= 0) { + add_error("Found invalid object id"); + continue; + } + + IdToCutObjectInfoMap::iterator object_item = m_cut_object_infos.find(obj_idx); + if (object_item != m_cut_object_infos.end()) { + add_error("Found duplicated cut_object_id"); + continue; + } + + CutObjectBase cut_id; + std::vector connectors; + + for (const auto& obj_cut_info : object_tree) { + if (obj_cut_info.first == "cut_id") { + pt::ptree cut_id_tree = obj_cut_info.second; + cut_id = CutObjectBase(ObjectID( cut_id_tree.get(".id")), + cut_id_tree.get(".check_sum"), + cut_id_tree.get(".connectors_cnt")); + } + if (obj_cut_info.first == "connectors") { + pt::ptree cut_connectors_tree = obj_cut_info.second; + for (const auto& cut_connector : cut_connectors_tree) { + if (cut_connector.first != "connector") + continue; + pt::ptree connector_tree = cut_connector.second; + CutObjectInfo::Connector connector = {connector_tree.get(".volume_id"), + connector_tree.get(".type"), + connector_tree.get(".r_tolerance"), + connector_tree.get(".h_tolerance")}; + connectors.emplace_back(connector); + } + } + } + + CutObjectInfo cut_info {cut_id, connectors}; + m_cut_object_infos.insert({ obj_idx, cut_info }); + } + } + } + void _3MF_Importer::_extract_print_config_from_archive( mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, @@ -2219,6 +2312,7 @@ namespace Slic3r { bool _add_object_to_model_stream(mz_zip_writer_staged_context &context, unsigned int& object_id, ModelObject& object, BuildItemsList& build_items, VolumeToOffsetsMap& volumes_offsets); bool _add_mesh_to_object_stream(mz_zip_writer_staged_context &context, ModelObject& object, VolumeToOffsetsMap& volumes_offsets); bool _add_build_to_model_stream(std::stringstream& stream, const BuildItemsList& build_items); + bool _add_cut_information_file_to_archive(mz_zip_archive& archive, Model& model); bool _add_layer_height_profile_file_to_archive(mz_zip_archive& archive, Model& model); bool _add_layer_config_ranges_file_to_archive(mz_zip_archive& archive, Model& model); bool _add_sla_support_points_file_to_archive(mz_zip_archive& archive, Model& model); @@ -2281,6 +2375,15 @@ namespace Slic3r { return false; } + // Adds file with information for object cut ("Metadata/Slic3r_PE_cut_information.txt"). + // All information for object cut of all ModelObjects are stored here, indexed by 1 based index of the ModelObject in Model. + // The index differes from the index of an object ID of an object instance of a 3MF file! + if (!_add_cut_information_file_to_archive(archive, model)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + // Adds layer height profile file ("Metadata/Slic3r_PE_layer_heights_profile.txt"). // All layer height profiles of all ModelObjects are stored here, indexed by 1 based index of the ModelObject in Model. // The index differes from the index of an object ID of an object instance of a 3MF file! @@ -2781,6 +2884,67 @@ namespace Slic3r { return true; } + bool _3MF_Exporter::_add_cut_information_file_to_archive(mz_zip_archive& archive, Model& model) + { + std::string out = ""; + pt::ptree tree; + + unsigned int object_cnt = 0; + for (const ModelObject* object : model.objects) { + object_cnt++; + pt::ptree& obj_tree = tree.add("objects.object", ""); + + obj_tree.put(".id", object_cnt); + + // Store info for cut_id + pt::ptree& cut_id_tree = obj_tree.add("cut_id", ""); + + // store cut_id atributes + cut_id_tree.put(".id", object->cut_id.id().id); + cut_id_tree.put(".check_sum", object->cut_id.check_sum()); + cut_id_tree.put(".connectors_cnt", object->cut_id.connectors_cnt()); + + int volume_idx = -1; + for (const ModelVolume* volume : object->volumes) { + ++volume_idx; + if (volume->is_cut_connector()) { + pt::ptree& connectors_tree = obj_tree.add("connectors.connector", ""); + connectors_tree.put(".volume_id", volume_idx); + connectors_tree.put(".type", int(volume->cut_info.connector_type)); + connectors_tree.put(".r_tolerance", volume->cut_info.radius_tolerance); + connectors_tree.put(".h_tolerance", volume->cut_info.height_tolerance); + } + } + } + + if (!tree.empty()) { + std::ostringstream oss; + pt::write_xml(oss, tree); + out = oss.str(); + + // Post processing("beautification") of the output string for a better preview + boost::replace_all(out, ">\n \n ", ">\n "); + boost::replace_all(out, ">\n ", ">\n "); + boost::replace_all(out, ">\n ", ">\n "); + boost::replace_all(out, ">", ">\n "); + // OR just + boost::replace_all(out, "><", ">\n<"); + } + + if (!out.empty()) { + if (!mz_zip_writer_add_mem(&archive, CUT_INFORMATION_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { + add_error("Unable to add cut information file to archive"); + return false; + } + } + + return true; + } + bool _3MF_Exporter::_add_layer_height_profile_file_to_archive(mz_zip_archive& archive, Model& model) { assert(is_decimal_separator_point()); diff --git a/src/libslic3r/Measure.cpp b/src/libslic3r/Measure.cpp new file mode 100644 index 000000000..f64b79e22 --- /dev/null +++ b/src/libslic3r/Measure.cpp @@ -0,0 +1,1082 @@ +#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); + }; + + 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()); + } + + assert(std::none_of(m_face_to_plane.begin(), m_face_to_plane.end(), [](size_t val) { return val == size_t(-1); })); + + SurfaceMesh sm(m_its); + for (int plane_id=0; plane_id < int(m_planes.size()); ++plane_id) { + //int plane_id = 5; { + 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); + while ( (int)m_face_to_plane[sm.face(he)] == plane_id && he != he_orig) + he = sm.next_around_target(he); + he = sm.opposite(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)); + 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(); + } + } + } + + m_planes.erase(std::remove_if(m_planes.begin(), m_planes.end(), + [](const PlaneData& p) { return p.borders.empty(); }), + m_planes.end()); +} + + + + + + +void MeasuringImpl::extract_features() +{ + auto N_to_angle = [](double N) -> double { return 2.*M_PI / N; }; + constexpr double polygon_upper_threshold = N_to_angle(4.5); + constexpr double polygon_lower_threshold = N_to_angle(8.5); + 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) { + assert(border.size() > 1); + int start_idx = -1; + + // 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.squaredNorm()); + } + assert(border.size() == angles.size()); + assert(border.size() == lengths.size()); + + + bool circle = false; + std::vector circles; + std::vector> circles_idxs; + 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) { + // Add the circle and remember indices into borders. + const auto& [center, radius] = get_center_and_radius(border, start_idx, i, trafo); + circles_idxs.emplace_back(start_idx, i); + circles.emplace_back(SurfaceFeature(SurfaceFeatureType::Circle, center, plane.normal, std::nullopt, radius)); + circle = false; + } + } + } + + // Some of the "circles" may actually be polygons. 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) { + assert(circles_idxs[i].first + 1 < angles.size() - 1); // Check that this is internal point of the circle, not the first, not the last. + double angle = angles[circles_idxs[i].first + 1]; + if (angle > polygon_lower_threshold) { + if (angle < polygon_upper_threshold) { + 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) + plane.surface_features.emplace_back(SurfaceFeature(SurfaceFeatureType::Edge, + border[j - 1], border[j], std::make_optional(center))); + } else { + // This will be handled just like a regular edge. + circles_idxs.erase(circles_idxs.begin() + i); + } + circles.erase(circles.begin() + i); + } + } + + + + + + + // We have the circles. Now go around again and pick edges. + int cidx = 0; // index of next circle in the way + for (int i=1; i (int)circles_idxs[cidx].first) + i = circles_idxs[cidx++].second; + else + plane.surface_features.emplace_back(SurfaceFeature(SurfaceFeatureType::Edge, border[i - 1], border[i])); + } + + // FIXME Throw away / do not create edges which are parts of circles or + // which lead to circle points (unless they belong to the same plane.) + + // FIXME Check and merge first and last circle if needed. + + // Now move the circles into the feature list. + assert(std::all_of(circles.begin(), circles.end(), [](const SurfaceFeature& f) { + return f.get_type() == SurfaceFeatureType::Circle; + })); + plane.surface_features.insert(plane.surface_features.end(), std::make_move_iterator(circles.begin()), + std::make_move_iterator(circles.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; + + /////////////////////////////////////////////////////////////////////////// + } 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()); + } + + // validation + if (result.distance_infinite.has_value() && result.distance_infinite->dist < EPSILON) + result.distance_infinite.reset(); + if (result.distance_strict.has_value() && result.distance_strict->dist < EPSILON) + result.distance_strict.reset(); + if (result.angle.has_value() && std::abs(result.angle->angle) < EPSILON) + result.angle.reset(); + + 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 000000000..d1a0e3866 --- /dev/null +++ b/src/libslic3r/Measure.hpp @@ -0,0 +1,197 @@ +#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(); + } +}; + +// 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 000000000..0ab4ac121 --- /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 9f3c142f8..52ddbea9b 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -464,12 +464,25 @@ static constexpr const double volume_threshold_inches = 9.0; // 9 = 3*3*3; bool Model::looks_like_imperial_units() const { - if (this->objects.size() == 0) + if (this->objects.empty()) return false; for (ModelObject* obj : this->objects) - if (obj->get_object_stl_stats().volume < volume_threshold_inches) - return true; + if (obj->get_object_stl_stats().volume < volume_threshold_inches) { + if (!obj->is_cut()) + return true; + bool all_cut_parts_look_like_imperial_units = true; + for (ModelObject* obj_other : this->objects) { + if (obj_other == obj) + continue; + if (obj_other->cut_id.is_equal(obj->cut_id) && obj_other->get_object_stl_stats().volume >= volume_threshold_inches) { + all_cut_parts_look_like_imperial_units = false; + break; + } + } + if (all_cut_parts_look_like_imperial_units) + return true; + } return false; } @@ -613,6 +626,7 @@ ModelObject& ModelObject::assign_copy(const ModelObject &rhs) this->layer_height_profile = rhs.layer_height_profile; this->printable = rhs.printable; this->origin_translation = rhs.origin_translation; + this->cut_id.copy(rhs.cut_id); m_bounding_box = rhs.m_bounding_box; m_bounding_box_valid = rhs.m_bounding_box_valid; m_raw_bounding_box = rhs.m_raw_bounding_box; @@ -715,6 +729,7 @@ ModelVolume* ModelObject::add_volume(const ModelVolume &other, ModelVolumeType t ModelVolume* v = new ModelVolume(this, other); if (type != ModelVolumeType::INVALID && v->type() != type) v->set_type(type); + v->cut_info = other.cut_info; this->volumes.push_back(v); // The volume should already be centered at this point of time when copying shared pointers of the triangle mesh and convex hull. // v->center_geometry_after_creation(); @@ -1189,34 +1204,355 @@ size_t ModelObject::parts_count() const return num; } -ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, ModelObjectCutAttributes attributes) +bool ModelObject::has_connectors() const { - if (! attributes.has(ModelObjectCutAttribute::KeepUpper) && ! attributes.has(ModelObjectCutAttribute::KeepLower)) + assert(is_cut()); + for (const ModelVolume* v : this->volumes) + if (v->cut_info.is_connector) + return true; + + return false; +} + +indexed_triangle_set ModelObject::get_connector_mesh(CutConnectorAttributes connector_attributes) +{ + indexed_triangle_set connector_mesh; + + int sectorCount {1}; + switch (CutConnectorShape(connector_attributes.shape)) { + case CutConnectorShape::Triangle: + sectorCount = 3; + break; + case CutConnectorShape::Square: + sectorCount = 4; + break; + case CutConnectorShape::Circle: + sectorCount = 360; + break; + case CutConnectorShape::Hexagon: + sectorCount = 6; + break; + default: + break; + } + + if (connector_attributes.style == CutConnectorStyle::Prizm) + connector_mesh = its_make_cylinder(1.0, 1.0, (2 * PI / sectorCount)); + else if (connector_attributes.type == CutConnectorType::Plug) + connector_mesh = its_make_cone(1.0, 1.0, (2 * PI / sectorCount)); + else + connector_mesh = its_make_frustum_dowel(1.0, 1.0, sectorCount); + + return connector_mesh; +} + +void ModelObject::apply_cut_connectors(const std::string& new_name) +{ + if (cut_connectors.empty()) + return; + + using namespace Geometry; + + size_t connector_id = cut_id.connectors_cnt(); + for (const CutConnector& connector : cut_connectors) { + TriangleMesh mesh = TriangleMesh(get_connector_mesh(connector.attribs)); + // Mesh will be centered when loading. + ModelVolume* new_volume = add_volume(std::move(mesh), ModelVolumeType::NEGATIVE_VOLUME); + + // Transform the new modifier to be aligned inside the instance + new_volume->set_transformation(assemble_transform(connector.pos) * connector.rotation_m * + scale_transform(Vec3f(connector.radius, connector.radius, connector.height).cast())); + + new_volume->cut_info = { connector.attribs.type, connector.radius_tolerance, connector.height_tolerance }; + new_volume->name = new_name + "-" + std::to_string(++connector_id); + } + cut_id.increase_connectors_cnt(cut_connectors.size()); + + // delete all connectors + cut_connectors.clear(); +} + +void ModelObject::invalidate_cut() +{ + this->cut_id.invalidate(); + for (ModelVolume* volume : this->volumes) + volume->invalidate_cut_info(); +} + +void ModelObject::synchronize_model_after_cut() +{ + for (ModelObject* obj : m_model->objects) { + if (obj == this || obj->cut_id.is_equal(this->cut_id)) + continue; + if (obj->is_cut() && obj->cut_id.has_same_id(this->cut_id)) + obj->cut_id.copy(this->cut_id); + } +} + +void ModelObject::apply_cut_attributes(ModelObjectCutAttributes attributes) +{ + // we don't save cut information, if result will not contains all parts of initial object + if (!attributes.has(ModelObjectCutAttribute::KeepUpper) || !attributes.has(ModelObjectCutAttribute::KeepLower)) + return; + + if (cut_id.id().invalid()) + cut_id.init(); + { + int cut_obj_cnt = -1; + if (attributes.has(ModelObjectCutAttribute::KeepUpper)) cut_obj_cnt++; + if (attributes.has(ModelObjectCutAttribute::KeepLower)) cut_obj_cnt++; + if (attributes.has(ModelObjectCutAttribute::CreateDowels)) cut_obj_cnt++; + if (cut_obj_cnt > 0) + cut_id.increase_check_sum(size_t(cut_obj_cnt)); + } +} + +void ModelObject::clone_for_cut(ModelObject** obj) +{ + (*obj) = ModelObject::new_clone(*this); + (*obj)->set_model(nullptr); + (*obj)->sla_support_points.clear(); + (*obj)->sla_drain_holes.clear(); + (*obj)->sla_points_status = sla::PointsStatus::NoPoints; + (*obj)->clear_volumes(); + (*obj)->input_file.clear(); +} + +void ModelVolume::reset_extra_facets() +{ + this->supported_facets.reset(); + this->seam_facets.reset(); + this->mmu_segmentation_facets.reset(); +} + +void ModelVolume::apply_tolerance() +{ + assert(cut_info.is_connector); + if (cut_info.is_processed) + return; + + Vec3d sf = get_scaling_factor(); +/* + // correct Z offset in respect to the new size + Vec3d pos = vol->get_offset(); + pos[Z] += sf[Z] * 0.5 * vol->cut_info.height_tolerance; + vol->set_offset(pos); +*/ + // make a "hole" wider + sf[X] *= 1. + double(cut_info.radius_tolerance); + sf[Y] *= 1. + double(cut_info.radius_tolerance); + + // make a "hole" dipper + sf[Z] *= 1. + double(cut_info.height_tolerance); + + set_scaling_factor(sf); +} + +void ModelObject::process_connector_cut(ModelVolume* volume, ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower, + std::vector& dowels, Vec3d& local_dowels_displace) +{ + assert(volume->cut_info.is_connector); + volume->cut_info.set_processed(); + + const auto volume_matrix = volume->get_matrix(); + + // ! Don't apply instance transformation for the conntectors. + // This transformation is already there + if (attributes.has(ModelObjectCutAttribute::KeepUpper)) { + ModelVolume* vol = upper->add_volume(*volume); + vol->set_transformation(volume_matrix); + vol->apply_tolerance(); + } + if (attributes.has(ModelObjectCutAttribute::KeepLower)) { + ModelVolume* vol = lower->add_volume(*volume); + vol->set_transformation(volume_matrix); + + if (volume->cut_info.connector_type == CutConnectorType::Dowel) + vol->apply_tolerance(); + else + // for lower part change type of connector from NEGATIVE_VOLUME to MODEL_PART if this connector is a plug + vol->set_type(ModelVolumeType::MODEL_PART); + } + if (volume->cut_info.connector_type == CutConnectorType::Dowel && + attributes.has(ModelObjectCutAttribute::CreateDowels)) { + ModelObject* dowel{ nullptr }; + // Clone the object to duplicate instances, materials etc. + clone_for_cut(&dowel); + + // add one more solid part same as connector if this connector is a dowel + ModelVolume* vol = dowel->add_volume(*volume); + vol->set_type(ModelVolumeType::MODEL_PART); + + // But discard rotation and Z-offset for this volume + vol->set_rotation(Vec3d::Zero()); + vol->set_offset(Z, 0.0); + + // Compute the displacement (in instance coordinates) to be applied to place the dowels + local_dowels_displace = lower->full_raw_mesh_bounding_box().size().cwiseProduct(Vec3d(1.0, 1.0, 0.0)); + + dowels.push_back(dowel); + } +} + +void ModelObject::process_modifier_cut(ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& inverse_cut_matrix, + ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower) +{ + const auto volume_matrix = instance_matrix * volume->get_matrix(); + + // Modifiers are not cut, but we still need to add the instance transformation + // to the modifier volume transformation to preserve their shape properly. + volume->set_transformation(Geometry::Transformation(volume_matrix)); + + // Some logic for the negative volumes/connectors. Add only needed modifiers + auto bb = volume->mesh().transformed_bounding_box(inverse_cut_matrix * volume_matrix); + bool is_crossed_by_cut = bb.min[Z] <= 0 && bb.max[Z] >= 0; + if (attributes.has(ModelObjectCutAttribute::KeepUpper) && (bb.min[Z] >= 0 || is_crossed_by_cut)) + upper->add_volume(*volume); + if (attributes.has(ModelObjectCutAttribute::KeepLower) && (bb.max[Z] <= 0 || is_crossed_by_cut)) + lower->add_volume(*volume); +} + +static void add_cut_volume(TriangleMesh& mesh, ModelObject* object, const ModelVolume* src_volume, const Transform3d& cut_matrix) +{ + if (mesh.empty()) + return; + + mesh.transform(cut_matrix); + ModelVolume* vol = object->add_volume(mesh); + + vol->name = src_volume->name; + // Don't copy the config's ID. + vol->config.assign_config(src_volume->config); + assert(vol->config.id().valid()); + assert(vol->config.id() != src_volume->config.id()); + vol->set_material(src_volume->material_id(), *src_volume->material()); + vol->cut_info = src_volume->cut_info; +} + +void ModelObject::process_solid_part_cut(ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& cut_matrix, + ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower, Vec3d& local_displace) +{ + const auto volume_matrix = volume->get_matrix(); + + using namespace Geometry; + + const Transformation cut_transformation = Transformation(cut_matrix); + const Transform3d invert_cut_matrix = cut_transformation.get_rotation_matrix().inverse() * assemble_transform(-1 * cut_transformation.get_offset()); + + // Transform the mesh by the combined transformation matrix. + // Flip the triangles in case the composite transformation is left handed. + TriangleMesh mesh(volume->mesh()); + mesh.transform(invert_cut_matrix * instance_matrix * volume_matrix, true); + + volume->reset_mesh(); + // Reset volume transformation except for offset + const Vec3d offset = volume->get_offset(); + volume->set_transformation(Geometry::Transformation()); + volume->set_offset(offset); + + // Perform cut + + TriangleMesh upper_mesh, lower_mesh; + { + indexed_triangle_set upper_its, lower_its; + cut_mesh(mesh.its, 0.0f, &upper_its, &lower_its); + if (attributes.has(ModelObjectCutAttribute::KeepUpper)) + upper_mesh = TriangleMesh(upper_its); + if (attributes.has(ModelObjectCutAttribute::KeepLower)) + lower_mesh = TriangleMesh(lower_its); + } + + // Add required cut parts to the objects + + if (attributes.has(ModelObjectCutAttribute::KeepUpper)) + add_cut_volume(upper_mesh, upper, volume, cut_matrix); + + if (attributes.has(ModelObjectCutAttribute::KeepLower) && !lower_mesh.empty()) { + add_cut_volume(lower_mesh, lower, volume, cut_matrix); + + // Compute the displacement (in instance coordinates) to be applied to place the upper parts + // The upper part displacement is set to half of the lower part bounding box + // this is done in hope at least a part of the upper part will always be visible and draggable + local_displace = lower->full_raw_mesh_bounding_box().size().cwiseProduct(Vec3d(-0.5, -0.5, 0.0)); + } +} + +static void invalidate_translations(ModelObject* object, const ModelInstance* src_instance) +{ + if (!object->origin_translation.isApprox(Vec3d::Zero()) && src_instance->get_offset().isApprox(Vec3d::Zero())) { + object->center_around_origin(); + object->translate_instances(-object->origin_translation); + object->origin_translation = Vec3d::Zero(); + } + else { + object->invalidate_bounding_box(); + object->center_around_origin(); + } +} + +static void reset_instance_transformation(ModelObject* object, size_t src_instance_idx, const Transform3d& cut_matrix, + bool place_on_cut = false, bool flip = false, Vec3d local_displace = Vec3d::Zero()) +{ + using namespace Geometry; + + // Reset instance transformation except offset and Z-rotation + + for (size_t i = 0; i < object->instances.size(); ++i) { + auto& obj_instance = object->instances[i]; + const Vec3d offset = obj_instance->get_offset(); + const double rot_z = obj_instance->get_rotation().z(); + + obj_instance->set_transformation(Transformation()); + + const Vec3d displace = local_displace.isApprox(Vec3d::Zero()) ? Vec3d::Zero() : + assemble_transform(Vec3d::Zero(), obj_instance->get_rotation()) * local_displace; + obj_instance->set_offset(offset + displace); + + Vec3d rotation = Vec3d::Zero(); + if (!flip && !place_on_cut) { + if ( i != src_instance_idx) + rotation[Z] = rot_z; + } + else { + Transform3d rotation_matrix = Transform3d::Identity(); + if (flip) + rotation_matrix = rotation_transform(PI * Vec3d::UnitX()); + + if (place_on_cut) + rotation_matrix = rotation_matrix * Transformation(cut_matrix).get_rotation_matrix().inverse(); + + if (i != src_instance_idx) + rotation_matrix = rotation_transform(rot_z * Vec3d::UnitZ()) * rotation_matrix; + + rotation = Transformation(rotation_matrix).get_rotation(); + } + + obj_instance->set_rotation(rotation); + } +} + +ModelObjectPtrs ModelObject::cut(size_t instance, const Transform3d& cut_matrix, ModelObjectCutAttributes attributes) +{ + if (!attributes.has(ModelObjectCutAttribute::KeepUpper) && !attributes.has(ModelObjectCutAttribute::KeepLower)) return {}; BOOST_LOG_TRIVIAL(trace) << "ModelObject::cut - start"; + // apply cut attributes for object + apply_cut_attributes(attributes); + // Clone the object to duplicate instances, materials etc. - ModelObject* upper = attributes.has(ModelObjectCutAttribute::KeepUpper) ? ModelObject::new_clone(*this) : nullptr; - ModelObject* lower = attributes.has(ModelObjectCutAttribute::KeepLower) ? ModelObject::new_clone(*this) : nullptr; + ModelObject* upper{ nullptr }; + if (attributes.has(ModelObjectCutAttribute::KeepUpper)) + clone_for_cut(&upper); - if (attributes.has(ModelObjectCutAttribute::KeepUpper)) { - upper->set_model(nullptr); - upper->sla_support_points.clear(); - upper->sla_drain_holes.clear(); - upper->sla_points_status = sla::PointsStatus::NoPoints; - upper->clear_volumes(); - upper->input_file.clear(); - } + ModelObject* lower{ nullptr }; + if (attributes.has(ModelObjectCutAttribute::KeepLower)) + clone_for_cut(&lower); - if (attributes.has(ModelObjectCutAttribute::KeepLower)) { - lower->set_model(nullptr); - lower->sla_support_points.clear(); - lower->sla_drain_holes.clear(); - lower->sla_points_status = sla::PointsStatus::NoPoints; - lower->clear_volumes(); - lower->input_file.clear(); - } + std::vector dowels; + + using namespace Geometry; // Because transformations are going to be applied to meshes directly, // we reset transformation of all instances and volumes, @@ -1224,128 +1560,72 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, ModelObjectCutAttr // in the transformation matrix and not applied to the mesh transform. // const auto instance_matrix = instances[instance]->get_matrix(true); - const auto instance_matrix = Geometry::assemble_transform( + const auto instance_matrix = assemble_transform( Vec3d::Zero(), // don't apply offset - instances[instance]->get_rotation().cwiseProduct(Vec3d(1.0, 1.0, 0.0)), // don't apply Z-rotation + instances[instance]->get_rotation(), instances[instance]->get_scaling_factor(), instances[instance]->get_mirror() ); - z -= instances[instance]->get_offset().z(); + const Transformation cut_transformation = Transformation(cut_matrix); + const Transform3d inverse_cut_matrix = cut_transformation.get_rotation_matrix().inverse() * assemble_transform(-1. * cut_transformation.get_offset()); // Displacement (in instance coordinates) to be applied to place the upper parts Vec3d local_displace = Vec3d::Zero(); + Vec3d local_dowels_displace = Vec3d::Zero(); - for (ModelVolume *volume : volumes) { - const auto volume_matrix = volume->get_matrix(); + for (ModelVolume* volume : volumes) { + volume->reset_extra_facets(); - volume->supported_facets.reset(); - volume->seam_facets.reset(); - volume->mmu_segmentation_facets.reset(); - - if (! volume->is_model_part()) { - // Modifiers are not cut, but we still need to add the instance transformation - // to the modifier volume transformation to preserve their shape properly. - - volume->set_transformation(Geometry::Transformation(instance_matrix * volume_matrix)); - - if (attributes.has(ModelObjectCutAttribute::KeepUpper)) - upper->add_volume(*volume); - if (attributes.has(ModelObjectCutAttribute::KeepLower)) - lower->add_volume(*volume); - } - else if (! volume->mesh().empty()) { - // Transform the mesh by the combined transformation matrix. - // Flip the triangles in case the composite transformation is left handed. - TriangleMesh mesh(volume->mesh()); - mesh.transform(instance_matrix * volume_matrix, true); - volume->reset_mesh(); - // Reset volume transformation except for offset - const Vec3d offset = volume->get_offset(); - volume->set_transformation(Geometry::Transformation()); - volume->set_offset(offset); - - // Perform cut - TriangleMesh upper_mesh, lower_mesh; - { - indexed_triangle_set upper_its, lower_its; - cut_mesh(mesh.its, float(z), &upper_its, &lower_its); - if (attributes.has(ModelObjectCutAttribute::KeepUpper)) - upper_mesh = TriangleMesh(upper_its); - if (attributes.has(ModelObjectCutAttribute::KeepLower)) - lower_mesh = TriangleMesh(lower_its); - } - - if (attributes.has(ModelObjectCutAttribute::KeepUpper) && ! upper_mesh.empty()) { - ModelVolume* vol = upper->add_volume(upper_mesh); - vol->name = volume->name; - // Don't copy the config's ID. - vol->config.assign_config(volume->config); - assert(vol->config.id().valid()); - assert(vol->config.id() != volume->config.id()); - vol->set_material(volume->material_id(), *volume->material()); - } - if (attributes.has(ModelObjectCutAttribute::KeepLower) && ! lower_mesh.empty()) { - ModelVolume* vol = lower->add_volume(lower_mesh); - vol->name = volume->name; - // Don't copy the config's ID. - vol->config.assign_config(volume->config); - assert(vol->config.id().valid()); - assert(vol->config.id() != volume->config.id()); - vol->set_material(volume->material_id(), *volume->material()); - - // Compute the displacement (in instance coordinates) to be applied to place the upper parts - // The upper part displacement is set to half of the lower part bounding box - // this is done in hope at least a part of the upper part will always be visible and draggable - local_displace = lower->full_raw_mesh_bounding_box().size().cwiseProduct(Vec3d(-0.5, -0.5, 0.0)); - } + if (!volume->is_model_part()) { + if (volume->cut_info.is_processed) + process_modifier_cut(volume, instance_matrix, inverse_cut_matrix, attributes, upper, lower); + else + process_connector_cut(volume, attributes, upper, lower, dowels, local_dowels_displace); } + else if (!volume->mesh().empty()) + process_solid_part_cut(volume, instance_matrix, cut_matrix, attributes, upper, lower, local_displace); } + // Post-process cut parts + ModelObjectPtrs res; - if (attributes.has(ModelObjectCutAttribute::KeepUpper) && upper->volumes.size() > 0) { - if (!upper->origin_translation.isApprox(Vec3d::Zero()) && instances[instance]->get_offset().isApprox(Vec3d::Zero())) { - upper->center_around_origin(); - upper->translate_instances(-upper->origin_translation); - upper->origin_translation = Vec3d::Zero(); - } - - // Reset instance transformation except offset and Z-rotation - for (size_t i = 0; i < instances.size(); ++i) { - auto &instance = upper->instances[i]; - const Vec3d offset = instance->get_offset(); - const double rot_z = instance->get_rotation().z(); - const Vec3d displace = Geometry::assemble_transform(Vec3d::Zero(), instance->get_rotation()) * local_displace; - - instance->set_transformation(Geometry::Transformation()); - instance->set_offset(offset + displace); - instance->set_rotation(Vec3d(0.0, 0.0, rot_z)); - } + if (attributes.has(ModelObjectCutAttribute::KeepUpper) && !upper->volumes.empty()) { + invalidate_translations(upper, instances[instance]); + reset_instance_transformation(upper, instance, cut_matrix, + attributes.has(ModelObjectCutAttribute::PlaceOnCutUpper), + attributes.has(ModelObjectCutAttribute::FlipUpper), + local_displace); res.push_back(upper); } - if (attributes.has(ModelObjectCutAttribute::KeepLower) && lower->volumes.size() > 0) { - if (!lower->origin_translation.isApprox(Vec3d::Zero()) && instances[instance]->get_offset().isApprox(Vec3d::Zero())) { - lower->center_around_origin(); - lower->translate_instances(-lower->origin_translation); - lower->origin_translation = Vec3d::Zero(); - } - // Reset instance transformation except offset and Z-rotation - for (auto *instance : lower->instances) { - const Vec3d offset = instance->get_offset(); - const double rot_z = instance->get_rotation().z(); - instance->set_transformation(Geometry::Transformation()); - instance->set_offset(offset); - instance->set_rotation(Vec3d(attributes.has(ModelObjectCutAttribute::FlipLower) ? Geometry::deg2rad(180.0) : 0.0, 0.0, rot_z)); - } + if (attributes.has(ModelObjectCutAttribute::KeepLower) && !lower->volumes.empty()) { + invalidate_translations(lower, instances[instance]); + reset_instance_transformation(lower, instance, cut_matrix, + attributes.has(ModelObjectCutAttribute::PlaceOnCutLower), + attributes.has(ModelObjectCutAttribute::PlaceOnCutLower) ? true : attributes.has(ModelObjectCutAttribute::FlipLower)); res.push_back(lower); } + if (attributes.has(ModelObjectCutAttribute::CreateDowels) && !dowels.empty()) { + for (auto dowel : dowels) { + invalidate_translations(dowel, instances[instance]); + + reset_instance_transformation(dowel, instance, Transform3d::Identity(), false, false, local_dowels_displace); + + local_dowels_displace += dowel->full_raw_mesh_bounding_box().size().cwiseProduct(Vec3d(-1.5, -1.5, 0.0)); + dowel->name += "-Dowel-" + dowel->volumes[0]->name; + res.push_back(dowel); + } + } + BOOST_LOG_TRIVIAL(trace) << "ModelObject::cut - end"; + synchronize_model_after_cut(); + return res; } @@ -1381,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(); @@ -2289,6 +2575,14 @@ bool model_has_multi_part_objects(const Model &model) return false; } +bool model_has_connectors(const Model &model) +{ + for (const ModelObject *model_object : model.objects) + if (!model_object->cut_connectors.empty()) + return true; + return false; +} + bool model_has_advanced_features(const Model &model) { auto config_is_advanced = [](const ModelConfig &config) { diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 0e5033475..ee611b006 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -1,1256 +1,1399 @@ -#ifndef slic3r_Model_hpp_ -#define slic3r_Model_hpp_ - -#include "libslic3r.h" -#include "enum_bitmask.hpp" -#include "Geometry.hpp" -#include "ObjectID.hpp" -#include "Point.hpp" -#include "PrintConfig.hpp" -#include "Slicing.hpp" -#include "SLA/SupportPoint.hpp" -#include "SLA/Hollowing.hpp" -#include "TriangleMesh.hpp" -#include "Arrange.hpp" -#include "CustomGCode.hpp" -#include "enum_bitmask.hpp" - -#include -#include -#include -#include -#include - -namespace cereal { - class BinaryInputArchive; - class BinaryOutputArchive; - template void load_optional(BinaryInputArchive &ar, std::shared_ptr &ptr); - template void save_optional(BinaryOutputArchive &ar, const std::shared_ptr &ptr); - template void load_by_value(BinaryInputArchive &ar, T &obj); - template void save_by_value(BinaryOutputArchive &ar, const T &obj); -} - -namespace Slic3r { -enum class ConversionType; - -class BuildVolume; -class Model; -class ModelInstance; -class ModelMaterial; -class ModelObject; -class ModelVolume; -class ModelWipeTower; -class Print; -class SLAPrint; -class TriangleSelector; - -namespace UndoRedo { - class StackImpl; -} - -class ModelConfigObject : public ObjectBase, public ModelConfig -{ -private: - friend class cereal::access; - friend class UndoRedo::StackImpl; - friend class ModelObject; - friend class ModelVolume; - friend class ModelMaterial; - - // Constructors to be only called by derived classes. - // Default constructor to assign a unique ID. - explicit ModelConfigObject() = default; - // Constructor with ignored int parameter to assign an invalid ID, to be replaced - // by an existing ID copied from elsewhere. - explicit ModelConfigObject(int) : ObjectBase(-1) {} - // Copy constructor copies the ID. - explicit ModelConfigObject(const ModelConfigObject &cfg) = default; - // Move constructor copies the ID. - explicit ModelConfigObject(ModelConfigObject &&cfg) = default; - - Timestamp timestamp() const throw() override { return this->ModelConfig::timestamp(); } - bool object_id_and_timestamp_match(const ModelConfigObject &rhs) const throw() { return this->id() == rhs.id() && this->timestamp() == rhs.timestamp(); } - - // called by ModelObject::assign_copy() - ModelConfigObject& operator=(const ModelConfigObject &rhs) = default; - ModelConfigObject& operator=(ModelConfigObject &&rhs) = default; - - template void serialize(Archive &ar) { - ar(cereal::base_class(this)); - } -}; - -namespace Internal { - template - class StaticSerializationWrapper - { - public: - StaticSerializationWrapper(T &wrap) : wrapped(wrap) {} - private: - friend class cereal::access; - friend class UndoRedo::StackImpl; - template void load(Archive &ar) { cereal::load_by_value(ar, wrapped); } - template void save(Archive &ar) const { cereal::save_by_value(ar, wrapped); } - T& wrapped; - }; -} - -typedef std::string t_model_material_id; -typedef std::string t_model_material_attribute; -typedef std::map t_model_material_attributes; - -typedef std::map ModelMaterialMap; -typedef std::vector ModelObjectPtrs; -typedef std::vector ModelVolumePtrs; -typedef std::vector ModelInstancePtrs; - -#define OBJECTBASE_DERIVED_COPY_MOVE_CLONE(TYPE) \ - /* Copy a model, copy the IDs. The Print::apply() will call the TYPE::copy() method */ \ - /* to make a private copy for background processing. */ \ - static TYPE* new_copy(const TYPE &rhs) { auto *ret = new TYPE(rhs); assert(ret->id() == rhs.id()); return ret; } \ - static TYPE* new_copy(TYPE &&rhs) { auto *ret = new TYPE(std::move(rhs)); assert(ret->id() == rhs.id()); return ret; } \ - static TYPE make_copy(const TYPE &rhs) { TYPE ret(rhs); assert(ret.id() == rhs.id()); return ret; } \ - static TYPE make_copy(TYPE &&rhs) { TYPE ret(std::move(rhs)); assert(ret.id() == rhs.id()); return ret; } \ - TYPE& assign_copy(const TYPE &rhs); \ - TYPE& assign_copy(TYPE &&rhs); \ - /* Copy a TYPE, generate new IDs. The front end will use this call. */ \ - static TYPE* new_clone(const TYPE &rhs) { \ - /* Default constructor assigning an invalid ID. */ \ - auto obj = new TYPE(-1); \ - obj->assign_clone(rhs); \ - assert(obj->id().valid() && obj->id() != rhs.id()); \ - return obj; \ - } \ - TYPE make_clone(const TYPE &rhs) { \ - /* Default constructor assigning an invalid ID. */ \ - TYPE obj(-1); \ - obj.assign_clone(rhs); \ - assert(obj.id().valid() && obj.id() != rhs.id()); \ - return obj; \ - } \ - TYPE& assign_clone(const TYPE &rhs) { \ - this->assign_copy(rhs); \ - assert(this->id().valid() && this->id() == rhs.id()); \ - this->assign_new_unique_ids_recursive(); \ - assert(this->id().valid() && this->id() != rhs.id()); \ - return *this; \ - } - -// Material, which may be shared across multiple ModelObjects of a single Model. -class ModelMaterial final : public ObjectBase -{ -public: - // Attributes are defined by the AMF file format, but they don't seem to be used by Slic3r for any purpose. - t_model_material_attributes attributes; - // Dynamic configuration storage for the object specific configuration values, overriding the global configuration. - ModelConfigObject config; - - Model* get_model() const { return m_model; } - void apply(const t_model_material_attributes &attributes) - { this->attributes.insert(attributes.begin(), attributes.end()); } - -private: - // Parent, owning this material. - Model *m_model; - - // To be accessed by the Model. - friend class Model; - // Constructor, which assigns a new unique ID to the material and to its config. - ModelMaterial(Model *model) : m_model(model) { assert(this->id().valid()); } - // Copy constructor copies the IDs of the ModelMaterial and its config, and m_model! - ModelMaterial(const ModelMaterial &rhs) = default; - void set_model(Model *model) { m_model = model; } - void set_new_unique_id() { ObjectBase::set_new_unique_id(); this->config.set_new_unique_id(); } - - // To be accessed by the serialization and Undo/Redo code. - friend class cereal::access; - friend class UndoRedo::StackImpl; - // Create an object for deserialization, don't allocate IDs for ModelMaterial and its config. - ModelMaterial() : ObjectBase(-1), config(-1), m_model(nullptr) { assert(this->id().invalid()); assert(this->config.id().invalid()); } - template void serialize(Archive &ar) { - assert(this->id().invalid()); assert(this->config.id().invalid()); - Internal::StaticSerializationWrapper config_wrapper(config); - ar(attributes, config_wrapper); - // assert(this->id().valid()); assert(this->config.id().valid()); - } - - // Disabled methods. - ModelMaterial(ModelMaterial &&rhs) = delete; - ModelMaterial& operator=(const ModelMaterial &rhs) = delete; - ModelMaterial& operator=(ModelMaterial &&rhs) = delete; -}; - -class LayerHeightProfile final : public ObjectWithTimestamp { -public: - // Assign the content if the timestamp differs, don't assign an ObjectID. - void assign(const LayerHeightProfile &rhs) { if (! this->timestamp_matches(rhs)) { m_data = rhs.m_data; this->copy_timestamp(rhs); } } - void assign(LayerHeightProfile &&rhs) { if (! this->timestamp_matches(rhs)) { m_data = std::move(rhs.m_data); this->copy_timestamp(rhs); } } - - std::vector get() const throw() { return m_data; } - bool empty() const throw() { return m_data.empty(); } - void set(const std::vector &data) { if (m_data != data) { m_data = data; this->touch(); } } - void set(std::vector &&data) { if (m_data != data) { m_data = std::move(data); this->touch(); } } - void clear() { m_data.clear(); this->touch(); } - - template void serialize(Archive &ar) - { - ar(cereal::base_class(this), m_data); - } - -private: - // Constructors to be only called by derived classes. - // Default constructor to assign a unique ID. - explicit LayerHeightProfile() = default; - // Constructor with ignored int parameter to assign an invalid ID, to be replaced - // by an existing ID copied from elsewhere. - explicit LayerHeightProfile(int) : ObjectWithTimestamp(-1) {} - // Copy constructor copies the ID. - explicit LayerHeightProfile(const LayerHeightProfile &rhs) = default; - // Move constructor copies the ID. - explicit LayerHeightProfile(LayerHeightProfile &&rhs) = default; - - // called by ModelObject::assign_copy() - LayerHeightProfile& operator=(const LayerHeightProfile &rhs) = default; - LayerHeightProfile& operator=(LayerHeightProfile &&rhs) = default; - - std::vector m_data; - - // to access set_new_unique_id() when copy / pasting an object - friend class ModelObject; -}; - -// Declared outside of ModelVolume, so it could be forward declared. -enum class ModelVolumeType : int { - INVALID = -1, - MODEL_PART = 0, - NEGATIVE_VOLUME, - PARAMETER_MODIFIER, - SUPPORT_BLOCKER, - SUPPORT_ENFORCER, -}; - -enum class ModelObjectCutAttribute : int { KeepUpper, KeepLower, FlipLower }; -using ModelObjectCutAttributes = enum_bitmask; -ENABLE_ENUM_BITMASK_OPERATORS(ModelObjectCutAttribute); - -// A printable object, possibly having multiple print volumes (each with its own set of parameters and materials), -// and possibly having multiple modifier volumes, each modifier volume with its set of parameters and materials. -// Each ModelObject may be instantiated mutliple times, each instance having different placement on the print bed, -// different rotation and different uniform scaling. -class ModelObject final : public ObjectBase -{ -public: - std::string name; - std::string input_file; // XXX: consider fs::path - // Instances of this ModelObject. Each instance defines a shift on the print bed, rotation around the Z axis and a uniform scaling. - // Instances are owned by this ModelObject. - ModelInstancePtrs instances; - // Printable and modifier volumes, each with its material ID and a set of override parameters. - // ModelVolumes are owned by this ModelObject. - ModelVolumePtrs volumes; - // Configuration parameters specific to a single ModelObject, overriding the global Slic3r settings. - ModelConfigObject config; - // Variation of a layer thickness for spans of Z coordinates + optional parameter overrides. - t_layer_config_ranges layer_config_ranges; - // Profile of increasing z to a layer height, to be linearly interpolated when calculating the layers. - // The pairs of are packed into a 1D array. - LayerHeightProfile layer_height_profile; - // Whether or not this object is printable - bool printable; - - // This vector holds position of selected support points for SLA. The data are - // saved in mesh coordinates to allow using them for several instances. - // The format is (x, y, z, point_size, supports_island) - sla::SupportPoints sla_support_points; - // To keep track of where the points came from (used for synchronization between - // the SLA gizmo and the backend). - sla::PointsStatus sla_points_status = sla::PointsStatus::NoPoints; - - // Holes to be drilled into the object so resin can flow out - sla::DrainHoles sla_drain_holes; - - /* This vector accumulates the total translation applied to the object by the - center_around_origin() method. Callers might want to apply the same translation - to new volumes before adding them to this object in order to preserve alignment - when user expects that. */ - Vec3d origin_translation; - - Model* get_model() { return m_model; } - const Model* get_model() const { return m_model; } - - ModelVolume* add_volume(const TriangleMesh &mesh); - ModelVolume* add_volume(TriangleMesh &&mesh, ModelVolumeType type = ModelVolumeType::MODEL_PART); - ModelVolume* add_volume(const ModelVolume &volume, ModelVolumeType type = ModelVolumeType::INVALID); - ModelVolume* add_volume(const ModelVolume &volume, TriangleMesh &&mesh); - void delete_volume(size_t idx); - void clear_volumes(); - void sort_volumes(bool full_sort); - bool is_multiparts() const { return volumes.size() > 1; } - // Checks if any of object volume is painted using the fdm support painting gizmo. - bool is_fdm_support_painted() const; - // Checks if any of object volume is painted using the seam painting gizmo. - bool is_seam_painted() const; - // Checks if any of object volume is painted using the multi-material painting gizmo. - bool is_mm_painted() const; - - ModelInstance* add_instance(); - ModelInstance* add_instance(const ModelInstance &instance); - ModelInstance* add_instance(const Vec3d &offset, const Vec3d &scaling_factor, const Vec3d &rotation, const Vec3d &mirror); - void delete_instance(size_t idx); - void delete_last_instance(); - void clear_instances(); - - // Returns the bounding box of the transformed instances. - // This bounding box is approximate and not snug. - // This bounding box is being cached. - const BoundingBoxf3& bounding_box() const; - void invalidate_bounding_box() { m_bounding_box_valid = false; m_raw_bounding_box_valid = false; m_raw_mesh_bounding_box_valid = false; } - - // A mesh containing all transformed instances of this object. - TriangleMesh mesh() const; - // Non-transformed (non-rotated, non-scaled, non-translated) sum of non-modifier object volumes. - // Currently used by ModelObject::mesh() and to calculate the 2D envelope for 2D plater. - TriangleMesh raw_mesh() const; - // The same as above, but producing a lightweight indexed_triangle_set. - indexed_triangle_set raw_indexed_triangle_set() const; - // A transformed snug bounding box around the non-modifier object volumes, without the translation applied. - // This bounding box is only used for the actual slicing. - const BoundingBoxf3& raw_bounding_box() const; - // A snug bounding box around the transformed non-modifier object volumes. - BoundingBoxf3 instance_bounding_box(size_t instance_idx, bool dont_translate = false) const; - // A snug bounding box of non-transformed (non-rotated, non-scaled, non-translated) sum of non-modifier object volumes. - const BoundingBoxf3& raw_mesh_bounding_box() const; - // A snug bounding box of non-transformed (non-rotated, non-scaled, non-translated) sum of all object volumes. - BoundingBoxf3 full_raw_mesh_bounding_box() const; - - // Calculate 2D convex hull of of a projection of the transformed printable volumes into the XY plane. - // This method is cheap in that it does not make any unnecessary copy of the volume meshes. - // This method is used by the auto arrange function. - Polygon convex_hull_2d(const Transform3d &trafo_instance) const; - - void center_around_origin(bool include_modifiers = true); - void ensure_on_bed(bool allow_negative_z = false); - - void translate_instances(const Vec3d& vector); - void translate_instance(size_t instance_idx, const Vec3d& vector); - void translate(const Vec3d &vector) { this->translate(vector(0), vector(1), vector(2)); } - void translate(double x, double y, double z); - void scale(const Vec3d &versor); - void scale(const double s) { this->scale(Vec3d(s, s, s)); } - void scale(double x, double y, double z) { this->scale(Vec3d(x, y, z)); } - /// Scale the current ModelObject to fit by altering the scaling factor of ModelInstances. - /// It operates on the total size by duplicating the object according to all the instances. - /// \param size Sizef3 the size vector - void scale_to_fit(const Vec3d &size); - void rotate(double angle, Axis axis); - void rotate(double angle, const Vec3d& axis); - void mirror(Axis axis); - - // This method could only be called before the meshes of this ModelVolumes are not shared! - void scale_mesh_after_creation(const float scale); - void convert_units(ModelObjectPtrs&new_objects, ConversionType conv_type, std::vector volume_idxs); - - size_t materials_count() const; - size_t facets_count() const; - size_t parts_count() const; - ModelObjectPtrs cut(size_t instance, coordf_t z, ModelObjectCutAttributes attributes); - void split(ModelObjectPtrs* new_objects); - void merge(); - // Support for non-uniform scaling of instances. If an instance is rotated by angles, which are not multiples of ninety degrees, - // then the scaling in world coordinate system is not representable by the Geometry::Transformation structure. - // This situation is solved by baking in the instance transformation into the mesh vertices. - // Rotation and mirroring is being baked in. In case the instance scaling was non-uniform, it is baked in as well. - void bake_xy_rotation_into_meshes(size_t instance_idx); - - double get_min_z() const; - double get_max_z() const; - double get_instance_min_z(size_t instance_idx) const; - double get_instance_max_z(size_t instance_idx) const; - - // Print object statistics to console. - void print_info() const; - - std::string get_export_filename() const; - - // Get full stl statistics for all object's meshes - TriangleMeshStats get_object_stl_stats() const; - // Get count of errors in the mesh( or all object's meshes, if volume index isn't defined) - int get_repaired_errors_count(const int vol_idx = -1) const; - -private: - friend class Model; - // This constructor assigns new ID to this ModelObject and its config. - explicit ModelObject(Model* model) : m_model(model), printable(true), origin_translation(Vec3d::Zero()), - m_bounding_box_valid(false), m_raw_bounding_box_valid(false), m_raw_mesh_bounding_box_valid(false) - { - assert(this->id().valid()); - assert(this->config.id().valid()); - assert(this->layer_height_profile.id().valid()); - } - explicit ModelObject(int) : ObjectBase(-1), config(-1), layer_height_profile(-1), m_model(nullptr), printable(true), origin_translation(Vec3d::Zero()), m_bounding_box_valid(false), m_raw_bounding_box_valid(false), m_raw_mesh_bounding_box_valid(false) - { - assert(this->id().invalid()); - assert(this->config.id().invalid()); - assert(this->layer_height_profile.id().invalid()); - } - ~ModelObject(); - void assign_new_unique_ids_recursive() override; - - // To be able to return an object from own copy / clone methods. Hopefully the compiler will do the "Copy elision" - // (Omits copy and move(since C++11) constructors, resulting in zero - copy pass - by - value semantics). - ModelObject(const ModelObject &rhs) : ObjectBase(-1), config(-1), layer_height_profile(-1), m_model(rhs.m_model) { - assert(this->id().invalid()); - assert(this->config.id().invalid()); - assert(this->layer_height_profile.id().invalid()); - assert(rhs.id() != rhs.config.id()); - assert(rhs.id() != rhs.layer_height_profile.id()); - this->assign_copy(rhs); - assert(this->id().valid()); - assert(this->config.id().valid()); - assert(this->layer_height_profile.id().valid()); - assert(this->id() != this->config.id()); - assert(this->id() != this->layer_height_profile.id()); - assert(this->id() == rhs.id()); - assert(this->config.id() == rhs.config.id()); - assert(this->layer_height_profile.id() == rhs.layer_height_profile.id()); - } - explicit ModelObject(ModelObject &&rhs) : ObjectBase(-1), config(-1), layer_height_profile(-1) { - assert(this->id().invalid()); - assert(this->config.id().invalid()); - assert(this->layer_height_profile.id().invalid()); - assert(rhs.id() != rhs.config.id()); - assert(rhs.id() != rhs.layer_height_profile.id()); - this->assign_copy(std::move(rhs)); - assert(this->id().valid()); - assert(this->config.id().valid()); - assert(this->layer_height_profile.id().valid()); - assert(this->id() != this->config.id()); - assert(this->id() != this->layer_height_profile.id()); - assert(this->id() == rhs.id()); - assert(this->config.id() == rhs.config.id()); - assert(this->layer_height_profile.id() == rhs.layer_height_profile.id()); - } - ModelObject& operator=(const ModelObject &rhs) { - this->assign_copy(rhs); - m_model = rhs.m_model; - assert(this->id().valid()); - assert(this->config.id().valid()); - assert(this->layer_height_profile.id().valid()); - assert(this->id() != this->config.id()); - assert(this->id() != this->layer_height_profile.id()); - assert(this->id() == rhs.id()); - assert(this->config.id() == rhs.config.id()); - assert(this->layer_height_profile.id() == rhs.layer_height_profile.id()); - return *this; - } - ModelObject& operator=(ModelObject &&rhs) { - this->assign_copy(std::move(rhs)); - m_model = rhs.m_model; - assert(this->id().valid()); - assert(this->config.id().valid()); - assert(this->layer_height_profile.id().valid()); - assert(this->id() != this->config.id()); - assert(this->id() != this->layer_height_profile.id()); - assert(this->id() == rhs.id()); - assert(this->config.id() == rhs.config.id()); - assert(this->layer_height_profile.id() == rhs.layer_height_profile.id()); - return *this; - } - void set_new_unique_id() { - ObjectBase::set_new_unique_id(); - this->config.set_new_unique_id(); - this->layer_height_profile.set_new_unique_id(); - } - - OBJECTBASE_DERIVED_COPY_MOVE_CLONE(ModelObject) - - // Parent object, owning this ModelObject. Set to nullptr here, so the macros above will have it initialized. - Model *m_model = nullptr; - - // Bounding box, cached. - mutable BoundingBoxf3 m_bounding_box; - mutable bool m_bounding_box_valid; - mutable BoundingBoxf3 m_raw_bounding_box; - mutable bool m_raw_bounding_box_valid; - mutable BoundingBoxf3 m_raw_mesh_bounding_box; - mutable bool m_raw_mesh_bounding_box_valid; - - // Called by Print::apply() to set the model pointer after making a copy. - friend class Print; - friend class SLAPrint; - void set_model(Model *model) { m_model = model; } - - // Undo / Redo through the cereal serialization library - friend class cereal::access; - friend class UndoRedo::StackImpl; - // Used for deserialization -> Don't allocate any IDs for the ModelObject or its config. - ModelObject() : - ObjectBase(-1), config(-1), layer_height_profile(-1), - m_model(nullptr), m_bounding_box_valid(false), m_raw_bounding_box_valid(false), m_raw_mesh_bounding_box_valid(false) { - assert(this->id().invalid()); - assert(this->config.id().invalid()); - assert(this->layer_height_profile.id().invalid()); - } - template void serialize(Archive &ar) { - ar(cereal::base_class(this)); - Internal::StaticSerializationWrapper config_wrapper(config); - Internal::StaticSerializationWrapper layer_heigth_profile_wrapper(layer_height_profile); - ar(name, input_file, instances, volumes, config_wrapper, layer_config_ranges, layer_heigth_profile_wrapper, - sla_support_points, sla_points_status, sla_drain_holes, printable, origin_translation, - m_bounding_box, m_bounding_box_valid, m_raw_bounding_box, m_raw_bounding_box_valid, m_raw_mesh_bounding_box, m_raw_mesh_bounding_box_valid); - } - - // Called by Print::validate() from the UI thread. - unsigned int update_instances_print_volume_state(const BuildVolume &build_volume); -}; - -enum class EnforcerBlockerType : int8_t { - // Maximum is 3. The value is serialized in TriangleSelector into 2 bits. - NONE = 0, - ENFORCER = 1, - BLOCKER = 2, - // Maximum is 15. The value is serialized in TriangleSelector into 6 bits using a 2 bit prefix code. - Extruder1 = ENFORCER, - Extruder2 = BLOCKER, - Extruder3, - Extruder4, - Extruder5, - Extruder6, - Extruder7, - Extruder8, - Extruder9, - Extruder10, - Extruder11, - Extruder12, - Extruder13, - Extruder14, - Extruder15, -}; - -enum class ConversionType : int { - CONV_TO_INCH, - CONV_FROM_INCH, - CONV_TO_METER, - CONV_FROM_METER, -}; - -class FacetsAnnotation final : public ObjectWithTimestamp { -public: - // Assign the content if the timestamp differs, don't assign an ObjectID. - void assign(const FacetsAnnotation& rhs) { if (! this->timestamp_matches(rhs)) { m_data = rhs.m_data; this->copy_timestamp(rhs); } } - void assign(FacetsAnnotation&& rhs) { if (! this->timestamp_matches(rhs)) { m_data = std::move(rhs.m_data); this->copy_timestamp(rhs); } } - const std::pair>, std::vector>& get_data() const throw() { return m_data; } - bool set(const TriangleSelector& selector); - indexed_triangle_set get_facets(const ModelVolume& mv, EnforcerBlockerType type) const; - indexed_triangle_set get_facets_strict(const ModelVolume& mv, EnforcerBlockerType type) const; - bool has_facets(const ModelVolume& mv, EnforcerBlockerType type) const; - bool empty() const { return m_data.first.empty(); } - - // Following method clears the config and increases its timestamp, so the deleted - // state is considered changed from perspective of the undo/redo stack. - void reset(); - - // Serialize triangle into string, for serialization into 3MF/AMF. - std::string get_triangle_as_string(int i) const; - - // Before deserialization, reserve space for n_triangles. - void reserve(int n_triangles) { m_data.first.reserve(n_triangles); } - // Deserialize triangles one by one, with strictly increasing triangle_id. - void set_triangle_from_string(int triangle_id, const std::string& str); - // After deserializing the last triangle, shrink data to fit. - void shrink_to_fit() { m_data.first.shrink_to_fit(); m_data.second.shrink_to_fit(); } - -private: - // Constructors to be only called by derived classes. - // Default constructor to assign a unique ID. - explicit FacetsAnnotation() = default; - // Constructor with ignored int parameter to assign an invalid ID, to be replaced - // by an existing ID copied from elsewhere. - explicit FacetsAnnotation(int) : ObjectWithTimestamp(-1) {} - // Copy constructor copies the ID. - explicit FacetsAnnotation(const FacetsAnnotation &rhs) = default; - // Move constructor copies the ID. - explicit FacetsAnnotation(FacetsAnnotation &&rhs) = default; - - // called by ModelVolume::assign_copy() - FacetsAnnotation& operator=(const FacetsAnnotation &rhs) = default; - FacetsAnnotation& operator=(FacetsAnnotation &&rhs) = default; - - friend class cereal::access; - friend class UndoRedo::StackImpl; - - template void serialize(Archive &ar) - { - ar(cereal::base_class(this), m_data); - } - - std::pair>, std::vector> m_data; - - // To access set_new_unique_id() when copy / pasting a ModelVolume. - friend class ModelVolume; -}; - -// An object STL, or a modifier volume, over which a different set of parameters shall be applied. -// ModelVolume instances are owned by a ModelObject. -class ModelVolume final : public ObjectBase -{ -public: - std::string name; - // struct used by reload from disk command to recover data from disk - struct Source - { - std::string input_file; - int object_idx{ -1 }; - int volume_idx{ -1 }; - Vec3d mesh_offset{ Vec3d::Zero() }; - Geometry::Transformation transform; - bool is_converted_from_inches{ false }; - bool is_converted_from_meters{ false }; - bool is_from_builtin_objects{ false }; - - template void serialize(Archive& ar) { - //FIXME Vojtech: Serialize / deserialize only if the Source is set. - // likely testing input_file or object_idx would be sufficient. - ar(input_file, object_idx, volume_idx, mesh_offset, transform, is_converted_from_inches, is_converted_from_meters, is_from_builtin_objects); - } - }; - Source source; - - // The triangular model. - const TriangleMesh& mesh() const { return *m_mesh.get(); } -#if ENABLE_RAYCAST_PICKING - std::shared_ptr mesh_ptr() const { return m_mesh; } -#endif // ENABLE_RAYCAST_PICKING - void set_mesh(const TriangleMesh &mesh) { m_mesh = std::make_shared(mesh); } - void set_mesh(TriangleMesh &&mesh) { m_mesh = std::make_shared(std::move(mesh)); } - void set_mesh(const indexed_triangle_set &mesh) { m_mesh = std::make_shared(mesh); } - void set_mesh(indexed_triangle_set &&mesh) { m_mesh = std::make_shared(std::move(mesh)); } - void set_mesh(std::shared_ptr &mesh) { m_mesh = mesh; } - void set_mesh(std::unique_ptr &&mesh) { m_mesh = std::move(mesh); } - void reset_mesh() { m_mesh = std::make_shared(); } - // Configuration parameters specific to an object model geometry or a modifier volume, - // overriding the global Slic3r settings and the ModelObject settings. - ModelConfigObject config; - - // List of mesh facets to be supported/unsupported. - FacetsAnnotation supported_facets; - - // List of seam enforcers/blockers. - FacetsAnnotation seam_facets; - - // List of mesh facets painted for MMU segmentation. - FacetsAnnotation mmu_segmentation_facets; - - // A parent object owning this modifier volume. - ModelObject* get_object() const { return this->object; } - ModelVolumeType type() const { return m_type; } - void set_type(const ModelVolumeType t) { m_type = t; } - bool is_model_part() const { return m_type == ModelVolumeType::MODEL_PART; } - bool is_negative_volume() const { return m_type == ModelVolumeType::NEGATIVE_VOLUME; } - bool is_modifier() const { return m_type == ModelVolumeType::PARAMETER_MODIFIER; } - bool is_support_enforcer() const { return m_type == ModelVolumeType::SUPPORT_ENFORCER; } - bool is_support_blocker() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER; } - bool is_support_modifier() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER || m_type == ModelVolumeType::SUPPORT_ENFORCER; } - t_model_material_id material_id() const { return m_material_id; } - void set_material_id(t_model_material_id material_id); - ModelMaterial* material() const; - void set_material(t_model_material_id material_id, const ModelMaterial &material); - // Extract the current extruder ID based on this ModelVolume's config and the parent ModelObject's config. - // Extruder ID is only valid for FFF. Returns -1 for SLA or if the extruder ID is not applicable (support volumes). - int extruder_id() const; - - bool is_splittable() const; - - // Split this volume, append the result to the object owning this volume. - // Return the number of volumes created from this one. - // This is useful to assign different materials to different volumes of an object. - size_t split(unsigned int max_extruders); - void translate(double x, double y, double z) { translate(Vec3d(x, y, z)); } - void translate(const Vec3d& displacement); - void scale(const Vec3d& scaling_factors); - void scale(double x, double y, double z) { scale(Vec3d(x, y, z)); } - void scale(double s) { scale(Vec3d(s, s, s)); } - void rotate(double angle, Axis axis); - void rotate(double angle, const Vec3d& axis); - void mirror(Axis axis); - - // This method could only be called before the meshes of this ModelVolumes are not shared! - void scale_geometry_after_creation(const Vec3f &versor); - void scale_geometry_after_creation(const float scale) { this->scale_geometry_after_creation(Vec3f(scale, scale, scale)); } - - // Translates the mesh and the convex hull so that the origin of their vertices is in the center of this volume's bounding box. - // Attention! This method may only be called just after ModelVolume creation! It must not be called once the TriangleMesh of this ModelVolume is shared! - void center_geometry_after_creation(bool update_source_offset = true); - - void calculate_convex_hull(); - const TriangleMesh& get_convex_hull() const; - const std::shared_ptr& get_convex_hull_shared_ptr() const { return m_convex_hull; } - // Get count of errors in the mesh - int get_repaired_errors_count() const; - - // Helpers for loading / storing into AMF / 3MF files. - static ModelVolumeType type_from_string(const std::string &s); - static std::string type_to_string(const ModelVolumeType t); - - const Geometry::Transformation& get_transformation() const { return m_transformation; } - void set_transformation(const Geometry::Transformation& transformation) { m_transformation = transformation; } -#if ENABLE_WORLD_COORDINATE - void set_transformation(const Transform3d& trafo) { m_transformation.set_matrix(trafo); } - - Vec3d get_offset() const { return m_transformation.get_offset(); } -#else - void set_transformation(const Transform3d &trafo) { m_transformation.set_from_transform(trafo); } - - const Vec3d& get_offset() const { return m_transformation.get_offset(); } -#endif // ENABLE_WORLD_COORDINATE - - double get_offset(Axis axis) const { return m_transformation.get_offset(axis); } - - void set_offset(const Vec3d& offset) { m_transformation.set_offset(offset); } - void set_offset(Axis axis, double offset) { m_transformation.set_offset(axis, offset); } - -#if ENABLE_WORLD_COORDINATE - Vec3d get_rotation() const { return m_transformation.get_rotation(); } -#else - const Vec3d& get_rotation() const { return m_transformation.get_rotation(); } -#endif // ENABLE_WORLD_COORDINATE - double get_rotation(Axis axis) const { return m_transformation.get_rotation(axis); } - - void set_rotation(const Vec3d& rotation) { m_transformation.set_rotation(rotation); } - void set_rotation(Axis axis, double rotation) { m_transformation.set_rotation(axis, rotation); } - - Vec3d get_scaling_factor() const { return m_transformation.get_scaling_factor(); } - double get_scaling_factor(Axis axis) const { return m_transformation.get_scaling_factor(axis); } - - void set_scaling_factor(const Vec3d& scaling_factor) { m_transformation.set_scaling_factor(scaling_factor); } - void set_scaling_factor(Axis axis, double scaling_factor) { m_transformation.set_scaling_factor(axis, scaling_factor); } - -#if ENABLE_WORLD_COORDINATE - Vec3d get_mirror() const { return m_transformation.get_mirror(); } -#else - const Vec3d& get_mirror() const { return m_transformation.get_mirror(); } -#endif // ENABLE_WORLD_COORDINATE - double get_mirror(Axis axis) const { return m_transformation.get_mirror(axis); } - bool is_left_handed() const { return m_transformation.is_left_handed(); } - - void set_mirror(const Vec3d& mirror) { m_transformation.set_mirror(mirror); } - void set_mirror(Axis axis, double mirror) { m_transformation.set_mirror(axis, mirror); } - void convert_from_imperial_units(); - void convert_from_meters(); - -#if ENABLE_WORLD_COORDINATE - const Transform3d& get_matrix() const { return m_transformation.get_matrix(); } - Transform3d get_matrix_no_offset() const { return m_transformation.get_matrix_no_offset(); } -#else - const Transform3d& get_matrix(bool dont_translate = false, bool dont_rotate = false, bool dont_scale = false, bool dont_mirror = false) const { return m_transformation.get_matrix(dont_translate, dont_rotate, dont_scale, dont_mirror); } -#endif // ENABLE_WORLD_COORDINATE - - void set_new_unique_id() { - ObjectBase::set_new_unique_id(); - this->config.set_new_unique_id(); - this->supported_facets.set_new_unique_id(); - this->seam_facets.set_new_unique_id(); - this->mmu_segmentation_facets.set_new_unique_id(); - } - - bool is_fdm_support_painted() const { return !this->supported_facets.empty(); } - bool is_seam_painted() const { return !this->seam_facets.empty(); } - bool is_mm_painted() const { return !this->mmu_segmentation_facets.empty(); } - -protected: - friend class Print; - friend class SLAPrint; - friend class Model; - friend class ModelObject; - friend void model_volume_list_update_supports(ModelObject& model_object_dst, const ModelObject& model_object_new); - - // Copies IDs of both the ModelVolume and its config. - explicit ModelVolume(const ModelVolume &rhs) = default; - void set_model_object(ModelObject *model_object) { object = model_object; } - void assign_new_unique_ids_recursive() override; - void transform_this_mesh(const Transform3d& t, bool fix_left_handed); - void transform_this_mesh(const Matrix3d& m, bool fix_left_handed); - -private: - // Parent object owning this ModelVolume. - ModelObject* object; - // The triangular model. - std::shared_ptr m_mesh; - // Is it an object to be printed, or a modifier volume? - ModelVolumeType m_type; - t_model_material_id m_material_id; - // The convex hull of this model's mesh. - std::shared_ptr m_convex_hull; - Geometry::Transformation m_transformation; - - // flag to optimize the checking if the volume is splittable - // -1 -> is unknown value (before first cheking) - // 0 -> is not splittable - // 1 -> is splittable - mutable int m_is_splittable{ -1 }; - - ModelVolume(ModelObject *object, const TriangleMesh &mesh, ModelVolumeType type = ModelVolumeType::MODEL_PART) : m_mesh(new TriangleMesh(mesh)), m_type(type), object(object) - { - assert(this->id().valid()); - assert(this->config.id().valid()); - assert(this->supported_facets.id().valid()); - assert(this->seam_facets.id().valid()); - assert(this->mmu_segmentation_facets.id().valid()); - assert(this->id() != this->config.id()); - assert(this->id() != this->supported_facets.id()); - assert(this->id() != this->seam_facets.id()); - assert(this->id() != this->mmu_segmentation_facets.id()); - if (mesh.facets_count() > 1) - calculate_convex_hull(); - } - ModelVolume(ModelObject *object, TriangleMesh &&mesh, TriangleMesh &&convex_hull, ModelVolumeType type = ModelVolumeType::MODEL_PART) : - m_mesh(new TriangleMesh(std::move(mesh))), m_convex_hull(new TriangleMesh(std::move(convex_hull))), m_type(type), object(object) { - assert(this->id().valid()); - assert(this->config.id().valid()); - assert(this->supported_facets.id().valid()); - assert(this->seam_facets.id().valid()); - assert(this->mmu_segmentation_facets.id().valid()); - assert(this->id() != this->config.id()); - assert(this->id() != this->supported_facets.id()); - assert(this->id() != this->seam_facets.id()); - assert(this->id() != this->mmu_segmentation_facets.id()); - } - - // Copying an existing volume, therefore this volume will get a copy of the ID assigned. - ModelVolume(ModelObject *object, const ModelVolume &other) : - ObjectBase(other), - name(other.name), source(other.source), m_mesh(other.m_mesh), m_convex_hull(other.m_convex_hull), - config(other.config), m_type(other.m_type), object(object), m_transformation(other.m_transformation), - supported_facets(other.supported_facets), seam_facets(other.seam_facets), mmu_segmentation_facets(other.mmu_segmentation_facets) - { - assert(this->id().valid()); - assert(this->config.id().valid()); - assert(this->supported_facets.id().valid()); - assert(this->seam_facets.id().valid()); - assert(this->mmu_segmentation_facets.id().valid()); - assert(this->id() != this->config.id()); - assert(this->id() != this->supported_facets.id()); - assert(this->id() != this->seam_facets.id()); - assert(this->id() != this->mmu_segmentation_facets.id()); - assert(this->id() == other.id()); - assert(this->config.id() == other.config.id()); - assert(this->supported_facets.id() == other.supported_facets.id()); - assert(this->seam_facets.id() == other.seam_facets.id()); - assert(this->mmu_segmentation_facets.id() == other.mmu_segmentation_facets.id()); - this->set_material_id(other.material_id()); - } - // Providing a new mesh, therefore this volume will get a new unique ID assigned. - ModelVolume(ModelObject *object, const ModelVolume &other, TriangleMesh &&mesh) : - name(other.name), source(other.source), config(other.config), object(object), m_mesh(new TriangleMesh(std::move(mesh))), m_type(other.m_type), m_transformation(other.m_transformation) - { - assert(this->id().valid()); - assert(this->config.id().valid()); - assert(this->supported_facets.id().valid()); - assert(this->seam_facets.id().valid()); - assert(this->mmu_segmentation_facets.id().valid()); - assert(this->id() != this->config.id()); - assert(this->id() != this->supported_facets.id()); - assert(this->id() != this->seam_facets.id()); - assert(this->id() != this->mmu_segmentation_facets.id()); - assert(this->id() != other.id()); - assert(this->config.id() == other.config.id()); - this->set_material_id(other.material_id()); - this->config.set_new_unique_id(); - if (m_mesh->facets_count() > 1) - calculate_convex_hull(); - assert(this->config.id().valid()); - assert(this->config.id() != other.config.id()); - assert(this->supported_facets.id() != other.supported_facets.id()); - assert(this->seam_facets.id() != other.seam_facets.id()); - assert(this->mmu_segmentation_facets.id() != other.mmu_segmentation_facets.id()); - assert(this->id() != this->config.id()); - assert(this->supported_facets.empty()); - assert(this->seam_facets.empty()); - assert(this->mmu_segmentation_facets.empty()); - } - - ModelVolume& operator=(ModelVolume &rhs) = delete; - - friend class cereal::access; - friend class UndoRedo::StackImpl; - // Used for deserialization, therefore no IDs are allocated. - ModelVolume() : ObjectBase(-1), config(-1), supported_facets(-1), seam_facets(-1), mmu_segmentation_facets(-1), object(nullptr) { - assert(this->id().invalid()); - assert(this->config.id().invalid()); - assert(this->supported_facets.id().invalid()); - assert(this->seam_facets.id().invalid()); - assert(this->mmu_segmentation_facets.id().invalid()); - } - template void load(Archive &ar) { - bool has_convex_hull; - ar(name, source, m_mesh, m_type, m_material_id, m_transformation, m_is_splittable, has_convex_hull); - cereal::load_by_value(ar, supported_facets); - cereal::load_by_value(ar, seam_facets); - cereal::load_by_value(ar, mmu_segmentation_facets); - cereal::load_by_value(ar, config); - assert(m_mesh); - if (has_convex_hull) { - cereal::load_optional(ar, m_convex_hull); - if (! m_convex_hull && ! m_mesh->empty()) - // The convex hull was released from the Undo / Redo stack to conserve memory. Recalculate it. - this->calculate_convex_hull(); - } else - m_convex_hull.reset(); - } - template void save(Archive &ar) const { - bool has_convex_hull = m_convex_hull.get() != nullptr; - ar(name, source, m_mesh, m_type, m_material_id, m_transformation, m_is_splittable, has_convex_hull); - cereal::save_by_value(ar, supported_facets); - cereal::save_by_value(ar, seam_facets); - cereal::save_by_value(ar, mmu_segmentation_facets); - cereal::save_by_value(ar, config); - if (has_convex_hull) - cereal::save_optional(ar, m_convex_hull); - } -}; - -inline void model_volumes_sort_by_id(ModelVolumePtrs &model_volumes) -{ - std::sort(model_volumes.begin(), model_volumes.end(), [](const ModelVolume *l, const ModelVolume *r) { return l->id() < r->id(); }); -} - -inline const ModelVolume* model_volume_find_by_id(const ModelVolumePtrs &model_volumes, const ObjectID id) -{ - auto it = lower_bound_by_predicate(model_volumes.begin(), model_volumes.end(), [id](const ModelVolume *mv) { return mv->id() < id; }); - return it != model_volumes.end() && (*it)->id() == id ? *it : nullptr; -} - -enum ModelInstanceEPrintVolumeState : unsigned char -{ - ModelInstancePVS_Inside, - ModelInstancePVS_Partly_Outside, - ModelInstancePVS_Fully_Outside, - ModelInstanceNum_BedStates -}; - -// A single instance of a ModelObject. -// Knows the affine transformation of an object. -class ModelInstance final : public ObjectBase -{ -private: - Geometry::Transformation m_transformation; - -public: - // flag showing the position of this instance with respect to the print volume (set by Print::validate() using ModelObject::check_instances_print_volume_state()) - ModelInstanceEPrintVolumeState print_volume_state; - // Whether or not this instance is printable - bool printable; - - ModelObject* get_object() const { return this->object; } - - const Geometry::Transformation& get_transformation() const { return m_transformation; } - void set_transformation(const Geometry::Transformation& transformation) { m_transformation = transformation; } - -#if ENABLE_WORLD_COORDINATE - Vec3d get_offset() const { return m_transformation.get_offset(); } -#else - const Vec3d& get_offset() const { return m_transformation.get_offset(); } -#endif // ENABLE_WORLD_COORDINATE - double get_offset(Axis axis) const { return m_transformation.get_offset(axis); } - - void set_offset(const Vec3d& offset) { m_transformation.set_offset(offset); } - void set_offset(Axis axis, double offset) { m_transformation.set_offset(axis, offset); } - -#if ENABLE_WORLD_COORDINATE - Vec3d get_rotation() const { return m_transformation.get_rotation(); } -#else - const Vec3d& get_rotation() const { return m_transformation.get_rotation(); } -#endif // ENABLE_WORLD_COORDINATE - double get_rotation(Axis axis) const { return m_transformation.get_rotation(axis); } - - void set_rotation(const Vec3d& rotation) { m_transformation.set_rotation(rotation); } - void set_rotation(Axis axis, double rotation) { m_transformation.set_rotation(axis, rotation); } - -#if ENABLE_WORLD_COORDINATE - Vec3d get_scaling_factor() const { return m_transformation.get_scaling_factor(); } -#else - const Vec3d& get_scaling_factor() const { return m_transformation.get_scaling_factor(); } -#endif // ENABLE_WORLD_COORDINATE - double get_scaling_factor(Axis axis) const { return m_transformation.get_scaling_factor(axis); } - - void set_scaling_factor(const Vec3d& scaling_factor) { m_transformation.set_scaling_factor(scaling_factor); } - void set_scaling_factor(Axis axis, double scaling_factor) { m_transformation.set_scaling_factor(axis, scaling_factor); } - -#if ENABLE_WORLD_COORDINATE - Vec3d get_mirror() const { return m_transformation.get_mirror(); } -#else - const Vec3d& get_mirror() const { return m_transformation.get_mirror(); } -#endif // ENABLE_WORLD_COORDINATE - double get_mirror(Axis axis) const { return m_transformation.get_mirror(axis); } - bool is_left_handed() const { return m_transformation.is_left_handed(); } - - void set_mirror(const Vec3d& mirror) { m_transformation.set_mirror(mirror); } - void set_mirror(Axis axis, double mirror) { m_transformation.set_mirror(axis, mirror); } - - // To be called on an external mesh - void transform_mesh(TriangleMesh* mesh, bool dont_translate = false) const; - // Calculate a bounding box of a transformed mesh. To be called on an external mesh. - BoundingBoxf3 transform_mesh_bounding_box(const TriangleMesh& mesh, bool dont_translate = false) const; - // Transform an external bounding box. - BoundingBoxf3 transform_bounding_box(const BoundingBoxf3 &bbox, bool dont_translate = false) const; - // Transform an external vector. - Vec3d transform_vector(const Vec3d& v, bool dont_translate = false) const; - // To be called on an external polygon. It does not translate the polygon, only rotates and scales. - void transform_polygon(Polygon* polygon) const; - -#if ENABLE_WORLD_COORDINATE - const Transform3d& get_matrix() const { return m_transformation.get_matrix(); } - Transform3d get_matrix_no_offset() const { return m_transformation.get_matrix_no_offset(); } -#else - const Transform3d& get_matrix(bool dont_translate = false, bool dont_rotate = false, bool dont_scale = false, bool dont_mirror = false) const { return m_transformation.get_matrix(dont_translate, dont_rotate, dont_scale, dont_mirror); } -#endif // ENABLE_WORLD_COORDINATE - - bool is_printable() const { return object->printable && printable && (print_volume_state == ModelInstancePVS_Inside); } - - // Getting the input polygon for arrange - arrangement::ArrangePolygon get_arrange_polygon() const; - - // Apply the arrange result on the ModelInstance - void apply_arrange_result(const Vec2d& offs, double rotation) - { - // write the transformation data into the model instance - set_rotation(Z, rotation); - set_offset(X, unscale(offs(X))); - set_offset(Y, unscale(offs(Y))); - this->object->invalidate_bounding_box(); - } - -protected: - friend class Print; - friend class SLAPrint; - friend class Model; - friend class ModelObject; - - explicit ModelInstance(const ModelInstance &rhs) = default; - void set_model_object(ModelObject *model_object) { object = model_object; } - -private: - // Parent object, owning this instance. - ModelObject* object; - - // Constructor, which assigns a new unique ID. - explicit ModelInstance(ModelObject* object) : print_volume_state(ModelInstancePVS_Inside), printable(true), object(object) { assert(this->id().valid()); } - // Constructor, which assigns a new unique ID. - explicit ModelInstance(ModelObject *object, const ModelInstance &other) : - m_transformation(other.m_transformation), print_volume_state(ModelInstancePVS_Inside), printable(other.printable), object(object) { assert(this->id().valid() && this->id() != other.id()); } - - explicit ModelInstance(ModelInstance &&rhs) = delete; - ModelInstance& operator=(const ModelInstance &rhs) = delete; - ModelInstance& operator=(ModelInstance &&rhs) = delete; - - friend class cereal::access; - friend class UndoRedo::StackImpl; - // Used for deserialization, therefore no IDs are allocated. - ModelInstance() : ObjectBase(-1), object(nullptr) { assert(this->id().invalid()); } - template void serialize(Archive &ar) { - ar(m_transformation, print_volume_state, printable); - } -}; - - -class ModelWipeTower final : public ObjectBase -{ -public: - Vec2d position; - double rotation; - -private: - friend class cereal::access; - friend class UndoRedo::StackImpl; - friend class Model; - - // Constructors to be only called by derived classes. - // Default constructor to assign a unique ID. - explicit ModelWipeTower() {} - // Constructor with ignored int parameter to assign an invalid ID, to be replaced - // by an existing ID copied from elsewhere. - explicit ModelWipeTower(int) : ObjectBase(-1) {} - // Copy constructor copies the ID. - explicit ModelWipeTower(const ModelWipeTower &cfg) = default; - - // Disabled methods. - ModelWipeTower(ModelWipeTower &&rhs) = delete; - ModelWipeTower& operator=(const ModelWipeTower &rhs) = delete; - ModelWipeTower& operator=(ModelWipeTower &&rhs) = delete; - - // For serialization / deserialization of ModelWipeTower composed into another class into the Undo / Redo stack as a separate object. - template void serialize(Archive &ar) { ar(position, rotation); } -}; - -// The print bed content. -// Description of a triangular model with multiple materials, multiple instances with various affine transformations -// and with multiple modifier meshes. -// A model groups multiple objects, each object having possibly multiple instances, -// all objects may share mutliple materials. -class Model final : public ObjectBase -{ -public: - // Materials are owned by a model and referenced by objects through t_model_material_id. - // Single material may be shared by multiple models. - ModelMaterialMap materials; - // Objects are owned by a model. Each model may have multiple instances, each instance having its own transformation (shift, scale, rotation). - ModelObjectPtrs objects; - // Wipe tower object. - ModelWipeTower wipe_tower; - - // Extensions for color print - CustomGCode::Info custom_gcode_per_print_z; - - // Default constructor assigns a new ID to the model. - Model() { assert(this->id().valid()); } - ~Model() { this->clear_objects(); this->clear_materials(); } - - /* To be able to return an object from own copy / clone methods. Hopefully the compiler will do the "Copy elision" */ - /* (Omits copy and move(since C++11) constructors, resulting in zero - copy pass - by - value semantics). */ - Model(const Model &rhs) : ObjectBase(-1) { assert(this->id().invalid()); this->assign_copy(rhs); assert(this->id().valid()); assert(this->id() == rhs.id()); } - explicit Model(Model &&rhs) : ObjectBase(-1) { assert(this->id().invalid()); this->assign_copy(std::move(rhs)); assert(this->id().valid()); assert(this->id() == rhs.id()); } - Model& operator=(const Model &rhs) { this->assign_copy(rhs); assert(this->id().valid()); assert(this->id() == rhs.id()); return *this; } - Model& operator=(Model &&rhs) { this->assign_copy(std::move(rhs)); assert(this->id().valid()); assert(this->id() == rhs.id()); return *this; } - - OBJECTBASE_DERIVED_COPY_MOVE_CLONE(Model) - - enum class LoadAttribute : int { - AddDefaultInstances, - CheckVersion - }; - using LoadAttributes = enum_bitmask; - - static Model read_from_file( - const std::string& input_file, - DynamicPrintConfig* config = nullptr, ConfigSubstitutionContext* config_substitutions = nullptr, - LoadAttributes options = LoadAttribute::AddDefaultInstances); - static Model read_from_archive( - const std::string& input_file, - DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, - LoadAttributes options = LoadAttribute::AddDefaultInstances); - - // Add a new ModelObject to this Model, generate a new ID for this ModelObject. - ModelObject* add_object(); - ModelObject* add_object(const char *name, const char *path, const TriangleMesh &mesh); - ModelObject* add_object(const char *name, const char *path, TriangleMesh &&mesh); - ModelObject* add_object(const ModelObject &other); - void delete_object(size_t idx); - bool delete_object(ObjectID id); - bool delete_object(ModelObject* object); - void clear_objects(); - - ModelMaterial* add_material(t_model_material_id material_id); - ModelMaterial* add_material(t_model_material_id material_id, const ModelMaterial &other); - ModelMaterial* get_material(t_model_material_id material_id) { - ModelMaterialMap::iterator i = this->materials.find(material_id); - return (i == this->materials.end()) ? nullptr : i->second; - } - - void delete_material(t_model_material_id material_id); - void clear_materials(); - bool add_default_instances(); - // Returns approximate axis aligned bounding box of this model - BoundingBoxf3 bounding_box() const; - // Set the print_volume_state of PrintObject::instances, - // return total number of printable objects. - unsigned int update_print_volume_state(const BuildVolume &build_volume); - // Returns true if any ModelObject was modified. - bool center_instances_around_point(const Vec2d &point); - void translate(coordf_t x, coordf_t y, coordf_t z) { for (ModelObject *o : this->objects) o->translate(x, y, z); } - TriangleMesh mesh() const; - - // Croaks if the duplicated objects do not fit the print bed. - void duplicate_objects_grid(size_t x, size_t y, coordf_t dist); - - bool looks_like_multipart_object() const; - void convert_multipart_object(unsigned int max_extruders); - bool looks_like_imperial_units() const; - void convert_from_imperial_units(bool only_small_volumes); - bool looks_like_saved_in_meters() const; - void convert_from_meters(bool only_small_volumes); - int removed_objects_with_zero_volume(); - - // Ensures that the min z of the model is not negative - void adjust_min_z(); - - void print_info() const { for (const ModelObject *o : this->objects) o->print_info(); } - - // Propose an output file name & path based on the first printable object's name and source input file's path. - std::string propose_export_file_name_and_path() const; - // Propose an output path, replace extension. The new_extension shall contain the initial dot. - std::string propose_export_file_name_and_path(const std::string &new_extension) const; - - // Checks if any of objects is painted using the fdm support painting gizmo. - bool is_fdm_support_painted() const; - // Checks if any of objects is painted using the seam painting gizmo. - bool is_seam_painted() const; - // Checks if any of objects is painted using the multi-material painting gizmo. - bool is_mm_painted() const; - -private: - explicit Model(int) : ObjectBase(-1) { assert(this->id().invalid()); } - void assign_new_unique_ids_recursive(); - void update_links_bottom_up_recursive(); - - friend class cereal::access; - friend class UndoRedo::StackImpl; - template void serialize(Archive &ar) { - Internal::StaticSerializationWrapper wipe_tower_wrapper(wipe_tower); - ar(materials, objects, wipe_tower_wrapper); - } -}; - -ENABLE_ENUM_BITMASK_OPERATORS(Model::LoadAttribute) - -#undef OBJECTBASE_DERIVED_COPY_MOVE_CLONE -#undef OBJECTBASE_DERIVED_PRIVATE_COPY_MOVE - -// Test whether the two models contain the same number of ModelObjects with the same set of IDs -// ordered in the same order. In that case it is not necessary to kill the background processing. -bool model_object_list_equal(const Model &model_old, const Model &model_new); - -// Test whether the new model is just an extension of the old model (new objects were added -// to the end of the original list. In that case it is not necessary to kill the background processing. -bool model_object_list_extended(const Model &model_old, const Model &model_new); - -// Test whether the new ModelObject contains a different set of volumes (or sorted in a different order) -// than the old ModelObject. -bool model_volume_list_changed(const ModelObject &model_object_old, const ModelObject &model_object_new, const ModelVolumeType type); -bool model_volume_list_changed(const ModelObject &model_object_old, const ModelObject &model_object_new, const std::initializer_list &types); - -// Test whether the now ModelObject has newer custom supports data than the old one. -// The function assumes that volumes list is synchronized. -bool model_custom_supports_data_changed(const ModelObject& mo, const ModelObject& mo_new); - -// Test whether the now ModelObject has newer custom seam data than the old one. -// The function assumes that volumes list is synchronized. -bool model_custom_seam_data_changed(const ModelObject& mo, const ModelObject& mo_new); - -// Test whether the now ModelObject has newer MMU segmentation data than the old one. -// The function assumes that volumes list is synchronized. -extern bool model_mmu_segmentation_data_changed(const ModelObject& mo, const ModelObject& mo_new); - -// If the model has multi-part objects, then it is currently not supported by the SLA mode. -// Either the model cannot be loaded, or a SLA printer has to be activated. -bool model_has_multi_part_objects(const Model &model); -// If the model has advanced features, then it cannot be processed in simple mode. -bool model_has_advanced_features(const Model &model); - -#ifndef NDEBUG -// Verify whether the IDs of Model / ModelObject / ModelVolume / ModelInstance / ModelMaterial are valid and unique. -void check_model_ids_validity(const Model &model); -void check_model_ids_equal(const Model &model1, const Model &model2); -#endif /* NDEBUG */ - -static const float SINKING_Z_THRESHOLD = -0.001f; -static const double SINKING_MIN_Z_THRESHOLD = 0.05; - -} // namespace Slic3r - -namespace cereal -{ - template struct specialize {}; - template struct specialize {}; -} - -#endif /* slic3r_Model_hpp_ */ +#ifndef slic3r_Model_hpp_ +#define slic3r_Model_hpp_ + +#include "libslic3r.h" +#include "enum_bitmask.hpp" +#include "Geometry.hpp" +#include "ObjectID.hpp" +#include "Point.hpp" +#include "PrintConfig.hpp" +#include "Slicing.hpp" +#include "SLA/SupportPoint.hpp" +#include "SLA/Hollowing.hpp" +#include "TriangleMesh.hpp" +#include "Arrange.hpp" +#include "CustomGCode.hpp" +#include "enum_bitmask.hpp" + +#include +#include +#include +#include +#include + +namespace cereal { + class BinaryInputArchive; + class BinaryOutputArchive; + template void load_optional(BinaryInputArchive &ar, std::shared_ptr &ptr); + template void save_optional(BinaryOutputArchive &ar, const std::shared_ptr &ptr); + template void load_by_value(BinaryInputArchive &ar, T &obj); + template void save_by_value(BinaryOutputArchive &ar, const T &obj); +} + +namespace Slic3r { +enum class ConversionType; + +class BuildVolume; +class Model; +class ModelInstance; +class ModelMaterial; +class ModelObject; +class ModelVolume; +class ModelWipeTower; +class Print; +class SLAPrint; +class TriangleSelector; + +namespace UndoRedo { + class StackImpl; +} + +class ModelConfigObject : public ObjectBase, public ModelConfig +{ +private: + friend class cereal::access; + friend class UndoRedo::StackImpl; + friend class ModelObject; + friend class ModelVolume; + friend class ModelMaterial; + + // Constructors to be only called by derived classes. + // Default constructor to assign a unique ID. + explicit ModelConfigObject() = default; + // Constructor with ignored int parameter to assign an invalid ID, to be replaced + // by an existing ID copied from elsewhere. + explicit ModelConfigObject(int) : ObjectBase(-1) {} + // Copy constructor copies the ID. + explicit ModelConfigObject(const ModelConfigObject &cfg) = default; + // Move constructor copies the ID. + explicit ModelConfigObject(ModelConfigObject &&cfg) = default; + + Timestamp timestamp() const throw() override { return this->ModelConfig::timestamp(); } + bool object_id_and_timestamp_match(const ModelConfigObject &rhs) const throw() { return this->id() == rhs.id() && this->timestamp() == rhs.timestamp(); } + + // called by ModelObject::assign_copy() + ModelConfigObject& operator=(const ModelConfigObject &rhs) = default; + ModelConfigObject& operator=(ModelConfigObject &&rhs) = default; + + template void serialize(Archive &ar) { + ar(cereal::base_class(this)); + } +}; + +namespace Internal { + template + class StaticSerializationWrapper + { + public: + StaticSerializationWrapper(T &wrap) : wrapped(wrap) {} + private: + friend class cereal::access; + friend class UndoRedo::StackImpl; + template void load(Archive &ar) { cereal::load_by_value(ar, wrapped); } + template void save(Archive &ar) const { cereal::save_by_value(ar, wrapped); } + T& wrapped; + }; +} + +typedef std::string t_model_material_id; +typedef std::string t_model_material_attribute; +typedef std::map t_model_material_attributes; + +typedef std::map ModelMaterialMap; +typedef std::vector ModelObjectPtrs; +typedef std::vector ModelVolumePtrs; +typedef std::vector ModelInstancePtrs; + +#define OBJECTBASE_DERIVED_COPY_MOVE_CLONE(TYPE) \ + /* Copy a model, copy the IDs. The Print::apply() will call the TYPE::copy() method */ \ + /* to make a private copy for background processing. */ \ + static TYPE* new_copy(const TYPE &rhs) { auto *ret = new TYPE(rhs); assert(ret->id() == rhs.id()); return ret; } \ + static TYPE* new_copy(TYPE &&rhs) { auto *ret = new TYPE(std::move(rhs)); assert(ret->id() == rhs.id()); return ret; } \ + static TYPE make_copy(const TYPE &rhs) { TYPE ret(rhs); assert(ret.id() == rhs.id()); return ret; } \ + static TYPE make_copy(TYPE &&rhs) { TYPE ret(std::move(rhs)); assert(ret.id() == rhs.id()); return ret; } \ + TYPE& assign_copy(const TYPE &rhs); \ + TYPE& assign_copy(TYPE &&rhs); \ + /* Copy a TYPE, generate new IDs. The front end will use this call. */ \ + static TYPE* new_clone(const TYPE &rhs) { \ + /* Default constructor assigning an invalid ID. */ \ + auto obj = new TYPE(-1); \ + obj->assign_clone(rhs); \ + assert(obj->id().valid() && obj->id() != rhs.id()); \ + return obj; \ + } \ + TYPE make_clone(const TYPE &rhs) { \ + /* Default constructor assigning an invalid ID. */ \ + TYPE obj(-1); \ + obj.assign_clone(rhs); \ + assert(obj.id().valid() && obj.id() != rhs.id()); \ + return obj; \ + } \ + TYPE& assign_clone(const TYPE &rhs) { \ + this->assign_copy(rhs); \ + assert(this->id().valid() && this->id() == rhs.id()); \ + this->assign_new_unique_ids_recursive(); \ + assert(this->id().valid() && this->id() != rhs.id()); \ + return *this; \ + } + +// Material, which may be shared across multiple ModelObjects of a single Model. +class ModelMaterial final : public ObjectBase +{ +public: + // Attributes are defined by the AMF file format, but they don't seem to be used by Slic3r for any purpose. + t_model_material_attributes attributes; + // Dynamic configuration storage for the object specific configuration values, overriding the global configuration. + ModelConfigObject config; + + Model* get_model() const { return m_model; } + void apply(const t_model_material_attributes &attributes) + { this->attributes.insert(attributes.begin(), attributes.end()); } + +private: + // Parent, owning this material. + Model *m_model; + + // To be accessed by the Model. + friend class Model; + // Constructor, which assigns a new unique ID to the material and to its config. + ModelMaterial(Model *model) : m_model(model) { assert(this->id().valid()); } + // Copy constructor copies the IDs of the ModelMaterial and its config, and m_model! + ModelMaterial(const ModelMaterial &rhs) = default; + void set_model(Model *model) { m_model = model; } + void set_new_unique_id() { ObjectBase::set_new_unique_id(); this->config.set_new_unique_id(); } + + // To be accessed by the serialization and Undo/Redo code. + friend class cereal::access; + friend class UndoRedo::StackImpl; + // Create an object for deserialization, don't allocate IDs for ModelMaterial and its config. + ModelMaterial() : ObjectBase(-1), config(-1), m_model(nullptr) { assert(this->id().invalid()); assert(this->config.id().invalid()); } + template void serialize(Archive &ar) { + assert(this->id().invalid()); assert(this->config.id().invalid()); + Internal::StaticSerializationWrapper config_wrapper(config); + ar(attributes, config_wrapper); + // assert(this->id().valid()); assert(this->config.id().valid()); + } + + // Disabled methods. + ModelMaterial(ModelMaterial &&rhs) = delete; + ModelMaterial& operator=(const ModelMaterial &rhs) = delete; + ModelMaterial& operator=(ModelMaterial &&rhs) = delete; +}; + +class LayerHeightProfile final : public ObjectWithTimestamp { +public: + // Assign the content if the timestamp differs, don't assign an ObjectID. + void assign(const LayerHeightProfile &rhs) { if (! this->timestamp_matches(rhs)) { m_data = rhs.m_data; this->copy_timestamp(rhs); } } + void assign(LayerHeightProfile &&rhs) { if (! this->timestamp_matches(rhs)) { m_data = std::move(rhs.m_data); this->copy_timestamp(rhs); } } + + std::vector get() const throw() { return m_data; } + bool empty() const throw() { return m_data.empty(); } + void set(const std::vector &data) { if (m_data != data) { m_data = data; this->touch(); } } + void set(std::vector &&data) { if (m_data != data) { m_data = std::move(data); this->touch(); } } + void clear() { m_data.clear(); this->touch(); } + + template void serialize(Archive &ar) + { + ar(cereal::base_class(this), m_data); + } + +private: + // Constructors to be only called by derived classes. + // Default constructor to assign a unique ID. + explicit LayerHeightProfile() = default; + // Constructor with ignored int parameter to assign an invalid ID, to be replaced + // by an existing ID copied from elsewhere. + explicit LayerHeightProfile(int) : ObjectWithTimestamp(-1) {} + // Copy constructor copies the ID. + explicit LayerHeightProfile(const LayerHeightProfile &rhs) = default; + // Move constructor copies the ID. + explicit LayerHeightProfile(LayerHeightProfile &&rhs) = default; + + // called by ModelObject::assign_copy() + LayerHeightProfile& operator=(const LayerHeightProfile &rhs) = default; + LayerHeightProfile& operator=(LayerHeightProfile &&rhs) = default; + + std::vector m_data; + + // to access set_new_unique_id() when copy / pasting an object + friend class ModelObject; +}; + +enum class CutConnectorType : int { + Plug + , Dowel + , Undef +}; + +enum class CutConnectorStyle : int { + Prizm + , Frustum + , Undef + //,Claw +}; + +enum class CutConnectorShape : int { + Triangle + , Square + , Hexagon + , Circle + , Undef + //,D-shape +}; + +struct CutConnectorAttributes +{ + CutConnectorType type{ CutConnectorType::Plug }; + CutConnectorStyle style{ CutConnectorStyle::Prizm }; + CutConnectorShape shape{ CutConnectorShape::Circle }; + + CutConnectorAttributes() {} + + CutConnectorAttributes(CutConnectorType t, CutConnectorStyle st, CutConnectorShape sh) + : type(t), style(st), shape(sh) + {} + + CutConnectorAttributes(const CutConnectorAttributes& rhs) : + CutConnectorAttributes(rhs.type, rhs.style, rhs.shape) {} + + bool operator==(const CutConnectorAttributes& other) const; + + bool operator!=(const CutConnectorAttributes& other) const { return !(other == (*this)); } + + bool operator<(const CutConnectorAttributes& other) const { + return this->type < other.type || + (this->type == other.type && this->style < other.style) || + (this->type == other.type && this->style == other.style && this->shape < other.shape); + } + + template inline void serialize(Archive& ar) { + ar(type, style, shape); + } +}; + +struct CutConnector +{ + Vec3d pos; + Transform3d rotation_m; + float radius; + float height; + float radius_tolerance;// [0.f : 1.f] + float height_tolerance;// [0.f : 1.f] + CutConnectorAttributes attribs; + + CutConnector() + : pos(Vec3d::Zero()), rotation_m(Transform3d::Identity()), radius(5.f), height(10.f), radius_tolerance(0.f), height_tolerance(0.1f) + {} + + CutConnector(Vec3d p, Transform3d rot, float r, float h, float rt, float ht, CutConnectorAttributes attributes) + : pos(p), rotation_m(rot), radius(r), height(h), radius_tolerance(rt), height_tolerance(ht), attribs(attributes) + {} + + CutConnector(const CutConnector& rhs) : + CutConnector(rhs.pos, rhs.rotation_m, rhs.radius, rhs.height, rhs.radius_tolerance, rhs.height_tolerance, rhs.attribs) {} + + bool operator==(const CutConnector& other) const; + + bool operator!=(const CutConnector& other) const { return !(other == (*this)); } + + template inline void serialize(Archive& ar) { + ar(pos, rotation_m, radius, height, radius_tolerance, height_tolerance, attribs); + } +}; + +using CutConnectors = std::vector; + + +// Declared outside of ModelVolume, so it could be forward declared. +enum class ModelVolumeType : int { + INVALID = -1, + MODEL_PART = 0, + NEGATIVE_VOLUME, + PARAMETER_MODIFIER, + SUPPORT_BLOCKER, + SUPPORT_ENFORCER, +}; + +enum class ModelObjectCutAttribute : int { KeepUpper, KeepLower, FlipUpper, FlipLower, PlaceOnCutUpper, PlaceOnCutLower, CreateDowels }; +using ModelObjectCutAttributes = enum_bitmask; +ENABLE_ENUM_BITMASK_OPERATORS(ModelObjectCutAttribute); + +// A printable object, possibly having multiple print volumes (each with its own set of parameters and materials), +// and possibly having multiple modifier volumes, each modifier volume with its set of parameters and materials. +// Each ModelObject may be instantiated mutliple times, each instance having different placement on the print bed, +// different rotation and different uniform scaling. +class ModelObject final : public ObjectBase +{ +public: + std::string name; + std::string input_file; // XXX: consider fs::path + // Instances of this ModelObject. Each instance defines a shift on the print bed, rotation around the Z axis and a uniform scaling. + // Instances are owned by this ModelObject. + ModelInstancePtrs instances; + // Printable and modifier volumes, each with its material ID and a set of override parameters. + // ModelVolumes are owned by this ModelObject. + ModelVolumePtrs volumes; + // Configuration parameters specific to a single ModelObject, overriding the global Slic3r settings. + ModelConfigObject config; + // Variation of a layer thickness for spans of Z coordinates + optional parameter overrides. + t_layer_config_ranges layer_config_ranges; + // Profile of increasing z to a layer height, to be linearly interpolated when calculating the layers. + // The pairs of are packed into a 1D array. + LayerHeightProfile layer_height_profile; + // Whether or not this object is printable + bool printable; + + // This vector holds position of selected support points for SLA. The data are + // saved in mesh coordinates to allow using them for several instances. + // The format is (x, y, z, point_size, supports_island) + sla::SupportPoints sla_support_points; + // To keep track of where the points came from (used for synchronization between + // the SLA gizmo and the backend). + sla::PointsStatus sla_points_status = sla::PointsStatus::NoPoints; + + // Holes to be drilled into the object so resin can flow out + sla::DrainHoles sla_drain_holes; + + // Connectors to be added into the object before cut and are used to create a solid/negative volumes during a cut perform + CutConnectors cut_connectors; + CutObjectBase cut_id; + + /* This vector accumulates the total translation applied to the object by the + center_around_origin() method. Callers might want to apply the same translation + to new volumes before adding them to this object in order to preserve alignment + when user expects that. */ + Vec3d origin_translation; + + Model* get_model() { return m_model; } + const Model* get_model() const { return m_model; } + + ModelVolume* add_volume(const TriangleMesh &mesh); + ModelVolume* add_volume(TriangleMesh &&mesh, ModelVolumeType type = ModelVolumeType::MODEL_PART); + ModelVolume* add_volume(const ModelVolume &volume, ModelVolumeType type = ModelVolumeType::INVALID); + ModelVolume* add_volume(const ModelVolume &volume, TriangleMesh &&mesh); + void delete_volume(size_t idx); + void clear_volumes(); + void sort_volumes(bool full_sort); + bool is_multiparts() const { return volumes.size() > 1; } + // Checks if any of object volume is painted using the fdm support painting gizmo. + bool is_fdm_support_painted() const; + // Checks if any of object volume is painted using the seam painting gizmo. + bool is_seam_painted() const; + // Checks if any of object volume is painted using the multi-material painting gizmo. + bool is_mm_painted() const; + + ModelInstance* add_instance(); + ModelInstance* add_instance(const ModelInstance &instance); + ModelInstance* add_instance(const Vec3d &offset, const Vec3d &scaling_factor, const Vec3d &rotation, const Vec3d &mirror); + void delete_instance(size_t idx); + void delete_last_instance(); + void clear_instances(); + + // Returns the bounding box of the transformed instances. + // This bounding box is approximate and not snug. + // This bounding box is being cached. + const BoundingBoxf3& bounding_box() const; + void invalidate_bounding_box() { m_bounding_box_valid = false; m_raw_bounding_box_valid = false; m_raw_mesh_bounding_box_valid = false; } + + // A mesh containing all transformed instances of this object. + TriangleMesh mesh() const; + // Non-transformed (non-rotated, non-scaled, non-translated) sum of non-modifier object volumes. + // Currently used by ModelObject::mesh() and to calculate the 2D envelope for 2D plater. + TriangleMesh raw_mesh() const; + // The same as above, but producing a lightweight indexed_triangle_set. + indexed_triangle_set raw_indexed_triangle_set() const; + // A transformed snug bounding box around the non-modifier object volumes, without the translation applied. + // This bounding box is only used for the actual slicing. + const BoundingBoxf3& raw_bounding_box() const; + // A snug bounding box around the transformed non-modifier object volumes. + BoundingBoxf3 instance_bounding_box(size_t instance_idx, bool dont_translate = false) const; + // A snug bounding box of non-transformed (non-rotated, non-scaled, non-translated) sum of non-modifier object volumes. + const BoundingBoxf3& raw_mesh_bounding_box() const; + // A snug bounding box of non-transformed (non-rotated, non-scaled, non-translated) sum of all object volumes. + BoundingBoxf3 full_raw_mesh_bounding_box() const; + + // Calculate 2D convex hull of of a projection of the transformed printable volumes into the XY plane. + // This method is cheap in that it does not make any unnecessary copy of the volume meshes. + // This method is used by the auto arrange function. + Polygon convex_hull_2d(const Transform3d &trafo_instance) const; + + void center_around_origin(bool include_modifiers = true); + void ensure_on_bed(bool allow_negative_z = false); + + void translate_instances(const Vec3d& vector); + void translate_instance(size_t instance_idx, const Vec3d& vector); + void translate(const Vec3d &vector) { this->translate(vector(0), vector(1), vector(2)); } + void translate(double x, double y, double z); + void scale(const Vec3d &versor); + void scale(const double s) { this->scale(Vec3d(s, s, s)); } + void scale(double x, double y, double z) { this->scale(Vec3d(x, y, z)); } + /// Scale the current ModelObject to fit by altering the scaling factor of ModelInstances. + /// It operates on the total size by duplicating the object according to all the instances. + /// \param size Sizef3 the size vector + void scale_to_fit(const Vec3d &size); + void rotate(double angle, Axis axis); + void rotate(double angle, const Vec3d& axis); + void mirror(Axis axis); + + // This method could only be called before the meshes of this ModelVolumes are not shared! + void scale_mesh_after_creation(const float scale); + void convert_units(ModelObjectPtrs&new_objects, ConversionType conv_type, std::vector volume_idxs); + + size_t materials_count() const; + size_t facets_count() const; + size_t parts_count() const; + static indexed_triangle_set get_connector_mesh(CutConnectorAttributes connector_attributes); + void apply_cut_connectors(const std::string& name); + // invalidate cut state for this object and its connectors/volumes + void invalidate_cut(); + void synchronize_model_after_cut(); + void apply_cut_attributes(ModelObjectCutAttributes attributes); + void clone_for_cut(ModelObject **obj); + void process_connector_cut(ModelVolume* volume, ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower, + std::vector& dowels, Vec3d& local_dowels_displace); + void process_modifier_cut(ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& inverse_cut_matrix, + ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower); + void process_solid_part_cut(ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& cut_matrix, + ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower, Vec3d& local_displace); + ModelObjectPtrs cut(size_t instance, const Transform3d&cut_matrix, ModelObjectCutAttributes attributes); + void split(ModelObjectPtrs*new_objects); + void merge(); + // Support for non-uniform scaling of instances. If an instance is rotated by angles, which are not multiples of ninety degrees, + // then the scaling in world coordinate system is not representable by the Geometry::Transformation structure. + // This situation is solved by baking in the instance transformation into the mesh vertices. + // Rotation and mirroring is being baked in. In case the instance scaling was non-uniform, it is baked in as well. + void bake_xy_rotation_into_meshes(size_t instance_idx); + + double get_min_z() const; + double get_max_z() const; + double get_instance_min_z(size_t instance_idx) const; + double get_instance_max_z(size_t instance_idx) const; + + // Print object statistics to console. + void print_info() const; + + std::string get_export_filename() const; + + // Get full stl statistics for all object's meshes + TriangleMeshStats get_object_stl_stats() const; + // Get count of errors in the mesh( or all object's meshes, if volume index isn't defined) + int get_repaired_errors_count(const int vol_idx = -1) const; + + bool is_cut() const { return cut_id.id().valid(); } + bool has_connectors() const; + +private: + friend class Model; + // This constructor assigns new ID to this ModelObject and its config. + explicit ModelObject(Model* model) : m_model(model), printable(true), origin_translation(Vec3d::Zero()), + m_bounding_box_valid(false), m_raw_bounding_box_valid(false), m_raw_mesh_bounding_box_valid(false) + { + assert(this->id().valid()); + assert(this->config.id().valid()); + assert(this->layer_height_profile.id().valid()); + } + explicit ModelObject(int) : ObjectBase(-1), config(-1), layer_height_profile(-1), m_model(nullptr), printable(true), origin_translation(Vec3d::Zero()), m_bounding_box_valid(false), m_raw_bounding_box_valid(false), m_raw_mesh_bounding_box_valid(false) + { + assert(this->id().invalid()); + assert(this->config.id().invalid()); + assert(this->layer_height_profile.id().invalid()); + } + ~ModelObject(); + void assign_new_unique_ids_recursive() override; + + // To be able to return an object from own copy / clone methods. Hopefully the compiler will do the "Copy elision" + // (Omits copy and move(since C++11) constructors, resulting in zero - copy pass - by - value semantics). + ModelObject(const ModelObject &rhs) : ObjectBase(-1), config(-1), layer_height_profile(-1), m_model(rhs.m_model) { + assert(this->id().invalid()); + assert(this->config.id().invalid()); + assert(this->layer_height_profile.id().invalid()); + assert(rhs.id() != rhs.config.id()); + assert(rhs.id() != rhs.layer_height_profile.id()); + this->assign_copy(rhs); + assert(this->id().valid()); + assert(this->config.id().valid()); + assert(this->layer_height_profile.id().valid()); + assert(this->id() != this->config.id()); + assert(this->id() != this->layer_height_profile.id()); + assert(this->id() == rhs.id()); + assert(this->config.id() == rhs.config.id()); + assert(this->layer_height_profile.id() == rhs.layer_height_profile.id()); + } + explicit ModelObject(ModelObject &&rhs) : ObjectBase(-1), config(-1), layer_height_profile(-1) { + assert(this->id().invalid()); + assert(this->config.id().invalid()); + assert(this->layer_height_profile.id().invalid()); + assert(rhs.id() != rhs.config.id()); + assert(rhs.id() != rhs.layer_height_profile.id()); + this->assign_copy(std::move(rhs)); + assert(this->id().valid()); + assert(this->config.id().valid()); + assert(this->layer_height_profile.id().valid()); + assert(this->id() != this->config.id()); + assert(this->id() != this->layer_height_profile.id()); + assert(this->id() == rhs.id()); + assert(this->config.id() == rhs.config.id()); + assert(this->layer_height_profile.id() == rhs.layer_height_profile.id()); + } + ModelObject& operator=(const ModelObject &rhs) { + this->assign_copy(rhs); + m_model = rhs.m_model; + assert(this->id().valid()); + assert(this->config.id().valid()); + assert(this->layer_height_profile.id().valid()); + assert(this->id() != this->config.id()); + assert(this->id() != this->layer_height_profile.id()); + assert(this->id() == rhs.id()); + assert(this->config.id() == rhs.config.id()); + assert(this->layer_height_profile.id() == rhs.layer_height_profile.id()); + return *this; + } + ModelObject& operator=(ModelObject &&rhs) { + this->assign_copy(std::move(rhs)); + m_model = rhs.m_model; + assert(this->id().valid()); + assert(this->config.id().valid()); + assert(this->layer_height_profile.id().valid()); + assert(this->id() != this->config.id()); + assert(this->id() != this->layer_height_profile.id()); + assert(this->id() == rhs.id()); + assert(this->config.id() == rhs.config.id()); + assert(this->layer_height_profile.id() == rhs.layer_height_profile.id()); + return *this; + } + void set_new_unique_id() { + ObjectBase::set_new_unique_id(); + this->config.set_new_unique_id(); + this->layer_height_profile.set_new_unique_id(); + } + + OBJECTBASE_DERIVED_COPY_MOVE_CLONE(ModelObject) + + // Parent object, owning this ModelObject. Set to nullptr here, so the macros above will have it initialized. + Model *m_model = nullptr; + + // Bounding box, cached. + mutable BoundingBoxf3 m_bounding_box; + mutable bool m_bounding_box_valid; + mutable BoundingBoxf3 m_raw_bounding_box; + mutable bool m_raw_bounding_box_valid; + mutable BoundingBoxf3 m_raw_mesh_bounding_box; + mutable bool m_raw_mesh_bounding_box_valid; + + // Called by Print::apply() to set the model pointer after making a copy. + friend class Print; + friend class SLAPrint; + void set_model(Model *model) { m_model = model; } + + // Undo / Redo through the cereal serialization library + friend class cereal::access; + friend class UndoRedo::StackImpl; + // Used for deserialization -> Don't allocate any IDs for the ModelObject or its config. + ModelObject() : + ObjectBase(-1), config(-1), layer_height_profile(-1), + m_model(nullptr), m_bounding_box_valid(false), m_raw_bounding_box_valid(false), m_raw_mesh_bounding_box_valid(false) { + assert(this->id().invalid()); + assert(this->config.id().invalid()); + assert(this->layer_height_profile.id().invalid()); + } + template void serialize(Archive &ar) { + ar(cereal::base_class(this)); + Internal::StaticSerializationWrapper config_wrapper(config); + Internal::StaticSerializationWrapper layer_heigth_profile_wrapper(layer_height_profile); + ar(name, input_file, instances, volumes, config_wrapper, layer_config_ranges, layer_heigth_profile_wrapper, + sla_support_points, sla_points_status, sla_drain_holes, printable, origin_translation, + m_bounding_box, m_bounding_box_valid, m_raw_bounding_box, m_raw_bounding_box_valid, m_raw_mesh_bounding_box, m_raw_mesh_bounding_box_valid, + cut_connectors, cut_id); + } + + // Called by Print::validate() from the UI thread. + unsigned int update_instances_print_volume_state(const BuildVolume &build_volume); +}; + +enum class EnforcerBlockerType : int8_t { + // Maximum is 3. The value is serialized in TriangleSelector into 2 bits. + NONE = 0, + ENFORCER = 1, + BLOCKER = 2, + // Maximum is 15. The value is serialized in TriangleSelector into 6 bits using a 2 bit prefix code. + Extruder1 = ENFORCER, + Extruder2 = BLOCKER, + Extruder3, + Extruder4, + Extruder5, + Extruder6, + Extruder7, + Extruder8, + Extruder9, + Extruder10, + Extruder11, + Extruder12, + Extruder13, + Extruder14, + Extruder15, +}; + +enum class ConversionType : int { + CONV_TO_INCH, + CONV_FROM_INCH, + CONV_TO_METER, + CONV_FROM_METER, +}; + +class FacetsAnnotation final : public ObjectWithTimestamp { +public: + // Assign the content if the timestamp differs, don't assign an ObjectID. + void assign(const FacetsAnnotation& rhs) { if (! this->timestamp_matches(rhs)) { m_data = rhs.m_data; this->copy_timestamp(rhs); } } + void assign(FacetsAnnotation&& rhs) { if (! this->timestamp_matches(rhs)) { m_data = std::move(rhs.m_data); this->copy_timestamp(rhs); } } + const std::pair>, std::vector>& get_data() const throw() { return m_data; } + bool set(const TriangleSelector& selector); + indexed_triangle_set get_facets(const ModelVolume& mv, EnforcerBlockerType type) const; + indexed_triangle_set get_facets_strict(const ModelVolume& mv, EnforcerBlockerType type) const; + bool has_facets(const ModelVolume& mv, EnforcerBlockerType type) const; + bool empty() const { return m_data.first.empty(); } + + // Following method clears the config and increases its timestamp, so the deleted + // state is considered changed from perspective of the undo/redo stack. + void reset(); + + // Serialize triangle into string, for serialization into 3MF/AMF. + std::string get_triangle_as_string(int i) const; + + // Before deserialization, reserve space for n_triangles. + void reserve(int n_triangles) { m_data.first.reserve(n_triangles); } + // Deserialize triangles one by one, with strictly increasing triangle_id. + void set_triangle_from_string(int triangle_id, const std::string& str); + // After deserializing the last triangle, shrink data to fit. + void shrink_to_fit() { m_data.first.shrink_to_fit(); m_data.second.shrink_to_fit(); } + +private: + // Constructors to be only called by derived classes. + // Default constructor to assign a unique ID. + explicit FacetsAnnotation() = default; + // Constructor with ignored int parameter to assign an invalid ID, to be replaced + // by an existing ID copied from elsewhere. + explicit FacetsAnnotation(int) : ObjectWithTimestamp(-1) {} + // Copy constructor copies the ID. + explicit FacetsAnnotation(const FacetsAnnotation &rhs) = default; + // Move constructor copies the ID. + explicit FacetsAnnotation(FacetsAnnotation &&rhs) = default; + + // called by ModelVolume::assign_copy() + FacetsAnnotation& operator=(const FacetsAnnotation &rhs) = default; + FacetsAnnotation& operator=(FacetsAnnotation &&rhs) = default; + + friend class cereal::access; + friend class UndoRedo::StackImpl; + + template void serialize(Archive &ar) + { + ar(cereal::base_class(this), m_data); + } + + std::pair>, std::vector> m_data; + + // To access set_new_unique_id() when copy / pasting a ModelVolume. + friend class ModelVolume; +}; + +// An object STL, or a modifier volume, over which a different set of parameters shall be applied. +// ModelVolume instances are owned by a ModelObject. +class ModelVolume final : public ObjectBase +{ +public: + std::string name; + // struct used by reload from disk command to recover data from disk + struct Source + { + std::string input_file; + int object_idx{ -1 }; + int volume_idx{ -1 }; + Vec3d mesh_offset{ Vec3d::Zero() }; + Geometry::Transformation transform; + bool is_converted_from_inches{ false }; + bool is_converted_from_meters{ false }; + bool is_from_builtin_objects{ false }; + + template void serialize(Archive& ar) { + //FIXME Vojtech: Serialize / deserialize only if the Source is set. + // likely testing input_file or object_idx would be sufficient. + ar(input_file, object_idx, volume_idx, mesh_offset, transform, is_converted_from_inches, is_converted_from_meters, is_from_builtin_objects); + } + }; + Source source; + + // struct used by cut command + // It contains information about connetors + struct CutInfo + { + bool is_connector{ false }; + bool is_processed{ true }; + CutConnectorType connector_type{ CutConnectorType::Plug }; + float radius_tolerance{ 0.f };// [0.f : 1.f] + float height_tolerance{ 0.f };// [0.f : 1.f] + + CutInfo() = default; + CutInfo(CutConnectorType type, float rad_tolerance, float h_tolerance, bool processed = false) : + is_connector(true), + is_processed(processed), + connector_type(type), + radius_tolerance(rad_tolerance), + height_tolerance(h_tolerance) + {} + + void set_processed() { is_processed = true; } + void invalidate() { is_connector = false; } + + template inline void serialize(Archive& ar) { + ar(is_connector, is_processed, connector_type, radius_tolerance, height_tolerance); + } + }; + CutInfo cut_info; + + bool is_cut_connector() const { return cut_info.is_processed && cut_info.is_connector; } + void invalidate_cut_info() { cut_info.invalidate(); } + + // The triangular model. + const TriangleMesh& mesh() const { return *m_mesh.get(); } +#if ENABLE_RAYCAST_PICKING + std::shared_ptr mesh_ptr() const { return m_mesh; } +#endif // ENABLE_RAYCAST_PICKING + void set_mesh(const TriangleMesh &mesh) { m_mesh = std::make_shared(mesh); } + void set_mesh(TriangleMesh &&mesh) { m_mesh = std::make_shared(std::move(mesh)); } + void set_mesh(const indexed_triangle_set &mesh) { m_mesh = std::make_shared(mesh); } + void set_mesh(indexed_triangle_set &&mesh) { m_mesh = std::make_shared(std::move(mesh)); } + void set_mesh(std::shared_ptr &mesh) { m_mesh = mesh; } + void set_mesh(std::unique_ptr &&mesh) { m_mesh = std::move(mesh); } + void reset_mesh() { m_mesh = std::make_shared(); } + // Configuration parameters specific to an object model geometry or a modifier volume, + // overriding the global Slic3r settings and the ModelObject settings. + ModelConfigObject config; + + // List of mesh facets to be supported/unsupported. + FacetsAnnotation supported_facets; + + // List of seam enforcers/blockers. + FacetsAnnotation seam_facets; + + // List of mesh facets painted for MMU segmentation. + FacetsAnnotation mmu_segmentation_facets; + + // A parent object owning this modifier volume. + ModelObject* get_object() const { return this->object; } + ModelVolumeType type() const { return m_type; } + void set_type(const ModelVolumeType t) { m_type = t; } + bool is_model_part() const { return m_type == ModelVolumeType::MODEL_PART; } + bool is_negative_volume() const { return m_type == ModelVolumeType::NEGATIVE_VOLUME; } + bool is_modifier() const { return m_type == ModelVolumeType::PARAMETER_MODIFIER; } + bool is_support_enforcer() const { return m_type == ModelVolumeType::SUPPORT_ENFORCER; } + bool is_support_blocker() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER; } + bool is_support_modifier() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER || m_type == ModelVolumeType::SUPPORT_ENFORCER; } + t_model_material_id material_id() const { return m_material_id; } + void reset_extra_facets(); + void apply_tolerance(); + void set_material_id(t_model_material_id material_id); + ModelMaterial* material() const; + void set_material(t_model_material_id material_id, const ModelMaterial &material); + // Extract the current extruder ID based on this ModelVolume's config and the parent ModelObject's config. + // Extruder ID is only valid for FFF. Returns -1 for SLA or if the extruder ID is not applicable (support volumes). + int extruder_id() const; + + bool is_splittable() const; + + // Split this volume, append the result to the object owning this volume. + // Return the number of volumes created from this one. + // This is useful to assign different materials to different volumes of an object. + size_t split(unsigned int max_extruders); + void translate(double x, double y, double z) { translate(Vec3d(x, y, z)); } + void translate(const Vec3d& displacement); + void scale(const Vec3d& scaling_factors); + void scale(double x, double y, double z) { scale(Vec3d(x, y, z)); } + void scale(double s) { scale(Vec3d(s, s, s)); } + void rotate(double angle, Axis axis); + void rotate(double angle, const Vec3d& axis); + void mirror(Axis axis); + + // This method could only be called before the meshes of this ModelVolumes are not shared! + void scale_geometry_after_creation(const Vec3f &versor); + void scale_geometry_after_creation(const float scale) { this->scale_geometry_after_creation(Vec3f(scale, scale, scale)); } + + // Translates the mesh and the convex hull so that the origin of their vertices is in the center of this volume's bounding box. + // Attention! This method may only be called just after ModelVolume creation! It must not be called once the TriangleMesh of this ModelVolume is shared! + void center_geometry_after_creation(bool update_source_offset = true); + + void calculate_convex_hull(); + const TriangleMesh& get_convex_hull() const; + const std::shared_ptr& get_convex_hull_shared_ptr() const { return m_convex_hull; } + // Get count of errors in the mesh + int get_repaired_errors_count() const; + + // Helpers for loading / storing into AMF / 3MF files. + static ModelVolumeType type_from_string(const std::string &s); + static std::string type_to_string(const ModelVolumeType t); + + const Geometry::Transformation& get_transformation() const { return m_transformation; } + void set_transformation(const Geometry::Transformation& transformation) { m_transformation = transformation; } +#if ENABLE_WORLD_COORDINATE + void set_transformation(const Transform3d& trafo) { m_transformation.set_matrix(trafo); } + + Vec3d get_offset() const { return m_transformation.get_offset(); } +#else + void set_transformation(const Transform3d &trafo) { m_transformation.set_from_transform(trafo); } + + const Vec3d& get_offset() const { return m_transformation.get_offset(); } +#endif // ENABLE_WORLD_COORDINATE + + double get_offset(Axis axis) const { return m_transformation.get_offset(axis); } + + void set_offset(const Vec3d& offset) { m_transformation.set_offset(offset); } + void set_offset(Axis axis, double offset) { m_transformation.set_offset(axis, offset); } + +#if ENABLE_WORLD_COORDINATE + Vec3d get_rotation() const { return m_transformation.get_rotation(); } +#else + const Vec3d& get_rotation() const { return m_transformation.get_rotation(); } +#endif // ENABLE_WORLD_COORDINATE + double get_rotation(Axis axis) const { return m_transformation.get_rotation(axis); } + + void set_rotation(const Vec3d& rotation) { m_transformation.set_rotation(rotation); } + void set_rotation(Axis axis, double rotation) { m_transformation.set_rotation(axis, rotation); } + + Vec3d get_scaling_factor() const { return m_transformation.get_scaling_factor(); } + double get_scaling_factor(Axis axis) const { return m_transformation.get_scaling_factor(axis); } + + void set_scaling_factor(const Vec3d& scaling_factor) { m_transformation.set_scaling_factor(scaling_factor); } + void set_scaling_factor(Axis axis, double scaling_factor) { m_transformation.set_scaling_factor(axis, scaling_factor); } + +#if ENABLE_WORLD_COORDINATE + Vec3d get_mirror() const { return m_transformation.get_mirror(); } +#else + const Vec3d& get_mirror() const { return m_transformation.get_mirror(); } +#endif // ENABLE_WORLD_COORDINATE + double get_mirror(Axis axis) const { return m_transformation.get_mirror(axis); } + bool is_left_handed() const { return m_transformation.is_left_handed(); } + + void set_mirror(const Vec3d& mirror) { m_transformation.set_mirror(mirror); } + void set_mirror(Axis axis, double mirror) { m_transformation.set_mirror(axis, mirror); } + void convert_from_imperial_units(); + void convert_from_meters(); + +#if ENABLE_WORLD_COORDINATE + const Transform3d& get_matrix() const { return m_transformation.get_matrix(); } + Transform3d get_matrix_no_offset() const { return m_transformation.get_matrix_no_offset(); } +#else + const Transform3d& get_matrix(bool dont_translate = false, bool dont_rotate = false, bool dont_scale = false, bool dont_mirror = false) const { return m_transformation.get_matrix(dont_translate, dont_rotate, dont_scale, dont_mirror); } +#endif // ENABLE_WORLD_COORDINATE + + void set_new_unique_id() { + ObjectBase::set_new_unique_id(); + this->config.set_new_unique_id(); + this->supported_facets.set_new_unique_id(); + this->seam_facets.set_new_unique_id(); + this->mmu_segmentation_facets.set_new_unique_id(); + } + + bool is_fdm_support_painted() const { return !this->supported_facets.empty(); } + bool is_seam_painted() const { return !this->seam_facets.empty(); } + bool is_mm_painted() const { return !this->mmu_segmentation_facets.empty(); } + +protected: + friend class Print; + friend class SLAPrint; + friend class Model; + friend class ModelObject; + friend void model_volume_list_update_supports(ModelObject& model_object_dst, const ModelObject& model_object_new); + + // Copies IDs of both the ModelVolume and its config. + explicit ModelVolume(const ModelVolume &rhs) = default; + void set_model_object(ModelObject *model_object) { object = model_object; } + void assign_new_unique_ids_recursive() override; + void transform_this_mesh(const Transform3d& t, bool fix_left_handed); + void transform_this_mesh(const Matrix3d& m, bool fix_left_handed); + +private: + // Parent object owning this ModelVolume. + ModelObject* object; + // The triangular model. + std::shared_ptr m_mesh; + // Is it an object to be printed, or a modifier volume? + ModelVolumeType m_type; + t_model_material_id m_material_id; + // The convex hull of this model's mesh. + std::shared_ptr m_convex_hull; + Geometry::Transformation m_transformation; + + // flag to optimize the checking if the volume is splittable + // -1 -> is unknown value (before first cheking) + // 0 -> is not splittable + // 1 -> is splittable + mutable int m_is_splittable{ -1 }; + + ModelVolume(ModelObject *object, const TriangleMesh &mesh, ModelVolumeType type = ModelVolumeType::MODEL_PART) : m_mesh(new TriangleMesh(mesh)), m_type(type), object(object) + { + assert(this->id().valid()); + assert(this->config.id().valid()); + assert(this->supported_facets.id().valid()); + assert(this->seam_facets.id().valid()); + assert(this->mmu_segmentation_facets.id().valid()); + assert(this->id() != this->config.id()); + assert(this->id() != this->supported_facets.id()); + assert(this->id() != this->seam_facets.id()); + assert(this->id() != this->mmu_segmentation_facets.id()); + if (mesh.facets_count() > 1) + calculate_convex_hull(); + } + ModelVolume(ModelObject *object, TriangleMesh &&mesh, TriangleMesh &&convex_hull, ModelVolumeType type = ModelVolumeType::MODEL_PART) : + m_mesh(new TriangleMesh(std::move(mesh))), m_convex_hull(new TriangleMesh(std::move(convex_hull))), m_type(type), object(object) { + assert(this->id().valid()); + assert(this->config.id().valid()); + assert(this->supported_facets.id().valid()); + assert(this->seam_facets.id().valid()); + assert(this->mmu_segmentation_facets.id().valid()); + assert(this->id() != this->config.id()); + assert(this->id() != this->supported_facets.id()); + assert(this->id() != this->seam_facets.id()); + assert(this->id() != this->mmu_segmentation_facets.id()); + } + + // Copying an existing volume, therefore this volume will get a copy of the ID assigned. + ModelVolume(ModelObject *object, const ModelVolume &other) : + ObjectBase(other), + name(other.name), source(other.source), m_mesh(other.m_mesh), m_convex_hull(other.m_convex_hull), + config(other.config), m_type(other.m_type), object(object), m_transformation(other.m_transformation), + supported_facets(other.supported_facets), seam_facets(other.seam_facets), mmu_segmentation_facets(other.mmu_segmentation_facets), + cut_info(other.cut_info) + { + assert(this->id().valid()); + assert(this->config.id().valid()); + assert(this->supported_facets.id().valid()); + assert(this->seam_facets.id().valid()); + assert(this->mmu_segmentation_facets.id().valid()); + assert(this->id() != this->config.id()); + assert(this->id() != this->supported_facets.id()); + assert(this->id() != this->seam_facets.id()); + assert(this->id() != this->mmu_segmentation_facets.id()); + assert(this->id() == other.id()); + assert(this->config.id() == other.config.id()); + assert(this->supported_facets.id() == other.supported_facets.id()); + assert(this->seam_facets.id() == other.seam_facets.id()); + assert(this->mmu_segmentation_facets.id() == other.mmu_segmentation_facets.id()); + this->set_material_id(other.material_id()); + } + // Providing a new mesh, therefore this volume will get a new unique ID assigned. + ModelVolume(ModelObject *object, const ModelVolume &other, TriangleMesh &&mesh) : + name(other.name), source(other.source), config(other.config), object(object), m_mesh(new TriangleMesh(std::move(mesh))), m_type(other.m_type), m_transformation(other.m_transformation), + cut_info(other.cut_info) + { + assert(this->id().valid()); + assert(this->config.id().valid()); + assert(this->supported_facets.id().valid()); + assert(this->seam_facets.id().valid()); + assert(this->mmu_segmentation_facets.id().valid()); + assert(this->id() != this->config.id()); + assert(this->id() != this->supported_facets.id()); + assert(this->id() != this->seam_facets.id()); + assert(this->id() != this->mmu_segmentation_facets.id()); + assert(this->id() != other.id()); + assert(this->config.id() == other.config.id()); + this->set_material_id(other.material_id()); + this->config.set_new_unique_id(); + if (m_mesh->facets_count() > 1) + calculate_convex_hull(); + assert(this->config.id().valid()); + assert(this->config.id() != other.config.id()); + assert(this->supported_facets.id() != other.supported_facets.id()); + assert(this->seam_facets.id() != other.seam_facets.id()); + assert(this->mmu_segmentation_facets.id() != other.mmu_segmentation_facets.id()); + assert(this->id() != this->config.id()); + assert(this->supported_facets.empty()); + assert(this->seam_facets.empty()); + assert(this->mmu_segmentation_facets.empty()); + } + + ModelVolume& operator=(ModelVolume &rhs) = delete; + + friend class cereal::access; + friend class UndoRedo::StackImpl; + // Used for deserialization, therefore no IDs are allocated. + ModelVolume() : ObjectBase(-1), config(-1), supported_facets(-1), seam_facets(-1), mmu_segmentation_facets(-1), object(nullptr) { + assert(this->id().invalid()); + assert(this->config.id().invalid()); + assert(this->supported_facets.id().invalid()); + assert(this->seam_facets.id().invalid()); + assert(this->mmu_segmentation_facets.id().invalid()); + } + template void load(Archive &ar) { + bool has_convex_hull; + ar(name, source, m_mesh, m_type, m_material_id, m_transformation, m_is_splittable, has_convex_hull, cut_info); + cereal::load_by_value(ar, supported_facets); + cereal::load_by_value(ar, seam_facets); + cereal::load_by_value(ar, mmu_segmentation_facets); + cereal::load_by_value(ar, config); + assert(m_mesh); + if (has_convex_hull) { + cereal::load_optional(ar, m_convex_hull); + if (! m_convex_hull && ! m_mesh->empty()) + // The convex hull was released from the Undo / Redo stack to conserve memory. Recalculate it. + this->calculate_convex_hull(); + } else + m_convex_hull.reset(); + } + template void save(Archive &ar) const { + bool has_convex_hull = m_convex_hull.get() != nullptr; + ar(name, source, m_mesh, m_type, m_material_id, m_transformation, m_is_splittable, has_convex_hull, cut_info); + cereal::save_by_value(ar, supported_facets); + cereal::save_by_value(ar, seam_facets); + cereal::save_by_value(ar, mmu_segmentation_facets); + cereal::save_by_value(ar, config); + if (has_convex_hull) + cereal::save_optional(ar, m_convex_hull); + } +}; + +inline void model_volumes_sort_by_id(ModelVolumePtrs &model_volumes) +{ + std::sort(model_volumes.begin(), model_volumes.end(), [](const ModelVolume *l, const ModelVolume *r) { return l->id() < r->id(); }); +} + +inline const ModelVolume* model_volume_find_by_id(const ModelVolumePtrs &model_volumes, const ObjectID id) +{ + auto it = lower_bound_by_predicate(model_volumes.begin(), model_volumes.end(), [id](const ModelVolume *mv) { return mv->id() < id; }); + return it != model_volumes.end() && (*it)->id() == id ? *it : nullptr; +} + +enum ModelInstanceEPrintVolumeState : unsigned char +{ + ModelInstancePVS_Inside, + ModelInstancePVS_Partly_Outside, + ModelInstancePVS_Fully_Outside, + ModelInstanceNum_BedStates +}; + +// A single instance of a ModelObject. +// Knows the affine transformation of an object. +class ModelInstance final : public ObjectBase +{ +private: + Geometry::Transformation m_transformation; + +public: + // flag showing the position of this instance with respect to the print volume (set by Print::validate() using ModelObject::check_instances_print_volume_state()) + ModelInstanceEPrintVolumeState print_volume_state; + // Whether or not this instance is printable + bool printable; + + ModelObject* get_object() const { return this->object; } + + const Geometry::Transformation& get_transformation() const { return m_transformation; } + void set_transformation(const Geometry::Transformation& transformation) { m_transformation = transformation; } + +#if ENABLE_WORLD_COORDINATE + Vec3d get_offset() const { return m_transformation.get_offset(); } +#else + const Vec3d& get_offset() const { return m_transformation.get_offset(); } +#endif // ENABLE_WORLD_COORDINATE + double get_offset(Axis axis) const { return m_transformation.get_offset(axis); } + + void set_offset(const Vec3d& offset) { m_transformation.set_offset(offset); } + void set_offset(Axis axis, double offset) { m_transformation.set_offset(axis, offset); } + +#if ENABLE_WORLD_COORDINATE + Vec3d get_rotation() const { return m_transformation.get_rotation(); } +#else + const Vec3d& get_rotation() const { return m_transformation.get_rotation(); } +#endif // ENABLE_WORLD_COORDINATE + double get_rotation(Axis axis) const { return m_transformation.get_rotation(axis); } + + void set_rotation(const Vec3d& rotation) { m_transformation.set_rotation(rotation); } + void set_rotation(Axis axis, double rotation) { m_transformation.set_rotation(axis, rotation); } + +#if ENABLE_WORLD_COORDINATE + Vec3d get_scaling_factor() const { return m_transformation.get_scaling_factor(); } +#else + const Vec3d& get_scaling_factor() const { return m_transformation.get_scaling_factor(); } +#endif // ENABLE_WORLD_COORDINATE + double get_scaling_factor(Axis axis) const { return m_transformation.get_scaling_factor(axis); } + + void set_scaling_factor(const Vec3d& scaling_factor) { m_transformation.set_scaling_factor(scaling_factor); } + void set_scaling_factor(Axis axis, double scaling_factor) { m_transformation.set_scaling_factor(axis, scaling_factor); } + +#if ENABLE_WORLD_COORDINATE + Vec3d get_mirror() const { return m_transformation.get_mirror(); } +#else + const Vec3d& get_mirror() const { return m_transformation.get_mirror(); } +#endif // ENABLE_WORLD_COORDINATE + double get_mirror(Axis axis) const { return m_transformation.get_mirror(axis); } + bool is_left_handed() const { return m_transformation.is_left_handed(); } + + void set_mirror(const Vec3d& mirror) { m_transformation.set_mirror(mirror); } + void set_mirror(Axis axis, double mirror) { m_transformation.set_mirror(axis, mirror); } + + // To be called on an external mesh + void transform_mesh(TriangleMesh* mesh, bool dont_translate = false) const; + // Calculate a bounding box of a transformed mesh. To be called on an external mesh. + BoundingBoxf3 transform_mesh_bounding_box(const TriangleMesh& mesh, bool dont_translate = false) const; + // Transform an external bounding box. + BoundingBoxf3 transform_bounding_box(const BoundingBoxf3 &bbox, bool dont_translate = false) const; + // Transform an external vector. + Vec3d transform_vector(const Vec3d& v, bool dont_translate = false) const; + // To be called on an external polygon. It does not translate the polygon, only rotates and scales. + void transform_polygon(Polygon* polygon) const; + +#if ENABLE_WORLD_COORDINATE + const Transform3d& get_matrix() const { return m_transformation.get_matrix(); } + Transform3d get_matrix_no_offset() const { return m_transformation.get_matrix_no_offset(); } +#else + const Transform3d& get_matrix(bool dont_translate = false, bool dont_rotate = false, bool dont_scale = false, bool dont_mirror = false) const { return m_transformation.get_matrix(dont_translate, dont_rotate, dont_scale, dont_mirror); } +#endif // ENABLE_WORLD_COORDINATE + + bool is_printable() const { return object->printable && printable && (print_volume_state == ModelInstancePVS_Inside); } + + // Getting the input polygon for arrange + arrangement::ArrangePolygon get_arrange_polygon() const; + + // Apply the arrange result on the ModelInstance + void apply_arrange_result(const Vec2d& offs, double rotation) + { + // write the transformation data into the model instance + set_rotation(Z, rotation); + set_offset(X, unscale(offs(X))); + set_offset(Y, unscale(offs(Y))); + this->object->invalidate_bounding_box(); + } + +protected: + friend class Print; + friend class SLAPrint; + friend class Model; + friend class ModelObject; + + explicit ModelInstance(const ModelInstance &rhs) = default; + void set_model_object(ModelObject *model_object) { object = model_object; } + +private: + // Parent object, owning this instance. + ModelObject* object; + + // Constructor, which assigns a new unique ID. + explicit ModelInstance(ModelObject* object) : print_volume_state(ModelInstancePVS_Inside), printable(true), object(object) { assert(this->id().valid()); } + // Constructor, which assigns a new unique ID. + explicit ModelInstance(ModelObject *object, const ModelInstance &other) : + m_transformation(other.m_transformation), print_volume_state(ModelInstancePVS_Inside), printable(other.printable), object(object) { assert(this->id().valid() && this->id() != other.id()); } + + explicit ModelInstance(ModelInstance &&rhs) = delete; + ModelInstance& operator=(const ModelInstance &rhs) = delete; + ModelInstance& operator=(ModelInstance &&rhs) = delete; + + friend class cereal::access; + friend class UndoRedo::StackImpl; + // Used for deserialization, therefore no IDs are allocated. + ModelInstance() : ObjectBase(-1), object(nullptr) { assert(this->id().invalid()); } + template void serialize(Archive &ar) { + ar(m_transformation, print_volume_state, printable); + } +}; + + +class ModelWipeTower final : public ObjectBase +{ +public: + Vec2d position; + double rotation; + +private: + friend class cereal::access; + friend class UndoRedo::StackImpl; + friend class Model; + + // Constructors to be only called by derived classes. + // Default constructor to assign a unique ID. + explicit ModelWipeTower() {} + // Constructor with ignored int parameter to assign an invalid ID, to be replaced + // by an existing ID copied from elsewhere. + explicit ModelWipeTower(int) : ObjectBase(-1) {} + // Copy constructor copies the ID. + explicit ModelWipeTower(const ModelWipeTower &cfg) = default; + + // Disabled methods. + ModelWipeTower(ModelWipeTower &&rhs) = delete; + ModelWipeTower& operator=(const ModelWipeTower &rhs) = delete; + ModelWipeTower& operator=(ModelWipeTower &&rhs) = delete; + + // For serialization / deserialization of ModelWipeTower composed into another class into the Undo / Redo stack as a separate object. + template void serialize(Archive &ar) { ar(position, rotation); } +}; + +// The print bed content. +// Description of a triangular model with multiple materials, multiple instances with various affine transformations +// and with multiple modifier meshes. +// A model groups multiple objects, each object having possibly multiple instances, +// all objects may share mutliple materials. +class Model final : public ObjectBase +{ +public: + // Materials are owned by a model and referenced by objects through t_model_material_id. + // Single material may be shared by multiple models. + ModelMaterialMap materials; + // Objects are owned by a model. Each model may have multiple instances, each instance having its own transformation (shift, scale, rotation). + ModelObjectPtrs objects; + // Wipe tower object. + ModelWipeTower wipe_tower; + + // Extensions for color print + CustomGCode::Info custom_gcode_per_print_z; + + // Default constructor assigns a new ID to the model. + Model() { assert(this->id().valid()); } + ~Model() { this->clear_objects(); this->clear_materials(); } + + /* To be able to return an object from own copy / clone methods. Hopefully the compiler will do the "Copy elision" */ + /* (Omits copy and move(since C++11) constructors, resulting in zero - copy pass - by - value semantics). */ + Model(const Model &rhs) : ObjectBase(-1) { assert(this->id().invalid()); this->assign_copy(rhs); assert(this->id().valid()); assert(this->id() == rhs.id()); } + explicit Model(Model &&rhs) : ObjectBase(-1) { assert(this->id().invalid()); this->assign_copy(std::move(rhs)); assert(this->id().valid()); assert(this->id() == rhs.id()); } + Model& operator=(const Model &rhs) { this->assign_copy(rhs); assert(this->id().valid()); assert(this->id() == rhs.id()); return *this; } + Model& operator=(Model &&rhs) { this->assign_copy(std::move(rhs)); assert(this->id().valid()); assert(this->id() == rhs.id()); return *this; } + + OBJECTBASE_DERIVED_COPY_MOVE_CLONE(Model) + + enum class LoadAttribute : int { + AddDefaultInstances, + CheckVersion + }; + using LoadAttributes = enum_bitmask; + + static Model read_from_file( + const std::string& input_file, + DynamicPrintConfig* config = nullptr, ConfigSubstitutionContext* config_substitutions = nullptr, + LoadAttributes options = LoadAttribute::AddDefaultInstances); + static Model read_from_archive( + const std::string& input_file, + DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, + LoadAttributes options = LoadAttribute::AddDefaultInstances); + + // Add a new ModelObject to this Model, generate a new ID for this ModelObject. + ModelObject* add_object(); + ModelObject* add_object(const char *name, const char *path, const TriangleMesh &mesh); + ModelObject* add_object(const char *name, const char *path, TriangleMesh &&mesh); + ModelObject* add_object(const ModelObject &other); + void delete_object(size_t idx); + bool delete_object(ObjectID id); + bool delete_object(ModelObject* object); + void clear_objects(); + + ModelMaterial* add_material(t_model_material_id material_id); + ModelMaterial* add_material(t_model_material_id material_id, const ModelMaterial &other); + ModelMaterial* get_material(t_model_material_id material_id) { + ModelMaterialMap::iterator i = this->materials.find(material_id); + return (i == this->materials.end()) ? nullptr : i->second; + } + + void delete_material(t_model_material_id material_id); + void clear_materials(); + bool add_default_instances(); + // Returns approximate axis aligned bounding box of this model + BoundingBoxf3 bounding_box() const; + // Set the print_volume_state of PrintObject::instances, + // return total number of printable objects. + unsigned int update_print_volume_state(const BuildVolume &build_volume); + // Returns true if any ModelObject was modified. + bool center_instances_around_point(const Vec2d &point); + void translate(coordf_t x, coordf_t y, coordf_t z) { for (ModelObject *o : this->objects) o->translate(x, y, z); } + TriangleMesh mesh() const; + + // Croaks if the duplicated objects do not fit the print bed. + void duplicate_objects_grid(size_t x, size_t y, coordf_t dist); + + bool looks_like_multipart_object() const; + void convert_multipart_object(unsigned int max_extruders); + bool looks_like_imperial_units() const; + void convert_from_imperial_units(bool only_small_volumes); + bool looks_like_saved_in_meters() const; + void convert_from_meters(bool only_small_volumes); + int removed_objects_with_zero_volume(); + + // Ensures that the min z of the model is not negative + void adjust_min_z(); + + void print_info() const { for (const ModelObject *o : this->objects) o->print_info(); } + + // Propose an output file name & path based on the first printable object's name and source input file's path. + std::string propose_export_file_name_and_path() const; + // Propose an output path, replace extension. The new_extension shall contain the initial dot. + std::string propose_export_file_name_and_path(const std::string &new_extension) const; + + // Checks if any of objects is painted using the fdm support painting gizmo. + bool is_fdm_support_painted() const; + // Checks if any of objects is painted using the seam painting gizmo. + bool is_seam_painted() const; + // Checks if any of objects is painted using the multi-material painting gizmo. + bool is_mm_painted() const; + +private: + explicit Model(int) : ObjectBase(-1) { assert(this->id().invalid()); } + void assign_new_unique_ids_recursive(); + void update_links_bottom_up_recursive(); + + friend class cereal::access; + friend class UndoRedo::StackImpl; + template void serialize(Archive &ar) { + Internal::StaticSerializationWrapper wipe_tower_wrapper(wipe_tower); + ar(materials, objects, wipe_tower_wrapper); + } +}; + +ENABLE_ENUM_BITMASK_OPERATORS(Model::LoadAttribute) + +#undef OBJECTBASE_DERIVED_COPY_MOVE_CLONE +#undef OBJECTBASE_DERIVED_PRIVATE_COPY_MOVE + +// Test whether the two models contain the same number of ModelObjects with the same set of IDs +// ordered in the same order. In that case it is not necessary to kill the background processing. +bool model_object_list_equal(const Model &model_old, const Model &model_new); + +// Test whether the new model is just an extension of the old model (new objects were added +// to the end of the original list. In that case it is not necessary to kill the background processing. +bool model_object_list_extended(const Model &model_old, const Model &model_new); + +// Test whether the new ModelObject contains a different set of volumes (or sorted in a different order) +// than the old ModelObject. +bool model_volume_list_changed(const ModelObject &model_object_old, const ModelObject &model_object_new, const ModelVolumeType type); +bool model_volume_list_changed(const ModelObject &model_object_old, const ModelObject &model_object_new, const std::initializer_list &types); + +// Test whether the now ModelObject has newer custom supports data than the old one. +// The function assumes that volumes list is synchronized. +bool model_custom_supports_data_changed(const ModelObject& mo, const ModelObject& mo_new); + +// Test whether the now ModelObject has newer custom seam data than the old one. +// The function assumes that volumes list is synchronized. +bool model_custom_seam_data_changed(const ModelObject& mo, const ModelObject& mo_new); + +// Test whether the now ModelObject has newer MMU segmentation data than the old one. +// The function assumes that volumes list is synchronized. +extern bool model_mmu_segmentation_data_changed(const ModelObject& mo, const ModelObject& mo_new); + +// If the model has multi-part objects, then it is currently not supported by the SLA mode. +// Either the model cannot be loaded, or a SLA printer has to be activated. +bool model_has_multi_part_objects(const Model &model); +// If the model has objects with cut connectrs, then it is currently not supported by the SLA mode. +bool model_has_connectors(const Model& model); +// If the model has advanced features, then it cannot be processed in simple mode. +bool model_has_advanced_features(const Model &model); + +#ifndef NDEBUG +// Verify whether the IDs of Model / ModelObject / ModelVolume / ModelInstance / ModelMaterial are valid and unique. +void check_model_ids_validity(const Model &model); +void check_model_ids_equal(const Model &model1, const Model &model2); +#endif /* NDEBUG */ + +static const float SINKING_Z_THRESHOLD = -0.001f; +static const double SINKING_MIN_Z_THRESHOLD = 0.05; + +} // namespace Slic3r + +namespace cereal +{ + template struct specialize {}; + template struct specialize {}; +} + +#endif /* slic3r_Model_hpp_ */ diff --git a/src/libslic3r/ObjectID.hpp b/src/libslic3r/ObjectID.hpp index 1030171e7..4f34572d2 100644 --- a/src/libslic3r/ObjectID.hpp +++ b/src/libslic3r/ObjectID.hpp @@ -2,6 +2,7 @@ #define slic3r_ObjectID_hpp_ #include +#include namespace Slic3r { @@ -89,7 +90,9 @@ private: friend class cereal::access; friend class Slic3r::UndoRedo::StackImpl; template void serialize(Archive &ar) { ar(m_id); } +protected: // #vbCHECKME && #ysFIXME ObjectBase(const ObjectID id) : m_id(id) {} +private: template static void load_and_construct(Archive & ar, cereal::construct &construct) { ObjectID id; ar(id); construct(id); } }; @@ -128,6 +131,64 @@ private: template void serialize(Archive &ar) { ar(m_timestamp); } }; +class CutObjectBase : public ObjectBase +{ + // check sum of CutParts in initial Object + size_t m_check_sum{ 1 }; + // connectors count + size_t m_connectors_cnt{ 0 }; + +public: + // Default Constructor to assign an invalid ID + CutObjectBase() : ObjectBase(-1) {} + // Constructor with ignored int parameter to assign an invalid ID, to be replaced + // by an existing ID copied from elsewhere. + CutObjectBase(int) : ObjectBase(-1) {} + // Constructor to initialize full information from 3mf + CutObjectBase(ObjectID id, size_t check_sum, size_t connectors_cnt) : ObjectBase(id), m_check_sum(check_sum), m_connectors_cnt(connectors_cnt) {} + // The class tree will have virtual tables and type information. + virtual ~CutObjectBase() = default; + + bool operator<(const CutObjectBase& other) const { return other.id() > this->id(); } + bool operator==(const CutObjectBase& other) const { return other.id() == this->id(); } + + void copy(const CutObjectBase& rhs) { + this->copy_id(rhs); + this->m_check_sum = rhs.check_sum(); + this->m_connectors_cnt = rhs.connectors_cnt() ; + } + CutObjectBase& operator=(const CutObjectBase& other) { + this->copy(other); + return *this; + } + + void invalidate() { + set_invalid_id(); + m_check_sum = 1; + m_connectors_cnt = 0; + } + + void init() { this->set_new_unique_id(); } + bool has_same_id(const CutObjectBase& rhs) { return this->id() == rhs.id(); } + bool is_equal(const CutObjectBase& rhs) { return this->id() == rhs.id() && + this->check_sum() == rhs.check_sum() && + this->connectors_cnt() == rhs.connectors_cnt() ; } + + size_t check_sum() const { return m_check_sum; } + void set_check_sum(size_t cs) { m_check_sum = cs; } + void increase_check_sum(size_t cnt) { m_check_sum += cnt; } + + size_t connectors_cnt() const { return m_connectors_cnt; } + void increase_connectors_cnt(size_t connectors_cnt) { m_connectors_cnt += connectors_cnt; } + +private: + friend class cereal::access; + template void serialize(Archive& ar) { + ar(cereal::base_class(this)); + ar(m_check_sum, m_connectors_cnt); + } +}; + // Unique object / instance ID for the wipe tower. extern ObjectID wipe_tower_object_id(); extern ObjectID wipe_tower_instance_id(); diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index c3792779c..330ad533c 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 000000000..9e547eec4 --- /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 695c58abe..e390ff209 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/libslic3r/TriangleMesh.cpp b/src/libslic3r/TriangleMesh.cpp index 97175ea89..ad7e1a2bb 100644 --- a/src/libslic3r/TriangleMesh.cpp +++ b/src/libslic3r/TriangleMesh.cpp @@ -1061,6 +1061,61 @@ indexed_triangle_set its_make_sphere(double radius, double fa) return mesh; } +// Generates mesh for a frustum dowel centered about the origin, using the count of sectors +// Note: This function uses code for sphere generation, but for stackCount = 2; +indexed_triangle_set its_make_frustum_dowel(double radius, double h, int sectorCount) +{ + int stackCount = 2; + float sectorStep = float(2. * M_PI / sectorCount); + float stackStep = float(M_PI / stackCount); + + indexed_triangle_set mesh; + auto& vertices = mesh.vertices; + vertices.reserve((stackCount - 1) * sectorCount + 2); + for (int i = 0; i <= stackCount; ++i) { + // from pi/2 to -pi/2 + double stackAngle = 0.5 * M_PI - stackStep * i; + double xy = radius * cos(stackAngle); + double z = radius * sin(stackAngle); + if (i == 0 || i == stackCount) + vertices.emplace_back(Vec3f(float(xy), 0.f, float(h * sin(stackAngle)))); + else + for (int j = 0; j < sectorCount; ++j) { + // from 0 to 2pi + double sectorAngle = sectorStep * j; + vertices.emplace_back(Vec3d(xy * std::cos(sectorAngle), xy * std::sin(sectorAngle), z).cast()); + } + } + + auto& facets = mesh.indices; + facets.reserve(2 * (stackCount - 1) * sectorCount); + for (int i = 0; i < stackCount; ++i) { + // Beginning of current stack. + int k1 = (i == 0) ? 0 : (1 + (i - 1) * sectorCount); + int k1_first = k1; + // Beginning of next stack. + int k2 = (i == 0) ? 1 : (k1 + sectorCount); + int k2_first = k2; + for (int j = 0; j < sectorCount; ++j) { + // 2 triangles per sector excluding first and last stacks + int k1_next = k1; + int k2_next = k2; + if (i != 0) { + k1_next = (j + 1 == sectorCount) ? k1_first : (k1 + 1); + facets.emplace_back(k1, k2, k1_next); + } + if (i + 1 != stackCount) { + k2_next = (j + 1 == sectorCount) ? k2_first : (k2 + 1); + facets.emplace_back(k1_next, k2, k2_next); + } + k1 = k1_next; + k2 = k2_next; + } + } + + return mesh; +} + indexed_triangle_set its_convex_hull(const std::vector &pts) { std::vector dst_vertices; diff --git a/src/libslic3r/TriangleMesh.hpp b/src/libslic3r/TriangleMesh.hpp index 6be4c4173..08595a2d3 100644 --- a/src/libslic3r/TriangleMesh.hpp +++ b/src/libslic3r/TriangleMesh.hpp @@ -301,6 +301,7 @@ indexed_triangle_set its_make_cube(double x, double y, double z); indexed_triangle_set its_make_prism(float width, float length, float height); indexed_triangle_set its_make_cylinder(double r, double h, double fa=(2*PI/360)); indexed_triangle_set its_make_cone(double r, double h, double fa=(2*PI/360)); +indexed_triangle_set its_make_frustum_dowel(double r, double h, int sectorCount); indexed_triangle_set its_make_pyramid(float base, float height); indexed_triangle_set its_make_sphere(double radius, double fa); diff --git a/src/libslic3r/libslic3r.h b/src/libslic3r/libslic3r.h index cd1b41c38..ac889cc73 100644 --- a/src/libslic3r/libslic3r.h +++ b/src/libslic3r/libslic3r.h @@ -263,9 +263,9 @@ constexpr inline T lerp(const T& a, const T& b, Number t) } template -constexpr inline bool is_approx(Number value, Number test_value) +constexpr inline bool is_approx(Number value, Number test_value, Number precision = EPSILON) { - return std::fabs(double(value) - double(test_value)) < double(EPSILON); + return std::fabs(double(value) - double(test_value)) < double(precision); } // A meta-predicate which is true for integers wider than or equal to coord_t diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 5a5e2bb5a..d37221b7b 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -63,6 +63,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 @@ -256,6 +258,8 @@ set(SLIC3R_GUI_SOURCES Utils/WinRegistry.hpp ) +find_package(NanoSVG REQUIRED) + if (APPLE) list(APPEND SLIC3R_GUI_SOURCES Utils/RetinaHelperImpl.mm diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp index e674b52d0..bedb3718e 100644 --- a/src/slic3r/GUI/Field.cpp +++ b/src/slic3r/GUI/Field.cpp @@ -33,7 +33,7 @@ wxString double_to_string(double const value, const int max_precision /*= 4*/) // Style_NoTrailingZeroes does not work on OSX. It also does not work correctly with some locales on Windows. // return wxNumberFormatter::ToString(value, max_precision, wxNumberFormatter::Style_NoTrailingZeroes); - wxString s = wxNumberFormatter::ToString(value, value < 0.0001 ? 10 : max_precision, wxNumberFormatter::Style_None); + wxString s = wxNumberFormatter::ToString(value, std::abs(value) < 0.0001 ? 10 : max_precision, wxNumberFormatter::Style_None); // The following code comes from wxNumberFormatter::RemoveTrailingZeroes(wxString& s) // with the exception that here one sets the decimal separator explicitely to dot. diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 7bdf43abc..ec3865a40 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1367,6 +1367,8 @@ void GLCanvas3D::toggle_model_objects_visibility(bool visible, const ModelObject && (instance_idx == -1 || vol->composite_id.instance_id == instance_idx) && (mv == nullptr || m_model->objects[vol->composite_id.object_id]->volumes[vol->composite_id.volume_id] == mv)) { vol->is_active = visible; + if (!vol->is_modifier) + vol->color.a(1.f); if (instance_idx == -1) { vol->force_native_color = false; @@ -1375,9 +1377,13 @@ 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) - && ! 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); + } else if (gizmo_type == GLGizmosManager::MmuSegmentation) vol->is_active = false; else @@ -2432,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 @@ -3371,6 +3379,11 @@ 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) { + wxGetApp().obj_list()->selection_changed(); + } return; } @@ -3446,6 +3459,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) if (m_gizmos.get_current_type() != GLGizmosManager::SlaSupports && 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::MmuSegmentation) { m_rectangle_selection.start_dragging(m_mouse.position, evt.ShiftDown() ? GLSelectionRectangle::EState::Select : GLSelectionRectangle::EState::Deselect); m_dirty = true; @@ -3495,7 +3509,8 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) const int volume_idx = get_first_hover_volume_idx(); BoundingBoxf3 volume_bbox = m_volumes.volumes[volume_idx]->transformed_bounding_box(); volume_bbox.offset(1.0); - if ((!any_gizmo_active || !evt.CmdDown()) && volume_bbox.contains(m_mouse.scene_position)) { + const bool is_cut_connector_selected = m_selection.is_any_connector(); + if ((!any_gizmo_active || !evt.CmdDown()) && volume_bbox.contains(m_mouse.scene_position) && !is_cut_connector_selected) { m_volumes.volumes[volume_idx]->hover = GLVolume::HS_None; // The dragging operation is initiated. m_mouse.drag.move_volume_idx = volume_idx; @@ -5428,10 +5443,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 @@ -5442,8 +5456,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 0adf1cb26..a5ec9c191 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 2d6a66d43..2d4d25f8c 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 c2845c885..2d1e352c4 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_App.cpp b/src/slic3r/GUI/GUI_App.cpp index c5f4d9672..7042d599b 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2890,6 +2890,13 @@ bool GUI_App::may_switch_to_SLA_preset(const wxString& caption) caption); return false; } + if (model_has_connectors(model())) { + show_info(nullptr, + _L("SLA technology doesn't support cut with connectors") + "\n\n" + + _L("Please check your object list before preset changing."), + caption); + return false; + } return true; } @@ -3244,7 +3251,7 @@ void GUI_App::app_updater(bool from_user) app_data.target_path =dwnld_dlg.get_download_path(); // start download - this->plater_->get_notification_manager()->push_download_progress_notification(_utf8("Download"), std::bind(&AppUpdater::cancel_callback, m_app_updater.get())); + this->plater_->get_notification_manager()->push_download_progress_notification(_utf8("Download"), std::bind(&AppUpdater::cancel_callback, this->m_app_updater.get())); app_data.start_after = dwnld_dlg.run_after_download(); m_app_updater->set_app_data(std::move(app_data)); m_app_updater->sync_download(); diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 8f69ca811..6ffe19218 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -493,7 +493,9 @@ void MenuFactory::append_menu_items_add_volume(wxMenu* menu) append_menu_item(menu, wxID_ANY, _(ADD_VOLUME_MENU_ITEMS[int(ModelVolumeType::MODEL_PART)].first), "", [](wxCommandEvent&) { obj_list()->load_subobject(ModelVolumeType::MODEL_PART); }, ADD_VOLUME_MENU_ITEMS[int(ModelVolumeType::MODEL_PART)].second, nullptr, - []() { return obj_list()->is_instance_or_object_selected(); }, m_parent); + []() { return obj_list()->is_instance_or_object_selected() + && !obj_list()->is_selected_object_cut(); + }, m_parent); } if (mode == comSimple) { append_menu_item(menu, wxID_ANY, _(ADD_VOLUME_MENU_ITEMS[int(ModelVolumeType::SUPPORT_ENFORCER)].first), "", @@ -514,7 +516,10 @@ void MenuFactory::append_menu_items_add_volume(wxMenu* menu) wxMenu* sub_menu = append_submenu_add_generic(menu, ModelVolumeType(type)); append_submenu(menu, sub_menu, wxID_ANY, _(item.first), "", item.second, - []() { return obj_list()->is_instance_or_object_selected(); }, m_parent); + [type]() { + bool can_add = type < size_t(ModelVolumeType::PARAMETER_MODIFIER) ? !obj_list()->is_selected_object_cut() : true; + return can_add && obj_list()->is_instance_or_object_selected(); + }, m_parent); } append_menu_item_layers_editing(menu); @@ -680,6 +685,21 @@ wxMenuItem* MenuFactory::append_menu_item_printable(wxMenu* menu) return menu_item_printable; } +void MenuFactory::append_menu_item_invalidate_cut_info(wxMenu* menu) +{ + const wxString menu_name = _L("Invalidate cut info"); + + auto menu_item_id = menu->FindItem(menu_name); + if (menu_item_id != wxNOT_FOUND) + // Delete old menu item if selected object isn't cut + menu->Destroy(menu_item_id); + + if (obj_list()->has_selected_cut_object()) + append_menu_item(menu, wxID_ANY, menu_name, "", + [](wxCommandEvent&) { obj_list()->invalidate_cut_info_for_selection(); }, "", menu, + []() { return true; }, m_parent); +} + void MenuFactory::append_menu_items_osx(wxMenu* menu) { append_menu_item(menu, wxID_ANY, _L("Rename"), "", @@ -816,6 +836,8 @@ void MenuFactory::append_menu_items_convert_unit(wxMenu* menu, int insert_pos/* ModelObjectPtrs objects; for (int obj_idx : obj_idxs) { ModelObject* object = obj_list()->object(obj_idx); + if (object->is_cut()) + return false; if (vol_idxs.empty()) { for (ModelVolume* volume : object->volumes) if (volume_respects_conversion(volume, conver_type)) @@ -1016,6 +1038,7 @@ wxMenu* MenuFactory::object_menu() append_menu_item_settings(&m_object_menu); append_menu_item_change_extruder(&m_object_menu); update_menu_items_instance_manipulation(mtObjectFFF); + append_menu_item_invalidate_cut_info(&m_object_menu); return &m_object_menu; } @@ -1025,6 +1048,7 @@ wxMenu* MenuFactory::sla_object_menu() append_menu_items_convert_unit(&m_sla_object_menu, 11); append_menu_item_settings(&m_sla_object_menu); update_menu_items_instance_manipulation(mtObjectSLA); + append_menu_item_invalidate_cut_info(&m_sla_object_menu); return &m_sla_object_menu; } @@ -1056,6 +1080,9 @@ wxMenu* MenuFactory::multi_selection_menu() wxDataViewItemArray sels; obj_list()->GetSelections(sels); + if (sels.IsEmpty()) + return nullptr; + for (const wxDataViewItem& item : sels) if (!(list_model()->GetItemType(item) & (itVolume | itObject | itInstance))) // show this menu only for Objects(Instances mixed with Objects)/Volumes selection diff --git a/src/slic3r/GUI/GUI_Factories.hpp b/src/slic3r/GUI/GUI_Factories.hpp index bbbc00d42..7190edb64 100644 --- a/src/slic3r/GUI/GUI_Factories.hpp +++ b/src/slic3r/GUI/GUI_Factories.hpp @@ -89,6 +89,7 @@ private: wxMenuItem* append_menu_item_change_type(wxMenu* menu); wxMenuItem* append_menu_item_instance_to_object(wxMenu* menu); wxMenuItem* append_menu_item_printable(wxMenu* menu); + void append_menu_item_invalidate_cut_info(wxMenu *menu); void append_menu_items_osx(wxMenu* menu); wxMenuItem* append_menu_item_fix_through_netfabb(wxMenu* menu); wxMenuItem* append_menu_item_simplify(wxMenu* menu); diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 0984ded3a..d6bbbeba1 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -11,6 +11,8 @@ #include "GalleryDialog.hpp" #include "MainFrame.hpp" #include "slic3r/Utils/UndoRedo.hpp" +#include "Gizmos/GLGizmoCut.hpp" +#include "Gizmos/GLGizmoScale.hpp" #include "OptionsGroup.hpp" #include "Tab.hpp" @@ -401,6 +403,13 @@ MeshErrorsInfo ObjectList::get_mesh_errors_info(const int obj_idx, const int vol if (obj_idx < 0) return { {}, {} }; // hide tooltip + const ModelObject* object = (*m_objects)[obj_idx]; + if (vol_idx != -1 && vol_idx >= int(object->volumes.size())) { + if (sidebar_info) + *sidebar_info = _L("Wrong volume index "); + return { {}, {} }; // hide tooltip + } + const TriangleMeshStats& stats = vol_idx == -1 ? (*m_objects)[obj_idx]->get_object_stl_stats() : (*m_objects)[obj_idx]->volumes[vol_idx]->mesh().stats(); @@ -673,6 +682,8 @@ void ObjectList::selection_changed() fix_multiselection_conflicts(); + fix_cut_selection(); + // update object selection on Plater if (!m_prevent_canvas_selection_update) update_selections_on_canvas(); @@ -1389,6 +1400,15 @@ bool ObjectList::is_instance_or_object_selected() return selection.is_single_full_instance() || selection.is_single_full_object(); } +bool ObjectList::is_selected_object_cut() +{ + const Selection& selection = scene_selection(); + int obj_idx = selection.get_object_idx(); + if (obj_idx < 0) + return false; + return object(obj_idx)->is_cut(); +} + void ObjectList::load_subobject(ModelVolumeType type, bool from_galery/* = false*/) { if (type == ModelVolumeType::INVALID && from_galery) { @@ -1678,11 +1698,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 @@ -1708,6 +1733,9 @@ void ObjectList::load_generic_subobject(const std::string& type_name, const Mode // update printable state on canvas wxGetApp().plater()->canvas3D()->update_instance_printable_state_for_object((size_t)obj_idx); + if (model_object.is_cut()) + update_info_items(obj_idx); + selection_changed(); } @@ -1804,22 +1832,22 @@ void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name #endif /* _DEBUG */ } -void ObjectList::del_object(const int obj_idx) +bool ObjectList::del_object(const int obj_idx) { - wxGetApp().plater()->delete_object_from_model(obj_idx); + return wxGetApp().plater()->delete_object_from_model(obj_idx); } // Delete subobject -void ObjectList::del_subobject_item(wxDataViewItem& item) +bool ObjectList::del_subobject_item(wxDataViewItem& item) { - if (!item) return; + if (!item) return false; int obj_idx, idx; ItemType type; m_objects_model->GetItemInfo(item, type, obj_idx, idx); if (type == itUndef) - return; + return false; wxDataViewItem parent = m_objects_model->GetParent(item); @@ -1833,19 +1861,21 @@ void ObjectList::del_subobject_item(wxDataViewItem& item) del_layer_from_object(obj_idx, m_objects_model->GetLayerRangeByItem(item)); else if (type & itInfo && obj_idx != -1) del_info_item(obj_idx, m_objects_model->GetInfoItemType(item)); - else if (idx == -1) - return; - else if (!del_subobject_from_object(obj_idx, idx, type)) - return; + else if (idx == -1 || !del_subobject_from_object(obj_idx, idx, type)) + return false; // If last volume item with warning was deleted, unmark object item if (type & itVolume) { + add_volumes_to_object_in_list(obj_idx); const std::string& icon_name = get_warning_icon_name(object(obj_idx)->get_object_stl_stats()); m_objects_model->UpdateWarningIcon(parent, icon_name); } + else + m_objects_model->Delete(item); - m_objects_model->Delete(item); update_info_items(obj_idx); + + return true; } void ObjectList::del_info_item(const int obj_idx, InfoItemType type) @@ -1868,6 +1898,10 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type) mv->seam_facets.reset(); break; + case InfoItemType::CutConnectors: + show_error(nullptr, _L("Connectors cannot be deleted from cut object.")); + break; + case InfoItemType::MmuSegmentation: cnv->get_gizmos_manager().reset_all_states(); Plater::TakeSnapshot(plater, _L("Remove Multi Material painting")); @@ -1978,6 +2012,16 @@ bool ObjectList::del_subobject_from_object(const int obj_idx, const int idx, con Slic3r::GUI::show_error(nullptr, _L("From Object List You can't delete the last solid part from object.")); return false; } + if (object->is_cut()) { + if (volume->is_model_part()) { + Slic3r::GUI::show_error(nullptr, _L("Solid part cannot be deleted from cut object.")); + return false; + } + if (volume->is_negative_volume()) { + Slic3r::GUI::show_error(nullptr, _L("Negative volume cannot be deleted from cut object.")); + return false; + } + } take_snapshot(_L("Delete Subobject")); @@ -2005,6 +2049,10 @@ bool ObjectList::del_subobject_from_object(const int obj_idx, const int idx, con Slic3r::GUI::show_error(nullptr, _L("Last instance of an object cannot be deleted.")); return false; } + if (object->is_cut()) { + Slic3r::GUI::show_error(nullptr, _L("Instance cannot be deleted from cut object.")); + return false; + } take_snapshot(_L("Delete Instance")); object->delete_instance(idx); @@ -2038,30 +2086,11 @@ void ObjectList::split() volume->split(nozzle_dmrs_cnt); + (*m_objects)[obj_idx]->input_file.clear(); + wxBusyCursor wait; - auto model_object = (*m_objects)[obj_idx]; - - auto parent = m_objects_model->GetTopParent(item); - if (parent) - m_objects_model->DeleteVolumeChildren(parent); - else - parent = item; - - for (const ModelVolume* volume : model_object->volumes) { - const wxDataViewItem& vol_item = m_objects_model->AddVolumeChild(parent, from_u8(volume->name), - volume->type(),// is_modifier() ? ModelVolumeType::PARAMETER_MODIFIER : ModelVolumeType::MODEL_PART, - get_warning_icon_name(volume->mesh().stats()), - volume->config.has("extruder") ? volume->config.extruder() : 0, - false); - // add settings to the part, if it has those - add_settings_item(vol_item, &volume->config.get()); - } - - model_object->input_file.clear(); - - if (parent == item) - Expand(parent); + add_volumes_to_object_in_list(obj_idx); changed_object(obj_idx); // update printable state for new volumes on canvas3D @@ -2380,9 +2409,12 @@ bool ObjectList::is_splittable(bool to_objects) auto obj_idx = get_selected_obj_idx(); if (obj_idx < 0) return false; - if ((*m_objects)[obj_idx]->volumes.size() > 1) + const ModelObject* object = (*m_objects)[obj_idx]; + if (object->is_cut()) + return false; + if (object->volumes.size() > 1) return true; - return (*m_objects)[obj_idx]->volumes[0]->is_splittable(); + return object->volumes[0]->is_splittable(); } return false; } @@ -2415,9 +2447,59 @@ bool ObjectList::can_split_instances() return selection.is_multiple_full_instance() || selection.is_single_full_instance(); } +bool ObjectList::has_selected_cut_object() const +{ + wxDataViewItemArray sels; + GetSelections(sels); + if (sels.IsEmpty()) + return false; + + for (wxDataViewItem item : sels) { + const int obj_idx = m_objects_model->GetObjectIdByItem(item); + if (obj_idx >= 0 && object(obj_idx)->is_cut()) + return true; + } + + return false; +} +void ObjectList::invalidate_cut_info_for_selection() +{ + const wxDataViewItem item = GetSelection(); + if (item) { + const int obj_idx = m_objects_model->GetObjectIdByItem(item); + if (obj_idx >= 0) + invalidate_cut_info_for_object(size_t(obj_idx)); + } +} + +void ObjectList::invalidate_cut_info_for_object(size_t obj_idx) +{ + ModelObject* init_obj = object(int(obj_idx)); + if (!init_obj->is_cut()) + return; + + take_snapshot(_L("Invalidate cut info")); + + auto invalidate_cut = [this](size_t obj_idx) { + object(int(obj_idx))->invalidate_cut(); + update_info_items(obj_idx); + add_volumes_to_object_in_list(obj_idx); + }; + + // invalidate cut for related objects (which have the same cut_id) + for (size_t idx = 0; idx < m_objects->size(); idx++) + if (ModelObject* obj = object(idx); obj != init_obj && obj->cut_id.is_equal(init_obj->cut_id)) + invalidate_cut(idx); + + // invalidate own cut information + invalidate_cut(size_t(obj_idx)); + + update_lock_icons_for_model(); +} + bool ObjectList::can_merge_to_multipart_object() const { - if (printer_technology() == ptSLA) + if (printer_technology() == ptSLA || has_selected_cut_object()) return false; wxDataViewItemArray sels; @@ -2445,17 +2527,7 @@ bool ObjectList::can_merge_to_single_object() const wxPoint ObjectList::get_mouse_position_in_control() const { - wxPoint pt = wxGetMousePosition() - this->GetScreenPosition(); - -#ifdef __APPLE__ - // Workaround for OSX. From wxWidgets 3.1.6 Hittest doesn't respect to the header of wxDataViewCtrl - if (wxDataViewItem top_item = this->GetTopItem(); top_item.IsOk()) { - auto rect = this->GetItemRect(top_item, this->GetColumn(0)); - pt.y -= rect.y; - } -#endif // __APPLE__ - - return pt; + return wxGetMousePosition() - this->GetScreenPosition(); } // NO_PARAMETERS function call means that changed object index will be determine from Selection() @@ -2476,14 +2548,65 @@ void ObjectList::part_selection_changed() bool update_and_show_settings = false; bool update_and_show_layers = false; + bool enable_manipulation {true}; + bool disable_ss_manipulation {false}; + bool disable_ununiform_scale {false}; + const auto item = GetSelection(); - if ( multiple_selection() || (item && m_objects_model->GetItemType(item) == itInstanceRoot )) { + GLGizmosManager& gizmos_mgr = wxGetApp().plater()->canvas3D()->get_gizmos_manager(); + + if (item && m_objects_model->GetItemType(item) == itInfo && m_objects_model->GetInfoItemType(item) == InfoItemType::CutConnectors) { + og_name = _L("Cut Connectors information"); + + update_and_show_manipulations = true; + enable_manipulation = false; + disable_ununiform_scale = true; + } + else if ( multiple_selection() || (item && m_objects_model->GetItemType(item) == itInstanceRoot )) { og_name = _L("Group manipulation"); const Selection& selection = scene_selection(); // don't show manipulation panel for case of all Object's parts selection update_and_show_manipulations = !selection.is_single_full_instance(); + + if (int obj_idx = selection.get_object_idx(); obj_idx >= 0) { + if (selection.is_any_volume() || selection.is_any_modifier()) + enable_manipulation = !(*m_objects)[obj_idx]->is_cut(); + else// if (item && m_objects_model->GetItemType(item) == itInstanceRoot) + disable_ss_manipulation = (*m_objects)[obj_idx]->is_cut(); + } + else { + wxDataViewItemArray sels; + GetSelections(sels); + if (selection.is_single_full_object() || selection.is_multiple_full_instance() ) { + int obj_idx = m_objects_model->GetObjectIdByItem(sels.front()); + disable_ss_manipulation = (*m_objects)[obj_idx]->is_cut(); + } + else if (selection.is_mixed() || selection.is_multiple_full_object()) { + std::map> cut_objects; + + // find cut objects + for (auto item : sels) { + int obj_idx = m_objects_model->GetObjectIdByItem(item); + const ModelObject* obj = object(obj_idx); + if (obj->is_cut()) { + if (cut_objects.find(obj->cut_id) == cut_objects.end()) + cut_objects[obj->cut_id] = std::set{ obj_idx }; + else + cut_objects.at(obj->cut_id).insert(obj_idx); + } + } + + // check if selected cut objects are "full selected" + for (auto cut_object : cut_objects) + if (cut_object.first.check_sum() != cut_object.second.size()) { + disable_ss_manipulation = true; + break; + } + disable_ununiform_scale = !cut_objects.empty(); + } + } } else { if (item) { @@ -2491,11 +2614,12 @@ void ObjectList::part_selection_changed() const wxDataViewItem parent = m_objects_model->GetParent(item); const ItemType parent_type = m_objects_model->GetItemType(parent); obj_idx = m_objects_model->GetObjectIdByItem(item); + ModelObject* object = (*m_objects)[obj_idx]; if (parent == wxDataViewItem(nullptr) || type == itInfo) { og_name = _L("Object manipulation"); - m_config = &(*m_objects)[obj_idx]->config; + m_config = &object->config; update_and_show_manipulations = true; if (type == itInfo) { @@ -2511,29 +2635,30 @@ void ObjectList::part_selection_changed() case InfoItemType::CustomSeam: case InfoItemType::MmuSegmentation: { - GLGizmosManager::EType gizmo_type = info_type == InfoItemType::CustomSupports ? GLGizmosManager::EType::FdmSupports : - info_type == InfoItemType::CustomSeam ? GLGizmosManager::EType::Seam : + GLGizmosManager::EType gizmo_type = info_type == InfoItemType::CustomSupports ? GLGizmosManager::EType::FdmSupports : + info_type == InfoItemType::CustomSeam ? GLGizmosManager::EType::Seam : GLGizmosManager::EType::MmuSegmentation; - GLGizmosManager& gizmos_mgr = wxGetApp().plater()->canvas3D()->get_gizmos_manager(); if (gizmos_mgr.get_current_type() != gizmo_type) gizmos_mgr.open_gizmo(gizmo_type); break; } - case InfoItemType::Sinking: { break; } + case InfoItemType::Sinking: default: { break; } } } + else + disable_ss_manipulation = object->is_cut(); } else { if (type & itSettings) { if (parent_type & itObject) { og_name = _L("Object Settings to modify"); - m_config = &(*m_objects)[obj_idx]->config; + m_config = &object->config; } else if (parent_type & itVolume) { og_name = _L("Part Settings to modify"); volume_id = m_objects_model->GetVolumeIdByItem(parent); - m_config = &(*m_objects)[obj_idx]->volumes[volume_id]->config; + m_config = &object->volumes[volume_id]->config; } else if (parent_type & itLayer) { og_name = _L("Layer range Settings to modify"); @@ -2544,15 +2669,18 @@ void ObjectList::part_selection_changed() else if (type & itVolume) { og_name = _L("Part manipulation"); volume_id = m_objects_model->GetVolumeIdByItem(item); - m_config = &(*m_objects)[obj_idx]->volumes[volume_id]->config; + m_config = &object->volumes[volume_id]->config; update_and_show_manipulations = true; + const ModelVolume* volume = object->volumes[volume_id]; + enable_manipulation = !(object->is_cut() && (volume->is_cut_connector() || volume->is_model_part())); } else if (type & itInstance) { og_name = _L("Instance manipulation"); update_and_show_manipulations = true; // fill m_config by object's values - m_config = &(*m_objects)[obj_idx]->config; + m_config = &object->config; + disable_ss_manipulation = object->is_cut(); } else if (type & (itLayerRoot|itLayer)) { og_name = type & itLayerRoot ? _L("Height ranges") : _L("Settings for height range"); @@ -2571,10 +2699,20 @@ void ObjectList::part_selection_changed() wxGetApp().obj_manipul()->get_og()->set_name(" " + og_name + " "); if (item) { - // wxGetApp().obj_manipul()->get_og()->set_value("object_name", m_objects_model->GetName(item)); wxGetApp().obj_manipul()->update_item_name(m_objects_model->GetName(item)); wxGetApp().obj_manipul()->update_warning_icon_state(get_mesh_errors_info(obj_idx, volume_id)); } + + if (disable_ss_manipulation) + wxGetApp().obj_manipul()->DisableScale(); + else { + wxGetApp().obj_manipul()->Enable(enable_manipulation); + if (disable_ununiform_scale) + wxGetApp().obj_manipul()->DisableUnuniformScale(); + } + + if (GLGizmoScale3D* scale = dynamic_cast(gizmos_mgr.get_gizmo(GLGizmosManager::Scale))) + scale->enable_ununiversal_scale(!disable_ununiform_scale); } if (update_and_show_settings) @@ -2597,6 +2735,7 @@ void ObjectList::part_selection_changed() #else wxGetApp().plater()->canvas3D()->handle_sidebar_focus_event("", false); #endif // ENABLE_WORLD_COORDINATE + wxGetApp().plater()->canvas3D()->enable_moving(enable_manipulation); // ysFIXME wxGetApp().obj_manipul() ->UpdateAndShow(update_and_show_manipulations); wxGetApp().obj_settings()->UpdateAndShow(update_and_show_settings); wxGetApp().obj_layers() ->UpdateAndShow(update_and_show_layers); @@ -2655,6 +2794,7 @@ void ObjectList::update_info_items(size_t obj_idx, wxDataViewItemArray* selectio for (InfoItemType type : {InfoItemType::CustomSupports, InfoItemType::CustomSeam, + InfoItemType::CutConnectors, InfoItemType::MmuSegmentation, InfoItemType::Sinking, InfoItemType::VariableLayerHeight}) { @@ -2675,6 +2815,9 @@ void ObjectList::update_info_items(size_t obj_idx, wxDataViewItemArray* selectio }); break; + case InfoItemType::CutConnectors: + should_show = model_object->is_cut() && model_object->has_connectors() && model_object->volumes.size() > 1; + break; case InfoItemType::VariableLayerHeight : should_show = printer_technology() == ptFFF && ! model_object->layer_height_profile.empty(); @@ -2714,31 +2857,73 @@ void ObjectList::update_info_items(size_t obj_idx, wxDataViewItemArray* selectio } } +static wxString extruder2str(int extruder) +{ + return extruder == 0 ? _L("default") : wxString::Format("%d", extruder); +} +static bool can_add_volumes_to_object(const ModelObject* object) +{ + bool can = object->volumes.size() > 1; + + if (can && object->is_cut()) { + int no_connectors_cnt = 0; + for (const ModelVolume* v : object->volumes) + if (!v->is_cut_connector()) { + if (!v->is_model_part()) + return true; + no_connectors_cnt++; + } + can = no_connectors_cnt > 1; + } + + return can; +} + +wxDataViewItemArray ObjectList::add_volumes_to_object_in_list(size_t obj_idx, std::function add_to_selection/* = nullptr*/) +{ + wxDataViewItem object_item = m_objects_model->GetItemById(int(obj_idx)); + m_objects_model->DeleteVolumeChildren(object_item); + + wxDataViewItemArray items; + + const ModelObject* object = (*m_objects)[obj_idx]; + // add volumes to the object + if (can_add_volumes_to_object(object)) { + int volume_idx{ -1 }; + for (const ModelVolume* volume : object->volumes) { + ++volume_idx; + if (object->is_cut() && volume->is_cut_connector()) + continue; + const wxDataViewItem& vol_item = m_objects_model->AddVolumeChild(object_item, + from_u8(volume->name), + volume_idx, + volume->type(), + get_warning_icon_name(volume->mesh().stats()), + extruder2str(volume->config.has("extruder") ? volume->config.extruder() : 0)); + add_settings_item(vol_item, &volume->config.get()); + + if (add_to_selection && add_to_selection(volume)) + items.Add(vol_item); + } + Expand(object_item); + } + + return items; +} void ObjectList::add_object_to_list(size_t obj_idx, bool call_selection_changed) { auto model_object = (*m_objects)[obj_idx]; const wxString& item_name = from_u8(model_object->name); - const auto item = m_objects_model->Add(item_name, - model_object->config.has("extruder") ? model_object->config.extruder() : 0, - get_warning_icon_name(model_object->mesh().stats())); + const auto item = m_objects_model->AddObject(item_name, + extruder2str(model_object->config.has("extruder") ? model_object->config.extruder() : 0), + get_warning_icon_name(model_object->mesh().stats()), + model_object->is_cut()); update_info_items(obj_idx, nullptr, call_selection_changed); - // add volumes to the object - if (model_object->volumes.size() > 1) { - for (const ModelVolume* volume : model_object->volumes) { - const wxDataViewItem& vol_item = m_objects_model->AddVolumeChild(item, - from_u8(volume->name), - volume->type(), - get_warning_icon_name(volume->mesh().stats()), - volume->config.has("extruder") ? volume->config.extruder() : 0, - false); - add_settings_item(vol_item, &volume->config.get()); - } - Expand(item); - } + add_volumes_to_object_in_list(obj_idx); // add instances to the object, if it has those if (model_object->instances.size()>1) @@ -2792,29 +2977,39 @@ void ObjectList::delete_instance_from_list(const size_t obj_idx, const size_t in select_item([this, obj_idx, inst_idx]() { return m_objects_model->Delete(m_objects_model->GetItemByInstanceId(obj_idx, inst_idx)); }); } -void ObjectList::delete_from_model_and_list(const ItemType type, const int obj_idx, const int sub_obj_idx) +void ObjectList::update_lock_icons_for_model() { - if ( !(type&(itObject|itVolume|itInstance)) ) - return; - - take_snapshot(_(L("Delete Selected Item"))); - - if (type&itObject) { - del_object(obj_idx); - delete_object_from_list(obj_idx); - } - else { - del_subobject_from_object(obj_idx, sub_obj_idx, type); - - type == itVolume ? delete_volume_from_list(obj_idx, sub_obj_idx) : - delete_instance_from_list(obj_idx, sub_obj_idx); - } + for (size_t obj_idx = 0; obj_idx < (*m_objects).size(); ++obj_idx) + if (!(*m_objects)[obj_idx]->is_cut()) + m_objects_model->UpdateLockIcon(m_objects_model->GetItemById(int(obj_idx)), false); } -void ObjectList::delete_from_model_and_list(const std::vector& items_for_delete) +bool ObjectList::delete_from_model_and_list(const ItemType type, const int obj_idx, const int sub_obj_idx) +{ + if (type & (itObject | itVolume | itInstance)) { + if (type & itObject) { + bool was_cut = object(obj_idx)->is_cut(); + if (del_object(obj_idx)) { + delete_object_from_list(obj_idx); + if (was_cut) + update_lock_icons_for_model(); + return true; + } + return false; + } + if (del_subobject_from_object(obj_idx, sub_obj_idx, type)) { + type == itVolume ? delete_volume_from_list(obj_idx, sub_obj_idx) : + delete_instance_from_list(obj_idx, sub_obj_idx); + return true; + } + } + return false; +} + +bool ObjectList::delete_from_model_and_list(const std::vector& items_for_delete) { if (items_for_delete.empty()) - return; + return false; m_prevent_list_events = true; @@ -2823,14 +3018,18 @@ void ObjectList::delete_from_model_and_list(const std::vector& it if (!(item->type&(itObject | itVolume | itInstance))) continue; if (item->type&itObject) { - del_object(item->obj_idx); + bool was_cut = object(item->obj_idx)->is_cut(); + if (!del_object(item->obj_idx)) + return false;// continue; m_objects_model->Delete(m_objects_model->GetItemById(item->obj_idx)); + if (was_cut) + update_lock_icons_for_model(); } else { if (!del_subobject_from_object(item->obj_idx, item->sub_obj_idx, item->type)) continue; if (item->type&itVolume) { - m_objects_model->Delete(m_objects_model->GetItemByVolumeId(item->obj_idx, item->sub_obj_idx)); + add_volumes_to_object_in_list(item->obj_idx); ModelObject* obj = object(item->obj_idx); if (obj->volumes.size() == 1) { wxDataViewItem parent = m_objects_model->GetItemById(item->obj_idx); @@ -2854,8 +3053,12 @@ void ObjectList::delete_from_model_and_list(const std::vector& it update_info_items(id); } - m_prevent_list_events = true; + m_prevent_list_events = false; + if (modified_objects_ids.empty()) + return false; part_selection_changed(); + + return true; } void ObjectList::delete_all_objects_from_list() @@ -2960,8 +3163,10 @@ void ObjectList::remove() { wxDataViewItem parent = m_objects_model->GetParent(item); ItemType type = m_objects_model->GetItemType(item); - if (type & itObject) - delete_from_model_and_list(itObject, m_objects_model->GetIdByItem(item), -1); + if (type & itObject) { + if (!delete_from_model_and_list(itObject, m_objects_model->GetIdByItem(item), -1)) + return item; + } else { if (type & (itLayer | itInstance)) { // In case there is just one layer or two instances and we delete it, del_subobject_item will @@ -2971,7 +3176,8 @@ void ObjectList::remove() parent = m_objects_model->GetTopParent(item); } - del_subobject_item(item); + if (!del_subobject_item(item)) + return item; } return parent; @@ -2997,6 +3203,8 @@ void ObjectList::remove() if (m_objects_model->InvalidItem(item)) // item can be deleted for this moment (like last 2 Instances or Volumes) continue; parent = delete_item(item); + if (parent == item && m_objects_model->GetItemType(item) & itObject) // Object wasn't deleted + break; } } @@ -3189,7 +3397,7 @@ void ObjectList::add_layer_item(const t_layer_height_range& range, const auto layer_item = m_objects_model->AddLayersChild(layers_item, range, - config.opt_int("extruder"), + extruder2str(config.opt_int("extruder")), layer_idx); add_settings_item(layer_item, &config); } @@ -3283,6 +3491,24 @@ bool ObjectList::is_selected(const ItemType type) const return false; } +bool ObjectList::is_connectors_item_selected() const +{ + const wxDataViewItem& item = GetSelection(); + if (item) + return m_objects_model->GetItemType(item) == itInfo && m_objects_model->GetInfoItemType(item) == InfoItemType::CutConnectors; + + return false; +} + +bool ObjectList::is_connectors_item_selected(const wxDataViewItemArray& sels) const +{ + for (auto item : sels) + if (m_objects_model->GetItemType(item) == itInfo && m_objects_model->GetInfoItemType(item) == InfoItemType::CutConnectors) + return true; + + return false; +} + int ObjectList::get_selected_layers_range_idx() const { const wxDataViewItem& item = GetSelection(); @@ -3409,11 +3635,18 @@ void ObjectList::update_selections() else { for (auto idx : selection.get_volume_idxs()) { const auto gl_vol = selection.get_volume(idx); - if (gl_vol->volume_idx() >= 0) + if (gl_vol->volume_idx() >= 0) { // Only add GLVolumes with non-negative volume_ids. GLVolumes with negative volume ids // are not associated with ModelVolumes, but they are temporarily generated by the backend // (for example, SLA supports or SLA pad). - sels.Add(m_objects_model->GetItemByVolumeId(gl_vol->object_idx(), gl_vol->volume_idx())); + int obj_idx = gl_vol->object_idx(); + int vol_idx = gl_vol->volume_idx(); + assert(obj_idx >= 0 && vol_idx >= 0); + if (object(obj_idx)->volumes[vol_idx]->is_cut_connector()) + sels.Add(m_objects_model->GetInfoItemByType(m_objects_model->GetItemById(obj_idx), InfoItemType::CutConnectors)); + else + sels.Add(m_objects_model->GetItemByVolumeId(obj_idx, vol_idx)); + } } m_selection_mode = smVolume; } } @@ -3463,11 +3696,34 @@ void ObjectList::update_selections() if (sels.size() == 0 || m_selection_mode & smSettings) m_selection_mode = smUndef; - - select_items(sels); - // Scroll selected Item in the middle of an object list - ensure_current_item_visible(); + if (fix_cut_selection(sels) || is_connectors_item_selected(sels)) { + m_prevent_list_events = true; + + // If some part is selected, unselect all items except of selected parts of the current object + UnselectAll(); + SetSelections(sels); + + m_prevent_list_events = false; + + // update object selection on Plater + if (!m_prevent_canvas_selection_update) + update_selections_on_canvas(); + + // to update the toolbar and info sizer + if (!GetSelection() || m_objects_model->GetItemType(GetSelection()) == itObject || is_connectors_item_selected()) { + auto event = SimpleEvent(EVT_OBJ_LIST_OBJECT_SELECT); + event.SetEventObject(this); + wxPostEvent(this, event); + } + part_selection_changed(); + } + else { + select_items(sels); + + // Scroll selected Item in the middle of an object list + ensure_current_item_visible(); + } } void ObjectList::update_selections_on_canvas() @@ -3501,16 +3757,29 @@ void ObjectList::update_selections_on_canvas() volume_idxs.insert(volume_idxs.end(), idxs.begin(), idxs.end()); } else if (type == itInfo) { - // When selecting an info item, select one instance of the - // respective object - a gizmo may want to be opened. - int inst_idx = selection.get_instance_idx(); - int scene_obj_idx = selection.get_object_idx(); - mode = Selection::Instance; - // select first instance, unless an instance of the object is already selected - if (scene_obj_idx == -1 || inst_idx == -1 || scene_obj_idx != obj_idx) - inst_idx = 0; - std::vector idxs = selection.get_volume_idxs_from_instance(obj_idx, inst_idx); - volume_idxs.insert(volume_idxs.end(), idxs.begin(), idxs.end()); + if (m_objects_model->GetInfoItemType(item) == InfoItemType::CutConnectors) { + mode = Selection::Volume; + + // When selecting CutConnectors info item, select all object volumes, which are marked as a connector + const ModelObject* obj = object(obj_idx); + for (unsigned int vol_idx = 0; vol_idx < obj->volumes.size(); vol_idx++) + if (obj->volumes[vol_idx]->is_cut_connector()) { + std::vector idxs = selection.get_volume_idxs_from_volume(obj_idx, std::max(instance_idx, 0), vol_idx); + volume_idxs.insert(volume_idxs.end(), idxs.begin(), idxs.end()); + } + } + else { + // When selecting an info item, select one instance of the + // respective object - a gizmo may want to be opened. + int inst_idx = selection.get_instance_idx(); + int scene_obj_idx = selection.get_object_idx(); + mode = Selection::Instance; + // select first instance, unless an instance of the object is already selected + if (scene_obj_idx == -1 || inst_idx == -1 || scene_obj_idx != obj_idx) + inst_idx = 0; + std::vector idxs = selection.get_volume_idxs_from_instance(obj_idx, inst_idx); + volume_idxs.insert(volume_idxs.end(), idxs.begin(), idxs.end()); + } } else { @@ -3526,6 +3795,8 @@ void ObjectList::update_selections_on_canvas() if (sel_cnt == 1) { wxDataViewItem item = GetSelection(); + if (m_objects_model->GetInfoItemType(item) == InfoItemType::CutConnectors) + selection.remove_all(); if (m_objects_model->GetItemType(item) & (itSettings | itInstanceRoot | itLayerRoot | itLayer)) add_to_selection(m_objects_model->GetParent(item), selection, instance_idx, mode); else @@ -3796,6 +4067,53 @@ void ObjectList::fix_multiselection_conflicts() m_prevent_list_events = false; } +bool ObjectList::fix_cut_selection(wxDataViewItemArray& sels) +{ + if (wxGetApp().plater()->canvas3D()->get_gizmos_manager().get_current_type() == GLGizmosManager::Scale) { + for (const auto& item : sels) { + if (m_objects_model->GetItemType(item) & (itInstance | itObject) || + (m_objects_model->GetItemType(item) & itSettings && + m_objects_model->GetItemType(m_objects_model->GetParent(item)) & itObject)) { + + bool is_instance_selection = m_objects_model->GetItemType(item) & itInstance; + + int object_idx = m_objects_model->GetObjectIdByItem(item); + int inst_idx = is_instance_selection ? m_objects_model->GetInstanceIdByItem(item) : 0; + + if (auto obj = object(object_idx); obj->is_cut()) { + sels.Clear(); + + auto cut_id = obj->cut_id; + + int objects_cnt = int((*m_objects).size()); + for (int obj_idx = 0; obj_idx < objects_cnt; ++obj_idx) { + auto object = (*m_objects)[obj_idx]; + if (object->is_cut() && object->cut_id.has_same_id(cut_id)) + sels.Add(is_instance_selection ? m_objects_model->GetItemByInstanceId(obj_idx, inst_idx) : m_objects_model->GetItemById(obj_idx)); + } + return true; + } + } + } + } + return false; +} + +void ObjectList::fix_cut_selection() +{ + wxDataViewItemArray sels; + GetSelections(sels); + if (fix_cut_selection(sels)) { + m_prevent_list_events = true; + + // If some part is selected, unselect all items except of selected parts of the current object + UnselectAll(); + SetSelections(sels); + + m_prevent_list_events = false; + } +} + ModelVolume* ObjectList::get_selected_model_volume() { wxDataViewItem item = GetSelection(); @@ -3826,10 +4144,11 @@ void ObjectList::change_part_type() if (obj_idx < 0) return; const ModelVolumeType type = volume->type(); + const ModelObject* obj = object(obj_idx); if (type == ModelVolumeType::MODEL_PART) { int model_part_cnt = 0; - for (auto vol : (*m_objects)[obj_idx]->volumes) { + for (auto vol : obj->volumes) { if (vol->type() == ModelVolumeType::MODEL_PART) ++model_part_cnt; } @@ -3840,9 +4159,18 @@ void ObjectList::change_part_type() } } - const wxString names[] = { _L("Part"), _L("Negative Volume"), _L("Modifier"), _L("Support Blocker"), _L("Support Enforcer") }; - auto new_type = ModelVolumeType(wxGetApp().GetSingleChoiceIndex(_L("Type:"), _L("Select type of part"), wxArrayString(5, names), int(type))); + const bool is_cut_object = obj->is_cut(); + wxArrayString names; + names.Alloc(is_cut_object ? 3 : 5); + if (!is_cut_object) + for (const wxString& type : { _L("Part"), _L("Negative Volume") }) + names.Add(type); + for (const wxString& type : { _L("Modifier"), _L("Support Blocker"), _L("Support Enforcer") } ) + names.Add(type); + + const int type_shift = is_cut_object ? 2 : 0; + auto new_type = ModelVolumeType(type_shift + wxGetApp().GetSingleChoiceIndex(_L("Type:"), _L("Select type of part"), names, int(type) - type_shift)); if (new_type == type || new_type == ModelVolumeType::INVALID) return; @@ -4362,33 +4690,14 @@ void ObjectList::set_extruder_for_selected_items(const int extruder) const wxGetApp().plater()->update(); } -wxDataViewItemArray ObjectList::reorder_volumes_and_get_selection(int obj_idx, std::function add_to_selection/* = nullptr*/) +wxDataViewItemArray ObjectList::reorder_volumes_and_get_selection(size_t obj_idx, std::function add_to_selection/* = nullptr*/) { - wxDataViewItemArray items; + (*m_objects)[obj_idx]->sort_volumes(wxGetApp().app_config->get("order_volumes") == "1"); - ModelObject* object = (*m_objects)[obj_idx]; - if (object->volumes.size() <= 1) - return items; + wxDataViewItemArray items = add_volumes_to_object_in_list(obj_idx, std::move(add_to_selection)); - object->sort_volumes(wxGetApp().app_config->get("order_volumes") == "1"); + changed_object(int(obj_idx)); - wxDataViewItem object_item = m_objects_model->GetItemById(obj_idx); - m_objects_model->DeleteVolumeChildren(object_item); - - for (const ModelVolume* volume : object->volumes) { - wxDataViewItem vol_item = m_objects_model->AddVolumeChild(object_item, from_u8(volume->name), - volume->type(), - get_warning_icon_name(volume->mesh().stats()), - volume->config.has("extruder") ? volume->config.extruder() : 0, - false); - // add settings to the part, if it has those - add_settings_item(vol_item, &volume->config.get()); - - if (add_to_selection && add_to_selection(volume)) - items.Add(vol_item); - } - - changed_object(obj_idx); return items; } diff --git a/src/slic3r/GUI/GUI_ObjectList.hpp b/src/slic3r/GUI/GUI_ObjectList.hpp index c838203ae..eb718b48d 100644 --- a/src/slic3r/GUI/GUI_ObjectList.hpp +++ b/src/slic3r/GUI/GUI_ObjectList.hpp @@ -247,7 +247,7 @@ public: void add_category_to_settings_from_frequent(const std::vector& category_options, wxDataViewItem item); void show_settings(const wxDataViewItem settings_item); bool is_instance_or_object_selected(); - + bool is_selected_object_cut(); void load_subobject(ModelVolumeType type, bool from_galery = false); // ! ysFIXME - delete commented code after testing and rename "load_modifier" to something common //void load_part(ModelObject& model_object, std::vector& added_volumes, ModelVolumeType type, bool from_galery = false); @@ -257,8 +257,8 @@ public: void load_shape_object_from_gallery(); void load_shape_object_from_gallery(const wxArrayString& input_files); void load_mesh_object(const TriangleMesh &mesh, const wxString &name, bool center = true); - void del_object(const int obj_idx); - void del_subobject_item(wxDataViewItem& item); + bool del_object(const int obj_idx); + bool del_subobject_item(wxDataViewItem& item); void del_settings_from_config(const wxDataViewItem& parent_item); void del_instances_from_object(const int obj_idx); void del_layer_from_object(const int obj_idx, const t_layer_height_range& layer_range); @@ -277,6 +277,9 @@ public: bool is_splittable(bool to_objects); bool selected_instances_of_same_object(); bool can_split_instances(); + bool has_selected_cut_object() const; + void invalidate_cut_info_for_selection(); + void invalidate_cut_info_for_object(size_t obj_idx); bool can_merge_to_multipart_object() const; bool can_merge_to_single_object() const; @@ -288,6 +291,9 @@ public: void changed_object(const int obj_idx = -1) const; void part_selection_changed(); + // Add object's volumes to the list + // Return selected items, if add_to_selection is defined + wxDataViewItemArray add_volumes_to_object_in_list(size_t obj_idx, std::function add_to_selection = nullptr); // Add object to the list void add_object_to_list(size_t obj_idx, bool call_selection_changed = true); // Delete object from the list @@ -295,8 +301,9 @@ public: void delete_object_from_list(const size_t obj_idx); void delete_volume_from_list(const size_t obj_idx, const size_t vol_idx); void delete_instance_from_list(const size_t obj_idx, const size_t inst_idx); - void delete_from_model_and_list(const ItemType type, const int obj_idx, const int sub_obj_idx); - void delete_from_model_and_list(const std::vector& items_for_delete); + void update_lock_icons_for_model(); + bool delete_from_model_and_list(const ItemType type, const int obj_idx, const int sub_obj_idx); + bool delete_from_model_and_list(const std::vector& items_for_delete); // Delete all objects from the list void delete_all_objects_from_list(); // Increase instances count @@ -339,6 +346,8 @@ public: void init_objects(); bool multiple_selection() const ; bool is_selected(const ItemType type) const; + bool is_connectors_item_selected() const; + bool is_connectors_item_selected(const wxDataViewItemArray& sels) const; int get_selected_layers_range_idx() const; void set_selected_layers_range_idx(const int range_idx) { m_selected_layers_range_idx = range_idx; } void set_selection_mode(SELECTION_MODE mode) { m_selection_mode = mode; } @@ -353,6 +362,9 @@ public: bool check_last_selection(wxString& msg_str); // correct current selections to avoid of the possible conflicts void fix_multiselection_conflicts(); + // correct selection in respect to the cut_id if any exists + void fix_cut_selection(); + bool fix_cut_selection(wxDataViewItemArray& sels); ModelVolume* get_selected_model_volume(); void change_part_type(); @@ -388,7 +400,7 @@ public: void toggle_printable_state(); void set_extruder_for_selected_items(const int extruder) const ; - wxDataViewItemArray reorder_volumes_and_get_selection(int obj_idx, std::function add_to_selection = nullptr); + wxDataViewItemArray reorder_volumes_and_get_selection(size_t obj_idx, std::function add_to_selection = nullptr); void apply_volumes_order(); bool has_paint_on_segmentation(); diff --git a/src/slic3r/GUI/GUI_ObjectManipulation.cpp b/src/slic3r/GUI/GUI_ObjectManipulation.cpp index 62de19811..a2d687db0 100644 --- a/src/slic3r/GUI/GUI_ObjectManipulation.cpp +++ b/src/slic3r/GUI/GUI_ObjectManipulation.cpp @@ -576,6 +576,35 @@ void ObjectManipulation::UpdateAndShow(const bool show) OG_Settings::UpdateAndShow(show); } +void ObjectManipulation::Enable(const bool enadle) +{ + for (auto editor : m_editors) + editor->Enable(enadle); + for (wxWindow* win : std::initializer_list{ m_reset_scale_button, m_reset_rotation_button, m_drop_to_bed_button, m_check_inch, m_lock_bnt +#if ENABLE_WORLD_COORDINATE + ,m_reset_skew_button +#endif // ENABLE_WORLD_COORDINATE + }) + win->Enable(enadle); +} + +void ObjectManipulation::DisableScale() +{ + for (auto editor : m_editors) + editor->Enable(editor->has_opt_key("scale") || editor->has_opt_key("size") ? false : true); + for (wxWindow* win : std::initializer_list{ m_reset_scale_button, m_lock_bnt +#if ENABLE_WORLD_COORDINATE + ,m_reset_skew_button +#endif // ENABLE_WORLD_COORDINATE + }) + win->Enable(false); +} + +void ObjectManipulation::DisableUnuniformScale() +{ + m_lock_bnt->Enable(false); +} + void ObjectManipulation::update_ui_from_settings() { if (m_imperial_units != (wxGetApp().app_config->get("use_inches") == "1")) { @@ -734,7 +763,7 @@ void ObjectManipulation::update_settings_value(const Selection& selection) #endif // ENABLE_WORLD_COORDINATE m_new_enabled = true; } - else if (obj_list->multiple_selection() || obj_list->is_selected(itInstanceRoot)) { + else if (obj_list->is_connectors_item_selected() || obj_list->multiple_selection() || obj_list->is_selected(itInstanceRoot)) { reset_settings_value(); m_new_move_label_string = L("Translate"); m_new_rotate_label_string = L("Rotate"); diff --git a/src/slic3r/GUI/GUI_ObjectManipulation.hpp b/src/slic3r/GUI/GUI_ObjectManipulation.hpp index e9ffe804a..9995b3e6f 100644 --- a/src/slic3r/GUI/GUI_ObjectManipulation.hpp +++ b/src/slic3r/GUI/GUI_ObjectManipulation.hpp @@ -64,6 +64,8 @@ public: const std::string& get_full_opt_name() const { return m_full_opt_name; } #endif // ENABLE_WORLD_COORDINATE + bool has_opt_key(const std::string& key) { return m_opt_key == key; } + private: double get_value(); }; @@ -197,6 +199,10 @@ public: void Show(const bool show) override; bool IsShown() override; void UpdateAndShow(const bool show) override; + void Enable(const bool enadle = true); + void Disable() { Enable(false); } + void DisableScale(); + void DisableUnuniformScale(); void update_ui_from_settings(); bool use_colors() { return m_use_colors; } diff --git a/src/slic3r/GUI/GUI_Utils.hpp b/src/slic3r/GUI/GUI_Utils.hpp index d7d3529fe..55ca43248 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/GLGizmoBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp index 38f72c613..6305cf917 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp @@ -1,537 +1,537 @@ -#include "GLGizmoBase.hpp" -#include "slic3r/GUI/GLCanvas3D.hpp" - -#include - -#include "slic3r/GUI/GUI_App.hpp" -#include "slic3r/GUI/GUI_ObjectManipulation.hpp" -#if ENABLE_LEGACY_OPENGL_REMOVAL -#include "slic3r/GUI/Plater.hpp" -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -// TODO: Display tooltips quicker on Linux - -namespace Slic3r { -namespace GUI { - -const float GLGizmoBase::Grabber::SizeFactor = 0.05f; -const float GLGizmoBase::Grabber::MinHalfSize = 1.5f; -const float GLGizmoBase::Grabber::DraggingScaleFactor = 1.25f; - -#if ENABLE_RAYCAST_PICKING -PickingModel GLGizmoBase::Grabber::s_cube; -PickingModel GLGizmoBase::Grabber::s_cone; -#else -GLModel GLGizmoBase::Grabber::s_cube; -GLModel GLGizmoBase::Grabber::s_cone; -#endif // ENABLE_RAYCAST_PICKING - -GLGizmoBase::Grabber::~Grabber() -{ -#if ENABLE_RAYCAST_PICKING - if (s_cube.model.is_initialized()) - s_cube.model.reset(); - - if (s_cone.model.is_initialized()) - s_cone.model.reset(); -#else - if (s_cube.is_initialized()) - s_cube.reset(); - - if (s_cone.is_initialized()) - s_cone.reset(); -#endif // ENABLE_RAYCAST_PICKING -} - -float GLGizmoBase::Grabber::get_half_size(float size) const -{ - return std::max(size * SizeFactor, MinHalfSize); -} - -float GLGizmoBase::Grabber::get_dragging_half_size(float size) const -{ - return get_half_size(size) * DraggingScaleFactor; -} - -#if ENABLE_RAYCAST_PICKING -void GLGizmoBase::Grabber::register_raycasters_for_picking(int id) -{ - picking_id = id; - // registration will happen on next call to render() -} - -void GLGizmoBase::Grabber::unregister_raycasters_for_picking() -{ - wxGetApp().plater()->canvas3D()->remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, picking_id); - picking_id = -1; - raycasters = { nullptr }; -} -#endif // ENABLE_RAYCAST_PICKING - -#if ENABLE_RAYCAST_PICKING -void GLGizmoBase::Grabber::render(float size, const ColorRGBA& render_color) -#else -void GLGizmoBase::Grabber::render(float size, const ColorRGBA& render_color, bool picking) -#endif // ENABLE_RAYCAST_PICKING -{ -#if ENABLE_LEGACY_OPENGL_REMOVAL - GLShaderProgram* shader = wxGetApp().get_current_shader(); - if (shader == nullptr) - return; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_RAYCAST_PICKING - if (!s_cube.model.is_initialized()) { -#else - if (!s_cube.is_initialized()) { -#endif // ENABLE_RAYCAST_PICKING - // This cannot be done in constructor, OpenGL is not yet - // initialized at that point (on Linux at least). - indexed_triangle_set its = its_make_cube(1.0, 1.0, 1.0); - its_translate(its, -0.5f * Vec3f::Ones()); -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_RAYCAST_PICKING - s_cube.model.init_from(its); - s_cube.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); -#else - s_cube.init_from(its); -#endif // ENABLE_RAYCAST_PICKING -#else - s_cube.init_from(its, BoundingBoxf3{ { -0.5, -0.5, -0.5 }, { 0.5, 0.5, 0.5 } }); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - -#if ENABLE_RAYCAST_PICKING - if (!s_cone.model.is_initialized()) { - indexed_triangle_set its = its_make_cone(0.375, 1.5, double(PI) / 18.0); - s_cone.model.init_from(its); - s_cone.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); - } -#else - if (!s_cone.is_initialized()) - s_cone.init_from(its_make_cone(0.375, 1.5, double(PI) / 18.0)); -#endif // ENABLE_RAYCAST_PICKING - - const float half_size = dragging ? get_dragging_half_size(size) : get_half_size(size); - -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_RAYCAST_PICKING - s_cube.model.set_color(render_color); - s_cone.model.set_color(render_color); -#else - s_cube.set_color(render_color); - s_cone.set_color(render_color); -#endif // ENABLE_RAYCAST_PICKING - - const Camera& camera = wxGetApp().plater()->get_camera(); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); -#if ENABLE_RAYCAST_PICKING - const Transform3d& view_matrix = camera.get_view_matrix(); - const Matrix3d view_matrix_no_offset = view_matrix.matrix().block(0, 0, 3, 3); - std::vector elements_matrices(GRABBER_ELEMENTS_MAX_COUNT, Transform3d::Identity()); - elements_matrices[0] = matrix * Geometry::assemble_transform(center, angles, 2.0 * half_size * Vec3d::Ones()); - Transform3d view_model_matrix = view_matrix * elements_matrices[0]; -#else - const Transform3d& view_matrix = camera.get_view_matrix(); - const Transform3d model_matrix = matrix * Geometry::assemble_transform(center, angles, 2.0 * half_size * Vec3d::Ones()); - const Transform3d view_model_matrix = view_matrix * model_matrix; -#endif // ENABLE_RAYCAST_PICKING - - shader->set_uniform("view_model_matrix", view_model_matrix); -#if ENABLE_RAYCAST_PICKING - Matrix3d view_normal_matrix = view_matrix_no_offset * elements_matrices[0].matrix().block(0, 0, 3, 3).inverse().transpose(); -#else - const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); -#endif // ENABLE_RAYCAST_PICKING - shader->set_uniform("view_normal_matrix", view_normal_matrix); -#else - s_cube.set_color(-1, render_color); - s_cone.set_color(-1, render_color); - glsafe(::glPushMatrix()); - glsafe(::glTranslated(center.x(), center.y(), center.z())); - glsafe(::glRotated(Geometry::rad2deg(angles.z()), 0.0, 0.0, 1.0)); - glsafe(::glRotated(Geometry::rad2deg(angles.y()), 0.0, 1.0, 0.0)); - glsafe(::glRotated(Geometry::rad2deg(angles.x()), 1.0, 0.0, 0.0)); - glsafe(::glScaled(2.0 * half_size, 2.0 * half_size, 2.0 * half_size)); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_RAYCAST_PICKING - s_cube.model.render(); - - auto render_extension = [&view_matrix, &view_matrix_no_offset, shader](const Transform3d& matrix) { - const Transform3d view_model_matrix = view_matrix * matrix; - shader->set_uniform("view_model_matrix", view_model_matrix); - const Matrix3d view_normal_matrix = view_matrix_no_offset * matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); - shader->set_uniform("view_normal_matrix", view_normal_matrix); - s_cone.model.render(); - }; -#else - s_cube.render(); -#endif // ENABLE_RAYCAST_PICKING - -#if ENABLE_LEGACY_OPENGL_REMOVAL - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosX)) != 0) { -#if ENABLE_RAYCAST_PICKING - elements_matrices[1] = elements_matrices[0] * Geometry::assemble_transform(Vec3d::UnitX(), Vec3d(0.0, 0.5 * double(PI), 0.0)); - render_extension(elements_matrices[1]); -#else - shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(Vec3d::UnitX(), Vec3d(0.0, 0.5 * double(PI), 0.0))); - s_cone.render(); -#endif // ENABLE_RAYCAST_PICKING - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegX)) != 0) { -#if ENABLE_RAYCAST_PICKING - elements_matrices[2] = elements_matrices[0] * Geometry::assemble_transform(-Vec3d::UnitX(), Vec3d(0.0, -0.5 * double(PI), 0.0)); - render_extension(elements_matrices[2]); -#else - shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(-Vec3d::UnitX(), Vec3d(0.0, -0.5 * double(PI), 0.0))); - s_cone.render(); -#endif // ENABLE_RAYCAST_PICKING - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosY)) != 0) { -#if ENABLE_RAYCAST_PICKING - elements_matrices[3] = elements_matrices[0] * Geometry::assemble_transform(Vec3d::UnitY(), Vec3d(-0.5 * double(PI), 0.0, 0.0)); - render_extension(elements_matrices[3]); -#else - shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(Vec3d::UnitY(), Vec3d(-0.5 * double(PI), 0.0, 0.0))); - s_cone.render(); -#endif // ENABLE_RAYCAST_PICKING - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegY)) != 0) { -#if ENABLE_RAYCAST_PICKING - elements_matrices[4] = elements_matrices[0] * Geometry::assemble_transform(-Vec3d::UnitY(), Vec3d(0.5 * double(PI), 0.0, 0.0)); - render_extension(elements_matrices[4]); -#else - shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(-Vec3d::UnitY(), Vec3d(0.5 * double(PI), 0.0, 0.0))); - s_cone.render(); -#endif // ENABLE_RAYCAST_PICKING - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosZ)) != 0) { -#if ENABLE_RAYCAST_PICKING - elements_matrices[5] = elements_matrices[0] * Geometry::assemble_transform(Vec3d::UnitZ()); - render_extension(elements_matrices[5]); -#else - shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(Vec3d::UnitZ())); - s_cone.render(); -#endif // ENABLE_RAYCAST_PICKING - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegZ)) != 0) { -#if ENABLE_RAYCAST_PICKING - elements_matrices[6] = elements_matrices[0] * Geometry::assemble_transform(-Vec3d::UnitZ(), Vec3d(double(PI), 0.0, 0.0)); - render_extension(elements_matrices[6]); -#else - shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(-Vec3d::UnitZ(), Vec3d(double(PI), 0.0, 0.0))); - s_cone.render(); -#endif // ENABLE_RAYCAST_PICKING - } -#else - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosX)) != 0) { - glsafe(::glPushMatrix()); - glsafe(::glTranslated(1.0, 0.0, 0.0)); - glsafe(::glRotated(0.5 * Geometry::rad2deg(double(PI)), 0.0, 1.0, 0.0)); - s_cone.render(); - glsafe(::glPopMatrix()); - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegX)) != 0) { - glsafe(::glPushMatrix()); - glsafe(::glTranslated(-1.0, 0.0, 0.0)); - glsafe(::glRotated(-0.5 * Geometry::rad2deg(double(PI)), 0.0, 1.0, 0.0)); - s_cone.render(); - glsafe(::glPopMatrix()); - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosY)) != 0) { - glsafe(::glPushMatrix()); - glsafe(::glTranslated(0.0, 1.0, 0.0)); - glsafe(::glRotated(-0.5 * Geometry::rad2deg(double(PI)), 1.0, 0.0, 0.0)); - s_cone.render(); - glsafe(::glPopMatrix()); - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegY)) != 0) { - glsafe(::glPushMatrix()); - glsafe(::glTranslated(0.0, -1.0, 0.0)); - glsafe(::glRotated(0.5 * Geometry::rad2deg(double(PI)), 1.0, 0.0, 0.0)); - s_cone.render(); - glsafe(::glPopMatrix()); - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosZ)) != 0) { - glsafe(::glPushMatrix()); - glsafe(::glTranslated(0.0, 0.0, 1.0)); - s_cone.render(); - glsafe(::glPopMatrix()); - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegZ)) != 0) { - glsafe(::glPushMatrix()); - glsafe(::glTranslated(0.0, 0.0, -1.0)); - glsafe(::glRotated(Geometry::rad2deg(double(PI)), 1.0, 0.0, 0.0)); - s_cone.render(); - glsafe(::glPopMatrix()); - } -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glPopMatrix()); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_RAYCAST_PICKING - if (raycasters[0] == nullptr) { - GLCanvas3D& canvas = *wxGetApp().plater()->canvas3D(); - raycasters[0] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cube.mesh_raycaster, elements_matrices[0]); - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosX)) != 0) - raycasters[1] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[1]); - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegX)) != 0) - raycasters[2] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[2]); - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosY)) != 0) - raycasters[3] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[3]); - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegY)) != 0) - raycasters[4] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[4]); - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosZ)) != 0) - raycasters[5] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[5]); - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegZ)) != 0) - raycasters[6] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[6]); - } - else { - for (size_t i = 0; i < GRABBER_ELEMENTS_MAX_COUNT; ++i) { - if (raycasters[i] != nullptr) - raycasters[i]->set_transform(elements_matrices[i]); - } - } -#endif // ENABLE_RAYCAST_PICKING -} - -GLGizmoBase::GLGizmoBase(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) - : m_parent(parent) - , m_icon_filename(icon_filename) - , m_sprite_id(sprite_id) - , m_imgui(wxGetApp().imgui()) -{ -} - -void GLGizmoBase::set_hover_id(int id) -{ - // do not change hover id during dragging - assert(!m_dragging); - - // allow empty grabbers when not using grabbers but use hover_id - flatten, rotate - if (!m_grabbers.empty() && id >= (int) m_grabbers.size()) - return; - - m_hover_id = id; - on_set_hover_id(); -} - -bool GLGizmoBase::update_items_state() -{ - bool res = m_dirty; - m_dirty = false; - return res; -} - -#if ENABLE_RAYCAST_PICKING -void GLGizmoBase::register_grabbers_for_picking() -{ - for (size_t i = 0; i < m_grabbers.size(); ++i) { - m_grabbers[i].register_raycasters_for_picking((m_group_id >= 0) ? m_group_id : i); - } -} - -void GLGizmoBase::unregister_grabbers_for_picking() -{ - for (size_t i = 0; i < m_grabbers.size(); ++i) { - m_grabbers[i].unregister_raycasters_for_picking(); - } -} -#else -ColorRGBA GLGizmoBase::picking_color_component(unsigned int id) const -{ - id = BASE_ID - id; - if (m_group_id > -1) - id -= m_group_id; - - return picking_decode(id); -} -#endif // ENABLE_RAYCAST_PICKING - -void GLGizmoBase::render_grabbers(const BoundingBoxf3& box) const -{ - render_grabbers((float)((box.size().x() + box.size().y() + box.size().z()) / 3.0)); -} - -void GLGizmoBase::render_grabbers(float size) const -{ - GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); - if (shader == nullptr) - return; - shader->start_using(); - shader->set_uniform("emission_factor", 0.1f); - for (int i = 0; i < (int)m_grabbers.size(); ++i) { - if (m_grabbers[i].enabled) - m_grabbers[i].render(m_hover_id == i, size); - } - shader->stop_using(); -} - -#if !ENABLE_RAYCAST_PICKING -void GLGizmoBase::render_grabbers_for_picking(const BoundingBoxf3& box) const -{ -#if ENABLE_LEGACY_OPENGL_REMOVAL - GLShaderProgram* shader = wxGetApp().get_shader("flat"); - if (shader != nullptr) { - shader->start_using(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - const float mean_size = float((box.size().x() + box.size().y() + box.size().z()) / 3.0); - - for (unsigned int i = 0; i < (unsigned int)m_grabbers.size(); ++i) { - if (m_grabbers[i].enabled) { - m_grabbers[i].color = picking_color_component(i); - m_grabbers[i].render_for_picking(mean_size); - } - } -#if ENABLE_LEGACY_OPENGL_REMOVAL - shader->stop_using(); - } -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} -#endif // !ENABLE_RAYCAST_PICKING - -// help function to process grabbers -// call start_dragging, stop_dragging, on_dragging -bool GLGizmoBase::use_grabbers(const wxMouseEvent &mouse_event) { - bool is_dragging_finished = false; - if (mouse_event.Moving()) { - // it should not happen but for sure - assert(!m_dragging); - if (m_dragging) is_dragging_finished = true; - else return false; - } - - if (mouse_event.LeftDown()) { - Selection &selection = m_parent.get_selection(); - if (!selection.is_empty() && m_hover_id != -1 && - (m_grabbers.empty() || m_hover_id < static_cast(m_grabbers.size()))) { - selection.setup_cache(); - - m_dragging = true; - for (auto &grabber : m_grabbers) grabber.dragging = false; - if (!m_grabbers.empty() && m_hover_id < int(m_grabbers.size())) - m_grabbers[m_hover_id].dragging = true; - - on_start_dragging(); - - // Let the plater know that the dragging started - m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_STARTED)); - m_parent.set_as_dirty(); - return true; - } - } else if (m_dragging) { - // when mouse cursor leave window than finish actual dragging operation - bool is_leaving = mouse_event.Leaving(); - if (mouse_event.Dragging()) { - Point mouse_coord(mouse_event.GetX(), mouse_event.GetY()); - auto ray = m_parent.mouse_ray(mouse_coord); - UpdateData data(ray, mouse_coord); - - on_dragging(data); - - wxGetApp().obj_manipul()->set_dirty(); - m_parent.set_as_dirty(); - return true; - } - else if (mouse_event.LeftUp() || is_leaving || is_dragging_finished) { -#if ENABLE_WORLD_COORDINATE - do_stop_dragging(is_leaving); -#else - for (auto &grabber : m_grabbers) grabber.dragging = false; - m_dragging = false; - - // NOTE: This should be part of GLCanvas3D - // Reset hover_id when leave window - if (is_leaving) m_parent.mouse_up_cleanup(); - - on_stop_dragging(); - - // There is prediction that after draggign, data are changed - // Data are updated twice also by canvas3D::reload_scene. - // Should be fixed. - m_parent.get_gizmos_manager().update_data(); - - wxGetApp().obj_manipul()->set_dirty(); - - // Let the plater know that the dragging finished, so a delayed - // refresh of the scene with the background processing data should - // be performed. - m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); - // updates camera target constraints - m_parent.refresh_camera_scene_box(); -#endif // ENABLE_WORLD_COORDINATE - return true; - } - } - return false; -} - -#if ENABLE_WORLD_COORDINATE -void GLGizmoBase::do_stop_dragging(bool perform_mouse_cleanup) -{ - for (auto& grabber : m_grabbers) grabber.dragging = false; - m_dragging = false; - - // NOTE: This should be part of GLCanvas3D - // Reset hover_id when leave window - if (perform_mouse_cleanup) m_parent.mouse_up_cleanup(); - - on_stop_dragging(); - - // There is prediction that after draggign, data are changed - // Data are updated twice also by canvas3D::reload_scene. - // Should be fixed. - m_parent.get_gizmos_manager().update_data(); - - wxGetApp().obj_manipul()->set_dirty(); - - // Let the plater know that the dragging finished, so a delayed - // refresh of the scene with the background processing data should - // be performed. - m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); - // updates camera target constraints - m_parent.refresh_camera_scene_box(); -} -#endif // ENABLE_WORLD_COORDINATE - -std::string GLGizmoBase::format(float value, unsigned int decimals) const -{ - return Slic3r::string_printf("%.*f", decimals, value); -} - -void GLGizmoBase::set_dirty() { - m_dirty = true; -} - - - -void GLGizmoBase::render_input_window(float x, float y, float bottom_limit) -{ - on_render_input_window(x, y, bottom_limit); - if (m_first_input_window_render) { - // imgui windows that don't have an initial size needs to be processed once to get one - // and are not rendered in the first frame - // so, we forces to render another frame the first time the imgui window is shown - // https://github.com/ocornut/imgui/issues/2949 - m_parent.set_as_dirty(); - m_parent.request_extra_frame(); - m_first_input_window_render = false; - } -} - - - -std::string GLGizmoBase::get_name(bool include_shortcut) const -{ - int key = get_shortcut_key(); - std::string out = on_get_name(); - if (include_shortcut && key >= WXK_CONTROL_A && key <= WXK_CONTROL_Z) - out += std::string(" [") + char(int('A') + key - int(WXK_CONTROL_A)) + "]"; - return out; -} - - -} // namespace GUI -} // namespace Slic3r - +#include "GLGizmoBase.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" + +#include + +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/GUI_ObjectManipulation.hpp" +#if ENABLE_LEGACY_OPENGL_REMOVAL +#include "slic3r/GUI/Plater.hpp" +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +// TODO: Display tooltips quicker on Linux + +namespace Slic3r { +namespace GUI { + +const float GLGizmoBase::Grabber::SizeFactor = 0.05f; +const float GLGizmoBase::Grabber::MinHalfSize = 1.5f; +const float GLGizmoBase::Grabber::DraggingScaleFactor = 1.25f; + +#if ENABLE_RAYCAST_PICKING +PickingModel GLGizmoBase::Grabber::s_cube; +PickingModel GLGizmoBase::Grabber::s_cone; +#else +GLModel GLGizmoBase::Grabber::s_cube; +GLModel GLGizmoBase::Grabber::s_cone; +#endif // ENABLE_RAYCAST_PICKING + +GLGizmoBase::Grabber::~Grabber() +{ +#if ENABLE_RAYCAST_PICKING + if (s_cube.model.is_initialized()) + s_cube.model.reset(); + + if (s_cone.model.is_initialized()) + s_cone.model.reset(); +#else + if (s_cube.is_initialized()) + s_cube.reset(); + + if (s_cone.is_initialized()) + s_cone.reset(); +#endif // ENABLE_RAYCAST_PICKING +} + +float GLGizmoBase::Grabber::get_half_size(float size) const +{ + return std::max(size * SizeFactor, MinHalfSize); +} + +float GLGizmoBase::Grabber::get_dragging_half_size(float size) const +{ + return get_half_size(size) * DraggingScaleFactor; +} + +#if ENABLE_RAYCAST_PICKING +void GLGizmoBase::Grabber::register_raycasters_for_picking(int id) +{ + picking_id = id; + // registration will happen on next call to render() +} + +void GLGizmoBase::Grabber::unregister_raycasters_for_picking() +{ + wxGetApp().plater()->canvas3D()->remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, picking_id); + picking_id = -1; + raycasters = { nullptr }; +} +#endif // ENABLE_RAYCAST_PICKING + +#if ENABLE_RAYCAST_PICKING +void GLGizmoBase::Grabber::render(float size, const ColorRGBA& render_color) +#else +void GLGizmoBase::Grabber::render(float size, const ColorRGBA& render_color, bool picking) +#endif // ENABLE_RAYCAST_PICKING +{ +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLShaderProgram* shader = wxGetApp().get_current_shader(); + if (shader == nullptr) + return; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_RAYCAST_PICKING + if (!s_cube.model.is_initialized()) { +#else + if (!s_cube.is_initialized()) { +#endif // ENABLE_RAYCAST_PICKING + // This cannot be done in constructor, OpenGL is not yet + // initialized at that point (on Linux at least). + indexed_triangle_set its = its_make_cube(1.0, 1.0, 1.0); + its_translate(its, -0.5f * Vec3f::Ones()); +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_RAYCAST_PICKING + s_cube.model.init_from(its); + s_cube.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); +#else + s_cube.init_from(its); +#endif // ENABLE_RAYCAST_PICKING +#else + s_cube.init_from(its, BoundingBoxf3{ { -0.5, -0.5, -0.5 }, { 0.5, 0.5, 0.5 } }); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + +#if ENABLE_RAYCAST_PICKING + if (!s_cone.model.is_initialized()) { + indexed_triangle_set its = its_make_cone(0.375, 1.5, double(PI) / 18.0); + s_cone.model.init_from(its); + s_cone.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); + } +#else + if (!s_cone.is_initialized()) + s_cone.init_from(its_make_cone(0.375, 1.5, double(PI) / 18.0)); +#endif // ENABLE_RAYCAST_PICKING + + const float half_size = dragging ? get_dragging_half_size(size) : get_half_size(size); + +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_RAYCAST_PICKING + s_cube.model.set_color(render_color); + s_cone.model.set_color(render_color); +#else + s_cube.set_color(render_color); + s_cone.set_color(render_color); +#endif // ENABLE_RAYCAST_PICKING + + const Camera& camera = wxGetApp().plater()->get_camera(); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); +#if ENABLE_RAYCAST_PICKING + const Transform3d& view_matrix = camera.get_view_matrix(); + const Matrix3d view_matrix_no_offset = view_matrix.matrix().block(0, 0, 3, 3); + std::vector elements_matrices(GRABBER_ELEMENTS_MAX_COUNT, Transform3d::Identity()); + elements_matrices[0] = matrix * Geometry::assemble_transform(center, angles, 2.0 * half_size * Vec3d::Ones()); + Transform3d view_model_matrix = view_matrix * elements_matrices[0]; +#else + const Transform3d& view_matrix = camera.get_view_matrix(); + const Transform3d model_matrix = matrix * Geometry::assemble_transform(center, angles, 2.0 * half_size * Vec3d::Ones()); + const Transform3d view_model_matrix = view_matrix * model_matrix; +#endif // ENABLE_RAYCAST_PICKING + + shader->set_uniform("view_model_matrix", view_model_matrix); +#if ENABLE_RAYCAST_PICKING + Matrix3d view_normal_matrix = view_matrix_no_offset * elements_matrices[0].matrix().block(0, 0, 3, 3).inverse().transpose(); +#else + const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); +#endif // ENABLE_RAYCAST_PICKING + shader->set_uniform("view_normal_matrix", view_normal_matrix); +#else + s_cube.set_color(-1, render_color); + s_cone.set_color(-1, render_color); + glsafe(::glPushMatrix()); + glsafe(::glTranslated(center.x(), center.y(), center.z())); + glsafe(::glRotated(Geometry::rad2deg(angles.z()), 0.0, 0.0, 1.0)); + glsafe(::glRotated(Geometry::rad2deg(angles.y()), 0.0, 1.0, 0.0)); + glsafe(::glRotated(Geometry::rad2deg(angles.x()), 1.0, 0.0, 0.0)); + glsafe(::glScaled(2.0 * half_size, 2.0 * half_size, 2.0 * half_size)); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_RAYCAST_PICKING + s_cube.model.render(); + + auto render_extension = [&view_matrix, &view_matrix_no_offset, shader](const Transform3d& matrix) { + const Transform3d view_model_matrix = view_matrix * matrix; + shader->set_uniform("view_model_matrix", view_model_matrix); + const Matrix3d view_normal_matrix = view_matrix_no_offset * matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); + shader->set_uniform("view_normal_matrix", view_normal_matrix); + s_cone.model.render(); + }; +#else + s_cube.render(); +#endif // ENABLE_RAYCAST_PICKING + +#if ENABLE_LEGACY_OPENGL_REMOVAL + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosX)) != 0) { +#if ENABLE_RAYCAST_PICKING + elements_matrices[1] = elements_matrices[0] * Geometry::assemble_transform(Vec3d::UnitX(), Vec3d(0.0, 0.5 * double(PI), 0.0)); + render_extension(elements_matrices[1]); +#else + shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(Vec3d::UnitX(), Vec3d(0.0, 0.5 * double(PI), 0.0))); + s_cone.render(); +#endif // ENABLE_RAYCAST_PICKING + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegX)) != 0) { +#if ENABLE_RAYCAST_PICKING + elements_matrices[2] = elements_matrices[0] * Geometry::assemble_transform(-Vec3d::UnitX(), Vec3d(0.0, -0.5 * double(PI), 0.0)); + render_extension(elements_matrices[2]); +#else + shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(-Vec3d::UnitX(), Vec3d(0.0, -0.5 * double(PI), 0.0))); + s_cone.render(); +#endif // ENABLE_RAYCAST_PICKING + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosY)) != 0) { +#if ENABLE_RAYCAST_PICKING + elements_matrices[3] = elements_matrices[0] * Geometry::assemble_transform(Vec3d::UnitY(), Vec3d(-0.5 * double(PI), 0.0, 0.0)); + render_extension(elements_matrices[3]); +#else + shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(Vec3d::UnitY(), Vec3d(-0.5 * double(PI), 0.0, 0.0))); + s_cone.render(); +#endif // ENABLE_RAYCAST_PICKING + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegY)) != 0) { +#if ENABLE_RAYCAST_PICKING + elements_matrices[4] = elements_matrices[0] * Geometry::assemble_transform(-Vec3d::UnitY(), Vec3d(0.5 * double(PI), 0.0, 0.0)); + render_extension(elements_matrices[4]); +#else + shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(-Vec3d::UnitY(), Vec3d(0.5 * double(PI), 0.0, 0.0))); + s_cone.render(); +#endif // ENABLE_RAYCAST_PICKING + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosZ)) != 0) { +#if ENABLE_RAYCAST_PICKING + elements_matrices[5] = elements_matrices[0] * Geometry::assemble_transform(Vec3d::UnitZ()); + render_extension(elements_matrices[5]); +#else + shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(Vec3d::UnitZ())); + s_cone.render(); +#endif // ENABLE_RAYCAST_PICKING + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegZ)) != 0) { +#if ENABLE_RAYCAST_PICKING + elements_matrices[6] = elements_matrices[0] * Geometry::assemble_transform(-Vec3d::UnitZ(), Vec3d(double(PI), 0.0, 0.0)); + render_extension(elements_matrices[6]); +#else + shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::assemble_transform(-Vec3d::UnitZ(), Vec3d(double(PI), 0.0, 0.0))); + s_cone.render(); +#endif // ENABLE_RAYCAST_PICKING + } +#else + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosX)) != 0) { + glsafe(::glPushMatrix()); + glsafe(::glTranslated(1.0, 0.0, 0.0)); + glsafe(::glRotated(0.5 * Geometry::rad2deg(double(PI)), 0.0, 1.0, 0.0)); + s_cone.render(); + glsafe(::glPopMatrix()); + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegX)) != 0) { + glsafe(::glPushMatrix()); + glsafe(::glTranslated(-1.0, 0.0, 0.0)); + glsafe(::glRotated(-0.5 * Geometry::rad2deg(double(PI)), 0.0, 1.0, 0.0)); + s_cone.render(); + glsafe(::glPopMatrix()); + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosY)) != 0) { + glsafe(::glPushMatrix()); + glsafe(::glTranslated(0.0, 1.0, 0.0)); + glsafe(::glRotated(-0.5 * Geometry::rad2deg(double(PI)), 1.0, 0.0, 0.0)); + s_cone.render(); + glsafe(::glPopMatrix()); + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegY)) != 0) { + glsafe(::glPushMatrix()); + glsafe(::glTranslated(0.0, -1.0, 0.0)); + glsafe(::glRotated(0.5 * Geometry::rad2deg(double(PI)), 1.0, 0.0, 0.0)); + s_cone.render(); + glsafe(::glPopMatrix()); + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosZ)) != 0) { + glsafe(::glPushMatrix()); + glsafe(::glTranslated(0.0, 0.0, 1.0)); + s_cone.render(); + glsafe(::glPopMatrix()); + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegZ)) != 0) { + glsafe(::glPushMatrix()); + glsafe(::glTranslated(0.0, 0.0, -1.0)); + glsafe(::glRotated(Geometry::rad2deg(double(PI)), 1.0, 0.0, 0.0)); + s_cone.render(); + glsafe(::glPopMatrix()); + } +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glPopMatrix()); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_RAYCAST_PICKING + if (raycasters[0] == nullptr) { + GLCanvas3D& canvas = *wxGetApp().plater()->canvas3D(); + raycasters[0] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cube.mesh_raycaster, elements_matrices[0]); + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosX)) != 0) + raycasters[1] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[1]); + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegX)) != 0) + raycasters[2] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[2]); + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosY)) != 0) + raycasters[3] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[3]); + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegY)) != 0) + raycasters[4] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[4]); + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosZ)) != 0) + raycasters[5] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[5]); + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegZ)) != 0) + raycasters[6] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[6]); + } + else { + for (size_t i = 0; i < GRABBER_ELEMENTS_MAX_COUNT; ++i) { + if (raycasters[i] != nullptr) + raycasters[i]->set_transform(elements_matrices[i]); + } + } +#endif // ENABLE_RAYCAST_PICKING +} + +GLGizmoBase::GLGizmoBase(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) + : m_parent(parent) + , m_icon_filename(icon_filename) + , m_sprite_id(sprite_id) + , m_imgui(wxGetApp().imgui()) +{ +} + +void GLGizmoBase::set_hover_id(int id) +{ + // do not change hover id during dragging + assert(!m_dragging); + + // allow empty grabbers when not using grabbers but use hover_id - flatten, rotate +// if (!m_grabbers.empty() && id >= (int) m_grabbers.size()) +// return; + + m_hover_id = id; + on_set_hover_id(); +} + +bool GLGizmoBase::update_items_state() +{ + bool res = m_dirty; + m_dirty = false; + return res; +} + +#if ENABLE_RAYCAST_PICKING +void GLGizmoBase::register_grabbers_for_picking() +{ + for (size_t i = 0; i < m_grabbers.size(); ++i) { + m_grabbers[i].register_raycasters_for_picking((m_group_id >= 0) ? m_group_id : i); + } +} + +void GLGizmoBase::unregister_grabbers_for_picking() +{ + for (size_t i = 0; i < m_grabbers.size(); ++i) { + m_grabbers[i].unregister_raycasters_for_picking(); + } +} +#else +ColorRGBA GLGizmoBase::picking_color_component(unsigned int id) const +{ + id = BASE_ID - id; + if (m_group_id > -1) + id -= m_group_id; + + return picking_decode(id); +} +#endif // ENABLE_RAYCAST_PICKING + +void GLGizmoBase::render_grabbers(const BoundingBoxf3& box) const +{ + render_grabbers((float)((box.size().x() + box.size().y() + box.size().z()) / 3.0)); +} + +void GLGizmoBase::render_grabbers(float size) const +{ + GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); + if (shader == nullptr) + return; + shader->start_using(); + shader->set_uniform("emission_factor", 0.1f); + for (int i = 0; i < (int)m_grabbers.size(); ++i) { + if (m_grabbers[i].enabled) + m_grabbers[i].render(m_hover_id == i, size); + } + shader->stop_using(); +} + +#if !ENABLE_RAYCAST_PICKING +void GLGizmoBase::render_grabbers_for_picking(const BoundingBoxf3& box) const +{ +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader != nullptr) { + shader->start_using(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + const float mean_size = float((box.size().x() + box.size().y() + box.size().z()) / 3.0); + + for (unsigned int i = 0; i < (unsigned int)m_grabbers.size(); ++i) { + if (m_grabbers[i].enabled) { + m_grabbers[i].color = picking_color_component(i); + m_grabbers[i].render_for_picking(mean_size); + } + } +#if ENABLE_LEGACY_OPENGL_REMOVAL + shader->stop_using(); + } +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} +#endif // !ENABLE_RAYCAST_PICKING + +// help function to process grabbers +// call start_dragging, stop_dragging, on_dragging +bool GLGizmoBase::use_grabbers(const wxMouseEvent &mouse_event) { + bool is_dragging_finished = false; + if (mouse_event.Moving()) { + // it should not happen but for sure + assert(!m_dragging); + if (m_dragging) is_dragging_finished = true; + else return false; + } + + if (mouse_event.LeftDown()) { + Selection &selection = m_parent.get_selection(); + if (!selection.is_empty() && m_hover_id != -1 /* && + (m_grabbers.empty() || m_hover_id < static_cast(m_grabbers.size()))*/) { + selection.setup_cache(); + + m_dragging = true; + for (auto &grabber : m_grabbers) grabber.dragging = false; +// if (!m_grabbers.empty() && m_hover_id < int(m_grabbers.size())) +// m_grabbers[m_hover_id].dragging = true; + + on_start_dragging(); + + // Let the plater know that the dragging started + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_STARTED)); + m_parent.set_as_dirty(); + return true; + } + } else if (m_dragging) { + // when mouse cursor leave window than finish actual dragging operation + bool is_leaving = mouse_event.Leaving(); + if (mouse_event.Dragging()) { + Point mouse_coord(mouse_event.GetX(), mouse_event.GetY()); + auto ray = m_parent.mouse_ray(mouse_coord); + UpdateData data(ray, mouse_coord); + + on_dragging(data); + + wxGetApp().obj_manipul()->set_dirty(); + m_parent.set_as_dirty(); + return true; + } + else if (mouse_event.LeftUp() || is_leaving || is_dragging_finished) { +#if ENABLE_WORLD_COORDINATE + do_stop_dragging(is_leaving); +#else + for (auto &grabber : m_grabbers) grabber.dragging = false; + m_dragging = false; + + // NOTE: This should be part of GLCanvas3D + // Reset hover_id when leave window + if (is_leaving) m_parent.mouse_up_cleanup(); + + on_stop_dragging(); + + // There is prediction that after draggign, data are changed + // Data are updated twice also by canvas3D::reload_scene. + // Should be fixed. + m_parent.get_gizmos_manager().update_data(); + + wxGetApp().obj_manipul()->set_dirty(); + + // Let the plater know that the dragging finished, so a delayed + // refresh of the scene with the background processing data should + // be performed. + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); + // updates camera target constraints + m_parent.refresh_camera_scene_box(); +#endif // ENABLE_WORLD_COORDINATE + return true; + } + } + return false; +} + +#if ENABLE_WORLD_COORDINATE +void GLGizmoBase::do_stop_dragging(bool perform_mouse_cleanup) +{ + for (auto& grabber : m_grabbers) grabber.dragging = false; + m_dragging = false; + + // NOTE: This should be part of GLCanvas3D + // Reset hover_id when leave window + if (perform_mouse_cleanup) m_parent.mouse_up_cleanup(); + + on_stop_dragging(); + + // There is prediction that after draggign, data are changed + // Data are updated twice also by canvas3D::reload_scene. + // Should be fixed. + m_parent.get_gizmos_manager().update_data(); + + wxGetApp().obj_manipul()->set_dirty(); + + // Let the plater know that the dragging finished, so a delayed + // refresh of the scene with the background processing data should + // be performed. + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); + // updates camera target constraints + m_parent.refresh_camera_scene_box(); +} +#endif // ENABLE_WORLD_COORDINATE + +std::string GLGizmoBase::format(float value, unsigned int decimals) const +{ + return Slic3r::string_printf("%.*f", decimals, value); +} + +void GLGizmoBase::set_dirty() { + m_dirty = true; +} + + + +void GLGizmoBase::render_input_window(float x, float y, float bottom_limit) +{ + on_render_input_window(x, y, bottom_limit); + if (m_first_input_window_render) { + // imgui windows that don't have an initial size needs to be processed once to get one + // and are not rendered in the first frame + // so, we forces to render another frame the first time the imgui window is shown + // https://github.com/ocornut/imgui/issues/2949 + m_parent.set_as_dirty(); + m_parent.request_extra_frame(); + m_first_input_window_render = false; + } +} + + + +std::string GLGizmoBase::get_name(bool include_shortcut) const +{ + int key = get_shortcut_key(); + std::string out = on_get_name(); + if (include_shortcut && key >= WXK_CONTROL_A && key <= WXK_CONTROL_Z) + out += std::string(" [") + char(int('A') + key - int(WXK_CONTROL_A)) + "]"; + return out; +} + + +} // namespace GUI +} // namespace Slic3r + diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp index 09766bbac..43624964a 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp @@ -210,7 +210,10 @@ public: #if ENABLE_RAYCAST_PICKING void register_raycasters_for_picking() { register_grabbers_for_picking(); on_register_raycasters_for_picking(); } void unregister_raycasters_for_picking() { unregister_grabbers_for_picking(); on_unregister_raycasters_for_picking(); } -#endif // ENABLE_RAYCAST_PICKING +#endif // ENABLE_RAYCAST_PICKING + + virtual bool is_in_editing_mode() const { return false; } + virtual bool is_selection_rectangle_dragging() const { return false; } protected: virtual bool on_init() = 0; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index 8767fecb2..818872bb4 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -4,465 +4,2299 @@ #include -#include -#include -#include -#include - #include #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/GUI_ObjectManipulation.hpp" +#include "slic3r/GUI/format.hpp" +#include "slic3r/Utils/UndoRedo.hpp" #include "libslic3r/AppConfig.hpp" -#include "libslic3r/Model.hpp" #include "libslic3r/TriangleMeshSlicer.hpp" +#include "imgui/imgui_internal.h" + namespace Slic3r { namespace GUI { -const double GLGizmoCut::Offset = 10.0; -const double GLGizmoCut::Margin = 20.0; -static const ColorRGBA GRABBER_COLOR = ColorRGBA::ORANGE(); +static const ColorRGBA GRABBER_COLOR = ColorRGBA::YELLOW(); -GLGizmoCut::GLGizmoCut(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) +// connector colors +static const ColorRGBA PLAG_COLOR = ColorRGBA::YELLOW(); +static const ColorRGBA DOWEL_COLOR = ColorRGBA::DARK_YELLOW(); +static const ColorRGBA HOVERED_PLAG_COLOR = ColorRGBA::CYAN(); +static const ColorRGBA HOVERED_DOWEL_COLOR = ColorRGBA(0.0f, 0.5f, 0.5f, 1.0f); +static const ColorRGBA SELECTED_PLAG_COLOR = ColorRGBA::GRAY(); +static const ColorRGBA SELECTED_DOWEL_COLOR = ColorRGBA::DARK_GRAY(); +static const ColorRGBA CONNECTOR_DEF_COLOR = ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f); +static const ColorRGBA CONNECTOR_ERR_COLOR = ColorRGBA(1.0f, 0.3f, 0.3f, 0.5f); + +const unsigned int AngleResolution = 64; +const unsigned int ScaleStepsCount = 72; +const float ScaleStepRad = 2.0f * float(PI) / ScaleStepsCount; +const unsigned int ScaleLongEvery = 2; +const float ScaleLongTooth = 0.1f; // in percent of radius +const unsigned int SnapRegionsCount = 8; + +const float UndefFloat = -999.f; +const std::string UndefLabel = " "; + +using namespace Geometry; + +// Generates mesh for a line +static GLModel::Geometry its_make_line(Vec3f beg_pos, Vec3f end_pos) +{ + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(2); + init_data.reserve_indices(2); + + // vertices + init_data.add_vertex(beg_pos); + init_data.add_vertex(end_pos); + + // indices + init_data.add_line(0, 1); + return init_data; +} + +// Generates mesh for a square plane +static GLModel::Geometry its_make_square_plane(float radius) +{ + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(4); + init_data.reserve_indices(6); + + // vertices + init_data.add_vertex(Vec3f(-radius, -radius, 0.0)); + init_data.add_vertex(Vec3f(radius , -radius, 0.0)); + init_data.add_vertex(Vec3f(radius , radius , 0.0)); + init_data.add_vertex(Vec3f(-radius, radius , 0.0)); + + // indices + init_data.add_triangle(0, 1, 2); + init_data.add_triangle(2, 3, 0); + return init_data; +} + +//! -- #ysFIXME those functions bodies are ported from GizmoRotation +// Generates mesh for a circle +static void init_from_circle(GLModel& model, double radius) +{ + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::LineLoop, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(ScaleStepsCount); + init_data.reserve_indices(ScaleStepsCount); + + // vertices + indices + for (unsigned int i = 0; i < ScaleStepsCount; ++i) { + const float angle = float(i * ScaleStepRad); + init_data.add_vertex(Vec3f(::cos(angle) * float(radius), ::sin(angle) * float(radius), 0.0f)); + init_data.add_index(i); + } + + model.init_from(std::move(init_data)); + model.set_color(ColorRGBA::WHITE()); +} + +// Generates mesh for a scale +static void init_from_scale(GLModel& model, double radius) +{ + const float out_radius_long = float(radius) * (1.0f + ScaleLongTooth); + const float out_radius_short = float(radius) * (1.0f + 0.5f * ScaleLongTooth); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(2 * ScaleStepsCount); + init_data.reserve_indices(2 * ScaleStepsCount); + + // vertices + indices + for (unsigned int i = 0; i < ScaleStepsCount; ++i) { + const float angle = float(i * ScaleStepRad); + const float cosa = ::cos(angle); + const float sina = ::sin(angle); + const float in_x = cosa * float(radius); + const float in_y = sina * float(radius); + const float out_x = (i % ScaleLongEvery == 0) ? cosa * out_radius_long : cosa * out_radius_short; + const float out_y = (i % ScaleLongEvery == 0) ? sina * out_radius_long : sina * out_radius_short; + + // vertices + init_data.add_vertex(Vec3f(in_x, in_y, 0.0f)); + init_data.add_vertex(Vec3f(out_x, out_y, 0.0f)); + + // indices + init_data.add_line(i * 2, i * 2 + 1); + } + + model.init_from(std::move(init_data)); + model.set_color(ColorRGBA::WHITE()); +} + +// Generates mesh for a snap_radii +static void init_from_snap_radii(GLModel& model, double radius) +{ + const float step = 2.0f * float(PI) / float(SnapRegionsCount); + const float in_radius = float(radius) / 3.0f; + const float out_radius = 2.0f * in_radius; + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(2 * ScaleStepsCount); + init_data.reserve_indices(2 * ScaleStepsCount); + + // vertices + indices + for (unsigned int i = 0; i < ScaleStepsCount; ++i) { + const float angle = float(i) * step; + const float cosa = ::cos(angle); + const float sina = ::sin(angle); + const float in_x = cosa * in_radius; + const float in_y = sina * in_radius; + const float out_x = cosa * out_radius; + const float out_y = sina * out_radius; + + // vertices + init_data.add_vertex(Vec3f(in_x, in_y, 0.0f)); + init_data.add_vertex(Vec3f(out_x, out_y, 0.0f)); + + // indices + init_data.add_line(i * 2, i * 2 + 1); + } + + model.init_from(std::move(init_data)); + model.set_color(ColorRGBA::WHITE()); +} + +// Generates mesh for a angle_arc +static void init_from_angle_arc(GLModel& model, double angle, double radius) +{ + model.reset(); + + const float step_angle = float(angle) / float(AngleResolution); + const float ex_radius = float(radius); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::LineStrip, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(1 + AngleResolution); + init_data.reserve_indices(1 + AngleResolution); + + // vertices + indices + for (unsigned int i = 0; i <= AngleResolution; ++i) { + const float angle = float(i) * step_angle; + init_data.add_vertex(Vec3f(::cos(angle) * ex_radius, ::sin(angle) * ex_radius, 0.0f)); + init_data.add_index(i); + } + + model.init_from(std::move(init_data)); +} + +//! -- + +GLGizmoCut3D::GLGizmoCut3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) : GLGizmoBase(parent, icon_filename, sprite_id) -{} - -std::string GLGizmoCut::get_tooltip() const + , m_connectors_group_id (3) + , m_connector_type (CutConnectorType::Plug) + , m_connector_style (size_t(CutConnectorStyle::Prizm)) + , m_connector_shape_id (size_t(CutConnectorShape::Circle)) { - double cut_z = m_cut_z; - if (wxGetApp().app_config->get("use_inches") == "1") - cut_z *= ObjectManipulation::mm_to_in; + m_modes = { _u8L("Planar")//, _u8L("Grid") +// , _u8L("Radial"), _u8L("Modular") + }; - return (m_hover_id == 0 || m_grabbers[0].dragging) ? "Z: " + format(cut_z, 2) : ""; + m_connector_modes = { _u8L("Auto"), _u8L("Manual") }; + m_connector_types = { _u8L("Plug"), _u8L("Dowel") }; + + m_connector_styles = { _u8L("Prizm"), _u8L("Frustum") +// , _u8L("Claw") + }; + + m_connector_shapes = { _u8L("Triangle"), _u8L("Square"), _u8L("Hexagon"), _u8L("Circle") +// , _u8L("D-shape") + }; + + m_axis_names = { "X", "Y", "Z" }; + + update_connector_shape(); } -bool GLGizmoCut::on_mouse(const wxMouseEvent &mouse_event) +std::string GLGizmoCut3D::get_tooltip() const { - return use_grabbers(mouse_event); + std::string tooltip; + if (m_hover_id == Z) { + double koef = m_imperial_units ? ObjectManipulation::mm_to_in : 1.0; + std::string unit_str = " " + (m_imperial_units ? _u8L("inch") : _u8L("mm")); + const BoundingBoxf3 tbb = transformed_bounding_box(m_plane_center); + if (tbb.max.z() >= 0.0) { + double top = (tbb.min.z() <= 0.0 ? tbb.max.z() : tbb.size().z()) * koef; + tooltip += format(top, 2) + " " + unit_str + " (" + _u8L("Top part") + ")"; + if (tbb.min.z() <= 0.0) + tooltip += "\n"; + } + if (tbb.min.z() <= 0.0) { + double bottom = (tbb.max.z() <= 0.0 ? tbb.size().z() : (tbb.min.z() * (-1))) * koef; + tooltip += format(bottom, 2) + " " + unit_str + " (" + _u8L("Bottom part") + ")"; + } + return tooltip; + } + if (tooltip.empty() && (m_hover_id == X || m_hover_id == Y)) { + std::string axis = m_hover_id == X ? "X" : "Y"; + return axis + ": " + format(float(rad2deg(m_angle)), 1) + _u8L("°"); + } + + return tooltip; } -bool GLGizmoCut::on_init() +bool GLGizmoCut3D::on_mouse(const wxMouseEvent &mouse_event) { - m_grabbers.emplace_back(); - m_shortcut_key = WXK_CONTROL_C; - return true; + Vec2i mouse_coord(mouse_event.GetX(), mouse_event.GetY()); + Vec2d mouse_pos = mouse_coord.cast(); + + if (mouse_event.ShiftDown() && mouse_event.LeftDown()) + return gizmo_event(SLAGizmoEventType::LeftDown, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), mouse_event.CmdDown()); + if (cut_line_processing()) { + if (mouse_event.ShiftDown()) { + if (mouse_event.Moving()|| mouse_event.Dragging()) + return gizmo_event(SLAGizmoEventType::Moving, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), mouse_event.CmdDown()); + if (mouse_event.LeftUp()) + return gizmo_event(SLAGizmoEventType::LeftUp, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), mouse_event.CmdDown()); + } + discard_cut_line_processing(); + } + else if (mouse_event.Moving()) + return false; + + if (use_grabbers(mouse_event)) { + if (m_hover_id >= m_connectors_group_id) { + if (mouse_event.LeftDown() && !mouse_event.CmdDown()&& !mouse_event.AltDown()) + unselect_all_connectors(); + if (mouse_event.LeftUp() && !mouse_event.ShiftDown()) + gizmo_event(SLAGizmoEventType::LeftUp, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), mouse_event.CmdDown()); + } + return true; + } + + static bool pending_right_up = false; + if (mouse_event.LeftDown()) { + bool grabber_contains_mouse = (get_hover_id() != -1); + const bool shift_down = mouse_event.ShiftDown(); + if ((!shift_down || grabber_contains_mouse) && + gizmo_event(SLAGizmoEventType::LeftDown, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), false)) + return true; + } + else if (mouse_event.Dragging()) { + bool control_down = mouse_event.CmdDown(); + if (m_parent.get_move_volume_id() != -1) { + // don't allow dragging objects with the Sla gizmo on + return true; + } + if (!control_down && + gizmo_event(SLAGizmoEventType::Dragging, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), false)) { + // the gizmo got the event and took some action, no need to do + // anything more here + m_parent.set_as_dirty(); + return true; + } + if (control_down && (mouse_event.LeftIsDown() || mouse_event.RightIsDown())) { + // CTRL has been pressed while already dragging -> stop current action + if (mouse_event.LeftIsDown()) + gizmo_event(SLAGizmoEventType::LeftUp, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), true); + else if (mouse_event.RightIsDown()) + pending_right_up = false; + } + } + else if (mouse_event.LeftUp() && !m_parent.is_mouse_dragging()) { + // in case SLA/FDM gizmo is selected, we just pass the LeftUp event + // and stop processing - neither object moving or selecting is + // suppressed in that case + gizmo_event(SLAGizmoEventType::LeftUp, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), mouse_event.CmdDown()); + return true; + } + else if (mouse_event.RightDown()) { + if (m_parent.get_selection().get_object_idx() != -1 && + gizmo_event(SLAGizmoEventType::RightDown, mouse_pos, false, false, false)) { + // we need to set the following right up as processed to avoid showing + // the context menu if the user release the mouse over the object + pending_right_up = true; + // event was taken care of by the SlaSupports gizmo + return true; + } + } + else if (pending_right_up && mouse_event.RightUp()) { + pending_right_up = false; + return true; + } + return false; } -std::string GLGizmoCut::on_get_name() const +void GLGizmoCut3D::shift_cut_z(double delta) { - return _u8L("Cut"); + Vec3d new_cut_center = m_plane_center; + new_cut_center[Z] += delta; + set_center(new_cut_center); } -void GLGizmoCut::on_set_state() +void GLGizmoCut3D::rotate_vec3d_around_plane_center(Vec3d&vec) { - // Reset m_cut_z on gizmo activation - if (m_state == On) - m_cut_z = bounding_box().center().z(); + vec = Transformation( assemble_transform(m_plane_center) * m_rotation_m * assemble_transform(-m_plane_center)).get_matrix() * vec; } -bool GLGizmoCut::on_is_activable() const +void GLGizmoCut3D::put_connectors_on_cut_plane(const Vec3d& cp_normal, double cp_offset) { - const Selection& selection = m_parent.get_selection(); - return selection.is_single_full_instance() && !selection.is_wipe_tower(); + ModelObject* mo = m_c->selection_info()->model_object(); + if (CutConnectors& connectors = mo->cut_connectors; !connectors.empty()) { + const float sla_shift = m_c->selection_info()->get_sla_shift(); + const Vec3d& instance_offset = mo->instances[m_c->selection_info()->get_active_instance()]->get_offset(); + + for (auto& connector : connectors) { + // convert connetor pos to the world coordinates + Vec3d pos = connector.pos + instance_offset; + pos[Z] += sla_shift; + // scalar distance from point to plane along the normal + double distance = -cp_normal.dot(pos) + cp_offset; + // move connector + connector.pos += distance * cp_normal; + } + } } -void GLGizmoCut::on_start_dragging() +void GLGizmoCut3D::update_clipper() { - if (m_hover_id == -1) + BoundingBoxf3 box = bounding_box(); + + Vec3d beg, end = beg = m_plane_center; + beg[Z] = box.center().z() - m_radius; + end[Z] = box.center().z() + m_radius; + + rotate_vec3d_around_plane_center(beg); + rotate_vec3d_around_plane_center(end); + + double dist = (m_plane_center - beg).norm(); + + // calculate normal and offset for clipping plane + Vec3d normal = end - beg; + if (normal == Vec3d::Zero()) + return; + dist = std::clamp(dist, 0.0001, normal.norm()); + normal.normalize(); + const double offset = normal.dot(beg) + dist; + + m_c->object_clipper()->set_range_and_pos(normal, offset, dist); + + put_connectors_on_cut_plane(normal, offset); + + if (m_raycasters.empty()) + on_register_raycasters_for_picking(); + else + update_raycasters_for_picking_transform(); +} + +void GLGizmoCut3D::update_clipper_on_render() +{ + update_clipper(); + force_update_clipper_on_render = false; +} + +void GLGizmoCut3D::set_center(const Vec3d& center) +{ + set_center_pos(center); + update_clipper(); +} + +bool GLGizmoCut3D::render_combo(const std::string& label, const std::vector& lines, size_t& selection_idx) +{ + ImGui::AlignTextToFramePadding(); + m_imgui->text(label); + ImGui::SameLine(m_label_width); + ImGui::PushItemWidth(m_control_width); + + size_t selection_out = selection_idx; + // It is necessary to use BeginGroup(). Otherwise, when using SameLine() is called, then other items will be drawn inside the combobox. + ImGui::BeginGroup(); + ImVec2 combo_pos = ImGui::GetCursorScreenPos(); + if (ImGui::BeginCombo(("##"+label).c_str(), "")) { + for (size_t line_idx = 0; line_idx < lines.size(); ++line_idx) { + ImGui::PushID(int(line_idx)); + if (ImGui::Selectable("", line_idx == selection_idx)) + selection_out = line_idx; + + ImGui::SameLine(); + ImGui::Text("%s", lines[line_idx].c_str()); + ImGui::PopID(); + } + + ImGui::EndCombo(); + } + + ImVec2 backup_pos = ImGui::GetCursorScreenPos(); + ImGuiStyle& style = ImGui::GetStyle(); + + ImGui::SetCursorScreenPos(ImVec2(combo_pos.x + style.FramePadding.x, combo_pos.y + style.FramePadding.y)); + ImGui::Text("%s", selection_out < lines.size() ? lines[selection_out].c_str() : UndefLabel.c_str()); + ImGui::SetCursorScreenPos(backup_pos); + ImGui::EndGroup(); + + bool is_changed = selection_idx != selection_out; + selection_idx = selection_out; + + if (is_changed) + update_connector_shape(); + + return is_changed; +} + +bool GLGizmoCut3D::render_double_input(const std::string& label, double& value_in) +{ + ImGui::AlignTextToFramePadding(); + m_imgui->text(label); + ImGui::SameLine(m_label_width); + ImGui::PushItemWidth(m_control_width); + + double value = value_in; + if (m_imperial_units) + value *= ObjectManipulation::mm_to_in; + double old_val = value; + ImGui::InputDouble(("##" + label).c_str(), &value, 0.0f, 0.0f, "%.2f", ImGuiInputTextFlags_CharsDecimal); + + ImGui::SameLine(); + m_imgui->text(m_imperial_units ? _L("in") : _L("mm")); + + value_in = value * (m_imperial_units ? ObjectManipulation::in_to_mm : 1.0); + return !is_approx(old_val, value); +} + +bool GLGizmoCut3D::render_slider_double_input(const std::string& label, float& value_in, float& tolerance_in) +{ + ImGui::AlignTextToFramePadding(); + m_imgui->text(label); + ImGui::SameLine(m_label_width); + ImGui::PushItemWidth(m_control_width * 0.85f); + + float value = value_in; + if (m_imperial_units) + value *= float(ObjectManipulation::mm_to_in); + float old_val = value; + + constexpr float UndefMinVal = -0.1f; + + const BoundingBoxf3 bbox = bounding_box(); + float mean_size = float((bbox.size().x() + bbox.size().y() + bbox.size().z()) / 9.0); + float min_size = value_in < 0.f ? UndefMinVal : 2.f; + if (m_imperial_units) { + mean_size *= float(ObjectManipulation::mm_to_in); + min_size *= float(ObjectManipulation::mm_to_in); + } + std::string format = value_in < 0.f ? UndefLabel : + m_imperial_units ? "%.4f " + _u8L("in") : "%.2f " + _u8L("mm"); + + m_imgui->slider_float(("##" + label).c_str(), &value, min_size, mean_size, format.c_str()); + value_in = value * float(m_imperial_units ? ObjectManipulation::in_to_mm : 1.0); + + ImGui::SameLine(m_label_width + m_control_width + 3); + ImGui::PushItemWidth(m_control_width * 0.3f); + + float old_tolerance, tolerance = old_tolerance = tolerance_in * 100.f; + std::string format_t = tolerance_in < 0.f ? UndefLabel : "%.f %%"; + float min_tolerance = tolerance_in < 0.f ? UndefMinVal : 0.f; + + m_imgui->slider_float(("##tolerance_" + label).c_str(), &tolerance, min_tolerance, 20.f, format_t.c_str(), 1.f, true, _L("Tolerance")); + tolerance_in = tolerance * 0.01f; + + return !is_approx(old_val, value) || !is_approx(old_tolerance, tolerance); +} + +void GLGizmoCut3D::render_move_center_input(int axis) +{ + m_imgui->text(m_axis_names[axis]+":"); + ImGui::SameLine(); + ImGui::PushItemWidth(0.3f*m_control_width); + + Vec3d move = m_plane_center; + double in_val, value = in_val = move[axis]; + if (m_imperial_units) + value *= ObjectManipulation::mm_to_in; + ImGui::InputDouble(("##move_" + m_axis_names[axis]).c_str(), &value, 0.0, 0.0, "%.2f", ImGuiInputTextFlags_CharsDecimal); + ImGui::SameLine(); + + double val = value * (m_imperial_units ? ObjectManipulation::in_to_mm : 1.0); + + if (in_val != val) { + move[axis] = val; + set_center(move); + } +} + +bool GLGizmoCut3D::render_connect_type_radio_button(CutConnectorType type) +{ + ImGui::SameLine(type == CutConnectorType::Plug ? m_label_width : 2*m_label_width); + ImGui::PushItemWidth(m_control_width); + if (m_imgui->radio_button(m_connector_types[size_t(type)], m_connector_type == type)) { + m_connector_type = type; + update_connector_shape(); + return true; + } + return false; +} + +void GLGizmoCut3D::render_connect_mode_radio_button(CutConnectorMode mode) +{ + ImGui::SameLine(mode == CutConnectorMode::Auto ? m_label_width : 2*m_label_width); + ImGui::PushItemWidth(m_control_width); + if (m_imgui->radio_button(m_connector_modes[int(mode)], m_connector_mode == mode)) + m_connector_mode = mode; +} + +bool GLGizmoCut3D::render_reset_button(const std::string& label_id, const std::string& tooltip) const +{ + const ImGuiStyle& style = ImGui::GetStyle(); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, { 1, style.ItemSpacing.y }); + + 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.4f, 0.4f, 0.4f, 1.0f }); + + std::string btn_label; + btn_label += ImGui::RevertButton; + const bool revert = ImGui::Button((btn_label +"##" + label_id).c_str()); + + ImGui::PopStyleColor(3); + + if (ImGui::IsItemHovered()) + m_imgui->tooltip(tooltip.c_str(), ImGui::GetFontSize() * 20.0f); + + ImGui::PopStyleVar(); + + return revert; +} + +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 Vec3d clip_to_ndc(const Vec4d& clip) { + return Vec3d(clip.x(), clip.y(), clip.z()) / clip.w(); +} +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 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 wxString get_label(Vec3d vec) +{ + wxString str = "x=" + double_to_string(vec.x(), 2) + + ", y=" + double_to_string(vec.y(), 2) + + ", z=" + double_to_string(vec.z(), 2); + return str; +} + +static wxString get_label(Vec2d vec) +{ + wxString str = "x=" + double_to_string(vec.x(), 2) + + ", y=" + double_to_string(vec.y(), 2); + return str; +} + +void GLGizmoCut3D::render_cut_plane() +{ + if (cut_line_processing()) return; - const BoundingBoxf3 box = bounding_box(); - m_max_z = box.max.z(); - m_start_z = m_cut_z; - m_drag_pos = m_grabbers[m_hover_id].center; - m_drag_center = box.center(); - m_drag_center.z() = m_cut_z; -} + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; -void GLGizmoCut::on_dragging(const UpdateData &data) -{ - assert(m_hover_id != -1); - set_cut_z(m_start_z + calc_projection(data.mouse_ray)); -} - -void GLGizmoCut::on_render() -{ - const BoundingBoxf3 box = bounding_box(); - Vec3d plane_center = box.center(); - plane_center.z() = m_cut_z; - m_max_z = box.max.z(); - set_cut_z(m_cut_z); - - update_contours(); - - const float min_x = box.min.x() - Margin; - const float max_x = box.max.x() + Margin; - const float min_y = box.min.y() - Margin; - const float max_y = box.max.y() + Margin; glsafe(::glEnable(GL_DEPTH_TEST)); glsafe(::glDisable(GL_CULL_FACE)); glsafe(::glEnable(GL_BLEND)); glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_GL_CORE_PROFILE - const Vec3d diff = plane_center - m_old_center; - // Z changed when move with cut plane - // X and Y changed when move with cutted object - bool is_changed = std::abs(diff.x()) > EPSILON || std::abs(diff.y()) > EPSILON || std::abs(diff.z()) > EPSILON; -#endif // ENABLE_GL_CORE_PROFILE + shader->start_using(); - GLShaderProgram* shader = wxGetApp().get_shader("flat"); - if (shader != nullptr) { - shader->start_using(); -#if !ENABLE_GL_CORE_PROFILE - const Vec3d diff = plane_center - m_old_center; - // Z changed when move with cut plane - // X and Y changed when move with cutted object - bool is_changed = std::abs(diff.x()) > EPSILON || - std::abs(diff.y()) > EPSILON || - std::abs(diff.z()) > EPSILON; -#endif // !ENABLE_GL_CORE_PROFILE - m_old_center = plane_center; + const Camera& camera = wxGetApp().plater()->get_camera(); + const Transform3d view_model_matrix = camera.get_view_matrix() * translation_transform(m_plane_center) * m_rotation_m; - if (!m_plane.is_initialized() || is_changed) { - m_plane.reset(); + shader->set_uniform("view_model_matrix", view_model_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; - init_data.color = { 0.8f, 0.8f, 0.8f, 0.5f }; - init_data.reserve_vertices(4); - init_data.reserve_indices(6); + if (can_perform_cut()) +// m_plane.set_color({ 0.8f, 0.8f, 0.8f, 0.5f }); + m_plane.set_color({ 0.9f, 0.9f, 0.9f, 0.5f }); + else + m_plane.set_color({ 1.0f, 0.8f, 0.8f, 0.5f }); + m_plane.render(); - // vertices - init_data.add_vertex(Vec3f(min_x, min_y, plane_center.z())); - init_data.add_vertex(Vec3f(max_x, min_y, plane_center.z())); - init_data.add_vertex(Vec3f(max_x, max_y, plane_center.z())); - init_data.add_vertex(Vec3f(min_x, max_y, plane_center.z())); + glsafe(::glEnable(GL_CULL_FACE)); + glsafe(::glDisable(GL_BLEND)); - // indices - init_data.add_triangle(0, 1, 2); - init_data.add_triangle(2, 3, 0); - - m_plane.init_from(std::move(init_data)); - } - - const Camera& camera = wxGetApp().plater()->get_camera(); - shader->set_uniform("view_model_matrix", camera.get_view_matrix()); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); - - m_plane.render(); -#else - // Draw the cutting plane - ::glBegin(GL_QUADS); - ::glColor4f(0.8f, 0.8f, 0.8f, 0.5f); - ::glVertex3f(min_x, min_y, plane_center.z()); - ::glVertex3f(max_x, min_y, plane_center.z()); - ::glVertex3f(max_x, max_y, plane_center.z()); - ::glVertex3f(min_x, max_y, plane_center.z()); - glsafe(::glEnd()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_GL_CORE_PROFILE - shader->stop_using(); - } - - shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); - if (shader != nullptr) { - shader->start_using(); - - const Camera& camera = wxGetApp().plater()->get_camera(); - shader->set_uniform("view_model_matrix", camera.get_view_matrix()); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); - const std::array& viewport = camera.get_viewport(); - shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); - shader->set_uniform("width", 0.5f); - shader->set_uniform("gap_size", 0.0f); -#endif // ENABLE_GL_CORE_PROFILE - - glsafe(::glEnable(GL_CULL_FACE)); - glsafe(::glDisable(GL_BLEND)); - - // Draw the grabber and the connecting line - m_grabbers[0].center = plane_center; - m_grabbers[0].center.z() = plane_center.z() + Offset; - - glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); - -#if ENABLE_GL_CORE_PROFILE - if (!OpenGLManager::get_gl_info().is_core_profile()) -#endif // ENABLE_GL_CORE_PROFILE - glsafe(::glLineWidth(m_hover_id != -1 ? 2.0f : 1.5f)); -#if ENABLE_LEGACY_OPENGL_REMOVAL - if (!m_grabber_connection.is_initialized() || is_changed) { - m_grabber_connection.reset(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; - init_data.color = ColorRGBA::YELLOW(); - init_data.reserve_vertices(2); - init_data.reserve_indices(2); - - // vertices - init_data.add_vertex((Vec3f)plane_center.cast()); - init_data.add_vertex((Vec3f)m_grabbers[0].center.cast()); - - // indices - init_data.add_line(0, 1); - - m_grabber_connection.init_from(std::move(init_data)); - } - - m_grabber_connection.render(); - -#if ENABLE_GL_CORE_PROFILE - shader->set_uniform("view_model_matrix", camera.get_view_matrix()* Geometry::assemble_transform(m_cut_contours.shift)); - m_cut_contours.contours.render(); -#endif // ENABLE_GL_CORE_PROFILE - - shader->stop_using(); - } - - shader = wxGetApp().get_shader("gouraud_light"); -#else - glsafe(::glColor3f(1.0, 1.0, 0.0)); - ::glBegin(GL_LINES); - ::glVertex3dv(plane_center.data()); - ::glVertex3dv(m_grabbers[0].center.data()); - glsafe(::glEnd()); - - GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - if (shader != nullptr) { - shader->start_using(); - shader->set_uniform("emission_factor", 0.1f); - - m_grabbers[0].color = GRABBER_COLOR; - m_grabbers[0].render(m_hover_id == 0, float((box.size().x() + box.size().y() + box.size().z()) / 3.0)); - - shader->stop_using(); - } - -#if !ENABLE_GL_CORE_PROFILE -#if ENABLE_LEGACY_OPENGL_REMOVAL - shader = wxGetApp().get_shader("flat"); - if (shader != nullptr) { - shader->start_using(); - - const Camera& camera = wxGetApp().plater()->get_camera(); - shader->set_uniform("view_model_matrix", camera.get_view_matrix()* Geometry::assemble_transform(m_cut_contours.shift)); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); -#else - glsafe(::glPushMatrix()); - glsafe(::glTranslated(m_cut_contours.shift.x(), m_cut_contours.shift.y(), m_cut_contours.shift.z())); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glLineWidth(2.0f)); - m_cut_contours.contours.render(); -#if ENABLE_LEGACY_OPENGL_REMOVAL - shader->stop_using(); - } -#else - glsafe(::glPopMatrix()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -#endif // !ENABLE_GL_CORE_PROFILE + shader->stop_using(); } -#if ENABLE_RAYCAST_PICKING -void GLGizmoCut::on_register_raycasters_for_picking() +static float get_grabber_mean_size(const BoundingBoxf3& bb) { + return float((bb.size().x() + bb.size().y() + bb.size().z()) / 3.0); +} + +void GLGizmoCut3D::render_model(GLModel& model, const ColorRGBA& color, Transform3d view_model_matrix) +{ + GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); + if (shader) { + shader->start_using(); + + shader->set_uniform("view_model_matrix", view_model_matrix); + shader->set_uniform("projection_matrix", wxGetApp().plater()->get_camera().get_projection_matrix()); + + model.set_color(color); + model.render(); + + shader->stop_using(); + } +} + +void GLGizmoCut3D::render_line(GLModel& line_model, const ColorRGBA& color, Transform3d view_model_matrix, float width) +{ + GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); + if (shader) { + shader->start_using(); + + shader->set_uniform("view_model_matrix", view_model_matrix); + shader->set_uniform("projection_matrix", wxGetApp().plater()->get_camera().get_projection_matrix()); + shader->set_uniform("width", width); + + line_model.set_color(color); + line_model.render(); + + shader->stop_using(); + } +} + +void GLGizmoCut3D::render_rotation_snapping(Axis axis, const ColorRGBA& color) +{ + GLShaderProgram* line_shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); + if (!line_shader) + return; + + const Camera& camera = wxGetApp().plater()->get_camera(); + Transform3d view_model_matrix = camera.get_view_matrix() * translation_transform(m_plane_center) * m_start_dragging_m; + + if (axis == X) + view_model_matrix = view_model_matrix * rotation_transform(0.5 * PI * Vec3d::UnitY()) * rotation_transform(-PI * Vec3d::UnitZ()); + else + view_model_matrix = view_model_matrix * rotation_transform(-0.5 * PI * Vec3d::UnitZ()) * rotation_transform(-0.5 * PI * Vec3d::UnitY()); + + line_shader->start_using(); + line_shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + line_shader->set_uniform("view_model_matrix", view_model_matrix); + line_shader->set_uniform("width", 0.25f); + + m_circle.render(); + m_scale.render(); + m_snap_radii.render(); + m_reference_radius.render(); + if (m_dragging) { + line_shader->set_uniform("width", 1.5f); + m_angle_arc.set_color(color); + m_angle_arc.render(); + } + + line_shader->stop_using(); +} + +void GLGizmoCut3D::render_grabber_connection(const ColorRGBA& color, Transform3d view_matrix) +{ + const Transform3d line_view_matrix = view_matrix * scale_transform(Vec3d(1.0, 1.0, m_grabber_connection_len)); + + render_line(m_grabber_connection, color, line_view_matrix, 0.2f); +}; + +void GLGizmoCut3D::render_cut_plane_grabbers() +{ + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); + + ColorRGBA color = m_hover_id == Z ? complementary(GRABBER_COLOR) : GRABBER_COLOR; + + const Transform3d view_matrix = wxGetApp().plater()->get_camera().get_view_matrix() * translation_transform(m_plane_center) * m_rotation_m; + + const Grabber& grabber = m_grabbers.front(); + const float mean_size = get_grabber_mean_size(bounding_box()); + + double size = m_dragging && m_hover_id == Z ? double(grabber.get_dragging_half_size(mean_size)) : double(grabber.get_half_size(mean_size)); + + Vec3d cone_scale = Vec3d(0.75 * size, 0.75 * size, 1.8 * size); + Vec3d offset = 1.25 * size * Vec3d::UnitZ(); + + // render Z grabber + + if (!m_dragging && m_hover_id < 0) + render_grabber_connection(color, view_matrix); + render_model(m_sphere.model, color, view_matrix * scale_transform(size)); + + if ((!m_dragging && m_hover_id < 0) || m_hover_id == Z) + { + const BoundingBoxf3 tbb = transformed_bounding_box(m_plane_center); + if (tbb.min.z() <= 0.0) + render_model(m_cone.model, color, view_matrix * assemble_transform(-offset, PI * Vec3d::UnitX(), cone_scale)); + + if (tbb.max.z() >= 0.0) + render_model(m_cone.model, color, view_matrix * assemble_transform(offset, Vec3d::Zero(), cone_scale)); + } + + // render top sphere for X/Y grabbers + + if ((!m_dragging && m_hover_id < 0) || m_hover_id == X || m_hover_id == Y) + { + size = m_dragging ? double(grabber.get_dragging_half_size(mean_size)) : double(grabber.get_half_size(mean_size)); + color = m_hover_id == Y ? complementary(ColorRGBA::GREEN()) : + m_hover_id == X ? complementary(ColorRGBA::RED()) : ColorRGBA::GRAY(); + render_model(m_sphere.model, color, view_matrix * assemble_transform(m_grabber_connection_len * Vec3d::UnitZ(), Vec3d::Zero(), size * Vec3d::Ones())); + } + + // render X grabber + + if ((!m_dragging && m_hover_id < 0) || m_hover_id == X) + { + size = m_dragging && m_hover_id == X ? double(grabber.get_dragging_half_size(mean_size)) : double(grabber.get_half_size(mean_size)); + cone_scale = Vec3d(0.75 * size, 0.75 * size, 1.8 * size); + color = m_hover_id == X ? complementary(ColorRGBA::RED()) : ColorRGBA::RED(); + + if (m_hover_id == X) { + render_grabber_connection(color, view_matrix); + render_rotation_snapping(X, color); + } + + offset = Vec3d(0.0, 1.25 * size, m_grabber_connection_len); + render_model(m_cone.model, color, view_matrix * assemble_transform(offset, -0.5 * PI * Vec3d::UnitX(), cone_scale)); + offset = Vec3d(0.0, -1.25 * size, m_grabber_connection_len); + render_model(m_cone.model, color, view_matrix * assemble_transform(offset, 0.5 * PI * Vec3d::UnitX(), cone_scale)); + } + + // render Y grabber + + if ((!m_dragging && m_hover_id < 0) || m_hover_id == Y) + { + size = m_dragging && m_hover_id == Y ? double(grabber.get_dragging_half_size(mean_size)) : double(grabber.get_half_size(mean_size)); + cone_scale = Vec3d(0.75 * size, 0.75 * size, 1.8 * size); + color = m_hover_id == Y ? complementary(ColorRGBA::GREEN()) : ColorRGBA::GREEN(); + + if (m_hover_id == Y) { + render_grabber_connection(color, view_matrix); + render_rotation_snapping(Y, color); + } + + offset = Vec3d(1.25 * size, 0.0, m_grabber_connection_len); + render_model(m_cone.model, color, view_matrix * assemble_transform(offset, 0.5 * PI * Vec3d::UnitY(), cone_scale)); + offset = Vec3d(-1.25 * size, 0.0, m_grabber_connection_len); + render_model(m_cone.model, color, view_matrix * assemble_transform(offset, -0.5 * PI * Vec3d::UnitY(), cone_scale)); + } +} + +void GLGizmoCut3D::render_cut_line() +{ + if (!cut_line_processing() || m_line_end == Vec3d::Zero()) + return; + + glsafe(::glEnable(GL_DEPTH_TEST)); + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); + + m_cut_line.reset(); + m_cut_line.init_from(its_make_line((Vec3f)m_line_beg.cast(), (Vec3f)m_line_end.cast())); + + render_line(m_cut_line, GRABBER_COLOR, wxGetApp().plater()->get_camera().get_view_matrix(), 0.25f); +} + +bool GLGizmoCut3D::on_init() +{ + m_grabbers.emplace_back(); + m_shortcut_key = WXK_CONTROL_C; + + // initiate info shortcuts + const wxString ctrl = GUI::shortkey_ctrl_prefix(); + const wxString alt = GUI::shortkey_alt_prefix(); + const wxString shift = "Shift+"; + + m_shortcuts.push_back(std::make_pair(_L("Left click"), _L("Add connector"))); + m_shortcuts.push_back(std::make_pair(_L("Right click"), _L("Remove connector"))); + m_shortcuts.push_back(std::make_pair(_L("Drag"), _L("Move connector"))); + m_shortcuts.push_back(std::make_pair(shift + _L("Left click"), _L("Add connector to selection"))); + m_shortcuts.push_back(std::make_pair(alt + _L("Left click"), _L("Remove connector from selection"))); + m_shortcuts.push_back(std::make_pair(ctrl + "A", _L("Select all connectors"))); + + return true; +} + +void GLGizmoCut3D::on_load(cereal::BinaryInputArchive& ar) +{ + ar( m_keep_upper, m_keep_lower, m_rotate_lower, m_rotate_upper, m_hide_cut_plane, m_mode, m_connectors_editing,//m_selected, + // m_connector_depth_ratio, m_connector_size, m_connector_mode, m_connector_type, m_connector_style, m_connector_shape_id, + m_ar_plane_center, m_rotation_m); + + set_center_pos(m_ar_plane_center, true); + + force_update_clipper_on_render = true; + + m_parent.request_extra_frame(); +} + +void GLGizmoCut3D::on_save(cereal::BinaryOutputArchive& ar) const +{ + ar( m_keep_upper, m_keep_lower, m_rotate_lower, m_rotate_upper, m_hide_cut_plane, m_mode, m_connectors_editing,//m_selected, + // m_connector_depth_ratio, m_connector_size, m_connector_mode, m_connector_type, m_connector_style, m_connector_shape_id, + m_ar_plane_center, m_start_dragging_m); +} + +std::string GLGizmoCut3D::on_get_name() const +{ + return _u8L("Cut"); +} + +void GLGizmoCut3D::on_set_state() +{ + if (m_state == On) { + update_bb(); + m_connectors_editing = !m_selected.empty(); + + // initiate archived values + m_ar_plane_center = m_plane_center; + m_start_dragging_m = m_rotation_m; + + m_parent.request_extra_frame(); + } + else { + 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; +} + +void GLGizmoCut3D::on_register_raycasters_for_picking() +{ + assert(m_raycasters.empty()); // the gizmo grabbers are rendered on top of the scene, so the raytraced picker should take it into account m_parent.set_raycaster_gizmos_on_top(true); + + init_picking_models(); + + if (m_connectors_editing) { + if (CommonGizmosDataObjects::SelectionInfo* si = m_c->selection_info()) { + const CutConnectors& connectors = si->model_object()->cut_connectors; + for (int i = 0; i < int(connectors.size()); ++i) + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, i + m_connectors_group_id, *(m_shapes[connectors[i].attribs]).mesh_raycaster, Transform3d::Identity())); + } + } + else { + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, X, *m_cone.mesh_raycaster, Transform3d::Identity())); + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, X, *m_cone.mesh_raycaster, Transform3d::Identity())); + + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, Y, *m_cone.mesh_raycaster, Transform3d::Identity())); + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, Y, *m_cone.mesh_raycaster, Transform3d::Identity())); + + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, Z, *m_sphere.mesh_raycaster, Transform3d::Identity())); + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, Z, *m_cone.mesh_raycaster, Transform3d::Identity())); + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, Z, *m_cone.mesh_raycaster, Transform3d::Identity())); + } + + update_raycasters_for_picking_transform(); } -void GLGizmoCut::on_unregister_raycasters_for_picking() +void GLGizmoCut3D::on_unregister_raycasters_for_picking() { + m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo); + m_raycasters.clear(); + // the gizmo grabbers are rendered on top of the scene, so the raytraced picker should take it into account m_parent.set_raycaster_gizmos_on_top(false); } -#else -void GLGizmoCut::on_render_for_picking() + +void GLGizmoCut3D::update_raycasters_for_picking() { - glsafe(::glDisable(GL_DEPTH_TEST)); - render_grabbers_for_picking(m_parent.get_selection().get_bounding_box()); + on_unregister_raycasters_for_picking(); + on_register_raycasters_for_picking(); } -#endif // ENABLE_RAYCAST_PICKING -void GLGizmoCut::on_render_input_window(float x, float y, float bottom_limit) +void GLGizmoCut3D::set_volumes_picking_state(bool state) { - static float last_y = 0.0f; - static float last_h = 0.0f; - - m_imgui->begin(_L("Cut"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); - - const bool imperial_units = wxGetApp().app_config->get("use_inches") == "1"; - - // 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; + std::vector>* raycasters = m_parent.get_raycasters_for_picking(SceneRaycaster::EType::Volume); + if (raycasters != nullptr) { + const Selection& selection = m_parent.get_selection(); + const Selection::IndicesList ids = selection.get_volume_idxs(); + for (unsigned int id : ids) { + const GLVolume* v = selection.get_volume(id); + auto it = std::find_if(raycasters->begin(), raycasters->end(), [v](std::shared_ptr item) { return item->get_raycaster() == v->mesh_raycaster.get(); }); + if (it != raycasters->end()) + (*it)->set_active(state); + } } - - ImGui::AlignTextToFramePadding(); - m_imgui->text("Z"); - ImGui::SameLine(); - ImGui::PushItemWidth(m_imgui->get_style_scaling() * 150.0f); - - double cut_z = m_cut_z; - if (imperial_units) - cut_z *= ObjectManipulation::mm_to_in; - ImGui::InputDouble("", &cut_z, 0.0f, 0.0f, "%.2f", ImGuiInputTextFlags_CharsDecimal); - - ImGui::SameLine(); - m_imgui->text(imperial_units ? _L("in") : _L("mm")); - - m_cut_z = cut_z * (imperial_units ? ObjectManipulation::in_to_mm : 1.0); - - ImGui::Separator(); - - m_imgui->checkbox(_L("Keep upper part"), m_keep_upper); - m_imgui->checkbox(_L("Keep lower part"), m_keep_lower); - m_imgui->checkbox(_L("Rotate lower part upwards"), m_rotate_lower); - - ImGui::Separator(); - - m_imgui->disabled_begin((!m_keep_upper && !m_keep_lower) || m_cut_z <= 0.0 || m_max_z <= m_cut_z); - const bool cut_clicked = m_imgui->button(_L("Perform cut")); - m_imgui->disabled_end(); - - m_imgui->end(); - - if (cut_clicked && (m_keep_upper || m_keep_lower)) - perform_cut(m_parent.get_selection()); } -void GLGizmoCut::set_cut_z(double cut_z) +void GLGizmoCut3D::update_raycasters_for_picking_transform() { - // Clamp the plane to the object's bounding box - m_cut_z = std::clamp(cut_z, 0.0, m_max_z); -} + if (m_connectors_editing) { + CommonGizmosDataObjects::SelectionInfo* si = m_c->selection_info(); + if (!si) + return; + const ModelObject* mo = si->model_object(); + const CutConnectors& connectors = mo->cut_connectors; + if (connectors.empty()) + return; + auto inst_id = m_c->selection_info()->get_active_instance(); + if (inst_id < 0) + return; -void GLGizmoCut::perform_cut(const Selection& selection) -{ - const int instance_idx = selection.get_instance_idx(); - const int object_idx = selection.get_object_idx(); + const Vec3d& instance_offset = mo->instances[inst_id]->get_offset(); + const double sla_shift = double(m_c->selection_info()->get_sla_shift()); - wxCHECK_RET(instance_idx >= 0 && object_idx >= 0, "GLGizmoCut: Invalid object selection"); + const ClippingPlane* cp = m_c->object_clipper()->get_clipping_plane(); + const Vec3d& normal = cp && cp->is_active() ? cp->get_normal() : m_clp_normal; - // m_cut_z is the distance from the bed. Subtract possible SLA elevation. - const GLVolume* first_glvolume = selection.get_first_volume(); - const double object_cut_z = m_cut_z - first_glvolume->get_sla_shift_z(); + for (size_t i = 0; i < connectors.size(); ++i) { + const CutConnector& connector = connectors[i]; - if (0.0 < object_cut_z && object_cut_z < m_max_z) - wxGetApp().plater()->cut(object_idx, instance_idx, object_cut_z, - only_if(m_keep_upper, ModelObjectCutAttribute::KeepUpper) | - only_if(m_keep_lower, ModelObjectCutAttribute::KeepLower) | - only_if(m_rotate_lower, ModelObjectCutAttribute::FlipLower)); + float height = connector.height; + // recalculate connector position to world position + Vec3d pos = connector.pos + instance_offset; + if (connector.attribs.type == CutConnectorType::Dowel && + connector.attribs.style == CutConnectorStyle::Prizm) { + pos -= height * normal; + height *= 2; + } + pos[Z] += sla_shift; + + const Transform3d scale_trafo = scale_transform(Vec3f(connector.radius, connector.radius, height).cast()); + m_raycasters[i]->set_transform(translation_transform(pos) * m_rotation_m * scale_trafo); + } + } else { - // the object is SLA-elevated and the plane is under it. + const Transform3d trafo = translation_transform(m_plane_center) * m_rotation_m; + + const BoundingBoxf3 box = bounding_box(); + const float mean_size = get_grabber_mean_size(box); + + double size = double(m_grabbers.front().get_half_size(mean_size)); + Vec3d scale = Vec3d(0.75 * size, 0.75 * size, 1.8 * size); + + Vec3d offset = Vec3d(0.0, 1.25 * size, m_grabber_connection_len); + m_raycasters[0]->set_transform(trafo * assemble_transform(offset, -0.5 * PI * Vec3d::UnitX(), scale)); + offset = Vec3d(0.0, -1.25 * size, m_grabber_connection_len); + m_raycasters[1]->set_transform(trafo * assemble_transform(offset, 0.5 * PI * Vec3d::UnitX(), scale)); + + offset = Vec3d(1.25 * size, 0.0, m_grabber_connection_len); + m_raycasters[2]->set_transform(trafo * assemble_transform(offset, 0.5 * PI * Vec3d::UnitY(), scale)); + offset = Vec3d(-1.25 * size, 0.0, m_grabber_connection_len); + m_raycasters[3]->set_transform(trafo * assemble_transform(offset, -0.5 * PI * Vec3d::UnitY(), scale)); + + offset = 1.25 * size * Vec3d::UnitZ(); + m_raycasters[4]->set_transform(trafo * scale_transform(size)); + m_raycasters[5]->set_transform(trafo * assemble_transform(-offset, PI * Vec3d::UnitX(), scale)); + m_raycasters[6]->set_transform(trafo * assemble_transform(offset, Vec3d::Zero(), scale)); } } -double GLGizmoCut::calc_projection(const Linef3& mouse_ray) const +void GLGizmoCut3D::on_set_hover_id() { - double projection = 0.0; +} - const Vec3d starting_vec = m_drag_pos - m_drag_center; - const double len_starting_vec = starting_vec.norm(); - if (len_starting_vec != 0.0) { - const Vec3d mouse_dir = mouse_ray.unit_vector(); +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 || selection.is_wipe_tower()) + return false; + + bool is_dowel_object = false; + if (const ModelObject* mo = wxGetApp().plater()->model().objects[object_idx]; mo->is_cut()) { + int solid_connector_cnt = 0; + int connectors_cnt = 0; + for (const ModelVolume* volume : mo->volumes) { + if (volume->is_cut_connector()) { + connectors_cnt++; + if (volume->is_model_part()) + solid_connector_cnt++; + } + if (connectors_cnt > 1) + break; + } + is_dowel_object = connectors_cnt == 1 && solid_connector_cnt == 1; + } + + // This is assumed in GLCanvas3D::do_rotate, do not change this + // without updating that function too. + return selection.is_single_full_instance() && !is_dowel_object && !m_parent.is_layers_editing_enabled(); +} + +bool GLGizmoCut3D::on_is_selectable() const +{ + return wxGetApp().get_mode() != comSimple; +} + +Vec3d GLGizmoCut3D::mouse_position_in_local_plane(Axis axis, const Linef3& mouse_ray) const +{ + double half_pi = 0.5 * PI; + + Transform3d m = Transform3d::Identity(); + + switch (axis) + { + case X: + { + m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitZ())); + m.rotate(Eigen::AngleAxisd(-half_pi, Vec3d::UnitY())); + break; + } + case Y: + { + m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitY())); + m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitZ())); + break; + } + default: + case Z: + { + // no rotation applied + break; + } + } + + m = m * m_start_dragging_m.inverse(); + m.translate(-m_plane_center); + + return transform(mouse_ray, m).intersect_plane(0.0); +} + +void GLGizmoCut3D::dragging_grabber_z(const GLGizmoBase::UpdateData &data) +{ + Vec3d starting_box_center = m_plane_center - Vec3d::UnitZ(); // some Margin + rotate_vec3d_around_plane_center(starting_box_center); + + const Vec3d&starting_drag_position = m_plane_center; + double projection = 0.0; + + Vec3d starting_vec = starting_drag_position - starting_box_center; + if (starting_vec.norm() != 0.0) { + Vec3d mouse_dir = data.mouse_ray.unit_vector(); // finds the intersection of the mouse ray with the plane parallel to the camera viewport and passing throught the starting position // use ray-plane intersection see i.e. https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection algebric form // in our case plane normal and ray direction are the same (orthogonal view) // when moving to perspective camera the negative z unit axis of the camera needs to be transformed in world space and used as plane normal - const Vec3d inters = mouse_ray.a + (m_drag_pos - mouse_ray.a).dot(mouse_dir) / mouse_dir.squaredNorm() * mouse_dir; + Vec3d inters = data.mouse_ray.a + (starting_drag_position - data.mouse_ray.a).dot(mouse_dir) / mouse_dir.squaredNorm() * mouse_dir; // vector from the starting position to the found intersection - const Vec3d inters_vec = inters - m_drag_pos; + Vec3d inters_vec = inters - starting_drag_position; + starting_vec.normalize(); // finds projection of the vector along the staring direction - projection = inters_vec.dot(starting_vec.normalized()); + projection = inters_vec.dot(starting_vec); } - return projection; + if (wxGetKeyState(WXK_SHIFT)) + projection = m_snap_step * (double)std::round(projection / m_snap_step); + + const Vec3d shift = starting_vec * projection; + + // move cut plane center + set_center(m_plane_center + shift); } -BoundingBoxf3 GLGizmoCut::bounding_box() const +void GLGizmoCut3D::dragging_grabber_xy(const GLGizmoBase::UpdateData &data) +{ + const Vec2d mouse_pos = to_2d(mouse_position_in_local_plane((Axis)m_hover_id, data.mouse_ray)); + + const Vec2d orig_dir = Vec2d::UnitX(); + const Vec2d new_dir = mouse_pos.normalized(); + + const double two_pi = 2.0 * PI; + + double theta = ::acos(std::clamp(new_dir.dot(orig_dir), -1.0, 1.0)); + if (cross2(orig_dir, new_dir) < 0.0) + theta = two_pi - theta; + + const double len = mouse_pos.norm(); + // snap to coarse snap region + if (m_snap_coarse_in_radius <= len && len <= m_snap_coarse_out_radius) { + const double step = two_pi / double(SnapRegionsCount); + theta = step * std::round(theta / step); + } + // snap to fine snap region (scale) + else if (m_snap_fine_in_radius <= len && len <= m_snap_fine_out_radius) { + const double step = two_pi / double(ScaleStepsCount); + theta = step * std::round(theta / step); + } + + if (is_approx(theta, two_pi)) + theta = 0.0; + if (m_hover_id == X) + theta += 0.5 * PI; + + Vec3d rotation = Vec3d::Zero(); + rotation[m_hover_id] = theta; + m_rotation_m = m_start_dragging_m * rotation_transform(rotation); + + m_angle = theta; + while (m_angle > two_pi) + m_angle -= two_pi; + if (m_angle < 0.0) + m_angle += two_pi; + + update_clipper(); +} + +void GLGizmoCut3D::dragging_connector(const GLGizmoBase::UpdateData &data) +{ + CutConnectors& connectors = m_c->selection_info()->model_object()->cut_connectors; + std::pair pos_and_normal; + Vec3d pos_world; + + if (unproject_on_cut_plane(data.mouse_pos.cast(), pos_and_normal, pos_world)) { + connectors[m_hover_id - m_connectors_group_id].pos = pos_and_normal.first; + update_raycasters_for_picking_transform(); + } +} + +void GLGizmoCut3D::on_dragging(const UpdateData& data) +{ + if (m_hover_id < 0) + return; + if (m_hover_id == Z) + dragging_grabber_z(data); + else if (m_hover_id == X || m_hover_id == Y) + dragging_grabber_xy(data); + else if (m_hover_id >= m_connectors_group_id && m_connector_mode == CutConnectorMode::Manual) + dragging_connector(data); +} + +void GLGizmoCut3D::on_start_dragging() +{ + m_angle = 0.0; + if (m_hover_id >= m_connectors_group_id && m_connector_mode == CutConnectorMode::Manual) + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Move connector"), UndoRedo::SnapshotType::GizmoAction); + + if (m_hover_id == X || m_hover_id == Y) + m_start_dragging_m = m_rotation_m; +} + +void GLGizmoCut3D::on_stop_dragging() +{ + if (m_hover_id == X || m_hover_id == Y) { + m_angle_arc.reset(); + m_angle = 0.0; + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Rotate cut plane"), UndoRedo::SnapshotType::GizmoAction); + m_start_dragging_m = m_rotation_m; + } + else if (m_hover_id == Z) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Move cut plane"), UndoRedo::SnapshotType::GizmoAction); + m_ar_plane_center = m_plane_center; + } +} + +void GLGizmoCut3D::set_center_pos(const Vec3d& center_pos, bool force/* = false*/) +{ + bool can_set_center_pos = force; + if (!can_set_center_pos) { + const BoundingBoxf3 tbb = transformed_bounding_box(center_pos); + if (tbb.max.z() > -1. && tbb.min.z() < 1.) + can_set_center_pos = true; + else { + const double old_dist = (m_bb_center - m_plane_center).norm(); + const double new_dist = (m_bb_center - center_pos).norm(); + // check if forcing is reasonable + if (new_dist < old_dist) + can_set_center_pos = true; + } + } + + if (can_set_center_pos) { + m_plane_center = center_pos; + m_center_offset = m_plane_center - m_bb_center; + } +} + +BoundingBoxf3 GLGizmoCut3D::bounding_box() const { BoundingBoxf3 ret; const Selection& selection = m_parent.get_selection(); const Selection::IndicesList& idxs = selection.get_volume_idxs(); - return selection.get_bounding_box(); - for (unsigned int i : idxs) { const GLVolume* volume = selection.get_volume(i); - if (!volume->is_modifier) + // respect just to the solid parts for FFF and ignore pad and supports for SLA + if (!volume->is_modifier && !volume->is_sla_pad() && !volume->is_sla_support()) ret.merge(volume->transformed_convex_hull_bounding_box()); } return ret; } -void GLGizmoCut::update_contours() +BoundingBoxf3 GLGizmoCut3D::transformed_bounding_box(const Vec3d& plane_center, bool revert_move /*= false*/) const { - const Selection& selection = m_parent.get_selection(); - const GLVolume* first_glvolume = selection.get_first_volume(); - const BoundingBoxf3& box = first_glvolume->transformed_convex_hull_bounding_box(); + // #ysFIXME !!! + BoundingBoxf3 ret; - const ModelObject* model_object = wxGetApp().model().objects[selection.get_object_idx()]; - const int instance_idx = selection.get_instance_idx(); - std::vector volumes_idxs = std::vector(model_object->volumes.size()); - std::vector volumes_trafos = std::vector(model_object->volumes.size()); - for (size_t i = 0; i < model_object->volumes.size(); ++i) { - volumes_idxs[i] = model_object->volumes[i]->id(); - volumes_trafos[i] = model_object->volumes[i]->get_matrix(); + const CommonGizmosDataObjects::SelectionInfo* sel_info = m_c->selection_info(); + if (!sel_info) + return ret; + const ModelObject* mo = sel_info->model_object(); + if (!mo) + return ret; + const int instance_idx = sel_info->get_active_instance(); + if (instance_idx < 0 || mo->instances.empty()) + return ret; + const ModelInstance* mi = mo->instances[instance_idx]; + + const Vec3d& instance_offset = mi->get_offset(); + Vec3d cut_center_offset = plane_center - instance_offset; + cut_center_offset[Z] -= sel_info->get_sla_shift(); + + const auto move = assemble_transform(-cut_center_offset); + const auto move2 = assemble_transform(plane_center); + + const auto cut_matrix = (revert_move ? move2 : Transform3d::Identity()) * m_rotation_m.inverse() * move; + + const Selection& selection = m_parent.get_selection(); + const Selection::IndicesList& idxs = selection.get_volume_idxs(); + for (unsigned int i : idxs) { + const GLVolume* volume = selection.get_volume(i); + // respect just to the solid parts for FFF and ignore pad and supports for SLA + if (!volume->is_modifier && !volume->is_sla_pad() && !volume->is_sla_support()) { + + const auto instance_matrix = assemble_transform( + Vec3d::Zero(), // don't apply offset + volume->get_instance_rotation().cwiseProduct(Vec3d(1.0, 1.0, 1.0)), + volume->get_instance_scaling_factor(), + volume->get_instance_mirror() + ); + + auto volume_trafo = instance_matrix * volume->get_volume_transformation().get_matrix(); + + ret.merge(volume->transformed_convex_hull_bounding_box(cut_matrix * volume_trafo)); + } + } + return ret; +} + +bool GLGizmoCut3D::update_bb() +{ + const BoundingBoxf3 box = bounding_box(); + if (m_max_pos != box.max || m_min_pos != box.min) { + + invalidate_cut_plane(); + + m_max_pos = box.max; + m_min_pos = box.min; + m_bb_center = box.center(); + if (box.contains(m_center_offset)) + set_center_pos(m_bb_center + m_center_offset, true); + else + set_center_pos(m_bb_center, true); + + m_radius = box.radius(); + m_grabber_connection_len = 0.75 * m_radius;// std::min(0.75 * m_radius, 35.0); + m_grabber_radius = m_grabber_connection_len * 0.85; + + m_snap_coarse_in_radius = m_grabber_radius / 3.0; + m_snap_coarse_out_radius = m_snap_coarse_in_radius * 2.; + m_snap_fine_in_radius = m_grabber_connection_len * 0.85; + m_snap_fine_out_radius = m_grabber_connection_len * 1.15; + + m_plane.reset(); + m_cone.reset(); + m_sphere.reset(); + m_grabber_connection.reset(); + m_circle.reset(); + m_scale.reset(); + m_snap_radii.reset(); + m_reference_radius.reset(); + + on_unregister_raycasters_for_picking(); + + clear_selection(); + if (CommonGizmosDataObjects::SelectionInfo* selection = m_c->selection_info()) + m_selected.resize(selection->model_object()->cut_connectors.size(), false); + + return true; + } + return false; +} + +void GLGizmoCut3D::init_picking_models() +{ + if (!m_cone.model.is_initialized()) { + indexed_triangle_set its = its_make_cone(1.0, 1.0, PI / 12.0); + m_cone.model.init_from(its); + m_cone.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); + } + if (!m_sphere.model.is_initialized()) { + indexed_triangle_set its = its_make_sphere(1.0, PI / 12.0); + m_sphere.model.init_from(its); + m_sphere.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); + } + if (m_shapes.empty()) + init_connector_shapes(); +} + +void GLGizmoCut3D::init_rendering_items() +{ + if (!m_grabber_connection.is_initialized()) + m_grabber_connection.init_from(its_make_line(Vec3f::Zero(), Vec3f::UnitZ())); + if (!m_circle.is_initialized()) + init_from_circle(m_circle, m_grabber_radius); + if (!m_scale.is_initialized()) + init_from_scale(m_scale, m_grabber_radius); + if (!m_snap_radii.is_initialized()) + init_from_snap_radii(m_snap_radii, m_grabber_radius); + if (!m_reference_radius.is_initialized()) { + m_reference_radius.init_from(its_make_line(Vec3f::Zero(), m_grabber_connection_len * Vec3f::UnitX())); + m_reference_radius.set_color(ColorRGBA::WHITE()); + } + if (!m_angle_arc.is_initialized() || m_angle != 0.0) + init_from_angle_arc(m_angle_arc, m_angle, m_grabber_connection_len); + + if (!m_plane.is_initialized() && !m_hide_cut_plane && !m_connectors_editing) { + if (m_cut_plane_as_circle) + m_plane.init_from(its_make_frustum_dowel(2. * m_radius, 0.3, 180)); + else + m_plane.init_from(its_make_square_plane(float(m_radius))); + } +} + +void GLGizmoCut3D::render_clipper_cut() +{ + if (! m_connectors_editing) + ::glDisable(GL_DEPTH_TEST); + m_c->object_clipper()->render_cut(); + if (! m_connectors_editing) + ::glEnable(GL_DEPTH_TEST); +} + +void GLGizmoCut3D::on_render() +{ + if (update_bb() || force_update_clipper_on_render) { + update_clipper_on_render(); + m_c->object_clipper()->set_behavior(m_connectors_editing, m_connectors_editing, 0.4); } - bool trafos_match = std::equal(volumes_trafos.begin(), volumes_trafos.end(), - m_cut_contours.volumes_trafos.begin(), m_cut_contours.volumes_trafos.end(), - [](const Transform3d& a, const Transform3d& b) { return a.isApprox(b); }); + init_picking_models(); - if (0.0 < m_cut_z && m_cut_z < m_max_z) { - if (m_cut_contours.cut_z != m_cut_z || m_cut_contours.object_id != model_object->id() || - m_cut_contours.instance_idx != instance_idx || m_cut_contours.volumes_idxs != volumes_idxs || - !trafos_match) { - m_cut_contours.cut_z = m_cut_z; + init_rendering_items(); - if (m_cut_contours.object_id != model_object->id() || m_cut_contours.volumes_idxs != volumes_idxs || !trafos_match) - m_cut_contours.mesh = model_object->raw_mesh(); + render_connectors(); - m_cut_contours.position = box.center(); - m_cut_contours.shift = Vec3d::Zero(); - m_cut_contours.object_id = model_object->id(); - m_cut_contours.instance_idx = instance_idx; - m_cut_contours.volumes_idxs = volumes_idxs; - m_cut_contours.volumes_trafos = volumes_trafos; - m_cut_contours.contours.reset(); + render_clipper_cut(); - MeshSlicingParams slicing_params; - slicing_params.trafo = first_glvolume->get_instance_transformation().get_matrix(); - slicing_params.trafo.pretranslate(Vec3d(0., 0., first_glvolume->get_sla_shift_z())); + if (!m_hide_cut_plane && !m_connectors_editing) { + render_cut_plane(); + render_cut_plane_grabbers(); + } - const Polygons polys = slice_mesh(m_cut_contours.mesh.its, m_cut_z, slicing_params); - if (!polys.empty()) { - m_cut_contours.contours.init_from(polys, static_cast(m_cut_z)); -#if ENABLE_LEGACY_OPENGL_REMOVAL - m_cut_contours.contours.set_color(ColorRGBA::WHITE()); -#else - m_cut_contours.contours.set_color(-1, { 1.0f, 1.0f, 1.0f, 1.0f }); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL + render_cut_line(); + + m_selection_rectangle.render(m_parent); +} + +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; + + m_imgui->checkbox(_L("Hide cut plane and grabbers"), m_hide_cut_plane); + if (m_imgui->checkbox("hide_clipped", hide_clipped) && !hide_clipped) + m_clp_normal = m_c->object_clipper()->get_clipping_plane()->get_normal(); + m_imgui->checkbox("fill_cut", fill_cut); + m_imgui->slider_float("contour_width", &contour_width, 0.f, 3.f); + if (auto oc = m_c->object_clipper()) + 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(); + + m_imgui->end(); +} + +void GLGizmoCut3D::adjust_window_position(float x, float y, float bottom_limit) +{ + static float last_y = 0.0f; + static float last_h = 0.0f; + + const float win_h = ImGui::GetWindowHeight(); + y = std::min(y, bottom_limit - win_h); + + ImGui::SetWindowPos(ImVec2(x, y), ImGuiCond_Always); + + if (!is_approx(last_h, win_h) || !is_approx(last_y, y)) { + // ask canvas for another frame to render the window in the correct position + m_imgui->set_requires_extra_frame(); + if (!is_approx(last_h, win_h)) + last_h = win_h; + if (!is_approx(last_y, y)) + last_y = y; + } +} + +void GLGizmoCut3D::unselect_all_connectors() +{ + std::fill(m_selected.begin(), m_selected.end(), false); + m_selected_count = 0; + validate_connector_settings(); +} + +void GLGizmoCut3D::select_all_connectors() +{ + std::fill(m_selected.begin(), m_selected.end(), true); + m_selected_count = int(m_selected.size()); +} + +void GLGizmoCut3D::render_shortcuts() +{ + if (m_imgui->button("? " + (m_show_shortcuts ? wxString(ImGui::CollapseBtn) : wxString(ImGui::ExpandBtn)))) + m_show_shortcuts = !m_show_shortcuts; + + if (m_show_shortcuts) + for (const auto&shortcut : m_shortcuts ){ + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, shortcut.first); + ImGui::SameLine(m_label_width); + m_imgui->text(shortcut.second); + } +} + +void GLGizmoCut3D::apply_selected_connectors(std::function apply_fn) +{ + for (size_t idx = 0; idx < m_selected.size(); idx++) + if (m_selected[idx]) + apply_fn(idx); + + update_raycasters_for_picking_transform(); +} + +void GLGizmoCut3D::render_connectors_input_window(CutConnectors &connectors) +{ + // add shortcuts panel + render_shortcuts(); + + // Connectors section + + ImGui::Separator(); + + // WIP : Auto : Need to implement + // m_imgui->text(_L("Mode")); + // render_connect_mode_radio_button(CutConnectorMode::Auto); + // render_connect_mode_radio_button(CutConnectorMode::Manual); + + ImGui::AlignTextToFramePadding(); + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Connectors")); + + m_imgui->disabled_begin(connectors.empty()); + ImGui::SameLine(m_label_width); + if (render_reset_button("connectors", _u8L("Remove connectors"))) + reset_connectors(); + m_imgui->disabled_end(); + + m_imgui->text(_L("Type")); + bool type_changed = render_connect_type_radio_button(CutConnectorType::Plug); + type_changed |= render_connect_type_radio_button(CutConnectorType::Dowel); + if (type_changed) + apply_selected_connectors([this, &connectors] (size_t idx) { connectors[idx].attribs.type = CutConnectorType(m_connector_type); }); + + m_imgui->disabled_begin(m_connector_type == CutConnectorType::Dowel); + if (type_changed && m_connector_type == CutConnectorType::Dowel) { + m_connector_style = size_t(CutConnectorStyle::Prizm); + apply_selected_connectors([this, &connectors](size_t idx) { connectors[idx].attribs.style = CutConnectorStyle(m_connector_style); }); + } + if (render_combo(_u8L("Style"), m_connector_styles, m_connector_style)) + apply_selected_connectors([this, &connectors](size_t idx) { connectors[idx].attribs.style = CutConnectorStyle(m_connector_style); }); + m_imgui->disabled_end(); + + if (render_combo(_u8L("Shape"), m_connector_shapes, m_connector_shape_id)) + apply_selected_connectors([this, &connectors](size_t idx) { connectors[idx].attribs.shape = CutConnectorShape(m_connector_shape_id); }); + + if (render_slider_double_input(_u8L("Depth ratio"), m_connector_depth_ratio, m_connector_depth_ratio_tolerance)) + apply_selected_connectors([this, &connectors](size_t idx) { + if (m_connector_depth_ratio > 0) + connectors[idx].height = m_connector_depth_ratio; + if (m_connector_depth_ratio_tolerance >= 0) + connectors[idx].height_tolerance = m_connector_depth_ratio_tolerance; + }); + + if (render_slider_double_input(_u8L("Size"), m_connector_size, m_connector_size_tolerance)) + apply_selected_connectors([this, &connectors](size_t idx) { + if (m_connector_size > 0) + connectors[idx].radius = 0.5f * m_connector_size; + if (m_connector_size_tolerance >= 0) + connectors[idx].radius_tolerance = m_connector_size_tolerance; + }); + + ImGui::Separator(); + + if (m_imgui->button(_L("Confirm connectors"))) { + 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() +{ + double koef = m_imperial_units ? ObjectManipulation::mm_to_in : 1.0; + wxString unit_str = " " + (m_imperial_units ? _L("in") : _L("mm")); + const BoundingBoxf3 tbb = transformed_bounding_box(m_plane_center); + + Vec3d tbb_sz = tbb.size(); + wxString size = "X: " + double_to_string(tbb_sz.x() * koef, 2) + unit_str + + ", Y: " + double_to_string(tbb_sz.y() * koef, 2) + unit_str + + ", Z: " + double_to_string(tbb_sz.z() * koef, 2) + unit_str; + + ImGui::AlignTextToFramePadding(); + m_imgui->text(_L("Build size")); + ImGui::SameLine(m_label_width); + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, size); +} + +void GLGizmoCut3D::reset_cut_plane() +{ + set_center(bounding_box().center()); + m_rotation_m = Transform3d::Identity(); + m_angle_arc.reset(); + update_clipper(); +} + +void GLGizmoCut3D::invalidate_cut_plane() +{ + m_rotation_m = Transform3d::Identity(); + m_plane_center = Vec3d::Zero(); + m_min_pos = Vec3d::Zero(); + m_max_pos = Vec3d::Zero(); + m_bb_center = Vec3d::Zero(); + m_center_offset = Vec3d::Zero(); +} + +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(); +} + +void GLGizmoCut3D::render_cut_plane_input_window(CutConnectors &connectors) +{ + // WIP : cut plane mode + // render_combo(_u8L("Mode"), m_modes, m_mode); + + if (m_mode == size_t(CutMode::cutPlanar)) { + ImGui::AlignTextToFramePadding(); + m_imgui->text(wxString(ImGui::InfoMarkerSmall)); + ImGui::SameLine(); + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Hold SHIFT key and connect some two points of an object to cut by line")); + + ImGui::Separator(); + + render_build_size(); + + ImGui::AlignTextToFramePadding(); + m_imgui->text(_L("Cut position: ")); + ImGui::SameLine(m_label_width); + render_move_center_input(Z); + ImGui::SameLine(); + + const bool is_cut_plane_init = m_rotation_m.isApprox(Transform3d::Identity()) && bounding_box().center() == m_plane_center; + m_imgui->disabled_begin(is_cut_plane_init); + if (render_reset_button("cut_plane", _u8L("Reset cutting plane"))) + 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"))) + set_connectors_editing(true); + m_imgui->disabled_end(); + } + + ImGui::Separator(); + + auto render_part_action_line = [this, connectors](const wxString& label, const wxString& suffix, bool& keep_part, bool& place_on_cut_part, bool& rotate_part) { + bool keep = true; + ImGui::AlignTextToFramePadding(); + m_imgui->text(label); + + ImGui::SameLine(m_label_width); + + m_imgui->disabled_begin(!connectors.empty()); + m_imgui->checkbox(_L("Keep") + suffix, connectors.empty() ? keep_part : keep); + m_imgui->disabled_end(); + + ImGui::SameLine(2 * m_label_width); + + m_imgui->disabled_begin(!keep_part); + if (m_imgui->checkbox(_L("Place on cut") + suffix, place_on_cut_part)) + rotate_part = false; + ImGui::SameLine(); + if (m_imgui->checkbox(_L("Flip") + suffix, rotate_part)) + place_on_cut_part = false; + m_imgui->disabled_end(); + }; + + m_imgui->text(_L("After cut") + ": "); + render_part_action_line( _L("Upper part"), "##upper", m_keep_upper, m_place_on_cut_upper, m_rotate_upper); + render_part_action_line( _L("Lower part"), "##lower", m_keep_lower, m_place_on_cut_lower, m_rotate_lower); + } + + ImGui::Separator(); + + 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(); +} + +void GLGizmoCut3D::validate_connector_settings() +{ + if (m_connector_depth_ratio < 0.f) + m_connector_depth_ratio = 3.f; + if (m_connector_depth_ratio_tolerance < 0.f) + m_connector_depth_ratio_tolerance = 0.1f; + if (m_connector_size < 0.f) + m_connector_size = 2.5f; + if (m_connector_size_tolerance < 0.f) + m_connector_size_tolerance = 0.f; + + if (m_connector_type == CutConnectorType::Undef) + m_connector_type = CutConnectorType::Plug; + if (m_connector_style == size_t(CutConnectorStyle::Undef)) + m_connector_style = size_t(CutConnectorStyle::Prizm); + if (m_connector_shape_id == size_t(CutConnectorShape::Undef)) + m_connector_shape_id = size_t(CutConnectorShape::Circle); +} + +void GLGizmoCut3D::init_input_window_data(CutConnectors &connectors) +{ + m_imperial_units = wxGetApp().app_config->get("use_inches") == "1"; + m_label_width = m_imgui->get_font_size() * 6.f; + m_control_width = m_imgui->get_font_size() * 9.f; + + if (m_connectors_editing && m_selected_count > 0) { + float depth_ratio { UndefFloat }; + float depth_ratio_tolerance { UndefFloat }; + float radius { UndefFloat }; + float radius_tolerance { UndefFloat }; + CutConnectorType type { CutConnectorType::Undef }; + CutConnectorStyle style { CutConnectorStyle::Undef }; + CutConnectorShape shape { CutConnectorShape::Undef }; + + bool is_init = false; + for (size_t idx = 0; idx < m_selected.size(); idx++) + if (m_selected[idx]) { + const CutConnector& connector = connectors[idx]; + if (!is_init) { + depth_ratio = connector.height; + depth_ratio_tolerance = connector.height_tolerance; + radius = connector.radius; + radius_tolerance = connector.radius_tolerance; + type = connector.attribs.type; + style = connector.attribs.style; + shape = connector.attribs.shape; + + if (m_selected_count == 1) + break; + is_init = true; + } + else { + if (!is_approx(depth_ratio, connector.height)) + depth_ratio = UndefFloat; + if (!is_approx(depth_ratio_tolerance, connector.height_tolerance)) + depth_ratio_tolerance = UndefFloat; + if (!is_approx(radius,connector.radius)) + radius = UndefFloat; + if (!is_approx(radius_tolerance, connector.radius_tolerance)) + radius_tolerance = UndefFloat; + + if (type != connector.attribs.type) + type = CutConnectorType::Undef; + if (style != connector.attribs.style) + style = CutConnectorStyle::Undef; + if (shape != connector.attribs.shape) + shape = CutConnectorShape::Undef; + } + } + + m_connector_depth_ratio = depth_ratio; + m_connector_depth_ratio_tolerance = depth_ratio_tolerance; + m_connector_size = 2.f * radius; + m_connector_size_tolerance = radius_tolerance; + m_connector_type = type; + m_connector_style = size_t(style); + m_connector_shape_id = size_t(shape); + } +} + +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)) + out += "\n - " + format_wxstr(_L_PLURAL("%1$d connector is out of cut contour", "%1$d connectors are out of cut contour", m_info_stats.outside_cut_contour), + m_info_stats.outside_cut_contour); + if (m_info_stats.outside_bb > size_t(0)) + out += "\n - " + format_wxstr(_L_PLURAL("%1$d connector is out of object", "%1$d connectors are out of object", m_info_stats.outside_bb), + m_info_stats.outside_bb); + if (m_info_stats.is_overlap) + out += "\n - " + _L("Some connectors are overlapped"); + m_imgui->text(out); + } + if (!m_keep_upper && !m_keep_lower) + m_imgui->text(wxString(ImGui::WarningMarkerSmall) + _L("Invalid state. \nNo one part is selected for keep after cut")); +} + +void GLGizmoCut3D::on_render_input_window(float x, float y, float bottom_limit) +{ + m_imgui->begin(get_name(), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + + // adjust window position to avoid overlap the view toolbar + adjust_window_position(x, y, bottom_limit); + + CutConnectors& connectors = m_c->selection_info()->model_object()->cut_connectors; + + init_input_window_data(connectors); + + if (m_connectors_editing) // connectors mode + render_connectors_input_window(connectors); + else + render_cut_plane_input_window(connectors); + + render_input_window_warning(); + + m_imgui->end(); + + render_debug_input_window(); +} + +// get volume transformation regarding to the "border". Border is related from the size of connectors +Transform3d GLGizmoCut3D::get_volume_transformation(const ModelVolume* volume) const +{ + bool is_prizm_dowel = m_connector_type == CutConnectorType::Dowel && m_connector_style == size_t(CutConnectorStyle::Prizm); + const Transform3d connector_trafo = assemble_transform( + is_prizm_dowel ? Vec3d(0.0, 0.0, -m_connector_depth_ratio) : Vec3d::Zero(), + Transformation(m_rotation_m).get_rotation(), + Vec3d(0.5*m_connector_size, 0.5*m_connector_size, is_prizm_dowel ? 2 * m_connector_depth_ratio : m_connector_depth_ratio), + Vec3d::Ones()); + const Vec3d connector_bb = m_connector_mesh.transformed_bounding_box(connector_trafo).size(); + + const Vec3d bb = volume->mesh().bounding_box().size(); + + // calculate an unused border - part of the the volume, where we can't put connectors + const Vec3d border_scale(connector_bb.x() / bb.x(), connector_bb.y() / bb.y(), connector_bb.z() / bb.z()); + + const Transform3d vol_matrix = volume->get_matrix(); + const Vec3d vol_trans = vol_matrix.translation(); + // offset of the volume will be changed after scaling, so calculate the needed offset and set it to a volume_trafo + const Vec3d offset(vol_trans.x() * border_scale.x(), vol_trans.y() * border_scale.y(), vol_trans.z() * border_scale.z()); + + // scale and translate volume to suppress to put connectors too close to the border + return assemble_transform(offset, Vec3d::Zero(), Vec3d::Ones() - border_scale, Vec3d::Ones()) * vol_matrix; +} + +bool GLGizmoCut3D::is_conflict_for_connector(size_t idx, const CutConnectors& connectors, const Vec3d cur_pos) +{ + // check if connector pos is out of clipping plane + if (m_c->object_clipper() && !m_c->object_clipper()->is_projection_inside_cut(cur_pos)) { + m_info_stats.outside_cut_contour++; + return true; + } + + const CutConnector& cur_connector = connectors[idx]; + const Transform3d matrix = translation_transform(cur_pos) * m_rotation_m * + scale_transform(Vec3f(cur_connector.radius, cur_connector.radius, cur_connector.height).cast()); + const BoundingBoxf3 cur_tbb = m_shapes[cur_connector.attribs].model.get_bounding_box().transformed(matrix); + + if (!bounding_box().contains(cur_tbb)) { + m_info_stats.outside_bb++; + return true; + } + + for (size_t i = 0; i < connectors.size(); ++i) { + if (i == idx) + continue; + const CutConnector& connector = connectors[i]; + + if ((connector.pos - cur_connector.pos).norm() < double(connector.radius + cur_connector.radius)) { + m_info_stats.is_overlap = true; + return true; + } + } + + return false; +} + +void GLGizmoCut3D::render_connectors() +{ + ::glEnable(GL_DEPTH_TEST); + + 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(); + auto inst_id = m_c->selection_info()->get_active_instance(); + if (inst_id < 0) + return; + const CutConnectors& connectors = mo->cut_connectors; + if (connectors.size() != m_selected.size()) { + // #ysFIXME + clear_selection(); + m_selected.resize(connectors.size(), false); + } + + ColorRGBA render_color = CONNECTOR_DEF_COLOR; + + const ModelInstance* mi = mo->instances[inst_id]; + const Vec3d& instance_offset = mi->get_offset(); + const double sla_shift = double(m_c->selection_info()->get_sla_shift()); + + const ClippingPlane* cp = m_c->object_clipper()->get_clipping_plane(); + const Vec3d& normal = cp && cp->is_active() ? cp->get_normal() : m_clp_normal; + + m_has_invalid_connector = false; + m_info_stats.invalidate(); + + for (size_t i = 0; i < connectors.size(); ++i) { + const CutConnector& connector = connectors[i]; + + float height = connector.height; + // recalculate connector position to world position + Vec3d pos = connector.pos + instance_offset + sla_shift * Vec3d::UnitZ(); + + // First decide about the color of the point. + if (is_conflict_for_connector(i, connectors, pos)) { + m_has_invalid_connector = true; + render_color = CONNECTOR_ERR_COLOR; + } + else if (!m_connectors_editing) + render_color = CONNECTOR_ERR_COLOR; + else if (size_t(m_hover_id - m_connectors_group_id) == i) + render_color = connector.attribs.type == CutConnectorType::Dowel ? HOVERED_DOWEL_COLOR : HOVERED_PLAG_COLOR; + else if (m_selected[i]) + render_color = connector.attribs.type == CutConnectorType::Dowel ? SELECTED_DOWEL_COLOR : SELECTED_PLAG_COLOR; + else // neither hover nor picking + render_color = connector.attribs.type == CutConnectorType::Dowel ? DOWEL_COLOR : PLAG_COLOR; + + const Camera& camera = wxGetApp().plater()->get_camera(); + if (connector.attribs.type == CutConnectorType::Dowel && + connector.attribs.style == CutConnectorStyle::Prizm) { + pos -= height * normal; + height *= 2; + } + const Transform3d view_model_matrix = camera.get_view_matrix() * translation_transform(pos) * m_rotation_m * + scale_transform(Vec3f(connector.radius, connector.radius, height).cast()); + + render_model(m_shapes[connector.attribs].model, render_color, view_model_matrix); + } +} + +bool GLGizmoCut3D::can_perform_cut() const +{ + if (m_has_invalid_connector || (!m_keep_upper && !m_keep_lower) || m_connectors_editing) + return false; + + const auto clipper = m_c->object_clipper(); + return clipper && clipper->has_valid_contour(); +} + +void GLGizmoCut3D::apply_connectors_in_model(ModelObject* mo, bool &create_dowels_as_separate_object) +{ + if (m_connector_mode == CutConnectorMode::Manual) { + clear_selection(); + + for (CutConnector&connector : mo->cut_connectors) { + connector.rotation_m = m_rotation_m; + + if (connector.attribs.type == CutConnectorType::Dowel) { + if (connector.attribs.style == CutConnectorStyle::Prizm) + connector.height *= 2; + create_dowels_as_separate_object = true; + } + else { + // culculate shift of the connector center regarding to the position on the cut plane + Vec3d shifted_center = m_plane_center + Vec3d::UnitZ(); + rotate_vec3d_around_plane_center(shifted_center); + Vec3d norm = (shifted_center - m_plane_center).normalized(); + connector.pos += norm * 0.5 * double(connector.height); } } - else if (box.center() != m_cut_contours.position) { - m_cut_contours.shift = box.center() - m_cut_contours.position; + mo->apply_cut_connectors(_u8L("Connector")); + } +} + +void GLGizmoCut3D::perform_cut(const Selection& selection) +{ + if (!can_perform_cut()) + return; + const int instance_idx = selection.get_instance_idx(); + const int object_idx = selection.get_object_idx(); + + wxCHECK_RET(instance_idx >= 0 && object_idx >= 0, "GLGizmoCut: Invalid object selection"); + + Plater* plater = wxGetApp().plater(); + ModelObject* mo = plater->model().objects[object_idx]; + if (!mo) + return; + + // deactivate CutGizmo and than perform a cut + m_parent.reset_all_gizmos(); + + // m_cut_z is the distance from the bed. Subtract possible SLA elevation. + const double sla_shift_z = selection.get_first_volume()->get_sla_shift_z(); + + const Vec3d instance_offset = mo->instances[instance_idx]->get_offset(); + Vec3d cut_center_offset = m_plane_center - instance_offset; + cut_center_offset[Z] -= sla_shift_z; + + // perform cut + { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Cut by Plane")); + + bool create_dowels_as_separate_object = false; + const bool has_connectors = !mo->cut_connectors.empty(); + // update connectors pos as offset of its center before cut performing + apply_connectors_in_model(mo, create_dowels_as_separate_object); + + plater->cut(object_idx, instance_idx, assemble_transform(cut_center_offset) * m_rotation_m, + only_if(has_connectors ? true : m_keep_upper, ModelObjectCutAttribute::KeepUpper) | + only_if(has_connectors ? true : m_keep_lower, ModelObjectCutAttribute::KeepLower) | + only_if(m_place_on_cut_upper, ModelObjectCutAttribute::PlaceOnCutUpper) | + only_if(m_place_on_cut_lower, ModelObjectCutAttribute::PlaceOnCutLower) | + only_if(m_rotate_upper, ModelObjectCutAttribute::FlipUpper) | + only_if(m_rotate_lower, ModelObjectCutAttribute::FlipLower) | + only_if(create_dowels_as_separate_object, ModelObjectCutAttribute::CreateDowels)); + } +} + + + +// Unprojects the mouse position on the mesh and saves hit point and normal of the facet into pos_and_normal +// Return false if no intersection was found, true otherwise. +bool GLGizmoCut3D::unproject_on_cut_plane(const Vec2d& mouse_position, std::pair& pos_and_normal, Vec3d& pos_world) +{ + const float sla_shift = m_c->selection_info()->get_sla_shift(); + + const ModelObject* mo = m_c->selection_info()->model_object(); + const ModelInstance* mi = mo->instances[m_c->selection_info()->get_active_instance()]; + const Transform3d instance_trafo = sla_shift > 0.f ? + assemble_transform(Vec3d(0.0, 0.0, sla_shift)) * mi->get_transformation().get_matrix() : mi->get_transformation().get_matrix(); + const Camera& camera = wxGetApp().plater()->get_camera(); + + int mesh_id = -1; + for (const ModelVolume* mv : mo->volumes) { + ++mesh_id; + if (!mv->is_model_part()) + continue; + Vec3f normal; + Vec3f hit; + bool clipping_plane_was_hit = false; + +// const Transform3d volume_trafo = get_volume_transformation(mv); + const Transform3d volume_trafo = mv->get_transformation().get_matrix(); + + m_c->raycaster()->raycasters()[mesh_id]->unproject_on_mesh(mouse_position, instance_trafo * volume_trafo, + camera, hit, normal, m_c->object_clipper()->get_clipping_plane(true), + nullptr, &clipping_plane_was_hit); + if (clipping_plane_was_hit) { + // recalculate hit to object's local position + Vec3d hit_d = hit.cast(); + hit_d -= mi->get_offset(); + hit_d[Z] -= sla_shift; + + // Return both the point and the facet normal. + pos_and_normal = std::make_pair(hit_d, normal.cast()); + pos_world = hit.cast(); + return true; } } + return false; +} + +void GLGizmoCut3D::clear_selection() +{ + m_selected.clear(); + m_selected_count = 0; +} + +void GLGizmoCut3D::reset_connectors() +{ + m_c->selection_info()->model_object()->cut_connectors.clear(); + update_raycasters_for_picking(); + clear_selection(); +} + +void GLGizmoCut3D::init_connector_shapes() +{ + for (const CutConnectorType& type : {CutConnectorType::Dowel, CutConnectorType::Plug}) + for (const CutConnectorStyle& style : {CutConnectorStyle::Frustum, CutConnectorStyle::Prizm}) + for (const CutConnectorShape& shape : {CutConnectorShape::Circle, CutConnectorShape::Hexagon, CutConnectorShape::Square, CutConnectorShape::Triangle}) { + const CutConnectorAttributes attribs = { type, style, shape }; + const indexed_triangle_set its = ModelObject::get_connector_mesh(attribs); + m_shapes[attribs].model.init_from(its); + m_shapes[attribs].mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); + } +} + +void GLGizmoCut3D::update_connector_shape() +{ + CutConnectorAttributes attribs = { m_connector_type, CutConnectorStyle(m_connector_style), CutConnectorShape(m_connector_shape_id) }; + + const indexed_triangle_set its = ModelObject::get_connector_mesh(attribs); + m_connector_mesh.clear(); + m_connector_mesh = TriangleMesh(its); +} + +bool GLGizmoCut3D::cut_line_processing() const +{ + return m_line_beg != Vec3d::Zero(); +} + +void GLGizmoCut3D::discard_cut_line_processing() +{ + m_line_beg = m_line_end = Vec3d::Zero(); +} + +bool GLGizmoCut3D::process_cut_line(SLAGizmoEventType action, const Vec2d& mouse_position) +{ + const Camera& camera = wxGetApp().plater()->get_camera(); + + Vec3d pt; + Vec3d dir; + MeshRaycaster::line_from_mouse_pos(mouse_position, Transform3d::Identity(), camera, pt, dir); + dir.normalize(); + pt += dir; // Move the pt along dir so it is not clipped. + + if (action == SLAGizmoEventType::LeftDown && !cut_line_processing()) { + m_line_beg = pt; + m_line_end = pt; + on_unregister_raycasters_for_picking(); + return true; + } + + if (cut_line_processing()) { + m_line_end = pt; + if (action == SLAGizmoEventType::LeftDown || action == SLAGizmoEventType::LeftUp) { + Vec3d line_dir = m_line_end - m_line_beg; + if (line_dir.norm() < 3.0) + return true; + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Cut by line"), UndoRedo::SnapshotType::GizmoAction); + + Vec3d cross_dir = line_dir.cross(dir).normalized(); + Eigen::Quaterniond q; + Transform3d m = Transform3d::Identity(); + m.matrix().block(0, 0, 3, 3) = q.setFromTwoVectors(Vec3d::UnitZ(), cross_dir).toRotationMatrix(); + + m_rotation_m = m; + m_angle_arc.reset(); + + set_center(m_plane_center + cross_dir * (cross_dir.dot(pt - m_plane_center))); + + discard_cut_line_processing(); + } + else if (action == SLAGizmoEventType::Moving) + this->set_dirty(); + return true; + } + return false; +} + +bool GLGizmoCut3D::add_connector(CutConnectors& connectors, const Vec2d& mouse_position) +{ + if (!m_connectors_editing) + return false; + + std::pair pos_and_normal; + Vec3d pos_world; + if (unproject_on_cut_plane(mouse_position.cast(), pos_and_normal, pos_world)) { + const Vec3d& hit = pos_and_normal.first; + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Add connector"), UndoRedo::SnapshotType::GizmoAction); + unselect_all_connectors(); + + connectors.emplace_back(hit, m_rotation_m, + m_connector_size * 0.5f, m_connector_depth_ratio, + m_connector_size_tolerance, m_connector_depth_ratio_tolerance, + CutConnectorAttributes( CutConnectorType(m_connector_type), + CutConnectorStyle(m_connector_style), + CutConnectorShape(m_connector_shape_id))); + m_selected.push_back(true); + m_selected_count = 1; + assert(m_selected.size() == connectors.size()); + update_raycasters_for_picking(); + m_parent.set_as_dirty(); + + return true; + } + return false; +} + +bool GLGizmoCut3D::delete_selected_connectors(CutConnectors& connectors) +{ + if (connectors.empty()) + return false; + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Delete connector"), UndoRedo::SnapshotType::GizmoAction); + + // remove connectors + for (int i = int(connectors.size()) - 1; i >= 0; i--) + if (m_selected[i]) + connectors.erase(connectors.begin() + i); + // remove selections + m_selected.erase(std::remove_if(m_selected.begin(), m_selected.end(), [](const auto& selected) { + return selected; }), m_selected.end()); + m_selected_count = 0; + + assert(m_selected.size() == connectors.size()); + update_raycasters_for_picking(); + m_parent.set_as_dirty(); + return true; +} + +void GLGizmoCut3D::select_connector(int idx, bool select) +{ + m_selected[idx] = select; + if (select) + ++m_selected_count; else - m_cut_contours.contours.reset(); + --m_selected_count; +} + +bool GLGizmoCut3D::is_selection_changed(bool alt_down, bool shift_down) +{ + if (m_hover_id >= m_connectors_group_id) { + if (alt_down) + select_connector(m_hover_id - m_connectors_group_id, false); + else { + if (!shift_down) + unselect_all_connectors(); + select_connector(m_hover_id - m_connectors_group_id, true); + } + return true; + } + return false; +} + +void GLGizmoCut3D::process_selection_rectangle(CutConnectors &connectors) +{ + GLSelectionRectangle::EState rectangle_status = m_selection_rectangle.get_state(); + + ModelObject* mo = m_c->selection_info()->model_object(); + int active_inst = m_c->selection_info()->get_active_instance(); + + // First collect positions of all the points in world coordinates. + Transformation trafo = mo->instances[active_inst]->get_transformation(); + trafo.set_offset(trafo.get_offset() + double(m_c->selection_info()->get_sla_shift()) * Vec3d::UnitZ()); + + std::vector points; + for (const CutConnector&connector : connectors) + points.push_back(connector.pos + trafo.get_offset()); + + // Now ask the rectangle which of the points are inside. + std::vector points_idxs = m_selection_rectangle.contains(points); + m_selection_rectangle.stop_dragging(); + + for (size_t idx : points_idxs) + select_connector(int(idx), rectangle_status == GLSelectionRectangle::EState::Select); +} + +bool GLGizmoCut3D::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down) +{ + if (is_dragging() || m_connector_mode == CutConnectorMode::Auto || (!m_keep_upper || !m_keep_lower)) + return false; + + if ( m_hover_id < 0 && shift_down && ! m_connectors_editing && + (action == SLAGizmoEventType::LeftDown || action == SLAGizmoEventType::LeftUp || action == SLAGizmoEventType::Moving) ) + return process_cut_line(action, mouse_position); + + if (!m_connectors_editing) + return false; + + CutConnectors& connectors = m_c->selection_info()->model_object()->cut_connectors; + + if (action == SLAGizmoEventType::LeftDown) { + if (shift_down || alt_down) { + // left down with shift - show the selection rectangle: + if (m_hover_id == -1) + m_selection_rectangle.start_dragging(mouse_position, shift_down ? GLSelectionRectangle::EState::Select : GLSelectionRectangle::EState::Deselect); + } + else + // If there is no selection and no hovering, add new point + if (m_hover_id == -1 && !shift_down && !alt_down) + if (!add_connector(connectors, mouse_position)) + m_ldown_mouse_position = mouse_position; + return true; + } + + if (action == SLAGizmoEventType::LeftUp && !m_selection_rectangle.is_dragging()) { + if ((m_ldown_mouse_position - mouse_position).norm() < 5.) + unselect_all_connectors(); + return is_selection_changed(alt_down, shift_down); + } + + // left up with selection rectangle - select points inside the rectangle: + if ((action == SLAGizmoEventType::LeftUp || action == SLAGizmoEventType::ShiftUp || action == SLAGizmoEventType::AltUp) && m_selection_rectangle.is_dragging()) { + // Is this a selection or deselection rectangle? + process_selection_rectangle(connectors); + return true; + } + + // dragging the selection rectangle: + if (action == SLAGizmoEventType::Dragging) { + if (m_selection_rectangle.is_dragging()) { + m_selection_rectangle.dragging(mouse_position); + return true; + } + return false; + } + + if (action == SLAGizmoEventType::RightDown && !shift_down) { + // If any point is in hover state, this should initiate its move - return control back to GLCanvas: + if (m_hover_id < m_connectors_group_id) + return false; + unselect_all_connectors(); + select_connector(m_hover_id - m_connectors_group_id, true); + return delete_selected_connectors(connectors); + } + + if (action == SLAGizmoEventType::Delete) + return delete_selected_connectors(connectors); + + if (action == SLAGizmoEventType::SelectAll) { + select_all_connectors(); + return true; + } + + return false; +} + +CommonGizmosDataID GLGizmoCut3D::on_get_requirements() const { + return CommonGizmosDataID( + int(CommonGizmosDataID::SelectionInfo) + | int(CommonGizmosDataID::InstancesHider) + | int(CommonGizmosDataID::ObjectClipper) + | int(CommonGizmosDataID::Raycaster)); } } // namespace GUI diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp index 0a7274e2e..8a3ee6d07 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp @@ -2,55 +2,164 @@ #define slic3r_GLGizmoCut_hpp_ #include "GLGizmoBase.hpp" +#include "slic3r/GUI/GLSelectionRectangle.hpp" #include "slic3r/GUI/GLModel.hpp" #include "libslic3r/TriangleMesh.hpp" -#include "libslic3r/ObjectID.hpp" +#include "libslic3r/Model.hpp" namespace Slic3r { + +enum class CutConnectorType : int; +class ModelVolume; +struct CutConnectorAttributes; + namespace GUI { class Selection; -class GLGizmoCut : public GLGizmoBase -{ - static const double Offset; - static const double Margin; +enum class SLAGizmoEventType : unsigned char; + +class GLGizmoCut3D : public GLGizmoBase +{ + Transform3d m_rotation_m{ Transform3d::Identity() }; + double m_snap_step{ 1.0 }; + int m_connectors_group_id; + + // archived values + Vec3d m_ar_plane_center { Vec3d::Zero() }; + + Vec3d m_plane_center{ Vec3d::Zero() }; + // data to check position of the cut palne center on gizmo activation + Vec3d m_min_pos{ Vec3d::Zero() }; + Vec3d m_max_pos{ Vec3d::Zero() }; + Vec3d m_bb_center{ Vec3d::Zero() }; + Vec3d m_center_offset{ Vec3d::Zero() }; + + // values from RotationGizmo + double m_radius{ 0.0 }; + double m_grabber_radius{ 0.0 }; + double m_grabber_connection_len{ 0.0 }; + + double m_snap_coarse_in_radius{ 0.0 }; + double m_snap_coarse_out_radius{ 0.0 }; + double m_snap_fine_in_radius{ 0.0 }; + double m_snap_fine_out_radius{ 0.0 }; + + // dragging angel in hovered axes + Transform3d m_start_dragging_m{ Transform3d::Identity() }; + double m_angle{ 0.0 }; + + TriangleMesh m_connector_mesh; + // workaround for using of the clipping plane normal + Vec3d m_clp_normal{ Vec3d::Ones() }; + + Vec3d m_line_beg{ Vec3d::Zero() }; + Vec3d m_line_end{ Vec3d::Zero() }; + + Vec2d m_ldown_mouse_position{ Vec2d::Zero() }; - double m_cut_z{ 0.0 }; - double m_max_z{ 0.0 }; - double m_start_z{ 0.0 }; - Vec3d m_drag_pos; - Vec3d m_drag_center; - bool m_keep_upper{ true }; - bool m_keep_lower{ true }; - bool m_rotate_lower{ false }; -#if ENABLE_LEGACY_OPENGL_REMOVAL GLModel m_plane; GLModel m_grabber_connection; - Vec3d m_old_center; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL + GLModel m_cut_line; - struct CutContours + PickingModel m_sphere; + PickingModel m_cone; + std::map m_shapes; + std::vector> m_raycasters; + + GLModel m_circle; + GLModel m_scale; + GLModel m_snap_radii; + GLModel m_reference_radius; + GLModel m_angle_arc; + + Vec3d m_old_center; + + struct InvalidConnectorsStatistics { - TriangleMesh mesh; - GLModel contours; - double cut_z{ 0.0 }; - Vec3d position{ Vec3d::Zero() }; - Vec3d shift{ Vec3d::Zero() }; - ObjectID object_id; - int instance_idx{ -1 }; - std::vector volumes_idxs; - std::vector volumes_trafos; + unsigned int outside_cut_contour; + unsigned int outside_bb; + bool is_overlap; + + void invalidate() { + outside_cut_contour = 0; + outside_bb = 0; + is_overlap = false; + } + } m_info_stats; + + bool m_keep_upper{ true }; + bool m_keep_lower{ true }; + bool m_place_on_cut_upper{ true }; + bool m_place_on_cut_lower{ false }; + bool m_rotate_upper{ false }; + bool m_rotate_lower{ false }; + + bool m_hide_cut_plane{ false }; + bool m_connectors_editing{ false }; + bool m_cut_plane_as_circle{ true }; + + float m_connector_depth_ratio{ 3.f }; + float m_connector_size{ 2.5f }; + + float m_connector_depth_ratio_tolerance{ 0.1f }; + float m_connector_size_tolerance{ 0.f }; + + float m_label_width{ 150.0 }; + float m_control_width{ 200.0 }; + 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 }; + + GLSelectionRectangle m_selection_rectangle; + + bool m_has_invalid_connector{ false }; + + bool m_show_shortcuts{ false }; + std::vector> m_shortcuts; + + enum class CutMode { + cutPlanar + , cutGrig + //,cutRadial + //,cutModular }; - CutContours m_cut_contours; + enum class CutConnectorMode { + Auto + , Manual + }; + + std::vector m_modes; + size_t m_mode{ size_t(CutMode::cutPlanar) }; + + std::vector m_connector_modes; + CutConnectorMode m_connector_mode{ CutConnectorMode::Manual }; + + std::vector m_connector_types; + CutConnectorType m_connector_type; + + std::vector m_connector_styles; + size_t m_connector_style; + + std::vector m_connector_shapes; + size_t m_connector_shape_id; + + std::vector m_axis_names; public: - GLGizmoCut(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); - - double get_cut_z() const { return m_cut_z; } - void set_cut_z(double cut_z); + GLGizmoCut3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); std::string get_tooltip() const override; + bool unproject_on_cut_plane(const Vec2d& mouse_pos, std::pair& pos_and_normal, Vec3d& pos_world); + bool gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down); + + bool is_in_editing_mode() const override { return m_connectors_editing; } + bool is_selection_rectangle_dragging() const override { return m_selection_rectangle.is_dragging(); } /// /// Drag of plane @@ -59,29 +168,104 @@ public: /// Return True when use the information otherwise False. bool on_mouse(const wxMouseEvent &mouse_event) override; + void shift_cut_z(double delta); + void rotate_vec3d_around_plane_center(Vec3d&vec); + void put_connectors_on_cut_plane(const Vec3d& cp_normal, double cp_offset); + void update_clipper(); + void update_clipper_on_render(); + void invalidate_cut_plane(); + + BoundingBoxf3 bounding_box() const; + BoundingBoxf3 transformed_bounding_box(const Vec3d& plane_center, bool revert_move = false) const; + protected: - virtual bool on_init() override; - virtual void on_load(cereal::BinaryInputArchive& ar) override { ar(m_cut_z, m_keep_upper, m_keep_lower, m_rotate_lower); } - virtual void on_save(cereal::BinaryOutputArchive& ar) const override { ar(m_cut_z, m_keep_upper, m_keep_lower, m_rotate_lower); } - virtual std::string on_get_name() const override; - virtual void on_set_state() override; - virtual bool on_is_activable() const override; - virtual void on_start_dragging() override; - virtual void on_dragging(const UpdateData& data) override; - virtual void on_render() override; -#if ENABLE_RAYCAST_PICKING + bool on_init() override; + void on_load(cereal::BinaryInputArchive&ar) override; + void on_save(cereal::BinaryOutputArchive&ar) const override; + std::string on_get_name() const override; + void on_set_state() override; + CommonGizmosDataID on_get_requirements() const override; + void on_set_hover_id() override; + bool on_is_activable() const override; + bool on_is_selectable() const override; + Vec3d mouse_position_in_local_plane(Axis axis, const Linef3&mouse_ray) const; + void dragging_grabber_z(const GLGizmoBase::UpdateData &data); + void dragging_grabber_xy(const GLGizmoBase::UpdateData &data); + void dragging_connector(const GLGizmoBase::UpdateData &data); + void on_dragging(const UpdateData&data) override; + void on_start_dragging() override; + void on_stop_dragging() override; + void on_render() override; + + void render_debug_input_window(); + void adjust_window_position(float x, float y, float bottom_limit); + void unselect_all_connectors(); + void select_all_connectors(); + void render_shortcuts(); + void apply_selected_connectors(std::function apply_fn); + void render_connectors_input_window(CutConnectors &connectors); + void render_build_size(); + void reset_cut_plane(); + void set_connectors_editing(bool connectors_editing); + void render_cut_plane_input_window(CutConnectors &connectors); + void init_input_window_data(CutConnectors &connectors); + void render_input_window_warning() const; + bool add_connector(CutConnectors&connectors, const Vec2d&mouse_position); + bool delete_selected_connectors(CutConnectors&connectors); + void select_connector(int idx, bool select); + bool is_selection_changed(bool alt_down, bool shift_down); + void process_selection_rectangle(CutConnectors &connectors); + virtual void on_register_raycasters_for_picking() override; virtual void on_unregister_raycasters_for_picking() override; -#else - virtual void on_render_for_picking() override; -#endif // ENABLE_RAYCAST_PICKING - virtual void on_render_input_window(float x, float y, float bottom_limit) override; + void update_raycasters_for_picking(); + void set_volumes_picking_state(bool state); + void update_raycasters_for_picking_transform(); + + void on_render_input_window(float x, float y, float bottom_limit) override; + + bool wants_enter_leave_snapshots() const override { return true; } + std::string get_gizmo_entering_text() const override { return _u8L("Entering Cut gizmo"); } + std::string get_gizmo_leaving_text() const override { return _u8L("Leaving Cut gizmo"); } + std::string get_action_snapshot_name() override { return _u8L("Cut gizmo editing"); } private: - void perform_cut(const Selection& selection); - double calc_projection(const Linef3& mouse_ray) const; - BoundingBoxf3 bounding_box() const; - void update_contours(); + void set_center(const Vec3d& center); + bool render_combo(const std::string& label, const std::vector& lines, size_t& selection_idx); + bool render_double_input(const std::string& label, double& value_in); + bool render_slider_double_input(const std::string& label, float& value_in, float& tolerance_in); + void render_move_center_input(int axis); + void render_connect_mode_radio_button(CutConnectorMode mode); + bool render_reset_button(const std::string& label_id, const std::string& tooltip) const; + bool render_connect_type_radio_button(CutConnectorType type); + Transform3d get_volume_transformation(const ModelVolume* volume) const; + bool is_conflict_for_connector(size_t idx, const CutConnectors& connectors, const Vec3d cur_pos); + void render_connectors(); + + bool can_perform_cut() const; + void apply_connectors_in_model(ModelObject* mo, bool &create_dowels_as_separate_object); + bool cut_line_processing() const; + void discard_cut_line_processing(); + + void render_cut_plane(); + void render_model(GLModel& model, const ColorRGBA& color, Transform3d view_model_matrix); + void render_line(GLModel& line_model, const ColorRGBA& color, Transform3d view_model_matrix, float width); + void render_rotation_snapping(Axis axis, const ColorRGBA& color); + void render_grabber_connection(const ColorRGBA& color, Transform3d view_matrix); + void render_cut_plane_grabbers(); + void render_cut_line(); + void perform_cut(const Selection&selection); + void set_center_pos(const Vec3d¢er_pos, bool force = false); + bool update_bb(); + void init_picking_models(); + void init_rendering_items(); + void render_clipper_cut(); + void clear_selection(); + void reset_connectors(); + void init_connector_shapes(); + void update_connector_shape(); + void validate_connector_settings(); + bool process_cut_line(SLAGizmoEventType action, const Vec2d& mouse_position); }; } // namespace GUI diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index b30c29db1..5f0a110ce 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -1,550 +1,550 @@ -#include "GLGizmoFdmSupports.hpp" - -#include "libslic3r/Model.hpp" - -//#include "slic3r/GUI/3DScene.hpp" -#include "slic3r/GUI/GLCanvas3D.hpp" -#include "slic3r/GUI/GUI_App.hpp" -#include "slic3r/GUI/ImGuiWrapper.hpp" -#include "slic3r/GUI/Plater.hpp" -#include "slic3r/GUI/GUI_ObjectList.hpp" -#include "slic3r/GUI/format.hpp" -#include "slic3r/Utils/UndoRedo.hpp" -#include "libslic3r/Print.hpp" -#include "slic3r/GUI/MsgDialog.hpp" - - -#include - - -namespace Slic3r::GUI { - - - -void GLGizmoFdmSupports::on_shutdown() -{ - m_highlight_by_angle_threshold_deg = 0.f; - m_parent.use_slope(false); - m_parent.toggle_model_objects_visibility(true); -} - - - -std::string GLGizmoFdmSupports::on_get_name() const -{ - return _u8L("Paint-on supports"); -} - - - -bool GLGizmoFdmSupports::on_init() -{ - m_shortcut_key = WXK_CONTROL_L; - - m_desc["auto_generate"] = _L("Auto-generate supports"); - m_desc["generating"] = _L("Generating supports..."); - m_desc["clipping_of_view"] = _L("Clipping of view") + ": "; - m_desc["reset_direction"] = _L("Reset direction"); - m_desc["cursor_size"] = _L("Brush size") + ": "; - m_desc["cursor_type"] = _L("Brush shape") + ": "; - m_desc["enforce_caption"] = _L("Left mouse button") + ": "; - m_desc["enforce"] = _L("Enforce supports"); - m_desc["block_caption"] = _L("Right mouse button") + ": "; - m_desc["block"] = _L("Block supports"); - m_desc["remove_caption"] = _L("Shift + Left mouse button") + ": "; - m_desc["remove"] = _L("Remove selection"); - m_desc["remove_all"] = _L("Remove all selection"); - m_desc["circle"] = _L("Circle"); - m_desc["sphere"] = _L("Sphere"); - m_desc["pointer"] = _L("Triangles"); - m_desc["highlight_by_angle"] = _L("Highlight overhang by angle"); - m_desc["enforce_button"] = _L("Enforce"); - m_desc["cancel"] = _L("Cancel"); - - m_desc["tool_type"] = _L("Tool type") + ": "; - m_desc["tool_brush"] = _L("Brush"); - m_desc["tool_smart_fill"] = _L("Smart fill"); - - m_desc["smart_fill_angle"] = _L("Smart fill angle"); - - m_desc["split_triangles"] = _L("Split triangles"); - m_desc["on_overhangs_only"] = _L("On overhangs only"); - - return true; -} - -void GLGizmoFdmSupports::render_painter_gizmo() -{ - const Selection& selection = m_parent.get_selection(); - - glsafe(::glEnable(GL_BLEND)); - glsafe(::glEnable(GL_DEPTH_TEST)); - - render_triangles(selection); - m_c->object_clipper()->render_cut(); - m_c->instances_hider()->render_cut(); - render_cursor(); - - glsafe(::glDisable(GL_BLEND)); -} - - - -void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_limit) -{ - if (! m_c->selection_info()->model_object()) - return; - - const float approx_height = m_imgui->scaled(25.f); - y = std::min(y, bottom_limit - approx_height); - m_imgui->set_next_window_pos(x, y, ImGuiCond_Always); - - m_imgui->begin(get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); - - // First calculate width of all the texts that are could possibly be shown. We will decide set the dialog width based on that: - const float clipping_slider_left = std::max(m_imgui->calc_text_size(m_desc.at("clipping_of_view")).x, - m_imgui->calc_text_size(m_desc.at("reset_direction")).x) + m_imgui->scaled(1.5f); - const float cursor_slider_left = m_imgui->calc_text_size(m_desc.at("cursor_size")).x + m_imgui->scaled(1.f); - const float smart_fill_slider_left = m_imgui->calc_text_size(m_desc.at("smart_fill_angle")).x + m_imgui->scaled(1.f); - const float autoset_slider_label_max_width = m_imgui->scaled(7.5f); - const float autoset_slider_left = m_imgui->calc_text_size(m_desc.at("highlight_by_angle"), autoset_slider_label_max_width).x + m_imgui->scaled(1.f); - - const float cursor_type_radio_circle = m_imgui->calc_text_size(m_desc["circle"]).x + m_imgui->scaled(2.5f); - const float cursor_type_radio_sphere = m_imgui->calc_text_size(m_desc["sphere"]).x + m_imgui->scaled(2.5f); - const float cursor_type_radio_pointer = m_imgui->calc_text_size(m_desc["pointer"]).x + m_imgui->scaled(2.5f); - - const float button_width = m_imgui->calc_text_size(m_desc.at("remove_all")).x + m_imgui->scaled(1.f); - const float button_enforce_width = m_imgui->calc_text_size(m_desc.at("enforce_button")).x; - const float button_cancel_width = m_imgui->calc_text_size(m_desc.at("cancel")).x; - const float buttons_width = std::max(button_enforce_width, button_cancel_width) + m_imgui->scaled(0.5f); - const float minimal_slider_width = m_imgui->scaled(4.f); - - const float tool_type_radio_left = m_imgui->calc_text_size(m_desc["tool_type"]).x + m_imgui->scaled(1.f); - const float tool_type_radio_brush = m_imgui->calc_text_size(m_desc["tool_brush"]).x + m_imgui->scaled(2.5f); - const float tool_type_radio_smart_fill = m_imgui->calc_text_size(m_desc["tool_smart_fill"]).x + m_imgui->scaled(2.5f); - - const float split_triangles_checkbox_width = m_imgui->calc_text_size(m_desc["split_triangles"]).x + m_imgui->scaled(2.5f); - const float on_overhangs_only_checkbox_width = m_imgui->calc_text_size(m_desc["on_overhangs_only"]).x + m_imgui->scaled(2.5f); - - float caption_max = 0.f; - float total_text_max = 0.f; - for (const auto &t : std::array{"enforce", "block", "remove"}) { - caption_max = std::max(caption_max, m_imgui->calc_text_size(m_desc[t + "_caption"]).x); - total_text_max = std::max(total_text_max, m_imgui->calc_text_size(m_desc[t]).x); - } - total_text_max += caption_max + m_imgui->scaled(1.f); - caption_max += m_imgui->scaled(1.f); - - const float sliders_left_width = std::max(std::max(autoset_slider_left, smart_fill_slider_left), std::max(cursor_slider_left, clipping_slider_left)); - const float slider_icon_width = m_imgui->get_slider_icon_size().x; - float window_width = minimal_slider_width + sliders_left_width + slider_icon_width; - window_width = std::max(window_width, total_text_max); - window_width = std::max(window_width, button_width); - window_width = std::max(window_width, split_triangles_checkbox_width); - window_width = std::max(window_width, on_overhangs_only_checkbox_width); - window_width = std::max(window_width, cursor_type_radio_circle + cursor_type_radio_sphere + cursor_type_radio_pointer); - window_width = std::max(window_width, tool_type_radio_left + tool_type_radio_brush + tool_type_radio_smart_fill); - window_width = std::max(window_width, 2.f * buttons_width + m_imgui->scaled(1.f)); - - auto draw_text_with_caption = [this, &caption_max](const wxString& caption, const wxString& text) { - m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, caption); - ImGui::SameLine(caption_max); - m_imgui->text(text); - }; - - for (const auto &t : std::array{"enforce", "block", "remove"}) - draw_text_with_caption(m_desc.at(t + "_caption"), m_desc.at(t)); - - ImGui::Separator(); - - if (waiting_for_autogenerated_supports) { - m_imgui->text(m_desc.at("generating")); - } else { - bool generate = m_imgui->button(m_desc.at("auto_generate")); - if (generate) - auto_generate(); - } - ImGui::Separator(); - - float position_before_text_y = ImGui::GetCursorPos().y; - ImGui::AlignTextToFramePadding(); - m_imgui->text_wrapped(m_desc["highlight_by_angle"] + ":", autoset_slider_label_max_width); - ImGui::AlignTextToFramePadding(); - float position_after_text_y = ImGui::GetCursorPos().y; - std::string format_str = std::string("%.f") + I18N::translate_utf8("°", - "Degree sign to use in the respective slider in FDM supports gizmo," - "placed after the number with no whitespace in between."); - ImGui::SameLine(sliders_left_width); - - float slider_height = m_imgui->get_slider_float_height(); - // Makes slider to be aligned to bottom of the multi-line text. - float slider_start_position_y = std::max(position_before_text_y, position_after_text_y - slider_height); - ImGui::SetCursorPosY(slider_start_position_y); - - ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); - wxString tooltip = format_wxstr(_L("Preselects faces by overhang angle. It is possible to restrict paintable facets to only preselected faces when " - "the option \"%1%\" is enabled."), m_desc["on_overhangs_only"]); - if (m_imgui->slider_float("##angle_threshold_deg", &m_highlight_by_angle_threshold_deg, 0.f, 90.f, format_str.data(), 1.0f, true, tooltip)) { - m_parent.set_slope_normal_angle(90.f - m_highlight_by_angle_threshold_deg); - if (! m_parent.is_using_slope()) { - m_parent.use_slope(true); - m_parent.set_as_dirty(); - } - } - - // Restores the cursor position to be below the multi-line text. - ImGui::SetCursorPosY(std::max(position_before_text_y + slider_height, position_after_text_y)); - - const float max_tooltip_width = ImGui::GetFontSize() * 20.0f; - - m_imgui->disabled_begin(m_highlight_by_angle_threshold_deg == 0.f); - ImGui::NewLine(); - ImGui::SameLine(window_width - 2.f*buttons_width - m_imgui->scaled(0.5f)); - if (m_imgui->button(m_desc["enforce_button"], buttons_width, 0.f)) { - select_facets_by_angle(m_highlight_by_angle_threshold_deg, false); - m_highlight_by_angle_threshold_deg = 0.f; - m_parent.use_slope(false); - } - ImGui::SameLine(window_width - buttons_width); - if (m_imgui->button(m_desc["cancel"], buttons_width, 0.f)) { - m_highlight_by_angle_threshold_deg = 0.f; - m_parent.use_slope(false); - } - m_imgui->disabled_end(); - - - ImGui::Separator(); - - ImGui::AlignTextToFramePadding(); - m_imgui->text(m_desc["tool_type"]); - - float tool_type_offset = tool_type_radio_left + (window_width - tool_type_radio_left - tool_type_radio_brush - tool_type_radio_smart_fill + m_imgui->scaled(0.5f)) / 2.f; - ImGui::SameLine(tool_type_offset); - ImGui::PushItemWidth(tool_type_radio_brush); - if (m_imgui->radio_button(m_desc["tool_brush"], m_tool_type == ToolType::BRUSH)) - m_tool_type = ToolType::BRUSH; - - if (ImGui::IsItemHovered()) - m_imgui->tooltip(_L("Paints facets according to the chosen painting brush."), max_tooltip_width); - - ImGui::SameLine(tool_type_offset + tool_type_radio_brush); - ImGui::PushItemWidth(tool_type_radio_smart_fill); - if (m_imgui->radio_button(m_desc["tool_smart_fill"], m_tool_type == ToolType::SMART_FILL)) - m_tool_type = ToolType::SMART_FILL; - - if (ImGui::IsItemHovered()) - m_imgui->tooltip(_L("Paints neighboring facets whose relative angle is less or equal to set angle."), max_tooltip_width); - - m_imgui->checkbox(m_desc["on_overhangs_only"], m_paint_on_overhangs_only); - if (ImGui::IsItemHovered()) - m_imgui->tooltip(format_wxstr(_L("Allows painting only on facets selected by: \"%1%\""), m_desc["highlight_by_angle"]), max_tooltip_width); - - ImGui::Separator(); - - if (m_tool_type == ToolType::BRUSH) { - m_imgui->text(m_desc.at("cursor_type")); - ImGui::NewLine(); - - float cursor_type_offset = (window_width - cursor_type_radio_sphere - cursor_type_radio_circle - cursor_type_radio_pointer + m_imgui->scaled(1.5f)) / 2.f; - ImGui::SameLine(cursor_type_offset); - ImGui::PushItemWidth(cursor_type_radio_sphere); - if (m_imgui->radio_button(m_desc["sphere"], m_cursor_type == TriangleSelector::CursorType::SPHERE)) - m_cursor_type = TriangleSelector::CursorType::SPHERE; - - if (ImGui::IsItemHovered()) - m_imgui->tooltip(_L("Paints all facets inside, regardless of their orientation."), max_tooltip_width); - - ImGui::SameLine(cursor_type_offset + cursor_type_radio_sphere); - ImGui::PushItemWidth(cursor_type_radio_circle); - - if (m_imgui->radio_button(m_desc["circle"], m_cursor_type == TriangleSelector::CursorType::CIRCLE)) - m_cursor_type = TriangleSelector::CursorType::CIRCLE; - - if (ImGui::IsItemHovered()) - m_imgui->tooltip(_L("Ignores facets facing away from the camera."), max_tooltip_width); - - ImGui::SameLine(cursor_type_offset + cursor_type_radio_sphere + cursor_type_radio_circle); - ImGui::PushItemWidth(cursor_type_radio_pointer); - - if (m_imgui->radio_button(m_desc["pointer"], m_cursor_type == TriangleSelector::CursorType::POINTER)) - m_cursor_type = TriangleSelector::CursorType::POINTER; - - if (ImGui::IsItemHovered()) - m_imgui->tooltip(_L("Paints only one facet."), max_tooltip_width); - - m_imgui->disabled_begin(m_cursor_type != TriangleSelector::CursorType::SPHERE && m_cursor_type != TriangleSelector::CursorType::CIRCLE); - - ImGui::AlignTextToFramePadding(); - m_imgui->text(m_desc.at("cursor_size")); - ImGui::SameLine(sliders_left_width); - ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); - m_imgui->slider_float("##cursor_radius", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f", 1.0f, true, _L("Alt + Mouse wheel")); - - m_imgui->checkbox(m_desc["split_triangles"], m_triangle_splitting_enabled); - - if (ImGui::IsItemHovered()) - m_imgui->tooltip(_L("Splits bigger facets into smaller ones while the object is painted."), max_tooltip_width); - - m_imgui->disabled_end(); - } else { - assert(m_tool_type == ToolType::SMART_FILL); - ImGui::AlignTextToFramePadding(); - m_imgui->text(m_desc["smart_fill_angle"] + ":"); - - ImGui::SameLine(sliders_left_width); - ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); - if (m_imgui->slider_float("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, format_str.data(), 1.0f, true, _L("Alt + Mouse wheel"))) - for (auto &triangle_selector : m_triangle_selectors) { - triangle_selector->seed_fill_unselect_all_triangles(); - triangle_selector->request_update_render_data(); - } - } - - ImGui::Separator(); - if (m_c->object_clipper()->get_position() == 0.f) { - ImGui::AlignTextToFramePadding(); - m_imgui->text(m_desc.at("clipping_of_view")); - } - else { - if (m_imgui->button(m_desc.at("reset_direction"))) { - wxGetApp().CallAfter([this](){ - m_c->object_clipper()->set_position(-1., false); - }); - } - } - - auto clp_dist = float(m_c->object_clipper()->get_position()); - ImGui::SameLine(sliders_left_width); - ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); - if (m_imgui->slider_float("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true, _L("Ctrl + Mouse wheel"))) - m_c->object_clipper()->set_position(clp_dist, true); - - ImGui::Separator(); - if (m_imgui->button(m_desc.at("remove_all"))) { - Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Reset selection"), UndoRedo::SnapshotType::GizmoAction); - ModelObject *mo = m_c->selection_info()->model_object(); - int idx = -1; - for (ModelVolume *mv : mo->volumes) - if (mv->is_model_part()) { - ++idx; - m_triangle_selectors[idx]->reset(); - m_triangle_selectors[idx]->request_update_render_data(); - } - - update_model_object(); - this->waiting_for_autogenerated_supports = false; - m_parent.set_as_dirty(); - } - - m_imgui->end(); -} - - - -void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) -{ - float threshold = (float(M_PI)/180.f)*threshold_deg; - const Selection& selection = m_parent.get_selection(); - const ModelObject* mo = m_c->selection_info()->model_object(); - const ModelInstance* mi = mo->instances[selection.get_instance_idx()]; - - int mesh_id = -1; - for (const ModelVolume* mv : mo->volumes) { - if (! mv->is_model_part()) - continue; - - ++mesh_id; - -#if ENABLE_WORLD_COORDINATE - const Transform3d trafo_matrix = mi->get_matrix_no_offset() * mv->get_matrix_no_offset(); -#else - const Transform3d trafo_matrix = mi->get_matrix(true) * mv->get_matrix(true); -#endif // ENABLE_WORLD_COORDINATE - Vec3f down = (trafo_matrix.inverse() * (-Vec3d::UnitZ())).cast().normalized(); - Vec3f limit = (trafo_matrix.inverse() * Vec3d(std::sin(threshold), 0, -std::cos(threshold))).cast().normalized(); - - float dot_limit = limit.dot(down); - - // Now calculate dot product of vert_direction and facets' normals. - int idx = 0; - const indexed_triangle_set &its = mv->mesh().its; - for (const stl_triangle_vertex_indices &face : its.indices) { - if (its_face_normal(its, face).dot(down) > dot_limit) { - m_triangle_selectors[mesh_id]->set_facet(idx, block ? EnforcerBlockerType::BLOCKER : EnforcerBlockerType::ENFORCER); - m_triangle_selectors.back()->request_update_render_data(); - } - ++ idx; - } - } - - Plater::TakeSnapshot snapshot(wxGetApp().plater(), block ? _L("Block supports by angle") - : _L("Add supports by angle")); - update_model_object(); - this->waiting_for_autogenerated_supports = false; - m_parent.set_as_dirty(); -} - -void GLGizmoFdmSupports::data_changed() -{ - GLGizmoPainterBase::data_changed(); - if (! m_c->selection_info()) - return; - - ModelObject* mo = m_c->selection_info()->model_object(); - if (mo && this->waiting_for_autogenerated_supports) { - get_data_from_backend(); - } else { - this->waiting_for_autogenerated_supports = false; - } -} - -void GLGizmoFdmSupports::get_data_from_backend() -{ - if (! has_backend_supports()) - return; - ModelObject* mo = m_c->selection_info()->model_object(); - - // find the respective PrintObject, we need a pointer to it - for (const PrintObject* po : m_parent.fff_print()->objects()) { - if (po->model_object()->id() == mo->id()) { - std::unordered_map mvs; - for (const ModelVolume* mv : po->model_object()->volumes) { - if (mv->is_model_part()) { - mvs.emplace(mv->id().id, mv); - } - } - int mesh_id = -1.0f; - for (ModelVolume* mv : mo->volumes){ - if (mv->is_model_part()){ - mesh_id++; - mv->supported_facets.assign(mvs[mv->id().id]->supported_facets); - m_triangle_selectors[mesh_id]->deserialize(mv->supported_facets.get_data(), true); - m_triangle_selectors[mesh_id]->request_update_render_data(); - } - } - this->waiting_for_autogenerated_supports = false; - m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); - m_parent.set_as_dirty(); - } - } -} - - -void GLGizmoFdmSupports::update_model_object() const -{ - bool updated = false; - ModelObject* mo = m_c->selection_info()->model_object(); - int idx = -1; - for (ModelVolume* mv : mo->volumes) { - if (! mv->is_model_part()) - continue; - ++idx; - updated |= mv->supported_facets.set(*m_triangle_selectors[idx].get()); - } - - if (updated) { - const ModelObjectPtrs& mos = wxGetApp().model().objects; - wxGetApp().obj_list()->update_info_items(std::find(mos.begin(), mos.end(), mo) - mos.begin()); - - m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); - } -} - -void GLGizmoFdmSupports::update_from_model_object() -{ - wxBusyCursor wait; - - const ModelObject* mo = m_c->selection_info()->model_object(); - m_triangle_selectors.clear(); - - int volume_id = -1; - for (const ModelVolume* mv : mo->volumes) { - if (! mv->is_model_part()) - continue; - - ++volume_id; - - // This mesh does not account for the possible Z up SLA offset. - const TriangleMesh* mesh = &mv->mesh(); - - m_triangle_selectors.emplace_back(std::make_unique(*mesh)); - // Reset of TriangleSelector is done inside TriangleSelectorGUI's constructor, so we don't need it to perform it again in deserialize(). - m_triangle_selectors.back()->deserialize(mv->supported_facets.get_data(), false); - m_triangle_selectors.back()->request_update_render_data(); - } -} - -bool GLGizmoFdmSupports::has_backend_supports() const -{ - const ModelObject* mo = m_c->selection_info()->model_object(); - if (! mo) - return false; - - // find PrintObject with this ID - for (const PrintObject* po : m_parent.fff_print()->objects()) { - if (po->model_object()->id() == mo->id()) - return po->is_step_done(posSupportSpotsSearch); - } - return false; -} - -void GLGizmoFdmSupports::reslice_FDM_supports(bool postpone_error_messages) const { - wxGetApp().CallAfter( - [this, postpone_error_messages]() { - wxGetApp().plater()->reslice_FFF_until_step(posSupportSpotsSearch, - *m_c->selection_info()->model_object(), postpone_error_messages); - }); -} - -void GLGizmoFdmSupports::auto_generate() -{ - ModelObject *mo = m_c->selection_info()->model_object(); - bool not_painted = std::all_of(mo->volumes.begin(), mo->volumes.end(), [](const ModelVolume* vol){ - return vol->type() != ModelVolumeType::MODEL_PART || vol->supported_facets.empty(); - }); - - MessageDialog dlg(GUI::wxGetApp().plater(), - _L("Autogeneration will erase all currently painted areas.") + "\n\n" + - _L("Are you sure you want to do it?") + "\n", - _L("Warning"), wxICON_WARNING | wxYES | wxNO); - - if (not_painted || dlg.ShowModal() == wxID_YES) { - Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Autogenerate support points")); - int mesh_id = -1.0f; - for (ModelVolume *mv : mo->volumes) { - if (mv->is_model_part()) { - mesh_id++; - mv->supported_facets.reset(); - m_triangle_selectors[mesh_id]->reset(); - m_triangle_selectors[mesh_id]->request_update_render_data(); - } - } - - mo->config.set("support_material", true); - mo->config.set("support_material_auto", false); - this->waiting_for_autogenerated_supports = true; - wxGetApp().CallAfter([this]() { reslice_FDM_supports(); }); - } -} - - -PainterGizmoType GLGizmoFdmSupports::get_painter_type() const -{ - return PainterGizmoType::FDM_SUPPORTS; -} - -wxString GLGizmoFdmSupports::handle_snapshot_action_name(bool shift_down, GLGizmoPainterBase::Button button_down) const -{ - wxString action_name; - if (shift_down) - action_name = _L("Remove selection"); - else { - if (button_down == Button::Left) - action_name = _L("Add supports"); - else - action_name = _L("Block supports"); - } - return action_name; -} - -} // namespace Slic3r::GUI +#include "GLGizmoFdmSupports.hpp" + +#include "libslic3r/Model.hpp" + +//#include "slic3r/GUI/3DScene.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/ImGuiWrapper.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/GUI/format.hpp" +#include "slic3r/Utils/UndoRedo.hpp" +#include "libslic3r/Print.hpp" +#include "slic3r/GUI/MsgDialog.hpp" + + +#include + + +namespace Slic3r::GUI { + + + +void GLGizmoFdmSupports::on_shutdown() +{ + m_highlight_by_angle_threshold_deg = 0.f; + m_parent.use_slope(false); + m_parent.toggle_model_objects_visibility(true); +} + + + +std::string GLGizmoFdmSupports::on_get_name() const +{ + return _u8L("Paint-on supports"); +} + + + +bool GLGizmoFdmSupports::on_init() +{ + m_shortcut_key = WXK_CONTROL_L; + + m_desc["auto_generate"] = _L("Auto-generate supports"); + m_desc["generating"] = _L("Generating supports..."); + m_desc["clipping_of_view"] = _L("Clipping of view") + ": "; + m_desc["reset_direction"] = _L("Reset direction"); + m_desc["cursor_size"] = _L("Brush size") + ": "; + m_desc["cursor_type"] = _L("Brush shape") + ": "; + m_desc["enforce_caption"] = _L("Left mouse button") + ": "; + m_desc["enforce"] = _L("Enforce supports"); + m_desc["block_caption"] = _L("Right mouse button") + ": "; + m_desc["block"] = _L("Block supports"); + m_desc["remove_caption"] = _L("Shift + Left mouse button") + ": "; + m_desc["remove"] = _L("Remove selection"); + m_desc["remove_all"] = _L("Remove all selection"); + m_desc["circle"] = _L("Circle"); + m_desc["sphere"] = _L("Sphere"); + m_desc["pointer"] = _L("Triangles"); + m_desc["highlight_by_angle"] = _L("Highlight overhang by angle"); + m_desc["enforce_button"] = _L("Enforce"); + m_desc["cancel"] = _L("Cancel"); + + m_desc["tool_type"] = _L("Tool type") + ": "; + m_desc["tool_brush"] = _L("Brush"); + m_desc["tool_smart_fill"] = _L("Smart fill"); + + m_desc["smart_fill_angle"] = _L("Smart fill angle"); + + m_desc["split_triangles"] = _L("Split triangles"); + m_desc["on_overhangs_only"] = _L("On overhangs only"); + + return true; +} + +void GLGizmoFdmSupports::render_painter_gizmo() +{ + const Selection& selection = m_parent.get_selection(); + + glsafe(::glEnable(GL_BLEND)); + glsafe(::glEnable(GL_DEPTH_TEST)); + + render_triangles(selection); + m_c->object_clipper()->render_cut(); + m_c->instances_hider()->render_cut(); + render_cursor(); + + glsafe(::glDisable(GL_BLEND)); +} + + + +void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_limit) +{ + if (! m_c->selection_info()->model_object()) + return; + + const float approx_height = m_imgui->scaled(25.f); + y = std::min(y, bottom_limit - approx_height); + m_imgui->set_next_window_pos(x, y, ImGuiCond_Always); + + m_imgui->begin(get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); + + // First calculate width of all the texts that are could possibly be shown. We will decide set the dialog width based on that: + const float clipping_slider_left = std::max(m_imgui->calc_text_size(m_desc.at("clipping_of_view")).x, + m_imgui->calc_text_size(m_desc.at("reset_direction")).x) + m_imgui->scaled(1.5f); + const float cursor_slider_left = m_imgui->calc_text_size(m_desc.at("cursor_size")).x + m_imgui->scaled(1.f); + const float smart_fill_slider_left = m_imgui->calc_text_size(m_desc.at("smart_fill_angle")).x + m_imgui->scaled(1.f); + const float autoset_slider_label_max_width = m_imgui->scaled(7.5f); + const float autoset_slider_left = m_imgui->calc_text_size(m_desc.at("highlight_by_angle"), autoset_slider_label_max_width).x + m_imgui->scaled(1.f); + + const float cursor_type_radio_circle = m_imgui->calc_text_size(m_desc["circle"]).x + m_imgui->scaled(2.5f); + const float cursor_type_radio_sphere = m_imgui->calc_text_size(m_desc["sphere"]).x + m_imgui->scaled(2.5f); + const float cursor_type_radio_pointer = m_imgui->calc_text_size(m_desc["pointer"]).x + m_imgui->scaled(2.5f); + + const float button_width = m_imgui->calc_text_size(m_desc.at("remove_all")).x + m_imgui->scaled(1.f); + const float button_enforce_width = m_imgui->calc_text_size(m_desc.at("enforce_button")).x; + const float button_cancel_width = m_imgui->calc_text_size(m_desc.at("cancel")).x; + const float buttons_width = std::max(button_enforce_width, button_cancel_width) + m_imgui->scaled(0.5f); + const float minimal_slider_width = m_imgui->scaled(4.f); + + const float tool_type_radio_left = m_imgui->calc_text_size(m_desc["tool_type"]).x + m_imgui->scaled(1.f); + const float tool_type_radio_brush = m_imgui->calc_text_size(m_desc["tool_brush"]).x + m_imgui->scaled(2.5f); + const float tool_type_radio_smart_fill = m_imgui->calc_text_size(m_desc["tool_smart_fill"]).x + m_imgui->scaled(2.5f); + + const float split_triangles_checkbox_width = m_imgui->calc_text_size(m_desc["split_triangles"]).x + m_imgui->scaled(2.5f); + const float on_overhangs_only_checkbox_width = m_imgui->calc_text_size(m_desc["on_overhangs_only"]).x + m_imgui->scaled(2.5f); + + float caption_max = 0.f; + float total_text_max = 0.f; + for (const auto &t : std::array{"enforce", "block", "remove"}) { + caption_max = std::max(caption_max, m_imgui->calc_text_size(m_desc[t + "_caption"]).x); + total_text_max = std::max(total_text_max, m_imgui->calc_text_size(m_desc[t]).x); + } + total_text_max += caption_max + m_imgui->scaled(1.f); + caption_max += m_imgui->scaled(1.f); + + const float sliders_left_width = std::max(std::max(autoset_slider_left, smart_fill_slider_left), std::max(cursor_slider_left, clipping_slider_left)); + const float slider_icon_width = m_imgui->get_slider_icon_size().x; + float window_width = minimal_slider_width + sliders_left_width + slider_icon_width; + window_width = std::max(window_width, total_text_max); + window_width = std::max(window_width, button_width); + window_width = std::max(window_width, split_triangles_checkbox_width); + window_width = std::max(window_width, on_overhangs_only_checkbox_width); + window_width = std::max(window_width, cursor_type_radio_circle + cursor_type_radio_sphere + cursor_type_radio_pointer); + window_width = std::max(window_width, tool_type_radio_left + tool_type_radio_brush + tool_type_radio_smart_fill); + window_width = std::max(window_width, 2.f * buttons_width + m_imgui->scaled(1.f)); + + auto draw_text_with_caption = [this, &caption_max](const wxString& caption, const wxString& text) { + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, caption); + ImGui::SameLine(caption_max); + m_imgui->text(text); + }; + + for (const auto &t : std::array{"enforce", "block", "remove"}) + draw_text_with_caption(m_desc.at(t + "_caption"), m_desc.at(t)); + + ImGui::Separator(); + + if (waiting_for_autogenerated_supports) { + m_imgui->text(m_desc.at("generating")); + } else { + bool generate = m_imgui->button(m_desc.at("auto_generate")); + if (generate) + auto_generate(); + } + ImGui::Separator(); + + float position_before_text_y = ImGui::GetCursorPos().y; + ImGui::AlignTextToFramePadding(); + m_imgui->text_wrapped(m_desc["highlight_by_angle"] + ":", autoset_slider_label_max_width); + ImGui::AlignTextToFramePadding(); + float position_after_text_y = ImGui::GetCursorPos().y; + std::string format_str = std::string("%.f") + I18N::translate_utf8("°", + "Degree sign to use in the respective slider in FDM supports gizmo," + "placed after the number with no whitespace in between."); + ImGui::SameLine(sliders_left_width); + + float slider_height = m_imgui->get_slider_float_height(); + // Makes slider to be aligned to bottom of the multi-line text. + float slider_start_position_y = std::max(position_before_text_y, position_after_text_y - slider_height); + ImGui::SetCursorPosY(slider_start_position_y); + + ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); + wxString tooltip = format_wxstr(_L("Preselects faces by overhang angle. It is possible to restrict paintable facets to only preselected faces when " + "the option \"%1%\" is enabled."), m_desc["on_overhangs_only"]); + if (m_imgui->slider_float("##angle_threshold_deg", &m_highlight_by_angle_threshold_deg, 0.f, 90.f, format_str.data(), 1.0f, true, tooltip)) { + m_parent.set_slope_normal_angle(90.f - m_highlight_by_angle_threshold_deg); + if (! m_parent.is_using_slope()) { + m_parent.use_slope(true); + m_parent.set_as_dirty(); + } + } + + // Restores the cursor position to be below the multi-line text. + ImGui::SetCursorPosY(std::max(position_before_text_y + slider_height, position_after_text_y)); + + const float max_tooltip_width = ImGui::GetFontSize() * 20.0f; + + m_imgui->disabled_begin(m_highlight_by_angle_threshold_deg == 0.f); + ImGui::NewLine(); + ImGui::SameLine(window_width - 2.f*buttons_width - m_imgui->scaled(0.5f)); + if (m_imgui->button(m_desc["enforce_button"], buttons_width, 0.f)) { + select_facets_by_angle(m_highlight_by_angle_threshold_deg, false); + m_highlight_by_angle_threshold_deg = 0.f; + m_parent.use_slope(false); + } + ImGui::SameLine(window_width - buttons_width); + if (m_imgui->button(m_desc["cancel"], buttons_width, 0.f)) { + m_highlight_by_angle_threshold_deg = 0.f; + m_parent.use_slope(false); + } + m_imgui->disabled_end(); + + + ImGui::Separator(); + + ImGui::AlignTextToFramePadding(); + m_imgui->text(m_desc["tool_type"]); + + float tool_type_offset = tool_type_radio_left + (window_width - tool_type_radio_left - tool_type_radio_brush - tool_type_radio_smart_fill + m_imgui->scaled(0.5f)) / 2.f; + ImGui::SameLine(tool_type_offset); + ImGui::PushItemWidth(tool_type_radio_brush); + if (m_imgui->radio_button(m_desc["tool_brush"], m_tool_type == ToolType::BRUSH)) + m_tool_type = ToolType::BRUSH; + + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_L("Paints facets according to the chosen painting brush."), max_tooltip_width); + + ImGui::SameLine(tool_type_offset + tool_type_radio_brush); + ImGui::PushItemWidth(tool_type_radio_smart_fill); + if (m_imgui->radio_button(m_desc["tool_smart_fill"], m_tool_type == ToolType::SMART_FILL)) + m_tool_type = ToolType::SMART_FILL; + + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_L("Paints neighboring facets whose relative angle is less or equal to set angle."), max_tooltip_width); + + m_imgui->checkbox(m_desc["on_overhangs_only"], m_paint_on_overhangs_only); + if (ImGui::IsItemHovered()) + m_imgui->tooltip(format_wxstr(_L("Allows painting only on facets selected by: \"%1%\""), m_desc["highlight_by_angle"]), max_tooltip_width); + + ImGui::Separator(); + + if (m_tool_type == ToolType::BRUSH) { + m_imgui->text(m_desc.at("cursor_type")); + ImGui::NewLine(); + + float cursor_type_offset = (window_width - cursor_type_radio_sphere - cursor_type_radio_circle - cursor_type_radio_pointer + m_imgui->scaled(1.5f)) / 2.f; + ImGui::SameLine(cursor_type_offset); + ImGui::PushItemWidth(cursor_type_radio_sphere); + if (m_imgui->radio_button(m_desc["sphere"], m_cursor_type == TriangleSelector::CursorType::SPHERE)) + m_cursor_type = TriangleSelector::CursorType::SPHERE; + + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_L("Paints all facets inside, regardless of their orientation."), max_tooltip_width); + + ImGui::SameLine(cursor_type_offset + cursor_type_radio_sphere); + ImGui::PushItemWidth(cursor_type_radio_circle); + + if (m_imgui->radio_button(m_desc["circle"], m_cursor_type == TriangleSelector::CursorType::CIRCLE)) + m_cursor_type = TriangleSelector::CursorType::CIRCLE; + + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_L("Ignores facets facing away from the camera."), max_tooltip_width); + + ImGui::SameLine(cursor_type_offset + cursor_type_radio_sphere + cursor_type_radio_circle); + ImGui::PushItemWidth(cursor_type_radio_pointer); + + if (m_imgui->radio_button(m_desc["pointer"], m_cursor_type == TriangleSelector::CursorType::POINTER)) + m_cursor_type = TriangleSelector::CursorType::POINTER; + + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_L("Paints only one facet."), max_tooltip_width); + + m_imgui->disabled_begin(m_cursor_type != TriangleSelector::CursorType::SPHERE && m_cursor_type != TriangleSelector::CursorType::CIRCLE); + + ImGui::AlignTextToFramePadding(); + m_imgui->text(m_desc.at("cursor_size")); + ImGui::SameLine(sliders_left_width); + ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); + m_imgui->slider_float("##cursor_radius", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f", 1.0f, true, _L("Alt + Mouse wheel")); + + m_imgui->checkbox(m_desc["split_triangles"], m_triangle_splitting_enabled); + + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_L("Splits bigger facets into smaller ones while the object is painted."), max_tooltip_width); + + m_imgui->disabled_end(); + } else { + assert(m_tool_type == ToolType::SMART_FILL); + ImGui::AlignTextToFramePadding(); + m_imgui->text(m_desc["smart_fill_angle"] + ":"); + + ImGui::SameLine(sliders_left_width); + ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); + if (m_imgui->slider_float("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, format_str.data(), 1.0f, true, _L("Alt + Mouse wheel"))) + for (auto &triangle_selector : m_triangle_selectors) { + triangle_selector->seed_fill_unselect_all_triangles(); + triangle_selector->request_update_render_data(); + } + } + + ImGui::Separator(); + if (m_c->object_clipper()->get_position() == 0.f) { + ImGui::AlignTextToFramePadding(); + m_imgui->text(m_desc.at("clipping_of_view")); + } + else { + if (m_imgui->button(m_desc.at("reset_direction"))) { + wxGetApp().CallAfter([this](){ + m_c->object_clipper()->set_position_by_ratio(-1., false); + }); + } + } + + auto clp_dist = float(m_c->object_clipper()->get_position()); + ImGui::SameLine(sliders_left_width); + ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); + if (m_imgui->slider_float("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true, _L("Ctrl + Mouse wheel"))) + m_c->object_clipper()->set_position_by_ratio(clp_dist, true); + + ImGui::Separator(); + if (m_imgui->button(m_desc.at("remove_all"))) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Reset selection"), UndoRedo::SnapshotType::GizmoAction); + ModelObject *mo = m_c->selection_info()->model_object(); + int idx = -1; + for (ModelVolume *mv : mo->volumes) + if (mv->is_model_part()) { + ++idx; + m_triangle_selectors[idx]->reset(); + m_triangle_selectors[idx]->request_update_render_data(); + } + + update_model_object(); + this->waiting_for_autogenerated_supports = false; + m_parent.set_as_dirty(); + } + + m_imgui->end(); +} + + + +void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) +{ + float threshold = (float(M_PI)/180.f)*threshold_deg; + const Selection& selection = m_parent.get_selection(); + const ModelObject* mo = m_c->selection_info()->model_object(); + const ModelInstance* mi = mo->instances[selection.get_instance_idx()]; + + int mesh_id = -1; + for (const ModelVolume* mv : mo->volumes) { + if (! mv->is_model_part()) + continue; + + ++mesh_id; + +#if ENABLE_WORLD_COORDINATE + const Transform3d trafo_matrix = mi->get_matrix_no_offset() * mv->get_matrix_no_offset(); +#else + const Transform3d trafo_matrix = mi->get_matrix(true) * mv->get_matrix(true); +#endif // ENABLE_WORLD_COORDINATE + Vec3f down = (trafo_matrix.inverse() * (-Vec3d::UnitZ())).cast().normalized(); + Vec3f limit = (trafo_matrix.inverse() * Vec3d(std::sin(threshold), 0, -std::cos(threshold))).cast().normalized(); + + float dot_limit = limit.dot(down); + + // Now calculate dot product of vert_direction and facets' normals. + int idx = 0; + const indexed_triangle_set &its = mv->mesh().its; + for (const stl_triangle_vertex_indices &face : its.indices) { + if (its_face_normal(its, face).dot(down) > dot_limit) { + m_triangle_selectors[mesh_id]->set_facet(idx, block ? EnforcerBlockerType::BLOCKER : EnforcerBlockerType::ENFORCER); + m_triangle_selectors.back()->request_update_render_data(); + } + ++ idx; + } + } + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), block ? _L("Block supports by angle") + : _L("Add supports by angle")); + update_model_object(); + this->waiting_for_autogenerated_supports = false; + m_parent.set_as_dirty(); +} + +void GLGizmoFdmSupports::data_changed() +{ + GLGizmoPainterBase::data_changed(); + if (! m_c->selection_info()) + return; + + ModelObject* mo = m_c->selection_info()->model_object(); + if (mo && this->waiting_for_autogenerated_supports) { + get_data_from_backend(); + } else { + this->waiting_for_autogenerated_supports = false; + } +} + +void GLGizmoFdmSupports::get_data_from_backend() +{ + if (! has_backend_supports()) + return; + ModelObject* mo = m_c->selection_info()->model_object(); + + // find the respective PrintObject, we need a pointer to it + for (const PrintObject* po : m_parent.fff_print()->objects()) { + if (po->model_object()->id() == mo->id()) { + std::unordered_map mvs; + for (const ModelVolume* mv : po->model_object()->volumes) { + if (mv->is_model_part()) { + mvs.emplace(mv->id().id, mv); + } + } + int mesh_id = -1.0f; + for (ModelVolume* mv : mo->volumes){ + if (mv->is_model_part()){ + mesh_id++; + mv->supported_facets.assign(mvs[mv->id().id]->supported_facets); + m_triangle_selectors[mesh_id]->deserialize(mv->supported_facets.get_data(), true); + m_triangle_selectors[mesh_id]->request_update_render_data(); + } + } + this->waiting_for_autogenerated_supports = false; + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); + m_parent.set_as_dirty(); + } + } +} + + +void GLGizmoFdmSupports::update_model_object() const +{ + bool updated = false; + ModelObject* mo = m_c->selection_info()->model_object(); + int idx = -1; + for (ModelVolume* mv : mo->volumes) { + if (! mv->is_model_part()) + continue; + ++idx; + updated |= mv->supported_facets.set(*m_triangle_selectors[idx].get()); + } + + if (updated) { + const ModelObjectPtrs& mos = wxGetApp().model().objects; + wxGetApp().obj_list()->update_info_items(std::find(mos.begin(), mos.end(), mo) - mos.begin()); + + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); + } +} + +void GLGizmoFdmSupports::update_from_model_object() +{ + wxBusyCursor wait; + + const ModelObject* mo = m_c->selection_info()->model_object(); + m_triangle_selectors.clear(); + + int volume_id = -1; + for (const ModelVolume* mv : mo->volumes) { + if (! mv->is_model_part()) + continue; + + ++volume_id; + + // This mesh does not account for the possible Z up SLA offset. + const TriangleMesh* mesh = &mv->mesh(); + + m_triangle_selectors.emplace_back(std::make_unique(*mesh)); + // Reset of TriangleSelector is done inside TriangleSelectorGUI's constructor, so we don't need it to perform it again in deserialize(). + m_triangle_selectors.back()->deserialize(mv->supported_facets.get_data(), false); + m_triangle_selectors.back()->request_update_render_data(); + } +} + +bool GLGizmoFdmSupports::has_backend_supports() const +{ + const ModelObject* mo = m_c->selection_info()->model_object(); + if (! mo) + return false; + + // find PrintObject with this ID + for (const PrintObject* po : m_parent.fff_print()->objects()) { + if (po->model_object()->id() == mo->id()) + return po->is_step_done(posSupportSpotsSearch); + } + return false; +} + +void GLGizmoFdmSupports::reslice_FDM_supports(bool postpone_error_messages) const { + wxGetApp().CallAfter( + [this, postpone_error_messages]() { + wxGetApp().plater()->reslice_FFF_until_step(posSupportSpotsSearch, + *m_c->selection_info()->model_object(), postpone_error_messages); + }); +} + +void GLGizmoFdmSupports::auto_generate() +{ + ModelObject *mo = m_c->selection_info()->model_object(); + bool not_painted = std::all_of(mo->volumes.begin(), mo->volumes.end(), [](const ModelVolume* vol){ + return vol->type() != ModelVolumeType::MODEL_PART || vol->supported_facets.empty(); + }); + + MessageDialog dlg(GUI::wxGetApp().plater(), + _L("Autogeneration will erase all currently painted areas.") + "\n\n" + + _L("Are you sure you want to do it?") + "\n", + _L("Warning"), wxICON_WARNING | wxYES | wxNO); + + if (not_painted || dlg.ShowModal() == wxID_YES) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Autogenerate support points")); + int mesh_id = -1.0f; + for (ModelVolume *mv : mo->volumes) { + if (mv->is_model_part()) { + mesh_id++; + mv->supported_facets.reset(); + m_triangle_selectors[mesh_id]->reset(); + m_triangle_selectors[mesh_id]->request_update_render_data(); + } + } + + mo->config.set("support_material", true); + mo->config.set("support_material_auto", false); + this->waiting_for_autogenerated_supports = true; + wxGetApp().CallAfter([this]() { reslice_FDM_supports(); }); + } +} + + +PainterGizmoType GLGizmoFdmSupports::get_painter_type() const +{ + return PainterGizmoType::FDM_SUPPORTS; +} + +wxString GLGizmoFdmSupports::handle_snapshot_action_name(bool shift_down, GLGizmoPainterBase::Button button_down) const +{ + wxString action_name; + if (shift_down) + action_name = _L("Remove selection"); + else { + if (button_down == Button::Left) + action_name = _L("Add supports"); + else + action_name = _L("Block supports"); + } + return action_name; +} + +} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFlatten.hpp b/src/slic3r/GUI/Gizmos/GLGizmoFlatten.hpp index f1813c4bd..b4e47d398 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 bbae3f242..1ce118d71 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(); } @@ -478,19 +478,19 @@ bool GLGizmoHollow::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_pos if (action == SLAGizmoEventType::MouseWheelUp && control_down) { double pos = m_c->object_clipper()->get_position(); pos = std::min(1., pos + 0.01); - m_c->object_clipper()->set_position(pos, true); + m_c->object_clipper()->set_position_by_ratio(pos, true); return true; } if (action == SLAGizmoEventType::MouseWheelDown && control_down) { double pos = m_c->object_clipper()->get_position(); pos = std::max(0., pos - 0.01); - m_c->object_clipper()->set_position(pos, true); + m_c->object_clipper()->set_position_by_ratio(pos, true); return true; } if (action == SLAGizmoEventType::ResetClippingPlane) { - m_c->object_clipper()->set_position(-1., false); + m_c->object_clipper()->set_position_by_ratio(-1., false); return true; } @@ -885,7 +885,7 @@ RENDER_AGAIN: else { if (m_imgui->button(m_desc.at("reset_direction"))) { wxGetApp().CallAfter([this](){ - m_c->object_clipper()->set_position(-1., false); + m_c->object_clipper()->set_position_by_ratio(-1., false); }); } } @@ -894,7 +894,7 @@ RENDER_AGAIN: ImGui::PushItemWidth(window_width - settings_sliders_left); float clp_dist = m_c->object_clipper()->get_position(); if (m_imgui->slider_float("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f")) - m_c->object_clipper()->set_position(clp_dist, true); + m_c->object_clipper()->set_position_by_ratio(clp_dist, true); // make sure supports are shown/hidden as appropriate bool show_sups = m_c->instances_hider()->are_supports_shown(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoHollow.hpp b/src/slic3r/GUI/Gizmos/GLGizmoHollow.hpp index aa8cdda04..0fdc3b2db 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoHollow.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoHollow.hpp @@ -31,7 +31,7 @@ public: void data_changed() override; bool gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down); void delete_selected_points(); - bool is_selection_rectangle_dragging() const { + bool is_selection_rectangle_dragging() const override { return m_selection_rectangle.is_dragging(); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp new file mode 100644 index 000000000..74be878f1 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp @@ -0,0 +1,1691 @@ +// 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 + if (m_measurement_result.angle.has_value()) + m_measurement_result.angle->transform(m_volume_matrix); + if (m_measurement_result.distance_infinite.has_value()) + m_measurement_result.distance_infinite->transform(m_volume_matrix); + if (m_measurement_result.distance_strict.has_value()) + m_measurement_result.distance_strict->transform(m_volume_matrix); + if (m_measurement_result.distance_xyz.has_value()) + m_measurement_result.distance_xyz = TransformHelper::model_to_world(*m_measurement_result.distance_xyz, m_volume_matrix); + } + + 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_imgui->set_requires_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; + 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 (v1.isApprox(v2)) + 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(); + }; + 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(); + if (has_distance) { + // 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); + } + + 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); + } + + // Where needed, draw also the extension of the edge to where the dist is measured: + if (has_distance && ft1 == Measure::SurfaceFeatureType::Point && ft2 == Measure::SurfaceFeatureType::Edge) + point_edge(*f1, *f2); + + // Now 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); + } + + 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 000000000..7d20ca26a --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoMeasure.hpp @@ -0,0 +1,163 @@ +#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_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/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index fd9f988da..e6c566d97 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -476,7 +476,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott m_imgui->text(m_desc.at("clipping_of_view")); } else { if (m_imgui->button(m_desc.at("reset_direction"))) { - wxGetApp().CallAfter([this]() { m_c->object_clipper()->set_position(-1., false); }); + wxGetApp().CallAfter([this]() { m_c->object_clipper()->set_position_by_ratio(-1., false); }); } } @@ -484,7 +484,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott ImGui::SameLine(sliders_left_width); ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); if (m_imgui->slider_float("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true, _L("Ctrl + Mouse wheel"))) - m_c->object_clipper()->set_position(clp_dist, true); + m_c->object_clipper()->set_position_by_ratio(clp_dist, true); ImGui::Separator(); if (m_imgui->button(m_desc.at("remove_all"))) { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp index 10b5c5870..12c783c43 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp @@ -1,485 +1,487 @@ -// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro. -#include "GLGizmoMove.hpp" -#include "slic3r/GUI/GLCanvas3D.hpp" -#include "slic3r/GUI/GUI_App.hpp" -#if ENABLE_WORLD_COORDINATE -#include "slic3r/GUI/GUI_ObjectManipulation.hpp" -#endif // ENABLE_WORLD_COORDINATE -#if ENABLE_LEGACY_OPENGL_REMOVAL -#include "slic3r/GUI/Plater.hpp" -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#include - -#include - -namespace Slic3r { -namespace GUI { - -const double GLGizmoMove3D::Offset = 10.0; - -GLGizmoMove3D::GLGizmoMove3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) - : GLGizmoBase(parent, icon_filename, sprite_id) -{} - -std::string GLGizmoMove3D::get_tooltip() const -{ -#if ENABLE_WORLD_COORDINATE - if (m_hover_id == 0) - return "X: " + format(m_displacement.x(), 2); - else if (m_hover_id == 1) - return "Y: " + format(m_displacement.y(), 2); - else if (m_hover_id == 2) - return "Z: " + format(m_displacement.z(), 2); - else - return ""; -#else - const Selection& selection = m_parent.get_selection(); - const bool show_position = selection.is_single_full_instance(); - const Vec3d& position = selection.get_bounding_box().center(); - - if (m_hover_id == 0 || m_grabbers[0].dragging) - return "X: " + format(show_position ? position.x() : m_displacement.x(), 2); - else if (m_hover_id == 1 || m_grabbers[1].dragging) - return "Y: " + format(show_position ? position.y() : m_displacement.y(), 2); - else if (m_hover_id == 2 || m_grabbers[2].dragging) - return "Z: " + format(show_position ? position.z() : m_displacement.z(), 2); - else - return ""; -#endif // ENABLE_WORLD_COORDINATE -} - -bool GLGizmoMove3D::on_mouse(const wxMouseEvent &mouse_event) { - return use_grabbers(mouse_event); -} - -void GLGizmoMove3D::data_changed() { - m_grabbers[2].enabled = !m_parent.get_selection().is_wipe_tower(); -} - -bool GLGizmoMove3D::on_init() -{ - for (int i = 0; i < 3; ++i) { - m_grabbers.push_back(Grabber()); - m_grabbers.back().extensions = GLGizmoBase::EGrabberExtension::PosZ; - } - - m_grabbers[0].angles = { 0.0, 0.5 * double(PI), 0.0 }; - m_grabbers[1].angles = { -0.5 * double(PI), 0.0, 0.0 }; - - m_shortcut_key = WXK_CONTROL_M; - - return true; -} - -std::string GLGizmoMove3D::on_get_name() const -{ - return _u8L("Move"); -} - -bool GLGizmoMove3D::on_is_activable() const -{ - return !m_parent.get_selection().is_empty(); -} - -void GLGizmoMove3D::on_start_dragging() -{ - assert(m_hover_id != -1); - - m_displacement = Vec3d::Zero(); -#if ENABLE_WORLD_COORDINATE - const Selection& selection = m_parent.get_selection(); - const ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); - if (coordinates_type == ECoordinatesType::World) - m_starting_drag_position = m_center + m_grabbers[m_hover_id].center; - else if (coordinates_type == ECoordinatesType::Local && selection.is_single_volume_or_modifier()) { - const GLVolume& v = *selection.get_first_volume(); - m_starting_drag_position = m_center + v.get_instance_transformation().get_rotation_matrix() * v.get_volume_transformation().get_rotation_matrix() * m_grabbers[m_hover_id].center; - } - else { - const GLVolume& v = *selection.get_first_volume(); - m_starting_drag_position = m_center + v.get_instance_transformation().get_rotation_matrix() * m_grabbers[m_hover_id].center; - } - m_starting_box_center = m_center; - m_starting_box_bottom_center = m_center; - m_starting_box_bottom_center.z() = m_bounding_box.min.z(); -#else - const BoundingBoxf3& box = m_parent.get_selection().get_bounding_box(); - m_starting_drag_position = m_grabbers[m_hover_id].center; - m_starting_box_center = box.center(); - m_starting_box_bottom_center = box.center(); - m_starting_box_bottom_center.z() = box.min.z(); -#endif // ENABLE_WORLD_COORDINATE -} - -void GLGizmoMove3D::on_stop_dragging() -{ - m_parent.do_move(L("Gizmo-Move")); - m_displacement = Vec3d::Zero(); -} - -void GLGizmoMove3D::on_dragging(const UpdateData& data) -{ - if (m_hover_id == 0) - m_displacement.x() = calc_projection(data); - else if (m_hover_id == 1) - m_displacement.y() = calc_projection(data); - else if (m_hover_id == 2) - m_displacement.z() = calc_projection(data); - - Selection &selection = m_parent.get_selection(); -#if ENABLE_WORLD_COORDINATE - TransformationType trafo_type; - trafo_type.set_relative(); - switch (wxGetApp().obj_manipul()->get_coordinates_type()) - { - case ECoordinatesType::Instance: { trafo_type.set_instance(); break; } - case ECoordinatesType::Local: { trafo_type.set_local(); break; } - default: { break; } - } - selection.translate(m_displacement, trafo_type); -#else - selection.translate(m_displacement); -#endif // ENABLE_WORLD_COORDINATE -} - -void GLGizmoMove3D::on_render() -{ - glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); - glsafe(::glEnable(GL_DEPTH_TEST)); - -#if ENABLE_WORLD_COORDINATE -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glPushMatrix()); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - calc_selection_box_and_center(); -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Transform3d base_matrix = local_transform(m_parent.get_selection()); - for (int i = 0; i < 3; ++i) { - m_grabbers[i].matrix = base_matrix; - } -#else - transform_to_local(m_parent.get_selection()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - const Vec3d zero = Vec3d::Zero(); - const Vec3d half_box_size = 0.5 * m_bounding_box.size(); - - // x axis - m_grabbers[0].center = { half_box_size.x() + Offset, 0.0, 0.0 }; - m_grabbers[0].color = AXES_COLOR[0]; - - // y axis - m_grabbers[1].center = { 0.0, half_box_size.y() + Offset, 0.0 }; - m_grabbers[1].color = AXES_COLOR[1]; - - // z axis - m_grabbers[2].center = { 0.0, 0.0, half_box_size.z() + Offset }; - m_grabbers[2].color = AXES_COLOR[2]; -#else - const Selection& selection = m_parent.get_selection(); - const BoundingBoxf3& box = selection.get_bounding_box(); - const Vec3d& center = box.center(); - - // x axis - m_grabbers[0].center = { box.max.x() + Offset, center.y(), center.z() }; - m_grabbers[0].color = AXES_COLOR[0]; - - // y axis - m_grabbers[1].center = { center.x(), box.max.y() + Offset, center.z() }; - m_grabbers[1].color = AXES_COLOR[1]; - - // z axis - m_grabbers[2].center = { center.x(), center.y(), box.max.z() + Offset }; - m_grabbers[2].color = AXES_COLOR[2]; -#endif // ENABLE_WORLD_COORDINATE - -#if ENABLE_GL_CORE_PROFILE - if (!OpenGLManager::get_gl_info().is_core_profile()) -#endif // ENABLE_GL_CORE_PROFILE - glsafe(::glLineWidth((m_hover_id != -1) ? 2.0f : 1.5f)); - -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_WORLD_COORDINATE - auto render_grabber_connection = [this, &zero](unsigned int id) { -#else - auto render_grabber_connection = [this, ¢er](unsigned int id) { -#endif // ENABLE_WORLD_COORDINATE - if (m_grabbers[id].enabled) { -#if ENABLE_WORLD_COORDINATE - if (!m_grabber_connections[id].model.is_initialized() || !m_grabber_connections[id].old_center.isApprox(m_grabbers[id].center)) { - m_grabber_connections[id].old_center = m_grabbers[id].center; -#else - if (!m_grabber_connections[id].model.is_initialized() || !m_grabber_connections[id].old_center.isApprox(center)) { - m_grabber_connections[id].old_center = center; -#endif // ENABLE_WORLD_COORDINATE - m_grabber_connections[id].model.reset(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; - init_data.color = AXES_COLOR[id]; - init_data.reserve_vertices(2); - init_data.reserve_indices(2); - - // vertices -#if ENABLE_WORLD_COORDINATE - init_data.add_vertex((Vec3f)zero.cast()); -#else - init_data.add_vertex((Vec3f)center.cast()); -#endif // ENABLE_WORLD_COORDINATE - init_data.add_vertex((Vec3f)m_grabbers[id].center.cast()); - - // indices - init_data.add_line(0, 1); - - m_grabber_connections[id].model.init_from(std::move(init_data)); - } - - m_grabber_connections[id].model.render(); - } - }; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - if (m_hover_id == -1) { -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_GL_CORE_PROFILE - GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); -#else - GLShaderProgram* shader = wxGetApp().get_shader("flat"); -#endif // ENABLE_GL_CORE_PROFILE - if (shader != nullptr) { - shader->start_using(); - const Camera& camera = wxGetApp().plater()->get_camera(); -#if ENABLE_WORLD_COORDINATE - shader->set_uniform("view_model_matrix", camera.get_view_matrix() * base_matrix); -#else - shader->set_uniform("view_model_matrix", camera.get_view_matrix()); -#endif // ENABLE_WORLD_COORDINATE - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); -#if ENABLE_GL_CORE_PROFILE - const std::array& viewport = camera.get_viewport(); - shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); - shader->set_uniform("width", 0.25f); - shader->set_uniform("gap_size", 0.0f); -#endif // ENABLE_GL_CORE_PROFILE -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - // draw axes - for (unsigned int i = 0; i < 3; ++i) { -#if ENABLE_LEGACY_OPENGL_REMOVAL - render_grabber_connection(i); -#else - if (m_grabbers[i].enabled) { - glsafe(::glColor4fv(AXES_COLOR[i].data())); - ::glBegin(GL_LINES); -#if ENABLE_WORLD_COORDINATE - ::glVertex3dv(zero.data()); -#else - ::glVertex3dv(center.data()); -#endif // ENABLE_WORLD_COORDINATE - ::glVertex3dv(m_grabbers[i].center.data()); - glsafe(::glEnd()); - } -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - -#if ENABLE_LEGACY_OPENGL_REMOVAL - shader->stop_using(); - } -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - // draw grabbers -#if ENABLE_WORLD_COORDINATE - render_grabbers(m_bounding_box); -#else - render_grabbers(box); -#endif // ENABLE_WORLD_COORDINATE - } - else { - // draw axis -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_GL_CORE_PROFILE - GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); -#else - GLShaderProgram* shader = wxGetApp().get_shader("flat"); -#endif // ENABLE_GL_CORE_PROFILE - if (shader != nullptr) { - shader->start_using(); - - const Camera& camera = wxGetApp().plater()->get_camera(); -#if ENABLE_WORLD_COORDINATE - shader->set_uniform("view_model_matrix", camera.get_view_matrix()* base_matrix); -#else - shader->set_uniform("view_model_matrix", camera.get_view_matrix()); -#endif // ENABLE_WORLD_COORDINATE - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); -#if ENABLE_GL_CORE_PROFILE - const std::array& viewport = camera.get_viewport(); - shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); - shader->set_uniform("width", 0.5f); - shader->set_uniform("gap_size", 0.0f); -#endif // ENABLE_GL_CORE_PROFILE - - render_grabber_connection(m_hover_id); - shader->stop_using(); - } - - shader = wxGetApp().get_shader("gouraud_light"); -#else - glsafe(::glColor4fv(AXES_COLOR[m_hover_id].data())); - ::glBegin(GL_LINES); -#if ENABLE_WORLD_COORDINATE - ::glVertex3dv(zero.data()); -#else - ::glVertex3dv(center.data()); -#endif // ENABLE_WORLD_COORDINATE - ::glVertex3dv(m_grabbers[m_hover_id].center.data()); - glsafe(::glEnd()); - - GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - if (shader != nullptr) { - shader->start_using(); - shader->set_uniform("emission_factor", 0.1f); - // draw grabber -#if ENABLE_WORLD_COORDINATE - const Vec3d box_size = m_bounding_box.size(); -#else - const Vec3d box_size = box.size(); -#endif // ENABLE_WORLD_COORDINATE - const float mean_size = (float)((box_size.x() + box_size.y() + box_size.z()) / 3.0); - m_grabbers[m_hover_id].render(true, mean_size); - shader->stop_using(); - } - } - -#if ENABLE_WORLD_COORDINATE -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glPopMatrix()); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL -#endif // ENABLE_WORLD_COORDINATE -} - -#if ENABLE_RAYCAST_PICKING -void GLGizmoMove3D::on_register_raycasters_for_picking() -{ - // the gizmo grabbers 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 GLGizmoMove3D::on_unregister_raycasters_for_picking() -{ - m_parent.set_raycaster_gizmos_on_top(false); -} -#else -void GLGizmoMove3D::on_render_for_picking() -{ - glsafe(::glDisable(GL_DEPTH_TEST)); - -#if ENABLE_WORLD_COORDINATE -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Transform3d base_matrix = local_transform(m_parent.get_selection()); - for (int i = 0; i < 3; ++i) { - m_grabbers[i].matrix = base_matrix; - } -#else - glsafe(::glPushMatrix()); - transform_to_local(m_parent.get_selection()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - render_grabbers_for_picking(m_bounding_box); -#if ENABLE_LEGACY_OPENGL_REMOVAL -#else - glsafe(::glPopMatrix()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -#else - const BoundingBoxf3& box = m_parent.get_selection().get_bounding_box(); - render_grabbers_for_picking(box); -#endif // ENABLE_WORLD_COORDINATE -} -#endif // ENABLE_RAYCAST_PICKING - -double GLGizmoMove3D::calc_projection(const UpdateData& data) const -{ - double projection = 0.0; - - const Vec3d starting_vec = m_starting_drag_position - m_starting_box_center; - const double len_starting_vec = starting_vec.norm(); - if (len_starting_vec != 0.0) { - const Vec3d mouse_dir = data.mouse_ray.unit_vector(); - // finds the intersection of the mouse ray with the plane parallel to the camera viewport and passing throught the starting position - // use ray-plane intersection see i.e. https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection algebric form - // in our case plane normal and ray direction are the same (orthogonal view) - // when moving to perspective camera the negative z unit axis of the camera needs to be transformed in world space and used as plane normal - const Vec3d inters = data.mouse_ray.a + (m_starting_drag_position - data.mouse_ray.a).dot(mouse_dir) / mouse_dir.squaredNorm() * mouse_dir; - // vector from the starting position to the found intersection - const Vec3d inters_vec = inters - m_starting_drag_position; - - // finds projection of the vector along the staring direction - projection = inters_vec.dot(starting_vec.normalized()); - } - - if (wxGetKeyState(WXK_SHIFT)) - projection = m_snap_step * (double)std::round(projection / m_snap_step); - - return projection; -} - -#if ENABLE_WORLD_COORDINATE -#if ENABLE_LEGACY_OPENGL_REMOVAL -Transform3d GLGizmoMove3D::local_transform(const Selection& selection) const -{ - Transform3d ret = Geometry::assemble_transform(m_center); - if (!wxGetApp().obj_manipul()->is_world_coordinates()) { - const GLVolume& v = *selection.get_first_volume(); - Transform3d orient_matrix = v.get_instance_transformation().get_rotation_matrix(); - if (selection.is_single_volume_or_modifier() && wxGetApp().obj_manipul()->is_local_coordinates()) - orient_matrix = orient_matrix * v.get_volume_transformation().get_rotation_matrix(); - ret = ret * orient_matrix; - } - return ret; -} -#else -void GLGizmoMove3D::transform_to_local(const Selection& selection) const -{ - glsafe(::glTranslated(m_center.x(), m_center.y(), m_center.z())); - - if (!wxGetApp().obj_manipul()->is_world_coordinates()) { - const GLVolume& v = *selection.get_first_volume(); - Transform3d orient_matrix = v.get_instance_transformation().get_matrix(true, false, true, true); - if (selection.is_single_volume_or_modifier() && wxGetApp().obj_manipul()->is_local_coordinates()) - orient_matrix = orient_matrix * v.get_volume_transformation().get_matrix(true, false, true, true); - glsafe(::glMultMatrixd(orient_matrix.data())); - } -} -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -void GLGizmoMove3D::calc_selection_box_and_center() -{ - const Selection& selection = m_parent.get_selection(); - const ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); - if (coordinates_type == ECoordinatesType::World) { - m_bounding_box = selection.get_bounding_box(); - m_center = m_bounding_box.center(); - } - else if (coordinates_type == ECoordinatesType::Local && selection.is_single_volume_or_modifier()) { - const GLVolume& v = *selection.get_first_volume(); - m_bounding_box = v.transformed_convex_hull_bounding_box( - v.get_instance_transformation().get_scaling_factor_matrix() * v.get_volume_transformation().get_scaling_factor_matrix()); - m_center = v.world_matrix() * m_bounding_box.center(); - } - else { - m_bounding_box.reset(); - const Selection::IndicesList& ids = selection.get_volume_idxs(); - for (unsigned int id : ids) { - const GLVolume& v = *selection.get_volume(id); - m_bounding_box.merge(v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_matrix())); - } - const Geometry::Transformation inst_trafo = selection.get_first_volume()->get_instance_transformation(); - m_bounding_box = m_bounding_box.transformed(inst_trafo.get_scaling_factor_matrix()); - m_center = inst_trafo.get_matrix_no_scaling_factor() * m_bounding_box.center(); - } -} -#endif // ENABLE_WORLD_COORDINATE - -} // namespace GUI -} // namespace Slic3r +// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro. +#include "GLGizmoMove.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#if ENABLE_WORLD_COORDINATE +#include "slic3r/GUI/GUI_ObjectManipulation.hpp" +#endif // ENABLE_WORLD_COORDINATE +#if ENABLE_LEGACY_OPENGL_REMOVAL +#include "slic3r/GUI/Plater.hpp" +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#include "libslic3r/Model.hpp" + +#include + +#include + +namespace Slic3r { +namespace GUI { + +const double GLGizmoMove3D::Offset = 10.0; + +GLGizmoMove3D::GLGizmoMove3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) + : GLGizmoBase(parent, icon_filename, sprite_id) +{} + +std::string GLGizmoMove3D::get_tooltip() const +{ +#if ENABLE_WORLD_COORDINATE + if (m_hover_id == 0) + return "X: " + format(m_displacement.x(), 2); + else if (m_hover_id == 1) + return "Y: " + format(m_displacement.y(), 2); + else if (m_hover_id == 2) + return "Z: " + format(m_displacement.z(), 2); + else + return ""; +#else + const Selection& selection = m_parent.get_selection(); + const bool show_position = selection.is_single_full_instance(); + const Vec3d& position = selection.get_bounding_box().center(); + + if (m_hover_id == 0 || m_grabbers[0].dragging) + return "X: " + format(show_position ? position.x() : m_displacement.x(), 2); + else if (m_hover_id == 1 || m_grabbers[1].dragging) + return "Y: " + format(show_position ? position.y() : m_displacement.y(), 2); + else if (m_hover_id == 2 || m_grabbers[2].dragging) + return "Z: " + format(show_position ? position.z() : m_displacement.z(), 2); + else + return ""; +#endif // ENABLE_WORLD_COORDINATE +} + +bool GLGizmoMove3D::on_mouse(const wxMouseEvent &mouse_event) { + return use_grabbers(mouse_event); +} + +void GLGizmoMove3D::data_changed() { + m_grabbers[2].enabled = !m_parent.get_selection().is_wipe_tower(); +} + +bool GLGizmoMove3D::on_init() +{ + for (int i = 0; i < 3; ++i) { + m_grabbers.push_back(Grabber()); + m_grabbers.back().extensions = GLGizmoBase::EGrabberExtension::PosZ; + } + + m_grabbers[0].angles = { 0.0, 0.5 * double(PI), 0.0 }; + m_grabbers[1].angles = { -0.5 * double(PI), 0.0, 0.0 }; + + m_shortcut_key = WXK_CONTROL_M; + + return true; +} + +std::string GLGizmoMove3D::on_get_name() const +{ + return _u8L("Move"); +} + +bool GLGizmoMove3D::on_is_activable() const +{ + const Selection& selection = m_parent.get_selection(); + return !selection.is_any_cut_volume() && !selection.is_any_connector() && !selection.is_empty(); +} + +void GLGizmoMove3D::on_start_dragging() +{ + assert(m_hover_id != -1); + + m_displacement = Vec3d::Zero(); +#if ENABLE_WORLD_COORDINATE + const Selection& selection = m_parent.get_selection(); + const ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); + if (coordinates_type == ECoordinatesType::World) + m_starting_drag_position = m_center + m_grabbers[m_hover_id].center; + else if (coordinates_type == ECoordinatesType::Local && selection.is_single_volume_or_modifier()) { + const GLVolume& v = *selection.get_first_volume(); + m_starting_drag_position = m_center + v.get_instance_transformation().get_rotation_matrix() * v.get_volume_transformation().get_rotation_matrix() * m_grabbers[m_hover_id].center; + } + else { + const GLVolume& v = *selection.get_first_volume(); + m_starting_drag_position = m_center + v.get_instance_transformation().get_rotation_matrix() * m_grabbers[m_hover_id].center; + } + m_starting_box_center = m_center; + m_starting_box_bottom_center = m_center; + m_starting_box_bottom_center.z() = m_bounding_box.min.z(); +#else + const BoundingBoxf3& box = m_parent.get_selection().get_bounding_box(); + m_starting_drag_position = m_grabbers[m_hover_id].center; + m_starting_box_center = box.center(); + m_starting_box_bottom_center = box.center(); + m_starting_box_bottom_center.z() = box.min.z(); +#endif // ENABLE_WORLD_COORDINATE +} + +void GLGizmoMove3D::on_stop_dragging() +{ + m_parent.do_move(L("Gizmo-Move")); + m_displacement = Vec3d::Zero(); +} + +void GLGizmoMove3D::on_dragging(const UpdateData& data) +{ + if (m_hover_id == 0) + m_displacement.x() = calc_projection(data); + else if (m_hover_id == 1) + m_displacement.y() = calc_projection(data); + else if (m_hover_id == 2) + m_displacement.z() = calc_projection(data); + + Selection &selection = m_parent.get_selection(); +#if ENABLE_WORLD_COORDINATE + TransformationType trafo_type; + trafo_type.set_relative(); + switch (wxGetApp().obj_manipul()->get_coordinates_type()) + { + case ECoordinatesType::Instance: { trafo_type.set_instance(); break; } + case ECoordinatesType::Local: { trafo_type.set_local(); break; } + default: { break; } + } + selection.translate(m_displacement, trafo_type); +#else + selection.translate(m_displacement); +#endif // ENABLE_WORLD_COORDINATE +} + +void GLGizmoMove3D::on_render() +{ + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); + glsafe(::glEnable(GL_DEPTH_TEST)); + +#if ENABLE_WORLD_COORDINATE +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glPushMatrix()); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + calc_selection_box_and_center(); +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Transform3d base_matrix = local_transform(m_parent.get_selection()); + for (int i = 0; i < 3; ++i) { + m_grabbers[i].matrix = base_matrix; + } +#else + transform_to_local(m_parent.get_selection()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + const Vec3d zero = Vec3d::Zero(); + const Vec3d half_box_size = 0.5 * m_bounding_box.size(); + + // x axis + m_grabbers[0].center = { half_box_size.x() + Offset, 0.0, 0.0 }; + m_grabbers[0].color = AXES_COLOR[0]; + + // y axis + m_grabbers[1].center = { 0.0, half_box_size.y() + Offset, 0.0 }; + m_grabbers[1].color = AXES_COLOR[1]; + + // z axis + m_grabbers[2].center = { 0.0, 0.0, half_box_size.z() + Offset }; + m_grabbers[2].color = AXES_COLOR[2]; +#else + const Selection& selection = m_parent.get_selection(); + const BoundingBoxf3& box = selection.get_bounding_box(); + const Vec3d& center = box.center(); + + // x axis + m_grabbers[0].center = { box.max.x() + Offset, center.y(), center.z() }; + m_grabbers[0].color = AXES_COLOR[0]; + + // y axis + m_grabbers[1].center = { center.x(), box.max.y() + Offset, center.z() }; + m_grabbers[1].color = AXES_COLOR[1]; + + // z axis + m_grabbers[2].center = { center.x(), center.y(), box.max.z() + Offset }; + m_grabbers[2].color = AXES_COLOR[2]; +#endif // ENABLE_WORLD_COORDINATE + +#if ENABLE_GL_CORE_PROFILE + if (!OpenGLManager::get_gl_info().is_core_profile()) +#endif // ENABLE_GL_CORE_PROFILE + glsafe(::glLineWidth((m_hover_id != -1) ? 2.0f : 1.5f)); + +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_WORLD_COORDINATE + auto render_grabber_connection = [this, &zero](unsigned int id) { +#else + auto render_grabber_connection = [this, ¢er](unsigned int id) { +#endif // ENABLE_WORLD_COORDINATE + if (m_grabbers[id].enabled) { +#if ENABLE_WORLD_COORDINATE + if (!m_grabber_connections[id].model.is_initialized() || !m_grabber_connections[id].old_center.isApprox(m_grabbers[id].center)) { + m_grabber_connections[id].old_center = m_grabbers[id].center; +#else + if (!m_grabber_connections[id].model.is_initialized() || !m_grabber_connections[id].old_center.isApprox(center)) { + m_grabber_connections[id].old_center = center; +#endif // ENABLE_WORLD_COORDINATE + m_grabber_connections[id].model.reset(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.color = AXES_COLOR[id]; + init_data.reserve_vertices(2); + init_data.reserve_indices(2); + + // vertices +#if ENABLE_WORLD_COORDINATE + init_data.add_vertex((Vec3f)zero.cast()); +#else + init_data.add_vertex((Vec3f)center.cast()); +#endif // ENABLE_WORLD_COORDINATE + init_data.add_vertex((Vec3f)m_grabbers[id].center.cast()); + + // indices + init_data.add_line(0, 1); + + m_grabber_connections[id].model.init_from(std::move(init_data)); + } + + m_grabber_connections[id].model.render(); + } + }; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + if (m_hover_id == -1) { +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_GL_CORE_PROFILE + GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); +#else + GLShaderProgram* shader = wxGetApp().get_shader("flat"); +#endif // ENABLE_GL_CORE_PROFILE + if (shader != nullptr) { + shader->start_using(); + const Camera& camera = wxGetApp().plater()->get_camera(); +#if ENABLE_WORLD_COORDINATE + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * base_matrix); +#else + shader->set_uniform("view_model_matrix", camera.get_view_matrix()); +#endif // ENABLE_WORLD_COORDINATE + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); +#if ENABLE_GL_CORE_PROFILE + const std::array& viewport = camera.get_viewport(); + shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); + shader->set_uniform("width", 0.25f); + shader->set_uniform("gap_size", 0.0f); +#endif // ENABLE_GL_CORE_PROFILE +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + // draw axes + for (unsigned int i = 0; i < 3; ++i) { +#if ENABLE_LEGACY_OPENGL_REMOVAL + render_grabber_connection(i); +#else + if (m_grabbers[i].enabled) { + glsafe(::glColor4fv(AXES_COLOR[i].data())); + ::glBegin(GL_LINES); +#if ENABLE_WORLD_COORDINATE + ::glVertex3dv(zero.data()); +#else + ::glVertex3dv(center.data()); +#endif // ENABLE_WORLD_COORDINATE + ::glVertex3dv(m_grabbers[i].center.data()); + glsafe(::glEnd()); + } +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + +#if ENABLE_LEGACY_OPENGL_REMOVAL + shader->stop_using(); + } +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + // draw grabbers +#if ENABLE_WORLD_COORDINATE + render_grabbers(m_bounding_box); +#else + render_grabbers(box); +#endif // ENABLE_WORLD_COORDINATE + } + else { + // draw axis +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_GL_CORE_PROFILE + GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); +#else + GLShaderProgram* shader = wxGetApp().get_shader("flat"); +#endif // ENABLE_GL_CORE_PROFILE + if (shader != nullptr) { + shader->start_using(); + + const Camera& camera = wxGetApp().plater()->get_camera(); +#if ENABLE_WORLD_COORDINATE + shader->set_uniform("view_model_matrix", camera.get_view_matrix()* base_matrix); +#else + shader->set_uniform("view_model_matrix", camera.get_view_matrix()); +#endif // ENABLE_WORLD_COORDINATE + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); +#if ENABLE_GL_CORE_PROFILE + const std::array& viewport = camera.get_viewport(); + shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); + shader->set_uniform("width", 0.5f); + shader->set_uniform("gap_size", 0.0f); +#endif // ENABLE_GL_CORE_PROFILE + + render_grabber_connection(m_hover_id); + shader->stop_using(); + } + + shader = wxGetApp().get_shader("gouraud_light"); +#else + glsafe(::glColor4fv(AXES_COLOR[m_hover_id].data())); + ::glBegin(GL_LINES); +#if ENABLE_WORLD_COORDINATE + ::glVertex3dv(zero.data()); +#else + ::glVertex3dv(center.data()); +#endif // ENABLE_WORLD_COORDINATE + ::glVertex3dv(m_grabbers[m_hover_id].center.data()); + glsafe(::glEnd()); + + GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + if (shader != nullptr) { + shader->start_using(); + shader->set_uniform("emission_factor", 0.1f); + // draw grabber +#if ENABLE_WORLD_COORDINATE + const Vec3d box_size = m_bounding_box.size(); +#else + const Vec3d box_size = box.size(); +#endif // ENABLE_WORLD_COORDINATE + const float mean_size = (float)((box_size.x() + box_size.y() + box_size.z()) / 3.0); + m_grabbers[m_hover_id].render(true, mean_size); + shader->stop_using(); + } + } + +#if ENABLE_WORLD_COORDINATE +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glPopMatrix()); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL +#endif // ENABLE_WORLD_COORDINATE +} + +#if ENABLE_RAYCAST_PICKING +void GLGizmoMove3D::on_register_raycasters_for_picking() +{ + // the gizmo grabbers 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 GLGizmoMove3D::on_unregister_raycasters_for_picking() +{ + m_parent.set_raycaster_gizmos_on_top(false); +} +#else +void GLGizmoMove3D::on_render_for_picking() +{ + glsafe(::glDisable(GL_DEPTH_TEST)); + +#if ENABLE_WORLD_COORDINATE +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Transform3d base_matrix = local_transform(m_parent.get_selection()); + for (int i = 0; i < 3; ++i) { + m_grabbers[i].matrix = base_matrix; + } +#else + glsafe(::glPushMatrix()); + transform_to_local(m_parent.get_selection()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + render_grabbers_for_picking(m_bounding_box); +#if ENABLE_LEGACY_OPENGL_REMOVAL +#else + glsafe(::glPopMatrix()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#else + const BoundingBoxf3& box = m_parent.get_selection().get_bounding_box(); + render_grabbers_for_picking(box); +#endif // ENABLE_WORLD_COORDINATE +} +#endif // ENABLE_RAYCAST_PICKING + +double GLGizmoMove3D::calc_projection(const UpdateData& data) const +{ + double projection = 0.0; + + const Vec3d starting_vec = m_starting_drag_position - m_starting_box_center; + const double len_starting_vec = starting_vec.norm(); + if (len_starting_vec != 0.0) { + const Vec3d mouse_dir = data.mouse_ray.unit_vector(); + // finds the intersection of the mouse ray with the plane parallel to the camera viewport and passing throught the starting position + // use ray-plane intersection see i.e. https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection algebric form + // in our case plane normal and ray direction are the same (orthogonal view) + // when moving to perspective camera the negative z unit axis of the camera needs to be transformed in world space and used as plane normal + const Vec3d inters = data.mouse_ray.a + (m_starting_drag_position - data.mouse_ray.a).dot(mouse_dir) / mouse_dir.squaredNorm() * mouse_dir; + // vector from the starting position to the found intersection + const Vec3d inters_vec = inters - m_starting_drag_position; + + // finds projection of the vector along the staring direction + projection = inters_vec.dot(starting_vec.normalized()); + } + + if (wxGetKeyState(WXK_SHIFT)) + projection = m_snap_step * (double)std::round(projection / m_snap_step); + + return projection; +} + +#if ENABLE_WORLD_COORDINATE +#if ENABLE_LEGACY_OPENGL_REMOVAL +Transform3d GLGizmoMove3D::local_transform(const Selection& selection) const +{ + Transform3d ret = Geometry::assemble_transform(m_center); + if (!wxGetApp().obj_manipul()->is_world_coordinates()) { + const GLVolume& v = *selection.get_first_volume(); + Transform3d orient_matrix = v.get_instance_transformation().get_rotation_matrix(); + if (selection.is_single_volume_or_modifier() && wxGetApp().obj_manipul()->is_local_coordinates()) + orient_matrix = orient_matrix * v.get_volume_transformation().get_rotation_matrix(); + ret = ret * orient_matrix; + } + return ret; +} +#else +void GLGizmoMove3D::transform_to_local(const Selection& selection) const +{ + glsafe(::glTranslated(m_center.x(), m_center.y(), m_center.z())); + + if (!wxGetApp().obj_manipul()->is_world_coordinates()) { + const GLVolume& v = *selection.get_first_volume(); + Transform3d orient_matrix = v.get_instance_transformation().get_matrix(true, false, true, true); + if (selection.is_single_volume_or_modifier() && wxGetApp().obj_manipul()->is_local_coordinates()) + orient_matrix = orient_matrix * v.get_volume_transformation().get_matrix(true, false, true, true); + glsafe(::glMultMatrixd(orient_matrix.data())); + } +} +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +void GLGizmoMove3D::calc_selection_box_and_center() +{ + const Selection& selection = m_parent.get_selection(); + const ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); + if (coordinates_type == ECoordinatesType::World) { + m_bounding_box = selection.get_bounding_box(); + m_center = m_bounding_box.center(); + } + else if (coordinates_type == ECoordinatesType::Local && selection.is_single_volume_or_modifier()) { + const GLVolume& v = *selection.get_first_volume(); + m_bounding_box = v.transformed_convex_hull_bounding_box( + v.get_instance_transformation().get_scaling_factor_matrix() * v.get_volume_transformation().get_scaling_factor_matrix()); + m_center = v.world_matrix() * m_bounding_box.center(); + } + else { + m_bounding_box.reset(); + const Selection::IndicesList& ids = selection.get_volume_idxs(); + for (unsigned int id : ids) { + const GLVolume& v = *selection.get_volume(id); + m_bounding_box.merge(v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_matrix())); + } + const Geometry::Transformation inst_trafo = selection.get_first_volume()->get_instance_transformation(); + m_bounding_box = m_bounding_box.transformed(inst_trafo.get_scaling_factor_matrix()); + m_center = inst_trafo.get_matrix_no_scaling_factor() * m_bounding_box.center(); + } +} +#endif // ENABLE_WORLD_COORDINATE + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 966c36b27..154a73062 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -537,7 +537,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous pos = action == SLAGizmoEventType::MouseWheelDown ? std::max(0., pos - 0.01) : std::min(1., pos + 0.01); - m_c->object_clipper()->set_position(pos, true); + m_c->object_clipper()->set_position_by_ratio(pos, true); return true; } else if (alt_down) { @@ -573,7 +573,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous } if (action == SLAGizmoEventType::ResetClippingPlane) { - m_c->object_clipper()->set_position(-1., false); + m_c->object_clipper()->set_position_by_ratio(-1., false); return true; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp index db5c61869..80d6f1aa0 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp @@ -1,999 +1,1001 @@ -// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro. -#include "GLGizmoRotate.hpp" -#include "slic3r/GUI/GLCanvas3D.hpp" -#include "slic3r/GUI/ImGuiWrapper.hpp" -#if ENABLE_WORLD_COORDINATE -#include "slic3r/GUI/GUI_ObjectManipulation.hpp" -#endif // ENABLE_WORLD_COORDINATE - -#include "slic3r/GUI/GUI_App.hpp" -#include "slic3r/GUI/GUI.hpp" -#include "slic3r/GUI/Plater.hpp" -#include "slic3r/GUI/Jobs/RotoptimizeJob.hpp" - -#include "libslic3r/PresetBundle.hpp" - -#include - -namespace Slic3r { -namespace GUI { - - -const float GLGizmoRotate::Offset = 5.0f; -const unsigned int GLGizmoRotate::AngleResolution = 64; -const unsigned int GLGizmoRotate::ScaleStepsCount = 72; -const float GLGizmoRotate::ScaleStepRad = 2.0f * float(PI) / GLGizmoRotate::ScaleStepsCount; -const unsigned int GLGizmoRotate::ScaleLongEvery = 2; -const float GLGizmoRotate::ScaleLongTooth = 0.1f; // in percent of radius -const unsigned int GLGizmoRotate::SnapRegionsCount = 8; -const float GLGizmoRotate::GrabberOffset = 0.15f; // in percent of radius - -GLGizmoRotate::GLGizmoRotate(GLCanvas3D& parent, GLGizmoRotate::Axis axis) - : GLGizmoBase(parent, "", -1) - , m_axis(axis) - , m_angle(0.0) - , m_center(0.0, 0.0, 0.0) - , m_radius(0.0f) - , m_snap_coarse_in_radius(0.0f) - , m_snap_coarse_out_radius(0.0f) - , m_snap_fine_in_radius(0.0f) - , m_snap_fine_out_radius(0.0f) - , m_drag_color(DEFAULT_DRAG_COLOR) - , m_highlight_color(DEFAULT_HIGHLIGHT_COLOR) -{ - m_group_id = static_cast(axis); -} - -void GLGizmoRotate::set_highlight_color(const ColorRGBA &color) -{ - m_highlight_color = color; -} - -void GLGizmoRotate::set_angle(double angle) -{ - if (std::abs(angle - 2.0 * double(PI)) < EPSILON) - angle = 0.0; - - m_angle = angle; -} - -std::string GLGizmoRotate::get_tooltip() const -{ - std::string axis; - switch (m_axis) - { - case X: { axis = "X"; break; } - case Y: { axis = "Y"; break; } - case Z: { axis = "Z"; break; } - } - return (m_hover_id == 0 || m_grabbers.front().dragging) ? axis + ": " + format(float(Geometry::rad2deg(m_angle)), 4) : ""; -} - -bool GLGizmoRotate::on_mouse(const wxMouseEvent &mouse_event) -{ - return use_grabbers(mouse_event); -} - -void GLGizmoRotate::dragging(const UpdateData &data) { on_dragging(data); } - -void GLGizmoRotate::start_dragging() -{ - m_grabbers[0].dragging = true; - on_start_dragging(); -} - -void GLGizmoRotate::stop_dragging() -{ - m_grabbers[0].dragging = false; - on_stop_dragging(); -} - -void GLGizmoRotate::enable_grabber() { m_grabbers[0].enabled = true; } -void GLGizmoRotate::disable_grabber() { m_grabbers[0].enabled = false; } - -bool GLGizmoRotate::on_init() -{ - m_grabbers.push_back(Grabber()); - m_grabbers.back().extensions = (GLGizmoBase::EGrabberExtension)(int(GLGizmoBase::EGrabberExtension::PosY) | int(GLGizmoBase::EGrabberExtension::NegY)); - return true; -} - -void GLGizmoRotate::on_start_dragging() -{ -#if ENABLE_WORLD_COORDINATE - init_data_from_selection(m_parent.get_selection()); -#else - const BoundingBoxf3& box = m_parent.get_selection().get_bounding_box(); - m_center = box.center(); - m_radius = Offset + box.radius(); - m_snap_coarse_in_radius = m_radius / 3.0f; - m_snap_coarse_out_radius = 2.0f * m_snap_coarse_in_radius; - m_snap_fine_in_radius = m_radius; - m_snap_fine_out_radius = m_snap_fine_in_radius + m_radius * ScaleLongTooth; -#endif // ENABLE_WORLD_COORDINATE -} - -void GLGizmoRotate::on_dragging(const UpdateData &data) -{ -#if ENABLE_WORLD_COORDINATE - const Vec2d mouse_pos = to_2d(mouse_position_in_local_plane(data.mouse_ray)); -#else - const Vec2d mouse_pos = to_2d(mouse_position_in_local_plane(data.mouse_ray, m_parent.get_selection())); -#endif // ENABLE_WORLD_COORDINATE - - const Vec2d orig_dir = Vec2d::UnitX(); - const Vec2d new_dir = mouse_pos.normalized(); - - double theta = ::acos(std::clamp(new_dir.dot(orig_dir), -1.0, 1.0)); - if (cross2(orig_dir, new_dir) < 0.0) - theta = 2.0 * (double)PI - theta; - - const double len = mouse_pos.norm(); - - // snap to coarse snap region - if (m_snap_coarse_in_radius <= len && len <= m_snap_coarse_out_radius) { - const double step = 2.0 * double(PI) / double(SnapRegionsCount); - theta = step * std::round(theta / step); - } - else { - // snap to fine snap region (scale) - if (m_snap_fine_in_radius <= len && len <= m_snap_fine_out_radius) { - const double step = 2.0 * double(PI) / double(ScaleStepsCount); - theta = step * std::round(theta / step); - } - } - - if (theta == 2.0 * double(PI)) - theta = 0.0; - - m_angle = theta; -} - -void GLGizmoRotate::on_render() -{ - if (!m_grabbers.front().enabled) - return; - - const Selection& selection = m_parent.get_selection(); -#if !ENABLE_WORLD_COORDINATE - const BoundingBoxf3& box = selection.get_bounding_box(); -#endif // !ENABLE_WORLD_COORDINATE - - if (m_hover_id != 0 && !m_grabbers.front().dragging) { -#if ENABLE_WORLD_COORDINATE - init_data_from_selection(selection); -#else - m_center = box.center(); - m_radius = Offset + box.radius(); - m_snap_coarse_in_radius = m_radius / 3.0f; - m_snap_coarse_out_radius = 2.0f * m_snap_coarse_in_radius; - m_snap_fine_in_radius = m_radius; - m_snap_fine_out_radius = m_radius * (1.0f + ScaleLongTooth); -#endif // ENABLE_WORLD_COORDINATE - } - - const double grabber_radius = (double)m_radius * (1.0 + (double)GrabberOffset); - m_grabbers.front().center = Vec3d(::cos(m_angle) * grabber_radius, ::sin(m_angle) * grabber_radius, 0.0); - m_grabbers.front().angles.z() = m_angle; - - glsafe(::glEnable(GL_DEPTH_TEST)); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - m_grabbers.front().matrix = local_transform(selection); -#else - glsafe(::glPushMatrix()); - transform_to_local(selection); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_GL_CORE_PROFILE - if (!OpenGLManager::get_gl_info().is_core_profile()) -#endif // ENABLE_GL_CORE_PROFILE - glsafe(::glLineWidth((m_hover_id != -1) ? 2.0f : 1.5f)); -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_GL_CORE_PROFILE - GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); -#else - GLShaderProgram* shader = wxGetApp().get_shader("flat"); -#endif // ENABLE_GL_CORE_PROFILE - if (shader != nullptr) { - shader->start_using(); - - const Camera& camera = wxGetApp().plater()->get_camera(); - const Transform3d view_model_matrix = camera.get_view_matrix() * m_grabbers.front().matrix; - shader->set_uniform("view_model_matrix", view_model_matrix); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); -#if ENABLE_GL_CORE_PROFILE - const std::array& viewport = camera.get_viewport(); - shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); - shader->set_uniform("width", 0.25f); - shader->set_uniform("gap_size", 0.0f); -#endif // ENABLE_GL_CORE_PROFILE - - const bool radius_changed = std::abs(m_old_radius - m_radius) > EPSILON; - m_old_radius = m_radius; - - const ColorRGBA color = (m_hover_id != -1) ? m_drag_color : m_highlight_color; - render_circle(color, radius_changed); - if (m_hover_id != -1) { - const bool hover_radius_changed = std::abs(m_old_hover_radius - m_radius) > EPSILON; - m_old_hover_radius = m_radius; - - render_scale(color, hover_radius_changed); - render_snap_radii(color, hover_radius_changed); - render_reference_radius(color, hover_radius_changed); - render_angle_arc(m_highlight_color, hover_radius_changed); - } - - render_grabber_connection(color, radius_changed); - shader->stop_using(); - } -#else - glsafe(::glColor4fv((m_hover_id != -1) ? m_drag_color.data() : m_highlight_color.data())); - - render_circle(); - - if (m_hover_id != -1) { - render_scale(); - render_snap_radii(); - render_reference_radius(); - } - - glsafe(::glColor4fv(m_highlight_color.data())); - - if (m_hover_id != -1) - render_angle(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_WORLD_COORDINATE - render_grabber(m_bounding_box); -#else - render_grabber(box); -#endif // ENABLE_WORLD_COORDINATE - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glPopMatrix()); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL -} - -#if !ENABLE_RAYCAST_PICKING -void GLGizmoRotate::on_render_for_picking() -{ - const Selection& selection = m_parent.get_selection(); - - glsafe(::glDisable(GL_DEPTH_TEST)); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - m_grabbers.front().matrix = local_transform(selection); -#else - glsafe(::glPushMatrix()); - transform_to_local(selection); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_WORLD_COORDINATE - render_grabbers_for_picking(m_bounding_box); -#else - const BoundingBoxf3& box = selection.get_bounding_box(); - render_grabbers_for_picking(box); -#endif // ENABLE_WORLD_COORDINATE - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glPopMatrix()); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL -} -#endif // !ENABLE_RAYCAST_PICKING - -#if ENABLE_WORLD_COORDINATE -void GLGizmoRotate::init_data_from_selection(const Selection& selection) -{ - ECoordinatesType coordinates_type; - if (selection.is_wipe_tower()) - coordinates_type = ECoordinatesType::Local; - else - coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); - if (coordinates_type == ECoordinatesType::World) { - m_bounding_box = selection.get_bounding_box(); - m_center = m_bounding_box.center(); - } - else if (coordinates_type == ECoordinatesType::Local && selection.is_single_volume_or_modifier()) { - const GLVolume& v = *selection.get_first_volume(); - m_bounding_box = v.transformed_convex_hull_bounding_box( - v.get_instance_transformation().get_scaling_factor_matrix() * v.get_volume_transformation().get_scaling_factor_matrix()); - m_center = v.world_matrix() * m_bounding_box.center(); - } - else { - m_bounding_box.reset(); - const Selection::IndicesList& ids = selection.get_volume_idxs(); - for (unsigned int id : ids) { - const GLVolume& v = *selection.get_volume(id); - m_bounding_box.merge(v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_matrix())); - } - const Geometry::Transformation inst_trafo = selection.get_first_volume()->get_instance_transformation(); - m_bounding_box = m_bounding_box.transformed(inst_trafo.get_scaling_factor_matrix()); - m_center = inst_trafo.get_matrix_no_scaling_factor() * m_bounding_box.center(); - } - - m_radius = Offset + m_bounding_box.radius(); - m_snap_coarse_in_radius = m_radius / 3.0f; - m_snap_coarse_out_radius = 2.0f * m_snap_coarse_in_radius; - m_snap_fine_in_radius = m_radius; - m_snap_fine_out_radius = m_snap_fine_in_radius + m_radius * ScaleLongTooth; - - if (coordinates_type == ECoordinatesType::World) - m_orient_matrix = Transform3d::Identity(); - else if (coordinates_type == ECoordinatesType::Local && (selection.is_wipe_tower() || selection.is_single_volume_or_modifier())) { - const GLVolume& v = *selection.get_first_volume(); - m_orient_matrix = v.get_instance_transformation().get_rotation_matrix() * v.get_volume_transformation().get_rotation_matrix(); - } - else { - const GLVolume& v = *selection.get_first_volume(); - m_orient_matrix = v.get_instance_transformation().get_rotation_matrix(); - } -} -#endif // ENABLE_WORLD_COORDINATE - -void GLGizmoRotate3D::on_render_input_window(float x, float y, float bottom_limit) -{ - if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA) - return; - - RotoptimzeWindow popup{m_imgui, m_rotoptimizewin_state, {x, y, bottom_limit}}; -} - -void GLGizmoRotate3D::load_rotoptimize_state() -{ - std::string accuracy_str = - wxGetApp().app_config->get("sla_auto_rotate", "accuracy"); - - std::string method_str = - wxGetApp().app_config->get("sla_auto_rotate", "method_id"); - - if (!accuracy_str.empty()) { - float accuracy = std::stof(accuracy_str); - accuracy = std::max(0.f, std::min(accuracy, 1.f)); - - m_rotoptimizewin_state.accuracy = accuracy; - } - - if (!method_str.empty()) { - int method_id = std::stoi(method_str); - if (method_id < int(RotoptimizeJob::get_methods_count())) - m_rotoptimizewin_state.method_id = method_id; - } -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -void GLGizmoRotate::render_circle(const ColorRGBA& color, bool radius_changed) -#else -void GLGizmoRotate::render_circle() const -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -{ -#if ENABLE_LEGACY_OPENGL_REMOVAL - if (!m_circle.is_initialized() || radius_changed) { - m_circle.reset(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::LineLoop, GLModel::Geometry::EVertexLayout::P3 }; - init_data.reserve_vertices(ScaleStepsCount); - init_data.reserve_indices(ScaleStepsCount); - - // vertices + indices - for (unsigned int i = 0; i < ScaleStepsCount; ++i) { - const float angle = float(i * ScaleStepRad); - init_data.add_vertex(Vec3f(::cos(angle) * m_radius, ::sin(angle) * m_radius, 0.0f)); - init_data.add_index(i); - } - - m_circle.init_from(std::move(init_data)); - } - - m_circle.set_color(color); - m_circle.render(); -#else - ::glBegin(GL_LINE_LOOP); - for (unsigned int i = 0; i < ScaleStepsCount; ++i) { - const float angle = float(i) * ScaleStepRad; - const float x = ::cos(angle) * m_radius; - const float y = ::sin(angle) * m_radius; - const float z = 0.0f; - ::glVertex3f((GLfloat)x, (GLfloat)y, (GLfloat)z); - } - glsafe(::glEnd()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -void GLGizmoRotate::render_scale(const ColorRGBA& color, bool radius_changed) -#else -void GLGizmoRotate::render_scale() const -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -{ - const float out_radius_long = m_snap_fine_out_radius; - const float out_radius_short = m_radius * (1.0f + 0.5f * ScaleLongTooth); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - if (!m_scale.is_initialized() || radius_changed) { - m_scale.reset(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; - init_data.reserve_vertices(2 * ScaleStepsCount); - init_data.reserve_indices(2 * ScaleStepsCount); - - // vertices + indices - for (unsigned int i = 0; i < ScaleStepsCount; ++i) { - const float angle = float(i * ScaleStepRad); - const float cosa = ::cos(angle); - const float sina = ::sin(angle); - const float in_x = cosa * m_radius; - const float in_y = sina * m_radius; - const float out_x = (i % ScaleLongEvery == 0) ? cosa * out_radius_long : cosa * out_radius_short; - const float out_y = (i % ScaleLongEvery == 0) ? sina * out_radius_long : sina * out_radius_short; - - // vertices - init_data.add_vertex(Vec3f(in_x, in_y, 0.0f)); - init_data.add_vertex(Vec3f(out_x, out_y, 0.0f)); - - // indices - init_data.add_line(i * 2, i * 2 + 1); - } - - m_scale.init_from(std::move(init_data)); - } - - m_scale.set_color(color); - m_scale.render(); -#else - ::glBegin(GL_LINES); - for (unsigned int i = 0; i < ScaleStepsCount; ++i) { - const float angle = (float)i * ScaleStepRad; - const float cosa = ::cos(angle); - const float sina = ::sin(angle); - const float in_x = cosa * m_radius; - const float in_y = sina * m_radius; - const float in_z = 0.0f; - const float out_x = (i % ScaleLongEvery == 0) ? cosa * out_radius_long : cosa * out_radius_short; - const float out_y = (i % ScaleLongEvery == 0) ? sina * out_radius_long : sina * out_radius_short; - const float out_z = 0.0f; - ::glVertex3f((GLfloat)in_x, (GLfloat)in_y, (GLfloat)in_z); - ::glVertex3f((GLfloat)out_x, (GLfloat)out_y, (GLfloat)out_z); - } - glsafe(::glEnd()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -void GLGizmoRotate::render_snap_radii(const ColorRGBA& color, bool radius_changed) -#else -void GLGizmoRotate::render_snap_radii() const -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -{ - const float step = 2.0f * float(PI) / float(SnapRegionsCount); - const float in_radius = m_radius / 3.0f; - const float out_radius = 2.0f * in_radius; - -#if ENABLE_LEGACY_OPENGL_REMOVAL - if (!m_snap_radii.is_initialized() || radius_changed) { - m_snap_radii.reset(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; - init_data.reserve_vertices(2 * ScaleStepsCount); - init_data.reserve_indices(2 * ScaleStepsCount); - - // vertices + indices - for (unsigned int i = 0; i < ScaleStepsCount; ++i) { - const float angle = float(i * step); - const float cosa = ::cos(angle); - const float sina = ::sin(angle); - const float in_x = cosa * in_radius; - const float in_y = sina * in_radius; - const float out_x = cosa * out_radius; - const float out_y = sina * out_radius; - - // vertices - init_data.add_vertex(Vec3f(in_x, in_y, 0.0f)); - init_data.add_vertex(Vec3f(out_x, out_y, 0.0f)); - - // indices - init_data.add_line(i * 2, i * 2 + 1); - } - - m_snap_radii.init_from(std::move(init_data)); - } - - m_snap_radii.set_color(color); - m_snap_radii.render(); -#else - ::glBegin(GL_LINES); - for (unsigned int i = 0; i < SnapRegionsCount; ++i) { - const float angle = (float)i * step; - const float cosa = ::cos(angle); - const float sina = ::sin(angle); - const float in_x = cosa * in_radius; - const float in_y = sina * in_radius; - const float in_z = 0.0f; - const float out_x = cosa * out_radius; - const float out_y = sina * out_radius; - const float out_z = 0.0f; - ::glVertex3f((GLfloat)in_x, (GLfloat)in_y, (GLfloat)in_z); - ::glVertex3f((GLfloat)out_x, (GLfloat)out_y, (GLfloat)out_z); - } - glsafe(::glEnd()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -void GLGizmoRotate::render_reference_radius(const ColorRGBA& color, bool radius_changed) -{ - if (!m_reference_radius.is_initialized() || radius_changed) { - m_reference_radius.reset(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; - 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(m_radius * (1.0f + GrabberOffset), 0.0f, 0.0f)); - - // indices - init_data.add_line(0, 1); - - m_reference_radius.init_from(std::move(init_data)); - } - - m_reference_radius.set_color(color); - m_reference_radius.render(); -} -#else -void GLGizmoRotate::render_reference_radius() const -{ - ::glBegin(GL_LINES); - ::glVertex3f(0.0f, 0.0f, 0.0f); - ::glVertex3f((GLfloat)(m_radius * (1.0f + GrabberOffset)), 0.0f, 0.0f); - glsafe(::glEnd()); -} -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_LEGACY_OPENGL_REMOVAL -void GLGizmoRotate::render_angle_arc(const ColorRGBA& color, bool radius_changed) -#else -void GLGizmoRotate::render_angle() const -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -{ - const float step_angle = float(m_angle) / float(AngleResolution); - const float ex_radius = m_radius * (1.0f + GrabberOffset); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - const bool angle_changed = std::abs(m_old_angle - m_angle) > EPSILON; - m_old_angle = m_angle; - - if (!m_angle_arc.is_initialized() || radius_changed || angle_changed) { - m_angle_arc.reset(); - if (m_angle > 0.0f) { - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::LineStrip, GLModel::Geometry::EVertexLayout::P3 }; - init_data.reserve_vertices(1 + AngleResolution); - init_data.reserve_indices(1 + AngleResolution); - - // vertices + indices - for (unsigned int i = 0; i <= AngleResolution; ++i) { - const float angle = float(i) * step_angle; - init_data.add_vertex(Vec3f(::cos(angle) * ex_radius, ::sin(angle) * ex_radius, 0.0f)); - init_data.add_index(i); - } - - m_angle_arc.init_from(std::move(init_data)); - } - } - - m_angle_arc.set_color(color); - m_angle_arc.render(); -#else - ::glBegin(GL_LINE_STRIP); - for (unsigned int i = 0; i <= AngleResolution; ++i) { - const float angle = float(i) * step_angle; - const float x = ::cos(angle) * ex_radius; - const float y = ::sin(angle) * ex_radius; - const float z = 0.0f; - ::glVertex3f((GLfloat)x, (GLfloat)y, (GLfloat)z); - } - glsafe(::glEnd()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -void GLGizmoRotate::render_grabber_connection(const ColorRGBA& color, bool radius_changed) -{ - if (!m_grabber_connection.model.is_initialized() || radius_changed || !m_grabber_connection.old_center.isApprox(m_grabbers.front().center)) { - m_grabber_connection.model.reset(); - m_grabber_connection.old_center = m_grabbers.front().center; - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; - 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)m_grabbers.front().center.cast()); - - // indices - init_data.add_line(0, 1); - - m_grabber_connection.model.init_from(std::move(init_data)); - } - - m_grabber_connection.model.set_color(color); - m_grabber_connection.model.render(); -} -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -void GLGizmoRotate::render_grabber(const BoundingBoxf3& box) -{ -#if !ENABLE_LEGACY_OPENGL_REMOVAL - const double grabber_radius = double(m_radius) * (1.0 + double(GrabberOffset)); - m_grabbers[0].center = Vec3d(::cos(m_angle) * grabber_radius, ::sin(m_angle) * grabber_radius, 0.0); - m_grabbers[0].angles.z() = m_angle; - - glsafe(::glColor4fv((m_hover_id != -1) ? m_drag_color.data() : m_highlight_color.data())); - - ::glBegin(GL_LINES); - ::glVertex3f(0.0f, 0.0f, 0.0f); - ::glVertex3dv(m_grabbers[0].center.data()); - glsafe(::glEnd()); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - - m_grabbers.front().color = m_highlight_color; - render_grabbers(box); -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -Transform3d GLGizmoRotate::local_transform(const Selection& selection) const -{ - Transform3d ret; - - switch (m_axis) - { - case X: - { -#if ENABLE_WORLD_COORDINATE - ret = Geometry::rotation_transform(0.5 * PI * Vec3d::UnitY()) * Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitZ()); -#else - ret = Geometry::assemble_transform(Vec3d::Zero(), 0.5 * PI * Vec3d::UnitY()) * Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitZ()); -#endif // ENABLE_WORLD_COORDINATE - break; - } - case Y: - { -#if ENABLE_WORLD_COORDINATE - ret = Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitZ()) * Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitY()); -#else - ret = Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitZ()) * Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitY()); -#endif // ENABLE_WORLD_COORDINATE - break; - } - default: - case Z: - { - ret = Transform3d::Identity(); - break; - } - } - -#if ENABLE_WORLD_COORDINATE - return Geometry::translation_transform(m_center) * m_orient_matrix * ret; -#else - if (selection.is_single_volume() || selection.is_single_modifier() || selection.requires_local_axes()) - ret = selection.get_first_volume()->get_instance_transformation().get_matrix(true, false, true, true) * ret; - - return Geometry::assemble_transform(m_center) * ret; -#endif // ENABLE_WORLD_COORDINATE -} -#else -void GLGizmoRotate::transform_to_local(const Selection& selection) const -{ - glsafe(::glTranslated(m_center.x(), m_center.y(), m_center.z())); - -#if ENABLE_WORLD_COORDINATE - glsafe(::glMultMatrixd(m_orient_matrix.data())); -#else - if (selection.is_single_volume() || selection.is_single_modifier() || selection.requires_local_axes()) { - const Transform3d orient_matrix = selection.get_first_volume()->get_instance_transformation().get_matrix(true, false, true, true); - glsafe(::glMultMatrixd(orient_matrix.data())); - } -#endif // ENABLE_WORLD_COORDINATE - - switch (m_axis) - { - case X: - { - glsafe(::glRotatef(90.0f, 0.0f, 1.0f, 0.0f)); - glsafe(::glRotatef(-90.0f, 0.0f, 0.0f, 1.0f)); - break; - } - case Y: - { - glsafe(::glRotatef(-90.0f, 0.0f, 0.0f, 1.0f)); - glsafe(::glRotatef(-90.0f, 0.0f, 1.0f, 0.0f)); - break; - } - default: - case Z: - { - // no rotation - break; - } - } -} -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_WORLD_COORDINATE -Vec3d GLGizmoRotate::mouse_position_in_local_plane(const Linef3& mouse_ray) const -#else -Vec3d GLGizmoRotate::mouse_position_in_local_plane(const Linef3& mouse_ray, const Selection& selection) const -#endif // ENABLE_WORLD_COORDINATE -{ - double half_pi = 0.5 * double(PI); - - Transform3d m = Transform3d::Identity(); - - switch (m_axis) - { - case X: - { - m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitZ())); - m.rotate(Eigen::AngleAxisd(-half_pi, Vec3d::UnitY())); - break; - } - case Y: - { - m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitY())); - m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitZ())); - break; - } - default: - case Z: - { - // no rotation applied - break; - } - } - -#if ENABLE_WORLD_COORDINATE - m = m * m_orient_matrix.inverse(); -#else - if (selection.is_single_volume() || selection.is_single_modifier() || selection.requires_local_axes()) - m = m * selection.get_first_volume()->get_instance_transformation().get_matrix(true, false, true, true).inverse(); -#endif // ENABLE_WORLD_COORDINATE - - m.translate(-m_center); - - const Linef3 local_mouse_ray = transform(mouse_ray, m); - if (std::abs(local_mouse_ray.vector().dot(Vec3d::UnitZ())) < EPSILON) { - // if the ray is parallel to the plane containing the circle - if (std::abs(local_mouse_ray.vector().dot(Vec3d::UnitY())) > 1.0 - EPSILON) - // if the ray is parallel to grabber direction - return Vec3d::UnitX(); - else { - const Vec3d world_pos = (local_mouse_ray.a.x() >= 0.0) ? mouse_ray.a - m_center : mouse_ray.b - m_center; - m.translate(m_center); - return m * world_pos; - } - } - else - return local_mouse_ray.intersect_plane(0.0); -} - -GLGizmoRotate3D::GLGizmoRotate3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) - : GLGizmoBase(parent, icon_filename, sprite_id) - , m_gizmos({ - GLGizmoRotate(parent, GLGizmoRotate::X), - GLGizmoRotate(parent, GLGizmoRotate::Y), - GLGizmoRotate(parent, GLGizmoRotate::Z) }) -{ - load_rotoptimize_state(); -} - -bool GLGizmoRotate3D::on_mouse(const wxMouseEvent &mouse_event) -{ - if (mouse_event.Dragging() && m_dragging) { - // Apply new temporary rotations -#if ENABLE_WORLD_COORDINATE - TransformationType transformation_type; - if (m_parent.get_selection().is_wipe_tower()) - transformation_type = TransformationType::Instance_Relative_Joint; - else { - switch (wxGetApp().obj_manipul()->get_coordinates_type()) - { - default: - case ECoordinatesType::World: { transformation_type = TransformationType::World_Relative_Joint; break; } - case ECoordinatesType::Instance: { transformation_type = TransformationType::Instance_Relative_Joint; break; } - case ECoordinatesType::Local: { transformation_type = TransformationType::Local_Relative_Joint; break; } - } - } -#else - TransformationType transformation_type(TransformationType::World_Relative_Joint); -#endif // ENABLE_WORLD_COORDINATE - if (mouse_event.AltDown()) - transformation_type.set_independent(); - m_parent.get_selection().rotate(get_rotation(), transformation_type); - } - return use_grabbers(mouse_event); -} - -void GLGizmoRotate3D::data_changed() { - if (m_parent.get_selection().is_wipe_tower()) { -#if !ENABLE_WORLD_COORDINATE - const DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; - const float wipe_tower_rotation_angle = - dynamic_cast( - config.option("wipe_tower_rotation_angle"))->value; - set_rotation(Vec3d(0., 0., (M_PI / 180.) * wipe_tower_rotation_angle)); -#endif // !ENABLE_WORLD_COORDINATE - m_gizmos[0].disable_grabber(); - m_gizmos[1].disable_grabber(); - } - else { -#if !ENABLE_WORLD_COORDINATE - set_rotation(Vec3d::Zero()); -#endif // !ENABLE_WORLD_COORDINATE - m_gizmos[0].enable_grabber(); - m_gizmos[1].enable_grabber(); - } -#if ENABLE_WORLD_COORDINATE - set_rotation(Vec3d::Zero()); -#endif // ENABLE_WORLD_COORDINATE -} - -bool GLGizmoRotate3D::on_init() -{ - for (GLGizmoRotate& g : m_gizmos) - if (!g.init()) return false; - - for (unsigned int i = 0; i < 3; ++i) - m_gizmos[i].set_highlight_color(AXES_COLOR[i]); - - m_shortcut_key = WXK_CONTROL_R; - - return true; -} - -std::string GLGizmoRotate3D::on_get_name() const -{ - return _u8L("Rotate"); -} - -bool GLGizmoRotate3D::on_is_activable() const -{ - return !m_parent.get_selection().is_empty(); -} - -void GLGizmoRotate3D::on_start_dragging() -{ - assert(0 <= m_hover_id && m_hover_id < 3); - m_gizmos[m_hover_id].start_dragging(); -} - -void GLGizmoRotate3D::on_stop_dragging() -{ - assert(0 <= m_hover_id && m_hover_id < 3); - m_parent.do_rotate(L("Gizmo-Rotate")); - m_gizmos[m_hover_id].stop_dragging(); -} - -void GLGizmoRotate3D::on_dragging(const UpdateData &data) -{ - assert(0 <= m_hover_id && m_hover_id < 3); - m_gizmos[m_hover_id].dragging(data); -} - -void GLGizmoRotate3D::on_render() -{ - glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); - - if (m_hover_id == -1 || m_hover_id == 0) - m_gizmos[X].render(); - - if (m_hover_id == -1 || m_hover_id == 1) - m_gizmos[Y].render(); - - if (m_hover_id == -1 || m_hover_id == 2) - m_gizmos[Z].render(); -} - -#if ENABLE_RAYCAST_PICKING -void GLGizmoRotate3D::on_register_raycasters_for_picking() -{ - // the gizmo grabbers are rendered on top of the scene, so the raytraced picker should take it into account - m_parent.set_raycaster_gizmos_on_top(true); - for (GLGizmoRotate& g : m_gizmos) { - g.register_raycasters_for_picking(); - } -} - -void GLGizmoRotate3D::on_unregister_raycasters_for_picking() -{ - for (GLGizmoRotate& g : m_gizmos) { - g.unregister_raycasters_for_picking(); - } - m_parent.set_raycaster_gizmos_on_top(false); -} -#endif // ENABLE_RAYCAST_PICKING - -GLGizmoRotate3D::RotoptimzeWindow::RotoptimzeWindow(ImGuiWrapper * imgui, - State & state, - const Alignment &alignment) - : m_imgui{imgui} -{ - imgui->begin(_L("Optimize orientation"), ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoCollapse); - - // adjust window position to avoid overlap the view toolbar - float win_h = ImGui::GetWindowHeight(); - float x = alignment.x, y = alignment.y; - y = std::min(y, alignment.bottom_limit - win_h); - ImGui::SetWindowPos(ImVec2(x, y), ImGuiCond_Always); - - float max_text_w = 0.; - auto padding = ImGui::GetStyle().FramePadding; - padding.x *= 2.f; - padding.y *= 2.f; - - for (size_t i = 0; i < RotoptimizeJob::get_methods_count(); ++i) { - float w = - ImGui::CalcTextSize(RotoptimizeJob::get_method_name(i).c_str()).x + - padding.x + ImGui::GetFrameHeight(); - max_text_w = std::max(w, max_text_w); - } - - ImGui::PushItemWidth(max_text_w); - - if (ImGui::BeginCombo("", RotoptimizeJob::get_method_name(state.method_id).c_str())) { - for (size_t i = 0; i < RotoptimizeJob::get_methods_count(); ++i) { - if (ImGui::Selectable(RotoptimizeJob::get_method_name(i).c_str())) { - state.method_id = i; - wxGetApp().app_config->set("sla_auto_rotate", - "method_id", - std::to_string(state.method_id)); - } - - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", RotoptimizeJob::get_method_description(i).c_str()); - } - - ImGui::EndCombo(); - } - - ImVec2 sz = ImGui::GetItemRectSize(); - - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", RotoptimizeJob::get_method_description(state.method_id).c_str()); - - ImGui::Separator(); - - auto btn_txt = _L("Apply"); - auto btn_txt_sz = ImGui::CalcTextSize(btn_txt.c_str()); - ImVec2 button_sz = {btn_txt_sz.x + padding.x, btn_txt_sz.y + padding.y}; - ImGui::SetCursorPosX(padding.x + sz.x - button_sz.x); - - if (!wxGetApp().plater()->get_ui_job_worker().is_idle()) - imgui->disabled_begin(true); - - if ( imgui->button(btn_txt) ) { - replace_job(wxGetApp().plater()->get_ui_job_worker(), - std::make_unique()); - } - - imgui->disabled_end(); -} - -GLGizmoRotate3D::RotoptimzeWindow::~RotoptimzeWindow() -{ - m_imgui->end(); -} - -} // namespace GUI -} // namespace Slic3r +// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro. +#include "GLGizmoRotate.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/ImGuiWrapper.hpp" +#if ENABLE_WORLD_COORDINATE +#include "slic3r/GUI/GUI_ObjectManipulation.hpp" +#endif // ENABLE_WORLD_COORDINATE + +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/Jobs/RotoptimizeJob.hpp" + +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Model.hpp" + +#include + +namespace Slic3r { +namespace GUI { + + +const float GLGizmoRotate::Offset = 5.0f; +const unsigned int GLGizmoRotate::AngleResolution = 64; +const unsigned int GLGizmoRotate::ScaleStepsCount = 72; +const float GLGizmoRotate::ScaleStepRad = 2.0f * float(PI) / GLGizmoRotate::ScaleStepsCount; +const unsigned int GLGizmoRotate::ScaleLongEvery = 2; +const float GLGizmoRotate::ScaleLongTooth = 0.1f; // in percent of radius +const unsigned int GLGizmoRotate::SnapRegionsCount = 8; +const float GLGizmoRotate::GrabberOffset = 0.15f; // in percent of radius + +GLGizmoRotate::GLGizmoRotate(GLCanvas3D& parent, GLGizmoRotate::Axis axis) + : GLGizmoBase(parent, "", -1) + , m_axis(axis) + , m_angle(0.0) + , m_center(0.0, 0.0, 0.0) + , m_radius(0.0f) + , m_snap_coarse_in_radius(0.0f) + , m_snap_coarse_out_radius(0.0f) + , m_snap_fine_in_radius(0.0f) + , m_snap_fine_out_radius(0.0f) + , m_drag_color(DEFAULT_DRAG_COLOR) + , m_highlight_color(DEFAULT_HIGHLIGHT_COLOR) +{ + m_group_id = static_cast(axis); +} + +void GLGizmoRotate::set_highlight_color(const ColorRGBA &color) +{ + m_highlight_color = color; +} + +void GLGizmoRotate::set_angle(double angle) +{ + if (std::abs(angle - 2.0 * double(PI)) < EPSILON) + angle = 0.0; + + m_angle = angle; +} + +std::string GLGizmoRotate::get_tooltip() const +{ + std::string axis; + switch (m_axis) + { + case X: { axis = "X"; break; } + case Y: { axis = "Y"; break; } + case Z: { axis = "Z"; break; } + } + return (m_hover_id == 0 || m_grabbers.front().dragging) ? axis + ": " + format(float(Geometry::rad2deg(m_angle)), 4) : ""; +} + +bool GLGizmoRotate::on_mouse(const wxMouseEvent &mouse_event) +{ + return use_grabbers(mouse_event); +} + +void GLGizmoRotate::dragging(const UpdateData &data) { on_dragging(data); } + +void GLGizmoRotate::start_dragging() +{ + m_grabbers[0].dragging = true; + on_start_dragging(); +} + +void GLGizmoRotate::stop_dragging() +{ + m_grabbers[0].dragging = false; + on_stop_dragging(); +} + +void GLGizmoRotate::enable_grabber() { m_grabbers[0].enabled = true; } +void GLGizmoRotate::disable_grabber() { m_grabbers[0].enabled = false; } + +bool GLGizmoRotate::on_init() +{ + m_grabbers.push_back(Grabber()); + m_grabbers.back().extensions = (GLGizmoBase::EGrabberExtension)(int(GLGizmoBase::EGrabberExtension::PosY) | int(GLGizmoBase::EGrabberExtension::NegY)); + return true; +} + +void GLGizmoRotate::on_start_dragging() +{ +#if ENABLE_WORLD_COORDINATE + init_data_from_selection(m_parent.get_selection()); +#else + const BoundingBoxf3& box = m_parent.get_selection().get_bounding_box(); + m_center = box.center(); + m_radius = Offset + box.radius(); + m_snap_coarse_in_radius = m_radius / 3.0f; + m_snap_coarse_out_radius = 2.0f * m_snap_coarse_in_radius; + m_snap_fine_in_radius = m_radius; + m_snap_fine_out_radius = m_snap_fine_in_radius + m_radius * ScaleLongTooth; +#endif // ENABLE_WORLD_COORDINATE +} + +void GLGizmoRotate::on_dragging(const UpdateData &data) +{ +#if ENABLE_WORLD_COORDINATE + const Vec2d mouse_pos = to_2d(mouse_position_in_local_plane(data.mouse_ray)); +#else + const Vec2d mouse_pos = to_2d(mouse_position_in_local_plane(data.mouse_ray, m_parent.get_selection())); +#endif // ENABLE_WORLD_COORDINATE + + const Vec2d orig_dir = Vec2d::UnitX(); + const Vec2d new_dir = mouse_pos.normalized(); + + double theta = ::acos(std::clamp(new_dir.dot(orig_dir), -1.0, 1.0)); + if (cross2(orig_dir, new_dir) < 0.0) + theta = 2.0 * (double)PI - theta; + + const double len = mouse_pos.norm(); + + // snap to coarse snap region + if (m_snap_coarse_in_radius <= len && len <= m_snap_coarse_out_radius) { + const double step = 2.0 * double(PI) / double(SnapRegionsCount); + theta = step * std::round(theta / step); + } + else { + // snap to fine snap region (scale) + if (m_snap_fine_in_radius <= len && len <= m_snap_fine_out_radius) { + const double step = 2.0 * double(PI) / double(ScaleStepsCount); + theta = step * std::round(theta / step); + } + } + + if (theta == 2.0 * double(PI)) + theta = 0.0; + + m_angle = theta; +} + +void GLGizmoRotate::on_render() +{ + if (!m_grabbers.front().enabled) + return; + + const Selection& selection = m_parent.get_selection(); +#if !ENABLE_WORLD_COORDINATE + const BoundingBoxf3& box = selection.get_bounding_box(); +#endif // !ENABLE_WORLD_COORDINATE + + if (m_hover_id != 0 && !m_grabbers.front().dragging) { +#if ENABLE_WORLD_COORDINATE + init_data_from_selection(selection); +#else + m_center = box.center(); + m_radius = Offset + box.radius(); + m_snap_coarse_in_radius = m_radius / 3.0f; + m_snap_coarse_out_radius = 2.0f * m_snap_coarse_in_radius; + m_snap_fine_in_radius = m_radius; + m_snap_fine_out_radius = m_radius * (1.0f + ScaleLongTooth); +#endif // ENABLE_WORLD_COORDINATE + } + + const double grabber_radius = (double)m_radius * (1.0 + (double)GrabberOffset); + m_grabbers.front().center = Vec3d(::cos(m_angle) * grabber_radius, ::sin(m_angle) * grabber_radius, 0.0); + m_grabbers.front().angles.z() = m_angle; + + glsafe(::glEnable(GL_DEPTH_TEST)); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + m_grabbers.front().matrix = local_transform(selection); +#else + glsafe(::glPushMatrix()); + transform_to_local(selection); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_GL_CORE_PROFILE + if (!OpenGLManager::get_gl_info().is_core_profile()) +#endif // ENABLE_GL_CORE_PROFILE + glsafe(::glLineWidth((m_hover_id != -1) ? 2.0f : 1.5f)); +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_GL_CORE_PROFILE + GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); +#else + GLShaderProgram* shader = wxGetApp().get_shader("flat"); +#endif // ENABLE_GL_CORE_PROFILE + if (shader != nullptr) { + shader->start_using(); + + const Camera& camera = wxGetApp().plater()->get_camera(); + const Transform3d view_model_matrix = camera.get_view_matrix() * m_grabbers.front().matrix; + shader->set_uniform("view_model_matrix", view_model_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); +#if ENABLE_GL_CORE_PROFILE + const std::array& viewport = camera.get_viewport(); + shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); + shader->set_uniform("width", 0.25f); + shader->set_uniform("gap_size", 0.0f); +#endif // ENABLE_GL_CORE_PROFILE + + const bool radius_changed = std::abs(m_old_radius - m_radius) > EPSILON; + m_old_radius = m_radius; + + const ColorRGBA color = (m_hover_id != -1) ? m_drag_color : m_highlight_color; + render_circle(color, radius_changed); + if (m_hover_id != -1) { + const bool hover_radius_changed = std::abs(m_old_hover_radius - m_radius) > EPSILON; + m_old_hover_radius = m_radius; + + render_scale(color, hover_radius_changed); + render_snap_radii(color, hover_radius_changed); + render_reference_radius(color, hover_radius_changed); + render_angle_arc(m_highlight_color, hover_radius_changed); + } + + render_grabber_connection(color, radius_changed); + shader->stop_using(); + } +#else + glsafe(::glColor4fv((m_hover_id != -1) ? m_drag_color.data() : m_highlight_color.data())); + + render_circle(); + + if (m_hover_id != -1) { + render_scale(); + render_snap_radii(); + render_reference_radius(); + } + + glsafe(::glColor4fv(m_highlight_color.data())); + + if (m_hover_id != -1) + render_angle(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_WORLD_COORDINATE + render_grabber(m_bounding_box); +#else + render_grabber(box); +#endif // ENABLE_WORLD_COORDINATE + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glPopMatrix()); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL +} + +#if !ENABLE_RAYCAST_PICKING +void GLGizmoRotate::on_render_for_picking() +{ + const Selection& selection = m_parent.get_selection(); + + glsafe(::glDisable(GL_DEPTH_TEST)); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + m_grabbers.front().matrix = local_transform(selection); +#else + glsafe(::glPushMatrix()); + transform_to_local(selection); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_WORLD_COORDINATE + render_grabbers_for_picking(m_bounding_box); +#else + const BoundingBoxf3& box = selection.get_bounding_box(); + render_grabbers_for_picking(box); +#endif // ENABLE_WORLD_COORDINATE + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glPopMatrix()); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL +} +#endif // !ENABLE_RAYCAST_PICKING + +#if ENABLE_WORLD_COORDINATE +void GLGizmoRotate::init_data_from_selection(const Selection& selection) +{ + ECoordinatesType coordinates_type; + if (selection.is_wipe_tower()) + coordinates_type = ECoordinatesType::Local; + else + coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); + if (coordinates_type == ECoordinatesType::World) { + m_bounding_box = selection.get_bounding_box(); + m_center = m_bounding_box.center(); + } + else if (coordinates_type == ECoordinatesType::Local && selection.is_single_volume_or_modifier()) { + const GLVolume& v = *selection.get_first_volume(); + m_bounding_box = v.transformed_convex_hull_bounding_box( + v.get_instance_transformation().get_scaling_factor_matrix() * v.get_volume_transformation().get_scaling_factor_matrix()); + m_center = v.world_matrix() * m_bounding_box.center(); + } + else { + m_bounding_box.reset(); + const Selection::IndicesList& ids = selection.get_volume_idxs(); + for (unsigned int id : ids) { + const GLVolume& v = *selection.get_volume(id); + m_bounding_box.merge(v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_matrix())); + } + const Geometry::Transformation inst_trafo = selection.get_first_volume()->get_instance_transformation(); + m_bounding_box = m_bounding_box.transformed(inst_trafo.get_scaling_factor_matrix()); + m_center = inst_trafo.get_matrix_no_scaling_factor() * m_bounding_box.center(); + } + + m_radius = Offset + m_bounding_box.radius(); + m_snap_coarse_in_radius = m_radius / 3.0f; + m_snap_coarse_out_radius = 2.0f * m_snap_coarse_in_radius; + m_snap_fine_in_radius = m_radius; + m_snap_fine_out_radius = m_snap_fine_in_radius + m_radius * ScaleLongTooth; + + if (coordinates_type == ECoordinatesType::World) + m_orient_matrix = Transform3d::Identity(); + else if (coordinates_type == ECoordinatesType::Local && (selection.is_wipe_tower() || selection.is_single_volume_or_modifier())) { + const GLVolume& v = *selection.get_first_volume(); + m_orient_matrix = v.get_instance_transformation().get_rotation_matrix() * v.get_volume_transformation().get_rotation_matrix(); + } + else { + const GLVolume& v = *selection.get_first_volume(); + m_orient_matrix = v.get_instance_transformation().get_rotation_matrix(); + } +} +#endif // ENABLE_WORLD_COORDINATE + +void GLGizmoRotate3D::on_render_input_window(float x, float y, float bottom_limit) +{ + if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA) + return; + + RotoptimzeWindow popup{m_imgui, m_rotoptimizewin_state, {x, y, bottom_limit}}; +} + +void GLGizmoRotate3D::load_rotoptimize_state() +{ + std::string accuracy_str = + wxGetApp().app_config->get("sla_auto_rotate", "accuracy"); + + std::string method_str = + wxGetApp().app_config->get("sla_auto_rotate", "method_id"); + + if (!accuracy_str.empty()) { + float accuracy = std::stof(accuracy_str); + accuracy = std::max(0.f, std::min(accuracy, 1.f)); + + m_rotoptimizewin_state.accuracy = accuracy; + } + + if (!method_str.empty()) { + int method_id = std::stoi(method_str); + if (method_id < int(RotoptimizeJob::get_methods_count())) + m_rotoptimizewin_state.method_id = method_id; + } +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +void GLGizmoRotate::render_circle(const ColorRGBA& color, bool radius_changed) +#else +void GLGizmoRotate::render_circle() const +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +{ +#if ENABLE_LEGACY_OPENGL_REMOVAL + if (!m_circle.is_initialized() || radius_changed) { + m_circle.reset(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::LineLoop, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(ScaleStepsCount); + init_data.reserve_indices(ScaleStepsCount); + + // vertices + indices + for (unsigned int i = 0; i < ScaleStepsCount; ++i) { + const float angle = float(i * ScaleStepRad); + init_data.add_vertex(Vec3f(::cos(angle) * m_radius, ::sin(angle) * m_radius, 0.0f)); + init_data.add_index(i); + } + + m_circle.init_from(std::move(init_data)); + } + + m_circle.set_color(color); + m_circle.render(); +#else + ::glBegin(GL_LINE_LOOP); + for (unsigned int i = 0; i < ScaleStepsCount; ++i) { + const float angle = float(i) * ScaleStepRad; + const float x = ::cos(angle) * m_radius; + const float y = ::sin(angle) * m_radius; + const float z = 0.0f; + ::glVertex3f((GLfloat)x, (GLfloat)y, (GLfloat)z); + } + glsafe(::glEnd()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +void GLGizmoRotate::render_scale(const ColorRGBA& color, bool radius_changed) +#else +void GLGizmoRotate::render_scale() const +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +{ + const float out_radius_long = m_snap_fine_out_radius; + const float out_radius_short = m_radius * (1.0f + 0.5f * ScaleLongTooth); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + if (!m_scale.is_initialized() || radius_changed) { + m_scale.reset(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(2 * ScaleStepsCount); + init_data.reserve_indices(2 * ScaleStepsCount); + + // vertices + indices + for (unsigned int i = 0; i < ScaleStepsCount; ++i) { + const float angle = float(i * ScaleStepRad); + const float cosa = ::cos(angle); + const float sina = ::sin(angle); + const float in_x = cosa * m_radius; + const float in_y = sina * m_radius; + const float out_x = (i % ScaleLongEvery == 0) ? cosa * out_radius_long : cosa * out_radius_short; + const float out_y = (i % ScaleLongEvery == 0) ? sina * out_radius_long : sina * out_radius_short; + + // vertices + init_data.add_vertex(Vec3f(in_x, in_y, 0.0f)); + init_data.add_vertex(Vec3f(out_x, out_y, 0.0f)); + + // indices + init_data.add_line(i * 2, i * 2 + 1); + } + + m_scale.init_from(std::move(init_data)); + } + + m_scale.set_color(color); + m_scale.render(); +#else + ::glBegin(GL_LINES); + for (unsigned int i = 0; i < ScaleStepsCount; ++i) { + const float angle = (float)i * ScaleStepRad; + const float cosa = ::cos(angle); + const float sina = ::sin(angle); + const float in_x = cosa * m_radius; + const float in_y = sina * m_radius; + const float in_z = 0.0f; + const float out_x = (i % ScaleLongEvery == 0) ? cosa * out_radius_long : cosa * out_radius_short; + const float out_y = (i % ScaleLongEvery == 0) ? sina * out_radius_long : sina * out_radius_short; + const float out_z = 0.0f; + ::glVertex3f((GLfloat)in_x, (GLfloat)in_y, (GLfloat)in_z); + ::glVertex3f((GLfloat)out_x, (GLfloat)out_y, (GLfloat)out_z); + } + glsafe(::glEnd()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +void GLGizmoRotate::render_snap_radii(const ColorRGBA& color, bool radius_changed) +#else +void GLGizmoRotate::render_snap_radii() const +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +{ + const float step = 2.0f * float(PI) / float(SnapRegionsCount); + const float in_radius = m_radius / 3.0f; + const float out_radius = 2.0f * in_radius; + +#if ENABLE_LEGACY_OPENGL_REMOVAL + if (!m_snap_radii.is_initialized() || radius_changed) { + m_snap_radii.reset(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(2 * ScaleStepsCount); + init_data.reserve_indices(2 * ScaleStepsCount); + + // vertices + indices + for (unsigned int i = 0; i < ScaleStepsCount; ++i) { + const float angle = float(i * step); + const float cosa = ::cos(angle); + const float sina = ::sin(angle); + const float in_x = cosa * in_radius; + const float in_y = sina * in_radius; + const float out_x = cosa * out_radius; + const float out_y = sina * out_radius; + + // vertices + init_data.add_vertex(Vec3f(in_x, in_y, 0.0f)); + init_data.add_vertex(Vec3f(out_x, out_y, 0.0f)); + + // indices + init_data.add_line(i * 2, i * 2 + 1); + } + + m_snap_radii.init_from(std::move(init_data)); + } + + m_snap_radii.set_color(color); + m_snap_radii.render(); +#else + ::glBegin(GL_LINES); + for (unsigned int i = 0; i < SnapRegionsCount; ++i) { + const float angle = (float)i * step; + const float cosa = ::cos(angle); + const float sina = ::sin(angle); + const float in_x = cosa * in_radius; + const float in_y = sina * in_radius; + const float in_z = 0.0f; + const float out_x = cosa * out_radius; + const float out_y = sina * out_radius; + const float out_z = 0.0f; + ::glVertex3f((GLfloat)in_x, (GLfloat)in_y, (GLfloat)in_z); + ::glVertex3f((GLfloat)out_x, (GLfloat)out_y, (GLfloat)out_z); + } + glsafe(::glEnd()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +void GLGizmoRotate::render_reference_radius(const ColorRGBA& color, bool radius_changed) +{ + if (!m_reference_radius.is_initialized() || radius_changed) { + m_reference_radius.reset(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + 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(m_radius * (1.0f + GrabberOffset), 0.0f, 0.0f)); + + // indices + init_data.add_line(0, 1); + + m_reference_radius.init_from(std::move(init_data)); + } + + m_reference_radius.set_color(color); + m_reference_radius.render(); +} +#else +void GLGizmoRotate::render_reference_radius() const +{ + ::glBegin(GL_LINES); + ::glVertex3f(0.0f, 0.0f, 0.0f); + ::glVertex3f((GLfloat)(m_radius * (1.0f + GrabberOffset)), 0.0f, 0.0f); + glsafe(::glEnd()); +} +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_LEGACY_OPENGL_REMOVAL +void GLGizmoRotate::render_angle_arc(const ColorRGBA& color, bool radius_changed) +#else +void GLGizmoRotate::render_angle() const +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +{ + const float step_angle = float(m_angle) / float(AngleResolution); + const float ex_radius = m_radius * (1.0f + GrabberOffset); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + const bool angle_changed = std::abs(m_old_angle - m_angle) > EPSILON; + m_old_angle = m_angle; + + if (!m_angle_arc.is_initialized() || radius_changed || angle_changed) { + m_angle_arc.reset(); + if (m_angle > 0.0f) { + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::LineStrip, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(1 + AngleResolution); + init_data.reserve_indices(1 + AngleResolution); + + // vertices + indices + for (unsigned int i = 0; i <= AngleResolution; ++i) { + const float angle = float(i) * step_angle; + init_data.add_vertex(Vec3f(::cos(angle) * ex_radius, ::sin(angle) * ex_radius, 0.0f)); + init_data.add_index(i); + } + + m_angle_arc.init_from(std::move(init_data)); + } + } + + m_angle_arc.set_color(color); + m_angle_arc.render(); +#else + ::glBegin(GL_LINE_STRIP); + for (unsigned int i = 0; i <= AngleResolution; ++i) { + const float angle = float(i) * step_angle; + const float x = ::cos(angle) * ex_radius; + const float y = ::sin(angle) * ex_radius; + const float z = 0.0f; + ::glVertex3f((GLfloat)x, (GLfloat)y, (GLfloat)z); + } + glsafe(::glEnd()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +void GLGizmoRotate::render_grabber_connection(const ColorRGBA& color, bool radius_changed) +{ + if (!m_grabber_connection.model.is_initialized() || radius_changed || !m_grabber_connection.old_center.isApprox(m_grabbers.front().center)) { + m_grabber_connection.model.reset(); + m_grabber_connection.old_center = m_grabbers.front().center; + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + 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)m_grabbers.front().center.cast()); + + // indices + init_data.add_line(0, 1); + + m_grabber_connection.model.init_from(std::move(init_data)); + } + + m_grabber_connection.model.set_color(color); + m_grabber_connection.model.render(); +} +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +void GLGizmoRotate::render_grabber(const BoundingBoxf3& box) +{ +#if !ENABLE_LEGACY_OPENGL_REMOVAL + const double grabber_radius = double(m_radius) * (1.0 + double(GrabberOffset)); + m_grabbers[0].center = Vec3d(::cos(m_angle) * grabber_radius, ::sin(m_angle) * grabber_radius, 0.0); + m_grabbers[0].angles.z() = m_angle; + + glsafe(::glColor4fv((m_hover_id != -1) ? m_drag_color.data() : m_highlight_color.data())); + + ::glBegin(GL_LINES); + ::glVertex3f(0.0f, 0.0f, 0.0f); + ::glVertex3dv(m_grabbers[0].center.data()); + glsafe(::glEnd()); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + + m_grabbers.front().color = m_highlight_color; + render_grabbers(box); +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +Transform3d GLGizmoRotate::local_transform(const Selection& selection) const +{ + Transform3d ret; + + switch (m_axis) + { + case X: + { +#if ENABLE_WORLD_COORDINATE + ret = Geometry::rotation_transform(0.5 * PI * Vec3d::UnitY()) * Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitZ()); +#else + ret = Geometry::assemble_transform(Vec3d::Zero(), 0.5 * PI * Vec3d::UnitY()) * Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitZ()); +#endif // ENABLE_WORLD_COORDINATE + break; + } + case Y: + { +#if ENABLE_WORLD_COORDINATE + ret = Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitZ()) * Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitY()); +#else + ret = Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitZ()) * Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitY()); +#endif // ENABLE_WORLD_COORDINATE + break; + } + default: + case Z: + { + ret = Transform3d::Identity(); + break; + } + } + +#if ENABLE_WORLD_COORDINATE + return Geometry::translation_transform(m_center) * m_orient_matrix * ret; +#else + if (selection.is_single_volume() || selection.is_single_modifier() || selection.requires_local_axes()) + ret = selection.get_first_volume()->get_instance_transformation().get_matrix(true, false, true, true) * ret; + + return Geometry::assemble_transform(m_center) * ret; +#endif // ENABLE_WORLD_COORDINATE +} +#else +void GLGizmoRotate::transform_to_local(const Selection& selection) const +{ + glsafe(::glTranslated(m_center.x(), m_center.y(), m_center.z())); + +#if ENABLE_WORLD_COORDINATE + glsafe(::glMultMatrixd(m_orient_matrix.data())); +#else + if (selection.is_single_volume() || selection.is_single_modifier() || selection.requires_local_axes()) { + const Transform3d orient_matrix = selection.get_first_volume()->get_instance_transformation().get_matrix(true, false, true, true); + glsafe(::glMultMatrixd(orient_matrix.data())); + } +#endif // ENABLE_WORLD_COORDINATE + + switch (m_axis) + { + case X: + { + glsafe(::glRotatef(90.0f, 0.0f, 1.0f, 0.0f)); + glsafe(::glRotatef(-90.0f, 0.0f, 0.0f, 1.0f)); + break; + } + case Y: + { + glsafe(::glRotatef(-90.0f, 0.0f, 0.0f, 1.0f)); + glsafe(::glRotatef(-90.0f, 0.0f, 1.0f, 0.0f)); + break; + } + default: + case Z: + { + // no rotation + break; + } + } +} +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_WORLD_COORDINATE +Vec3d GLGizmoRotate::mouse_position_in_local_plane(const Linef3& mouse_ray) const +#else +Vec3d GLGizmoRotate::mouse_position_in_local_plane(const Linef3& mouse_ray, const Selection& selection) const +#endif // ENABLE_WORLD_COORDINATE +{ + double half_pi = 0.5 * double(PI); + + Transform3d m = Transform3d::Identity(); + + switch (m_axis) + { + case X: + { + m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitZ())); + m.rotate(Eigen::AngleAxisd(-half_pi, Vec3d::UnitY())); + break; + } + case Y: + { + m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitY())); + m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitZ())); + break; + } + default: + case Z: + { + // no rotation applied + break; + } + } + +#if ENABLE_WORLD_COORDINATE + m = m * m_orient_matrix.inverse(); +#else + if (selection.is_single_volume() || selection.is_single_modifier() || selection.requires_local_axes()) + m = m * selection.get_first_volume()->get_instance_transformation().get_matrix(true, false, true, true).inverse(); +#endif // ENABLE_WORLD_COORDINATE + + m.translate(-m_center); + + const Linef3 local_mouse_ray = transform(mouse_ray, m); + if (std::abs(local_mouse_ray.vector().dot(Vec3d::UnitZ())) < EPSILON) { + // if the ray is parallel to the plane containing the circle + if (std::abs(local_mouse_ray.vector().dot(Vec3d::UnitY())) > 1.0 - EPSILON) + // if the ray is parallel to grabber direction + return Vec3d::UnitX(); + else { + const Vec3d world_pos = (local_mouse_ray.a.x() >= 0.0) ? mouse_ray.a - m_center : mouse_ray.b - m_center; + m.translate(m_center); + return m * world_pos; + } + } + else + return local_mouse_ray.intersect_plane(0.0); +} + +GLGizmoRotate3D::GLGizmoRotate3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) + : GLGizmoBase(parent, icon_filename, sprite_id) + , m_gizmos({ + GLGizmoRotate(parent, GLGizmoRotate::X), + GLGizmoRotate(parent, GLGizmoRotate::Y), + GLGizmoRotate(parent, GLGizmoRotate::Z) }) +{ + load_rotoptimize_state(); +} + +bool GLGizmoRotate3D::on_mouse(const wxMouseEvent &mouse_event) +{ + if (mouse_event.Dragging() && m_dragging) { + // Apply new temporary rotations +#if ENABLE_WORLD_COORDINATE + TransformationType transformation_type; + if (m_parent.get_selection().is_wipe_tower()) + transformation_type = TransformationType::Instance_Relative_Joint; + else { + switch (wxGetApp().obj_manipul()->get_coordinates_type()) + { + default: + case ECoordinatesType::World: { transformation_type = TransformationType::World_Relative_Joint; break; } + case ECoordinatesType::Instance: { transformation_type = TransformationType::Instance_Relative_Joint; break; } + case ECoordinatesType::Local: { transformation_type = TransformationType::Local_Relative_Joint; break; } + } + } +#else + TransformationType transformation_type(TransformationType::World_Relative_Joint); +#endif // ENABLE_WORLD_COORDINATE + if (mouse_event.AltDown()) + transformation_type.set_independent(); + m_parent.get_selection().rotate(get_rotation(), transformation_type); + } + return use_grabbers(mouse_event); +} + +void GLGizmoRotate3D::data_changed() { + if (m_parent.get_selection().is_wipe_tower()) { +#if !ENABLE_WORLD_COORDINATE + const DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + const float wipe_tower_rotation_angle = + dynamic_cast( + config.option("wipe_tower_rotation_angle"))->value; + set_rotation(Vec3d(0., 0., (M_PI / 180.) * wipe_tower_rotation_angle)); +#endif // !ENABLE_WORLD_COORDINATE + m_gizmos[0].disable_grabber(); + m_gizmos[1].disable_grabber(); + } + else { +#if !ENABLE_WORLD_COORDINATE + set_rotation(Vec3d::Zero()); +#endif // !ENABLE_WORLD_COORDINATE + m_gizmos[0].enable_grabber(); + m_gizmos[1].enable_grabber(); + } +#if ENABLE_WORLD_COORDINATE + set_rotation(Vec3d::Zero()); +#endif // ENABLE_WORLD_COORDINATE +} + +bool GLGizmoRotate3D::on_init() +{ + for (GLGizmoRotate& g : m_gizmos) + if (!g.init()) return false; + + for (unsigned int i = 0; i < 3; ++i) + m_gizmos[i].set_highlight_color(AXES_COLOR[i]); + + m_shortcut_key = WXK_CONTROL_R; + + return true; +} + +std::string GLGizmoRotate3D::on_get_name() const +{ + return _u8L("Rotate"); +} + +bool GLGizmoRotate3D::on_is_activable() const +{ + const Selection& selection = m_parent.get_selection(); + return !selection.is_any_cut_volume() && !selection.is_any_connector() && !selection.is_empty(); +} + +void GLGizmoRotate3D::on_start_dragging() +{ + assert(0 <= m_hover_id && m_hover_id < 3); + m_gizmos[m_hover_id].start_dragging(); +} + +void GLGizmoRotate3D::on_stop_dragging() +{ + assert(0 <= m_hover_id && m_hover_id < 3); + m_parent.do_rotate(L("Gizmo-Rotate")); + m_gizmos[m_hover_id].stop_dragging(); +} + +void GLGizmoRotate3D::on_dragging(const UpdateData &data) +{ + assert(0 <= m_hover_id && m_hover_id < 3); + m_gizmos[m_hover_id].dragging(data); +} + +void GLGizmoRotate3D::on_render() +{ + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); + + if (m_hover_id == -1 || m_hover_id == 0) + m_gizmos[X].render(); + + if (m_hover_id == -1 || m_hover_id == 1) + m_gizmos[Y].render(); + + if (m_hover_id == -1 || m_hover_id == 2) + m_gizmos[Z].render(); +} + +#if ENABLE_RAYCAST_PICKING +void GLGizmoRotate3D::on_register_raycasters_for_picking() +{ + // the gizmo grabbers are rendered on top of the scene, so the raytraced picker should take it into account + m_parent.set_raycaster_gizmos_on_top(true); + for (GLGizmoRotate& g : m_gizmos) { + g.register_raycasters_for_picking(); + } +} + +void GLGizmoRotate3D::on_unregister_raycasters_for_picking() +{ + for (GLGizmoRotate& g : m_gizmos) { + g.unregister_raycasters_for_picking(); + } + m_parent.set_raycaster_gizmos_on_top(false); +} +#endif // ENABLE_RAYCAST_PICKING + +GLGizmoRotate3D::RotoptimzeWindow::RotoptimzeWindow(ImGuiWrapper * imgui, + State & state, + const Alignment &alignment) + : m_imgui{imgui} +{ + imgui->begin(_L("Optimize orientation"), ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoCollapse); + + // adjust window position to avoid overlap the view toolbar + float win_h = ImGui::GetWindowHeight(); + float x = alignment.x, y = alignment.y; + y = std::min(y, alignment.bottom_limit - win_h); + ImGui::SetWindowPos(ImVec2(x, y), ImGuiCond_Always); + + float max_text_w = 0.; + auto padding = ImGui::GetStyle().FramePadding; + padding.x *= 2.f; + padding.y *= 2.f; + + for (size_t i = 0; i < RotoptimizeJob::get_methods_count(); ++i) { + float w = + ImGui::CalcTextSize(RotoptimizeJob::get_method_name(i).c_str()).x + + padding.x + ImGui::GetFrameHeight(); + max_text_w = std::max(w, max_text_w); + } + + ImGui::PushItemWidth(max_text_w); + + if (ImGui::BeginCombo("", RotoptimizeJob::get_method_name(state.method_id).c_str())) { + for (size_t i = 0; i < RotoptimizeJob::get_methods_count(); ++i) { + if (ImGui::Selectable(RotoptimizeJob::get_method_name(i).c_str())) { + state.method_id = i; + wxGetApp().app_config->set("sla_auto_rotate", + "method_id", + std::to_string(state.method_id)); + } + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", RotoptimizeJob::get_method_description(i).c_str()); + } + + ImGui::EndCombo(); + } + + ImVec2 sz = ImGui::GetItemRectSize(); + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", RotoptimizeJob::get_method_description(state.method_id).c_str()); + + ImGui::Separator(); + + auto btn_txt = _L("Apply"); + auto btn_txt_sz = ImGui::CalcTextSize(btn_txt.c_str()); + ImVec2 button_sz = {btn_txt_sz.x + padding.x, btn_txt_sz.y + padding.y}; + ImGui::SetCursorPosX(padding.x + sz.x - button_sz.x); + + if (!wxGetApp().plater()->get_ui_job_worker().is_idle()) + imgui->disabled_begin(true); + + if ( imgui->button(btn_txt) ) { + replace_job(wxGetApp().plater()->get_ui_job_worker(), + std::make_unique()); + } + + imgui->disabled_end(); +} + +GLGizmoRotate3D::RotoptimzeWindow::~RotoptimzeWindow() +{ + m_imgui->end(); +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp index 68a764359..54a40dd59 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp @@ -1,233 +1,233 @@ -#ifndef slic3r_GLGizmoRotate_hpp_ -#define slic3r_GLGizmoRotate_hpp_ - -#include "GLGizmoBase.hpp" - -namespace Slic3r { -namespace GUI { -class Selection; - -class GLGizmoRotate : public GLGizmoBase -{ - static const float Offset; - static const unsigned int AngleResolution; - static const unsigned int ScaleStepsCount; - static const float ScaleStepRad; - static const unsigned int ScaleLongEvery; - static const float ScaleLongTooth; - static const unsigned int SnapRegionsCount; - static const float GrabberOffset; - -public: - enum Axis : unsigned char - { - X=0, - Y=1, - Z=2 - }; - -private: - Axis m_axis; - double m_angle{ 0.0 }; - Vec3d m_center{ Vec3d::Zero() }; - float m_radius{ 0.0f }; - float m_snap_coarse_in_radius{ 0.0f }; - float m_snap_coarse_out_radius{ 0.0f }; - float m_snap_fine_in_radius{ 0.0f }; - float m_snap_fine_out_radius{ 0.0f }; -#if ENABLE_WORLD_COORDINATE - BoundingBoxf3 m_bounding_box; - Transform3d m_orient_matrix{ Transform3d::Identity() }; -#endif // ENABLE_WORLD_COORDINATE - -#if ENABLE_LEGACY_OPENGL_REMOVAL - GLModel m_circle; - GLModel m_scale; - GLModel m_snap_radii; - GLModel m_reference_radius; - GLModel m_angle_arc; - struct GrabberConnection - { - GLModel model; - Vec3d old_center{ Vec3d::Zero() }; - }; - GrabberConnection m_grabber_connection; - float m_old_radius{ 0.0f }; - float m_old_hover_radius{ 0.0f }; - float m_old_angle{ 0.0f }; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - ColorRGBA m_drag_color; - ColorRGBA m_highlight_color; - -public: - GLGizmoRotate(GLCanvas3D& parent, Axis axis); - virtual ~GLGizmoRotate() = default; - - double get_angle() const { return m_angle; } - void set_angle(double angle); - - std::string get_tooltip() const override; - - void start_dragging(); - void stop_dragging(); - - void enable_grabber(); - void disable_grabber(); - - void set_highlight_color(const ColorRGBA &color); - - /// - /// Postpone to Grabber for move - /// Detect move of object by dragging - /// - /// Keep information about mouse click - /// Return True when use the information otherwise False. - bool on_mouse(const wxMouseEvent &mouse_event) override; - void dragging(const UpdateData &data); - -protected: - bool on_init() override; - std::string on_get_name() const override { return ""; } - void on_start_dragging() override; - void on_dragging(const UpdateData &data) override; - void on_render() override; -#if !ENABLE_RAYCAST_PICKING - void on_render_for_picking() override; -#endif // !ENABLE_RAYCAST_PICKING - -private: -#if ENABLE_LEGACY_OPENGL_REMOVAL - void render_circle(const ColorRGBA& color, bool radius_changed); - void render_scale(const ColorRGBA& color, bool radius_changed); - void render_snap_radii(const ColorRGBA& color, bool radius_changed); - void render_reference_radius(const ColorRGBA& color, bool radius_changed); - void render_angle_arc(const ColorRGBA& color, bool radius_changed); - void render_grabber_connection(const ColorRGBA& color, bool radius_changed); -#else - void render_circle() const; - void render_scale() const; - void render_snap_radii() const; - void render_reference_radius() const; - void render_angle() const; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - void render_grabber(const BoundingBoxf3& box); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - Transform3d local_transform(const Selection& selection) const; -#else - void transform_to_local(const Selection& selection) const; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - // returns the intersection of the mouse ray with the plane perpendicular to the gizmo axis, in local coordinate -#if ENABLE_WORLD_COORDINATE - Vec3d mouse_position_in_local_plane(const Linef3& mouse_ray) const; -#else - Vec3d mouse_position_in_local_plane(const Linef3& mouse_ray, const Selection& selection) const; -#endif // ENABLE_WORLD_COORDINATE - -#if ENABLE_WORLD_COORDINATE - void init_data_from_selection(const Selection& selection); -#endif // ENABLE_WORLD_COORDINATE -}; - -class GLGizmoRotate3D : public GLGizmoBase -{ - std::array m_gizmos; - -public: - GLGizmoRotate3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); - - Vec3d get_rotation() const { return Vec3d(m_gizmos[X].get_angle(), m_gizmos[Y].get_angle(), m_gizmos[Z].get_angle()); } - void set_rotation(const Vec3d& rotation) { m_gizmos[X].set_angle(rotation.x()); m_gizmos[Y].set_angle(rotation.y()); m_gizmos[Z].set_angle(rotation.z()); } - - std::string get_tooltip() const override { - std::string tooltip = m_gizmos[X].get_tooltip(); - if (tooltip.empty()) - tooltip = m_gizmos[Y].get_tooltip(); - if (tooltip.empty()) - tooltip = m_gizmos[Z].get_tooltip(); - return tooltip; - } - - /// - /// Postpone to Rotation - /// - /// 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; -protected: - bool on_init() override; - std::string on_get_name() const override; - void on_set_state() override { - for (GLGizmoRotate& g : m_gizmos) - g.set_state(m_state); - } - void on_set_hover_id() override { - for (int i = 0; i < 3; ++i) - m_gizmos[i].set_hover_id((m_hover_id == i) ? 0 : -1); - } - void on_enable_grabber(unsigned int id) override { - if (id < 3) - m_gizmos[id].enable_grabber(); - } - void on_disable_grabber(unsigned int id) override { - if (id < 3) - m_gizmos[id].disable_grabber(); - } - bool on_is_activable() const override; - void on_start_dragging() override; - void on_stop_dragging() override; - void on_dragging(const UpdateData &data) override; - - void on_render() override; -#if ENABLE_RAYCAST_PICKING - virtual void on_register_raycasters_for_picking() override; - virtual void on_unregister_raycasters_for_picking() override; -#else - void on_render_for_picking() override { - for (GLGizmoRotate& g : m_gizmos) { - g.render_for_picking(); - } - } -#endif // ENABLE_RAYCAST_PICKING - - void on_render_input_window(float x, float y, float bottom_limit) override; - -private: - - class RotoptimzeWindow - { - ImGuiWrapper *m_imgui = nullptr; - - public: - struct State { - float accuracy = 1.f; - int method_id = 0; - }; - - struct Alignment { float x, y, bottom_limit; }; - - RotoptimzeWindow(ImGuiWrapper * imgui, - State & state, - const Alignment &bottom_limit); - - ~RotoptimzeWindow(); - - RotoptimzeWindow(const RotoptimzeWindow&) = delete; - RotoptimzeWindow(RotoptimzeWindow &&) = delete; - RotoptimzeWindow& operator=(const RotoptimzeWindow &) = delete; - RotoptimzeWindow& operator=(RotoptimzeWindow &&) = delete; - }; - - RotoptimzeWindow::State m_rotoptimizewin_state = {}; - - void load_rotoptimize_state(); -}; - -} // namespace GUI -} // namespace Slic3r - -#endif // slic3r_GLGizmoRotate_hpp_ +#ifndef slic3r_GLGizmoRotate_hpp_ +#define slic3r_GLGizmoRotate_hpp_ + +#include "GLGizmoBase.hpp" + +namespace Slic3r { +namespace GUI { +class Selection; + +class GLGizmoRotate : public GLGizmoBase +{ + static const float Offset; + static const unsigned int AngleResolution; + static const unsigned int ScaleStepsCount; + static const float ScaleStepRad; + static const unsigned int ScaleLongEvery; + static const float ScaleLongTooth; + static const unsigned int SnapRegionsCount; + static const float GrabberOffset; + +public: + enum Axis : unsigned char + { + X=0, + Y=1, + Z=2 + }; + +private: + Axis m_axis; + double m_angle{ 0.0 }; + Vec3d m_center{ Vec3d::Zero() }; + float m_radius{ 0.0f }; + float m_snap_coarse_in_radius{ 0.0f }; + float m_snap_coarse_out_radius{ 0.0f }; + float m_snap_fine_in_radius{ 0.0f }; + float m_snap_fine_out_radius{ 0.0f }; +#if ENABLE_WORLD_COORDINATE + BoundingBoxf3 m_bounding_box; + Transform3d m_orient_matrix{ Transform3d::Identity() }; +#endif // ENABLE_WORLD_COORDINATE + +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLModel m_circle; + GLModel m_scale; + GLModel m_snap_radii; + GLModel m_reference_radius; + GLModel m_angle_arc; + struct GrabberConnection + { + GLModel model; + Vec3d old_center{ Vec3d::Zero() }; + }; + GrabberConnection m_grabber_connection; + float m_old_radius{ 0.0f }; + float m_old_hover_radius{ 0.0f }; + float m_old_angle{ 0.0f }; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + ColorRGBA m_drag_color; + ColorRGBA m_highlight_color; + +public: + GLGizmoRotate(GLCanvas3D& parent, Axis axis); + virtual ~GLGizmoRotate() = default; + + double get_angle() const { return m_angle; } + void set_angle(double angle); + + std::string get_tooltip() const override; + + void start_dragging(); + void stop_dragging(); + + void enable_grabber(); + void disable_grabber(); + + void set_highlight_color(const ColorRGBA &color); + + /// + /// Postpone to Grabber for move + /// Detect move of object by dragging + /// + /// Keep information about mouse click + /// Return True when use the information otherwise False. + bool on_mouse(const wxMouseEvent &mouse_event) override; + void dragging(const UpdateData &data); + +protected: + bool on_init() override; + std::string on_get_name() const override { return ""; } + void on_start_dragging() override; + void on_dragging(const UpdateData &data) override; + void on_render() override; +#if !ENABLE_RAYCAST_PICKING + void on_render_for_picking() override; +#endif // !ENABLE_RAYCAST_PICKING + +private: +#if ENABLE_LEGACY_OPENGL_REMOVAL + void render_circle(const ColorRGBA& color, bool radius_changed); + void render_scale(const ColorRGBA& color, bool radius_changed); + void render_snap_radii(const ColorRGBA& color, bool radius_changed); + void render_reference_radius(const ColorRGBA& color, bool radius_changed); + void render_angle_arc(const ColorRGBA& color, bool radius_changed); + void render_grabber_connection(const ColorRGBA& color, bool radius_changed); +#else + void render_circle() const; + void render_scale() const; + void render_snap_radii() const; + void render_reference_radius() const; + void render_angle() const; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + void render_grabber(const BoundingBoxf3& box); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + Transform3d local_transform(const Selection& selection) const; +#else + void transform_to_local(const Selection& selection) const; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + // returns the intersection of the mouse ray with the plane perpendicular to the gizmo axis, in local coordinate +#if ENABLE_WORLD_COORDINATE + Vec3d mouse_position_in_local_plane(const Linef3& mouse_ray) const; +#else + Vec3d mouse_position_in_local_plane(const Linef3& mouse_ray, const Selection& selection) const; +#endif // ENABLE_WORLD_COORDINATE + +#if ENABLE_WORLD_COORDINATE + void init_data_from_selection(const Selection& selection); +#endif // ENABLE_WORLD_COORDINATE +}; + +class GLGizmoRotate3D : public GLGizmoBase +{ + std::array m_gizmos; + +public: + GLGizmoRotate3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); + + Vec3d get_rotation() const { return Vec3d(m_gizmos[X].get_angle(), m_gizmos[Y].get_angle(), m_gizmos[Z].get_angle()); } + void set_rotation(const Vec3d& rotation) { m_gizmos[X].set_angle(rotation.x()); m_gizmos[Y].set_angle(rotation.y()); m_gizmos[Z].set_angle(rotation.z()); } + + std::string get_tooltip() const override { + std::string tooltip = m_gizmos[X].get_tooltip(); + if (tooltip.empty()) + tooltip = m_gizmos[Y].get_tooltip(); + if (tooltip.empty()) + tooltip = m_gizmos[Z].get_tooltip(); + return tooltip; + } + + /// + /// Postpone to Rotation + /// + /// 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; +protected: + bool on_init() override; + std::string on_get_name() const override; + void on_set_state() override { + for (GLGizmoRotate& g : m_gizmos) + g.set_state(m_state); + } + void on_set_hover_id() override { + for (int i = 0; i < 3; ++i) + m_gizmos[i].set_hover_id((m_hover_id == i) ? 0 : -1); + } + void on_enable_grabber(unsigned int id) override { + if (id < 3) + m_gizmos[id].enable_grabber(); + } + void on_disable_grabber(unsigned int id) override { + if (id < 3) + m_gizmos[id].disable_grabber(); + } + bool on_is_activable() const override; + void on_start_dragging() override; + void on_stop_dragging() override; + void on_dragging(const UpdateData &data) override; + + void on_render() override; +#if ENABLE_RAYCAST_PICKING + virtual void on_register_raycasters_for_picking() override; + virtual void on_unregister_raycasters_for_picking() override; +#else + void on_render_for_picking() override { + for (GLGizmoRotate& g : m_gizmos) { + g.render_for_picking(); + } + } +#endif // ENABLE_RAYCAST_PICKING + + void on_render_input_window(float x, float y, float bottom_limit) override; + +private: + + class RotoptimzeWindow + { + ImGuiWrapper *m_imgui = nullptr; + + public: + struct State { + float accuracy = 1.f; + int method_id = 0; + }; + + struct Alignment { float x, y, bottom_limit; }; + + RotoptimzeWindow(ImGuiWrapper * imgui, + State & state, + const Alignment &bottom_limit); + + ~RotoptimzeWindow(); + + RotoptimzeWindow(const RotoptimzeWindow&) = delete; + RotoptimzeWindow(RotoptimzeWindow &&) = delete; + RotoptimzeWindow& operator=(const RotoptimzeWindow &) = delete; + RotoptimzeWindow& operator=(RotoptimzeWindow &&) = delete; + }; + + RotoptimzeWindow::State m_rotoptimizewin_state = {}; + + void load_rotoptimize_state(); +}; + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_GLGizmoRotate_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp b/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp index 572e011e5..9b49cf0b4 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp @@ -8,6 +8,7 @@ #if ENABLE_LEGACY_OPENGL_REMOVAL #include "slic3r/GUI/Plater.hpp" #endif // ENABLE_LEGACY_OPENGL_REMOVAL +#include "libslic3r/Model.hpp" #include @@ -103,6 +104,12 @@ bool GLGizmoScale3D::on_mouse(const wxMouseEvent &mouse_event) return use_grabbers(mouse_event); } +void GLGizmoScale3D::enable_ununiversal_scale(bool enable) +{ + for (unsigned int i = 0; i < 6; ++i) + m_grabbers[i].enabled = enable; +} + void GLGizmoScale3D::data_changed() { #if ENABLE_WORLD_COORDINATE @@ -161,7 +168,7 @@ std::string GLGizmoScale3D::on_get_name() const bool GLGizmoScale3D::on_is_activable() const { const Selection& selection = m_parent.get_selection(); - return !selection.is_empty() && !selection.is_wipe_tower(); + return !selection.is_any_cut_volume() && !selection.is_any_connector() && !selection.is_empty() && !selection.is_wipe_tower(); } void GLGizmoScale3D::on_start_dragging() @@ -448,7 +455,7 @@ void GLGizmoScale3D::on_render() // draw grabbers render_grabbers(grabber_mean_size); } - else if (m_hover_id == 0 || m_hover_id == 1) { + else if ((m_hover_id == 0 || m_hover_id == 1) && m_grabbers[0].enabled && m_grabbers[1].enabled) { #if ENABLE_LEGACY_OPENGL_REMOVAL // draw connections #if ENABLE_GL_CORE_PROFILE @@ -493,7 +500,7 @@ void GLGizmoScale3D::on_render() shader->stop_using(); } } - else if (m_hover_id == 2 || m_hover_id == 3) { + else if ((m_hover_id == 2 || m_hover_id == 3) && m_grabbers[2].enabled && m_grabbers[3].enabled) { #if ENABLE_LEGACY_OPENGL_REMOVAL // draw connections #if ENABLE_GL_CORE_PROFILE @@ -538,7 +545,7 @@ void GLGizmoScale3D::on_render() shader->stop_using(); } } - else if (m_hover_id == 4 || m_hover_id == 5) { + else if ((m_hover_id == 4 || m_hover_id == 5) && m_grabbers[4].enabled && m_grabbers[5].enabled) { #if ENABLE_LEGACY_OPENGL_REMOVAL // draw connections #if ENABLE_GL_CORE_PROFILE diff --git a/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp b/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp index b66da89bc..b92c54f18 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp @@ -85,6 +85,7 @@ public: bool on_mouse(const wxMouseEvent &mouse_event) override; void data_changed() override; + void enable_ununiversal_scale(bool enable); protected: virtual bool on_init() override; virtual std::string on_get_name() const override; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp index 418f180de..a49356579 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp @@ -156,7 +156,7 @@ void GLGizmoSeam::on_render_input_window(float x, float y, float bottom_limit) else { if (m_imgui->button(m_desc.at("reset_direction"))) { wxGetApp().CallAfter([this](){ - m_c->object_clipper()->set_position(-1., false); + m_c->object_clipper()->set_position_by_ratio(-1., false); }); } } @@ -165,7 +165,7 @@ void GLGizmoSeam::on_render_input_window(float x, float y, float bottom_limit) ImGui::SameLine(sliders_left_width); ImGui::PushItemWidth(window_width - sliders_left_width - slider_icon_width); if (m_imgui->slider_float("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true, _L("Ctrl + Mouse wheel"))) - m_c->object_clipper()->set_position(clp_dist, true); + m_c->object_clipper()->set_position_by_ratio(clp_dist, true); ImGui::Separator(); if (m_imgui->button(m_desc.at("remove_all"))) { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp index b562eb49a..caceefff0 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(); } @@ -673,19 +673,19 @@ bool GLGizmoSlaSupports::gizmo_event(SLAGizmoEventType action, const Vec2d& mous if (action == SLAGizmoEventType::MouseWheelUp && control_down) { double pos = m_c->object_clipper()->get_position(); pos = std::min(1., pos + 0.01); - m_c->object_clipper()->set_position(pos, true); + m_c->object_clipper()->set_position_by_ratio(pos, true); return true; } if (action == SLAGizmoEventType::MouseWheelDown && control_down) { double pos = m_c->object_clipper()->get_position(); pos = std::max(0., pos - 0.01); - m_c->object_clipper()->set_position(pos, true); + m_c->object_clipper()->set_position_by_ratio(pos, true); return true; } if (action == SLAGizmoEventType::ResetClippingPlane) { - m_c->object_clipper()->set_position(-1., false); + m_c->object_clipper()->set_position_by_ratio(-1., false); return true; } @@ -972,7 +972,7 @@ RENDER_AGAIN: else { if (m_imgui->button(m_desc.at("reset_direction"))) { wxGetApp().CallAfter([this](){ - m_c->object_clipper()->set_position(-1., false); + m_c->object_clipper()->set_position_by_ratio(-1., false); }); } } @@ -981,7 +981,7 @@ RENDER_AGAIN: ImGui::PushItemWidth(window_width - clipping_slider_left); float clp_dist = m_c->object_clipper()->get_position(); if (m_imgui->slider_float("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f")) - m_c->object_clipper()->set_position(clp_dist, true); + m_c->object_clipper()->set_position_by_ratio(clp_dist, true); if (m_imgui->button("?")) { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp index ed48b6e5e..136276803 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp @@ -62,8 +62,8 @@ public: void delete_selected_points(bool force = false); //ClippingPlane get_sla_clipping_plane() const; - bool is_in_editing_mode() const { return m_editing_mode; } - bool is_selection_rectangle_dragging() const { return m_selection_rectangle.is_dragging(); } + bool is_in_editing_mode() const override { return m_editing_mode; } + bool is_selection_rectangle_dragging() const override { return m_selection_rectangle.is_dragging(); } bool has_backend_supports() const; void reslice_SLA_supports(bool postpone_error_messages = false) const; diff --git a/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp b/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp index ce3ba1d4a..394e879b7 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()); } } @@ -425,8 +435,11 @@ void ObjectClipper::render_cut() const if (m_clp_ratio == 0.) return; const SelectionInfo* sel_info = get_pool()->selection_info(); + int sel_instance_idx = sel_info->get_active_instance(); + if (sel_instance_idx < 0) + return; const ModelObject* mo = sel_info->model_object(); - const Geometry::Transformation inst_trafo = mo->instances[sel_info->get_active_instance()]->get_transformation(); + const Geometry::Transformation inst_trafo = mo->instances[sel_instance_idx]->get_transformation(); size_t clipper_id = 0; for (const ModelVolume* mv : mo->volumes) { @@ -440,19 +453,30 @@ void ObjectClipper::render_cut() const clipper->set_limiting_plane(ClippingPlane(Vec3d::UnitZ(), -SINKING_Z_THRESHOLD)); #if ENABLE_LEGACY_OPENGL_REMOVAL clipper->render_cut({ 1.0f, 0.37f, 0.0f, 1.0f }); + clipper->render_contour({ 1.f, 1.f, 1.f, 1.f}); #else glsafe(::glPushMatrix()); glsafe(::glColor3f(1.0f, 0.37f, 0.0f)); clipper->render_cut(); - glsafe(::glPopMatrix()); + glsafe(::glColor3f(1.f, 1.f, 1.f)); + clipper->render_contour(); #endif // ENABLE_LEGACY_OPENGL_REMOVAL ++clipper_id; } } +bool ObjectClipper::is_projection_inside_cut(const Vec3d& point) const +{ + return m_clp_ratio != 0. && std::any_of(m_clippers.begin(), m_clippers.end(), [point](const std::unique_ptr& cl) { return cl->is_projection_inside_cut(point); }); +} -void ObjectClipper::set_position(double pos, bool keep_normal) +bool ObjectClipper::has_valid_contour() const +{ + return m_clp_ratio != 0. && std::any_of(m_clippers.begin(), m_clippers.end(), [](const std::unique_ptr& cl) { return cl->has_valid_contour(); }); +} + +void ObjectClipper::set_position_by_ratio(double pos, bool keep_normal) { const ModelObject* mo = get_pool()->selection_info()->model_object(); int active_inst = get_pool()->selection_info()->get_active_instance(); @@ -470,7 +494,36 @@ void ObjectClipper::set_position(double pos, bool keep_normal) get_pool()->get_canvas()->set_as_dirty(); } +void ObjectClipper::set_range_and_pos(const Vec3d& cpl_normal, double cpl_offset, double pos) +{ + m_clp.reset(new ClippingPlane(cpl_normal, cpl_offset)); + m_clp_ratio = pos; + get_pool()->get_canvas()->set_as_dirty(); +} +const ClippingPlane* ObjectClipper::get_clipping_plane(bool ignore_hide_clipped) const +{ + static const ClippingPlane no_clip = ClippingPlane::ClipsNothing(); + return (ignore_hide_clipped || m_hide_clipped) ? m_clp.get() : &no_clip; +} + +void ObjectClipper::set_behavior(bool hide_clipped, bool fill_cut, double contour_width) +{ + m_hide_clipped = hide_clipped; + for (auto& clipper : m_clippers) + clipper->set_behaviour(fill_cut, contour_width); +} + +void ObjectClipper::pass_mouse_click(const Vec3d& pt) +{ + for (auto& clipper : m_clippers) + clipper->pass_mouse_click(pt); +} + +std::vector ObjectClipper::get_disabled_contours() const +{ + return std::vector(); +} void SupportsClipper::on_update() { @@ -557,11 +610,13 @@ void SupportsClipper::render_cut() const #if ENABLE_LEGACY_OPENGL_REMOVAL m_clipper->render_cut({ 1.0f, 0.f, 0.37f, 1.0f }); + m_clipper->render_contour({ 1.f, 1.f, 1.f, 1.f }); #else glsafe(::glPushMatrix()); glsafe(::glColor3f(1.0f, 0.f, 0.37f)); m_clipper->render_cut(); - glsafe(::glPopMatrix()); + glsafe(::glColor3f(1.0f, 1.f, 1.f)); + m_clipper->render_contour(); #endif // ENABLE_LEGACY_OPENGL_REMOVAL } diff --git a/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp b/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp index 7dd2c110e..757940226 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; }; @@ -255,10 +261,19 @@ public: CommonGizmosDataID get_dependencies() const override { return CommonGizmosDataID::SelectionInfo; } #endif // NDEBUG - void set_position(double pos, bool keep_normal); + void set_normal(const Vec3d& dir); double get_position() const { return m_clp_ratio; } - ClippingPlane* get_clipping_plane() const { return m_clp.get(); } + const ClippingPlane* get_clipping_plane(bool ignore_hide_clipped = false) const; void render_cut() const; + void set_position_by_ratio(double pos, bool keep_normal); + void set_range_and_pos(const Vec3d& cpl_normal, double cpl_offset, double pos); + void set_behavior(bool hide_clipped, bool fill_cut, double contour_width); + + void pass_mouse_click(const Vec3d& pt); + std::vector get_disabled_contours() const; + + bool is_projection_inside_cut(const Vec3d& point_in) const; + bool has_valid_contour() const; protected: @@ -271,6 +286,7 @@ private: std::unique_ptr m_clp; double m_clp_ratio = 0.; double m_active_inst_bb_radius = 0.; + bool m_hide_clipped = true; }; diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 8759e880d..51a0386ed 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -21,6 +21,7 @@ #include "slic3r/GUI/Gizmos/GLGizmoSeam.hpp" #include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp" #include "slic3r/GUI/Gizmos/GLGizmoSimplify.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoMeasure.hpp" #include "libslic3r/format.hpp" #include "libslic3r/Model.hpp" @@ -99,13 +100,14 @@ bool GLGizmosManager::init() m_gizmos.emplace_back(new GLGizmoScale3D(m_parent, "scale.svg", 1)); m_gizmos.emplace_back(new GLGizmoRotate3D(m_parent, "rotate.svg", 2)); m_gizmos.emplace_back(new GLGizmoFlatten(m_parent, "place.svg", 3)); - m_gizmos.emplace_back(new GLGizmoCut(m_parent, "cut.svg", 4)); + m_gizmos.emplace_back(new GLGizmoCut3D(m_parent, "cut.svg", 4)); m_gizmos.emplace_back(new GLGizmoHollow(m_parent, "hollow.svg", 5)); m_gizmos.emplace_back(new GLGizmoSlaSupports(m_parent, "sla_supports.svg", 6)); 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 GLGizmoSimplify(m_parent, "cut.svg", 10)); + m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, "measure.svg", 11)); m_common_gizmos_data.reset(new CommonGizmosDataPool(&m_parent)); @@ -288,6 +290,10 @@ 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 return false; } @@ -493,8 +499,7 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt) bool processed = false; - if ((evt.GetModifiers() & ctrlMask) != 0) - { + if ((evt.GetModifiers() & ctrlMask) != 0) { switch (keyCode) { #ifdef __APPLE__ @@ -505,22 +510,20 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt) #endif /* __APPLE__ */ { // Sla gizmo selects all support points - if ((m_current == SlaSupports || m_current == Hollow) && gizmo_event(SLAGizmoEventType::SelectAll)) + if ((m_current == SlaSupports || m_current == Hollow || m_current == Cut) && gizmo_event(SLAGizmoEventType::SelectAll)) processed = true; break; } } } - 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(); @@ -549,7 +552,7 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt) case WXK_BACK: case WXK_DELETE: { - if ((m_current == SlaSupports || m_current == Hollow) && gizmo_event(SLAGizmoEventType::Delete)) + if ((m_current == SlaSupports || m_current == Hollow || m_current == Cut) && gizmo_event(SLAGizmoEventType::Delete)) processed = true; break; @@ -557,8 +560,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; @@ -576,8 +578,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(); @@ -589,8 +590,7 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt) } } - if (!processed && !evt.HasModifiers()) - { + if (!processed && !evt.HasModifiers()) { if (handle_shortcut(keyCode)) processed = true; } @@ -608,34 +608,26 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt) if (evt.GetEventType() == wxEVT_KEY_UP) { - if (m_current == SlaSupports || m_current == Hollow) + if (m_current == SlaSupports || m_current == Hollow || m_current == Cut) { - bool is_editing = true; - bool is_rectangle_dragging = false; + GLGizmoBase* gizmo = get_current(); + const bool is_editing = m_current == Hollow ? true : gizmo->is_in_editing_mode(); + const bool is_rectangle_dragging = gizmo->is_selection_rectangle_dragging(); - if (m_current == SlaSupports) { - GLGizmoSlaSupports* gizmo = dynamic_cast(get_current()); - is_editing = gizmo->is_in_editing_mode(); - is_rectangle_dragging = gizmo->is_selection_rectangle_dragging(); - } - else { - GLGizmoHollow* gizmo = dynamic_cast(get_current()); - 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,16 +635,15 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt) else if (evt.GetEventType() == wxEVT_KEY_DOWN) { if ((m_current == SlaSupports) && ((keyCode == WXK_SHIFT) || (keyCode == WXK_ALT)) - && dynamic_cast(get_current())->is_in_editing_mode()) + && get_current()->is_in_editing_mode()) { // 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) { - GLGizmoCut* cut = dynamic_cast(get_current()); - cut->set_cut_z(delta_z + cut->get_cut_z()); + GLGizmoCut3D* cut = dynamic_cast(get_current()); + cut->shift_cut_z(delta_z); processed = true; }; @@ -660,13 +651,20 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt) { case WXK_NUMPAD_UP: case WXK_UP: { do_move(1.0); break; } case WXK_NUMPAD_DOWN: case WXK_DOWN: { do_move(-1.0); break; } + case WXK_SHIFT : case WXK_ALT: { + processed = get_current()->is_in_editing_mode(); + } 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) @@ -1042,6 +1040,11 @@ GLGizmoBase* GLGizmosManager::get_current() const return ((m_current == Undefined) || m_gizmos.empty()) ? nullptr : m_gizmos[m_current].get(); } +GLGizmoBase* GLGizmosManager::get_gizmo(GLGizmosManager::EType type) const +{ + return ((type == Undefined) || m_gizmos.empty()) ? nullptr : m_gizmos[type].get(); +} + GLGizmosManager::EType GLGizmosManager::get_gizmo_from_name(const std::string& gizmo_name) const { std::vector selectable_idxs = get_selectable_idxs(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 8a708f62a..1689cf461 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -80,6 +80,7 @@ public: Seam, MmuSegmentation, Simplify, + Measure, Undefined }; @@ -199,6 +200,7 @@ public: EType get_current_type() const { return m_current; } GLGizmoBase* get_current() const; + GLGizmoBase* get_gizmo(GLGizmosManager::EType type) const; EType get_gizmo_from_name(const std::string& gizmo_name) const; bool is_running() const; diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index fe3f69a58..01cd8ec30 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -56,6 +56,12 @@ 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" }, + {ImGui::WarningMarkerSmall , "notification_warning" }, + {ImGui::InfoMarkerSmall , "notification_info" }, }; static const std::map font_icons_large = { @@ -423,36 +429,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); @@ -511,20 +487,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 @@ -666,6 +642,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: @@ -1128,6 +1127,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 769deccb8..040e5e491 100644 --- a/src/slic3r/GUI/ImGuiWrapper.hpp +++ b/src/slic3r/GUI/ImGuiWrapper.hpp @@ -89,9 +89,6 @@ public: bool button(const wxString& label, float width, float height); 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); void text(const char *label); void text(const std::string &label); @@ -112,6 +109,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); @@ -132,6 +130,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/MeshUtils.cpp b/src/slic3r/GUI/MeshUtils.cpp index 960948b24..e33d29ba1 100644 --- a/src/slic3r/GUI/MeshUtils.cpp +++ b/src/slic3r/GUI/MeshUtils.cpp @@ -16,15 +16,27 @@ #include +#include + namespace Slic3r { namespace GUI { +void MeshClipper::set_behaviour(bool fill_cut, double contour_width) +{ + if (fill_cut != m_fill_cut || contour_width != m_contour_width) + m_result.reset(); + m_fill_cut = fill_cut; + m_contour_width = contour_width; +} + + + void MeshClipper::set_plane(const ClippingPlane& plane) { if (m_plane != plane) { m_plane = plane; - m_triangles_valid = false; + m_result.reset(); } } @@ -33,7 +45,7 @@ void MeshClipper::set_limiting_plane(const ClippingPlane& plane) { if (m_limiting_plane != plane) { m_limiting_plane = plane; - m_triangles_valid = false; + m_result.reset(); } } @@ -43,8 +55,7 @@ void MeshClipper::set_mesh(const TriangleMesh& mesh) { if (m_mesh != &mesh) { m_mesh = &mesh; - m_triangles_valid = false; - m_triangles2d.resize(0); + m_result.reset(); } } @@ -52,8 +63,7 @@ void MeshClipper::set_negative_mesh(const TriangleMesh& mesh) { if (m_negative_mesh != &mesh) { m_negative_mesh = &mesh; - m_triangles_valid = false; - m_triangles2d.resize(0); + m_result.reset(); } } @@ -63,8 +73,7 @@ void MeshClipper::set_transformation(const Geometry::Transformation& trafo) { if (! m_trafo.get_matrix().isApprox(trafo.get_matrix())) { m_trafo = trafo; - m_triangles_valid = false; - m_triangles2d.resize(0); + m_result.reset(); } } @@ -74,13 +83,9 @@ void MeshClipper::render_cut(const ColorRGBA& color) void MeshClipper::render_cut() #endif // ENABLE_LEGACY_OPENGL_REMOVAL { - if (! m_triangles_valid) + if (! m_result) recalculate_triangles(); - #if ENABLE_LEGACY_OPENGL_REMOVAL - if (m_model.vertices_count() == 0 || m_model.indices_count() == 0) - return; - GLShaderProgram* curr_shader = wxGetApp().get_current_shader(); if (curr_shader != nullptr) curr_shader->stop_using(); @@ -91,8 +96,10 @@ void MeshClipper::render_cut() const Camera& camera = wxGetApp().plater()->get_camera(); shader->set_uniform("view_model_matrix", camera.get_view_matrix()); shader->set_uniform("projection_matrix", camera.get_projection_matrix()); - m_model.set_color(color); - m_model.render(); + for (CutIsland& isl : m_result->cut_islands) { + isl.model.set_color(isl.disabled ? ColorRGBA(1.f, 0.f, 0.f, 1.f) : color); + isl.model.render(); + } shader->stop_using(); } @@ -105,19 +112,80 @@ void MeshClipper::render_cut() } +#if ENABLE_LEGACY_OPENGL_REMOVAL +void MeshClipper::render_contour(const ColorRGBA& color) +#else +void MeshClipper::render_contour() +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +{ + if (! m_result) + recalculate_triangles(); +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLShaderProgram* curr_shader = wxGetApp().get_current_shader(); + if (curr_shader != nullptr) + curr_shader->stop_using(); + + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader != nullptr) { + shader->start_using(); + const Camera& camera = wxGetApp().plater()->get_camera(); + shader->set_uniform("view_model_matrix", camera.get_view_matrix()); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + for (CutIsland& isl : m_result->cut_islands) { + isl.model_expanded.set_color(color); + isl.model_expanded.render(); + } + shader->stop_using(); + } + + if (curr_shader != nullptr) + curr_shader->start_using(); +#else + if (m_vertex_array_expanded.has_VBOs()) + m_vertex_array_expanded.render(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +bool MeshClipper::is_projection_inside_cut(const Vec3d& point_in) const +{ + if (!m_result || m_result->cut_islands.empty()) + return false; + Vec3d point = m_result->trafo.inverse() * point_in; + Point pt_2d = Point::new_scale(Vec2d(point.x(), point.y())); + + for (const CutIsland& isl : m_result->cut_islands) { + if (isl.expoly_bb.contains(pt_2d) && isl.expoly.contains(pt_2d)) + return true; + } + return false; +} + +bool MeshClipper::has_valid_contour() const +{ + return m_result && std::any_of(m_result->cut_islands.begin(), m_result->cut_islands.end(), [](const CutIsland& isl) { return !isl.expoly.empty(); }); +} + + +void MeshClipper::pass_mouse_click(const Vec3d& point_in) +{ + if (! m_result || m_result->cut_islands.empty()) + return; + Vec3d point = m_result->trafo.inverse() * point_in; + Point pt_2d = Point::new_scale(Vec2d(point.x(), point.y())); + + for (CutIsland& isl : m_result->cut_islands) { + if (isl.expoly_bb.contains(pt_2d) && isl.expoly.contains(pt_2d)) + isl.disabled = ! isl.disabled; + } +} void MeshClipper::recalculate_triangles() { -#if ENABLE_WORLD_COORDINATE - const Transform3f instance_matrix_no_translation_no_scaling = m_trafo.get_rotation_matrix().cast(); -#else - const Transform3f& instance_matrix_no_translation_no_scaling = m_trafo.get_matrix(true,false,true).cast(); -#endif // ENABLE_WORLD_COORDINATE - // Calculate clipping plane normal in mesh coordinates. - const Vec3f up_noscale = instance_matrix_no_translation_no_scaling.inverse() * m_plane.get_normal().cast(); - const Vec3d up = up_noscale.cast().cwiseProduct(m_trafo.get_scaling_factor()); - // Calculate distance from mesh origin to the clipping plane (in mesh coordinates). - const float height_mesh = m_plane.distance(m_trafo.get_offset()) * (up_noscale.norm()/up.norm()); + m_result = ClipResult(); + + auto plane_mesh = Eigen::Hyperplane(m_plane.get_normal(), -m_plane.distance(Vec3d::Zero())).transform(m_trafo.get_matrix().inverse()); + const Vec3d up = plane_mesh.normal(); + const float height_mesh = -plane_mesh.offset(); // Now do the cutting MeshSlicingParams slicing_params; @@ -137,6 +205,8 @@ void MeshClipper::recalculate_triangles() tr.rotate(q); tr = m_trafo.get_matrix() * tr; + m_result->trafo = tr; + if (m_limiting_plane != ClippingPlane::ClipsNothing()) { // Now remove whatever ended up below the limiting plane (e.g. sinking objects). @@ -190,42 +260,108 @@ void MeshClipper::recalculate_triangles() } } - m_triangles2d = triangulate_expolygons_2f(expolys, m_trafo.get_matrix().matrix().determinant() < 0.); - tr.pretranslate(0.001 * m_plane.get_normal().normalized()); // to avoid z-fighting + Transform3d tr2 = tr; + tr2.pretranslate(0.002 * m_plane.get_normal().normalized()); + #if ENABLE_LEGACY_OPENGL_REMOVAL - m_model.reset(); + std::vector triangles2d; - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; - init_data.reserve_vertices(m_triangles2d.size()); - init_data.reserve_indices(m_triangles2d.size()); + for (const ExPolygon& exp : expolys) { + triangles2d.clear(); - // vertices + indices - for (auto it = m_triangles2d.cbegin(); it != m_triangles2d.cend(); it = it + 3) { - init_data.add_vertex((Vec3f)(tr * Vec3d((*(it + 0)).x(), (*(it + 0)).y(), height_mesh)).cast(), (Vec3f)up.cast()); - init_data.add_vertex((Vec3f)(tr * Vec3d((*(it + 1)).x(), (*(it + 1)).y(), height_mesh)).cast(), (Vec3f)up.cast()); - init_data.add_vertex((Vec3f)(tr * Vec3d((*(it + 2)).x(), (*(it + 2)).y(), height_mesh)).cast(), (Vec3f)up.cast()); - const size_t idx = it - m_triangles2d.cbegin(); - init_data.add_triangle((unsigned int)idx, (unsigned int)idx + 1, (unsigned int)idx + 2); + m_result->cut_islands.push_back(CutIsland()); + CutIsland& isl = m_result->cut_islands.back(); + + if (m_fill_cut) { + triangles2d = triangulate_expolygon_2f(exp, m_trafo.get_matrix().matrix().determinant() < 0.); + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; + init_data.reserve_vertices(triangles2d.size()); + init_data.reserve_indices(triangles2d.size()); + + // vertices + indices + for (auto it = triangles2d.cbegin(); it != triangles2d.cend(); it = it + 3) { + init_data.add_vertex((Vec3f)(tr * Vec3d((*(it + 0)).x(), (*(it + 0)).y(), height_mesh)).cast(), (Vec3f)up.cast()); + init_data.add_vertex((Vec3f)(tr * Vec3d((*(it + 1)).x(), (*(it + 1)).y(), height_mesh)).cast(), (Vec3f)up.cast()); + init_data.add_vertex((Vec3f)(tr * Vec3d((*(it + 2)).x(), (*(it + 2)).y(), height_mesh)).cast(), (Vec3f)up.cast()); + const size_t idx = it - triangles2d.cbegin(); + init_data.add_triangle((unsigned int)idx, (unsigned int)idx + 1, (unsigned int)idx + 2); + } + + if (!init_data.is_empty()) + isl.model.init_from(std::move(init_data)); + } + + if (m_contour_width != 0. && ! exp.contour.empty()) { + triangles2d.clear(); + + // The contours must not scale with the object. Check the scale factor + // in the respective directions, create a scaled copy of the ExPolygon + // offset it and then unscale the result again. + + Transform3d t = tr; + t.translation() = Vec3d::Zero(); + double scale_x = (t * Vec3d::UnitX()).norm(); + double scale_y = (t * Vec3d::UnitY()).norm(); + + // To prevent overflow after scaling, downscale the input if needed: + double extra_scale = 1.; + int32_t limit = int32_t(std::min(std::numeric_limits::max() / (2. * scale_x), std::numeric_limits::max() / (2. * scale_y))); + int32_t max_coord = 0; + for (const Point& pt : exp.contour) + max_coord = std::max(max_coord, std::max(std::abs(pt.x()), std::abs(pt.y()))); + if (max_coord + m_contour_width >= limit) + extra_scale = 0.9 * double(limit) / max_coord; + + ExPolygon exp_copy = exp; + if (extra_scale != 1.) + exp_copy.scale(extra_scale); + exp_copy.scale(scale_x, scale_y); + + ExPolygons expolys_exp = offset_ex(exp_copy, scale_(m_contour_width)); + expolys_exp = diff_ex(expolys_exp, ExPolygons({exp_copy})); + + for (ExPolygon& e : expolys_exp) { + e.scale(1./scale_x, 1./scale_y); + if (extra_scale != 1.) + e.scale(1./extra_scale); + } + + + triangles2d = triangulate_expolygons_2f(expolys_exp, m_trafo.get_matrix().matrix().determinant() < 0.); + GLModel::Geometry init_data = GLModel::Geometry(); + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; + init_data.reserve_vertices(triangles2d.size()); + init_data.reserve_indices(triangles2d.size()); + + // vertices + indices + for (auto it = triangles2d.cbegin(); it != triangles2d.cend(); it = it + 3) { + init_data.add_vertex((Vec3f)(tr2 * Vec3d((*(it + 0)).x(), (*(it + 0)).y(), height_mesh)).cast(), (Vec3f)up.cast()); + init_data.add_vertex((Vec3f)(tr2 * Vec3d((*(it + 1)).x(), (*(it + 1)).y(), height_mesh)).cast(), (Vec3f)up.cast()); + init_data.add_vertex((Vec3f)(tr2 * Vec3d((*(it + 2)).x(), (*(it + 2)).y(), height_mesh)).cast(), (Vec3f)up.cast()); + const size_t idx = it - triangles2d.cbegin(); + init_data.add_triangle((unsigned short)idx, (unsigned short)idx + 1, (unsigned short)idx + 2); + } + + if (!init_data.is_empty()) + isl.model_expanded.init_from(std::move(init_data)); + } + + isl.expoly = std::move(exp); + isl.expoly_bb = get_extents(exp); } - - if (!init_data.is_empty()) - m_model.init_from(std::move(init_data)); #else - m_vertex_array.release_geometry(); - for (auto it=m_triangles2d.cbegin(); it != m_triangles2d.cend(); it=it+3) { - m_vertex_array.push_geometry(tr * Vec3d((*(it+0))(0), (*(it+0))(1), height_mesh), up); - m_vertex_array.push_geometry(tr * Vec3d((*(it+1))(0), (*(it+1))(1), height_mesh), up); - m_vertex_array.push_geometry(tr * Vec3d((*(it+2))(0), (*(it+2))(1), height_mesh), up); - const size_t idx = it - m_triangles2d.cbegin(); - m_vertex_array.push_triangle(idx, idx+1, idx+2); - } - m_vertex_array.finalize_geometry(true); + #error NOT IMPLEMENTED #endif // ENABLE_LEGACY_OPENGL_REMOVAL - m_triangles_valid = true; + + +#if ENABLE_LEGACY_OPENGL_REMOVAL +#else + #error NOT IMPLEMENTED +#endif // ENABLE_LEGACY_OPENGL_REMOVAL } @@ -239,7 +375,7 @@ void MeshRaycaster::line_from_mouse_pos(const Vec2d& mouse_pos, const Transform3 Vec3d& point, Vec3d& direction) #else void MeshRaycaster::line_from_mouse_pos(const Vec2d& mouse_pos, const Transform3d& trafo, const Camera& camera, - Vec3d& point, Vec3d& direction) const + Vec3d& point, Vec3d& direction) #endif // ENABLE_RAYCAST_PICKING { Matrix4d modelview = camera.get_view_matrix().matrix(); @@ -264,8 +400,11 @@ void MeshRaycaster::line_from_mouse_pos(const Vec2d& mouse_pos, const Transform3 bool MeshRaycaster::unproject_on_mesh(const Vec2d& mouse_pos, const Transform3d& trafo, const Camera& camera, Vec3f& position, Vec3f& normal, const ClippingPlane* clipping_plane, - size_t* facet_idx) const + size_t* facet_idx, bool* was_clipping_plane_hit) const { + if (was_clipping_plane_hit) + *was_clipping_plane_hit = false; + Vec3d point; Vec3d direction; line_from_mouse_pos(mouse_pos, trafo, camera, point, direction); @@ -286,9 +425,26 @@ bool MeshRaycaster::unproject_on_mesh(const Vec2d& mouse_pos, const Transform3d& break; } - if (i==hits.size() || (hits.size()-i) % 2 != 0) { - // All hits are either clipped, or there is an odd number of unclipped - // hits - meaning the nearest must be from inside the mesh. + if (i==hits.size()) { + // All hits are clipped. + return false; + } + if ((hits.size()-i) % 2 != 0) { + // There is an odd number of unclipped hits - meaning the nearest must be from inside the mesh. + // In that case, calculate intersection with the clipping place. + if (clipping_plane && was_clipping_plane_hit) { + direction = direction + point; + point = trafo * point; // transform to world coords + direction = trafo * direction - point; + + Vec3d normal = -clipping_plane->get_normal().cast(); + double den = normal.dot(direction); + if (den != 0.) { + double t = (-clipping_plane->get_offset() - normal.dot(point))/den; + position = (point + t * direction).cast(); + *was_clipping_plane_hit = true; + } + } return false; } @@ -302,6 +458,35 @@ bool MeshRaycaster::unproject_on_mesh(const Vec2d& mouse_pos, const Transform3d& return true; } +bool MeshRaycaster::unproject_on_mesh(const Vec2d& mouse_pos, const Transform3d& trafo, const Camera& camera, + Vec3d& position, Vec3d& normal) const +{ + Vec3d point; + Vec3d direction; + line_from_mouse_pos(mouse_pos, trafo, camera, point, direction); + + std::vector hits = m_emesh.query_ray_hits(point, direction); + + if (hits.empty()) + return false; // no intersection found + + // Now stuff the points in the provided vector and calculate normals if asked about them: + position = hits[0].position(); + normal = hits[0].normal(); + + return true; +} + +bool MeshRaycaster::is_valid_intersection(Vec3d point, Vec3d direction, const Transform3d& trafo) const +{ + point = trafo.inverse() * point; + + std::vector hits = m_emesh.query_ray_hits(point, direction); + std::vector neg_hits = m_emesh.query_ray_hits(point, -direction); + + return !hits.empty() && !neg_hits.empty(); +} + std::vector MeshRaycaster::get_unobscured_idxs(const Geometry::Transformation& trafo, const Camera& camera, const std::vector& points, const ClippingPlane* clipping_plane) const diff --git a/src/slic3r/GUI/MeshUtils.hpp b/src/slic3r/GUI/MeshUtils.hpp index 9f7d394a9..9db2ed1b1 100644 --- a/src/slic3r/GUI/MeshUtils.hpp +++ b/src/slic3r/GUI/MeshUtils.hpp @@ -14,6 +14,7 @@ #endif // ENABLE_LEGACY_OPENGL_REMOVAL #include +#include #if ENABLE_RAYCAST_PICKING #include #endif // ENABLE_RAYCAST_PICKING @@ -80,6 +81,10 @@ public: class MeshClipper { public: + // Set whether the cut should be triangulated and whether a cut + // contour should be calculated and shown. + void set_behaviour(bool fill_cut, double contour_width); + // Inform MeshClipper about which plane we want to use to cut the mesh // This is supposed to be in world coordinates. void set_plane(const ClippingPlane& plane); @@ -103,10 +108,16 @@ public: // be set in world coords. #if ENABLE_LEGACY_OPENGL_REMOVAL void render_cut(const ColorRGBA& color); + void render_contour(const ColorRGBA& color); #else void render_cut(); #endif // ENABLE_LEGACY_OPENGL_REMOVAL + void pass_mouse_click(const Vec3d& pt); + + bool is_projection_inside_cut(const Vec3d& point) const; + bool has_valid_contour() const; + private: void recalculate_triangles(); @@ -115,13 +126,27 @@ private: const TriangleMesh* m_negative_mesh = nullptr; ClippingPlane m_plane; ClippingPlane m_limiting_plane = ClippingPlane::ClipsNothing(); - std::vector m_triangles2d; #if ENABLE_LEGACY_OPENGL_REMOVAL - GLModel m_model; + + struct CutIsland { + GLModel model; + GLModel model_expanded; + ExPolygon expoly; + BoundingBox expoly_bb; + bool disabled = false; + }; + struct ClipResult { + std::vector cut_islands; + Transform3d trafo; // this rotates the cut into world coords + }; + std::optional m_result; + #else + #error NOT IMLEMENTED GLIndexedVertexArray m_vertex_array; #endif // ENABLE_LEGACY_OPENGL_REMOVAL - bool m_triangles_valid = false; + bool m_fill_cut = true; + double m_contour_width = 0.; }; @@ -150,8 +175,10 @@ public: { } - void line_from_mouse_pos(const Vec2d& mouse_pos, const Transform3d& trafo, const Camera& camera, - Vec3d& point, Vec3d& direction) const; + static void line_from_mouse_pos(const Vec2d& mouse_pos, const Transform3d& trafo, const Camera& camera, + Vec3d& point, Vec3d& direction); +// void line_from_mouse_pos(const Vec2d& mouse_pos, const Transform3d& trafo, const Camera& camera, +// Vec3d& point, Vec3d& direction) const; #endif // ENABLE_RAYCAST_PICKING // Given a mouse position, this returns true in case it is on the mesh. @@ -159,12 +186,18 @@ public: const Vec2d& mouse_pos, const Transform3d& trafo, // how to get the mesh into world coords const Camera& camera, // current camera position - Vec3f& position, // where to save the positibon of the hit (mesh coords) + Vec3f& position, // where to save the positibon of the hit (mesh coords if mesh, world coords if clipping plane) Vec3f& normal, // normal of the triangle that was hit const ClippingPlane* clipping_plane = nullptr, // clipping plane (if active) - size_t* facet_idx = nullptr // index of the facet hit + size_t* facet_idx = nullptr, // index of the facet hit + bool* was_clipping_plane_hit = nullptr // is the hit on the clipping place cross section? ) const; + // Given a mouse position, this returns true in case it is on the mesh. + bool unproject_on_mesh(const Vec2d& mouse_pos, const Transform3d& trafo, const Camera& camera, Vec3d& position, Vec3d& normal) const; + + bool is_valid_intersection(Vec3d point, Vec3d direction, const Transform3d& trafo) const; + // Given a vector of points in woorld coordinates, this returns vector // of indices of points that are visible (i.e. not cut by clipping plane // or obscured by part of the mesh. diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 2e74270da..594e0716c 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -1228,6 +1228,7 @@ void NotificationManager::UpdatedItemsInfoNotification::add_type(InfoItemType ty case InfoItemType::MmuSegmentation: text += format(_L_PLURAL("%1$d object was loaded with multimaterial painting.", "%1$d objects were loaded with multimaterial painting.",(*it).second), (*it).second) + "\n"; break; case InfoItemType::VariableLayerHeight: text += format(_L_PLURAL("%1$d object was loaded with variable layer height.", "%1$d objects were loaded with variable layer height.", (*it).second), (*it).second) + "\n"; break; case InfoItemType::Sinking: text += format(_L_PLURAL("%1$d object was loaded with partial sinking.", "%1$d objects were loaded with partial sinking.", (*it).second), (*it).second) + "\n"; break; + case InfoItemType::CutConnectors: text += format(_L_PLURAL("%1$d object was loaded as a part of cut object.", "%1$d objects were loaded as parts of cut object", (*it).second), (*it).second) + "\n"; break; default: BOOST_LOG_TRIVIAL(error) << "Unknown InfoItemType: " << (*it).second; break; } } diff --git a/src/slic3r/GUI/ObjectDataViewModel.cpp b/src/slic3r/GUI/ObjectDataViewModel.cpp index ba70f0ebd..066fc4522 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 @@ -34,10 +42,19 @@ void ObjectDataViewModelNode::init_container() #endif //__WXGTK__ } +void ObjectDataViewModelNode::invalidate_container() +{ +#ifndef __WXGTK__ + if (this->GetChildCount() == 0) + this->m_container = false; +#endif //__WXGTK__ +} + static constexpr char LayerRootIcon[] = "edit_layers_all"; static constexpr char LayerIcon[] = "edit_layers_some"; static constexpr char WarningIcon[] = "exclamation"; static constexpr char WarningManifoldIcon[] = "exclamation_manifold"; +static constexpr char LockIcon[] = "cut_"; struct InfoItemAtributes { std::string name; @@ -48,6 +65,7 @@ const std::map INFO_ITEMS{ // info_item Type info_item Name info_item BitmapName { InfoItemType::CustomSupports, {L("Paint-on supports"), "fdm_supports_" }, }, { InfoItemType::CustomSeam, {L("Paint-on seam"), "seam_" }, }, + { InfoItemType::CutConnectors, {L("Cut connectors"), "cut_connectors" }, }, { InfoItemType::MmuSegmentation, {L("Multimaterial painting"), "mmu_segmentation_"}, }, { InfoItemType::Sinking, {L("Sinking"), "sinking"}, }, { InfoItemType::VariableLayerHeight, {L("Variable layer height"), "layers"}, }, @@ -56,19 +74,15 @@ const std::map INFO_ITEMS{ ObjectDataViewModelNode::ObjectDataViewModelNode(ObjectDataViewModelNode* parent, const wxString& sub_obj_name, Slic3r::ModelVolumeType type, - const wxBitmapBundle& bmp, const wxString& extruder, - const int idx/* = -1*/, - const std::string& warning_icon_name /*= std::string*/) : + const int idx/* = -1*/) : m_parent(parent), m_name(sub_obj_name), m_type(itVolume), m_volume_type(type), m_idx(idx), - m_extruder(type == Slic3r::ModelVolumeType::MODEL_PART || type == Slic3r::ModelVolumeType::PARAMETER_MODIFIER ? extruder : ""), - m_warning_icon_name(warning_icon_name) + m_extruder(type == Slic3r::ModelVolumeType::MODEL_PART || type == Slic3r::ModelVolumeType::PARAMETER_MODIFIER ? extruder : "") { - m_bmp = bmp; set_action_and_extruder_icons(); init_container(); } @@ -173,13 +187,6 @@ void ObjectDataViewModelNode::set_printable_icon(PrintIndicator printable) *get_bmp_bundle(m_printable == piPrintable ? "eye_open" : "eye_closed"); } -void ObjectDataViewModelNode::set_warning_icon(const std::string& warning_icon_name) -{ - m_warning_icon_name = warning_icon_name; - if (warning_icon_name.empty()) - m_bmp = m_empty_bmp; -} - void ObjectDataViewModelNode::update_settings_digest_bitmaps() { m_bmp = m_empty_bmp; @@ -187,7 +194,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) @@ -322,11 +329,10 @@ 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_warning_bmp = *get_bmp_bundle(WarningIcon); m_warning_manifold_bmp = *get_bmp_bundle(WarningManifoldIcon); + m_lock_bmp = *get_bmp_bundle(LockIcon); for (auto item : INFO_ITEMS) m_info_bmps[item.first] = get_bmp_bundle(item.second.bmp_name); @@ -340,19 +346,55 @@ ObjectDataViewModel::~ObjectDataViewModel() m_bitmap_cache = nullptr; } -wxBitmapBundle& ObjectDataViewModel::GetWarningBitmap(const std::string& warning_icon_name) +void ObjectDataViewModel::UpdateBitmapForNode(ObjectDataViewModelNode* node) { - return warning_icon_name.empty() ? m_empty_bmp : warning_icon_name == WarningIcon ? m_warning_bmp : m_warning_manifold_bmp; + int vol_type = static_cast(node->GetVolumeType()); + bool is_volume_node = vol_type >= 0; + + if (!node->has_warning_icon() && !node->has_lock()) { + node->SetBitmap(is_volume_node ? *m_volume_bmps.at(vol_type) : m_empty_bmp); + return; + } + + std::string scaled_bitmap_name = std::string(); + if (node->has_warning_icon()) + scaled_bitmap_name += node->warning_icon_name(); + if (node->has_lock()) + scaled_bitmap_name += LockIcon; + if (is_volume_node) + scaled_bitmap_name += std::to_string(vol_type); + scaled_bitmap_name += (wxGetApp().dark_mode() ? "-dm" : "-lm"); + + wxBitmapBundle* bmp = find_bndl(scaled_bitmap_name); + if (!bmp) { + std::vector bmps; + if (node->has_warning_icon()) + bmps.emplace_back(node->warning_icon_name() == WarningIcon ? &m_warning_bmp : &m_warning_manifold_bmp); + if (node->has_lock()) + bmps.emplace_back(&m_lock_bmp); + if (is_volume_node) + bmps.emplace_back(m_volume_bmps[vol_type]); + bmp = m_bitmap_cache->insert_bndl(scaled_bitmap_name, bmps); + } + + node->SetBitmap(*bmp); } -wxDataViewItem ObjectDataViewModel::Add(const wxString &name, - const int extruder, - const std::string& warning_icon_name/* = std::string()*/ ) +void ObjectDataViewModel::UpdateBitmapForNode(ObjectDataViewModelNode* node, const std::string& warning_icon_name, bool has_lock) { - const wxString extruder_str = extruder == 0 ? _L("default") : wxString::Format("%d", extruder); - auto root = new ObjectDataViewModelNode(name, extruder_str); + node->SetWarningIconName(warning_icon_name); + node->SetLock(has_lock); + UpdateBitmapForNode(node); +} + +wxDataViewItem ObjectDataViewModel::AddObject(const wxString &name, + const wxString& extruder, + const std::string& warning_icon_name, + const bool has_lock) +{ + auto root = new ObjectDataViewModelNode(name, extruder); // Add warning icon if detected auto-repaire - root->SetWarningBitmap(GetWarningBitmap(warning_icon_name), warning_icon_name); + UpdateBitmapForNode(root, warning_icon_name, has_lock); m_objects.push_back(root); // notify control @@ -365,42 +407,29 @@ wxDataViewItem ObjectDataViewModel::Add(const wxString &name, wxDataViewItem ObjectDataViewModel::AddVolumeChild( const wxDataViewItem &parent_item, const wxString &name, + const int volume_idx, const Slic3r::ModelVolumeType volume_type, - const std::string& warning_icon_name/* = std::string()*/, - const int extruder/* = 0*/, - const bool create_frst_child/* = true*/) + const std::string& warning_icon_name, + const wxString& extruder) { ObjectDataViewModelNode *root = static_cast(parent_item.GetID()); if (!root) return wxDataViewItem(0); - wxString extruder_str = extruder == 0 ? _(L("default")) : wxString::Format("%d", extruder); - // get insertion position according to the existed Layers and/or Instances Items int insert_position = get_root_idx(root, itLayerRoot); if (insert_position < 0) insert_position = get_root_idx(root, itInstanceRoot); - if (create_frst_child && root->m_volumes_cnt == 0) - { - const Slic3r::ModelVolumeType type = Slic3r::ModelVolumeType::MODEL_PART; - const auto node = new ObjectDataViewModelNode(root, root->m_name, type, GetVolumeIcon(type, root->m_warning_icon_name), extruder_str, 0, root->m_warning_icon_name); - - insert_position < 0 ? root->Append(node) : root->Insert(node, insert_position); - // notify control - const wxDataViewItem child((void*)node); - ItemAdded(parent_item, child); - - root->m_volumes_cnt++; - if (insert_position >= 0) insert_position++; - } - - const auto node = new ObjectDataViewModelNode(root, name, volume_type, GetVolumeIcon(volume_type, warning_icon_name), extruder_str, root->m_volumes_cnt, warning_icon_name); + const auto node = new ObjectDataViewModelNode(root, name, volume_type, extruder, volume_idx); + UpdateBitmapForNode(node, warning_icon_name, root->has_lock() && volume_type < ModelVolumeType::PARAMETER_MODIFIER); insert_position < 0 ? root->Append(node) : root->Insert(node, insert_position); // if part with errors is added, but object wasn't marked, then mark it - if (!warning_icon_name.empty() && warning_icon_name != root->m_warning_icon_name && - (root->m_warning_icon_name.empty() || root->m_warning_icon_name == WarningManifoldIcon) ) - root->SetWarningBitmap(GetWarningBitmap(warning_icon_name), warning_icon_name); + if (!warning_icon_name.empty() && warning_icon_name != root->warning_icon_name() && + (!root->has_warning_icon() || root->warning_icon_name() == WarningManifoldIcon)) { + root->SetWarningIconName(warning_icon_name); + UpdateBitmapForNode(root); + } // notify control const wxDataViewItem child((void*)node); @@ -598,14 +627,12 @@ wxDataViewItem ObjectDataViewModel::AddLayersRoot(const wxDataViewItem &parent_i wxDataViewItem ObjectDataViewModel::AddLayersChild(const wxDataViewItem &parent_item, const t_layer_height_range& layer_range, - const int extruder/* = 0*/, + const wxString& extruder, const int index /* = -1*/) { ObjectDataViewModelNode *parent_node = static_cast(parent_item.GetID()); if (!parent_node) return wxDataViewItem(0); - wxString extruder_str = extruder == 0 ? _(L("default")) : wxString::Format("%d", extruder); - // get LayerRoot node ObjectDataViewModelNode *layer_root_node; wxDataViewItem layer_root_item; @@ -622,7 +649,7 @@ wxDataViewItem ObjectDataViewModel::AddLayersChild(const wxDataViewItem &parent_ } // Add layer node - ObjectDataViewModelNode *layer_node = new ObjectDataViewModelNode(layer_root_node, layer_range, index, extruder_str); + ObjectDataViewModelNode *layer_node = new ObjectDataViewModelNode(layer_root_node, layer_range, index, extruder); if (index < 0) layer_root_node->Append(layer_node); else @@ -711,10 +738,7 @@ wxDataViewItem ObjectDataViewModel::Delete(const wxDataViewItem &item) delete node_parent; ret_item = wxDataViewItem(obj_node); -#ifndef __WXGTK__ - if (obj_node->GetChildCount() == 0) - obj_node->m_container = false; -#endif //__WXGTK__ + obj_node->invalidate_container(); ItemDeleted(ret_item, wxDataViewItem(node_parent)); return ret_item; } @@ -730,10 +754,7 @@ wxDataViewItem ObjectDataViewModel::Delete(const wxDataViewItem &item) delete node_parent; ret_item = wxDataViewItem(obj_node); -#ifndef __WXGTK__ - if (obj_node->GetChildCount() == 0) - obj_node->m_container = false; -#endif //__WXGTK__ + obj_node->invalidate_container(); ItemDeleted(ret_item, wxDataViewItem(node_parent)); return ret_item; } @@ -755,10 +776,7 @@ wxDataViewItem ObjectDataViewModel::Delete(const wxDataViewItem &item) node_parent->m_volumes_cnt = 0; delete last_child_node; -#ifndef __WXGTK__ - if (node_parent->GetChildCount() == 0) - node_parent->m_container = false; -#endif //__WXGTK__ + node_parent->invalidate_container(); ItemDeleted(parent, wxDataViewItem(last_child_node)); wxCommandEvent event(wxCUSTOMEVT_LAST_VOLUME_IS_DELETED); @@ -793,10 +811,7 @@ wxDataViewItem ObjectDataViewModel::Delete(const wxDataViewItem &item) // set m_containet to FALSE if parent has no child if (node_parent) { -#ifndef __WXGTK__ - if (node_parent->GetChildCount() == 0) - node_parent->m_container = false; -#endif //__WXGTK__ + node_parent->invalidate_container(); ret_item = parent; } @@ -838,10 +853,7 @@ wxDataViewItem ObjectDataViewModel::DeleteLastInstance(const wxDataViewItem &par parent_node->set_printable_icon(last_inst_printable); ItemDeleted(parent_item, inst_root_item); ItemChanged(parent_item); -#ifndef __WXGTK__ - if (parent_node->GetChildCount() == 0) - parent_node->m_container = false; -#endif //__WXGTK__ + parent_node->invalidate_container(); } // update object_node printable property @@ -886,10 +898,7 @@ void ObjectDataViewModel::DeleteChildren(wxDataViewItem& parent) ItemDeleted(parent, item); } - // set m_containet to FALSE if parent has no child -#ifndef __WXGTK__ - root->m_container = false; -#endif //__WXGTK__ + root->invalidate_container(); } void ObjectDataViewModel::DeleteVolumeChildren(wxDataViewItem& parent) @@ -919,11 +928,7 @@ void ObjectDataViewModel::DeleteVolumeChildren(wxDataViewItem& parent) ItemDeleted(parent, item); } root->m_volumes_cnt = 0; - - // set m_containet to FALSE if parent has no child -#ifndef __WXGTK__ - root->m_container = false; -#endif //__WXGTK__ + root->invalidate_container(); } void ObjectDataViewModel::DeleteSettings(const wxDataViewItem& parent) @@ -1681,6 +1686,7 @@ void ObjectDataViewModel::UpdateBitmaps() m_volume_bmps = MenuFactory::get_volume_bitmaps(); m_warning_bmp = *get_bmp_bundle(WarningIcon); m_warning_manifold_bmp = *get_bmp_bundle(WarningManifoldIcon); + m_lock_bmp = *get_bmp_bundle(LockIcon); for (auto item : INFO_ITEMS) m_info_bmps[item.first] = get_bmp_bundle(item.second.bmp_name); @@ -1699,10 +1705,8 @@ void ObjectDataViewModel::UpdateBitmaps() switch (node->m_type) { case itObject: - if (node->m_bmp.IsOk()) node->m_bmp = GetWarningBitmap(node->m_warning_icon_name); - break; case itVolume: - node->m_bmp = GetVolumeIcon(node->m_volume_type, node->m_warning_icon_name); + UpdateBitmapForNode(node); break; case itLayerRoot: node->m_bmp = *get_bmp_bundle(LayerRootIcon); @@ -1720,27 +1724,6 @@ void ObjectDataViewModel::UpdateBitmaps() } } -wxBitmapBundle ObjectDataViewModel::GetVolumeIcon(const Slic3r::ModelVolumeType vol_type, const std::string& warning_icon_name/* = std::string()*/) -{ - if (warning_icon_name.empty()) - return *m_volume_bmps[static_cast(vol_type)]; - - std::string scaled_bitmap_name = warning_icon_name + std::to_string(static_cast(vol_type)); - scaled_bitmap_name += "-em" + std::to_string(wxGetApp().em_unit()) + (wxGetApp().dark_mode() ? "-dm" : "-lm"); - - wxBitmapBundle *bmp = m_bitmap_cache->find_bndl(scaled_bitmap_name); - if (bmp == nullptr) { - std::vector bmps; - - bmps.emplace_back(&GetWarningBitmap(warning_icon_name)); - bmps.emplace_back(m_volume_bmps[static_cast(vol_type)]); - - bmp = m_bitmap_cache->insert_bndl(scaled_bitmap_name, bmps); - } - - return *bmp; -} - void ObjectDataViewModel::AddWarningIcon(const wxDataViewItem& item, const std::string& warning_icon_name) { if (!item.IsOk()) @@ -1748,13 +1731,14 @@ void ObjectDataViewModel::AddWarningIcon(const wxDataViewItem& item, const std:: ObjectDataViewModelNode *node = static_cast(item.GetID()); if (node->GetType() & itObject) { - node->SetWarningBitmap(GetWarningBitmap(warning_icon_name), warning_icon_name); + UpdateBitmapForNode(node, warning_icon_name, node->has_lock()); return; } if (node->GetType() & itVolume) { - node->SetWarningBitmap(GetVolumeIcon(node->GetVolumeType(), warning_icon_name), warning_icon_name); - node->GetParent()->SetWarningBitmap(GetWarningBitmap(warning_icon_name), warning_icon_name); + UpdateBitmapForNode(node, warning_icon_name, node->has_lock()); + if (ObjectDataViewModelNode* parent = node->GetParent()) + UpdateBitmapForNode(parent, warning_icon_name, parent->has_lock()); return; } } @@ -1769,12 +1753,9 @@ void ObjectDataViewModel::DeleteWarningIcon(const wxDataViewItem& item, const bo if (!node->GetBitmap().IsOk() || !(node->GetType() & (itVolume | itObject))) return; - if (node->GetType() & itVolume) { - node->SetWarningBitmap(*m_volume_bmps[static_cast(node->volume_type())], ""); - return; - } + node->SetWarningIconName(std::string()); + UpdateBitmapForNode(node); - node->SetWarningBitmap(wxNullBitmap, ""); if (unmark_object) { wxDataViewItemArray children; @@ -1801,6 +1782,26 @@ void ObjectDataViewModel::UpdateWarningIcon(const wxDataViewItem& item, const st AddWarningIcon(item, warning_icon_name); } +void ObjectDataViewModel::UpdateLockIcon(const wxDataViewItem& item, bool has_lock) +{ + if (!item.IsOk()) + return; + ObjectDataViewModelNode* node = static_cast(item.GetID()); + if (node->has_lock() == has_lock) + return; + + node->SetLock(has_lock); + UpdateBitmapForNode(node); + + if (node->GetType() & itObject) { + wxDataViewItemArray children; + GetChildren(item, children); + for (const wxDataViewItem& child : children) + UpdateLockIcon(child, has_lock); + } + ItemChanged(item); +} + } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/ObjectDataViewModel.hpp b/src/slic3r/GUI/ObjectDataViewModel.hpp index 7014acccb..6f2d1519c 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.hpp +++ b/src/slic3r/GUI/ObjectDataViewModel.hpp @@ -51,6 +51,7 @@ enum class InfoItemType Undef, CustomSupports, CustomSeam, + CutConnectors, MmuSegmentation, Sinking, VariableLayerHeight @@ -79,9 +80,10 @@ class ObjectDataViewModelNode PrintIndicator m_printable {piUndef}; wxBitmapBundle m_printable_icon; std::string m_warning_icon_name{ "" }; + bool m_has_lock{false}; std::string m_action_icon_name = ""; - ModelVolumeType m_volume_type; + ModelVolumeType m_volume_type{ -1 }; InfoItemType m_info_item_type {InfoItemType::Undef}; public: @@ -99,10 +101,8 @@ public: ObjectDataViewModelNode(ObjectDataViewModelNode* parent, const wxString& sub_obj_name, Slic3r::ModelVolumeType type, - const wxBitmapBundle& bmp, const wxString& extruder, - const int idx = -1, - const std::string& warning_icon_name = std::string()); + const int idx = -1 ); ObjectDataViewModelNode(ObjectDataViewModelNode* parent, const t_layer_height_range& layer_range, @@ -128,7 +128,9 @@ public: } void init_container(); - bool IsContainer() const + void invalidate_container(); + + bool IsContainer() const { return m_container; } @@ -181,7 +183,8 @@ public: void SetVolumeType(ModelVolumeType type) { m_volume_type = type; } void SetBitmap(const wxBitmapBundle &icon) { m_bmp = icon; } void SetExtruder(const wxString &extruder) { m_extruder = extruder; } - void SetWarningBitmap(const wxBitmapBundle& icon, const std::string& warning_icon_name) { m_bmp = icon; m_warning_icon_name = warning_icon_name; } + void SetWarningIconName(const std::string& warning_icon_name) { m_warning_icon_name = warning_icon_name; } + void SetLock(bool has_lock) { m_has_lock = has_lock; } const wxBitmapBundle& GetBitmap() const { return m_bmp; } const wxString& GetName() const { return m_name; } ItemType GetType() const { return m_type; } @@ -228,8 +231,6 @@ public: void set_extruder_icon(); // Set printable icon for node void set_printable_icon(PrintIndicator printable); - // Set warning icon for node - void set_warning_icon(const std::string& warning_icon); void update_settings_digest_bitmaps(); bool update_settings_digest(const std::vector& categories); @@ -240,7 +241,9 @@ public: bool valid(); #endif /* NDEBUG */ bool invalid() const { return m_idx < -1; } - bool has_warning_icon() const { return !m_warning_icon_name.empty(); } + bool has_warning_icon() const { return !m_warning_icon_name.empty(); } + bool has_lock() const { return m_has_lock; } + const std::string& warning_icon_name() const { return m_warning_icon_name; } private: friend class ObjectDataViewModel; @@ -262,6 +265,7 @@ class ObjectDataViewModel :public wxDataViewModel wxBitmapBundle m_empty_bmp; wxBitmapBundle m_warning_bmp; wxBitmapBundle m_warning_manifold_bmp; + wxBitmapBundle m_lock_bmp; wxDataViewCtrl* m_ctrl { nullptr }; @@ -269,15 +273,16 @@ public: ObjectDataViewModel(); ~ObjectDataViewModel(); - wxDataViewItem Add( const wxString &name, - const int extruder, - const std::string& warning_icon_name = std::string()); + wxDataViewItem AddObject( const wxString &name, + const wxString& extruder, + const std::string& warning_icon_name, + const bool has_lock); wxDataViewItem AddVolumeChild( const wxDataViewItem &parent_item, const wxString &name, + const int volume_idx, const Slic3r::ModelVolumeType volume_type, - const std::string& warning_icon_name = std::string(), - const int extruder = 0, - const bool create_frst_child = true); + const std::string& warning_icon_name, + const wxString& extruder); wxDataViewItem AddSettingsChild(const wxDataViewItem &parent_item); wxDataViewItem AddInfoChild(const wxDataViewItem &parent_item, InfoItemType info_type); wxDataViewItem AddInstanceChild(const wxDataViewItem &parent_item, size_t num); @@ -285,7 +290,7 @@ public: wxDataViewItem AddLayersRoot(const wxDataViewItem &parent_item); wxDataViewItem AddLayersChild( const wxDataViewItem &parent_item, const t_layer_height_range& layer_range, - const int extruder = 0, + const wxString& extruder, const int index = -1); size_t GetItemIndexForFirstVolume(ObjectDataViewModelNode* node_parent); wxDataViewItem Delete(const wxDataViewItem &item); @@ -390,6 +395,7 @@ public: void AddWarningIcon(const wxDataViewItem& item, const std::string& warning_name); void DeleteWarningIcon(const wxDataViewItem& item, const bool unmark_object = false); void UpdateWarningIcon(const wxDataViewItem& item, const std::string& warning_name); + void UpdateLockIcon(const wxDataViewItem& item, bool has_lock); bool HasWarningIcon(const wxDataViewItem& item) const; t_layer_height_range GetLayerRangeByItem(const wxDataViewItem& item) const; @@ -403,7 +409,8 @@ private: wxDataViewItem AddInstanceRoot(const wxDataViewItem& parent_item); void AddAllChildren(const wxDataViewItem& parent); - wxBitmapBundle& GetWarningBitmap(const std::string& warning_icon_name); + void UpdateBitmapForNode(ObjectDataViewModelNode* node); + void UpdateBitmapForNode(ObjectDataViewModelNode* node, const std::string& warning_icon_name, bool has_lock); }; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index eb8b5b01a..dd47325b5 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -97,6 +97,7 @@ #include "MsgDialog.hpp" #include "ProjectDirtyStateManager.hpp" #include "Gizmos/GLGizmoSimplify.hpp" // create suggestion notification +#include "Gizmos/GLGizmoCut.hpp" #ifdef __APPLE__ #include "Gizmos/GLGizmosManager.hpp" @@ -1792,7 +1793,7 @@ struct Plater::priv std::string get_config(const std::string &key) const; std::vector load_files(const std::vector& input_files, bool load_model, bool load_config, bool used_inches = false); - std::vector load_model_objects(const ModelObjectPtrs& model_objects, bool allow_negative_z = false); + std::vector load_model_objects(const ModelObjectPtrs& model_objects, bool allow_negative_z = false, bool call_selection_changed = true); fs::path get_export_file_path(GUI::FileType file_type); wxString get_export_file(GUI::FileType file_type); @@ -1807,7 +1808,7 @@ struct Plater::priv void select_all(); void deselect_all(); void remove(size_t obj_idx); - void delete_object_from_model(size_t obj_idx); + bool delete_object_from_model(size_t obj_idx); void delete_all_objects_from_model(); void reset(); void mirror(Axis axis); @@ -2718,7 +2719,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ // #define AUTOPLACEMENT_ON_LOAD -std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& model_objects, bool allow_negative_z) +std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& model_objects, bool allow_negative_z, bool call_selection_changed /*= true*/) { const Vec3d bed_size = Slic3r::to_3d(this->bed.build_volume().bounding_volume2d().size(), 1.0) - 2.0 * Vec3d::Ones(); @@ -2801,17 +2802,18 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode notification_manager->close_notification_of_type(NotificationType::UpdatedItemsInfo); for (const size_t idx : obj_idxs) { - wxGetApp().obj_list()->add_object_to_list(idx); + wxGetApp().obj_list()->add_object_to_list(idx, call_selection_changed); } - update(); - // Update InfoItems in ObjectList after update() to use of a correct value of the GLCanvas3D::is_sinking(), - // which is updated after a view3D->reload_scene(false, flags & (unsigned int)UpdateParams::FORCE_FULL_SCREEN_REFRESH) call - for (const size_t idx : obj_idxs) - wxGetApp().obj_list()->update_info_items(idx); - - object_list_changed(); + if (call_selection_changed) { + update(); + // Update InfoItems in ObjectList after update() to use of a correct value of the GLCanvas3D::is_sinking(), + // which is updated after a view3D->reload_scene(false, flags & (unsigned int)UpdateParams::FORCE_FULL_SCREEN_REFRESH) call + for (const size_t idx : obj_idxs) + wxGetApp().obj_list()->update_info_items(idx); + object_list_changed(); + } this->schedule_background_process(); return obj_idxs; @@ -2990,16 +2992,35 @@ void Plater::priv::remove(size_t obj_idx) } -void Plater::priv::delete_object_from_model(size_t obj_idx) +bool Plater::priv::delete_object_from_model(size_t obj_idx) { + // check if object isn't cut + // show warning message that "cut consistancy" will not be supported any more + ModelObject* obj = model.objects[obj_idx]; + if (obj->is_cut()) { + InfoDialog dialog(q, _L("Delete object which is a part of cut object"), + _L("You try to delete an object which is a part of a cut object.\n" + "This action will break a cut correspondence.\n" + "After that PrusaSlicer can't garantie model consistency"), + false, wxYES | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING); + dialog.SetButtonLabel(wxID_YES, _L("Delete object")); + if (dialog.ShowModal() == wxID_CANCEL) + return false; + } + wxString snapshot_label = _L("Delete Object"); - if (! model.objects[obj_idx]->name.empty()) - snapshot_label += ": " + wxString::FromUTF8(model.objects[obj_idx]->name.c_str()); + if (!obj->name.empty()) + snapshot_label += ": " + wxString::FromUTF8(obj->name.c_str()); Plater::TakeSnapshot snapshot(q, snapshot_label); m_worker.cancel_all(); + + if (obj->is_cut()) + sidebar->obj_list()->invalidate_cut_info_for_object(obj_idx); + model.delete_object(obj_idx); update(); object_list_changed(); + return true; } void Plater::priv::delete_all_objects_from_model() @@ -3013,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(); @@ -4159,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. @@ -4438,7 +4455,10 @@ void Plater::priv::on_action_split_volumes(SimpleEvent&) void Plater::priv::on_action_layersediting(SimpleEvent&) { - view3D->enable_layers_editing(!view3D->is_layers_editing_enabled()); + const bool enable_layersediting = !view3D->is_layers_editing_enabled(); + view3D->enable_layers_editing(enable_layersediting); + if (enable_layersediting) + view3D->get_canvas3d()->reset_all_gizmos(); notification_manager->set_move_from_overlay(view3D->is_layers_editing_enabled()); } @@ -4481,7 +4501,7 @@ void Plater::priv::on_right_click(RBtnEvent& evt) selection.is_single_full_object() || selection.is_multiple_full_instance(); #if ENABLE_WORLD_COORDINATE - const bool is_part = selection.is_single_volume_or_modifier(); + const bool is_part = selection.is_single_volume_or_modifier() && ! selection.is_any_connector(); #else const bool is_part = selection.is_single_volume() || selection.is_single_modifier(); #endif // ENABLE_WORLD_COORDINATE @@ -4753,7 +4773,8 @@ bool Plater::priv::can_split(bool to_objects) const bool Plater::priv::can_scale_to_print_volume() const { const BuildVolume::Type type = this->bed.build_volume().type(); - return !view3D->get_canvas3d()->get_selection().is_empty() && (type == BuildVolume::Type::Rectangle || type == BuildVolume::Type::Circle); + return !sidebar->obj_list()->has_selected_cut_object() && + !view3D->get_canvas3d()->get_selection().is_empty() && (type == BuildVolume::Type::Rectangle || type == BuildVolume::Type::Circle); } bool Plater::priv::layers_height_allowed() const @@ -4768,16 +4789,19 @@ bool Plater::priv::layers_height_allowed() const bool Plater::priv::can_mirror() const { - return get_selection().is_from_single_instance(); + return !sidebar->obj_list()->has_selected_cut_object() && get_selection().is_from_single_instance(); } bool Plater::priv::can_replace_with_stl() const { - return get_selection().get_volume_idxs().size() == 1; + return !sidebar->obj_list()->has_selected_cut_object() && get_selection().get_volume_idxs().size() == 1; } bool Plater::priv::can_reload_from_disk() const { + if (sidebar->obj_list()->has_selected_cut_object()) + return false; + #if ENABLE_RELOAD_FROM_DISK_REWORK // collect selected reloadable ModelVolumes std::vector> selected_volumes = reloadable_volumes(model, get_selection()); @@ -4891,8 +4915,12 @@ bool Plater::priv::can_fix_through_netfabb() const bool Plater::priv::can_simplify() const { + const int obj_idx = get_selected_object_idx(); // is object for simplification selected - if (get_selected_object_idx() < 0) return false; + // cut object can't be simplify + if (obj_idx < 0 || model.objects[obj_idx]->is_cut()) + return false; + // is already opened? if (q->canvas3D()->get_gizmos_manager().get_current_type() == GLGizmosManager::EType::Simplify) @@ -4906,8 +4934,9 @@ bool Plater::priv::can_increase_instances() const || q->canvas3D()->get_gizmos_manager().is_in_editing_mode()) return false; - int obj_idx = get_selected_object_idx(); - return (0 <= obj_idx) && (obj_idx < (int)model.objects.size()); + 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 @@ -4916,8 +4945,10 @@ bool Plater::priv::can_decrease_instances() const || q->canvas3D()->get_gizmos_manager().is_in_editing_mode()) return false; - int obj_idx = get_selected_object_idx(); - return (0 <= obj_idx) && (obj_idx < (int)model.objects.size()) && (model.objects[obj_idx]->instances.size() > 1); + 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 @@ -5721,7 +5752,7 @@ void Plater::reset_with_confirm() reset(); } -void Plater::delete_object_from_model(size_t obj_idx) { p->delete_object_from_model(obj_idx); } +bool Plater::delete_object_from_model(size_t obj_idx) { return p->delete_object_from_model(obj_idx); } void Plater::remove_selected() { @@ -5898,23 +5929,29 @@ void Plater::toggle_layers_editing(bool enable) canvas3D()->force_main_toolbar_left_action(canvas3D()->get_main_toolbar_item_id("layersediting")); } -void Plater::cut(size_t obj_idx, size_t instance_idx, coordf_t z, ModelObjectCutAttributes attributes) +void Plater::cut(size_t obj_idx, size_t instance_idx, const Transform3d& cut_matrix, ModelObjectCutAttributes attributes) { wxCHECK_RET(obj_idx < p->model.objects.size(), "obj_idx out of bounds"); - auto *object = p->model.objects[obj_idx]; + auto* object = p->model.objects[obj_idx]; wxCHECK_RET(instance_idx < object->instances.size(), "instance_idx out of bounds"); - if (! attributes.has(ModelObjectCutAttribute::KeepUpper) && ! attributes.has(ModelObjectCutAttribute::KeepLower)) - return; - - Plater::TakeSnapshot snapshot(this, _L("Cut by Plane")); - wxBusyCursor wait; - const auto new_objects = object->cut(instance_idx, z, attributes); - remove(obj_idx); - p->load_model_objects(new_objects); + const auto new_objects = object->cut(instance_idx, cut_matrix, attributes); + + model().delete_object(obj_idx); + sidebar().obj_list()->delete_object_from_list(obj_idx); + + // suppress to call selection update for Object List to avoid call of early Gizmos on/off update + p->load_model_objects(new_objects, false, false); + + // now process all updates of the 3d scene + update(); + // Update InfoItems in ObjectList after update() to use of a correct value of the GLCanvas3D::is_sinking(), + // which is updated after a view3D->reload_scene(false, flags & (unsigned int)UpdateParams::FORCE_FULL_SCREEN_REFRESH) call + for (size_t idx = 0; idx < p->model.objects.size(); idx++) + wxGetApp().obj_list()->update_info_items(idx); Selection& selection = p->get_selection(); size_t last_id = p->model.objects.size() - 1; diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index d099e0361..9dc9f6316 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -242,7 +242,7 @@ public: void remove(size_t obj_idx); void reset(); void reset_with_confirm(); - void delete_object_from_model(size_t obj_idx); + bool delete_object_from_model(size_t obj_idx); void remove_selected(); void increase_instances(size_t num = 1); void decrease_instances(size_t num = 1); @@ -253,7 +253,7 @@ public: void convert_unit(ConversionType conv_type); void toggle_layers_editing(bool enable); - void cut(size_t obj_idx, size_t instance_idx, coordf_t z, ModelObjectCutAttributes attributes); + void cut(size_t obj_idx, size_t instance_idx, const Transform3d& cut_matrix, ModelObjectCutAttributes attributes); void export_gcode(bool prefer_removable); void export_stl_obj(bool extended = false, bool selection_only = false); diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index 65469e6a0..200cd8a30 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/SavePresetDialog.cpp b/src/slic3r/GUI/SavePresetDialog.cpp index 57aa5da98..40f981722 100644 --- a/src/slic3r/GUI/SavePresetDialog.cpp +++ b/src/slic3r/GUI/SavePresetDialog.cpp @@ -163,7 +163,7 @@ void SavePresetDialog::Item::update() if (m_valid_type == ValidationType::Valid && existing) { if (m_preset_name == m_presets->get_selected_preset_name()) { - if (!rename && m_presets->get_edited_preset().is_dirty || + if ((!rename && m_presets->get_edited_preset().is_dirty) || m_parent->get_preset_bundle()) // means that we save modifications from the DiffDialog info_line = _L("Save preset modifications to existing user profile"); else diff --git a/src/slic3r/GUI/SceneRaycaster.cpp b/src/slic3r/GUI/SceneRaycaster.cpp index 6d255ccdb..96ae536ca 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 7541badbb..2254a2022 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/Selection.cpp b/src/slic3r/GUI/Selection.cpp index 2423b10a0..9a4169b31 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -510,6 +510,28 @@ void Selection::volumes_changed(const std::vector &map_volume_old_to_new this->set_bounding_boxes_dirty(); } +bool Selection::is_any_connector() const +{ + const int obj_idx = get_object_idx(); + + if ((is_any_volume() || is_any_modifier() || is_mixed()) && // some solid_part AND/OR modifier is selected + obj_idx >= 0 && m_model->objects[obj_idx]->is_cut()) { + const ModelVolumePtrs& obj_volumes = m_model->objects[obj_idx]->volumes; + for (size_t vol_idx = 0; vol_idx < obj_volumes.size(); vol_idx++) + if (obj_volumes[vol_idx]->is_cut_connector()) + for (const GLVolume* v : *m_volumes) + if (v->object_idx() == obj_idx && v->volume_idx() == (int)vol_idx && v->selected) + return true; + } + return false; +} + +bool Selection::is_any_cut_volume() const +{ + const int obj_idx = get_object_idx(); + return is_any_volume() && obj_idx >= 0 && m_model->objects[obj_idx]->is_cut(); +} + bool Selection::is_single_full_instance() const { if (m_type == SingleFullInstance) diff --git a/src/slic3r/GUI/Selection.hpp b/src/slic3r/GUI/Selection.hpp index cebea29e0..c4720a308 100644 --- a/src/slic3r/GUI/Selection.hpp +++ b/src/slic3r/GUI/Selection.hpp @@ -320,6 +320,8 @@ public: bool is_single_volume() const { return m_type == SingleVolume; } bool is_multiple_volume() const { return m_type == MultipleVolume; } bool is_any_volume() const { return is_single_volume() || is_multiple_volume(); } + bool is_any_connector() const; + bool is_any_cut_volume() const; bool is_mixed() const { return m_type == Mixed; } bool is_from_single_instance() const { return get_instance_idx() != -1; } bool is_from_single_object() const; @@ -328,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 contains_volume(unsigned int volume_idx) const { return m_list.find(volume_idx) != m_list.end(); } // returns true if the selection contains all the given indices diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 8f38a1525..7e441342f 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 448e4be05..5adeb278a 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/UnsavedChangesDialog.hpp b/src/slic3r/GUI/UnsavedChangesDialog.hpp index 69f808451..a0b53dadf 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.hpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.hpp @@ -414,7 +414,7 @@ public: std::string get_left_preset_name(Preset::Type type); std::string get_right_preset_name(Preset::Type type); - std::vector get_selected_options(Preset::Type type) const { return std::move(m_tree->options(type, true)); } + std::vector get_selected_options(Preset::Type type) const { return m_tree->options(type, true); } protected: void on_dpi_changed(const wxRect& suggested_rect) override; diff --git a/src/slic3r/GUI/format.hpp b/src/slic3r/GUI/format.hpp index 51beafff1..15a3f04c3 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 a78720807..ed7d8a92b 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -26,6 +26,7 @@ add_executable(${_TEST_NAME}_tests test_voronoi.cpp test_optimizers.cpp test_png_io.cpp + test_surface_mesh.cpp test_timeutils.cpp test_indexed_triangle_set.cpp test_astar.cpp diff --git a/tests/libslic3r/test_surface_mesh.cpp b/tests/libslic3r/test_surface_mesh.cpp new file mode 100644 index 000000000..34ff35667 --- /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()); + } +}