diff --git a/resources/profiles/Templates.idx b/resources/profiles/Templates.idx new file mode 100644 index 0000000000..3edb4400fb --- /dev/null +++ b/resources/profiles/Templates.idx @@ -0,0 +1,2 @@ +min_slic3r_version = 2.6.0-alpha0 +1.0.0 Initial diff --git a/resources/profiles/Templates.ini b/resources/profiles/Templates.ini new file mode 100644 index 0000000000..6c6ff646c6 --- /dev/null +++ b/resources/profiles/Templates.ini @@ -0,0 +1,2144 @@ +# Generic filament profile templates + +[vendor] +name = Filaments Templates +config_version = 1.0.0 +config_update_url = https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/Templates/ +templates_profile = 1 + +## Generic filament profiles + +# Print profiles for the Prusa Research printers. + +[filament:*common*] +cooling = 1 +compatible_printers = +compatible_printers_condition = +end_filament_gcode = "; Filament-specific end gcode" +extrusion_multiplier = 1 +filament_loading_speed = 14 +filament_loading_speed_start = 19 +filament_unloading_speed = 90 +filament_unloading_speed_start = 100 +filament_toolchange_delay = 0 +filament_cooling_moves = 1 +filament_cooling_initial_speed = 3 +filament_cooling_final_speed = 2 +filament_load_time = 0 +filament_unload_time = 0 +filament_ramming_parameters = "130 120 2.70968 2.93548 3.32258 3.83871 4.58065 5.54839 6.51613 7.35484 7.93548 8.16129| 0.05 2.66451 0.45 3.05805 0.95 4.05807 1.45 5.97742 1.95 7.69999 2.45 8.1936 2.95 11.342 3.45 11.4065 3.95 7.6 4.45 7.6 4.95 7.6" +filament_minimal_purge_on_wipe_tower = 0 +filament_cost = 0 +filament_density = 0 +filament_diameter = 1.75 +filament_notes = "" +filament_settings_id = "" +filament_soluble = 0 +min_print_speed = 10 +slowdown_below_layer_time = 10 +start_filament_gcode = + +[filament:*PLA*] +inherits = *common* +bed_temperature = 60 +bridge_fan_speed = 100 +disable_fan_first_layers = 1 +full_fan_speed_layer = 3 +fan_always_on = 1 +fan_below_layer_time = 100 +filament_colour = #FF8000 +filament_max_volumetric_speed = 0 +filament_type = PLA +first_layer_bed_temperature = 60 +first_layer_temperature = 215 +max_fan_speed = 100 +min_fan_speed = 100 +temperature = 210 + +[filament:*PET*] +inherits = *common* +bed_temperature = 90 +bridge_fan_speed = 50 +disable_fan_first_layers = 3 +full_fan_speed_layer = 5 +fan_always_on = 1 +fan_below_layer_time = 20 +filament_colour = #FF8000 +filament_max_volumetric_speed = 0 +filament_type = PETG +first_layer_bed_temperature = 85 +first_layer_temperature = 230 +max_fan_speed = 50 +min_fan_speed = 30 +temperature = 240 + +[filament:*ABS*] +inherits = *common* +bed_temperature = 100 +bridge_fan_speed = 25 +cooling = 0 +disable_fan_first_layers = 3 +fan_always_on = 0 +fan_below_layer_time = 20 +filament_colour = #FFF2EC +filament_max_volumetric_speed = 0 +filament_ramming_parameters = "120 100 5.70968 6.03226 7 8.25806 9 9.19355 9.3871 9.77419 10.129 10.3226 10.4516 10.5161| 0.05 5.69677 0.45 6.15484 0.95 8.76774 1.45 9.20323 1.95 9.95806 2.45 10.3871 2.95 10.5677 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6" +filament_type = ABS +first_layer_bed_temperature = 100 +first_layer_temperature = 255 +max_fan_speed = 30 +min_fan_speed = 20 +temperature = 255 + +[filament:*ABSC*] +inherits = *common* +bed_temperature = 100 +bridge_fan_speed = 25 +cooling = 1 +disable_fan_first_layers = 4 +fan_always_on = 0 +fan_below_layer_time = 30 +slowdown_below_layer_time = 20 +filament_colour = #FFF2EC +filament_max_volumetric_speed = 0 +filament_ramming_parameters = "120 100 5.70968 6.03226 7 8.25806 9 9.19355 9.3871 9.77419 10.129 10.3226 10.4516 10.5161| 0.05 5.69677 0.45 6.15484 0.95 8.76774 1.45 9.20323 1.95 9.95806 2.45 10.3871 2.95 10.5677 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6" +filament_type = ABS +first_layer_bed_temperature = 100 +first_layer_temperature = 255 +max_fan_speed = 15 +min_fan_speed = 15 +min_print_speed = 15 +temperature = 255 + +[filament:*FLEX*] +inherits = *common* +bed_temperature = 50 +bridge_fan_speed = 80 +cooling = 0 +disable_fan_first_layers = 3 +extrusion_multiplier = 1.15 +fan_always_on = 0 +fan_below_layer_time = 100 +filament_colour = #008000 +filament_max_volumetric_speed = 1.8 +filament_type = FLEX +first_layer_bed_temperature = 50 +first_layer_temperature = 240 +max_fan_speed = 90 +min_fan_speed = 70 +temperature = 240 + +[filament:ColorFabb bronzeFill] +inherits = *PLA* +filament_vendor = ColorFabb +extrusion_multiplier = 1.12 +filament_cost = 80.65 +filament_density = 3.9 +filament_spool_weight = 236 +filament_colour = #804040 + +[filament:ColorFabb steelFill] +inherits = *PLA* +filament_vendor = ColorFabb +extrusion_multiplier = 1.1 +filament_cost = 80.65 +filament_density = 3.13 +filament_spool_weight = 236 +filament_colour = #808080 + +[filament:ColorFabb copperFill] +inherits = *PLA* +filament_vendor = ColorFabb +extrusion_multiplier = 1.1 +filament_cost = 80.65 +filament_density = 3.9 +filament_spool_weight = 236 +filament_colour = #82603E + +[filament:ColorFabb HT] +inherits = *PET* +filament_vendor = ColorFabb +bed_temperature = 100 +bridge_fan_speed = 30 +cooling = 1 +disable_fan_first_layers = 3 +fan_always_on = 0 +fan_below_layer_time = 10 +filament_cost = 65.66 +filament_density = 1.18 +filament_spool_weight = 236 +first_layer_bed_temperature = 100 +first_layer_temperature = 270 +max_fan_speed = 20 +min_fan_speed = 10 +temperature = 270 + +[filament:ColorFabb PLA-PHA] +inherits = *PLA* +filament_vendor = ColorFabb +filament_cost = 54.84 +filament_density = 1.24 +filament_spool_weight = 236 + +[filament:ColorFabb woodFill] +inherits = *PLA* +filament_vendor = ColorFabb +extrusion_multiplier = 1.1 +filament_cost = 78.63 +filament_density = 1.15 +filament_spool_weight = 236 +filament_colour = #dfc287 +first_layer_temperature = 200 +temperature = 200 + +[filament:ColorFabb corkFill] +inherits = *PLA* +filament_vendor = ColorFabb +extrusion_multiplier = 1.1 +filament_cost = 78.63 +filament_density = 1.18 +filament_spool_weight = 236 +filament_colour = #634d33 +filament_max_volumetric_speed = 6 +first_layer_temperature = 220 +temperature = 220 + +[filament:ColorFabb XT] +inherits = *PET* +filament_vendor = ColorFabb +filament_cost = 62.90 +filament_density = 1.27 +filament_spool_weight = 236 +first_layer_bed_temperature = 90 +first_layer_temperature = 260 +temperature = 270 + +[filament:ColorFabb XT-CF20] +inherits = *PET* +filament_vendor = ColorFabb +extrusion_multiplier = 1.05 +filament_cost = 80.65 +filament_density = 1.35 +filament_spool_weight = 236 +filament_colour = #804040 +filament_max_volumetric_speed = 2 +first_layer_bed_temperature = 90 +first_layer_temperature = 260 +temperature = 260 + +[filament:ColorFabb nGen] +inherits = *PET* +filament_vendor = ColorFabb +filament_cost = 52.46 +filament_density = 1.2 +filament_spool_weight = 236 +bridge_fan_speed = 40 +fan_always_on = 0 +fan_below_layer_time = 10 +filament_type = NGEN +first_layer_temperature = 240 +max_fan_speed = 35 +min_fan_speed = 20 + +[filament:ColorFabb nGen flex] +inherits = *FLEX* +filament_vendor = ColorFabb +filament_cost = 58.30 +filament_density = 1 +filament_spool_weight = 236 +bed_temperature = 85 +bridge_fan_speed = 40 +cooling = 1 +disable_fan_first_layers = 3 +extrusion_multiplier = 1 +fan_below_layer_time = 10 +filament_max_volumetric_speed = 5 +first_layer_bed_temperature = 85 +first_layer_temperature = 260 +max_fan_speed = 35 +min_fan_speed = 20 +temperature = 260 + +[filament:Kimya PETG Carbon] +inherits = *PET* +filament_vendor = Kimya +extrusion_multiplier = 1.05 +filament_cost = 150.02 +filament_density = 1.317 +filament_colour = #804040 +first_layer_bed_temperature = 85 +first_layer_temperature = 240 +temperature = 240 + +[filament:Kimya ABS Carbon] +inherits = *ABSC* +filament_vendor = Kimya +filament_cost = 140.34 +filament_density = 1.032 +filament_colour = #804040 +first_layer_temperature = 260 +temperature = 260 + +[filament:Kimya ABS Kevlar] +inherits = Kimya ABS Carbon +filament_vendor = Kimya +filament_density = 1.037 + +[filament:E3D Edge] +inherits = *PET* +filament_vendor = E3D +filament_cost = 56.9 +filament_density = 1.26 +filament_type = EDGE + +[filament:E3D PC-ABS] +inherits = *ABS* +filament_vendor = E3D +filament_cost = 0 +filament_type = PC +filament_density = 1.05 +first_layer_temperature = 270 +temperature = 270 + +[filament:Fillamentum PLA] +inherits = *PLA* +filament_vendor = Fillamentum +filament_cost = 35.48 +filament_density = 1.24 +filament_spool_weight = 230 + +[filament:Fillamentum ABS] +inherits = *ABSC* +filament_vendor = Fillamentum +filament_cost = 32.4 +filament_density = 1.04 +filament_spool_weight = 230 +first_layer_temperature = 240 +temperature = 240 + +[filament:Fillamentum ASA] +inherits = *ABS* +filament_vendor = Fillamentum +filament_cost = 38.7 +filament_density = 1.07 +filament_spool_weight = 230 +fan_always_on = 1 +cooling = 1 +min_fan_speed = 20 +max_fan_speed = 20 +min_print_speed = 15 +slowdown_below_layer_time = 15 +first_layer_temperature = 260 +temperature = 260 +filament_type = ASA + +[filament:Prusament ASA] +inherits = *ABS* +filament_vendor = Prusa Polymers +filament_cost = 42.69 +filament_density = 1.07 +filament_spool_weight = 201 +fan_always_on = 1 +first_layer_temperature = 260 +first_layer_bed_temperature = 100 +temperature = 260 +bed_temperature = 100 +cooling = 1 +min_fan_speed = 20 +max_fan_speed = 20 +bridge_fan_speed = 30 +min_print_speed = 15 +slowdown_below_layer_time = 15 +disable_fan_first_layers = 4 +filament_type = ASA +filament_colour = #FFF2EC + +[filament:Prusament PC Blend] +inherits = *ABS* +filament_vendor = Prusa Polymers +filament_cost = 62.36 +filament_density = 1.22 +filament_spool_weight = 201 +fan_always_on = 0 +first_layer_temperature = 275 +first_layer_bed_temperature = 105 +temperature = 275 +bed_temperature = 105 +cooling = 1 +min_fan_speed = 20 +max_fan_speed = 20 +bridge_fan_speed = 30 +min_print_speed = 15 +slowdown_below_layer_time = 20 +disable_fan_first_layers = 4 +fan_below_layer_time = 30 +filament_type = PC +filament_colour = #DEE0E6 + +[filament:Prusament PC Blend Carbon Fiber] +inherits = Prusament PC Blend +filament_cost = 90.73 +filament_density = 1.16 +extrusion_multiplier = 1.04 +first_layer_temperature = 285 +temperature = 285 +disable_fan_first_layers = 4 +fan_below_layer_time = 10 +filament_colour = #BBBBBB + +[filament:Prusament PA11 Carbon Fiber] +inherits = Prusament PC Blend Carbon Fiber +filament_cost = 151.24 +filament_density = 1.11 +filament_type = PA +extrusion_multiplier = 1.05 +first_layer_temperature = 275 +temperature = 285 +first_layer_bed_temperature = 90 +bed_temperature = 105 +fan_below_layer_time = 10 + +[filament:Fillamentum CPE] +inherits = *PET* +filament_vendor = Fillamentum +filament_cost = 56.45 +filament_density = 1.25 +filament_spool_weight = 230 +filament_type = CPE +first_layer_bed_temperature = 90 +first_layer_temperature = 275 +min_fan_speed = 30 +max_fan_speed = 50 +disable_fan_first_layers = 3 +full_fan_speed_layer = 5 +temperature = 275 + +[filament:Fillamentum Timberfill] +inherits = *PLA* +filament_vendor = Fillamentum +extrusion_multiplier = 1.05 +filament_cost = 68 +filament_density = 1.15 +filament_spool_weight = 230 +filament_colour = #804040 +first_layer_temperature = 190 +temperature = 190 +filament_retract_lift = 0.2 + +[filament:Smartfil Wood] +inherits = *PLA* +filament_vendor = Smart Materials 3D +extrusion_multiplier = 1.05 +filament_cost = 68 +filament_density = 1.58 +filament_colour = #804040 +first_layer_temperature = 220 +temperature = 220 + +[filament:Generic ABS] +inherits = *ABSC* +filament_vendor = Generic +filament_cost = 27.82 +filament_density = 1.04 + +[filament:Esun ABS] +inherits = *ABSC* +filament_vendor = Esun +filament_cost = 27.82 +filament_density = 1.01 +filament_spool_weight = 265 + +[filament:Hatchbox ABS] +inherits = *ABSC* +filament_vendor = Hatchbox +filament_cost = 27.82 +filament_density = 1.04 +filament_spool_weight = 245 + +[filament:Filament PM ABS] +inherits = *ABSC* +filament_vendor = Filament PM +filament_cost = 27.82 +filament_density = 1.08 +filament_spool_weight = 230 + +[filament:Verbatim ABS] +inherits = *ABSC* +filament_vendor = Verbatim +filament_cost = 25.87 +filament_density = 1.05 +filament_spool_weight = 235 + +[filament:Generic PETG] +inherits = *PET* +filament_vendor = Generic +filament_cost = 27.82 +filament_density = 1.27 + +[filament:Extrudr DuraPro ASA] +inherits = Fillamentum ASA +filament_vendor = Extrudr +bed_temperature = 90 +filament_cost = 34.64 +filament_density = 1.05 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?material=120" +first_layer_bed_temperature = 90 +first_layer_temperature = 220 +temperature = 220 +filament_spool_weight = 230 + +[filament:Extrudr PETG] +inherits = *PET* +filament_vendor = Extrudr +bed_temperature = 70 +filament_cost = 35.45 +filament_density = 1.29 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?material=94" +first_layer_bed_temperature = 70 +first_layer_temperature = 220 +temperature = 220 +slowdown_below_layer_time = 20 +filament_spool_weight = 262 +full_fan_speed_layer = 0 + +[filament:Extrudr XPETG CF] +inherits = Extrudr PETG +filament_cost = 62.49 +filament_density = 1.29 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?material=198" +first_layer_temperature = 235 +temperature = 235 +filament_spool_weight = 230 + +[filament:Extrudr XPETG Matt] +inherits = Extrudr PETG +filament_cost = 29.99 +filament_density = 1.41 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?material=199" +first_layer_temperature = 230 +temperature = 230 + +[filament:Extrudr BioFusion] +inherits = *PLA* +filament_vendor = Extrudr +disable_fan_first_layers = 3 +full_fan_speed_layer = 0 +filament_cost = 31.23 +filament_density = 1.25 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?material=121" +first_layer_temperature = 220 +temperature = 220 +max_fan_speed = 45 +min_fan_speed = 25 +slowdown_below_layer_time = 20 +filament_spool_weight = 230 + +[filament:Extrudr Flax] +inherits = *PLA* +filament_vendor = Extrudr +filament_cost = 50.91 +filament_density = 1.45 +filament_notes = "High Performance Filament for decorative parts.\nPrints as easily as PLA with much higher strength and temperature resistance.\nFully biodegradable with a nice matt finish.\n\nhttps://www.extrudr.com/en/products/catalogue/?material=131" +first_layer_temperature = 190 +temperature = 190 +max_fan_speed = 80 +min_fan_speed = 30 +full_fan_speed_layer = 0 +slowdown_below_layer_time = 20 +filament_spool_weight = 262 + +[filament:Extrudr GreenTEC] +inherits = *PLA* +filament_vendor = Extrudr +filament_cost = 50.91 +filament_density = 1.3 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?ignorechildren=1&material=106" +first_layer_temperature = 208 +temperature = 208 +slowdown_below_layer_time = 20 +filament_spool_weight = 262 + +[filament:Extrudr GreenTEC Pro] +inherits = *PLA* +filament_vendor = Extrudr +filament_cost = 56.23 +filament_density = 1.35 +filament_notes = "High Performance Filament for technical parts.\nPrints as easily as PLA with much higher strength and temperature resistance.\nFully biodegradable with a nice matt finish.\n\nhttps://www.extrudr.com/en/products/catalogue/?material=134" +temperature = 215 +max_fan_speed = 80 +min_fan_speed = 30 +full_fan_speed_layer = 0 +slowdown_below_layer_time = 20 +filament_spool_weight = 230 + +[filament:Extrudr GreenTEC Pro Carbon] +inherits = *PLA* +filament_vendor = Extrudr +filament_cost = 62.49 +filament_density = 1.2 +filament_notes = "High Performance Filament for technical parts.\nPrints as easily as PLA with much higher stregnth and temperature resistance.\nFully biodegradable with a nice matt finish.\n\nhttps://www.extrudr.com/en/products/catalogue/?material=138" +first_layer_temperature = 225 +max_fan_speed = 80 +min_fan_speed = 30 +temperature = 225 +full_fan_speed_layer = 0 +slowdown_below_layer_time = 20 +filament_spool_weight = 230 + +[filament:Extrudr PLA NX1] +inherits = *PLA* +filament_vendor = Extrudr +filament_cost = 22.76 +filament_density = 1.24 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?material=97" +temperature = 205 +bed_temperature = 60 +first_layer_temperature = 205 +first_layer_bed_temperature = 60 +full_fan_speed_layer = 0 +max_fan_speed = 90 +min_fan_speed = 30 +slowdown_below_layer_time = 20 +filament_spool_weight = 262 + +[filament:Extrudr PLA NX2] +inherits = Extrudr PLA NX1 +filament_cost = 23.63 +filament_density = 1.3 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?material=128" + +[filament:Extrudr Flex Hard] +inherits = *FLEX* +filament_vendor = Extrudr +disable_fan_first_layers = 1 +extrusion_multiplier = 1.15 +filament_cost = 39.98 +filament_density = 1.2 +filament_max_volumetric_speed = 3 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?material=115" +filament_spool_weight = 230 +slowdown_below_layer_time = 20 + +[filament:Extrudr Flex Medium] +inherits = *FLEX* +filament_vendor = Extrudr +disable_fan_first_layers = 1 +extrusion_multiplier = 1.15 +filament_cost = 39.98 +filament_density = 1.19 +filament_max_volumetric_speed = 3 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?material=117" +filament_spool_weight = 230 +slowdown_below_layer_time = 20 + +[filament:Extrudr Flex SemiSoft] +inherits = *FLEX* +filament_vendor = Extrudr +disable_fan_first_layers = 1 +extrusion_multiplier = 1.15 +filament_cost = 39.98 +filament_density = 1.18 +filament_max_volumetric_speed = 1.8 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?material=116" +filament_spool_weight = 230 +slowdown_below_layer_time = 20 + +[filament:addnorth Adamant S1] +inherits = *FLEX* +filament_vendor = addnorth +disable_fan_first_layers = 3 +extrusion_multiplier = 1 +filament_cost = +filament_density = 1.22 +temperature = 250 +bed_temperature = 50 +first_layer_temperature = 245 +first_layer_bed_temperature = 50 +slowdown_below_layer_time = 20 +min_print_speed = 20 +fan_below_layer_time = 15 +fan_always_on = 1 +cooling = 1 +min_fan_speed = 40 +max_fan_speed = 70 +bridge_fan_speed = 60 +filament_max_volumetric_speed = 1.7 + +[filament:addnorth Adura X] +inherits = *PET* +filament_vendor = addnorth +filament_cost = 29.99 +filament_density = 1.27 +filament_type = PA +extrusion_multiplier = 0.98 +bed_temperature = 105 +first_layer_bed_temperature = 105 +first_layer_temperature = 265 +temperature = 270 +fan_always_on = 0 +min_fan_speed = 20 +max_fan_speed = 40 +bridge_fan_speed = 70 +slowdown_below_layer_time = 10 +min_print_speed = 20 +fan_below_layer_time = 10 +disable_fan_first_layers = 3 +full_fan_speed_layer = 0 + +[filament:addnorth E-PLA] +inherits = *PLA* +filament_vendor = addnorth +filament_cost = 24.99 +filament_density = 1.24 +extrusion_multiplier = 0.98 +temperature = 215 +bed_temperature = 60 +first_layer_temperature = 215 +first_layer_bed_temperature = 60 +full_fan_speed_layer = 3 +slowdown_below_layer_time = 15 +filament_spool_weight = 0 + +[filament:addnorth ESD-PETG] +inherits = *PET* +filament_vendor = addnorth +filament_cost = 29.99 +filament_density = 1.27 +extrusion_multiplier = 1 +bed_temperature = 80 +first_layer_bed_temperature = 85 +first_layer_temperature = 245 +temperature = 265 +fan_always_on = 1 +min_fan_speed = 15 +max_fan_speed = 30 +bridge_fan_speed = 35 +slowdown_below_layer_time = 10 +min_print_speed = 15 +fan_below_layer_time = 8 +disable_fan_first_layers = 3 +full_fan_speed_layer = 0 + +[filament:addnorth OBC Polyethylene] +inherits = *FLEX* +filament_vendor = addnorth +disable_fan_first_layers = 3 +extrusion_multiplier = 1 +filament_cost = 82 +filament_density = 1.22 +temperature = 200 +bed_temperature = 100 +first_layer_temperature = 195 +first_layer_bed_temperature = 100 +slowdown_below_layer_time = 5 +fan_below_layer_time = 15 +fan_always_on = 1 +cooling = 1 +min_fan_speed = 20 +max_fan_speed = 30 +bridge_fan_speed = 40 +min_print_speed = 20 +filament_spool_weight = 0 +filament_notes = "Use Magigoo PP bed adhesive or PP packing tape (on a cold printbed)." + +[filament:addnorth PETG] +inherits = *PET* +filament_vendor = addnorth +filament_cost = 29.99 +filament_density = 1.27 +bed_temperature = 80 +first_layer_bed_temperature = 85 +first_layer_temperature = 240 +temperature = 250 +fan_always_on = 1 +min_fan_speed = 15 +max_fan_speed = 40 +bridge_fan_speed = 50 +slowdown_below_layer_time = 10 +min_print_speed = 15 +fan_below_layer_time = 15 +disable_fan_first_layers = 3 +full_fan_speed_layer = 0 +filament_spool_weight = 0 + +[filament:addnorth Rigid X] +inherits = *PET* +filament_vendor = addnorth +filament_cost = 29.99 +filament_density = 1.27 +extrusion_multiplier = 1 +bed_temperature = 85 +first_layer_bed_temperature = 90 +first_layer_temperature = 250 +temperature = 260 +fan_always_on = 1 +min_fan_speed = 20 +max_fan_speed = 60 +bridge_fan_speed = 70 +slowdown_below_layer_time = 10 +fan_below_layer_time = 20 +min_print_speed = 20 +disable_fan_first_layers = 3 +full_fan_speed_layer = 0 +filament_spool_weight = 0 +filament_notes = "Please use a nozzle that is resistant to abrasive filaments, like hardened steel." + +[filament:addnorth Textura] +inherits = *PLA* +filament_vendor = addnorth +filament_cost = 24.99 +filament_density = 1.24 +extrusion_multiplier = 0.95 +temperature = 215 +bed_temperature = 65 +first_layer_temperature = 215 +first_layer_bed_temperature = 65 +min_fan_speed = 20 +max_fan_speed = 40 +bridge_fan_speed = 60 +full_fan_speed_layer = 0 +slowdown_below_layer_time = 15 +min_print_speed = 20 +filament_spool_weight = 0 + +[filament:Filamentworld ABS] +inherits = *ABSC* +filament_vendor = Filamentworld +filament_cost = 24.9 +filament_density = 1.04 +temperature = 230 +bed_temperature = 95 +first_layer_temperature = 240 +first_layer_bed_temperature = 100 +max_fan_speed = 20 +min_fan_speed = 10 +min_print_speed = 20 +disable_fan_first_layers = 3 +fan_below_layer_time = 60 +slowdown_below_layer_time = 15 +bridge_fan_speed = 20 + +[filament:Filamentworld PETG] +inherits = *PET* +filament_vendor = Filamentworld +filament_cost = 34.9 +filament_density = 1.27 +bed_temperature = 70 +first_layer_bed_temperature = 85 +first_layer_temperature = 240 +temperature = 235 +fan_always_on = 1 +min_fan_speed = 25 +max_fan_speed = 55 +bridge_fan_speed = 55 +slowdown_below_layer_time = 20 +min_print_speed = 20 +fan_below_layer_time = 35 +disable_fan_first_layers = 2 +full_fan_speed_layer = 0 +filament_spool_weight = 0 + +[filament:Filamentworld PLA] +inherits = *PLA* +filament_vendor = Filamentworld +filament_cost = 24.9 +filament_density = 1.24 +temperature = 205 +bed_temperature = 55 +first_layer_temperature = 215 +first_layer_bed_temperature = 60 +full_fan_speed_layer = 3 +slowdown_below_layer_time = 10 +filament_spool_weight = 0 +min_print_speed = 20 + +[filament:Filament PM PETG] +inherits = *PET* +filament_vendor = Filament PM +filament_cost = 27.82 +filament_density = 1.27 +filament_spool_weight = 230 + +[filament:Generic PLA] +inherits = *PLA* +filament_vendor = Generic +filament_cost = 25.4 +filament_density = 1.24 + +[filament:3D-Fuel Standard PLA] +inherits = *PLA* +filament_vendor = 3D-Fuel +filament_cost = 22.14 +filament_density = 1.24 +first_layer_temperature = 210 +temperature = 200 + +[filament:3D-Fuel EasiPrint PLA] +inherits = 3D-Fuel Standard PLA +filament_cost = 30.44 + +[filament:3D-Fuel Pro PLA] +inherits = *PLA* +filament_vendor = 3D-Fuel +filament_cost = 26.57 +filament_density = 1.22 +first_layer_temperature = 220 +temperature = 215 + +[filament:3D-Fuel Buzzed] +inherits = 3D-Fuel Standard PLA +filament_cost = 44.27 +filament_retract_lift = 0 +first_layer_temperature = 210 +temperature = 195 + +[filament:3D-Fuel Wound up] +inherits = 3D-Fuel Buzzed +filament_cost = 44.27 +filament_retract_lift = nil +first_layer_temperature = 215 +temperature = 210 + +[filament:3D-Fuel Workday ABS] +inherits = *ABSC* +filament_vendor = 3D-Fuel +filament_cost = 23.25 +filament_density = 1.04 + +[filament:Jessie PLA] +inherits = *PLA* +filament_vendor = Printed Solid +filament_cost = 21 +filament_density = 1.24 + +[filament:Jessie PETG] +inherits = *PET* +filament_vendor = Printed Solid +filament_cost = 22 +filament_density = 1.27 +first_layer_temperature = 240 +first_layer_bed_temperature = 85 +temperature = 245 +bed_temperature = 90 + +[filament:Devil Design PLA] +inherits = *PLA* +filament_vendor = Devil Design +filament_cost = 20.99 +filament_density = 1.24 +filament_spool_weight = 250 + +[filament:Devil Design PETG] +inherits = *PET* +filament_vendor = Devil Design +filament_cost = 20.99 +filament_density = 1.23 +filament_spool_weight = 250 +first_layer_temperature = 230 +first_layer_bed_temperature = 85 +temperature = 230 +bed_temperature = 90 + +[filament:Spectrum PLA] +inherits = *PLA* +filament_vendor = Spectrum +filament_cost = 21.50 +filament_density = 1.24 + +[filament:Generic FLEX] +inherits = *FLEX* +filament_vendor = Generic +filament_cost = 82 +filament_density = 1.22 +filament_max_volumetric_speed = 1.2 +filament_retract_length = 0 +filament_retract_speed = nil +filament_retract_lift = nil + +[filament:Fillamentum Flexfill 92A] +inherits = *FLEX* +filament_vendor = Fillamentum +filament_cost = 33.99 +filament_density = 1.20 +filament_spool_weight = 230 +filament_max_volumetric_speed = 1.2 +fan_always_on = 1 +cooling = 0 +max_fan_speed = 60 +min_fan_speed = 60 +disable_fan_first_layers = 4 +full_fan_speed_layer = 6 + +[filament:AmazonBasics TPU] +inherits = *FLEX* +filament_vendor = AmazonBasics +fan_always_on = 1 +filament_max_volumetric_speed = 1.8 +extrusion_multiplier = 1.14 +first_layer_temperature = 235 +first_layer_bed_temperature = 50 +temperature = 235 +bed_temperature = 50 +bridge_fan_speed = 100 +max_fan_speed = 80 +min_fan_speed = 80 +filament_retract_before_travel = 3 +filament_cost = 19.99 +filament_density = 1.21 +disable_fan_first_layers = 4 +full_fan_speed_layer = 6 +min_print_speed = 15 +slowdown_below_layer_time = 10 +cooling = 1 + +[filament:SainSmart TPU] +inherits = *FLEX* +filament_vendor = SainSmart +fan_always_on = 1 +filament_max_volumetric_speed = 2.5 +extrusion_multiplier = 1.05 +first_layer_temperature = 230 +first_layer_bed_temperature = 50 +temperature = 230 +bed_temperature = 50 +bridge_fan_speed = 100 +max_fan_speed = 80 +min_fan_speed = 80 +filament_retract_before_travel = 3 +filament_cost = 32.99 +filament_density = 1.21 +disable_fan_first_layers = 4 +full_fan_speed_layer = 6 +min_print_speed = 15 +slowdown_below_layer_time = 10 +cooling = 1 + +[filament:NinjaTek NinjaFlex TPU] +inherits = *FLEX* +filament_vendor = NinjaTek +fan_always_on = 1 +filament_max_volumetric_speed = 1.2 +extrusion_multiplier = 1.15 +first_layer_temperature = 238 +first_layer_bed_temperature = 50 +temperature = 238 +bed_temperature = 50 +bridge_fan_speed = 75 +max_fan_speed = 60 +min_fan_speed = 60 +filament_cost = 85 +filament_density = 1.19 +disable_fan_first_layers = 1 +full_fan_speed_layer = 3 +min_print_speed = 10 +slowdown_below_layer_time = 10 +cooling = 1 + +[filament:NinjaTek Cheetah TPU] +inherits = NinjaTek NinjaFlex TPU +filament_retract_length = 1.5 +filament_density = 1.22 +filament_max_volumetric_speed = 4 +extrusion_multiplier = 1.05 +filament_retract_speed = 45 +filament_deretract_speed = 25 +first_layer_temperature = 240 +temperature = 240 + +[filament:Filatech FilaFlex40] +inherits = *FLEX* +filament_vendor = Filatech +fan_always_on = 1 +filament_max_volumetric_speed = 2.5 +extrusion_multiplier = 1.1 +first_layer_temperature = 230 +first_layer_bed_temperature = 50 +temperature = 230 +bed_temperature = 50 +bridge_fan_speed = 100 +max_fan_speed = 50 +min_fan_speed = 50 +filament_cost = 84.68 +filament_density = 1.22 +disable_fan_first_layers = 4 +full_fan_speed_layer = 6 +min_print_speed = 15 +slowdown_below_layer_time = 10 +cooling = 1 + +[filament:Filatech FilaFlex30] +inherits = Filatech FilaFlex40 +temperature = 225 +filament_density = 1.15 +extrusion_multiplier = 1.1 +filament_cost = + +[filament:Filatech FilaFlex55] +inherits = Filatech FilaFlex40 +temperature = 230 +filament_density = 1.18 +bed_temperature = 60 +fan_always_on = 0 +fan_below_layer_time = 60 +filament_cost = +first_layer_temperature = 235 +extrusion_multiplier = 1 + +[filament:Filatech TPU] +inherits = Filatech FilaFlex40 +first_layer_temperature = 230 +filament_density = 1.2 +fan_below_layer_time = 60 +max_fan_speed = 80 +min_fan_speed = 80 +fan_always_on = 1 +temperature = 235 + +[filament:Filatech ABS] +inherits = *ABSC* +filament_vendor = Filatech +filament_cost = +extrusion_multiplier = 1 +filament_density = 1.05 + +[filament:Filatech FilaCarbon] +inherits = *ABSC* +filament_vendor = Filatech +filament_cost = +extrusion_multiplier = 0.95 +filament_density = 1.1 +first_layer_bed_temperature = 100 +bed_temperature = 100 + +[filament:Filatech FilaPLA] +inherits = *PLA* +filament_vendor = Filatech +filament_cost = +filament_density = 1.3 +first_layer_temperature = 235 +first_layer_bed_temperature = 50 +temperature = 230 +bed_temperature = 55 + +[filament:Filatech PLA] +inherits = *PLA* +filament_vendor = Filatech +filament_cost = +filament_density = 1.25 +first_layer_temperature = 215 +temperature = 210 + +[filament:Filatech PLA+] +inherits = Filatech PLA +filament_density = 1.24 + +[filament:Filatech FilaTough] +inherits = Filatech ABS +filament_cost = +extrusion_multiplier = 0.95 +filament_density = 1.29 +first_layer_temperature = 245 +first_layer_bed_temperature = 80 +temperature = 240 +bed_temperature = 90 +cooling = 0 + +[filament:Filatech HIPS] +inherits = Prusa HIPS +filament_vendor = Filatech +filament_density = 1.07 +filament_spool_weight = +first_layer_temperature = 230 +first_layer_bed_temperature = 100 +temperature = 225 +bed_temperature = 100 + +[filament:Filatech PA] +inherits = *ABSC* +filament_vendor = Filatech +filament_density = 1.1 +first_layer_temperature = 275 +first_layer_bed_temperature = 105 +temperature = 275 +bed_temperature = 105 +fan_always_on = 0 +cooling = 0 +bridge_fan_speed = 25 +filament_type = PA + +[filament:Filatech PC] +inherits = Filatech PA +filament_density = 1.2 +filament_type = PC + +[filament:Filatech PC-ABS] +inherits = Filatech PC +first_layer_temperature = 270 +temperature = 270 +first_layer_bed_temperature = 105 +bed_temperature = 105 +filament_density = 1.08 +filament_type = PC +fan_always_on = 0 +cooling = 1 +extrusion_multiplier = 0.95 +disable_fan_first_layers = 6 + +[filament:Filatech PETG] +inherits = *PET* +filament_vendor = Filatech +filament_cost = +filament_density = 1.27 +first_layer_temperature = 240 +first_layer_bed_temperature = 75 +temperature = 245 +bed_temperature = 80 +extrusion_multiplier = 0.95 +fan_always_on = 0 + +[filament:Filatech Wood-PLA] +inherits = Filatech PLA +filament_density = 1.05 +first_layer_temperature = 210 + +[filament:Ultrafuse PET] +inherits = *PET* +filament_vendor = BASF +filament_density = 1.33 +filament_colour = #F7F7F7 +first_layer_temperature = 220 +first_layer_bed_temperature = 70 +temperature = 215 +bed_temperature = 70 +fan_below_layer_time = 10 +min_fan_speed = 75 +max_fan_speed = 100 +bridge_fan_speed = 100 +filament_type = PET +disable_fan_first_layers = 1 +full_fan_speed_layer = 3 +filament_notes = "Material Description\nUltrafuse PET is made from a premium PET and prints as easy as PLA, but is much stronger. The filament has a large operating window for printing (temperature vs. speed), so it can be used on every 3D-printer. PET will give you outstanding printing results: a good layer adhesion, a high resolution and it is easy to handle. Ultrafuse PET can be 100% recycled, is watertight and has great colors and finish.\n\nPrinting Recommendations:\nUltrafuse PET can be printed directly onto a clean build plate. For challenging prints, use 3dLac to improve adhesion." + +[filament:Ultrafuse PRO1] +inherits = Prusament PLA +filament_vendor = BASF +filament_cost = +filament_density = 1.25 +filament_spool_weight = 0 +filament_colour = #FFFFFF +filament_notes = "Material Description\nPLA PRO1 is an extremely versatile tough PLA filament made for professionals. It reduces your printing time by 30% – 80%, (subject to printer and object limitations) and the strength exceeds overall mechanical properties of printed ABS parts. Printer settings can be tuned to achieve blazing fast speeds or an unrivaled surface finish. The excellent quality control ensures the highest levels of consistency between colors and batches, it will perform as expected, every time.\n\nPrinting Recommendations:\nUltrafuse PLA PRO1 can be printed directly onto a clean build plate." + +[filament:Ultrafuse ABS] +inherits = *ABSC* +filament_vendor = BASF +filament_density = 1.04 +min_fan_speed = 10 +max_fan_speed = 20 +bed_temperature = 100 +disable_fan_first_layers = 3 +filament_colour = #FFFFFF +filament_notes = "Material Description\nABS is the second most used 3D printing material. It is strong, flexible and has a high heat resistance. ABS is a preferred plastic for engineers and professional applications. ABS can be smoothened with acetone. To make a proper 3D print with ABS you will need a heated print bed. The filament is available in 9 colors.\n\nPrinting Recommendations:\n\nApply Tape, adhesion spray or glue to a clean build plate to improve adhesion." + +[filament:Ultrafuse ABS Fusion+] +inherits = Ultrafuse ABS +filament_density = 1.08 +first_layer_bed_temperature = 100 +first_layer_temperature = 270 +temperature = 270 +filament_colour = #FFF8D9 +filament_notes = "Material Description\nABS Fusion+ made with Polyscope XILOY™ 3D is an engineering filament which has been optimized for 3D-printing. This special grade has been developed in collaboration with Polyscope Polymers - renowned for its material solutions in the automotive industry. ABS is a thermoplastic which is used in many applications. Although ABS has been classified as a standard material in 3D-printing it is known to be quite challenging to process. ABS Fusion+ combines the properties of ABS with an improved processability. The filament is based on an ABS grade which can be directly printed on glass without any adhesives or tape and has a higher success rate of prints due to extreme low warping." + +[filament:Ultrafuse ASA] +inherits = Ultrafuse ABS Fusion+ +filament_density = 1.07 +filament_colour = #FFF4CA +first_layer_temperature = 275 +temperature = 275 +first_layer_bed_temperature = 100 +bed_temperature = 100 +filament_type = ASA +min_fan_speed = 25 +max_fan_speed = 50 +bridge_fan_speed = 100 +disable_fan_first_layers = 4 +filament_notes = "Material Description\nUltrafuse ASA is a high-performance thermoplastic with similar mechanical properties as ABS. ASA offers additional benefits such as high outdoor weather resistance. The UV resistance, toughness, and rigidity make it an ideal material to 3D-print outdoor fixtures and appliances without losing its properties or color. When also taking into account the high heat resistance and high chemical resistance, this filament is a good choice for many types of applications.\n\nPrinting Recommendations:\nApply Magigoo PC, 3D lac or Dimafix to a clean build plate to improve adhesion." + +[filament:Ultrafuse HIPS] +inherits = Ultrafuse ABS +temperature = 250 +filament_density = 1.02 +filament_type = HIPS +min_fan_speed = 20 +max_fan_speed = 20 +filament_soluble = 1 +filament_notes = "Material Description\nUltrafuse HIPS is a high-quality engineering thermoplastic, which is well known in the 3D-printing industry as a support material for ABS. But this material has additional properties to offer like good impact resistance, good dimensional stability, and easy post-processing. HiPS is a great material to use as a support for ABS because there is a good compatibility between the two materials, and HIPS is an easy breakaway support. Now you have the opportunity to create ABS models with complex geometry. HIPS is easy to post process with glue or with sanding paper." + +[filament:Ultrafuse PA] +inherits = Fillamentum Nylon FX256 +filament_vendor = BASF +filament_density = 1.12 +filament_colour = #ECFAFF +first_layer_temperature = 240 +temperature = 240 +first_layer_bed_temperature = 80 +bed_temperature = 70 +min_fan_speed = 0 +max_fan_speed = 0 +bridge_fan_speed = 0 +fan_below_layer_time = 30 +slowdown_below_layer_time = 20 +min_print_speed = 15 +filament_notes = "Material Description\nThe key features of Ultrafuse PA are the high strength and high modulus. Furthermore, Ultrafuse PA shows a good thermal distortion stability.\n\nPrinting Recommendations:\nApply PVA glue, Kapton tape or PA adhesive to a clean buildplate to improve adhesion." + +[filament:Ultrafuse PA6 GF30] +inherits = Ultrafuse PA +filament_density = 1.17 +first_layer_temperature = 270 +temperature = 270 +first_layer_bed_temperature = 100 +bed_temperature = 100 +filament_colour = #404040 +fan_always_on = 1 +min_fan_speed = 0 +max_fan_speed = 50 +bridge_fan_speed = 100 +disable_fan_first_layers = 1 +full_fan_speed_layer = 3 +slowdown_below_layer_time = 15 +filament_notes = "Material Description\nUltrafuse® PA6 GF30 is a unique compound specifically developed for FFF printing. Due to the glass fiber content of 30%, parts tend to warp less. In addition the excellent layer adhesion and its compatibility with the water soluble support Ultrafuse® BVOH make this material the perfect solution to develop industrial applications on an FFF printer.\n\nWith its high wear and chemical resistance, high stiffness and strength, Ultrafuse® PA6 GF30 is perfect for a wide variety of applications in automotive, electronics or transportation.\n\nUltrafuse PA6 GF30 is designed for functional prototyping and demanding applications such as industrial tooling, transportation, electronics, small appliances, sports & leisure\n\nPrinting Recommendations:\nThis material contains fibers that have an abrasive effect on printer components. Use a hardened or Ruby nozzle with a diameter of 0.6 or larger for optimal performance and avoid damage to the nozzle.\n\nUltrafuse PA6 GF30 can be printed directly onto a clean build plate. For challenging prints, use Magigoo PA gluestick to improve adhesion." + +[filament:Ultrafuse PAHT-CF15] +inherits = Ultrafuse PA6 GF30 +filament_density = 1.23 +filament_notes = "Material Description\nPAHT CF15 is a high-performance 3D printing filament that opens new application fields in FFF printing. In parallel to its advanced mechanical properties, dimensional stability, and chemical resistance, it has very good processability. It works in any FFF printer with a hardened nozzle. In addition to that, it is compatible with water-soluble support material and HiPS, which allow printing complex geometries that work in challenging environments. PAHT CF15 has high heat resistance up to 130 °C and low moisture absorption.\n\nPrinting Recommendations:\nThis material contains fibers that have an abrasive effect on printer components. Use a hardened or Ruby nozzle with a diameter of 0.6 or larger for optimal performance and avoid damage to the nozzle.\n\nUltrafuse PAHT-CF can be printed directly onto a clean build plate. For challenging prints, use 3dLac to improve adhesion." + +[filament:Ultrafuse PC-ABS-FR] +inherits = Ultrafuse ABS +filament_colour = #505050 +filament_density = 1.17 +first_layer_temperature = 275 +temperature = 275 +first_layer_bed_temperature = 105 +bed_temperature = 105 +filament_type = PC +min_fan_speed = 20 +max_fan_speed = 20 +bridge_fan_speed = 30 +disable_fan_first_layers = 4 +filament_notes = "Material Description\nUltrafuse® PC/ABS FR Black is a V-0 flame retardant blend of Polycarbonate and ABS – two of the most used thermoplastics for engineering & electrical applications. The combination of these two materials results in a premium material with a mix of the excellent mechanical properties of PC and the comparably low printing temperature of ABS. Combined with a halogen free flame retardant, parts printed with Ultrafuse® PC/ABS FR Black feature great tensile and impact strength, higher thermal resistance than ABS and can fulfill the requirements of the UL94 V-0 standard.\n\nPrinting Recommendations:\nApply Magigoo PC to a clean build plate to improve adhesion." + +[filament:Ultrafuse PET-CF15] +inherits = Ultrafuse PET +filament_density = 1.36 +filament_colour = #404040 +first_layer_temperature = 270 +temperature = 270 +first_layer_bed_temperature = 75 +bed_temperature = 75 +min_fan_speed = 60 +max_fan_speed = 100 +bridge_fan_speed = 100 +disable_fan_first_layers = 1 +full_fan_speed_layer = 3 +slowdown_below_layer_time = 15 +fan_below_layer_time = 30 +filament_notes = "Material Description\nPET CF15 is a Carbon Fiber reinforced PET which has precisely tuned material properties, for a wide range of technical applications. The filament is very strong and stiff and has high heat resistance. With its high dimensional stability and low abrasiveness, the filament offers an easy to print experience which allows direct printing on glass or a PEI sheet. It is compatible with HiPS for breakaway support and water soluble support and has an excellent surface finish.\n\nPrinting Recommendations:\nThis material contains fibers that have an abrasive effect on printer components. Use a hardened or Ruby nozzle with a diameter of 0.6 or larger for optimal performance and avoid damage to the nozzle.\n\nUltrafuse PET-CF15 can be printed directly onto a clean build plate. For challenging prints, use 3dLac to improve adhesion." + +[filament:Ultrafuse PLA] +inherits = *PLA* +filament_vendor = BASF +filament_density = 1.25 +full_fan_speed_layer = 3 +filament_notes = "Material Description\nPLA is one of the most used materials for 3D printing. Ultrafuse PLA is available in a wide range of colors. The glossy feel often attracts those who print display models or items for household use. Many appreciate the plant-based origin of this material. When properly cooled, PLA has a high maximum printing speed and sharp printed corners. Combining this with low warping of the print makes it a popular plastic for home printers, hobbyists, prototyping and schools.\n\nPrinting Recommendations:\nUltrafuse PLA can be printed directly onto a clean build plate." + +[filament:Ultrafuse PP] +inherits = Ultrafuse ABS +filament_density = 0.91 +filament_colour = #F0F0F0 +first_layer_temperature = 240 +temperature = 240 +first_layer_bed_temperature = 80 +bed_temperature = 70 +min_fan_speed = 100 +max_fan_speed = 100 +bridge_fan_speed = 100 +disable_fan_first_layers = 1 +full_fan_speed_layer = 3 +fan_below_layer_time = 60 +slowdown_below_layer_time = 20 +min_print_speed = 10 +filament_type = PP +filament_max_volumetric_speed = 2.5 +filament_notes = "Material Description\nUltrafuse PP is high-performance thermoplastic with low density, high elasticity and high resistance to fatigue. The mechanical properties make it an ideal material for 3D-printing applications which have to endure high stress or strain. The filament has high chemical resistance and a high isolation value. PP is one of the most used materials in the world, due to its versatility and ability to engineer lightweight tough parts.\n\nPrinting Recommendations:\nApply PP tape or Magigoo PP adhesive to the buildplate for optimal adhesion." + +[filament:Ultrafuse PP-GF30] +inherits = Ultrafuse PP +filament_density = 1.07 +filament_colour = #404040 +first_layer_temperature = 260 +temperature = 250 +first_layer_bed_temperature = 90 +bed_temperature = 40 +min_fan_speed = 40 +max_fan_speed = 75 +fan_always_on = 1 +fan_below_layer_time = 30 +slowdown_below_layer_time = 15 +min_print_speed = 15 +filament_notes = "Ultrafuse PP GF30 is polypropylene, reinforced with 30% glass fiber content. The fibers in this compound are specially designed for 3D-printing filaments and are compatible with a wide range of standard FFF 3D-printers. The extreme stiffness makes this material highly suitable for demanding applications. Other key properties of PPGF30 are high heat resistance and improved UV-resistance. All these excellent properties make this filament highly suitable in an industrial environment.\n\nPrinting Recommendations:\nThis material contains fibers that have an abrasive effect on printer components. Use a hardened or Ruby nozzle with a diameter of 0.6 or larger for optimal performance and avoid damage to the nozzle.\n\nApply PP strapping tape or PPGF adhesive to a clean build plate for optimal adhesion." + +[filament:Ultrafuse TPC-45D] +inherits = *FLEX* +filament_vendor = BASF +extrusion_multiplier = 1 +filament_density = 1.15 +filament_colour = #0035EC +first_layer_temperature = 235 +temperature = 235 +first_layer_bed_temperature = 60 +bed_temperature = 60 +min_fan_speed = 10 +max_fan_speed = 50 +bridge_fan_speed = 80 +fan_below_layer_time = 30 +slowdown_below_layer_time = 15 +min_print_speed = 15 +fan_always_on = 1 +cooling = 1 +filament_max_volumetric_speed = 1.2 +filament_notes = "Material Description\nTPC 45D is a flexible, shore 45D, rubber-like Thermoplastic Copolyester Elastomer (TPE-C), which is derived from rapeseed oil and combines the best properties of elastomers (rubbers) and polyesters. The material delivers excellent adhesion in the Z-direction, meaning that the printed layers do not detach - even with extreme deformation.\n\nPrinting Recommendations:\nApply Magigoo Flex to a clean build plate to improve adhesion." + +[filament:Ultrafuse TPU-64D] +inherits = Ultrafuse TPC-45D +filament_density = 1.16 +first_layer_temperature = 230 +temperature = 225 +first_layer_bed_temperature = 40 +bed_temperature = 40 +min_fan_speed = 20 +max_fan_speed = 100 +filament_notes = "Material Description\nUltrafuse® TPU 64D is the hardest elastomer in BASF Forward AM’s flexible productline. The material shows a relatively high rigidity while maintaining a certain flexibility. This filament is the perfect match for industrial applications requiring rigid parts being resistant to impact, wear and tear. Due to its property profile, the material can be used as an alternative for parts made from ABS and rubbers. Ultrafuse® TPU 64D is easy to print on direct drive and bowden style printers and is compatible with soluble BVOH support to realize the most complex geometries.\n\nPrinting Recommendations:\nUltrafuse TPU can be printed directly onto a clean build plate. A small amount of 3Dlac can make removal easier after printing." + +[filament:Ultrafuse TPU-85A] +inherits = Ultrafuse TPU-64D +filament_density = 1.11 +first_layer_temperature = 225 +temperature = 220 +filament_notes = "Material Description\nUltrafuse® TPU 85A comes in its natural white color. Chemical properties (e.g. resistance against particular substances) and tolerance for solvents can be made available, if these factors are relevant for a specific application. Generally, these properties correspond to publicly available data on polyether based TPUs. This material is not FDA conform. Good flexibility at low temperature, good wear performance and good damping behavior are the key features of Ultrafuse® TPU 85A.\n\nPrinting Recommendations:\nUltrafuse TPU can be printed directly onto a clean build plate. A small amount of 3Dlac can make removal easier after printing." + +[filament:Ultrafuse TPU-95A] +inherits = Ultrafuse TPU-85A +filament_density = 1.14 +first_layer_temperature = 230 +temperature = 225 +filament_notes = "Material Description\nUltrafuse® TPU 95A comes with a well-balanced profile of flexibility and durability. On top of that, it allows for easier and faster printing then softer TPU grades. Parts printed with Ultrafuse® TPU 95A show a high elongation, good impact resistance, excellent layer adhesion and a good resistance to oils and common industrially used chemicals. Due to its good printing behavior, Ultrafuse® TPU 95A is a good choice for starting printing flexible materials on both direct drive and bowden style printers.\n\nPrinting Recommendations:\nUltrafuse TPU can be printed directly onto a clean build plate. A small amount of 3Dlac can make removal easier after printing." + +[filament:Ultrafuse rPET] +inherits = Ultrafuse PET +filament_density = 1.27 +filament_colour = #9DC5FF +first_layer_temperature = 235 +temperature = 235 +first_layer_bed_temperature = 80 +bed_temperature = 75 +min_fan_speed = 50 +max_fan_speed = 100 +fan_below_layer_time = 15 +filament_notes = "Material Description\nPET is mainly known by the well-known PET bottle material. This recycled has a natural transparent blueish look. It has excellent 3D printing properties and good mechanical characteristics." + +[filament:Ultrafuse Metal] +inherits = *ABSC* +filament_vendor = BASF +filament_density = 4.5 +extrusion_multiplier = 1.08 +first_layer_temperature = 250 +first_layer_bed_temperature = 100 +temperature = 250 +bed_temperature = 100 +min_fan_speed = 0 +max_fan_speed = 0 +bridge_fan_speed = 0 +cooling = 0 +fan_always_on = 0 +filament_max_volumetric_speed = 4 +filament_type = METAL +compatible_printers_condition = nozzle_diameter[0]>=0.4 +filament_colour = #FFFFFF + +[filament:Polymaker PC-Max] +inherits = *ABS* +filament_vendor = Polymaker +filament_cost = 77.3 +filament_density = 1.20 +filament_type = PC +bed_temperature = 115 +filament_colour = #FFF2EC +first_layer_bed_temperature = 100 +first_layer_temperature = 270 +temperature = 270 +bridge_fan_speed = 0 + +[filament:PrimaSelect PVA+] +inherits = *PLA* +filament_vendor = PrimaSelect +filament_cost = 122.1 +filament_density = 1.23 +cooling = 0 +fan_always_on = 0 +filament_colour = #FFFFD7 +filament_soluble = 1 +filament_type = PVA +first_layer_temperature = 195 +temperature = 195 + +[filament:Prusa ABS] +inherits = *ABSC* +filament_vendor = Made for Prusa +filament_cost = 27.82 +filament_density = 1.08 +filament_spool_weight = 230 + +[filament:Prusa HIPS] +inherits = *Generic HIPS* +filament_vendor = Made for Prusa +first_layer_temperature = 220 +temperature = 220 + +[filament:Generic HIPS] +inherits = *ABS* +filament_vendor = Generic +filament_cost = 27.3 +filament_density = 1.04 +bridge_fan_speed = 50 +cooling = 1 +extrusion_multiplier = 1 +fan_always_on = 1 +fan_below_layer_time = 10 +filament_colour = #FFFFD7 +filament_soluble = 1 +filament_type = HIPS +first_layer_temperature = 230 +max_fan_speed = 20 +min_fan_speed = 20 +temperature = 230 +compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_model!="MINI" and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) + +[filament:Prusa PETG] +inherits = *PET* +filament_vendor = Made for Prusa +filament_cost = 27.82 +filament_density = 1.27 +filament_spool_weight = 230 + +[filament:Verbatim PETG] +inherits = *PET* +filament_vendor = Verbatim +filament_cost = 27.90 +filament_density = 1.27 +filament_spool_weight = 235 + +[filament:Prusament PETG] +inherits = *PET* +filament_vendor = Prusa Polymers +first_layer_temperature = 240 +temperature = 250 +filament_cost = 36.29 +filament_density = 1.27 +filament_spool_weight = 201 +filament_type = PETG + +[filament:Prusa PLA] +inherits = *PLA* +filament_vendor = Made for Prusa +filament_cost = 27.82 +filament_density = 1.24 +filament_spool_weight = 230 + +[filament:Eolas Prints PLA] +inherits = *PLA* +filament_vendor = Eolas Prints +filament_cost = 23.50 +filament_density = 1.24 +filament_spool_weight = 0 +filament_colour = #4D9398 +temperature = 208 + +[filament:Eolas Prints PLA Matte] +inherits = Eolas Prints PLA +filament_cost = 25.50 +temperature = 212 + +[filament:Eolas Prints INGEO 850] +inherits = Eolas Prints PLA +filament_cost = 25.90 +temperature = 210 + +[filament:Eolas Prints INGEO 870] +inherits = Eolas Prints PLA +filament_cost = 25.90 +temperature = 215 +first_layer_bed_temperature = 68 +first_layer_temperature = 220 +bed_temperature = 65 + +[filament:Eolas Prints PETG] +inherits = *PET* +filament_vendor = Eolas Prints +filament_cost = 29.90 +filament_density = 1.27 +filament_spool_weight = 0 +filament_colour = #4D9398 +temperature = 240 +first_layer_bed_temperature = 85 +first_layer_temperature = 235 +bed_temperature = 90 + +[filament:Eolas Prints PETG - UV Resistant] +inherits = Eolas Prints PETG +filament_cost = 35.90 +temperature = 237 +first_layer_temperature = 232 + +[filament:Eolas Prints TPU 93A] +inherits = *FLEX* +filament_vendor = Eolas Prints +filament_cost = 34.99 +filament_density = 1.21 +filament_colour = #4D9398 +filament_max_volumetric_speed = 1.2 +temperature = 235 +first_layer_bed_temperature = 30 +bed_temperature = 30 +filament_retract_length = 0 +extrusion_multiplier = 1.16 + +[filament:Fiberlogy Easy PLA] +inherits = *PLA* +filament_vendor = Fiberlogy +filament_cost = 20 +filament_density = 1.24 +first_layer_temperature = 220 +temperature = 220 +filament_spool_weight = 330 + +[filament:Fiberlogy Easy PET-G] +inherits = *PET* +filament_vendor = Fiberlogy +filament_spool_weight = 330 +filament_cost = 20 +filament_density = 1.27 +first_layer_bed_temperature = 80 +bed_temperature = 80 +first_layer_temperature = 235 +temperature = 235 +min_fan_speed = 15 +max_fan_speed = 30 +bridge_fan_speed = 60 +disable_fan_first_layers = 5 +full_fan_speed_layer = 5 +slowdown_below_layer_time = 15 + +[filament:Fiberlogy ASA] +inherits = *ABS* +filament_vendor = Fiberlogy +filament_cost = 33 +filament_density = 1.07 +filament_spool_weight = 330 +fan_always_on = 0 +cooling = 1 +min_fan_speed = 10 +max_fan_speed = 15 +bridge_fan_speed = 30 +min_print_speed = 15 +slowdown_below_layer_time = 15 +first_layer_temperature = 260 +temperature = 260 +first_layer_bed_temperature = 100 +bed_temperature = 100 +filament_type = ASA +fan_below_layer_time = 30 +disable_fan_first_layers = 5 + +[filament:Fiberlogy Easy ABS] +inherits = Fiberlogy ASA +filament_cost = 22.67 +filament_density = 1.09 +fan_always_on = 0 +cooling = 1 +min_fan_speed = 10 +max_fan_speed = 15 +min_print_speed = 15 +slowdown_below_layer_time = 15 +first_layer_temperature = 250 +temperature = 250 +first_layer_bed_temperature = 100 +bed_temperature = 100 +filament_type = ABS +fan_below_layer_time = 25 +disable_fan_first_layers = 5 + +[filament:Fiberlogy CPE HT] +inherits = *PET* +filament_vendor = Fiberlogy +filament_cost = 42.67 +filament_density = 1.18 +extrusion_multiplier = 0.98 +filament_spool_weight = 330 +fan_always_on = 1 +cooling = 1 +min_fan_speed = 0 +max_fan_speed = 0 +bridge_fan_speed = 50 +min_print_speed = 15 +first_layer_temperature = 275 +temperature = 275 +first_layer_bed_temperature = 105 +bed_temperature = 105 +filament_type = CPE +fan_below_layer_time = 20 +slowdown_below_layer_time = 15 +disable_fan_first_layers = 5 + +[filament:Fiberlogy PCTG] +inherits = Fiberlogy CPE HT +filament_cost = 29.41 +filament_density = 1.23 +extrusion_multiplier = 1 +min_fan_speed = 10 +max_fan_speed = 15 +first_layer_temperature = 265 +temperature = 265 +first_layer_bed_temperature = 90 +bed_temperature = 90 +filament_type = PCTG + +[filament:Fiberlogy FiberFlex 40D] +inherits = *FLEX* +filament_vendor = Fiberlogy +fan_always_on = 1 +filament_max_volumetric_speed = 1.5 +extrusion_multiplier = 1.12 +first_layer_temperature = 230 +first_layer_bed_temperature = 60 +temperature = 230 +bed_temperature = 60 +bridge_fan_speed = 75 +min_fan_speed = 25 +max_fan_speed = 75 +filament_cost = 39.41 +filament_density = 1.16 +disable_fan_first_layers = 5 +full_fan_speed_layer = 5 +min_print_speed = 15 +cooling = 1 +filament_spool_weight = 330 + +[filament:Fiberlogy MattFlex 40D] +inherits = Fiberlogy FiberFlex 40D +filament_vendor = Fiberlogy +fan_always_on = 1 +filament_max_volumetric_speed = 1.35 +extrusion_multiplier = 1.1 +filament_cost = 49.11 + +[filament:Fiberlogy FiberFlex 30D] +inherits = Fiberlogy FiberFlex 40D +filament_max_volumetric_speed = 1.2 +extrusion_multiplier = 1.15 +first_layer_temperature = 240 +temperature = 240 +min_fan_speed = 25 +max_fan_speed = 60 +filament_density = 1.07 + +[filament:Fiberlogy FiberSatin] +inherits = Fiberlogy Easy PLA +first_layer_temperature = 215 +temperature = 215 +extrusion_multiplier = 1 +filament_density = 1.2 +filament_cost = 32.35 + +[filament:Fiberlogy FiberSilk] +inherits = Fiberlogy FiberSatin +first_layer_temperature = 230 +temperature = 230 +extrusion_multiplier = 0.97 +filament_density = 1.22 +filament_cost = 32.35 + +[filament:Fiberlogy FiberWood] +inherits = Fiberlogy Easy PLA +first_layer_temperature = 185 +temperature = 185 +extrusion_multiplier = 1 +filament_density = 1.23 +filament_cost = 38.66 + +[filament:Fiberlogy HD PLA] +inherits = Fiberlogy Easy PLA +first_layer_temperature = 230 +temperature = 230 +extrusion_multiplier = 1 +filament_density = 1.24 +filament_cost = 30.59 + +[filament:Fiberlogy PLA Mineral] +inherits = Fiberlogy Easy PLA +first_layer_temperature = 195 +temperature = 190 +extrusion_multiplier = 0.98 +filament_density = 1.38 +filament_cost = 37.64 + +[filament:Fiberlogy Impact PLA] +inherits = Fiberlogy HD PLA +filament_density = 1.22 +filament_cost = 27.65 + +[filament:Fiberlogy Nylon PA12] +inherits = Fiberlogy ASA +filament_type = PA +filament_density = 1.01 +filament_cost = 48 +first_layer_bed_temperature = 105 +bed_temperature = 105 +first_layer_temperature = 265 +temperature = 265 +min_fan_speed = 10 +max_fan_speed = 15 +fan_below_layer_time = 20 +bridge_fan_speed = 30 +fan_always_on = 0 + +[filament:Fiberlogy Nylon PA12+CF15] +inherits = Fiberlogy Nylon PA12 +extrusion_multiplier = 0.97 +filament_density = 1.07 +filament_cost = 87.5 +first_layer_bed_temperature = 105 +bed_temperature = 105 +first_layer_temperature = 265 +temperature = 265 +min_fan_speed = 10 +max_fan_speed = 15 +fan_below_layer_time = 20 +bridge_fan_speed = 30 +fan_always_on = 0 + +[filament:Fiberlogy Nylon PA12+GF15] +inherits = Fiberlogy Nylon PA12+CF15 +filament_density = 1.13 + +[filament:Fiberlogy PP] +inherits = *ABS* +filament_vendor = Fiberlogy +filament_cost = 36.67 +filament_density = 1.05 +extrusion_multiplier = 1.05 +filament_spool_weight = 330 +fan_always_on = 1 +cooling = 1 +min_fan_speed = 0 +max_fan_speed = 25 +bridge_fan_speed = 70 +min_print_speed = 15 +slowdown_below_layer_time = 15 +first_layer_temperature = 245 +temperature = 245 +first_layer_bed_temperature = 0 +bed_temperature = 0 +filament_type = PP +fan_below_layer_time = 100 +disable_fan_first_layers = 5 +filament_max_volumetric_speed = 5 + +[filament:Filament PM PLA] +inherits = *PLA* +filament_vendor = Filament PM +filament_cost = 27.82 +filament_density = 1.24 +filament_spool_weight = 230 + +[filament:AmazonBasics PLA] +inherits = *PLA* +filament_vendor = AmazonBasics +filament_cost = 25.4 +filament_density = 1.24 + +[filament:Overture PLA] +inherits = *PLA* +filament_vendor = Overture +filament_cost = 22 +filament_density = 1.24 +filament_spool_weight = 235 + +[filament:Hatchbox PLA] +inherits = *PLA* +filament_vendor = Hatchbox +filament_cost = 25.4 +filament_density = 1.27 +filament_spool_weight = 245 + +[filament:Esun PLA] +inherits = *PLA* +filament_vendor = Esun +filament_cost = 25.4 +filament_density = 1.24 +filament_spool_weight = 265 + +[filament:Das Filament PLA] +inherits = *PLA* +filament_vendor = Das Filament +filament_cost = 25.4 +filament_density = 1.24 + +[filament:EUMAKERS PLA] +inherits = *PLA* +filament_vendor = EUMAKERS +filament_cost = 25.4 +filament_density = 1.24 + +[filament:Floreon3D PLA] +inherits = *PLA* +filament_vendor = Floreon3D +filament_cost = 25.4 +filament_density = 1.24 + +[filament:Prusament PLA] +inherits = *PLA* +filament_vendor = Prusa Polymers +temperature = 215 +filament_cost = 36.29 +filament_density = 1.24 +filament_spool_weight = 201 +filament_notes = "Affordable filament for everyday printing in premium quality manufactured in-house by Josef Prusa" + +[filament:Prusament PVB] +inherits = *PLA* +filament_vendor = Prusa Polymers +temperature = 215 +bed_temperature = 75 +first_layer_bed_temperature = 75 +filament_cost = 60.48 +filament_density = 1.09 +filament_spool_weight = 201 +filament_max_volumetric_speed = 8 +filament_type = PVB +filament_soluble = 1 +filament_colour = #FFFF6F +slowdown_below_layer_time = 20 + +[filament:Fillamentum Flexfill 98A] +inherits = *FLEX* +filament_vendor = Fillamentum +filament_cost = 82.26 +filament_density = 1.23 +filament_spool_weight = 230 +extrusion_multiplier = 1.1 +filament_max_volumetric_speed = 1.5 +fan_always_on = 1 +cooling = 0 +max_fan_speed = 60 +min_fan_speed = 60 +disable_fan_first_layers = 4 +full_fan_speed_layer = 6 + +[filament:ColorFabb VarioShore TPU] +inherits = Fillamentum Flexfill 98A +filament_vendor = ColorFabb +filament_colour = #BBBBBB +filament_cost = 71.35 +filament_density = 1.22 +filament_spool_weight = 0 +extrusion_multiplier = 0.85 +first_layer_temperature = 220 +temperature = 220 + +[filament:Taulman Bridge] +inherits = *common* +filament_vendor = Taulman +filament_cost = 40 +filament_density = 1.13 +bed_temperature = 110 +bridge_fan_speed = 40 +cooling = 0 +disable_fan_first_layers = 3 +fan_always_on = 0 +fan_below_layer_time = 20 +filament_colour = #DEE0E6 +filament_soluble = 0 +filament_type = PA +first_layer_bed_temperature = 90 +first_layer_temperature = 260 +temperature = 260 +max_fan_speed = 0 +min_fan_speed = 0 +filament_max_volumetric_speed = 6 + +[filament:Fillamentum Nylon FX256] +inherits = *common* +filament_vendor = Fillamentum +filament_cost = 56.99 +filament_density = 1.01 +filament_spool_weight = 230 +bed_temperature = 90 +bridge_fan_speed = 30 +cooling = 1 +disable_fan_first_layers = 6 +fan_always_on = 0 +fan_below_layer_time = 20 +min_print_speed = 15 +slowdown_below_layer_time = 20 +filament_colour = #DEE0E6 +filament_max_volumetric_speed = 6 +filament_soluble = 0 +filament_type = PA +first_layer_bed_temperature = 90 +first_layer_temperature = 250 +max_fan_speed = 0 +min_fan_speed = 0 +temperature = 250 + +[filament:Fiberthree F3 PA Pure Pro] +inherits = *common* +filament_vendor = Fiberthree +filament_cost = 200.84 +filament_density = 1.2 +bed_temperature = 90 +first_layer_bed_temperature = 90 +first_layer_temperature = 285 +temperature = 285 +bridge_fan_speed = 30 +cooling = 1 +disable_fan_first_layers = 3 +fan_always_on = 1 +fan_below_layer_time = 20 +min_print_speed = 15 +slowdown_below_layer_time = 10 +filament_colour = #DEE0E6 +filament_max_volumetric_speed = 5 +filament_soluble = 0 +filament_type = PA +max_fan_speed = 20 +min_fan_speed = 20 + +[filament:Fiberthree F3 PA-CF Pro] +inherits = *common* +filament_vendor = Fiberthree +filament_cost = 208.1 +filament_density = 1.25 +bed_temperature = 90 +first_layer_bed_temperature = 90 +first_layer_temperature = 285 +temperature = 285 +bridge_fan_speed = 30 +cooling = 1 +disable_fan_first_layers = 3 +fan_always_on = 0 +fan_below_layer_time = 20 +min_print_speed = 15 +slowdown_below_layer_time = 10 +filament_colour = #DEE0E6 +filament_max_volumetric_speed = 5 +filament_soluble = 0 +filament_type = PA +max_fan_speed = 0 +min_fan_speed = 0 + +[filament:Fiberthree F3 PA-GF Pro] +inherits = Fiberthree F3 PA-CF Pro +filament_vendor = Fiberthree +filament_cost = 205.68 +filament_density = 1.27 +fan_always_on = 1 +max_fan_speed = 15 +min_fan_speed = 15 + +[filament:Taulman T-Glase] +inherits = *PET* +filament_vendor = Taulman +filament_cost = 40 +filament_density = 1.27 +bridge_fan_speed = 40 +cooling = 0 +fan_always_on = 0 +first_layer_bed_temperature = 90 +first_layer_temperature = 240 +max_fan_speed = 5 +min_fan_speed = 0 + +[filament:Verbatim PLA] +inherits = *PLA* +filament_vendor = Verbatim +filament_cost = 42.99 +filament_density = 1.24 +filament_spool_weight = 235 + +[filament:Verbatim BVOH] +inherits = *common* +filament_vendor = Verbatim +filament_cost = 193.58 +filament_density = 1.14 +filament_spool_weight = 235 +bed_temperature = 60 +bridge_fan_speed = 100 +cooling = 0 +disable_fan_first_layers = 1 +extrusion_multiplier = 1 +fan_always_on = 0 +fan_below_layer_time = 100 +filament_colour = #FFFFD7 +filament_soluble = 1 +filament_type = PVA +first_layer_bed_temperature = 60 +first_layer_temperature = 215 +max_fan_speed = 100 +min_fan_speed = 100 +temperature = 210 + +[filament:Verbatim PP] +inherits = *common* +filament_vendor = Verbatim +filament_cost = 72 +filament_density = 0.89 +filament_spool_weight = 235 +bed_temperature = 100 +bridge_fan_speed = 100 +cooling = 1 +disable_fan_first_layers = 2 +extrusion_multiplier = 1 +fan_always_on = 1 +fan_below_layer_time = 100 +filament_colour = #DEE0E6 +filament_max_volumetric_speed = 5 +filament_type = PP +first_layer_bed_temperature = 100 +first_layer_temperature = 220 +max_fan_speed = 100 +min_fan_speed = 100 +temperature = 220 + +[filament:FormFutura Centaur PP] +inherits = *common* +filament_vendor = FormFutura +filament_cost = 70 +filament_density = 0.89 +filament_spool_weight = 212 +bridge_fan_speed = 100 +cooling = 1 +disable_fan_first_layers = 2 +extrusion_multiplier = 1.05 +fan_always_on = 1 +fan_below_layer_time = 100 +filament_colour = #DEE0E6 +filament_max_volumetric_speed = 4 +filament_type = PP +first_layer_bed_temperature = 85 +bed_temperature = 85 +first_layer_temperature = 235 +max_fan_speed = 70 +min_fan_speed = 70 +temperature = 235 +filament_wipe = 0 +filament_retract_lift = 0 \ No newline at end of file diff --git a/src/libslic3r/AABBMesh.hpp b/src/libslic3r/AABBMesh.hpp index 337935839d..3ef25977b4 100644 --- a/src/libslic3r/AABBMesh.hpp +++ b/src/libslic3r/AABBMesh.hpp @@ -67,9 +67,9 @@ public: double m_t = infty(); int m_face_id = -1; const AABBMesh *m_mesh = nullptr; - Vec3d m_dir; - Vec3d m_source; - Vec3d m_normal; + Vec3d m_dir = Vec3d::Zero(); + Vec3d m_source = Vec3d::Zero(); + Vec3d m_normal = Vec3d::Zero(); friend class AABBMesh; // A valid object of this class can only be obtained from diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 8c879cde05..0d9888d404 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -36,6 +36,10 @@ static const std::string MODEL_PREFIX = "model:"; // are phased out, then we will revert to the original name. //static const std::string VERSION_CHECK_URL = "https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/PrusaSlicer.version"; static const std::string VERSION_CHECK_URL = "https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/PrusaSlicer.version2"; +// Url to index archive zip that contains latest indicies +static const std::string INDEX_ARCHIVE_URL= "https://files.prusa3d.com/wp-content/uploads/repository/vendor_indices.zip"; +// Url to folder with vendor profile files. Used when downloading new profiles that are not in resources folder. +static const std::string PROFILE_FOLDER_URL = "https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/"; const std::string AppConfig::SECTION_FILAMENTS = "filaments"; const std::string AppConfig::SECTION_MATERIALS = "sla_materials"; @@ -68,6 +72,8 @@ void AppConfig::set_defaults() // If set, the "- default -" selections of print/filament/printer are suppressed, if there is a valid preset available. if (get("no_defaults").empty()) set("no_defaults", "1"); + if (get("no_templates").empty()) + set("no_templates", "0"); if (get("show_incompatible_presets").empty()) set("show_incompatible_presets", "0"); @@ -665,6 +671,26 @@ std::string AppConfig::version_check_url() const return from_settings.empty() ? VERSION_CHECK_URL : from_settings; } +std::string AppConfig::index_archive_url() const +{ +#if 0 + // this code is for debug & testing purposes only - changed url wont get trough inner checks anyway. + auto from_settings = get("index_archive_url"); + return from_settings.empty() ? INDEX_ARCHIVE_URL : from_settings; +#endif + return INDEX_ARCHIVE_URL; +} + +std::string AppConfig::profile_folder_url() const +{ +#if 0 + // this code is for debug & testing purposes only - changed url wont get trough inner checks anyway. + auto from_settings = get("profile_folder_url"); + return from_settings.empty() ? PROFILE_FOLDER_URL : from_settings; +#endif + return PROFILE_FOLDER_URL; +} + bool AppConfig::exists() { return boost::filesystem::exists(config_path()); diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index 5658f142d5..78a7065d90 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -139,6 +139,11 @@ public: // Get the Slic3r version check url. // This returns a hardcoded string unless it is overriden by "version_check_url" in the ini file. std::string version_check_url() const; + // Get the Slic3r url to vendor index archive zip. + std::string index_archive_url() const; + // Get the Slic3r url to folder with vendor profile files. + std::string profile_folder_url() const; + // Returns the original Slic3r version found in the ini file before it was overwritten // by the current version diff --git a/src/libslic3r/BranchingTree/BranchingTree.cpp b/src/libslic3r/BranchingTree/BranchingTree.cpp index 6463a30de9..98261311b1 100644 --- a/src/libslic3r/BranchingTree/BranchingTree.cpp +++ b/src/libslic3r/BranchingTree/BranchingTree.cpp @@ -5,7 +5,7 @@ #include #include -#include "libslic3r/SLA/SupportTreeUtils.hpp" +#include "libslic3r/TriangleMesh.hpp" namespace Slic3r { namespace branchingtree { @@ -76,18 +76,20 @@ void build_tree(PointCloud &nodes, Builder &builder) switch (type) { case BED: { closest_node.weight = w; - if (closest_it->dst_branching > nodes.properties().max_branch_length()) { - auto hl_br_len = float(nodes.properties().max_branch_length()) / 2.f; - Node new_node {{node.pos.x(), node.pos.y(), node.pos.z() - hl_br_len}, node.Rmin}; - new_node.id = int(nodes.next_junction_id()); - new_node.weight = nodes.get(node_id).weight + hl_br_len; - new_node.left = node.id; + double max_br_len = nodes.properties().max_branch_length(); + if (closest_it->dst_branching > max_br_len) { + std::optional avo = builder.suggest_avoidance(node, max_br_len); + if (!avo) + break; + + Node new_node {*avo, node.Rmin}; + new_node.weight = nodes.get(node_id).weight + (node.pos - *avo).norm(); + new_node.left = node.id; if ((routed = builder.add_bridge(node, new_node))) { size_t new_idx = nodes.insert_junction(new_node); ptsqueue.push(new_idx); } - } - else if ((routed = builder.add_ground_bridge(node, closest_node))) { + } else if ((routed = builder.add_ground_bridge(node, closest_node))) { closest_node.left = closest_node.right = node_id; nodes.get(closest_node_id) = closest_node; nodes.mark_unreachable(closest_node_id); diff --git a/src/libslic3r/BranchingTree/BranchingTree.hpp b/src/libslic3r/BranchingTree/BranchingTree.hpp index c88410b3ab..7e59e6f1ae 100644 --- a/src/libslic3r/BranchingTree/BranchingTree.hpp +++ b/src/libslic3r/BranchingTree/BranchingTree.hpp @@ -5,7 +5,6 @@ #include #include "libslic3r/ExPolygon.hpp" -#include "libslic3r/BoundingBox.hpp" namespace Slic3r { namespace branchingtree { @@ -21,6 +20,7 @@ class Properties ExPolygons m_bed_shape; public: + // Maximum slope for bridges of the tree Properties &max_slope(double val) noexcept { @@ -77,6 +77,11 @@ struct Node {} }; +inline bool is_occupied(const Node &n) +{ + return n.left != Node::ID_NONE && n.right != Node::ID_NONE; +} + // An output interface for the branching tree generator function. Consider each // method as a callback and implement the actions that need to be done. class Builder @@ -100,6 +105,12 @@ public: // Add an anchor bridge to the model body virtual bool add_mesh_bridge(const Node &from, const Node &to) = 0; + virtual std::optional suggest_avoidance(const Node &from, + float max_bridge_len) const + { + return {}; + } + // Report nodes that can not be routed to an endpoint (model or ground) virtual void report_unroutable(const Node &j) = 0; diff --git a/src/libslic3r/BranchingTree/PointCloud.cpp b/src/libslic3r/BranchingTree/PointCloud.cpp index f1d7ae5212..1497f08945 100644 --- a/src/libslic3r/BranchingTree/PointCloud.cpp +++ b/src/libslic3r/BranchingTree/PointCloud.cpp @@ -1,82 +1,15 @@ #include "PointCloud.hpp" -#include "libslic3r/Geometry.hpp" #include "libslic3r/Tesselate.hpp" +#include "libslic3r/SLA/SupportTreeUtils.hpp" #include namespace Slic3r { namespace branchingtree { -std::optional find_merge_pt(const Vec3f &A, - const Vec3f &B, - float critical_angle) +std::optional find_merge_pt(const Vec3f &A, const Vec3f &B, float max_slope) { - // The idea is that A and B both have their support cones. But searching - // for the intersection of these support cones is difficult and its enough - // to reduce this problem to 2D and search for the intersection of two - // rays that merge somewhere between A and B. The 2D plane is a vertical - // slice of the 3D scene where the 2D Y axis is equal to the 3D Z axis and - // the 2D X axis is determined by the XY direction of the AB vector. - // - // Z^ - // | A * - // | . . B * - // | . . . . - // | . . . . - // | . x . - // -------------------> XY - - // Determine the transformation matrix for the 2D projection: - Vec3f diff = {B.x() - A.x(), B.y() - A.y(), 0.f}; - Vec3f dir = diff.normalized(); // TODO: avoid normalization - - Eigen::Matrix tr2D; - tr2D.row(0) = Vec3f{dir.x(), dir.y(), dir.z()}; - tr2D.row(1) = Vec3f{0.f, 0.f, 1.f}; - - // Transform the 2 vectors A and B into 2D vector 'a' and 'b'. Here we can - // omit 'a', pretend that its the origin and use BA as the vector b. - Vec2f b = tr2D * (B - A); - - // Get the square sine of the ray emanating from 'a' towards 'b'. This ray might - // exceed the allowed angle but that is corrected subsequently. - // The sign of the original sine is also needed, hence b.y is multiplied by - // abs(b.y) - float b_sqn = b.squaredNorm(); - float sin2sig_a = b_sqn > EPSILON ? (b.y() * std::abs(b.y())) / b_sqn : 0.f; - - // sine2 from 'b' to 'a' is the opposite of sine2 from a to b - float sin2sig_b = -sin2sig_a; - - // Derive the allowed angles from the given critical angle. - // critical_angle is measured from the horizontal X axis. - // The rays need to go downwards which corresponds to negative angles - - float sincrit = std::sin(critical_angle); // sine of the critical angle - float sin2crit = -sincrit * sincrit; // signed sine squared - sin2sig_a = std::min(sin2sig_a, sin2crit); // Do the angle saturation of both rays - sin2sig_b = std::min(sin2sig_b, sin2crit); // - float sin2_a = std::abs(sin2sig_a); // Get cosine squared values - float sin2_b = std::abs(sin2sig_b); - float cos2_a = 1.f - sin2_a; - float cos2_b = 1.f - sin2_b; - - // Derive the new direction vectors. This is by square rooting the sin2 - // and cos2 values and restoring the original signs - Vec2f Da = {std::copysign(std::sqrt(cos2_a), b.x()), std::copysign(std::sqrt(sin2_a), sin2sig_a)}; - Vec2f Db = {-std::copysign(std::sqrt(cos2_b), b.x()), std::copysign(std::sqrt(sin2_b), sin2sig_b)}; - - // Determine where two rays ([0, 0], Da), (b, Db) intersect. - // Based on - // https://stackoverflow.com/questions/27459080/given-two-points-and-two-direction-vectors-find-the-point-where-they-intersect - // One ray is emanating from (0, 0) so the formula is simplified - double t1 = (Db.y() * b.x() - b.y() * Db.x()) / - (Da.x() * Db.y() - Da.y() * Db.x()); - - Vec2f mp = t1 * Da; - Vec3f Mp = A + tr2D.transpose() * mp; - - return t1 >= 0.f ? Mp : Vec3f{}; + return sla::find_merge_pt(A, B, max_slope); } void to_eigen_mesh(const indexed_triangle_set &its, @@ -141,8 +74,6 @@ std::vector sample_mesh(const indexed_triangle_set &its, double radius) std::vector sample_bed(const ExPolygons &bed, float z, double radius) { - std::vector ret; - auto triangles = triangulate_expolygons_3d(bed, z); indexed_triangle_set its; its.vertices.reserve(triangles.size()); @@ -198,7 +129,10 @@ PointCloud::PointCloud(std::vector meshpts, for (size_t i = 0; i < m_leafs.size(); ++i) { Node &n = m_leafs[i]; - n.id = int(LEAFS_BEGIN + i); + n.id = int(LEAFS_BEGIN + i); + n.left = Node::ID_NONE; + n.right = Node::ID_NONE; + m_ktree.insert({n.pos, n.id}); } } diff --git a/src/libslic3r/BranchingTree/PointCloud.hpp b/src/libslic3r/BranchingTree/PointCloud.hpp index f99b179902..03b935f765 100644 --- a/src/libslic3r/BranchingTree/PointCloud.hpp +++ b/src/libslic3r/BranchingTree/PointCloud.hpp @@ -5,7 +5,7 @@ #include "BranchingTree.hpp" -#include "libslic3r/Execution/Execution.hpp" +//#include "libslic3r/Execution/Execution.hpp" #include "libslic3r/MutablePriorityQueue.hpp" #include "libslic3r/BoostAdapter.hpp" @@ -78,14 +78,6 @@ private: rtree /* ? */> m_ktree; - bool is_outside_support_cone(const Vec3f &supp, const Vec3f &pt) const - { - Vec3d D = (pt - supp).cast(); - double dot_sq = -D.z() * std::abs(-D.z()); - - return dot_sq < D.squaredNorm() * cos2bridge_slope; - } - template static auto *get_node(PC &&pc, size_t id) { @@ -104,6 +96,14 @@ private: public: + bool is_outside_support_cone(const Vec3f &supp, const Vec3f &pt) const + { + Vec3d D = (pt - supp).cast(); + double dot_sq = -D.z() * std::abs(-D.z()); + + return dot_sq < D.squaredNorm() * cos2bridge_slope; + } + static constexpr auto Unqueued = size_t(-1); struct ZCompareFn @@ -255,18 +255,42 @@ public: } }; +template constexpr bool IsTraverseFn = std::is_invocable_v; + +struct TraverseReturnT +{ + bool to_left : 1; // if true, continue traversing to the left + bool to_right : 1; // if true, continue traversing to the right +}; + template void traverse(PC &&pc, size_t root, Fn &&fn) { if (auto nodeptr = pc.find(root); nodeptr != nullptr) { auto &nroot = *nodeptr; - fn(nroot); - if (nroot.left >= 0) traverse(pc, nroot.left, fn); - if (nroot.right >= 0) traverse(pc, nroot.right, fn); + TraverseReturnT ret{true, true}; + + if constexpr (std::is_same_v, void>) { + // Our fn has no return value + fn(nroot); + } else { + // Fn returns instructions about how to continue traversing + ret = fn(nroot); + } + + if (ret.to_left && nroot.left >= 0) + traverse(pc, nroot.left, fn); + if (ret.to_right && nroot.right >= 0) + traverse(pc, nroot.right, fn); } } void build_tree(PointCloud &pcloud, Builder &builder); +inline void build_tree(PointCloud &&pc, Builder &builder) +{ + build_tree(pc, builder); +} + }} // namespace Slic3r::branchingtree #endif // POINTCLOUD_HPP diff --git a/src/libslic3r/BridgeDetector.hpp b/src/libslic3r/BridgeDetector.hpp index b11736417e..bc5da9712f 100644 --- a/src/libslic3r/BridgeDetector.hpp +++ b/src/libslic3r/BridgeDetector.hpp @@ -82,12 +82,11 @@ inline std::tuple detect_bridging_direction(const Polygons &to_co if (floating_polylines.empty()) { // consider this area anchored from all sides, pick bridging direction that will likely yield shortest bridges - //use 3mm resolution (should be quite fast, and rough estimation should not cause any problems here) - auto [pc1, pc2] = compute_principal_components(overhang_area, 3.0); - if (pc2 == Vec2d::Zero()) { // overhang may be smaller than resolution. In this case, any direction is ok + auto [pc1, pc2] = compute_principal_components(overhang_area); + if (pc2 == Vec2f::Zero()) { // overhang may be smaller than resolution. In this case, any direction is ok return {Vec2d{1.0,0.0}, 0.0}; } else { - return {pc2.normalized(), 0.0}; + return {pc2.normalized().cast(), 0.0}; } } diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 4bba9ae563..09a7ed65d2 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -331,6 +331,7 @@ set(SLIC3R_SOURCES SLA/SupportTreeMesher.hpp SLA/SupportTreeMesher.cpp SLA/SupportTreeUtils.hpp + SLA/SupportTreeUtilsLegacy.hpp SLA/SupportTreeBuilder.cpp SLA/SupportTree.hpp SLA/SupportTree.cpp diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index 01c551fb63..e0e3bdc5dc 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -445,8 +445,11 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: f->angle = surface_fill.params.angle; f->adapt_fill_octree = (surface_fill.params.pattern == ipSupportCubic) ? support_fill_octree : adaptive_fill_octree; - if (surface_fill.params.pattern == ipLightning) - dynamic_cast(f.get())->generator = lightning_generator; + if (surface_fill.params.pattern == ipLightning) { + auto *lf = dynamic_cast(f.get()); + lf->generator = lightning_generator; + lf->num_raft_layers = this->object()->slicing_parameters().raft_layers(); + } if (perimeter_generator.value == PerimeterGeneratorType::Arachne && surface_fill.params.pattern == ipConcentric) { FillConcentric *fill_concentric = dynamic_cast(f.get()); diff --git a/src/libslic3r/Fill/FillLightning.cpp b/src/libslic3r/Fill/FillLightning.cpp index 36a48e5547..8c3ac8f1ac 100644 --- a/src/libslic3r/Fill/FillLightning.cpp +++ b/src/libslic3r/Fill/FillLightning.cpp @@ -13,7 +13,7 @@ void Filler::_fill_surface_single( ExPolygon expolygon, Polylines &polylines_out) { - const Layer &layer = generator->getTreesForLayer(this->layer_id); + const Layer &layer = generator->getTreesForLayer(this->layer_id - this->num_raft_layers); Polylines fill_lines = layer.convertToLines(to_polygons(expolygon), scaled(0.5 * this->spacing - this->overlap)); if (params.dont_connect() || fill_lines.size() <= 1) { diff --git a/src/libslic3r/Fill/FillLightning.hpp b/src/libslic3r/Fill/FillLightning.hpp index 341399508f..0705dc657f 100644 --- a/src/libslic3r/Fill/FillLightning.hpp +++ b/src/libslic3r/Fill/FillLightning.hpp @@ -22,6 +22,7 @@ public: ~Filler() override = default; Generator *generator { nullptr }; + size_t num_raft_layers { 0 }; protected: Fill* clone() const override { return new Filler(*this); } diff --git a/src/libslic3r/Layer.cpp b/src/libslic3r/Layer.cpp index 44fc18f917..9b0004e989 100644 --- a/src/libslic3r/Layer.cpp +++ b/src/libslic3r/Layer.cpp @@ -577,12 +577,22 @@ void Layer::sort_perimeters_into_islands( } if (! sample_set) { // If there is no infill, take a sample of some inner perimeter. - for (uint32_t iperimeter : extrusions.first) - if (const ExtrusionEntity &ee = *this_layer_region.perimeters().entities[iperimeter]; ! ee.role().is_external()) { - sample = ee.first_point(); + for (uint32_t iperimeter : extrusions.first) { + const ExtrusionEntity &ee = *this_layer_region.perimeters().entities[iperimeter]; + if (ee.is_collection()) { + for (const ExtrusionEntity *ee2 : dynamic_cast(ee).entities) + if (! ee2->role().is_external()) { + sample = ee2->first_point(); + sample_set = true; + goto loop_end; + } + } else if (! ee.role().is_external()) { + sample = ee.first_point(); sample_set = true; break; } + } + loop_end: if (! sample_set) { if (! extrusions.second.empty()) { // If there is no inner perimeter, take a sample of some gap fill extrusion. @@ -752,7 +762,9 @@ void Layer::sort_perimeters_into_islands( const PrintRegionConfig ®ion_config = this_layer_region.region().config(); const auto bbox_eps = scaled( EPSILON + print_config.gcode_resolution.value + - (region_config.fuzzy_skin.value == FuzzySkinType::None ? 0. : region_config.fuzzy_skin_thickness.value)); + (region_config.fuzzy_skin.value == FuzzySkinType::None ? 0. : region_config.fuzzy_skin_thickness.value + //FIXME it looks as if Arachne could extend open lines by fuzzy_skin_point_dist, which does not seem right. + + region_config.fuzzy_skin_point_dist.value)); auto point_inside_surface_dist2 = [&lslices = this->lslices, &lslices_ex = this->lslices_ex, bbox_eps] (const size_t lslice_idx, const Point &point) { diff --git a/src/libslic3r/Optimize/BruteforceOptimizer.hpp b/src/libslic3r/Optimize/BruteforceOptimizer.hpp index 2daef538e7..2f6b422245 100644 --- a/src/libslic3r/Optimize/BruteforceOptimizer.hpp +++ b/src/libslic3r/Optimize/BruteforceOptimizer.hpp @@ -13,7 +13,9 @@ template long num_iter(const std::array &idx, size_t gridsz) { long ret = 0; - for (size_t i = 0; i < N; ++i) ret += idx[i] * std::pow(gridsz, i); + for (size_t i = 0; i < N; ++i) + ret += idx[i] * std::pow(gridsz, i); + return ret; } diff --git a/src/libslic3r/Optimize/NLoptOptimizer.hpp b/src/libslic3r/Optimize/NLoptOptimizer.hpp index f5d3140463..9e423ff919 100644 --- a/src/libslic3r/Optimize/NLoptOptimizer.hpp +++ b/src/libslic3r/Optimize/NLoptOptimizer.hpp @@ -13,7 +13,7 @@ #include -#include +#include "Optimizer.hpp" namespace Slic3r { namespace opt { @@ -40,69 +40,163 @@ struct IsNLoptAlg> { static const constexpr bool value = true; }; +// NLopt can wrap any of its algorithms to use the augmented lagrangian method +// for deriving an object function from all equality and inequality constraints +// This way one can use algorithms that do not support these constraints natively +template struct NLoptAUGLAG {}; + +template +struct IsNLoptAlg>> { + static const constexpr bool value = true; +}; + +template struct IsNLoptAlg>> { + static const constexpr bool value = true; +}; + template using NLoptOnly = std::enable_if_t::value, T>; +template struct GetNLoptAlg_ { + static constexpr nlopt_algorithm Local = NLOPT_NUM_ALGORITHMS; + static constexpr nlopt_algorithm Global = NLOPT_NUM_ALGORITHMS; + static constexpr bool IsAUGLAG = false; +}; + +template struct GetNLoptAlg_> { + static constexpr nlopt_algorithm Local = NLOPT_NUM_ALGORITHMS; + static constexpr nlopt_algorithm Global = a; + static constexpr bool IsAUGLAG = false; +}; + +template +struct GetNLoptAlg_> { + static constexpr nlopt_algorithm Local = l; + static constexpr nlopt_algorithm Global = g; + static constexpr bool IsAUGLAG = false; +}; + +template constexpr nlopt_algorithm GetNLoptAlg_Global = GetNLoptAlg_>::Global; +template constexpr nlopt_algorithm GetNLoptAlg_Local = GetNLoptAlg_>::Local; +template constexpr bool IsAUGLAG = GetNLoptAlg_>::IsAUGLAG; + +template struct GetNLoptAlg_> { + static constexpr nlopt_algorithm Local = GetNLoptAlg_Local; + static constexpr nlopt_algorithm Global = GetNLoptAlg_Global; + static constexpr bool IsAUGLAG = true; +}; enum class OptDir { MIN, MAX }; // Where to optimize -struct NLopt { // Helper RAII class for nlopt_opt +struct NLoptRAII { // Helper RAII class for nlopt_opt nlopt_opt ptr = nullptr; - template explicit NLopt(A&&...a) + template explicit NLoptRAII(A&&...a) { ptr = nlopt_create(std::forward(a)...); } - NLopt(const NLopt&) = delete; - NLopt(NLopt&&) = delete; - NLopt& operator=(const NLopt&) = delete; - NLopt& operator=(NLopt&&) = delete; + NLoptRAII(const NLoptRAII&) = delete; + NLoptRAII(NLoptRAII&&) = delete; + NLoptRAII& operator=(const NLoptRAII&) = delete; + NLoptRAII& operator=(NLoptRAII&&) = delete; - ~NLopt() { nlopt_destroy(ptr); } + ~NLoptRAII() { nlopt_destroy(ptr); } }; -template class NLoptOpt {}; +// Map a generic function to each argument following the mapping function +template +Fn for_each_argument(Fn &&fn, Args&&...args) +{ + // see https://www.fluentcpp.com/2019/03/05/for_each_arg-applying-a-function-to-each-argument-of-a-function-in-cpp/ + (fn(std::forward(args)),...); -// Optimizers based on NLopt. -template class NLoptOpt> { -protected: + return fn; +} + +// Call fn on each element of the input tuple tup. +template +Fn for_each_in_tuple(Fn fn, Tup &&tup) +{ + auto mpfn = [&fn](auto&...pack) { + for_each_argument(fn, pack...); + }; + + std::apply(mpfn, tup); + + return fn; +} + +// Wrap each element of the tuple tup into a wrapper class W and return +// a new tuple with each element being of type W where T_i is the type of +// i-th element of tup. +template class W, class...Args> +auto wrap_tup(const std::tuple &tup) +{ + return std::tuple...>(tup); +} + +template> +class NLoptOpt { StopCriteria m_stopcr; - OptDir m_dir; + StopCriteria m_loc_stopcr; + OptDir m_dir = OptDir::MIN; - template using TOptData = - std::tuple*, NLoptOpt*, nlopt_opt>; + static constexpr double ConstraintEps = 1e-6; + + template struct OptData { + Fn fn; + NLoptOpt *self = nullptr; + nlopt_opt opt_raw = nullptr; + + OptData(const Fn &f): fn{f} {} + + OptData(const Fn &f, NLoptOpt *s, nlopt_opt nlopt_raw) + : fn{f}, self{s}, opt_raw{nlopt_raw} {} + }; template static double optfunc(unsigned n, const double *params, - double *gradient, - void *data) + double *gradient, void *data) { - assert(n >= N); + assert(n == N); - auto tdata = static_cast*>(data); + auto tdata = static_cast*>(data); - if (std::get<1>(*tdata)->m_stopcr.stop_condition()) - nlopt_force_stop(std::get<2>(*tdata)); + if (tdata->self->m_stopcr.stop_condition()) + nlopt_force_stop(tdata->opt_raw); - auto fnptr = std::get<0>(*tdata); auto funval = to_arr(params); double scoreval = 0.; - using RetT = decltype((*fnptr)(funval)); + using RetT = decltype(tdata->fn(funval)); if constexpr (std::is_convertible_v>) { - ScoreGradient score = (*fnptr)(funval); + ScoreGradient score = tdata->fn(funval); for (size_t i = 0; i < n; ++i) gradient[i] = (*score.gradient)[i]; scoreval = score.score; } else { - scoreval = (*fnptr)(funval); + scoreval = tdata->fn(funval); } return scoreval; } + template + static double constrain_func(unsigned n, const double *params, + double *gradient, void *data) + { + assert(n == N); + + auto tdata = static_cast*>(data); + auto funval = to_arr(params); + + return tdata->fn(funval); + } + template - void set_up(NLopt &nl, const Bounds& bounds) + static void set_up(NLoptRAII &nl, + const Bounds &bounds, + const StopCriteria &stopcr) { std::array lb, ub; @@ -114,23 +208,45 @@ protected: nlopt_set_lower_bounds(nl.ptr, lb.data()); nlopt_set_upper_bounds(nl.ptr, ub.data()); - double abs_diff = m_stopcr.abs_score_diff(); - double rel_diff = m_stopcr.rel_score_diff(); - double stopval = m_stopcr.stop_score(); + double abs_diff = stopcr.abs_score_diff(); + double rel_diff = stopcr.rel_score_diff(); + double stopval = stopcr.stop_score(); if(!std::isnan(abs_diff)) nlopt_set_ftol_abs(nl.ptr, abs_diff); if(!std::isnan(rel_diff)) nlopt_set_ftol_rel(nl.ptr, rel_diff); if(!std::isnan(stopval)) nlopt_set_stopval(nl.ptr, stopval); - if(m_stopcr.max_iterations() > 0) - nlopt_set_maxeval(nl.ptr, m_stopcr.max_iterations()); + if(stopcr.max_iterations() > 0) + nlopt_set_maxeval(nl.ptr, stopcr.max_iterations()); } - template - Result optimize(NLopt &nl, Fn &&fn, const Input &initvals) + template + Result optimize(NLoptRAII &nl, Fn &&fn, const Input &initvals, + const std::tuple &equalities, + const std::tuple &inequalities) { Result r; - TOptData data = std::make_tuple(&fn, this, nl.ptr); + OptData data {fn, this, nl.ptr}; + + auto do_for_each_eq = [this, &nl](auto &arg) { + arg.self = this; + arg.opt_raw = nl.ptr; + using F = decltype(arg.fn); + nlopt_add_equality_constraint (nl.ptr, constrain_func, &arg, ConstraintEps); + }; + + auto do_for_each_ineq = [this, &nl](auto &arg) { + arg.self = this; + arg.opt_raw = nl.ptr; + using F = decltype(arg.fn); + nlopt_add_inequality_constraint (nl.ptr, constrain_func, &arg, ConstraintEps); + }; + + auto eq_data = wrap_tup(equalities); + for_each_in_tuple(do_for_each_eq, eq_data); + + auto ineq_data = wrap_tup(inequalities); + for_each_in_tuple(do_for_each_ineq, ineq_data); switch(m_dir) { case OptDir::MIN: @@ -147,51 +263,81 @@ protected: public: - template - Result optimize(Func&& func, + template + Result optimize(Fn&& f, const Input &initvals, - const Bounds& bounds) + const Bounds& bounds, + const std::tuple &equalities, + const std::tuple &inequalities) { - NLopt nl{alg, N}; - set_up(nl, bounds); + if constexpr (IsAUGLAG) { + NLoptRAII nl_wrap{NLOPT_AUGLAG, N}; + set_up(nl_wrap, bounds, get_criteria()); - return optimize(nl, std::forward(func), initvals); + NLoptRAII nl_glob{GetNLoptAlg_Global, N}; + set_up(nl_glob, bounds, get_criteria()); + nlopt_set_local_optimizer(nl_wrap.ptr, nl_glob.ptr); + + if constexpr (GetNLoptAlg_Local < NLOPT_NUM_ALGORITHMS) { + NLoptRAII nl_loc{GetNLoptAlg_Local, N}; + set_up(nl_loc, bounds, m_loc_stopcr); + nlopt_set_local_optimizer(nl_glob.ptr, nl_loc.ptr); + + return optimize(nl_wrap, std::forward(f), initvals, + equalities, inequalities); + } else { + return optimize(nl_wrap, std::forward(f), initvals, + equalities, inequalities); + } + } else { + NLoptRAII nl_glob{GetNLoptAlg_Global, N}; + set_up(nl_glob, bounds, get_criteria()); + + if constexpr (GetNLoptAlg_Local < NLOPT_NUM_ALGORITHMS) { + NLoptRAII nl_loc{GetNLoptAlg_Local, N}; + set_up(nl_loc, bounds, m_loc_stopcr); + nlopt_set_local_optimizer(nl_glob.ptr, nl_loc.ptr); + + return optimize(nl_glob, std::forward(f), initvals, + equalities, inequalities); + } else { + return optimize(nl_glob, std::forward(f), initvals, + equalities, inequalities); + } + } + + assert(false); + + return {}; } - explicit NLoptOpt(StopCriteria stopcr = {}) : m_stopcr(stopcr) {} + explicit NLoptOpt(const StopCriteria &stopcr_glob = {}) + : m_stopcr(stopcr_glob) + {} void set_criteria(const StopCriteria &cr) { m_stopcr = cr; } const StopCriteria &get_criteria() const noexcept { return m_stopcr; } - void set_dir(OptDir dir) noexcept { m_dir = dir; } + void set_loc_criteria(const StopCriteria &cr) { m_loc_stopcr = cr; } + const StopCriteria &get_loc_criteria() const noexcept { return m_loc_stopcr; } + + void set_dir(OptDir dir) noexcept { m_dir = dir; } void seed(long s) { nlopt_srand(s); } }; -template -class NLoptOpt>: public NLoptOpt> -{ - using Base = NLoptOpt>; -public: - - template - Result optimize(Fn&& f, - const Input &initvals, - const Bounds& bounds) - { - NLopt nl_glob{glob, N}, nl_loc{loc, N}; - - Base::set_up(nl_glob, bounds); - Base::set_up(nl_loc, bounds); - nlopt_set_local_optimizer(nl_glob.ptr, nl_loc.ptr); - - return Base::optimize(nl_glob, std::forward(f), initvals); - } - - explicit NLoptOpt(StopCriteria stopcr = {}) : Base{stopcr} {} +template struct AlgFeatures_ { + static constexpr bool SupportsInequalities = false; + static constexpr bool SupportsEqualities = false; }; } // namespace detail; +template constexpr bool SupportsEqualities = + detail::AlgFeatures_>::SupportsEqualities; + +template constexpr bool SupportsInequalities = + detail::AlgFeatures_>::SupportsInequalities; + // Optimizers based on NLopt. template class Optimizer> { detail::NLoptOpt m_opt; @@ -201,12 +347,24 @@ public: Optimizer& to_max() { m_opt.set_dir(detail::OptDir::MAX); return *this; } Optimizer& to_min() { m_opt.set_dir(detail::OptDir::MIN); return *this; } - template + template Result optimize(Func&& func, const Input &initvals, - const Bounds& bounds) + const Bounds& bounds, + const std::tuple &eq_constraints = {}, + const std::tuple &ineq_constraint = {}) { - return m_opt.optimize(std::forward(func), initvals, bounds); + static_assert(std::tuple_size_v> == 0 + || SupportsEqualities, + "Equality constraints are not supported."); + + static_assert(std::tuple_size_v> == 0 + || SupportsInequalities, + "Inequality constraints are not supported."); + + return m_opt.optimize(std::forward(func), initvals, bounds, + eq_constraints, + ineq_constraint); } explicit Optimizer(StopCriteria stopcr = {}) : m_opt(stopcr) {} @@ -219,14 +377,56 @@ public: const StopCriteria &get_criteria() const { return m_opt.get_criteria(); } void seed(long s) { m_opt.seed(s); } + + void set_loc_criteria(const StopCriteria &cr) { m_opt.set_loc_criteria(cr); } + const StopCriteria &get_loc_criteria() const noexcept { return m_opt.get_loc_criteria(); } }; // Predefinded NLopt algorithms -using AlgNLoptGenetic = detail::NLoptAlgComb; -using AlgNLoptSubplex = detail::NLoptAlg; -using AlgNLoptSimplex = detail::NLoptAlg; -using AlgNLoptDIRECT = detail::NLoptAlg; -using AlgNLoptMLSL = detail::NLoptAlg; +using AlgNLoptGenetic = detail::NLoptAlgComb; +using AlgNLoptSubplex = detail::NLoptAlg; +using AlgNLoptSimplex = detail::NLoptAlg; +using AlgNLoptCobyla = detail::NLoptAlg; +using AlgNLoptDIRECT = detail::NLoptAlg; +using AlgNLoptORIG_DIRECT = detail::NLoptAlg; +using AlgNLoptISRES = detail::NLoptAlg; +using AlgNLoptAGS = detail::NLoptAlg; + +using AlgNLoptMLSL_Subplx = detail::NLoptAlgComb; +using AlgNLoptMLSL_Cobyla = detail::NLoptAlgComb; +using AlgNLoptGenetic_Subplx = detail::NLoptAlgComb; + +// To craft auglag algorithms (constraint support through object function transformation) +using detail::NLoptAUGLAG; + +namespace detail { + +template<> struct AlgFeatures_ { + static constexpr bool SupportsInequalities = true; + static constexpr bool SupportsEqualities = true; +}; + +template<> struct AlgFeatures_ { + static constexpr bool SupportsInequalities = true; + static constexpr bool SupportsEqualities = false; +}; + +template<> struct AlgFeatures_ { + static constexpr bool SupportsInequalities = true; + static constexpr bool SupportsEqualities = false; +}; + +template<> struct AlgFeatures_ { + static constexpr bool SupportsInequalities = true; + static constexpr bool SupportsEqualities = true; +}; + +template struct AlgFeatures_> { + static constexpr bool SupportsInequalities = true; + static constexpr bool SupportsEqualities = true; +}; + +} // namespace detail }} // namespace Slic3r::opt diff --git a/src/libslic3r/Optimize/Optimizer.hpp b/src/libslic3r/Optimize/Optimizer.hpp index bf95d9ee07..6212a5f59d 100644 --- a/src/libslic3r/Optimize/Optimizer.hpp +++ b/src/libslic3r/Optimize/Optimizer.hpp @@ -12,6 +12,15 @@ namespace Slic3r { namespace opt { +template +using FloatingOnly = std::enable_if_t::value, O>; + +template> +constexpr T NaN = std::numeric_limits::quiet_NaN(); + +constexpr float NaNf = NaN; +constexpr double NaNd = NaN; + // A type to hold the complete result of the optimization. template struct Result { int resultcode; // Method dependent @@ -79,7 +88,7 @@ public: double stop_score() const { return m_stop_score; } - StopCriteria & max_iterations(double val) + StopCriteria & max_iterations(unsigned val) { m_max_iterations = val; return *this; } @@ -137,16 +146,25 @@ public: // For each dimension an interval (Bound) has to be given marking the bounds // for that dimension. // + // Optionally, some constraints can be given in the form of double(Input) + // functors. The parameters eq_constraints and ineq_constraints can be used + // to add equality and inequality (<= 0) constraints to the optimization. + // Note that it is up the the particular method if it accepts these + // constraints. + // // initvals have to be within the specified bounds, otherwise its undefined // behavior. // // Func can return a score of type double or optionally a ScoreGradient // class to indicate the function gradient for a optimization methods that // make use of the gradient. - template + template Result optimize(Func&& /*func*/, const Input &/*initvals*/, - const Bounds& /*bounds*/) { return {}; } + const Bounds& /*bounds*/, + const std::tuple &eq_constraints = {}, + const std::tuple &ineq_constraint = {} + ) { return {}; } // optional for randomized methods: void seed(long /*s*/) {} diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 9438a823a4..53eca40c6d 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -146,6 +146,11 @@ VendorProfile VendorProfile::from_ini(const ptree &tree, const boost::filesystem res.changelog_url = changelog_url->second.data(); } + const auto templates_profile = vendor_section.find("templates_profile"); + if (templates_profile != vendor_section.not_found()) { + res.templates_profile = templates_profile->second.data() == "1"; + } + if (! load_all) { return res; } @@ -200,6 +205,10 @@ VendorProfile VendorProfile::from_ini(const ptree &tree, const boost::filesystem } model.bed_model = section.second.get("bed_model", ""); model.bed_texture = section.second.get("bed_texture", ""); + model.thumbnail = section.second.get("thumbnail", ""); + if (model.thumbnail.empty()) + model.thumbnail = model.id + "_thumbnail.png"; + if (! model.id.empty() && ! model.variants.empty()) res.models.push_back(std::move(model)); } @@ -336,7 +345,8 @@ std::string Preset::label() const bool is_compatible_with_print(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_print, const PresetWithVendorProfile &active_printer) { - if (preset.vendor != nullptr && preset.vendor != active_printer.vendor) + // templates_profile vendor profiles should be decided as same vendor profiles + if (preset.vendor != nullptr && preset.vendor != active_printer.vendor && !preset.vendor->templates_profile) // The current profile has a vendor assigned and it is different from the active print's vendor. return false; auto &condition = preset.preset.compatible_prints_condition(); @@ -358,7 +368,8 @@ bool is_compatible_with_print(const PresetWithVendorProfile &preset, const Prese bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_printer, const DynamicPrintConfig *extra_config) { - if (preset.vendor != nullptr && preset.vendor != active_printer.vendor) + // templates_profile vendor profiles should be decided as same vendor profiles + if (preset.vendor != nullptr && preset.vendor != active_printer.vendor && !preset.vendor->templates_profile) // The current profile has a vendor assigned and it is different from the active print's vendor. return false; auto &condition = preset.preset.compatible_printers_condition(); @@ -495,12 +506,14 @@ static std::vector s_Preset_sla_print_options { "faded_layers", "supports_enable", "support_tree_type", + "support_head_front_diameter", "support_head_penetration", "support_head_width", "support_pillar_diameter", "support_small_pillar_diameter_percent", "support_max_bridges_on_pillar", + "support_max_weight_on_model", "support_pillar_connection_mode", "support_buildplate_only", "support_enforcers_only", @@ -512,6 +525,25 @@ static std::vector s_Preset_sla_print_options { "support_max_bridge_length", "support_max_pillar_link_distance", "support_object_elevation", + + "branchingsupport_head_front_diameter", + "branchingsupport_head_penetration", + "branchingsupport_head_width", + "branchingsupport_pillar_diameter", + "branchingsupport_small_pillar_diameter_percent", + "branchingsupport_max_bridges_on_pillar", + "branchingsupport_max_weight_on_model", + "branchingsupport_pillar_connection_mode", + "branchingsupport_buildplate_only", + "branchingsupport_pillar_widening_factor", + "branchingsupport_base_diameter", + "branchingsupport_base_height", + "branchingsupport_base_safety_distance", + "branchingsupport_critical_angle", + "branchingsupport_max_bridge_length", + "branchingsupport_max_pillar_link_distance", + "branchingsupport_object_elevation", + "support_points_density_relative", "support_points_minimal_distance", "slice_closing_radius", @@ -1165,6 +1197,7 @@ size_t PresetCollection::update_compatible_internal(const PresetWithVendorProfil if (opt) config.set_key_value("num_extruders", new ConfigOptionInt((int)static_cast(opt)->values.size())); bool some_compatible = false; + std::vector indices_of_template_presets; for (size_t idx_preset = m_num_default_presets; idx_preset < m_presets.size(); ++ idx_preset) { bool selected = idx_preset == m_idx_selected; Preset &preset_selected = m_presets[idx_preset]; @@ -1181,7 +1214,29 @@ size_t PresetCollection::update_compatible_internal(const PresetWithVendorProfil m_idx_selected = size_t(-1); if (selected) preset_selected.is_compatible = preset_edited.is_compatible; + if (preset_edited.vendor && preset_edited.vendor->templates_profile) { + indices_of_template_presets.push_back(idx_preset); + } } + // filter out template profiles where profile with same alias and compability exists + if (!indices_of_template_presets.empty()) { + for (size_t idx_preset = m_num_default_presets; idx_preset < m_presets.size(); ++idx_preset) { + if (m_presets[idx_preset].vendor && !m_presets[idx_preset].vendor->templates_profile && m_presets[idx_preset].is_compatible) { + std::string preset_alias = m_presets[idx_preset].alias; + for (size_t idx_of_template_in_presets : indices_of_template_presets) { + if (m_presets[idx_of_template_in_presets].alias == preset_alias) { + // unselect selected template filament if there is non-template alias compatible + if (idx_of_template_in_presets == m_idx_selected && (unselect_if_incompatible == PresetSelectCompatibleType::Always || unselect_if_incompatible == PresetSelectCompatibleType::OnlyIfWasCompatible)) { + m_idx_selected = size_t(-1); + } + m_presets[idx_of_template_in_presets].is_compatible = false; + break; + } + } + } + } + } + // Update visibility of the default profiles here if the defaults are suppressed, the current profile is not compatible and we don't want to select another compatible profile. if (m_idx_selected >= m_num_default_presets && m_default_suppressed) for (size_t i = 0; i < m_num_default_presets; ++ i) @@ -2079,6 +2134,25 @@ namespace PresetUtils { } return out; } + + bool vendor_profile_has_all_resources(const VendorProfile& vp) + { + namespace fs = boost::filesystem; + + std::string vendor_folder = Slic3r::data_dir() + "/vendor/" + vp.id + "/"; + std::string rsrc_folder = Slic3r::resources_dir() + "/profiles/" + vp.id + "/"; + std::string cache_folder = Slic3r::data_dir() + "/cache/" + vp.id + "/"; + for (const VendorProfile::PrinterModel& model : vp.models) { + for (const std::string& res : { model.bed_texture, model.bed_model, model.thumbnail } ) { + if (! res.empty() + && !fs::exists(fs::path(vendor_folder + res)) + && !fs::exists(fs::path(rsrc_folder + res)) + && !fs::exists(fs::path(cache_folder + res))) + return false; + } + } + return true; + } } // namespace PresetUtils } // namespace Slic3r diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index 74ba2e0f5b..9a16a16a99 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -34,6 +34,7 @@ public: Semver config_version; std::string config_update_url; std::string changelog_url; + bool templates_profile { false }; struct PrinterVariant { PrinterVariant() {} @@ -52,6 +53,7 @@ public: // Vendor & Printer Model specific print bed model & texture. std::string bed_model; std::string bed_texture; + std::string thumbnail; PrinterVariant* variant(const std::string &name) { for (auto &v : this->variants) @@ -619,6 +621,7 @@ namespace PresetUtils { const VendorProfile::PrinterModel* system_printer_model(const Preset &preset); std::string system_printer_bed_model(const Preset& preset); std::string system_printer_bed_texture(const Preset& preset); + bool vendor_profile_has_all_resources(const VendorProfile& vp); } // namespace PresetUtils diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index b783c8aebc..81de207167 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -159,6 +159,7 @@ void PresetBundle::setup_directories() data_dir, data_dir / "vendor", data_dir / "cache", + data_dir / "cache" / "vendor", data_dir / "shapes", #ifdef SLIC3R_PROFILE_USE_PRESETS_SUBDIR // Store the print/filament/printer presets into a "presets" directory. @@ -1307,17 +1308,26 @@ std::pair PresetBundle::load_configbundle( try { pt::read_ini(ifs, tree); } catch (const boost::property_tree::ini_parser::ini_parser_error &err) { - throw Slic3r::RuntimeError(format("Failed loading config bundle \"%1%\"\nError: \"%2%\" at line %3%", path, err.message(), err.line()).c_str()); + // This throw was uncatched. While other similar problems later are just returning empty pair. + //throw Slic3r::RuntimeError(format("Failed loading config bundle \"%1%\"\nError: \"%2%\" at line %3%", path, err.message(), err.line()).c_str()); + BOOST_LOG_TRIVIAL(error) << format("Failed loading config bundle \"%1%\"\nError: \"%2%\" at line %3%", path, err.message(), err.line()).c_str(); + return std::make_pair(PresetsConfigSubstitutions{}, 0); } } const VendorProfile *vendor_profile = nullptr; if (flags.has(LoadConfigBundleAttribute::LoadSystem) || flags.has(LoadConfigBundleAttribute::LoadVendorOnly)) { - auto vp = VendorProfile::from_ini(tree, path); - if (vp.models.size() == 0) { + VendorProfile vp; + try { + vp = VendorProfile::from_ini(tree, path); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: Failed to open profile file.") % path; + return std::make_pair(PresetsConfigSubstitutions{}, 0); + } + if (vp.models.size() == 0 && !vp.templates_profile) { BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No printer model defined.") % path; return std::make_pair(PresetsConfigSubstitutions{}, 0); - } else if (vp.num_variants() == 0) { + } else if (vp.num_variants() == 0 && !vp.templates_profile) { BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No printer variant defined") % path; return std::make_pair(PresetsConfigSubstitutions{}, 0); } @@ -1359,6 +1369,9 @@ std::pair PresetBundle::load_configbundle( } else if (boost::starts_with(section.first, "filament:")) { presets = &this->filaments; preset_name = section.first.substr(9); + if (vendor_profile && vendor_profile->templates_profile) { + preset_name += " @Template"; + } } else if (boost::starts_with(section.first, "sla_print:")) { presets = &this->sla_prints; preset_name = section.first.substr(10); diff --git a/src/libslic3r/PrincipalComponents2D.cpp b/src/libslic3r/PrincipalComponents2D.cpp index 4b7c3a1da2..7bdf793157 100644 --- a/src/libslic3r/PrincipalComponents2D.cpp +++ b/src/libslic3r/PrincipalComponents2D.cpp @@ -3,53 +3,97 @@ namespace Slic3r { -// returns two eigenvectors of the area covered by given polygons. The vectors are sorted by their corresponding eigenvalue, largest first -std::tuple compute_principal_components(const Polygons &polys, const double unscaled_resolution) + + +// returns triangle area, first_moment_of_area_xy, second_moment_of_area_xy, second_moment_of_area_covariance +// none of the values is divided/normalized by area. +// The function computes intgeral over the area of the triangle, with function f(x,y) = x for first moments of area (y is analogous) +// f(x,y) = x^2 for second moment of area +// and f(x,y) = x*y for second moment of area covariance +std::tuple compute_moments_of_area_of_triangle(const Vec2f &a, const Vec2f &b, const Vec2f &c) { - // USING UNSCALED VALUES - const Vec2d pixel_size = Vec2d(unscaled_resolution, unscaled_resolution); - const auto bb = get_extents(polys); - const Vec2i pixel_count = unscaled(bb.size()).cwiseQuotient(pixel_size).cast() + Vec2i::Ones(); + // based on the following guide: + // Denote the vertices of S by a, b, c. Then the map + // g:(u,v)↦a+u(b−a)+v(c−a) , + // which in coordinates appears as + // g:(u,v)↦{x(u,v)y(u,v)=a1+u(b1−a1)+v(c1−a1)=a2+u(b2−a2)+v(c2−a2) ,(1) + // obviously maps S′ bijectively onto S. Therefore the transformation formula for multiple integrals steps into action, and we obtain + // ∫Sf(x,y)d(x,y)=∫S′f(x(u,v),y(u,v))∣∣Jg(u,v)∣∣ d(u,v) . + // In the case at hand the Jacobian determinant is a constant: From (1) we obtain + // Jg(u,v)=det[xuyuxvyv]=(b1−a1)(c2−a2)−(c1−a1)(b2−a2) . + // Therefore we can write + // ∫Sf(x,y)d(x,y)=∣∣Jg∣∣∫10∫1−u0f~(u,v) dv du , + // where f~ denotes the pullback of f to S′: + // f~(u,v):=f(x(u,v),y(u,v)) . + // Don't forget taking the absolute value of Jg! - std::vector lines{}; - for (Line l : to_lines(polys)) { lines.emplace_back(unscaled(l.a), unscaled(l.b)); } - AABBTreeIndirect::Tree<2, double> tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); - auto is_inside = [&](const Vec2d &point) { - size_t nearest_line_index_out = 0; - Vec2d nearest_point_out = Vec2d::Zero(); - auto distance = AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, point, nearest_line_index_out, nearest_point_out); - if (distance < 0) return false; - const Linef &line = lines[nearest_line_index_out]; - Vec2d v1 = line.b - line.a; - Vec2d v2 = point - line.a; - if ((v1.x() * v2.y()) - (v1.y() * v2.x()) > 0.0) { return true; } - return false; - }; + float jacobian_determinant_abs = std::abs((b.x() - a.x()) * (c.y() - a.y()) - (c.x() - a.x()) * (b.y() - a.y())); - double pixel_area = pixel_size.x() * pixel_size.y(); - Vec2d centroid_accumulator = Vec2d::Zero(); - Vec2d second_moment_of_area_accumulator = Vec2d::Zero(); - double second_moment_of_area_covariance_accumulator = 0.0; - double area = 0.0; + // coordinate transform: gx(u,v) = a.x + u * (b.x - a.x) + v * (c.x - a.x) + // coordinate transform: gy(u,v) = a.y + u * (b.y - a.y) + v * (c.y - a.y) + // second moment of area for x: f(x, y) = x^2; + // f(gx(u,v), gy(u,v)) = gx(u,v)^2 = ... (long expanded form) - for (int x = 0; x < pixel_count.x(); x++) { - for (int y = 0; y < pixel_count.y(); y++) { - Vec2d position = unscaled(bb.min) + pixel_size.cwiseProduct(Vec2d{x, y}); - if (is_inside(position)) { - area += pixel_area; - centroid_accumulator += pixel_area * position; - second_moment_of_area_accumulator += pixel_area * position.cwiseProduct(position); - second_moment_of_area_covariance_accumulator += pixel_area * position.x() * position.y(); - } + // result is Int_T func = jacobian_determinant_abs * Int_0^1 Int_0^1-u func(gx(u,v), gy(u,v)) dv du + // integral_0^1 integral_0^(1 - u) (a + u (b - a) + v (c - a))^2 dv du = 1/12 (a^2 + a (b + c) + b^2 + b c + c^2) + + Vec2f second_moment_of_area_xy = jacobian_determinant_abs * + (a.cwiseProduct(a) + b.cwiseProduct(b) + b.cwiseProduct(c) + c.cwiseProduct(c) + + a.cwiseProduct(b + c)) / + 12.0f; + // second moment of area covariance : f(x, y) = x*y; + // f(gx(u,v), gy(u,v)) = gx(u,v)*gy(u,v) = ... (long expanded form) + //(a_1 + u * (b_1 - a_1) + v * (c_1 - a_1)) * (a_2 + u * (b_2 - a_2) + v * (c_2 - a_2)) + // == (a_1 + u (b_1 - a_1) + v (c_1 - a_1)) (a_2 + u (b_2 - a_2) + v (c_2 - a_2)) + + // intermediate result: integral_0^(1 - u) (a_1 + u (b_1 - a_1) + v (c_1 - a_1)) (a_2 + u (b_2 - a_2) + v (c_2 - a_2)) dv = + // 1/6 (u - 1) (-c_1 (u - 1) (a_2 (u - 1) - 3 b_2 u) - c_2 (u - 1) (a_1 (u - 1) - 3 b_1 u + 2 c_1 (u - 1)) + 3 b_1 u (a_2 (u - 1) - 2 + // b_2 u) + a_1 (u - 1) (3 b_2 u - 2 a_2 (u - 1))) result = integral_0^1 1/6 (u - 1) (-c_1 (u - 1) (a_2 (u - 1) - 3 b_2 u) - c_2 (u - + // 1) (a_1 (u - 1) - 3 b_1 u + 2 c_1 (u - 1)) + 3 b_1 u (a_2 (u - 1) - 2 b_2 u) + a_1 (u - 1) (3 b_2 u - 2 a_2 (u - 1))) du = + // 1/24 (a_2 (b_1 + c_1) + a_1 (2 a_2 + b_2 + c_2) + b_2 c_1 + b_1 c_2 + 2 b_1 b_2 + 2 c_1 c_2) + // result is Int_T func = jacobian_determinant_abs * Int_0^1 Int_0^1-u func(gx(u,v), gy(u,v)) dv du + float second_moment_of_area_covariance = jacobian_determinant_abs * (1.0f / 24.0f) * + (a.y() * (b.x() + c.x()) + a.x() * (2.0f * a.y() + b.y() + c.y()) + b.y() * c.x() + + b.x() * c.y() + 2.0f * b.x() * b.y() + 2.0f * c.x() * c.y()); + + float area = jacobian_determinant_abs * 0.5f; + + Vec2f first_moment_of_area_xy = jacobian_determinant_abs * (a + b + c) / 6.0f; + + return {area, first_moment_of_area_xy, second_moment_of_area_xy, second_moment_of_area_covariance}; +}; + +// returns two eigenvectors of the area covered by given polygons. The vectors are sorted by their corresponding eigenvalue, largest first +std::tuple compute_principal_components(const Polygons &polys) +{ + Vec2f centroid_accumulator = Vec2f::Zero(); + Vec2f second_moment_of_area_accumulator = Vec2f::Zero(); + float second_moment_of_area_covariance_accumulator = 0.0f; + float area = 0.0f; + + for (const Polygon &poly : polys) { + Vec2f p0 = unscaled(poly.first_point()).cast(); + for (size_t i = 2; i < poly.points.size(); i++) { + Vec2f p1 = unscaled(poly.points[i - 1]).cast(); + Vec2f p2 = unscaled(poly.points[i]).cast(); + + float sign = cross2(p1 - p0, p2 - p1) > 0 ? 1.0f : -1.0f; + + auto [triangle_area, first_moment_of_area, second_moment_area, + second_moment_of_area_covariance] = compute_moments_of_area_of_triangle(p0, p1, p2); + area += sign * triangle_area; + centroid_accumulator += sign * first_moment_of_area; + second_moment_of_area_accumulator += sign * second_moment_area; + second_moment_of_area_covariance_accumulator += sign * second_moment_of_area_covariance; } } if (area <= 0.0) { - return {Vec2d::Zero(), Vec2d::Zero()}; + return {Vec2f::Zero(), Vec2f::Zero()}; } - Vec2d centroid = centroid_accumulator / area; - Vec2d variance = second_moment_of_area_accumulator / area - centroid.cwiseProduct(centroid); + Vec2f centroid = centroid_accumulator / area; + Vec2f variance = second_moment_of_area_accumulator / area - centroid.cwiseProduct(centroid); double covariance = second_moment_of_area_covariance_accumulator / area - centroid.x() * centroid.y(); #if 0 std::cout << "area : " << area << std::endl; @@ -58,7 +102,7 @@ std::tuple compute_principal_components(const Polygons &polys, con std::cout << "covariance : " << covariance << std::endl; #endif if (abs(covariance) < EPSILON) { - std::tuple result{Vec2d{variance.x(), 0.0}, Vec2d{0.0, variance.y()}}; + std::tuple result{Vec2f{variance.x(), 0.0}, Vec2f{0.0, variance.y()}}; if (variance.y() > variance.x()) { return {std::get<1>(result), std::get<0>(result)}; } else @@ -72,12 +116,12 @@ std::tuple compute_principal_components(const Polygons &polys, con // Eigenvalues are solutions to det(C - lI) = 0, where l is the eigenvalue and I unit matrix // Eigenvector for eigenvalue l is any vector v such that Cv = lv - double eigenvalue_a = 0.5 * (variance.x() + variance.y() + - sqrt((variance.x() - variance.y()) * (variance.x() - variance.y()) + 4 * covariance * covariance)); - double eigenvalue_b = 0.5 * (variance.x() + variance.y() - - sqrt((variance.x() - variance.y()) * (variance.x() - variance.y()) + 4 * covariance * covariance)); - Vec2d eigenvector_a{(eigenvalue_a - variance.y()) / covariance, 1.0}; - Vec2d eigenvector_b{(eigenvalue_b - variance.y()) / covariance, 1.0}; + float eigenvalue_a = 0.5f * (variance.x() + variance.y() + + sqrt((variance.x() - variance.y()) * (variance.x() - variance.y()) + 4.0f * covariance * covariance)); + float eigenvalue_b = 0.5f * (variance.x() + variance.y() - + sqrt((variance.x() - variance.y()) * (variance.x() - variance.y()) + 4.0f * covariance * covariance)); + Vec2f eigenvector_a{(eigenvalue_a - variance.y()) / covariance, 1.0f}; + Vec2f eigenvector_b{(eigenvalue_b - variance.y()) / covariance, 1.0f}; #if 0 std::cout << "eigenvalue_a: " << eigenvalue_a << std::endl; diff --git a/src/libslic3r/PrincipalComponents2D.hpp b/src/libslic3r/PrincipalComponents2D.hpp index 0eccdfcc5e..dc8897a7a7 100644 --- a/src/libslic3r/PrincipalComponents2D.hpp +++ b/src/libslic3r/PrincipalComponents2D.hpp @@ -9,8 +9,15 @@ namespace Slic3r { +// returns triangle area, first_moment_of_area_xy, second_moment_of_area_xy, second_moment_of_area_covariance +// none of the values is divided/normalized by area. +// The function computes intgeral over the area of the triangle, with function f(x,y) = x for first moments of area (y is analogous) +// f(x,y) = x^2 for second moment of area +// and f(x,y) = x*y for second moment of area covariance +std::tuple compute_moments_of_area_of_triangle(const Vec2f &a, const Vec2f &b, const Vec2f &c); + // returns two eigenvectors of the area covered by given polygons. The vectors are sorted by their corresponding eigenvalue, largest first -std::tuple compute_principal_components(const Polygons &polys, const double unscaled_resolution); +std::tuple compute_principal_components(const Polygons &polys); } diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 4f586a17c9..cc0a790d33 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1201,7 +1201,6 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ } // Invalidate just the supports step. for (const PrintObjectStatus &print_object_status : print_objects_range) { - update_apply_status(print_object_status.print_object->invalidate_step(posSupportSpotsSearch)); update_apply_status(print_object_status.print_object->invalidate_step(posSupportMaterial)); } if (supports_differ) { diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 4639cb4138..c19722a145 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -181,7 +181,8 @@ CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SLAMaterialSpeed); static inline const t_config_enum_values s_keys_map_SLASupportTreeType = { {"default", int(sla::SupportTreeType::Default)}, - {"branching", int(sla::SupportTreeType::Branching)} + {"branching", int(sla::SupportTreeType::Branching)}, + //TODO: {"organic", int(sla::SupportTreeType::Organic)} }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SLASupportTreeType); @@ -534,7 +535,7 @@ void PrintConfigDef::init_fff_params() def->category = L("Speed"); def->tooltip = L("This setting enables dynamic speed control on overhangs."); def->mode = comAdvanced; - def->set_default_value(new ConfigOptionBool(true)); + def->set_default_value(new ConfigOptionBool(false)); def = this->add("overhang_overlap_levels", coPercents); def->full_label = L("Overhang overlap levels"); @@ -841,7 +842,8 @@ void PrintConfigDef::init_fff_params() def = this->add("extra_perimeters_on_overhangs", coBool); def->label = L("Extra perimeters on overhangs (Experimental)"); def->category = L("Layers and Perimeters"); - def->tooltip = L("Create additional perimeter paths over steep overhangs and areas where bridges cannot be anchored."); + def->tooltip = L("Detect overhang areas where bridges cannot be anchored, and fill them with " + "extra perimeter paths. These paths are anchored to the nearby non-overhang area when possible."); def->mode = comExpert; def->set_default_value(new ConfigOptionBool(false)); @@ -3271,6 +3273,219 @@ void PrintConfigDef::init_extruder_option_keys() assert(std::is_sorted(m_extruder_retract_keys.begin(), m_extruder_retract_keys.end())); } +void PrintConfigDef::init_sla_support_params(const std::string &prefix) +{ + ConfigOptionDef* def; + + constexpr const char * pretext_unavailable = L("Unavailable for this method.\n"); + std::string pretext; + + def = this->add(prefix + "support_head_front_diameter", coFloat); + def->label = L("Pinhead front diameter"); + def->category = L("Supports"); + def->tooltip = L("Diameter of the pointing side of the head"); + def->sidetext = L("mm"); + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.4)); + + def = this->add(prefix + "support_head_penetration", coFloat); + def->label = L("Head penetration"); + def->category = L("Supports"); + def->tooltip = L("How much the pinhead has to penetrate the model surface"); + def->sidetext = L("mm"); + def->mode = comAdvanced; + def->min = 0; + def->set_default_value(new ConfigOptionFloat(0.2)); + + def = this->add(prefix + "support_head_width", coFloat); + def->label = L("Pinhead width"); + def->category = L("Supports"); + def->tooltip = L("Width from the back sphere center to the front sphere center"); + def->sidetext = L("mm"); + def->min = 0; + def->max = 20; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(1.0)); + + def = this->add(prefix + "support_pillar_diameter", coFloat); + def->label = L("Pillar diameter"); + def->category = L("Supports"); + def->tooltip = L("Diameter in mm of the support pillars"); + def->sidetext = L("mm"); + def->min = 0; + def->max = 15; + def->mode = comSimple; + def->set_default_value(new ConfigOptionFloat(1.0)); + + def = this->add(prefix + "support_small_pillar_diameter_percent", coPercent); + def->label = L("Small pillar diameter percent"); + def->category = L("Supports"); + def->tooltip = L("The percentage of smaller pillars compared to the normal pillar diameter " + "which are used in problematic areas where a normal pilla cannot fit."); + def->sidetext = L("%"); + def->min = 1; + def->max = 100; + def->mode = comExpert; + def->set_default_value(new ConfigOptionPercent(50)); + + pretext = ""; + if (prefix == "branching") + pretext = pretext_unavailable; + + def = this->add(prefix + "support_max_bridges_on_pillar", coInt); + def->label = L("Max bridges on a pillar"); + def->tooltip = pretext + L( + "Maximum number of bridges that can be placed on a pillar. Bridges " + "hold support point pinheads and connect to pillars as small branches."); + def->min = 0; + def->max = 50; + def->mode = comExpert; + def->set_default_value(new ConfigOptionInt(prefix == "branching" ? 2 : 3)); + + pretext = ""; + if (prefix.empty()) + pretext = pretext_unavailable; + + def = this->add(prefix + "support_max_weight_on_model", coFloat); + def->label = L("Max weight on model"); + def->category = L("Supports"); + def->tooltip = pretext + L( + "Maximum weight of sub-trees that terminate on the model instead of the print bed. The weight is the sum of the lenghts of all " + "branches emanating from the endpoint."); + def->sidetext = L("mm"); + def->min = 0; + def->mode = comExpert; + def->set_default_value(new ConfigOptionFloat(10.)); + + pretext = ""; + if (prefix == "branching") + pretext = pretext_unavailable; + + def = this->add(prefix + "support_pillar_connection_mode", coEnum); + def->label = L("Pillar connection mode"); + def->tooltip = pretext + L("Controls the bridge type between two neighboring pillars." + " Can be zig-zag, cross (double zig-zag) or dynamic which" + " will automatically switch between the first two depending" + " on the distance of the two pillars."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = ConfigOptionEnum::get_enum_names(); + def->enum_labels = ConfigOptionEnum::get_enum_names(); + def->enum_labels[0] = L("Zig-Zag"); + def->enum_labels[1] = L("Cross"); + def->enum_labels[2] = L("Dynamic"); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(SLAPillarConnectionMode::dynamic)); + + def = this->add(prefix + "support_buildplate_only", coBool); + def->label = L("Support on build plate only"); + def->category = L("Supports"); + def->tooltip = L("Only create support if it lies on a build plate. Don't create support on a print."); + def->mode = comSimple; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add(prefix + "support_pillar_widening_factor", coFloat); + def->label = L("Pillar widening factor"); + def->category = L("Supports"); + + pretext = ""; + if (prefix.empty()) + pretext = pretext_unavailable; + + def->tooltip = pretext + + L("Merging bridges or pillars into another pillars can " + "increase the radius. Zero means no increase, one means " + "full increase. The exact amount of increase is unspecified and can " + "change in the future."); + + def->min = 0; + def->max = 1; + def->mode = comExpert; + def->set_default_value(new ConfigOptionFloat(0.5)); + + def = this->add(prefix + "support_base_diameter", coFloat); + def->label = L("Support base diameter"); + def->category = L("Supports"); + def->tooltip = L("Diameter in mm of the pillar base"); + def->sidetext = L("mm"); + def->min = 0; + def->max = 30; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(4.0)); + + def = this->add(prefix + "support_base_height", coFloat); + def->label = L("Support base height"); + def->category = L("Supports"); + def->tooltip = L("The height of the pillar base cone"); + def->sidetext = L("mm"); + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(1.0)); + + def = this->add(prefix + "support_base_safety_distance", coFloat); + def->label = L("Support base safety distance"); + def->category = L("Supports"); + def->tooltip = L( + "The minimum distance of the pillar base from the model in mm. " + "Makes sense in zero elevation mode where a gap according " + "to this parameter is inserted between the model and the pad."); + def->sidetext = L("mm"); + def->min = 0; + def->max = 10; + def->mode = comExpert; + def->set_default_value(new ConfigOptionFloat(1)); + + def = this->add(prefix + "support_critical_angle", coFloat); + def->label = L("Critical angle"); + def->category = L("Supports"); + def->tooltip = L("The default angle for connecting support sticks and junctions."); + def->sidetext = L("°"); + def->min = 0; + def->max = 90; + def->mode = comExpert; + def->set_default_value(new ConfigOptionFloat(45)); + + def = this->add(prefix + "support_max_bridge_length", coFloat); + def->label = L("Max bridge length"); + def->category = L("Supports"); + def->tooltip = L("The max length of a bridge"); + def->sidetext = L("mm"); + def->min = 0; + def->mode = comAdvanced; + + double default_val = 15.0; + if (prefix == "branching") + default_val = 5.0; + + def->set_default_value(new ConfigOptionFloat(default_val)); + + pretext = ""; + if (prefix == "branching") + pretext = pretext_unavailable; + + def = this->add(prefix + "support_max_pillar_link_distance", coFloat); + def->label = L("Max pillar linking distance"); + def->category = L("Supports"); + def->tooltip = pretext + L("The max distance of two pillars to get linked with each other." + " A zero value will prohibit pillar cascading."); + def->sidetext = L("mm"); + def->min = 0; // 0 means no linking + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(10.0)); + + def = this->add(prefix + "support_object_elevation", coFloat); + def->label = L("Object elevation"); + def->category = L("Supports"); + def->tooltip = L("How much the supports should lift up the supported object. " + "If \"Pad around object\" is enabled, this value is ignored."); + def->sidetext = L("mm"); + def->min = 0; + def->max = 150; // This is the max height of print on SL1 + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(5.0)); +} + void PrintConfigDef::init_sla_params() { ConfigOptionDef* def; @@ -3613,90 +3828,12 @@ void PrintConfigDef::init_sla_params() def->enum_labels = ConfigOptionEnum::get_enum_names(); def->enum_labels[0] = L("Default"); def->enum_labels[1] = L("Branching"); - def->mode = comAdvanced; + // TODO: def->enum_labels[2] = L("Organic"); + def->mode = comSimple; def->set_default_value(new ConfigOptionEnum(sla::SupportTreeType::Default)); - def = this->add("support_head_front_diameter", coFloat); - def->label = L("Pinhead front diameter"); - def->category = L("Supports"); - def->tooltip = L("Diameter of the pointing side of the head"); - def->sidetext = L("mm"); - def->min = 0; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(0.4)); - - def = this->add("support_head_penetration", coFloat); - def->label = L("Head penetration"); - def->category = L("Supports"); - def->tooltip = L("How much the pinhead has to penetrate the model surface"); - def->sidetext = L("mm"); - def->mode = comAdvanced; - def->min = 0; - def->set_default_value(new ConfigOptionFloat(0.2)); - - def = this->add("support_head_width", coFloat); - def->label = L("Pinhead width"); - def->category = L("Supports"); - def->tooltip = L("Width from the back sphere center to the front sphere center"); - def->sidetext = L("mm"); - def->min = 0; - def->max = 20; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(1.0)); - - def = this->add("support_pillar_diameter", coFloat); - def->label = L("Pillar diameter"); - def->category = L("Supports"); - def->tooltip = L("Diameter in mm of the support pillars"); - def->sidetext = L("mm"); - def->min = 0; - def->max = 15; - def->mode = comSimple; - def->set_default_value(new ConfigOptionFloat(1.0)); - - def = this->add("support_small_pillar_diameter_percent", coPercent); - def->label = L("Small pillar diameter percent"); - def->category = L("Supports"); - def->tooltip = L("The percentage of smaller pillars compared to the normal pillar diameter " - "which are used in problematic areas where a normal pilla cannot fit."); - def->sidetext = L("%"); - def->min = 1; - def->max = 100; - def->mode = comExpert; - def->set_default_value(new ConfigOptionPercent(50)); - - def = this->add("support_max_bridges_on_pillar", coInt); - def->label = L("Max bridges on a pillar"); - def->tooltip = L( - "Maximum number of bridges that can be placed on a pillar. Bridges " - "hold support point pinheads and connect to pillars as small branches."); - def->min = 0; - def->max = 50; - def->mode = comExpert; - def->set_default_value(new ConfigOptionInt(3)); - - def = this->add("support_pillar_connection_mode", coEnum); - def->label = L("Pillar connection mode"); - def->tooltip = L("Controls the bridge type between two neighboring pillars." - " Can be zig-zag, cross (double zig-zag) or dynamic which" - " will automatically switch between the first two depending" - " on the distance of the two pillars."); - def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); - def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); - def->enum_values = ConfigOptionEnum::get_enum_names(); - def->enum_labels = ConfigOptionEnum::get_enum_names(); - def->enum_labels[0] = L("Zig-Zag"); - def->enum_labels[1] = L("Cross"); - def->enum_labels[2] = L("Dynamic"); - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionEnum(SLAPillarConnectionMode::dynamic)); - - def = this->add("support_buildplate_only", coBool); - def->label = L("Support on build plate only"); - def->category = L("Supports"); - def->tooltip = L("Only create support if it lies on a build plate. Don't create support on a print."); - def->mode = comSimple; - def->set_default_value(new ConfigOptionBool(false)); + init_sla_support_params(""); + init_sla_support_params("branching"); def = this->add("support_enforcers_only", coBool); def->label = L("Support only in enforced regions"); @@ -3705,93 +3842,6 @@ void PrintConfigDef::init_sla_params() def->mode = comSimple; def->set_default_value(new ConfigOptionBool(false)); - def = this->add("support_pillar_widening_factor", coFloat); - def->label = L("Pillar widening factor"); - def->category = L("Supports"); - def->tooltip = L( - "Merging bridges or pillars into another pillars can " - "increase the radius. Zero means no increase, one means " - "full increase. The exact amount of increase is unspecified and can " - "change in the future. What is garanteed is that thickness will not " - "exceed \"support_base_diameter\""); - - def->min = 0; - def->max = 1; - def->mode = comExpert; - def->set_default_value(new ConfigOptionFloat(0.15)); - - def = this->add("support_base_diameter", coFloat); - def->label = L("Support base diameter"); - def->category = L("Supports"); - def->tooltip = L("Diameter in mm of the pillar base"); - def->sidetext = L("mm"); - def->min = 0; - def->max = 30; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(4.0)); - - def = this->add("support_base_height", coFloat); - def->label = L("Support base height"); - def->category = L("Supports"); - def->tooltip = L("The height of the pillar base cone"); - def->sidetext = L("mm"); - def->min = 0; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(1.0)); - - def = this->add("support_base_safety_distance", coFloat); - def->label = L("Support base safety distance"); - def->category = L("Supports"); - def->tooltip = L( - "The minimum distance of the pillar base from the model in mm. " - "Makes sense in zero elevation mode where a gap according " - "to this parameter is inserted between the model and the pad."); - def->sidetext = L("mm"); - def->min = 0; - def->max = 10; - def->mode = comExpert; - def->set_default_value(new ConfigOptionFloat(1)); - - def = this->add("support_critical_angle", coFloat); - def->label = L("Critical angle"); - def->category = L("Supports"); - def->tooltip = L("The default angle for connecting support sticks and junctions."); - def->sidetext = L("°"); - def->min = 0; - def->max = 90; - def->mode = comExpert; - def->set_default_value(new ConfigOptionFloat(45)); - - def = this->add("support_max_bridge_length", coFloat); - def->label = L("Max bridge length"); - def->category = L("Supports"); - def->tooltip = L("The max length of a bridge"); - def->sidetext = L("mm"); - def->min = 0; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(15.0)); - - def = this->add("support_max_pillar_link_distance", coFloat); - def->label = L("Max pillar linking distance"); - def->category = L("Supports"); - def->tooltip = L("The max distance of two pillars to get linked with each other." - " A zero value will prohibit pillar cascading."); - def->sidetext = L("mm"); - def->min = 0; // 0 means no linking - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(10.0)); - - def = this->add("support_object_elevation", coFloat); - def->label = L("Object elevation"); - def->category = L("Supports"); - def->tooltip = L("How much the supports should lift up the supported object. " - "If \"Pad around object\" is enabled, this value is ignored."); - def->sidetext = L("mm"); - def->min = 0; - def->max = 150; // This is the max height of print on SL1 - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(5.0)); - def = this->add("support_points_density_relative", coInt); def->label = L("Support points density"); def->category = L("Supports"); @@ -4759,6 +4809,23 @@ Points get_bed_shape(const PrintConfig &cfg) Points get_bed_shape(const SLAPrinterConfig &cfg) { return to_points(cfg.bed_shape.values); } +std::string get_sla_suptree_prefix(const DynamicPrintConfig &config) +{ + const auto *suptreetype = config.option>("support_tree_type"); + std::string slatree = ""; + if (suptreetype) { + auto ttype = static_cast(suptreetype->getInt()); + switch (ttype) { + case sla::SupportTreeType::Branching: slatree = "branching"; break; + case sla::SupportTreeType::Organic: slatree = "organic"; break; + default: + ; + } + } + + return slatree; +} + } // namespace Slic3r #include diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 045200906e..85c2ca794a 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -185,6 +185,7 @@ private: void init_fff_params(); void init_extruder_option_keys(); void init_sla_params(); + void init_sla_support_params(const std::string &method_prefix); std::vector m_extruder_option_keys; std::vector m_extruder_retract_keys; @@ -861,6 +862,8 @@ PRINT_CONFIG_CLASS_DEFINE( // Generate only ground facing supports ((ConfigOptionBool, support_buildplate_only)) + ((ConfigOptionFloat, support_max_weight_on_model)) + // Generate only ground facing supports ((ConfigOptionBool, support_enforcers_only)) @@ -892,6 +895,62 @@ PRINT_CONFIG_CLASS_DEFINE( // and the model object's bounding box bottom. Units in mm. ((ConfigOptionFloat, support_object_elevation))/*= 5.0*/ + + // Branching tree + + // Diameter in mm of the pointing side of the head. + ((ConfigOptionFloat, branchingsupport_head_front_diameter))/*= 0.2*/ + + // How much the pinhead has to penetrate the model surface + ((ConfigOptionFloat, branchingsupport_head_penetration))/*= 0.2*/ + + // Width in mm from the back sphere center to the front sphere center. + ((ConfigOptionFloat, branchingsupport_head_width))/*= 1.0*/ + + // Radius in mm of the support pillars. + ((ConfigOptionFloat, branchingsupport_pillar_diameter))/*= 0.8*/ + + // The percentage of smaller pillars compared to the normal pillar diameter + // which are used in problematic areas where a normal pilla cannot fit. + ((ConfigOptionPercent, branchingsupport_small_pillar_diameter_percent)) + + // How much bridge (supporting another pinhead) can be placed on a pillar. + ((ConfigOptionInt, branchingsupport_max_bridges_on_pillar)) + + // How the pillars are bridged together + ((ConfigOptionEnum, branchingsupport_pillar_connection_mode)) + + // Generate only ground facing supports + ((ConfigOptionBool, branchingsupport_buildplate_only)) + + ((ConfigOptionFloat, branchingsupport_max_weight_on_model)) + + ((ConfigOptionFloat, branchingsupport_pillar_widening_factor)) + + // Radius in mm of the pillar base. + ((ConfigOptionFloat, branchingsupport_base_diameter))/*= 2.0*/ + + // The height of the pillar base cone in mm. + ((ConfigOptionFloat, branchingsupport_base_height))/*= 1.0*/ + + // The minimum distance of the pillar base from the model in mm. + ((ConfigOptionFloat, branchingsupport_base_safety_distance)) /*= 1.0*/ + + // The default angle for connecting support sticks and junctions. + ((ConfigOptionFloat, branchingsupport_critical_angle))/*= 45*/ + + // The max length of a bridge in mm + ((ConfigOptionFloat, branchingsupport_max_bridge_length))/*= 15.0*/ + + // The max distance of two pillars to get cross linked. + ((ConfigOptionFloat, branchingsupport_max_pillar_link_distance)) + + // The elevation in Z direction upwards. This is the space between the pad + // and the model object's bounding box bottom. Units in mm. + ((ConfigOptionFloat, branchingsupport_object_elevation))/*= 5.0*/ + + + /////// Following options influence automatic support points placement: ((ConfigOptionInt, support_points_density_relative)) ((ConfigOptionFloat, support_points_minimal_distance)) @@ -1111,6 +1170,8 @@ Points get_bed_shape(const DynamicPrintConfig &cfg); Points get_bed_shape(const PrintConfig &cfg); Points get_bed_shape(const SLAPrinterConfig &cfg); +std::string get_sla_suptree_prefix(const DynamicPrintConfig &config); + // ModelConfig is a wrapper around DynamicPrintConfig with an addition of a timestamp. // Each change of ModelConfig is tracked by assigning a new timestamp from a global counter. // The counter is used for faster synchronization of the background slicing thread diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 42bb6bf31d..abbf0fb936 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -789,10 +789,10 @@ bool PrintObject::invalidate_step(PrintObjectStep step) // propagate to dependent steps if (step == posPerimeters) { - invalidated |= this->invalidate_steps({ posPrepareInfill, posInfill, posIroning, posEstimateCurledExtrusions }); + invalidated |= this->invalidate_steps({ posPrepareInfill, posInfill, posIroning, posSupportSpotsSearch, posEstimateCurledExtrusions }); invalidated |= m_print->invalidate_steps({ psSkirtBrim }); } else if (step == posPrepareInfill) { - invalidated |= this->invalidate_steps({ posInfill, posIroning }); + invalidated |= this->invalidate_steps({ posInfill, posIroning, posSupportSpotsSearch }); } else if (step == posInfill) { invalidated |= this->invalidate_steps({ posIroning, posSupportSpotsSearch }); invalidated |= m_print->invalidate_steps({ psSkirtBrim }); diff --git a/src/libslic3r/SLA/BranchingTreeSLA.cpp b/src/libslic3r/SLA/BranchingTreeSLA.cpp index 72314c9d83..bb97126161 100644 --- a/src/libslic3r/SLA/BranchingTreeSLA.cpp +++ b/src/libslic3r/SLA/BranchingTreeSLA.cpp @@ -13,20 +13,28 @@ namespace Slic3r { namespace sla { +inline constexpr const auto &beam_ex_policy = ex_tbb; + class BranchingTreeBuilder: public branchingtree::Builder { SupportTreeBuilder &m_builder; const SupportableMesh &m_sm; const branchingtree::PointCloud &m_cloud; + std::vector m_pillars; // to put an index over them + + // cache succesfull ground connections + mutable std::map m_gnd_connections; + mutable execution::SpinningMutex m_gnd_connections_mtx; + // Scaling of the input value 'widening_factor:<0, 1>' to produce resonable // widening behaviour - static constexpr double WIDENING_SCALE = 0.02; + static constexpr double WIDENING_SCALE = 0.05; - double get_radius(const branchingtree::Node &j) + double get_radius(const branchingtree::Node &j) const { double w = WIDENING_SCALE * m_sm.cfg.pillar_widening_factor * j.weight; - return std::min(m_sm.cfg.base_radius_mm, double(j.Rmin) + w); + return double(j.Rmin) + w; } std::vector m_unroutable_pinheads; @@ -78,6 +86,44 @@ class BranchingTreeBuilder: public branchingtree::Builder { }); } + void discard_subtree_rescure(size_t root) + { + // Discard all the support points connecting to this branch. + // As a last resort, try to route child nodes to ground and stop + // traversing if any child branch succeeds. + traverse(m_cloud, root, [this](const branchingtree::Node &node) { + branchingtree::TraverseReturnT ret{true, true}; + + int suppid_parent = m_cloud.get_leaf_id(node.id); + int suppid_left = branchingtree::Node::ID_NONE; + int suppid_right = branchingtree::Node::ID_NONE; + + double glvl = ground_level(m_sm); + branchingtree::Node dst = node; + dst.pos.z() = glvl; + dst.weight += node.pos.z() - glvl; + + if (node.left >= 0 && add_ground_bridge(m_cloud.get(node.left), dst)) + ret.to_left = false; + else + suppid_left = m_cloud.get_leaf_id(node.left); + + if (node.right >= 0 && add_ground_bridge(m_cloud.get(node.right), dst)) + ret.to_right = false; + else + suppid_right = m_cloud.get_leaf_id(node.right); + + if (suppid_parent >= 0) + m_unroutable_pinheads.emplace_back(suppid_parent); + if (suppid_left >= 0) + m_unroutable_pinheads.emplace_back(suppid_left); + if (suppid_right >= 0) + m_unroutable_pinheads.emplace_back(suppid_right); + + return ret; + }); + } + public: BranchingTreeBuilder(SupportTreeBuilder &builder, const SupportableMesh &sm, @@ -98,13 +144,24 @@ public: bool add_mesh_bridge(const branchingtree::Node &from, const branchingtree::Node &to) override; + std::optional suggest_avoidance(const branchingtree::Node &from, + float max_bridge_len) const override; + void report_unroutable(const branchingtree::Node &j) override { - BOOST_LOG_TRIVIAL(error) << "Cannot route junction at " << j.pos.x() - << " " << j.pos.y() << " " << j.pos.z(); + double glvl = ground_level(m_sm); + branchingtree::Node dst = j; + dst.pos.z() = glvl; + dst.weight += j.pos.z() - glvl; + if (add_ground_bridge(j, dst)) + return; + + BOOST_LOG_TRIVIAL(warning) << "Cannot route junction at " << j.pos.x() + << " " << j.pos.y() << " " << j.pos.z(); // Discard all the support points connecting to this branch. - discard_subtree(j.id); + discard_subtree_rescure(j.id); +// discard_subtree(j.id); } const std::vector& unroutable_pinheads() const @@ -113,15 +170,28 @@ public: } bool is_valid() const override { return !m_builder.ctl().stopcondition(); } + + const std::vector & pillars() const { return m_pillars; } + + const GroundConnection *ground_conn(size_t pillar) const + { + const GroundConnection *ret = nullptr; + + auto it = m_gnd_connections.find(m_pillars[pillar].id); + if (it != m_gnd_connections.end()) + ret = &it->second; + + return ret; + } }; bool BranchingTreeBuilder::add_bridge(const branchingtree::Node &from, - const branchingtree::Node &to) + const branchingtree::Node &to) { Vec3d fromd = from.pos.cast(), tod = to.pos.cast(); double fromR = get_radius(from), toR = get_radius(to); Beam beam{Ball{fromd, fromR}, Ball{tod, toR}}; - auto hit = beam_mesh_hit(ex_tbb, m_sm.emesh, beam, + auto hit = beam_mesh_hit(beam_ex_policy , m_sm.emesh, beam, m_sm.cfg.safety_distance_mm); bool ret = hit.distance() > (tod - fromd).norm(); @@ -144,8 +214,8 @@ bool BranchingTreeBuilder::add_merger(const branchingtree::Node &node, Beam beam2{Ball{from2d, closestR}, Ball{tod, mergeR}}; auto sd = m_sm.cfg.safety_distance_mm ; - auto hit1 = beam_mesh_hit(ex_tbb, m_sm.emesh, beam1, sd); - auto hit2 = beam_mesh_hit(ex_tbb, m_sm.emesh, beam2, sd); + auto hit1 = beam_mesh_hit(beam_ex_policy , m_sm.emesh, beam1, sd); + auto hit2 = beam_mesh_hit(beam_ex_policy , m_sm.emesh, beam2, sd); bool ret = hit1.distance() > (tod - from1d).norm() && hit2.distance() > (tod - from2d).norm(); @@ -156,12 +226,31 @@ bool BranchingTreeBuilder::add_merger(const branchingtree::Node &node, bool BranchingTreeBuilder::add_ground_bridge(const branchingtree::Node &from, const branchingtree::Node &to) { - bool ret = search_ground_route(ex_tbb, m_builder, m_sm, - sla::Junction{from.pos.cast(), - get_radius(from)}, - get_radius(to)).first; + bool ret = false; - if (ret) { + namespace bgi = boost::geometry::index; + + auto it = m_gnd_connections.find(from.id); + const GroundConnection *connptr = nullptr; + + if (it == m_gnd_connections.end()) { + sla::Junction j{from.pos.cast(), get_radius(from)}; + Vec3d init_dir = (to.pos - from.pos).cast().normalized(); + + auto conn = deepsearch_ground_connection(beam_ex_policy , m_sm, j, + get_radius(to), init_dir); + + // Remember that this node was tested if can go to ground, don't + // test it with any other destination ground point because + // it is unlikely that search_ground_route would find a better solution + connptr = &(m_gnd_connections[from.id] = conn); + } else { + connptr = &(it->second); + } + + if (connptr && *connptr) { + m_pillars.emplace_back(from); + ret = true; build_subtree(from.id); } @@ -171,17 +260,20 @@ bool BranchingTreeBuilder::add_ground_bridge(const branchingtree::Node &from, bool BranchingTreeBuilder::add_mesh_bridge(const branchingtree::Node &from, const branchingtree::Node &to) { + if (from.weight > m_sm.cfg.max_weight_on_model_support) + return false; + sla::Junction fromj = {from.pos.cast(), get_radius(from)}; auto anchor = m_sm.cfg.ground_facing_only ? std::optional{} : // If no mesh connections are allowed - calculate_anchor_placement(ex_tbb, m_sm, fromj, + calculate_anchor_placement(beam_ex_policy , m_sm, fromj, to.pos.cast()); if (anchor) { sla::Junction toj = {anchor->junction_point(), anchor->r_back_mm}; - auto hit = beam_mesh_hit(ex_tbb, m_sm.emesh, + auto hit = beam_mesh_hit(beam_ex_policy , m_sm.emesh, Beam{{fromj.pos, fromj.r}, {toj.pos, toj.r}}, 0.); if (hit.distance() > distance(fromj.pos, toj.pos)) { @@ -197,6 +289,70 @@ bool BranchingTreeBuilder::add_mesh_bridge(const branchingtree::Node &from, return bool(anchor); } +static std::optional get_avoidance(const GroundConnection &conn, + float maxdist) +{ + std::optional ret; + + if (conn) { + if (conn.path.size() > 1) { + ret = conn.path[1].pos.cast(); + } else { + Vec3f pbeg = conn.path[0].pos.cast(); + Vec3f pend = conn.pillar_base->pos.cast(); + pbeg.z() = std::max(pbeg.z() - maxdist, pend.z()); + ret = pbeg; + } + } + + return ret; +} + +std::optional BranchingTreeBuilder::suggest_avoidance( + const branchingtree::Node &from, float max_bridge_len) const +{ + std::optional ret; + + double glvl = ground_level(m_sm); + branchingtree::Node dst = from; + dst.pos.z() = glvl; + dst.weight += from.pos.z() - glvl; + sla::Junction j{from.pos.cast(), get_radius(from)}; + + auto found_it = m_gnd_connections.end(); + { + std::lock_guard lk{m_gnd_connections_mtx}; + found_it = m_gnd_connections.find(from.id); + } + + if (found_it != m_gnd_connections.end()) { + ret = get_avoidance(found_it->second, max_bridge_len); + } else { + auto conn = deepsearch_ground_connection( + beam_ex_policy , m_sm, j, get_radius(dst), sla::DOWN); + + { + std::lock_guard lk{m_gnd_connections_mtx}; + m_gnd_connections[from.id] = conn; + } + + ret = get_avoidance(conn, max_bridge_len); + } + + return ret; +} + +inline void build_pillars(SupportTreeBuilder &builder, + BranchingTreeBuilder &vbuilder, + const SupportableMesh &sm) +{ + for (size_t pill_id = 0; pill_id < vbuilder.pillars().size(); ++pill_id) { + auto * conn = vbuilder.ground_conn(pill_id); + if (conn) + build_ground_connection(builder, sm, *conn); + } +} + void create_branching_tree(SupportTreeBuilder &builder, const SupportableMesh &sm) { auto coordfn = [&sm](size_t id, size_t dim) { return sm.pts[id].pos(dim); }; @@ -221,7 +377,7 @@ void create_branching_tree(SupportTreeBuilder &builder, const SupportableMesh &s for (auto &h : heads) if (h && h->is_valid()) { leafs.emplace_back(h->junction_point().cast(), h->r_back_mm); - h->id = leafs.size() - 1; + h->id = long(leafs.size() - 1); builder.add_head(h->id, *h); } @@ -240,15 +396,29 @@ void create_branching_tree(SupportTreeBuilder &builder, const SupportableMesh &s props.sampling_radius()); auto bedpts = branchingtree::sample_bed(props.bed_shape(), - props.ground_level(), + float(props.ground_level()), props.sampling_radius()); + for (auto &bp : bedpts) + bp.Rmin = sm.cfg.head_back_radius_mm; + branchingtree::PointCloud nodes{std::move(meshpts), std::move(bedpts), std::move(leafs), props}; BranchingTreeBuilder vbuilder{builder, sm, nodes}; + + execution::for_each(ex_tbb, + size_t(0), + nodes.get_leafs().size(), + [&nodes, &vbuilder](size_t leaf_idx) { + vbuilder.suggest_avoidance(nodes.get_leafs()[leaf_idx], + nodes.properties().max_branch_length()); + }); + branchingtree::build_tree(nodes, vbuilder); + build_pillars(builder, vbuilder, sm); + for (size_t id : vbuilder.unroutable_pinheads()) builder.head(id).invalidate(); diff --git a/src/libslic3r/SLA/DefaultSupportTree.cpp b/src/libslic3r/SLA/DefaultSupportTree.cpp index 9e21fca454..429cd45c6e 100644 --- a/src/libslic3r/SLA/DefaultSupportTree.cpp +++ b/src/libslic3r/SLA/DefaultSupportTree.cpp @@ -340,14 +340,17 @@ bool DefaultSupportTree::connect_to_nearpillar(const Head &head, return true; } -bool DefaultSupportTree::create_ground_pillar(const Vec3d &hjp, +bool DefaultSupportTree::create_ground_pillar(const Junction &hjp, const Vec3d &sourcedir, - double radius, long head_id) { auto [ret, pillar_id] = sla::create_ground_pillar(suptree_ex_policy, - m_builder, m_sm, hjp, - sourcedir, radius, radius, + m_builder, + m_sm, + hjp.pos, + sourcedir, + hjp.r, + hjp.r, head_id); if (pillar_id >= 0) // Save the pillar endpoint in the spatial index @@ -362,20 +365,19 @@ void DefaultSupportTree::add_pinheads() // The minimum distance for two support points to remain valid. const double /*constexpr*/ D_SP = 0.1; - // Get the points that are too close to each other and keep only the - // first one + // Get the points that are too close to each other and keep only the + // first one auto aliases = cluster(m_points, D_SP, 2); PtIndices filtered_indices; filtered_indices.reserve(aliases.size()); m_iheads.reserve(aliases.size()); - m_iheadless.reserve(aliases.size()); for(auto& a : aliases) { // Here we keep only the front point of the cluster. filtered_indices.emplace_back(a.front()); } - // calculate the normals to the triangles for filtered points + // calculate the normals to the triangles for filtered points auto nmls = normals(suptree_ex_policy, m_points, m_sm.emesh, m_sm.cfg.head_front_radius_mm, m_thr, filtered_indices); @@ -404,22 +406,22 @@ void DefaultSupportTree::add_pinheads() Vec3d n = nmls.row(Eigen::Index(i)); - // for all normals we generate the spherical coordinates and - // saturate the polar angle to 45 degrees from the bottom then - // convert back to standard coordinates to get the new normal. - // Then we just create a quaternion from the two normals - // (Quaternion::FromTwoVectors) and apply the rotation to the - // arrow head. + // for all normals we generate the spherical coordinates and + // saturate the polar angle to 45 degrees from the bottom then + // convert back to standard coordinates to get the new normal. + // Then we just create a quaternion from the two normals + // (Quaternion::FromTwoVectors) and apply the rotation to the + // arrow head. auto [polar, azimuth] = dir_to_spheric(n); - // skip if the tilt is not sane + // skip if the tilt is not sane if (polar < PI - m_sm.cfg.normal_cutoff_angle) return; - // We saturate the polar angle to 3pi/4 + // We saturate the polar angle to 3pi/4 polar = std::max(polar, PI - m_sm.cfg.bridge_slope); - // save the head (pinpoint) position + // save the head (pinpoint) position Vec3d hp = m_points.row(fidx); double lmin = m_sm.cfg.head_width_mm, lmax = lmin; @@ -428,18 +430,17 @@ void DefaultSupportTree::add_pinheads() lmin = 0., lmax = m_sm.cfg.head_penetration_mm; } - // The distance needed for a pinhead to not collide with model. + // The distance needed for a pinhead to not collide with model. double w = lmin + 2 * back_r + 2 * m_sm.cfg.head_front_radius_mm - m_sm.cfg.head_penetration_mm; double pin_r = double(m_sm.pts[fidx].head_front_radius); - // Reassemble the now corrected normal + // Reassemble the now corrected normal auto nn = spheric_to_dir(polar, azimuth).normalized(); - // check available distance - AABBMesh::hit_result t = pinhead_mesh_intersect(hp, nn, pin_r, - back_r, w); + // check available distance + AABBMesh::hit_result t = pinhead_mesh_intersect(hp, nn, pin_r, back_r, w); if (t.distance() < w) { // Let's try to optimize this angle, there might be a @@ -509,10 +510,10 @@ void DefaultSupportTree::classify() ground_head_indices.reserve(m_iheads.size()); m_iheads_onmodel.reserve(m_iheads.size()); - // First we decide which heads reach the ground and can be full - // pillars and which shall be connected to the model surface (or - // search a suitable path around the surface that leads to the - // ground -- TODO) + // First we decide which heads reach the ground and can be full + // pillars and which shall be connected to the model surface (or + // search a suitable path around the surface that leads to the + // ground -- TODO) for(unsigned i : m_iheads) { m_thr(); @@ -530,10 +531,10 @@ void DefaultSupportTree::classify() m_head_to_ground_scans[i] = hit; } - // We want to search for clusters of points that are far enough - // from each other in the XY plane to not cross their pillar bases - // These clusters of support points will join in one pillar, - // possibly in their centroid support point. + // We want to search for clusters of points that are far enough + // from each other in the XY plane to not cross their pillar bases + // These clusters of support points will join in one pillar, + // possibly in their centroid support point. auto pointfn = [this](unsigned i) { return m_builder.head(i).junction_point(); @@ -559,13 +560,13 @@ void DefaultSupportTree::routing_to_ground() for (auto &cl : m_pillar_clusters) { m_thr(); - // place all the centroid head positions into the index. We - // will query for alternative pillar positions. If a sidehead - // cannot connect to the cluster centroid, we have to search - // for another head with a full pillar. Also when there are two - // elements in the cluster, the centroid is arbitrary and the - // sidehead is allowed to connect to a nearby pillar to - // increase structural stability. + // place all the centroid head positions into the index. We + // will query for alternative pillar positions. If a sidehead + // cannot connect to the cluster centroid, we have to search + // for another head with a full pillar. Also when there are two + // elements in the cluster, the centroid is arbitrary and the + // sidehead is allowed to connect to a nearby pillar to + // increase structural stability. if (cl.empty()) continue; @@ -587,7 +588,7 @@ void DefaultSupportTree::routing_to_ground() Head &h = m_builder.head(hid); - if (!create_ground_pillar(h.junction_point(), h.dir, h.r_back_mm, h.id)) { + if (!create_ground_pillar(h.junction(), h.dir, h.id)) { BOOST_LOG_TRIVIAL(warning) << "Pillar cannot be created for support point id: " << hid; m_iheads_onmodel.emplace_back(h.id); @@ -595,11 +596,11 @@ void DefaultSupportTree::routing_to_ground() } } - // now we will go through the clusters ones again and connect the - // sidepoints with the cluster centroid (which is a ground pillar) - // or a nearby pillar if the centroid is unreachable. + // now we will go through the clusters ones again and connect the + // sidepoints with the cluster centroid (which is a ground pillar) + // or a nearby pillar if the centroid is unreachable. size_t ci = 0; - for (auto cl : m_pillar_clusters) { + for (const std::vector &cl : m_pillar_clusters) { m_thr(); auto cidx = cl_centroids[ci++]; @@ -615,10 +616,9 @@ void DefaultSupportTree::routing_to_ground() if (!connect_to_nearpillar(sidehead, centerpillarID) && !search_pillar_and_connect(sidehead)) { - Vec3d pstart = sidehead.junction_point(); // Vec3d pend = Vec3d{pstart.x(), pstart.y(), gndlvl}; // Could not find a pillar, create one - create_ground_pillar(pstart, sidehead.dir, sidehead.r_back_mm, sidehead.id); + create_ground_pillar(sidehead.junction(), sidehead.dir, sidehead.id); } } } @@ -664,10 +664,10 @@ bool DefaultSupportTree::connect_to_model_body(Head &head) zangle = std::max(zangle, PI/4); double h = std::sin(zangle) * head.fullwidth(); - // The width of the tail head that we would like to have... + // The width of the tail head that we would like to have... h = std::min(hit.distance() - head.r_back_mm, h); - // If this is a mini pillar dont bother with the tail width, can be 0. + // If this is a mini pillar dont bother with the tail width, can be 0. if (head.r_back_mm < m_sm.cfg.head_back_radius_mm) h = std::max(h, 0.); else if (h <= 0.) return false; @@ -773,23 +773,23 @@ void DefaultSupportTree::interconnect_pillars() // Ideally every pillar should be connected with at least one of its // neighbors if that neighbor is within max_pillar_link_distance - // Pillars with height exceeding H1 will require at least one neighbor - // to connect with. Height exceeding H2 require two neighbors. + // Pillars with height exceeding H1 will require at least one neighbor + // to connect with. Height exceeding H2 require two neighbors. double H1 = m_sm.cfg.max_solo_pillar_height_mm; double H2 = m_sm.cfg.max_dual_pillar_height_mm; double d = m_sm.cfg.max_pillar_link_distance_mm; - //A connection between two pillars only counts if the height ratio is - // bigger than 50% + // A connection between two pillars only counts if the height ratio is + // bigger than 50% double min_height_ratio = 0.5; std::set pairs; - // A function to connect one pillar with its neighbors. THe number of - // neighbors is given in the configuration. This function if called - // for every pillar in the pillar index. A pair of pillar will not - // be connected multiple times this is ensured by the 'pairs' set which - // remembers the processed pillar pairs + // A function to connect one pillar with its neighbors. THe number of + // neighbors is given in the configuration. This function if called + // for every pillar in the pillar index. A pair of pillar will not + // be connected multiple times this is ensured by the 'pairs' set which + // remembers the processed pillar pairs auto cascadefn = [this, d, &pairs, min_height_ratio, H1] (const PointIndexEl& el) { @@ -797,10 +797,10 @@ void DefaultSupportTree::interconnect_pillars() const Pillar& pillar = m_builder.pillar(el.second); // actual pillar - // Get the max number of neighbors a pillar should connect to + // Get the max number of neighbors a pillar should connect to unsigned neighbors = m_sm.cfg.pillar_cascade_neighbors; - // connections are already enough for the pillar + // connections are already enough for the pillar if(pillar.links >= neighbors) return; double max_d = d * pillar.r_start / m_sm.cfg.head_back_radius_mm; @@ -809,7 +809,7 @@ void DefaultSupportTree::interconnect_pillars() return distance(e.first, qp) < max_d; }); - // sort the result by distance (have to check if this is needed) + // sort the result by distance (have to check if this is needed) std::sort(qres.begin(), qres.end(), [qp](const PointIndexEl& e1, const PointIndexEl& e2){ return distance(e1.first, qp) < distance(e2.first, qp); @@ -821,23 +821,23 @@ void DefaultSupportTree::interconnect_pillars() auto a = el.second, b = re.second; - // Get unique hash for the given pair (order doesn't matter) + // Get unique hash for the given pair (order doesn't matter) auto hashval = pairhash(a, b); - // Search for the pair amongst the remembered pairs + // Search for the pair amongst the remembered pairs if(pairs.find(hashval) != pairs.end()) continue; const Pillar& neighborpillar = m_builder.pillar(re.second); - // this neighbor is occupied, skip + // this neighbor is occupied, skip if (neighborpillar.links >= neighbors) continue; if (neighborpillar.r_start < pillar.r_start) continue; if(interconnect(pillar, neighborpillar)) { pairs.insert(hashval); - // If the interconnection length between the two pillars is - // less than 50% of the longer pillar's height, don't count + // If the interconnection length between the two pillars is + // less than 50% of the longer pillar's height, don't count if(pillar.height < H1 || neighborpillar.height / pillar.height > min_height_ratio) m_builder.increment_links(pillar); @@ -848,30 +848,30 @@ void DefaultSupportTree::interconnect_pillars() } - // connections are enough for one pillar + // connections are enough for one pillar if(pillar.links >= neighbors) break; } }; - // Run the cascade for the pillars in the index + // Run the cascade for the pillars in the index m_pillar_index.foreach(cascadefn); - // We would be done here if we could allow some pillars to not be - // connected with any neighbors. But this might leave the support tree - // unprintable. - // - // The current solution is to insert additional pillars next to these - // lonely pillars. One or even two additional pillar might get inserted - // depending on the length of the lonely pillar. + // We would be done here if we could allow some pillars to not be + // connected with any neighbors. But this might leave the support tree + // unprintable. + // + // The current solution is to insert additional pillars next to these + // lonely pillars. One or even two additional pillar might get inserted + // depending on the length of the lonely pillar. size_t pillarcount = m_builder.pillarcount(); - // Again, go through all pillars, this time in the whole support tree - // not just the index. + // Again, go through all pillars, this time in the whole support tree + // not just the index. for(size_t pid = 0; pid < pillarcount; pid++) { auto pillar = [this, pid]() { return m_builder.pillar(pid); }; - // Decide how many additional pillars will be needed: + // Decide how many additional pillars will be needed: unsigned needpillars = 0; if (pillar().bridges > m_sm.cfg.max_bridges_on_pillar) @@ -887,17 +887,17 @@ void DefaultSupportTree::interconnect_pillars() needpillars = std::max(pillar().links, needpillars) - pillar().links; if (needpillars == 0) continue; - // Search for new pillar locations: + // Search for new pillar locations: bool found = false; double alpha = 0; // goes to 2Pi double r = 2 * m_sm.cfg.base_radius_mm; Vec3d pillarsp = pillar().startpoint(); - // temp value for starting point detection + // temp value for starting point detection Vec3d sp(pillarsp.x(), pillarsp.y(), pillarsp.z() - r); - // A vector of bool for placement feasbility + // A vector of bool for placement feasbility std::vector canplace(needpillars, false); std::vector spts(needpillars); // vector of starting points @@ -916,12 +916,12 @@ void DefaultSupportTree::interconnect_pillars() s.y() += std::sin(a) * r; spts[n] = s; - // Check the path vertically down + // Check the path vertically down Vec3d check_from = s + Vec3d{0., 0., pillar().r_start}; auto hr = bridge_mesh_intersect(check_from, DOWN, pillar().r_start); Vec3d gndsp{s.x(), s.y(), gnd}; - // If the path is clear, check for pillar base collisions + // If the path is clear, check for pillar base collisions canplace[n] = std::isinf(hr.distance()) && std::sqrt(m_sm.emesh.squared_distance(gndsp)) > min_dist; @@ -930,7 +930,7 @@ void DefaultSupportTree::interconnect_pillars() found = std::all_of(canplace.begin(), canplace.end(), [](bool v) { return v; }); - // 20 angles will be tried... + // 20 angles will be tried... alpha += 0.1 * PI; } diff --git a/src/libslic3r/SLA/DefaultSupportTree.hpp b/src/libslic3r/SLA/DefaultSupportTree.hpp index ff0269978d..08990a8df9 100644 --- a/src/libslic3r/SLA/DefaultSupportTree.hpp +++ b/src/libslic3r/SLA/DefaultSupportTree.hpp @@ -1,7 +1,7 @@ #ifndef LEGACYSUPPORTTREE_HPP #define LEGACYSUPPORTTREE_HPP -#include "SupportTreeUtils.hpp" +#include "SupportTreeUtilsLegacy.hpp" #include #include @@ -62,7 +62,6 @@ class DefaultSupportTree { PtIndices m_iheads; // support points with pinhead PtIndices m_iheads_onmodel; - PtIndices m_iheadless; // headless support points std::map m_head_to_ground_scans; @@ -176,9 +175,8 @@ class DefaultSupportTree { // jp is the starting junction point which needs to be routed down. // sourcedir is the allowed direction of an optional bridge between the // jp junction and the final pillar. - bool create_ground_pillar(const Vec3d &jp, + bool create_ground_pillar(const Junction &jp, const Vec3d &sourcedir, - double radius, long head_id = SupportTreeNode::ID_UNSET); void add_pillar_base(long pid) diff --git a/src/libslic3r/SLA/SupportTree.cpp b/src/libslic3r/SLA/SupportTree.cpp index b221c4330a..cfafdf7e97 100644 --- a/src/libslic3r/SLA/SupportTree.cpp +++ b/src/libslic3r/SLA/SupportTree.cpp @@ -18,6 +18,8 @@ #include #include +#include + //! macro used to mark string used at localization, //! return same string #define L(s) Slic3r::I18N::translate(s) @@ -30,6 +32,9 @@ indexed_triangle_set create_support_tree(const SupportableMesh &sm, auto builder = make_unique(ctl); if (sm.cfg.enabled) { + Benchmark bench; + bench.start(); + switch (sm.cfg.tree_type) { case SupportTreeType::Default: { create_default_tree(*builder, sm); @@ -39,9 +44,18 @@ indexed_triangle_set create_support_tree(const SupportableMesh &sm, create_branching_tree(*builder, sm); break; } + case SupportTreeType::Organic: { + // TODO + } default:; } + bench.stop(); + + BOOST_LOG_TRIVIAL(info) << "Support tree creation took: " + << bench.getElapsedSec() + << " seconds"; + builder->merge_and_cleanup(); // clean metadata, leave only the meshes. } diff --git a/src/libslic3r/SLA/SupportTree.hpp b/src/libslic3r/SLA/SupportTree.hpp index ea785e640a..53fb16f6e7 100644 --- a/src/libslic3r/SLA/SupportTree.hpp +++ b/src/libslic3r/SLA/SupportTree.hpp @@ -50,7 +50,7 @@ struct SupportTreeConfig // when bridges and pillars are merged. The resulting pillar should be a bit // thicker than the ones merging into it. How much thicker? I don't know // but it will be derived from this value. - double pillar_widening_factor = .05; + double pillar_widening_factor = .5; // Radius in mm of the pillar base. double base_radius_mm = 2.0; @@ -76,12 +76,20 @@ struct SupportTreeConfig double pillar_base_safety_distance_mm = 0.5; unsigned max_bridges_on_pillar = 3; - + + double max_weight_on_model_support = 10.f; + double head_fullwidth() const { return 2 * head_front_radius_mm + head_width_mm + 2 * head_back_radius_mm - head_penetration_mm; } + double safety_distance() const { return safety_distance_mm; } + double safety_distance(double r) const + { + return std::min(safety_distance_mm, r * safety_distance_mm / head_back_radius_mm); + } + // ///////////////////////////////////////////////////////////////////////// // Compile time configuration values (candidates for runtime) // ///////////////////////////////////////////////////////////////////////// @@ -89,13 +97,15 @@ struct SupportTreeConfig // The max Z angle for a normal at which it will get completely ignored. static const double constexpr normal_cutoff_angle = 150.0 * M_PI / 180.0; - // The shortest distance of any support structure from the model surface + // The safety gap between a support structure and model body. For support + // struts smaller than head_back_radius, the safety distance is scaled + // down accordingly. see method safety_distance() static const double constexpr safety_distance_mm = 0.5; static const double constexpr max_solo_pillar_height_mm = 15.0; static const double constexpr max_dual_pillar_height_mm = 35.0; - static const double constexpr optimizer_rel_score_diff = 1e-6; - static const unsigned constexpr optimizer_max_iterations = 1000; + static const double constexpr optimizer_rel_score_diff = 1e-10; + static const unsigned constexpr optimizer_max_iterations = 2000; static const unsigned constexpr pillar_cascade_neighbors = 3; }; @@ -116,11 +126,11 @@ struct SupportableMesh : emesh{trmsh}, pts{sp}, cfg{c} {} - explicit SupportableMesh(const AABBMesh &em, - const SupportPoints &sp, - const SupportTreeConfig &c) - : emesh{em}, pts{sp}, cfg{c} - {} +// explicit SupportableMesh(const AABBMesh &em, +// const SupportPoints &sp, +// const SupportTreeConfig &c) +// : emesh{em}, pts{sp}, cfg{c} +// {} }; inline double ground_level(const SupportableMesh &sm) diff --git a/src/libslic3r/SLA/SupportTreeBuilder.hpp b/src/libslic3r/SLA/SupportTreeBuilder.hpp index 29d34ab8e3..93fbead997 100644 --- a/src/libslic3r/SLA/SupportTreeBuilder.hpp +++ b/src/libslic3r/SLA/SupportTreeBuilder.hpp @@ -67,25 +67,32 @@ struct SupportTreeNode long id = ID_UNSET; // For identification withing a tree. }; +// A junction connecting bridges and pillars +struct Junction: public SupportTreeNode { + double r = 1; + Vec3d pos; + + Junction(const Vec3d &tr, double r_mm) : r(r_mm), pos(tr) {} +}; + // A pinhead originating from a support point struct Head: public SupportTreeNode { Vec3d dir = DOWN; Vec3d pos = {0, 0, 0}; - + double r_back_mm = 1; double r_pin_mm = 0.5; double width_mm = 2; double penetration_mm = 0.5; - // If there is a pillar connecting to this head, then the id will be set. long pillar_id = ID_UNSET; - + long bridge_id = ID_UNSET; - + inline void invalidate() { id = ID_UNSET; } inline bool is_valid() const { return id >= 0; } - + Head(double r_big_mm, double r_small_mm, double length_mm, @@ -103,21 +110,21 @@ struct Head: public SupportTreeNode { { return real_width() - penetration_mm; } - + + inline Junction junction() const + { + Junction j{pos + (fullwidth() - r_back_mm) * dir, r_back_mm}; + j.id = -this->id; // Remember that this junction is from a head + + return j; + } + inline Vec3d junction_point() const { - return pos + (fullwidth() - r_back_mm) * dir; + return junction().pos; } }; -// A junction connecting bridges and pillars -struct Junction: public SupportTreeNode { - double r = 1; - Vec3d pos; - - Junction(const Vec3d &tr, double r_mm) : r(r_mm), pos(tr) {} -}; - // A straight pillar. Only has an endpoint and a height. No explicit starting // point is given, as it would allow the pillar to be angled. // Some connection info with other primitives can also be tracked. @@ -189,6 +196,10 @@ struct DiffBridge: public Bridge { DiffBridge(const Vec3d &p_s, const Vec3d &p_e, double r_s, double r_e) : Bridge{p_s, p_e, r_s}, end_r{r_e} {} + + DiffBridge(const Junction &j_s, const Junction &j_e) + : Bridge{j_s.pos, j_e.pos, j_s.r}, end_r{j_e.r} + {} }; // This class will hold the support tree parts (not meshes, but logical parts) diff --git a/src/libslic3r/SLA/SupportTreeMesher.cpp b/src/libslic3r/SLA/SupportTreeMesher.cpp index 3f0b6c8413..6d91de7e62 100644 --- a/src/libslic3r/SLA/SupportTreeMesher.cpp +++ b/src/libslic3r/SLA/SupportTreeMesher.cpp @@ -165,7 +165,8 @@ indexed_triangle_set halfcone(double baseheight, { assert(steps > 0); - if (baseheight <= 0 || steps <= 0) return {}; + if (baseheight <= 0 || steps <= 0 || (r_bottom <= 0. && r_top <= 0.)) + return {}; indexed_triangle_set base; diff --git a/src/libslic3r/SLA/SupportTreeStrategies.hpp b/src/libslic3r/SLA/SupportTreeStrategies.hpp index 487f43575b..2cef1a8a94 100644 --- a/src/libslic3r/SLA/SupportTreeStrategies.hpp +++ b/src/libslic3r/SLA/SupportTreeStrategies.hpp @@ -5,7 +5,7 @@ namespace Slic3r { namespace sla { -enum class SupportTreeType { Default, Branching }; +enum class SupportTreeType { Default, Branching, Organic }; enum class PillarConnectionMode { zigzag, cross, dynamic }; }} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/SupportTreeUtils.hpp b/src/libslic3r/SLA/SupportTreeUtils.hpp index 38515f8793..aebd50950b 100644 --- a/src/libslic3r/SLA/SupportTreeUtils.hpp +++ b/src/libslic3r/SLA/SupportTreeUtils.hpp @@ -6,10 +6,13 @@ #include #include +#include #include #include #include +#include +#include #include namespace Slic3r { namespace sla { @@ -23,36 +26,12 @@ using Slic3r::opt::AlgNLoptGenetic; using Slic3r::Geometry::dir_to_spheric; using Slic3r::Geometry::spheric_to_dir; -// Helper function for pillar interconnection where pairs of already connected -// pillars should be checked for not to be processed again. This can be done -// in constant time with a set of hash values uniquely representing a pair of -// integers. The order of numbers within the pair should not matter, it has -// the same unique hash. The hash value has to have twice as many bits as the -// arguments need. If the same integral type is used for args and return val, -// make sure the arguments use only the half of the type's bit depth. -template> -IntegerOnly pairhash(I a, I b) -{ - using std::ceil; using std::log2; using std::max; using std::min; - static const auto constexpr Ibits = int(sizeof(I) * CHAR_BIT); - static const auto constexpr DoubleIbits = int(sizeof(DoubleI) * CHAR_BIT); - static const auto constexpr shift = DoubleIbits / 2 < Ibits ? Ibits / 2 : Ibits; - - I g = min(a, b), l = max(a, b); - - // Assume the hash will fit into the output variable - assert((g ? (ceil(log2(g))) : 0) <= shift); - assert((l ? (ceil(log2(l))) : 0) <= shift); - - return (DoubleI(g) << shift) + l; -} - // Give points on a 3D ring with given center, radius and orientation // method based on: // https://math.stackexchange.com/questions/73237/parametric-equation-of-a-circle-in-3d-space template class PointRing { - std::array m_phis; + std::array m_phis; // Two vectors that will be perpendicular to each other and to the // axis. Values for a(X) and a(Y) are now arbitrary, a(Z) is just a @@ -72,7 +51,7 @@ class PointRing { public: - PointRing(const Vec3d &n) : m_phis{linspace_array(0., 2 * PI)} + PointRing(const Vec3d &n) : m_phis{linspace_array(0., 2 * PI)} { // We have to address the case when the direction vector v (same as // dir) is coincident with one of the world axes. In this case two of @@ -91,7 +70,10 @@ public: Vec3d get(size_t idx, const Vec3d &src, double r) const { - double phi = m_phis[idx]; + if (idx == 0) + return src; + + double phi = m_phis[idx - 1]; double sinphi = std::sin(phi); double cosphi = std::cos(phi); @@ -131,31 +113,40 @@ inline StopCriteria get_criteria(const SupportTreeConfig &cfg) // A simple sphere with a center and a radius struct Ball { Vec3d p; double R; }; -struct Beam { // Defines a set of rays displaced along a cone's surface - static constexpr size_t SAMPLES = 8; +template +struct Beam_ { // Defines a set of rays displaced along a cone's surface + static constexpr size_t SAMPLES = Samples; - Vec3d src; - Vec3d dir; + Vec3d src; + Vec3d dir; double r1; double r2; // radius of the beam 1 unit further from src in dir direction - Beam(const Vec3d &s, const Vec3d &d, double R1, double R2): - src{s}, dir{d}, r1{R1}, r2{R2} {}; + Beam_(const Vec3d &s, const Vec3d &d, double R1, double R2) + : src{s}, dir{d}, r1{R1}, r2{R2} {}; - Beam(const Ball &src_ball, const Ball &dst_ball): - src{src_ball.p}, dir{dirv(src_ball.p, dst_ball.p)}, r1{src_ball.R} + Beam_(const Ball &src_ball, const Ball &dst_ball) + : src{src_ball.p}, dir{dirv(src_ball.p, dst_ball.p)}, r1{src_ball.R} { - r2 = src_ball.R + - (dst_ball.R - src_ball.R) / distance(src_ball.p, dst_ball.p); + r2 = src_ball.R; + auto d = distance(src_ball.p, dst_ball.p); + + if (d > EPSILON) + r2 += (dst_ball.R - src_ball.R) / d; } - Beam(const Vec3d &s, const Vec3d &d, double R) + Beam_(const Vec3d &s, const Vec3d &d, double R) : src{s}, dir{d}, r1{R}, r2{R} {} }; -template -Hit beam_mesh_hit(Ex ex, const AABBMesh &mesh, const Beam &beam, double sd) +using Beam = Beam_<>; + +template +Hit beam_mesh_hit(Ex policy, + const AABBMesh &mesh, + const Beam_ &beam, + double sd) { Vec3d src = beam.src; Vec3d dst = src + beam.dir; @@ -164,19 +155,19 @@ Hit beam_mesh_hit(Ex ex, const AABBMesh &mesh, const Beam &beam, double sd) Vec3d D = (dst - src); Vec3d dir = D.normalized(); - PointRing ring{dir}; + PointRing ring{dir}; using Hit = AABBMesh::hit_result; // Hit results - std::array hits; + std::array hits; execution::for_each( - ex, size_t(0), hits.size(), + policy, size_t(0), hits.size(), [&mesh, r_src, r_dst, src, dst, &ring, dir, sd, &hits](size_t i) { Hit &hit = hits[i]; - // Point on the circle on the pin sphere + // Point on the circle on the pin sphere Vec3d p_src = ring.get(i, src, r_src + sd); Vec3d p_dst = ring.get(i, dst, r_dst + sd); Vec3d raydir = (p_dst - p_src).normalized(); @@ -193,7 +184,7 @@ Hit beam_mesh_hit(Ex ex, const AABBMesh &mesh, const Beam &beam, double sd) } } else hit = hr; - }, std::min(execution::max_concurrency(ex), Beam::SAMPLES)); + }, std::min(execution::max_concurrency(policy), RayCount)); return min_hit(hits.begin(), hits.end()); } @@ -208,7 +199,10 @@ Hit pinhead_mesh_hit(Ex ex, double width, double sd) { - static const size_t SAMPLES = 8; + // Support tree generation speed depends heavily on this value. 8 is almost + // ok, but to prevent rare cases of collision, 16 is necessary, which makes + // the algorithm run about 60% longer. + static const size_t SAMPLES = 16; // Move away slightly from the touching point to avoid raycasting on the // inner surface of the mesh. @@ -283,199 +277,12 @@ Hit pinhead_mesh_hit(Ex ex, template Hit pinhead_mesh_hit(Ex ex, - const AABBMesh &mesh, - const Head &head, - double safety_d) + const AABBMesh &mesh, + const Head &head, + double safety_d) { return pinhead_mesh_hit(ex, mesh, head.pos, head.dir, head.r_pin_mm, - head.r_back_mm, head.width_mm, safety_d); -} - -template -std::optional search_widening_path(Ex policy, - const SupportableMesh &sm, - const Vec3d &jp, - const Vec3d &dir, - double radius, - double new_radius) -{ - double w = radius + 2 * sm.cfg.head_back_radius_mm; - double stopval = w + jp.z() - ground_level(sm); - Optimizer solver(get_criteria(sm.cfg).stop_score(stopval)); - - auto [polar, azimuth] = dir_to_spheric(dir); - - double fallback_ratio = radius / sm.cfg.head_back_radius_mm; - - auto oresult = solver.to_max().optimize( - [&policy, &sm, jp, radius, new_radius](const opt::Input<3> &input) { - auto &[plr, azm, t] = input; - - auto d = spheric_to_dir(plr, azm).normalized(); - - auto sd = new_radius * sm.cfg.safety_distance_mm / - sm.cfg.head_back_radius_mm; - - double ret = pinhead_mesh_hit(policy, sm.emesh, jp, d, radius, - new_radius, t, sd) - .distance(); - - Beam beam{jp + t * d, d, new_radius}; - double down = beam_mesh_hit(policy, sm.emesh, beam, sd).distance(); - - if (ret > t && std::isinf(down)) - ret += jp.z() - ground_level(sm); - - return ret; - }, - initvals({polar, azimuth, w}), // start with what we have - bounds({ - {PI - sm.cfg.bridge_slope, PI}, // Must not exceed the slope limit - {-PI, PI}, // azimuth can be a full search - {radius + sm.cfg.head_back_radius_mm, - fallback_ratio * sm.cfg.max_bridge_length_mm} - })); - - if (oresult.score >= stopval) { - polar = std::get<0>(oresult.optimum); - azimuth = std::get<1>(oresult.optimum); - double t = std::get<2>(oresult.optimum); - Vec3d endp = jp + t * spheric_to_dir(polar, azimuth); - - return DiffBridge(jp, endp, radius, sm.cfg.head_back_radius_mm); - } - - return {}; -} - -// This is a proxy function for pillar creation which will mind the gap -// between the pad and the model bottom in zero elevation mode. -// 'pinhead_junctionpt' is the starting junction point which needs to be -// routed down. sourcedir is the allowed direction of an optional bridge -// between the jp junction and the final pillar. -template -std::pair create_ground_pillar( - Ex policy, - SupportTreeBuilder &builder, - const SupportableMesh &sm, - const Vec3d &pinhead_junctionpt, - const Vec3d &sourcedir, - double radius, - double end_radius, - long head_id = SupportTreeNode::ID_UNSET) -{ - Vec3d jp = pinhead_junctionpt, endp = jp, dir = sourcedir; - long pillar_id = SupportTreeNode::ID_UNSET; - bool can_add_base = false, non_head = false; - - double gndlvl = 0.; // The Z level where pedestals should be - double jp_gnd = 0.; // The lowest Z where a junction center can be - double gap_dist = 0.; // The gap distance between the model and the pad - - double r2 = radius + (end_radius - radius) / (jp.z() - ground_level(sm)); - - auto to_floor = [&gndlvl](const Vec3d &p) { return Vec3d{p.x(), p.y(), gndlvl}; }; - - auto eval_limits = [&sm, &radius, &can_add_base, &gndlvl, &gap_dist, &jp_gnd] - (bool base_en = true) - { - can_add_base = base_en && radius >= sm.cfg.head_back_radius_mm; - double base_r = can_add_base ? sm.cfg.base_radius_mm : 0.; - gndlvl = ground_level(sm); - if (!can_add_base) gndlvl -= sm.pad_cfg.wall_thickness_mm; - jp_gnd = gndlvl + (can_add_base ? 0. : sm.cfg.head_back_radius_mm); - gap_dist = sm.cfg.pillar_base_safety_distance_mm + base_r + EPSILON; - }; - - eval_limits(); - - // We are dealing with a mini pillar that's potentially too long - if (radius < sm.cfg.head_back_radius_mm && jp.z() - gndlvl > 20 * radius) - { - std::optional diffbr = - search_widening_path(policy, sm, jp, dir, radius, - sm.cfg.head_back_radius_mm); - - if (diffbr && diffbr->endp.z() > jp_gnd) { - auto &br = builder.add_diffbridge(*diffbr); - if (head_id >= 0) builder.head(head_id).bridge_id = br.id; - endp = diffbr->endp; - radius = diffbr->end_r; - builder.add_junction(endp, radius); - non_head = true; - dir = diffbr->get_dir(); - eval_limits(); - } else return {false, pillar_id}; - } - - if (sm.cfg.object_elevation_mm < EPSILON) - { - // get a suitable direction for the corrector bridge. It is the - // original sourcedir's azimuth but the polar angle is saturated to the - // configured bridge slope. - auto [polar, azimuth] = dir_to_spheric(dir); - polar = PI - sm.cfg.bridge_slope; - Vec3d d = spheric_to_dir(polar, azimuth).normalized(); - auto sd = radius * sm.cfg.safety_distance_mm / sm.cfg.head_back_radius_mm; - double t = beam_mesh_hit(policy, sm.emesh, Beam{endp, d, radius, r2}, sd).distance(); - double tmax = std::min(sm.cfg.max_bridge_length_mm, t); - t = 0.; - - double zd = endp.z() - jp_gnd; - double tmax2 = zd / std::sqrt(1 - sm.cfg.bridge_slope * sm.cfg.bridge_slope); - tmax = std::min(tmax, tmax2); - - Vec3d nexp = endp; - double dlast = 0.; - while (((dlast = std::sqrt(sm.emesh.squared_distance(to_floor(nexp)))) < gap_dist || - !std::isinf(beam_mesh_hit(policy, sm.emesh, Beam{nexp, DOWN, radius, r2}, sd).distance())) && - t < tmax) - { - t += radius; - nexp = endp + t * d; - } - - if (dlast < gap_dist && can_add_base) { - nexp = endp; - t = 0.; - can_add_base = false; - eval_limits(can_add_base); - - zd = endp.z() - jp_gnd; - tmax2 = zd / std::sqrt(1 - sm.cfg.bridge_slope * sm.cfg.bridge_slope); - tmax = std::min(tmax, tmax2); - - while (((dlast = std::sqrt(sm.emesh.squared_distance(to_floor(nexp)))) < gap_dist || - !std::isinf(beam_mesh_hit(policy, sm.emesh, Beam{nexp, DOWN, radius}, sd).distance())) && t < tmax) { - t += radius; - nexp = endp + t * d; - } - } - - // Could not find a path to avoid the pad gap - if (dlast < gap_dist) return {false, pillar_id}; - - if (t > 0.) { // Need to make additional bridge - const Bridge& br = builder.add_bridge(endp, nexp, radius); - if (head_id >= 0) builder.head(head_id).bridge_id = br.id; - - builder.add_junction(nexp, radius); - endp = nexp; - non_head = true; - } - } - - Vec3d gp = to_floor(endp); - double h = endp.z() - gp.z(); - - pillar_id = head_id >= 0 && !non_head ? builder.add_pillar(head_id, h) : - builder.add_pillar(gp, h, radius, end_radius); - - if (can_add_base) - builder.add_pillar_base(pillar_id, sm.cfg.base_height_mm, - sm.cfg.base_radius_mm); - - return {true, pillar_id}; + head.r_back_mm, head.width_mm, safety_d); } inline double distance(const SupportPoint &a, const SupportPoint &b) @@ -530,13 +337,13 @@ bool optimize_pinhead_placement(Ex policy, double back_r = head.r_back_mm; - // skip if the tilt is not sane + // skip if the tilt is not sane if (polar < PI - m.cfg.normal_cutoff_angle) return false; - // We saturate the polar angle to 3pi/4 + // We saturate the polar angle to 3pi/4 polar = std::max(polar, PI - m.cfg.bridge_slope); - // save the head (pinpoint) position + // save the head (pinpoint) position Vec3d hp = head.pos; double lmin = m.cfg.head_width_mm, lmax = lmin; @@ -545,28 +352,26 @@ bool optimize_pinhead_placement(Ex policy, lmin = 0., lmax = m.cfg.head_penetration_mm; } - // The distance needed for a pinhead to not collide with model. + // The distance needed for a pinhead to not collide with model. double w = lmin + 2 * back_r + 2 * m.cfg.head_front_radius_mm - m.cfg.head_penetration_mm; double pin_r = head.r_pin_mm; - // Reassemble the now corrected normal + // Reassemble the now corrected normal auto nn = spheric_to_dir(polar, azimuth).normalized(); - double sd = back_r * m.cfg.safety_distance_mm / - m.cfg.head_back_radius_mm; + double sd = m.cfg.safety_distance(back_r); - // check available distance - Hit t = pinhead_mesh_hit(policy, m.emesh, hp, nn, pin_r, back_r, w, - sd); + // check available distance + Hit t = pinhead_mesh_hit(policy, m.emesh, hp, nn, pin_r, back_r, w, sd); if (t.distance() < w) { // Let's try to optimize this angle, there might be a // viable normal that doesn't collide with the model // geometry and its very close to the default. - Optimizer solver(get_criteria(m.cfg).stop_score(w).max_iterations(100)); + Optimizer solver(get_criteria(m.cfg).stop_score(w).max_iterations(100)); solver.seed(0); // we want deterministic behavior auto oresult = solver.to_max().optimize( @@ -627,7 +432,7 @@ std::optional calculate_pinhead_placement(Ex policy, }; if (optimize_pinhead_placement(policy, sm, head)) { - head.id = suppt_idx; + head.id = long(suppt_idx); return head; } @@ -635,81 +440,335 @@ std::optional calculate_pinhead_placement(Ex policy, return {}; } -template -std::pair connect_to_ground(Ex policy, - SupportTreeBuilder &builder, - const SupportableMesh &sm, - const Junction &j, - const Vec3d &dir, - double end_r) +struct GroundConnection { + // Currently, a ground connection will contain at most 2 additional junctions + // which will not require any allocations. If I come up with an algo that + // can produce a route to ground with more junctions, this design will be + // able to handle that. + static constexpr size_t MaxExpectedJunctions = 3; + + boost::container::small_vector path; + std::optional pillar_base; + + operator bool() const { return pillar_base.has_value() && !path.empty(); } +}; + +inline long build_ground_connection(SupportTreeBuilder &builder, + const SupportableMesh &sm, + const GroundConnection &conn) { - auto hjp = j.pos; - double r = j.r; - auto sd = r * sm.cfg.safety_distance_mm / sm.cfg.head_back_radius_mm; - double r2 = j.r + (end_r - j.r) / (j.pos.z() - ground_level(sm)); + long ret = SupportTreeNode::ID_UNSET; - double t = beam_mesh_hit(policy, sm.emesh, Beam{hjp, dir, r, r2}, sd).distance(); - double d = 0, tdown = 0; - t = std::min(t, sm.cfg.max_bridge_length_mm * r / sm.cfg.head_back_radius_mm); + if (!conn) + return ret; - while (d < t && - !std::isinf(tdown = beam_mesh_hit(policy, sm.emesh, - Beam{hjp + d * dir, DOWN, r, r2}, sd) - .distance())) { - d += r; + auto it = conn.path.begin(); + auto itnx = std::next(it); + + while (itnx != conn.path.end()) { + builder.add_diffbridge(*it, *itnx); + builder.add_junction(*itnx); + ++it; ++itnx; } - if(!std::isinf(tdown)) - return {false, SupportTreeNode::ID_UNSET}; + auto gp = conn.path.back().pos; + gp.z() = ground_level(sm); + double h = conn.path.back().pos.z() - gp.z(); - Vec3d endp = hjp + d * dir; - auto ret = create_ground_pillar(policy, builder, sm, endp, dir, r, end_r); + if (conn.pillar_base->r_top < sm.cfg.head_back_radius_mm) { + h += sm.pad_cfg.wall_thickness_mm; + gp.z() -= sm.pad_cfg.wall_thickness_mm; + } - if (ret.second >= 0) { - builder.add_bridge(hjp, endp, r); - builder.add_junction(endp, r); +// TODO: does not work yet +// if (conn.path.back().id < 0) { +// // this is a head +// long head_id = std::abs(conn.path.back().id); +// ret = builder.add_pillar(head_id, h); +// } else + + ret = builder.add_pillar(gp, h, conn.path.back().r, conn.pillar_base->r_top); + + if (conn.pillar_base->r_top >= sm.cfg.head_back_radius_mm) + builder.add_pillar_base(ret, conn.pillar_base->height, conn.pillar_base->r_bottom); + + return ret; +} + +template +constexpr bool IsWideningFn = std::is_invocable_r_v; + +// A widening function can determine how many ray samples should a beam contain +// (see in beam_mesh_hit) +template struct BeamSamples { static constexpr size_t Value = 8; }; +template constexpr size_t BeamSamplesV = BeamSamples>::Value; + +// To use with check_ground_route, full will check the bridge and the pillar, +// PillarOnly checks only the pillar for collisions. +enum class GroundRouteCheck { Full, PillarOnly }; + +// Returns the collision point with mesh if there is a collision or a ground point, +// given a source point with a direction of a potential avoidance bridge and +// a bridge length. +template> > +Vec3d check_ground_route( + Ex policy, + const SupportableMesh &sm, + const Junction &source, // source location + const Vec3d &dir, // direction of the bridge from the source + double bridge_len, // lenght of the avoidance bridge + WideningFn &&wideningfn, // Widening strategy + GroundRouteCheck type = GroundRouteCheck::Full + ) +{ + static const constexpr auto Samples = BeamSamplesV; + + Vec3d ret; + + const auto sd = sm.cfg.safety_distance(source.r); + const auto gndlvl = ground_level(sm); + + // Intersection of the suggested bridge with ground plane. If the bridge + // spans below ground, stop it at ground level. + double t = (gndlvl - source.pos.z()) / dir.z(); + bridge_len = std::min(t, bridge_len); + + Vec3d bridge_end = source.pos + bridge_len * dir; + + double down_l = bridge_end.z() - gndlvl; + double bridge_r = wideningfn(Ball{source.pos, source.r}, dir, bridge_len); + double brhit_dist = 0.; + + if (bridge_len > EPSILON && type == GroundRouteCheck::Full) { + // beam_mesh_hit with a zero lenght bridge is invalid + + Beam_ bridgebeam{Ball{source.pos, source.r}, + Ball{bridge_end, bridge_r}}; + + auto brhit = beam_mesh_hit(policy, sm.emesh, bridgebeam, sd); + brhit_dist = brhit.distance(); + } else { + brhit_dist = bridge_len; + } + + if (brhit_dist < bridge_len) { + ret = (source.pos + brhit_dist * dir); + } else if (down_l > 0.) { + // check if pillar can be placed below + auto gp = Vec3d{bridge_end.x(), bridge_end.y(), gndlvl}; + double end_radius = wideningfn( + Ball{bridge_end, bridge_r}, DOWN, bridge_end.z() - gndlvl); + + Beam_ gndbeam {{bridge_end, bridge_r}, {gp, end_radius}}; + auto gndhit = beam_mesh_hit(policy, sm.emesh, gndbeam, sd); + double gnd_hit_d = std::min(gndhit.distance(), down_l + EPSILON); + + if (source.r >= sm.cfg.head_back_radius_mm && gndhit.distance() > down_l && sm.cfg.object_elevation_mm < EPSILON) { + // Dealing with zero elevation mode, to not route pillars + // into the gap between the optional pad and the model + double gap = std::sqrt(sm.emesh.squared_distance(gp)); + double base_r = std::max(sm.cfg.base_radius_mm, end_radius); + double min_gap = sm.cfg.pillar_base_safety_distance_mm + base_r; + + if (gap < min_gap) { + gnd_hit_d = down_l - min_gap + gap; + } + } + + ret = Vec3d{bridge_end.x(), bridge_end.y(), bridge_end.z() - gnd_hit_d}; + } else { + ret = bridge_end; } return ret; } -template -std::pair search_ground_route(Ex policy, - SupportTreeBuilder &builder, - const SupportableMesh &sm, - const Junction &j, - double end_radius, - const Vec3d &init_dir = DOWN) +// Searching a ground connection from an arbitrary source point. +// Currently, the result will contain one avoidance bridge (at most) and a +// pillar to the ground, if it's feasible +template> > +GroundConnection deepsearch_ground_connection( + Ex policy, + const SupportableMesh &sm, + const Junction &source, + WideningFn &&wideningfn, + const Vec3d &init_dir = DOWN) { - double downdst = j.pos.z() - ground_level(sm); + constexpr unsigned MaxIterationsGlobal = 5000; + constexpr unsigned MaxIterationsLocal = 100; + constexpr double RelScoreDiff = 0.05; - auto res = connect_to_ground(policy, builder, sm, j, init_dir, end_radius); - if (res.first) - return res; + const auto gndlvl = ground_level(sm); - // Optimize bridge direction: - // Straight path failed so we will try to search for a suitable - // direction out of the cavity. - auto [polar, azimuth] = dir_to_spheric(init_dir); + // The used solver (AlgNLoptMLSL_Subplx search method) is composed of a global (MLSL) + // and a local (Subplex) search method. Criteria can be set in a way that + // local searches are quick and less accurate. The global method will only + // consider the max iteration number and the stop score (Z level <= ground) - Optimizer solver(get_criteria(sm.cfg).stop_score(1e6)); - solver.seed(0); // we want deterministic behavior + auto criteria = get_criteria(sm.cfg); // get defaults from cfg + criteria.max_iterations(MaxIterationsGlobal); + criteria.abs_score_diff(NaNd); + criteria.rel_score_diff(NaNd); + criteria.stop_score(gndlvl); - auto sd = j.r * sm.cfg.safety_distance_mm / sm.cfg.head_back_radius_mm; - auto oresult = solver.to_max().optimize( - [&j, sd, &policy, &sm, &downdst, &end_radius](const opt::Input<2> &input) { - auto &[plr, azm] = input; - Vec3d n = spheric_to_dir(plr, azm).normalized(); - Beam beam{Ball{j.pos, j.r}, Ball{j.pos + downdst * n, end_radius}}; - return beam_mesh_hit(policy, sm.emesh, beam, sd).distance(); - }, - initvals({polar, azimuth}), // let's start with what we have - bounds({ {PI - sm.cfg.bridge_slope, PI}, {-PI, PI} }) - ); + auto criteria_loc = criteria; + criteria_loc.max_iterations(MaxIterationsLocal); + criteria_loc.abs_score_diff(EPSILON); + criteria_loc.rel_score_diff(RelScoreDiff); - Vec3d bridgedir = spheric_to_dir(oresult.optimum).normalized(); + Optimizer solver(criteria); + solver.set_loc_criteria(criteria_loc); + solver.seed(0); // require repeatability - return connect_to_ground(policy, builder, sm, j, bridgedir, end_radius); + // functor returns the z height of collision point, given a polar and + // azimuth angles as bridge direction and bridge length. The route is + // traced from source, through this bridge and an attached pillar. If there + // is a collision with the mesh, the Z height is returned. Otherwise the + // z level of ground is returned. + auto z_fn = [&](const opt::Input<3> &input) { + // solver suggests polar, azimuth and bridge length values: + auto &[plr, azm, bridge_len] = input; + + Vec3d n = spheric_to_dir(plr, azm); + + Vec3d hitpt = check_ground_route(policy, sm, source, n, bridge_len, wideningfn); + + return hitpt.z(); + }; + + // Calculate the initial direction of the search by + // saturating the polar angle to max tilt defined in config + auto [plr_init, azm_init] = dir_to_spheric(init_dir); + plr_init = std::max(plr_init, PI - sm.cfg.bridge_slope); + + auto bound_constraints = + bounds({ + {PI - sm.cfg.bridge_slope, PI}, // bounds for polar angle + {-PI, PI}, // bounds for azimuth + {0., sm.cfg.max_bridge_length_mm} // bounds bridge length + }); + + // The optimizer can navigate fairly well on the mesh surface, finding + // lower and lower Z coordinates as collision points. MLSL is not a local + // search method, so it should not be trapped in a local minima. Eventually, + // this search should arrive at a ground location. + auto oresult = solver.to_min().optimize( + z_fn, + initvals({plr_init, azm_init, 0.}), + bound_constraints + ); + + GroundConnection conn; + + // Extract and apply the result + auto [plr, azm, bridge_l] = oresult.optimum; + Vec3d n = spheric_to_dir(plr, azm); + assert(std::abs(n.norm() - 1.) < EPSILON); + + double t = (gndlvl - source.pos.z()) / n.z(); + bridge_l = std::min(t, bridge_l); + + // Now the optimizer gave a possible route to ground with a bridge direction + // and length. This length can be shortened further by brute-force queries + // of free route straigt down for a possible pillar. + // NOTE: This requirement could be incorporated into the optimization as a + // constraint, but it would not find quickly enough an accurate solution, + // and it would be very hard to define a stop score which is very useful in + // terminating the search as soon as the ground is found. + double l = 0., l_max = bridge_l; + double zlvl = std::numeric_limits::infinity(); + while(zlvl > gndlvl && l <= l_max) { + + zlvl = check_ground_route(policy, sm, source, n, l, wideningfn, + GroundRouteCheck::PillarOnly).z(); + + if (zlvl <= gndlvl) + bridge_l = l; + + l += source.r; + } + + Vec3d bridge_end = source.pos + bridge_l * n; + Vec3d gp{bridge_end.x(), bridge_end.y(), gndlvl}; + + double bridge_r = wideningfn(Ball{source.pos, source.r}, n, bridge_l); + double down_l = bridge_end.z() - gndlvl; + double end_radius = wideningfn(Ball{bridge_end, bridge_r}, DOWN, down_l); + double base_r = std::max(sm.cfg.base_radius_mm, end_radius); + + // Even if the search was not succesful, the result is populated by the + // source and the last best result of the optimization. + conn.path.emplace_back(source); + if (bridge_l > EPSILON) + conn.path.emplace_back(Junction{bridge_end, bridge_r}); + + // The resulting ground connection is only valid if the pillar base is set. + // At this point it will only be set if the search was succesful. + if (z_fn(opt::Input<3>({plr, azm, bridge_l})) <= gndlvl) + conn.pillar_base = + Pedestal{gp, sm.cfg.base_height_mm, base_r, end_radius}; + + return conn; +} + +// Ground route search with a predefined end radius +template +GroundConnection deepsearch_ground_connection(Ex policy, + const SupportableMesh &sm, + const Junction &source, + double end_radius, + const Vec3d &init_dir = DOWN) +{ + double gndlvl = ground_level(sm); + auto wfn = [end_radius, gndlvl](const Ball &src, const Vec3d &dir, double len) { + if (len < EPSILON) + return src.R; + + Vec3d dst = src.p + len * dir; + double widening = end_radius - src.R; + double zlen = dst.z() - gndlvl; + double full_len = len + zlen; + double r = src.R + widening * len / full_len; + + return r; + }; + + static_assert(IsWideningFn, "Not a widening function"); + + return deepsearch_ground_connection(policy, sm, source, wfn, init_dir); +} + +struct DefaultWideningModel { + static constexpr double WIDENING_SCALE = 0.02; + const SupportableMesh &sm; + + double operator()(const Ball &src, const Vec3d & /*dir*/, double len) { + static_assert(IsWideningFn, + "DefaultWideningModel is not a widening function"); + + double w = WIDENING_SCALE * sm.cfg.pillar_widening_factor * len; + return std::max(src.R, sm.cfg.head_back_radius_mm) + w; + }; +}; + +template<> struct BeamSamples { + static constexpr size_t Value = 16; +}; + +template +GroundConnection deepsearch_ground_connection(Ex policy, + const SupportableMesh &sm, + const Junction &source, + const Vec3d &init_dir = DOWN) +{ + return deepsearch_ground_connection(policy, sm, source, + DefaultWideningModel{sm}, init_dir); } template @@ -729,8 +788,7 @@ bool optimize_anchor_placement(Ex policy, double lmax = std::min(sm.cfg.head_width_mm, distance(from.pos, anchor.pos) - 2 * from.r); - double sd = anchor.r_back_mm * sm.cfg.safety_distance_mm / - sm.cfg.head_back_radius_mm; + double sd = sm.cfg.safety_distance(anchor.r_back_mm); Optimizer solver(get_criteria(sm.cfg) .stop_score(anchor.fullwidth()) @@ -796,6 +854,92 @@ std::optional calculate_anchor_placement(Ex policy, return anchor; } +inline bool is_outside_support_cone(const Vec3f &supp, + const Vec3f &pt, + float angle) +{ + using namespace Slic3r; + + Vec3d D = (pt - supp).cast(); + double dot_sq = -D.z() * std::abs(-D.z()); + + return dot_sq < + D.squaredNorm() * std::cos(angle) * std::abs(std::cos(angle)); +} + +inline // TODO: should be in a cpp +std::optional find_merge_pt(const Vec3f &A, + const Vec3f &B, + float critical_angle) +{ + // The idea is that A and B both have their support cones. But searching + // for the intersection of these support cones is difficult and its enough + // to reduce this problem to 2D and search for the intersection of two + // rays that merge somewhere between A and B. The 2D plane is a vertical + // slice of the 3D scene where the 2D Y axis is equal to the 3D Z axis and + // the 2D X axis is determined by the XY direction of the AB vector. + // + // Z^ + // | A * + // | . . B * + // | . . . . + // | . . . . + // | . x . + // -------------------> XY + + // Determine the transformation matrix for the 2D projection: + Vec3f diff = {B.x() - A.x(), B.y() - A.y(), 0.f}; + Vec3f dir = diff.normalized(); // TODO: avoid normalization + + Eigen::Matrix tr2D; + tr2D.row(0) = Vec3f{dir.x(), dir.y(), dir.z()}; + tr2D.row(1) = Vec3f{0.f, 0.f, 1.f}; + + // Transform the 2 vectors A and B into 2D vector 'a' and 'b'. Here we can + // omit 'a', pretend that its the origin and use BA as the vector b. + Vec2f b = tr2D * (B - A); + + // Get the square sine of the ray emanating from 'a' towards 'b'. This ray might + // exceed the allowed angle but that is corrected subsequently. + // The sign of the original sine is also needed, hence b.y is multiplied by + // abs(b.y) + float b_sqn = b.squaredNorm(); + float sin2sig_a = b_sqn > EPSILON ? (b.y() * std::abs(b.y())) / b_sqn : 0.f; + + // sine2 from 'b' to 'a' is the opposite of sine2 from a to b + float sin2sig_b = -sin2sig_a; + + // Derive the allowed angles from the given critical angle. + // critical_angle is measured from the horizontal X axis. + // The rays need to go downwards which corresponds to negative angles + + float sincrit = std::sin(critical_angle); // sine of the critical angle + float sin2crit = -sincrit * sincrit; // signed sine squared + sin2sig_a = std::min(sin2sig_a, sin2crit); // Do the angle saturation of both rays + sin2sig_b = std::min(sin2sig_b, sin2crit); // + float sin2_a = std::abs(sin2sig_a); // Get cosine squared values + float sin2_b = std::abs(sin2sig_b); + float cos2_a = 1.f - sin2_a; + float cos2_b = 1.f - sin2_b; + + // Derive the new direction vectors. This is by square rooting the sin2 + // and cos2 values and restoring the original signs + Vec2f Da = {std::copysign(std::sqrt(cos2_a), b.x()), std::copysign(std::sqrt(sin2_a), sin2sig_a)}; + Vec2f Db = {-std::copysign(std::sqrt(cos2_b), b.x()), std::copysign(std::sqrt(sin2_b), sin2sig_b)}; + + // Determine where two rays ([0, 0], Da), (b, Db) intersect. + // Based on + // https://stackoverflow.com/questions/27459080/given-two-points-and-two-direction-vectors-find-the-point-where-they-intersect + // One ray is emanating from (0, 0) so the formula is simplified + double t1 = (Db.y() * b.x() - b.y() * Db.x()) / + (Da.x() * Db.y() - Da.y() * Db.x()); + + Vec2f mp = t1 * Da; + Vec3f Mp = A + tr2D.transpose() * mp; + + return t1 >= 0.f ? Mp : Vec3f{}; +} + }} // namespace Slic3r::sla #endif // SLASUPPORTTREEUTILS_H diff --git a/src/libslic3r/SLA/SupportTreeUtilsLegacy.hpp b/src/libslic3r/SLA/SupportTreeUtilsLegacy.hpp new file mode 100644 index 0000000000..b504d82fb3 --- /dev/null +++ b/src/libslic3r/SLA/SupportTreeUtilsLegacy.hpp @@ -0,0 +1,300 @@ +#ifndef SUPPORTTREEUTILSLEGACY_HPP +#define SUPPORTTREEUTILSLEGACY_HPP + +#include "SupportTreeUtils.hpp" + +// Old functions are gathered here that are used in DefaultSupportTree +// to maintain functionality that was well tested. + +namespace Slic3r { namespace sla { + +// Helper function for pillar interconnection where pairs of already connected +// pillars should be checked for not to be processed again. This can be done +// in constant time with a set of hash values uniquely representing a pair of +// integers. The order of numbers within the pair should not matter, it has +// the same unique hash. The hash value has to have twice as many bits as the +// arguments need. If the same integral type is used for args and return val, +// make sure the arguments use only the half of the type's bit depth. +template> +IntegerOnly pairhash(I a, I b) +{ + using std::ceil; using std::log2; using std::max; using std::min; + static const auto constexpr Ibits = int(sizeof(I) * CHAR_BIT); + static const auto constexpr DoubleIbits = int(sizeof(DoubleI) * CHAR_BIT); + static const auto constexpr shift = DoubleIbits / 2 < Ibits ? Ibits / 2 : Ibits; + + I g = min(a, b), l = max(a, b); + + // Assume the hash will fit into the output variable + assert((g ? (ceil(log2(g))) : 0) <= shift); + assert((l ? (ceil(log2(l))) : 0) <= shift); + + return (DoubleI(g) << shift) + l; +} + +template +std::optional search_widening_path(Ex policy, + const SupportableMesh &sm, + const Vec3d &jp, + const Vec3d &dir, + double radius, + double new_radius) +{ + double w = radius + 2 * sm.cfg.head_back_radius_mm; + double stopval = w + jp.z() - ground_level(sm); + Optimizer solver(get_criteria(sm.cfg).stop_score(stopval)); + + auto [polar, azimuth] = dir_to_spheric(dir); + + double fallback_ratio = radius / sm.cfg.head_back_radius_mm; + + auto oresult = solver.to_max().optimize( + [&policy, &sm, jp, radius, new_radius](const opt::Input<3> &input) { + auto &[plr, azm, t] = input; + + auto d = spheric_to_dir(plr, azm).normalized(); + + auto sd = sm.cfg.safety_distance(new_radius); + + double ret = pinhead_mesh_hit(policy, sm.emesh, jp, d, radius, + new_radius, t, sd) + .distance(); + + Beam beam{jp + t * d, d, new_radius}; + double down = beam_mesh_hit(policy, sm.emesh, beam, sd).distance(); + + if (ret > t && std::isinf(down)) + ret += jp.z() - ground_level(sm); + + return ret; + }, + initvals({polar, azimuth, w}), // start with what we have + bounds({ + {PI - sm.cfg.bridge_slope, PI}, // Must not exceed the slope limit + {-PI, PI}, // azimuth can be a full search + {radius + sm.cfg.head_back_radius_mm, + fallback_ratio * sm.cfg.max_bridge_length_mm} + })); + + if (oresult.score >= stopval) { + polar = std::get<0>(oresult.optimum); + azimuth = std::get<1>(oresult.optimum); + double t = std::get<2>(oresult.optimum); + Vec3d endp = jp + t * spheric_to_dir(polar, azimuth); + + return DiffBridge(jp, endp, radius, sm.cfg.head_back_radius_mm); + } + + return {}; +} + +// This is a proxy function for pillar creation which will mind the gap +// between the pad and the model bottom in zero elevation mode. +// 'pinhead_junctionpt' is the starting junction point which needs to be +// routed down. sourcedir is the allowed direction of an optional bridge +// between the jp junction and the final pillar. +template +std::pair create_ground_pillar( + Ex policy, + SupportTreeBuilder &builder, + const SupportableMesh &sm, + const Vec3d &pinhead_junctionpt, + const Vec3d &sourcedir, + double radius, + double end_radius, + long head_id = SupportTreeNode::ID_UNSET) +{ + Vec3d jp = pinhead_junctionpt, endp = jp, dir = sourcedir; + long pillar_id = SupportTreeNode::ID_UNSET; + bool can_add_base = false, non_head = false; + + double gndlvl = 0.; // The Z level where pedestals should be + double jp_gnd = 0.; // The lowest Z where a junction center can be + double gap_dist = 0.; // The gap distance between the model and the pad + + double r2 = radius + (end_radius - radius) / (jp.z() - ground_level(sm)); + + auto to_floor = [&gndlvl](const Vec3d &p) { return Vec3d{p.x(), p.y(), gndlvl}; }; + + auto eval_limits = [&sm, &radius, &can_add_base, &gndlvl, &gap_dist, &jp_gnd] + (bool base_en = true) + { + can_add_base = base_en && radius >= sm.cfg.head_back_radius_mm; + double base_r = can_add_base ? sm.cfg.base_radius_mm : 0.; + gndlvl = ground_level(sm); + if (!can_add_base) gndlvl -= sm.pad_cfg.wall_thickness_mm; + jp_gnd = gndlvl + (can_add_base ? 0. : sm.cfg.head_back_radius_mm); + gap_dist = sm.cfg.pillar_base_safety_distance_mm + base_r + EPSILON; + }; + + eval_limits(); + + // We are dealing with a mini pillar that's potentially too long + if (radius < sm.cfg.head_back_radius_mm && jp.z() - gndlvl > 20 * radius) + { + std::optional diffbr = + search_widening_path(policy, sm, jp, dir, radius, + sm.cfg.head_back_radius_mm); + + if (diffbr && diffbr->endp.z() > jp_gnd) { + auto &br = builder.add_diffbridge(*diffbr); + if (head_id >= 0) builder.head(head_id).bridge_id = br.id; + endp = diffbr->endp; + radius = diffbr->end_r; + builder.add_junction(endp, radius); + non_head = true; + dir = diffbr->get_dir(); + eval_limits(); + } else return {false, pillar_id}; + } + + if (sm.cfg.object_elevation_mm < EPSILON) + { + // get a suitable direction for the corrector bridge. It is the + // original sourcedir's azimuth but the polar angle is saturated to the + // configured bridge slope. + auto [polar, azimuth] = dir_to_spheric(dir); + polar = PI - sm.cfg.bridge_slope; + Vec3d d = spheric_to_dir(polar, azimuth).normalized(); + auto sd = radius * sm.cfg.safety_distance_mm / sm.cfg.head_back_radius_mm; + double t = beam_mesh_hit(policy, sm.emesh, Beam{endp, d, radius, r2}, sd).distance(); + double tmax = std::min(sm.cfg.max_bridge_length_mm, t); + t = 0.; + + double zd = endp.z() - jp_gnd; + double tmax2 = zd / std::sqrt(1 - sm.cfg.bridge_slope * sm.cfg.bridge_slope); + tmax = std::min(tmax, tmax2); + + Vec3d nexp = endp; + double dlast = 0.; + while (((dlast = std::sqrt(sm.emesh.squared_distance(to_floor(nexp)))) < gap_dist || + !std::isinf(beam_mesh_hit(policy, sm.emesh, Beam{nexp, DOWN, radius, r2}, sd).distance())) && + t < tmax) + { + t += radius; + nexp = endp + t * d; + } + + if (dlast < gap_dist && can_add_base) { + nexp = endp; + t = 0.; + can_add_base = false; + eval_limits(can_add_base); + + zd = endp.z() - jp_gnd; + tmax2 = zd / std::sqrt(1 - sm.cfg.bridge_slope * sm.cfg.bridge_slope); + tmax = std::min(tmax, tmax2); + + while (((dlast = std::sqrt(sm.emesh.squared_distance(to_floor(nexp)))) < gap_dist || + !std::isinf(beam_mesh_hit(policy, sm.emesh, Beam{nexp, DOWN, radius}, sd).distance())) && t < tmax) { + t += radius; + nexp = endp + t * d; + } + } + + // Could not find a path to avoid the pad gap + if (dlast < gap_dist) return {false, pillar_id}; + + if (t > 0.) { // Need to make additional bridge + const Bridge& br = builder.add_bridge(endp, nexp, radius); + if (head_id >= 0) builder.head(head_id).bridge_id = br.id; + + builder.add_junction(nexp, radius); + endp = nexp; + non_head = true; + } + } + + Vec3d gp = to_floor(endp); + double h = endp.z() - gp.z(); + + pillar_id = head_id >= 0 && !non_head ? builder.add_pillar(head_id, h) : + builder.add_pillar(gp, h, radius, end_radius); + + if (can_add_base) + builder.add_pillar_base(pillar_id, sm.cfg.base_height_mm, + sm.cfg.base_radius_mm); + + return {true, pillar_id}; +} + +template +std::pair connect_to_ground(Ex policy, + SupportTreeBuilder &builder, + const SupportableMesh &sm, + const Junction &j, + const Vec3d &dir, + double end_r) +{ + auto hjp = j.pos; + double r = j.r; + auto sd = r * sm.cfg.safety_distance_mm / sm.cfg.head_back_radius_mm; + double r2 = j.r + (end_r - j.r) / (j.pos.z() - ground_level(sm)); + + double t = beam_mesh_hit(policy, sm.emesh, Beam{hjp, dir, r, r2}, sd).distance(); + double d = 0, tdown = 0; + t = std::min(t, sm.cfg.max_bridge_length_mm * r / sm.cfg.head_back_radius_mm); + + while (d < t && + !std::isinf(tdown = beam_mesh_hit(policy, sm.emesh, + Beam{hjp + d * dir, DOWN, r, r2}, sd) + .distance())) { + d += r; + } + + if(!std::isinf(tdown)) + return {false, SupportTreeNode::ID_UNSET}; + + Vec3d endp = hjp + d * dir; + auto ret = create_ground_pillar(policy, builder, sm, endp, dir, r, end_r); + + if (ret.second >= 0) { + builder.add_bridge(hjp, endp, r); + builder.add_junction(endp, r); + } + + return ret; +} + +template +std::pair search_ground_route(Ex policy, + SupportTreeBuilder &builder, + const SupportableMesh &sm, + const Junction &j, + double end_radius, + const Vec3d &init_dir = DOWN) +{ + double downdst = j.pos.z() - ground_level(sm); + + auto res = connect_to_ground(policy, builder, sm, j, init_dir, end_radius); + if (res.first) + return res; + + // Optimize bridge direction: + // Straight path failed so we will try to search for a suitable + // direction out of the cavity. + auto [polar, azimuth] = dir_to_spheric(init_dir); + + Optimizer solver(get_criteria(sm.cfg).stop_score(1e6)); + solver.seed(0); // we want deterministic behavior + + auto sd = j.r * sm.cfg.safety_distance_mm / sm.cfg.head_back_radius_mm; + auto oresult = solver.to_max().optimize( + [&j, sd, &policy, &sm, &downdst, &end_radius](const opt::Input<2> &input) { + auto &[plr, azm] = input; + Vec3d n = spheric_to_dir(plr, azm).normalized(); + Beam beam{Ball{j.pos, j.r}, Ball{j.pos + downdst * n, end_radius}}; + return beam_mesh_hit(policy, sm.emesh, beam, sd).distance(); + }, + initvals({polar, azimuth}), // let's start with what we have + bounds({ {PI - sm.cfg.bridge_slope, PI}, {-PI, PI} }) + ); + + Vec3d bridgedir = spheric_to_dir(oresult.optimum).normalized(); + + return connect_to_ground(policy, builder, sm, j, bridgedir, end_radius); +} + +}} // namespace Slic3r::sla + +#endif // SUPPORTTREEUTILSLEGACY_HPP diff --git a/src/libslic3r/SLAPrint.cpp b/src/libslic3r/SLAPrint.cpp index 71d6e889dd..d4023f64d3 100644 --- a/src/libslic3r/SLAPrint.cpp +++ b/src/libslic3r/SLAPrint.cpp @@ -40,28 +40,63 @@ sla::SupportTreeConfig make_support_cfg(const SLAPrintObjectConfig& c) scfg.enabled = c.supports_enable.getBool(); scfg.tree_type = c.support_tree_type.value; - scfg.head_front_radius_mm = 0.5*c.support_head_front_diameter.getFloat(); - double pillar_r = 0.5 * c.support_pillar_diameter.getFloat(); - scfg.head_back_radius_mm = pillar_r; - scfg.head_fallback_radius_mm = - 0.01 * c.support_small_pillar_diameter_percent.getFloat() * pillar_r; - scfg.head_penetration_mm = c.support_head_penetration.getFloat(); - scfg.head_width_mm = c.support_head_width.getFloat(); - scfg.object_elevation_mm = is_zero_elevation(c) ? - 0. : c.support_object_elevation.getFloat(); - scfg.bridge_slope = c.support_critical_angle.getFloat() * PI / 180.0 ; - scfg.max_bridge_length_mm = c.support_max_bridge_length.getFloat(); - scfg.max_pillar_link_distance_mm = c.support_max_pillar_link_distance.getFloat(); - scfg.pillar_connection_mode = c.support_pillar_connection_mode.value; - scfg.ground_facing_only = c.support_buildplate_only.getBool(); - scfg.pillar_widening_factor = c.support_pillar_widening_factor.getFloat(); - scfg.base_radius_mm = 0.5*c.support_base_diameter.getFloat(); - scfg.base_height_mm = c.support_base_height.getFloat(); - scfg.pillar_base_safety_distance_mm = - c.support_base_safety_distance.getFloat() < EPSILON ? - scfg.safety_distance_mm : c.support_base_safety_distance.getFloat(); - scfg.max_bridges_on_pillar = unsigned(c.support_max_bridges_on_pillar.getInt()); + switch(scfg.tree_type) { + case sla::SupportTreeType::Default: { + scfg.head_front_radius_mm = 0.5*c.support_head_front_diameter.getFloat(); + double pillar_r = 0.5 * c.support_pillar_diameter.getFloat(); + scfg.head_back_radius_mm = pillar_r; + scfg.head_fallback_radius_mm = + 0.01 * c.support_small_pillar_diameter_percent.getFloat() * pillar_r; + scfg.head_penetration_mm = c.support_head_penetration.getFloat(); + scfg.head_width_mm = c.support_head_width.getFloat(); + scfg.object_elevation_mm = is_zero_elevation(c) ? + 0. : c.support_object_elevation.getFloat(); + scfg.bridge_slope = c.support_critical_angle.getFloat() * PI / 180.0 ; + scfg.max_bridge_length_mm = c.support_max_bridge_length.getFloat(); + scfg.max_pillar_link_distance_mm = c.support_max_pillar_link_distance.getFloat(); + scfg.pillar_connection_mode = c.support_pillar_connection_mode.value; + scfg.ground_facing_only = c.support_buildplate_only.getBool(); + scfg.pillar_widening_factor = c.support_pillar_widening_factor.getFloat(); + scfg.base_radius_mm = 0.5*c.support_base_diameter.getFloat(); + scfg.base_height_mm = c.support_base_height.getFloat(); + scfg.pillar_base_safety_distance_mm = + c.support_base_safety_distance.getFloat() < EPSILON ? + scfg.safety_distance_mm : c.support_base_safety_distance.getFloat(); + + scfg.max_bridges_on_pillar = unsigned(c.support_max_bridges_on_pillar.getInt()); + scfg.max_weight_on_model_support = c.support_max_weight_on_model.getFloat(); + break; + } + case sla::SupportTreeType::Branching: + [[fallthrough]]; + case sla::SupportTreeType::Organic:{ + scfg.head_front_radius_mm = 0.5*c.branchingsupport_head_front_diameter.getFloat(); + double pillar_r = 0.5 * c.branchingsupport_pillar_diameter.getFloat(); + scfg.head_back_radius_mm = pillar_r; + scfg.head_fallback_radius_mm = + 0.01 * c.branchingsupport_small_pillar_diameter_percent.getFloat() * pillar_r; + scfg.head_penetration_mm = c.branchingsupport_head_penetration.getFloat(); + scfg.head_width_mm = c.branchingsupport_head_width.getFloat(); + scfg.object_elevation_mm = is_zero_elevation(c) ? + 0. : c.branchingsupport_object_elevation.getFloat(); + scfg.bridge_slope = c.branchingsupport_critical_angle.getFloat() * PI / 180.0 ; + scfg.max_bridge_length_mm = c.branchingsupport_max_bridge_length.getFloat(); + scfg.max_pillar_link_distance_mm = c.branchingsupport_max_pillar_link_distance.getFloat(); + scfg.pillar_connection_mode = c.branchingsupport_pillar_connection_mode.value; + scfg.ground_facing_only = c.branchingsupport_buildplate_only.getBool(); + scfg.pillar_widening_factor = c.branchingsupport_pillar_widening_factor.getFloat(); + scfg.base_radius_mm = 0.5*c.branchingsupport_base_diameter.getFloat(); + scfg.base_height_mm = c.branchingsupport_base_height.getFloat(); + scfg.pillar_base_safety_distance_mm = + c.branchingsupport_base_safety_distance.getFloat() < EPSILON ? + scfg.safety_distance_mm : c.branchingsupport_base_safety_distance.getFloat(); + + scfg.max_bridges_on_pillar = unsigned(c.branchingsupport_max_bridges_on_pillar.getInt()); + scfg.max_weight_on_model_support = c.branchingsupport_max_weight_on_model.getFloat(); + break; + } + } return scfg; } @@ -822,6 +857,7 @@ bool SLAPrintObject::invalidate_state_by_config_options(const std::vector= 0); assert(cell_coords.x() < cell_count.x()); assert(cell_coords.y() >= 0); assert(cell_coords.y() < cell_count.y()); assert(cell_coords.z() >= 0); assert(cell_coords.z() < cell_count.z()); - +#endif return cell_coords.z() * cell_count.x() * cell_count.y() + cell_coords.y() * cell_count.x() + cell_coords.x(); } @@ -244,6 +247,7 @@ std::vector check_extrusion_entity_stability(const ExtrusionEntit const float flow_width = get_flow_width(layer_region, entity->role()); + // Compute only unsigned distance - prev_layer_lines can contain unconnected paths, thus the sign of the distance is unreliable std::vector annotated_points = estimate_points_properties(entity->as_polyline().points, prev_layer_lines, flow_width, params.bridge_distance); @@ -262,6 +266,7 @@ std::vector check_extrusion_entity_stability(const ExtrusionEntit prev_layer_lines.get_line(curr_point.nearest_prev_layer_line) : ExtrusionLine{}; + // correctify the distance sign using slice polygons float sign = (prev_layer_boundary.distance_from_lines(curr_point.position) + 0.5f * flow_width) < 0.0f ? -1.0f : 1.0f; curr_point.distance *= sign; @@ -297,84 +302,39 @@ std::vector check_extrusion_entity_stability(const ExtrusionEntit } } -// returns triangle area, first_moment_of_area_xy, second_moment_of_area_xy, second_moment_of_area_covariance -// none of the values is divided/normalized by area. -// The function computes intgeral over the area of the triangle, with function f(x,y) = x for first moments of area (y is analogous) -// f(x,y) = x^2 for second moment of area -// and f(x,y) = x*y for second moment of area covariance -std::tuple compute_triangle_moments_of_area(const Vec2f &a, const Vec2f &b, const Vec2f &c) -{ - // based on the following guide: - // Denote the vertices of S by a, b, c. Then the map - // g:(u,v)↦a+u(b−a)+v(c−a) , - // which in coordinates appears as - // g:(u,v)↦{x(u,v)y(u,v)=a1+u(b1−a1)+v(c1−a1)=a2+u(b2−a2)+v(c2−a2) ,(1) - // obviously maps S′ bijectively onto S. Therefore the transformation formula for multiple integrals steps into action, and we obtain - // ∫Sf(x,y)d(x,y)=∫S′f(x(u,v),y(u,v))∣∣Jg(u,v)∣∣ d(u,v) . - // In the case at hand the Jacobian determinant is a constant: From (1) we obtain - // Jg(u,v)=det[xuyuxvyv]=(b1−a1)(c2−a2)−(c1−a1)(b2−a2) . - // Therefore we can write - // ∫Sf(x,y)d(x,y)=∣∣Jg∣∣∫10∫1−u0f~(u,v) dv du , - // where f~ denotes the pullback of f to S′: - // f~(u,v):=f(x(u,v),y(u,v)) . - // Don't forget taking the absolute value of Jg! - - float jacobian_determinant_abs = std::abs((b.x() - a.x()) * (c.y() - a.y()) - (c.x() - a.x()) * (b.y() - a.y())); - - // coordinate transform: gx(u,v) = a.x + u * (b.x - a.x) + v * (c.x - a.x) - // coordinate transform: gy(u,v) = a.y + u * (b.y - a.y) + v * (c.y - a.y) - // second moment of area for x: f(x, y) = x^2; - // f(gx(u,v), gy(u,v)) = gx(u,v)^2 = ... (long expanded form) - - // result is Int_T func = jacobian_determinant_abs * Int_0^1 Int_0^1-u func(gx(u,v), gy(u,v)) dv du - // integral_0^1 integral_0^(1 - u) (a + u (b - a) + v (c - a))^2 dv du = 1/12 (a^2 + a (b + c) + b^2 + b c + c^2) - - Vec2f second_moment_of_area_xy = jacobian_determinant_abs * - (a.cwiseProduct(a) + b.cwiseProduct(b) + b.cwiseProduct(c) + c.cwiseProduct(c) + - a.cwiseProduct(b + c)) / - 12.0f; - // second moment of area covariance : f(x, y) = x*y; - // f(gx(u,v), gy(u,v)) = gx(u,v)*gy(u,v) = ... (long expanded form) - //(a_1 + u * (b_1 - a_1) + v * (c_1 - a_1)) * (a_2 + u * (b_2 - a_2) + v * (c_2 - a_2)) - // == (a_1 + u (b_1 - a_1) + v (c_1 - a_1)) (a_2 + u (b_2 - a_2) + v (c_2 - a_2)) - - // intermediate result: integral_0^(1 - u) (a_1 + u (b_1 - a_1) + v (c_1 - a_1)) (a_2 + u (b_2 - a_2) + v (c_2 - a_2)) dv = - // 1/6 (u - 1) (-c_1 (u - 1) (a_2 (u - 1) - 3 b_2 u) - c_2 (u - 1) (a_1 (u - 1) - 3 b_1 u + 2 c_1 (u - 1)) + 3 b_1 u (a_2 (u - 1) - 2 - // b_2 u) + a_1 (u - 1) (3 b_2 u - 2 a_2 (u - 1))) result = integral_0^1 1/6 (u - 1) (-c_1 (u - 1) (a_2 (u - 1) - 3 b_2 u) - c_2 (u - - // 1) (a_1 (u - 1) - 3 b_1 u + 2 c_1 (u - 1)) + 3 b_1 u (a_2 (u - 1) - 2 b_2 u) + a_1 (u - 1) (3 b_2 u - 2 a_2 (u - 1))) du = - // 1/24 (a_2 (b_1 + c_1) + a_1 (2 a_2 + b_2 + c_2) + b_2 c_1 + b_1 c_2 + 2 b_1 b_2 + 2 c_1 c_2) - // result is Int_T func = jacobian_determinant_abs * Int_0^1 Int_0^1-u func(gx(u,v), gy(u,v)) dv du - float second_moment_of_area_covariance = jacobian_determinant_abs * (1.0f / 24.0f) * - (a.y() * (b.x() + c.x()) + a.x() * (2.0f * a.y() + b.y() + c.y()) + b.y() * c.x() + - b.x() * c.y() + 2.0f * b.x() * b.y() + 2.0f * c.x() * c.y()); - - float area = jacobian_determinant_abs * 0.5f; - - Vec2f first_moment_of_area_xy = jacobian_determinant_abs * (a + b + c) / 6.0f; - - return {area, first_moment_of_area_xy, second_moment_of_area_xy, second_moment_of_area_covariance}; -}; - SliceConnection estimate_slice_connection(size_t slice_idx, const Layer *layer) { SliceConnection connection; const LayerSlice &slice = layer->lslices_ex[slice_idx]; - ExPolygon slice_poly = layer->lslices[slice_idx]; + Polygons slice_polys = to_polygons(layer->lslices[slice_idx]); + BoundingBox slice_bb = get_extents(slice_polys); const Layer *lower_layer = layer->lower_layer; - ExPolygons below_polys{}; - for (const auto &link : slice.overlaps_below) { below_polys.push_back(lower_layer->lslices[link.slice_idx]); } - ExPolygons overlap = intersection_ex({slice_poly}, below_polys); + ExPolygons below{}; + for (const auto &link : slice.overlaps_below) { below.push_back(lower_layer->lslices[link.slice_idx]); } + Polygons below_polys = to_polygons(below); - std::vector triangles = triangulate_expolygons_2f(overlap); - for (size_t idx = 0; idx < triangles.size(); idx += 3) { - auto [area, first_moment_of_area, second_moment_area, - second_moment_of_area_covariance] = compute_triangle_moments_of_area(triangles[idx], triangles[idx + 1], triangles[idx + 2]); - connection.area += area; - connection.centroid_accumulator += Vec3f(first_moment_of_area.x(), first_moment_of_area.y(), layer->print_z * area); - connection.second_moment_of_area_accumulator += second_moment_area; - connection.second_moment_of_area_covariance_accumulator += second_moment_of_area_covariance; + BoundingBox below_bb = get_extents(below_polys); + + Polygons overlap = intersection(ClipperUtils::clip_clipper_polygons_with_subject_bbox(slice_polys, below_bb), + ClipperUtils::clip_clipper_polygons_with_subject_bbox(below_polys, slice_bb)); + + for (const Polygon &poly : overlap) { + Vec2f p0 = unscaled(poly.first_point()).cast(); + for (size_t i = 2; i < poly.points.size(); i++) { + Vec2f p1 = unscaled(poly.points[i - 1]).cast(); + Vec2f p2 = unscaled(poly.points[i]).cast(); + + float sign = cross2(p1 - p0, p2 - p1) > 0 ? 1.0f : -1.0f; + + auto [area, first_moment_of_area, second_moment_area, + second_moment_of_area_covariance] = compute_moments_of_area_of_triangle(p0, p1, p2); + connection.area += sign * area; + connection.centroid_accumulator += sign * Vec3f(first_moment_of_area.x(), first_moment_of_area.y(), layer->print_z * area); + connection.second_moment_of_area_accumulator += sign * second_moment_area; + connection.second_moment_of_area_covariance_accumulator += sign * second_moment_of_area_covariance; + } } return connection; @@ -973,6 +933,9 @@ void estimate_malformations(LayerPtrs &layers, const Params ¶ms) std::vector current_layer_lines; for (const LayerRegion *layer_region : l->regions()) { for (const ExtrusionEntity *extrusion : layer_region->perimeters().flatten().entities) { + + if (!extrusion->role().is_external_perimeter()) continue; + Points extrusion_pts; extrusion->collect_points(extrusion_pts); float flow_width = get_flow_width(layer_region, extrusion->role()); diff --git a/src/libslic3r/TreeModelVolumes.cpp b/src/libslic3r/TreeModelVolumes.cpp index b416db634f..e8ab377d70 100644 --- a/src/libslic3r/TreeModelVolumes.cpp +++ b/src/libslic3r/TreeModelVolumes.cpp @@ -18,6 +18,8 @@ #include "PrintConfig.hpp" #include "Utils.hpp" +#include + #include #include @@ -26,6 +28,8 @@ namespace Slic3r::FFFTreeSupport { +using namespace std::literals; + // or warning // had to use a define beacuse the macro processing inside macro BOOST_LOG_TRIVIAL() #define error_level_not_in_cache error @@ -336,7 +340,7 @@ const Polygons& TreeModelVolumes::getCollision(const coord_t orig_radius, LayerI return (*result).get(); if (m_precalculated) { BOOST_LOG_TRIVIAL(error_level_not_in_cache) << "Had to calculate collision at radius " << radius << " and layer " << layer_idx << ", but precalculate was called. Performance may suffer!"; - tree_supports_show_error("Not precalculated Collision requested.", false); + tree_supports_show_error("Not precalculated Collision requested."sv, false); } const_cast(this)->calculateCollision(radius, layer_idx); return getCollision(orig_radius, layer_idx, min_xy_dist); @@ -351,7 +355,7 @@ const Polygons& TreeModelVolumes::getCollisionHolefree(coord_t radius, LayerInde return (*result).get(); if (m_precalculated) { BOOST_LOG_TRIVIAL(error_level_not_in_cache) << "Had to calculate collision holefree at radius " << radius << " and layer " << layer_idx << ", but precalculate was called. Performance may suffer!"; - tree_supports_show_error("Not precalculated Holefree Collision requested.", false); + tree_supports_show_error("Not precalculated Holefree Collision requested."sv, false); } const_cast(this)->calculateCollisionHolefree({ radius, layer_idx }); return getCollisionHolefree(radius, layer_idx); @@ -375,10 +379,10 @@ const Polygons& TreeModelVolumes::getAvoidance(const coord_t orig_radius, LayerI if (m_precalculated) { if (to_model) { BOOST_LOG_TRIVIAL(error_level_not_in_cache) << "Had to calculate Avoidance to model at radius " << radius << " and layer " << layer_idx << ", but precalculate was called. Performance may suffer!"; - tree_supports_show_error("Not precalculated Avoidance(to model) requested.", false); + tree_supports_show_error("Not precalculated Avoidance(to model) requested."sv, false); } else { BOOST_LOG_TRIVIAL(error_level_not_in_cache) << "Had to calculate Avoidance at radius " << radius << " and layer " << layer_idx << ", but precalculate was called. Performance may suffer!"; - tree_supports_show_error("Not precalculated Avoidance(to buildplate) requested.", false); + tree_supports_show_error("Not precalculated Avoidance(to buildplate) requested."sv, false); } } const_cast(this)->calculateAvoidance({ radius, layer_idx }, ! to_model, to_model); @@ -396,7 +400,7 @@ const Polygons& TreeModelVolumes::getPlaceableAreas(const coord_t orig_radius, L return (*result).get(); if (m_precalculated) { BOOST_LOG_TRIVIAL(error_level_not_in_cache) << "Had to calculate Placeable Areas at radius " << radius << " and layer " << layer_idx << ", but precalculate was called. Performance may suffer!"; - tree_supports_show_error("Not precalculated Placeable areas requested.", false); + tree_supports_show_error("Not precalculated Placeable areas requested."sv, false); } const_cast(this)->calculatePlaceables(radius, layer_idx); return getPlaceableAreas(orig_radius, layer_idx); @@ -422,7 +426,7 @@ const Polygons& TreeModelVolumes::getWallRestriction(const coord_t orig_radius, tree_supports_show_error( min_xy_dist ? "Not precalculated Wall restriction of minimum xy distance requested )." : - "Not precalculated Wall restriction requested )." + "Not precalculated Wall restriction requested )."sv , false); } const_cast(this)->calculateWallRestrictions({ radius, layer_idx }); diff --git a/src/libslic3r/TreeSupport.cpp b/src/libslic3r/TreeSupport.cpp index 46a43b5a8f..5a9e2f646b 100644 --- a/src/libslic3r/TreeSupport.cpp +++ b/src/libslic3r/TreeSupport.cpp @@ -58,6 +58,7 @@ enum class LineStatus using LineInformation = std::vector>; using LineInformations = std::vector; +using namespace std::literals; static inline void validate_range(const Point &pt) { @@ -191,10 +192,9 @@ static std::vector>> group_me } #endif -//todo Remove! Only relevant for public BETA! static bool inline g_showed_critical_error = false; static bool inline g_showed_performance_warning = false; -void tree_supports_show_error(std::string message, bool critical) +void tree_supports_show_error(std::string_view message, bool critical) { // todo Remove! ONLY FOR PUBLIC BETA!! #if defined(_WIN32) && defined(TREE_SUPPORT_SHOW_ERRORS) @@ -204,7 +204,7 @@ void tree_supports_show_error(std::string message, bool critical) bool show = (critical && !g_showed_critical_error) || (!critical && !g_showed_performance_warning); (critical ? g_showed_critical_error : g_showed_performance_warning) = true; if (show) - MessageBoxA(nullptr, std::string("TreeSupport_2 MOD detected an error while generating the tree support.\nPlease report this back to me with profile and model.\nRevision 5.0\n" + message + "\n" + bugtype).c_str(), + MessageBoxA(nullptr, std::string("TreeSupport_2 MOD detected an error while generating the tree support.\nPlease report this back to me with profile and model.\nRevision 5.0\n" + std::string(message) + "\n" + bugtype).c_str(), "Bug detected!", MB_OK | MB_SYSTEMMODAL | MB_SETFOREGROUND | MB_ICONWARNING); #endif // WIN32 } @@ -572,7 +572,7 @@ static std::optional> polyline_sample_next_point_at_dis // In case a fixpoint is encountered, better aggressively overcompensate so the code does not become stuck here... BOOST_LOG_TRIVIAL(warning) << "Tree Support: Encountered a fixpoint in polyline_sample_next_point_at_distance. This is expected to happen if the distance (currently " << next_distance << ") is smaller than 100"; - tree_supports_show_error("Encountered issue while placing tips. Some tips may be missing.", true); + tree_supports_show_error("Encountered issue while placing tips. Some tips may be missing."sv, true); if (next_distance > 2 * current_distance) // This case should never happen, but better safe than sorry. break; @@ -759,7 +759,7 @@ static std::optional> polyline_sample_next_point_at_dis return do_final_difference ? diff(ret, collision_trimmed()) : union_(ret); if (safe_step_size < 0 || last_step_offset_without_check < 0) { BOOST_LOG_TRIVIAL(error) << "Offset increase got invalid parameter!"; - tree_supports_show_error("Negative offset distance... How did you manage this ?", true); + tree_supports_show_error("Negative offset distance... How did you manage this ?"sv, true); return do_final_difference ? diff(ret, collision_trimmed()) : union_(ret); } @@ -951,7 +951,7 @@ static void generate_initial_areas( bool safe_radius = p.second == LineStatus::TO_BP_SAFE || p.second == LineStatus::TO_MODEL_GRACIOUS_SAFE; if (!mesh_config.support_rests_on_model && !to_bp) { BOOST_LOG_TRIVIAL(warning) << "Tried to add an invalid support point"; - tree_supports_show_error("Unable to add tip. Some overhang may not be supported correctly.", true); + tree_supports_show_error("Unable to add tip. Some overhang may not be supported correctly."sv, true); return; } Polygons circle{ base_circle }; @@ -1517,7 +1517,7 @@ static Point move_inside_if_outside(const Polygons &polygons, Point from, int di if (area(check_layer_data) < tiny_area_threshold) { BOOST_LOG_TRIVIAL(error) << "Lost area by doing catch up from " << ceil_radius_before << " to radius " << volumes.ceilRadius(config.getCollisionRadius(current_elem), settings.use_min_distance); - tree_supports_show_error("Area lost catching up radius. May not cause visible malformation.", true); + tree_supports_show_error("Area lost catching up radius. May not cause visible malformation."sv, true); } } } @@ -1783,7 +1783,7 @@ static void increase_areas_one_layer( "Parent " << &parent << ": Radius: " << config.getCollisionRadius(parent.state) << " at layer: " << layer_idx << " NextTarget: " << parent.state.layer_idx << " Distance to top: " << parent.state.distance_to_top << " Elephant foot increases " << parent.state.elephant_foot_increases << " use_min_xy_dist " << parent.state.use_min_xy_dist << " to buildplate " << parent.state.to_buildplate << " gracious " << parent.state.to_model_gracious << " safe " << parent.state.can_use_safe_radius << " until move " << parent.state.dont_move_until; - tree_supports_show_error("Potentially lost branch!", true); + tree_supports_show_error("Potentially lost branch!"sv, true); } else result = increase_single_area(volumes, config, settings, layer_idx, parent, settings.increase_speed == slow_speed ? offset_slow : offset_fast, to_bp_data, to_model_data, inc_wo_collision, 0, mergelayer); @@ -2270,7 +2270,7 @@ static void create_layer_pathing(const TreeModelVolumes &volumes, const TreeSupp if (elem.areas.to_bp_areas.empty() && elem.areas.to_model_areas.empty()) { if (area(elem.areas.influence_areas) < tiny_area_threshold) { BOOST_LOG_TRIVIAL(error) << "Insert Error of Influence area bypass on layer " << layer_idx - 1; - tree_supports_show_error("Insert error of area after bypassing merge.\n", true); + tree_supports_show_error("Insert error of area after bypassing merge.\n"sv, true); } // Move the area to output. this_layer.emplace_back(elem.state, std::move(elem.parents), std::move(elem.areas.influence_areas)); @@ -2303,7 +2303,7 @@ static void create_layer_pathing(const TreeModelVolumes &volumes, const TreeSupp Polygons new_area = safe_union(elem.areas.influence_areas); if (area(new_area) < tiny_area_threshold) { BOOST_LOG_TRIVIAL(error) << "Insert Error of Influence area on layer " << layer_idx - 1 << ". Origin of " << elem.parents.size() << " areas. Was to bp " << elem.state.to_buildplate; - tree_supports_show_error("Insert error of area after merge.\n", true); + tree_supports_show_error("Insert error of area after merge.\n"sv, true); } this_layer.emplace_back(elem.state, std::move(elem.parents), std::move(new_area)); } @@ -2331,7 +2331,7 @@ static void set_points_on_areas(const SupportElement &elem, SupportElements *lay // Based on the branch center point of the current layer, the point on the next (further up) layer is calculated. if (! elem.state.result_on_layer_is_set()) { BOOST_LOG_TRIVIAL(error) << "Uninitialized support element"; - tree_supports_show_error("Uninitialized support element. A branch may be missing.\n", true); + tree_supports_show_error("Uninitialized support element. A branch may be missing.\n"sv, true); return; } @@ -2394,7 +2394,7 @@ static void set_to_model_contact_to_model_gracious( // Could not find valid placement, even though it should exist => error handling if (last_successfull_layer == nullptr) { BOOST_LOG_TRIVIAL(warning) << "No valid placement found for to model gracious element on layer " << first_elem.state.layer_idx; - tree_supports_show_error("Could not fine valid placement on model! Just placing it down anyway. Could cause floating branches.", true); + tree_supports_show_error("Could not fine valid placement on model! Just placing it down anyway. Could cause floating branches."sv, true); first_elem.state.to_model_gracious = false; set_to_model_contact_simple(first_elem); } else { @@ -2494,7 +2494,7 @@ static void create_nodes_from_area( if (elem.state.to_buildplate) { BOOST_LOG_TRIVIAL(error) << "Uninitialized Influence area targeting " << elem.state.target_position.x() << "," << elem.state.target_position.y() << ") " "at target_height: " << elem.state.target_height << " layer: " << layer_idx; - tree_supports_show_error("Uninitialized support element! A branch could be missing or exist partially.", true); + tree_supports_show_error("Uninitialized support element! A branch could be missing or exist partially."sv, true); } // we dont need to remove yet the parents as they will have a lower dtt and also no result_on_layer set elem.state.deleted = true; diff --git a/src/libslic3r/TreeSupport.hpp b/src/libslic3r/TreeSupport.hpp index 0d5e967d96..b0ab46891a 100644 --- a/src/libslic3r/TreeSupport.hpp +++ b/src/libslic3r/TreeSupport.hpp @@ -17,7 +17,7 @@ #include "BoundingBox.hpp" #include "Utils.hpp" - #define TREE_SUPPORT_SHOW_ERRORS +// #define TREE_SUPPORT_SHOW_ERRORS #ifdef SLIC3R_TREESUPPORTS_PROGRESS // The various stages of the process can be weighted differently in the progress bar. @@ -576,8 +576,7 @@ public: } }; -// todo Remove! ONLY FOR PUBLIC BETA!! -void tree_supports_show_error(std::string message, bool critical); +void tree_supports_show_error(std::string_view message, bool critical); } // namespace FFFTreeSupport diff --git a/src/libslic3r/libslic3r.h b/src/libslic3r/libslic3r.h index 9e101f32a9..2b96994fa7 100644 --- a/src/libslic3r/libslic3r.h +++ b/src/libslic3r/libslic3r.h @@ -385,6 +385,8 @@ inline IntegerOnly fast_round_up(double a) return a == 0.49999999999999994 ? I(0) : I(floor(a + 0.5)); } +template using SamePair = std::pair; + } // namespace Slic3r #endif // _libslic3r_h_ diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 82b49e1ff4..d442a86466 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -374,23 +374,43 @@ void ConfigManipulation::toggle_print_sla_options(DynamicPrintConfig* config) bool is_default_tree = treetype == sla::SupportTreeType::Default; bool is_branching_tree = treetype == sla::SupportTreeType::Branching; - toggle_field("support_head_front_diameter", supports_en); - toggle_field("support_head_penetration", supports_en); - toggle_field("support_head_width", supports_en); - toggle_field("support_pillar_diameter", supports_en); - toggle_field("support_small_pillar_diameter_percent", supports_en); + toggle_field("support_tree_type", supports_en); + + toggle_field("support_head_front_diameter", supports_en && is_default_tree); + toggle_field("support_head_penetration", supports_en && is_default_tree); + toggle_field("support_head_width", supports_en && is_default_tree); + toggle_field("support_pillar_diameter", supports_en && is_default_tree); + toggle_field("support_small_pillar_diameter_percent", supports_en && is_default_tree); toggle_field("support_max_bridges_on_pillar", supports_en && is_default_tree); toggle_field("support_pillar_connection_mode", supports_en && is_default_tree); - toggle_field("support_tree_type", supports_en); - toggle_field("support_buildplate_only", supports_en); + toggle_field("support_buildplate_only", supports_en && is_default_tree); + toggle_field("support_base_diameter", supports_en && is_default_tree); + toggle_field("support_base_height", supports_en && is_default_tree); + toggle_field("support_base_safety_distance", supports_en && is_default_tree); + toggle_field("support_critical_angle", supports_en && is_default_tree); + toggle_field("support_max_bridge_length", supports_en && is_default_tree); toggle_field("support_enforcers_only", supports_en); - toggle_field("support_base_diameter", supports_en); - toggle_field("support_base_height", supports_en); - toggle_field("support_base_safety_distance", supports_en); - toggle_field("support_critical_angle", supports_en); - toggle_field("support_max_bridge_length", supports_en); toggle_field("support_max_pillar_link_distance", supports_en && is_default_tree); - toggle_field("support_pillar_widening_factor", supports_en && is_branching_tree); + toggle_field("support_pillar_widening_factor", false); + toggle_field("support_max_weight_on_model", false); + + toggle_field("branchingsupport_head_front_diameter", supports_en && is_branching_tree); + toggle_field("branchingsupport_head_penetration", supports_en && is_branching_tree); + toggle_field("branchingsupport_head_width", supports_en && is_branching_tree); + toggle_field("branchingsupport_pillar_diameter", supports_en && is_branching_tree); + toggle_field("branchingsupport_small_pillar_diameter_percent", supports_en && is_branching_tree); + toggle_field("branchingsupport_max_bridges_on_pillar", false); + toggle_field("branchingsupport_pillar_connection_mode", false); + toggle_field("branchingsupport_buildplate_only", supports_en && is_branching_tree); + toggle_field("branchingsupport_base_diameter", supports_en && is_branching_tree); + toggle_field("branchingsupport_base_height", supports_en && is_branching_tree); + toggle_field("branchingsupport_base_safety_distance", supports_en && is_branching_tree); + toggle_field("branchingsupport_critical_angle", supports_en && is_branching_tree); + toggle_field("branchingsupport_max_bridge_length", supports_en && is_branching_tree); + toggle_field("branchingsupport_max_pillar_link_distance", false); + toggle_field("branchingsupport_pillar_widening_factor", supports_en && is_branching_tree); + toggle_field("branchingsupport_max_weight_on_model", supports_en && is_branching_tree); + toggle_field("support_points_density_relative", supports_en); toggle_field("support_points_minimal_distance", supports_en); @@ -407,7 +427,8 @@ void ConfigManipulation::toggle_print_sla_options(DynamicPrintConfig* config) bool zero_elev = config->opt_bool("pad_around_object") && pad_en; - toggle_field("support_object_elevation", supports_en && !zero_elev); + toggle_field("support_object_elevation", supports_en && is_default_tree && !zero_elev); + toggle_field("branchingsupport_object_elevation", supports_en && is_branching_tree && !zero_elev); toggle_field("pad_object_gap", zero_elev); toggle_field("pad_around_object_everywhere", zero_elev); toggle_field("pad_object_connector_stride", zero_elev); diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index ef6f53bf08..6ba866ac14 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -1,3360 +1,3512 @@ -// FIXME: extract absolute units -> em - -#include "ConfigWizard_private.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef WIN32 -#include -#include -#include -#endif // WIN32 - -#ifdef _MSW_DARK_MODE -#include -#endif // _MSW_DARK_MODE - -#include "libslic3r/Platform.hpp" -#include "libslic3r/Utils.hpp" -#include "libslic3r/Config.hpp" -#include "libslic3r/libslic3r.h" -#include "libslic3r/Model.hpp" -#include "libslic3r/Color.hpp" -#include "GUI.hpp" -#include "GUI_App.hpp" -#include "GUI_Utils.hpp" -#include "GUI_ObjectManipulation.hpp" -#include "Field.hpp" -#include "DesktopIntegrationDialog.hpp" -#include "slic3r/Config/Snapshot.hpp" -#include "slic3r/Utils/PresetUpdater.hpp" -#include "format.hpp" -#include "MsgDialog.hpp" -#include "UnsavedChangesDialog.hpp" -#include "slic3r/Utils/AppUpdater.hpp" - -#if defined(__linux__) && defined(__WXGTK3__) -#define wxLinux_gtk3 true -#else -#define wxLinux_gtk3 false -#endif //defined(__linux__) && defined(__WXGTK3__) - -namespace Slic3r { -namespace GUI { - - -using Config::Snapshot; -using Config::SnapshotDB; - - -// Configuration data structures extensions needed for the wizard - -bool Bundle::load(fs::path source_path, bool ais_in_resources, bool ais_prusa_bundle) -{ - this->preset_bundle = std::make_unique(); - this->is_in_resources = ais_in_resources; - this->is_prusa_bundle = ais_prusa_bundle; - - std::string path_string = source_path.string(); - // Throw when parsing invalid configuration. Only valid configuration is supposed to be provided over the air. - auto [config_substitutions, presets_loaded] = preset_bundle->load_configbundle( - path_string, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable); - UNUSED(config_substitutions); - // No substitutions shall be reported when loading a system config bundle, no substitutions are allowed. - assert(config_substitutions.empty()); - auto first_vendor = preset_bundle->vendors.begin(); - if (first_vendor == preset_bundle->vendors.end()) { - BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % path_string; - return false; - } - if (presets_loaded == 0) { - BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No profile loaded.") % path_string; - return false; - } - - BOOST_LOG_TRIVIAL(trace) << boost::format("Vendor bundle: `%1%`: %2% profiles loaded.") % path_string % presets_loaded; - this->vendor_profile = &first_vendor->second; - return true; -} - -Bundle::Bundle(Bundle &&other) - : preset_bundle(std::move(other.preset_bundle)) - , vendor_profile(other.vendor_profile) - , is_in_resources(other.is_in_resources) - , is_prusa_bundle(other.is_prusa_bundle) -{ - other.vendor_profile = nullptr; -} - -BundleMap BundleMap::load() -{ - BundleMap res; - - const auto vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / "vendor").make_preferred(); - const auto rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); - - auto prusa_bundle_path = (vendor_dir / PresetBundle::PRUSA_BUNDLE).replace_extension(".ini"); - auto prusa_bundle_rsrc = false; - if (! boost::filesystem::exists(prusa_bundle_path)) { - prusa_bundle_path = (rsrc_vendor_dir / PresetBundle::PRUSA_BUNDLE).replace_extension(".ini"); - prusa_bundle_rsrc = true; - } - { - Bundle prusa_bundle; - if (prusa_bundle.load(std::move(prusa_bundle_path), prusa_bundle_rsrc, true)) - res.emplace(PresetBundle::PRUSA_BUNDLE, std::move(prusa_bundle)); - } - - // Load the other bundles in the datadir/vendor directory - // and then additionally from resources/profiles. - bool is_in_resources = false; - for (auto dir : { &vendor_dir, &rsrc_vendor_dir }) { - for (const auto &dir_entry : boost::filesystem::directory_iterator(*dir)) { - if (Slic3r::is_ini_file(dir_entry)) { - std::string id = dir_entry.path().stem().string(); // stem() = filename() without the trailing ".ini" part - - // Don't load this bundle if we've already loaded it. - if (res.find(id) != res.end()) { continue; } - - Bundle bundle; - if (bundle.load(dir_entry.path(), is_in_resources)) - res.emplace(std::move(id), std::move(bundle)); - } - } - - is_in_resources = true; - } - - return res; -} - -Bundle& BundleMap::prusa_bundle() -{ - auto it = find(PresetBundle::PRUSA_BUNDLE); - if (it == end()) { - throw Slic3r::RuntimeError("ConfigWizard: Internal error in BundleMap: PRUSA_BUNDLE not loaded"); - } - - return it->second; -} - -const Bundle& BundleMap::prusa_bundle() const -{ - return const_cast(this)->prusa_bundle(); -} - - -// Printer model picker GUI control - -struct PrinterPickerEvent : public wxEvent -{ - std::string vendor_id; - std::string model_id; - std::string variant_name; - bool enable; - - PrinterPickerEvent(wxEventType eventType, int winid, std::string vendor_id, std::string model_id, std::string variant_name, bool enable) - : wxEvent(winid, eventType) - , vendor_id(std::move(vendor_id)) - , model_id(std::move(model_id)) - , variant_name(std::move(variant_name)) - , enable(enable) - {} - - virtual wxEvent *Clone() const - { - return new PrinterPickerEvent(*this); - } -}; - -wxDEFINE_EVENT(EVT_PRINTER_PICK, PrinterPickerEvent); - -const std::string PrinterPicker::PRINTER_PLACEHOLDER = "printer_placeholder.png"; - -PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxString title, size_t max_cols, const AppConfig &appconfig, const ModelFilter &filter) - : wxPanel(parent) - , vendor_id(vendor.id) - , width(0) -{ - wxGetApp().UpdateDarkUI(this); - const auto &models = vendor.models; - - auto *sizer = new wxBoxSizer(wxVERTICAL); - - const auto font_title = GetFont().MakeBold().Scaled(1.3f); - const auto font_name = GetFont().MakeBold(); - const auto font_alt_nozzle = GetFont().Scaled(0.9f); - - // wxGrid appends widgets by rows, but we need to construct them in columns. - // These vectors are used to hold the elements so that they can be appended in the right order. - std::vector titles; - std::vector bitmaps; - std::vector variants_panels; - - int max_row_width = 0; - int current_row_width = 0; - - bool is_variants = false; - - for (const auto &model : models) { - if (! filter(model)) { continue; } - - wxBitmap bitmap; - int bitmap_width = 0; - auto load_bitmap = [](const wxString& bitmap_file, wxBitmap& bitmap, int& bitmap_width)->bool { - if (wxFileExists(bitmap_file)) { - bitmap.LoadFile(bitmap_file, wxBITMAP_TYPE_PNG); - bitmap_width = bitmap.GetWidth(); - return true; - } - return false; - }; - if (!load_bitmap(GUI::from_u8(Slic3r::data_dir() + "/vendor/" + vendor.id + "/" + model.id + "_thumbnail.png"), bitmap, bitmap_width)) { - if (!load_bitmap(GUI::from_u8(Slic3r::resources_dir() + "/profiles/" + vendor.id + "/" + model.id + "_thumbnail.png"), bitmap, bitmap_width)) { - BOOST_LOG_TRIVIAL(warning) << boost::format("Can't find bitmap file `%1%` for vendor `%2%`, printer `%3%`, using placeholder icon instead") - % (Slic3r::resources_dir() + "/profiles/" + vendor.id + "/" + model.id + "_thumbnail.png") - % vendor.id - % model.id; - load_bitmap(Slic3r::var(PRINTER_PLACEHOLDER), bitmap, bitmap_width); - } - } - auto *title = new wxStaticText(this, wxID_ANY, from_u8(model.name), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); - title->SetFont(font_name); - const int wrap_width = std::max((int)MODEL_MIN_WRAP, bitmap_width); - title->Wrap(wrap_width); - - current_row_width += wrap_width; - if (titles.size() % max_cols == max_cols - 1) { - max_row_width = std::max(max_row_width, current_row_width); - current_row_width = 0; - } - - titles.push_back(title); - - auto *bitmap_widget = new wxStaticBitmap(this, wxID_ANY, bitmap); - bitmaps.push_back(bitmap_widget); - - auto *variants_panel = new wxPanel(this); - wxGetApp().UpdateDarkUI(variants_panel); - auto *variants_sizer = new wxBoxSizer(wxVERTICAL); - variants_panel->SetSizer(variants_sizer); - const auto model_id = model.id; - - for (size_t i = 0; i < model.variants.size(); i++) { - const auto &variant = model.variants[i]; - - const auto label = model.technology == ptFFF - ? from_u8((boost::format("%1% %2% %3%") % variant.name % _utf8(L("mm")) % _utf8(L("nozzle"))).str()) - : from_u8(model.name); - - if (i == 1) { - auto *alt_label = new wxStaticText(variants_panel, wxID_ANY, _L("Alternate nozzles:")); - alt_label->SetFont(font_alt_nozzle); - variants_sizer->Add(alt_label, 0, wxBOTTOM, 3); - is_variants = true; - } - - auto *cbox = new Checkbox(variants_panel, label, model_id, variant.name); - i == 0 ? cboxes.push_back(cbox) : cboxes_alt.push_back(cbox); - - const bool enabled = appconfig.get_variant(vendor.id, model_id, variant.name); - cbox->SetValue(enabled); - - variants_sizer->Add(cbox, 0, wxBOTTOM, 3); - - cbox->Bind(wxEVT_CHECKBOX, [this, cbox](wxCommandEvent &event) { - on_checkbox(cbox, event.IsChecked()); - }); - } - - variants_panels.push_back(variants_panel); - } - - width = std::max(max_row_width, current_row_width); - - const size_t cols = std::min(max_cols, titles.size()); - - auto *printer_grid = new wxFlexGridSizer(cols, 0, 20); - printer_grid->SetFlexibleDirection(wxVERTICAL | wxHORIZONTAL); - - if (titles.size() > 0) { - const size_t odd_items = titles.size() % cols; - - for (size_t i = 0; i < titles.size() - odd_items; i += cols) { - for (size_t j = i; j < i + cols; j++) { printer_grid->Add(bitmaps[j], 0, wxBOTTOM, 20); } - for (size_t j = i; j < i + cols; j++) { printer_grid->Add(titles[j], 0, wxBOTTOM, 3); } - for (size_t j = i; j < i + cols; j++) { printer_grid->Add(variants_panels[j]); } - - // Add separator space to multiliners - if (titles.size() > cols) { - for (size_t j = i; j < i + cols; j++) { printer_grid->Add(1, 30); } - } - } - if (odd_items > 0) { - const size_t rem = titles.size() - odd_items; - - for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(bitmaps[i], 0, wxBOTTOM, 20); } - for (size_t i = 0; i < cols - odd_items; i++) { printer_grid->AddSpacer(1); } - for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(titles[i], 0, wxBOTTOM, 3); } - for (size_t i = 0; i < cols - odd_items; i++) { printer_grid->AddSpacer(1); } - for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(variants_panels[i]); } - } - } - - auto *title_sizer = new wxBoxSizer(wxHORIZONTAL); - if (! title.IsEmpty()) { - auto *title_widget = new wxStaticText(this, wxID_ANY, title); - title_widget->SetFont(font_title); - title_sizer->Add(title_widget); - } - title_sizer->AddStretchSpacer(); - - if (titles.size() > 1 || is_variants) { - // It only makes sense to add the All / None buttons if there's multiple printers - // All Standard button is added when there are more variants for at least one printer - auto *sel_all_std = new wxButton(this, wxID_ANY, titles.size() > 1 ? _L("All standard") : _L("Standard")); - auto *sel_all = new wxButton(this, wxID_ANY, _L("All")); - auto *sel_none = new wxButton(this, wxID_ANY, _L("None")); - if (is_variants) - sel_all_std->Bind(wxEVT_BUTTON, [this](const wxCommandEvent& event) { this->select_all(true, false); }); - sel_all->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->select_all(true, true); }); - sel_none->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->select_all(false); }); - if (is_variants) - title_sizer->Add(sel_all_std, 0, wxRIGHT, BTN_SPACING); - title_sizer->Add(sel_all, 0, wxRIGHT, BTN_SPACING); - title_sizer->Add(sel_none); - - wxGetApp().UpdateDarkUI(sel_all_std); - wxGetApp().UpdateDarkUI(sel_all); - wxGetApp().UpdateDarkUI(sel_none); - - // fill button indexes used later for buttons rescaling - if (is_variants) - m_button_indexes = { sel_all_std->GetId(), sel_all->GetId(), sel_none->GetId() }; - else { - sel_all_std->Destroy(); - m_button_indexes = { sel_all->GetId(), sel_none->GetId() }; - } - } - - sizer->Add(title_sizer, 0, wxEXPAND | wxBOTTOM, BTN_SPACING); - sizer->Add(printer_grid); - - SetSizer(sizer); -} - -PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxString title, size_t max_cols, const AppConfig &appconfig) - : PrinterPicker(parent, vendor, std::move(title), max_cols, appconfig, [](const VendorProfile::PrinterModel&) { return true; }) -{} - -void PrinterPicker::select_all(bool select, bool alternates) -{ - for (const auto &cb : cboxes) { - if (cb->GetValue() != select) { - cb->SetValue(select); - on_checkbox(cb, select); - } - } - - if (! select) { alternates = false; } - - for (const auto &cb : cboxes_alt) { - if (cb->GetValue() != alternates) { - cb->SetValue(alternates); - on_checkbox(cb, alternates); - } - } -} - -void PrinterPicker::select_one(size_t i, bool select) -{ - if (i < cboxes.size() && cboxes[i]->GetValue() != select) { - cboxes[i]->SetValue(select); - on_checkbox(cboxes[i], select); - } -} - -bool PrinterPicker::any_selected() const -{ - for (const auto &cb : cboxes) { - if (cb->GetValue()) { return true; } - } - - for (const auto &cb : cboxes_alt) { - if (cb->GetValue()) { return true; } - } - - return false; -} - -std::set PrinterPicker::get_selected_models() const -{ - std::set ret_set; - - for (const auto& cb : cboxes) - if (cb->GetValue()) - ret_set.emplace(cb->model); - - for (const auto& cb : cboxes_alt) - if (cb->GetValue()) - ret_set.emplace(cb->model); - - return ret_set; -} - -void PrinterPicker::on_checkbox(const Checkbox *cbox, bool checked) -{ - PrinterPickerEvent evt(EVT_PRINTER_PICK, GetId(), vendor_id, cbox->model, cbox->variant, checked); - AddPendingEvent(evt); -} - - -// Wizard page base - -ConfigWizardPage::ConfigWizardPage(ConfigWizard *parent, wxString title, wxString shortname, unsigned indent) - : wxPanel(parent->p->hscroll) - , parent(parent) - , shortname(std::move(shortname)) - , indent(indent) -{ - wxGetApp().UpdateDarkUI(this); - - auto *sizer = new wxBoxSizer(wxVERTICAL); - - auto *text = new wxStaticText(this, wxID_ANY, std::move(title), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); - const auto font = GetFont().MakeBold().Scaled(1.5); - text->SetFont(font); - sizer->Add(text, 0, wxALIGN_LEFT, 0); - sizer->AddSpacer(10); - - content = new wxBoxSizer(wxVERTICAL); - sizer->Add(content, 1, wxEXPAND); - - SetSizer(sizer); - - // There is strange layout on Linux with GTK3, - // see https://github.com/prusa3d/PrusaSlicer/issues/5103 and https://github.com/prusa3d/PrusaSlicer/issues/4861 - // So, non-active pages will be hidden later, on wxEVT_SHOW, after completed Layout() for all pages - if (!wxLinux_gtk3) - this->Hide(); - - Bind(wxEVT_SIZE, [this](wxSizeEvent &event) { - this->Layout(); - event.Skip(); - }); -} - -ConfigWizardPage::~ConfigWizardPage() {} - -wxStaticText* ConfigWizardPage::append_text(wxString text) -{ - auto *widget = new wxStaticText(this, wxID_ANY, text, wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); - widget->Wrap(WRAP_WIDTH); - widget->SetMinSize(wxSize(WRAP_WIDTH, -1)); - append(widget); - return widget; -} - -void ConfigWizardPage::append_spacer(int space) -{ - // FIXME: scaling - content->AddSpacer(space); -} - -// Wizard pages - -PageWelcome::PageWelcome(ConfigWizard *parent) - : ConfigWizardPage(parent, from_u8((boost::format( -#ifdef __APPLE__ - _utf8(L("Welcome to the %s Configuration Assistant")) -#else - _utf8(L("Welcome to the %s Configuration Wizard")) -#endif - ) % SLIC3R_APP_NAME).str()), _L("Welcome")) - , welcome_text(append_text(from_u8((boost::format( - _utf8(L("Hello, welcome to %s! This %s helps you with the initial configuration; just a few settings and you will be ready to print."))) - % SLIC3R_APP_NAME - % _utf8(ConfigWizard::name())).str()) - )) - , cbox_reset(append( - new wxCheckBox(this, wxID_ANY, _L("Remove user profiles (a snapshot will be taken beforehand)")) - )) - , cbox_integrate(append( - new wxCheckBox(this, wxID_ANY, _L("Perform desktop integration (Sets this binary to be searchable by the system).")) - )) -{ - welcome_text->Hide(); - cbox_reset->Hide(); - cbox_integrate->Hide(); -} - -void PageWelcome::set_run_reason(ConfigWizard::RunReason run_reason) -{ - const bool data_empty = run_reason == ConfigWizard::RR_DATA_EMPTY; - welcome_text->Show(data_empty); - cbox_reset->Show(!data_empty); -#if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) - if (!DesktopIntegrationDialog::is_integrated()) - cbox_integrate->Show(true); - else - cbox_integrate->Hide(); -#else - cbox_integrate->Hide(); -#endif -} - - -PagePrinters::PagePrinters(ConfigWizard *parent, - wxString title, - wxString shortname, - const VendorProfile &vendor, - unsigned indent, - Technology technology) - : ConfigWizardPage(parent, std::move(title), std::move(shortname), indent) - , technology(technology) - , install(false) // only used for 3rd party vendors -{ - enum { - COL_SIZE = 200, - }; - - AppConfig *appconfig = &this->wizard_p()->appconfig_new; - - const auto families = vendor.families(); - for (const auto &family : families) { - const auto filter = [&](const VendorProfile::PrinterModel &model) { - return ((model.technology == ptFFF && technology & T_FFF) - || (model.technology == ptSLA && technology & T_SLA)) - && model.family == family; - }; - - if (std::find_if(vendor.models.begin(), vendor.models.end(), filter) == vendor.models.end()) { - continue; - } - - const auto picker_title = family.empty() ? wxString() : from_u8((boost::format(_utf8(L("%s Family"))) % family).str()); - auto *picker = new PrinterPicker(this, vendor, picker_title, MAX_COLS, *appconfig, filter); - - picker->Bind(EVT_PRINTER_PICK, [this, appconfig](const PrinterPickerEvent &evt) { - appconfig->set_variant(evt.vendor_id, evt.model_id, evt.variant_name, evt.enable); - wizard_p()->on_printer_pick(this, evt); - }); - - append(new StaticLine(this)); - - append(picker); - printer_pickers.push_back(picker); - has_printers = true; - } - -} - -void PagePrinters::select_all(bool select, bool alternates) -{ - for (auto picker : printer_pickers) { - picker->select_all(select, alternates); - } -} - -int PagePrinters::get_width() const -{ - return std::accumulate(printer_pickers.begin(), printer_pickers.end(), 0, - [](int acc, const PrinterPicker *picker) { return std::max(acc, picker->get_width()); }); -} - -bool PagePrinters::any_selected() const -{ - for (const auto *picker : printer_pickers) { - if (picker->any_selected()) { return true; } - } - - return false; -} - -std::set PagePrinters::get_selected_models() -{ - std::set ret_set; - - for (const auto *picker : printer_pickers) - { - std::set tmp_models = picker->get_selected_models(); - ret_set.insert(tmp_models.begin(), tmp_models.end()); - } - - return ret_set; -} - -void PagePrinters::set_run_reason(ConfigWizard::RunReason run_reason) -{ - if (is_primary_printer_page - && (run_reason == ConfigWizard::RR_DATA_EMPTY || run_reason == ConfigWizard::RR_DATA_LEGACY) - && printer_pickers.size() > 0 - && printer_pickers[0]->vendor_id == PresetBundle::PRUSA_BUNDLE) { - printer_pickers[0]->select_one(0, true); - } -} - - -const std::string PageMaterials::EMPTY; - -PageMaterials::PageMaterials(ConfigWizard *parent, Materials *materials, wxString title, wxString shortname, wxString list1name) - : ConfigWizardPage(parent, std::move(title), std::move(shortname)) - , materials(materials) - , list_printer(new StringList(this, wxLB_MULTIPLE)) - , list_type(new StringList(this)) - , list_vendor(new StringList(this)) - , list_profile(new PresetList(this)) -{ - append_spacer(VERTICAL_SPACING); - - const int em = parent->em_unit(); - const int list_h = 30*em; - - - list_printer->SetMinSize(wxSize(23*em, list_h)); - list_type->SetMinSize(wxSize(13*em, list_h)); - list_vendor->SetMinSize(wxSize(13*em, list_h)); - list_profile->SetMinSize(wxSize(23*em, list_h)); - - - - grid = new wxFlexGridSizer(4, em/2, em); - grid->AddGrowableCol(3, 1); - grid->AddGrowableRow(1, 1); - - grid->Add(new wxStaticText(this, wxID_ANY, _L("Printer:"))); - grid->Add(new wxStaticText(this, wxID_ANY, list1name)); - grid->Add(new wxStaticText(this, wxID_ANY, _L("Vendor:"))); - grid->Add(new wxStaticText(this, wxID_ANY, _L("Profile:"))); - - grid->Add(list_printer, 0, wxEXPAND); - grid->Add(list_type, 0, wxEXPAND); - grid->Add(list_vendor, 0, wxEXPAND); - grid->Add(list_profile, 1, wxEXPAND); - - auto *btn_sizer = new wxBoxSizer(wxHORIZONTAL); - auto *sel_all = new wxButton(this, wxID_ANY, _L("All")); - auto *sel_none = new wxButton(this, wxID_ANY, _L("None")); - btn_sizer->Add(sel_all, 0, wxRIGHT, em / 2); - btn_sizer->Add(sel_none); - - wxGetApp().UpdateDarkUI(list_printer); - wxGetApp().UpdateDarkUI(list_type); - wxGetApp().UpdateDarkUI(list_vendor); - wxGetApp().UpdateDarkUI(sel_all); - wxGetApp().UpdateDarkUI(sel_none); - - grid->Add(new wxBoxSizer(wxHORIZONTAL)); - grid->Add(new wxBoxSizer(wxHORIZONTAL)); - grid->Add(new wxBoxSizer(wxHORIZONTAL)); - grid->Add(btn_sizer, 0, wxALIGN_RIGHT); - - append(grid, 1, wxEXPAND); - - append_spacer(VERTICAL_SPACING); - - html_window = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, - wxSize(60 * em, 20 * em), wxHW_SCROLLBAR_AUTO); - append(html_window, 0, wxEXPAND); - - list_printer->Bind(wxEVT_LISTBOX, [this](wxCommandEvent& evt) { - update_lists(list_type->GetSelection(), list_vendor->GetSelection(), evt.GetInt()); - }); - list_type->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { - update_lists(list_type->GetSelection(), list_vendor->GetSelection()); - }); - list_vendor->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { - update_lists(list_type->GetSelection(), list_vendor->GetSelection()); - }); - - list_profile->Bind(wxEVT_CHECKLISTBOX, [this](wxCommandEvent &evt) { select_material(evt.GetInt()); }); - list_profile->Bind(wxEVT_LISTBOX, [this](wxCommandEvent& evt) { on_material_highlighted(evt.GetInt()); }); - - sel_all->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { select_all(true); }); - sel_none->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { select_all(false); }); - /* - Bind(wxEVT_PAINT, [this](wxPaintEvent& evt) {on_paint();}); - - list_profile->Bind(wxEVT_MOTION, [this](wxMouseEvent& evt) { on_mouse_move_on_profiles(evt); }); - list_profile->Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& evt) { on_mouse_enter_profiles(evt); }); - list_profile->Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& evt) { on_mouse_leave_profiles(evt); }); - */ - reload_presets(); - set_compatible_printers_html_window(std::vector(), false); -} -void PageMaterials::on_paint() -{ -} -void PageMaterials::on_mouse_move_on_profiles(wxMouseEvent& evt) -{ - const wxClientDC dc(list_profile); - const wxPoint pos = evt.GetLogicalPosition(dc); - int item = list_profile->HitTest(pos); - on_material_hovered(item); -} -void PageMaterials::on_mouse_enter_profiles(wxMouseEvent& evt) -{} -void PageMaterials::on_mouse_leave_profiles(wxMouseEvent& evt) -{ - on_material_hovered(-1); -} -void PageMaterials::reload_presets() -{ - clear(); - - list_printer->append(_L("(All)"), &EMPTY); - //list_printer->SetLabelMarkup("bald"); - for (const Preset* printer : materials->printers) { - list_printer->append(printer->name, &printer->name); - } - sort_list_data(list_printer, true, false); - if (list_printer->GetCount() > 0) { - list_printer->SetSelection(0); - sel_printers_prev.Clear(); - sel_type_prev = wxNOT_FOUND; - sel_vendor_prev = wxNOT_FOUND; - update_lists(0, 0, 0); - } - - presets_loaded = true; -} - -void PageMaterials::set_compatible_printers_html_window(const std::vector& printer_names, bool all_printers) -{ - const auto bgr_clr = -#if defined(__APPLE__) - html_window->GetParent()->GetBackgroundColour(); -#else -#if defined(_WIN32) - wxGetApp().get_window_default_clr(); -#else - wxSystemSettings::GetColour(wxSYS_COLOUR_MENU); -#endif -#endif - const auto text_clr = wxGetApp().get_label_clr_default(); - const auto bgr_clr_str = encode_color(ColorRGB(bgr_clr.Red(), bgr_clr.Green(), bgr_clr.Blue())); - const auto text_clr_str = encode_color(ColorRGB(text_clr.Red(), text_clr.Green(), text_clr.Blue())); - wxString first_line = format_wxstr(_L("%1% marked with * are not compatible with some installed printers."), materials->technology == T_FFF ? _L("Filaments") : _L("SLA materials")); - wxString text; - if (all_printers) { - wxString second_line = format_wxstr(_L("All installed printers are compatible with the selected %1%."), materials->technology == T_FFF ? _L("filament") : _L("SLA material")); - text = wxString::Format( - "" - "" - "" - "" - "" - "%s

%s" - "
" - "
" - "" - "" - , bgr_clr_str - , text_clr_str - , first_line - , second_line - ); - } else { - wxString second_line; - if (!printer_names.empty()) - second_line = (materials->technology == T_FFF ? - _L("Only the following installed printers are compatible with the selected filaments") : - _L("Only the following installed printers are compatible with the selected SLA materials")) + ":"; - text = wxString::Format( - "" - "" - "" - "" - "" - "%s

%s" - "" - "" - , bgr_clr_str - , text_clr_str - , first_line - , second_line); - for (size_t i = 0; i < printer_names.size(); ++i) - { - text += wxString::Format("", boost::nowide::widen(printer_names[i])); - if (i % 3 == 2) { - text += wxString::Format( - "" - ""); - } - } - text += wxString::Format( - "" - "
%s
" - "
" - "
" - "" - "" - ); - } - - wxFont font = get_default_font_for_dpi(this, get_dpi_for_window(this)); - const int fs = font.GetPointSize(); - int size[] = { fs,fs,fs,fs,fs,fs,fs }; - html_window->SetFonts(font.GetFaceName(), font.GetFaceName(), size); - html_window->SetPage(text); -} - -void PageMaterials::clear_compatible_printers_label() -{ - set_compatible_printers_html_window(std::vector(), false); -} - -void PageMaterials::on_material_hovered(int sel_material) -{ - -} - -void PageMaterials::on_material_highlighted(int sel_material) -{ - if (sel_material == last_hovered_item) - return; - if (sel_material == -1) { - clear_compatible_printers_label(); - return; - } - last_hovered_item = sel_material; - std::vector tabs; - tabs.push_back(std::string()); - tabs.push_back(std::string()); - tabs.push_back(std::string()); - //selected material string - std::string material_name = list_profile->get_data(sel_material); - // get material preset - const std::vector matching_materials = materials->get_presets_by_alias(material_name); - if (matching_materials.empty()) - { - clear_compatible_printers_label(); - return; - } - //find matching printers - std::vector names; - for (const Preset* printer : materials->printers) { - for (const Preset* material : matching_materials) { - if (is_compatible_with_printer(PresetWithVendorProfile(*material, material->vendor), PresetWithVendorProfile(*printer, printer->vendor))) { - names.push_back(printer->name); - break; - } - } - } - set_compatible_printers_html_window(names, names.size() == materials->printers.size()); -} - -void PageMaterials::update_lists(int sel_type, int sel_vendor, int last_selected_printer/* = -1*/) -{ - wxWindowUpdateLocker freeze_guard(this); - (void)freeze_guard; - - wxArrayInt sel_printers; - int sel_printers_count = list_printer->GetSelections(sel_printers); - - // Does our wxWidgets version support operator== for wxArrayInt ? - // https://github.com/prusa3d/PrusaSlicer/issues/5152#issuecomment-787208614 -#if wxCHECK_VERSION(3, 1, 1) - if (sel_printers != sel_printers_prev) { -#else - auto are_equal = [](const wxArrayInt& arr_first, const wxArrayInt& arr_second) { - if (arr_first.GetCount() != arr_second.GetCount()) - return false; - for (size_t i = 0; i < arr_first.GetCount(); i++) - if (arr_first[i] != arr_second[i]) - return false; - return true; - }; - if (!are_equal(sel_printers, sel_printers_prev)) { -#endif - - // Refresh type list - list_type->Clear(); - list_type->append(_L("(All)"), &EMPTY); - if (sel_printers_count > 0) { - // If all is selected with other printers - // unselect "all" or all printers depending on last value - if (sel_printers[0] == 0 && sel_printers_count > 1) { - if (last_selected_printer == 0) { - list_printer->SetSelection(wxNOT_FOUND); - list_printer->SetSelection(0); - } else { - list_printer->SetSelection(0, false); - sel_printers_count = list_printer->GetSelections(sel_printers); - } - } - if (sel_printers[0] != 0) { - for (int i = 0; i < sel_printers_count; i++) { - const std::string& printer_name = list_printer->get_data(sel_printers[i]); - const Preset* printer = nullptr; - for (const Preset* it : materials->printers) { - if (it->name == printer_name) { - printer = it; - break; - } - } - materials->filter_presets(printer, EMPTY, EMPTY, [this](const Preset* p) { - const std::string& type = this->materials->get_type(p); - if (list_type->find(type) == wxNOT_FOUND) { - list_type->append(type, &type); - } - }); - } - } else { - //clear selection except "ALL" - list_printer->SetSelection(wxNOT_FOUND); - list_printer->SetSelection(0); - sel_printers_count = list_printer->GetSelections(sel_printers); - - materials->filter_presets(nullptr, EMPTY, EMPTY, [this](const Preset* p) { - const std::string& type = this->materials->get_type(p); - if (list_type->find(type) == wxNOT_FOUND) { - list_type->append(type, &type); - } - }); - } - sort_list_data(list_type, true, true); - } - - sel_printers_prev = sel_printers; - sel_type = 0; - sel_type_prev = wxNOT_FOUND; - list_type->SetSelection(sel_type); - list_profile->Clear(); - } - - if (sel_type != sel_type_prev) { - // Refresh vendor list - - // XXX: The vendor list is created with quadratic complexity here, - // but the number of vendors is going to be very small this shouldn't be a problem. - - list_vendor->Clear(); - list_vendor->append(_L("(All)"), &EMPTY); - if (sel_printers_count != 0 && sel_type != wxNOT_FOUND) { - const std::string& type = list_type->get_data(sel_type); - // find printer preset - for (int i = 0; i < sel_printers_count; i++) { - const std::string& printer_name = list_printer->get_data(sel_printers[i]); - const Preset* printer = nullptr; - for (const Preset* it : materials->printers) { - if (it->name == printer_name) { - printer = it; - break; - } - } - materials->filter_presets(printer, type, EMPTY, [this](const Preset* p) { - const std::string& vendor = this->materials->get_vendor(p); - if (list_vendor->find(vendor) == wxNOT_FOUND) { - list_vendor->append(vendor, &vendor); - } - }); - } - sort_list_data(list_vendor, true, false); - } - - sel_type_prev = sel_type; - sel_vendor = 0; - sel_vendor_prev = wxNOT_FOUND; - list_vendor->SetSelection(sel_vendor); - list_profile->Clear(); - } - - if (sel_vendor != sel_vendor_prev) { - // Refresh material list - - list_profile->Clear(); - clear_compatible_printers_label(); - if (sel_printers_count != 0 && sel_type != wxNOT_FOUND && sel_vendor != wxNOT_FOUND) { - const std::string& type = list_type->get_data(sel_type); - const std::string& vendor = list_vendor->get_data(sel_vendor); - // finst printer preset - std::vector to_list; - for (int i = 0; i < sel_printers_count; i++) { - const std::string& printer_name = list_printer->get_data(sel_printers[i]); - const Preset* printer = nullptr; - for (const Preset* it : materials->printers) { - if (it->name == printer_name) { - printer = it; - break; - } - } - - materials->filter_presets(printer, type, vendor, [this, &to_list](const Preset* p) { - const std::string& section = materials->appconfig_section(); - bool checked = wizard_p()->appconfig_new.has(section, p->name); - bool was_checked = false; - - int cur_i = list_profile->find(p->alias); - if (cur_i == wxNOT_FOUND) { - cur_i = list_profile->append(p->alias + (materials->get_omnipresent(p) ? "" : " *"), &p->alias); - to_list.emplace_back(p->alias, materials->get_omnipresent(p), checked); - } - else { - was_checked = list_profile->IsChecked(cur_i); - to_list[cur_i].checked = checked || was_checked; - } - list_profile->Check(cur_i, checked || was_checked); - - /* Update preset selection in config. - * If one preset from aliases bundle is selected, - * than mark all presets with this aliases as selected - * */ - if (checked && !was_checked) - wizard_p()->update_presets_in_config(section, p->alias, true); - else if (!checked && was_checked) - wizard_p()->appconfig_new.set(section, p->name, "1"); - }); - } - sort_list_data(list_profile, to_list); - } - - sel_vendor_prev = sel_vendor; - } - wxGetApp().UpdateDarkUI(list_profile); -} - -void PageMaterials::sort_list_data(StringList* list, bool add_All_item, bool material_type_ordering) -{ -// get data from list -// sort data -// first should be -// then prusa profiles -// then the rest -// in alphabetical order - - std::vector> prusa_profiles; - std::vector> other_profiles; - for (int i = 0 ; i < list->size(); ++i) { - const std::string& data = list->get_data(i); - if (data == EMPTY) // do not sort item - continue; - if (!material_type_ordering && data.find("Prusa") != std::string::npos) - prusa_profiles.push_back(data); - else - other_profiles.push_back(data); - } - if(material_type_ordering) { - - const ConfigOptionDef* def = print_config_def.get("filament_type"); - std::vectorenum_values = def->enum_values; - size_t end_of_sorted = 0; - for (size_t vals = 0; vals < enum_values.size(); vals++) { - for (size_t profs = end_of_sorted; profs < other_profiles.size(); profs++) - { - // find instead compare because PET vs PETG - if (other_profiles[profs].get().find(enum_values[vals]) != std::string::npos) { - //swap - if(profs != end_of_sorted) { - std::reference_wrapper aux = other_profiles[end_of_sorted]; - other_profiles[end_of_sorted] = other_profiles[profs]; - other_profiles[profs] = aux; - } - end_of_sorted++; - break; - } - } - } - } else { - std::sort(prusa_profiles.begin(), prusa_profiles.end(), [](std::reference_wrapper a, std::reference_wrapper b) { - return a.get() < b.get(); - }); - std::sort(other_profiles.begin(), other_profiles.end(), [](std::reference_wrapper a, std::reference_wrapper b) { - return a.get() < b.get(); - }); - } - - list->Clear(); - if (add_All_item) - list->append(_L("(All)"), &EMPTY); - for (const auto& item : prusa_profiles) - list->append(item, &const_cast(item.get())); - for (const auto& item : other_profiles) - list->append(item, &const_cast(item.get())); -} - -void PageMaterials::sort_list_data(PresetList* list, const std::vector& data) -{ - // sort data - // then prusa profiles - // then the rest - // in alphabetical order - std::vector prusa_profiles; - std::vector other_profiles; - //for (int i = 0; i < data.size(); ++i) { - for (const auto& item : data) { - const std::string& name = item.name; - if (name.find("Prusa") != std::string::npos) - prusa_profiles.emplace_back(item); - else - other_profiles.emplace_back(item); - } - std::sort(prusa_profiles.begin(), prusa_profiles.end(), [](ProfilePrintData a, ProfilePrintData b) { - return a.name.get() < b.name.get(); - }); - std::sort(other_profiles.begin(), other_profiles.end(), [](ProfilePrintData a, ProfilePrintData b) { - return a.name.get() < b.name.get(); - }); - list->Clear(); - for (size_t i = 0; i < prusa_profiles.size(); ++i) { - list->append(std::string(prusa_profiles[i].name) + (prusa_profiles[i].omnipresent ? "" : " *"), &const_cast(prusa_profiles[i].name.get())); - list->Check(i, prusa_profiles[i].checked); - } - for (size_t i = 0; i < other_profiles.size(); ++i) { - list->append(std::string(other_profiles[i].name) + (other_profiles[i].omnipresent ? "" : " *"), &const_cast(other_profiles[i].name.get())); - list->Check(i + prusa_profiles.size(), other_profiles[i].checked); - } -} - -void PageMaterials::select_material(int i) -{ - const bool checked = list_profile->IsChecked(i); - - const std::string& alias_key = list_profile->get_data(i); - wizard_p()->update_presets_in_config(materials->appconfig_section(), alias_key, checked); -} - -void PageMaterials::select_all(bool select) -{ - wxWindowUpdateLocker freeze_guard(this); - (void)freeze_guard; - - for (unsigned i = 0; i < list_profile->GetCount(); i++) { - const bool current = list_profile->IsChecked(i); - if (current != select) { - list_profile->Check(i, select); - select_material(i); - } - } -} - -void PageMaterials::clear() -{ - list_printer->Clear(); - list_type->Clear(); - list_vendor->Clear(); - list_profile->Clear(); - sel_printers_prev.Clear(); - sel_type_prev = wxNOT_FOUND; - sel_vendor_prev = wxNOT_FOUND; - presets_loaded = false; -} - -void PageMaterials::on_activate() -{ - if (! presets_loaded) { - wizard_p()->update_materials(materials->technology); - reload_presets(); - } - first_paint = true; -} - - -const char *PageCustom::default_profile_name = "My Settings"; - -PageCustom::PageCustom(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Custom Printer Setup"), _L("Custom Printer")) -{ - cb_custom = new wxCheckBox(this, wxID_ANY, _L("Define a custom printer profile")); - auto *label = new wxStaticText(this, wxID_ANY, _L("Custom profile name:")); - - wxBoxSizer* profile_name_sizer = new wxBoxSizer(wxVERTICAL); - profile_name_editor = new SavePresetDialog::Item{ this, profile_name_sizer, default_profile_name }; - profile_name_editor->Enable(false); - - cb_custom->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &) { - profile_name_editor->Enable(custom_wanted()); - wizard_p()->on_custom_setup(custom_wanted()); - }); - - append(cb_custom); - append(label); - append(profile_name_sizer); -} - -PageUpdate::PageUpdate(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Automatic updates"), _L("Updates")) - , version_check(true) - , preset_update(true) -{ - const AppConfig *app_config = wxGetApp().app_config; - auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - boldfont.SetWeight(wxFONTWEIGHT_BOLD); - - auto *box_slic3r = new wxCheckBox(this, wxID_ANY, _L("Check for application updates")); - box_slic3r->SetValue(app_config->get("notify_release") != "none"); - append(box_slic3r); - append_text(wxString::Format(_L( - "If enabled, %s checks for new application versions online. When a new version becomes available, " - "a notification is displayed at the next application startup (never during program usage). " - "This is only a notification mechanisms, no automatic installation is done."), SLIC3R_APP_NAME)); - - append_spacer(VERTICAL_SPACING); - - auto *box_presets = new wxCheckBox(this, wxID_ANY, _L("Update built-in Presets automatically")); - box_presets->SetValue(app_config->get("preset_update") == "1"); - append(box_presets); - append_text(wxString::Format(_L( - "If enabled, %s downloads updates of built-in system presets in the background." - "These updates are downloaded into a separate temporary location." - "When a new preset version becomes available it is offered at application startup."), SLIC3R_APP_NAME)); - const auto text_bold = _L("Updates are never applied without user's consent and never overwrite user's customized settings."); - auto *label_bold = new wxStaticText(this, wxID_ANY, text_bold); - label_bold->SetFont(boldfont); - label_bold->Wrap(WRAP_WIDTH); - append(label_bold); - append_text(_L("Additionally a backup snapshot of the whole configuration is created before an update is applied.")); - - box_slic3r->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->version_check = event.IsChecked(); }); - box_presets->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->preset_update = event.IsChecked(); }); -} - -namespace DownloaderUtils -{ -#ifdef _WIN32 - - wxString get_downloads_path() - { - wxString ret; - PWSTR path = NULL; - HRESULT hr = SHGetKnownFolderPath(FOLDERID_Downloads, 0, NULL, &path); - if (SUCCEEDED(hr)) { - ret = wxString(path); - } - CoTaskMemFree(path); - return ret; - } - -#elif __APPLE__ - wxString get_downloads_path() - { - // call objective-c implementation - return wxString::FromUTF8(get_downloads_path_mac()); - } -#else - wxString get_downloads_path() - { - wxString command = "xdg-user-dir DOWNLOAD"; - wxArrayString output; - GUI::desktop_execute_get_result(command, output); - if (output.GetCount() > 0) { - return output[0]; - } - return wxString(); - } - -#endif - -Worker::Worker(wxWindow* parent) -: wxBoxSizer(wxHORIZONTAL) -, m_parent(parent) -{ - m_input_path = new wxTextCtrl(m_parent, wxID_ANY); - set_path_name(get_app_config()->get("url_downloader_dest")); - - auto* path_label = new wxStaticText(m_parent, wxID_ANY, _L("Download path") + ":"); - - this->Add(path_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); - this->Add(m_input_path, 1, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 5); - - auto* button_path = new wxButton(m_parent, wxID_ANY, _L("Browse")); - this->Add(button_path, 0, wxEXPAND | wxTOP | wxLEFT, 5); - button_path->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { - boost::filesystem::path chosen_dest(boost::nowide::narrow(m_input_path->GetValue())); - - wxDirDialog dialog(m_parent, L("Choose folder:"), chosen_dest.string() ); - if (dialog.ShowModal() == wxID_OK) - this->m_input_path->SetValue(dialog.GetPath()); - }); - - for (wxSizerItem* item : this->GetChildren()) - if (item->IsWindow()) { - wxWindow* win = item->GetWindow(); - wxGetApp().UpdateDarkUI(win); - } -} - -void Worker::set_path_name(wxString path) -{ - if (path.empty()) - path = boost::nowide::widen(get_app_config()->get("url_downloader_dest")); - - if (path.empty()) { - // What should be default path? Each system has Downloads folder, that could be good one. - // Other would be program location folder - not so good: access rights, apple bin is inside bundle... - // default_path = boost::dll::program_location().parent_path().string(); - path = get_downloads_path(); - } - - m_input_path->SetValue(path); -} - -void Worker::set_path_name(const std::string& name) -{ - if (!m_input_path) - return; - - set_path_name(boost::nowide::widen(name)); -} - -} // DownLoader - -PageDownloader::PageDownloader(ConfigWizard* parent) - : ConfigWizardPage(parent, _L("Downloads from URL"), _L("Downloads")) -{ - const AppConfig* app_config = wxGetApp().app_config; - auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - boldfont.SetWeight(wxFONTWEIGHT_BOLD); - - append_spacer(VERTICAL_SPACING); - - auto* box_allow_downloads = new wxCheckBox(this, wxID_ANY, _L("Allow build-in downloader")); - // TODO: Do we want it like this? The downloader is allowed for very first time the wizard is run. - bool box_allow_value = (app_config->has("downloader_url_registered") ? app_config->get("downloader_url_registered") == "1" : true); - box_allow_downloads->SetValue(box_allow_value); - append(box_allow_downloads); - - append_text(wxString::Format(_L( - "If enabled, %s registers to start on custom URL on www.printables.com." - " You will be able to use button with %s logo to open models in this %s." - " The model will be downloaded into folder you choose bellow." - ), SLIC3R_APP_NAME, SLIC3R_APP_NAME, SLIC3R_APP_NAME)); - -#ifdef __linux__ - append_text(wxString::Format(_L( - "On Linux systems the process of registration also creates desktop integration files for this version of application." - ))); -#endif - - box_allow_downloads->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) { this->downloader->allow(event.IsChecked()); }); - - downloader = new DownloaderUtils::Worker(this); - append(downloader); - downloader->allow(box_allow_value); -} - -bool PageDownloader::on_finish_downloader() const -{ - return downloader->on_finish(); -} - -bool DownloaderUtils::Worker::perform_register() -{ - //boost::filesystem::path chosen_dest/*(path_text_ctrl->GetValue());*/(boost::nowide::narrow(path_text_ctrl->GetValue())); - boost::filesystem::path chosen_dest (GUI::format(path_name())); - boost::system::error_code ec; - if (chosen_dest.empty() || !boost::filesystem::is_directory(chosen_dest, ec) || ec) { - std::string err_msg = GUI::format("%1%\n\n%2%",_L("Chosen directory for downloads does not Exists.") ,chosen_dest.string()); - BOOST_LOG_TRIVIAL(error) << err_msg; - show_error(m_parent, err_msg); - return false; - } - BOOST_LOG_TRIVIAL(info) << "Downloader registration: Directory for downloads: " << chosen_dest.string(); - wxGetApp().app_config->set("url_downloader_dest", chosen_dest.string()); -#ifdef _WIN32 - // Registry key creation for "prusaslicer://" URL - - boost::filesystem::path binary_path(boost::filesystem::canonical(boost::dll::program_location())); - // the path to binary needs to be correctly saved in string with respect to localized characters - wxString wbinary = wxString::FromUTF8(binary_path.string()); - std::string binary_string = (boost::format("%1%") % wbinary).str(); - BOOST_LOG_TRIVIAL(info) << "Downloader registration: Path of binary: " << binary_string; - - //std::string key_string = "\"" + binary_string + "\" \"-u\" \"%1\""; - //std::string key_string = "\"" + binary_string + "\" \"%1\""; - std::string key_string = "\"" + binary_string + "\" \"--single-instance\" \"%1\""; - - wxRegKey key_first(wxRegKey::HKCU, "Software\\Classes\\prusaslicer"); - wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\prusaslicer\\shell\\open\\command"); - if (!key_first.Exists()) { - key_first.Create(false); - } - key_first.SetValue("URL Protocol", ""); - - if (!key_full.Exists()) { - key_full.Create(false); - } - //key_full = "\"C:\\Program Files\\Prusa3D\\PrusaSlicer\\prusa-slicer-console.exe\" \"%1\""; - key_full = key_string; -#elif __APPLE__ - // Apple registers for custom url in info.plist thus it has to be already registered since build. - // The url will always trigger opening of prusaslicer and we have to check that user has allowed it. (GUI_App::MacOpenURL is the triggered method) -#else - // the performation should be called later during desktop integration - perform_registration_linux = true; -#endif - return true; -} - -void DownloaderUtils::Worker::deregister() -{ -#ifdef _WIN32 - std::string key_string = ""; - wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\prusaslicer\\shell\\open\\command"); - if (!key_full.Exists()) { - return; - } - key_full = key_string; -#elif __APPLE__ - // TODO -#else - BOOST_LOG_TRIVIAL(debug) << "DesktopIntegrationDialog::undo_downloader_registration"; - DesktopIntegrationDialog::undo_downloader_registration(); - perform_registration_linux = false; -#endif -} - -bool DownloaderUtils::Worker::on_finish() { - AppConfig* app_config = wxGetApp().app_config; - bool ac_value = app_config->get("downloader_url_registered") == "1"; - BOOST_LOG_TRIVIAL(debug) << "PageDownloader::on_finish_downloader ac_value " << ac_value << " downloader_checked " << downloader_checked; - if (ac_value && downloader_checked) { - // already registered but we need to do it again - if (!perform_register()) - return false; - app_config->set("downloader_url_registered", "1"); - } else if (!ac_value && downloader_checked) { - // register - if (!perform_register()) - return false; - app_config->set("downloader_url_registered", "1"); - } else if (ac_value && !downloader_checked) { - // deregister, downloads are banned now - deregister(); - app_config->set("downloader_url_registered", "0"); - } /*else if (!ac_value && !downloader_checked) { - // not registered and we dont want to do it - // do not deregister as other instance might be registered - } */ - return true; -} - - -PageReloadFromDisk::PageReloadFromDisk(ConfigWizard* parent) - : ConfigWizardPage(parent, _L("Reload from disk"), _L("Reload from disk")) - , full_pathnames(false) -{ - auto* box_pathnames = new wxCheckBox(this, wxID_ANY, _L("Export full pathnames of models and parts sources into 3mf and amf files")); - box_pathnames->SetValue(wxGetApp().app_config->get("export_sources_full_pathnames") == "1"); - append(box_pathnames); - append_text(_L( - "If enabled, allows the Reload from disk command to automatically find and load the files when invoked.\n" - "If not enabled, the Reload from disk command will ask to select each file using an open file dialog." - )); - - box_pathnames->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) { this->full_pathnames = event.IsChecked(); }); -} - -#ifdef _WIN32 -PageFilesAssociation::PageFilesAssociation(ConfigWizard* parent) - : ConfigWizardPage(parent, _L("Files association"), _L("Files association")) -{ - cb_3mf = new wxCheckBox(this, wxID_ANY, _L("Associate .3mf files to PrusaSlicer")); - cb_stl = new wxCheckBox(this, wxID_ANY, _L("Associate .stl files to PrusaSlicer")); -// cb_gcode = new wxCheckBox(this, wxID_ANY, _L("Associate .gcode files to PrusaSlicer G-code Viewer")); - - append(cb_3mf); - append(cb_stl); -// append(cb_gcode); -} -#endif // _WIN32 - -PageMode::PageMode(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("View mode"), _L("View mode")) -{ - append_text(_L("PrusaSlicer's user interfaces comes in three variants:\nSimple, Advanced, and Expert.\n" - "The Simple mode shows only the most frequently used settings relevant for regular 3D printing. " - "The other two offer progressively more sophisticated fine-tuning, " - "they are suitable for advanced and expert users, respectively.")); - - radio_simple = new wxRadioButton(this, wxID_ANY, _L("Simple mode")); - radio_advanced = new wxRadioButton(this, wxID_ANY, _L("Advanced mode")); - radio_expert = new wxRadioButton(this, wxID_ANY, _L("Expert mode")); - - std::string mode { "simple" }; - wxGetApp().app_config->get("", "view_mode", mode); - - if (mode == "advanced") { radio_advanced->SetValue(true); } - else if (mode == "expert") { radio_expert->SetValue(true); } - else { radio_simple->SetValue(true); } - - append(radio_simple); - append(radio_advanced); - append(radio_expert); - - append_text("\n" + _L("The size of the object can be specified in inches")); - check_inch = new wxCheckBox(this, wxID_ANY, _L("Use inches")); - check_inch->SetValue(wxGetApp().app_config->get("use_inches") == "1"); - append(check_inch); - - on_activate(); -} - -void PageMode::serialize_mode(AppConfig *app_config) const -{ - std::string mode = ""; - - if (radio_simple->GetValue()) { mode = "simple"; } - if (radio_advanced->GetValue()) { mode = "advanced"; } - if (radio_expert->GetValue()) { mode = "expert"; } - - app_config->set("view_mode", mode); - app_config->set("use_inches", check_inch->GetValue() ? "1" : "0"); -} - -PageVendors::PageVendors(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Other Vendors"), _L("Other Vendors")) -{ - const AppConfig &appconfig = this->wizard_p()->appconfig_new; - - append_text(wxString::Format(_L("Pick another vendor supported by %s"), SLIC3R_APP_NAME) + ":"); - - auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - boldfont.SetWeight(wxFONTWEIGHT_BOLD); - - for (const auto &pair : wizard_p()->bundles) { - const VendorProfile *vendor = pair.second.vendor_profile; - if (vendor->id == PresetBundle::PRUSA_BUNDLE) { continue; } - - auto *cbox = new wxCheckBox(this, wxID_ANY, vendor->name); - cbox->Bind(wxEVT_CHECKBOX, [=](wxCommandEvent &event) { - wizard_p()->on_3rdparty_install(vendor, cbox->IsChecked()); - }); - - const auto &vendors = appconfig.vendors(); - const bool enabled = vendors.find(pair.first) != vendors.end(); - if (enabled) { - cbox->SetValue(true); - - auto pages = wizard_p()->pages_3rdparty.find(vendor->id); - wxCHECK_RET(pages != wizard_p()->pages_3rdparty.end(), "Internal error: 3rd party vendor printers page not created"); - - for (PagePrinters* page : { pages->second.first, pages->second.second }) - if (page) page->install = true; - } - - append(cbox); - } -} - -PageFirmware::PageFirmware(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Firmware Type"), _L("Firmware"), 1) - , gcode_opt(*print_config_def.get("gcode_flavor")) - , gcode_picker(nullptr) -{ - append_text(_L("Choose the type of firmware used by your printer.")); - append_text(_(gcode_opt.tooltip)); - - wxArrayString choices; - choices.Alloc(gcode_opt.enum_labels.size()); - for (const auto &label : gcode_opt.enum_labels) { - choices.Add(label); - } - - gcode_picker = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices); - wxGetApp().UpdateDarkUI(gcode_picker); - const auto &enum_values = gcode_opt.enum_values; - auto needle = enum_values.cend(); - if (gcode_opt.default_value) { - needle = std::find(enum_values.cbegin(), enum_values.cend(), gcode_opt.default_value->serialize()); - } - if (needle != enum_values.cend()) { - gcode_picker->SetSelection(needle - enum_values.cbegin()); - } else { - gcode_picker->SetSelection(0); - } - - append(gcode_picker); -} - -void PageFirmware::apply_custom_config(DynamicPrintConfig &config) -{ - auto sel = gcode_picker->GetSelection(); - if (sel >= 0 && (size_t)sel < gcode_opt.enum_labels.size()) { - auto *opt = new ConfigOptionEnum(static_cast(sel)); - config.set_key_value("gcode_flavor", opt); - } -} - -static void focus_event(wxFocusEvent& e, wxTextCtrl* ctrl, double def_value) -{ - e.Skip(); - wxString str = ctrl->GetValue(); - - const char dec_sep = is_decimal_separator_point() ? '.' : ','; - const char dec_sep_alt = dec_sep == '.' ? ',' : '.'; - // Replace the first incorrect separator in decimal number. - bool was_replaced = str.Replace(dec_sep_alt, dec_sep, false) != 0; - - double val = 0.0; - if (!str.ToDouble(&val)) { - if (val == 0.0) - val = def_value; - ctrl->SetValue(double_to_string(val)); - show_error(nullptr, _L("Invalid numeric input.")); - // On Windows, this SetFocus creates an invisible marker. - //ctrl->SetFocus(); - } - else if (was_replaced) - ctrl->SetValue(double_to_string(val)); -} - -class DiamTextCtrl : public wxTextCtrl -{ -public: - DiamTextCtrl(wxWindow* parent) - { -#ifdef _WIN32 - long style = wxBORDER_SIMPLE; -#else - long style = 0; -#endif - Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(Field::def_width_thinner() * wxGetApp().em_unit(), wxDefaultCoord), style); - wxGetApp().UpdateDarkUI(this); - } - ~DiamTextCtrl() {} -}; - -PageBedShape::PageBedShape(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Bed Shape and Size"), _L("Bed Shape"), 1) - , shape_panel(new BedShapePanel(this)) -{ - append_text(_L("Set the shape of your printer's bed.")); - - shape_panel->build_panel(*wizard_p()->custom_config->option("bed_shape"), - *wizard_p()->custom_config->option("bed_custom_texture"), - *wizard_p()->custom_config->option("bed_custom_model")); - - append(shape_panel); -} - -void PageBedShape::apply_custom_config(DynamicPrintConfig &config) -{ - const std::vector& points = shape_panel->get_shape(); - const std::string& custom_texture = shape_panel->get_custom_texture(); - const std::string& custom_model = shape_panel->get_custom_model(); - config.set_key_value("bed_shape", new ConfigOptionPoints(points)); - config.set_key_value("bed_custom_texture", new ConfigOptionString(custom_texture)); - config.set_key_value("bed_custom_model", new ConfigOptionString(custom_model)); -} - -PageBuildVolume::PageBuildVolume(ConfigWizard* parent) - : ConfigWizardPage(parent, _L("Build Volume"), _L("Build Volume"), 1) - , build_volume(new DiamTextCtrl(this)) -{ - append_text(_L("Set verctical size of your printer.")); - - wxString value = "200"; - build_volume->SetValue(value); - - build_volume->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { - double def_value = 200.0; - double max_value = 1200.0; - e.Skip(); - wxString str = build_volume->GetValue(); - - const char dec_sep = is_decimal_separator_point() ? '.' : ','; - const char dec_sep_alt = dec_sep == '.' ? ',' : '.'; - // Replace the first incorrect separator in decimal number. - bool was_replaced = str.Replace(dec_sep_alt, dec_sep, false) != 0; - - double val = 0.0; - if (!str.ToDouble(&val)) { - val = def_value; - build_volume->SetValue(double_to_string(val)); - show_error(nullptr, _L("Invalid numeric input.")); - //build_volume->SetFocus(); - } else if (val < 0.0) { - val = def_value; - build_volume->SetValue(double_to_string(val)); - show_error(nullptr, _L("Invalid numeric input.")); - //build_volume->SetFocus(); - } else if (val > max_value) { - val = max_value; - build_volume->SetValue(double_to_string(val)); - show_error(nullptr, _L("Invalid numeric input.")); - //build_volume->SetFocus(); - } else if (was_replaced) - build_volume->SetValue(double_to_string(val)); - }, build_volume->GetId()); - - auto* sizer_volume = new wxFlexGridSizer(3, 5, 5); - auto* text_volume = new wxStaticText(this, wxID_ANY, _L("Max print height:")); - auto* unit_volume = new wxStaticText(this, wxID_ANY, _L("mm")); - sizer_volume->AddGrowableCol(0, 1); - sizer_volume->Add(text_volume, 0, wxALIGN_CENTRE_VERTICAL); - sizer_volume->Add(build_volume); - sizer_volume->Add(unit_volume, 0, wxALIGN_CENTRE_VERTICAL); - append(sizer_volume); -} - -void PageBuildVolume::apply_custom_config(DynamicPrintConfig& config) -{ - double val = 0.0; - build_volume->GetValue().ToDouble(&val); - auto* opt_volume = new ConfigOptionFloat(val); - config.set_key_value("max_print_height", opt_volume); -} - -PageDiameters::PageDiameters(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Filament and Nozzle Diameters"), _L("Print Diameters"), 1) - , diam_nozzle(new DiamTextCtrl(this)) - , diam_filam (new DiamTextCtrl(this)) -{ - auto *default_nozzle = print_config_def.get("nozzle_diameter")->get_default_value(); - wxString value = double_to_string(default_nozzle != nullptr && default_nozzle->size() > 0 ? default_nozzle->get_at(0) : 0.5); - diam_nozzle->SetValue(value); - - auto *default_filam = print_config_def.get("filament_diameter")->get_default_value(); - value = double_to_string(default_filam != nullptr && default_filam->size() > 0 ? default_filam->get_at(0) : 3.0); - diam_filam->SetValue(value); - - diam_nozzle->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { focus_event(e, diam_nozzle, 0.5); }, diam_nozzle->GetId()); - diam_filam ->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { focus_event(e, diam_filam , 3.0); }, diam_filam->GetId()); - - append_text(_L("Enter the diameter of your printer's hot end nozzle.")); - - auto *sizer_nozzle = new wxFlexGridSizer(3, 5, 5); - auto *text_nozzle = new wxStaticText(this, wxID_ANY, _L("Nozzle Diameter:")); - auto *unit_nozzle = new wxStaticText(this, wxID_ANY, _L("mm")); - sizer_nozzle->AddGrowableCol(0, 1); - sizer_nozzle->Add(text_nozzle, 0, wxALIGN_CENTRE_VERTICAL); - sizer_nozzle->Add(diam_nozzle); - sizer_nozzle->Add(unit_nozzle, 0, wxALIGN_CENTRE_VERTICAL); - append(sizer_nozzle); - - append_spacer(VERTICAL_SPACING); - - append_text(_L("Enter the diameter of your filament.")); - append_text(_L("Good precision is required, so use a caliper and do multiple measurements along the filament, then compute the average.")); - - auto *sizer_filam = new wxFlexGridSizer(3, 5, 5); - auto *text_filam = new wxStaticText(this, wxID_ANY, _L("Filament Diameter:")); - auto *unit_filam = new wxStaticText(this, wxID_ANY, _L("mm")); - sizer_filam->AddGrowableCol(0, 1); - sizer_filam->Add(text_filam, 0, wxALIGN_CENTRE_VERTICAL); - sizer_filam->Add(diam_filam, 0, wxALIGN_CENTRE_VERTICAL); - sizer_filam->Add(unit_filam, 0, wxALIGN_CENTRE_VERTICAL); - append(sizer_filam); -} - -void PageDiameters::apply_custom_config(DynamicPrintConfig &config) -{ - double val = 0.0; - diam_nozzle->GetValue().ToDouble(&val); - auto *opt_nozzle = new ConfigOptionFloats(1, val); - config.set_key_value("nozzle_diameter", opt_nozzle); - - val = 0.0; - diam_filam->GetValue().ToDouble(&val); - auto * opt_filam = new ConfigOptionFloats(1, val); - config.set_key_value("filament_diameter", opt_filam); - - auto set_extrusion_width = [&config, opt_nozzle](const char *key, double dmr) { - char buf[64]; // locales don't matter here (sprintf/atof) - sprintf(buf, "%.2lf", dmr * opt_nozzle->values.front() / 0.4); - config.set_key_value(key, new ConfigOptionFloatOrPercent(atof(buf), false)); - }; - - set_extrusion_width("support_material_extrusion_width", 0.35); - set_extrusion_width("top_infill_extrusion_width", 0.40); - set_extrusion_width("first_layer_extrusion_width", 0.42); - - set_extrusion_width("extrusion_width", 0.45); - set_extrusion_width("perimeter_extrusion_width", 0.45); - set_extrusion_width("external_perimeter_extrusion_width", 0.45); - set_extrusion_width("infill_extrusion_width", 0.45); - set_extrusion_width("solid_infill_extrusion_width", 0.45); -} - -class SpinCtrlDouble: public wxSpinCtrlDouble -{ -public: - SpinCtrlDouble(wxWindow* parent) - { -#ifdef _WIN32 - long style = wxSP_ARROW_KEYS | wxBORDER_SIMPLE; -#else - long style = wxSP_ARROW_KEYS; -#endif - Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, style); -#ifdef _WIN32 - wxGetApp().UpdateDarkUI(this->GetText()); -#endif - this->Refresh(); - } - ~SpinCtrlDouble() {} -}; - -PageTemperatures::PageTemperatures(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Nozzle and Bed Temperatures"), _L("Temperatures"), 1) - , spin_extr(new SpinCtrlDouble(this)) - , spin_bed (new SpinCtrlDouble(this)) -{ - spin_extr->SetIncrement(5.0); - const auto &def_extr = *print_config_def.get("temperature"); - spin_extr->SetRange(def_extr.min, def_extr.max); - auto *default_extr = def_extr.get_default_value(); - spin_extr->SetValue(default_extr != nullptr && default_extr->size() > 0 ? default_extr->get_at(0) : 200); - - spin_bed->SetIncrement(5.0); - const auto &def_bed = *print_config_def.get("bed_temperature"); - spin_bed->SetRange(def_bed.min, def_bed.max); - auto *default_bed = def_bed.get_default_value(); - spin_bed->SetValue(default_bed != nullptr && default_bed->size() > 0 ? default_bed->get_at(0) : 0); - - append_text(_L("Enter the temperature needed for extruding your filament.")); - append_text(_L("A rule of thumb is 160 to 230 °C for PLA, and 215 to 250 °C for ABS.")); - - auto *sizer_extr = new wxFlexGridSizer(3, 5, 5); - auto *text_extr = new wxStaticText(this, wxID_ANY, _L("Extrusion Temperature:")); - auto *unit_extr = new wxStaticText(this, wxID_ANY, _L("°C")); - sizer_extr->AddGrowableCol(0, 1); - sizer_extr->Add(text_extr, 0, wxALIGN_CENTRE_VERTICAL); - sizer_extr->Add(spin_extr); - sizer_extr->Add(unit_extr, 0, wxALIGN_CENTRE_VERTICAL); - append(sizer_extr); - - append_spacer(VERTICAL_SPACING); - - append_text(_L("Enter the bed temperature needed for getting your filament to stick to your heated bed.")); - append_text(_L("A rule of thumb is 60 °C for PLA and 110 °C for ABS. Leave zero if you have no heated bed.")); - - auto *sizer_bed = new wxFlexGridSizer(3, 5, 5); - auto *text_bed = new wxStaticText(this, wxID_ANY, _L("Bed Temperature:")); - auto *unit_bed = new wxStaticText(this, wxID_ANY, _L("°C")); - sizer_bed->AddGrowableCol(0, 1); - sizer_bed->Add(text_bed, 0, wxALIGN_CENTRE_VERTICAL); - sizer_bed->Add(spin_bed); - sizer_bed->Add(unit_bed, 0, wxALIGN_CENTRE_VERTICAL); - append(sizer_bed); -} - -void PageTemperatures::apply_custom_config(DynamicPrintConfig &config) -{ - auto *opt_extr = new ConfigOptionInts(1, spin_extr->GetValue()); - config.set_key_value("temperature", opt_extr); - auto *opt_extr1st = new ConfigOptionInts(1, spin_extr->GetValue()); - config.set_key_value("first_layer_temperature", opt_extr1st); - auto *opt_bed = new ConfigOptionInts(1, spin_bed->GetValue()); - config.set_key_value("bed_temperature", opt_bed); - auto *opt_bed1st = new ConfigOptionInts(1, spin_bed->GetValue()); - config.set_key_value("first_layer_bed_temperature", opt_bed1st); -} - - -// Index - -ConfigWizardIndex::ConfigWizardIndex(wxWindow *parent) - : wxPanel(parent) - , bg(ScalableBitmap(parent, "PrusaSlicer_192px_transparent.png", 192)) - , bullet_black(ScalableBitmap(parent, "bullet_black.png")) - , bullet_blue(ScalableBitmap(parent, "bullet_blue.png")) - , bullet_white(ScalableBitmap(parent, "bullet_white.png")) - , item_active(NO_ITEM) - , item_hover(NO_ITEM) - , last_page((size_t)-1) -{ -#ifndef __WXOSX__ - SetDoubleBuffered(true);// SetDoubleBuffered exists on Win and Linux/GTK, but is missing on OSX -#endif //__WXOSX__ - SetMinSize(bg.GetSize()); - - const wxSize size = GetTextExtent("m"); - em_w = size.x; - em_h = size.y; - - Bind(wxEVT_PAINT, &ConfigWizardIndex::on_paint, this); - Bind(wxEVT_SIZE, [this](wxEvent& e) { e.Skip(); Refresh(); }); - Bind(wxEVT_MOTION, &ConfigWizardIndex::on_mouse_move, this); - - Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent &evt) { - if (item_hover != -1) { - item_hover = -1; - Refresh(); - } - evt.Skip(); - }); - - Bind(wxEVT_LEFT_UP, [this](wxMouseEvent &evt) { - if (item_hover >= 0) { go_to(item_hover); } - }); -} - -wxDECLARE_EVENT(EVT_INDEX_PAGE, wxCommandEvent); - -void ConfigWizardIndex::add_page(ConfigWizardPage *page) -{ - last_page = items.size(); - items.emplace_back(Item { page->shortname, page->indent, page }); - Refresh(); -} - -void ConfigWizardIndex::add_label(wxString label, unsigned indent) -{ - items.emplace_back(Item { std::move(label), indent, nullptr }); - Refresh(); -} - -ConfigWizardPage* ConfigWizardIndex::active_page() const -{ - if (item_active >= items.size()) { return nullptr; } - - return items[item_active].page; -} - -void ConfigWizardIndex::go_prev() -{ - // Search for a preceiding item that is a page (not a label, ie. page != nullptr) - - if (item_active == NO_ITEM) { return; } - - for (size_t i = item_active; i > 0; i--) { - if (items[i - 1].page != nullptr) { - go_to(i - 1); - return; - } - } -} - -void ConfigWizardIndex::go_next() -{ - // Search for a next item that is a page (not a label, ie. page != nullptr) - - if (item_active == NO_ITEM) { return; } - - for (size_t i = item_active + 1; i < items.size(); i++) { - if (items[i].page != nullptr) { - go_to(i); - return; - } - } -} - -// This one actually performs the go-to op -void ConfigWizardIndex::go_to(size_t i) -{ - if (i != item_active - && i < items.size() - && items[i].page != nullptr) { - auto *new_active = items[i].page; - auto *former_active = active_page(); - if (former_active != nullptr) { - former_active->Hide(); - } - - item_active = i; - new_active->Show(); - - wxCommandEvent evt(EVT_INDEX_PAGE, GetId()); - AddPendingEvent(evt); - - Refresh(); - - new_active->on_activate(); - } -} - -void ConfigWizardIndex::go_to(const ConfigWizardPage *page) -{ - if (page == nullptr) { return; } - - for (size_t i = 0; i < items.size(); i++) { - if (items[i].page == page) { - go_to(i); - return; - } - } -} - -void ConfigWizardIndex::clear() -{ - auto *former_active = active_page(); - if (former_active != nullptr) { former_active->Hide(); } - - items.clear(); - item_active = NO_ITEM; -} - -void ConfigWizardIndex::on_paint(wxPaintEvent & evt) -{ - const auto size = GetClientSize(); - if (size.GetHeight() == 0 || size.GetWidth() == 0) { return; } - - wxPaintDC dc(this); - - const auto bullet_w = bullet_black.GetWidth(); - const auto bullet_h = bullet_black.GetHeight(); - const int yoff_icon = bullet_h < em_h ? (em_h - bullet_h) / 2 : 0; - const int yoff_text = bullet_h > em_h ? (bullet_h - em_h) / 2 : 0; - const int yinc = item_height(); - - int index_width = 0; - - unsigned y = 0; - for (size_t i = 0; i < items.size(); i++) { - const Item& item = items[i]; - unsigned x = em_w/2 + item.indent * em_w; - - if (i == item_active || (item_hover >= 0 && i == (size_t)item_hover)) { - dc.DrawBitmap(bullet_blue.get_bitmap(), x, y + yoff_icon, false); - } - else if (i < item_active) { dc.DrawBitmap(bullet_black.get_bitmap(), x, y + yoff_icon, false); } - else if (i > item_active) { dc.DrawBitmap(bullet_white.get_bitmap(), x, y + yoff_icon, false); } - - x += + bullet_w + em_w/2; - const auto text_size = dc.GetTextExtent(item.label); - dc.SetTextForeground(wxGetApp().get_label_clr_default()); - dc.DrawText(item.label, x, y + yoff_text); - - y += yinc; - index_width = std::max(index_width, (int)x + text_size.x); - } - - //draw logo - if (int y = size.y - bg.GetHeight(); y>=0) { - dc.DrawBitmap(bg.get_bitmap(), 0, y, false); - index_width = std::max(index_width, bg.GetWidth() + em_w / 2); - } - - if (GetMinSize().x < index_width) { - CallAfter([this, index_width]() { - SetMinSize(wxSize(index_width, GetMinSize().y)); - Refresh(); - }); - } -} - -void ConfigWizardIndex::on_mouse_move(wxMouseEvent &evt) -{ - const wxClientDC dc(this); - const wxPoint pos = evt.GetLogicalPosition(dc); - - const ssize_t item_hover_new = pos.y / item_height(); - - if (item_hover_new < ssize_t(items.size()) && item_hover_new != item_hover) { - item_hover = item_hover_new; - Refresh(); - } - - evt.Skip(); -} - -void ConfigWizardIndex::msw_rescale() -{ - const wxSize size = GetTextExtent("m"); - em_w = size.x; - em_h = size.y; - - SetMinSize(bg.GetSize()); - - Refresh(); -} - - -// Materials - -const std::string Materials::UNKNOWN = "(Unknown)"; - -void Materials::push(const Preset *preset) -{ - presets.emplace_back(preset); - types.insert(technology & T_FFF - ? Materials::get_filament_type(preset) - : Materials::get_material_type(preset)); -} - -void Materials::add_printer(const Preset* preset) -{ - printers.insert(preset); -} - -void Materials::clear() -{ - presets.clear(); - types.clear(); - printers.clear(); - compatibility_counter.clear(); -} - -const std::string& Materials::appconfig_section() const -{ - return (technology & T_FFF) ? AppConfig::SECTION_FILAMENTS : AppConfig::SECTION_MATERIALS; -} - -const std::string& Materials::get_type(const Preset *preset) const -{ - return (technology & T_FFF) ? get_filament_type(preset) : get_material_type(preset); -} - -const std::string& Materials::get_vendor(const Preset *preset) const -{ - return (technology & T_FFF) ? get_filament_vendor(preset) : get_material_vendor(preset); -} - -const std::string& Materials::get_filament_type(const Preset *preset) -{ - const auto *opt = preset->config.opt("filament_type"); - if (opt != nullptr && opt->values.size() > 0) { - return opt->values[0]; - } else { - return UNKNOWN; - } -} - -const std::string& Materials::get_filament_vendor(const Preset *preset) -{ - const auto *opt = preset->config.opt("filament_vendor"); - return opt != nullptr ? opt->value : UNKNOWN; -} - -const std::string& Materials::get_material_type(const Preset *preset) -{ - const auto *opt = preset->config.opt("material_type"); - if (opt != nullptr) { - return opt->value; - } else { - return UNKNOWN; - } -} - -const std::string& Materials::get_material_vendor(const Preset *preset) -{ - const auto *opt = preset->config.opt("material_vendor"); - return opt != nullptr ? opt->value : UNKNOWN; -} - -// priv - -static const std::unordered_map> legacy_preset_map {{ - { "Original Prusa i3 MK2.ini", std::make_pair("MK2S", "0.4") }, - { "Original Prusa i3 MK2 MM Single Mode.ini", std::make_pair("MK2SMM", "0.4") }, - { "Original Prusa i3 MK2 MM Single Mode 0.6 nozzle.ini", std::make_pair("MK2SMM", "0.6") }, - { "Original Prusa i3 MK2 MultiMaterial.ini", std::make_pair("MK2SMM", "0.4") }, - { "Original Prusa i3 MK2 MultiMaterial 0.6 nozzle.ini", std::make_pair("MK2SMM", "0.6") }, - { "Original Prusa i3 MK2 0.25 nozzle.ini", std::make_pair("MK2S", "0.25") }, - { "Original Prusa i3 MK2 0.6 nozzle.ini", std::make_pair("MK2S", "0.6") }, - { "Original Prusa i3 MK3.ini", std::make_pair("MK3", "0.4") }, -}}; - -void ConfigWizard::priv::load_pages() -{ - wxWindowUpdateLocker freeze_guard(q); - (void)freeze_guard; - - const ConfigWizardPage *former_active = index->active_page(); - - index->clear(); - - index->add_page(page_welcome); - - // Printers - if (!only_sla_mode) - index->add_page(page_fff); - index->add_page(page_msla); - if (!only_sla_mode) { - index->add_page(page_vendors); - for (const auto &pages : pages_3rdparty) { - for ( PagePrinters* page : { pages.second.first, pages.second.second }) - if (page && page->install) - index->add_page(page); - } - - index->add_page(page_custom); - if (page_custom->custom_wanted()) { - index->add_page(page_firmware); - index->add_page(page_bed); - index->add_page(page_bvolume); - index->add_page(page_diams); - index->add_page(page_temps); - } - - // Filaments & Materials - if (any_fff_selected) { index->add_page(page_filaments); } - } - if (any_sla_selected) { index->add_page(page_sla_materials); } - - // there should to be selected at least one printer - btn_finish->Enable(any_fff_selected || any_sla_selected || custom_printer_selected); - - index->add_page(page_update); - index->add_page(page_downloader); - index->add_page(page_reload_from_disk); -#ifdef _WIN32 - index->add_page(page_files_association); -#endif // _WIN32 - index->add_page(page_mode); - - index->go_to(former_active); // Will restore the active item/page if possible - - q->Layout(); -// This Refresh() is needed to avoid ugly artifacts after printer selection, when no one vendor was selected from the very beginnig - q->Refresh(); -} - -void ConfigWizard::priv::init_dialog_size() -{ - // Clamp the Wizard size based on screen dimensions - - const auto idx = wxDisplay::GetFromWindow(q); - wxDisplay display(idx != wxNOT_FOUND ? idx : 0u); - - const auto disp_rect = display.GetClientArea(); - wxRect window_rect( - disp_rect.x + disp_rect.width / 20, - disp_rect.y + disp_rect.height / 20, - 9*disp_rect.width / 10, - 9*disp_rect.height / 10); - - const int width_hint = index->GetSize().GetWidth() + std::max(90 * em(), (only_sla_mode ? page_msla->get_width() : page_fff->get_width()) + 30 * em()); // XXX: magic constant, I found no better solution - if (width_hint < window_rect.width) { - window_rect.x += (window_rect.width - width_hint) / 2; - window_rect.width = width_hint; - } - - q->SetSize(window_rect); -} - -void ConfigWizard::priv::load_vendors() -{ - bundles = BundleMap::load(); - - // Load up the set of vendors / models / variants the user has had enabled up till now - AppConfig *app_config = wxGetApp().app_config; - if (! app_config->legacy_datadir()) { - appconfig_new.set_vendors(*app_config); - } else { - // In case of legacy datadir, try to guess the preference based on the printer preset files that are present - const auto printer_dir = fs::path(Slic3r::data_dir()) / "printer"; - for (auto &dir_entry : boost::filesystem::directory_iterator(printer_dir)) - if (Slic3r::is_ini_file(dir_entry)) { - auto needle = legacy_preset_map.find(dir_entry.path().filename().string()); - if (needle == legacy_preset_map.end()) { continue; } - - const auto &model = needle->second.first; - const auto &variant = needle->second.second; - appconfig_new.set_variant("PrusaResearch", model, variant, true); - } - } - - // Initialize the is_visible flag in printer Presets - for (auto &pair : bundles) { - pair.second.preset_bundle->load_installed_printers(appconfig_new); - } - - // Copy installed filaments and SLA material names from app_config to appconfig_new - // while resolving current names of profiles, which were renamed in the meantime. - for (PrinterTechnology technology : { ptFFF, ptSLA }) { - const std::string §ion_name = (technology == ptFFF) ? AppConfig::SECTION_FILAMENTS : AppConfig::SECTION_MATERIALS; - std::map section_new; - if (app_config->has_section(section_name)) { - const std::map §ion_old = app_config->get_section(section_name); - for (const auto& material_name_and_installed : section_old) - if (material_name_and_installed.second == "1") { - // Material is installed. Resolve it in bundles. - size_t num_found = 0; - const std::string &material_name = material_name_and_installed.first; - for (auto &bundle : bundles) { - const PresetCollection &materials = bundle.second.preset_bundle->materials(technology); - const Preset *preset = materials.find_preset(material_name); - if (preset == nullptr) { - // Not found. Maybe the material preset is there, bu it was was renamed? - const std::string *new_name = materials.get_preset_name_renamed(material_name); - if (new_name != nullptr) - preset = materials.find_preset(*new_name); - } - if (preset != nullptr) { - // Materal preset was found, mark it as installed. - section_new[preset->name] = "1"; - ++ num_found; - } - } - if (num_found == 0) - BOOST_LOG_TRIVIAL(error) << boost::format("Profile %1% was not found in installed vendor Preset Bundles.") % material_name; - else if (num_found > 1) - BOOST_LOG_TRIVIAL(error) << boost::format("Profile %1% was found in %2% vendor Preset Bundles.") % material_name % num_found; - } - } - appconfig_new.set_section(section_name, section_new); - }; -} - -void ConfigWizard::priv::add_page(ConfigWizardPage *page) -{ - const int proportion = (page->shortname == _L("Filaments")) || (page->shortname == _L("SLA Materials")) ? 1 : 0; - hscroll_sizer->Add(page, proportion, wxEXPAND); - all_pages.push_back(page); -} - -void ConfigWizard::priv::enable_next(bool enable) -{ - btn_next->Enable(enable); - btn_finish->Enable(enable); -} - -void ConfigWizard::priv::set_start_page(ConfigWizard::StartPage start_page) -{ - switch (start_page) { - case ConfigWizard::SP_PRINTERS: - index->go_to(page_fff); - btn_next->SetFocus(); - break; - case ConfigWizard::SP_FILAMENTS: - index->go_to(page_filaments); - btn_finish->SetFocus(); - break; - case ConfigWizard::SP_MATERIALS: - index->go_to(page_sla_materials); - btn_finish->SetFocus(); - break; - default: - index->go_to(page_welcome); - btn_next->SetFocus(); - break; - } -} - -void ConfigWizard::priv::create_3rdparty_pages() -{ - for (const auto &pair : bundles) { - const VendorProfile *vendor = pair.second.vendor_profile; - if (vendor->id == PresetBundle::PRUSA_BUNDLE) { continue; } - - bool is_fff_technology = false; - bool is_sla_technology = false; - - for (auto& model: vendor->models) - { - if (!is_fff_technology && model.technology == ptFFF) - is_fff_technology = true; - if (!is_sla_technology && model.technology == ptSLA) - is_sla_technology = true; - } - - PagePrinters* pageFFF = nullptr; - PagePrinters* pageSLA = nullptr; - - if (is_fff_technology) { - pageFFF = new PagePrinters(q, vendor->name + " " +_L("FFF Technology Printers"), vendor->name+" FFF", *vendor, 1, T_FFF); - add_page(pageFFF); - } - - if (is_sla_technology) { - pageSLA = new PagePrinters(q, vendor->name + " " + _L("SLA Technology Printers"), vendor->name+" MSLA", *vendor, 1, T_SLA); - add_page(pageSLA); - } - - pages_3rdparty.insert({vendor->id, {pageFFF, pageSLA}}); - } -} - -void ConfigWizard::priv::set_run_reason(RunReason run_reason) -{ - this->run_reason = run_reason; - for (auto &page : all_pages) { - page->set_run_reason(run_reason); - } -} - -void ConfigWizard::priv::update_materials(Technology technology) -{ - if (any_fff_selected && (technology & T_FFF)) { - filaments.clear(); - aliases_fff.clear(); - // Iterate filaments in all bundles - for (const auto &pair : bundles) { - for (const auto &filament : pair.second.preset_bundle->filaments) { - // Check if filament is already added - if (filaments.containts(&filament)) - continue; - // Iterate printers in all bundles - for (const auto &printer : pair.second.preset_bundle->printers) { - if (!printer.is_visible || printer.printer_technology() != ptFFF) - continue; - // Filter out inapplicable printers - if (is_compatible_with_printer(PresetWithVendorProfile(filament, filament.vendor), PresetWithVendorProfile(printer, printer.vendor))) { - if (!filaments.containts(&filament)) { - filaments.push(&filament); - if (!filament.alias.empty()) - aliases_fff[filament.alias].insert(filament.name); - } - filaments.add_printer(&printer); - } - } - - } - } - // count compatible printers - for (const auto& preset : filaments.presets) { - - const auto filter = [preset](const std::pair element) { - return preset->alias == element.first; - }; - if (std::find_if(filaments.compatibility_counter.begin(), filaments.compatibility_counter.end(), filter) != filaments.compatibility_counter.end()) { - continue; - } - std::vector idx_with_same_alias; - for (size_t i = 0; i < filaments.presets.size(); ++i) { - if (preset->alias == filaments.presets[i]->alias) - idx_with_same_alias.push_back(i); - } - size_t counter = 0; - for (const auto& printer : filaments.printers) { - if (!(*printer).is_visible || (*printer).printer_technology() != ptFFF) - continue; - bool compatible = false; - // Test otrher materials with same alias - for (size_t i = 0; i < idx_with_same_alias.size() && !compatible; ++i) { - const Preset& prst = *(filaments.presets[idx_with_same_alias[i]]); - const Preset& prntr = *printer; - if (is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) { - compatible = true; - break; - } - } - if (compatible) - counter++; - } - filaments.compatibility_counter.emplace_back(preset->alias, counter); - } - } - - if (any_sla_selected && (technology & T_SLA)) { - sla_materials.clear(); - aliases_sla.clear(); - - // Iterate SLA materials in all bundles - for (const auto &pair : bundles) { - for (const auto &material : pair.second.preset_bundle->sla_materials) { - // Check if material is already added - if (sla_materials.containts(&material)) - continue; - // Iterate printers in all bundles - // For now, we only allow the profiles to be compatible with another profiles inside the same bundle. - for (const auto& printer : pair.second.preset_bundle->printers) { - if(!printer.is_visible || printer.printer_technology() != ptSLA) - continue; - // Filter out inapplicable printers - if (is_compatible_with_printer(PresetWithVendorProfile(material, nullptr), PresetWithVendorProfile(printer, nullptr))) { - // Check if material is already added - if(!sla_materials.containts(&material)) { - sla_materials.push(&material); - if (!material.alias.empty()) - aliases_sla[material.alias].insert(material.name); - } - sla_materials.add_printer(&printer); - } - } - } - } - // count compatible printers - for (const auto& preset : sla_materials.presets) { - - const auto filter = [preset](const std::pair element) { - return preset->alias == element.first; - }; - if (std::find_if(sla_materials.compatibility_counter.begin(), sla_materials.compatibility_counter.end(), filter) != sla_materials.compatibility_counter.end()) { - continue; - } - std::vector idx_with_same_alias; - for (size_t i = 0; i < sla_materials.presets.size(); ++i) { - if(preset->alias == sla_materials.presets[i]->alias) - idx_with_same_alias.push_back(i); - } - size_t counter = 0; - for (const auto& printer : sla_materials.printers) { - if (!(*printer).is_visible || (*printer).printer_technology() != ptSLA) - continue; - bool compatible = false; - // Test otrher materials with same alias - for (size_t i = 0; i < idx_with_same_alias.size() && !compatible; ++i) { - const Preset& prst = *(sla_materials.presets[idx_with_same_alias[i]]); - const Preset& prntr = *printer; - if (is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) { - compatible = true; - break; - } - } - if (compatible) - counter++; - } - sla_materials.compatibility_counter.emplace_back(preset->alias, counter); - } - } -} - -void ConfigWizard::priv::on_custom_setup(const bool custom_wanted) -{ - custom_printer_selected = custom_wanted; - load_pages(); -} - -void ConfigWizard::priv::on_printer_pick(PagePrinters *page, const PrinterPickerEvent &evt) -{ - if (check_sla_selected() != any_sla_selected || - check_fff_selected() != any_fff_selected) { - any_fff_selected = check_fff_selected(); - any_sla_selected = check_sla_selected(); - - load_pages(); - } - - // Update the is_visible flag on relevant printer profiles - for (auto &pair : bundles) { - if (pair.first != evt.vendor_id) { continue; } - - for (auto &preset : pair.second.preset_bundle->printers) { - if (preset.config.opt_string("printer_model") == evt.model_id - && preset.config.opt_string("printer_variant") == evt.variant_name) { - preset.is_visible = evt.enable; - } - } - - // When a printer model is picked, but there is no material installed compatible with this printer model, - // install default materials for selected printer model silently. - check_and_install_missing_materials(page->technology, evt.model_id); - } - - if (page->technology & T_FFF) { - page_filaments->clear(); - } else if (page->technology & T_SLA) { - page_sla_materials->clear(); - } -} - -void ConfigWizard::priv::select_default_materials_for_printer_model(const VendorProfile::PrinterModel &printer_model, Technology technology) -{ - PageMaterials* page_materials = technology & T_FFF ? page_filaments : page_sla_materials; - for (const std::string& material : printer_model.default_materials) - appconfig_new.set(page_materials->materials->appconfig_section(), material, "1"); -} - -void ConfigWizard::priv::select_default_materials_for_printer_models(Technology technology, const std::set &printer_models) -{ - PageMaterials *page_materials = technology & T_FFF ? page_filaments : page_sla_materials; - const std::string &appconfig_section = page_materials->materials->appconfig_section(); - - // Following block was unnecessary. Its enough to iterate printer_models once. Not for every vendor printer page. - // Filament is selected on same page for all printers of same technology. - /* - auto select_default_materials_for_printer_page = [this, appconfig_section, printer_models, technology](PagePrinters *page_printers, Technology technology) - { - const std::string vendor_id = page_printers->get_vendor_id(); - for (auto& pair : bundles) - if (pair.first == vendor_id) - for (const VendorProfile::PrinterModel *printer_model : printer_models) - for (const std::string &material : printer_model->default_materials) - appconfig_new.set(appconfig_section, material, "1"); - }; - - PagePrinters* page_printers = technology & T_FFF ? page_fff : page_msla; - select_default_materials_for_printer_page(page_printers, technology); - - for (const auto& printer : pages_3rdparty) - { - page_printers = technology & T_FFF ? printer.second.first : printer.second.second; - if (page_printers) - select_default_materials_for_printer_page(page_printers, technology); - } - */ - - // Iterate printer_models and select default materials. If none available -> msg to user. - std::vector models_without_default; - for (const VendorProfile::PrinterModel* printer_model : printer_models) { - if (printer_model->default_materials.empty()) { - models_without_default.emplace_back(printer_model); - } else { - for (const std::string& material : printer_model->default_materials) - appconfig_new.set(appconfig_section, material, "1"); - } - } - - if (!models_without_default.empty()) { - std::string printer_names = "\n\n"; - for (const VendorProfile::PrinterModel* printer_model : models_without_default) { - printer_names += printer_model->name + "\n"; - } - printer_names += "\n\n"; - std::string message = (technology & T_FFF ? - GUI::format(_L("Following printer profiles has no default filament: %1%Please select one manually."), printer_names) : - GUI::format(_L("Following printer profiles has no default material: %1%Please select one manually."), printer_names)); - MessageDialog msg(q, message, _L("Notice"), wxOK); - msg.ShowModal(); - } - - update_materials(technology); - ((technology & T_FFF) ? page_filaments : page_sla_materials)->reload_presets(); -} - -void ConfigWizard::priv::on_3rdparty_install(const VendorProfile *vendor, bool install) -{ - auto it = pages_3rdparty.find(vendor->id); - wxCHECK_RET(it != pages_3rdparty.end(), "Internal error: GUI page not found for 3rd party vendor profile"); - - for (PagePrinters* page : { it->second.first, it->second.second }) - if (page) { - if (page->install && !install) - page->select_all(false); - page->install = install; - // if some 3rd vendor is selected, select first printer for them - if (install) - page->printer_pickers[0]->select_one(0, true); - page->Layout(); - } - - load_pages(); -} - -bool ConfigWizard::priv::on_bnt_finish() -{ - wxBusyCursor wait; - - if (!page_downloader->on_finish_downloader()) { - index->go_to(page_downloader); - return false; - } - /* When Filaments or Sla Materials pages are activated, - * materials for this pages are automaticaly updated and presets are reloaded. - * - * But, if _Finish_ button was clicked without activation of those pages - * (for example, just some printers were added/deleted), - * than last changes wouldn't be updated for filaments/materials. - * SO, do that before close of Wizard - */ - update_materials(T_ANY); - if (any_fff_selected) - page_filaments->reload_presets(); - if (any_sla_selected) - page_sla_materials->reload_presets(); - - // theres no need to check that filament is selected if we have only custom printer - if (custom_printer_selected && !any_fff_selected && !any_sla_selected) return true; - // check, that there is selected at least one filament/material - return check_and_install_missing_materials(T_ANY); -} - -// This allmighty method verifies, whether there is at least a single compatible filament or SLA material installed -// for each Printer preset of each Printer Model installed. -// -// In case only_for_model_id is set, then the test is done for that particular printer model only, and the default materials are installed silently. -// Otherwise the user is quieried whether to install the missing default materials or not. -// -// Return true if the tested Printer Models already had materials installed. -// Return false if there were some Printer Models with missing materials, independent from whether the defaults were installed for these -// respective Printer Models or not. -bool ConfigWizard::priv::check_and_install_missing_materials(Technology technology, const std::string &only_for_model_id) -{ - // Walk over all installed Printer presets and verify whether there is a filament or SLA material profile installed at the same PresetBundle, - // which is compatible with it. - const auto printer_models_missing_materials = [this, only_for_model_id](PrinterTechnology technology, const std::string §ion) - { - const std::map &appconfig_presets = appconfig_new.has_section(section) ? appconfig_new.get_section(section) : std::map(); - std::set printer_models_without_material; - for (const auto &pair : bundles) { - const PresetCollection &materials = pair.second.preset_bundle->materials(technology); - for (const auto &printer : pair.second.preset_bundle->printers) { - if (printer.is_visible && printer.printer_technology() == technology) { - const VendorProfile::PrinterModel *printer_model = PresetUtils::system_printer_model(printer); - assert(printer_model != nullptr); - if ((only_for_model_id.empty() || only_for_model_id == printer_model->id) && - printer_models_without_material.find(printer_model) == printer_models_without_material.end()) { - bool has_material = false; - for (const auto& preset : appconfig_presets) { - if (preset.second == "1") { - const Preset *material = materials.find_preset(preset.first, false); - if (material != nullptr && is_compatible_with_printer(PresetWithVendorProfile(*material, nullptr), PresetWithVendorProfile(printer, nullptr))) { - has_material = true; - break; - } - } - } - if (! has_material) - printer_models_without_material.insert(printer_model); - } - } - } - } - assert(printer_models_without_material.empty() || only_for_model_id.empty() || only_for_model_id == (*printer_models_without_material.begin())->id); - return printer_models_without_material; - }; - - const auto ask_and_select_default_materials = [this](const wxString &message, const std::set &printer_models, Technology technology) - { - //wxMessageDialog msg(q, message, _L("Notice"), wxYES_NO); - MessageDialog msg(q, message, _L("Notice"), wxYES_NO); - if (msg.ShowModal() == wxID_YES) - select_default_materials_for_printer_models(technology, printer_models); - }; - - const auto printer_model_list = [](const std::set &printer_models) -> wxString { - wxString out; - for (const VendorProfile::PrinterModel *printer_model : printer_models) { - wxString name = from_u8(printer_model->name); - out += "\t\t"; - out += name; - out += "\n"; - } - return out; - }; - - if (any_fff_selected && (technology & T_FFF)) { - std::set printer_models_without_material = printer_models_missing_materials(ptFFF, AppConfig::SECTION_FILAMENTS); - if (! printer_models_without_material.empty()) { - if (only_for_model_id.empty()) - ask_and_select_default_materials( - _L("The following FFF printer models have no filament selected:") + - "\n\n\t" + - printer_model_list(printer_models_without_material) + - "\n\n\t" + - _L("Do you want to select default filaments for these FFF printer models?"), - printer_models_without_material, - T_FFF); - else - select_default_materials_for_printer_model(**printer_models_without_material.begin(), T_FFF); - return false; - } - } - - if (any_sla_selected && (technology & T_SLA)) { - std::set printer_models_without_material = printer_models_missing_materials(ptSLA, AppConfig::SECTION_MATERIALS); - if (! printer_models_without_material.empty()) { - if (only_for_model_id.empty()) - ask_and_select_default_materials( - _L("The following SLA printer models have no materials selected:") + - "\n\n\t" + - printer_model_list(printer_models_without_material) + - "\n\n\t" + - _L("Do you want to select default SLA materials for these printer models?"), - printer_models_without_material, - T_SLA); - else - select_default_materials_for_printer_model(**printer_models_without_material.begin(), T_SLA); - return false; - } - } - - return true; -} - -static std::set get_new_added_presets(const std::map& old_data, const std::map& new_data) -{ - auto get_aliases = [](const std::map& data) { - std::set old_aliases; - for (auto item : data) { - const std::string& name = item.first; - size_t pos = name.find("@"); - old_aliases.emplace(pos == std::string::npos ? name : name.substr(0, pos-1)); - } - return old_aliases; - }; - - std::set old_aliases = get_aliases(old_data); - std::set new_aliases = get_aliases(new_data); - std::set diff; - std::set_difference(new_aliases.begin(), new_aliases.end(), old_aliases.begin(), old_aliases.end(), std::inserter(diff, diff.begin())); - - return diff; -} - -static std::string get_first_added_preset(const std::map& old_data, const std::map& new_data) -{ - std::set diff = get_new_added_presets(old_data, new_data); - if (diff.empty()) - return std::string(); - return *diff.begin(); -} - -bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater, bool& apply_keeped_changes) -{ - wxString header, caption = _L("Configuration is edited in ConfigWizard"); - const auto enabled_vendors = appconfig_new.vendors(); - const auto enabled_vendors_old = app_config->vendors(); - - bool suppress_sla_printer = model_has_multi_part_objects(wxGetApp().model()); - PrinterTechnology preferred_pt = ptAny; - auto get_preferred_printer_technology = [enabled_vendors, enabled_vendors_old, suppress_sla_printer](const std::string& bundle_name, const Bundle& bundle) { - const auto config = enabled_vendors.find(bundle_name); - PrinterTechnology pt = ptAny; - if (config != enabled_vendors.end()) { - for (const auto& model : bundle.vendor_profile->models) { - if (const auto model_it = config->second.find(model.id); - model_it != config->second.end() && model_it->second.size() > 0) { - pt = model.technology; - const auto config_old = enabled_vendors_old.find(bundle_name); - if (config_old == enabled_vendors_old.end() || config_old->second.find(model.id) == config_old->second.end()) { - // if preferred printer model has SLA printer technology it's important to check the model for multi-part state - if (pt == ptSLA && suppress_sla_printer) - continue; - return pt; - } - - if (const auto model_it_old = config_old->second.find(model.id); - model_it_old == config_old->second.end() || model_it_old->second != model_it->second) { - // if preferred printer model has SLA printer technology it's important to check the model for multi-part state - if (pt == ptSLA && suppress_sla_printer) - continue; - return pt; - } - } - } - } - return pt; - }; - // Prusa printers are considered first, then 3rd party. - if (preferred_pt = get_preferred_printer_technology("PrusaResearch", bundles.prusa_bundle()); - preferred_pt == ptAny || (preferred_pt == ptSLA && suppress_sla_printer)) { - for (const auto& bundle : bundles) { - if (bundle.second.is_prusa_bundle) { continue; } - if (PrinterTechnology pt = get_preferred_printer_technology(bundle.first, bundle.second); pt == ptAny) - continue; - else if (preferred_pt == ptAny) - preferred_pt = pt; - if(!(preferred_pt == ptAny || (preferred_pt == ptSLA && suppress_sla_printer))) - break; - } - } - - if (preferred_pt == ptSLA && !wxGetApp().may_switch_to_SLA_preset(caption)) - return false; - - bool check_unsaved_preset_changes = page_welcome->reset_user_profile(); - if (check_unsaved_preset_changes) - header = _L("All user presets will be deleted."); - int act_btns = ActionButtons::KEEP; - if (!check_unsaved_preset_changes) - act_btns |= ActionButtons::SAVE; - - // Install bundles from resources if needed: - std::vector install_bundles; - for (const auto &pair : bundles) { - if (! pair.second.is_in_resources) { continue; } - - if (pair.second.is_prusa_bundle) { - // Always install Prusa bundle, because it has a lot of filaments/materials - // likely to be referenced by other profiles. - install_bundles.emplace_back(pair.first); - continue; - } - - const auto vendor = enabled_vendors.find(pair.first); - if (vendor == enabled_vendors.end()) { continue; } - - size_t size_sum = 0; - for (const auto &model : vendor->second) { size_sum += model.second.size(); } - - if (size_sum > 0) { - // This vendor needs to be installed - install_bundles.emplace_back(pair.first); - } - } - if (!check_unsaved_preset_changes) - if ((check_unsaved_preset_changes = install_bundles.size() > 0)) - header = _L_PLURAL("A new vendor was installed and one of its printers will be activated", "New vendors were installed and one of theirs printers will be activated", install_bundles.size()); - -#ifdef __linux__ - // Desktop integration on Linux - BOOST_LOG_TRIVIAL(debug) << "ConfigWizard::priv::apply_config integrate_desktop" << page_welcome->integrate_desktop() << " perform_registration_linux " << page_downloader->downloader->get_perform_registration_linux(); - if (page_welcome->integrate_desktop() || page_downloader->downloader->get_perform_registration_linux()) - DesktopIntegrationDialog::perform_desktop_integration(page_downloader->downloader->get_perform_registration_linux()); -#endif - - // Decide whether to create snapshot based on run_reason and the reset profile checkbox - bool snapshot = true; - Snapshot::Reason snapshot_reason = Snapshot::SNAPSHOT_UPGRADE; - switch (run_reason) { - case ConfigWizard::RR_DATA_EMPTY: - snapshot = false; - break; - case ConfigWizard::RR_DATA_LEGACY: - snapshot = true; - break; - case ConfigWizard::RR_DATA_INCOMPAT: - // In this case snapshot has already been taken by - // PresetUpdater with the appropriate reason - snapshot = false; - break; - case ConfigWizard::RR_USER: - snapshot = page_welcome->reset_user_profile(); - snapshot_reason = Snapshot::SNAPSHOT_USER; - break; - } - - if (snapshot && ! take_config_snapshot_cancel_on_error(*app_config, snapshot_reason, "", _u8L("Do you want to continue changing the configuration?"))) - return false; - - if (check_unsaved_preset_changes && - !wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) - return false; - - if (install_bundles.size() > 0) { - // Install bundles from resources. - // Don't create snapshot - we've already done that above if applicable. - if (! updater->install_bundles_rsrc(std::move(install_bundles), false)) - return false; - } else { - BOOST_LOG_TRIVIAL(info) << "No bundles need to be installed from resources"; - } - - if (page_welcome->reset_user_profile()) { - BOOST_LOG_TRIVIAL(info) << "Resetting user profiles..."; - preset_bundle->reset(true); - } - - std::string preferred_model; - std::string preferred_variant; - auto get_preferred_printer_model = [enabled_vendors, enabled_vendors_old, preferred_pt](const std::string& bundle_name, const Bundle& bundle, std::string& variant) { - const auto config = enabled_vendors.find(bundle_name); - if (config == enabled_vendors.end()) - return std::string(); - for (const auto& model : bundle.vendor_profile->models) { - if (const auto model_it = config->second.find(model.id); - model_it != config->second.end() && model_it->second.size() > 0 && - preferred_pt == model.technology) { - variant = *model_it->second.begin(); - const auto config_old = enabled_vendors_old.find(bundle_name); - if (config_old == enabled_vendors_old.end()) - return model.id; - const auto model_it_old = config_old->second.find(model.id); - if (model_it_old == config_old->second.end()) - return model.id; - else if (model_it_old->second != model_it->second) { - for (const auto& var : model_it->second) - if (model_it_old->second.find(var) == model_it_old->second.end()) { - variant = var; - return model.id; - } - } - } - } - if (!variant.empty()) - variant.clear(); - return std::string(); - }; - // Prusa printers are considered first, then 3rd party. - if (preferred_model = get_preferred_printer_model("PrusaResearch", bundles.prusa_bundle(), preferred_variant); - preferred_model.empty()) { - for (const auto& bundle : bundles) { - if (bundle.second.is_prusa_bundle) { continue; } - if (preferred_model = get_preferred_printer_model(bundle.first, bundle.second, preferred_variant); - !preferred_model.empty()) - break; - } - } - - // if unsaved changes was not cheched till this moment - if (!check_unsaved_preset_changes) { - if ((check_unsaved_preset_changes = !preferred_model.empty())) { - header = _L("A new Printer was installed and it will be activated."); - if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) - return false; - } - else if ((check_unsaved_preset_changes = enabled_vendors_old != enabled_vendors)) { - header = _L("Some Printers were uninstalled."); - if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) - return false; - } - } - - std::string first_added_filament, first_added_sla_material; - auto get_first_added_material_preset = [this, app_config](const std::string& section_name, std::string& first_added_preset) { - if (appconfig_new.has_section(section_name)) { - // get first of new added preset names - const std::map& old_presets = app_config->has_section(section_name) ? app_config->get_section(section_name) : std::map(); - first_added_preset = get_first_added_preset(old_presets, appconfig_new.get_section(section_name)); - } - }; - get_first_added_material_preset(AppConfig::SECTION_FILAMENTS, first_added_filament); - get_first_added_material_preset(AppConfig::SECTION_MATERIALS, first_added_sla_material); - - // if unsaved changes was not cheched till this moment - if (!check_unsaved_preset_changes) { - if ((check_unsaved_preset_changes = !first_added_filament.empty() || !first_added_sla_material.empty())) { - header = !first_added_filament.empty() ? - _L("A new filament was installed and it will be activated.") : - _L("A new SLA material was installed and it will be activated."); - if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) - return false; - } - else { - auto changed = [app_config, &appconfig_new = std::as_const(this->appconfig_new)](const std::string& section_name) { - return (app_config->has_section(section_name) ? app_config->get_section(section_name) : std::map()) != appconfig_new.get_section(section_name); - }; - bool is_filaments_changed = changed(AppConfig::SECTION_FILAMENTS); - bool is_sla_materials_changed = changed(AppConfig::SECTION_MATERIALS); - if ((check_unsaved_preset_changes = is_filaments_changed || is_sla_materials_changed)) { - header = is_filaments_changed ? _L("Some filaments were uninstalled.") : _L("Some SLA materials were uninstalled."); - if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) - return false; - } - } - } - - // apply materials in app_config - for (const std::string& section_name : {AppConfig::SECTION_FILAMENTS, AppConfig::SECTION_MATERIALS}) - app_config->set_section(section_name, appconfig_new.get_section(section_name)); - - app_config->set_vendors(appconfig_new); - - app_config->set("notify_release", page_update->version_check ? "all" : "none"); - app_config->set("preset_update", page_update->preset_update ? "1" : "0"); - app_config->set("export_sources_full_pathnames", page_reload_from_disk->full_pathnames ? "1" : "0"); - -#ifdef _WIN32 - app_config->set("associate_3mf", page_files_association->associate_3mf() ? "1" : "0"); - app_config->set("associate_stl", page_files_association->associate_stl() ? "1" : "0"); -// app_config->set("associate_gcode", page_files_association->associate_gcode() ? "1" : "0"); - - if (wxGetApp().is_editor()) { - if (page_files_association->associate_3mf()) - wxGetApp().associate_3mf_files(); - if (page_files_association->associate_stl()) - wxGetApp().associate_stl_files(); - } -// else { -// if (page_files_association->associate_gcode()) -// wxGetApp().associate_gcode_files(); -// } -#endif // _WIN32 - - page_mode->serialize_mode(app_config); - - if (check_unsaved_preset_changes) - preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem, - {preferred_model, preferred_variant, first_added_filament, first_added_sla_material}); - - if (!only_sla_mode && page_custom->custom_wanted() && page_custom->is_valid_profile_name()) { - // if unsaved changes was not cheched till this moment - if (!check_unsaved_preset_changes && - !wxGetApp().check_and_keep_current_preset_changes(caption, _L("Custom printer was installed and it will be activated."), act_btns, &apply_keeped_changes)) - return false; - - page_firmware->apply_custom_config(*custom_config); - page_bed->apply_custom_config(*custom_config); - page_bvolume->apply_custom_config(*custom_config); - page_diams->apply_custom_config(*custom_config); - page_temps->apply_custom_config(*custom_config); - - copy_bed_model_and_texture_if_needed(*custom_config); - - const std::string profile_name = page_custom->profile_name(); - preset_bundle->load_config_from_wizard(profile_name, *custom_config); - } - - // Update the selections from the compatibilty. - preset_bundle->export_selections(*app_config); - - return true; -} -void ConfigWizard::priv::update_presets_in_config(const std::string& section, const std::string& alias_key, bool add) -{ - const PresetAliases& aliases = section == AppConfig::SECTION_FILAMENTS ? aliases_fff : aliases_sla; - - auto update = [this, add](const std::string& s, const std::string& key) { - assert(! s.empty()); - if (add) - appconfig_new.set(s, key, "1"); - else - appconfig_new.erase(s, key); - }; - - // add or delete presets had a same alias - auto it = aliases.find(alias_key); - if (it != aliases.end()) - for (const std::string& name : it->second) - update(section, name); -} - -bool ConfigWizard::priv::check_fff_selected() -{ - bool ret = page_fff->any_selected(); - for (const auto& printer: pages_3rdparty) - if (printer.second.first) // FFF page - ret |= printer.second.first->any_selected(); - return ret; -} - -bool ConfigWizard::priv::check_sla_selected() -{ - bool ret = page_msla->any_selected(); - for (const auto& printer: pages_3rdparty) - if (printer.second.second) // SLA page - ret |= printer.second.second->any_selected(); - return ret; -} - - -// Public - -ConfigWizard::ConfigWizard(wxWindow *parent) - : DPIDialog(parent, wxID_ANY, wxString(SLIC3R_APP_NAME) + " - " + _(name()), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) - , p(new priv(this)) -{ - this->SetFont(wxGetApp().normal_font()); - - p->load_vendors(); - p->custom_config.reset(DynamicPrintConfig::new_from_defaults_keys({ - "gcode_flavor", "bed_shape", "bed_custom_texture", "bed_custom_model", "nozzle_diameter", "filament_diameter", "temperature", "bed_temperature", - })); - - p->index = new ConfigWizardIndex(this); - - auto *vsizer = new wxBoxSizer(wxVERTICAL); - auto *topsizer = new wxBoxSizer(wxHORIZONTAL); - auto* hline = new StaticLine(this); - p->btnsizer = new wxBoxSizer(wxHORIZONTAL); - - // Initially we _do not_ SetScrollRate in order to figure out the overall width of the Wizard without scrolling. - // Later, we compare that to the size of the current screen and set minimum width based on that (see below). - p->hscroll = new wxScrolledWindow(this); - p->hscroll_sizer = new wxBoxSizer(wxHORIZONTAL); - p->hscroll->SetSizer(p->hscroll_sizer); - - topsizer->Add(p->index, 0, wxEXPAND); - topsizer->AddSpacer(INDEX_MARGIN); - topsizer->Add(p->hscroll, 1, wxEXPAND); - - p->btn_sel_all = new wxButton(this, wxID_ANY, _L("Select all standard printers")); - p->btnsizer->Add(p->btn_sel_all); - - p->btn_prev = new wxButton(this, wxID_ANY, _L("< &Back")); - p->btn_next = new wxButton(this, wxID_ANY, _L("&Next >")); - p->btn_finish = new wxButton(this, wxID_APPLY, _L("&Finish")); - p->btn_cancel = new wxButton(this, wxID_CANCEL, _L("Cancel")); // Note: The label needs to be present, otherwise we get accelerator bugs on Mac - p->btnsizer->AddStretchSpacer(); - p->btnsizer->Add(p->btn_prev, 0, wxLEFT, BTN_SPACING); - p->btnsizer->Add(p->btn_next, 0, wxLEFT, BTN_SPACING); - p->btnsizer->Add(p->btn_finish, 0, wxLEFT, BTN_SPACING); - p->btnsizer->Add(p->btn_cancel, 0, wxLEFT, BTN_SPACING); - - wxGetApp().UpdateDarkUI(p->btn_sel_all); - wxGetApp().UpdateDarkUI(p->btn_prev); - wxGetApp().UpdateDarkUI(p->btn_next); - wxGetApp().UpdateDarkUI(p->btn_finish); - wxGetApp().UpdateDarkUI(p->btn_cancel); - - const auto prusa_it = p->bundles.find("PrusaResearch"); - wxCHECK_RET(prusa_it != p->bundles.cend(), "Vendor PrusaResearch not found"); - const VendorProfile *vendor_prusa = prusa_it->second.vendor_profile; - - p->add_page(p->page_welcome = new PageWelcome(this)); - - - p->page_fff = new PagePrinters(this, _L("Prusa FFF Technology Printers"), "Prusa FFF", *vendor_prusa, 0, T_FFF); - p->only_sla_mode = !p->page_fff->has_printers; - if (!p->only_sla_mode) { - p->add_page(p->page_fff); - p->page_fff->is_primary_printer_page = true; - } - - - p->page_msla = new PagePrinters(this, _L("Prusa MSLA Technology Printers"), "Prusa MSLA", *vendor_prusa, 0, T_SLA); - p->add_page(p->page_msla); - if (p->only_sla_mode) { - p->page_msla->is_primary_printer_page = true; - } - - if (!p->only_sla_mode) { - // Pages for 3rd party vendors - p->create_3rdparty_pages(); // Needs to be done _before_ creating PageVendors - p->add_page(p->page_vendors = new PageVendors(this)); - p->add_page(p->page_custom = new PageCustom(this)); - p->custom_printer_selected = p->page_custom->custom_wanted(); - } - - p->any_sla_selected = p->check_sla_selected(); - p->any_fff_selected = ! p->only_sla_mode && p->check_fff_selected(); - - p->update_materials(T_ANY); - if (!p->only_sla_mode) - p->add_page(p->page_filaments = new PageMaterials(this, &p->filaments, - _L("Filament Profiles Selection"), _L("Filaments"), _L("Type:") )); - - p->add_page(p->page_sla_materials = new PageMaterials(this, &p->sla_materials, - _L("SLA Material Profiles Selection") + " ", _L("SLA Materials"), _L("Type:") )); - - - p->add_page(p->page_update = new PageUpdate(this)); - p->add_page(p->page_downloader = new PageDownloader(this)); - p->add_page(p->page_reload_from_disk = new PageReloadFromDisk(this)); -#ifdef _WIN32 - p->add_page(p->page_files_association = new PageFilesAssociation(this)); -#endif // _WIN32 - p->add_page(p->page_mode = new PageMode(this)); - p->add_page(p->page_firmware = new PageFirmware(this)); - p->add_page(p->page_bed = new PageBedShape(this)); - p->add_page(p->page_bvolume = new PageBuildVolume(this)); - p->add_page(p->page_diams = new PageDiameters(this)); - p->add_page(p->page_temps = new PageTemperatures(this)); - - p->load_pages(); - p->index->go_to(size_t{0}); - - vsizer->Add(topsizer, 1, wxEXPAND | wxALL, DIALOG_MARGIN); - vsizer->Add(hline, 0, wxEXPAND | wxLEFT | wxRIGHT, VERTICAL_SPACING); - vsizer->Add(p->btnsizer, 0, wxEXPAND | wxALL, DIALOG_MARGIN); - - SetSizer(vsizer); - SetSizerAndFit(vsizer); - - // We can now enable scrolling on hscroll - p->hscroll->SetScrollRate(30, 30); - - on_window_geometry(this, [this]() { - p->init_dialog_size(); - }); - - p->btn_prev->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { this->p->index->go_prev(); }); - - p->btn_next->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) - { - // check, that there is selected at least one filament/material - ConfigWizardPage* active_page = this->p->index->active_page(); - if (// Leaving the filaments or SLA materials page and - (active_page == p->page_filaments || active_page == p->page_sla_materials) && - // some Printer models had no filament or SLA material selected. - ! p->check_and_install_missing_materials(dynamic_cast(active_page)->materials->technology)) - // In that case don't leave the page and the function above queried the user whether to install default materials. - return; - this->p->index->go_next(); - }); - - p->btn_finish->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) - { - if (p->on_bnt_finish()) - this->EndModal(wxID_OK); - }); - - p->btn_sel_all->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { - p->any_sla_selected = true; - p->load_pages(); - p->page_fff->select_all(true, false); - p->page_msla->select_all(true, false); - p->index->go_to(p->page_mode); - }); - - p->index->Bind(EVT_INDEX_PAGE, [this](const wxCommandEvent &) { - const bool is_last = p->index->active_is_last(); - p->btn_next->Show(! is_last); - if (is_last) - p->btn_finish->SetFocus(); - - Layout(); - }); - - if (wxLinux_gtk3) - this->Bind(wxEVT_SHOW, [this, vsizer](const wxShowEvent& e) { - ConfigWizardPage* active_page = p->index->active_page(); - if (!active_page) - return; - for (auto page : p->all_pages) - if (page != active_page) - page->Hide(); - // update best size for the dialog after hiding of the non-active pages - vsizer->SetSizeHints(this); - // set initial dialog size - p->init_dialog_size(); - }); -} - -ConfigWizard::~ConfigWizard() {} - -bool ConfigWizard::run(RunReason reason, StartPage start_page) -{ - BOOST_LOG_TRIVIAL(info) << boost::format("Running ConfigWizard, reason: %1%, start_page: %2%") % reason % start_page; - - GUI_App &app = wxGetApp(); - - p->set_run_reason(reason); - p->set_start_page(start_page); - - if (ShowModal() == wxID_OK) { - bool apply_keeped_changes = false; - if (! p->apply_config(app.app_config, app.preset_bundle, app.preset_updater, apply_keeped_changes)) - return false; - - if (apply_keeped_changes) - app.apply_keeped_preset_modifications(); - - app.app_config->set_legacy_datadir(false); - app.update_mode(); - app.obj_manipul()->update_ui_from_settings(); - BOOST_LOG_TRIVIAL(info) << "ConfigWizard applied"; - return true; - } else { - BOOST_LOG_TRIVIAL(info) << "ConfigWizard cancelled"; - return false; - } -} - -const wxString& ConfigWizard::name(const bool from_menu/* = false*/) -{ - // A different naming convention is used for the Wizard on Windows & GTK vs. OSX. - // Note: Don't call _() macro here. - // This function just return the current name according to the OS. - // Translation is implemented inside GUI_App::add_config_menu() -#if __APPLE__ - static const wxString config_wizard_name = L("Configuration Assistant"); - static const wxString config_wizard_name_menu = L("Configuration &Assistant"); -#else - static const wxString config_wizard_name = L("Configuration Wizard"); - static const wxString config_wizard_name_menu = L("Configuration &Wizard"); -#endif - return from_menu ? config_wizard_name_menu : config_wizard_name; -} - -void ConfigWizard::on_dpi_changed(const wxRect &suggested_rect) -{ - p->index->msw_rescale(); - - const int em = em_unit(); - - msw_buttons_rescale(this, em, { wxID_APPLY, - wxID_CANCEL, - p->btn_sel_all->GetId(), - p->btn_next->GetId(), - p->btn_prev->GetId() }); - - for (auto printer_picker: p->page_fff->printer_pickers) - msw_buttons_rescale(this, em, printer_picker->get_button_indexes()); - - p->init_dialog_size(); - - Refresh(); -} - -void ConfigWizard::on_sys_color_changed() -{ - wxGetApp().UpdateDlgDarkUI(this); - Refresh(); -} - -} -} +// FIXME: extract absolute units -> em + +#include "ConfigWizard_private.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef WIN32 +#include +#include +#include +#endif // WIN32 + +#ifdef _MSW_DARK_MODE +#include +#endif // _MSW_DARK_MODE + +#include "libslic3r/Platform.hpp" +#include "libslic3r/Utils.hpp" +#include "libslic3r/Config.hpp" +#include "libslic3r/libslic3r.h" +#include "libslic3r/Model.hpp" +#include "libslic3r/Color.hpp" +#include "GUI.hpp" +#include "GUI_App.hpp" +#include "GUI_Utils.hpp" +#include "GUI_ObjectManipulation.hpp" +#include "Field.hpp" +#include "DesktopIntegrationDialog.hpp" +#include "slic3r/Config/Snapshot.hpp" +#include "slic3r/Utils/PresetUpdater.hpp" +#include "format.hpp" +#include "MsgDialog.hpp" +#include "UnsavedChangesDialog.hpp" +#include "slic3r/Utils/AppUpdater.hpp" + +#if defined(__linux__) && defined(__WXGTK3__) +#define wxLinux_gtk3 true +#else +#define wxLinux_gtk3 false +#endif //defined(__linux__) && defined(__WXGTK3__) + +namespace Slic3r { +namespace GUI { + + +using Config::Snapshot; +using Config::SnapshotDB; + + +// Configuration data structures extensions needed for the wizard + +bool Bundle::load(fs::path source_path, BundleLocation location, bool ais_prusa_bundle) +{ + this->preset_bundle = std::make_unique(); + this->location = location; + this->is_prusa_bundle = ais_prusa_bundle; + + std::string path_string = source_path.string(); + // Throw when parsing invalid configuration. Only valid configuration is supposed to be provided over the air. + auto [config_substitutions, presets_loaded] = preset_bundle->load_configbundle( + path_string, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable); + UNUSED(config_substitutions); + // No substitutions shall be reported when loading a system config bundle, no substitutions are allowed. + assert(config_substitutions.empty()); + auto first_vendor = preset_bundle->vendors.begin(); + if (first_vendor == preset_bundle->vendors.end()) { + BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % path_string; + return false; + } + if (presets_loaded == 0) { + BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No profile loaded.") % path_string; + return false; + } + + BOOST_LOG_TRIVIAL(trace) << boost::format("Vendor bundle: `%1%`: %2% profiles loaded.") % path_string % presets_loaded; + this->vendor_profile = &first_vendor->second; + return true; +} + +Bundle::Bundle(Bundle &&other) + : preset_bundle(std::move(other.preset_bundle)) + , vendor_profile(other.vendor_profile) + , location(other.location) + , is_prusa_bundle(other.is_prusa_bundle) +{ + other.vendor_profile = nullptr; +} + +BundleMap BundleMap::load() +{ + BundleMap res; + + const auto vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / "vendor").make_preferred(); + const auto archive_dir = (boost::filesystem::path(Slic3r::data_dir()) / "cache" / "vendor").make_preferred(); + const auto rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); + + // Load Prusa bundle from the datadir/vendor directory or from datadir/cache/vendor (archive) or from resources/profiles. + auto prusa_bundle_path = (vendor_dir / PresetBundle::PRUSA_BUNDLE).replace_extension(".ini"); + BundleLocation prusa_bundle_loc = BundleLocation::IN_VENDOR; + if (! boost::filesystem::exists(prusa_bundle_path)) { + prusa_bundle_path = (archive_dir / PresetBundle::PRUSA_BUNDLE).replace_extension(".ini"); + prusa_bundle_loc = BundleLocation::IN_ARCHIVE; + } + if (!boost::filesystem::exists(prusa_bundle_path)) { + prusa_bundle_path = (rsrc_vendor_dir / PresetBundle::PRUSA_BUNDLE).replace_extension(".ini"); + prusa_bundle_loc = BundleLocation::IN_RESOURCES; + } + { + Bundle prusa_bundle; + if (prusa_bundle.load(std::move(prusa_bundle_path), prusa_bundle_loc, true)) + res.emplace(PresetBundle::PRUSA_BUNDLE, std::move(prusa_bundle)); + } + + // Load the other bundles in the datadir/vendor directory + // and then additionally from datadir/cache/vendor (archive) and resources/profiles. + // Should we concider case where archive has older profiles than resources (shouldnt happen)? + typedef std::pair DirData; + std::vector dir_list { {vendor_dir, BundleLocation::IN_VENDOR}, {archive_dir, BundleLocation::IN_ARCHIVE}, {rsrc_vendor_dir, BundleLocation::IN_RESOURCES} }; + for ( auto dir : dir_list) { + if (!fs::exists(dir.first)) + continue; + for (const auto &dir_entry : boost::filesystem::directory_iterator(dir.first)) { + if (Slic3r::is_ini_file(dir_entry)) { + std::string id = dir_entry.path().stem().string(); // stem() = filename() without the trailing ".ini" part + + // Don't load this bundle if we've already loaded it. + if (res.find(id) != res.end()) { continue; } + + Bundle bundle; + if (bundle.load(dir_entry.path(), dir.second)) + res.emplace(std::move(id), std::move(bundle)); + } + } + } + + return res; +} + +Bundle& BundleMap::prusa_bundle() +{ + auto it = find(PresetBundle::PRUSA_BUNDLE); + if (it == end()) { + throw Slic3r::RuntimeError("ConfigWizard: Internal error in BundleMap: PRUSA_BUNDLE not loaded"); + } + + return it->second; +} + +const Bundle& BundleMap::prusa_bundle() const +{ + return const_cast(this)->prusa_bundle(); +} + + +// Printer model picker GUI control + +struct PrinterPickerEvent : public wxEvent +{ + std::string vendor_id; + std::string model_id; + std::string variant_name; + bool enable; + + PrinterPickerEvent(wxEventType eventType, int winid, std::string vendor_id, std::string model_id, std::string variant_name, bool enable) + : wxEvent(winid, eventType) + , vendor_id(std::move(vendor_id)) + , model_id(std::move(model_id)) + , variant_name(std::move(variant_name)) + , enable(enable) + {} + + virtual wxEvent *Clone() const + { + return new PrinterPickerEvent(*this); + } +}; + +wxDEFINE_EVENT(EVT_PRINTER_PICK, PrinterPickerEvent); + +const std::string PrinterPicker::PRINTER_PLACEHOLDER = "printer_placeholder.png"; + +PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxString title, size_t max_cols, const AppConfig &appconfig, const ModelFilter &filter) + : wxPanel(parent) + , vendor_id(vendor.id) + , width(0) +{ + wxGetApp().UpdateDarkUI(this); + const auto &models = vendor.models; + + auto *sizer = new wxBoxSizer(wxVERTICAL); + + const auto font_title = GetFont().MakeBold().Scaled(1.3f); + const auto font_name = GetFont().MakeBold(); + const auto font_alt_nozzle = GetFont().Scaled(0.9f); + + // wxGrid appends widgets by rows, but we need to construct them in columns. + // These vectors are used to hold the elements so that they can be appended in the right order. + std::vector titles; + std::vector bitmaps; + std::vector variants_panels; + + int max_row_width = 0; + int current_row_width = 0; + + bool is_variants = false; + + const fs::path vendor_dir_path = (fs::path(Slic3r::data_dir()) / "vendor").make_preferred(); + const fs::path cache_dir_path = (fs::path(Slic3r::data_dir()) / "cache").make_preferred(); + const fs::path rsrc_dir_path = (fs::path(resources_dir()) / "profiles").make_preferred(); + + for (const auto &model : models) { + if (! filter(model)) { continue; } + + wxBitmap bitmap; + int bitmap_width = 0; + auto load_bitmap = [](const wxString& bitmap_file, wxBitmap& bitmap, int& bitmap_width) { + bitmap.LoadFile(bitmap_file, wxBITMAP_TYPE_PNG); + bitmap_width = bitmap.GetWidth(); + }; + + bool found = false; + for (const fs::path& res : { rsrc_dir_path / vendor.id / model.thumbnail + , vendor_dir_path / vendor.id / model.thumbnail + , cache_dir_path / vendor.id / model.thumbnail }) + { + if (!fs::exists(res)) + continue; + load_bitmap(GUI::from_u8(res.string()), bitmap, bitmap_width); + found = true; + break; + } + + if (!found) { + BOOST_LOG_TRIVIAL(warning) << boost::format("Can't find bitmap file `%1%` for vendor `%2%`, printer `%3%`, using placeholder icon instead") + % model.thumbnail + % vendor.id + % model.id; + load_bitmap(Slic3r::var(PRINTER_PLACEHOLDER), bitmap, bitmap_width); + } + + wxStaticText* title = new wxStaticText(this, wxID_ANY, from_u8(model.name), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); + title->SetFont(font_name); + const int wrap_width = std::max((int)MODEL_MIN_WRAP, bitmap_width); + title->Wrap(wrap_width); + + current_row_width += wrap_width; + if (titles.size() % max_cols == max_cols - 1) { + max_row_width = std::max(max_row_width, current_row_width); + current_row_width = 0; + } + + titles.push_back(title); + + wxStaticBitmap* bitmap_widget = new wxStaticBitmap(this, wxID_ANY, bitmap); + bitmaps.push_back(bitmap_widget); + + auto *variants_panel = new wxPanel(this); + wxGetApp().UpdateDarkUI(variants_panel); + auto *variants_sizer = new wxBoxSizer(wxVERTICAL); + variants_panel->SetSizer(variants_sizer); + const auto model_id = model.id; + + for (size_t i = 0; i < model.variants.size(); i++) { + const auto &variant = model.variants[i]; + + const auto label = model.technology == ptFFF + ? from_u8((boost::format("%1% %2% %3%") % variant.name % _utf8(L("mm")) % _utf8(L("nozzle"))).str()) + : from_u8(model.name); + + if (i == 1) { + auto *alt_label = new wxStaticText(variants_panel, wxID_ANY, _L("Alternate nozzles:")); + alt_label->SetFont(font_alt_nozzle); + variants_sizer->Add(alt_label, 0, wxBOTTOM, 3); + is_variants = true; + } + + Checkbox* cbox = new Checkbox(variants_panel, label, model_id, variant.name); + i == 0 ? cboxes.push_back(cbox) : cboxes_alt.push_back(cbox); + + const bool enabled = appconfig.get_variant(vendor.id, model_id, variant.name); + cbox->SetValue(enabled); + + variants_sizer->Add(cbox, 0, wxBOTTOM, 3); + + cbox->Bind(wxEVT_CHECKBOX, [this, cbox](wxCommandEvent &event) { + on_checkbox(cbox, event.IsChecked()); + }); + } + + variants_panels.push_back(variants_panel); + } + + width = std::max(max_row_width, current_row_width); + + const size_t cols = std::min(max_cols, titles.size()); + + auto *printer_grid = new wxFlexGridSizer(cols, 0, 20); + printer_grid->SetFlexibleDirection(wxVERTICAL | wxHORIZONTAL); + + if (titles.size() > 0) { + const size_t odd_items = titles.size() % cols; + + for (size_t i = 0; i < titles.size() - odd_items; i += cols) { + for (size_t j = i; j < i + cols; j++) { printer_grid->Add(bitmaps[j], 0, wxBOTTOM, 20); } + for (size_t j = i; j < i + cols; j++) { printer_grid->Add(titles[j], 0, wxBOTTOM, 3); } + for (size_t j = i; j < i + cols; j++) { printer_grid->Add(variants_panels[j]); } + + // Add separator space to multiliners + if (titles.size() > cols) { + for (size_t j = i; j < i + cols; j++) { printer_grid->Add(1, 30); } + } + } + if (odd_items > 0) { + const size_t rem = titles.size() - odd_items; + + for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(bitmaps[i], 0, wxBOTTOM, 20); } + for (size_t i = 0; i < cols - odd_items; i++) { printer_grid->AddSpacer(1); } + for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(titles[i], 0, wxBOTTOM, 3); } + for (size_t i = 0; i < cols - odd_items; i++) { printer_grid->AddSpacer(1); } + for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(variants_panels[i]); } + } + } + + auto *title_sizer = new wxBoxSizer(wxHORIZONTAL); + if (! title.IsEmpty()) { + auto *title_widget = new wxStaticText(this, wxID_ANY, title); + title_widget->SetFont(font_title); + title_sizer->Add(title_widget); + } + title_sizer->AddStretchSpacer(); + + if (titles.size() > 1 || is_variants) { + // It only makes sense to add the All / None buttons if there's multiple printers + // All Standard button is added when there are more variants for at least one printer + auto *sel_all_std = new wxButton(this, wxID_ANY, titles.size() > 1 ? _L("All standard") : _L("Standard")); + auto *sel_all = new wxButton(this, wxID_ANY, _L("All")); + auto *sel_none = new wxButton(this, wxID_ANY, _L("None")); + if (is_variants) + sel_all_std->Bind(wxEVT_BUTTON, [this](const wxCommandEvent& event) { this->select_all(true, false); }); + sel_all->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->select_all(true, true); }); + sel_none->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->select_all(false); }); + if (is_variants) + title_sizer->Add(sel_all_std, 0, wxRIGHT, BTN_SPACING); + title_sizer->Add(sel_all, 0, wxRIGHT, BTN_SPACING); + title_sizer->Add(sel_none); + + wxGetApp().UpdateDarkUI(sel_all_std); + wxGetApp().UpdateDarkUI(sel_all); + wxGetApp().UpdateDarkUI(sel_none); + + // fill button indexes used later for buttons rescaling + if (is_variants) + m_button_indexes = { sel_all_std->GetId(), sel_all->GetId(), sel_none->GetId() }; + else { + sel_all_std->Destroy(); + m_button_indexes = { sel_all->GetId(), sel_none->GetId() }; + } + } + + sizer->Add(title_sizer, 0, wxEXPAND | wxBOTTOM, BTN_SPACING); + sizer->Add(printer_grid); + + SetSizer(sizer); +} + +PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxString title, size_t max_cols, const AppConfig &appconfig) + : PrinterPicker(parent, vendor, std::move(title), max_cols, appconfig, [](const VendorProfile::PrinterModel&) { return true; }) +{} + +void PrinterPicker::select_all(bool select, bool alternates) +{ + for (const auto &cb : cboxes) { + if (cb->GetValue() != select) { + cb->SetValue(select); + on_checkbox(cb, select); + } + } + + if (! select) { alternates = false; } + + for (const auto &cb : cboxes_alt) { + if (cb->GetValue() != alternates) { + cb->SetValue(alternates); + on_checkbox(cb, alternates); + } + } +} + +void PrinterPicker::select_one(size_t i, bool select) +{ + if (i < cboxes.size() && cboxes[i]->GetValue() != select) { + cboxes[i]->SetValue(select); + on_checkbox(cboxes[i], select); + } +} + +bool PrinterPicker::any_selected() const +{ + for (const auto &cb : cboxes) { + if (cb->GetValue()) { return true; } + } + + for (const auto &cb : cboxes_alt) { + if (cb->GetValue()) { return true; } + } + + return false; +} + +std::set PrinterPicker::get_selected_models() const +{ + std::set ret_set; + + for (const auto& cb : cboxes) + if (cb->GetValue()) + ret_set.emplace(cb->model); + + for (const auto& cb : cboxes_alt) + if (cb->GetValue()) + ret_set.emplace(cb->model); + + return ret_set; +} + +void PrinterPicker::on_checkbox(const Checkbox *cbox, bool checked) +{ + PrinterPickerEvent evt(EVT_PRINTER_PICK, GetId(), vendor_id, cbox->model, cbox->variant, checked); + AddPendingEvent(evt); +} + + +// Wizard page base + +ConfigWizardPage::ConfigWizardPage(ConfigWizard *parent, wxString title, wxString shortname, unsigned indent) + : wxPanel(parent->p->hscroll) + , parent(parent) + , shortname(std::move(shortname)) + , indent(indent) +{ + wxGetApp().UpdateDarkUI(this); + + auto *sizer = new wxBoxSizer(wxVERTICAL); + + auto *text = new wxStaticText(this, wxID_ANY, std::move(title), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); + const auto font = GetFont().MakeBold().Scaled(1.5); + text->SetFont(font); + sizer->Add(text, 0, wxALIGN_LEFT, 0); + sizer->AddSpacer(10); + + content = new wxBoxSizer(wxVERTICAL); + sizer->Add(content, 1, wxEXPAND); + + SetSizer(sizer); + + // There is strange layout on Linux with GTK3, + // see https://github.com/prusa3d/PrusaSlicer/issues/5103 and https://github.com/prusa3d/PrusaSlicer/issues/4861 + // So, non-active pages will be hidden later, on wxEVT_SHOW, after completed Layout() for all pages + if (!wxLinux_gtk3) + this->Hide(); + + Bind(wxEVT_SIZE, [this](wxSizeEvent &event) { + this->Layout(); + event.Skip(); + }); +} + +ConfigWizardPage::~ConfigWizardPage() {} + +wxStaticText* ConfigWizardPage::append_text(wxString text) +{ + auto *widget = new wxStaticText(this, wxID_ANY, text, wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); + widget->Wrap(WRAP_WIDTH); + widget->SetMinSize(wxSize(WRAP_WIDTH, -1)); + append(widget); + return widget; +} + +void ConfigWizardPage::append_spacer(int space) +{ + // FIXME: scaling + content->AddSpacer(space); +} + +// Wizard pages + +PageWelcome::PageWelcome(ConfigWizard *parent) + : ConfigWizardPage(parent, from_u8((boost::format( +#ifdef __APPLE__ + _utf8(L("Welcome to the %s Configuration Assistant")) +#else + _utf8(L("Welcome to the %s Configuration Wizard")) +#endif + ) % SLIC3R_APP_NAME).str()), _L("Welcome")) + , welcome_text(append_text(from_u8((boost::format( + _utf8(L("Hello, welcome to %s! This %s helps you with the initial configuration; just a few settings and you will be ready to print."))) + % SLIC3R_APP_NAME + % _utf8(ConfigWizard::name())).str()) + )) + , cbox_reset(append( + new wxCheckBox(this, wxID_ANY, _L("Remove user profiles (a snapshot will be taken beforehand)")) + )) + , cbox_integrate(append( + new wxCheckBox(this, wxID_ANY, _L("Perform desktop integration (Sets this binary to be searchable by the system).")) + )) +{ + welcome_text->Hide(); + cbox_reset->Hide(); + cbox_integrate->Hide(); +} + +void PageWelcome::set_run_reason(ConfigWizard::RunReason run_reason) +{ + const bool data_empty = run_reason == ConfigWizard::RR_DATA_EMPTY; + welcome_text->Show(data_empty); + cbox_reset->Show(!data_empty); +#if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) + if (!DesktopIntegrationDialog::is_integrated()) + cbox_integrate->Show(true); + else + cbox_integrate->Hide(); +#else + cbox_integrate->Hide(); +#endif +} + + +PagePrinters::PagePrinters(ConfigWizard *parent, + wxString title, + wxString shortname, + const VendorProfile &vendor, + unsigned indent, + Technology technology) + : ConfigWizardPage(parent, std::move(title), std::move(shortname), indent) + , technology(technology) + , install(false) // only used for 3rd party vendors +{ + enum { + COL_SIZE = 200, + }; + + AppConfig *appconfig = &this->wizard_p()->appconfig_new; + + const auto families = vendor.families(); + for (const auto &family : families) { + const auto filter = [&](const VendorProfile::PrinterModel &model) { + return ((model.technology == ptFFF && technology & T_FFF) + || (model.technology == ptSLA && technology & T_SLA)) + && model.family == family; + }; + + if (std::find_if(vendor.models.begin(), vendor.models.end(), filter) == vendor.models.end()) { + continue; + } + + const auto picker_title = family.empty() ? wxString() : from_u8((boost::format(_utf8(L("%s Family"))) % family).str()); + auto *picker = new PrinterPicker(this, vendor, picker_title, MAX_COLS, *appconfig, filter); + + picker->Bind(EVT_PRINTER_PICK, [this, appconfig](const PrinterPickerEvent &evt) { + appconfig->set_variant(evt.vendor_id, evt.model_id, evt.variant_name, evt.enable); + wizard_p()->on_printer_pick(this, evt); + }); + + append(new StaticLine(this)); + + append(picker); + printer_pickers.push_back(picker); + has_printers = true; + } + +} + +void PagePrinters::select_all(bool select, bool alternates) +{ + for (auto picker : printer_pickers) { + picker->select_all(select, alternates); + } +} + +int PagePrinters::get_width() const +{ + return std::accumulate(printer_pickers.begin(), printer_pickers.end(), 0, + [](int acc, const PrinterPicker *picker) { return std::max(acc, picker->get_width()); }); +} + +bool PagePrinters::any_selected() const +{ + for (const auto *picker : printer_pickers) { + if (picker->any_selected()) { return true; } + } + + return false; +} + +std::set PagePrinters::get_selected_models() +{ + std::set ret_set; + + for (const auto *picker : printer_pickers) + { + std::set tmp_models = picker->get_selected_models(); + ret_set.insert(tmp_models.begin(), tmp_models.end()); + } + + return ret_set; +} + +void PagePrinters::set_run_reason(ConfigWizard::RunReason run_reason) +{ + if (is_primary_printer_page + && (run_reason == ConfigWizard::RR_DATA_EMPTY || run_reason == ConfigWizard::RR_DATA_LEGACY) + && printer_pickers.size() > 0 + && printer_pickers[0]->vendor_id == PresetBundle::PRUSA_BUNDLE) { + printer_pickers[0]->select_one(0, true); + } +} + + +const std::string PageMaterials::EMPTY; +const std::string PageMaterials::TEMPLATES = "templates"; + +PageMaterials::PageMaterials(ConfigWizard *parent, Materials *materials, wxString title, wxString shortname, wxString list1name) + : ConfigWizardPage(parent, std::move(title), std::move(shortname)) + , materials(materials) + , list_printer(new StringList(this, wxLB_MULTIPLE)) + , list_type(new StringList(this)) + , list_vendor(new StringList(this)) + , list_profile(new PresetList(this)) +{ + append_spacer(VERTICAL_SPACING); + + const int em = parent->em_unit(); + const int list_h = 30*em; + + + list_printer->SetMinSize(wxSize(23*em, list_h)); + list_type->SetMinSize(wxSize(13*em, list_h)); + list_vendor->SetMinSize(wxSize(13*em, list_h)); + list_profile->SetMinSize(wxSize(23*em, list_h)); + + + + grid = new wxFlexGridSizer(4, em/2, em); + grid->AddGrowableCol(3, 1); + grid->AddGrowableRow(1, 1); + + grid->Add(new wxStaticText(this, wxID_ANY, _L("Printer:"))); + grid->Add(new wxStaticText(this, wxID_ANY, list1name)); + grid->Add(new wxStaticText(this, wxID_ANY, _L("Vendor:"))); + grid->Add(new wxStaticText(this, wxID_ANY, _L("Profile:"))); + + grid->Add(list_printer, 0, wxEXPAND); + grid->Add(list_type, 0, wxEXPAND); + grid->Add(list_vendor, 0, wxEXPAND); + grid->Add(list_profile, 1, wxEXPAND); + + auto *btn_sizer = new wxBoxSizer(wxHORIZONTAL); + auto *sel_all = new wxButton(this, wxID_ANY, _L("All")); + auto *sel_none = new wxButton(this, wxID_ANY, _L("None")); + btn_sizer->Add(sel_all, 0, wxRIGHT, em / 2); + btn_sizer->Add(sel_none); + + wxGetApp().UpdateDarkUI(list_printer); + wxGetApp().UpdateDarkUI(list_type); + wxGetApp().UpdateDarkUI(list_vendor); + wxGetApp().UpdateDarkUI(sel_all); + wxGetApp().UpdateDarkUI(sel_none); + + grid->Add(new wxBoxSizer(wxHORIZONTAL)); + grid->Add(new wxBoxSizer(wxHORIZONTAL)); + grid->Add(new wxBoxSizer(wxHORIZONTAL)); + grid->Add(btn_sizer, 0, wxALIGN_RIGHT); + + append(grid, 1, wxEXPAND); + + append_spacer(VERTICAL_SPACING); + + html_window = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, + wxSize(60 * em, 20 * em), wxHW_SCROLLBAR_AUTO); + append(html_window, 0, wxEXPAND); + + list_printer->Bind(wxEVT_LISTBOX, [this](wxCommandEvent& evt) { + update_lists(list_type->GetSelection(), list_vendor->GetSelection(), evt.GetInt()); + }); + list_type->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { + update_lists(list_type->GetSelection(), list_vendor->GetSelection()); + }); + list_vendor->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { + update_lists(list_type->GetSelection(), list_vendor->GetSelection()); + }); + + list_profile->Bind(wxEVT_CHECKLISTBOX, [this](wxCommandEvent &evt) { select_material(evt.GetInt()); }); + list_profile->Bind(wxEVT_LISTBOX, [this](wxCommandEvent& evt) { on_material_highlighted(evt.GetInt()); }); + + sel_all->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { select_all(true); }); + sel_none->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { select_all(false); }); + /* + Bind(wxEVT_PAINT, [this](wxPaintEvent& evt) {on_paint();}); + + list_profile->Bind(wxEVT_MOTION, [this](wxMouseEvent& evt) { on_mouse_move_on_profiles(evt); }); + list_profile->Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& evt) { on_mouse_enter_profiles(evt); }); + list_profile->Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& evt) { on_mouse_leave_profiles(evt); }); + */ + reload_presets(); + set_compatible_printers_html_window(std::vector(), false); +} +void PageMaterials::on_paint() +{ +} +void PageMaterials::on_mouse_move_on_profiles(wxMouseEvent& evt) +{ + const wxClientDC dc(list_profile); + const wxPoint pos = evt.GetLogicalPosition(dc); + int item = list_profile->HitTest(pos); + on_material_hovered(item); +} +void PageMaterials::on_mouse_enter_profiles(wxMouseEvent& evt) +{} +void PageMaterials::on_mouse_leave_profiles(wxMouseEvent& evt) +{ + on_material_hovered(-1); +} +void PageMaterials::reload_presets() +{ + clear(); + + list_printer->append(_L("(All)"), &EMPTY); + + const AppConfig* app_config = wxGetApp().app_config; + if (materials->technology == T_FFF && app_config->get("no_templates") == "0") + list_printer->append(_L("(Templates)"), &TEMPLATES); + + //list_printer->SetLabelMarkup("bald"); + for (const Preset* printer : materials->printers) { + list_printer->append(printer->name, &printer->name); + } + sort_list_data(list_printer, true, false); + if (list_printer->GetCount() > 0) { + list_printer->SetSelection(0); + sel_printers_prev.Clear(); + sel_type_prev = wxNOT_FOUND; + sel_vendor_prev = wxNOT_FOUND; + update_lists(0, 0, 0); + } + + presets_loaded = true; +} + +void PageMaterials::set_compatible_printers_html_window(const std::vector& printer_names, bool all_printers) +{ + const auto bgr_clr = +#if defined(__APPLE__) + html_window->GetParent()->GetBackgroundColour(); +#else +#if defined(_WIN32) + wxGetApp().get_window_default_clr(); +#else + wxSystemSettings::GetColour(wxSYS_COLOUR_MENU); +#endif +#endif + const auto text_clr = wxGetApp().get_label_clr_default(); + const auto bgr_clr_str = encode_color(ColorRGB(bgr_clr.Red(), bgr_clr.Green(), bgr_clr.Blue())); + const auto text_clr_str = encode_color(ColorRGB(text_clr.Red(), text_clr.Green(), text_clr.Blue())); + wxString text; + if (materials->technology == T_FFF && template_shown) { + text = format_wxstr(_L("%1% visible for (\"Template\") printer are universal profiles available for all printers. These might not be compatible with your printer."), materials->technology == T_FFF ? _L("Filaments") : _L("SLA materials")); + } else { + wxString first_line = format_wxstr(_L("%1% marked with * are not compatible with some installed printers."), materials->technology == T_FFF ? _L("Filaments") : _L("SLA materials")); + + if (all_printers) { + wxString second_line = format_wxstr(_L("All installed printers are compatible with the selected %1%."), materials->technology == T_FFF ? _L("filament") : _L("SLA material")); + text = wxString::Format( + "" + "" + "" + "" + "" + "%s

%s" + "
" + "
" + "" + "" + , bgr_clr_str + , text_clr_str + , first_line + , second_line + ); + } + else { + wxString second_line; + if (!printer_names.empty()) + second_line = (materials->technology == T_FFF ? + _L("Only the following installed printers are compatible with the selected filaments") : + _L("Only the following installed printers are compatible with the selected SLA materials")) + ":"; + text = wxString::Format( + "" + "" + "" + "" + "" + "%s

%s" + "" + "" + , bgr_clr_str + , text_clr_str + , first_line + , second_line); + for (size_t i = 0; i < printer_names.size(); ++i) + { + text += wxString::Format("", boost::nowide::widen(printer_names[i])); + if (i % 3 == 2) { + text += wxString::Format( + "" + ""); + } + } + text += wxString::Format( + "" + "
%s
" + "
" + "
" + "" + "" + ); + } + } + + + wxFont font = get_default_font_for_dpi(this, get_dpi_for_window(this)); + const int fs = font.GetPointSize(); + int size[] = { fs,fs,fs,fs,fs,fs,fs }; + html_window->SetFonts(font.GetFaceName(), font.GetFaceName(), size); + html_window->SetPage(text); +} + +void PageMaterials::clear_compatible_printers_label() +{ + set_compatible_printers_html_window(std::vector(), false); +} + +void PageMaterials::on_material_hovered(int sel_material) +{ + +} + +void PageMaterials::on_material_highlighted(int sel_material) +{ + if (sel_material == last_hovered_item) + return; + if (sel_material == -1) { + clear_compatible_printers_label(); + return; + } + last_hovered_item = sel_material; + std::vector tabs; + tabs.push_back(std::string()); + tabs.push_back(std::string()); + tabs.push_back(std::string()); + //selected material string + std::string material_name = list_profile->get_data(sel_material); + // get material preset + const std::vector matching_materials = materials->get_presets_by_alias(material_name); + if (matching_materials.empty()) + { + clear_compatible_printers_label(); + return; + } + //find matching printers + std::vector names; + for (const Preset* printer : materials->printers) { + for (const Preset* material : matching_materials) { + if (material->vendor && material->vendor->templates_profile) + continue; + if (is_compatible_with_printer(PresetWithVendorProfile(*material, material->vendor), PresetWithVendorProfile(*printer, printer->vendor))) { + names.push_back(printer->name); + break; + } + } + } + set_compatible_printers_html_window(names, names.size() == materials->printers.size()); +} + +void PageMaterials::update_lists(int sel_type, int sel_vendor, int last_selected_printer/* = -1*/) +{ + wxWindowUpdateLocker freeze_guard(this); + (void)freeze_guard; + + wxArrayInt sel_printers; + int sel_printers_count = list_printer->GetSelections(sel_printers); + + bool templates_available = list_printer->size() > 1 && list_printer->get_data(1) == TEMPLATES; + + // Does our wxWidgets version support operator== for wxArrayInt ? + // https://github.com/prusa3d/PrusaSlicer/issues/5152#issuecomment-787208614 +#if wxCHECK_VERSION(3, 1, 1) + if (sel_printers != sel_printers_prev) { +#else + auto are_equal = [](const wxArrayInt& arr_first, const wxArrayInt& arr_second) { + if (arr_first.GetCount() != arr_second.GetCount()) + return false; + for (size_t i = 0; i < arr_first.GetCount(); i++) + if (arr_first[i] != arr_second[i]) + return false; + return true; + }; + if (!are_equal(sel_printers, sel_printers_prev)) { +#endif + template_shown = false; + // Refresh type list + list_type->Clear(); + list_type->append(_L("(All)"), &EMPTY); + if (sel_printers_count > 1) { + // If all is selected with other printers + // unselect "all" or all printers depending on last value + // same with "templates" + if (sel_printers[0] == 0 && sel_printers_count > 1) { + if (last_selected_printer == 0) { + list_printer->SetSelection(wxNOT_FOUND); + list_printer->SetSelection(0); + } else { + list_printer->SetSelection(0, false); + sel_printers_count = list_printer->GetSelections(sel_printers); + } + } + if (materials->technology == T_FFF && templates_available && (sel_printers[0] == 1 || sel_printers[1] == 1) && sel_printers_count > 1) { + if (last_selected_printer == 1) { + list_printer->SetSelection(wxNOT_FOUND); + list_printer->SetSelection(1); + } + else if (last_selected_printer != 0) { + list_printer->SetSelection(1, false); + sel_printers_count = list_printer->GetSelections(sel_printers); + } + } + } + if (sel_printers_count > 0 && sel_printers[0] != 0 && ((materials->technology == T_FFF && templates_available && sel_printers[0] != 1) || materials->technology != T_FFF || !templates_available)) { + for (int i = 0; i < sel_printers_count; i++) { + const std::string& printer_name = list_printer->get_data(sel_printers[i]); + const Preset* printer = nullptr; + for (const Preset* it : materials->printers) { + if (it->name == printer_name) { + printer = it; + break; + } + } + materials->filter_presets(printer, printer_name, EMPTY, EMPTY, [this](const Preset* p) { + const std::string& type = this->materials->get_type(p); + if (list_type->find(type) == wxNOT_FOUND) { + list_type->append(type, &type); + } + }); + } + } + else if (sel_printers_count > 0 && last_selected_printer == 0) { + //clear selection except "ALL" + list_printer->SetSelection(wxNOT_FOUND); + list_printer->SetSelection(0); + sel_printers_count = list_printer->GetSelections(sel_printers); + + materials->filter_presets(nullptr, EMPTY, EMPTY, EMPTY, [this](const Preset* p) { + const std::string& type = this->materials->get_type(p); + if (list_type->find(type) == wxNOT_FOUND) { + list_type->append(type, &type); + } + }); + } + else if (materials->technology == T_FFF && templates_available && sel_printers_count > 0 && last_selected_printer == 1) { + //clear selection except "TEMPLATES" + list_printer->SetSelection(wxNOT_FOUND); + list_printer->SetSelection(1); + sel_printers_count = list_printer->GetSelections(sel_printers); + template_shown = true; + materials->filter_presets(nullptr, TEMPLATES, EMPTY, EMPTY, + [this](const Preset* p) { + const std::string& type = this->materials->get_type(p); + if (list_type->find(type) == wxNOT_FOUND) { + list_type->append(type, &type); + } + }); + } + sort_list_data(list_type, true, true); + + sel_printers_prev = sel_printers; + sel_type = 0; + sel_type_prev = wxNOT_FOUND; + list_type->SetSelection(sel_type); + list_profile->Clear(); + } + + if (sel_type != sel_type_prev) { + // Refresh vendor list + + // XXX: The vendor list is created with quadratic complexity here, + // but the number of vendors is going to be very small this shouldn't be a problem. + + list_vendor->Clear(); + list_vendor->append(_L("(All)"), &EMPTY); + if (sel_printers_count != 0 && sel_type != wxNOT_FOUND) { + const std::string& type = list_type->get_data(sel_type); + // find printer preset + for (int i = 0; i < sel_printers_count; i++) { + const std::string& printer_name = list_printer->get_data(sel_printers[i]); + const Preset* printer = nullptr; + for (const Preset* it : materials->printers) { + if (it->name == printer_name) { + printer = it; + break; + } + } + materials->filter_presets(printer, printer_name, type, EMPTY, [this](const Preset* p) { + const std::string& vendor = this->materials->get_vendor(p); + if (list_vendor->find(vendor) == wxNOT_FOUND) { + list_vendor->append(vendor, &vendor); + } + }); + } + sort_list_data(list_vendor, true, false); + } + + sel_type_prev = sel_type; + sel_vendor = 0; + sel_vendor_prev = wxNOT_FOUND; + list_vendor->SetSelection(sel_vendor); + list_profile->Clear(); + } + + if (sel_vendor != sel_vendor_prev) { + // Refresh material list + + list_profile->Clear(); + clear_compatible_printers_label(); + if (sel_printers_count != 0 && sel_type != wxNOT_FOUND && sel_vendor != wxNOT_FOUND) { + const std::string& type = list_type->get_data(sel_type); + const std::string& vendor = list_vendor->get_data(sel_vendor); + // first printer preset + std::vector to_list; + for (int i = 0; i < sel_printers_count; i++) { + const std::string& printer_name = list_printer->get_data(sel_printers[i]); + const Preset* printer = nullptr; + for (const Preset* it : materials->printers) { + if (it->name == printer_name) { + printer = it; + break; + } + } + materials->filter_presets(printer, printer_name, type, vendor, [this, &to_list](const Preset* p) { + const std::string& section = materials->appconfig_section(); + bool checked = wizard_p()->appconfig_new.has(section, p->name); + bool was_checked = false; + + int cur_i = list_profile->find(p->alias); + if (cur_i == wxNOT_FOUND) { + cur_i = list_profile->append(p->alias + (materials->get_omnipresent(p) || template_shown ? "" : " *"), &p->alias); + to_list.emplace_back(p->alias, materials->get_omnipresent(p), checked); + } + else { + was_checked = list_profile->IsChecked(cur_i); + to_list[cur_i].checked = checked || was_checked; + } + list_profile->Check(cur_i, checked || was_checked); + + /* Update preset selection in config. + * If one preset from aliases bundle is selected, + * than mark all presets with this aliases as selected + * */ + if (checked && !was_checked) + wizard_p()->update_presets_in_config(section, p->alias, true); + else if (!checked && was_checked) + wizard_p()->appconfig_new.set(section, p->name, "1"); + }); + } + sort_list_data(list_profile, to_list); + } + + sel_vendor_prev = sel_vendor; + } + wxGetApp().UpdateDarkUI(list_profile); +} + +void PageMaterials::sort_list_data(StringList* list, bool add_All_item, bool material_type_ordering) +{ +// get data from list +// sort data +// first should be +// then prusa profiles +// then the rest +// in alphabetical order + + std::vector> prusa_profiles; + std::vector> other_profiles; + bool add_TEMPLATES_item = false; + for (int i = 0 ; i < list->size(); ++i) { + const std::string& data = list->get_data(i); + if (data == EMPTY) // do not sort item + continue; + if (data == TEMPLATES) {// do not sort item + add_TEMPLATES_item = true; + continue; + } + if (!material_type_ordering && data.find("Prusa") != std::string::npos) + prusa_profiles.push_back(data); + else + other_profiles.push_back(data); + } + if(material_type_ordering) { + + const ConfigOptionDef* def = print_config_def.get("filament_type"); + std::vectorenum_values = def->enum_values; + size_t end_of_sorted = 0; + for (size_t vals = 0; vals < enum_values.size(); vals++) { + for (size_t profs = end_of_sorted; profs < other_profiles.size(); profs++) + { + // find instead compare because PET vs PETG + if (other_profiles[profs].get().find(enum_values[vals]) != std::string::npos) { + //swap + if(profs != end_of_sorted) { + std::reference_wrapper aux = other_profiles[end_of_sorted]; + other_profiles[end_of_sorted] = other_profiles[profs]; + other_profiles[profs] = aux; + } + end_of_sorted++; + break; + } + } + } + } else { + std::sort(prusa_profiles.begin(), prusa_profiles.end(), [](std::reference_wrapper a, std::reference_wrapper b) { + return a.get() < b.get(); + }); + std::sort(other_profiles.begin(), other_profiles.end(), [](std::reference_wrapper a, std::reference_wrapper b) { + return a.get() < b.get(); + }); + } + + list->Clear(); + if (add_All_item) + list->append(_L("(All)"), &EMPTY); + if (materials->technology == T_FFF && add_TEMPLATES_item) + list->append(_L("(Templates)"), &TEMPLATES); + for (const auto& item : prusa_profiles) + list->append(item, &const_cast(item.get())); + for (const auto& item : other_profiles) + list->append(item, &const_cast(item.get())); + +} + +void PageMaterials::sort_list_data(PresetList* list, const std::vector& data) +{ + // sort data + // then prusa profiles + // then the rest + // in alphabetical order + std::vector prusa_profiles; + std::vector other_profiles; + //for (int i = 0; i < data.size(); ++i) { + for (const auto& item : data) { + const std::string& name = item.name; + if (name.find("Prusa") != std::string::npos) + prusa_profiles.emplace_back(item); + else + other_profiles.emplace_back(item); + } + std::sort(prusa_profiles.begin(), prusa_profiles.end(), [](ProfilePrintData a, ProfilePrintData b) { + return a.name.get() < b.name.get(); + }); + std::sort(other_profiles.begin(), other_profiles.end(), [](ProfilePrintData a, ProfilePrintData b) { + return a.name.get() < b.name.get(); + }); + list->Clear(); + for (size_t i = 0; i < prusa_profiles.size(); ++i) { + list->append(std::string(prusa_profiles[i].name) + (prusa_profiles[i].omnipresent || template_shown ? "" : " *"), &const_cast(prusa_profiles[i].name.get())); + list->Check(i, prusa_profiles[i].checked); + } + for (size_t i = 0; i < other_profiles.size(); ++i) { + list->append(std::string(other_profiles[i].name) + (other_profiles[i].omnipresent || template_shown ? "" : " *"), &const_cast(other_profiles[i].name.get())); + list->Check(i + prusa_profiles.size(), other_profiles[i].checked); + } +} + +void PageMaterials::select_material(int i) +{ + const bool checked = list_profile->IsChecked(i); + + const std::string& alias_key = list_profile->get_data(i); + if (checked && template_shown && !notification_shown) { + notification_shown = true; + wxString message = _L("You have selected template filament. Please note that these filaments are available for all printers but are NOT certain to be compatible with your printer. Do you still wish to have this filament selected?\n(This message won't be displayed again.)"); + MessageDialog msg(this, message, _L("Notice"), wxYES_NO); + if (msg.ShowModal() == wxID_NO) { + list_profile->Check(i, false); + return; + } + } + wizard_p()->update_presets_in_config(materials->appconfig_section(), alias_key, checked); +} + +void PageMaterials::select_all(bool select) +{ + wxWindowUpdateLocker freeze_guard(this); + (void)freeze_guard; + + for (unsigned i = 0; i < list_profile->GetCount(); i++) { + const bool current = list_profile->IsChecked(i); + if (current != select) { + list_profile->Check(i, select); + select_material(i); + } + } +} + +void PageMaterials::clear() +{ + list_printer->Clear(); + list_type->Clear(); + list_vendor->Clear(); + list_profile->Clear(); + sel_printers_prev.Clear(); + sel_type_prev = wxNOT_FOUND; + sel_vendor_prev = wxNOT_FOUND; + presets_loaded = false; +} + +void PageMaterials::on_activate() +{ + if (! presets_loaded) { + wizard_p()->update_materials(materials->technology); + reload_presets(); + } + first_paint = true; +} + + +const char *PageCustom::default_profile_name = "My Settings"; + +PageCustom::PageCustom(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Custom Printer Setup"), _L("Custom Printer")) +{ + cb_custom = new wxCheckBox(this, wxID_ANY, _L("Define a custom printer profile")); + auto *label = new wxStaticText(this, wxID_ANY, _L("Custom profile name:")); + + wxBoxSizer* profile_name_sizer = new wxBoxSizer(wxVERTICAL); + profile_name_editor = new SavePresetDialog::Item{ this, profile_name_sizer, default_profile_name }; + profile_name_editor->Enable(false); + + cb_custom->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &) { + profile_name_editor->Enable(custom_wanted()); + wizard_p()->on_custom_setup(custom_wanted()); + }); + + append(cb_custom); + append(label); + append(profile_name_sizer); +} + +PageUpdate::PageUpdate(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Automatic updates"), _L("Updates")) + , version_check(true) + , preset_update(true) +{ + const AppConfig *app_config = wxGetApp().app_config; + auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + boldfont.SetWeight(wxFONTWEIGHT_BOLD); + + auto *box_slic3r = new wxCheckBox(this, wxID_ANY, _L("Check for application updates")); + box_slic3r->SetValue(app_config->get("notify_release") != "none"); + append(box_slic3r); + append_text(wxString::Format(_L( + "If enabled, %s checks for new application versions online. When a new version becomes available, " + "a notification is displayed at the next application startup (never during program usage). " + "This is only a notification mechanisms, no automatic installation is done."), SLIC3R_APP_NAME)); + + append_spacer(VERTICAL_SPACING); + + auto *box_presets = new wxCheckBox(this, wxID_ANY, _L("Update built-in Presets automatically")); + box_presets->SetValue(app_config->get("preset_update") == "1"); + append(box_presets); + append_text(wxString::Format(_L( + "If enabled, %s downloads updates of built-in system presets in the background." + "These updates are downloaded into a separate temporary location." + "When a new preset version becomes available it is offered at application startup."), SLIC3R_APP_NAME)); + const auto text_bold = _L("Updates are never applied without user's consent and never overwrite user's customized settings."); + auto *label_bold = new wxStaticText(this, wxID_ANY, text_bold); + label_bold->SetFont(boldfont); + label_bold->Wrap(WRAP_WIDTH); + append(label_bold); + append_text(_L("Additionally a backup snapshot of the whole configuration is created before an update is applied.")); + + box_slic3r->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->version_check = event.IsChecked(); }); + box_presets->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->preset_update = event.IsChecked(); }); +} + +namespace DownloaderUtils +{ +#ifdef _WIN32 + + wxString get_downloads_path() + { + wxString ret; + PWSTR path = NULL; + HRESULT hr = SHGetKnownFolderPath(FOLDERID_Downloads, 0, NULL, &path); + if (SUCCEEDED(hr)) { + ret = wxString(path); + } + CoTaskMemFree(path); + return ret; + } + +#elif __APPLE__ + wxString get_downloads_path() + { + // call objective-c implementation + return wxString::FromUTF8(get_downloads_path_mac()); + } +#else + wxString get_downloads_path() + { + wxString command = "xdg-user-dir DOWNLOAD"; + wxArrayString output; + GUI::desktop_execute_get_result(command, output); + if (output.GetCount() > 0) { + return output[0]; + } + return wxString(); + } + +#endif + +Worker::Worker(wxWindow* parent) +: wxBoxSizer(wxHORIZONTAL) +, m_parent(parent) +{ + m_input_path = new wxTextCtrl(m_parent, wxID_ANY); + set_path_name(get_app_config()->get("url_downloader_dest")); + + auto* path_label = new wxStaticText(m_parent, wxID_ANY, _L("Download path") + ":"); + + this->Add(path_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + this->Add(m_input_path, 1, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 5); + + auto* button_path = new wxButton(m_parent, wxID_ANY, _L("Browse")); + this->Add(button_path, 0, wxEXPAND | wxTOP | wxLEFT, 5); + button_path->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { + boost::filesystem::path chosen_dest(boost::nowide::narrow(m_input_path->GetValue())); + + wxDirDialog dialog(m_parent, L("Choose folder:"), chosen_dest.string() ); + if (dialog.ShowModal() == wxID_OK) + this->m_input_path->SetValue(dialog.GetPath()); + }); + + for (wxSizerItem* item : this->GetChildren()) + if (item->IsWindow()) { + wxWindow* win = item->GetWindow(); + wxGetApp().UpdateDarkUI(win); + } +} + +void Worker::set_path_name(wxString path) +{ + if (path.empty()) + path = boost::nowide::widen(get_app_config()->get("url_downloader_dest")); + + if (path.empty()) { + // What should be default path? Each system has Downloads folder, that could be good one. + // Other would be program location folder - not so good: access rights, apple bin is inside bundle... + // default_path = boost::dll::program_location().parent_path().string(); + path = get_downloads_path(); + } + + m_input_path->SetValue(path); +} + +void Worker::set_path_name(const std::string& name) +{ + if (!m_input_path) + return; + + set_path_name(boost::nowide::widen(name)); +} + +} // DownLoader + +PageDownloader::PageDownloader(ConfigWizard* parent) + : ConfigWizardPage(parent, _L("Downloads from URL"), _L("Downloads")) +{ + const AppConfig* app_config = wxGetApp().app_config; + auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + boldfont.SetWeight(wxFONTWEIGHT_BOLD); + + append_spacer(VERTICAL_SPACING); + + auto* box_allow_downloads = new wxCheckBox(this, wxID_ANY, _L("Allow build-in downloader")); + // TODO: Do we want it like this? The downloader is allowed for very first time the wizard is run. + bool box_allow_value = (app_config->has("downloader_url_registered") ? app_config->get("downloader_url_registered") == "1" : true); + box_allow_downloads->SetValue(box_allow_value); + append(box_allow_downloads); + + append_text(wxString::Format(_L( + "If enabled, %s registers to start on custom URL on www.printables.com." + " You will be able to use button with %s logo to open models in this %s." + " The model will be downloaded into folder you choose bellow." + ), SLIC3R_APP_NAME, SLIC3R_APP_NAME, SLIC3R_APP_NAME)); + +#ifdef __linux__ + append_text(wxString::Format(_L( + "On Linux systems the process of registration also creates desktop integration files for this version of application." + ))); +#endif + + box_allow_downloads->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) { this->downloader->allow(event.IsChecked()); }); + + downloader = new DownloaderUtils::Worker(this); + append(downloader); + downloader->allow(box_allow_value); +} + +bool PageDownloader::on_finish_downloader() const +{ + return downloader->on_finish(); +} + +bool DownloaderUtils::Worker::perform_register() +{ + //boost::filesystem::path chosen_dest/*(path_text_ctrl->GetValue());*/(boost::nowide::narrow(path_text_ctrl->GetValue())); + boost::filesystem::path chosen_dest (GUI::format(path_name())); + boost::system::error_code ec; + if (chosen_dest.empty() || !boost::filesystem::is_directory(chosen_dest, ec) || ec) { + std::string err_msg = GUI::format("%1%\n\n%2%",_L("Chosen directory for downloads does not Exists.") ,chosen_dest.string()); + BOOST_LOG_TRIVIAL(error) << err_msg; + show_error(m_parent, err_msg); + return false; + } + BOOST_LOG_TRIVIAL(info) << "Downloader registration: Directory for downloads: " << chosen_dest.string(); + wxGetApp().app_config->set("url_downloader_dest", chosen_dest.string()); +#ifdef _WIN32 + // Registry key creation for "prusaslicer://" URL + + boost::filesystem::path binary_path(boost::filesystem::canonical(boost::dll::program_location())); + // the path to binary needs to be correctly saved in string with respect to localized characters + wxString wbinary = wxString::FromUTF8(binary_path.string()); + std::string binary_string = (boost::format("%1%") % wbinary).str(); + BOOST_LOG_TRIVIAL(info) << "Downloader registration: Path of binary: " << binary_string; + + //std::string key_string = "\"" + binary_string + "\" \"-u\" \"%1\""; + //std::string key_string = "\"" + binary_string + "\" \"%1\""; + std::string key_string = "\"" + binary_string + "\" \"--single-instance\" \"%1\""; + + wxRegKey key_first(wxRegKey::HKCU, "Software\\Classes\\prusaslicer"); + wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\prusaslicer\\shell\\open\\command"); + if (!key_first.Exists()) { + key_first.Create(false); + } + key_first.SetValue("URL Protocol", ""); + + if (!key_full.Exists()) { + key_full.Create(false); + } + //key_full = "\"C:\\Program Files\\Prusa3D\\PrusaSlicer\\prusa-slicer-console.exe\" \"%1\""; + key_full = key_string; +#elif __APPLE__ + // Apple registers for custom url in info.plist thus it has to be already registered since build. + // The url will always trigger opening of prusaslicer and we have to check that user has allowed it. (GUI_App::MacOpenURL is the triggered method) +#else + // the performation should be called later during desktop integration + perform_registration_linux = true; +#endif + return true; +} + +void DownloaderUtils::Worker::deregister() +{ +#ifdef _WIN32 + std::string key_string = ""; + wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\prusaslicer\\shell\\open\\command"); + if (!key_full.Exists()) { + return; + } + key_full = key_string; +#elif __APPLE__ + // TODO +#else + BOOST_LOG_TRIVIAL(debug) << "DesktopIntegrationDialog::undo_downloader_registration"; + DesktopIntegrationDialog::undo_downloader_registration(); + perform_registration_linux = false; +#endif +} + +bool DownloaderUtils::Worker::on_finish() { + AppConfig* app_config = wxGetApp().app_config; + bool ac_value = app_config->get("downloader_url_registered") == "1"; + BOOST_LOG_TRIVIAL(debug) << "PageDownloader::on_finish_downloader ac_value " << ac_value << " downloader_checked " << downloader_checked; + if (ac_value && downloader_checked) { + // already registered but we need to do it again + if (!perform_register()) + return false; + app_config->set("downloader_url_registered", "1"); + } else if (!ac_value && downloader_checked) { + // register + if (!perform_register()) + return false; + app_config->set("downloader_url_registered", "1"); + } else if (ac_value && !downloader_checked) { + // deregister, downloads are banned now + deregister(); + app_config->set("downloader_url_registered", "0"); + } /*else if (!ac_value && !downloader_checked) { + // not registered and we dont want to do it + // do not deregister as other instance might be registered + } */ + return true; +} + + +PageReloadFromDisk::PageReloadFromDisk(ConfigWizard* parent) + : ConfigWizardPage(parent, _L("Reload from disk"), _L("Reload from disk")) + , full_pathnames(false) +{ + auto* box_pathnames = new wxCheckBox(this, wxID_ANY, _L("Export full pathnames of models and parts sources into 3mf and amf files")); + box_pathnames->SetValue(wxGetApp().app_config->get("export_sources_full_pathnames") == "1"); + append(box_pathnames); + append_text(_L( + "If enabled, allows the Reload from disk command to automatically find and load the files when invoked.\n" + "If not enabled, the Reload from disk command will ask to select each file using an open file dialog." + )); + + box_pathnames->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) { this->full_pathnames = event.IsChecked(); }); +} + +#ifdef _WIN32 +PageFilesAssociation::PageFilesAssociation(ConfigWizard* parent) + : ConfigWizardPage(parent, _L("Files association"), _L("Files association")) +{ + cb_3mf = new wxCheckBox(this, wxID_ANY, _L("Associate .3mf files to PrusaSlicer")); + cb_stl = new wxCheckBox(this, wxID_ANY, _L("Associate .stl files to PrusaSlicer")); +// cb_gcode = new wxCheckBox(this, wxID_ANY, _L("Associate .gcode files to PrusaSlicer G-code Viewer")); + + append(cb_3mf); + append(cb_stl); +// append(cb_gcode); +} +#endif // _WIN32 + +PageMode::PageMode(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("View mode"), _L("View mode")) +{ + append_text(_L("PrusaSlicer's user interfaces comes in three variants:\nSimple, Advanced, and Expert.\n" + "The Simple mode shows only the most frequently used settings relevant for regular 3D printing. " + "The other two offer progressively more sophisticated fine-tuning, " + "they are suitable for advanced and expert users, respectively.")); + + radio_simple = new wxRadioButton(this, wxID_ANY, _L("Simple mode")); + radio_advanced = new wxRadioButton(this, wxID_ANY, _L("Advanced mode")); + radio_expert = new wxRadioButton(this, wxID_ANY, _L("Expert mode")); + + std::string mode { "simple" }; + wxGetApp().app_config->get("", "view_mode", mode); + + if (mode == "advanced") { radio_advanced->SetValue(true); } + else if (mode == "expert") { radio_expert->SetValue(true); } + else { radio_simple->SetValue(true); } + + append(radio_simple); + append(radio_advanced); + append(radio_expert); + + append_text("\n" + _L("The size of the object can be specified in inches")); + check_inch = new wxCheckBox(this, wxID_ANY, _L("Use inches")); + check_inch->SetValue(wxGetApp().app_config->get("use_inches") == "1"); + append(check_inch); + + on_activate(); +} + +void PageMode::serialize_mode(AppConfig *app_config) const +{ + std::string mode = ""; + + if (radio_simple->GetValue()) { mode = "simple"; } + if (radio_advanced->GetValue()) { mode = "advanced"; } + if (radio_expert->GetValue()) { mode = "expert"; } + + app_config->set("view_mode", mode); + app_config->set("use_inches", check_inch->GetValue() ? "1" : "0"); +} + +PageVendors::PageVendors(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Other Vendors"), _L("Other Vendors")) +{ + const AppConfig &appconfig = this->wizard_p()->appconfig_new; + + append_text(wxString::Format(_L("Pick another vendor supported by %s"), SLIC3R_APP_NAME) + ":"); + + auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + boldfont.SetWeight(wxFONTWEIGHT_BOLD); + + for (const auto &pair : wizard_p()->bundles) { + const VendorProfile *vendor = pair.second.vendor_profile; + if (vendor->id == PresetBundle::PRUSA_BUNDLE) { continue; } + if (vendor && vendor->templates_profile) + continue; + + auto *cbox = new wxCheckBox(this, wxID_ANY, vendor->name); + cbox->Bind(wxEVT_CHECKBOX, [=](wxCommandEvent &event) { + wizard_p()->on_3rdparty_install(vendor, cbox->IsChecked()); + }); + + const auto &vendors = appconfig.vendors(); + const bool enabled = vendors.find(pair.first) != vendors.end(); + if (enabled) { + cbox->SetValue(true); + + auto pages = wizard_p()->pages_3rdparty.find(vendor->id); + wxCHECK_RET(pages != wizard_p()->pages_3rdparty.end(), "Internal error: 3rd party vendor printers page not created"); + + for (PagePrinters* page : { pages->second.first, pages->second.second }) + if (page) page->install = true; + } + + append(cbox); + } +} + +PageFirmware::PageFirmware(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Firmware Type"), _L("Firmware"), 1) + , gcode_opt(*print_config_def.get("gcode_flavor")) + , gcode_picker(nullptr) +{ + append_text(_L("Choose the type of firmware used by your printer.")); + append_text(_(gcode_opt.tooltip)); + + wxArrayString choices; + choices.Alloc(gcode_opt.enum_labels.size()); + for (const auto &label : gcode_opt.enum_labels) { + choices.Add(label); + } + + gcode_picker = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices); + wxGetApp().UpdateDarkUI(gcode_picker); + const auto &enum_values = gcode_opt.enum_values; + auto needle = enum_values.cend(); + if (gcode_opt.default_value) { + needle = std::find(enum_values.cbegin(), enum_values.cend(), gcode_opt.default_value->serialize()); + } + if (needle != enum_values.cend()) { + gcode_picker->SetSelection(needle - enum_values.cbegin()); + } else { + gcode_picker->SetSelection(0); + } + + append(gcode_picker); +} + +void PageFirmware::apply_custom_config(DynamicPrintConfig &config) +{ + auto sel = gcode_picker->GetSelection(); + if (sel >= 0 && (size_t)sel < gcode_opt.enum_labels.size()) { + auto *opt = new ConfigOptionEnum(static_cast(sel)); + config.set_key_value("gcode_flavor", opt); + } +} + +static void focus_event(wxFocusEvent& e, wxTextCtrl* ctrl, double def_value) +{ + e.Skip(); + wxString str = ctrl->GetValue(); + + const char dec_sep = is_decimal_separator_point() ? '.' : ','; + const char dec_sep_alt = dec_sep == '.' ? ',' : '.'; + // Replace the first incorrect separator in decimal number. + bool was_replaced = str.Replace(dec_sep_alt, dec_sep, false) != 0; + + double val = 0.0; + if (!str.ToDouble(&val)) { + if (val == 0.0) + val = def_value; + ctrl->SetValue(double_to_string(val)); + show_error(nullptr, _L("Invalid numeric input.")); + // On Windows, this SetFocus creates an invisible marker. + //ctrl->SetFocus(); + } + else if (was_replaced) + ctrl->SetValue(double_to_string(val)); +} + +class DiamTextCtrl : public wxTextCtrl +{ +public: + DiamTextCtrl(wxWindow* parent) + { +#ifdef _WIN32 + long style = wxBORDER_SIMPLE; +#else + long style = 0; +#endif + Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(Field::def_width_thinner() * wxGetApp().em_unit(), wxDefaultCoord), style); + wxGetApp().UpdateDarkUI(this); + } + ~DiamTextCtrl() {} +}; + +PageBedShape::PageBedShape(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Bed Shape and Size"), _L("Bed Shape"), 1) + , shape_panel(new BedShapePanel(this)) +{ + append_text(_L("Set the shape of your printer's bed.")); + + shape_panel->build_panel(*wizard_p()->custom_config->option("bed_shape"), + *wizard_p()->custom_config->option("bed_custom_texture"), + *wizard_p()->custom_config->option("bed_custom_model")); + + append(shape_panel); +} + +void PageBedShape::apply_custom_config(DynamicPrintConfig &config) +{ + const std::vector& points = shape_panel->get_shape(); + const std::string& custom_texture = shape_panel->get_custom_texture(); + const std::string& custom_model = shape_panel->get_custom_model(); + config.set_key_value("bed_shape", new ConfigOptionPoints(points)); + config.set_key_value("bed_custom_texture", new ConfigOptionString(custom_texture)); + config.set_key_value("bed_custom_model", new ConfigOptionString(custom_model)); +} + +PageBuildVolume::PageBuildVolume(ConfigWizard* parent) + : ConfigWizardPage(parent, _L("Build Volume"), _L("Build Volume"), 1) + , build_volume(new DiamTextCtrl(this)) +{ + append_text(_L("Set verctical size of your printer.")); + + wxString value = "200"; + build_volume->SetValue(value); + + build_volume->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { + double def_value = 200.0; + double max_value = 1200.0; + e.Skip(); + wxString str = build_volume->GetValue(); + + const char dec_sep = is_decimal_separator_point() ? '.' : ','; + const char dec_sep_alt = dec_sep == '.' ? ',' : '.'; + // Replace the first incorrect separator in decimal number. + bool was_replaced = str.Replace(dec_sep_alt, dec_sep, false) != 0; + + double val = 0.0; + if (!str.ToDouble(&val)) { + val = def_value; + build_volume->SetValue(double_to_string(val)); + show_error(nullptr, _L("Invalid numeric input.")); + //build_volume->SetFocus(); + } else if (val < 0.0) { + val = def_value; + build_volume->SetValue(double_to_string(val)); + show_error(nullptr, _L("Invalid numeric input.")); + //build_volume->SetFocus(); + } else if (val > max_value) { + val = max_value; + build_volume->SetValue(double_to_string(val)); + show_error(nullptr, _L("Invalid numeric input.")); + //build_volume->SetFocus(); + } else if (was_replaced) + build_volume->SetValue(double_to_string(val)); + }, build_volume->GetId()); + + auto* sizer_volume = new wxFlexGridSizer(3, 5, 5); + auto* text_volume = new wxStaticText(this, wxID_ANY, _L("Max print height:")); + auto* unit_volume = new wxStaticText(this, wxID_ANY, _L("mm")); + sizer_volume->AddGrowableCol(0, 1); + sizer_volume->Add(text_volume, 0, wxALIGN_CENTRE_VERTICAL); + sizer_volume->Add(build_volume); + sizer_volume->Add(unit_volume, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_volume); +} + +void PageBuildVolume::apply_custom_config(DynamicPrintConfig& config) +{ + double val = 0.0; + build_volume->GetValue().ToDouble(&val); + auto* opt_volume = new ConfigOptionFloat(val); + config.set_key_value("max_print_height", opt_volume); +} + +PageDiameters::PageDiameters(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Filament and Nozzle Diameters"), _L("Print Diameters"), 1) + , diam_nozzle(new DiamTextCtrl(this)) + , diam_filam (new DiamTextCtrl(this)) +{ + auto *default_nozzle = print_config_def.get("nozzle_diameter")->get_default_value(); + wxString value = double_to_string(default_nozzle != nullptr && default_nozzle->size() > 0 ? default_nozzle->get_at(0) : 0.5); + diam_nozzle->SetValue(value); + + auto *default_filam = print_config_def.get("filament_diameter")->get_default_value(); + value = double_to_string(default_filam != nullptr && default_filam->size() > 0 ? default_filam->get_at(0) : 3.0); + diam_filam->SetValue(value); + + diam_nozzle->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { focus_event(e, diam_nozzle, 0.5); }, diam_nozzle->GetId()); + diam_filam ->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { focus_event(e, diam_filam , 3.0); }, diam_filam->GetId()); + + append_text(_L("Enter the diameter of your printer's hot end nozzle.")); + + auto *sizer_nozzle = new wxFlexGridSizer(3, 5, 5); + auto *text_nozzle = new wxStaticText(this, wxID_ANY, _L("Nozzle Diameter:")); + auto *unit_nozzle = new wxStaticText(this, wxID_ANY, _L("mm")); + sizer_nozzle->AddGrowableCol(0, 1); + sizer_nozzle->Add(text_nozzle, 0, wxALIGN_CENTRE_VERTICAL); + sizer_nozzle->Add(diam_nozzle); + sizer_nozzle->Add(unit_nozzle, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_nozzle); + + append_spacer(VERTICAL_SPACING); + + append_text(_L("Enter the diameter of your filament.")); + append_text(_L("Good precision is required, so use a caliper and do multiple measurements along the filament, then compute the average.")); + + auto *sizer_filam = new wxFlexGridSizer(3, 5, 5); + auto *text_filam = new wxStaticText(this, wxID_ANY, _L("Filament Diameter:")); + auto *unit_filam = new wxStaticText(this, wxID_ANY, _L("mm")); + sizer_filam->AddGrowableCol(0, 1); + sizer_filam->Add(text_filam, 0, wxALIGN_CENTRE_VERTICAL); + sizer_filam->Add(diam_filam, 0, wxALIGN_CENTRE_VERTICAL); + sizer_filam->Add(unit_filam, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_filam); +} + +void PageDiameters::apply_custom_config(DynamicPrintConfig &config) +{ + double val = 0.0; + diam_nozzle->GetValue().ToDouble(&val); + auto *opt_nozzle = new ConfigOptionFloats(1, val); + config.set_key_value("nozzle_diameter", opt_nozzle); + + val = 0.0; + diam_filam->GetValue().ToDouble(&val); + auto * opt_filam = new ConfigOptionFloats(1, val); + config.set_key_value("filament_diameter", opt_filam); + + auto set_extrusion_width = [&config, opt_nozzle](const char *key, double dmr) { + char buf[64]; // locales don't matter here (sprintf/atof) + sprintf(buf, "%.2lf", dmr * opt_nozzle->values.front() / 0.4); + config.set_key_value(key, new ConfigOptionFloatOrPercent(atof(buf), false)); + }; + + set_extrusion_width("support_material_extrusion_width", 0.35); + set_extrusion_width("top_infill_extrusion_width", 0.40); + set_extrusion_width("first_layer_extrusion_width", 0.42); + + set_extrusion_width("extrusion_width", 0.45); + set_extrusion_width("perimeter_extrusion_width", 0.45); + set_extrusion_width("external_perimeter_extrusion_width", 0.45); + set_extrusion_width("infill_extrusion_width", 0.45); + set_extrusion_width("solid_infill_extrusion_width", 0.45); +} + +class SpinCtrlDouble: public wxSpinCtrlDouble +{ +public: + SpinCtrlDouble(wxWindow* parent) + { +#ifdef _WIN32 + long style = wxSP_ARROW_KEYS | wxBORDER_SIMPLE; +#else + long style = wxSP_ARROW_KEYS; +#endif + Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, style); +#ifdef _WIN32 + wxGetApp().UpdateDarkUI(this->GetText()); +#endif + this->Refresh(); + } + ~SpinCtrlDouble() {} +}; + +PageTemperatures::PageTemperatures(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Nozzle and Bed Temperatures"), _L("Temperatures"), 1) + , spin_extr(new SpinCtrlDouble(this)) + , spin_bed (new SpinCtrlDouble(this)) +{ + spin_extr->SetIncrement(5.0); + const auto &def_extr = *print_config_def.get("temperature"); + spin_extr->SetRange(def_extr.min, def_extr.max); + auto *default_extr = def_extr.get_default_value(); + spin_extr->SetValue(default_extr != nullptr && default_extr->size() > 0 ? default_extr->get_at(0) : 200); + + spin_bed->SetIncrement(5.0); + const auto &def_bed = *print_config_def.get("bed_temperature"); + spin_bed->SetRange(def_bed.min, def_bed.max); + auto *default_bed = def_bed.get_default_value(); + spin_bed->SetValue(default_bed != nullptr && default_bed->size() > 0 ? default_bed->get_at(0) : 0); + + append_text(_L("Enter the temperature needed for extruding your filament.")); + append_text(_L("A rule of thumb is 160 to 230 °C for PLA, and 215 to 250 °C for ABS.")); + + auto *sizer_extr = new wxFlexGridSizer(3, 5, 5); + auto *text_extr = new wxStaticText(this, wxID_ANY, _L("Extrusion Temperature:")); + auto *unit_extr = new wxStaticText(this, wxID_ANY, _L("°C")); + sizer_extr->AddGrowableCol(0, 1); + sizer_extr->Add(text_extr, 0, wxALIGN_CENTRE_VERTICAL); + sizer_extr->Add(spin_extr); + sizer_extr->Add(unit_extr, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_extr); + + append_spacer(VERTICAL_SPACING); + + append_text(_L("Enter the bed temperature needed for getting your filament to stick to your heated bed.")); + append_text(_L("A rule of thumb is 60 °C for PLA and 110 °C for ABS. Leave zero if you have no heated bed.")); + + auto *sizer_bed = new wxFlexGridSizer(3, 5, 5); + auto *text_bed = new wxStaticText(this, wxID_ANY, _L("Bed Temperature:")); + auto *unit_bed = new wxStaticText(this, wxID_ANY, _L("°C")); + sizer_bed->AddGrowableCol(0, 1); + sizer_bed->Add(text_bed, 0, wxALIGN_CENTRE_VERTICAL); + sizer_bed->Add(spin_bed); + sizer_bed->Add(unit_bed, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_bed); +} + +void PageTemperatures::apply_custom_config(DynamicPrintConfig &config) +{ + auto *opt_extr = new ConfigOptionInts(1, spin_extr->GetValue()); + config.set_key_value("temperature", opt_extr); + auto *opt_extr1st = new ConfigOptionInts(1, spin_extr->GetValue()); + config.set_key_value("first_layer_temperature", opt_extr1st); + auto *opt_bed = new ConfigOptionInts(1, spin_bed->GetValue()); + config.set_key_value("bed_temperature", opt_bed); + auto *opt_bed1st = new ConfigOptionInts(1, spin_bed->GetValue()); + config.set_key_value("first_layer_bed_temperature", opt_bed1st); +} + + +// Index + +ConfigWizardIndex::ConfigWizardIndex(wxWindow *parent) + : wxPanel(parent) + , bg(ScalableBitmap(parent, "PrusaSlicer_192px_transparent.png", 192)) + , bullet_black(ScalableBitmap(parent, "bullet_black.png")) + , bullet_blue(ScalableBitmap(parent, "bullet_blue.png")) + , bullet_white(ScalableBitmap(parent, "bullet_white.png")) + , item_active(NO_ITEM) + , item_hover(NO_ITEM) + , last_page((size_t)-1) +{ +#ifndef __WXOSX__ + SetDoubleBuffered(true);// SetDoubleBuffered exists on Win and Linux/GTK, but is missing on OSX +#endif //__WXOSX__ + SetMinSize(bg.GetSize()); + + const wxSize size = GetTextExtent("m"); + em_w = size.x; + em_h = size.y; + + Bind(wxEVT_PAINT, &ConfigWizardIndex::on_paint, this); + Bind(wxEVT_SIZE, [this](wxEvent& e) { e.Skip(); Refresh(); }); + Bind(wxEVT_MOTION, &ConfigWizardIndex::on_mouse_move, this); + + Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent &evt) { + if (item_hover != -1) { + item_hover = -1; + Refresh(); + } + evt.Skip(); + }); + + Bind(wxEVT_LEFT_UP, [this](wxMouseEvent &evt) { + if (item_hover >= 0) { go_to(item_hover); } + }); +} + +wxDECLARE_EVENT(EVT_INDEX_PAGE, wxCommandEvent); + +void ConfigWizardIndex::add_page(ConfigWizardPage *page) +{ + last_page = items.size(); + items.emplace_back(Item { page->shortname, page->indent, page }); + Refresh(); +} + +void ConfigWizardIndex::add_label(wxString label, unsigned indent) +{ + items.emplace_back(Item { std::move(label), indent, nullptr }); + Refresh(); +} + +ConfigWizardPage* ConfigWizardIndex::active_page() const +{ + if (item_active >= items.size()) { return nullptr; } + + return items[item_active].page; +} + +void ConfigWizardIndex::go_prev() +{ + // Search for a preceiding item that is a page (not a label, ie. page != nullptr) + + if (item_active == NO_ITEM) { return; } + + for (size_t i = item_active; i > 0; i--) { + if (items[i - 1].page != nullptr) { + go_to(i - 1); + return; + } + } +} + +void ConfigWizardIndex::go_next() +{ + // Search for a next item that is a page (not a label, ie. page != nullptr) + + if (item_active == NO_ITEM) { return; } + + for (size_t i = item_active + 1; i < items.size(); i++) { + if (items[i].page != nullptr) { + go_to(i); + return; + } + } +} + +// This one actually performs the go-to op +void ConfigWizardIndex::go_to(size_t i) +{ + if (i != item_active + && i < items.size() + && items[i].page != nullptr) { + auto *new_active = items[i].page; + auto *former_active = active_page(); + if (former_active != nullptr) { + former_active->Hide(); + } + + item_active = i; + new_active->Show(); + + wxCommandEvent evt(EVT_INDEX_PAGE, GetId()); + AddPendingEvent(evt); + + Refresh(); + + new_active->on_activate(); + } +} + +void ConfigWizardIndex::go_to(const ConfigWizardPage *page) +{ + if (page == nullptr) { return; } + + for (size_t i = 0; i < items.size(); i++) { + if (items[i].page == page) { + go_to(i); + return; + } + } +} + +void ConfigWizardIndex::clear() +{ + auto *former_active = active_page(); + if (former_active != nullptr) { former_active->Hide(); } + + items.clear(); + item_active = NO_ITEM; +} + +void ConfigWizardIndex::on_paint(wxPaintEvent & evt) +{ + const auto size = GetClientSize(); + if (size.GetHeight() == 0 || size.GetWidth() == 0) { return; } + + wxPaintDC dc(this); + + const auto bullet_w = bullet_black.GetWidth(); + const auto bullet_h = bullet_black.GetHeight(); + const int yoff_icon = bullet_h < em_h ? (em_h - bullet_h) / 2 : 0; + const int yoff_text = bullet_h > em_h ? (bullet_h - em_h) / 2 : 0; + const int yinc = item_height(); + + int index_width = 0; + + unsigned y = 0; + for (size_t i = 0; i < items.size(); i++) { + const Item& item = items[i]; + unsigned x = em_w/2 + item.indent * em_w; + + if (i == item_active || (item_hover >= 0 && i == (size_t)item_hover)) { + dc.DrawBitmap(bullet_blue.get_bitmap(), x, y + yoff_icon, false); + } + else if (i < item_active) { dc.DrawBitmap(bullet_black.get_bitmap(), x, y + yoff_icon, false); } + else if (i > item_active) { dc.DrawBitmap(bullet_white.get_bitmap(), x, y + yoff_icon, false); } + + x += + bullet_w + em_w/2; + const auto text_size = dc.GetTextExtent(item.label); + dc.SetTextForeground(wxGetApp().get_label_clr_default()); + dc.DrawText(item.label, x, y + yoff_text); + + y += yinc; + index_width = std::max(index_width, (int)x + text_size.x); + } + + //draw logo + if (int y = size.y - bg.GetHeight(); y>=0) { + dc.DrawBitmap(bg.get_bitmap(), 0, y, false); + index_width = std::max(index_width, bg.GetWidth() + em_w / 2); + } + + if (GetMinSize().x < index_width) { + CallAfter([this, index_width]() { + SetMinSize(wxSize(index_width, GetMinSize().y)); + Refresh(); + }); + } +} + +void ConfigWizardIndex::on_mouse_move(wxMouseEvent &evt) +{ + const wxClientDC dc(this); + const wxPoint pos = evt.GetLogicalPosition(dc); + + const ssize_t item_hover_new = pos.y / item_height(); + + if (item_hover_new < ssize_t(items.size()) && item_hover_new != item_hover) { + item_hover = item_hover_new; + Refresh(); + } + + evt.Skip(); +} + +void ConfigWizardIndex::msw_rescale() +{ + const wxSize size = GetTextExtent("m"); + em_w = size.x; + em_h = size.y; + + SetMinSize(bg.GetSize()); + + Refresh(); +} + + +// Materials + +const std::string Materials::UNKNOWN = "(Unknown)"; + +void Materials::push(const Preset *preset) +{ + presets.emplace_back(preset); + types.insert(technology & T_FFF + ? Materials::get_filament_type(preset) + : Materials::get_material_type(preset)); +} + +void Materials::add_printer(const Preset* preset) +{ + printers.insert(preset); +} + +void Materials::clear() +{ + presets.clear(); + types.clear(); + printers.clear(); + compatibility_counter.clear(); +} + +const std::string& Materials::appconfig_section() const +{ + return (technology & T_FFF) ? AppConfig::SECTION_FILAMENTS : AppConfig::SECTION_MATERIALS; +} + +const std::string& Materials::get_type(const Preset *preset) const +{ + return (technology & T_FFF) ? get_filament_type(preset) : get_material_type(preset); +} + +const std::string& Materials::get_vendor(const Preset *preset) const +{ + return (technology & T_FFF) ? get_filament_vendor(preset) : get_material_vendor(preset); +} + +const std::string& Materials::get_filament_type(const Preset *preset) +{ + const auto *opt = preset->config.opt("filament_type"); + if (opt != nullptr && opt->values.size() > 0) { + return opt->values[0]; + } else { + return UNKNOWN; + } +} + +const std::string& Materials::get_filament_vendor(const Preset *preset) +{ + const auto *opt = preset->config.opt("filament_vendor"); + return opt != nullptr ? opt->value : UNKNOWN; +} + +const std::string& Materials::get_material_type(const Preset *preset) +{ + const auto *opt = preset->config.opt("material_type"); + if (opt != nullptr) { + return opt->value; + } else { + return UNKNOWN; + } +} + +const std::string& Materials::get_material_vendor(const Preset *preset) +{ + const auto *opt = preset->config.opt("material_vendor"); + return opt != nullptr ? opt->value : UNKNOWN; +} + +// priv + +static const std::unordered_map> legacy_preset_map {{ + { "Original Prusa i3 MK2.ini", std::make_pair("MK2S", "0.4") }, + { "Original Prusa i3 MK2 MM Single Mode.ini", std::make_pair("MK2SMM", "0.4") }, + { "Original Prusa i3 MK2 MM Single Mode 0.6 nozzle.ini", std::make_pair("MK2SMM", "0.6") }, + { "Original Prusa i3 MK2 MultiMaterial.ini", std::make_pair("MK2SMM", "0.4") }, + { "Original Prusa i3 MK2 MultiMaterial 0.6 nozzle.ini", std::make_pair("MK2SMM", "0.6") }, + { "Original Prusa i3 MK2 0.25 nozzle.ini", std::make_pair("MK2S", "0.25") }, + { "Original Prusa i3 MK2 0.6 nozzle.ini", std::make_pair("MK2S", "0.6") }, + { "Original Prusa i3 MK3.ini", std::make_pair("MK3", "0.4") }, +}}; + +void ConfigWizard::priv::load_pages() +{ + wxWindowUpdateLocker freeze_guard(q); + (void)freeze_guard; + + const ConfigWizardPage *former_active = index->active_page(); + + index->clear(); + + index->add_page(page_welcome); + + // Printers + if (!only_sla_mode) + index->add_page(page_fff); + index->add_page(page_msla); + if (!only_sla_mode) { + index->add_page(page_vendors); + for (const auto &pages : pages_3rdparty) { + for ( PagePrinters* page : { pages.second.first, pages.second.second }) + if (page && page->install) + index->add_page(page); + } + + index->add_page(page_custom); + if (page_custom->custom_wanted()) { + index->add_page(page_firmware); + index->add_page(page_bed); + index->add_page(page_bvolume); + index->add_page(page_diams); + index->add_page(page_temps); + } + + // Filaments & Materials + if (any_fff_selected) { index->add_page(page_filaments); } + // Filaments page if only custom printer is selected + const AppConfig* app_config = wxGetApp().app_config; + if (!any_fff_selected && (custom_printer_selected || custom_printer_in_bundle) && (app_config->get("no_templates") == "0")) { + update_materials(T_ANY); + index->add_page(page_filaments); + } + } + if (any_sla_selected) { index->add_page(page_sla_materials); } + + // there should to be selected at least one printer + btn_finish->Enable(any_fff_selected || any_sla_selected || custom_printer_selected || custom_printer_in_bundle); + + index->add_page(page_update); + index->add_page(page_downloader); + index->add_page(page_reload_from_disk); +#ifdef _WIN32 + index->add_page(page_files_association); +#endif // _WIN32 + index->add_page(page_mode); + + index->go_to(former_active); // Will restore the active item/page if possible + + q->Layout(); +// This Refresh() is needed to avoid ugly artifacts after printer selection, when no one vendor was selected from the very beginnig + q->Refresh(); +} + +void ConfigWizard::priv::init_dialog_size() +{ + // Clamp the Wizard size based on screen dimensions + + const auto idx = wxDisplay::GetFromWindow(q); + wxDisplay display(idx != wxNOT_FOUND ? idx : 0u); + + const auto disp_rect = display.GetClientArea(); + wxRect window_rect( + disp_rect.x + disp_rect.width / 20, + disp_rect.y + disp_rect.height / 20, + 9*disp_rect.width / 10, + 9*disp_rect.height / 10); + + const int width_hint = index->GetSize().GetWidth() + std::max(90 * em(), (only_sla_mode ? page_msla->get_width() : page_fff->get_width()) + 30 * em()); // XXX: magic constant, I found no better solution + if (width_hint < window_rect.width) { + window_rect.x += (window_rect.width - width_hint) / 2; + window_rect.width = width_hint; + } + + q->SetSize(window_rect); +} + +void ConfigWizard::priv::load_vendors() +{ + bundles = BundleMap::load(); + + // Load up the set of vendors / models / variants the user has had enabled up till now + AppConfig *app_config = wxGetApp().app_config; + if (! app_config->legacy_datadir()) { + appconfig_new.set_vendors(*app_config); + } else { + // In case of legacy datadir, try to guess the preference based on the printer preset files that are present + const auto printer_dir = fs::path(Slic3r::data_dir()) / "printer"; + for (auto &dir_entry : boost::filesystem::directory_iterator(printer_dir)) + if (Slic3r::is_ini_file(dir_entry)) { + auto needle = legacy_preset_map.find(dir_entry.path().filename().string()); + if (needle == legacy_preset_map.end()) { continue; } + + const auto &model = needle->second.first; + const auto &variant = needle->second.second; + appconfig_new.set_variant("PrusaResearch", model, variant, true); + } + } + + for (const auto& printer : wxGetApp().preset_bundle->printers) { + if (!printer.is_default && !printer.is_system && printer.is_visible) { + custom_printer_in_bundle = true; + break; + } + } + + // Initialize the is_visible flag in printer Presets + for (auto &pair : bundles) { + pair.second.preset_bundle->load_installed_printers(appconfig_new); + } + + // Copy installed filaments and SLA material names from app_config to appconfig_new + // while resolving current names of profiles, which were renamed in the meantime. + for (PrinterTechnology technology : { ptFFF, ptSLA }) { + const std::string §ion_name = (technology == ptFFF) ? AppConfig::SECTION_FILAMENTS : AppConfig::SECTION_MATERIALS; + std::map section_new; + if (app_config->has_section(section_name)) { + const std::map §ion_old = app_config->get_section(section_name); + for (const auto& material_name_and_installed : section_old) + if (material_name_and_installed.second == "1") { + // Material is installed. Resolve it in bundles. + size_t num_found = 0; + const std::string &material_name = material_name_and_installed.first; + for (auto &bundle : bundles) { + const PresetCollection &materials = bundle.second.preset_bundle->materials(technology); + const Preset *preset = materials.find_preset(material_name); + if (preset == nullptr) { + // Not found. Maybe the material preset is there, bu it was was renamed? + const std::string *new_name = materials.get_preset_name_renamed(material_name); + if (new_name != nullptr) + preset = materials.find_preset(*new_name); + } + if (preset != nullptr) { + // Materal preset was found, mark it as installed. + section_new[preset->name] = "1"; + ++ num_found; + } + } + if (num_found == 0) + BOOST_LOG_TRIVIAL(error) << boost::format("Profile %1% was not found in installed vendor Preset Bundles.") % material_name; + else if (num_found > 1) + BOOST_LOG_TRIVIAL(error) << boost::format("Profile %1% was found in %2% vendor Preset Bundles.") % material_name % num_found; + } + } + appconfig_new.set_section(section_name, section_new); + }; +} + +void ConfigWizard::priv::add_page(ConfigWizardPage *page) +{ + const int proportion = (page->shortname == _L("Filaments")) || (page->shortname == _L("SLA Materials")) ? 1 : 0; + hscroll_sizer->Add(page, proportion, wxEXPAND); + all_pages.push_back(page); +} + +void ConfigWizard::priv::enable_next(bool enable) +{ + btn_next->Enable(enable); + btn_finish->Enable(enable); +} + +void ConfigWizard::priv::set_start_page(ConfigWizard::StartPage start_page) +{ + switch (start_page) { + case ConfigWizard::SP_PRINTERS: + index->go_to(page_fff); + btn_next->SetFocus(); + break; + case ConfigWizard::SP_FILAMENTS: + index->go_to(page_filaments); + btn_finish->SetFocus(); + break; + case ConfigWizard::SP_MATERIALS: + index->go_to(page_sla_materials); + btn_finish->SetFocus(); + break; + default: + index->go_to(page_welcome); + btn_next->SetFocus(); + break; + } +} + +void ConfigWizard::priv::create_3rdparty_pages() +{ + for (const auto &pair : bundles) { + const VendorProfile *vendor = pair.second.vendor_profile; + if (vendor->id == PresetBundle::PRUSA_BUNDLE) { continue; } + + bool is_fff_technology = false; + bool is_sla_technology = false; + + for (auto& model: vendor->models) + { + if (!is_fff_technology && model.technology == ptFFF) + is_fff_technology = true; + if (!is_sla_technology && model.technology == ptSLA) + is_sla_technology = true; + } + + PagePrinters* pageFFF = nullptr; + PagePrinters* pageSLA = nullptr; + + if (is_fff_technology) { + pageFFF = new PagePrinters(q, vendor->name + " " +_L("FFF Technology Printers"), vendor->name+" FFF", *vendor, 1, T_FFF); + add_page(pageFFF); + } + + if (is_sla_technology) { + pageSLA = new PagePrinters(q, vendor->name + " " + _L("SLA Technology Printers"), vendor->name+" MSLA", *vendor, 1, T_SLA); + add_page(pageSLA); + } + + pages_3rdparty.insert({vendor->id, {pageFFF, pageSLA}}); + } +} + +void ConfigWizard::priv::set_run_reason(RunReason run_reason) +{ + this->run_reason = run_reason; + for (auto &page : all_pages) { + page->set_run_reason(run_reason); + } +} + +void ConfigWizard::priv::update_materials(Technology technology) +{ + if ((any_fff_selected || custom_printer_in_bundle || custom_printer_selected) && (technology & T_FFF)) { + filaments.clear(); + aliases_fff.clear(); + // Iterate filaments in all bundles + for (const auto &pair : bundles) { + for (const auto &filament : pair.second.preset_bundle->filaments) { + // Check if filament is already added + if (filaments.containts(&filament)) + continue; + // Iterate printers in all bundles + for (const auto &printer : pair.second.preset_bundle->printers) { + if (!printer.is_visible || printer.printer_technology() != ptFFF) + continue; + // Filter out inapplicable printers + if (is_compatible_with_printer(PresetWithVendorProfile(filament, filament.vendor), PresetWithVendorProfile(printer, printer.vendor))) { + if (!filaments.containts(&filament)) { + filaments.push(&filament); + if (!filament.alias.empty()) + aliases_fff[filament.alias].insert(filament.name); + } + filaments.add_printer(&printer); + } + } + // template filament bundle has no printers - filament would be never added + if(pair.second.vendor_profile&& pair.second.vendor_profile->templates_profile && pair.second.preset_bundle->printers.begin() == pair.second.preset_bundle->printers.end()) + { + if (!filaments.containts(&filament)) { + filaments.push(&filament); + if (!filament.alias.empty()) + aliases_fff[filament.alias].insert(filament.name); + } + } + } + } + // count compatible printers + for (const auto& preset : filaments.presets) { + // skip template filaments + if (preset->vendor && preset->vendor->templates_profile) + continue; + + const auto filter = [preset](const std::pair element) { + return preset->alias == element.first; + }; + if (std::find_if(filaments.compatibility_counter.begin(), filaments.compatibility_counter.end(), filter) != filaments.compatibility_counter.end()) { + continue; + } + // find all aliases (except templates) + std::vector idx_with_same_alias; + for (size_t i = 0; i < filaments.presets.size(); ++i) { + if (preset->alias == filaments.presets[i]->alias && ((filaments.presets[i]->vendor && !filaments.presets[i]->vendor->templates_profile) || !filaments.presets[i]->vendor)) + idx_with_same_alias.push_back(i); + } + // check compatibility with each printer + size_t counter = 0; + for (const auto& printer : filaments.printers) { + if (!(*printer).is_visible || (*printer).printer_technology() != ptFFF) + continue; + bool compatible = false; + // Test other materials with same alias + for (size_t i = 0; i < idx_with_same_alias.size() && !compatible; ++i) { + const Preset& prst = *(filaments.presets[idx_with_same_alias[i]]); + const Preset& prntr = *printer; + if (is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) { + compatible = true; + break; + } + } + if (compatible) + counter++; + } + filaments.compatibility_counter.emplace_back(preset->alias, counter); + } + } + + if (any_sla_selected && (technology & T_SLA)) { + sla_materials.clear(); + aliases_sla.clear(); + + // Iterate SLA materials in all bundles + for (const auto &pair : bundles) { + for (const auto &material : pair.second.preset_bundle->sla_materials) { + // Check if material is already added + if (sla_materials.containts(&material)) + continue; + // Iterate printers in all bundles + // For now, we only allow the profiles to be compatible with another profiles inside the same bundle. + for (const auto& printer : pair.second.preset_bundle->printers) { + if(!printer.is_visible || printer.printer_technology() != ptSLA) + continue; + // Filter out inapplicable printers + if (is_compatible_with_printer(PresetWithVendorProfile(material, nullptr), PresetWithVendorProfile(printer, nullptr))) { + // Check if material is already added + if(!sla_materials.containts(&material)) { + sla_materials.push(&material); + if (!material.alias.empty()) + aliases_sla[material.alias].insert(material.name); + } + sla_materials.add_printer(&printer); + } + } + } + } + // count compatible printers + for (const auto& preset : sla_materials.presets) { + + const auto filter = [preset](const std::pair element) { + return preset->alias == element.first; + }; + if (std::find_if(sla_materials.compatibility_counter.begin(), sla_materials.compatibility_counter.end(), filter) != sla_materials.compatibility_counter.end()) { + continue; + } + std::vector idx_with_same_alias; + for (size_t i = 0; i < sla_materials.presets.size(); ++i) { + if(preset->alias == sla_materials.presets[i]->alias) + idx_with_same_alias.push_back(i); + } + size_t counter = 0; + for (const auto& printer : sla_materials.printers) { + if (!(*printer).is_visible || (*printer).printer_technology() != ptSLA) + continue; + bool compatible = false; + // Test otrher materials with same alias + for (size_t i = 0; i < idx_with_same_alias.size() && !compatible; ++i) { + const Preset& prst = *(sla_materials.presets[idx_with_same_alias[i]]); + const Preset& prntr = *printer; + if (is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) { + compatible = true; + break; + } + } + if (compatible) + counter++; + } + sla_materials.compatibility_counter.emplace_back(preset->alias, counter); + } + } +} + +void ConfigWizard::priv::on_custom_setup(const bool custom_wanted) +{ + custom_printer_selected = custom_wanted; + load_pages(); +} + +void ConfigWizard::priv::on_printer_pick(PagePrinters *page, const PrinterPickerEvent &evt) +{ + if (check_sla_selected() != any_sla_selected || + check_fff_selected() != any_fff_selected) { + any_fff_selected = check_fff_selected(); + any_sla_selected = check_sla_selected(); + + load_pages(); + } + + // Update the is_visible flag on relevant printer profiles + for (auto &pair : bundles) { + if (pair.first != evt.vendor_id) { continue; } + + for (auto &preset : pair.second.preset_bundle->printers) { + if (preset.config.opt_string("printer_model") == evt.model_id + && preset.config.opt_string("printer_variant") == evt.variant_name) { + preset.is_visible = evt.enable; + } + } + + // When a printer model is picked, but there is no material installed compatible with this printer model, + // install default materials for selected printer model silently. + check_and_install_missing_materials(page->technology, evt.model_id); + } + + if (page->technology & T_FFF) { + page_filaments->clear(); + } else if (page->technology & T_SLA) { + page_sla_materials->clear(); + } +} + +void ConfigWizard::priv::select_default_materials_for_printer_model(const VendorProfile::PrinterModel &printer_model, Technology technology) +{ + PageMaterials* page_materials = technology & T_FFF ? page_filaments : page_sla_materials; + for (const std::string& material : printer_model.default_materials) + appconfig_new.set(page_materials->materials->appconfig_section(), material, "1"); +} + +void ConfigWizard::priv::select_default_materials_for_printer_models(Technology technology, const std::set &printer_models) +{ + PageMaterials *page_materials = technology & T_FFF ? page_filaments : page_sla_materials; + const std::string &appconfig_section = page_materials->materials->appconfig_section(); + + // Following block was unnecessary. Its enough to iterate printer_models once. Not for every vendor printer page. + // Filament is selected on same page for all printers of same technology. + /* + auto select_default_materials_for_printer_page = [this, appconfig_section, printer_models, technology](PagePrinters *page_printers, Technology technology) + { + const std::string vendor_id = page_printers->get_vendor_id(); + for (auto& pair : bundles) + if (pair.first == vendor_id) + for (const VendorProfile::PrinterModel *printer_model : printer_models) + for (const std::string &material : printer_model->default_materials) + appconfig_new.set(appconfig_section, material, "1"); + }; + + PagePrinters* page_printers = technology & T_FFF ? page_fff : page_msla; + select_default_materials_for_printer_page(page_printers, technology); + + for (const auto& printer : pages_3rdparty) + { + page_printers = technology & T_FFF ? printer.second.first : printer.second.second; + if (page_printers) + select_default_materials_for_printer_page(page_printers, technology); + } + */ + + // Iterate printer_models and select default materials. If none available -> msg to user. + std::vector models_without_default; + for (const VendorProfile::PrinterModel* printer_model : printer_models) { + if (printer_model->default_materials.empty()) { + models_without_default.emplace_back(printer_model); + } else { + for (const std::string& material : printer_model->default_materials) + appconfig_new.set(appconfig_section, material, "1"); + } + } + + if (!models_without_default.empty()) { + std::string printer_names = "\n\n"; + for (const VendorProfile::PrinterModel* printer_model : models_without_default) { + printer_names += printer_model->name + "\n"; + } + printer_names += "\n\n"; + std::string message = (technology & T_FFF ? + GUI::format(_L("Following printer profiles has no default filament: %1%Please select one manually."), printer_names) : + GUI::format(_L("Following printer profiles has no default material: %1%Please select one manually."), printer_names)); + MessageDialog msg(q, message, _L("Notice"), wxOK); + msg.ShowModal(); + } + + update_materials(technology); + ((technology & T_FFF) ? page_filaments : page_sla_materials)->reload_presets(); +} + +void ConfigWizard::priv::on_3rdparty_install(const VendorProfile *vendor, bool install) +{ + auto it = pages_3rdparty.find(vendor->id); + wxCHECK_RET(it != pages_3rdparty.end(), "Internal error: GUI page not found for 3rd party vendor profile"); + + for (PagePrinters* page : { it->second.first, it->second.second }) + if (page) { + if (page->install && !install) + page->select_all(false); + page->install = install; + // if some 3rd vendor is selected, select first printer for them + if (install) + page->printer_pickers[0]->select_one(0, true); + page->Layout(); + } + + load_pages(); +} + +bool ConfigWizard::priv::on_bnt_finish() +{ + wxBusyCursor wait; + + if (!page_downloader->on_finish_downloader()) { + index->go_to(page_downloader); + return false; + } + /* When Filaments or Sla Materials pages are activated, + * materials for this pages are automaticaly updated and presets are reloaded. + * + * But, if _Finish_ button was clicked without activation of those pages + * (for example, just some printers were added/deleted), + * than last changes wouldn't be updated for filaments/materials. + * SO, do that before close of Wizard + */ + update_materials(T_ANY); + if (any_fff_selected) + page_filaments->reload_presets(); + if (any_sla_selected) + page_sla_materials->reload_presets(); + + // theres no need to check that filament is selected if we have only custom printer + if (custom_printer_selected && !any_fff_selected && !any_sla_selected) return true; + // check, that there is selected at least one filament/material + return check_and_install_missing_materials(T_ANY); +} + +// This allmighty method verifies, whether there is at least a single compatible filament or SLA material installed +// for each Printer preset of each Printer Model installed. +// +// In case only_for_model_id is set, then the test is done for that particular printer model only, and the default materials are installed silently. +// Otherwise the user is quieried whether to install the missing default materials or not. +// +// Return true if the tested Printer Models already had materials installed. +// Return false if there were some Printer Models with missing materials, independent from whether the defaults were installed for these +// respective Printer Models or not. +bool ConfigWizard::priv::check_and_install_missing_materials(Technology technology, const std::string &only_for_model_id) +{ + // Walk over all installed Printer presets and verify whether there is a filament or SLA material profile installed at the same PresetBundle, + // which is compatible with it. + const auto printer_models_missing_materials = [this, only_for_model_id](PrinterTechnology technology, const std::string §ion) + { + const std::map &appconfig_presets = appconfig_new.has_section(section) ? appconfig_new.get_section(section) : std::map(); + std::set printer_models_without_material; + for (const auto &pair : bundles) { + const PresetCollection &materials = pair.second.preset_bundle->materials(technology); + for (const auto &printer : pair.second.preset_bundle->printers) { + if (printer.is_visible && printer.printer_technology() == technology) { + const VendorProfile::PrinterModel *printer_model = PresetUtils::system_printer_model(printer); + assert(printer_model != nullptr); + if ((only_for_model_id.empty() || only_for_model_id == printer_model->id) && + printer_models_without_material.find(printer_model) == printer_models_without_material.end()) { + bool has_material = false; + for (const auto& preset : appconfig_presets) { + if (preset.second == "1") { + const Preset *material = materials.find_preset(preset.first, false); + if (material != nullptr && is_compatible_with_printer(PresetWithVendorProfile(*material, nullptr), PresetWithVendorProfile(printer, nullptr))) { + has_material = true; + break; + } + + // find if preset.first is part of the templates profile (up is searching if preset.first is part of printer vendor preset) + for (const auto& bp : bundles) { + if (!bp.second.preset_bundle->vendors.empty() && bp.second.preset_bundle->vendors.begin()->second.templates_profile) { + const PresetCollection& template_materials = bp.second.preset_bundle->materials(technology); + const Preset* template_material = template_materials.find_preset(preset.first, false); + if (template_material && is_compatible_with_printer(PresetWithVendorProfile(*template_material, &bp.second.preset_bundle->vendors.begin()->second), PresetWithVendorProfile(printer, nullptr))) { + has_material = true; + break; + } + } + } + if (has_material) + break; + + } + } + if (! has_material) + printer_models_without_material.insert(printer_model); + } + } + } + } + // todo: just workaround so template_profile_selected wont get to false after this function is called for SLA + // this will work unltil there are no SLA template filaments + if (technology == ptFFF) { + // template_profile_selected check + template_profile_selected = false; + for (const auto& bp : bundles) { + if (!bp.second.preset_bundle->vendors.empty() && bp.second.preset_bundle->vendors.begin()->second.templates_profile) { + for (const auto& preset : appconfig_presets) { + const PresetCollection& template_materials = bp.second.preset_bundle->materials(technology); + const Preset* template_material = template_materials.find_preset(preset.first, false); + if (template_material){ + template_profile_selected = true; + break; + } + } + if (template_profile_selected) { + break; + } + } + } + } + assert(printer_models_without_material.empty() || only_for_model_id.empty() || only_for_model_id == (*printer_models_without_material.begin())->id); + return printer_models_without_material; + }; + + const auto ask_and_select_default_materials = [this](const wxString &message, const std::set &printer_models, Technology technology) + { + //wxMessageDialog msg(q, message, _L("Notice"), wxYES_NO); + MessageDialog msg(q, message, _L("Notice"), wxYES_NO); + if (msg.ShowModal() == wxID_YES) + select_default_materials_for_printer_models(technology, printer_models); + }; + + const auto printer_model_list = [](const std::set &printer_models) -> wxString { + wxString out; + for (const VendorProfile::PrinterModel *printer_model : printer_models) { + wxString name = from_u8(printer_model->name); + out += "\t\t"; + out += name; + out += "\n"; + } + return out; + }; + + if (any_fff_selected && (technology & T_FFF)) { + std::set printer_models_without_material = printer_models_missing_materials(ptFFF, AppConfig::SECTION_FILAMENTS); + if (! printer_models_without_material.empty()) { + if (only_for_model_id.empty()) + ask_and_select_default_materials( + _L("The following FFF printer models have no filament selected:") + + "\n\n\t" + + printer_model_list(printer_models_without_material) + + "\n\n\t" + + _L("Do you want to select default filaments for these FFF printer models?"), + printer_models_without_material, + T_FFF); + else + select_default_materials_for_printer_model(**printer_models_without_material.begin(), T_FFF); + return false; + } + } + + if (any_sla_selected && (technology & T_SLA)) { + std::set printer_models_without_material = printer_models_missing_materials(ptSLA, AppConfig::SECTION_MATERIALS); + if (! printer_models_without_material.empty()) { + if (only_for_model_id.empty()) + ask_and_select_default_materials( + _L("The following SLA printer models have no materials selected:") + + "\n\n\t" + + printer_model_list(printer_models_without_material) + + "\n\n\t" + + _L("Do you want to select default SLA materials for these printer models?"), + printer_models_without_material, + T_SLA); + else + select_default_materials_for_printer_model(**printer_models_without_material.begin(), T_SLA); + return false; + } + } + + return true; +} + +static std::set get_new_added_presets(const std::map& old_data, const std::map& new_data) +{ + auto get_aliases = [](const std::map& data) { + std::set old_aliases; + for (auto item : data) { + const std::string& name = item.first; + size_t pos = name.find("@"); + old_aliases.emplace(pos == std::string::npos ? name : name.substr(0, pos-1)); + } + return old_aliases; + }; + + std::set old_aliases = get_aliases(old_data); + std::set new_aliases = get_aliases(new_data); + std::set diff; + std::set_difference(new_aliases.begin(), new_aliases.end(), old_aliases.begin(), old_aliases.end(), std::inserter(diff, diff.begin())); + + return diff; +} + +static std::string get_first_added_preset(const std::map& old_data, const std::map& new_data) +{ + std::set diff = get_new_added_presets(old_data, new_data); + if (diff.empty()) + return std::string(); + return *diff.begin(); +} + +bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater, bool& apply_keeped_changes) +{ + wxString header, caption = _L("Configuration is edited in ConfigWizard"); + const auto enabled_vendors = appconfig_new.vendors(); + const auto enabled_vendors_old = app_config->vendors(); + + bool suppress_sla_printer = model_has_multi_part_objects(wxGetApp().model()); + PrinterTechnology preferred_pt = ptAny; + auto get_preferred_printer_technology = [enabled_vendors, enabled_vendors_old, suppress_sla_printer](const std::string& bundle_name, const Bundle& bundle) { + const auto config = enabled_vendors.find(bundle_name); + PrinterTechnology pt = ptAny; + if (config != enabled_vendors.end()) { + for (const auto& model : bundle.vendor_profile->models) { + if (const auto model_it = config->second.find(model.id); + model_it != config->second.end() && model_it->second.size() > 0) { + pt = model.technology; + const auto config_old = enabled_vendors_old.find(bundle_name); + if (config_old == enabled_vendors_old.end() || config_old->second.find(model.id) == config_old->second.end()) { + // if preferred printer model has SLA printer technology it's important to check the model for multi-part state + if (pt == ptSLA && suppress_sla_printer) + continue; + return pt; + } + + if (const auto model_it_old = config_old->second.find(model.id); + model_it_old == config_old->second.end() || model_it_old->second != model_it->second) { + // if preferred printer model has SLA printer technology it's important to check the model for multi-part state + if (pt == ptSLA && suppress_sla_printer) + continue; + return pt; + } + } + } + } + return pt; + }; + // Prusa printers are considered first, then 3rd party. + if (preferred_pt = get_preferred_printer_technology("PrusaResearch", bundles.prusa_bundle()); + preferred_pt == ptAny || (preferred_pt == ptSLA && suppress_sla_printer)) { + for (const auto& bundle : bundles) { + if (bundle.second.is_prusa_bundle) { continue; } + if (PrinterTechnology pt = get_preferred_printer_technology(bundle.first, bundle.second); pt == ptAny) + continue; + else if (preferred_pt == ptAny) + preferred_pt = pt; + if(!(preferred_pt == ptAny || (preferred_pt == ptSLA && suppress_sla_printer))) + break; + } + } + + if (preferred_pt == ptSLA && !wxGetApp().may_switch_to_SLA_preset(caption)) + return false; + + bool check_unsaved_preset_changes = page_welcome->reset_user_profile(); + if (check_unsaved_preset_changes) + header = _L("All user presets will be deleted."); + int act_btns = ActionButtons::KEEP; + if (!check_unsaved_preset_changes) + act_btns |= ActionButtons::SAVE; + + // Install bundles from resources or cache / vendor if needed: + std::vector install_bundles; + for (const auto &pair : bundles) { + if (pair.second.location == BundleLocation::IN_VENDOR) { continue; } + + if (pair.second.is_prusa_bundle) { + // Always install Prusa bundle, because it has a lot of filaments/materials + // likely to be referenced by other profiles. + install_bundles.emplace_back(pair.first); + continue; + } + + const auto vendor = enabled_vendors.find(pair.first); + if (vendor == enabled_vendors.end()) { + // vendor not found + // if templates vendor and needs to be installed, add it + // then continue + if (template_profile_selected && pair.second.vendor_profile && pair.second.vendor_profile->templates_profile) + install_bundles.emplace_back(pair.first); + continue; + } + + size_t size_sum = 0; + for (const auto &model : vendor->second) { size_sum += model.second.size(); } + + if (size_sum > 0) { + // This vendor needs to be installed + install_bundles.emplace_back(pair.first); + } + } + if (!check_unsaved_preset_changes) + if ((check_unsaved_preset_changes = install_bundles.size() > 0)) + header = _L_PLURAL("A new vendor was installed and one of its printers will be activated", "New vendors were installed and one of theirs printers will be activated", install_bundles.size()); + +#ifdef __linux__ + // Desktop integration on Linux + BOOST_LOG_TRIVIAL(debug) << "ConfigWizard::priv::apply_config integrate_desktop" << page_welcome->integrate_desktop() << " perform_registration_linux " << page_downloader->downloader->get_perform_registration_linux(); + if (page_welcome->integrate_desktop() || page_downloader->downloader->get_perform_registration_linux()) + DesktopIntegrationDialog::perform_desktop_integration(page_downloader->downloader->get_perform_registration_linux()); +#endif + + // Decide whether to create snapshot based on run_reason and the reset profile checkbox + bool snapshot = true; + Snapshot::Reason snapshot_reason = Snapshot::SNAPSHOT_UPGRADE; + switch (run_reason) { + case ConfigWizard::RR_DATA_EMPTY: + snapshot = false; + break; + case ConfigWizard::RR_DATA_LEGACY: + snapshot = true; + break; + case ConfigWizard::RR_DATA_INCOMPAT: + // In this case snapshot has already been taken by + // PresetUpdater with the appropriate reason + snapshot = false; + break; + case ConfigWizard::RR_USER: + snapshot = page_welcome->reset_user_profile(); + snapshot_reason = Snapshot::SNAPSHOT_USER; + break; + } + + if (snapshot && ! take_config_snapshot_cancel_on_error(*app_config, snapshot_reason, "", _u8L("Do you want to continue changing the configuration?"))) + return false; + + if (check_unsaved_preset_changes && + !wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + + if (install_bundles.size() > 0) { + // Install bundles from resources or cache / vendor. + // Don't create snapshot - we've already done that above if applicable. + + bool install_result = updater->install_bundles_rsrc_or_cache_vendor(std::move(install_bundles), false); + if (!install_result) + return false; + } else { + BOOST_LOG_TRIVIAL(info) << "No bundles need to be installed from resources or cache / vendor"; + } + + if (page_welcome->reset_user_profile()) { + BOOST_LOG_TRIVIAL(info) << "Resetting user profiles..."; + preset_bundle->reset(true); + } + + std::string preferred_model; + std::string preferred_variant; + auto get_preferred_printer_model = [enabled_vendors, enabled_vendors_old, preferred_pt](const std::string& bundle_name, const Bundle& bundle, std::string& variant) { + const auto config = enabled_vendors.find(bundle_name); + if (config == enabled_vendors.end()) + return std::string(); + for (const auto& model : bundle.vendor_profile->models) { + if (const auto model_it = config->second.find(model.id); + model_it != config->second.end() && model_it->second.size() > 0 && + preferred_pt == model.technology) { + variant = *model_it->second.begin(); + const auto config_old = enabled_vendors_old.find(bundle_name); + if (config_old == enabled_vendors_old.end()) + return model.id; + const auto model_it_old = config_old->second.find(model.id); + if (model_it_old == config_old->second.end()) + return model.id; + else if (model_it_old->second != model_it->second) { + for (const auto& var : model_it->second) + if (model_it_old->second.find(var) == model_it_old->second.end()) { + variant = var; + return model.id; + } + } + } + } + if (!variant.empty()) + variant.clear(); + return std::string(); + }; + // Prusa printers are considered first, then 3rd party. + if (preferred_model = get_preferred_printer_model("PrusaResearch", bundles.prusa_bundle(), preferred_variant); + preferred_model.empty()) { + for (const auto& bundle : bundles) { + if (bundle.second.is_prusa_bundle) { continue; } + if (preferred_model = get_preferred_printer_model(bundle.first, bundle.second, preferred_variant); + !preferred_model.empty()) + break; + } + } + + // if unsaved changes was not cheched till this moment + if (!check_unsaved_preset_changes) { + if ((check_unsaved_preset_changes = !preferred_model.empty())) { + header = _L("A new Printer was installed and it will be activated."); + if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + } + else if ((check_unsaved_preset_changes = enabled_vendors_old != enabled_vendors)) { + header = _L("Some Printers were uninstalled."); + if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + } + } + + std::string first_added_filament, first_added_sla_material; + auto get_first_added_material_preset = [this, app_config](const std::string& section_name, std::string& first_added_preset) { + if (appconfig_new.has_section(section_name)) { + // get first of new added preset names + const std::map& old_presets = app_config->has_section(section_name) ? app_config->get_section(section_name) : std::map(); + first_added_preset = get_first_added_preset(old_presets, appconfig_new.get_section(section_name)); + } + }; + get_first_added_material_preset(AppConfig::SECTION_FILAMENTS, first_added_filament); + get_first_added_material_preset(AppConfig::SECTION_MATERIALS, first_added_sla_material); + + // if unsaved changes was not cheched till this moment + if (!check_unsaved_preset_changes) { + if ((check_unsaved_preset_changes = !first_added_filament.empty() || !first_added_sla_material.empty())) { + header = !first_added_filament.empty() ? + _L("A new filament was installed and it will be activated.") : + _L("A new SLA material was installed and it will be activated."); + if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + } + else { + auto changed = [app_config, &appconfig_new = std::as_const(this->appconfig_new)](const std::string& section_name) { + return (app_config->has_section(section_name) ? app_config->get_section(section_name) : std::map()) != appconfig_new.get_section(section_name); + }; + bool is_filaments_changed = changed(AppConfig::SECTION_FILAMENTS); + bool is_sla_materials_changed = changed(AppConfig::SECTION_MATERIALS); + if ((check_unsaved_preset_changes = is_filaments_changed || is_sla_materials_changed)) { + header = is_filaments_changed ? _L("Some filaments were uninstalled.") : _L("Some SLA materials were uninstalled."); + if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + } + } + } + + // apply materials in app_config + for (const std::string& section_name : {AppConfig::SECTION_FILAMENTS, AppConfig::SECTION_MATERIALS}) + app_config->set_section(section_name, appconfig_new.get_section(section_name)); + + app_config->set_vendors(appconfig_new); + + app_config->set("notify_release", page_update->version_check ? "all" : "none"); + app_config->set("preset_update", page_update->preset_update ? "1" : "0"); + app_config->set("export_sources_full_pathnames", page_reload_from_disk->full_pathnames ? "1" : "0"); + +#ifdef _WIN32 + app_config->set("associate_3mf", page_files_association->associate_3mf() ? "1" : "0"); + app_config->set("associate_stl", page_files_association->associate_stl() ? "1" : "0"); +// app_config->set("associate_gcode", page_files_association->associate_gcode() ? "1" : "0"); + + if (wxGetApp().is_editor()) { + if (page_files_association->associate_3mf()) + wxGetApp().associate_3mf_files(); + if (page_files_association->associate_stl()) + wxGetApp().associate_stl_files(); + } +// else { +// if (page_files_association->associate_gcode()) +// wxGetApp().associate_gcode_files(); +// } +#endif // _WIN32 + + page_mode->serialize_mode(app_config); + + if (check_unsaved_preset_changes) + preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem, + {preferred_model, preferred_variant, first_added_filament, first_added_sla_material}); + + if (!only_sla_mode && page_custom->custom_wanted() && page_custom->is_valid_profile_name()) { + // if unsaved changes was not cheched till this moment + if (!check_unsaved_preset_changes && + !wxGetApp().check_and_keep_current_preset_changes(caption, _L("Custom printer was installed and it will be activated."), act_btns, &apply_keeped_changes)) + return false; + + page_firmware->apply_custom_config(*custom_config); + page_bed->apply_custom_config(*custom_config); + page_bvolume->apply_custom_config(*custom_config); + page_diams->apply_custom_config(*custom_config); + page_temps->apply_custom_config(*custom_config); + + copy_bed_model_and_texture_if_needed(*custom_config); + + const std::string profile_name = page_custom->profile_name(); + preset_bundle->load_config_from_wizard(profile_name, *custom_config); + } + + // Update the selections from the compatibilty. + preset_bundle->export_selections(*app_config); + + return true; +} +void ConfigWizard::priv::update_presets_in_config(const std::string& section, const std::string& alias_key, bool add) +{ + const PresetAliases& aliases = section == AppConfig::SECTION_FILAMENTS ? aliases_fff : aliases_sla; + + auto update = [this, add](const std::string& s, const std::string& key) { + assert(! s.empty()); + if (add) + appconfig_new.set(s, key, "1"); + else + appconfig_new.erase(s, key); + }; + + // add or delete presets had a same alias + auto it = aliases.find(alias_key); + if (it != aliases.end()) + for (const std::string& name : it->second) + update(section, name); +} + +bool ConfigWizard::priv::check_fff_selected() +{ + bool ret = page_fff->any_selected(); + for (const auto& printer: pages_3rdparty) + if (printer.second.first) // FFF page + ret |= printer.second.first->any_selected(); + return ret; +} + +bool ConfigWizard::priv::check_sla_selected() +{ + bool ret = page_msla->any_selected(); + for (const auto& printer: pages_3rdparty) + if (printer.second.second) // SLA page + ret |= printer.second.second->any_selected(); + return ret; +} + + +// Public + +ConfigWizard::ConfigWizard(wxWindow *parent) + : DPIDialog(parent, wxID_ANY, wxString(SLIC3R_APP_NAME) + " - " + _(name()), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) + , p(new priv(this)) +{ + this->SetFont(wxGetApp().normal_font()); + + p->load_vendors(); + p->custom_config.reset(DynamicPrintConfig::new_from_defaults_keys({ + "gcode_flavor", "bed_shape", "bed_custom_texture", "bed_custom_model", "nozzle_diameter", "filament_diameter", "temperature", "bed_temperature", + })); + + p->index = new ConfigWizardIndex(this); + + auto *vsizer = new wxBoxSizer(wxVERTICAL); + auto *topsizer = new wxBoxSizer(wxHORIZONTAL); + auto* hline = new StaticLine(this); + p->btnsizer = new wxBoxSizer(wxHORIZONTAL); + + // Initially we _do not_ SetScrollRate in order to figure out the overall width of the Wizard without scrolling. + // Later, we compare that to the size of the current screen and set minimum width based on that (see below). + p->hscroll = new wxScrolledWindow(this); + p->hscroll_sizer = new wxBoxSizer(wxHORIZONTAL); + p->hscroll->SetSizer(p->hscroll_sizer); + + topsizer->Add(p->index, 0, wxEXPAND); + topsizer->AddSpacer(INDEX_MARGIN); + topsizer->Add(p->hscroll, 1, wxEXPAND); + + p->btn_sel_all = new wxButton(this, wxID_ANY, _L("Select all standard printers")); + p->btnsizer->Add(p->btn_sel_all); + + p->btn_prev = new wxButton(this, wxID_ANY, _L("< &Back")); + p->btn_next = new wxButton(this, wxID_ANY, _L("&Next >")); + p->btn_finish = new wxButton(this, wxID_APPLY, _L("&Finish")); + p->btn_cancel = new wxButton(this, wxID_CANCEL, _L("Cancel")); // Note: The label needs to be present, otherwise we get accelerator bugs on Mac + p->btnsizer->AddStretchSpacer(); + p->btnsizer->Add(p->btn_prev, 0, wxLEFT, BTN_SPACING); + p->btnsizer->Add(p->btn_next, 0, wxLEFT, BTN_SPACING); + p->btnsizer->Add(p->btn_finish, 0, wxLEFT, BTN_SPACING); + p->btnsizer->Add(p->btn_cancel, 0, wxLEFT, BTN_SPACING); + + wxGetApp().UpdateDarkUI(p->btn_sel_all); + wxGetApp().UpdateDarkUI(p->btn_prev); + wxGetApp().UpdateDarkUI(p->btn_next); + wxGetApp().UpdateDarkUI(p->btn_finish); + wxGetApp().UpdateDarkUI(p->btn_cancel); + + const auto prusa_it = p->bundles.find("PrusaResearch"); + wxCHECK_RET(prusa_it != p->bundles.cend(), "Vendor PrusaResearch not found"); + const VendorProfile *vendor_prusa = prusa_it->second.vendor_profile; + + p->add_page(p->page_welcome = new PageWelcome(this)); + + + p->page_fff = new PagePrinters(this, _L("Prusa FFF Technology Printers"), "Prusa FFF", *vendor_prusa, 0, T_FFF); + p->only_sla_mode = !p->page_fff->has_printers; + if (!p->only_sla_mode) { + p->add_page(p->page_fff); + p->page_fff->is_primary_printer_page = true; + } + + + p->page_msla = new PagePrinters(this, _L("Prusa MSLA Technology Printers"), "Prusa MSLA", *vendor_prusa, 0, T_SLA); + p->add_page(p->page_msla); + if (p->only_sla_mode) { + p->page_msla->is_primary_printer_page = true; + } + + if (!p->only_sla_mode) { + // Pages for 3rd party vendors + p->create_3rdparty_pages(); // Needs to be done _before_ creating PageVendors + p->add_page(p->page_vendors = new PageVendors(this)); + p->add_page(p->page_custom = new PageCustom(this)); + p->custom_printer_selected = p->page_custom->custom_wanted(); + } + + p->any_sla_selected = p->check_sla_selected(); + p->any_fff_selected = ! p->only_sla_mode && p->check_fff_selected(); + + p->update_materials(T_ANY); + if (!p->only_sla_mode) + p->add_page(p->page_filaments = new PageMaterials(this, &p->filaments, + _L("Filament Profiles Selection"), _L("Filaments"), _L("Type:") )); + + p->add_page(p->page_sla_materials = new PageMaterials(this, &p->sla_materials, + _L("SLA Material Profiles Selection") + " ", _L("SLA Materials"), _L("Type:") )); + + + p->add_page(p->page_update = new PageUpdate(this)); + p->add_page(p->page_downloader = new PageDownloader(this)); + p->add_page(p->page_reload_from_disk = new PageReloadFromDisk(this)); +#ifdef _WIN32 + p->add_page(p->page_files_association = new PageFilesAssociation(this)); +#endif // _WIN32 + p->add_page(p->page_mode = new PageMode(this)); + p->add_page(p->page_firmware = new PageFirmware(this)); + p->add_page(p->page_bed = new PageBedShape(this)); + p->add_page(p->page_bvolume = new PageBuildVolume(this)); + p->add_page(p->page_diams = new PageDiameters(this)); + p->add_page(p->page_temps = new PageTemperatures(this)); + + p->load_pages(); + p->index->go_to(size_t{0}); + + vsizer->Add(topsizer, 1, wxEXPAND | wxALL, DIALOG_MARGIN); + vsizer->Add(hline, 0, wxEXPAND | wxLEFT | wxRIGHT, VERTICAL_SPACING); + vsizer->Add(p->btnsizer, 0, wxEXPAND | wxALL, DIALOG_MARGIN); + + SetSizer(vsizer); + SetSizerAndFit(vsizer); + + // We can now enable scrolling on hscroll + p->hscroll->SetScrollRate(30, 30); + + on_window_geometry(this, [this]() { + p->init_dialog_size(); + }); + + p->btn_prev->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { this->p->index->go_prev(); }); + + p->btn_next->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) + { + // check, that there is selected at least one filament/material + ConfigWizardPage* active_page = this->p->index->active_page(); + if (// Leaving the filaments or SLA materials page and + (active_page == p->page_filaments || active_page == p->page_sla_materials) && + // some Printer models had no filament or SLA material selected. + ! p->check_and_install_missing_materials(dynamic_cast(active_page)->materials->technology)) + // In that case don't leave the page and the function above queried the user whether to install default materials. + return; + this->p->index->go_next(); + }); + + p->btn_finish->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) + { + if (p->on_bnt_finish()) + this->EndModal(wxID_OK); + }); + + p->btn_sel_all->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { + p->any_sla_selected = true; + p->load_pages(); + p->page_fff->select_all(true, false); + p->page_msla->select_all(true, false); + p->index->go_to(p->page_mode); + }); + + p->index->Bind(EVT_INDEX_PAGE, [this](const wxCommandEvent &) { + const bool is_last = p->index->active_is_last(); + p->btn_next->Show(! is_last); + if (is_last) + p->btn_finish->SetFocus(); + + Layout(); + }); + + if (wxLinux_gtk3) + this->Bind(wxEVT_SHOW, [this, vsizer](const wxShowEvent& e) { + ConfigWizardPage* active_page = p->index->active_page(); + if (!active_page) + return; + for (auto page : p->all_pages) + if (page != active_page) + page->Hide(); + // update best size for the dialog after hiding of the non-active pages + vsizer->SetSizeHints(this); + // set initial dialog size + p->init_dialog_size(); + }); +} + +ConfigWizard::~ConfigWizard() {} + +bool ConfigWizard::run(RunReason reason, StartPage start_page) +{ + BOOST_LOG_TRIVIAL(info) << boost::format("Running ConfigWizard, reason: %1%, start_page: %2%") % reason % start_page; + + GUI_App &app = wxGetApp(); + + p->set_run_reason(reason); + p->set_start_page(start_page); + + if (ShowModal() == wxID_OK) { + bool apply_keeped_changes = false; + if (! p->apply_config(app.app_config, app.preset_bundle, app.preset_updater, apply_keeped_changes)) + return false; + + if (apply_keeped_changes) + app.apply_keeped_preset_modifications(); + + app.app_config->set_legacy_datadir(false); + app.update_mode(); + app.obj_manipul()->update_ui_from_settings(); + BOOST_LOG_TRIVIAL(info) << "ConfigWizard applied"; + return true; + } else { + BOOST_LOG_TRIVIAL(info) << "ConfigWizard cancelled"; + return false; + } +} + +const wxString& ConfigWizard::name(const bool from_menu/* = false*/) +{ + // A different naming convention is used for the Wizard on Windows & GTK vs. OSX. + // Note: Don't call _() macro here. + // This function just return the current name according to the OS. + // Translation is implemented inside GUI_App::add_config_menu() +#if __APPLE__ + static const wxString config_wizard_name = L("Configuration Assistant"); + static const wxString config_wizard_name_menu = L("Configuration &Assistant"); +#else + static const wxString config_wizard_name = L("Configuration Wizard"); + static const wxString config_wizard_name_menu = L("Configuration &Wizard"); +#endif + return from_menu ? config_wizard_name_menu : config_wizard_name; +} + +void ConfigWizard::on_dpi_changed(const wxRect &suggested_rect) +{ + p->index->msw_rescale(); + + const int em = em_unit(); + + msw_buttons_rescale(this, em, { wxID_APPLY, + wxID_CANCEL, + p->btn_sel_all->GetId(), + p->btn_next->GetId(), + p->btn_prev->GetId() }); + + for (auto printer_picker: p->page_fff->printer_pickers) + msw_buttons_rescale(this, em, printer_picker->get_button_indexes()); + + p->init_dialog_size(); + + Refresh(); +} + +void ConfigWizard::on_sys_color_changed() +{ + wxGetApp().UpdateDlgDarkUI(this); + Refresh(); +} + +} +} diff --git a/src/slic3r/GUI/ConfigWizard_private.hpp b/src/slic3r/GUI/ConfigWizard_private.hpp index a8ac09d9b1..17c9fb25fb 100644 --- a/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/src/slic3r/GUI/ConfigWizard_private.hpp @@ -60,18 +60,25 @@ enum Technology { T_ANY = ~0, }; +enum BundleLocation{ + IN_VENDOR, + IN_ARCHIVE, + IN_RESOURCES +}; + struct Bundle { std::unique_ptr preset_bundle; VendorProfile* vendor_profile{ nullptr }; - bool is_in_resources{ false }; + //bool is_in_resources{ false }; + BundleLocation location; bool is_prusa_bundle{ false }; Bundle() = default; Bundle(Bundle&& other); // Returns false if not loaded. Reason for that is logged as boost::log error. - bool load(fs::path source_path, bool is_in_resources, bool is_prusa_bundle = false); + bool load(fs::path source_path, BundleLocation location, bool is_prusa_bundle = false); const std::string& vendor_id() const { return vendor_profile->id; } }; @@ -84,74 +91,8 @@ struct BundleMap : std::map const Bundle& prusa_bundle() const; }; -struct Materials -{ - Technology technology; - // use vector for the presets to purpose of save of presets sorting in the bundle - std::vector presets; - // String is alias of material, size_t number of compatible counters - std::vector> compatibility_counter; - std::set types; - std::set printers; +struct Materials; - Materials(Technology technology) : technology(technology) {} - - void push(const Preset *preset); - void add_printer(const Preset* preset); - void clear(); - bool containts(const Preset *preset) const { - //return std::find(presets.begin(), presets.end(), preset) != presets.end(); - return std::find_if(presets.begin(), presets.end(), - [preset](const Preset* element) { return element == preset; }) != presets.end(); - - } - - bool get_omnipresent(const Preset* preset) { - return get_printer_counter(preset) == printers.size(); - } - - const std::vector get_presets_by_alias(const std::string name) { - std::vector ret_vec; - for (auto it = presets.begin(); it != presets.end(); ++it) { - if ((*it)->alias == name) - ret_vec.push_back((*it)); - } - return ret_vec; - } - - - - size_t get_printer_counter(const Preset* preset) { - for (auto it : compatibility_counter) { - if (it.first == preset->alias) - return it.second; - } - return 0; - } - - const std::string& appconfig_section() const; - const std::string& get_type(const Preset *preset) const; - const std::string& get_vendor(const Preset *preset) const; - - template void filter_presets(const Preset* printer, const std::string& type, const std::string& vendor, F cb) { - for (auto preset : presets) { - const Preset& prst = *(preset); - const Preset& prntr = *printer; - if ((printer == nullptr || is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) && - (type.empty() || get_type(preset) == type) && - (vendor.empty() || get_vendor(preset) == vendor)) { - - cb(preset); - } - } - } - - static const std::string UNKNOWN; - static const std::string& get_filament_type(const Preset *preset); - static const std::string& get_filament_vendor(const Preset *preset); - static const std::string& get_material_type(const Preset *preset); - static const std::string& get_material_vendor(const Preset *preset); -}; struct PrinterPickerEvent; @@ -344,6 +285,10 @@ struct PageMaterials: ConfigWizardPage std::string empty_printers_label; bool first_paint = { false }; static const std::string EMPTY; + static const std::string TEMPLATES; + // notify user first time they choose template profile + bool template_shown = { false }; + bool notification_shown = { false }; int last_hovered_item = { -1 } ; PageMaterials(ConfigWizard *parent, Materials *materials, wxString title, wxString shortname, wxString list1name); @@ -368,6 +313,82 @@ struct PageMaterials: ConfigWizardPage virtual void on_activate() override; }; +struct Materials +{ + Technology technology; + // use vector for the presets to purpose of save of presets sorting in the bundle + std::vector presets; + // String is alias of material, size_t number of compatible counters + std::vector> compatibility_counter; + std::set types; + std::set printers; + + Materials(Technology technology) : technology(technology) {} + + void push(const Preset* preset); + void add_printer(const Preset* preset); + void clear(); + bool containts(const Preset* preset) const { + //return std::find(presets.begin(), presets.end(), preset) != presets.end(); + return std::find_if(presets.begin(), presets.end(), + [preset](const Preset* element) { return element == preset; }) != presets.end(); + + } + + bool get_omnipresent(const Preset* preset) { + return get_printer_counter(preset) == printers.size(); + } + + const std::vector get_presets_by_alias(const std::string name) { + std::vector ret_vec; + for (auto it = presets.begin(); it != presets.end(); ++it) { + if ((*it)->alias == name) + ret_vec.push_back((*it)); + } + return ret_vec; + } + + + + size_t get_printer_counter(const Preset* preset) { + for (auto it : compatibility_counter) { + if (it.first == preset->alias) + return it.second; + } + return 0; + } + + const std::string& appconfig_section() const; + const std::string& get_type(const Preset* preset) const; + const std::string& get_vendor(const Preset* preset) const; + + template void filter_presets(const Preset* printer, const std::string& printer_name, const std::string& type, const std::string& vendor, F cb) { + for (auto preset : presets) { + const Preset& prst = *(preset); + const Preset& prntr = *printer; + if (((printer == nullptr && printer_name == PageMaterials::EMPTY) || (printer != nullptr && is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor)))) && + (type.empty() || get_type(preset) == type) && + (vendor.empty() || get_vendor(preset) == vendor) && + prst.vendor && !prst.vendor->templates_profile) { + + cb(preset); + } + else if ((printer == nullptr && printer_name == PageMaterials::TEMPLATES) && prst.vendor && prst.vendor->templates_profile && + (type.empty() || get_type(preset) == type) && + (vendor.empty() || get_vendor(preset) == vendor)) { + cb(preset); + } + } + } + + static const std::string UNKNOWN; + static const std::string& get_filament_type(const Preset* preset); + static const std::string& get_filament_vendor(const Preset* preset); + static const std::string& get_material_type(const Preset* preset); + static const std::string& get_material_vendor(const Preset* preset); +}; + + struct PageCustom: ConfigWizardPage { PageCustom(ConfigWizard *parent); @@ -608,9 +629,11 @@ struct ConfigWizard::priv std::unique_ptr custom_config; // Backing for custom printer definition bool any_fff_selected; // Used to decide whether to display Filaments page bool any_sla_selected; // Used to decide whether to display SLA Materials page - bool custom_printer_selected { false }; + bool custom_printer_selected { false }; // New custom printer is requested + bool custom_printer_in_bundle { false }; // Older custom printer already exists when wizard starts // Set to true if there are none FFF printers on the main FFF page. If true, only SLA printers are shown (not even custum printers) bool only_sla_mode { false }; + bool template_profile_selected { false }; // This bool has one purpose - to tell that template profile should be installed if its not (because it cannot be added to appconfig) wxScrolledWindow *hscroll = nullptr; wxBoxSizer *hscroll_sizer = nullptr; diff --git a/src/slic3r/GUI/DownloaderFileGet.cpp b/src/slic3r/GUI/DownloaderFileGet.cpp index deac45ad55..2e591a75a0 100644 --- a/src/slic3r/GUI/DownloaderFileGet.cpp +++ b/src/slic3r/GUI/DownloaderFileGet.cpp @@ -163,6 +163,8 @@ void FileGet::priv::get_perform() m_stopped = true; fclose(file); cancel = true; + if (m_written == 0) + std::remove(m_tmp_path.string().c_str()); wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_PAUSED); evt->SetInt(m_id); m_evt_handler->QueueEvent(evt); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 1577d96ea0..d5b2679275 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -808,6 +808,7 @@ void GUI_App::post_init() // Configuration is not compatible and reconfigure was refused by the user. Application is closing. return; CallAfter([this] { + // preset_updater->sync downloads profile updates on background so it must begin after config wizard finished. bool cw_showed = this->config_wizard_startup(); this->preset_updater->sync(preset_bundle); this->app_version_check(false); @@ -1248,7 +1249,7 @@ bool GUI_App::on_init_inner() std::string evt_string = into_u8(evt.GetString()); if (*Semver::parse(SLIC3R_VERSION) < *Semver::parse(evt_string)) { auto notif_type = (evt_string.find("beta") != std::string::npos ? NotificationType::NewBetaAvailable : NotificationType::NewAlphaAvailable); - this->plater_->get_notification_manager()->push_notification( notif_type + this->plater_->get_notification_manager()->push_version_notification( notif_type , NotificationManager::NotificationLevel::ImportantNotificationLevel , Slic3r::format(_u8L("New prerelease version %1% is available."), evt_string) , _u8L("See Releases page.") @@ -1281,7 +1282,8 @@ bool GUI_App::on_init_inner() associate_gcode_files(); #endif // __WXMSW__ } - + + std::string delayed_error_load_presets; // Suppress the '- default -' presets. preset_bundle->set_default_suppressed(app_config->get("no_defaults") == "1"); try { @@ -1290,7 +1292,7 @@ bool GUI_App::on_init_inner() // installation of a compatible system preset, thus nullifying the system preset substitutions. init_params->preset_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSystemSilent); } catch (const std::exception &ex) { - show_error(nullptr, ex.what()); + delayed_error_load_presets = ex.what(); } #ifdef WIN32 @@ -1307,6 +1309,9 @@ bool GUI_App::on_init_inner() if (scrn && is_editor()) scrn->SetText(_L("Preparing settings tabs") + dots); + if (!delayed_error_load_presets.empty()) + show_error(nullptr, delayed_error_load_presets); + mainframe = new MainFrame(); // hide settings tabs after first Layout if (is_editor()) @@ -3007,6 +3012,9 @@ bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); if (reason == ConfigWizard::RR_USER) { + // Cancel sync before starting wizard to prevent two downloads at same time + preset_updater->cancel_sync(); + preset_updater->update_index_db(); if (preset_updater->config_update(app_config->orig_version(), PresetUpdater::UpdateParams::FORCED_BEFORE_WIZARD) == PresetUpdater::R_ALL_CANCELED) return false; } @@ -3189,6 +3197,7 @@ bool GUI_App::check_updates(const bool verbose) { PresetUpdater::UpdateResult updater_result; try { + preset_updater->update_index_db(); updater_result = preset_updater->config_update(app_config->orig_version(), verbose ? PresetUpdater::UpdateParams::SHOW_TEXT_BOX : PresetUpdater::UpdateParams::SHOW_NOTIFICATION); if (updater_result == PresetUpdater::R_INCOMPAT_EXIT) { mainframe->Close(); @@ -3300,6 +3309,19 @@ void GUI_App::on_version_read(wxCommandEvent& evt) return; } if (*Semver::parse(SLIC3R_VERSION) >= *Semver::parse(into_u8(evt.GetString()))) { + if (m_app_updater->get_triggered_by_user()) + { + std::string text = (*Semver::parse(into_u8(evt.GetString())) == Semver()) + ? Slic3r::format(_u8L("Check for application update has failed.")) + : Slic3r::format(_u8L("No new version is available. Latest release version is %1%."), evt.GetString()); + + this->plater_->get_notification_manager()->push_version_notification(NotificationType::NoNewReleaseAvailable + , NotificationManager::NotificationLevel::RegularNotificationLevel + , text + , std::string() + , std::function() + ); + } return; } // notification diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 6dece9a224..3e01de834f 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -558,8 +558,8 @@ void MenuFactory::append_menu_items_add_volume(MenuType menu_type) menu->Destroy(range_id); if (wxGetApp().get_mode() == comSimple) { - append_menu_item_add_text(menu, ModelVolumeType::MODEL_PART, false); - append_menu_item_add_text(menu, ModelVolumeType::NEGATIVE_VOLUME, false); + //append_menu_item_add_text(menu, ModelVolumeType::MODEL_PART, false); + //append_menu_item_add_text(menu, ModelVolumeType::NEGATIVE_VOLUME, false); append_menu_item(menu, wxID_ANY, _(ADD_VOLUME_MENU_ITEMS[int(ModelVolumeType::SUPPORT_ENFORCER)].first), "", [](wxCommandEvent&) { obj_list()->load_generic_subobject(L("Box"), ModelVolumeType::SUPPORT_ENFORCER); }, diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 0b03ecabe2..5529c34b99 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -2480,6 +2480,9 @@ bool ObjectList::is_splittable(bool to_objects) return false; } + if (wxGetApp().get_mode() == comSimple) + return false; // suppress to split to parts for simple mode + ModelVolume* volume; if (!get_volume_by_item(item, volume) || !volume) return false; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp index e032d924e8..4d7685eeb9 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp @@ -47,7 +47,6 @@ #define ALLOW_ADD_FONT_BY_OS_SELECTOR #define SHOW_WX_FONT_DESCRIPTOR // OS specific descriptor | file path --> in edit style #define SHOW_FONT_FILE_PROPERTY // ascent, descent, line gap, cache --> in advanced -#define SHOW_FONT_COUNT // count of enumerated font --> in font combo box #define SHOW_CONTAIN_3MF_FIX // when contain fix matrix --> show gray '3mf' next to close button #define SHOW_OFFSET_DURING_DRAGGING // when drag with text over surface visualize used center #define SHOW_IMGUI_ATLAS @@ -403,7 +402,7 @@ Transform3d priv::world_matrix(const GLVolume *gl_volume, const Model *model) if (!fix.has_value()) return res; - return res * (*fix); + return res * fix->inverse(); } Transform3d priv::world_matrix(const Selection &selection) @@ -861,6 +860,9 @@ void GLGizmoEmboss::on_set_state() if (m_volume == nullptr) { // reopen gizmo when new object is created GLGizmoBase::m_state = GLGizmoBase::Off; + if (wxGetApp().get_mode() == comSimple) + // It's impossible to add a part in simple mode + return; // start creating new object create_volume(ModelVolumeType::MODEL_PART); } @@ -1830,6 +1832,9 @@ void GLGizmoEmboss::init_font_name_texture() { face.cancel = nullptr; face.is_created = nullptr; } + + // Prepare filtration cache + m_face_names.hide = std::vector(m_face_names.faces.size(), {false}); } void GLGizmoEmboss::draw_font_preview(FaceName& face, bool is_visible) @@ -1960,13 +1965,60 @@ void GLGizmoEmboss::draw_font_list() }; } + // Code + const char *popup_id = "##font_list_popup"; + const char *input_id = "##font_list_input"; ImGui::SetNextItemWidth(m_gui_cfg->input_width); - if (ImGui::BeginCombo("##font_selector", selected)) { - if (!m_face_names.is_init) init_face_names(); - if (m_face_names.texture_id == 0) init_font_name_texture(); + ImGuiInputTextFlags input_flags = ImGuiInputTextFlags_CharsUppercase; + + // change color of hint to normal text + bool is_popup_open = ImGui::IsPopupOpen(popup_id); + if (!is_popup_open) + ImGui::PushStyleColor(ImGuiCol_TextDisabled, ImGui::GetStyleColorVec4(ImGuiCol_Text)); + if (ImGui::InputTextWithHint(input_id, selected, &m_face_names.search, input_flags)) { + // update filtration result + m_face_names.hide = std::vector(m_face_names.faces.size(), {false}); + for (FaceName &face : m_face_names.faces) { + size_t index = &face - &m_face_names.faces.front(); + std::string name(face.wx_name.ToUTF8().data()); + std::transform(name.begin(), name.end(), name.begin(), ::toupper); + + // It should use C++ 20 feature https://en.cppreference.com/w/cpp/string/basic_string/starts_with + bool start_with = boost::starts_with(name, m_face_names.search); + m_face_names.hide[index] = !start_with; + } + } + if (!is_popup_open) + ImGui::PopStyleColor(); // revert changes for hint color + + const bool is_input_text_active = ImGui::IsItemActive(); + + // is_input_text_activated + if (ImGui::IsItemActivated()) + ImGui::OpenPopup(popup_id); + + ImGui::SetNextWindowPos(ImVec2(ImGui::GetItemRectMin().x, ImGui::GetItemRectMax().y)); + ImGui::SetNextWindowSize({2*m_gui_cfg->input_width, ImGui::GetTextLineHeight()*10}); + ImGuiWindowFlags popup_flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_ChildWindow; + if (ImGui::BeginPopup(popup_id, popup_flags)) + { + bool set_selection_focus = false; + if (!m_face_names.is_init) { + init_face_names(); + set_selection_focus = true; + } + if (m_face_names.texture_id == 0) + init_font_name_texture(); + for (FaceName &face : m_face_names.faces) { const wxString &wx_face_name = face.wx_name; size_t index = &face - &m_face_names.faces.front(); + + // Filter for face names + if (m_face_names.hide[index]) + continue; + ImGui::PushID(index); ScopeGuard sg([]() { ImGui::PopID(); }); bool is_selected = (actual_face_name == wx_face_name); @@ -1978,24 +2030,32 @@ void GLGizmoEmboss::draw_font_list() wxMessageBox(GUI::format(_L("Font face \"%1%\" can't be selected."), wx_face_name)); } } + // tooltip as full name of font face if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", wx_face_name.ToUTF8().data()); - if (is_selected) ImGui::SetItemDefaultFocus(); - draw_font_preview(face, ImGui::IsItemVisible()); - } -#ifdef SHOW_FONT_COUNT - ImGui::TextColored(ImGuiWrapper::COL_GREY_LIGHT, "Count %d", - static_cast(m_face_names.names.size())); -#endif // SHOW_FONT_COUNT - ImGui::EndCombo(); - } else if (m_face_names.is_init) { - // Just one after close combo box - // free texture and set id to zero + // on first draw set focus on selected font + if (set_selection_focus && is_selected) + ImGui::SetScrollHereY(); + draw_font_preview(face, ImGui::IsItemVisible()); + } + + if (!ImGui::IsWindowFocused() || + !is_input_text_active && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape))) { + // closing of popup + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } else if (m_face_names.is_init) { + // Just one after close combo box + // free texture and set id to zero m_face_names.is_init = false; + m_face_names.search.clear(); + m_face_names.hide.clear(); // cancel all process for generation of texture for (FaceName &face : m_face_names.faces) - if (face.cancel != nullptr) face.cancel->store(true); + if (face.cancel != nullptr) + face.cancel->store(true); glsafe(::glDeleteTextures(1, &m_face_names.texture_id)); m_face_names.texture_id = 0; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp index 6340b6faeb..2fabf0566f 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp @@ -270,6 +270,10 @@ private: // hash created from enumerated font from OS // check when new font was installed size_t hash = 0; + + // filtration pattern + std::string search = ""; + std::vector hide; // result of filtration } m_face_names; static bool store(const Facenames &facenames); static bool load(Facenames &facenames); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 9b06e53408..6e12527d58 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -517,14 +517,6 @@ bool GLGizmoFdmSupports::has_backend_supports() return done; } -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(); @@ -549,8 +541,10 @@ void GLGizmoFdmSupports::auto_generate() } } - this->waiting_for_autogenerated_supports = true; - wxGetApp().CallAfter([this]() { reslice_FDM_supports(); }); + wxGetApp().CallAfter([this]() { + wxGetApp().plater()->reslice_FFF_until_step(posSupportSpotsSearch, *m_c->selection_info()->model_object(), false); + this->waiting_for_autogenerated_supports = true; + }); } } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp index fb20181d87..b79e1dda7e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp @@ -44,7 +44,6 @@ private: bool waiting_for_autogenerated_supports = false; bool has_backend_supports(); - void reslice_FDM_supports(bool postpone_error_messages = false) const; void auto_generate(); void apply_data_from_backend(); }; diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index e6f9d952a6..e8fd68c81f 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -2246,6 +2246,29 @@ void NotificationManager::push_simplify_suggestion_notification(const std::stri notification->object_id = object_id; push_notification_data(std::move(notification), 0); } +void NotificationManager::push_version_notification(NotificationType type, NotificationLevel level, const std::string& text, const std::string& hypertext, std::function callback) +{ + assert (type == NotificationType::NewAlphaAvailable + || type == NotificationType::NewBetaAvailable + || type == NotificationType::NoNewReleaseAvailable); + + for (std::unique_ptr& notification : m_pop_notifications) { + // NoNewReleaseAvailable must not show if alfa / beta is on. + NotificationType nttype = notification->get_type(); + if (type == NotificationType::NoNewReleaseAvailable + && (notification->get_type() == NotificationType::NewAlphaAvailable + || notification->get_type() == NotificationType::NewBetaAvailable)) { + return; + } + // NoNewReleaseAvailable must close if alfa / beta is being push. + if (notification->get_type() == NotificationType::NoNewReleaseAvailable + && (type == NotificationType::NewAlphaAvailable + || type == NotificationType::NewBetaAvailable)) { + notification->close(); + } + } + push_notification(type, level, text, hypertext, callback); +} void NotificationManager::close_notification_of_type(const NotificationType type) { for (std::unique_ptr ¬ification : m_pop_notifications) { diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 7cd77a3048..b3a39a936f 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -56,6 +56,7 @@ enum class NotificationType // Like NewAppAvailable but with text and link for alpha / bet release NewAlphaAvailable, NewBetaAvailable, + NoNewReleaseAvailable, // Notification on the start of PrusaSlicer, when updates of system profiles are detected. // Contains a hyperlink to execute installation of the new system profiles. PresetUpdateAvailable, @@ -191,6 +192,9 @@ public: // Object warning with ObjectID, closes when object is deleted. ID used is of object not print like in slicing warning. void push_simplify_suggestion_notification(const std::string& text, ObjectID object_id, const std::string& hypertext = "", std::function callback = std::function()); + // Could be either NewAlphaAvailable, NewBetaAvailable or NoNewReleaseAvailable - this function only makes sure only 1 is visible. + void push_version_notification(NotificationType type, NotificationLevel level, const std::string& text, const std::string& hypertext, + std::function callback); // Close object warnings, whose ObjectID is not in the list. // living_oids is expected to be sorted. void remove_simplify_suggestion_of_released_objects(const std::vector& living_oids); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 3b9fa6042d..673c3892f4 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -553,12 +553,12 @@ FreqChangedParams::FreqChangedParams(wxWindow* parent) : const bool supports_enable = selection == _("None") ? false : true; new_conf.set_key_value("supports_enable", new ConfigOptionBool(supports_enable)); - new_conf.set_key_value("support_enforcers_only", new ConfigOptionBool(false)); + std::string treetype = get_sla_suptree_prefix(new_conf); if (selection == _("Everywhere")) - new_conf.set_key_value("support_buildplate_only", new ConfigOptionBool(false)); + new_conf.set_key_value(treetype + "support_buildplate_only", new ConfigOptionBool(false)); else if (selection == _("Support on build plate only")) - new_conf.set_key_value("support_buildplate_only", new ConfigOptionBool(true)); + new_conf.set_key_value(treetype + "support_buildplate_only", new ConfigOptionBool(true)); else if (selection == _("For support enforcers only")) { new_conf.set_key_value("support_enforcers_only", new ConfigOptionBool(true)); } @@ -2643,7 +2643,6 @@ std::vector Plater::priv::load_files(const std::vector& input_ } if (model.looks_like_multipart_object()) { - //wxMessageDialog msg_dlg(q, _L( MessageDialog msg_dlg(q, _L( "This file contains several objects positioned at multiple heights.\n" "Instead of considering them as multiple objects, should \n" @@ -2654,10 +2653,9 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } } - else if ((wxGetApp().get_mode() == comSimple) && (type_3mf || type_any_amf) && model_has_advanced_features(model)) { - //wxMessageDialog msg_dlg(q, _L("This file cannot be loaded in a simple mode. Do you want to switch to an advanced mode?")+"\n", + if ((wxGetApp().get_mode() == comSimple) && (type_3mf || type_any_amf) && model_has_advanced_features(model)) { MessageDialog msg_dlg(q, _L("This file cannot be loaded in a simple mode. Do you want to switch to an advanced mode?")+"\n", - _L("Detected advanced data"), wxICON_WARNING | wxYES | wxNO); + _L("Detected advanced data"), wxICON_WARNING | wxYES | wxCANCEL); if (msg_dlg.ShowModal() == wxID_YES) { Slic3r::GUI::wxGetApp().save_mode(comAdvanced); view3D->set_as_dirty(); @@ -6147,7 +6145,7 @@ bool Plater::load_files(const wxArrayString& filenames, bool delete_after_load/* break; } case ProjectDropDialog::LoadType::LoadGeometry: { - Plater::TakeSnapshot snapshot(this, _L("Import Object")); +// Plater::TakeSnapshot snapshot(this, _L("Import Object")); load_files({ *it }, true, false); break; } diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 106937c46b..cc57bcffdc 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -301,6 +301,11 @@ void PreferencesDialog::build() L("Suppress \" - default - \" presets in the Print / Filament / Printer selections once there are any other valid presets available."), app_config->get("no_defaults") == "1"); + append_bool_option(m_optgroup_general, "no_templates", + L("Suppress \" Template \" filament presets"), + L("Suppress \" Template \" filament presets in configuration wizard and sidebar visibility."), + app_config->get("no_templates") == "1"); + append_bool_option(m_optgroup_general, "show_incompatible_presets", L("Show incompatible print and filament presets"), L("When checked, the print and filament presets are shown in the preset editor " @@ -692,6 +697,8 @@ void PreferencesDialog::accept(wxEvent&) DesktopIntegrationDialog::perform_desktop_integration(true); #endif // __linux__ + bool update_filament_sidebar = (m_values.find("no_templates") != m_values.end()); + std::vector options_to_recreate_GUI = { "no_defaults", "tabs_as_menu", "sys_menu_enabled" }; for (const std::string& option : options_to_recreate_GUI) { @@ -761,6 +768,9 @@ void PreferencesDialog::accept(wxEvent&) wxGetApp().update_ui_from_settings(); clear_cache(); + + if (update_filament_sidebar) + wxGetApp().plater()->sidebar().update_presets(Preset::Type::TYPE_FILAMENT); } void PreferencesDialog::revert(wxEvent&) diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index c4d0fc6706..d25f5adfa5 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -798,7 +798,7 @@ void PlaterPresetComboBox::show_edit_menu() wxString PlaterPresetComboBox::get_preset_name(const Preset& preset) { - std::string name = preset.alias.empty() ? preset.name : preset.alias; + std::string name = preset.alias.empty() ? preset.name : (preset.vendor && preset.vendor->templates_profile ? preset.name : preset.alias); return from_u8(name + suffix(preset)); } @@ -836,6 +836,7 @@ void PlaterPresetComboBox::update() null_icon_width = (wide_icons ? 3 : 2) * norm_icon_width + thin_space_icon_width + wide_space_icon_width; std::map nonsys_presets; + std::map template_presets; wxString selected_user_preset; wxString tooltip; @@ -883,10 +884,18 @@ void PlaterPresetComboBox::update() const std::string name = preset.alias.empty() ? preset.name : preset.alias; if (preset.is_default || preset.is_system) { - Append(get_preset_name(preset), *bmp); - validate_selection(is_selected); - if (is_selected) - tooltip = from_u8(preset.name); + if (preset.vendor && preset.vendor->templates_profile) { + template_presets.emplace(get_preset_name(preset), bmp); + if (is_selected) { + selected_user_preset = get_preset_name(preset); + tooltip = from_u8(preset.name); + } + } else { + Append(get_preset_name(preset), *bmp); + validate_selection(is_selected); + if (is_selected) + tooltip = from_u8(preset.name); + } } else { @@ -899,6 +908,8 @@ void PlaterPresetComboBox::update() if (i + 1 == m_collection->num_default_presets()) set_label_marker(Append(separator(L("System presets")), NullBitmapBndl())); } + + if (!nonsys_presets.empty()) { set_label_marker(Append(separator(L("User presets")), NullBitmapBndl())); @@ -908,6 +919,15 @@ void PlaterPresetComboBox::update() } } + const AppConfig* app_config = wxGetApp().app_config; + if (!template_presets.empty() && app_config->get("no_templates") == "0") { + set_label_marker(Append(separator(L("Template presets")), wxNullBitmap)); + for (std::map::iterator it = template_presets.begin(); it != template_presets.end(); ++it) { + Append(it->first, *it->second); + validate_selection(it->first == selected_user_preset); + } + } + if (m_type == Preset::TYPE_PRINTER) { // add Physical printers, if any exists @@ -1046,6 +1066,8 @@ void TabPresetComboBox::update() const std::deque& presets = m_collection->get_presets(); std::map> nonsys_presets; + std::map> template_presets; + wxString selected = ""; if (!presets.front().is_visible) set_label_marker(Append(separator(L("System presets")), NullBitmapBndl())); @@ -1078,11 +1100,19 @@ void TabPresetComboBox::update() auto bmp = get_bmp(bitmap_key, main_icon_name, "lock_closed", is_enabled, preset.is_compatible, preset.is_system || preset.is_default); assert(bmp); - if (preset.is_default || preset.is_system) { - int item_id = Append(get_preset_name(preset), *bmp); - if (!is_enabled) - set_label_marker(item_id, LABEL_ITEM_DISABLED); - validate_selection(i == idx_selected); + if (preset.is_default || preset.is_system) { + if (preset.vendor && preset.vendor->templates_profile) { + template_presets.emplace(get_preset_name(preset), std::pair(bmp, is_enabled)); + if (i == idx_selected) + selected = get_preset_name(preset); + } else { + int item_id = Append(get_preset_name(preset), *bmp); + if (!is_enabled) + set_label_marker(item_id, LABEL_ITEM_DISABLED); + validate_selection(i == idx_selected); + } + + } else { @@ -1094,6 +1124,7 @@ void TabPresetComboBox::update() if (i + 1 == m_collection->num_default_presets()) set_label_marker(Append(separator(L("System presets")), NullBitmapBndl())); } + if (!nonsys_presets.empty()) { set_label_marker(Append(separator(L("User presets")), NullBitmapBndl())); @@ -1105,7 +1136,19 @@ void TabPresetComboBox::update() validate_selection(it->first == selected); } } - + + const AppConfig* app_config = wxGetApp().app_config; + if (!template_presets.empty() && app_config->get("no_templates") == "0") { + set_label_marker(Append(separator(L("Template presets")), wxNullBitmap)); + for (std::map>::iterator it = template_presets.begin(); it != template_presets.end(); ++it) { + int item_id = Append(it->first, *it->second.first); + bool is_enabled = it->second.second; + if (!is_enabled) + set_label_marker(item_id, LABEL_ITEM_DISABLED); + validate_selection(it->first == selected); + } + } + if (m_type == Preset::TYPE_PRINTER) { // add Physical printers, if any exists diff --git a/src/slic3r/GUI/SavePresetDialog.cpp b/src/slic3r/GUI/SavePresetDialog.cpp index d9ee5f58c1..d4c2ba58ca 100644 --- a/src/slic3r/GUI/SavePresetDialog.cpp +++ b/src/slic3r/GUI/SavePresetDialog.cpp @@ -285,17 +285,17 @@ void SavePresetDialog::Item::Enable(bool enable /*= true*/) // SavePresetDialog //----------------------------------------------- -SavePresetDialog::SavePresetDialog(wxWindow* parent, Preset::Type type, std::string suffix) +SavePresetDialog::SavePresetDialog(wxWindow* parent, Preset::Type type, std::string suffix, bool template_filament) : DPIDialog(parent, wxID_ANY, _L("Save preset"), wxDefaultPosition, wxSize(45 * wxGetApp().em_unit(), 5 * wxGetApp().em_unit()), wxDEFAULT_DIALOG_STYLE | wxICON_WARNING | wxRESIZE_BORDER) { - build(std::vector{type}, suffix); + build(std::vector{type}, suffix, template_filament); } -SavePresetDialog::SavePresetDialog(wxWindow* parent, std::vector types, std::string suffix, PresetBundle* preset_bundle/* = nullptr*/) +SavePresetDialog::SavePresetDialog(wxWindow* parent, std::vector types, std::string suffix, bool template_filament/* =false*/, PresetBundle* preset_bundle/* = nullptr*/) : DPIDialog(parent, wxID_ANY, _L("Save presets"), wxDefaultPosition, wxSize(45 * wxGetApp().em_unit(), 5 * wxGetApp().em_unit()), wxDEFAULT_DIALOG_STYLE | wxICON_WARNING | wxRESIZE_BORDER), m_preset_bundle(preset_bundle) { - build(types, suffix); + build(types, suffix, template_filament); } SavePresetDialog::SavePresetDialog(wxWindow* parent, Preset::Type type, bool rename, const wxString& info_line_extention) @@ -312,7 +312,7 @@ SavePresetDialog::~SavePresetDialog() } } -void SavePresetDialog::build(std::vector types, std::string suffix) +void SavePresetDialog::build(std::vector types, std::string suffix, bool template_filament) { #if defined(__WXMSW__) // ys_FIXME! temporary workaround for correct font scaling @@ -341,6 +341,15 @@ void SavePresetDialog::build(std::vector types, std::string suffix btnOK->Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { evt.Enable(enable_ok_btn()); }); topSizer->Add(m_presets_sizer, 0, wxEXPAND | wxALL, BORDER_W); + + // Add checkbox for Template filament saving + if (template_filament && types.size() == 1 && *types.begin() == Preset::Type::TYPE_FILAMENT) { + m_template_filament_checkbox = new wxCheckBox(this, wxID_ANY, _L("Save as profile derived from current printer only.")); + wxBoxSizer* check_sizer = new wxBoxSizer(wxVERTICAL); + check_sizer->Add(m_template_filament_checkbox); + topSizer->Add(check_sizer, 0, wxEXPAND | wxALL, BORDER_W); + } + topSizer->Add(btns, 0, wxEXPAND | wxALL, BORDER_W); SetSizer(topSizer); @@ -371,6 +380,15 @@ std::string SavePresetDialog::get_name(Preset::Type type) return ""; } +bool SavePresetDialog::get_template_filament_checkbox() +{ + if (m_template_filament_checkbox) + { + return m_template_filament_checkbox->GetValue(); + } + return false; +} + bool SavePresetDialog::enable_ok_btn() const { for (const Item* item : m_items) diff --git a/src/slic3r/GUI/SavePresetDialog.hpp b/src/slic3r/GUI/SavePresetDialog.hpp index e34ed9e5df..3088d457f6 100644 --- a/src/slic3r/GUI/SavePresetDialog.hpp +++ b/src/slic3r/GUI/SavePresetDialog.hpp @@ -76,6 +76,7 @@ private: wxStaticText* m_label {nullptr}; wxBoxSizer* m_radio_sizer {nullptr}; ActionType m_action {UndefAction}; + wxCheckBox* m_template_filament_checkbox {nullptr}; std::string m_ph_printer_name; std::string m_old_preset_name; @@ -88,8 +89,8 @@ public: const wxString& get_info_line_extention() { return m_info_line_extention; } - SavePresetDialog(wxWindow* parent, Preset::Type type, std::string suffix = ""); - SavePresetDialog(wxWindow* parent, std::vector types, std::string suffix = "", PresetBundle* preset_bundle = nullptr); + SavePresetDialog(wxWindow* parent, Preset::Type type, std::string suffix = "", bool template_filament = false); + SavePresetDialog(wxWindow* parent, std::vector types, std::string suffix = "", bool template_filament = false, PresetBundle* preset_bundle = nullptr); SavePresetDialog(wxWindow* parent, Preset::Type type, bool rename, const wxString& info_line_extention); ~SavePresetDialog() override; @@ -105,12 +106,13 @@ public: bool Layout() override; bool is_for_rename() { return m_use_for_rename; } + bool get_template_filament_checkbox(); protected: void on_dpi_changed(const wxRect& suggested_rect) override; void on_sys_color_changed() override {} private: - void build(std::vector types, std::string suffix = ""); + void build(std::vector types, std::string suffix = "", bool template_filament = false); void update_physical_printers(const std::string& preset_name); void accept(); }; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 1483a482f4..e519a52e36 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1038,8 +1038,11 @@ void Tab::load_key_value(const std::string& opt_key, const boost::any& value, bo static wxString support_combo_value_for_config(const DynamicPrintConfig &config, bool is_fff) { + std::string slatree = is_fff ? "" : get_sla_suptree_prefix(config); + const std::string support = is_fff ? "support_material" : "supports_enable"; - const std::string buildplate_only = is_fff ? "support_material_buildplate_only" : "support_buildplate_only"; + const std::string buildplate_only = is_fff ? "support_material_buildplate_only" : slatree + "support_buildplate_only"; + return ! config.opt_bool(support) ? _("None") : @@ -1082,7 +1085,7 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) if (is_fff ? (opt_key == "support_material" || opt_key == "support_material_auto" || opt_key == "support_material_buildplate_only") : - (opt_key == "supports_enable" || opt_key == "support_buildplate_only")) + (opt_key == "supports_enable" || opt_key == "support_tree_type" || opt_key == get_sla_suptree_prefix(*m_config) + "support_buildplate_only")) og_freq_chng_params->set_value("support", support_combo_value_for_config(*m_config, is_fff)); if (! is_fff && (opt_key == "pad_enable" || opt_key == "pad_around_object")) @@ -3712,11 +3715,25 @@ void Tab::save_preset(std::string name /*= ""*/, bool detach) // focus currently.is there anything better than this ? //! m_treectrl->OnSetFocus(); + auto& old_preset = m_presets->get_edited_preset(); + bool from_template = false; + std::string edited_printer; + if (m_type == Preset::TYPE_FILAMENT && old_preset.vendor && old_preset.vendor->templates_profile) + { + //TODO: is this really the best way to get "printer_model" option of currently edited printer? + edited_printer = wxGetApp().preset_bundle->printers.get_edited_preset().config.opt("printer_model")->serialize(); + if (!edited_printer.empty()) + from_template = true; + + } + if (name.empty()) { - SavePresetDialog dlg(m_parent, m_type, detach ? _u8L("Detached") : ""); + SavePresetDialog dlg(m_parent, m_type, detach ? _u8L("Detached") : "", from_template); if (dlg.ShowModal() != wxID_OK) return; name = dlg.get_name(); + if (from_template) + from_template = dlg.get_template_filament_checkbox(); } if (detach && m_type == Preset::TYPE_PRINTER) @@ -3728,6 +3745,19 @@ void Tab::save_preset(std::string name /*= ""*/, bool detach) if (detach && m_type == Preset::TYPE_PRINTER) wxGetApp().mainframe->on_config_changed(m_config); + // Update compatible printers + if (from_template && !edited_printer.empty()) { + auto& new_preset = m_presets->get_edited_preset(); + std::string cond = new_preset.compatible_printers_condition(); + if (!cond.empty()) + cond += " and "; + cond += "printer_model == \""+edited_printer+"\""; + new_preset.config.set("compatible_printers_condition", cond); + new_preset.save(); + m_presets->save_current_preset(name, detach); + load_current_preset(); + } + // Mark the print & filament enabled if they are compatible with the currently selected preset. // If saving the preset changes compatibility with other presets, keep the now incompatible dependent presets selected, however with a "red flag" icon showing that they are no more compatible. m_preset_bundle->update_compatible(PresetSelectCompatibleType::Never); @@ -4821,6 +4851,60 @@ void TabSLAMaterial::update() wxGetApp().mainframe->on_config_changed(m_config); } +static void add_options_into_line(ConfigOptionsGroupShp &optgroup, + const std::vector> &prefixes, + const std::string &optkey) +{ + auto opt = optgroup->get_option(prefixes.front().first + optkey); + Line line{ opt.opt.label, "" }; + line.full_width = 1; + for (auto &prefix : prefixes) { + opt = optgroup->get_option(prefix.first + optkey); + opt.opt.label = prefix.second; + opt.opt.width = 12; // TODO + line.append_option(opt); + } + optgroup->append_line(line); +} + +void TabSLAPrint::build_sla_support_params(const std::vector> &prefixes, + const Slic3r::GUI::PageShp &page) +{ + + auto optgroup = page->new_optgroup(L("Support head")); + add_options_into_line(optgroup, prefixes, "support_head_front_diameter"); + add_options_into_line(optgroup, prefixes, "support_head_penetration"); + add_options_into_line(optgroup, prefixes, "support_head_width"); + + optgroup = page->new_optgroup(L("Support pillar")); + add_options_into_line(optgroup, prefixes, "support_pillar_diameter"); + add_options_into_line(optgroup, prefixes, "support_small_pillar_diameter_percent"); + add_options_into_line(optgroup, prefixes, "support_max_bridges_on_pillar"); + + add_options_into_line(optgroup, prefixes, "support_pillar_connection_mode"); + add_options_into_line(optgroup, prefixes, "support_buildplate_only"); + add_options_into_line(optgroup, prefixes, "support_pillar_widening_factor"); + add_options_into_line(optgroup, prefixes, "support_max_weight_on_model"); + add_options_into_line(optgroup, prefixes, "support_base_diameter"); + add_options_into_line(optgroup, prefixes, "support_base_height"); + add_options_into_line(optgroup, prefixes, "support_base_safety_distance"); + + // Mirrored parameter from Pad page for toggling elevation on the same page + add_options_into_line(optgroup, prefixes, "support_object_elevation"); + + Line line{ "", "" }; + line.full_width = 1; + line.widget = [this](wxWindow* parent) { + return description_line_widget(parent, &m_support_object_elevation_description_line); + }; + optgroup->append_line(line); + + optgroup = page->new_optgroup(L("Connection of the support sticks and junctions")); + add_options_into_line(optgroup, prefixes, "support_critical_angle"); + add_options_into_line(optgroup, prefixes, "support_max_bridge_length"); + add_options_into_line(optgroup, prefixes, "support_max_pillar_link_distance"); +} + void TabSLAPrint::build() { m_presets = &m_preset_bundle->sla_prints; @@ -4833,42 +4917,14 @@ void TabSLAPrint::build() optgroup->append_single_option_line("faded_layers"); page = add_options_page(L("Supports"), "support"/*"sla_supports"*/); + optgroup = page->new_optgroup(L("Supports")); optgroup->append_single_option_line("supports_enable"); optgroup->append_single_option_line("support_tree_type"); - - optgroup = page->new_optgroup(L("Support head")); - optgroup->append_single_option_line("support_head_front_diameter"); - optgroup->append_single_option_line("support_head_penetration"); - optgroup->append_single_option_line("support_head_width"); - - optgroup = page->new_optgroup(L("Support pillar")); - optgroup->append_single_option_line("support_pillar_diameter"); - optgroup->append_single_option_line("support_small_pillar_diameter_percent"); - optgroup->append_single_option_line("support_max_bridges_on_pillar"); - - optgroup->append_single_option_line("support_pillar_connection_mode"); - optgroup->append_single_option_line("support_buildplate_only"); optgroup->append_single_option_line("support_enforcers_only"); - optgroup->append_single_option_line("support_pillar_widening_factor"); - optgroup->append_single_option_line("support_base_diameter"); - optgroup->append_single_option_line("support_base_height"); - optgroup->append_single_option_line("support_base_safety_distance"); - // Mirrored parameter from Pad page for toggling elevation on the same page - optgroup->append_single_option_line("support_object_elevation"); + build_sla_support_params({{"", "Default"}, {"branching", "Branching"}}, page); - Line line{ "", "" }; - line.full_width = 1; - line.widget = [this](wxWindow* parent) { - return description_line_widget(parent, &m_support_object_elevation_description_line); - }; - optgroup->append_line(line); - - optgroup = page->new_optgroup(L("Connection of the support sticks and junctions")); - optgroup->append_single_option_line("support_critical_angle"); - optgroup->append_single_option_line("support_max_bridge_length"); - optgroup->append_single_option_line("support_max_pillar_link_distance"); optgroup = page->new_optgroup(L("Automatic generation")); optgroup->append_single_option_line("support_points_density_relative"); diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index f5dd4c5225..c060eb7fdd 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -339,7 +339,7 @@ public: void on_roll_back_value(const bool to_sys = false); - PageShp add_options_page(const wxString& title, const std::string& icon, bool is_extruder_pages = false); + PageShp add_options_page(const wxString& title, const std::string& icon, bool is_extruder_pages = false); static wxString translate_category(const wxString& title, Preset::Type preset_type); virtual void OnActivate(); @@ -526,6 +526,12 @@ public: class TabSLAPrint : public Tab { + // Methods are a vector of method prefix -> method label pairs + // method prefix is the prefix whith which all the config values are prefixed + // for a particular method. The label is the friendly name for the method + void build_sla_support_params(const std::vector> &methods, + const Slic3r::GUI::PageShp &page); + public: TabSLAPrint(wxBookCtrlBase* parent) : Tab(parent, _(L("Print Settings")), Slic3r::Preset::TYPE_SLA_PRINT) {} diff --git a/src/slic3r/Utils/AppUpdater.cpp b/src/slic3r/Utils/AppUpdater.cpp index a17adea8a5..bd71e86ec1 100644 --- a/src/slic3r/Utils/AppUpdater.cpp +++ b/src/slic3r/Utils/AppUpdater.cpp @@ -323,6 +323,11 @@ void AppUpdater::priv::parse_version_string(const std::string& body) return; #endif // 0 BOOST_LOG_TRIVIAL(error) << "Could not find property tree in version file. Checking for application update has failed."; + // Lets send event with current version, this way if user triggered this check, it will notify him about no new version online. + std::string version = Semver().to_string(); + wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_VERSION_ONLINE); + evt->SetString(GUI::from_u8(version)); + GUI::wxGetApp().QueueEvent(evt); return; } std::string tree_string = body.substr(start); diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index f4863ff20a..cd3935ea00 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -12,14 +12,17 @@ #include #include #include +#include #include #include +#include #include "libslic3r/libslic3r.h" #include "libslic3r/format.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/PresetBundle.hpp" +#include "libslic3r/miniz_extension.hpp" #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/UpdateDialogs.hpp" @@ -49,7 +52,7 @@ namespace Slic3r { static const char *INDEX_FILENAME = "index.idx"; static const char *TMP_EXTENSION = ".download"; - +namespace { void copy_file_fix(const fs::path &source, const fs::path &target) { BOOST_LOG_TRIVIAL(debug) << format("PresetUpdater: Copying %1% -> %2%", source, target); @@ -66,7 +69,21 @@ void copy_file_fix(const fs::path &source, const fs::path &target) static constexpr const auto perms = fs::owner_read | fs::owner_write | fs::group_read | fs::others_read; fs::permissions(target, perms); } - +std::string escape_string_url(const std::string& unescaped) +{ + std::string ret_val; + CURL* curl = curl_easy_init(); + if (curl) { + char* decoded = curl_easy_escape(curl, unescaped.c_str(), unescaped.size()); + if (decoded) { + ret_val = std::string(decoded); + curl_free(decoded); + } + curl_easy_cleanup(curl); + } + return ret_val; +} +} struct Update { fs::path source; @@ -144,6 +161,7 @@ struct PresetUpdater::priv std::string version_check_url; fs::path cache_path; + fs::path cache_vendor_path; fs::path rsrc_path; fs::path vendor_path; @@ -155,19 +173,25 @@ struct PresetUpdater::priv priv(); - void set_download_prefs(AppConfig *app_config); + void set_download_prefs(const AppConfig *app_config); bool get_file(const std::string &url, const fs::path &target_path) const; void prune_tmps() const; - void sync_config(const VendorMap vendors); + void sync_config(const VendorMap vendors, const std::string& index_archive_url); void check_install_indices() const; Updates get_config_updates(const Semver& old_slic3r_version) const; bool perform_updates(Updates &&updates, bool snapshot = true) const; void set_waiting_updates(Updates u); + // checks existence and downloads resource to cache + void get_missing_resource(const std::string& vendor, const std::string& filename, const std::string& url) const; + // checks existence and downloads resource to vendor or copy from cache to vendor + void get_or_copy_missing_resource(const std::string& vendor, const std::string& filename, const std::string& url) const; + void update_index_db(); }; PresetUpdater::priv::priv() : cache_path(fs::path(Slic3r::data_dir()) / "cache") + , cache_vendor_path(cache_path / "vendor") , rsrc_path(fs::path(resources_dir()) / "profiles") , vendor_path(fs::path(Slic3r::data_dir()) / "vendor") , cancel(false) @@ -179,8 +203,13 @@ PresetUpdater::priv::priv() index_db = Index::load_db(); } +void PresetUpdater::priv::update_index_db() +{ + index_db = Index::load_db(); +} + // Pull relevant preferences from AppConfig -void PresetUpdater::priv::set_download_prefs(AppConfig *app_config) +void PresetUpdater::priv::set_download_prefs(const AppConfig *app_config) { enabled_version_check = app_config->get("notify_release") != "none"; version_check_url = app_config->version_check_url(); @@ -232,46 +261,208 @@ void PresetUpdater::priv::prune_tmps() const } } +void PresetUpdater::priv::get_missing_resource(const std::string& vendor, const std::string& filename, const std::string& url) const +{ + if (filename.empty() || vendor.empty()) + return; + + if (!boost::starts_with(url, "http://files.prusa3d.com/wp-content/uploads/repository/") && + !boost::starts_with(url, "https://files.prusa3d.com/wp-content/uploads/repository/")) + { + throw Slic3r::CriticalException(GUI::format("URL outside prusa3d.com network: %1%", url)); + } + + std::string escaped_filename = escape_string_url(filename); + const fs::path file_in_vendor(vendor_path / (vendor + "/" + filename)); + const fs::path file_in_rsrc(rsrc_path / (vendor + "/" + filename)); + const fs::path file_in_cache(cache_path / (vendor + "/" + filename)); + + if (fs::exists(file_in_vendor)) { // Already in vendor. No need to do anything. + BOOST_LOG_TRIVIAL(info) << "Resource " << vendor << " / " << filename << " found in vendor folder. No need to download."; + return; + } + if (fs::exists(file_in_rsrc)) { // In resources dir since installation. No need to do anything. + BOOST_LOG_TRIVIAL(info) << "Resource " << vendor << " / " << filename << " found in resources folder. No need to download."; + return; + } + if (fs::exists(file_in_cache)) { // In cache/venodr_name/ dir. No need to do anything. + BOOST_LOG_TRIVIAL(info) << "Resource " << vendor << " / " << filename << " found in cache folder. No need to download."; + return; + } + + BOOST_LOG_TRIVIAL(info) << "Resources check could not find " << vendor << " / " << filename << " bed texture. Downloading."; + + const auto resource_url = format("%1%%2%%3%", url, url.back() == '/' ? "" : "/", escaped_filename); // vendor should already be in url + + if (!fs::exists(file_in_cache.parent_path())) + fs::create_directory(file_in_cache.parent_path()); + + get_file(resource_url, file_in_cache); + return; +} + +void PresetUpdater::priv::get_or_copy_missing_resource(const std::string& vendor, const std::string& filename, const std::string& url) const +{ + if (filename.empty() || vendor.empty()) + return; + + std::string escaped_filename = escape_string_url(filename); + const fs::path file_in_vendor(vendor_path / (vendor + "/" + filename)); + const fs::path file_in_rsrc(rsrc_path / (vendor + "/" + filename)); + const fs::path file_in_cache(cache_path / (vendor + "/" + filename)); + + if (fs::exists(file_in_vendor)) { // Already in vendor. No need to do anything. + BOOST_LOG_TRIVIAL(info) << "Resource " << vendor << " / " << filename << " found in vendor folder. No need to download."; + return; + } + if (fs::exists(file_in_rsrc)) { // In resources dir since installation. No need to do anything. + BOOST_LOG_TRIVIAL(info) << "Resource " << vendor << " / " << filename << " found in resources folder. No need to download."; + return; + } + if (!fs::exists(file_in_cache)) { // No file to copy. Download it to straight to the vendor dir. + if (!boost::starts_with(url, "http://files.prusa3d.com/wp-content/uploads/repository/") && + !boost::starts_with(url, "https://files.prusa3d.com/wp-content/uploads/repository/")) + { + throw Slic3r::CriticalException(GUI::format("URL outside prusa3d.com network: %1%", url)); + } + BOOST_LOG_TRIVIAL(info) << "Downloading resources missing in cache directory: " << vendor << " / " << filename; + + const auto resource_url = format("%1%%2%%3%", url, url.back() == '/' ? "" : "/", escaped_filename); // vendor should already be in url + + if (!fs::exists(file_in_vendor.parent_path())) + fs::create_directory(file_in_vendor.parent_path()); + + get_file(resource_url, file_in_vendor); + return; + } + + if (!fs::exists(file_in_vendor.parent_path())) // create vendor_name dir in vendor + fs::create_directory(file_in_vendor.parent_path()); + + BOOST_LOG_TRIVIAL(debug) << "Copiing: " << file_in_cache << " to " << file_in_vendor; + copy_file_fix(file_in_cache, file_in_vendor); +} + // Download vendor indices. Also download new bundles if an index indicates there's a new one available. // Both are saved in cache. -void PresetUpdater::priv::sync_config(const VendorMap vendors) +void PresetUpdater::priv::sync_config(const VendorMap vendors, const std::string& index_archive_url) { BOOST_LOG_TRIVIAL(info) << "Syncing configuration cache"; if (!enabled_config_update) { return; } - // Donwload vendor preset bundles + // Download profiles archive zip + // dk: Do we want to return here on error? Or skip archive dwnld and unzip and work with previous run state cache / vendor? I think return. + // Any error here also doesnt show any info in UI. Do we want maybe notification? + fs::path archive_path(cache_path / "vendor_indices.zip"); + if (index_archive_url.empty()) { + BOOST_LOG_TRIVIAL(error) << "Downloading profile archive failed - url has no value."; + return; + } + BOOST_LOG_TRIVIAL(info) << "Downloading vedor profiles archive zip from " << index_archive_url; + //check if idx_url is leading to our site + if (!boost::starts_with(index_archive_url, "http://files.prusa3d.com/wp-content/uploads/repository/") && + !boost::starts_with(index_archive_url, "https://files.prusa3d.com/wp-content/uploads/repository/")) + { + BOOST_LOG_TRIVIAL(error) << "Unsafe url path for vedor profiles archive zip. Download is rejected."; + return; + } + if (!get_file(index_archive_url, archive_path)) { + BOOST_LOG_TRIVIAL(error) << "Download of vedor profiles archive zip failed."; + return; + } + if (cancel) { + return; + } + + enum class VendorStatus + { + IN_ARCHIVE, + IN_CACHE, + NEW_VERSION, + INSTALLED + }; + + std::vector> vendors_with_status; + // Unzip archive to cache / vendor + mz_zip_archive archive; + mz_zip_zero_struct(&archive); + if (!open_zip_reader(&archive, archive_path.string())) { + BOOST_LOG_TRIVIAL(error) << "Couldn't open zipped bundle."; + return; + } else { + mz_uint num_entries = mz_zip_reader_get_num_files(&archive); + // loop the entries + mz_zip_archive_file_stat stat; + for (mz_uint i = 0; i < num_entries; ++i) { + if (mz_zip_reader_file_stat(&archive, i, &stat)) { + std::string name(stat.m_filename); + 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) { + BOOST_LOG_TRIVIAL(error) << "Failed to unzip " << stat.m_filename; + continue; + } + // create file from buffer + fs::path tmp_path(cache_vendor_path / (name + ".tmp")); + if (!fs::exists(tmp_path.parent_path())) { + BOOST_LOG_TRIVIAL(error) << "Failed to unzip file " << name << ". Directories are not supported. Skipping file."; + continue; + } + fs::path target_path(cache_vendor_path / name); + fs::fstream file(tmp_path, std::ios::out | std::ios::binary | std::ios::trunc); + file.write(buffer.c_str(), buffer.size()); + file.close(); + boost::system::error_code ec; + bool exists = fs::exists(tmp_path, ec); + if(!exists || ec) { + BOOST_LOG_TRIVIAL(error) << "Failed to find unzipped file at " << tmp_path << ". Terminating Preset updater synchorinzation." ; + close_zip_reader(&archive); + return; + } + fs::rename(tmp_path, target_path, ec); + if (ec) { + BOOST_LOG_TRIVIAL(error) << "Failed to rename unzipped file at " << tmp_path << ". Terminating Preset updater synchorinzation. Error message: " << ec.message(); + close_zip_reader(&archive); + return; + } + // TODO: what if unexpected happens here (folder inside zip) - crash! + + if (name.substr(name.size() - 3) == "idx") + vendors_with_status.emplace_back(name.substr(0, name.size() - 4), VendorStatus::IN_ARCHIVE); // asume for now its only in archive - if not, it will change later. + } + } + } + close_zip_reader(&archive); + } + + // Update vendor preset bundles if in Vendor // Over all indices from the cache directory: for (auto &index : index_db) { - if (cancel) { return; } + if (cancel) { + return; + } + auto archive_it = std::find_if(vendors_with_status.begin(), vendors_with_status.end(), + [&index](const std::pair& element) { return element.first == index.vendor(); }); + //assert(archive_it != vendors_with_status.end()); // this would mean there is a index for vendor that is missing in recently downloaded archive const auto vendor_it = vendors.find(index.vendor()); if (vendor_it == vendors.end()) { - BOOST_LOG_TRIVIAL(warning) << "No such vendor: " << index.vendor(); + // Not installed vendor yet we need to check missing thumbnails (of new printers) + BOOST_LOG_TRIVIAL(debug) << "No such vendor: " << index.vendor(); + if (archive_it != vendors_with_status.end()) + archive_it->second = VendorStatus::IN_CACHE; continue; } + if (archive_it != vendors_with_status.end()) + archive_it->second = VendorStatus::INSTALLED; + const VendorProfile &vendor = vendor_it->second; - if (vendor.config_update_url.empty()) { - BOOST_LOG_TRIVIAL(info) << "Vendor has no config_update_url: " << vendor.name; - continue; - } - - // Download a fresh index - BOOST_LOG_TRIVIAL(info) << "Downloading index for vendor: " << vendor.name; - const auto idx_url = vendor.config_update_url + "/" + INDEX_FILENAME; const std::string idx_path = (cache_path / (vendor.id + ".idx")).string(); - const std::string idx_path_temp = idx_path + "-update"; - //check if idx_url is leading to our site - if (! boost::starts_with(idx_url, "http://files.prusa3d.com/wp-content/uploads/repository/") && - ! boost::starts_with(idx_url, "https://files.prusa3d.com/wp-content/uploads/repository/")) - { - BOOST_LOG_TRIVIAL(warning) << "unsafe url path for vendor \"" << vendor.name << "\" rejected: " << idx_url; - continue; - } - if (!get_file(idx_url, idx_path_temp)) { continue; } - if (cancel) { return; } - + const std::string idx_path_temp = (cache_vendor_path / (vendor.id + ".idx")).string(); + // Load the fresh index up { Index new_index; @@ -282,10 +473,11 @@ void PresetUpdater::priv::sync_config(const VendorMap vendors) continue; } if (new_index.version() < index.version()) { - BOOST_LOG_TRIVIAL(warning) << format("The downloaded index %1% for vendor %2% is older than the active one. Ignoring the downloaded index.", idx_path_temp, vendor.name); + BOOST_LOG_TRIVIAL(info) << format("The downloaded index %1% for vendor %2% is older than the active one. Ignoring the downloaded index.", idx_path_temp, vendor.name); continue; } - Slic3r::rename_file(idx_path_temp, idx_path); + copy_file_fix(idx_path_temp, idx_path); + //if we rename path we need to change it in Index object too or create the object again //index = std::move(new_index); try { @@ -315,12 +507,265 @@ void PresetUpdater::priv::sync_config(const VendorMap vendors) if (vendor.config_version >= recommended) { continue; } - // Download a fresh bundle + // vendors that are checked here, doesnt need to be checked again later + if (archive_it != vendors_with_status.end()) + archive_it->second = VendorStatus::NEW_VERSION; + + // Download recomended ini to cache + const auto path_in_cache = cache_path / (vendor.id + ".ini"); BOOST_LOG_TRIVIAL(info) << "Downloading new bundle for vendor: " << vendor.name; const auto bundle_url = format("%1%/%2%.ini", vendor.config_update_url, recommended.to_string()); const auto bundle_path = cache_path / (vendor.id + ".ini"); - if (! get_file(bundle_url, bundle_path)) { continue; } - if (cancel) { return; } + if (!get_file(bundle_url, bundle_path)) + continue; + if (cancel) + return; + // vp is fully loaded to get all resources + VendorProfile vp; + try { + vp = VendorProfile::from_ini(bundle_path, true); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.id, bundle_path, e.what()); + continue; + } + // check the fresh bundle for missing resources + // for that, the ini file must be parsed (done above) + for (const auto& model : vp.models) { + for (const std::string& res : { model.bed_texture, model.bed_model, model.thumbnail/*id +"_thumbnail.png"*/} ) { + if (! res.empty()) { + try + { + get_missing_resource(vp.id, res, vendor.config_update_url); + } + catch (const std::exception& e) + { + BOOST_LOG_TRIVIAL(error) << "Failed to get " << res << " for " << vp.id << " " << model.id << ": " << e.what(); + } + + } + if (cancel) + return; + } + } + } + // Download missing thumbnails for not-installed vendors. + //for (const std::string& vendor : vendors_only_in_archive) + for (const std::pair& vendor : vendors_with_status) { + if (vendor.second == VendorStatus::IN_ARCHIVE) { + // index in archive and not in cache and not installed vendor + + const auto idx_path_in_archive = cache_vendor_path / (vendor.first + ".idx"); + const auto ini_path_in_archive = cache_vendor_path / (vendor.first + ".ini"); + if (!fs::exists(idx_path_in_archive)) + continue; + Index index; + try { + index.load(idx_path_in_archive); + } + catch (const std::exception& /* err */) { + BOOST_LOG_TRIVIAL(error) << format("Could not load downloaded index %1% for vendor %2%: invalid index?", idx_path_in_archive, vendor.first); + continue; + } + const auto recommended_it = index.recommended(); + if (recommended_it == index.end()) { + BOOST_LOG_TRIVIAL(error) << format("No recommended version for vendor: %1%, invalid index? (%2%)", vendor.first, idx_path_in_archive); + continue; + } + const auto recommended = recommended_it->config_version; + if (!fs::exists(ini_path_in_archive)){ + // Download recommneded to vendor - we do not have any existing ini file so we have to use hardcoded url. + const std::string fixed_url = GUI::wxGetApp().app_config->profile_folder_url(); + const auto bundle_url = format("%1%/%2%/%3%.ini", fixed_url, vendor.first, recommended.to_string()); + if (!get_file(bundle_url, ini_path_in_archive)) + continue; + } else { + // check existing ini version + // then download recommneded to vendor if needed + VendorProfile vp; + try { + vp = VendorProfile::from_ini(ini_path_in_archive, true); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.first, ini_path_in_archive, e.what()); + continue; + } + if (vp.config_version != recommended) { + const std::string fixed_url = GUI::wxGetApp().app_config->profile_folder_url(); + const auto bundle_url = format("%1%/%2%/%3%.ini", fixed_url, vendor.first, recommended.to_string()); + if (!get_file(bundle_url, ini_path_in_archive)) + continue; + } + } + // check missing thumbnails + VendorProfile vp; + try { + vp = VendorProfile::from_ini(ini_path_in_archive, true); + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.first, ini_path_in_archive, e.what()); + continue; + } + for (const auto& model : vp.models) { + if (!model.thumbnail.empty()) { + try + { + get_missing_resource(vp.id, model.thumbnail, vp.config_update_url); + } + catch (const std::exception& e) + { + BOOST_LOG_TRIVIAL(error) << "Failed to get " << model.thumbnail << " for " << vp.id << " " << model.id << ": " << e.what(); + } + } + if (cancel) + return; + } + } else if (vendor.second == VendorStatus::IN_CACHE) { + // find those where archive index recommends other version than index in cache and get it if not present + const auto idx_path_in_archive = cache_vendor_path / (vendor.first + ".idx"); + const auto ini_path_in_archive = cache_vendor_path / (vendor.first + ".ini"); + const auto idx_path_in_cache = cache_path / (vendor.first + ".idx"); + + if (!fs::exists(idx_path_in_archive) || !fs::exists(idx_path_in_cache)) + continue; + + // Compare index in cache and recetly downloaded one as part of zip archive + Index index_cache; + try { + index_cache.load(idx_path_in_cache); + } + catch (const std::exception& /* err */) { + BOOST_LOG_TRIVIAL(error) << format("Could not load downloaded index %1% for vendor %2%: invalid index?", idx_path_in_cache, vendor.first); + continue; + } + const auto recommended_it_cache = index_cache.recommended(); + if (recommended_it_cache == index_cache.end()) { + BOOST_LOG_TRIVIAL(error) << format("No recommended version for vendor: %1%, invalid index? (%2%)", vendor.first, idx_path_in_cache); + continue; + } + const auto recommended_cache = recommended_it_cache->config_version; + + Index index_archive; + try { + index_archive.load(idx_path_in_archive); + } + catch (const std::exception& /* err */) { + BOOST_LOG_TRIVIAL(error) << format("Could not load downloaded index %1% for vendor %2%: invalid index?", idx_path_in_archive, vendor.first); + continue; + } + const auto recommended_it_archive = index_archive.recommended(); + if (recommended_it_archive == index_archive.end()) { + BOOST_LOG_TRIVIAL(error) << format("No recommended version for vendor: %1%, invalid index? (%2%)", vendor.first, idx_path_in_archive); + continue; + } + const auto recommended_archive = recommended_it_archive->config_version; + + if (recommended_archive <= recommended_cache) { + // There isn't more recent recomended version online. This vendor is also not istalled. + // Thus only .ini is in resources and came with installation. + // And we expect all resources are present. + continue; + } + + // Download new .ini if needed. So next time user runs Wizard, most recent profiles are shown & installed. + if (!fs::exists(ini_path_in_archive) || fs::is_empty(ini_path_in_archive)) { + // download recommneded to vendor + const fs::path ini_path_in_rsrc = rsrc_path / (vendor.first + ".ini"); + if (!fs::exists(ini_path_in_rsrc)) { + // THIS SHOULD NOT HAPPEN + continue; + } + // Get download path from existing ini. + VendorProfile vp; + try { + vp = VendorProfile::from_ini(ini_path_in_rsrc, false); + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.first, ini_path_in_rsrc, e.what()); + continue; + } + const auto bundle_url = format("%1%/%2%.ini", vp.config_update_url, recommended_archive.to_string()); + if (!get_file(bundle_url, ini_path_in_archive)) { + BOOST_LOG_TRIVIAL(error) << format("Failed to open vendor .ini file when checking missing resources: %1%", ini_path_in_rsrc); + continue; + } + } else { + // Check existing ini version. + // Then download recommneded to vendor if needed. + VendorProfile vp; + try { + vp = VendorProfile::from_ini(ini_path_in_archive, false); + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.first, ini_path_in_archive, e.what()); + continue; + } + if (vp.config_version != recommended_archive) { + const auto bundle_url = format("%1%/%2%.ini", vp.config_update_url, recommended_archive.to_string()); + if (!get_file(bundle_url, ini_path_in_archive)) { + BOOST_LOG_TRIVIAL(error) << format("Failed to open vendor .ini file when checking missing resources: %1%", ini_path_in_archive); + continue; + } + } + } + + if (!fs::exists(ini_path_in_archive)) { + BOOST_LOG_TRIVIAL(error) << "Resources check failed to find ini file for vendor: " << vendor.first; + continue; + } + // check missing thumbnails + VendorProfile vp; + try { + vp = VendorProfile::from_ini(ini_path_in_archive, true); + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.first, ini_path_in_archive, e.what()); + continue; + } + for (const auto& model : vp.models) { + if (!model.thumbnail.empty()) { + try + { + get_missing_resource(vp.id, model.thumbnail, vp.config_update_url); + } + catch (const std::exception& e) + { + BOOST_LOG_TRIVIAL(error) << "Failed to get " << model.thumbnail << " for " << vp.id << " " << model.id << ": " << e.what(); + } + } + if (cancel) + return; + } + } else if (vendor.second == VendorStatus::INSTALLED || vendor.second == VendorStatus::NEW_VERSION) { + // Installed vendors need to check that no resource is missing. Do this only for files in vendor folder (not in resorces) + // VendorStatus::NEW_VERSION might seem like a mistake here since files are downloaded when preparing update higher in this function. + // But this is a check for ini file in vendor where resources might be still missing since last update. + const auto path_in_vendor = vendor_path / (vendor.first + ".ini"); + if(!fs::exists(path_in_vendor)) + continue; + VendorProfile vp; + try { + vp = VendorProfile::from_ini(path_in_vendor, true); + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.first, path_in_vendor, e.what()); + continue; + } + for (const auto& model : vp.models) { + for (const std::string& res : { model.bed_texture, model.bed_model, model.thumbnail }) { + if (!model.thumbnail.empty()) { + try + { + get_or_copy_missing_resource(vp.id, res, vp.config_update_url); + } + catch (const std::exception& e) + { + BOOST_LOG_TRIVIAL(error) << "Failed to get " << model.thumbnail << " for " << vp.id << " " << model.id << ": " << e.what(); + } + } + if (cancel) + return; + } + } + } } } @@ -370,8 +815,14 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version } // Perform a basic load and check the version of the installed preset bundle. - auto vp = VendorProfile::from_ini(bundle_path, false); - + VendorProfile vp; + try { + vp = VendorProfile::from_ini(bundle_path, false); + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", idx.vendor(), bundle_path, e.what()); + continue; + } // Getting a recommended version from the latest index, wich may have been downloaded // from the internet, or installed / updated from the installation resources. auto recommended = idx.recommended(); @@ -401,7 +852,7 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version bool current_not_supported = false; //if slcr is incompatible but situation is not downgrade, we do forced updated and this bool is information to do it if (ver_current_found && !ver_current->is_current_slic3r_supported()){ - if(ver_current->is_current_slic3r_downgrade()) { + if (ver_current->is_current_slic3r_downgrade()) { // "Reconfigure" situation. BOOST_LOG_TRIVIAL(warning) << "Current Slic3r incompatible with installed bundle: " << bundle_path.string(); updates.incompats.emplace_back(std::move(bundle_path), *ver_current, vp.name); @@ -443,10 +894,13 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version fs::path bundle_path_idx_to_install; if (fs::exists(path_in_cache)) { try { - VendorProfile new_vp = VendorProfile::from_ini(path_in_cache, false); + VendorProfile new_vp = VendorProfile::from_ini(path_in_cache, true); if (new_vp.config_version == recommended->config_version) { // The config bundle from the cache directory matches the recommended version of the index from the cache directory. // This is the newest known recommended config. Use it. + if (!PresetUtils::vendor_profile_has_all_resources(new_vp)) { + BOOST_LOG_TRIVIAL(warning) << "Some resources are missing for update of vedor " << new_vp.id; + } new_update = Update(std::move(path_in_cache), std::move(bundle_path), *recommended, vp.name, vp.changelog_url, current_not_supported); // and install the config index from the cache into vendor's directory. bundle_path_idx_to_install = idx.path(); @@ -564,6 +1018,9 @@ bool PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) cons BOOST_LOG_TRIVIAL(info) << format("Performing %1% updates", updates.updates.size()); + wxProgressDialog progress_dialog(_L("Installing profiles"), _L("Installing profiles") , 100, nullptr, wxPD_AUTO_HIDE); + progress_dialog.Pulse(); + for (const auto &update : updates.updates) { BOOST_LOG_TRIVIAL(info) << '\t' << update; @@ -601,12 +1058,40 @@ bool PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) cons for (const auto &name : bundle.obsolete_presets.sla_prints) { obsolete_remover("sla_print", name); } for (const auto &name : bundle.obsolete_presets.sla_materials/*filaments*/) { obsolete_remover("sla_material", name); } for (const auto &name : bundle.obsolete_presets.printers) { obsolete_remover("printer", name); } + + // check if any resorces of installed bundle are missing. If so, new ones should be already downloaded at cache/vendor_id/ + VendorProfile vp; + try { + vp = VendorProfile::from_ini(update.target, true); + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1%, message: %2%", update.target, e.what()); + continue; + } + progress_dialog.Update(1, GUI::format_wxstr(_L("Downloading resources for %1%."),vp.id)); + progress_dialog.Pulse(); + for (const auto& model : vp.models) { + for (const std::string& resource : { model.bed_texture, model.bed_model, model.thumbnail }) { + if (resource.empty()) + continue; + try + { + get_or_copy_missing_resource(vp.id, resource, vp.config_update_url); + } + catch (const std::exception& e) + { + BOOST_LOG_TRIVIAL(error) << "Failed to prepare " << resource << " for " << vp.id << " " << model.id << ": " << e.what(); + } + } + } } + + progress_dialog.Destroy(); } return true; } - + void PresetUpdater::priv::set_waiting_updates(Updates u) { waiting_updates = u; @@ -630,7 +1115,7 @@ PresetUpdater::~PresetUpdater() } } -void PresetUpdater::sync(PresetBundle *preset_bundle) +void PresetUpdater::sync(const PresetBundle *preset_bundle) { p->set_download_prefs(GUI::wxGetApp().app_config); if (!p->enabled_version_check && !p->enabled_config_update) { return; } @@ -639,13 +1124,24 @@ void PresetUpdater::sync(PresetBundle *preset_bundle) // Unfortunatelly as of C++11, it needs to be copied again // into the closure (but perhaps the compiler can elide this). VendorMap vendors = preset_bundle->vendors; + std::string index_archive_url = GUI::wxGetApp().app_config->index_archive_url(); - p->thread = std::thread([this, vendors]() { + p->thread = std::thread([this, vendors, index_archive_url]() { this->p->prune_tmps(); - this->p->sync_config(std::move(vendors)); + this->p->sync_config(std::move(vendors), index_archive_url); }); } +void PresetUpdater::cancel_sync() +{ + if (p && p->thread.joinable()) { + // This will stop transfers being done by the thread, if any. + // Cancelling takes some time, but should complete soon enough. + p->cancel = true; + p->thread.join(); + } +} + void PresetUpdater::slic3r_update_notify() { if (! p->enabled_version_check) @@ -810,7 +1306,7 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3 return R_NOOP; } -bool PresetUpdater::install_bundles_rsrc(std::vector bundles, bool snapshot) const +bool PresetUpdater::install_bundles_rsrc_or_cache_vendor(std::vector bundles, bool snapshot) const { Updates updates; @@ -818,8 +1314,66 @@ bool PresetUpdater::install_bundles_rsrc(std::vector bundles, bool for (const auto &bundle : bundles) { auto path_in_rsrc = (p->rsrc_path / bundle).replace_extension(".ini"); + auto path_in_cache_vendor = (p->cache_vendor_path / bundle).replace_extension(".ini"); auto path_in_vendors = (p->vendor_path / bundle).replace_extension(".ini"); - updates.updates.emplace_back(std::move(path_in_rsrc), std::move(path_in_vendors), Version(), "", ""); + + bool is_in_rsrc = fs::exists(path_in_rsrc); + bool is_in_cache_vendor = fs::exists(path_in_cache_vendor) && !fs::is_empty(path_in_cache_vendor); + + // find if in cache vendor is newer version than in resources + if (is_in_cache_vendor) { + Semver version_cache = Semver::zero(); + try { + auto vp_cache = VendorProfile::from_ini(path_in_cache_vendor, false); + version_cache = vp_cache.config_version; + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1%, message: %2%", path_in_cache_vendor, e.what()); + // lets use file in resources + if (is_in_rsrc) { + updates.updates.emplace_back(std::move(path_in_rsrc), std::move(path_in_vendors), Version(), "", ""); + } + continue; + } + + Semver version_rsrc = Semver::zero(); + try { + if (is_in_rsrc) { + auto vp = VendorProfile::from_ini(path_in_rsrc, false); + version_rsrc = vp.config_version; + } + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1%, message: %2%", path_in_rsrc, e.what()); + continue; + } + + if (!is_in_rsrc || version_cache > version_rsrc) { + // in case we are installing from cache / vendor. we should also copy index to cache + // This needs to be done now bcs the current one would be missing this version on next start + // dk: Should we copy it to vendor dir too? + auto path_idx_cache_vendor(path_in_cache_vendor); + path_idx_cache_vendor.replace_extension(".idx"); + auto path_idx_cache = (p->cache_path / bundle).replace_extension(".idx"); + // DK: do this during perform_updates() too? + if (fs::exists(path_idx_cache_vendor)) + copy_file_fix(path_idx_cache_vendor, path_idx_cache); + else // Should we dialog this? + BOOST_LOG_TRIVIAL(error) << GUI::format(_L("Couldn't locate idx file %1% when performing updates."), path_idx_cache_vendor.string()); + updates.updates.emplace_back(std::move(path_in_cache_vendor), std::move(path_in_vendors), Version(), "", ""); + + } else { + if (is_in_rsrc) + updates.updates.emplace_back(std::move(path_in_rsrc), std::move(path_in_vendors), Version(), "", ""); + } + } else { + if (! is_in_rsrc) { + // This should not happen. Instead of an assert, make it crash in Release mode too. + BOOST_LOG_TRIVIAL(error) << "Internal error in PresetUpdater! Terminating the application."; + std::terminate(); + } + updates.updates.emplace_back(std::move(path_in_rsrc), std::move(path_in_vendors), Version(), "", ""); + } } return p->perform_updates(std::move(updates), snapshot); @@ -857,4 +1411,9 @@ bool PresetUpdater::version_check_enabled() const return p->enabled_version_check; } +void PresetUpdater::update_index_db() +{ + p->update_index_db(); +} + } diff --git a/src/slic3r/Utils/PresetUpdater.hpp b/src/slic3r/Utils/PresetUpdater.hpp index 974a7dcfae..85875e5a31 100644 --- a/src/slic3r/Utils/PresetUpdater.hpp +++ b/src/slic3r/Utils/PresetUpdater.hpp @@ -26,7 +26,8 @@ public: ~PresetUpdater(); // If either version check or config updating is enabled, get the appropriate data in the background and cache it. - void sync(PresetBundle *preset_bundle); + void sync(const PresetBundle *preset_bundle); + void cancel_sync(); // If version check is enabled, check if chaced online slic3r version is newer, notify if so. void slic3r_update_notify(); @@ -52,9 +53,11 @@ public: // Providing old slic3r version upgrade profiles on upgrade of an application even in case // that the config index installed from the Internet is equal to the index contained in the installation package. UpdateResult config_update(const Semver &old_slic3r_version, UpdateParams params) const; + + void update_index_db(); - // "Update" a list of bundles from resources (behaves like an online update). - bool install_bundles_rsrc(std::vector bundles, bool snapshot = true) const; + // "Update" a list of bundles from resources or cache/vendor (behaves like an online update). + bool install_bundles_rsrc_or_cache_vendor(std::vector bundles, bool snapshot = true) const; void on_update_notification_confirm(); diff --git a/tests/data/U_overhang.obj b/tests/data/U_overhang.obj new file mode 100644 index 0000000000..70453e58f7 --- /dev/null +++ b/tests/data/U_overhang.obj @@ -0,0 +1,76 @@ +#### +# +# OBJ File Generated by Meshlab +# +#### +# Object U_overhang.obj +# +# Vertices: 16 +# Faces: 28 +# +#### +vn 1.570797 1.570796 1.570796 +v 10.000000 10.000000 11.000000 +vn 4.712389 1.570796 -1.570796 +v 10.000000 1.000000 10.000000 +vn 1.570796 1.570796 -1.570796 +v 10.000000 10.000000 10.000000 +vn 1.570796 -1.570796 1.570796 +v 10.000000 0.000000 11.000000 +vn 4.712389 1.570796 1.570796 +v 10.000000 1.000000 1.000000 +vn 1.570797 1.570796 -1.570796 +v 10.000000 10.000000 0.000000 +vn 1.570796 1.570796 1.570796 +v 10.000000 10.000000 1.000000 +vn 1.570796 -1.570796 -1.570796 +v 10.000000 0.000000 0.000000 +vn -1.570796 1.570796 1.570796 +v 0.000000 10.000000 1.000000 +vn -4.712389 1.570796 1.570796 +v 0.000000 1.000000 1.000000 +vn -1.570796 -1.570796 -1.570796 +v 0.000000 0.000000 0.000000 +vn -1.570797 1.570796 -1.570796 +v 0.000000 10.000000 0.000000 +vn -4.712389 1.570796 -1.570796 +v 0.000000 1.000000 10.000000 +vn -1.570797 1.570796 1.570796 +v 0.000000 10.000000 11.000000 +vn -1.570796 1.570796 -1.570796 +v 0.000000 10.000000 10.000000 +vn -1.570796 -1.570796 1.570796 +v 0.000000 0.000000 11.000000 +# 16 vertices, 0 vertices normals + +f 1//1 2//2 3//3 +f 2//2 4//4 5//5 +f 4//4 2//2 1//1 +f 5//5 6//6 7//7 +f 5//5 8//8 6//6 +f 8//8 5//5 4//4 +f 9//9 5//5 7//7 +f 5//5 9//9 10//10 +f 11//11 6//6 8//8 +f 6//6 11//11 12//12 +f 12//12 10//10 9//9 +f 10//10 11//11 13//13 +f 11//11 10//10 12//12 +f 13//13 14//14 15//15 +f 13//13 16//16 14//14 +f 16//16 13//13 11//11 +f 6//6 9//9 7//7 +f 9//9 6//6 12//12 +f 11//11 4//4 16//16 +f 4//4 11//11 8//8 +f 13//13 3//3 2//2 +f 3//3 13//13 15//15 +f 5//5 13//13 2//2 +f 13//13 5//5 10//10 +f 14//14 4//4 1//1 +f 4//4 14//14 16//16 +f 3//3 14//14 1//1 +f 14//14 3//3 15//15 +# 28 faces, 0 coords texture + +# End of File diff --git a/tests/sla_print/CMakeLists.txt b/tests/sla_print/CMakeLists.txt index 24e9552c5b..2a800cc50e 100644 --- a/tests/sla_print/CMakeLists.txt +++ b/tests/sla_print/CMakeLists.txt @@ -4,6 +4,7 @@ add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp sla_test_utils.hpp sla_test_utils.cpp sla_supptgen_tests.cpp sla_raycast_tests.cpp + sla_supptreeutils_tests.cpp sla_archive_readwrite_tests.cpp) # mold linker for successful linking needs also to link TBB library and link it before libslic3r. diff --git a/tests/sla_print/sla_print_tests.cpp b/tests/sla_print/sla_print_tests.cpp index 8ea91d57a1..a733e77cc5 100644 --- a/tests/sla_print/sla_print_tests.cpp +++ b/tests/sla_print/sla_print_tests.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -32,13 +31,6 @@ const char *const SUPPORT_TEST_MODELS[] = { } // namespace -TEST_CASE("Pillar pairhash should be unique", "[SLASupportGeneration]") { - test_pairhash(); - test_pairhash(); - test_pairhash(); - test_pairhash(); -} - TEST_CASE("Support point generator should be deterministic if seeded", "[SLASupportGeneration], [SLAPointGen]") { TriangleMesh mesh = load_model("A_upsidedown.obj"); @@ -173,106 +165,6 @@ TEST_CASE("DefaultSupports::FloorSupportsDoNotPierceModel", "[SLASupportGenerati // for (auto &fname: SUPPORT_TEST_MODELS) test_supports(fname, supportcfg); //} -bool is_outside_support_cone(const Vec3f &supp, const Vec3f &pt, float angle) -{ - Vec3d D = (pt - supp).cast(); - double dot_sq = -D.z() * std::abs(-D.z()); - - return dot_sq < - D.squaredNorm() * std::cos(angle) * std::abs(std::cos(angle)); -} - -TEST_CASE("BranchingSupports::MergePointFinder", "[SLASupportGeneration][Branching]") { - SECTION("Identical points have the same merge point") { - Vec3f a{0.f, 0.f, 0.f}, b = a; - auto slope = float(PI / 4.); - - auto mergept = branchingtree::find_merge_pt(a, b, slope); - - REQUIRE(bool(mergept)); - REQUIRE((*mergept - b).norm() < EPSILON); - REQUIRE((*mergept - a).norm() < EPSILON); - } - - // ^ Z - // | a * - // | - // | b * <= mergept - SECTION("Points at different heights have the lower point as mergepoint") { - Vec3f a{0.f, 0.f, 0.f}, b = {0.f, 0.f, -1.f}; - auto slope = float(PI / 4.); - - auto mergept = branchingtree::find_merge_pt(a, b, slope); - - REQUIRE(bool(mergept)); - REQUIRE((*mergept - b).squaredNorm() < 2 * EPSILON); - } - - // -|---------> X - // a b - // * * - // * <= mergept - SECTION("Points at different X have mergept in the middle at lower Z") { - Vec3f a{0.f, 0.f, 0.f}, b = {1.f, 0.f, 0.f}; - auto slope = float(PI / 4.); - - auto mergept = branchingtree::find_merge_pt(a, b, slope); - - REQUIRE(bool(mergept)); - - // Distance of mergept should be equal from both input points - float D = std::abs((*mergept - b).squaredNorm() - (*mergept - a).squaredNorm()); - - REQUIRE(D < EPSILON); - REQUIRE(!is_outside_support_cone(a, *mergept, slope)); - REQUIRE(!is_outside_support_cone(b, *mergept, slope)); - } - - // -|---------> Y - // a b - // * * - // * <= mergept - SECTION("Points at different Y have mergept in the middle at lower Z") { - Vec3f a{0.f, 0.f, 0.f}, b = {0.f, 1.f, 0.f}; - auto slope = float(PI / 4.); - - auto mergept = branchingtree::find_merge_pt(a, b, slope); - - REQUIRE(bool(mergept)); - - // Distance of mergept should be equal from both input points - float D = std::abs((*mergept - b).squaredNorm() - (*mergept - a).squaredNorm()); - - REQUIRE(D < EPSILON); - REQUIRE(!is_outside_support_cone(a, *mergept, slope)); - REQUIRE(!is_outside_support_cone(b, *mergept, slope)); - } - - SECTION("Points separated by less than critical angle have the lower point as mergept") { - Vec3f a{-1.f, -1.f, -1.f}, b = {-1.5f, -1.5f, -2.f}; - auto slope = float(PI / 4.); - - auto mergept = branchingtree::find_merge_pt(a, b, slope); - - REQUIRE(bool(mergept)); - REQUIRE((*mergept - b).norm() < 2 * EPSILON); - } - - // -|----------------------------> Y - // a b - // * * <= mergept * - // - SECTION("Points at same height have mergepoint in the middle if critical angle is zero ") { - Vec3f a{-1.f, -1.f, -1.f}, b = {-1.5f, -1.5f, -1.f}; - auto slope = EPSILON; - - auto mergept = branchingtree::find_merge_pt(a, b, slope); - - REQUIRE(bool(mergept)); - Vec3f middle = (b + a) / 2.; - REQUIRE((*mergept - middle).norm() < 4 * EPSILON); - } -} TEST_CASE("BranchingSupports::ElevatedSupportsDoNotPierceModel", "[SLASupportGeneration][Branching]") { diff --git a/tests/sla_print/sla_supptreeutils_tests.cpp b/tests/sla_print/sla_supptreeutils_tests.cpp new file mode 100644 index 0000000000..c8abbb8315 --- /dev/null +++ b/tests/sla_print/sla_supptreeutils_tests.cpp @@ -0,0 +1,377 @@ +#include +#include + +#include + +#include "libslic3r/Execution/ExecutionSeq.hpp" +#include "libslic3r/SLA/SupportTreeUtils.hpp" +#include "libslic3r/SLA/SupportTreeUtilsLegacy.hpp" + +// Test pair hash for 'nums' random number pairs. +template void test_pairhash() +{ + const constexpr size_t nums = 1000; + I A[nums] = {0}, B[nums] = {0}; + std::unordered_set CH; + std::unordered_map> ints; + + std::random_device rd; + std::mt19937 gen(rd()); + + const I Ibits = int(sizeof(I) * CHAR_BIT); + const II IIbits = int(sizeof(II) * CHAR_BIT); + + int bits = IIbits / 2 < Ibits ? Ibits / 2 : Ibits; + if (std::is_signed::value) bits -= 1; + const I Imin = 0; + const I Imax = I(std::pow(2., bits) - 1); + + std::uniform_int_distribution dis(Imin, Imax); + + for (size_t i = 0; i < nums;) { + I a = dis(gen); + if (CH.find(a) == CH.end()) { CH.insert(a); A[i] = a; ++i; } + } + + for (size_t i = 0; i < nums;) { + I b = dis(gen); + if (CH.find(b) == CH.end()) { CH.insert(b); B[i] = b; ++i; } + } + + for (size_t i = 0; i < nums; ++i) { + I a = A[i], b = B[i]; + + REQUIRE(a != b); + + II hash_ab = Slic3r::sla::pairhash(a, b); + II hash_ba = Slic3r::sla::pairhash(b, a); + REQUIRE(hash_ab == hash_ba); + + auto it = ints.find(hash_ab); + + if (it != ints.end()) { + REQUIRE(( + (it->second.first == a && it->second.second == b) || + (it->second.first == b && it->second.second == a) + )); + } else + ints[hash_ab] = std::make_pair(a, b); + } +} + +TEST_CASE("Pillar pairhash should be unique", "[suptreeutils]") { + test_pairhash(); + test_pairhash(); + test_pairhash(); + test_pairhash(); +} + +static void eval_ground_conn(const Slic3r::sla::GroundConnection &conn, + const Slic3r::sla::SupportableMesh &sm, + const Slic3r::sla::Junction &j, + double end_r, + const std::string &stl_fname = "output.stl") +{ + using namespace Slic3r; + +//#ifndef NDEBUG + + sla::SupportTreeBuilder builder; + + if (!conn) + builder.add_junction(j); + + sla::build_ground_connection(builder, sm, conn); + + indexed_triangle_set mesh = *sm.emesh.get_triangle_mesh(); + its_merge(mesh, builder.merged_mesh()); + + its_write_stl_ascii(stl_fname.c_str(), "stl_fname", mesh); +//#endif + + REQUIRE(bool(conn)); + + // The route should include the source and one avoidance junction. + REQUIRE(conn.path.size() == 2); + + // Check if the radius increases with each node + REQUIRE(conn.path.front().r < conn.path.back().r); + REQUIRE(conn.path.back().r < conn.pillar_base->r_top); + + // The end radius and the pillar base's upper radius should match + REQUIRE(conn.pillar_base->r_top == Approx(end_r)); +} + +TEST_CASE("Pillar search dumb case", "[suptreeutils]") { + using namespace Slic3r; + + constexpr double FromR = 0.5; + auto j = sla::Junction{Vec3d::Zero(), FromR}; + + SECTION("with empty mesh") { + sla::SupportableMesh sm{indexed_triangle_set{}, + sla::SupportPoints{}, + sla::SupportTreeConfig{}}; + + constexpr double EndR = 1.; + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndR, sla::DOWN); + + REQUIRE(conn); +// REQUIRE(conn.path.size() == 1); + REQUIRE(conn.pillar_base->pos.z() == Approx(ground_level(sm))); + } + + SECTION("with zero R source and destination") { + sla::SupportableMesh sm{indexed_triangle_set{}, + sla::SupportPoints{}, + sla::SupportTreeConfig{}}; + + j.r = 0.; + constexpr double EndR = 0.; + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndR, sla::DOWN); + + REQUIRE(conn); +// REQUIRE(conn.path.size() == 1); + REQUIRE(conn.pillar_base->pos.z() == Approx(ground_level(sm))); + REQUIRE(conn.pillar_base->r_top == Approx(0.)); + } + + SECTION("with zero init direction") { + sla::SupportableMesh sm{indexed_triangle_set{}, + sla::SupportPoints{}, + sla::SupportTreeConfig{}}; + + constexpr double EndR = 1.; + Vec3d init_dir = Vec3d::Zero(); + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndR, init_dir); + + REQUIRE(conn); +// REQUIRE(conn.path.size() == 1); + REQUIRE(conn.pillar_base->pos.z() == Approx(ground_level(sm))); + } +} + +TEST_CASE("Avoid disk below junction", "[suptreeutils]") +{ + // In this test there will be a disk mesh with some radius, centered at + // (0, 0, 0) and above the disk, a junction from which the support pillar + // should be routed. The algorithm needs to find an avoidance route. + + using namespace Slic3r; + + constexpr double FromRadius = .5; + constexpr double EndRadius = 1.; + constexpr double CylRadius = 4.; + constexpr double CylHeight = 1.; + + sla::SupportTreeConfig cfg; + + indexed_triangle_set disk = its_make_cylinder(CylRadius, CylHeight); + + // 2.5 * CyRadius height should be enough to be able to insert a bridge + // with 45 degree tilt above the disk. + sla::Junction j{Vec3d{0., 0., 2.5 * CylRadius}, FromRadius}; + + sla::SupportableMesh sm{disk, sla::SupportPoints{}, cfg}; + + SECTION("with elevation") { + + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_tbb, sm, j, EndRadius, sla::DOWN); + + eval_ground_conn(conn, sm, j, EndRadius, "disk.stl"); + + // Check if the avoidance junction is indeed outside of the disk barrier's + // edge. + auto p = conn.path.back().pos; + double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y()); + REQUIRE(pR + FromRadius > CylRadius); + } + + SECTION("without elevation") { + sm.cfg.object_elevation_mm = 0.; + + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_tbb, sm, j, EndRadius, sla::DOWN); + + eval_ground_conn(conn, sm, j, EndRadius, "disk_ze.stl"); + + // Check if the avoidance junction is indeed outside of the disk barrier's + // edge. + auto p = conn.path.back().pos; + double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y()); + REQUIRE(pR + FromRadius > CylRadius); + } +} + +TEST_CASE("Avoid disk below junction with barrier on the side", "[suptreeutils]") +{ + // In this test there will be a disk mesh with some radius, centered at + // (0, 0, 0) and above the disk, a junction from which the support pillar + // should be routed. The algorithm needs to find an avoidance route. + + using namespace Slic3r; + + constexpr double FromRadius = .5; + constexpr double EndRadius = 1.; + constexpr double CylRadius = 4.; + constexpr double CylHeight = 1.; + constexpr double JElevX = 2.5; + + sla::SupportTreeConfig cfg; + + indexed_triangle_set disk = its_make_cylinder(CylRadius, CylHeight); + indexed_triangle_set wall = its_make_cube(1., 2 * CylRadius, JElevX * CylRadius); + its_translate(wall, Vec3f{float(FromRadius), -float(CylRadius), 0.f}); + its_merge(disk, wall); + + // 2.5 * CyRadius height should be enough to be able to insert a bridge + // with 45 degree tilt above the disk. + sla::Junction j{Vec3d{0., 0., JElevX * CylRadius}, FromRadius}; + + sla::SupportableMesh sm{disk, sla::SupportPoints{}, cfg}; + + SECTION("with elevation") { + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndRadius, sla::DOWN); + + eval_ground_conn(conn, sm, j, EndRadius, "disk_with_barrier.stl"); + + // Check if the avoidance junction is indeed outside of the disk barrier's + // edge. + auto p = conn.path.back().pos; + double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y()); + REQUIRE(pR + FromRadius > CylRadius); + } + + SECTION("without elevation") { + sm.cfg.object_elevation_mm = 0.; + + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndRadius, sla::DOWN); + + eval_ground_conn(conn, sm, j, EndRadius, "disk_with_barrier_ze.stl"); + + // Check if the avoidance junction is indeed outside of the disk barrier's + // edge. + auto p = conn.path.back().pos; + double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y()); + REQUIRE(pR + FromRadius > CylRadius); + } +} + +TEST_CASE("Find ground route just above ground", "[suptreeutils]") { + using namespace Slic3r; + + sla::SupportTreeConfig cfg; + cfg.object_elevation_mm = 0.; + + sla::Junction j{Vec3d{0., 0., 2. * cfg.head_back_radius_mm}, cfg.head_back_radius_mm}; + + sla::SupportableMesh sm{{}, sla::SupportPoints{}, cfg}; + + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, Geometry::spheric_to_dir(3 * PI/ 4, PI)); + + REQUIRE(conn); + + REQUIRE(conn.pillar_base->pos.z() >= Approx(ground_level(sm))); +} + +TEST_CASE("BranchingSupports::MergePointFinder", "[suptreeutils]") { + using namespace Slic3r; + + SECTION("Identical points have the same merge point") { + Vec3f a{0.f, 0.f, 0.f}, b = a; + auto slope = float(PI / 4.); + + auto mergept = sla::find_merge_pt(a, b, slope); + + REQUIRE(bool(mergept)); + REQUIRE((*mergept - b).norm() < EPSILON); + REQUIRE((*mergept - a).norm() < EPSILON); + } + + // ^ Z + // | a * + // | + // | b * <= mergept + SECTION("Points at different heights have the lower point as mergepoint") { + Vec3f a{0.f, 0.f, 0.f}, b = {0.f, 0.f, -1.f}; + auto slope = float(PI / 4.); + + auto mergept = sla::find_merge_pt(a, b, slope); + + REQUIRE(bool(mergept)); + REQUIRE((*mergept - b).squaredNorm() < 2 * EPSILON); + } + + // -|---------> X + // a b + // * * + // * <= mergept + SECTION("Points at different X have mergept in the middle at lower Z") { + Vec3f a{0.f, 0.f, 0.f}, b = {1.f, 0.f, 0.f}; + auto slope = float(PI / 4.); + + auto mergept = sla::find_merge_pt(a, b, slope); + + REQUIRE(bool(mergept)); + + // Distance of mergept should be equal from both input points + float D = std::abs((*mergept - b).squaredNorm() - (*mergept - a).squaredNorm()); + + REQUIRE(D < EPSILON); + REQUIRE(!sla::is_outside_support_cone(a, *mergept, slope)); + REQUIRE(!sla::is_outside_support_cone(b, *mergept, slope)); + } + + // -|---------> Y + // a b + // * * + // * <= mergept + SECTION("Points at different Y have mergept in the middle at lower Z") { + Vec3f a{0.f, 0.f, 0.f}, b = {0.f, 1.f, 0.f}; + auto slope = float(PI / 4.); + + auto mergept = sla::find_merge_pt(a, b, slope); + + REQUIRE(bool(mergept)); + + // Distance of mergept should be equal from both input points + float D = std::abs((*mergept - b).squaredNorm() - (*mergept - a).squaredNorm()); + + REQUIRE(D < EPSILON); + REQUIRE(!sla::is_outside_support_cone(a, *mergept, slope)); + REQUIRE(!sla::is_outside_support_cone(b, *mergept, slope)); + } + + SECTION("Points separated by less than critical angle have the lower point as mergept") { + Vec3f a{-1.f, -1.f, -1.f}, b = {-1.5f, -1.5f, -2.f}; + auto slope = float(PI / 4.); + + auto mergept = sla::find_merge_pt(a, b, slope); + + REQUIRE(bool(mergept)); + REQUIRE((*mergept - b).norm() < 2 * EPSILON); + } + + // -|----------------------------> Y + // a b + // * * <= mergept * + // + SECTION("Points at same height have mergepoint in the middle if critical angle is zero ") { + Vec3f a{-1.f, -1.f, -1.f}, b = {-1.5f, -1.5f, -1.f}; + auto slope = EPSILON; + + auto mergept = sla::find_merge_pt(a, b, slope); + + REQUIRE(bool(mergept)); + Vec3f middle = (b + a) / 2.; + REQUIRE((*mergept - middle).norm() < 4 * EPSILON); + } +} + diff --git a/tests/sla_print/sla_test_utils.cpp b/tests/sla_print/sla_test_utils.cpp index d681d13900..f294abccb8 100644 --- a/tests/sla_print/sla_test_utils.cpp +++ b/tests/sla_print/sla_test_utils.cpp @@ -118,7 +118,7 @@ void test_supports(const std::string &obj_filename, // Create the special index-triangle mesh with spatial indexing which // is the input of the support point and support mesh generators - AABBMesh emesh{mesh}; + sla::SupportableMesh sm{mesh.its, {}, supportcfg}; #ifdef SLIC3R_HOLE_RAYCASTER if (hollowingcfg.enabled) @@ -130,23 +130,23 @@ void test_supports(const std::string &obj_filename, // Create the support point generator sla::SupportPointGenerator::Config autogencfg; autogencfg.head_diameter = float(2 * supportcfg.head_front_radius_mm); - sla::SupportPointGenerator point_gen{emesh, autogencfg, [] {}, [](int) {}}; + sla::SupportPointGenerator point_gen{sm.emesh, autogencfg, [] {}, [](int) {}}; point_gen.seed(0); // Make the test repeatable point_gen.execute(out.model_slices, out.slicegrid); // Get the calculated support points. - std::vector support_points = point_gen.output(); + sm.pts = point_gen.output(); int validityflags = ASSUME_NO_REPAIR; // If there is no elevation, support points shall be removed from the // bottom of the object. if (std::abs(supportcfg.object_elevation_mm) < EPSILON) { - sla::remove_bottom_points(support_points, zmin + supportcfg.base_height_mm); + sla::remove_bottom_points(sm.pts, zmin + supportcfg.base_height_mm); } else { // Should be support points at least on the bottom of the model - REQUIRE_FALSE(support_points.empty()); + REQUIRE_FALSE(sm.pts.empty()); // Also the support mesh should not be empty. validityflags |= ASSUME_NO_EMPTY; @@ -154,7 +154,6 @@ void test_supports(const std::string &obj_filename, // Generate the actual support tree sla::SupportTreeBuilder treebuilder; - sla::SupportableMesh sm{emesh, support_points, supportcfg}; switch (sm.cfg.tree_type) { case sla::SupportTreeType::Default: { @@ -182,6 +181,18 @@ void test_supports(const std::string &obj_filename, if (std::abs(supportcfg.object_elevation_mm) < EPSILON) allowed_zmin = zmin - 2 * supportcfg.head_back_radius_mm; +#ifndef NDEBUG + if (!(obb.min.z() >= Approx(allowed_zmin)) || !(obb.max.z() <= Approx(zmax))) + { + indexed_triangle_set its; + treebuilder.retrieve_full_mesh(its); + TriangleMesh m{its}; + m.merge(mesh); + m.WriteOBJFile((Catch::getResultCapture().getCurrentTestName() + "_" + + obj_filename).c_str()); + } +#endif + REQUIRE(obb.min.z() >= Approx(allowed_zmin)); REQUIRE(obb.max.z() <= Approx(zmax)); diff --git a/tests/sla_print/sla_test_utils.hpp b/tests/sla_print/sla_test_utils.hpp index 187a72d545..103b2f66a4 100644 --- a/tests/sla_print/sla_test_utils.hpp +++ b/tests/sla_print/sla_test_utils.hpp @@ -14,14 +14,12 @@ #include "libslic3r/TriangleMesh.hpp" #include "libslic3r/SLA/Pad.hpp" #include "libslic3r/SLA/SupportTreeBuilder.hpp" -#include "libslic3r/SLA/SupportTreeUtils.hpp" #include "libslic3r/SLA/SupportPointGenerator.hpp" #include "libslic3r/SLA/AGGRaster.hpp" #include "libslic3r/SLA/ConcaveHull.hpp" #include "libslic3r/MTUtils.hpp" #include "libslic3r/SVG.hpp" -#include "libslic3r/Format/OBJ.hpp" using namespace Slic3r; @@ -111,58 +109,6 @@ inline void test_support_model_collision( test_support_model_collision(obj_filename, input_supportcfg, hcfg, {}); } -// Test pair hash for 'nums' random number pairs. -template void test_pairhash() -{ - const constexpr size_t nums = 1000; - I A[nums] = {0}, B[nums] = {0}; - std::unordered_set CH; - std::unordered_map> ints; - - std::random_device rd; - std::mt19937 gen(rd()); - - const I Ibits = int(sizeof(I) * CHAR_BIT); - const II IIbits = int(sizeof(II) * CHAR_BIT); - - int bits = IIbits / 2 < Ibits ? Ibits / 2 : Ibits; - if (std::is_signed::value) bits -= 1; - const I Imin = 0; - const I Imax = I(std::pow(2., bits) - 1); - - std::uniform_int_distribution dis(Imin, Imax); - - for (size_t i = 0; i < nums;) { - I a = dis(gen); - if (CH.find(a) == CH.end()) { CH.insert(a); A[i] = a; ++i; } - } - - for (size_t i = 0; i < nums;) { - I b = dis(gen); - if (CH.find(b) == CH.end()) { CH.insert(b); B[i] = b; ++i; } - } - - for (size_t i = 0; i < nums; ++i) { - I a = A[i], b = B[i]; - - REQUIRE(a != b); - - II hash_ab = sla::pairhash(a, b); - II hash_ba = sla::pairhash(b, a); - REQUIRE(hash_ab == hash_ba); - - auto it = ints.find(hash_ab); - - if (it != ints.end()) { - REQUIRE(( - (it->second.first == a && it->second.second == b) || - (it->second.first == b && it->second.second == a) - )); - } else - ints[hash_ab] = std::make_pair(a, b); - } -} - // SLA Raster test utils: using TPixel = uint8_t;