diff --git a/CMakeLists.txt b/CMakeLists.txt index 2bfb01a657..a208c5e7a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1084,10 +1084,10 @@ endif () find_path(SPNAV_INCLUDE_DIR spnav.h) if (SPNAV_INCLUDE_DIR) - find_library(SPNAV_LIB NAMES libspnav.a) # Force linking libspnav statically + find_library(SPNAV_LIB NAMES libspnav.a spnav) if (SPNAV_LIB) add_definitions(-DHAVE_SPNAV) - message(STATUS "SPNAV library found") + message(STATUS "SPNAV library found: ${SPNAV_LIB}") else() message(STATUS "SPNAV library NOT found, Spacenavd not supported") endif() diff --git a/docs/HLSD/separated-infills.md b/docs/HLSD/separated-infills.md index b7f12f4199..10742eb042 100644 --- a/docs/HLSD/separated-infills.md +++ b/docs/HLSD/separated-infills.md @@ -79,6 +79,13 @@ the same `infill_body()`. The octree of the whole object is built only for an object of a single body, or when some body received no triangles, which then uses it. +The line spacing of an octree comes from the density, line width and multiline +count of a region, so a modifier or a part with its own density needs octrees of +its own. `adaptive_fill_line_spacing()` gives the spacing of each region, and +`FillAdaptive::RegionOctrees` holds one set of octrees per distinct spacing, +shared by the regions that have it. A set is built only for the bodies its +regions fill. The fill takes the set of its region, then the octree of its body. + ## Patterns left out Lightning grows its trees over the whole object, so moving a reference point diff --git a/resources/images/toolbar_tooltip.svg b/resources/images/toolbar_tooltip.svg index 96c5684cb0..49279b8ed9 100644 --- a/resources/images/toolbar_tooltip.svg +++ b/resources/images/toolbar_tooltip.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/resources/images/toolbar_tooltip_hover.svg b/resources/images/toolbar_tooltip_hover.svg deleted file mode 100644 index c8d606829e..0000000000 --- a/resources/images/toolbar_tooltip_hover.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/resources/images/toolbar_video_guide.svg b/resources/images/toolbar_video_guide.svg new file mode 100644 index 0000000000..b39e485af2 --- /dev/null +++ b/resources/images/toolbar_video_guide.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/toolbar_wiki.svg b/resources/images/toolbar_wiki.svg new file mode 100644 index 0000000000..b10b1dcb17 --- /dev/null +++ b/resources/images/toolbar_wiki.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/profiles/BBL/machine/Bambu Lab P2S 0.4 nozzle template machine_start_gcode.json b/resources/profiles/BBL/machine/Bambu Lab P2S 0.4 nozzle template machine_start_gcode.json index a7dee998fb..5abab59c71 100644 --- a/resources/profiles/BBL/machine/Bambu Lab P2S 0.4 nozzle template machine_start_gcode.json +++ b/resources/profiles/BBL/machine/Bambu Lab P2S 0.4 nozzle template machine_start_gcode.json @@ -2,5 +2,5 @@ "type": "machine", "name": "Bambu Lab P2S 0.4 nozzle template machine_start_gcode", "instantiation": "false", - "machine_start_gcode": ";M1002 set_flag extrude_cali_flag=1\n;M1002 set_flag g29_before_print_flag=1\n;M1002 set_flag auto_cali_toolhead_offset_flag=1\n;M1002 set_flag build_plate_detect_flag=1\n\n;======== P2S start gcode==========\n;===== 2026/05/18 =====\n\n M140 S[bed_temperature_initial_layer_single] ; heat heatbed first\n M993 A0 B0 C0 ; nozzle cam detection not allowed.\n M400\n\n;=====printer start sound ===================\nM17\nM400 S1\nM1006 S1\nM1006 A53 B9 L50 C53 D9 M50 E53 F9 N50\nM1006 A56 B9 L50 C56 D9 M50 E56 F9 N50\nM1006 A61 B9 L50 C61 D9 M50 E61 F9 N50\nM1006 A53 B9 L50 C53 D9 M50 E53 F9 N50\nM1006 A56 B9 L50 C56 D9 M50 E56 F9 N50\nM1006 A61 B18 L50 C61 D18 M50 E61 F18 N50\nM1006 W\n;=====printer start sound ===================\n\n M620 M ;enable remap\n G389\n\n;===== avoid end stop =================\n G91\n G380 S2 Z22 F1200\n G380 S2 Z-12 F1200\n G90\n;===== avoid end stop =================\n\n;===== reset machine status =================\n M204 S10000\n M630 S0 P1\n G90\n M17 D ; reset motor current to default\n M960 S5 P1 ; turn on logo lamp\n G90\n M220 S100 ;Reset Feedrate\n M1002 set_gcode_claim_speed_level: 5\n M221 S100 ;Reset Flowrate\n M73.2 R1.0 ;Reset left time magnitude\n G29.1 Z{+0.0} ; clear z-trim value first\n M983.1 M1\n M982.2 S1 ; turn on cog noise reduction\n M983.4 S0\n;===== reset machine status =================\n\n;==== set airduct mode ====\n;==== if Chamber Cooling is necessary ====\n{if (overall_chamber_temperature >= 40)}\nM145 P1 ; set airduct mode to heating mode for heating\nM106 P2 S255 ; turn on filter fan\nM622.1 S0\nM1002 judge_flag ventobox_replace_aux1_fan_flag\nM622 J0\nM106 P10 S0 ; turn off left aux fan\nM623\n{else}\n{if (min_vitrification_temperature <= 50)}\nM145 P0 ; set airduct mode to cooling mode for cooling\nM106 P2 S255 ; turn on auxiliary fan for cooling\nM106 P3 S127 ; turn on chamber fan for cooling\nM1002 gcode_claim_action : 29\nM191 S0 ; wait for chamber temp\nM106 P2 S102 ; turn on chamber cooling fan\nM622.1 S0\nM1002 judge_flag ventobox_replace_aux1_fan_flag\nM622 J0\nM106 P10 S0 ; turn off left aux fan\nM623\nM142 P6 R30 S40 U0.3 V0.8 ; set PETG exhaust chamber autocooling\n{else}\nM145 P1 ; set airduct mode to heating mode for heating\nM106 P2 S127 ; turn on 50% filter fan\nM142 P6 R30 S40 U0.3 V0.8 ; set PLA/TPU exhaust chamber autocooling\n{endif}\n{endif}\n;==== set airduct mode ====\n\n;===== start to heat heatbed & hotend==========\n M1002 gcode_claim_action : 2\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M104 S140 A\n\n G29.2 S0 ; avoid invalid abl data\n\n;===== first homing start =====\n M1002 gcode_claim_action : 13\n G28 X T300\n G150.1 F8000 ; wipe mouth to avoid filament stick to heatbed\n G150.3\n M972 S24 P0\n M972 S26 P0 C0\n M972 S42 P0 T5000\n G150.1 F8000 ; wipe mouth to avoid filament stick to heatbed\n G90\n G1 X128 Y128 F30000\n G28 Z P0 T400\n M400\n;===== first homign end =====\n\n;===== detection start =====\n M1002 gcode_claim_action : 11\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]-80} A ; rise temp in advance\n M972 S19 P0 T5000 ;plate type detection\n\n {if max_print_z >= 145}\n M1002 gcode_claim_action : 75 ; Detect obstacles at the botton of the heated bed\n G150.3\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]} ; rise temp in advance\n G3811 Z{max_print_z} ; Detect obstacles at the bottom of the heated bed\n {endif}\n;===== detection end =====\n\n;===== prepare print temperature and material ==========\n M400\n M211 X0 Y0 Z0 ;turn off soft endstop\n M975 S1 ; turn on input shaping\n\n G29.2 S0 ; avoid invalid abl data\n G150.3\n{if ((filament_type[initial_no_support_filament_id] == \"PLA\") || (filament_type[initial_no_support_filament_id] == \"PLA-CF\") || (filament_type[initial_no_support_filament_id] == \"PETG\")) && (nozzle_diameter_at_nozzle_id[initial_nozzle_id] == 0.2)}\nM620.10 A0 F74.8347 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\nM620.10 A1 F74.8347 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\n{else}\nM620.10 A0 F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60} H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\nM620.10 A1 F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60} H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\n{endif}\n\n M620.11 P0 L0 I[initial_no_support_filament_id] E0\n M620.11 K0 I[initial_no_support_filament_id] R0\n\n M620 S[initial_no_support_filament_id]A ; switch material if AMS exist\n M1002 gcode_claim_action : 4\n M1002 set_filament_type:UNKNOWN\n M400\n T[initial_no_support_filament_id]\n M400\n M628 S0\n M629\n M400\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M621 S[initial_no_support_filament_id]A\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n M400\n M106 P1 S0\n M400\n G29.2 S1\n;===== prepare print temperature and material ==========\n\n\n;===== auto extrude cali start =========================\n M975 S1\n M1002 judge_flag extrude_cali_flag\n M622 J0\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n M623\n\n M622 J1\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M1002 gcode_claim_action : 8\n M109 S{nozzle_temperature[initial_no_support_filament_id]}\n G90\n M83\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n M400\n M106 P1 S255\n M400 S5\n M106 P1 S0\n G150.3\n M623\n\n M622 J2\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M1002 gcode_claim_action : 8\n M109 S{nozzle_temperature[initial_no_support_filament_id]}\n G90\n M83\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n M400\n M106 P1 S255\n M400 S5\n M106 P1 S0\n G150.3\n M623\n;===== auto extrude cali end =========================\n\n {if hold_chamber_temp_for_flat_print}\n M1002 gcode_claim_action : 58\n M104 S{first_layer_temperature[initial_no_support_filament_id]}\n {if bed_temperature_initial_layer_single > 89}\n M1030 S1800\n SYNC R0 T1800\n {else}\n M1030 S300\n SYNC R0 T300\n {endif}\n M1030 C\n {endif}\n\n {if filament_type[initial_filament_id] == \"TPU\" || filament_type[initial_filament_id] == \"PVA\"}\n {else}\n M83\n G1 E-3 F1800\n M400 P500\n {endif}\n G150.2\n G150.1 F8000\n G150.2\n G150.1 F8000\n\n G91\n G1 Y-16 F12000 ; move away from the trash bin\n G90\n M400\n\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]-80} A\n\n;===== wipe right nozzle start =====\n M1002 gcode_claim_action : 14\n G150 T{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n M400\n\n{if filament_type[initial_filament_id] == \"PC\"}\n M109 S170 A\n{else}\n M109 S140 A\n{endif}\n G91\n G1 Z5 F1200\n G90\n M400\n G150.1\n;===== wipe left nozzle end =====\n\n\n;===== mech mode sweep start =====\n M1002 gcode_claim_action : 3\n G90\n G1 X128 Y128 F20000\n G1 Z5 F1200\n M400 P200\n M970.3 Q1 A5 K0 O1\n M970.2 Q1 K1 W74 Z0.01\n M974 Q1 S2 P0\n M970.3 Q0 A7 K0 O1\n M970.2 Q0 K1 W74 Z0.01\n M974 Q0 S2 P0\n M975 S1\n M400\n;===== mech mode sweep end =====\n\n;===== bed leveling ==================================\n M1002 gcode_claim_action : 54\n M190 S[bed_temperature_initial_layer_single]; ensure bed temp\n M109 S140 A\n M106 S0 ; turn off fan , too noisy\n M1002 judge_flag g29_before_print_flag\n M622 J1\n M1002 gcode_claim_action : 1\n {if hold_chamber_temp_for_flat_print}\n G29 H\n {else}\n G29 A1 X{first_layer_print_min[0]} Y{first_layer_print_min[1]} I{first_layer_print_size[0]} J{first_layer_print_size[1]}\n {endif}\n M400\n M623\n\n M622 J2\n M1002 gcode_claim_action : 1\n {if hold_chamber_temp_for_flat_print}\n G29 H\n {else}\n G29 A2 X{first_layer_print_min[0]} Y{first_layer_print_min[1]} I{first_layer_print_size[0]} J{first_layer_print_size[1]}\n {endif}\n M400\n M623\n\n M622 J0\n G28\n M623\n G29.2 S1\n G28\n;===== bed leveling end ================================\n\n M985.1 U0 E2\n M985.1 U1 E2\n\n M104 S{nozzle_temperature_initial_layer[initial_filament_id]} A\n G150.3 ; move to garbage can to wait for temp\n\n;===== wait temperature reaching the reference value =======\n M190 S[bed_temperature_initial_layer_single]\n\n ;========turn off light and fans =============\n M960 S1 P0 ; turn off laser\n M960 S2 P0 ; turn off laser\n M106 S0 ; turn off cooling fan\n\n;===== wait temperature reaching the reference value =======\n\n M1002 gcode_claim_action : 255\n M400\n M975 S1 ; turn on mech mode supression\n\n;============switch again==================\n M211 X0 Y0 Z0 ;turn off soft endstop\n G91\n G1 Z6 F1200\n G90\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M620 S[initial_no_support_filament_id]A\n M400\n T[initial_no_support_filament_id]\n M400\n M628 S0\n M629\n M400\n M621 S[initial_no_support_filament_id]A\n;============switch again==================\n\n;===== for Textured PEI Plate , lower the nozzle as the nozzle was touching topmost of the texture when homing ==\n {if bed_temperature_initial_layer_single > 89}\n {if curr_bed_type==\"Textured PEI Plate\"}\n G29.1 Z{-0.02} ; for Textured PEI Plate\n {else}\n G29.1 Z{0.0}\n {endif}\n {else}\n {if curr_bed_type==\"Textured PEI Plate\"}\n G29.1 Z{0.01} ; for Textured PEI Plate\n {else}\n G29.1 Z{0.03}\n {endif}\n {endif}\n\n\n;===== nozzle load line ===============================\nM1002 gcode_claim_action : 51\n G29.2 S1 ; ensure z comp turn on\n G90\n M83\n M400 P50\n M500 D1\n M400 S3\n M109 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n G0 X100 Y0 F24000\n M400\n ;G130 O0 X100 Y-0.4 Z0.8 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2/2.4053} L40 E20 D5\n G130 O0 X100 Y-0.2 Z0.6 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2/2.4053} L40 E12 D4\n G90\n M83\n G1 Z1\n M400\n;===== noozle load line end ===========================\nM1002 gcode_claim_action : 0\n G29.99\n\n{if (filament_type[initial_no_support_filament_id] == \"TPU\") ||\n(filament_type[initial_no_support_filament_id] == \"PLA\") || (filament_type[initial_no_support_filament_id] == \"PETG\")}\nM1015.3 S1 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]};enable tpu, pla and petg clog detect\n{else}\nM1015.3 S0;disable clog detect\n{endif}\n\n{if (filament_type[initial_no_support_filament_id] == \"PLA\") || (filament_type[initial_no_support_filament_id] == \"PETG\")\n || (filament_type[initial_no_support_filament_id] == \"PLA-CF\") || (filament_type[initial_no_support_filament_id] == \"PETG-CF\")}\nM1015.4 S1 K1 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} ;enable E air printing detect\n{else}\nM1015.4 S0 K0 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} ;disable E air printing detect\n{endif}\n\nM620.6 I[initial_no_support_filament_id] W1 ;enable ams air printing detect\n\nM1010 Q0 B0.023 S0.01\nM1010 Q1 B0.005 S0.01\nM1010.1 S1\n" + "machine_start_gcode": ";M1002 set_flag extrude_cali_flag=1\n;M1002 set_flag g29_before_print_flag=1\n;M1002 set_flag auto_cali_toolhead_offset_flag=1\n;M1002 set_flag build_plate_detect_flag=1\n\n;======== P2S start gcode==========\n;===== Based on official 2026/05/18 start gcode =====\n\n M140 S[bed_temperature_initial_layer_single] ; heat heatbed first\n M993 A0 B0 C0 ; nozzle cam detection not allowed.\n M400\n\n;=====printer start sound ===================\nM17\nM400 S1\nM1006 S1\nM1006 A53 B9 L50 C53 D9 M50 E53 F9 N50\nM1006 A56 B9 L50 C56 D9 M50 E56 F9 N50\nM1006 A61 B9 L50 C61 D9 M50 E61 F9 N50\nM1006 A53 B9 L50 C53 D9 M50 E53 F9 N50\nM1006 A56 B9 L50 C56 D9 M50 E56 F9 N50\nM1006 A61 B18 L50 C61 D18 M50 E61 F18 N50\nM1006 W\n;=====printer start sound ===================\n\n M620 M ;enable remap\n G389\n\n;===== avoid end stop =================\n G91\n G380 S2 Z22 F1200\n G380 S2 Z-12 F1200\n G90\n;===== avoid end stop =================\n\n;===== reset machine status =================\n M204 S10000\n M630 S0 P1\n G90\n M17 D ; reset motor current to default\n M960 S5 P1 ; turn on logo lamp\n G90\n M220 S100 ;Reset Feedrate\n M1002 set_gcode_claim_speed_level: 5\n M221 S100 ;Reset Flowrate\n M73.2 R1.0 ;Reset left time magnitude\n G29.1 Z{+0.0} ; clear z-trim value first\n M983.1 M1\n M975 S1 ; turn on input shaping\n M982.2 S1 ; turn on cog noise reduction\n M983.4 S0\n;===== reset machine status =================\n\n;==== set airduct mode ====\n;==== if Chamber Cooling is necessary ====\n{if (overall_chamber_temperature >= 40)}\nM145 P1 ; set airduct mode to heating mode for heating\nM106 P2 S255 ; turn on filter fan\nM622.1 S0\nM1002 judge_flag ventobox_replace_aux1_fan_flag\nM622 J0\nM106 P10 S0 ; turn off left aux fan\nM623\n{else}\n{if (min_vitrification_temperature <= 50)}\nM145 P0 ; set airduct mode to cooling mode for cooling\nM106 P2 S255 ; turn on auxiliary fan for cooling\nM106 P3 S127 ; turn on chamber fan for cooling\nM1002 gcode_claim_action : 29\nM191 S0 ; wait for chamber temp\nM106 P2 S102 ; turn on chamber cooling fan\nM622.1 S0\nM1002 judge_flag ventobox_replace_aux1_fan_flag\nM622 J0\nM106 P10 S0 ; turn off left aux fan\nM623\nM142 P6 R30 S40 U0.3 V0.8 ; set PETG exhaust chamber autocooling\n{else}\nM145 P1 ; set airduct mode to heating mode for heating\nM106 P2 S127 ; turn on 50% filter fan\nM142 P6 R30 S40 U0.3 V0.8 ; set PLA/TPU exhaust chamber autocooling\n{endif}\n{endif}\n;==== set airduct mode ====\n\n;===== start to heat heatbed & hotend==========\n M1002 gcode_claim_action : 2\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M104 S140 A\n\n G29.2 S0 ; avoid invalid abl data\n\n;===== first homing start =====\n M1002 gcode_claim_action : 13\n G28 X T300\n G150.1 F8000 ; wipe mouth to avoid filament stick to heatbed\n G150.3\n M972 S24 P0\n M972 S26 P0 C0\n M972 S42 P0 T5000\n G150.1 F8000 ; wipe mouth to avoid filament stick to heatbed\n G90\n G1 X128 Y128 F30000\n G28 Z P0 T400\n M400\n;===== first homing end =====\n\n;===== detection start =====\n M1002 gcode_claim_action : 11\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]-80} A ; rise temp in advance\n M972 S19 P0 T5000 ;plate type detection\n\n {if max_print_z >= 145}\n M1002 gcode_claim_action : 75 ; Detect obstacles at the botton of the heated bed\n G150.3\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]} ; rise temp in advance\n G3811 Z{max_print_z} ; Detect obstacles at the bottom of the heated bed\n {endif}\n;===== detection end =====\n\n;===== prepare print temperature and material ==========\n M400\n M211 X0 Y0 Z0 ;turn off soft endstop\n G29.2 S0 ; avoid invalid abl data\n G150.3\n{if ((filament_type[initial_no_support_filament_id] == \"PLA\") || (filament_type[initial_no_support_filament_id] == \"PLA-CF\") || (filament_type[initial_no_support_filament_id] == \"PETG\")) && (nozzle_diameter_at_nozzle_id[initial_nozzle_id] == 0.2)}\nM620.10 A0 F74.8347 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\nM620.10 A1 F74.8347 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\n{else}\nM620.10 A0 F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60} H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\nM620.10 A1 F{flush_volumetric_speeds[initial_no_support_filament_id]/2.4053*60} H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} T{flush_temperatures[initial_no_support_filament_id]} P{nozzle_temperature_initial_layer[initial_no_support_filament_id]} S1\n{endif}\n\n M620.11 P0 L0 I[initial_no_support_filament_id] E0\n M620.11 K0 I[initial_no_support_filament_id] R0\n\n M620 S[initial_no_support_filament_id]A ; switch material if AMS exist\n M1002 gcode_claim_action : 4\n M1002 set_filament_type:UNKNOWN\n M400\n T[initial_no_support_filament_id]\n M400\n M628 S0\n M629\n M400\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M621 S[initial_no_support_filament_id]A\n M104 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n M400\n M106 P1 S0\n M400\n G29.2 S1\n;===== prepare print temperature and material ==========\n\n\n;===== auto extrude cali start =========================\n M975 S1\n M1002 judge_flag extrude_cali_flag\n M622 J0\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n M623\n\n M622 J1\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M1002 gcode_claim_action : 8\n M109 S{nozzle_temperature[initial_no_support_filament_id]}\n G90\n M83\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n M400\n M106 P1 S255\n M400 S5\n M106 P1 S0\n G150.3\n M623\n\n M622 J2\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M1002 gcode_claim_action : 8\n M109 S{nozzle_temperature[initial_no_support_filament_id]}\n G90\n M83\n M983.3 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2.4} A0.4 ; cali dynamic extrusion compensation\n M400\n M106 P1 S255\n M400 S5\n M106 P1 S0\n G150.3\n M623\n;===== auto extrude cali end =========================\n\n {if hold_chamber_temp_for_flat_print}\n M1002 gcode_claim_action : 58\n M104 S{first_layer_temperature[initial_no_support_filament_id]}\n {if bed_temperature_initial_layer_single > 89}\n M1030 S1800\n SYNC R0 T1800\n {else}\n M1030 S300\n SYNC R0 T300\n {endif}\n M1030 C\n {endif}\n\n {if filament_type[initial_filament_id] == \"TPU\" || filament_type[initial_filament_id] == \"PVA\"}\n {else}\n M83\n G1 E-3 F1800\n M400 P500\n {endif}\n\n ; series of wipes to ensure the nozzle is as clean as possible to avoid potential z-drift issues.\n M1002 gcode_claim_action : 14\n M104 S180\n G150.2\n G150.1 F8000\n G150.2\n G150.1 F8000\n M106 P1 S255\n M109 S180 A\n G150.1 F8000 \n M109 S140 A\n M106 P1 S0\n G150.1 F8000\n G91\n G1 Y-16 F12000\n G1 X100 F6000\n G90\n M400\n\n;===== wipe right nozzle start =====\n G150 T{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n M400\n\n{if filament_type[initial_filament_id] == \"PC\"}\n M109 S170 A\n{else}\n M109 S140 A\n{endif}\n G91\n G1 Z5 F1200\n G90\n M400\n G150.1\n;===== wipe left nozzle end =====\n\n\n;===== mech mode sweep start =====\n M1002 gcode_claim_action : 3\n G90\n G1 X128 Y128 F20000\n G1 Z5 F1200\n M400 P200\n M970.3 Q1 A5 K0 O1\n M970.2 Q1 K1 W74 Z0.01\n M974 Q1 S2 P0\n M970.3 Q0 A7 K0 O1\n M970.2 Q0 K1 W74 Z0.01\n M974 Q0 S2 P0\n M975 S1\n M400\n;===== mech mode sweep end =====\n\n;===== bed leveling ==================================\n M1002 gcode_claim_action : 54\n M190 S[bed_temperature_initial_layer_single]; ensure bed temp\n M109 S140 A\n M106 S0 ; turn off fan , too noisy\n M1002 judge_flag g29_before_print_flag\n M622 J1\n M1002 gcode_claim_action : 1\n {if hold_chamber_temp_for_flat_print}\n G29 H\n {else}\n G29 A1 X{first_layer_print_min[0]} Y{first_layer_print_min[1]} I{first_layer_print_size[0]} J{first_layer_print_size[1]}\n {endif}\n M400\n M623\n\n M622 J2\n M1002 gcode_claim_action : 1\n {if hold_chamber_temp_for_flat_print}\n G29 H\n {else}\n G29 A2 X{first_layer_print_min[0]} Y{first_layer_print_min[1]} I{first_layer_print_size[0]} J{first_layer_print_size[1]}\n {endif}\n M400\n M623\n\n M622 J0\n ;skip to homing\n M623\n G29.2 S1\n M1002 gcode_claim_action : 13\n G28\n;===== bed leveling end ================================\n\n M985.1 U0 E2\n M985.1 U1 E2\n\n M104 S{nozzle_temperature_initial_layer[initial_filament_id]} A\n G150.3 ; move to garbage can to wait for temp\n\n;===== wait temperature reaching the reference value =======\n M190 S[bed_temperature_initial_layer_single]\n\n ;========turn off light and fans =============\n M960 S1 P0 ; turn off laser\n M960 S2 P0 ; turn off laser\n M106 S0 ; turn off cooling fan\n\n;===== wait temperature reaching the reference value =======\n\n M1002 gcode_claim_action : 255\n M400\n M975 S1 ; turn on mech mode supression\n\n;============switch again==================\n M211 X0 Y0 Z0 ;turn off soft endstop\n G91\n G1 Z6 F1200\n G90\n M1002 set_filament_type:{filament_type[initial_no_support_filament_id]}\n M620 S[initial_no_support_filament_id]A\n M400\n T[initial_no_support_filament_id]\n M400\n M628 S0\n M629\n M400\n M621 S[initial_no_support_filament_id]A\n;============switch again==================\n\n;===== for Textured PEI Plate , lower the nozzle as the nozzle was touching topmost of the texture when homing ==\n {if bed_temperature_initial_layer_single > 89}\n {if curr_bed_type==\"Textured PEI Plate\"}\n G29.1 Z{-0.02} ; for Textured PEI Plate\n {else}\n G29.1 Z{0.0}\n {endif}\n {else}\n {if curr_bed_type==\"Textured PEI Plate\"}\n G29.1 Z{0.01} ; for Textured PEI Plate\n {else}\n G29.1 Z{0.03}\n {endif}\n {endif}\n\n\n;===== nozzle load line ===============================\n M1002 gcode_claim_action : 7\n G29.2 S1 ; ensure z comp turn on\n G90\n M83\n M400 P50\n M500 D1\n M400 S3\n M109 S{nozzle_temperature_initial_layer[initial_no_support_filament_id]}\n M1002 gcode_claim_action : 51\n G0 X100 Y0 F24000\n M400\n ;G130 O0 X100 Y-0.4 Z0.8 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2/2.4053} L40 E20 D5\n G130 O0 X100 Y-0.2 Z0.6 F{filament_max_volumetric_speed[initial_no_support_filament_id]/2/2.4053} L40 E12 D4\n G90\n M83\n G1 Z1\n M400\n;===== nozzle load line end ===========================\nM1002 gcode_claim_action : 0\n G29.99\n\n{if (filament_type[initial_no_support_filament_id] == \"TPU\") ||\n(filament_type[initial_no_support_filament_id] == \"PLA\") || (filament_type[initial_no_support_filament_id] == \"PETG\")}\nM1015.3 S1 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]};enable tpu, pla and petg clog detect\n{else}\nM1015.3 S0;disable clog detect\n{endif}\n\n{if (filament_type[initial_no_support_filament_id] == \"PLA\") || (filament_type[initial_no_support_filament_id] == \"PETG\")\n || (filament_type[initial_no_support_filament_id] == \"PLA-CF\") || (filament_type[initial_no_support_filament_id] == \"PETG-CF\")}\nM1015.4 S1 K1 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} ;enable E air printing detect\n{else}\nM1015.4 S0 K0 H{nozzle_diameter_at_nozzle_id[initial_nozzle_id]} ;disable E air printing detect\n{endif}\n\nM620.6 I[initial_no_support_filament_id] W1 ;enable ams air printing detect\n\nM1010 Q0 B0.023 S0.01\nM1010 Q1 B0.005 S0.01\nM1010.1 S1\n" } diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index ab44fc4d3c..1b764fe468 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -1820,6 +1820,8 @@ int CLI::run(int argc, char **argv) old_printable_width = static_cast(old_printable_bbox.size().x()); old_printable_depth = static_cast(old_printable_bbox.size().y()); } + // A 3mf can carry an empty project_settings.config - the models in + // resources/handy_models do - and opt_float() dereferences without checking. if (config.option("printable_height")) old_printable_height = (int)(config.opt_float("printable_height")); @@ -2505,7 +2507,8 @@ int CLI::run(int argc, char **argv) orig_printable_width = static_cast(orig_printable_bbox.size().x()); orig_printable_depth = static_cast(orig_printable_bbox.size().y()); } - orig_printable_height = (int)(config.opt_float("printable_height")); + if (config.option("printable_height")) + orig_printable_height = (int)(config.opt_float("printable_height")); BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(":%1%, check printable size: old_printable_width=%2%, orig_printable_width=%3%, old_printable_depth=%4%, orig_printable_depth=%5%, old_printable_height=%6%, orig_printable_height=%7%") %__LINE__ %old_printable_width %orig_printable_width %old_printable_depth %orig_printable_depth %old_printable_height %orig_printable_height; if ((orig_printable_width > 0) && (orig_printable_depth > 0) && (orig_printable_height > 0)) @@ -4650,7 +4653,8 @@ int CLI::run(int argc, char **argv) BoundingBoxf temp_printable_bbox(temp_printable_area); printer_plate.printable_width = static_cast(temp_printable_bbox.size().x()); printer_plate.printable_depth = static_cast(temp_printable_bbox.size().y()); - printer_plate.printable_height = (int)(config.opt_float("printable_height")); + if (config.option("printable_height")) + printer_plate.printable_height = (int)(config.opt_float("printable_height")); } if (temp_exclude_area.size() >= 4) { printer_plate.exclude_width = (int)(temp_exclude_area[2].x() - temp_exclude_area[0].x()); diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index 3dadb20253..2f23917be6 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -1320,7 +1320,7 @@ void export_group_fills_to_svg(const char *path, const std::vector #endif // friend to Layer -void Layer::make_fills(const FillAdaptive::Octrees* adaptive_fill_octrees, const FillAdaptive::Octrees* support_fill_octrees, FillLightning::Generator* lightning_generator) +void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLightning::Generator* lightning_generator) { for (LayerRegion *layerm : m_regions) layerm->fills.clear(); @@ -1353,7 +1353,7 @@ void Layer::make_fills(const FillAdaptive::Octrees* adaptive_fill_octrees, const f->z = this->print_z; f->angle = surface_fill.params.angle; f->fixed_angle = surface_fill.params.fixed_angle; - const FillAdaptive::Octrees *octrees = surface_fill.params.pattern == ipSupportCubic ? support_fill_octrees : adaptive_fill_octrees; + const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr; f->print_config = &this->object()->print()->config(); f->print_object_config = &this->object()->config(); if (surface_fill.params.pattern == ipConcentricInternal) { @@ -1516,7 +1516,7 @@ void Layer::make_fills(const FillAdaptive::Octrees* adaptive_fill_octrees, const * - For lightning/adaptive patterns, the respective generators are wired so their * polylines match the final infill layout. */ -Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::Octrees* adaptive_fill_octrees, const FillAdaptive::Octrees* support_fill_octrees, FillLightning::Generator* lightning_generator) const +Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::RegionOctrees* fill_octrees, FillLightning::Generator* lightning_generator) const { LockRegionParam skin_inner_param; std::vector surface_fills = group_fills(*this, skin_inner_param); @@ -1574,7 +1574,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti f->z = this->print_z; f->angle = surface_fill.params.angle; f->fixed_angle = surface_fill.params.fixed_angle; - const FillAdaptive::Octrees *octrees = surface_fill.params.pattern == ipSupportCubic ? support_fill_octrees : adaptive_fill_octrees; + const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr; f->print_config = &this->object()->print()->config(); f->print_object_config = &this->object()->config(); diff --git a/src/libslic3r/Fill/FillAdaptive.cpp b/src/libslic3r/Fill/FillAdaptive.cpp index 210394ea51..b693ae7cfa 100644 --- a/src/libslic3r/Fill/FillAdaptive.cpp +++ b/src/libslic3r/Fill/FillAdaptive.cpp @@ -301,88 +301,25 @@ void OctreeDeleter::operator()(Octree *p) { delete p; } -std::pair adaptive_fill_line_spacing(const PrintObject &print_object) +std::vector adaptive_fill_line_spacing(const PrintObject &print_object) { - // Output, spacing for icAdaptiveCubic and icSupportCubic - double adaptive_line_spacing = 0.; - double support_line_spacing = 0.; - - enum class Tristate { - Yes, - No, - Maybe - }; - struct RegionFillData { - Tristate has_adaptive_infill; - Tristate has_support_infill; - double density; - double extrusion_width; - }; - std::vector region_fill_data; - region_fill_data.reserve(print_object.num_printing_regions()); - bool build_octree = false; + std::vector line_spacing(print_object.num_printing_regions(), 0.); const std::vector &nozzle_diameters = print_object.print()->config().nozzle_diameter.values; double max_nozzle_diameter = *std::max_element(nozzle_diameters.begin(), nozzle_diameters.end()); double default_infill_extrusion_width = Flow::auto_extrusion_width(FlowRole::frInfill, float(max_nozzle_diameter)); - for (size_t region_id = 0; region_id < print_object.num_printing_regions(); ++ region_id) { - const PrintRegionConfig &config = print_object.printing_region(region_id).config(); - bool nonempty = config.sparse_infill_density > 0; - bool has_adaptive_infill = nonempty && config.sparse_infill_pattern == ipAdaptiveCubic; - bool has_support_infill = nonempty && config.sparse_infill_pattern == ipSupportCubic; - double sparse_infill_line_width = config.sparse_infill_line_width.get_abs_value(max_nozzle_diameter); - region_fill_data.push_back(RegionFillData({ - has_adaptive_infill ? Tristate::Maybe : Tristate::No, - has_support_infill ? Tristate::Maybe : Tristate::No, - config.sparse_infill_density, - sparse_infill_line_width != 0. ? sparse_infill_line_width : default_infill_extrusion_width - })); - build_octree |= has_adaptive_infill || has_support_infill; + for (size_t region_id = 0; region_id < line_spacing.size(); ++ region_id) { + const PrintRegionConfig &config = print_object.printing_region(region_id).config(); + if (config.sparse_infill_density <= 0 || ! is_octree_infill_pattern(config.sparse_infill_pattern) || + std::none_of(print_object.layers().begin(), print_object.layers().end(), [region_id](const Layer *layer) { + return region_id < layer->regions().size() && ! layer->regions()[region_id]->fill_surfaces.empty(); + })) + continue; + double extrusion_width = config.sparse_infill_line_width.get_abs_value(max_nozzle_diameter); + if (extrusion_width == 0.) + extrusion_width = default_infill_extrusion_width; + line_spacing[region_id] = extrusion_width / ((config.sparse_infill_density / 100.0f) * 0.333333333f) * config.fill_multiline.value; } - - if (build_octree) { - // Compute the average of above parameters over all layers - for (const Layer *layer : print_object.layers()) - for (size_t region_id = 0; region_id < layer->regions().size(); ++ region_id) { - RegionFillData &rd = region_fill_data[region_id]; - if (rd.has_adaptive_infill == Tristate::Maybe && ! layer->regions()[region_id]->fill_surfaces.empty()) - rd.has_adaptive_infill = Tristate::Yes; - if (rd.has_support_infill == Tristate::Maybe && ! layer->regions()[region_id]->fill_surfaces.empty()) - rd.has_support_infill = Tristate::Yes; - } - - double adaptive_fill_density = 0.; - double adaptive_infill_extrusion_width = 0.; - int adaptive_cnt = 0; - double support_fill_density = 0.; - double support_infill_extrusion_width = 0.; - int support_cnt = 0; - - for (const RegionFillData &rd : region_fill_data) { - if (rd.has_adaptive_infill == Tristate::Yes) { - adaptive_fill_density += rd.density; - adaptive_infill_extrusion_width += rd.extrusion_width; - ++ adaptive_cnt; - } else if (rd.has_support_infill == Tristate::Yes) { - support_fill_density += rd.density; - support_infill_extrusion_width += rd.extrusion_width; - ++ support_cnt; - } - } - - auto to_line_spacing = [](int cnt, double density, double extrusion_width) { - if (cnt) { - density /= double(cnt); - extrusion_width /= double(cnt); - return extrusion_width / ((density / 100.0f) * 0.333333333f); - } else - return 0.; - }; - const int n_multiline = print_object.printing_region(0).config().fill_multiline.value; - adaptive_line_spacing = to_line_spacing(adaptive_cnt, adaptive_fill_density, adaptive_infill_extrusion_width) * n_multiline; - support_line_spacing = to_line_spacing(support_cnt, support_fill_density, support_infill_extrusion_width) * n_multiline; - } - - return std::make_pair(adaptive_line_spacing, support_line_spacing); + return line_spacing; } // Context used by generate_infill_lines() when recursively traversing an octree in a DDA fashion diff --git a/src/libslic3r/Fill/FillAdaptive.hpp b/src/libslic3r/Fill/FillAdaptive.hpp index 57cbe713f1..47b6f62e28 100644 --- a/src/libslic3r/Fill/FillAdaptive.hpp +++ b/src/libslic3r/Fill/FillAdaptive.hpp @@ -59,11 +59,22 @@ struct Octrees } }; -// Calculate line spacing for -// 1) adaptive cubic infill -// 2) adaptive internal support cubic infill -// Returns zero for a particular infill type if no such infill is to be generated. -std::pair adaptive_fill_line_spacing(const PrintObject &print_object); +// Orca: The octrees of each line spacing the regions of an object fill with. +struct RegionOctrees +{ + std::vector sets; + // Index into sets for each region, -1 for a region without adaptive or support cubic infill. + std::vector region_set; + + const Octrees *region(size_t region_id) const + { + return region_id < region_set.size() && region_set[region_id] >= 0 ? &sets[region_set[region_id]] : nullptr; + } +}; + +// Line spacing of the adaptive or support cubic infill of each region of the object, +// zero for a region that generates no such infill. +std::vector adaptive_fill_line_spacing(const PrintObject &print_object); // Rotation of the octree to stand on one of its corners. Eigen::Quaterniond transform_to_world(); diff --git a/src/libslic3r/Layer.hpp b/src/libslic3r/Layer.hpp index 74df3e370c..d691a1da9d 100644 --- a/src/libslic3r/Layer.hpp +++ b/src/libslic3r/Layer.hpp @@ -33,7 +33,7 @@ class PrintObject; class Print; namespace FillAdaptive { - struct Octrees; + struct RegionOctrees; }; namespace FillLightning { @@ -207,10 +207,9 @@ public: static bool is_perimeter_compatible(const Print& print, const PrintRegion& a, const PrintRegion& b); void make_perimeters(); // Phony version of make_fills() without parameters for Perl integration only. - void make_fills() { this->make_fills(nullptr, nullptr); } - void make_fills(const FillAdaptive::Octrees* adaptive_fill_octrees, const FillAdaptive::Octrees* support_fill_octrees, FillLightning::Generator* lightning_generator = nullptr); - Polylines generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::Octrees *adaptive_fill_octrees, - const FillAdaptive::Octrees *support_fill_octrees, + void make_fills() { this->make_fills(nullptr); } + void make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLightning::Generator* lightning_generator = nullptr); + Polylines generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::RegionOctrees *fill_octrees, FillLightning::Generator* lightning_generator) const; void make_ironing(); // Returns the filament id (1-based) the region is ironed with, or -1 when the diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index f8416fa194..062a8eb3a3 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -647,7 +647,7 @@ private: void discover_horizontal_shells(); void combine_infill(); void _generate_support_material(); - std::pair prepare_adaptive_infill_data( + FillAdaptive::RegionOctrees prepare_adaptive_infill_data( const std::vector>& surfaces_w_layer) const; FillLightning::GeneratorPtr prepare_lightning_infill_data(); @@ -697,7 +697,7 @@ private: // so that next call to make_perimeters() performs a union() before computing loops bool m_typed_slices = false; - std::pair m_adaptive_fill_octrees; + FillAdaptive::RegionOctrees m_adaptive_fill_octrees; std::vector m_separated_body_bboxes; FillLightning::GeneratorPtr m_lightning_generator; diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index ae5517a04a..3387d22ba8 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -852,7 +852,7 @@ void PrintObject::infill() [this](const tbb::blocked_range& range) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) { m_print->throw_if_canceled(); - m_layers[layer_idx]->make_fills(&m_adaptive_fill_octrees.first, &m_adaptive_fill_octrees.second, this->m_lightning_generator.get()); + m_layers[layer_idx]->make_fills(&m_adaptive_fill_octrees, this->m_lightning_generator.get()); } } ); @@ -1192,14 +1192,27 @@ static std::vector split_mesh_by_body(const PrintObject &o return bodies; } -std::pair PrintObject::prepare_adaptive_infill_data( +FillAdaptive::RegionOctrees PrintObject::prepare_adaptive_infill_data( const std::vector> &surfaces_w_layer) const { using namespace FillAdaptive; - auto [adaptive_line_spacing, support_line_spacing] = adaptive_fill_line_spacing(*this); - if ((adaptive_line_spacing == 0. && support_line_spacing == 0.) || this->layers().empty()) + // Orca: Each region fills with the octrees of its own line spacing, shared by the regions of equal spacing. + const std::vector line_spacing = adaptive_fill_line_spacing(*this); + std::vector> spacings; // Line spacing, support cubic. + RegionOctrees octrees; + octrees.region_set.assign(line_spacing.size(), -1); + for (size_t region_id = 0; region_id < line_spacing.size(); ++ region_id) + if (line_spacing[region_id] > 0.) { + const std::pair spacing(line_spacing[region_id], this->printing_region(region_id).config().sparse_infill_pattern == ipSupportCubic); + const auto it = std::find(spacings.begin(), spacings.end(), spacing); + octrees.region_set[region_id] = int(it - spacings.begin()); + if (it == spacings.end()) + spacings.push_back(spacing); + } + if (spacings.empty() || this->layers().empty()) return {}; + octrees.sets.resize(spacings.size()); indexed_triangle_set mesh = this->model_object()->raw_indexed_triangle_set(); // Rotate mesh and build octree on it with axis-aligned (standart base) cubes. @@ -1225,44 +1238,50 @@ std::pair PrintObject::prepare_ada } }); - // Orca: Each body gets the octree it has when sliced on its own, from its own triangles. - std::pair octrees; - const size_t num_bodies = m_separated_body_bboxes.size(); - bool need_object = num_bodies <= 1; + // Orca: Each body gets the octree it has when sliced on its own, from its own triangles, for each line spacing + // its regions fill with. Body num_bodies stands for the whole object, which serves an object of a single body + // and the surfaces of bodies that have no octree of their own. + const size_t num_bodies = m_separated_body_bboxes.size(); + std::vector> to_build; // Set, body. + std::vector body_meshes; + std::vector> body_overhangs(num_bodies); if (num_bodies > 1) { - const std::vector body_meshes = split_mesh_by_body(*this, mesh, num_bodies); - need_object = std::any_of(body_meshes.begin(), body_meshes.end(), [](const indexed_triangle_set &its) { return its.indices.empty(); }); - std::vector> body_overhangs(num_bodies); + body_meshes = split_mesh_by_body(*this, mesh, num_bodies); for (size_t i = 0; i < surfaces_w_layer.size(); ++ i) if (const int body = separated_body_at(*surfaces_w_layer[i].second, surfaces_w_layer[i].first->expolygon.contour.points.front()); body >= 0) append(body_overhangs[body], overhangs[i]); - if (adaptive_line_spacing) - octrees.first.bodies.resize(num_bodies); - if (support_line_spacing) - octrees.second.bodies.resize(num_bodies); - tbb::parallel_for(tbb::blocked_range(0, num_bodies), [&, adaptive_spacing = adaptive_line_spacing, support_spacing = support_line_spacing]( - const tbb::blocked_range &range) { - for (size_t body = range.begin(); body < range.end(); ++ body) { - m_print->throw_if_canceled(); - if (body_meshes[body].indices.empty()) - continue; - if (adaptive_spacing) - octrees.first.bodies[body] = build_octree(body_meshes[body], body_overhangs[body], adaptive_spacing, false); - if (support_spacing) - octrees.second.bodies[body] = build_octree(body_meshes[body], body_overhangs[body], support_spacing, true); - } - }); - } + std::vector> fills(spacings.size(), std::vector(num_bodies + 1, false)); + for (const Layer *layer : m_layers) + for (size_t region_id = 0; region_id < layer->regions().size() && region_id < octrees.region_set.size(); ++ region_id) + if (const int set = octrees.region_set[region_id]; set >= 0) + for (const Surface &surface : layer->regions()[region_id]->fill_surfaces) { + const int body = separated_body_at(*layer, surface.expolygon.contour.points.front()); + fills[set][body >= 0 && ! body_meshes[body].indices.empty() ? size_t(body) : num_bodies] = true; + } + for (size_t set = 0; set < spacings.size(); ++ set) { + octrees.sets[set].bodies.resize(num_bodies); + for (size_t body = 0; body <= num_bodies; ++ body) + if (fills[set][body]) + to_build.emplace_back(set, body); + } + } else + for (size_t set = 0; set < spacings.size(); ++ set) + to_build.emplace_back(set, num_bodies); // and gather them. for (size_t i = 1; i < overhangs.size(); ++ i) append(overhangs.front(), std::move(overhangs[i])); - // Orca: The object's octree only serves bodies that have none of their own. - if (need_object && adaptive_line_spacing) - octrees.first.object = build_octree(mesh, overhangs.front(), adaptive_line_spacing, false); - if (need_object && support_line_spacing) - octrees.second.object = build_octree(mesh, overhangs.front(), support_line_spacing, true); + tbb::parallel_for(tbb::blocked_range(0, to_build.size()), [&](const tbb::blocked_range &range) { + for (size_t i = range.begin(); i < range.end(); ++ i) { + m_print->throw_if_canceled(); + const auto [set, body] = to_build[i]; + const bool object = body == num_bodies; + (object ? octrees.sets[set].object : octrees.sets[set].bodies[body]) = + build_octree(object ? mesh : body_meshes[body], object ? overhangs.front() : body_overhangs[body], spacings[set].first, + spacings[set].second); + } + }); return octrees; } @@ -3203,8 +3222,7 @@ void PrintObject::bridge_over_infill() for (size_t job_idx = r.begin(); job_idx < r.end(); job_idx++) { size_t lidx = layers_to_generate_infill[job_idx]; infill_lines.at( - lidx) = po->get_layer(lidx)->generate_sparse_infill_polylines_for_anchoring(&po->m_adaptive_fill_octrees.first, - &po->m_adaptive_fill_octrees.second, + lidx) = po->get_layer(lidx)->generate_sparse_infill_polylines_for_anchoring(&po->m_adaptive_fill_octrees, po->m_lightning_generator.get()); } }); diff --git a/src/slic3r/GUI/AMSMaterialsSetting.cpp b/src/slic3r/GUI/AMSMaterialsSetting.cpp index 3859daf8e9..903524caaa 100644 --- a/src/slic3r/GUI/AMSMaterialsSetting.cpp +++ b/src/slic3r/GUI/AMSMaterialsSetting.cpp @@ -19,7 +19,6 @@ #include "slic3r/GUI/Widgets/Label.hpp" #include #include "slic3r/GUI/wxExtensions.hpp" -#include "slic3r/GUI/Widgets/HyperLink.hpp" #include #include #include "slic3r/GUI/DeviceCore/DevDefs.h" @@ -48,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -103,6 +103,14 @@ void AMSMaterialsSetting::create() m_sizer_button->Add(0, 0, 1, wxEXPAND, 0); + // Orca: link to the Orca Slicer pressure-advance wiki (region-agnostic). + m_wiki_ctrl = new Button(this, "", "toolbar_wiki", 0, 15); + auto wiki_url = "https://www.orcaslicer.com/wiki/pressure_advance_calib"; + m_wiki_ctrl->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url); + m_wiki_ctrl->SetStyle(ButtonStyle::Confirm, ButtonType::Circle); + m_wiki_ctrl->SetCanFocus(false); + m_wiki_ctrl->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);})); + m_button_confirm = new Button(this, _L("Confirm")); m_button_confirm->SetStyle(ButtonStyle::Confirm, ButtonType::Choice); m_button_confirm->Bind(wxEVT_BUTTON, &AMSMaterialsSetting::on_select_ok, this); @@ -115,8 +123,9 @@ void AMSMaterialsSetting::create() m_button_close->SetStyle(ButtonStyle::Regular, ButtonType::Choice); m_button_close->Bind(wxEVT_BUTTON, &AMSMaterialsSetting::on_select_close, this); - m_sizer_button->Add(m_button_confirm, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(20)); - m_sizer_button->Add(m_button_reset, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(20)); + m_sizer_button->Add(m_wiki_ctrl, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(20)); + m_sizer_button->Add(m_button_confirm, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(10)); + m_sizer_button->Add(m_button_reset, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(10)); m_sizer_button->Add(m_button_close, 0, wxALIGN_CENTER, 0); m_sizer_main->Add(m_panel_normal, 0, wxALL, FromDIP(2)); @@ -340,11 +349,7 @@ void AMSMaterialsSetting::create_panel_kn(wxWindow* parent) m_ratio_text->SetForegroundColour(wxColour(50, 58, 61)); m_ratio_text->SetFont(Label::Head_14); - // Orca: link to the Orca Slicer pressure-advance wiki (region-agnostic). - wxString link_url = "https://www.orcaslicer.com/wiki/pressure_advance_calib"; - m_wiki_ctrl = new HyperLink(parent, _L("Wiki Guide"), link_url); cali_title_sizer->Add(m_ratio_text, 0, wxALIGN_CENTER_VERTICAL); - cali_title_sizer->Add(m_wiki_ctrl, 0, wxALIGN_CENTER_VERTICAL); wxBoxSizer *m_sizer_cali_resutl = new wxBoxSizer(wxHORIZONTAL); // pa profile diff --git a/src/slic3r/GUI/AMSMaterialsSetting.hpp b/src/slic3r/GUI/AMSMaterialsSetting.hpp index 0ecc8809be..2628e6caf4 100644 --- a/src/slic3r/GUI/AMSMaterialsSetting.hpp +++ b/src/slic3r/GUI/AMSMaterialsSetting.hpp @@ -14,7 +14,6 @@ #include "Widgets/CheckBox.hpp" #include "Widgets/ComboBox.hpp" #include "Widgets/TextInput.hpp" -#include "Widgets/HyperLink.hpp" #include "slic3r/Utils/CalibUtils.hpp" #include #include @@ -194,7 +193,7 @@ protected: wxPanel * m_panel_kn; wxStaticText* m_ratio_text; - HyperLink * m_wiki_ctrl; + Button * m_wiki_ctrl; wxStaticText* m_k_param; TextInput* m_input_k_val; wxStaticText* m_n_param; diff --git a/src/slic3r/GUI/CalibrationWizardPage.cpp b/src/slic3r/GUI/CalibrationWizardPage.cpp index 7017d52e34..d80bf4edf7 100644 --- a/src/slic3r/GUI/CalibrationWizardPage.cpp +++ b/src/slic3r/GUI/CalibrationWizardPage.cpp @@ -21,7 +21,6 @@ #include "slic3r/GUI/PresetComboBoxes.hpp" #include "slic3r/GUI/wxExtensions.hpp" #include -#include "slic3r/GUI/Widgets/HyperLink.hpp" #include #include #include "slic3r/GUI/Widgets/PopupWindow.hpp" @@ -494,7 +493,10 @@ void CaliPageCaption::init_bitmaps() { void CaliPageCaption::create_wiki(wxWindow* parent) { // ORCA standardized HyperLink - m_wiki_text = new HyperLink(parent, _L("Wiki Guide")); + m_wiki_text = new Button(parent, "", "toolbar_wiki", 0, 15); + m_wiki_text->SetToolTip(_L("Wiki Guide")); + m_wiki_text->SetStyle(ButtonStyle::Confirm, ButtonType::Circle); + m_wiki_text->SetCanFocus(false); m_wiki_text->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& e) { if (!m_wiki_url.empty()) wxLaunchDefaultBrowser(m_wiki_url); diff --git a/src/slic3r/GUI/CalibrationWizardPage.hpp b/src/slic3r/GUI/CalibrationWizardPage.hpp index 8624d7c9f7..8f95137e4d 100644 --- a/src/slic3r/GUI/CalibrationWizardPage.hpp +++ b/src/slic3r/GUI/CalibrationWizardPage.hpp @@ -7,7 +7,6 @@ #include "Widgets/TextInput.hpp" #include "Widgets/AMSControl.hpp" #include "Widgets/ProgressBar.hpp" -#include "Widgets/HyperLink.hpp" #include "wxExtensions.hpp" #include "PresetComboBoxes.hpp" @@ -155,7 +154,7 @@ private: void init_bitmaps(); void create_wiki(wxWindow* parent); - HyperLink* m_wiki_text; // ORCA + Button* m_wiki_text; // ORCA wxString m_wiki_url; ScalableBitmap m_prev_bmp_normal; ScalableBitmap m_prev_bmp_hover; diff --git a/src/slic3r/GUI/FilamentGroupPopup.cpp b/src/slic3r/GUI/FilamentGroupPopup.cpp index d8442071b4..bb081a6fc1 100644 --- a/src/slic3r/GUI/FilamentGroupPopup.cpp +++ b/src/slic3r/GUI/FilamentGroupPopup.cpp @@ -4,7 +4,6 @@ #include "wx/dcgraph.h" #include "I18N.hpp" #include "PartPlate.hpp" -#include "Widgets/HyperLink.hpp" #include #include "libslic3r/Config.hpp" #include "libslic3r/PrintConfig.hpp" @@ -27,6 +26,7 @@ #include "slic3r/GUI/Widgets/StaticBox.hpp" #include #include "slic3r/GUI/Widgets/SwitchButton.hpp" +#include "slic3r/GUI/Widgets/Button.hpp" #include namespace Slic3r { namespace GUI { @@ -203,18 +203,24 @@ FilamentGroupPopup::FilamentGroupPopup(wxWindow *parent) : PopupWindow(parent, w { wxBoxSizer *button_sizer = new wxBoxSizer(wxHORIZONTAL); // ORCA Unified hyperlinks - video_link = new HyperLink(this, _L("Video tutorial")); - video_link->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent& e) - { - play_dual_extruder_slice_video(); - wxGetApp().app_config->set("play_slicing_video", "false"); - }); - button_sizer->Add(video_link, 0, wxLEFT, horizontal_margin + FromDIP(3)); - button_sizer->AddStretchSpacer(); - wiki_link = new HyperLink(this, _L("Wiki Guide")); - wiki_link->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent&) { open_filament_group_wiki(); }); - button_sizer->Add(wiki_link, 0, wxLEFT, horizontal_margin); + auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15); + wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + "https://e.bambulab.com/t?c=mOkvsXkJ9pldGYp9"); + wiki_btn->SetStyle(ButtonStyle::Confirm, ::ButtonType::Circle); + wiki_btn->SetCanFocus(false); + wiki_btn->Bind(wxEVT_LEFT_DOWN, ([](auto& e) {open_filament_group_wiki();})); + + auto video_btn = new Button(this, "", "toolbar_video_guide", 0, 15); + video_btn->SetToolTip(_L("Video Guide")); + video_btn->SetStyle(ButtonStyle::Confirm, ::ButtonType::Circle); + video_btn->SetCanFocus(false); + video_btn->Bind(wxEVT_LEFT_DOWN, ([](auto& e) { + play_dual_extruder_slice_video(); + wxGetApp().app_config->set("play_slicing_video", "false"); + })); + + button_sizer->Add(wiki_btn , 0, wxLEFT, horizontal_margin + FromDIP(3)); + button_sizer->Add(video_btn, 0, wxLEFT, FromDIP(10)); top_sizer->Add(button_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, horizontal_margin); } diff --git a/src/slic3r/GUI/FilamentGroupPopup.hpp b/src/slic3r/GUI/FilamentGroupPopup.hpp index 09b6b8d344..bc43876af7 100644 --- a/src/slic3r/GUI/FilamentGroupPopup.hpp +++ b/src/slic3r/GUI/FilamentGroupPopup.hpp @@ -29,6 +29,7 @@ public: FilamentGroupPopup(wxWindow *parent); void tryPopup(Plater* plater,PartPlate* plate, bool slice_all); void tryClose(); + void Dismiss() override; FilamentMapMode GetSelectedMode() const { return m_mode; } private: @@ -40,7 +41,6 @@ private: void OnLeaveWindow(wxMouseEvent &); void OnEnterWindow(wxMouseEvent &); void OnTimer(wxTimerEvent &event); - void Dismiss(); void CreateBmps(); @@ -80,10 +80,6 @@ private: wxBitmap unchecked_hover_bmp; wxBitmap global_tag_bmp; - - wxStaticText *wiki_link; - wxStaticText *video_link; - StaticBox *m_smart_filament_panel{nullptr}; wxSizerItem *m_smart_filament_spacer{nullptr}; SwitchButton *m_smart_filament_switch{nullptr}; diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 2bd2730e76..aa4b3ca2bb 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -35,6 +35,7 @@ #include "slic3r/GUI/ImGuiWrapper.hpp" #include "slic3r/GUI/GLModel.hpp" #include "slic3r/GUI/GLShader.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoUtils.hpp" #include "libslic3r/Point.hpp" #include #include "libvgcode/include/Viewer.hpp" @@ -3337,27 +3338,6 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding) } }; - auto link_filament_group_wiki = [&](const std::string& label) { - ImVec2 wiki_part_size = ImGui::CalcTextSize(label.c_str()); - ImColor HyperColor = ImColor(0, 150, 136, 255); // ORCA match color - ImGui::PushStyleColor(ImGuiCol_Text, HyperColor.Value); - imgui.text(label.c_str()); - ImGui::PopStyleColor(); - - // ORCA use underline to match hyperlink style - ImVec2 lineEnd = ImGui::GetItemRectMax(); - lineEnd.y -= 2.0f; - ImVec2 lineStart = lineEnd; - lineStart.x = ImGui::GetItemRectMin().x; - ImGui::GetWindowDrawList()->AddLine(lineStart, lineEnd, HyperColor); - // click behavior - if (ImGui::IsMouseHoveringRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), true)) { - if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { - open_filament_group_wiki(); - } - } - }; - auto draw_dash_line = [&](ImDrawList* draw_list, int dash_length = 5, int gap_length = 3) { ImVec2 p1 = ImGui::GetCursorScreenPos(); ImVec2 p2 = ImVec2(p1.x + ImGui::GetContentRegionAvail().x, p1.y); @@ -3431,7 +3411,7 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding) else tips_count = 5; - float AMS_container_height = ams_item_height + line_height * tips_count + line_height; + float AMS_container_height = ams_item_height + line_height * tips_count + line_height + window_padding * 2.f; ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // this shold be 0 since its child of gcodeviewer ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 1, 1)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(window_padding * 3, 0)); @@ -3446,10 +3426,6 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding) ImGui::Dummy({window_padding, window_padding}); ImGui::PushStyleColor(ImGuiCol_Separator, ImVec4(1.0f,1.0f,1.0f,0.6f)); imgui.bold_text(_u8L("Filament Grouping")); - ImGui::SameLine(); - std::string tip_str = _u8L("Why this grouping"); - ImGui::SetCursorPosX(ImGui::GetWindowContentRegionWidth() - window_padding - ImGui::CalcTextSize(tip_str.c_str()).x); - link_filament_group_wiki(tip_str); ImGui::Separator(); ImGui::PopStyleColor(); ImGui::Dummy({window_padding, window_padding}); @@ -3554,6 +3530,9 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding) } ImGui::Dummy({window_padding, window_padding}); + GLGizmoUtils::render_wiki_guide_button(*wxGetApp().plater()->get_current_canvas3D(), m_scale,"https://e.bambulab.com/t?c=mOkvsXkJ9pldGYp9"); + ImGui::SameLine(); + if (!is_optimal_group) { link_text_set_to_optional(_u8L("Set to Optimal")); ImGui::SameLine(); @@ -3562,10 +3541,7 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding) } link_text(_u8L("Regroup filament")); - ImGui::SameLine(); - std::string wiki_str = _u8L("Wiki Guide"); // ORCA - ImGui::SetCursorPosX(ImGui::GetWindowContentRegionWidth() - window_padding - ImGui::CalcTextSize(wiki_str.c_str()).x); - link_filament_group_wiki(wiki_str); + ImGui::Dummy({window_padding, window_padding}); ImGui::EndChild(); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp index f1b0e2b2ee..c467a7768d 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp @@ -27,6 +27,7 @@ #include "slic3r/GUI/MsgDialog.hpp" #include "slic3r/GUI/OpenGLManager.hpp" #include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/Shortcuts.hpp" #include "slic3r/GUI/TextureLibrary.hpp" #include "slic3r/GUI/TextureProjectorFrame.hpp" #include "slic3r/GUI/UVEditorCanvas.hpp" @@ -407,11 +408,48 @@ GLGizmoTextureDisplacement::GLGizmoTextureDisplacement(GLCanvas3D& parent, const bool GLGizmoTextureDisplacement::on_init() { + m_shortcut = Shortcut::GizmoDisplacement; + const wxString ctrl = GUI::shortkey_ctrl_prefix(); + const wxString alt = GUI::shortkey_alt_prefix(); + const wxString shift = GUI::shortkey_shift_prefix(); + + m_desc["cursor_size"] = _L("Brush size"); m_desc["circle"] = _L("Circle"); m_desc["sphere"] = _L("Sphere"); m_desc["remove_layer"] = _L("Remove"); m_desc["bake"] = _L_CONTEXT("Bake", "Texture Displacement"); + + + m_desc["paint"] = _L("Paint"); + m_desc["erase"] = _L("Erase"); + m_desc["gap_area"] = _L("Gap area"); + m_desc["smart_fill_angle"] = _L("Smart fill angle"); + m_desc["toggle_wireframe"] = _L("Toggle Wireframe"); + + std::pair paint_shortcut = {_L("Left mouse button"), m_desc["paint"]}; + std::pair erase_shortcut = {shift + _L("Left mouse button"), m_desc["erase"]}; + std::pair toggle_wireframe_shortcut = {alt + shift + _L_CONTEXT("Enter", "Keyboard Shortcut"), m_desc["toggle_wireframe"]}; + + m_shortcuts_brush = { + paint_shortcut, + erase_shortcut, + {ctrl + _L("Mouse wheel"), m_desc["cursor_size"]}, + toggle_wireframe_shortcut + }; + + m_shortcuts_bucket_fill = { + paint_shortcut, + erase_shortcut, + {ctrl + _L("Mouse wheel"), m_desc["smart_fill_angle"]}, + toggle_wireframe_shortcut + }; + + m_shortcuts_gap_fill = { + {ctrl + _L("Mouse wheel"), m_desc["gap_area"]}, + toggle_wireframe_shortcut + }; + return true; } @@ -523,7 +561,15 @@ void GLGizmoTextureDisplacement::render_painter_gizmo() // volume for a shaded pass that then draws nothing is what made the model vanish - most obviously // with zero layers, but equally with a layer that has no texture picked yet. const bool use_shaded = m_use_shaded_preview && m_shaded_preview_glmodel.is_initialized() && shaded_preview_ready(); - const bool use_true_preview = !use_shaded && m_preview_glmodel.is_initialized(); + // Checker/Distortion are built from the *base* patch and drawn with a polygon offset, which biases + // depth values - it does not move the geometry. It therefore cannot win against a surface that + // genuinely stands in front, and the displaced preview does exactly that: it rises above the base + // surface by the layer's depth. Drawn underneath a UV-check overlay it simply occludes it, which is + // why those two modes looked like they did nothing. Leave it out and let the undisplaced volume show + // through instead (toggle_model_objects_visibility below) - that one *is* coincident with the + // overlay, which is what the offset assumes, and it is the surface whose mapping is being inspected. + const bool use_true_preview = !use_shaded && m_uv_check_mode == UVCheckMode::None && + m_preview_glmodel.is_initialized(); // In Checker/Distortion mode the UV-check overlay *is* the surface visualization the user is // looking at, so the opaque paint-selection highlight must not be drawn on top of it - same // reasoning as skipping it for the shaded preview (see bug #12). Without this the painted area @@ -551,7 +597,13 @@ void GLGizmoTextureDisplacement::render_painter_gizmo() render_triangles(selection); glsafe(::glDisable(GL_POLYGON_OFFSET_FILL)); } - } else if (show_paint_overlay) { + } else { + // render_triangles() *is* the model in a painter gizmo (it draws every model-part volume with the + // selector's colours), not an overlay on top of one - so it still has to run under a UV-check + // overlay, or nothing draws the surface at all and the checker floats alone over an empty scene. + // Deliberately without the depth bias the branch above applies: the checker/heatmap is drawn later + // with its own -1 offset and has to win against this. Biasing both by the same amount is what made + // the painted area cover the checker and is why this call used to be skipped outright. render_triangles(selection); } @@ -5313,6 +5365,12 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float return; ModelVolume *mv = texture_volume(); + float scale = m_parent.get_scale(); + #ifdef WIN32 + int dpi = get_dpi_for_window(wxGetApp().GetTopWindow()); + scale *= (float) dpi / (float) DPI_DEFAULT; + #endif // WIN32 + const float approx_height = m_imgui->scaled(24.f); y = std::min(y, bottom_limit - approx_height); @@ -5742,14 +5800,20 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float const int cur_mode = m_use_shaded_preview ? 1 : m_uv_check_mode == UVCheckMode::Checker ? 2 : m_uv_check_mode == UVCheckMode::Distortion ? 3 : 0; - int new_mode = cur_mode; - bool wf_toggle = false; - const wxString distortion_na = active == nullptr ? _L("Add a layer first.") : - active->projection_method != TextureProjectionMethod::LSCM ? - _L("Needs the active layer mapped with Unwrap (LSCM).") : - wxString(); - // Distortion over a layer that stopped being an unwrap shows nothing at all, so fall back to Normal. - if (cur_mode == 3 && !distortion_na.empty()) + int new_mode = cur_mode; + bool wf_toggle = false; + bool open_uv_editor = false; + // Checker and Distortion both draw *the unwrap* - the first the texture grid laid over it, the second + // its stretch - so they only mean anything for a layer mapped with Unwrap (LSCM). On the default + // triplanar mapping (or cylindrical / spherical / from view) they are faded out with the reason in the + // tooltip, rather than being offered and then showing nothing. + const wxString uv_view_na = active == nullptr ? _L("Add a layer first.") : + active->projection_method != TextureProjectionMethod::LSCM ? + _L("Only for a layer mapped with Unwrap (LSCM) - set the " + "active layer's Mapping to Unwrap to use this view.") : + wxString(); + // Either view over a layer that stopped being an unwrap shows nothing at all, so fall back to Normal. + if ((cur_mode == 2 || cur_mode == 3) && !uv_view_na.empty()) new_mode = 0; const float x0 = ImGui::GetCursorPosX(); @@ -5768,12 +5832,20 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float ImGui::SameLine(0.f, gap_s); if (icon_toggle(703, "texture_displacement_checker.svg", cur_mode == 2, icon_md, _L("Checker"), _L("Checker - a test grid instead of the texture. Where the squares stay square the " - "texture is undistorted; where they stretch, it will too"))) - new_mode = 2; + "texture is undistorted; where they stretch, it will too. Opens the UV editor if " + "it is closed"), + uv_view_na)) { + new_mode = 2; + open_uv_editor = true; + } ImGui::SameLine(0.f, gap_s); if (icon_toggle(704, "texture_displacement_distortion.svg", cur_mode == 3, icon_md, _L("Distortion"), - _L("Distortion - blue-to-red stretch heatmap over the unwrap"), distortion_na)) - new_mode = 3; + _L("Distortion - blue-to-red stretch heatmap over the unwrap. Opens the UV editor if " + "it is closed"), + uv_view_na)) { + new_mode = 3; + open_uv_editor = true; + } vsep(icon_md); if (icon_toggle(705, "texture_displacement_wireframe.svg", m_wireframe_overlay, icon_md, _L("Wireframe"), _L("Wireframe - overlay the mesh edges; independent of the view above"))) @@ -5788,6 +5860,13 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float hover_tip(_u8L("Rebuilds the preview as soon as anything changes. Turn it off on a heavy model if painting " "or dragging a slider starts to stutter - the preview then waits until you let go.")); + // Both are views of the unwrap, so picking one brings the UV editor up with it - including when that + // view is already the active one and only the pane is missing. + if (open_uv_editor && !m_show_uv_editor) { + m_show_uv_editor = true; + if (new_mode == cur_mode) + update_uv_editor(); // otherwise apply_view_mode() below does it + } if (new_mode != cur_mode) apply_view_mode(new_mode); if (wf_toggle) { @@ -6891,18 +6970,34 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float } const float button_h = std::round(frame_h * 1.25f); - const float third = std::floor((panel_w - style.ItemSpacing.x) / 3.f); - if (busy) { - if (ImGui::Button((_u8L("Stop") + "##stop").c_str(), ImVec2(third, button_h))) - wxGetApp().plater()->get_ui_job_worker().cancel_all(); - hover_tip(_u8L("Stops the bake. Whatever it had already finished stays on the model, and can be undone.")); - } else { - if (ImGui::Button((_u8L("Close") + "##close").c_str(), ImVec2(third, button_h))) - m_parent.reset_all_gizmos(); - hover_tip(_u8L("Closes the tool without baking. Your paint, layers and settings stay with the model.")); - } + const float button_w = std::max({ + ImGui::CalcTextSize(_u8L("Close").c_str()).x, + ImGui::CalcTextSize(_u8L("Stop").c_str()).x, + ImGui::CalcTextSize(_u8L("Preparing...").c_str()).x, + ImGui::CalcTextSize(_u8L("Baking...").c_str()).x, + }) + m_imgui->scaled(0.5f); + const float row_y = ImGui::GetCursorPosY(); + const float icon_h = 21.f * scale; + const float row_h = std::max(icon_h, button_h); + const std::vector> shortcut = + m_tool_type == ToolType::BUCKET_FILL ? m_shortcuts_bucket_fill + : m_tool_type == ToolType::SMART_FILL ? m_shortcuts_bucket_fill + : m_tool_type == ToolType::BRUSH ? m_shortcuts_brush + : m_tool_type == ToolType::GAP_FILL ? m_shortcuts_gap_fill + : std::vector>{}; + + ImGui::SetCursorPosY(row_y + (row_h - icon_h) * .5f); // center vertically + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(int(m_imgui->scaled(.5f)), style.ItemSpacing.y)); + GLGizmoUtils::render_tooltip_button(m_imgui, m_parent, shortcut, x, y); ImGui::SameLine(); + GLGizmoUtils::render_wiki_guide_button(m_parent, scale, "https://www.orcaslicer.com/wiki/print_prepare/prepare_texture_displacement"); + ImGui::SameLine(); + GLGizmoUtils::render_video_guide_button(m_parent, scale, "https://www.youtube.com/watch?v=D7w3tG1kdvE"); + ImGui::PopStyleVar(1); + + ImGui::SameLine(x0 + panel_w - button_w * 2 - style.ItemSpacing.x); + const bool can_bake = !busy && mv != nullptr && mv->is_texture_displacement_painted(); const std::string bake_label = m_prepare_in_progress ? _u8L("Preparing...") : m_bake_in_progress ? _u8L("Baking...") : @@ -6910,7 +7005,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float GLGizmoUtils::push_orca_button_style(); m_imgui->push_bold_font(); m_imgui->disabled_begin(!can_bake); - if (ImGui::Button((bake_label + "##bake").c_str(), ImVec2(x0 + panel_w - ImGui::GetCursorPosX(), button_h))) { + ImGui::SetCursorPosY(row_y + (row_h - button_h) * .5f); // center vertically + if (ImGui::Button((bake_label + "##bake").c_str(), ImVec2(button_w, button_h))) { // Standard mode's Bake is the whole pipeline (remesh -> refine -> displace); Pro's is only the // displacement, because there the user has already prepared the mesh with the controls above. if (pro_mode()) @@ -6935,6 +7031,17 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float "land on - all in one step."), wrap_w); + ImGui::SameLine(); + if (busy) { + if (ImGui::Button((_u8L("Stop") + "##stop").c_str(), ImVec2(button_w, button_h))) + wxGetApp().plater()->get_ui_job_worker().cancel_all(); + hover_tip(_u8L("Stops the bake. Whatever it had already finished stays on the model, and can be undone.")); + } else { + if (ImGui::Button((_u8L("Close") + "##close").c_str(), ImVec2(button_w, button_h))) + m_parent.reset_all_gizmos(); + hover_tip(_u8L("Closes the tool without baking. Your paint, layers and settings stay with the model.")); + } + // What Bake will produce, and which layers it will skip. if (mv != nullptr) { const std::string base_count = base_k > 0 ? Slic3r::format(_u8L("%1% k"), base_k) : diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp index 6685f8d713..2c6f5bc6c7 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp @@ -943,6 +943,13 @@ private: std::map m_desc; + // Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")} + std::vector> m_shortcuts_brush; + // Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")} + std::vector> m_shortcuts_bucket_fill; + // Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")} + std::vector> m_shortcuts_gap_fill; + // Icons for the panel's icon buttons (tools, views, mapping, tiling, layer actions). Loaded through IconManager with // the same colour/monochrome variants the main toolbar uses, so an inactive button shows the icon in // the theme's normal (grey) foreground colour and an active one shows it in its original colours - diff --git a/src/slic3r/GUI/Gizmos/GLGizmoUtils.cpp b/src/slic3r/GUI/Gizmos/GLGizmoUtils.cpp index 19bd47fb6c..2b52fc486e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoUtils.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoUtils.cpp @@ -3,6 +3,7 @@ #include "GLGizmosManager.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/I18N.hpp" #include #include #include @@ -10,6 +11,7 @@ #include #include #include +#include #include #include "slic3r/GUI/GUI_Utils.hpp" @@ -55,7 +57,6 @@ namespace Slic3r::GUI::GLGizmoUtils { auto& gizmos_manager = canvas.get_gizmos_manager(); ImTextureID normal_id = gizmos_manager.get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP); - ImTextureID hover_id = gizmos_manager.get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP_HOVER); float scale = canvas.get_scale(); #ifdef WIN32 @@ -63,13 +64,7 @@ namespace Slic3r::GUI::GLGizmoUtils { scale *= (float)dpi / (float)DPI_DEFAULT; #endif - ImVec2 button_size = ImVec2(25 * scale, 25 * scale); - - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, { 0, 0 }); - - ImGui::ImageButton3(normal_id, hover_id, button_size); - + toolbar_circular_button(normal_id, "##tooltip_btn", scale); if (ImGui::IsItemHovered()) { ImGui::BeginTooltip2(ImVec2(x, caption_y)); for (const auto& item : shortcuts) { @@ -79,7 +74,60 @@ namespace Slic3r::GUI::GLGizmoUtils { } ImGui::EndTooltip(); } - ImGui::PopStyleVar(2); + } + + bool render_wiki_guide_button(const GLCanvas3D& canvas, float scale, const char* url) + { + auto& gm = canvas.get_gizmos_manager(); + auto icon = gm.get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_WIKI_GUIDE); + bool clicked = toolbar_circular_button(icon, "##wiki_guide_btn", scale); + if (url && *url && clicked) + wxLaunchDefaultBrowser(wxString::FromUTF8(url)); + if (ImGui::IsItemHovered()){ + if(url && *url) + ImGui::SetTooltip("%s\n%s", _u8L("Wiki Guide").c_str(), url); + else + ImGui::SetTooltip("%s", _u8L("Wiki Guide").c_str()); + } + return clicked; // for dynamically generated links + } + + bool render_video_guide_button(const GLCanvas3D& canvas, float scale, const char* url) + { + auto& gm = canvas.get_gizmos_manager(); + auto icon = gm.get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_VIDEO_GUIDE); + bool clicked = toolbar_circular_button(icon, "##video_guide_btn", scale); + if (url && *url && clicked) + wxLaunchDefaultBrowser(wxString::FromUTF8(url)); + if (ImGui::IsItemHovered()){ + if(url && *url) + ImGui::SetTooltip("%s\n%s", _u8L("Video Guide").c_str(), url); + else + ImGui::SetTooltip("%s", _u8L("Video Guide").c_str()); + } + return clicked; // for dynamically generated links + } + + bool toolbar_circular_button(ImTextureID textureID, const char* id, float scale) + { + ImVec2 btn_sz = ImVec2(21 * scale, 21 * scale); + ImVec2 icon_sz = ImVec2(15 * scale, 15 * scale); + float btn_pad = (btn_sz.x - icon_sz.x) * .5f; + + ImVec2 p = ImGui::GetCursorScreenPos(); + bool clicked = ImGui::InvisibleButton(id, btn_sz); + ImDrawList* dl = ImGui::GetWindowDrawList(); + bool is_dark = ImGuiWrapper::COL_WINDOW_BG.x != ImGui::GetStyleColorVec4(ImGuiCol_WindowBg).x; + ImVec4 col = is_dark ? ImGuiWrapper::COL_ORCA_DARK : ImGuiWrapper::COL_ORCA; + ImVec4 col_hvr = is_dark ? ImGuiWrapper::COL_ORCA_HOVER_DARK : ImGuiWrapper::COL_ORCA_HOVER; + dl->AddCircleFilled( + ImVec2(p.x + btn_sz.x * .5f, p.y + btn_sz.y * .5f), + btn_sz.x * .5f, + ImGui::ColorConvertFloat4ToU32((ImGui::IsItemActive()||ImGui::IsItemHovered()) ? col_hvr : col) + ); + dl->AddImage(textureID, ImVec2(p.x + btn_pad, p.y + btn_pad), ImVec2(p.x + btn_sz.x - btn_pad, p.y + btn_sz.y - btn_pad)); + + return clicked; } void begin_right_aligned_buttons(const std::vector& labels) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoUtils.hpp b/src/slic3r/GUI/Gizmos/GLGizmoUtils.hpp index f6feee3df1..83699f8347 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoUtils.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoUtils.hpp @@ -19,6 +19,11 @@ namespace GLGizmoUtils { // Renders a tooltip button using the provided shortcuts void render_tooltip_button( ImGuiWrapper* imgui_wrapper, const GLCanvas3D& canvas, const std::vector>& shortcuts, float x, float y); + + void render_wiki_buttons( + ImGuiWrapper* imgui_wrapper, const GLCanvas3D& canvas, const char* wiki_url, const char* video_url = ""); + bool render_wiki_guide_button(const GLCanvas3D& canvas, float scale, const char* url = ""); + bool render_video_guide_button(const GLCanvas3D& canvas, float scale, const char* url = ""); // Sets up ImGui to render buttons that are right-aligned within the current window, using the provided labels to calculate spacing. void begin_right_aligned_buttons(const std::vector& labels); @@ -27,6 +32,8 @@ namespace GLGizmoUtils { void pop_orca_button_style(); + bool toolbar_circular_button(ImTextureID textureID, const char* id, float scale); + } // namespace GLGizmoUtils } // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 00bdd13bcb..d840754ac2 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -308,13 +308,18 @@ bool GLGizmosManager::init_icon_textures() else return false; - if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/toolbar_tooltip.svg", 25, 25, texture_id)) // ORCA: Use same resolution with gizmos to prevent blur on icon + if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/toolbar_tooltip.svg", 15, 15, texture_id)) // ORCA: Use same resolution with gizmos to prevent blur on icon icon_list.insert(std::make_pair((int)IC_TOOLBAR_TOOLTIP, texture_id)); else return false; - if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/toolbar_tooltip_hover.svg", 25, 25, texture_id)) // ORCA: Use same resolution with gizmos to prevent blur on icon - icon_list.insert(std::make_pair((int)IC_TOOLBAR_TOOLTIP_HOVER, texture_id)); + if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/toolbar_wiki.svg", 15, 15, texture_id)) + icon_list.insert(std::make_pair((int)IC_TOOLBAR_WIKI_GUIDE, texture_id)); + else + return false; + + if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/toolbar_video_guide.svg", 15, 15, texture_id)) + icon_list.insert(std::make_pair((int)IC_TOOLBAR_VIDEO_GUIDE, texture_id)); else return false; diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 3f53eeaa93..0914f8d958 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -182,7 +182,8 @@ public: IC_TOOLBAR_RESET_ZERO, IC_TOOLBAR_RESET_ZERO_HOVER, IC_TOOLBAR_TOOLTIP, - IC_TOOLBAR_TOOLTIP_HOVER, + IC_TOOLBAR_WIKI_GUIDE, + IC_TOOLBAR_VIDEO_GUIDE, IC_NAME_COUNT, IC_CANVAS_MENU, IC_CANVAS_MENU_HOVER, diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 84b702d7e5..bb5ffc87de 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -2309,6 +2309,12 @@ wxBoxSizer* MainFrame::create_side_tools() bool slice = true; + // The Slice-plate hover popup is a transient popup that keeps grabbing + // the mouse capture while shown. Left behind the modal grouping dialog it + // would starve that dialog of mouse events, so close it synchronously first. + if (m_filament_group_popup) + m_filament_group_popup->Dismiss(); + auto curr_plate = m_plater->get_partplate_list().get_curr_plate(); #ifdef __linux__ PresetBundle* preset = wxGetApp().preset_bundle; diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index d4ef74f77b..ce1c2f8cf4 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -3140,7 +3140,7 @@ void PartPlate::set_vase_mode_related_object_config(int obj_id) { new_conf.set_key_value("detect_thin_wall", new ConfigOptionBool(false)); new_conf.set_key_value("timelapse_type", new ConfigOptionEnum(tlTraditional)); new_conf.set_key_value("overhang_reverse", new ConfigOptionBool(false)); - auto applying_keys = global_config->diff(new_conf); + const auto applying_keys = global_config->diff(new_conf); for (ModelObject* object : obj_ptrs) { ModelConfigObject& config = object->config; @@ -3149,8 +3149,8 @@ void PartPlate::set_vase_mode_related_object_config(int obj_id) { config.set_key_value(opt_key, new_conf.option(opt_key)->clone()); } - applying_keys = config.get().diff(new_conf); - for (auto opt_key : applying_keys) { + const auto object_keys = config.get().diff(new_conf); + for (auto opt_key : object_keys) { config.set_key_value(opt_key, new_conf.option(opt_key)->clone()); } } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 9bf989cde6..a666e23c9c 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -8807,6 +8807,20 @@ void read_binary_stl(const std::string& filename, std::string& model_id, std::st return; } +// Logs what show_substitutions_info() would list, for loads that don't show the dialog. +static void log_substitutions(const ConfigSubstitutions& substitutions, const std::string& source) +{ + for (const ConfigSubstitution& substitution : substitutions) + BOOST_LOG_TRIVIAL(warning) << "Loading " << source << ": " << substitution.opt_def->opt_key << " = \"" << substitution.old_value + << "\" replaced with \"" << substitution.new_value->serialize() << "\""; +} + +static void log_substitutions(const PresetsConfigSubstitutions& substitutions, const std::string& source) +{ + for (const PresetConfigSubstitutions& preset : substitutions) + log_substitutions(preset.substitutions, source + " (preset " + preset.preset_name + ")"); +} + // BBS: backup & restore std::vector Plater::priv::load_files(const std::vector& input_files, LoadStrategy strategy, @@ -9108,7 +9122,6 @@ std::vector Plater::priv::load_files(const std::vector& input_ Semver app_version = *(Semver::parse(SoftFever_VERSION)); const wxString load_3mf_title = _L("Load 3MF"); const wxString newer_3mf_title = _L("Newer 3MF version"); - const wxString bambu_project_title = _L("BambuStudio Project"); const wxString msg_unsupported_geometry = _L("The 3MF is not supported by OrcaSlicer, loading geometry data only."); const wxString msg_old_orca_geometry = _L("The 3MF file was generated by an old OrcaSlicer version, loading geometry data only."); const wxString msg_older_geometry = _L("The 3MF file was generated by an older version, loading geometry data only."); @@ -9119,6 +9132,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ << boost::format("3MF import message [%1%]: %2% | file: %3%") % into_u8(title) % into_u8(text) % path.string(); show_info(q, text, title); }; + // Untagged files up to 2.3.2 may also come from OrcaSlicer, which only started tagging its 3MFs after it. + const bool is_bambu_studio_project = en_3mf_file_type == En3mfType::From_BBS && file_version > Semver(2, 3, 2); if (en_3mf_file_type == En3mfType::From_Prusa) { // do not reset the model config load_config = false; @@ -9178,8 +9193,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ } else if (en_3mf_file_type == En3mfType::From_BBS) { // No OrcaSlicer tag - check Bambu/Application version - Semver orca_tag_start_version(2, 3, 2); - if (file_version <= orca_tag_start_version) { + if (!is_bambu_studio_project) { // Compatible old version (before OrcaSlicer tagging was introduced after 2.3.2). // Any version prior or equal to 2.3.2 is older than the current one, no version warnings needed. // Still apply migration fixes for known old versions. @@ -9212,33 +9226,17 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } else { // BambuStudio project (version > 2.3.2 without OrcaSlicer tag) - // Report that a BambuStudio project is being imported and compare with SLIC3R_VERSION - Semver slic3r_version = *(Semver::parse(SLIC3R_VERSION)); if (load_config && config_loaded.empty()) { load_config = false; log_and_show_3mf_info(msg_bambu_geometry, load_3mf_title); } - else if (load_config && (file_version > slic3r_version)) { - // BambuStudio file version is newer than our compatible SLIC3R_VERSION - if (config_substitutions.unrecogized_keys.size() > 0) { - wxString text = wxString::Format(_L("The 3MF was created by BambuStudio (version %s), which is newer than the compatible version %s. Found unrecognized settings:"), - file_version.to_string(), slic3r_version.to_string()); - text += "\n"; - wxString context = text; - wxString append = _L("You should update your software.\n"); - context += "\n\n"; - context += append; - log_and_show_3mf_info(context, bambu_project_title); - } else { - wxString text = wxString::Format(_L("The 3MF was created by BambuStudio (version %s), which is newer than the compatible version %s. Some settings may not be fully compatible."), - file_version.to_string(), slic3r_version.to_string()); - text += "\n"; - log_and_show_3mf_info(text, bambu_project_title); - } - } else if (load_config && !published_config.published) { - // BambuStudio version is older or same as our SLIC3R_VERSION - wxString text = _L("The 3MF was created by BambuStudio. Some settings may differ from OrcaSlicer."); - log_and_show_3mf_info(text, bambu_project_title); + else if (load_config) { + // Logged, not shown: it is the same for every BambuStudio project and needs no action. + std::string unrecognized; + for (const std::string& key : config_substitutions.unrecogized_keys) + unrecognized += (unrecognized.empty() ? "" : ", ") + key; + BOOST_LOG_TRIVIAL(info) << "BambuStudio " << file_version.to_string() << " project " << path.string() + << ", unrecognized settings: " << (unrecognized.empty() ? "none" : unrecognized); } } } @@ -9327,7 +9325,10 @@ std::vector Plater::priv::load_files(const std::vector& input_ PresetsConfigSubstitutions preset_substitutions; PresetBundle & preset_bundle = *wxGetApp().preset_bundle; preset_substitutions = preset_bundle.load_project_embedded_presets(project_presets, ForwardCompatibilitySubstitutionRule::Enable); - if (!preset_substitutions.empty()) show_substitutions_info(preset_substitutions); + if (is_bambu_studio_project) + log_substitutions(preset_substitutions, path.string()); + else if (!preset_substitutions.empty()) + show_substitutions_info(preset_substitutions); } if (project_presets.size() > 0) { for (unsigned int i = 0; i < project_presets.size(); i++) { delete project_presets[i]; } @@ -9365,7 +9366,11 @@ std::vector Plater::priv::load_files(const std::vector& input_ notify_manager->bbl_show_3mf_warn_notification(error_message); } } - if (!config_substitutions.empty()) show_substitutions_info(config_substitutions.substitutions, filename.string()); + // BambuStudio projects routinely carry values Orca replaces; log them rather than showing a dialog on every open. + if (is_bambu_studio_project) + log_substitutions(config_substitutions.substitutions, path.string()); + else if (!config_substitutions.empty()) + show_substitutions_info(config_substitutions.substitutions, filename.string()); // BBS if (load_model && !load_config) { diff --git a/src/slic3r/GUI/PrintOptionsDialog.cpp b/src/slic3r/GUI/PrintOptionsDialog.cpp index e43e47c9aa..571ec0114a 100644 --- a/src/slic3r/GUI/PrintOptionsDialog.cpp +++ b/src/slic3r/GUI/PrintOptionsDialog.cpp @@ -20,7 +20,6 @@ #include "slic3r/GUI/DeviceCore/DevConfigUtil.h" #include "slic3r/GUI/DeviceManager.hpp" #include -#include "slic3r/GUI/Widgets/HyperLink.hpp" #include "slic3r/GUI/Widgets/Button.hpp" #include "libslic3r/CommonDefs.hpp" #include "slic3r/GUI/DeviceCore/DevDefs.h" @@ -1545,12 +1544,14 @@ PrinterPartsDialog::PrinterPartsDialog(wxWindow* parent) change_nozzle_tips->SetFont(Label::Body_13); change_nozzle_tips->SetForegroundColour(STATIC_TEXT_CAPTION_COL); - m_wiki_link = new HyperLink(single_panel, _L("Wiki Guide")); // ORCA - m_wiki_link->SetFont(Label::Body_13); + m_wiki_link = new Button(single_panel, "", "toolbar_wiki", 0, 15); + m_wiki_link->SetToolTip(_L("Wiki Guide")); + m_wiki_link->SetStyle(ButtonStyle::Confirm, ButtonType::Circle); + m_wiki_link->SetCanFocus(false); m_wiki_link->Bind(wxEVT_LEFT_DOWN, &PrinterPartsDialog::OnWikiClicked, this); - h_tips_sizer->Add(change_nozzle_tips, 0, wxLEFT); - h_tips_sizer->Add(m_wiki_link, 0, wxLEFT, FromDIP(5)); + h_tips_sizer->Add(m_wiki_link); + h_tips_sizer->Add(change_nozzle_tips, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(5)); wxSizer* single_update_nozzle_sizer = new wxBoxSizer(wxHORIZONTAL); m_single_update_nozzle_button = new Button(single_panel, _L("Refresh")); @@ -1656,13 +1657,15 @@ PrinterPartsDialog::PrinterPartsDialog(wxWindow* parent) multiple_change_nozzle_tips->SetFont(Label::Body_13); multiple_change_nozzle_tips->SetForegroundColour(STATIC_TEXT_CAPTION_COL); - multiple_wiki_link = new HyperLink(multiple_panel, _L("Wiki Guide")); // ORCA - multiple_wiki_link->SetFont(Label::Body_13); + multiple_wiki_link = new Button(multiple_panel, "", "toolbar_wiki", 0, 15); + multiple_wiki_link->SetToolTip(_L("Wiki Guide")); + multiple_wiki_link->SetStyle(ButtonStyle::Confirm, ButtonType::Circle); + multiple_wiki_link->SetCanFocus(false); multiple_wiki_link->Bind(wxEVT_LEFT_DOWN, &PrinterPartsDialog::OnWikiClicked, this); wxSizer* multiple_change_tips_sizer = new wxBoxSizer(wxHORIZONTAL); - multiple_change_tips_sizer->Add(multiple_change_nozzle_tips, 0, wxLEFT); - multiple_change_tips_sizer->Add(multiple_wiki_link, 0, wxLEFT, FromDIP(5)); + multiple_change_tips_sizer->Add(multiple_wiki_link); + multiple_change_tips_sizer->Add(multiple_change_nozzle_tips, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(5)); wxSizer* multiple_update_nozzle_sizer = new wxBoxSizer(wxHORIZONTAL); m_multiple_update_nozzle_button = new Button(multiple_panel, _L("Refresh")); diff --git a/src/slic3r/GUI/PrintOptionsDialog.hpp b/src/slic3r/GUI/PrintOptionsDialog.hpp index 803f35c7ff..6e49e19625 100644 --- a/src/slic3r/GUI/PrintOptionsDialog.hpp +++ b/src/slic3r/GUI/PrintOptionsDialog.hpp @@ -22,9 +22,9 @@ #include "Widgets/CheckBox.hpp" #include "Widgets/StaticLine.hpp" #include "Widgets/ComboBox.hpp" -#include "Widgets/HyperLink.hpp" // Previous definitions +class Button; class SwitchBoard; class MultiSwitchButton; @@ -41,7 +41,7 @@ protected: Label* nozzle_flow_type_label; ComboBox* nozzle_flow_type_checkbox; Label *change_nozzle_tips; - HyperLink* m_wiki_link; + Button* m_wiki_link; Button* m_single_update_nozzle_button; Button* m_multiple_update_nozzle_button; @@ -54,7 +54,7 @@ protected: ComboBox *multiple_right_nozzle_flow_checkbox; Label *multiple_change_nozzle_tips; - HyperLink* multiple_wiki_link; + Button* multiple_wiki_link; wxPanel *single_panel; wxPanel *multiple_panel; diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 7beb3bd9dd..080182a1b6 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -8,8 +8,8 @@ #include "ConfigValueFormatter.hpp" #include "FilamentBitmapUtils.hpp" #include "Widgets/Label.hpp" +#include "Widgets/Button.hpp" #include "Widgets/CheckBox.hpp" -#include "Widgets/HyperLink.hpp" #include "Widgets/TextInput.hpp" #include "Widgets/DialogButtons.hpp" #include "Widgets/StaticLine.hpp" @@ -756,11 +756,24 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent, dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); }); // Guide links, bottom-left, sharing the footer row with the OK/Cancel buttons (pushed right). - wxBoxSizer* links_sizer = new wxBoxSizer(wxVERTICAL); - auto* wiki_link = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/publishing_3mf/publish_3mf.html"); - auto* video_link = new HyperLink(this, _L("Video Guide"), "https://www.youtube.com/watch?v=-xt1N29UIOg"); - links_sizer->Add(wiki_link , 0, wxALIGN_LEFT); - links_sizer->Add(video_link, 0, wxTOP | wxALIGN_LEFT, FromDIP(4)); + wxBoxSizer* links_sizer = new wxBoxSizer(wxHORIZONTAL); + + auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15); + auto wiki_url = "https://www.orcaslicer.com/wiki/publishing_3mf/publish_3mf"; + wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url); + wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle); + wiki_btn->SetCanFocus(false); + wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);})); + + auto video_btn = new Button(this, "", "toolbar_video_guide", 0, 15); + auto video_url = "https://www.youtube.com/watch?v=-xt1N29UIOg"; + video_btn->SetToolTip(_L("Video Guide") + "\n" + video_url); + video_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle); + video_btn->SetCanFocus(false); + video_btn->Bind(wxEVT_LEFT_DOWN, ([video_url](auto& e) {wxLaunchDefaultBrowser(video_url);})); + + links_sizer->Add(wiki_btn , 0, wxLEFT, FromDIP(10)); + links_sizer->Add(video_btn, 0, wxLEFT, FromDIP(10)); wxBoxSizer* footer = new wxBoxSizer(wxHORIZONTAL); footer->Add(links_sizer, 0, wxALIGN_CENTER_VERTICAL); @@ -771,7 +784,7 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent, footer_line->SetMinSize(wxSize(-1, 1)); footer_line->SetMaxSize(wxSize(-1, 1)); w_sizer->Add(footer_line, 0, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10)); - w_sizer->Add(footer, 0, wxRIGHT | wxLEFT | wxTOP | wxBOTTOM | wxEXPAND, FromDIP(10)); + w_sizer->Add(footer, 0, wxEXPAND, FromDIP(10)); SetSizerAndFit(w_sizer); fit_to_content(); // initial size only; the dialog is resizable diff --git a/src/slic3r/GUI/Shortcuts.cpp b/src/slic3r/GUI/Shortcuts.cpp index 2395d31778..df8d7c00ca 100644 --- a/src/slic3r/GUI/Shortcuts.cpp +++ b/src/slic3r/GUI/Shortcuts.cpp @@ -113,6 +113,7 @@ constexpr std::array shortcut_table = {{ SHORTCUT(GizmoMeasure, "gizmo_measure", L("Gizmo measure"), PLATER, { 'U' }), SHORTCUT(GizmoAssembly, "gizmo_assembly", L("Gizmo assemble"), PLATER, { 'Y' }), SHORTCUT(GizmoBrimEars, "gizmo_brim_ears", L("Gizmo brim ears"), PLATER, { 'E' }), + SHORTCUT(GizmoDisplacement, "gizmo_displacement", L("Gizmo texture displacement painting"), PLATER, { 'D' }), // Sliders SHORTCUT(GoToLayer, "go_to_layer", L("Jump to layer"), PREVIEW, { 'G', SHIFT }), diff --git a/src/slic3r/GUI/Shortcuts.hpp b/src/slic3r/GUI/Shortcuts.hpp index 6a58d64971..aba315d521 100644 --- a/src/slic3r/GUI/Shortcuts.hpp +++ b/src/slic3r/GUI/Shortcuts.hpp @@ -38,7 +38,7 @@ enum class Shortcut : uint8_t { MoveSelectionLeft, MoveSelectionRight, MoveSelectionUp, MoveSelectionDown, RotateSelectionLeft, RotateSelectionRight, // Gizmos GizmoMove, GizmoRotate, GizmoScale, GizmoFlatten, GizmoCut, GizmoMeshBoolean, GizmoFdmSupports, GizmoSeam, GizmoFuzzySkin, - GizmoMmuSegmentation, GizmoEmboss, GizmoMeasure, GizmoAssembly, GizmoBrimEars, + GizmoMmuSegmentation, GizmoEmboss, GizmoMeasure, GizmoAssembly, GizmoBrimEars, GizmoDisplacement, // Sliders GoToLayer, LayerSliderUp, LayerSliderDown, MovesSliderLeft, MovesSliderRight, MovesSliderStart, MovesSliderEnd, // Painting tools diff --git a/src/slic3r/GUI/StepMeshDialog.cpp b/src/slic3r/GUI/StepMeshDialog.cpp index 07dc559528..379c354e72 100644 --- a/src/slic3r/GUI/StepMeshDialog.cpp +++ b/src/slic3r/GUI/StepMeshDialog.cpp @@ -4,7 +4,6 @@ #include "slic3r/GUI/GUI_Utils.hpp" #include "slic3r/GUI/Widgets/StateColor.hpp" #include "slic3r/GUI/Widgets/Label.hpp" -#include "slic3r/GUI/Widgets/HyperLink.hpp" #include "slic3r/GUI/MsgDialog.hpp" #include "libslic3r/LocalesUtils.hpp" #include "libslic3r/Thread.hpp" @@ -26,6 +25,7 @@ #include #include #include +#include #include using namespace Slic3r; @@ -131,19 +131,14 @@ StepMeshDialog::StepMeshDialog(wxWindow* parent, Slic3r::Step& file, double line auto tip_frame = new RoundedRectangle(this, StateColor::darkModeColorFor(wxColour("#F1F1F1")), wxDefaultPosition, wxSize(-1,-1), 6, 0); - wxBoxSizer* tips_sizer = new wxBoxSizer(wxVERTICAL); + wxBoxSizer* tips_sizer = new wxBoxSizer(wxHORIZONTAL); wxStaticText* info = new wxStaticText(tip_frame, wxID_ANY, _L("Smaller linear and angular deflections result in higher-quality transformations but increase the processing time.")); info->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); info->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F1F1F1"))); info->SetFont(::Label::Body_14); info->Wrap(FromDIP(450)); - // ORCA standardized HyperLink - HyperLink *tips = new HyperLink(tip_frame, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/import_export#step"); - tips->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F1F1F1"))); - - tips_sizer->Add(info, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(10)); - tips_sizer->Add(tips, 0, wxALL, FromDIP(10)); + tips_sizer->Add(info, 0, wxEXPAND |wxALL , FromDIP(10)); tip_frame->SetSizer(tips_sizer); tip_frame->Layout(); @@ -289,12 +284,24 @@ StepMeshDialog::StepMeshDialog(wxWindow* parent, Slic3r::Step& file, double line save_default_sizer->Add(m_save_default_checkbox, 0, wxALIGN_LEFT); bSizer->Add(save_default_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, LEFT_RIGHT_PADING); - wxBoxSizer* bSizer_button = new wxBoxSizer(wxHORIZONTAL); - bSizer_button->SetMinSize(wxSize(FromDIP(100), -1)); + bSizer->AddSpacer(FromDIP(10)); + m_checkbox = new wxCheckBox(this, wxID_ANY, _L("Don't show again"), wxDefaultPosition, wxDefaultSize, 0); m_checkbox->SetFont(::Label::Body_14); m_checkbox->SetForegroundColour(StateColor::darkModeColorFor(FONT_COLOR)); - bSizer_button->Add(m_checkbox, 0, wxALIGN_LEFT | wxLEFT | wxALIGN_CENTER_VERTICAL, LEFT_RIGHT_PADING); + bSizer->Add(m_checkbox, 0, wxLEFT, LEFT_RIGHT_PADING); + + wxBoxSizer* bSizer_button = new wxBoxSizer(wxHORIZONTAL); + + // ORCA standardized HyperLink + auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15); + auto wiki_url = "https://www.orcaslicer.com/wiki/import_export#step"; + wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url); + wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle); + wiki_btn->SetCanFocus(false); + wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);})); + bSizer_button->Add(wiki_btn, 0, wxALIGN_LEFT | wxLEFT | wxALIGN_CENTER_VERTICAL, LEFT_RIGHT_PADING); + bSizer_button->AddStretchSpacer(1); auto dlg_btns = new DialogButtons(this, {"OK", "Cancel"}); diff --git a/src/slic3r/GUI/TroubleshootDialog.cpp b/src/slic3r/GUI/TroubleshootDialog.cpp index 04e53fa4e4..aa6aa44fb2 100644 --- a/src/slic3r/GUI/TroubleshootDialog.cpp +++ b/src/slic3r/GUI/TroubleshootDialog.cpp @@ -236,7 +236,19 @@ TroubleshootDialog::TroubleshootDialog() Fit(); }); - auto link_wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/troubleshoot_center"); + auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15); + auto wiki_url = "https://www.orcaslicer.com/wiki/troubleshoot_center"; + wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url); + wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle); + wiki_btn->SetCanFocus(false); + wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);})); + + auto video_btn = new Button(this, "", "toolbar_video_guide", 0, 15); + auto video_url = "https://www.youtube.com/watch?v=CFzt8W7OCx0"; + video_btn->SetToolTip(_L("Video Guide") + "\n" + video_url); + video_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle); + video_btn->SetCanFocus(false); + video_btn->Bind(wxEVT_LEFT_DOWN, ([video_url](auto& e) {wxLaunchDefaultBrowser(video_url);})); // RIGHT SIZER ////////////////////// @@ -367,6 +379,10 @@ TroubleshootDialog::TroubleshootDialog() sys_btn_sizer->AddStretchSpacer(); sys_btn_sizer->Add(sys_copy_btn, 0, wxLEFT | wxRIGHT, FromDIP(5)); + wxBoxSizer *link_btn_sizer = new wxBoxSizer(wxHORIZONTAL); + link_btn_sizer->Add(wiki_btn); + link_btn_sizer->Add(video_btn, 0, wxLEFT, FromDIP(10)); + left_sizer->Add(m_header_logo , 0, wxEXPAND | wxALIGN_CENTER); left_sizer->Add(logo_line , 0, wxEXPAND | wxTOP, FromDIP(12)); left_sizer->Add(version , 0, wxEXPAND | wxTOP, FromDIP(6)); @@ -374,8 +390,7 @@ TroubleshootDialog::TroubleshootDialog() left_sizer->Add(sys_panel , 0, wxEXPAND | wxTOP, FromDIP(15)); left_sizer->AddStretchSpacer(); left_sizer->Add(sys_btn_sizer , 0, wxEXPAND | wxTOP, FromDIP(15)); - left_sizer->Add(link_wiki , 0, wxALIGN_CENTER | wxTOP, FromDIP(15)); - left_sizer->AddSpacer(FromDIP(5)); + left_sizer->Add(link_btn_sizer , 0, wxALIGN_CENTER | wxTOP, FromDIP(15)); wxBoxSizer *right_sizer = new wxBoxSizer(wxVERTICAL); diff --git a/src/slic3r/GUI/UVEditorCanvas.cpp b/src/slic3r/GUI/UVEditorCanvas.cpp index eedf39a527..2432026689 100644 --- a/src/slic3r/GUI/UVEditorCanvas.cpp +++ b/src/slic3r/GUI/UVEditorCanvas.cpp @@ -199,6 +199,7 @@ UVEditorCanvas::UVEditorCanvas(wxWindow *parent) Bind(wxEVT_MIDDLE_DOWN, &UVEditorCanvas::on_mouse, this); Bind(wxEVT_MIDDLE_UP, &UVEditorCanvas::on_mouse, this); Bind(wxEVT_MOTION, &UVEditorCanvas::on_mouse, this); + Bind(wxEVT_MOUSE_CAPTURE_LOST, &UVEditorCanvas::on_capture_lost, this); Bind(wxEVT_MOUSEWHEEL, &UVEditorCanvas::on_mouse, this); Bind(wxEVT_LEAVE_WINDOW, &UVEditorCanvas::on_leave, this); Bind(wxEVT_KEY_DOWN, &UVEditorCanvas::on_key, this); @@ -750,10 +751,47 @@ void UVEditorCanvas::end_gesture() m_rot_raw_deg = 0.f; m_rot_applied_deg = 0.f; m_modal_scale_accum = 1.f; + drop_mouse(); +} + +void UVEditorCanvas::cancel_gesture() +{ + // Undo what the gesture already applied live, as the Esc path does, and commit nothing. + if (m_on_island_edit) { + if (m_gesture == Gesture::RotateIslandModal && m_rot_applied_deg != 0.f) + m_on_island_edit(m_selected_island, Vec2f::Zero(), -m_rot_applied_deg, 1.f, false); + if (m_gesture == Gesture::ScaleIslandModal && m_modal_scale_accum != 1.f) + m_on_island_edit(m_selected_island, Vec2f::Zero(), 0.f, 1.f / m_modal_scale_accum, false); + } + + m_gesture = Gesture::None; + m_rot_raw_deg = 0.f; + m_rot_applied_deg = 0.f; + m_modal_scale_accum = 1.f; + m_vertex_edit_moved = false; +} + +void UVEditorCanvas::grab_mouse() +{ + if (!HasCapture()) + CaptureMouse(); +} + +void UVEditorCanvas::drop_mouse() +{ if (HasCapture()) ReleaseMouse(); } +// The capture was taken from us (a dialog opened, another application grabbed the pointer). wx +// requires this to cancel the gesture: no commit, no Skip(), and no ReleaseMouse() - the capture is +// already gone, and releasing it again would unbalance the stack. +void UVEditorCanvas::on_capture_lost(wxMouseCaptureLostEvent &) +{ + cancel_gesture(); + Refresh(); +} + void UVEditorCanvas::on_key(wxKeyEvent &evt) { const int key = evt.GetKeyCode(); @@ -932,7 +970,7 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt) } m_gesture = (m_selected_island >= 0) ? Gesture::MoveIsland : Gesture::Pan; } - CaptureMouse(); + grab_mouse(); Refresh(); } else if (type == wxEVT_RIGHT_DOWN && m_selected_island >= 0 && m_select_mode == SelectMode::Island) { const Vec2f rel = screen_to_uv(pos) - island_centroid(m_selected_island); @@ -942,12 +980,16 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt) m_rot_base_deg = island_rotation_deg(m_selected_island); m_rot_display_deg = m_rot_base_deg; m_gesture_last_angle = std::atan2(rel.y(), rel.x()); - CaptureMouse(); + grab_mouse(); } else if (type == wxEVT_MIDDLE_DOWN) { m_gesture = Gesture::Pan; m_drag_last_px = pos; - CaptureMouse(); + grab_mouse(); } else if (type == wxEVT_LEFT_UP || type == wxEVT_RIGHT_UP || type == wxEVT_MIDDLE_UP) { + // The drag is over either way. A modal R/S keeps running until a click confirms it, but it + // tracks the pointer over this canvas and needs no capture to do so, so the capture goes back + // here rather than waiting for that click - which may never come. + drop_mouse(); if (m_gesture != Gesture::RotateIslandModal && m_gesture != Gesture::ScaleIslandModal) { end_gesture(); Refresh(); @@ -1735,7 +1777,7 @@ public: bool toggle, bool accent = false, int size_dip = 26) : wxWindow(parent, id, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE | wxFULL_REPAINT_ON_RESIZE) , m_icon_name(icon), m_icon_dip(size_dip >= 26 ? 16 : 14), m_label(label), m_toggle(toggle), m_accent(accent) - , m_size_dip(size_dip) + , m_size_dip(size_dip), m_tip(tip) { SetBackgroundStyle(wxBG_STYLE_PAINT); SetToolTip(tip); @@ -1748,7 +1790,7 @@ public: Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent &) { m_hover = true; Refresh(); }); Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent &) { m_hover = false; m_pressed = false; Refresh(); }); Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent &) { - if (IsEnabled()) { + if (usable()) { m_pressed = true; Refresh(); } @@ -1757,7 +1799,7 @@ public: const bool was_pressed = m_pressed; m_pressed = false; Refresh(); - if (!was_pressed || !IsEnabled() || !GetClientRect().Contains(e.GetPosition())) + if (!was_pressed || !usable() || !GetClientRect().Contains(e.GetPosition())) return; if (m_toggle) m_on = !m_on; @@ -1797,6 +1839,30 @@ public: Refresh(); return changed; } + // Soft-disable: the button is drawn faded and swallows clicks, but stays a live window, so hovering it + // still raises its tooltip - now with `reason` appended, saying what to do to make it usable. A window + // really disabled with Enable(false) gets no mouse events at all on GTK and MSW, which leaves the user + // guessing; this is the same trade-off the gizmo panel's icon_toggle() makes with its `unavailable`. + // An empty reason makes the button usable again. + void SetUnavailable(const wxString &reason) + { + if (reason == m_unavailable) + return; + m_unavailable = reason; + SetToolTip(reason.empty() || m_tip.empty() ? m_tip : m_tip + "\n\n" + reason); + if (!m_unavailable.empty()) + m_pressed = false; // a reason appearing mid-press cancels the press + Refresh(); + } + // Replaces the plain tooltip, keeping whatever reason is currently appended to it. + void SetTip(const wxString &tip) + { + if (tip == m_tip) + return; + m_tip = tip; + SetToolTip(m_unavailable.empty() || m_tip.empty() ? m_tip : m_tip + "\n\n" + m_unavailable); + } + bool usable() const { return IsEnabled() && m_unavailable.empty(); } protected: wxSize DoGetBestSize() const override @@ -1815,7 +1881,7 @@ private: const PaneColors c = PaneColors::current(); const wxRect r = GetClientRect(); const wxColour teal(0x00, 0x96, 0x88); - const bool enabled = IsEnabled(); + const bool enabled = usable(); wxColour fill = c.bg, border = c.frame, text = c.ink; if (m_accent) { @@ -1875,6 +1941,8 @@ private: bool m_toggle = false; bool m_accent = false; int m_size_dip = 26; + wxString m_tip; // the tooltip without any m_unavailable reason appended + wxString m_unavailable; // non-empty: faded and unclickable, and why (see SetUnavailable()) bool m_on = false; bool m_badge = false; bool m_hover = false; @@ -1920,6 +1988,9 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) }); m_layer_name->SetMinSize(wxSize(FromDIP(30), -1)); m_tile = text(wxEmptyString, c.dim); + m_tile->SetToolTip(_L("The active layer's tile size: how much of the model one repeat of the texture covers. The " + "canvas is measured in tiles, so one grid cell is one repeat. Change it with Tiling in the " + "layer's settings.")); header->Add(m_thumb, 0, wxALIGN_CENTER_VERTICAL); header->Add(m_layer_name, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, gap); header->Add(m_tile, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, gap); @@ -1973,9 +2044,19 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) strip->Add(r, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, FromDIP(11)); strip->AddSpacer(FromDIP(7)); }; - m_select[0] = tool(ID_UV_SELECT_ISLAND, "texture_displacement_uv_select_island", _L("Island - move, rotate and scale whole islands"), true); - m_select[1] = tool(ID_UV_SELECT_VERTEX, "texture_displacement_uv_select_vertex", _L("Vertex - drag vertices to reshape; Shift/Ctrl to multi-select"), true); - m_select[2] = tool(ID_UV_SELECT_EDGE, "texture_displacement_uv_select_edge", _L("Edge - drag edges to reshape; Shift/Ctrl to multi-select"), true); + m_select[0] = tool(ID_UV_SELECT_ISLAND, "texture_displacement_uv_select_island", + _L("Island - work on whole islands. Click one to select it, then drag to move it, right-drag to " + "rotate it, or press R to rotate and S to scale with the mouse (click or Enter to confirm, Esc " + "to cancel)."), + true); + m_select[1] = tool(ID_UV_SELECT_VERTEX, "texture_displacement_uv_select_vertex", + _L("Vertex - drag vertices to reshape an island by hand; Shift adds to the selection, Ctrl " + "toggles one in or out of it."), + true); + m_select[2] = tool(ID_UV_SELECT_EDGE, "texture_displacement_uv_select_edge", + _L("Edge - drag edges to reshape an island by hand; Shift adds to the selection, Ctrl toggles " + "one in or out of it."), + true); strip_rule(); m_mark_seams = tool(ID_UV_MARK_SEAMS, "texture_displacement_uv_seam", _L("Mark seams - click edges on the model to cut the unwrap along them. The edge under the cursor is " @@ -1996,7 +2077,11 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) m_clear_edits = tool(ID_UV_CLEAR_EDITS, "texture_displacement_uv_clear_edits", _L("Clear UV edits - discard all manual vertex/edge moves and return the unwrap to its automatic shape"), false); m_snap = tool(ID_UV_SNAP, "texture_displacement_uv_snap", _L("Snap - stick islands together when dragging one against another"), true); - m_frame = tool(ID_UV_FRAME, "texture_displacement_uv_frame", _L("Frame all islands (Home)"), false); + m_frame = tool(ID_UV_FRAME, "texture_displacement_uv_frame", + _L("Frame all islands, fitting every one of them in view (Home or F).\n" + "Elsewhere on the canvas: scroll to zoom around the cursor, and middle-drag - or drag empty space - " + "to pan."), + false); strip->AddSpacer(FromDIP(4)); m_canvas = new UVEditorCanvas(this); @@ -2007,6 +2092,7 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) // ---- status line: the current gesture on the left, the unwrap summary on the right ---- m_status = text(wxEmptyString, c.dim, wxST_ELLIPSIZE_END); + m_status->SetToolTip(_L("What is selected, and the exact figures of the move, rotation or scale while you drag one.")); m_status->SetMinSize(wxSize(FromDIP(40), -1)); m_stats = text(wxEmptyString, c.dim); auto *status = new wxBoxSizer(wxHORIZONTAL); @@ -2082,19 +2168,25 @@ void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s) } else { m_thumb->SetBitmap(wxNullBitmap); } - m_thumb->Enable(s.has_layer); + // Every tool that cannot be used right now is faded with the reason appended to its tooltip, rather than + // being hard-disabled (which would hide the tooltip too - see UVToolButton::SetUnavailable()). + const wxString no_layer = s.has_layer ? wxString() : + _L("The pane follows the active texture layer, and that layer has to be mapped " + "with Unwrap (LSCM). Add a layer and set its Mapping to Unwrap."); + m_thumb->SetUnavailable(no_layer); + m_layer_name->SetToolTip(s.has_layer ? m_thumb->GetToolTipText() : no_layer); for (int i = 0; i < 3; ++i) { m_background[i]->SetValue(int(s.background) == i); - m_background[i]->Enable(s.has_layer); + m_background[i]->SetUnavailable(no_layer); } - m_unwrap->Enable(s.has_layer); + m_unwrap->SetUnavailable(no_layer); m_unwrap->SetBadge(s.unwrap_stale); - m_unwrap->SetToolTip(s.unwrap_stale ? - _L("Out of date - the paint, the seams or the seam angle changed since this unwrap was made. " - "Press to unwrap again.") : - _L("Flatten the painted area into UV islands. It is computed only when you press this, not on " - "every edit - so paint, change the seam angle or mark seams first, then press Unwrap.")); + m_unwrap->SetTip(s.unwrap_stale ? + _L("Out of date - the paint, the seams or the seam angle changed since this unwrap was made. " + "Press to unwrap again.") : + _L("Flatten the painted area into UV islands. It is computed only when you press this, not on " + "every edit - so paint, change the seam angle or mark seams first, then press Unwrap.")); if (m_seam_angle->GetValue() != int(std::lround(s.seam_angle_deg))) m_seam_angle->SetValue(int(std::lround(s.seam_angle_deg))); @@ -2103,15 +2195,26 @@ void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s) m_connect->Enable(s.has_layer); m_mark_seams->SetValue(s.mark_seams); - m_mark_seams->Enable(s.has_layer); + m_mark_seams->SetUnavailable(no_layer); m_seam_path->SetValue(s.seam_path); - m_seam_path->Enable(s.has_layer && s.mark_seams); - m_clear_seams->Enable(s.has_layer && s.has_seams); - m_clear_edits->Enable(s.has_layer && s.has_uv_edits); + m_seam_path->SetUnavailable(!no_layer.empty() ? no_layer : + s.mark_seams ? wxString() : + _L("Turn Mark seams on first - Path is a quicker way of marking them.")); + m_clear_seams->SetUnavailable(!no_layer.empty() ? no_layer : + s.has_seams ? wxString() : + _L("No seams are marked on this layer.")); + m_clear_edits->SetUnavailable(!no_layer.empty() ? no_layer : + s.has_uv_edits ? wxString() : + _L("No islands have been reshaped by hand, so there is nothing to " + "discard.")); m_stats->SetLabel(s.unwrapped ? wxString::Format(_L("%d islands, %s faces"), s.island_count, wxString(std::to_string(s.face_count))) : wxString()); + m_stats->SetToolTip(s.unwrapped ? _L("How the painted area came out of the unwrap: the number of separate pieces it " + "was cut into (at the seams and at edges sharper than the seam angle), and how " + "many triangles they hold in total.") : + wxString()); refresh_selection_tools(); if (relayout) Layout(); @@ -2121,19 +2224,24 @@ void UVEditorPanel::refresh_selection_tools() { const bool has_islands = m_canvas->has_islands(); const int mode = int(m_canvas->select_mode()); + // Faded rather than hard-disabled, so the tooltip still says what is missing (see apply_state()). + const wxString not_unwrapped = has_islands ? wxString() : _L("Press Unwrap first - there are no islands to work on yet."); for (int i = 0; i < 3; ++i) { m_select[i]->SetValue(i == mode); - m_select[i]->Enable(has_islands); + m_select[i]->SetUnavailable(not_unwrapped); } const bool island_picked = has_islands && m_canvas->select_mode() == UVEditorCanvas::SelectMode::Island && m_canvas->selected_island() >= 0; - m_avg_scale->Enable(has_islands); - m_cut->Enable(island_picked); - m_join->Enable(island_picked); - m_unjoin->Enable(island_picked); - m_snap->Enable(has_islands); + const wxString no_island = !not_unwrapped.empty() ? not_unwrapped : + island_picked ? wxString() : + _L("Click an island on the canvas first, in Island mode."); + m_avg_scale->SetUnavailable(not_unwrapped); + m_cut->SetUnavailable(no_island); + m_join->SetUnavailable(no_island); + m_unjoin->SetUnavailable(no_island); + m_snap->SetUnavailable(not_unwrapped); m_snap->SetValue(m_canvas->snap_enabled()); - m_frame->Enable(has_islands); + m_frame->SetUnavailable(not_unwrapped); } void UVEditorPanel::on_tool(wxCommandEvent &evt) diff --git a/src/slic3r/GUI/UVEditorCanvas.hpp b/src/slic3r/GUI/UVEditorCanvas.hpp index 2b3ee0781d..2fc1055cb8 100644 --- a/src/slic3r/GUI/UVEditorCanvas.hpp +++ b/src/slic3r/GUI/UVEditorCanvas.hpp @@ -200,6 +200,7 @@ private: void on_paint(wxPaintEvent &evt); void on_size(wxSizeEvent &evt); void on_mouse(wxMouseEvent &evt); + void on_capture_lost(wxMouseCaptureLostEvent &evt); void on_key(wxKeyEvent &evt); void on_leave(wxMouseEvent &evt); // drops the +/- cursor hint when the pointer leaves the canvas void on_erase_background(wxEraseEvent &evt) {} // required to avoid flicker on MSW, deliberately a no-op @@ -244,6 +245,16 @@ private: // vertex of some *other* island, in texture-UV space. Zero if nothing is within reach (#2). Vec2f snap_correction(int island) const; void end_gesture(); + // Cancels the gesture instead of finishing it: nothing is committed, and a modal rotate/scale is + // put back the way Esc puts it back. Used when the capture is taken away from us. + void cancel_gesture(); + // One capture at a time, released exactly once. Pressing a second button mid-drag would otherwise + // nest a second capture that the single release on button-up cannot undo, and macOS never sends + // wxEVT_MOUSE_CAPTURE_LOST to recover from that. A leaked capture is not a local problem there: + // while any wx window holds one, wxOSX routes every mouse event to it and the application stops + // seeing enter/leave and motion entirely, which also takes its tooltips down. + void grab_mouse(); + void drop_mouse(); // Rebuilds the status line from the current gesture/selection and pushes it to m_on_status. void update_status(); // Re-picks what a click at `pos` would grab in the current select mode, and repaints when that changed. diff --git a/src/slic3r/GUI/UnsavedChangesDialog.cpp b/src/slic3r/GUI/UnsavedChangesDialog.cpp index b2bc818ad1..ffb3724411 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.cpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -62,7 +63,6 @@ #include "PresetComboBoxes.hpp" #include "Widgets/CheckBox.hpp" #include "Widgets/DialogButtons.hpp" -#include "Widgets/HyperLink.hpp" #ifdef __linux__ #define wxLinux true @@ -1011,8 +1011,14 @@ void UnsavedChangesDialog::build(Preset::Type type, PresetCollection *dependent_ checkbox_sizer->Show(bool(m_buttons & REMEMBER_CHOISE)); if (dependent_presets != nullptr) { - auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/transfer_discard_changes"); - m_sizer_button->Add(wiki, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(22)); + auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15); + auto wiki_url = "https://www.orcaslicer.com/wiki/transfer_discard_changes"; + wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url); + wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle); + wiki_btn->SetCanFocus(false); + wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);})); + + m_sizer_button->Add(wiki_btn, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(22)); } m_sizer_button->Add(0, 0, 1, 0, 0); diff --git a/src/slic3r/GUI/Widgets/Button.cpp b/src/slic3r/GUI/Widgets/Button.cpp index 5ecbafda2b..63558245c8 100644 --- a/src/slic3r/GUI/Widgets/Button.cpp +++ b/src/slic3r/GUI/Widgets/Button.cpp @@ -223,6 +223,12 @@ void Button::SetStyle(const ButtonStyle style, const ButtonType type) this->SetMinSize(FromDIP(wxSize(26, 26))); this->SetSize(FromDIP(wxSize(26, 26))); this->SetCornerRadius(this->FromDIP(4)); + } else if (type == ButtonType::Circle) { + this->SetPaddingSize(FromDIP(wxSize(6, 6))); + this->SetMinSize(FromDIP(wxSize(25, 25))); + this->SetMaxSize(FromDIP(wxSize(25, 25))); + this->SetSize(FromDIP(wxSize(25, 25))); + this->SetCornerRadius(this->FromDIP(12)); } else if (type == ButtonType::Expanded) { this->SetMinSize(FromDIP(wxSize(-1, 32))); this->SetPaddingSize(FromDIP(wxSize(12, 8))); diff --git a/src/slic3r/GUI/Widgets/Button.hpp b/src/slic3r/GUI/Widgets/Button.hpp index b4b1e24bb2..f5f675051f 100644 --- a/src/slic3r/GUI/Widgets/Button.hpp +++ b/src/slic3r/GUI/Widgets/Button.hpp @@ -34,6 +34,7 @@ enum class ButtonType { Parameter, // Font14 Semi-Rounded For buttons that near parameter boxes Icon, // ------ Semi-Rounded For buttons that only has icons. icons should be 16x16 and iconSize has to be defined as 16 while // creation of button + Circle, // ------ FullyRounded Same as icon Expanded, // Font14 Semi-Rounded For full length buttons. ex. buttons in static box }; diff --git a/src/slic3r/GUI/calib_dlg.cpp b/src/slic3r/GUI/calib_dlg.cpp index d8b6e29343..ee8da8f0b2 100644 --- a/src/slic3r/GUI/calib_dlg.cpp +++ b/src/slic3r/GUI/calib_dlg.cpp @@ -12,6 +12,7 @@ #include #include "slic3r/GUI/GUI_Utils.hpp" #include +#include "slic3r/GUI/Widgets/Button.hpp" #include "slic3r/GUI/Widgets/Label.hpp" #include "slic3r/GUI/Widgets/LabeledStaticBox.hpp" #include "slic3r/GUI/Widgets/RadioGroup.hpp" @@ -25,7 +26,6 @@ #include #include "MainFrame.hpp" #include "Widgets/DialogButtons.hpp" -#include "Widgets/HyperLink.hpp" #include #include #include @@ -35,6 +35,7 @@ #include #include #include +#include #include #include "libslic3r/PrintConfig.hpp" #include "libslic3r/Flow.hpp" @@ -250,8 +251,15 @@ PA_Calibration_Dlg::PA_Calibration_Dlg(wxWindow* parent, wxWindowID id, Plater* auto dlg_btns = new DialogButtons(this, {"OK"}); auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL); - auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/pressure_advance_calib"); - bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20)); + + auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15); + auto wiki_url = "https://www.orcaslicer.com/wiki/pressure_advance_calib"; + wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url); + wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle); + wiki_btn->SetCanFocus(false); + wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);})); + + bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10)); bottom_sizer->AddStretchSpacer(); bottom_sizer->Add(dlg_btns, 0, wxEXPAND); v_sizer->Add(bottom_sizer, 0, wxEXPAND); @@ -505,8 +513,15 @@ Temp_Calibration_Dlg::Temp_Calibration_Dlg(wxWindow* parent, wxWindowID id, Plat auto dlg_btns = new DialogButtons(this, {"OK"}); auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL); - auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/temp_calib"); - bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20)); + + auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15); + auto wiki_url = "https://www.orcaslicer.com/wiki/temp_calib"; + wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url); + wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle); + wiki_btn->SetCanFocus(false); + wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);})); + + bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10)); bottom_sizer->AddStretchSpacer(); bottom_sizer->Add(dlg_btns, 0, wxEXPAND); v_sizer->Add(bottom_sizer, 0, wxEXPAND); @@ -709,8 +724,15 @@ MaxVolumetricSpeed_Test_Dlg::MaxVolumetricSpeed_Test_Dlg(wxWindow* parent, wxWin auto dlg_btns = new DialogButtons(this, {"OK"}); auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL); - auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/volumetric_speed_calib"); - bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20)); + + auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15); + auto wiki_url = "https://www.orcaslicer.com/wiki/volumetric_speed_calib"; + wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url); + wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle); + wiki_btn->SetCanFocus(false); + wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);})); + + bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10)); bottom_sizer->AddStretchSpacer(); bottom_sizer->Add(dlg_btns, 0, wxEXPAND); v_sizer->Add(bottom_sizer, 0, wxEXPAND); @@ -833,8 +855,15 @@ VFA_Test_Dlg::VFA_Test_Dlg(wxWindow* parent, wxWindowID id, Plater* plater) auto dlg_btns = new DialogButtons(this, {"OK"}); auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL); - auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/vfa_calib"); - bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20)); + + auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15); + auto wiki_url = "https://www.orcaslicer.com/wiki/vfa_calib"; + wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url); + wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle); + wiki_btn->SetCanFocus(false); + wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);})); + + bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10)); bottom_sizer->AddStretchSpacer(); bottom_sizer->Add(dlg_btns, 0, wxEXPAND); v_sizer->Add(bottom_sizer, 0, wxEXPAND); @@ -1072,8 +1101,15 @@ Retraction_Test_Dlg::Retraction_Test_Dlg(wxWindow* parent, wxWindowID id, Plater auto dlg_btns = new DialogButtons(this, {"OK"}); auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL); - auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/retraction_calib"); - bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20)); + + auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15); + auto wiki_url = "https://www.orcaslicer.com/wiki/retraction_calib"; + wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url); + wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle); + wiki_btn->SetCanFocus(false); + wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);})); + + bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10)); bottom_sizer->AddStretchSpacer(); bottom_sizer->Add(dlg_btns, 0, wxEXPAND); v_sizer->Add(bottom_sizer, 0, wxEXPAND); @@ -1252,8 +1288,15 @@ Input_Shaping_Freq_Test_Dlg::Input_Shaping_Freq_Test_Dlg(wxWindow* parent, wxWin auto dlg_btns = new DialogButtons(this, {"OK"}); auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL); - auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/input_shaping_calib"); - bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20)); + + auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15); + auto wiki_url = "https://www.orcaslicer.com/wiki/input_shaping_calib"; + wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url); + wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle); + wiki_btn->SetCanFocus(false); + wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);})); + + bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10)); bottom_sizer->AddStretchSpacer(); bottom_sizer->Add(dlg_btns, 0, wxEXPAND); v_sizer->Add(bottom_sizer, 0, wxEXPAND); @@ -1451,8 +1494,15 @@ Input_Shaping_Damp_Test_Dlg::Input_Shaping_Damp_Test_Dlg(wxWindow* parent, wxWin auto dlg_btns = new DialogButtons(this, {"OK"}); auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL); - auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/input_shaping_calib"); - bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20)); + + auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15); + auto wiki_url = "https://www.orcaslicer.com/wiki/input_shaping_calib"; + wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url); + wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle); + wiki_btn->SetCanFocus(false); + wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);})); + + bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10)); bottom_sizer->AddStretchSpacer(); bottom_sizer->Add(dlg_btns, 0, wxEXPAND); v_sizer->Add(bottom_sizer, 0, wxEXPAND); @@ -1647,8 +1697,15 @@ Cornering_Test_Dlg::Cornering_Test_Dlg(wxWindow* parent, wxWindowID id, Plater* auto dlg_btns = new DialogButtons(this, {"OK"}); auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL); - auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/cornering_calib"); - bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20)); + + auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15); + auto wiki_url = "https://www.orcaslicer.com/wiki/cornering_calib"; + wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url); + wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle); + wiki_btn->SetCanFocus(false); + wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);})); + + bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10)); bottom_sizer->AddStretchSpacer(); bottom_sizer->Add(dlg_btns, 0, wxEXPAND); v_sizer->Add(bottom_sizer, 0, wxEXPAND); @@ -1786,8 +1843,15 @@ FlowRateCalibrationDialog::FlowRateCalibrationDialog(wxWindow* parent, wxWindowI auto dlg_btns = new DialogButtons(this, {"OK"}); auto bottom_sizer = new wxBoxSizer(wxHORIZONTAL); - auto wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/flow_ratio_calib"); - bottom_sizer->Add(wiki, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(20)); + + auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15); + auto wiki_url = "https://www.orcaslicer.com/wiki/flow_ratio_calib"; + wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url); + wiki_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Circle); + wiki_btn->SetCanFocus(false); + wiki_btn->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);})); + + bottom_sizer->Add(wiki_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10)); bottom_sizer->AddStretchSpacer(); bottom_sizer->Add(dlg_btns, 0, wxEXPAND); v_sizer->Add(bottom_sizer, 0, wxEXPAND); diff --git a/src/slic3r/Utils/Http.cpp b/src/slic3r/Utils/Http.cpp index 552c0b6a39..c268ef251c 100644 --- a/src/slic3r/Utils/Http.cpp +++ b/src/slic3r/Utils/Http.cpp @@ -947,13 +947,12 @@ std::string Http::tls_global_init() std::string Http::tls_system_cert_store() { - std::string ret; - #ifdef OPENSSL_CERT_OVERRIDE - ret = ::getenv(X509_get_default_cert_file_env()); + if (const char *cert_file = ::getenv(X509_get_default_cert_file_env())) + return cert_file; #endif - return ret; + return {}; } std::string Http::url_encode(const std::string &str) diff --git a/src/slic3r/plugin/PluginFsUtils.hpp b/src/slic3r/plugin/PluginFsUtils.hpp index bd335373d0..d485ac5efe 100644 --- a/src/slic3r/plugin/PluginFsUtils.hpp +++ b/src/slic3r/plugin/PluginFsUtils.hpp @@ -25,11 +25,19 @@ extern const char* const INSTALL_STATE_FILE; // Plugin config and orca.host.ui payloads both cross the boundary as plain JSON-compatible // values, so both go through these. -inline pybind11::object json_to_py(const nlohmann::json& j) +// Maximum nesting depth for JSON <-> Python conversion. A self-referential or pathologically +// deep value would otherwise recurse until the native C stack overflows, an uncatchable crash; +// past this bound we raise instead. 200 is far beyond any legitimate plugin config or UI payload. +inline constexpr int kMaxJsonConversionDepth = 200; + +inline pybind11::object json_to_py(const nlohmann::json& j, int depth = 0) { namespace py = pybind11; using json = nlohmann::json; + if (depth > kMaxJsonConversionDepth) + throw py::value_error("Plugin JSON value nested too deeply"); + switch (j.type()) { case json::value_t::null: return py::none(); case json::value_t::boolean: return py::bool_(j.get()); @@ -40,24 +48,27 @@ inline pybind11::object json_to_py(const nlohmann::json& j) case json::value_t::array: { py::list lst; for (const auto& e : j) - lst.append(json_to_py(e)); + lst.append(json_to_py(e, depth + 1)); return lst; } case json::value_t::object: { py::dict d; for (auto it = j.begin(); it != j.end(); ++it) - d[py::str(it.key())] = json_to_py(it.value()); + d[py::str(it.key())] = json_to_py(it.value(), depth + 1); return d; } default: return py::none(); } } -inline nlohmann::json py_to_json(const pybind11::handle& o) +inline nlohmann::json py_to_json(const pybind11::handle& o, int depth = 0) { namespace py = pybind11; using json = nlohmann::json; + if (depth > kMaxJsonConversionDepth) + throw py::value_error("Plugin value nested too deeply (possible cycle)"); + if (o.is_none()) return json(nullptr); if (py::isinstance(o)) // bool before int (bool subclasses int in Python) @@ -73,13 +84,13 @@ inline nlohmann::json py_to_json(const pybind11::handle& o) if (py::isinstance(o)) { json obj = json::object(); for (auto item : py::reinterpret_borrow(o)) - obj[py::str(item.first).cast()] = py_to_json(item.second); + obj[py::str(item.first).cast()] = py_to_json(item.second, depth + 1); return obj; } if (py::isinstance(o) || py::isinstance(o)) { json arr = json::array(); for (auto e : o) - arr.push_back(py_to_json(e)); + arr.push_back(py_to_json(e, depth + 1)); return arr; } return py::str(o).cast(); // fallback: str() diff --git a/tests/cli/CMakeLists.txt b/tests/cli/CMakeLists.txt index 171140c775..dddebee566 100644 --- a/tests/cli/CMakeLists.txt +++ b/tests/cli/CMakeLists.txt @@ -11,6 +11,13 @@ endif () add_test(NAME cli_strict_mode COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_strict.sh $ ${ORCA_CLI_TEST_PYTHON}) +add_test(NAME cli_empty_project_config + COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_empty_project_config.sh $ ${ORCA_CLI_TEST_PYTHON} ${CMAKE_SOURCE_DIR}) +set_tests_properties(cli_empty_project_config PROPERTIES + LABELS "CLI;RequiresApp" + SKIP_RETURN_CODE 77 + TIMEOUT 300) + set_tests_properties(cli_strict_mode PROPERTIES LABELS "CLI;RequiresApp" SKIP_RETURN_CODE 77 diff --git a/tests/cli/test_cli_empty_project_config.sh b/tests/cli/test_cli_empty_project_config.sh new file mode 100755 index 0000000000..8b4dd0b480 --- /dev/null +++ b/tests/cli/test_cli_empty_project_config.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Regression check: a 3mf whose Metadata/project_settings.config carries no settings must load. +# +# The CLI reads printable_height out of the project config with opt_float(), which dereferences +# what option<>() returns. With create = false that is null when the key is absent, so a project +# saved without settings used to take the CLI down with a segfault. Both models in +# resources/handy_models are such files. +# +# usage: test_cli_empty_project_config.sh +set -u + +BIN="${1:-}" +PY="${2:-python3}" +SRC="${3:-}" +# 77 is the test's SKIP_RETURN_CODE. +[ -x "$BIN" ] || { echo "SKIP: orca-slicer binary not found: $BIN"; exit 77; } +[ -f "$SRC/resources/handy_models/OrcaBadge.3mf" ] || { echo "SKIP: handy model not found"; exit 77; } + +WORK="$(mktemp -d "${TMPDIR:-/tmp}/orca-cli-emptycfg.XXXXXX")" +trap 'rm -rf "$WORK"' EXIT +mkdir -p "$WORK/datadir" + +# Rewrite the project settings to an empty object, so the test holds no matter what the shipped +# models carry later on. +cp "$SRC/resources/handy_models/OrcaBadge.3mf" "$WORK/empty_config.3mf" +"$PY" - "$WORK/empty_config.3mf" <<'PYEOF' +import shutil, sys, zipfile + +path = sys.argv[1] +entry = "Metadata/project_settings.config" +with zipfile.ZipFile(path) as src: + items = [(i, src.read(i.filename)) for i in src.infolist()] +with zipfile.ZipFile(path + ".new", "w", zipfile.ZIP_DEFLATED) as dst: + seen = False + for info, data in items: + if info.filename == entry: + data, seen = b"{\n}\n", True + dst.writestr(info, data) + if not seen: + dst.writestr(entry, b"{\n}\n") +shutil.move(path + ".new", path) +PYEOF + +"$BIN" --datadir "$WORK/datadir" --info "$WORK/empty_config.3mf" > "$WORK/info.txt" 2>&1 +rc=$? +if [ $rc -ne 0 ]; then + echo "FAIL: --info on a project with empty settings exited $rc" + tail -20 "$WORK/info.txt" + exit 1 +fi +grep -q "size_x" "$WORK/info.txt" || { echo "FAIL: --info printed no geometry"; cat "$WORK/info.txt"; exit 1; } +echo "PASS: a project with empty settings loads" diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp index 19ad1711b8..e1d93c5e15 100644 --- a/tests/fff_print/test_fill.cpp +++ b/tests/fff_print/test_fill.cpp @@ -1780,7 +1780,7 @@ TEST_CASE("Sparse plane-path anchors match the printed infill", "[Fill][Internal const AABBTreeLines::LinesDistancer printed_tree(to_lines(printed)); // Orca: Exclude perimeter connections: anchoring and extrusion can trim those differently. - const Polylines anchors = intersection_pl(layer.generate_sparse_infill_polylines_for_anchoring(nullptr, nullptr, nullptr), + const Polylines anchors = intersection_pl(layer.generate_sparse_infill_polylines_for_anchoring(nullptr, nullptr), shrink(to_polygons(layer.lslices), scale_(3.))); REQUIRE_FALSE(anchors.empty()); double max_distance = 0.; @@ -1792,8 +1792,9 @@ TEST_CASE("Sparse plane-path anchors match the printed infill", "[Fill][Internal CHECK(unscale(max_distance) <= config.opt_float("resolution")); } -// Orca: Slices the meshes as the parts of one object, where they are. -static Print &slice_parts(Print &print, DynamicPrintConfig config, const std::vector &parts) +// Orca: Slices the meshes as the parts of one object, where they are, with modifiers of their own config. +static Print &slice_parts(Print &print, DynamicPrintConfig config, const std::vector &parts, + const std::vector> &modifiers = {}) { config.set_deserialize_strict({{"layer_height", 0.2}, {"initial_layer_print_height", 0.2}, @@ -1804,6 +1805,8 @@ static Print &slice_parts(Print &print, DynamicPrintConfig config, const std::ve Slic3r::Test::init_print({parts.front()}, print, model, config, nullptr, false); for (size_t i = 1; i < parts.size(); ++ i) model.objects.front()->add_volume(TriangleMesh(parts[i]), ModelVolumeType::MODEL_PART, false); + for (const auto &[mesh, modifier_config] : modifiers) + model.objects.front()->add_volume(TriangleMesh(mesh), ModelVolumeType::PARAMETER_MODIFIER, false)->config.apply(modifier_config); print.apply(model, config); print.process(); return print; @@ -1988,3 +1991,33 @@ TEST_CASE("Adaptive infill fills each body like the body sliced alone", "[Fill][ CHECK(unmatched.first < 0.02); CHECK(unmatched.second < 0.02); } + +TEST_CASE("Adaptive infill of a modifier leaves the density of the other regions", "[Fill][Regression]") +{ + const std::string pattern = GENERATE("adaptivecubic", "supportcubic"); + CAPTURE(pattern); + auto config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({{"sparse_infill_pattern", pattern}, + {"sparse_infill_density", "15%"}, + {"top_shell_layers", 0}, + {"bottom_shell_layers", 0}}); + TriangleMesh bodies = make_cube(30, 30, 6), second = make_cube(30, 30, 6); + second.translate(40, 0, 0); + bodies.merge(second); + // Orca: A denser modifier over the right half of the second body. + TriangleMesh modifier = make_cube(20, 40, 10); + modifier.translate(55, -5, -2); + DynamicPrintConfig dense = config; + dense.set_deserialize_strict({{"sparse_infill_density", "60%"}}); + Print print, print_sparse, print_dense; + slice_parts(print, config, {bodies}, {{modifier, dense}}); + slice_parts(print_sparse, config, {bodies}); + slice_parts(print_dense, dense, {bodies}); + + // Orca: Bed regions 3 mm inside the walls and the modifier, away from the links along them. + auto rect = [](double x0, double y0, double x1, double y1) { + return Polygon({Point::new_scale(x0, y0), Point::new_scale(x1, y0), Point::new_scale(x1, y1), Point::new_scale(x0, y1)}); + }; + CHECK(unmatched_between_prints(print, print_sparse, erInternalInfill, {rect(3, 3, 27, 27), rect(43, 3, 52, 27)}) < 0.02); + CHECK(unmatched_between_prints(print, print_dense, erInternalInfill, {rect(58, 3, 67, 27)}) < 0.02); +} diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 0ca1c66f93..cf9a69e5d6 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -37,6 +37,7 @@ add_executable(${_TEST_NAME}_tests test_geometry.cpp test_multimaterial_segmentation.cpp test_placeholder_parser.cpp + test_png_read_write.cpp test_polygon.cpp test_mutable_polygon.cpp test_mutable_priority_queue.cpp diff --git a/tests/libslic3r/test_png_read_write.cpp b/tests/libslic3r/test_png_read_write.cpp new file mode 100644 index 0000000000..2aa28e1a3a --- /dev/null +++ b/tests/libslic3r/test_png_read_write.cpp @@ -0,0 +1,83 @@ +#include + +#include +#include +#include +#include + +#include + +#include "libslic3r/PNGReadWrite.hpp" + +using namespace Slic3r; + +// libpng reports a corrupt or truncated file by longjmp()ing out of the decoder, so the decoders have +// to come back with false rather than crash or hand back a half filled image. +namespace { + +// A real PNG, produced by the writer next door, so the bytes are a file libpng accepts. +std::vector encoded_png(size_t w, size_t h) +{ + std::vector pixels(w * h); + for (size_t i = 0; i < pixels.size(); ++ i) + pixels[i] = uint8_t((i * 7) % 256); + + const boost::filesystem::path path = boost::filesystem::temp_directory_path() / + boost::filesystem::unique_path("png_rw_%%%%%%%%.png"); + REQUIRE(png::write_gray_to_file(path.string(), w, h, pixels)); + std::vector bytes; + { + std::ifstream ifs(path.string(), std::ios::binary); + bytes.assign(std::istreambuf_iterator(ifs), std::istreambuf_iterator()); + } + boost::system::error_code ec; + boost::filesystem::remove(path, ec); + REQUIRE(bytes.size() > 64); + return bytes; +} + +png::ReadBuf buf_of(const std::vector &bytes, size_t size) +{ + return png::ReadBuf{ bytes.data(), size }; +} + +} // namespace + +TEST_CASE("A whole PNG decodes", "[PNG]") { + const std::vector bytes = encoded_png(24, 16); + + png::ImageGreyscale grey; + REQUIRE(png::decode_png(buf_of(bytes, bytes.size()), grey)); + CHECK(grey.cols == 24); + CHECK(grey.rows == 16); + CHECK(grey.buf.size() == 24 * 16); +} + +TEST_CASE("A truncated PNG is refused instead of crashing", "[PNG]") { + const std::vector bytes = encoded_png(64, 64); + + // Cut past the signature: inside the header, and inside the pixel data. Not in the trailing + // chunks - decode_png() does not read those, so a file missing only its IEND still decodes, and + // that is the pre-existing contract rather than anything this change touches. + const size_t size = GENERATE_COPY(size_t(16), size_t(40), bytes.size() / 2, bytes.size() * 3 / 4); + REQUIRE(size < bytes.size()); + + png::ImageGreyscale grey; + CHECK_FALSE(png::decode_png(buf_of(bytes, size), grey)); + + png::ImageColorscale colour; + CHECK_FALSE(png::decode_colored_png(buf_of(bytes, size), colour)); +} + +TEST_CASE("A PNG whose body is garbage is refused", "[PNG]") { + std::vector bytes = encoded_png(32, 32); + // Keep the signature, scribble over everything after it. + for (size_t i = 8; i < bytes.size(); ++ i) + bytes[i] = uint8_t(0xA5); + + png::ImageGreyscale grey; + CHECK_FALSE(png::decode_png(buf_of(bytes, bytes.size()), grey)); + + png::ImageColorscale colour; + CHECK_FALSE(png::decode_colored_png(buf_of(bytes, bytes.size()), colour)); +} diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index 97a282825b..2e0fcdb444 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -34,6 +34,7 @@ add_executable(${_TEST_NAME}_tests test_plugin_sort.cpp test_plugin_cloud_metadata.cpp test_plugin_audit.cpp + test_plugin_json_depth.cpp test_shortcuts.cpp test_file_url.cpp test_user_manager.cpp diff --git a/tests/slic3rutils/test_plugin_json_depth.cpp b/tests/slic3rutils/test_plugin_json_depth.cpp new file mode 100644 index 0000000000..ad3c834b93 --- /dev/null +++ b/tests/slic3rutils/test_plugin_json_depth.cpp @@ -0,0 +1,73 @@ +#include +#include + +#include +#include +#include + +#include "plugin_test_utils.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +using namespace Slic3r; + +namespace { +// Brings the embedded interpreter up for one test and tears it down before boost::log does, +// mirroring the ScopedPluginManager idiom in the other plugin tests. +struct ScopedPluginManager +{ + ScopedDataDir python_data_dir{"plugin-json-depth"}; + bool initialized = PluginManager::instance().initialize(); + ~ScopedPluginManager() + { + PluginManager::instance().shutdown(); + PythonInterpreter::instance().shutdown(); + } +}; +} // namespace + +TEST_CASE("py_to_json raises instead of overflowing on pathologically deep input", "[PluginHost][Python]") +{ + ScopedPluginManager manager; + REQUIRE(manager.initialized); + namespace py = pybind11; + py::gil_scoped_acquire gil; + + // [[[ ... 0 ... ]]] nested 300 deep: past the 200 conversion-depth cap, but shallow enough + // that the pre-fix code returns without crashing, so a regression fails cleanly rather than + // taking the process down. Built in C++ so the test does not depend on Python builtins. + py::object deep = py::int_(0); + for (int i = 0; i < 300; ++i) { + py::list wrapper; + wrapper.append(deep); + deep = std::move(wrapper); + } + CHECK_THROWS_AS(py_to_json(deep), std::exception); +} + +TEST_CASE("py_to_json still converts reasonably nested input", "[PluginHost][Python]") +{ + ScopedPluginManager manager; + REQUIRE(manager.initialized); + namespace py = pybind11; + py::gil_scoped_acquire gil; + + py::dict d; + d["a"] = py::int_(1); + py::list inner; + inner.append(py::str("x")); + inner.append(py::int_(2)); + d["b"] = inner; + + const nlohmann::json j = py_to_json(d); + CHECK(j.at("a").get() == 1); + CHECK(j.at("b").at(0).get() == "x"); + CHECK(j.at("b").at(1).get() == 2); +}