diff --git a/deps/NanoSVG/NanoSVG.cmake b/deps/NanoSVG/NanoSVG.cmake index 9623d3226..1b0fb9960 100644 --- a/deps/NanoSVG/NanoSVG.cmake +++ b/deps/NanoSVG/NanoSVG.cmake @@ -1,4 +1,9 @@ +# In PrusaSlicer 2.6.0 we switched from https://github.com/memononen/nanosvg to its fork https://github.com/fltk/nanosvg +# because this last implements the new function nsvgRasterizeXY() which we now use in GLTexture::load_from_svg() +# for rasterizing svg files from their original size to a squared power of two texture on Windows systems using +# AMD Radeon graphics cards + prusaslicer_add_cmake_project(NanoSVG - URL https://github.com/memononen/nanosvg/archive/4c8f0139b62c6e7faa3b67ce1fbe6e63590ed148.zip - URL_HASH SHA256=584e084af1a75bf633f79753ce2f6f6ec8686002ca27f35f1037c25675fecfb6 + URL https://github.com/fltk/nanosvg/archive/abcd277ea45e9098bed752cf9c6875b533c0892f.zip + URL_HASH SHA256=e859938fbaee4b351bd8a8b3d3c7a75b40c36885ce00b73faa1ce0b98aa0ad34 ) \ No newline at end of file diff --git a/resources/profiles/PrusaResearch.idx b/resources/profiles/PrusaResearch.idx index 68dff19c1..bd671ae74 100644 --- a/resources/profiles/PrusaResearch.idx +++ b/resources/profiles/PrusaResearch.idx @@ -1,219 +1,221 @@ -min_slic3r_version = 2.5.0-alpha0 -1.5.5 Added new Prusament Resin material profiles. Enabled g-code thumbnails for MK2.5 family printers. -1.5.4 Added material profiles for Prusament Resin BioBased60. -1.5.3 Added filament profiles for ColorFabb VarioShore TPU, FormFutura PP, NinjaTek NinjaFlex/Cheetah TPU and for multiple Eolas Prints filaments. Updated bridging settings in 50um and 70um profiles. -1.5.2 Added SLA material profiles. -1.5.1 Renamed filament type "NYLON" to "PA". Updated Adura X profile. Updated PETG fan settings for Prusa MINI (removed fan ramp up). -1.5.0 Updated arachne parameters. Added profiles for Jessie filaments. -1.5.0-alpha1 Added filament profile for Prusament PA11 Carbon Fiber. Added profiles for multiple 3D-Fuel filaments. -1.5.0-alpha0 Added parameters for Arachne perimeter generator. Changed default seam position. Updated output filename format. -min_slic3r_version = 2.4.0-rc -1.4.8 Added filament and SLA material profiles. Updated settings. -1.4.7 Added filament profile for Prusament PA11 Carbon Fiber. Added profiles for multiple 3D-Fuel filaments. -1.4.6 Added SLA materials. Updated filament profiles. -1.4.5 Added MMU2/S profiles for 0.25mm nozzle. Updated FW version. Enabled g-code thumbnails for MK3 family printers. Updated end g-code. -1.4.4 Added multiple Fiberlogy filament profiles. Updated Extrudr filament profiles. -1.4.3 Added new filament profiles and SLA materials. -1.4.2 Added SLA material profiles. -1.4.1 Updated firmware version. -1.4.0 Updated for the PrusaSlicer 2.4.0-rc release. Updated SLA material colors. -min_slic3r_version = 2.4.0-beta2 -1.4.0-beta3 Added material profiles for Prusament Resins. -1.4.0-beta2 Added SLA material colors. Updated BASF filament profiles. -min_slic3r_version = 2.4.0-beta0 -1.4.0-beta1 Updated pad wall slope angle for SLA printers. Updated Filatech Filacarbon profile for Prusa MINI. -1.4.0-beta0 Added multiple Filatech and BASF filament profiles. Added material profiles for SL1S. -min_slic3r_version = 2.4.0-alpha0 -1.4.0-alpha8 Added material profiles for Prusament Resin. Detect bridging perimeters enabled by default. -1.4.0-alpha7 Updated brim_separation value. Updated Prusa MINI end g-code. Added Filamentworld filament profiles. -1.4.0-alpha6 Added nozzle priming after M600. Added nozzle diameter checks for 0.8 nozzle printer profiles. Updated FW version. Increased number of top solid infill layers (0.2 layer height). -1.4.0-alpha5 Added multiple add:north and Extrudr filament profiles. Updated support head settings (SL1S). -1.4.0-alpha4 Decreased Area Fill (SL1S). -1.4.0-alpha3 Updated SL1S tilt times. -1.4.0-alpha2 Updated Prusa MINI machine limits. -1.4.0-alpha1 Added new SL1S resin profiles. -1.4.0-alpha0 Bumped up config version. -1.3.0-alpha2 Added SL1S SPEED profiles. -1.3.0-alpha1 Added Prusament PCCF. Increased travel acceleration for Prusa MINI. Updated start g-code for Prusa MINI. Added multiple add:north and Extrudr filament profiles. Updated Z travel speed values. -1.3.0-alpha0 Disabled thick bridges, updated support settings. -min_slic3r_version = 2.3.2-alpha0 -1.3.7 Updated firmware version. -1.3.6 Updated firmware version. -1.3.5 Added material profiles for Prusament Resins. -1.3.4 Added material profiles for new Prusament Resins. Added profiles for multiple BASF filaments. -1.3.3 Added multiple profiles for Filatech filaments. Added material profiles for SL1S SPEED. Updated SLA print settings. -1.3.2 Added material profiles for Prusament Resin. -1.3.1 Added multiple add:north and Extrudr filament profiles. Updated support head settings (SL1S). -1.3.0 Added SL1S SPEED profiles. -min_slic3r_version = 2.3.0-rc1 -1.2.12 Updated firmware version. -1.2.11 Updated firmware version. -1.2.10 Added multiple profiles for Filatech filaments. Updated SLA print settings (pad wall slope angle). -1.2.9 Added material profiles for Prusament Resin. -1.2.8 Added multiple add:north and Extrudr filament profiles. -1.2.7 Updated "Prusament PC Blend Carbon Fiber" profile for Prusa MINI. -1.2.6 Added filament profile for "Prusament PC Blend Carbon Fiber". -1.2.5 Updated firmware version. Added filament profiles. Various improvements. -1.2.4 Updated cost/density values in filament settings. Various changes in print settings. -1.2.3 Updated firmware version. Updated end g-code in MMU2 printer profiles. -1.2.2 Added Prusament PVB filament profile. Added 0.8mm nozzle profiles. -1.2.1 Updated FW version for MK2.5 family printers. -1.2.0 Added full_fan_speed_layer value for PETG. Increased support interface spacing for 0.6mm nozzle profiles. Updated firmware version. -min_slic3r_version = 2.3.0-beta2 -1.2.0-beta1 Updated end g-code. Added full_fan_speed_layer values. -min_slic3r_version = 2.3.0-beta0 -1.2.0-beta0 Adjusted infill anchor limits. Added filament spool weights. -min_slic3r_version = 2.3.0-alpha4 -1.2.0-alpha1 Renamed MK3S and MINI printer profiles. Updated end g-code (MINI). Added new SLA materials and filament profiles. -1.2.0-alpha0 Added filament spool weights -min_slic3r_version = 2.2.0-alpha3 -1.1.16 Updated firmware version. -1.1.15 Updated firmware version. -1.1.14 Updated firmware version. -1.1.13 Updated firmware version. Updated end g-code in MMU2 printer profiles. -1.1.12 Added Prusament PVB filament profile. Added 0.8mm nozzle profiles. -1.1.11 Renamed MK3S and MINI printer profiles. Updated end g-code (MINI). Added new SLA materials and filament profiles. -1.1.10 Updated firmware version. -1.1.9 Updated K values in filament profiles (linear advance). Added new filament profiles and SLA materials. -1.1.8 Updated start/end g-code scripts for MK3 family printer profiles (reduced extruder motor current for some print profiles). Added new filament and SLA material profiles. -1.1.7 Updated end g-code for MMU2 Single printer profiles. Added/updated filament and SLA material profiles. -1.1.6 Updated firmware version for MK2.5/S and MK3/S. -1.1.5 Updated MMU1 specific retraction settings for Prusament PC Blend -1.1.4 Added Prusament PC Blend filament profile. -1.1.3 Added SLA material and filament profile -1.1.2 Added renamed_from fields for PETG filaments to indicate that they were renamed from PET. -1.1.1 Added Verbatim and Fiberlogy PETG filament profiles. Updated auto cooling settings for ABS. -1.1.1-beta Updated for PrusaSlicer 2.2.0-beta -1.1.1-alpha4 Extended list of default filaments to be installed, top/bottom_solid_min_thickness defined, infill_acceleration changed etc -1.1.1-alpha3 Print bed textures are now configurable from the Preset Bundle. Requires PrusaSlicer 2.2.0-alpha3 and newer. -# The following line (max_slic3r_version) forces the users of PrusaSlicer 2.2.0-alpha3 and newer to update the profiles to 1.1.1-alpha3 and newer, -# so they will see the print bed. -max_slic3r_version = 2.2.0-alpha2 -min_slic3r_version = 2.2.0-alpha0 -1.1.1-alpha2 Bumped up config version, so our in house customer will get updated profiles. -1.1.0 Filament aliases, Creality profiles and other goodies for PrusaSlicer 2.2.0-alpha0 -min_slic3r_version = 2.1.1-beta0 -1.0.12 Updated firmware version. -1.0.11 Updated firmware version. -1.0.10 Updated firmware version for MK2.5/S and MK3/S. -1.0.9 Updated firmware version for MK2.5/S and MK3/S. -1.0.8 Various changes in FFF profiles, new filaments/materials added. See changelog. -1.0.7 Updated layer height limits for MINI -1.0.6 Added Prusa MINI profiles -min_slic3r_version = 2.1.0-alpha0 -1.0.5 Added SLA materials -1.0.4 Updated firmware version and 0.25mm nozzle profiles -1.0.3 Added filament profiles -1.0.2 Added SLA materials -1.0.1 Updated MK3 firmware version check to 3.8.0, new soluble support profiles for 0.6mm nozzle diameter MMU2S printers. -1.0.0 Updated end G-code for the MMU2 profiles to lift the extruder at the end of print. Wipe tower bridging distance was made smaller for soluble supports. -1.0.0-beta1 Updated color for the ASA filaments to differ from the other filaments. Single extruder printers now have no extruder color assigned, obects and toolpaths will be colored with the color of the active filament. -1.0.0-beta0 Printer model checks in start G-codes, ASA filament profiles, limits on min / max SL1 exposition times -1.0.0-alpha2 Printer model and nozzle diameter check -1.0.0-alpha1 Added Prusament ASA profile -1.0.0-alpha0 Filament specific retract for PET and similar copolymers, and for FLEX -min_slic3r_version = 1.42.0-alpha6 -0.8.11 Updated firmware version. -0.8.10 Updated firmware version. -0.8.9 Updated firmware version for MK2.5/S and MK3/S. -0.8.8 Updated firmware version for MK2.5/S and MK3/S. -0.8.7 Updated firmware version -0.8.6 Updated firmware version for MK2.5/S and MK3/S -0.8.5 Updated SL1 printer and material settings -0.8.4 Added Prusament ASA profile -0.8.3 FW version and SL1 materials update -0.8.2 FFF and SL1 settings update -0.8.1 Output settings and SLA materials update -0.8.0 Updated for the PrusaSlicer 2.0.0 final release -0.8.0-rc2 Updated firmware versions for MK2.5/S and MK3/S -0.8.0-rc1 Updated SLA profiles -0.8.0-rc Updated for the PrusaSlicer 2.0.0-rc release -0.8.0-beta4 Updated SLA profiles -0.8.0-beta3 Updated SLA profiles -0.8.0-beta2 Updated SLA profiles -0.8.0-beta1 Updated SLA profiles -0.8.0-beta Updated SLA profiles -0.8.0-alpha9 Updated SLA and FFF profiles -0.8.0-alpha8 Updated SLA profiles -0.8.0-alpha7 Updated SLA profiles -0.8.0-alpha6 Updated SLA profiles -min_slic3r_version = 1.42.0-alpha -0.8.0-alpha Updated SLA profiles -0.4.0-alpha4 Updated SLA profiles -0.4.0-alpha3 Update of SLA profiles -0.4.0-alpha2 First SLA profiles -min_slic3r_version = 1.41.3-alpha -0.4.12 Updated firmware version for MK2.5/S and MK3/S. -0.4.11 Updated firmware version for MK2.5/S and MK3/S. -0.4.10 Updated firmware version -0.4.9 Updated firmware version for MK2.5/S and MK3/S -0.4.8 MK2.5/3/S FW update -0.4.7 MK2/S/MMU FW update -0.4.6 Updated firmware versions for MK2.5/S and MK3/S -0.4.5 Enabled remaining time support for MK2/S/MMU1 -0.4.4 Changelog: https://github.com/prusa3d/Slic3r-settings/blob/master/live/PrusaResearch/changelog.txt -0.4.3 Changelog: https://github.com/prusa3d/Slic3r-settings/blob/master/live/PrusaResearch/changelog.txt -0.4.2 Changelog: https://github.com/prusa3d/Slic3r-settings/blob/master/live/PrusaResearch/changelog.txt -0.4.1 New MK2.5S and MK3S FW versions -0.4.0 Changelog: https://github.com/prusa3d/Slic3r-settings/blob/master/live/PrusaResearch/changelog.txt -min_slic3r_version = 1.41.1 -0.3.11 Updated firmware version for MK2.5/S and MK3/S. -0.3.10 Updated firmware version -0.3.9 Updated firmware version for MK2.5/S and MK3/S -0.3.8 MK2.5/3/S FW update -0.3.7 MK2/S/MMU FW update -0.3.6 Updated firmware versions for MK2.5 and MK3 -0.3.5 New MK2.5 and MK3 FW versions -0.3.4 Changelog: https://github.com/prusa3d/Slic3r-settings/blob/master/live/PrusaResearch/changelog.txt -0.3.3 Prusament PETG released -0.3.2 New MK2.5 and MK3 FW versions -0.3.1 New MK2.5 and MK3 FW versions -0.3.0 New MK2.5 and MK3 FW version -min_slic3r_version = 1.41.0-alpha -0.2.9 New MK2.5 and MK3 FW versions -0.2.8 New MK2.5 and MK3 FW version -min_slic3r_version = 1.41.1 -0.2.7 New MK2.5 and MK3 FW version -0.2.6 Added MMU2 MK2.5 settings -min_slic3r_version = 1.41.0-alpha -0.2.5 Prusament is out - added prusament settings -0.2.4 Added soluble support profiles for MMU2 -0.2.3 Added materials for MMU2 single mode, edited MK3 xy stealth feedrate limit -0.2.2 Edited MMU2 Single mode purge line -0.2.1 Added PET and BVOH settings for MMU2 -0.2.0-beta5 Fixed MMU1 ramming parameters -0.2.0-beta4 Added filament loading speed at start, increased minimal purge on wipe tower -0.2.0-beta3 Edited ramming parameters and filament cooling moves for MMU2 -0.2.0-beta2 Edited first layer speed and wipe tower position -0.2.0-beta Removed limit on the MK3MMU2 height, added legacy M204 S T format to the MK2 profiles -0.2.0-alpha8 Added filament_load/unload_time for the PLA/ABS MMU2 filament presets. -0.2.0-alpha7 Vojtech's fix the incorrect *MK3* references -0.2.0-alpha6 Jindra's way to fix the 0.2.0-alpha5 version -0.2.0-alpha5 Bumped up firmware versions for MK2.5/MK3 to 3.3.1, disabled priming areas for MK3MMU2 -0.2.0-alpha4 Extended the custom start/end G-codes of the MMU2.0 printers for no priming towers. -0.2.0-alpha3 Adjusted machine limits for time estimates, added filament density and cost -0.2.0-alpha2 Renamed the key MK3SMMU to MK3MMU2, added a generic PLA MMU2 material -0.2.0-alpha1 added initial profiles for the i3 MK3 Multi Material Upgrade 2.0 -0.2.0-alpha moved machine limits from the start G-code to the new print profile parameters -min_slic3r_version = 1.40.0 -0.1.18 Updated firmware version -0.1.17 Updated firmware version for MK2.5/S and MK3/S -0.1.16 MK2.5/3/S FW update -0.1.15 MK2/S/MMU FW update -0.1.14 Updated firmware versions for MK2.5 and MK3 -0.1.13 New MK2.5 and MK3 FW versions -0.1.12 New MK2.5 and MK3 FW versions -0.1.11 fw version changed to 3.3.1 -0.1.10 MK3 jerk and acceleration update -0.1.9 edited support extrusion width for 0.25 and 0.6 nozzles -0.1.8 extrusion width for 0,25, 0.6 and variable layer height fixes -0.1.7 Fixed errors in 0.25mm and 0.6mm profiles -0.1.6 Split the MK2.5 profile from the MK2S -min_slic3r_version = 1.40.0-beta -0.1.5 fixed printer_variant fields for the i3 MK3 0.25 and 0.6mm nozzles -0.1.4 edited fw version, added z-raise after print -min_slic3r_version = 1.40.0-alpha -0.1.3 Fixed an incorrect position of the max_print_height parameter -0.1.2 Wipe tower changes -0.1.1 Minor print speed adjustments -0.1.0 Initial +min_slic3r_version = 2.6.0-alpha1 +1.6.0-alpha0 Default top fill set to monotonic lines. Updated infill/perimeter overlap values. Updated output filename format. Enabled dynamic overhang speeds. +min_slic3r_version = 2.5.0-alpha0 +1.5.5 Added new Prusament Resin material profiles. Enabled g-code thumbnails for MK2.5 family printers. +1.5.4 Added material profiles for Prusament Resin BioBased60. +1.5.3 Added filament profiles for ColorFabb VarioShore TPU, FormFutura PP, NinjaTek NinjaFlex/Cheetah TPU and for multiple Eolas Prints filaments. Updated bridging settings in 50um and 70um profiles. +1.5.2 Added SLA material profiles. +1.5.1 Renamed filament type "NYLON" to "PA". Updated Adura X profile. Updated PETG fan settings for Prusa MINI (removed fan ramp up). +1.5.0 Updated arachne parameters. Added profiles for Jessie filaments. +1.5.0-alpha1 Added filament profile for Prusament PA11 Carbon Fiber. Added profiles for multiple 3D-Fuel filaments. +1.5.0-alpha0 Added parameters for Arachne perimeter generator. Changed default seam position. Updated output filename format. +min_slic3r_version = 2.4.0-rc +1.4.8 Added filament and SLA material profiles. Updated settings. +1.4.7 Added filament profile for Prusament PA11 Carbon Fiber. Added profiles for multiple 3D-Fuel filaments. +1.4.6 Added SLA materials. Updated filament profiles. +1.4.5 Added MMU2/S profiles for 0.25mm nozzle. Updated FW version. Enabled g-code thumbnails for MK3 family printers. Updated end g-code. +1.4.4 Added multiple Fiberlogy filament profiles. Updated Extrudr filament profiles. +1.4.3 Added new filament profiles and SLA materials. +1.4.2 Added SLA material profiles. +1.4.1 Updated firmware version. +1.4.0 Updated for the PrusaSlicer 2.4.0-rc release. Updated SLA material colors. +min_slic3r_version = 2.4.0-beta2 +1.4.0-beta3 Added material profiles for Prusament Resins. +1.4.0-beta2 Added SLA material colors. Updated BASF filament profiles. +min_slic3r_version = 2.4.0-beta0 +1.4.0-beta1 Updated pad wall slope angle for SLA printers. Updated Filatech Filacarbon profile for Prusa MINI. +1.4.0-beta0 Added multiple Filatech and BASF filament profiles. Added material profiles for SL1S. +min_slic3r_version = 2.4.0-alpha0 +1.4.0-alpha8 Added material profiles for Prusament Resin. Detect bridging perimeters enabled by default. +1.4.0-alpha7 Updated brim_separation value. Updated Prusa MINI end g-code. Added Filamentworld filament profiles. +1.4.0-alpha6 Added nozzle priming after M600. Added nozzle diameter checks for 0.8 nozzle printer profiles. Updated FW version. Increased number of top solid infill layers (0.2 layer height). +1.4.0-alpha5 Added multiple add:north and Extrudr filament profiles. Updated support head settings (SL1S). +1.4.0-alpha4 Decreased Area Fill (SL1S). +1.4.0-alpha3 Updated SL1S tilt times. +1.4.0-alpha2 Updated Prusa MINI machine limits. +1.4.0-alpha1 Added new SL1S resin profiles. +1.4.0-alpha0 Bumped up config version. +1.3.0-alpha2 Added SL1S SPEED profiles. +1.3.0-alpha1 Added Prusament PCCF. Increased travel acceleration for Prusa MINI. Updated start g-code for Prusa MINI. Added multiple add:north and Extrudr filament profiles. Updated Z travel speed values. +1.3.0-alpha0 Disabled thick bridges, updated support settings. +min_slic3r_version = 2.3.2-alpha0 +1.3.7 Updated firmware version. +1.3.6 Updated firmware version. +1.3.5 Added material profiles for Prusament Resins. +1.3.4 Added material profiles for new Prusament Resins. Added profiles for multiple BASF filaments. +1.3.3 Added multiple profiles for Filatech filaments. Added material profiles for SL1S SPEED. Updated SLA print settings. +1.3.2 Added material profiles for Prusament Resin. +1.3.1 Added multiple add:north and Extrudr filament profiles. Updated support head settings (SL1S). +1.3.0 Added SL1S SPEED profiles. +min_slic3r_version = 2.3.0-rc1 +1.2.12 Updated firmware version. +1.2.11 Updated firmware version. +1.2.10 Added multiple profiles for Filatech filaments. Updated SLA print settings (pad wall slope angle). +1.2.9 Added material profiles for Prusament Resin. +1.2.8 Added multiple add:north and Extrudr filament profiles. +1.2.7 Updated "Prusament PC Blend Carbon Fiber" profile for Prusa MINI. +1.2.6 Added filament profile for "Prusament PC Blend Carbon Fiber". +1.2.5 Updated firmware version. Added filament profiles. Various improvements. +1.2.4 Updated cost/density values in filament settings. Various changes in print settings. +1.2.3 Updated firmware version. Updated end g-code in MMU2 printer profiles. +1.2.2 Added Prusament PVB filament profile. Added 0.8mm nozzle profiles. +1.2.1 Updated FW version for MK2.5 family printers. +1.2.0 Added full_fan_speed_layer value for PETG. Increased support interface spacing for 0.6mm nozzle profiles. Updated firmware version. +min_slic3r_version = 2.3.0-beta2 +1.2.0-beta1 Updated end g-code. Added full_fan_speed_layer values. +min_slic3r_version = 2.3.0-beta0 +1.2.0-beta0 Adjusted infill anchor limits. Added filament spool weights. +min_slic3r_version = 2.3.0-alpha4 +1.2.0-alpha1 Renamed MK3S and MINI printer profiles. Updated end g-code (MINI). Added new SLA materials and filament profiles. +1.2.0-alpha0 Added filament spool weights +min_slic3r_version = 2.2.0-alpha3 +1.1.16 Updated firmware version. +1.1.15 Updated firmware version. +1.1.14 Updated firmware version. +1.1.13 Updated firmware version. Updated end g-code in MMU2 printer profiles. +1.1.12 Added Prusament PVB filament profile. Added 0.8mm nozzle profiles. +1.1.11 Renamed MK3S and MINI printer profiles. Updated end g-code (MINI). Added new SLA materials and filament profiles. +1.1.10 Updated firmware version. +1.1.9 Updated K values in filament profiles (linear advance). Added new filament profiles and SLA materials. +1.1.8 Updated start/end g-code scripts for MK3 family printer profiles (reduced extruder motor current for some print profiles). Added new filament and SLA material profiles. +1.1.7 Updated end g-code for MMU2 Single printer profiles. Added/updated filament and SLA material profiles. +1.1.6 Updated firmware version for MK2.5/S and MK3/S. +1.1.5 Updated MMU1 specific retraction settings for Prusament PC Blend +1.1.4 Added Prusament PC Blend filament profile. +1.1.3 Added SLA material and filament profile +1.1.2 Added renamed_from fields for PETG filaments to indicate that they were renamed from PET. +1.1.1 Added Verbatim and Fiberlogy PETG filament profiles. Updated auto cooling settings for ABS. +1.1.1-beta Updated for PrusaSlicer 2.2.0-beta +1.1.1-alpha4 Extended list of default filaments to be installed, top/bottom_solid_min_thickness defined, infill_acceleration changed etc +1.1.1-alpha3 Print bed textures are now configurable from the Preset Bundle. Requires PrusaSlicer 2.2.0-alpha3 and newer. +# The following line (max_slic3r_version) forces the users of PrusaSlicer 2.2.0-alpha3 and newer to update the profiles to 1.1.1-alpha3 and newer, +# so they will see the print bed. +max_slic3r_version = 2.2.0-alpha2 +min_slic3r_version = 2.2.0-alpha0 +1.1.1-alpha2 Bumped up config version, so our in house customer will get updated profiles. +1.1.0 Filament aliases, Creality profiles and other goodies for PrusaSlicer 2.2.0-alpha0 +min_slic3r_version = 2.1.1-beta0 +1.0.12 Updated firmware version. +1.0.11 Updated firmware version. +1.0.10 Updated firmware version for MK2.5/S and MK3/S. +1.0.9 Updated firmware version for MK2.5/S and MK3/S. +1.0.8 Various changes in FFF profiles, new filaments/materials added. See changelog. +1.0.7 Updated layer height limits for MINI +1.0.6 Added Prusa MINI profiles +min_slic3r_version = 2.1.0-alpha0 +1.0.5 Added SLA materials +1.0.4 Updated firmware version and 0.25mm nozzle profiles +1.0.3 Added filament profiles +1.0.2 Added SLA materials +1.0.1 Updated MK3 firmware version check to 3.8.0, new soluble support profiles for 0.6mm nozzle diameter MMU2S printers. +1.0.0 Updated end G-code for the MMU2 profiles to lift the extruder at the end of print. Wipe tower bridging distance was made smaller for soluble supports. +1.0.0-beta1 Updated color for the ASA filaments to differ from the other filaments. Single extruder printers now have no extruder color assigned, obects and toolpaths will be colored with the color of the active filament. +1.0.0-beta0 Printer model checks in start G-codes, ASA filament profiles, limits on min / max SL1 exposition times +1.0.0-alpha2 Printer model and nozzle diameter check +1.0.0-alpha1 Added Prusament ASA profile +1.0.0-alpha0 Filament specific retract for PET and similar copolymers, and for FLEX +min_slic3r_version = 1.42.0-alpha6 +0.8.11 Updated firmware version. +0.8.10 Updated firmware version. +0.8.9 Updated firmware version for MK2.5/S and MK3/S. +0.8.8 Updated firmware version for MK2.5/S and MK3/S. +0.8.7 Updated firmware version +0.8.6 Updated firmware version for MK2.5/S and MK3/S +0.8.5 Updated SL1 printer and material settings +0.8.4 Added Prusament ASA profile +0.8.3 FW version and SL1 materials update +0.8.2 FFF and SL1 settings update +0.8.1 Output settings and SLA materials update +0.8.0 Updated for the PrusaSlicer 2.0.0 final release +0.8.0-rc2 Updated firmware versions for MK2.5/S and MK3/S +0.8.0-rc1 Updated SLA profiles +0.8.0-rc Updated for the PrusaSlicer 2.0.0-rc release +0.8.0-beta4 Updated SLA profiles +0.8.0-beta3 Updated SLA profiles +0.8.0-beta2 Updated SLA profiles +0.8.0-beta1 Updated SLA profiles +0.8.0-beta Updated SLA profiles +0.8.0-alpha9 Updated SLA and FFF profiles +0.8.0-alpha8 Updated SLA profiles +0.8.0-alpha7 Updated SLA profiles +0.8.0-alpha6 Updated SLA profiles +min_slic3r_version = 1.42.0-alpha +0.8.0-alpha Updated SLA profiles +0.4.0-alpha4 Updated SLA profiles +0.4.0-alpha3 Update of SLA profiles +0.4.0-alpha2 First SLA profiles +min_slic3r_version = 1.41.3-alpha +0.4.12 Updated firmware version for MK2.5/S and MK3/S. +0.4.11 Updated firmware version for MK2.5/S and MK3/S. +0.4.10 Updated firmware version +0.4.9 Updated firmware version for MK2.5/S and MK3/S +0.4.8 MK2.5/3/S FW update +0.4.7 MK2/S/MMU FW update +0.4.6 Updated firmware versions for MK2.5/S and MK3/S +0.4.5 Enabled remaining time support for MK2/S/MMU1 +0.4.4 Changelog: https://github.com/prusa3d/Slic3r-settings/blob/master/live/PrusaResearch/changelog.txt +0.4.3 Changelog: https://github.com/prusa3d/Slic3r-settings/blob/master/live/PrusaResearch/changelog.txt +0.4.2 Changelog: https://github.com/prusa3d/Slic3r-settings/blob/master/live/PrusaResearch/changelog.txt +0.4.1 New MK2.5S and MK3S FW versions +0.4.0 Changelog: https://github.com/prusa3d/Slic3r-settings/blob/master/live/PrusaResearch/changelog.txt +min_slic3r_version = 1.41.1 +0.3.11 Updated firmware version for MK2.5/S and MK3/S. +0.3.10 Updated firmware version +0.3.9 Updated firmware version for MK2.5/S and MK3/S +0.3.8 MK2.5/3/S FW update +0.3.7 MK2/S/MMU FW update +0.3.6 Updated firmware versions for MK2.5 and MK3 +0.3.5 New MK2.5 and MK3 FW versions +0.3.4 Changelog: https://github.com/prusa3d/Slic3r-settings/blob/master/live/PrusaResearch/changelog.txt +0.3.3 Prusament PETG released +0.3.2 New MK2.5 and MK3 FW versions +0.3.1 New MK2.5 and MK3 FW versions +0.3.0 New MK2.5 and MK3 FW version +min_slic3r_version = 1.41.0-alpha +0.2.9 New MK2.5 and MK3 FW versions +0.2.8 New MK2.5 and MK3 FW version +min_slic3r_version = 1.41.1 +0.2.7 New MK2.5 and MK3 FW version +0.2.6 Added MMU2 MK2.5 settings +min_slic3r_version = 1.41.0-alpha +0.2.5 Prusament is out - added prusament settings +0.2.4 Added soluble support profiles for MMU2 +0.2.3 Added materials for MMU2 single mode, edited MK3 xy stealth feedrate limit +0.2.2 Edited MMU2 Single mode purge line +0.2.1 Added PET and BVOH settings for MMU2 +0.2.0-beta5 Fixed MMU1 ramming parameters +0.2.0-beta4 Added filament loading speed at start, increased minimal purge on wipe tower +0.2.0-beta3 Edited ramming parameters and filament cooling moves for MMU2 +0.2.0-beta2 Edited first layer speed and wipe tower position +0.2.0-beta Removed limit on the MK3MMU2 height, added legacy M204 S T format to the MK2 profiles +0.2.0-alpha8 Added filament_load/unload_time for the PLA/ABS MMU2 filament presets. +0.2.0-alpha7 Vojtech's fix the incorrect *MK3* references +0.2.0-alpha6 Jindra's way to fix the 0.2.0-alpha5 version +0.2.0-alpha5 Bumped up firmware versions for MK2.5/MK3 to 3.3.1, disabled priming areas for MK3MMU2 +0.2.0-alpha4 Extended the custom start/end G-codes of the MMU2.0 printers for no priming towers. +0.2.0-alpha3 Adjusted machine limits for time estimates, added filament density and cost +0.2.0-alpha2 Renamed the key MK3SMMU to MK3MMU2, added a generic PLA MMU2 material +0.2.0-alpha1 added initial profiles for the i3 MK3 Multi Material Upgrade 2.0 +0.2.0-alpha moved machine limits from the start G-code to the new print profile parameters +min_slic3r_version = 1.40.0 +0.1.18 Updated firmware version +0.1.17 Updated firmware version for MK2.5/S and MK3/S +0.1.16 MK2.5/3/S FW update +0.1.15 MK2/S/MMU FW update +0.1.14 Updated firmware versions for MK2.5 and MK3 +0.1.13 New MK2.5 and MK3 FW versions +0.1.12 New MK2.5 and MK3 FW versions +0.1.11 fw version changed to 3.3.1 +0.1.10 MK3 jerk and acceleration update +0.1.9 edited support extrusion width for 0.25 and 0.6 nozzles +0.1.8 extrusion width for 0,25, 0.6 and variable layer height fixes +0.1.7 Fixed errors in 0.25mm and 0.6mm profiles +0.1.6 Split the MK2.5 profile from the MK2S +min_slic3r_version = 1.40.0-beta +0.1.5 fixed printer_variant fields for the i3 MK3 0.25 and 0.6mm nozzles +0.1.4 edited fw version, added z-raise after print +min_slic3r_version = 1.40.0-alpha +0.1.3 Fixed an incorrect position of the max_print_height parameter +0.1.2 Wipe tower changes +0.1.1 Minor print speed adjustments +0.1.0 Initial diff --git a/resources/profiles/PrusaResearch.ini b/resources/profiles/PrusaResearch.ini index 5922b216a..90486e438 100644 --- a/resources/profiles/PrusaResearch.ini +++ b/resources/profiles/PrusaResearch.ini @@ -5,7 +5,7 @@ name = Prusa Research # Configuration version of this file. Config file will only be installed, if the config_version differs. # This means, the server may force the PrusaSlicer configuration to be downgraded. -config_version = 1.5.5 +config_version = 1.6.0-alpha0 # Where to get the updates from? config_update_url = https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/PrusaResearch/ changelog_url = https://files.prusa3d.com/?latest=slicer-profiles&lng=%1% @@ -173,7 +173,7 @@ infill_extruder = 1 infill_extrusion_width = 0.45 infill_first = 0 infill_only_where_needed = 0 -infill_overlap = 25% +infill_overlap = 10% interface_shells = 0 max_print_speed = 100 max_volumetric_extrusion_rate_slope_negative = 0 @@ -184,7 +184,7 @@ notes = overhangs = 1 only_retract_when_crossing_perimeters = 0 ooze_prevention = 0 -output_filename_format = {input_filename_base}_{layer_height}mm_{initial_filament_type}_{printer_model}_{print_time}.gcode +output_filename_format = {input_filename_base}_{layer_height}mm_{printing_filament_types}_{printer_model}_{print_time}.gcode perimeters = 2 perimeter_extruder = 1 perimeter_extrusion_width = 0.45 @@ -248,6 +248,8 @@ wall_transition_filter_deviation = 25% wall_transition_length = 0.4 wall_distribution_count = 1 min_bead_width = 85% +enable_dynamic_overhang_speeds = 1 +top_fill_pattern = monotoniclines [print:*MK3*] fill_pattern = grid @@ -289,7 +291,7 @@ support_material_interface_spacing = 0.15 support_material_spacing = 1 support_material_xy_spacing = 150% support_material_contact_distance = 0.1 -output_filename_format = {input_filename_base}_{nozzle_diameter[0]}n_{layer_height}mm_{initial_filament_type}_{printer_model}_{print_time}.gcode +output_filename_format = {input_filename_base}_{nozzle_diameter[0]}n_{layer_height}mm_{printing_filament_types}_{printer_model}_{print_time}.gcode thick_bridges = 0 bridge_flow_ratio = 1 bridge_speed = 20 @@ -299,6 +301,8 @@ wall_transition_filter_deviation = 25% wall_transition_length = 0.25 wall_distribution_count = 1 min_bead_width = 85% +infill_overlap = 10% +dynamic_overhang_speeds[0] = 20,20,15,15 [print:*0.25nozzleMK3*] inherits = *0.25nozzle* @@ -340,7 +344,7 @@ support_material_extrusion_width = 0.55 support_material_contact_distance = 0.15 support_material_xy_spacing = 80% support_material_interface_spacing = 0.3 -output_filename_format = {input_filename_base}_{nozzle_diameter[0]}n_{layer_height}mm_{initial_filament_type}_{printer_model}_{print_time}.gcode +output_filename_format = {input_filename_base}_{nozzle_diameter[0]}n_{layer_height}mm_{printing_filament_types}_{printer_model}_{print_time}.gcode infill_anchor_max = 15 top_solid_min_thickness = 0.9 bottom_solid_min_thickness = 0.6 @@ -352,6 +356,7 @@ wall_transition_filter_deviation = 25% wall_transition_length = 0.6 wall_distribution_count = 1 min_bead_width = 85% +infill_overlap = 15% [print:*0.6nozzleMK3*] inherits = *0.6nozzle* @@ -390,7 +395,7 @@ support_material_interface_speed = 100% support_material_spacing = 2 support_material_xy_spacing = 80% support_material_threshold = 50 -output_filename_format = {input_filename_base}_{nozzle_diameter[0]}n_{layer_height}mm_{initial_filament_type}_{printer_model}_{print_time}.gcode +output_filename_format = {input_filename_base}_{nozzle_diameter[0]}n_{layer_height}mm_{printing_filament_types}_{printer_model}_{print_time}.gcode fill_pattern = gyroid fill_density = 15% infill_anchor_max = 20 @@ -399,7 +404,7 @@ bottom_solid_layers = 3 skirt_distance = 3 skirt_height = 2 first_layer_height = 0.3 -infill_overlap = 30% +infill_overlap = 15% bridge_speed = 22 gap_fill_speed = 30 bridge_flow_ratio = 0.9 @@ -4494,6 +4499,8 @@ filament_soluble = 1 filament_type = PVB filament_colour = #FFFF6F filament_spool_weight = 201 +bed_temperature = 75 +first_layer_bed_temperature = 75 slowdown_below_layer_time = 20 filament_ramming_parameters = "120 110 1.74194 1.90323 2.16129 2.48387 2.83871 3.25806 3.83871 4.6129 5.41935 5.96774| 0.05 1.69677 0.45 1.96128 0.95 2.63872 1.45 3.46129 1.95 4.99031 2.45 6.12908 2.95 8.30974 3.45 11.4065 3.95 7.6 4.45 7.6 4.95 7.6" start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.6}0.12{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.8}0.06{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/}0.2{elsif nozzle_diameter[0]==0.8}0.02{elsif nozzle_diameter[0]==0.6}0.05{else}0.08{endif} ; Filament gcode LA 1.5\n{if printer_notes=~/.*PRINTER_MODEL_MINI.*/};{elsif printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}M900 K200{elsif nozzle_diameter[0]==0.6}M900 K24{elsif nozzle_diameter[0]==0.8};{else}M900 K45{endif} ; Filament gcode LA 1.0" diff --git a/resources/profiles/Templates.idx b/resources/profiles/Templates.idx new file mode 100644 index 000000000..3edb4400f --- /dev/null +++ b/resources/profiles/Templates.idx @@ -0,0 +1,2 @@ +min_slic3r_version = 2.6.0-alpha0 +1.0.0 Initial diff --git a/resources/profiles/Templates.ini b/resources/profiles/Templates.ini new file mode 100644 index 000000000..26b0c0f12 --- /dev/null +++ b/resources/profiles/Templates.ini @@ -0,0 +1,2144 @@ +# Generic filament profile templates + +[vendor] +name = Templates +config_version = 1.0.0 +config_update_url = https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/Templates/ +templates_profile = 1 + +## Generic filament profiles + +# Print profiles for the Prusa Research printers. + +[filament:*common*] +cooling = 1 +compatible_printers = +compatible_printers_condition = +end_filament_gcode = "; Filament-specific end gcode" +extrusion_multiplier = 1 +filament_loading_speed = 14 +filament_loading_speed_start = 19 +filament_unloading_speed = 90 +filament_unloading_speed_start = 100 +filament_toolchange_delay = 0 +filament_cooling_moves = 1 +filament_cooling_initial_speed = 3 +filament_cooling_final_speed = 2 +filament_load_time = 0 +filament_unload_time = 0 +filament_ramming_parameters = "130 120 2.70968 2.93548 3.32258 3.83871 4.58065 5.54839 6.51613 7.35484 7.93548 8.16129| 0.05 2.66451 0.45 3.05805 0.95 4.05807 1.45 5.97742 1.95 7.69999 2.45 8.1936 2.95 11.342 3.45 11.4065 3.95 7.6 4.45 7.6 4.95 7.6" +filament_minimal_purge_on_wipe_tower = 0 +filament_cost = 0 +filament_density = 0 +filament_diameter = 1.75 +filament_notes = "" +filament_settings_id = "" +filament_soluble = 0 +min_print_speed = 10 +slowdown_below_layer_time = 10 +start_filament_gcode = + +[filament:*PLA*] +inherits = *common* +bed_temperature = 60 +bridge_fan_speed = 100 +disable_fan_first_layers = 1 +full_fan_speed_layer = 3 +fan_always_on = 1 +fan_below_layer_time = 100 +filament_colour = #FF8000 +filament_max_volumetric_speed = 0 +filament_type = PLA +first_layer_bed_temperature = 60 +first_layer_temperature = 215 +max_fan_speed = 100 +min_fan_speed = 100 +temperature = 210 + +[filament:*PET*] +inherits = *common* +bed_temperature = 90 +bridge_fan_speed = 50 +disable_fan_first_layers = 3 +full_fan_speed_layer = 5 +fan_always_on = 1 +fan_below_layer_time = 20 +filament_colour = #FF8000 +filament_max_volumetric_speed = 0 +filament_type = PETG +first_layer_bed_temperature = 85 +first_layer_temperature = 230 +max_fan_speed = 50 +min_fan_speed = 30 +temperature = 240 + +[filament:*ABS*] +inherits = *common* +bed_temperature = 100 +bridge_fan_speed = 25 +cooling = 0 +disable_fan_first_layers = 3 +fan_always_on = 0 +fan_below_layer_time = 20 +filament_colour = #FFF2EC +filament_max_volumetric_speed = 0 +filament_ramming_parameters = "120 100 5.70968 6.03226 7 8.25806 9 9.19355 9.3871 9.77419 10.129 10.3226 10.4516 10.5161| 0.05 5.69677 0.45 6.15484 0.95 8.76774 1.45 9.20323 1.95 9.95806 2.45 10.3871 2.95 10.5677 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6" +filament_type = ABS +first_layer_bed_temperature = 100 +first_layer_temperature = 255 +max_fan_speed = 30 +min_fan_speed = 20 +temperature = 255 + +[filament:*ABSC*] +inherits = *common* +bed_temperature = 100 +bridge_fan_speed = 25 +cooling = 1 +disable_fan_first_layers = 4 +fan_always_on = 0 +fan_below_layer_time = 30 +slowdown_below_layer_time = 20 +filament_colour = #FFF2EC +filament_max_volumetric_speed = 0 +filament_ramming_parameters = "120 100 5.70968 6.03226 7 8.25806 9 9.19355 9.3871 9.77419 10.129 10.3226 10.4516 10.5161| 0.05 5.69677 0.45 6.15484 0.95 8.76774 1.45 9.20323 1.95 9.95806 2.45 10.3871 2.95 10.5677 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6" +filament_type = ABS +first_layer_bed_temperature = 100 +first_layer_temperature = 255 +max_fan_speed = 15 +min_fan_speed = 15 +min_print_speed = 15 +temperature = 255 + +[filament:*FLEX*] +inherits = *common* +bed_temperature = 50 +bridge_fan_speed = 80 +cooling = 0 +disable_fan_first_layers = 3 +extrusion_multiplier = 1.15 +fan_always_on = 0 +fan_below_layer_time = 100 +filament_colour = #008000 +filament_max_volumetric_speed = 1.8 +filament_type = FLEX +first_layer_bed_temperature = 50 +first_layer_temperature = 240 +max_fan_speed = 90 +min_fan_speed = 70 +temperature = 240 + +[filament:ColorFabb bronzeFill] +inherits = *PLA* +filament_vendor = ColorFabb +extrusion_multiplier = 1.12 +filament_cost = 80.65 +filament_density = 3.9 +filament_spool_weight = 236 +filament_colour = #804040 + +[filament:ColorFabb steelFill] +inherits = *PLA* +filament_vendor = ColorFabb +extrusion_multiplier = 1.1 +filament_cost = 80.65 +filament_density = 3.13 +filament_spool_weight = 236 +filament_colour = #808080 + +[filament:ColorFabb copperFill] +inherits = *PLA* +filament_vendor = ColorFabb +extrusion_multiplier = 1.1 +filament_cost = 80.65 +filament_density = 3.9 +filament_spool_weight = 236 +filament_colour = #82603E + +[filament:ColorFabb HT] +inherits = *PET* +filament_vendor = ColorFabb +bed_temperature = 100 +bridge_fan_speed = 30 +cooling = 1 +disable_fan_first_layers = 3 +fan_always_on = 0 +fan_below_layer_time = 10 +filament_cost = 65.66 +filament_density = 1.18 +filament_spool_weight = 236 +first_layer_bed_temperature = 100 +first_layer_temperature = 270 +max_fan_speed = 20 +min_fan_speed = 10 +temperature = 270 + +[filament:ColorFabb PLA-PHA] +inherits = *PLA* +filament_vendor = ColorFabb +filament_cost = 54.84 +filament_density = 1.24 +filament_spool_weight = 236 + +[filament:ColorFabb woodFill] +inherits = *PLA* +filament_vendor = ColorFabb +extrusion_multiplier = 1.1 +filament_cost = 78.63 +filament_density = 1.15 +filament_spool_weight = 236 +filament_colour = #dfc287 +first_layer_temperature = 200 +temperature = 200 + +[filament:ColorFabb corkFill] +inherits = *PLA* +filament_vendor = ColorFabb +extrusion_multiplier = 1.1 +filament_cost = 78.63 +filament_density = 1.18 +filament_spool_weight = 236 +filament_colour = #634d33 +filament_max_volumetric_speed = 6 +first_layer_temperature = 220 +temperature = 220 + +[filament:ColorFabb XT] +inherits = *PET* +filament_vendor = ColorFabb +filament_cost = 62.90 +filament_density = 1.27 +filament_spool_weight = 236 +first_layer_bed_temperature = 90 +first_layer_temperature = 260 +temperature = 270 + +[filament:ColorFabb XT-CF20] +inherits = *PET* +filament_vendor = ColorFabb +extrusion_multiplier = 1.05 +filament_cost = 80.65 +filament_density = 1.35 +filament_spool_weight = 236 +filament_colour = #804040 +filament_max_volumetric_speed = 2 +first_layer_bed_temperature = 90 +first_layer_temperature = 260 +temperature = 260 + +[filament:ColorFabb nGen] +inherits = *PET* +filament_vendor = ColorFabb +filament_cost = 52.46 +filament_density = 1.2 +filament_spool_weight = 236 +bridge_fan_speed = 40 +fan_always_on = 0 +fan_below_layer_time = 10 +filament_type = NGEN +first_layer_temperature = 240 +max_fan_speed = 35 +min_fan_speed = 20 + +[filament:ColorFabb nGen flex] +inherits = *FLEX* +filament_vendor = ColorFabb +filament_cost = 58.30 +filament_density = 1 +filament_spool_weight = 236 +bed_temperature = 85 +bridge_fan_speed = 40 +cooling = 1 +disable_fan_first_layers = 3 +extrusion_multiplier = 1 +fan_below_layer_time = 10 +filament_max_volumetric_speed = 5 +first_layer_bed_temperature = 85 +first_layer_temperature = 260 +max_fan_speed = 35 +min_fan_speed = 20 +temperature = 260 + +[filament:Kimya PETG Carbon] +inherits = *PET* +filament_vendor = Kimya +extrusion_multiplier = 1.05 +filament_cost = 150.02 +filament_density = 1.317 +filament_colour = #804040 +first_layer_bed_temperature = 85 +first_layer_temperature = 240 +temperature = 240 + +[filament:Kimya ABS Carbon] +inherits = *ABSC* +filament_vendor = Kimya +filament_cost = 140.34 +filament_density = 1.032 +filament_colour = #804040 +first_layer_temperature = 260 +temperature = 260 + +[filament:Kimya ABS Kevlar] +inherits = Kimya ABS Carbon +filament_vendor = Kimya +filament_density = 1.037 + +[filament:E3D Edge] +inherits = *PET* +filament_vendor = E3D +filament_cost = 56.9 +filament_density = 1.26 +filament_type = EDGE + +[filament:E3D PC-ABS] +inherits = *ABS* +filament_vendor = E3D +filament_cost = 0 +filament_type = PC +filament_density = 1.05 +first_layer_temperature = 270 +temperature = 270 + +[filament:Fillamentum PLA] +inherits = *PLA* +filament_vendor = Fillamentum +filament_cost = 35.48 +filament_density = 1.24 +filament_spool_weight = 230 + +[filament:Fillamentum ABS] +inherits = *ABSC* +filament_vendor = Fillamentum +filament_cost = 32.4 +filament_density = 1.04 +filament_spool_weight = 230 +first_layer_temperature = 240 +temperature = 240 + +[filament:Fillamentum ASA] +inherits = *ABS* +filament_vendor = Fillamentum +filament_cost = 38.7 +filament_density = 1.07 +filament_spool_weight = 230 +fan_always_on = 1 +cooling = 1 +min_fan_speed = 20 +max_fan_speed = 20 +min_print_speed = 15 +slowdown_below_layer_time = 15 +first_layer_temperature = 260 +temperature = 260 +filament_type = ASA + +[filament:Prusament ASA] +inherits = *ABS* +filament_vendor = Prusa Polymers +filament_cost = 42.69 +filament_density = 1.07 +filament_spool_weight = 201 +fan_always_on = 1 +first_layer_temperature = 260 +first_layer_bed_temperature = 100 +temperature = 260 +bed_temperature = 100 +cooling = 1 +min_fan_speed = 20 +max_fan_speed = 20 +bridge_fan_speed = 30 +min_print_speed = 15 +slowdown_below_layer_time = 15 +disable_fan_first_layers = 4 +filament_type = ASA +filament_colour = #FFF2EC + +[filament:Prusament PC Blend] +inherits = *ABS* +filament_vendor = Prusa Polymers +filament_cost = 62.36 +filament_density = 1.22 +filament_spool_weight = 201 +fan_always_on = 0 +first_layer_temperature = 275 +first_layer_bed_temperature = 105 +temperature = 275 +bed_temperature = 105 +cooling = 1 +min_fan_speed = 20 +max_fan_speed = 20 +bridge_fan_speed = 30 +min_print_speed = 15 +slowdown_below_layer_time = 20 +disable_fan_first_layers = 4 +fan_below_layer_time = 30 +filament_type = PC +filament_colour = #DEE0E6 + +[filament:Prusament PC Blend Carbon Fiber] +inherits = Prusament PC Blend +filament_cost = 90.73 +filament_density = 1.16 +extrusion_multiplier = 1.04 +first_layer_temperature = 285 +temperature = 285 +disable_fan_first_layers = 4 +fan_below_layer_time = 10 +filament_colour = #BBBBBB + +[filament:Prusament PA11 Carbon Fiber] +inherits = Prusament PC Blend Carbon Fiber +filament_cost = 151.24 +filament_density = 1.11 +filament_type = PA +extrusion_multiplier = 1.05 +first_layer_temperature = 275 +temperature = 285 +first_layer_bed_temperature = 90 +bed_temperature = 105 +fan_below_layer_time = 10 + +[filament:Fillamentum CPE] +inherits = *PET* +filament_vendor = Fillamentum +filament_cost = 56.45 +filament_density = 1.25 +filament_spool_weight = 230 +filament_type = CPE +first_layer_bed_temperature = 90 +first_layer_temperature = 275 +min_fan_speed = 30 +max_fan_speed = 50 +disable_fan_first_layers = 3 +full_fan_speed_layer = 5 +temperature = 275 + +[filament:Fillamentum Timberfill] +inherits = *PLA* +filament_vendor = Fillamentum +extrusion_multiplier = 1.05 +filament_cost = 68 +filament_density = 1.15 +filament_spool_weight = 230 +filament_colour = #804040 +first_layer_temperature = 190 +temperature = 190 +filament_retract_lift = 0.2 + +[filament:Smartfil Wood] +inherits = *PLA* +filament_vendor = Smart Materials 3D +extrusion_multiplier = 1.05 +filament_cost = 68 +filament_density = 1.58 +filament_colour = #804040 +first_layer_temperature = 220 +temperature = 220 + +[filament:Generic ABS] +inherits = *ABSC* +filament_vendor = Generic +filament_cost = 27.82 +filament_density = 1.04 + +[filament:Esun ABS] +inherits = *ABSC* +filament_vendor = Esun +filament_cost = 27.82 +filament_density = 1.01 +filament_spool_weight = 265 + +[filament:Hatchbox ABS] +inherits = *ABSC* +filament_vendor = Hatchbox +filament_cost = 27.82 +filament_density = 1.04 +filament_spool_weight = 245 + +[filament:Filament PM ABS] +inherits = *ABSC* +filament_vendor = Filament PM +filament_cost = 27.82 +filament_density = 1.08 +filament_spool_weight = 230 + +[filament:Verbatim ABS] +inherits = *ABSC* +filament_vendor = Verbatim +filament_cost = 25.87 +filament_density = 1.05 +filament_spool_weight = 235 + +[filament:Generic PETG] +inherits = *PET* +filament_vendor = Generic +filament_cost = 27.82 +filament_density = 1.27 + +[filament:Extrudr DuraPro ASA] +inherits = Fillamentum ASA +filament_vendor = Extrudr +bed_temperature = 90 +filament_cost = 34.64 +filament_density = 1.05 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?material=120" +first_layer_bed_temperature = 90 +first_layer_temperature = 220 +temperature = 220 +filament_spool_weight = 230 + +[filament:Extrudr PETG] +inherits = *PET* +filament_vendor = Extrudr +bed_temperature = 70 +filament_cost = 35.45 +filament_density = 1.29 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?material=94" +first_layer_bed_temperature = 70 +first_layer_temperature = 220 +temperature = 220 +slowdown_below_layer_time = 20 +filament_spool_weight = 262 +full_fan_speed_layer = 0 + +[filament:Extrudr XPETG CF] +inherits = Extrudr PETG +filament_cost = 62.49 +filament_density = 1.29 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?material=198" +first_layer_temperature = 235 +temperature = 235 +filament_spool_weight = 230 + +[filament:Extrudr XPETG Matt] +inherits = Extrudr PETG +filament_cost = 29.99 +filament_density = 1.41 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?material=199" +first_layer_temperature = 230 +temperature = 230 + +[filament:Extrudr BioFusion] +inherits = *PLA* +filament_vendor = Extrudr +disable_fan_first_layers = 3 +full_fan_speed_layer = 0 +filament_cost = 31.23 +filament_density = 1.25 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?material=121" +first_layer_temperature = 220 +temperature = 220 +max_fan_speed = 45 +min_fan_speed = 25 +slowdown_below_layer_time = 20 +filament_spool_weight = 230 + +[filament:Extrudr Flax] +inherits = *PLA* +filament_vendor = Extrudr +filament_cost = 50.91 +filament_density = 1.45 +filament_notes = "High Performance Filament for decorative parts.\nPrints as easily as PLA with much higher strength and temperature resistance.\nFully biodegradable with a nice matt finish.\n\nhttps://www.extrudr.com/en/products/catalogue/?material=131" +first_layer_temperature = 190 +temperature = 190 +max_fan_speed = 80 +min_fan_speed = 30 +full_fan_speed_layer = 0 +slowdown_below_layer_time = 20 +filament_spool_weight = 262 + +[filament:Extrudr GreenTEC] +inherits = *PLA* +filament_vendor = Extrudr +filament_cost = 50.91 +filament_density = 1.3 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?ignorechildren=1&material=106" +first_layer_temperature = 208 +temperature = 208 +slowdown_below_layer_time = 20 +filament_spool_weight = 262 + +[filament:Extrudr GreenTEC Pro] +inherits = *PLA* +filament_vendor = Extrudr +filament_cost = 56.23 +filament_density = 1.35 +filament_notes = "High Performance Filament for technical parts.\nPrints as easily as PLA with much higher strength and temperature resistance.\nFully biodegradable with a nice matt finish.\n\nhttps://www.extrudr.com/en/products/catalogue/?material=134" +temperature = 215 +max_fan_speed = 80 +min_fan_speed = 30 +full_fan_speed_layer = 0 +slowdown_below_layer_time = 20 +filament_spool_weight = 230 + +[filament:Extrudr GreenTEC Pro Carbon] +inherits = *PLA* +filament_vendor = Extrudr +filament_cost = 62.49 +filament_density = 1.2 +filament_notes = "High Performance Filament for technical parts.\nPrints as easily as PLA with much higher stregnth and temperature resistance.\nFully biodegradable with a nice matt finish.\n\nhttps://www.extrudr.com/en/products/catalogue/?material=138" +first_layer_temperature = 225 +max_fan_speed = 80 +min_fan_speed = 30 +temperature = 225 +full_fan_speed_layer = 0 +slowdown_below_layer_time = 20 +filament_spool_weight = 230 + +[filament:Extrudr PLA NX1] +inherits = *PLA* +filament_vendor = Extrudr +filament_cost = 22.76 +filament_density = 1.24 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?material=97" +temperature = 205 +bed_temperature = 60 +first_layer_temperature = 205 +first_layer_bed_temperature = 60 +full_fan_speed_layer = 0 +max_fan_speed = 90 +min_fan_speed = 30 +slowdown_below_layer_time = 20 +filament_spool_weight = 262 + +[filament:Extrudr PLA NX2] +inherits = Extrudr PLA NX1 +filament_cost = 23.63 +filament_density = 1.3 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?material=128" + +[filament:Extrudr Flex Hard] +inherits = *FLEX* +filament_vendor = Extrudr +disable_fan_first_layers = 1 +extrusion_multiplier = 1.15 +filament_cost = 39.98 +filament_density = 1.2 +filament_max_volumetric_speed = 3 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?material=115" +filament_spool_weight = 230 +slowdown_below_layer_time = 20 + +[filament:Extrudr Flex Medium] +inherits = *FLEX* +filament_vendor = Extrudr +disable_fan_first_layers = 1 +extrusion_multiplier = 1.15 +filament_cost = 39.98 +filament_density = 1.19 +filament_max_volumetric_speed = 3 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?material=117" +filament_spool_weight = 230 +slowdown_below_layer_time = 20 + +[filament:Extrudr Flex SemiSoft] +inherits = *FLEX* +filament_vendor = Extrudr +disable_fan_first_layers = 1 +extrusion_multiplier = 1.15 +filament_cost = 39.98 +filament_density = 1.18 +filament_max_volumetric_speed = 1.8 +filament_notes = "https://www.extrudr.com/en/products/catalogue/?material=116" +filament_spool_weight = 230 +slowdown_below_layer_time = 20 + +[filament:addnorth Adamant S1] +inherits = *FLEX* +filament_vendor = addnorth +disable_fan_first_layers = 3 +extrusion_multiplier = 1 +filament_cost = +filament_density = 1.22 +temperature = 250 +bed_temperature = 50 +first_layer_temperature = 245 +first_layer_bed_temperature = 50 +slowdown_below_layer_time = 20 +min_print_speed = 20 +fan_below_layer_time = 15 +fan_always_on = 1 +cooling = 1 +min_fan_speed = 40 +max_fan_speed = 70 +bridge_fan_speed = 60 +filament_max_volumetric_speed = 1.7 + +[filament:addnorth Adura X] +inherits = *PET* +filament_vendor = addnorth +filament_cost = 29.99 +filament_density = 1.27 +filament_type = PA +extrusion_multiplier = 0.98 +bed_temperature = 105 +first_layer_bed_temperature = 105 +first_layer_temperature = 265 +temperature = 270 +fan_always_on = 0 +min_fan_speed = 20 +max_fan_speed = 40 +bridge_fan_speed = 70 +slowdown_below_layer_time = 10 +min_print_speed = 20 +fan_below_layer_time = 10 +disable_fan_first_layers = 3 +full_fan_speed_layer = 0 + +[filament:addnorth E-PLA] +inherits = *PLA* +filament_vendor = addnorth +filament_cost = 24.99 +filament_density = 1.24 +extrusion_multiplier = 0.98 +temperature = 215 +bed_temperature = 60 +first_layer_temperature = 215 +first_layer_bed_temperature = 60 +full_fan_speed_layer = 3 +slowdown_below_layer_time = 15 +filament_spool_weight = 0 + +[filament:addnorth ESD-PETG] +inherits = *PET* +filament_vendor = addnorth +filament_cost = 29.99 +filament_density = 1.27 +extrusion_multiplier = 1 +bed_temperature = 80 +first_layer_bed_temperature = 85 +first_layer_temperature = 245 +temperature = 265 +fan_always_on = 1 +min_fan_speed = 15 +max_fan_speed = 30 +bridge_fan_speed = 35 +slowdown_below_layer_time = 10 +min_print_speed = 15 +fan_below_layer_time = 8 +disable_fan_first_layers = 3 +full_fan_speed_layer = 0 + +[filament:addnorth OBC Polyethylene] +inherits = *FLEX* +filament_vendor = addnorth +disable_fan_first_layers = 3 +extrusion_multiplier = 1 +filament_cost = 82 +filament_density = 1.22 +temperature = 200 +bed_temperature = 100 +first_layer_temperature = 195 +first_layer_bed_temperature = 100 +slowdown_below_layer_time = 5 +fan_below_layer_time = 15 +fan_always_on = 1 +cooling = 1 +min_fan_speed = 20 +max_fan_speed = 30 +bridge_fan_speed = 40 +min_print_speed = 20 +filament_spool_weight = 0 +filament_notes = "Use Magigoo PP bed adhesive or PP packing tape (on a cold printbed)." + +[filament:addnorth PETG] +inherits = *PET* +filament_vendor = addnorth +filament_cost = 29.99 +filament_density = 1.27 +bed_temperature = 80 +first_layer_bed_temperature = 85 +first_layer_temperature = 240 +temperature = 250 +fan_always_on = 1 +min_fan_speed = 15 +max_fan_speed = 40 +bridge_fan_speed = 50 +slowdown_below_layer_time = 10 +min_print_speed = 15 +fan_below_layer_time = 15 +disable_fan_first_layers = 3 +full_fan_speed_layer = 0 +filament_spool_weight = 0 + +[filament:addnorth Rigid X] +inherits = *PET* +filament_vendor = addnorth +filament_cost = 29.99 +filament_density = 1.27 +extrusion_multiplier = 1 +bed_temperature = 85 +first_layer_bed_temperature = 90 +first_layer_temperature = 250 +temperature = 260 +fan_always_on = 1 +min_fan_speed = 20 +max_fan_speed = 60 +bridge_fan_speed = 70 +slowdown_below_layer_time = 10 +fan_below_layer_time = 20 +min_print_speed = 20 +disable_fan_first_layers = 3 +full_fan_speed_layer = 0 +filament_spool_weight = 0 +filament_notes = "Please use a nozzle that is resistant to abrasive filaments, like hardened steel." + +[filament:addnorth Textura] +inherits = *PLA* +filament_vendor = addnorth +filament_cost = 24.99 +filament_density = 1.24 +extrusion_multiplier = 0.95 +temperature = 215 +bed_temperature = 65 +first_layer_temperature = 215 +first_layer_bed_temperature = 65 +min_fan_speed = 20 +max_fan_speed = 40 +bridge_fan_speed = 60 +full_fan_speed_layer = 0 +slowdown_below_layer_time = 15 +min_print_speed = 20 +filament_spool_weight = 0 + +[filament:Filamentworld ABS] +inherits = *ABSC* +filament_vendor = Filamentworld +filament_cost = 24.9 +filament_density = 1.04 +temperature = 230 +bed_temperature = 95 +first_layer_temperature = 240 +first_layer_bed_temperature = 100 +max_fan_speed = 20 +min_fan_speed = 10 +min_print_speed = 20 +disable_fan_first_layers = 3 +fan_below_layer_time = 60 +slowdown_below_layer_time = 15 +bridge_fan_speed = 20 + +[filament:Filamentworld PETG] +inherits = *PET* +filament_vendor = Filamentworld +filament_cost = 34.9 +filament_density = 1.27 +bed_temperature = 70 +first_layer_bed_temperature = 85 +first_layer_temperature = 240 +temperature = 235 +fan_always_on = 1 +min_fan_speed = 25 +max_fan_speed = 55 +bridge_fan_speed = 55 +slowdown_below_layer_time = 20 +min_print_speed = 20 +fan_below_layer_time = 35 +disable_fan_first_layers = 2 +full_fan_speed_layer = 0 +filament_spool_weight = 0 + +[filament:Filamentworld PLA] +inherits = *PLA* +filament_vendor = Filamentworld +filament_cost = 24.9 +filament_density = 1.24 +temperature = 205 +bed_temperature = 55 +first_layer_temperature = 215 +first_layer_bed_temperature = 60 +full_fan_speed_layer = 3 +slowdown_below_layer_time = 10 +filament_spool_weight = 0 +min_print_speed = 20 + +[filament:Filament PM PETG] +inherits = *PET* +filament_vendor = Filament PM +filament_cost = 27.82 +filament_density = 1.27 +filament_spool_weight = 230 + +[filament:Generic PLA] +inherits = *PLA* +filament_vendor = Generic +filament_cost = 25.4 +filament_density = 1.24 + +[filament:3D-Fuel Standard PLA] +inherits = *PLA* +filament_vendor = 3D-Fuel +filament_cost = 22.14 +filament_density = 1.24 +first_layer_temperature = 210 +temperature = 200 + +[filament:3D-Fuel EasiPrint PLA] +inherits = 3D-Fuel Standard PLA +filament_cost = 30.44 + +[filament:3D-Fuel Pro PLA] +inherits = *PLA* +filament_vendor = 3D-Fuel +filament_cost = 26.57 +filament_density = 1.22 +first_layer_temperature = 220 +temperature = 215 + +[filament:3D-Fuel Buzzed] +inherits = 3D-Fuel Standard PLA +filament_cost = 44.27 +filament_retract_lift = 0 +first_layer_temperature = 210 +temperature = 195 + +[filament:3D-Fuel Wound up] +inherits = 3D-Fuel Buzzed +filament_cost = 44.27 +filament_retract_lift = nil +first_layer_temperature = 215 +temperature = 210 + +[filament:3D-Fuel Workday ABS] +inherits = *ABSC* +filament_vendor = 3D-Fuel +filament_cost = 23.25 +filament_density = 1.04 + +[filament:Jessie PLA] +inherits = *PLA* +filament_vendor = Printed Solid +filament_cost = 21 +filament_density = 1.24 + +[filament:Jessie PETG] +inherits = *PET* +filament_vendor = Printed Solid +filament_cost = 22 +filament_density = 1.27 +first_layer_temperature = 240 +first_layer_bed_temperature = 85 +temperature = 245 +bed_temperature = 90 + +[filament:Devil Design PLA] +inherits = *PLA* +filament_vendor = Devil Design +filament_cost = 20.99 +filament_density = 1.24 +filament_spool_weight = 250 + +[filament:Devil Design PETG] +inherits = *PET* +filament_vendor = Devil Design +filament_cost = 20.99 +filament_density = 1.23 +filament_spool_weight = 250 +first_layer_temperature = 230 +first_layer_bed_temperature = 85 +temperature = 230 +bed_temperature = 90 + +[filament:Spectrum PLA] +inherits = *PLA* +filament_vendor = Spectrum +filament_cost = 21.50 +filament_density = 1.24 + +[filament:Generic FLEX] +inherits = *FLEX* +filament_vendor = Generic +filament_cost = 82 +filament_density = 1.22 +filament_max_volumetric_speed = 1.2 +filament_retract_length = 0 +filament_retract_speed = nil +filament_retract_lift = nil + +[filament:Fillamentum Flexfill 92A] +inherits = *FLEX* +filament_vendor = Fillamentum +filament_cost = 33.99 +filament_density = 1.20 +filament_spool_weight = 230 +filament_max_volumetric_speed = 1.2 +fan_always_on = 1 +cooling = 0 +max_fan_speed = 60 +min_fan_speed = 60 +disable_fan_first_layers = 4 +full_fan_speed_layer = 6 + +[filament:AmazonBasics TPU] +inherits = *FLEX* +filament_vendor = AmazonBasics +fan_always_on = 1 +filament_max_volumetric_speed = 1.8 +extrusion_multiplier = 1.14 +first_layer_temperature = 235 +first_layer_bed_temperature = 50 +temperature = 235 +bed_temperature = 50 +bridge_fan_speed = 100 +max_fan_speed = 80 +min_fan_speed = 80 +filament_retract_before_travel = 3 +filament_cost = 19.99 +filament_density = 1.21 +disable_fan_first_layers = 4 +full_fan_speed_layer = 6 +min_print_speed = 15 +slowdown_below_layer_time = 10 +cooling = 1 + +[filament:SainSmart TPU] +inherits = *FLEX* +filament_vendor = SainSmart +fan_always_on = 1 +filament_max_volumetric_speed = 2.5 +extrusion_multiplier = 1.05 +first_layer_temperature = 230 +first_layer_bed_temperature = 50 +temperature = 230 +bed_temperature = 50 +bridge_fan_speed = 100 +max_fan_speed = 80 +min_fan_speed = 80 +filament_retract_before_travel = 3 +filament_cost = 32.99 +filament_density = 1.21 +disable_fan_first_layers = 4 +full_fan_speed_layer = 6 +min_print_speed = 15 +slowdown_below_layer_time = 10 +cooling = 1 + +[filament:NinjaTek NinjaFlex TPU] +inherits = *FLEX* +filament_vendor = NinjaTek +fan_always_on = 1 +filament_max_volumetric_speed = 1.2 +extrusion_multiplier = 1.15 +first_layer_temperature = 238 +first_layer_bed_temperature = 50 +temperature = 238 +bed_temperature = 50 +bridge_fan_speed = 75 +max_fan_speed = 60 +min_fan_speed = 60 +filament_cost = 85 +filament_density = 1.19 +disable_fan_first_layers = 1 +full_fan_speed_layer = 3 +min_print_speed = 10 +slowdown_below_layer_time = 10 +cooling = 1 + +[filament:NinjaTek Cheetah TPU] +inherits = NinjaTek NinjaFlex TPU +filament_retract_length = 1.5 +filament_density = 1.22 +filament_max_volumetric_speed = 4 +extrusion_multiplier = 1.05 +filament_retract_speed = 45 +filament_deretract_speed = 25 +first_layer_temperature = 240 +temperature = 240 + +[filament:Filatech FilaFlex40] +inherits = *FLEX* +filament_vendor = Filatech +fan_always_on = 1 +filament_max_volumetric_speed = 2.5 +extrusion_multiplier = 1.1 +first_layer_temperature = 230 +first_layer_bed_temperature = 50 +temperature = 230 +bed_temperature = 50 +bridge_fan_speed = 100 +max_fan_speed = 50 +min_fan_speed = 50 +filament_cost = 84.68 +filament_density = 1.22 +disable_fan_first_layers = 4 +full_fan_speed_layer = 6 +min_print_speed = 15 +slowdown_below_layer_time = 10 +cooling = 1 + +[filament:Filatech FilaFlex30] +inherits = Filatech FilaFlex40 +temperature = 225 +filament_density = 1.15 +extrusion_multiplier = 1.1 +filament_cost = + +[filament:Filatech FilaFlex55] +inherits = Filatech FilaFlex40 +temperature = 230 +filament_density = 1.18 +bed_temperature = 60 +fan_always_on = 0 +fan_below_layer_time = 60 +filament_cost = +first_layer_temperature = 235 +extrusion_multiplier = 1 + +[filament:Filatech TPU] +inherits = Filatech FilaFlex40 +first_layer_temperature = 230 +filament_density = 1.2 +fan_below_layer_time = 60 +max_fan_speed = 80 +min_fan_speed = 80 +fan_always_on = 1 +temperature = 235 + +[filament:Filatech ABS] +inherits = *ABSC* +filament_vendor = Filatech +filament_cost = +extrusion_multiplier = 1 +filament_density = 1.05 + +[filament:Filatech FilaCarbon] +inherits = *ABSC* +filament_vendor = Filatech +filament_cost = +extrusion_multiplier = 0.95 +filament_density = 1.1 +first_layer_bed_temperature = 100 +bed_temperature = 100 + +[filament:Filatech FilaPLA] +inherits = *PLA* +filament_vendor = Filatech +filament_cost = +filament_density = 1.3 +first_layer_temperature = 235 +first_layer_bed_temperature = 50 +temperature = 230 +bed_temperature = 55 + +[filament:Filatech PLA] +inherits = *PLA* +filament_vendor = Filatech +filament_cost = +filament_density = 1.25 +first_layer_temperature = 215 +temperature = 210 + +[filament:Filatech PLA+] +inherits = Filatech PLA +filament_density = 1.24 + +[filament:Filatech FilaTough] +inherits = Filatech ABS +filament_cost = +extrusion_multiplier = 0.95 +filament_density = 1.29 +first_layer_temperature = 245 +first_layer_bed_temperature = 80 +temperature = 240 +bed_temperature = 90 +cooling = 0 + +[filament:Filatech HIPS] +inherits = Prusa HIPS +filament_vendor = Filatech +filament_density = 1.07 +filament_spool_weight = +first_layer_temperature = 230 +first_layer_bed_temperature = 100 +temperature = 225 +bed_temperature = 100 + +[filament:Filatech PA] +inherits = *ABSC* +filament_vendor = Filatech +filament_density = 1.1 +first_layer_temperature = 275 +first_layer_bed_temperature = 105 +temperature = 275 +bed_temperature = 105 +fan_always_on = 0 +cooling = 0 +bridge_fan_speed = 25 +filament_type = PA + +[filament:Filatech PC] +inherits = Filatech PA +filament_density = 1.2 +filament_type = PC + +[filament:Filatech PC-ABS] +inherits = Filatech PC +first_layer_temperature = 270 +temperature = 270 +first_layer_bed_temperature = 105 +bed_temperature = 105 +filament_density = 1.08 +filament_type = PC +fan_always_on = 0 +cooling = 1 +extrusion_multiplier = 0.95 +disable_fan_first_layers = 6 + +[filament:Filatech PETG] +inherits = *PET* +filament_vendor = Filatech +filament_cost = +filament_density = 1.27 +first_layer_temperature = 240 +first_layer_bed_temperature = 75 +temperature = 245 +bed_temperature = 80 +extrusion_multiplier = 0.95 +fan_always_on = 0 + +[filament:Filatech Wood-PLA] +inherits = Filatech PLA +filament_density = 1.05 +first_layer_temperature = 210 + +[filament:Ultrafuse PET] +inherits = *PET* +filament_vendor = BASF +filament_density = 1.33 +filament_colour = #F7F7F7 +first_layer_temperature = 220 +first_layer_bed_temperature = 70 +temperature = 215 +bed_temperature = 70 +fan_below_layer_time = 10 +min_fan_speed = 75 +max_fan_speed = 100 +bridge_fan_speed = 100 +filament_type = PET +disable_fan_first_layers = 1 +full_fan_speed_layer = 3 +filament_notes = "Material Description\nUltrafuse PET is made from a premium PET and prints as easy as PLA, but is much stronger. The filament has a large operating window for printing (temperature vs. speed), so it can be used on every 3D-printer. PET will give you outstanding printing results: a good layer adhesion, a high resolution and it is easy to handle. Ultrafuse PET can be 100% recycled, is watertight and has great colors and finish.\n\nPrinting Recommendations:\nUltrafuse PET can be printed directly onto a clean build plate. For challenging prints, use 3dLac to improve adhesion." + +[filament:Ultrafuse PRO1] +inherits = Prusament PLA +filament_vendor = BASF +filament_cost = +filament_density = 1.25 +filament_spool_weight = 0 +filament_colour = #FFFFFF +filament_notes = "Material Description\nPLA PRO1 is an extremely versatile tough PLA filament made for professionals. It reduces your printing time by 30% – 80%, (subject to printer and object limitations) and the strength exceeds overall mechanical properties of printed ABS parts. Printer settings can be tuned to achieve blazing fast speeds or an unrivaled surface finish. The excellent quality control ensures the highest levels of consistency between colors and batches, it will perform as expected, every time.\n\nPrinting Recommendations:\nUltrafuse PLA PRO1 can be printed directly onto a clean build plate." + +[filament:Ultrafuse ABS] +inherits = *ABSC* +filament_vendor = BASF +filament_density = 1.04 +min_fan_speed = 10 +max_fan_speed = 20 +bed_temperature = 100 +disable_fan_first_layers = 3 +filament_colour = #FFFFFF +filament_notes = "Material Description\nABS is the second most used 3D printing material. It is strong, flexible and has a high heat resistance. ABS is a preferred plastic for engineers and professional applications. ABS can be smoothened with acetone. To make a proper 3D print with ABS you will need a heated print bed. The filament is available in 9 colors.\n\nPrinting Recommendations:\n\nApply Tape, adhesion spray or glue to a clean build plate to improve adhesion." + +[filament:Ultrafuse ABS Fusion+] +inherits = Ultrafuse ABS +filament_density = 1.08 +first_layer_bed_temperature = 100 +first_layer_temperature = 270 +temperature = 270 +filament_colour = #FFF8D9 +filament_notes = "Material Description\nABS Fusion+ made with Polyscope XILOYâ„¢ 3D is an engineering filament which has been optimized for 3D-printing. This special grade has been developed in collaboration with Polyscope Polymers - renowned for its material solutions in the automotive industry. ABS is a thermoplastic which is used in many applications. Although ABS has been classified as a standard material in 3D-printing it is known to be quite challenging to process. ABS Fusion+ combines the properties of ABS with an improved processability. The filament is based on an ABS grade which can be directly printed on glass without any adhesives or tape and has a higher success rate of prints due to extreme low warping." + +[filament:Ultrafuse ASA] +inherits = Ultrafuse ABS Fusion+ +filament_density = 1.07 +filament_colour = #FFF4CA +first_layer_temperature = 275 +temperature = 275 +first_layer_bed_temperature = 100 +bed_temperature = 100 +filament_type = ASA +min_fan_speed = 25 +max_fan_speed = 50 +bridge_fan_speed = 100 +disable_fan_first_layers = 4 +filament_notes = "Material Description\nUltrafuse ASA is a high-performance thermoplastic with similar mechanical properties as ABS. ASA offers additional benefits such as high outdoor weather resistance. The UV resistance, toughness, and rigidity make it an ideal material to 3D-print outdoor fixtures and appliances without losing its properties or color. When also taking into account the high heat resistance and high chemical resistance, this filament is a good choice for many types of applications.\n\nPrinting Recommendations:\nApply Magigoo PC, 3D lac or Dimafix to a clean build plate to improve adhesion." + +[filament:Ultrafuse HIPS] +inherits = Ultrafuse ABS +temperature = 250 +filament_density = 1.02 +filament_type = HIPS +min_fan_speed = 20 +max_fan_speed = 20 +filament_soluble = 1 +filament_notes = "Material Description\nUltrafuse HIPS is a high-quality engineering thermoplastic, which is well known in the 3D-printing industry as a support material for ABS. But this material has additional properties to offer like good impact resistance, good dimensional stability, and easy post-processing. HiPS is a great material to use as a support for ABS because there is a good compatibility between the two materials, and HIPS is an easy breakaway support. Now you have the opportunity to create ABS models with complex geometry. HIPS is easy to post process with glue or with sanding paper." + +[filament:Ultrafuse PA] +inherits = Fillamentum Nylon FX256 +filament_vendor = BASF +filament_density = 1.12 +filament_colour = #ECFAFF +first_layer_temperature = 240 +temperature = 240 +first_layer_bed_temperature = 80 +bed_temperature = 70 +min_fan_speed = 0 +max_fan_speed = 0 +bridge_fan_speed = 0 +fan_below_layer_time = 30 +slowdown_below_layer_time = 20 +min_print_speed = 15 +filament_notes = "Material Description\nThe key features of Ultrafuse PA are the high strength and high modulus. Furthermore, Ultrafuse PA shows a good thermal distortion stability.\n\nPrinting Recommendations:\nApply PVA glue, Kapton tape or PA adhesive to a clean buildplate to improve adhesion." + +[filament:Ultrafuse PA6 GF30] +inherits = Ultrafuse PA +filament_density = 1.17 +first_layer_temperature = 270 +temperature = 270 +first_layer_bed_temperature = 100 +bed_temperature = 100 +filament_colour = #404040 +fan_always_on = 1 +min_fan_speed = 0 +max_fan_speed = 50 +bridge_fan_speed = 100 +disable_fan_first_layers = 1 +full_fan_speed_layer = 3 +slowdown_below_layer_time = 15 +filament_notes = "Material Description\nUltrafuse® PA6 GF30 is a unique compound specifically developed for FFF printing. Due to the glass fiber content of 30%, parts tend to warp less. In addition the excellent layer adhesion and its compatibility with the water soluble support Ultrafuse® BVOH make this material the perfect solution to develop industrial applications on an FFF printer.\n\nWith its high wear and chemical resistance, high stiffness and strength, Ultrafuse® PA6 GF30 is perfect for a wide variety of applications in automotive, electronics or transportation.\n\nUltrafuse PA6 GF30 is designed for functional prototyping and demanding applications such as industrial tooling, transportation, electronics, small appliances, sports & leisure\n\nPrinting Recommendations:\nThis material contains fibers that have an abrasive effect on printer components. Use a hardened or Ruby nozzle with a diameter of 0.6 or larger for optimal performance and avoid damage to the nozzle.\n\nUltrafuse PA6 GF30 can be printed directly onto a clean build plate. For challenging prints, use Magigoo PA gluestick to improve adhesion." + +[filament:Ultrafuse PAHT-CF15] +inherits = Ultrafuse PA6 GF30 +filament_density = 1.23 +filament_notes = "Material Description\nPAHT CF15 is a high-performance 3D printing filament that opens new application fields in FFF printing. In parallel to its advanced mechanical properties, dimensional stability, and chemical resistance, it has very good processability. It works in any FFF printer with a hardened nozzle. In addition to that, it is compatible with water-soluble support material and HiPS, which allow printing complex geometries that work in challenging environments. PAHT CF15 has high heat resistance up to 130 °C and low moisture absorption.\n\nPrinting Recommendations:\nThis material contains fibers that have an abrasive effect on printer components. Use a hardened or Ruby nozzle with a diameter of 0.6 or larger for optimal performance and avoid damage to the nozzle.\n\nUltrafuse PAHT-CF can be printed directly onto a clean build plate. For challenging prints, use 3dLac to improve adhesion." + +[filament:Ultrafuse PC-ABS-FR] +inherits = Ultrafuse ABS +filament_colour = #505050 +filament_density = 1.17 +first_layer_temperature = 275 +temperature = 275 +first_layer_bed_temperature = 105 +bed_temperature = 105 +filament_type = PC +min_fan_speed = 20 +max_fan_speed = 20 +bridge_fan_speed = 30 +disable_fan_first_layers = 4 +filament_notes = "Material Description\nUltrafuse® PC/ABS FR Black is a V-0 flame retardant blend of Polycarbonate and ABS – two of the most used thermoplastics for engineering & electrical applications. The combination of these two materials results in a premium material with a mix of the excellent mechanical properties of PC and the comparably low printing temperature of ABS. Combined with a halogen free flame retardant, parts printed with Ultrafuse® PC/ABS FR Black feature great tensile and impact strength, higher thermal resistance than ABS and can fulfill the requirements of the UL94 V-0 standard.\n\nPrinting Recommendations:\nApply Magigoo PC to a clean build plate to improve adhesion." + +[filament:Ultrafuse PET-CF15] +inherits = Ultrafuse PET +filament_density = 1.36 +filament_colour = #404040 +first_layer_temperature = 270 +temperature = 270 +first_layer_bed_temperature = 75 +bed_temperature = 75 +min_fan_speed = 60 +max_fan_speed = 100 +bridge_fan_speed = 100 +disable_fan_first_layers = 1 +full_fan_speed_layer = 3 +slowdown_below_layer_time = 15 +fan_below_layer_time = 30 +filament_notes = "Material Description\nPET CF15 is a Carbon Fiber reinforced PET which has precisely tuned material properties, for a wide range of technical applications. The filament is very strong and stiff and has high heat resistance. With its high dimensional stability and low abrasiveness, the filament offers an easy to print experience which allows direct printing on glass or a PEI sheet. It is compatible with HiPS for breakaway support and water soluble support and has an excellent surface finish.\n\nPrinting Recommendations:\nThis material contains fibers that have an abrasive effect on printer components. Use a hardened or Ruby nozzle with a diameter of 0.6 or larger for optimal performance and avoid damage to the nozzle.\n\nUltrafuse PET-CF15 can be printed directly onto a clean build plate. For challenging prints, use 3dLac to improve adhesion." + +[filament:Ultrafuse PLA] +inherits = *PLA* +filament_vendor = BASF +filament_density = 1.25 +full_fan_speed_layer = 3 +filament_notes = "Material Description\nPLA is one of the most used materials for 3D printing. Ultrafuse PLA is available in a wide range of colors. The glossy feel often attracts those who print display models or items for household use. Many appreciate the plant-based origin of this material. When properly cooled, PLA has a high maximum printing speed and sharp printed corners. Combining this with low warping of the print makes it a popular plastic for home printers, hobbyists, prototyping and schools.\n\nPrinting Recommendations:\nUltrafuse PLA can be printed directly onto a clean build plate." + +[filament:Ultrafuse PP] +inherits = Ultrafuse ABS +filament_density = 0.91 +filament_colour = #F0F0F0 +first_layer_temperature = 240 +temperature = 240 +first_layer_bed_temperature = 80 +bed_temperature = 70 +min_fan_speed = 100 +max_fan_speed = 100 +bridge_fan_speed = 100 +disable_fan_first_layers = 1 +full_fan_speed_layer = 3 +fan_below_layer_time = 60 +slowdown_below_layer_time = 20 +min_print_speed = 10 +filament_type = PP +filament_max_volumetric_speed = 2.5 +filament_notes = "Material Description\nUltrafuse PP is high-performance thermoplastic with low density, high elasticity and high resistance to fatigue. The mechanical properties make it an ideal material for 3D-printing applications which have to endure high stress or strain. The filament has high chemical resistance and a high isolation value. PP is one of the most used materials in the world, due to its versatility and ability to engineer lightweight tough parts.\n\nPrinting Recommendations:\nApply PP tape or Magigoo PP adhesive to the buildplate for optimal adhesion." + +[filament:Ultrafuse PP-GF30] +inherits = Ultrafuse PP +filament_density = 1.07 +filament_colour = #404040 +first_layer_temperature = 260 +temperature = 250 +first_layer_bed_temperature = 90 +bed_temperature = 40 +min_fan_speed = 40 +max_fan_speed = 75 +fan_always_on = 1 +fan_below_layer_time = 30 +slowdown_below_layer_time = 15 +min_print_speed = 15 +filament_notes = "Ultrafuse PP GF30 is polypropylene, reinforced with 30% glass fiber content. The fibers in this compound are specially designed for 3D-printing filaments and are compatible with a wide range of standard FFF 3D-printers. The extreme stiffness makes this material highly suitable for demanding applications. Other key properties of PPGF30 are high heat resistance and improved UV-resistance. All these excellent properties make this filament highly suitable in an industrial environment.\n\nPrinting Recommendations:\nThis material contains fibers that have an abrasive effect on printer components. Use a hardened or Ruby nozzle with a diameter of 0.6 or larger for optimal performance and avoid damage to the nozzle.\n\nApply PP strapping tape or PPGF adhesive to a clean build plate for optimal adhesion." + +[filament:Ultrafuse TPC-45D] +inherits = *FLEX* +filament_vendor = BASF +extrusion_multiplier = 1 +filament_density = 1.15 +filament_colour = #0035EC +first_layer_temperature = 235 +temperature = 235 +first_layer_bed_temperature = 60 +bed_temperature = 60 +min_fan_speed = 10 +max_fan_speed = 50 +bridge_fan_speed = 80 +fan_below_layer_time = 30 +slowdown_below_layer_time = 15 +min_print_speed = 15 +fan_always_on = 1 +cooling = 1 +filament_max_volumetric_speed = 1.2 +filament_notes = "Material Description\nTPC 45D is a flexible, shore 45D, rubber-like Thermoplastic Copolyester Elastomer (TPE-C), which is derived from rapeseed oil and combines the best properties of elastomers (rubbers) and polyesters. The material delivers excellent adhesion in the Z-direction, meaning that the printed layers do not detach - even with extreme deformation.\n\nPrinting Recommendations:\nApply Magigoo Flex to a clean build plate to improve adhesion." + +[filament:Ultrafuse TPU-64D] +inherits = Ultrafuse TPC-45D +filament_density = 1.16 +first_layer_temperature = 230 +temperature = 225 +first_layer_bed_temperature = 40 +bed_temperature = 40 +min_fan_speed = 20 +max_fan_speed = 100 +filament_notes = "Material Description\nUltrafuse® TPU 64D is the hardest elastomer in BASF Forward AM’s flexible productline. The material shows a relatively high rigidity while maintaining a certain flexibility. This filament is the perfect match for industrial applications requiring rigid parts being resistant to impact, wear and tear. Due to its property profile, the material can be used as an alternative for parts made from ABS and rubbers. Ultrafuse® TPU 64D is easy to print on direct drive and bowden style printers and is compatible with soluble BVOH support to realize the most complex geometries.\n\nPrinting Recommendations:\nUltrafuse TPU can be printed directly onto a clean build plate. A small amount of 3Dlac can make removal easier after printing." + +[filament:Ultrafuse TPU-85A] +inherits = Ultrafuse TPU-64D +filament_density = 1.11 +first_layer_temperature = 225 +temperature = 220 +filament_notes = "Material Description\nUltrafuse® TPU 85A comes in its natural white color. Chemical properties (e.g. resistance against particular substances) and tolerance for solvents can be made available, if these factors are relevant for a specific application. Generally, these properties correspond to publicly available data on polyether based TPUs. This material is not FDA conform. Good flexibility at low temperature, good wear performance and good damping behavior are the key features of Ultrafuse® TPU 85A.\n\nPrinting Recommendations:\nUltrafuse TPU can be printed directly onto a clean build plate. A small amount of 3Dlac can make removal easier after printing." + +[filament:Ultrafuse TPU-95A] +inherits = Ultrafuse TPU-85A +filament_density = 1.14 +first_layer_temperature = 230 +temperature = 225 +filament_notes = "Material Description\nUltrafuse® TPU 95A comes with a well-balanced profile of flexibility and durability. On top of that, it allows for easier and faster printing then softer TPU grades. Parts printed with Ultrafuse® TPU 95A show a high elongation, good impact resistance, excellent layer adhesion and a good resistance to oils and common industrially used chemicals. Due to its good printing behavior, Ultrafuse® TPU 95A is a good choice for starting printing flexible materials on both direct drive and bowden style printers.\n\nPrinting Recommendations:\nUltrafuse TPU can be printed directly onto a clean build plate. A small amount of 3Dlac can make removal easier after printing." + +[filament:Ultrafuse rPET] +inherits = Ultrafuse PET +filament_density = 1.27 +filament_colour = #9DC5FF +first_layer_temperature = 235 +temperature = 235 +first_layer_bed_temperature = 80 +bed_temperature = 75 +min_fan_speed = 50 +max_fan_speed = 100 +fan_below_layer_time = 15 +filament_notes = "Material Description\nPET is mainly known by the well-known PET bottle material. This recycled has a natural transparent blueish look. It has excellent 3D printing properties and good mechanical characteristics." + +[filament:Ultrafuse Metal] +inherits = *ABSC* +filament_vendor = BASF +filament_density = 4.5 +extrusion_multiplier = 1.08 +first_layer_temperature = 250 +first_layer_bed_temperature = 100 +temperature = 250 +bed_temperature = 100 +min_fan_speed = 0 +max_fan_speed = 0 +bridge_fan_speed = 0 +cooling = 0 +fan_always_on = 0 +filament_max_volumetric_speed = 4 +filament_type = METAL +compatible_printers_condition = nozzle_diameter[0]>=0.4 +filament_colour = #FFFFFF + +[filament:Polymaker PC-Max] +inherits = *ABS* +filament_vendor = Polymaker +filament_cost = 77.3 +filament_density = 1.20 +filament_type = PC +bed_temperature = 115 +filament_colour = #FFF2EC +first_layer_bed_temperature = 100 +first_layer_temperature = 270 +temperature = 270 +bridge_fan_speed = 0 + +[filament:PrimaSelect PVA+] +inherits = *PLA* +filament_vendor = PrimaSelect +filament_cost = 122.1 +filament_density = 1.23 +cooling = 0 +fan_always_on = 0 +filament_colour = #FFFFD7 +filament_soluble = 1 +filament_type = PVA +first_layer_temperature = 195 +temperature = 195 + +[filament:Prusa ABS] +inherits = *ABSC* +filament_vendor = Made for Prusa +filament_cost = 27.82 +filament_density = 1.08 +filament_spool_weight = 230 + +[filament:Prusa HIPS] +inherits = Generic HIPS +filament_vendor = Made for Prusa +first_layer_temperature = 220 +temperature = 220 + +[filament:Generic HIPS] +inherits = *ABS* +filament_vendor = Generic +filament_cost = 27.3 +filament_density = 1.04 +bridge_fan_speed = 50 +cooling = 1 +extrusion_multiplier = 1 +fan_always_on = 1 +fan_below_layer_time = 10 +filament_colour = #FFFFD7 +filament_soluble = 1 +filament_type = HIPS +first_layer_temperature = 230 +max_fan_speed = 20 +min_fan_speed = 20 +temperature = 230 +compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_model!="MINI" and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) + +[filament:Prusa PETG] +inherits = *PET* +filament_vendor = Made for Prusa +filament_cost = 27.82 +filament_density = 1.27 +filament_spool_weight = 230 + +[filament:Verbatim PETG] +inherits = *PET* +filament_vendor = Verbatim +filament_cost = 27.90 +filament_density = 1.27 +filament_spool_weight = 235 + +[filament:Prusament PETG] +inherits = *PET* +filament_vendor = Prusa Polymers +first_layer_temperature = 240 +temperature = 250 +filament_cost = 36.29 +filament_density = 1.27 +filament_spool_weight = 201 +filament_type = PETG + +[filament:Prusa PLA] +inherits = *PLA* +filament_vendor = Made for Prusa +filament_cost = 27.82 +filament_density = 1.24 +filament_spool_weight = 230 + +[filament:Eolas Prints PLA] +inherits = *PLA* +filament_vendor = Eolas Prints +filament_cost = 23.50 +filament_density = 1.24 +filament_spool_weight = 0 +filament_colour = #4D9398 +temperature = 208 + +[filament:Eolas Prints PLA Matte] +inherits = Eolas Prints PLA +filament_cost = 25.50 +temperature = 212 + +[filament:Eolas Prints INGEO 850] +inherits = Eolas Prints PLA +filament_cost = 25.90 +temperature = 210 + +[filament:Eolas Prints INGEO 870] +inherits = Eolas Prints PLA +filament_cost = 25.90 +temperature = 215 +first_layer_bed_temperature = 68 +first_layer_temperature = 220 +bed_temperature = 65 + +[filament:Eolas Prints PETG] +inherits = *PET* +filament_vendor = Eolas Prints +filament_cost = 29.90 +filament_density = 1.27 +filament_spool_weight = 0 +filament_colour = #4D9398 +temperature = 240 +first_layer_bed_temperature = 85 +first_layer_temperature = 235 +bed_temperature = 90 + +[filament:Eolas Prints PETG - UV Resistant] +inherits = Eolas Prints PETG +filament_cost = 35.90 +temperature = 237 +first_layer_temperature = 232 + +[filament:Eolas Prints TPU 93A] +inherits = *FLEX* +filament_vendor = Eolas Prints +filament_cost = 34.99 +filament_density = 1.21 +filament_colour = #4D9398 +filament_max_volumetric_speed = 1.2 +temperature = 235 +first_layer_bed_temperature = 30 +bed_temperature = 30 +filament_retract_length = 0 +extrusion_multiplier = 1.16 + +[filament:Fiberlogy Easy PLA] +inherits = *PLA* +filament_vendor = Fiberlogy +filament_cost = 20 +filament_density = 1.24 +first_layer_temperature = 220 +temperature = 220 +filament_spool_weight = 330 + +[filament:Fiberlogy Easy PET-G] +inherits = *PET* +filament_vendor = Fiberlogy +filament_spool_weight = 330 +filament_cost = 20 +filament_density = 1.27 +first_layer_bed_temperature = 80 +bed_temperature = 80 +first_layer_temperature = 235 +temperature = 235 +min_fan_speed = 15 +max_fan_speed = 30 +bridge_fan_speed = 60 +disable_fan_first_layers = 5 +full_fan_speed_layer = 5 +slowdown_below_layer_time = 15 + +[filament:Fiberlogy ASA] +inherits = *ABS* +filament_vendor = Fiberlogy +filament_cost = 33 +filament_density = 1.07 +filament_spool_weight = 330 +fan_always_on = 0 +cooling = 1 +min_fan_speed = 10 +max_fan_speed = 15 +bridge_fan_speed = 30 +min_print_speed = 15 +slowdown_below_layer_time = 15 +first_layer_temperature = 260 +temperature = 260 +first_layer_bed_temperature = 100 +bed_temperature = 100 +filament_type = ASA +fan_below_layer_time = 30 +disable_fan_first_layers = 5 + +[filament:Fiberlogy Easy ABS] +inherits = Fiberlogy ASA +filament_cost = 22.67 +filament_density = 1.09 +fan_always_on = 0 +cooling = 1 +min_fan_speed = 10 +max_fan_speed = 15 +min_print_speed = 15 +slowdown_below_layer_time = 15 +first_layer_temperature = 250 +temperature = 250 +first_layer_bed_temperature = 100 +bed_temperature = 100 +filament_type = ABS +fan_below_layer_time = 25 +disable_fan_first_layers = 5 + +[filament:Fiberlogy CPE HT] +inherits = *PET* +filament_vendor = Fiberlogy +filament_cost = 42.67 +filament_density = 1.18 +extrusion_multiplier = 0.98 +filament_spool_weight = 330 +fan_always_on = 1 +cooling = 1 +min_fan_speed = 0 +max_fan_speed = 0 +bridge_fan_speed = 50 +min_print_speed = 15 +first_layer_temperature = 275 +temperature = 275 +first_layer_bed_temperature = 105 +bed_temperature = 105 +filament_type = CPE +fan_below_layer_time = 20 +slowdown_below_layer_time = 15 +disable_fan_first_layers = 5 + +[filament:Fiberlogy PCTG] +inherits = Fiberlogy CPE HT +filament_cost = 29.41 +filament_density = 1.23 +extrusion_multiplier = 1 +min_fan_speed = 10 +max_fan_speed = 15 +first_layer_temperature = 265 +temperature = 265 +first_layer_bed_temperature = 90 +bed_temperature = 90 +filament_type = PCTG + +[filament:Fiberlogy FiberFlex 40D] +inherits = *FLEX* +filament_vendor = Fiberlogy +fan_always_on = 1 +filament_max_volumetric_speed = 1.5 +extrusion_multiplier = 1.12 +first_layer_temperature = 230 +first_layer_bed_temperature = 60 +temperature = 230 +bed_temperature = 60 +bridge_fan_speed = 75 +min_fan_speed = 25 +max_fan_speed = 75 +filament_cost = 39.41 +filament_density = 1.16 +disable_fan_first_layers = 5 +full_fan_speed_layer = 5 +min_print_speed = 15 +cooling = 1 +filament_spool_weight = 330 + +[filament:Fiberlogy MattFlex 40D] +inherits = Fiberlogy FiberFlex 40D +filament_vendor = Fiberlogy +fan_always_on = 1 +filament_max_volumetric_speed = 1.35 +extrusion_multiplier = 1.1 +filament_cost = 49.11 + +[filament:Fiberlogy FiberFlex 30D] +inherits = Fiberlogy FiberFlex 40D +filament_max_volumetric_speed = 1.2 +extrusion_multiplier = 1.15 +first_layer_temperature = 240 +temperature = 240 +min_fan_speed = 25 +max_fan_speed = 60 +filament_density = 1.07 + +[filament:Fiberlogy FiberSatin] +inherits = Fiberlogy Easy PLA +first_layer_temperature = 215 +temperature = 215 +extrusion_multiplier = 1 +filament_density = 1.2 +filament_cost = 32.35 + +[filament:Fiberlogy FiberSilk] +inherits = Fiberlogy FiberSatin +first_layer_temperature = 230 +temperature = 230 +extrusion_multiplier = 0.97 +filament_density = 1.22 +filament_cost = 32.35 + +[filament:Fiberlogy FiberWood] +inherits = Fiberlogy Easy PLA +first_layer_temperature = 185 +temperature = 185 +extrusion_multiplier = 1 +filament_density = 1.23 +filament_cost = 38.66 + +[filament:Fiberlogy HD PLA] +inherits = Fiberlogy Easy PLA +first_layer_temperature = 230 +temperature = 230 +extrusion_multiplier = 1 +filament_density = 1.24 +filament_cost = 30.59 + +[filament:Fiberlogy PLA Mineral] +inherits = Fiberlogy Easy PLA +first_layer_temperature = 195 +temperature = 190 +extrusion_multiplier = 0.98 +filament_density = 1.38 +filament_cost = 37.64 + +[filament:Fiberlogy Impact PLA] +inherits = Fiberlogy HD PLA +filament_density = 1.22 +filament_cost = 27.65 + +[filament:Fiberlogy Nylon PA12] +inherits = Fiberlogy ASA +filament_type = PA +filament_density = 1.01 +filament_cost = 48 +first_layer_bed_temperature = 105 +bed_temperature = 105 +first_layer_temperature = 265 +temperature = 265 +min_fan_speed = 10 +max_fan_speed = 15 +fan_below_layer_time = 20 +bridge_fan_speed = 30 +fan_always_on = 0 + +[filament:Fiberlogy Nylon PA12+CF15] +inherits = Fiberlogy Nylon PA12 +extrusion_multiplier = 0.97 +filament_density = 1.07 +filament_cost = 87.5 +first_layer_bed_temperature = 105 +bed_temperature = 105 +first_layer_temperature = 265 +temperature = 265 +min_fan_speed = 10 +max_fan_speed = 15 +fan_below_layer_time = 20 +bridge_fan_speed = 30 +fan_always_on = 0 + +[filament:Fiberlogy Nylon PA12+GF15] +inherits = Fiberlogy Nylon PA12+CF15 +filament_density = 1.13 + +[filament:Fiberlogy PP] +inherits = *ABS* +filament_vendor = Fiberlogy +filament_cost = 36.67 +filament_density = 1.05 +extrusion_multiplier = 1.05 +filament_spool_weight = 330 +fan_always_on = 1 +cooling = 1 +min_fan_speed = 0 +max_fan_speed = 25 +bridge_fan_speed = 70 +min_print_speed = 15 +slowdown_below_layer_time = 15 +first_layer_temperature = 245 +temperature = 245 +first_layer_bed_temperature = 0 +bed_temperature = 0 +filament_type = PP +fan_below_layer_time = 100 +disable_fan_first_layers = 5 +filament_max_volumetric_speed = 5 + +[filament:Filament PM PLA] +inherits = *PLA* +filament_vendor = Filament PM +filament_cost = 27.82 +filament_density = 1.24 +filament_spool_weight = 230 + +[filament:AmazonBasics PLA] +inherits = *PLA* +filament_vendor = AmazonBasics +filament_cost = 25.4 +filament_density = 1.24 + +[filament:Overture PLA] +inherits = *PLA* +filament_vendor = Overture +filament_cost = 22 +filament_density = 1.24 +filament_spool_weight = 235 + +[filament:Hatchbox PLA] +inherits = *PLA* +filament_vendor = Hatchbox +filament_cost = 25.4 +filament_density = 1.27 +filament_spool_weight = 245 + +[filament:Esun PLA] +inherits = *PLA* +filament_vendor = Esun +filament_cost = 25.4 +filament_density = 1.24 +filament_spool_weight = 265 + +[filament:Das Filament PLA] +inherits = *PLA* +filament_vendor = Das Filament +filament_cost = 25.4 +filament_density = 1.24 + +[filament:EUMAKERS PLA] +inherits = *PLA* +filament_vendor = EUMAKERS +filament_cost = 25.4 +filament_density = 1.24 + +[filament:Floreon3D PLA] +inherits = *PLA* +filament_vendor = Floreon3D +filament_cost = 25.4 +filament_density = 1.24 + +[filament:Prusament PLA] +inherits = *PLA* +filament_vendor = Prusa Polymers +temperature = 215 +filament_cost = 36.29 +filament_density = 1.24 +filament_spool_weight = 201 +filament_notes = "Affordable filament for everyday printing in premium quality manufactured in-house by Josef Prusa" + +[filament:Prusament PVB] +inherits = *PLA* +filament_vendor = Prusa Polymers +temperature = 215 +bed_temperature = 75 +first_layer_bed_temperature = 75 +filament_cost = 60.48 +filament_density = 1.09 +filament_spool_weight = 201 +filament_max_volumetric_speed = 8 +filament_type = PVB +filament_soluble = 1 +filament_colour = #FFFF6F +slowdown_below_layer_time = 20 + +[filament:Fillamentum Flexfill 98A] +inherits = *FLEX* +filament_vendor = Fillamentum +filament_cost = 82.26 +filament_density = 1.23 +filament_spool_weight = 230 +extrusion_multiplier = 1.1 +filament_max_volumetric_speed = 1.5 +fan_always_on = 1 +cooling = 0 +max_fan_speed = 60 +min_fan_speed = 60 +disable_fan_first_layers = 4 +full_fan_speed_layer = 6 + +[filament:ColorFabb VarioShore TPU] +inherits = Fillamentum Flexfill 98A +filament_vendor = ColorFabb +filament_colour = #BBBBBB +filament_cost = 71.35 +filament_density = 1.22 +filament_spool_weight = 0 +extrusion_multiplier = 0.85 +first_layer_temperature = 220 +temperature = 220 + +[filament:Taulman Bridge] +inherits = *common* +filament_vendor = Taulman +filament_cost = 40 +filament_density = 1.13 +bed_temperature = 110 +bridge_fan_speed = 40 +cooling = 0 +disable_fan_first_layers = 3 +fan_always_on = 0 +fan_below_layer_time = 20 +filament_colour = #DEE0E6 +filament_soluble = 0 +filament_type = PA +first_layer_bed_temperature = 90 +first_layer_temperature = 260 +temperature = 260 +max_fan_speed = 0 +min_fan_speed = 0 +filament_max_volumetric_speed = 6 + +[filament:Fillamentum Nylon FX256] +inherits = *common* +filament_vendor = Fillamentum +filament_cost = 56.99 +filament_density = 1.01 +filament_spool_weight = 230 +bed_temperature = 90 +bridge_fan_speed = 30 +cooling = 1 +disable_fan_first_layers = 6 +fan_always_on = 0 +fan_below_layer_time = 20 +min_print_speed = 15 +slowdown_below_layer_time = 20 +filament_colour = #DEE0E6 +filament_max_volumetric_speed = 6 +filament_soluble = 0 +filament_type = PA +first_layer_bed_temperature = 90 +first_layer_temperature = 250 +max_fan_speed = 0 +min_fan_speed = 0 +temperature = 250 + +[filament:Fiberthree F3 PA Pure Pro] +inherits = *common* +filament_vendor = Fiberthree +filament_cost = 200.84 +filament_density = 1.2 +bed_temperature = 90 +first_layer_bed_temperature = 90 +first_layer_temperature = 285 +temperature = 285 +bridge_fan_speed = 30 +cooling = 1 +disable_fan_first_layers = 3 +fan_always_on = 1 +fan_below_layer_time = 20 +min_print_speed = 15 +slowdown_below_layer_time = 10 +filament_colour = #DEE0E6 +filament_max_volumetric_speed = 5 +filament_soluble = 0 +filament_type = PA +max_fan_speed = 20 +min_fan_speed = 20 + +[filament:Fiberthree F3 PA-CF Pro] +inherits = *common* +filament_vendor = Fiberthree +filament_cost = 208.1 +filament_density = 1.25 +bed_temperature = 90 +first_layer_bed_temperature = 90 +first_layer_temperature = 285 +temperature = 285 +bridge_fan_speed = 30 +cooling = 1 +disable_fan_first_layers = 3 +fan_always_on = 0 +fan_below_layer_time = 20 +min_print_speed = 15 +slowdown_below_layer_time = 10 +filament_colour = #DEE0E6 +filament_max_volumetric_speed = 5 +filament_soluble = 0 +filament_type = PA +max_fan_speed = 0 +min_fan_speed = 0 + +[filament:Fiberthree F3 PA-GF Pro] +inherits = Fiberthree F3 PA-CF Pro +filament_vendor = Fiberthree +filament_cost = 205.68 +filament_density = 1.27 +fan_always_on = 1 +max_fan_speed = 15 +min_fan_speed = 15 + +[filament:Taulman T-Glase] +inherits = *PET* +filament_vendor = Taulman +filament_cost = 40 +filament_density = 1.27 +bridge_fan_speed = 40 +cooling = 0 +fan_always_on = 0 +first_layer_bed_temperature = 90 +first_layer_temperature = 240 +max_fan_speed = 5 +min_fan_speed = 0 + +[filament:Verbatim PLA] +inherits = *PLA* +filament_vendor = Verbatim +filament_cost = 42.99 +filament_density = 1.24 +filament_spool_weight = 235 + +[filament:Verbatim BVOH] +inherits = *common* +filament_vendor = Verbatim +filament_cost = 193.58 +filament_density = 1.14 +filament_spool_weight = 235 +bed_temperature = 60 +bridge_fan_speed = 100 +cooling = 0 +disable_fan_first_layers = 1 +extrusion_multiplier = 1 +fan_always_on = 0 +fan_below_layer_time = 100 +filament_colour = #FFFFD7 +filament_soluble = 1 +filament_type = PVA +first_layer_bed_temperature = 60 +first_layer_temperature = 215 +max_fan_speed = 100 +min_fan_speed = 100 +temperature = 210 + +[filament:Verbatim PP] +inherits = *common* +filament_vendor = Verbatim +filament_cost = 72 +filament_density = 0.89 +filament_spool_weight = 235 +bed_temperature = 100 +bridge_fan_speed = 100 +cooling = 1 +disable_fan_first_layers = 2 +extrusion_multiplier = 1 +fan_always_on = 1 +fan_below_layer_time = 100 +filament_colour = #DEE0E6 +filament_max_volumetric_speed = 5 +filament_type = PP +first_layer_bed_temperature = 100 +first_layer_temperature = 220 +max_fan_speed = 100 +min_fan_speed = 100 +temperature = 220 + +[filament:FormFutura Centaur PP] +inherits = *common* +filament_vendor = FormFutura +filament_cost = 70 +filament_density = 0.89 +filament_spool_weight = 212 +bridge_fan_speed = 100 +cooling = 1 +disable_fan_first_layers = 2 +extrusion_multiplier = 1.05 +fan_always_on = 1 +fan_below_layer_time = 100 +filament_colour = #DEE0E6 +filament_max_volumetric_speed = 4 +filament_type = PP +first_layer_bed_temperature = 85 +bed_temperature = 85 +first_layer_temperature = 235 +max_fan_speed = 70 +min_fan_speed = 70 +temperature = 235 +filament_wipe = 0 +filament_retract_lift = 0 \ No newline at end of file diff --git a/resources/shaders/110/gouraud_light_clip.fs b/resources/shaders/110/gouraud_light_clip.fs new file mode 100644 index 000000000..5c7068709 --- /dev/null +++ b/resources/shaders/110/gouraud_light_clip.fs @@ -0,0 +1,17 @@ +#version 110 + +uniform vec4 uniform_color; +uniform float emission_factor; + +// x = tainted, y = specular; +varying vec2 intensity; + +varying float clipping_planes_dot; + +void main() +{ + if (clipping_planes_dot < 0.0) + discard; + + gl_FragColor = vec4(vec3(intensity.y) + uniform_color.rgb * (intensity.x + emission_factor), uniform_color.a); +} diff --git a/resources/shaders/110/gouraud_light_clip.vs b/resources/shaders/110/gouraud_light_clip.vs new file mode 100644 index 000000000..6d7c32e1b --- /dev/null +++ b/resources/shaders/110/gouraud_light_clip.vs @@ -0,0 +1,54 @@ +#version 110 + +#define INTENSITY_CORRECTION 0.6 + +// normalized values for (-0.6/1.31, 0.6/1.31, 1./1.31) +const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929); +#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SHININESS 20.0 + +// normalized values for (1./1.43, 0.2/1.43, 1./1.43) +const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074); +#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION) + +#define INTENSITY_AMBIENT 0.3 + +uniform mat4 view_model_matrix; +uniform mat4 projection_matrix; +uniform mat3 view_normal_matrix; +uniform mat4 volume_world_matrix; + +// Clipping plane - general orientation. Used by the SLA gizmo. +uniform vec4 clipping_plane; + +attribute vec3 v_position; +attribute vec3 v_normal; + +// x = tainted, y = specular; +varying vec2 intensity; + +varying float clipping_planes_dot; + +void main() +{ + // First transform the normal into camera space and normalize the result. + vec3 eye_normal = normalize(view_normal_matrix * v_normal); + + // Compute the cos of the angle between the normal and lights direction. The light is directional so the direction is constant for every vertex. + // Since these two are normalized the cosine is the dot product. We also need to clamp the result to the [0,1] range. + float NdotL = max(dot(eye_normal, LIGHT_TOP_DIR), 0.0); + + intensity.x = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE; + vec4 eye_position = view_model_matrix * vec4(v_position, 1.0); + intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(eye_position.xyz), reflect(-LIGHT_TOP_DIR, eye_normal)), 0.0), LIGHT_TOP_SHININESS); + + // Perform the same lighting calculation for the 2nd light source (no specular applied). + NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0); + intensity.x += NdotL * LIGHT_FRONT_DIFFUSE; + + gl_Position = projection_matrix * eye_position; + + // Fill in the scalar for fragment shader clipping. Fragments with this value lower than zero are discarded. + clipping_planes_dot = dot(volume_world_matrix * vec4(v_position, 1.0), clipping_plane); +} diff --git a/resources/shaders/140/gouraud_light_clip.fs b/resources/shaders/140/gouraud_light_clip.fs new file mode 100644 index 000000000..714e5bcaa --- /dev/null +++ b/resources/shaders/140/gouraud_light_clip.fs @@ -0,0 +1,19 @@ +#version 140 + +uniform vec4 uniform_color; +uniform float emission_factor; + +// x = tainted, y = specular; +in vec2 intensity; + +in float clipping_planes_dot; + +out vec4 out_color; + +void main() +{ + if (clipping_planes_dot < 0.0) + discard; + + out_color = vec4(vec3(intensity.y) + uniform_color.rgb * (intensity.x + emission_factor), uniform_color.a); +} diff --git a/resources/shaders/140/gouraud_light_clip.vs b/resources/shaders/140/gouraud_light_clip.vs new file mode 100644 index 000000000..8fca59380 --- /dev/null +++ b/resources/shaders/140/gouraud_light_clip.vs @@ -0,0 +1,54 @@ +#version 140 + +#define INTENSITY_CORRECTION 0.6 + +// normalized values for (-0.6/1.31, 0.6/1.31, 1./1.31) +const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929); +#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SHININESS 20.0 + +// normalized values for (1./1.43, 0.2/1.43, 1./1.43) +const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074); +#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION) + +#define INTENSITY_AMBIENT 0.3 + +uniform mat4 view_model_matrix; +uniform mat4 projection_matrix; +uniform mat3 view_normal_matrix; +uniform mat4 volume_world_matrix; + +// Clipping plane - general orientation. Used by the SLA gizmo. +uniform vec4 clipping_plane; + +in vec3 v_position; +in vec3 v_normal; + +// x = tainted, y = specular; +out vec2 intensity; + +out float clipping_planes_dot; + +void main() +{ + // First transform the normal into camera space and normalize the result. + vec3 eye_normal = normalize(view_normal_matrix * v_normal); + + // Compute the cos of the angle between the normal and lights direction. The light is directional so the direction is constant for every vertex. + // Since these two are normalized the cosine is the dot product. We also need to clamp the result to the [0,1] range. + float NdotL = max(dot(eye_normal, LIGHT_TOP_DIR), 0.0); + + intensity.x = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE; + vec4 eye_position = view_model_matrix * vec4(v_position, 1.0); + intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(eye_position.xyz), reflect(-LIGHT_TOP_DIR, eye_normal)), 0.0), LIGHT_TOP_SHININESS); + + // Perform the same lighting calculation for the 2nd light source (no specular applied). + NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0); + intensity.x += NdotL * LIGHT_FRONT_DIFFUSE; + + gl_Position = projection_matrix * eye_position; + + // Fill in the scalar for fragment shader clipping. Fragments with this value lower than zero are discarded. + clipping_planes_dot = dot(volume_world_matrix * vec4(v_position, 1.0), clipping_plane); +} diff --git a/resources/shaders/ES/gouraud_light_clip.fs b/resources/shaders/ES/gouraud_light_clip.fs new file mode 100644 index 000000000..45cae0ddb --- /dev/null +++ b/resources/shaders/ES/gouraud_light_clip.fs @@ -0,0 +1,19 @@ +#version 100 + +precision highp float; + +uniform vec4 uniform_color; +uniform float emission_factor; + +// x = tainted, y = specular; +varying vec2 intensity; + +varying float clipping_planes_dot; + +void main() +{ + if (clipping_planes_dot < 0.0) + discard; + + gl_FragColor = vec4(vec3(intensity.y) + uniform_color.rgb * (intensity.x + emission_factor), uniform_color.a); +} diff --git a/resources/shaders/ES/gouraud_light_clip.vs b/resources/shaders/ES/gouraud_light_clip.vs new file mode 100644 index 000000000..f3ab8c3dc --- /dev/null +++ b/resources/shaders/ES/gouraud_light_clip.vs @@ -0,0 +1,54 @@ +#version 100 + +#define INTENSITY_CORRECTION 0.6 + +// normalized values for (-0.6/1.31, 0.6/1.31, 1./1.31) +const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929); +#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SHININESS 20.0 + +// normalized values for (1./1.43, 0.2/1.43, 1./1.43) +const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074); +#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION) + +#define INTENSITY_AMBIENT 0.3 + +uniform mat4 view_model_matrix; +uniform mat4 projection_matrix; +uniform mat3 view_normal_matrix; +uniform mat4 volume_world_matrix; + +// Clipping plane - general orientation. Used by the SLA gizmo. +uniform vec4 clipping_plane; + +attribute vec3 v_position; +attribute vec3 v_normal; + +// x = tainted, y = specular; +varying vec2 intensity; + +varying float clipping_planes_dot; + +void main() +{ + // First transform the normal into camera space and normalize the result. + vec3 eye_normal = normalize(view_normal_matrix * v_normal); + + // Compute the cos of the angle between the normal and lights direction. The light is directional so the direction is constant for every vertex. + // Since these two are normalized the cosine is the dot product. We also need to clamp the result to the [0,1] range. + float NdotL = max(dot(eye_normal, LIGHT_TOP_DIR), 0.0); + + intensity.x = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE; + vec4 eye_position = view_model_matrix * vec4(v_position, 1.0); + intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(eye_position.xyz), reflect(-LIGHT_TOP_DIR, eye_normal)), 0.0), LIGHT_TOP_SHININESS); + + // Perform the same lighting calculation for the 2nd light source (no specular applied). + NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0); + intensity.x += NdotL * LIGHT_FRONT_DIFFUSE; + + gl_Position = projection_matrix * eye_position; + + // Fill in the scalar for fragment shader clipping. Fragments with this value lower than zero are discarded. + clipping_planes_dot = dot(volume_world_matrix * vec4(v_position, 1.0), clipping_plane); +} diff --git a/src/libslic3r/AABBMesh.cpp b/src/libslic3r/AABBMesh.cpp index 835df2ebe..ca7042c60 100644 --- a/src/libslic3r/AABBMesh.cpp +++ b/src/libslic3r/AABBMesh.cpp @@ -68,9 +68,6 @@ public: template void AABBMesh::init(const M &mesh, bool calculate_epsilon) { - BoundingBoxf3 bb = bounding_box(mesh); - m_ground_level += bb.min(Z); - // Build the AABB accelaration tree m_aabb->init(*m_tm, calculate_epsilon); } @@ -97,7 +94,6 @@ AABBMesh::~AABBMesh() {} AABBMesh::AABBMesh(const AABBMesh &other) : m_tm(other.m_tm) - , m_ground_level(other.m_ground_level) , m_aabb(new AABBImpl(*other.m_aabb)) , m_vfidx{other.m_vfidx} , m_fnidx{other.m_fnidx} @@ -106,7 +102,6 @@ AABBMesh::AABBMesh(const AABBMesh &other) AABBMesh &AABBMesh::operator=(const AABBMesh &other) { m_tm = other.m_tm; - m_ground_level = other.m_ground_level; m_aabb.reset(new AABBImpl(*other.m_aabb)); m_vfidx = other.m_vfidx; m_fnidx = other.m_fnidx; diff --git a/src/libslic3r/AABBMesh.hpp b/src/libslic3r/AABBMesh.hpp index 6a08c4303..3ef25977b 100644 --- a/src/libslic3r/AABBMesh.hpp +++ b/src/libslic3r/AABBMesh.hpp @@ -26,10 +26,9 @@ class TriangleMesh; // casting and other higher level operations. class AABBMesh { class AABBImpl; - + const indexed_triangle_set* m_tm; - double m_ground_level = 0/*, m_gnd_offset = 0*/; - + std::unique_ptr m_aabb; VertexFaceIndex m_vfidx; // vertex-face index std::vector m_fnidx; // face-neighbor index @@ -43,7 +42,7 @@ class AABBMesh { template void init(const M &mesh, bool calculate_epsilon); public: - + // calculate_epsilon ... calculate epsilon for triangle-ray intersection from an average triangle edge length. // If set to false, a default epsilon is used, which works for "reasonable" meshes. explicit AABBMesh(const indexed_triangle_set &tmesh, bool calculate_epsilon = false); @@ -51,30 +50,26 @@ public: AABBMesh(const AABBMesh& other); AABBMesh& operator=(const AABBMesh&); - + AABBMesh(AABBMesh &&other); AABBMesh& operator=(AABBMesh &&other); - + ~AABBMesh(); - - inline double ground_level() const { return m_ground_level /*+ m_gnd_offset*/; } -// inline void ground_level_offset(double o) { m_gnd_offset = o; } -// inline double ground_level_offset() const { return m_gnd_offset; } - + const std::vector& vertices() const; const std::vector& indices() const; const Vec3f& vertices(size_t idx) const; const Vec3i& indices(size_t idx) const; - + // Result of a raycast class hit_result { // m_t holds a distance from m_source to the intersection. double m_t = infty(); int m_face_id = -1; const AABBMesh *m_mesh = nullptr; - Vec3d m_dir; - Vec3d m_source; - Vec3d m_normal; + Vec3d m_dir = Vec3d::Zero(); + Vec3d m_source = Vec3d::Zero(); + Vec3d m_normal = Vec3d::Zero(); friend class AABBMesh; // A valid object of this class can only be obtained from diff --git a/src/libslic3r/AnyPtr.hpp b/src/libslic3r/AnyPtr.hpp new file mode 100644 index 000000000..c40d10093 --- /dev/null +++ b/src/libslic3r/AnyPtr.hpp @@ -0,0 +1,130 @@ +#ifndef ANYPTR_HPP +#define ANYPTR_HPP + +#include +#include +#include + +namespace Slic3r { + +// A general purpose pointer holder that can hold any type of smart pointer +// or raw pointer which can own or not own any object they point to. +// In case a raw pointer is stored, it is not destructed so ownership is +// assumed to be foreign. +// +// The stored pointer is not checked for being null when dereferenced. +// +// This is a movable only object due to the fact that it can possibly hold +// a unique_ptr which a non-copy. +template +class AnyPtr { + enum { RawPtr, UPtr, ShPtr, WkPtr }; + + boost::variant, std::shared_ptr, std::weak_ptr> ptr; + + template static T *get_ptr(Self &&s) + { + switch (s.ptr.which()) { + case RawPtr: return boost::get(s.ptr); + case UPtr: return boost::get>(s.ptr).get(); + case ShPtr: return boost::get>(s.ptr).get(); + case WkPtr: { + auto shptr = boost::get>(s.ptr).lock(); + return shptr.get(); + } + } + + return nullptr; + } + +public: + template>> + AnyPtr(TT *p = nullptr) : ptr{p} + {} + template>> + AnyPtr(std::unique_ptr p) : ptr{std::unique_ptr(std::move(p))} + {} + template>> + AnyPtr(std::shared_ptr p) : ptr{std::shared_ptr(std::move(p))} + {} + template>> + AnyPtr(std::weak_ptr p) : ptr{std::weak_ptr(std::move(p))} + {} + + ~AnyPtr() = default; + + AnyPtr(AnyPtr &&other) noexcept : ptr{std::move(other.ptr)} {} + AnyPtr(const AnyPtr &other) = delete; + + AnyPtr &operator=(AnyPtr &&other) noexcept { ptr = std::move(other.ptr); return *this; } + AnyPtr &operator=(const AnyPtr &other) = delete; + + template>> + AnyPtr &operator=(TT *p) { ptr = p; return *this; } + + template>> + AnyPtr &operator=(std::unique_ptr p) { ptr = std::move(p); return *this; } + + template>> + AnyPtr &operator=(std::shared_ptr p) { ptr = p; return *this; } + + template>> + AnyPtr &operator=(std::weak_ptr p) { ptr = std::move(p); return *this; } + + const T &operator*() const { return *get_ptr(*this); } + T &operator*() { return *get_ptr(*this); } + + T *operator->() { return get_ptr(*this); } + const T *operator->() const { return get_ptr(*this); } + + T *get() { return get_ptr(*this); } + const T *get() const { return get_ptr(*this); } + + operator bool() const + { + switch (ptr.which()) { + case RawPtr: return bool(boost::get(ptr)); + case UPtr: return bool(boost::get>(ptr)); + case ShPtr: return bool(boost::get>(ptr)); + case WkPtr: { + auto shptr = boost::get>(ptr).lock(); + return bool(shptr); + } + } + + return false; + } + + // If the stored pointer is a shared or weak pointer, returns a reference + // counted copy. Empty shared pointer is returned otherwise. + std::shared_ptr get_shared_cpy() const + { + std::shared_ptr ret; + + switch (ptr.which()) { + case ShPtr: ret = boost::get>(ptr); break; + case WkPtr: ret = boost::get>(ptr).lock(); break; + default: + ; + } + + return ret; + } + + // If the underlying pointer is unique, convert to shared pointer + void convert_unique_to_shared() + { + if (ptr.which() == UPtr) + ptr = std::shared_ptr{std::move(boost::get>(ptr))}; + } + + // Returns true if the data is owned by this AnyPtr instance + bool is_owned() const noexcept + { + return ptr.which() == UPtr || ptr.which() == ShPtr; + } +}; + +} // namespace Slic3r + +#endif // ANYPTR_HPP diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 8c879cde0..9c2fe7f15 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -34,8 +34,13 @@ static const std::string MODEL_PREFIX = "model:"; // to show this notification. On the other hand, we would like PrusaSlicer 2.3.2 to show an update notification of the upcoming PrusaSlicer 2.4.0. // Thus we will let PrusaSlicer 2.3.2 and couple of follow-up versions to download the version number from an alternate file until the PrusaSlicer 2.3.0/2.3.1 // are phased out, then we will revert to the original name. -//static const std::string VERSION_CHECK_URL = "https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/PrusaSlicer.version"; -static const std::string VERSION_CHECK_URL = "https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/PrusaSlicer.version2"; +// For 2.6.0-alpha1 we have switched back to the original. The file should contain data for AppUpdater.cpp +static const std::string VERSION_CHECK_URL = "https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/PrusaSlicer.version"; +//static const std::string VERSION_CHECK_URL = "https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/PrusaSlicer.version2"; +// Url to index archive zip that contains latest indicies +static const std::string INDEX_ARCHIVE_URL= "https://files.prusa3d.com/wp-content/uploads/repository/vendor_indices.zip"; +// Url to folder with vendor profile files. Used when downloading new profiles that are not in resources folder. +static const std::string PROFILE_FOLDER_URL = "https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/"; const std::string AppConfig::SECTION_FILAMENTS = "filaments"; const std::string AppConfig::SECTION_MATERIALS = "sla_materials"; @@ -68,6 +73,8 @@ void AppConfig::set_defaults() // If set, the "- default -" selections of print/filament/printer are suppressed, if there is a valid preset available. if (get("no_defaults").empty()) set("no_defaults", "1"); + if (get("no_templates").empty()) + set("no_templates", "0"); if (get("show_incompatible_presets").empty()) set("show_incompatible_presets", "0"); @@ -665,6 +672,26 @@ std::string AppConfig::version_check_url() const return from_settings.empty() ? VERSION_CHECK_URL : from_settings; } +std::string AppConfig::index_archive_url() const +{ +#if 0 + // this code is for debug & testing purposes only - changed url wont get trough inner checks anyway. + auto from_settings = get("index_archive_url"); + return from_settings.empty() ? INDEX_ARCHIVE_URL : from_settings; +#endif + return INDEX_ARCHIVE_URL; +} + +std::string AppConfig::profile_folder_url() const +{ +#if 0 + // this code is for debug & testing purposes only - changed url wont get trough inner checks anyway. + auto from_settings = get("profile_folder_url"); + return from_settings.empty() ? PROFILE_FOLDER_URL : from_settings; +#endif + return PROFILE_FOLDER_URL; +} + bool AppConfig::exists() { return boost::filesystem::exists(config_path()); diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index 5658f142d..78a7065d9 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -139,6 +139,11 @@ public: // Get the Slic3r version check url. // This returns a hardcoded string unless it is overriden by "version_check_url" in the ini file. std::string version_check_url() const; + // Get the Slic3r url to vendor index archive zip. + std::string index_archive_url() const; + // Get the Slic3r url to folder with vendor profile files. + std::string profile_folder_url() const; + // Returns the original Slic3r version found in the ini file before it was overwritten // by the current version diff --git a/src/libslic3r/BranchingTree/BranchingTree.cpp b/src/libslic3r/BranchingTree/BranchingTree.cpp index 6463a30de..98261311b 100644 --- a/src/libslic3r/BranchingTree/BranchingTree.cpp +++ b/src/libslic3r/BranchingTree/BranchingTree.cpp @@ -5,7 +5,7 @@ #include #include -#include "libslic3r/SLA/SupportTreeUtils.hpp" +#include "libslic3r/TriangleMesh.hpp" namespace Slic3r { namespace branchingtree { @@ -76,18 +76,20 @@ void build_tree(PointCloud &nodes, Builder &builder) switch (type) { case BED: { closest_node.weight = w; - if (closest_it->dst_branching > nodes.properties().max_branch_length()) { - auto hl_br_len = float(nodes.properties().max_branch_length()) / 2.f; - Node new_node {{node.pos.x(), node.pos.y(), node.pos.z() - hl_br_len}, node.Rmin}; - new_node.id = int(nodes.next_junction_id()); - new_node.weight = nodes.get(node_id).weight + hl_br_len; - new_node.left = node.id; + double max_br_len = nodes.properties().max_branch_length(); + if (closest_it->dst_branching > max_br_len) { + std::optional avo = builder.suggest_avoidance(node, max_br_len); + if (!avo) + break; + + Node new_node {*avo, node.Rmin}; + new_node.weight = nodes.get(node_id).weight + (node.pos - *avo).norm(); + new_node.left = node.id; if ((routed = builder.add_bridge(node, new_node))) { size_t new_idx = nodes.insert_junction(new_node); ptsqueue.push(new_idx); } - } - else if ((routed = builder.add_ground_bridge(node, closest_node))) { + } else if ((routed = builder.add_ground_bridge(node, closest_node))) { closest_node.left = closest_node.right = node_id; nodes.get(closest_node_id) = closest_node; nodes.mark_unreachable(closest_node_id); diff --git a/src/libslic3r/BranchingTree/BranchingTree.hpp b/src/libslic3r/BranchingTree/BranchingTree.hpp index c88410b3a..7e59e6f1a 100644 --- a/src/libslic3r/BranchingTree/BranchingTree.hpp +++ b/src/libslic3r/BranchingTree/BranchingTree.hpp @@ -5,7 +5,6 @@ #include #include "libslic3r/ExPolygon.hpp" -#include "libslic3r/BoundingBox.hpp" namespace Slic3r { namespace branchingtree { @@ -21,6 +20,7 @@ class Properties ExPolygons m_bed_shape; public: + // Maximum slope for bridges of the tree Properties &max_slope(double val) noexcept { @@ -77,6 +77,11 @@ struct Node {} }; +inline bool is_occupied(const Node &n) +{ + return n.left != Node::ID_NONE && n.right != Node::ID_NONE; +} + // An output interface for the branching tree generator function. Consider each // method as a callback and implement the actions that need to be done. class Builder @@ -100,6 +105,12 @@ public: // Add an anchor bridge to the model body virtual bool add_mesh_bridge(const Node &from, const Node &to) = 0; + virtual std::optional suggest_avoidance(const Node &from, + float max_bridge_len) const + { + return {}; + } + // Report nodes that can not be routed to an endpoint (model or ground) virtual void report_unroutable(const Node &j) = 0; diff --git a/src/libslic3r/BranchingTree/PointCloud.cpp b/src/libslic3r/BranchingTree/PointCloud.cpp index f1d7ae521..1497f0894 100644 --- a/src/libslic3r/BranchingTree/PointCloud.cpp +++ b/src/libslic3r/BranchingTree/PointCloud.cpp @@ -1,82 +1,15 @@ #include "PointCloud.hpp" -#include "libslic3r/Geometry.hpp" #include "libslic3r/Tesselate.hpp" +#include "libslic3r/SLA/SupportTreeUtils.hpp" #include namespace Slic3r { namespace branchingtree { -std::optional find_merge_pt(const Vec3f &A, - const Vec3f &B, - float critical_angle) +std::optional find_merge_pt(const Vec3f &A, const Vec3f &B, float max_slope) { - // The idea is that A and B both have their support cones. But searching - // for the intersection of these support cones is difficult and its enough - // to reduce this problem to 2D and search for the intersection of two - // rays that merge somewhere between A and B. The 2D plane is a vertical - // slice of the 3D scene where the 2D Y axis is equal to the 3D Z axis and - // the 2D X axis is determined by the XY direction of the AB vector. - // - // Z^ - // | A * - // | . . B * - // | . . . . - // | . . . . - // | . x . - // -------------------> XY - - // Determine the transformation matrix for the 2D projection: - Vec3f diff = {B.x() - A.x(), B.y() - A.y(), 0.f}; - Vec3f dir = diff.normalized(); // TODO: avoid normalization - - Eigen::Matrix tr2D; - tr2D.row(0) = Vec3f{dir.x(), dir.y(), dir.z()}; - tr2D.row(1) = Vec3f{0.f, 0.f, 1.f}; - - // Transform the 2 vectors A and B into 2D vector 'a' and 'b'. Here we can - // omit 'a', pretend that its the origin and use BA as the vector b. - Vec2f b = tr2D * (B - A); - - // Get the square sine of the ray emanating from 'a' towards 'b'. This ray might - // exceed the allowed angle but that is corrected subsequently. - // The sign of the original sine is also needed, hence b.y is multiplied by - // abs(b.y) - float b_sqn = b.squaredNorm(); - float sin2sig_a = b_sqn > EPSILON ? (b.y() * std::abs(b.y())) / b_sqn : 0.f; - - // sine2 from 'b' to 'a' is the opposite of sine2 from a to b - float sin2sig_b = -sin2sig_a; - - // Derive the allowed angles from the given critical angle. - // critical_angle is measured from the horizontal X axis. - // The rays need to go downwards which corresponds to negative angles - - float sincrit = std::sin(critical_angle); // sine of the critical angle - float sin2crit = -sincrit * sincrit; // signed sine squared - sin2sig_a = std::min(sin2sig_a, sin2crit); // Do the angle saturation of both rays - sin2sig_b = std::min(sin2sig_b, sin2crit); // - float sin2_a = std::abs(sin2sig_a); // Get cosine squared values - float sin2_b = std::abs(sin2sig_b); - float cos2_a = 1.f - sin2_a; - float cos2_b = 1.f - sin2_b; - - // Derive the new direction vectors. This is by square rooting the sin2 - // and cos2 values and restoring the original signs - Vec2f Da = {std::copysign(std::sqrt(cos2_a), b.x()), std::copysign(std::sqrt(sin2_a), sin2sig_a)}; - Vec2f Db = {-std::copysign(std::sqrt(cos2_b), b.x()), std::copysign(std::sqrt(sin2_b), sin2sig_b)}; - - // Determine where two rays ([0, 0], Da), (b, Db) intersect. - // Based on - // https://stackoverflow.com/questions/27459080/given-two-points-and-two-direction-vectors-find-the-point-where-they-intersect - // One ray is emanating from (0, 0) so the formula is simplified - double t1 = (Db.y() * b.x() - b.y() * Db.x()) / - (Da.x() * Db.y() - Da.y() * Db.x()); - - Vec2f mp = t1 * Da; - Vec3f Mp = A + tr2D.transpose() * mp; - - return t1 >= 0.f ? Mp : Vec3f{}; + return sla::find_merge_pt(A, B, max_slope); } void to_eigen_mesh(const indexed_triangle_set &its, @@ -141,8 +74,6 @@ std::vector sample_mesh(const indexed_triangle_set &its, double radius) std::vector sample_bed(const ExPolygons &bed, float z, double radius) { - std::vector ret; - auto triangles = triangulate_expolygons_3d(bed, z); indexed_triangle_set its; its.vertices.reserve(triangles.size()); @@ -198,7 +129,10 @@ PointCloud::PointCloud(std::vector meshpts, for (size_t i = 0; i < m_leafs.size(); ++i) { Node &n = m_leafs[i]; - n.id = int(LEAFS_BEGIN + i); + n.id = int(LEAFS_BEGIN + i); + n.left = Node::ID_NONE; + n.right = Node::ID_NONE; + m_ktree.insert({n.pos, n.id}); } } diff --git a/src/libslic3r/BranchingTree/PointCloud.hpp b/src/libslic3r/BranchingTree/PointCloud.hpp index f99b17990..03b935f76 100644 --- a/src/libslic3r/BranchingTree/PointCloud.hpp +++ b/src/libslic3r/BranchingTree/PointCloud.hpp @@ -5,7 +5,7 @@ #include "BranchingTree.hpp" -#include "libslic3r/Execution/Execution.hpp" +//#include "libslic3r/Execution/Execution.hpp" #include "libslic3r/MutablePriorityQueue.hpp" #include "libslic3r/BoostAdapter.hpp" @@ -78,14 +78,6 @@ private: rtree /* ? */> m_ktree; - bool is_outside_support_cone(const Vec3f &supp, const Vec3f &pt) const - { - Vec3d D = (pt - supp).cast(); - double dot_sq = -D.z() * std::abs(-D.z()); - - return dot_sq < D.squaredNorm() * cos2bridge_slope; - } - template static auto *get_node(PC &&pc, size_t id) { @@ -104,6 +96,14 @@ private: public: + bool is_outside_support_cone(const Vec3f &supp, const Vec3f &pt) const + { + Vec3d D = (pt - supp).cast(); + double dot_sq = -D.z() * std::abs(-D.z()); + + return dot_sq < D.squaredNorm() * cos2bridge_slope; + } + static constexpr auto Unqueued = size_t(-1); struct ZCompareFn @@ -255,18 +255,42 @@ public: } }; +template constexpr bool IsTraverseFn = std::is_invocable_v; + +struct TraverseReturnT +{ + bool to_left : 1; // if true, continue traversing to the left + bool to_right : 1; // if true, continue traversing to the right +}; + template void traverse(PC &&pc, size_t root, Fn &&fn) { if (auto nodeptr = pc.find(root); nodeptr != nullptr) { auto &nroot = *nodeptr; - fn(nroot); - if (nroot.left >= 0) traverse(pc, nroot.left, fn); - if (nroot.right >= 0) traverse(pc, nroot.right, fn); + TraverseReturnT ret{true, true}; + + if constexpr (std::is_same_v, void>) { + // Our fn has no return value + fn(nroot); + } else { + // Fn returns instructions about how to continue traversing + ret = fn(nroot); + } + + if (ret.to_left && nroot.left >= 0) + traverse(pc, nroot.left, fn); + if (ret.to_right && nroot.right >= 0) + traverse(pc, nroot.right, fn); } } void build_tree(PointCloud &pcloud, Builder &builder); +inline void build_tree(PointCloud &&pc, Builder &builder) +{ + build_tree(pc, builder); +} + }} // namespace Slic3r::branchingtree #endif // POINTCLOUD_HPP diff --git a/src/libslic3r/BridgeDetector.hpp b/src/libslic3r/BridgeDetector.hpp index b11736417..bc5da9712 100644 --- a/src/libslic3r/BridgeDetector.hpp +++ b/src/libslic3r/BridgeDetector.hpp @@ -82,12 +82,11 @@ inline std::tuple detect_bridging_direction(const Polygons &to_co if (floating_polylines.empty()) { // consider this area anchored from all sides, pick bridging direction that will likely yield shortest bridges - //use 3mm resolution (should be quite fast, and rough estimation should not cause any problems here) - auto [pc1, pc2] = compute_principal_components(overhang_area, 3.0); - if (pc2 == Vec2d::Zero()) { // overhang may be smaller than resolution. In this case, any direction is ok + auto [pc1, pc2] = compute_principal_components(overhang_area); + if (pc2 == Vec2f::Zero()) { // overhang may be smaller than resolution. In this case, any direction is ok return {Vec2d{1.0,0.0}, 0.0}; } else { - return {pc2.normalized(), 0.0}; + return {pc2.normalized().cast(), 0.0}; } } diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 6217a11df..09a7ed65d 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -11,7 +11,7 @@ endif () set(OpenVDBUtils_SOURCES "") if (TARGET OpenVDB::openvdb) - set(OpenVDBUtils_SOURCES OpenVDBUtils.cpp OpenVDBUtils.hpp) + set(OpenVDBUtils_SOURCES OpenVDBUtils.cpp OpenVDBUtils.hpp OpenVDBUtilsLegacy.hpp) endif() set(SLIC3R_SOURCES @@ -22,6 +22,7 @@ set(SLIC3R_SOURCES AABBTreeLines.hpp AABBMesh.hpp AABBMesh.cpp + AnyPtr.hpp BoundingBox.cpp BoundingBox.hpp BridgeDetector.cpp @@ -39,6 +40,13 @@ set(SLIC3R_SOURCES Color.hpp Config.cpp Config.hpp + CSGMesh/CSGMesh.hpp + CSGMesh/SliceCSGMesh.hpp + CSGMesh/ModelToCSGMesh.hpp + CSGMesh/PerformCSGMeshBooleans.hpp + CSGMesh/VoxelizeCSGMesh.hpp + CSGMesh/TriangleMeshAdapter.hpp + CSGMesh/CSGMeshCopy.hpp EdgeGrid.cpp EdgeGrid.hpp ElephantFootCompensation.cpp @@ -323,6 +331,7 @@ set(SLIC3R_SOURCES SLA/SupportTreeMesher.hpp SLA/SupportTreeMesher.cpp SLA/SupportTreeUtils.hpp + SLA/SupportTreeUtilsLegacy.hpp SLA/SupportTreeBuilder.cpp SLA/SupportTree.hpp SLA/SupportTree.cpp diff --git a/src/libslic3r/CSGMesh/CSGMesh.hpp b/src/libslic3r/CSGMesh/CSGMesh.hpp new file mode 100644 index 000000000..d14ed7659 --- /dev/null +++ b/src/libslic3r/CSGMesh/CSGMesh.hpp @@ -0,0 +1,86 @@ +#ifndef CSGMESH_HPP +#define CSGMESH_HPP + +#include +#include + +namespace Slic3r { namespace csg { + +// A CSGPartT should be an object that can provide at least a mesh + trafo and an +// associated csg operation. A collection of CSGPartT objects can then +// be interpreted as one model and used in various contexts. It can be assembled +// with CGAL or OpenVDB, rendered with OpenCSG or provided to a ray-tracer to +// deal with various parts of it according to the supported CSG types... +// +// A few simple templated interface functions are provided here and a default +// CSGPart class that implements the necessary means to be usable as a +// CSGPartT object. + +// Supported CSG operation types +enum class CSGType { Union, Difference, Intersection }; + +// A CSG part can instruct the processing to push the sub-result until a new +// csg part with a pop instruction appears. This can be used to implement +// parentheses in a CSG expression represented by the collection of csg parts. +// A CSG part can not contain another CSG collection, only a mesh, this is why +// its easier to do this stacking instead of recursion in the data definition. +// CSGStackOp::Continue means no stack operation required. +// When a CSG part contains a Push instruction, it is expected that the CSG +// operation it contains refers to the whole collection spanning to the nearest +// part with a Pop instruction. +// e.g.: +// { +// CUBE1: { mesh: cube, op: Union, stack op: Continue }, +// CUBE2: { mesh: cube, op: Difference, stack op: Push}, +// CUBE3: { mesh: cube, op: Union, stack op: Pop} +// } +// is a collection of csg parts representing the expression CUBE1 - (CUBE2 + CUBE3) +enum class CSGStackOp { Push, Continue, Pop }; + +// Get the CSG operation of the part. Can be overriden for any type +template CSGType get_operation(const CSGPartT &part) +{ + return part.operation; +} + +// Get the stack operation required by the CSG part. +template CSGStackOp get_stack_operation(const CSGPartT &part) +{ + return part.stack_operation; +} + +// Get the mesh for the part. Can be overriden for any type +template +const indexed_triangle_set *get_mesh(const CSGPartT &part) +{ + return part.its_ptr.get(); +} + +// Get the transformation associated with the mesh inside a CSGPartT object. +// Can be overriden for any type. +template +Transform3f get_transform(const CSGPartT &part) +{ + return part.trafo; +} + +// Default implementation +struct CSGPart { + AnyPtr its_ptr; + Transform3f trafo; + CSGType operation; + CSGStackOp stack_operation; + + CSGPart(AnyPtr ptr = {}, + CSGType op = CSGType::Union, + const Transform3f &tr = Transform3f::Identity()) + : its_ptr{std::move(ptr)} + , operation{op} + , stack_operation{CSGStackOp::Continue} + , trafo{tr} + {} +}; + +}} // namespace Slic3r::csg + +#endif // CSGMESH_HPP diff --git a/src/libslic3r/CSGMesh/CSGMeshCopy.hpp b/src/libslic3r/CSGMesh/CSGMeshCopy.hpp new file mode 100644 index 000000000..78800f9bb --- /dev/null +++ b/src/libslic3r/CSGMesh/CSGMeshCopy.hpp @@ -0,0 +1,80 @@ +#ifndef CSGMESHCOPY_HPP +#define CSGMESHCOPY_HPP + +#include "CSGMesh.hpp" + +namespace Slic3r { namespace csg { + +// Copy a csg range but for the meshes, only copy the pointers. If the copy +// is made from a CSGPart compatible object, and the pointer is a shared one, +// it will be copied with reference counting. +template +void copy_csgrange_shallow(const Range &csgrange, OutIt out) +{ + for (const auto &part : csgrange) { + CSGPart cpy{{}, + get_operation(part), + get_transform(part)}; + + cpy.stack_operation = get_stack_operation(part); + + if constexpr (std::is_convertible_v) { + if (auto shptr = part.its_ptr.get_shared_cpy()) { + cpy.its_ptr = shptr; + } + } + + if (!cpy.its_ptr) + cpy.its_ptr = AnyPtr{get_mesh(part)}; + + *out = std::move(cpy); + ++out; + } +} + +// Copy the csg range, allocating new meshes +template +void copy_csgrange_deep(const Range &csgrange, OutIt out) +{ + for (const auto &part : csgrange) { + + CSGPart cpy{{}, get_operation(part), get_transform(part)}; + + if (auto meshptr = get_mesh(part)) { + cpy.its_ptr = std::make_unique(*meshptr); + } + + cpy.stack_operation = get_stack_operation(part); + + *out = std::move(cpy); + ++out; + } +} + +template +bool is_same(const Range &A, const Range &B) +{ + bool ret = true; + + size_t s = A.size(); + + if (B.size() != s) + ret = false; + + size_t i = 0; + auto itA = A.begin(); + auto itB = B.begin(); + for (; ret && i < s; ++itA, ++itB, ++i) { + ret = ret && + get_mesh(*itA) == get_mesh(*itB) && + get_operation(*itA) == get_operation(*itB) && + get_stack_operation(*itA) == get_stack_operation(*itB) && + get_transform(*itA).isApprox(get_transform(*itB)); + } + + return ret; +} + +}} // namespace Slic3r::csg + +#endif // CSGCOPY_HPP diff --git a/src/libslic3r/CSGMesh/ModelToCSGMesh.hpp b/src/libslic3r/CSGMesh/ModelToCSGMesh.hpp new file mode 100644 index 000000000..9e413594e --- /dev/null +++ b/src/libslic3r/CSGMesh/ModelToCSGMesh.hpp @@ -0,0 +1,88 @@ +#ifndef MODELTOCSGMESH_HPP +#define MODELTOCSGMESH_HPP + +#include "CSGMesh.hpp" + +#include "libslic3r/Model.hpp" +#include "libslic3r/SLA/Hollowing.hpp" +#include "libslic3r/MeshSplitImpl.hpp" + +namespace Slic3r { namespace csg { + +// Flags to select which parts to export from Model into a csg part collection. +// These flags can be chained with the | operator +enum ModelParts { + mpartsPositive = 1, // Include positive parts + mpartsNegative = 2, // Include negative parts + mpartsDrillHoles = 4, // Include drill holes + mpartsDoSplits = 8, // Split each splitable mesh and export as a union of csg parts +}; + +template +void model_to_csgmesh(const ModelObject &mo, + const Transform3d &trafo, // Applies to all exported parts + OutIt out, // Output iterator + // values of ModelParts OR-ed + int parts_to_include = mpartsPositive + ) +{ + bool do_positives = parts_to_include & mpartsPositive; + bool do_negatives = parts_to_include & mpartsNegative; + bool do_drillholes = parts_to_include & mpartsDrillHoles; + bool do_splits = parts_to_include & mpartsDoSplits; + + for (const ModelVolume *vol : mo.volumes) { + if (vol && vol->mesh_ptr() && + ((do_positives && vol->is_model_part()) || + (do_negatives && vol->is_negative_volume()))) { + + if (do_splits && its_is_splittable(vol->mesh().its)) { + CSGPart part_begin{{}, vol->is_model_part() ? CSGType::Union : CSGType::Difference}; + part_begin.stack_operation = CSGStackOp::Push; + *out = std::move(part_begin); + ++out; + + its_split(vol->mesh().its, SplitOutputFn{[&out, &vol, &trafo](indexed_triangle_set &&its) { + if (its.empty()) + return; + + CSGPart part{std::make_unique(std::move(its)), + CSGType::Union, + (trafo * vol->get_matrix()).cast()}; + + *out = std::move(part); + ++out; + }}); + + CSGPart part_end{{}}; + part_end.stack_operation = CSGStackOp::Pop; + *out = std::move(part_end); + ++out; + } else { + CSGPart part{&(vol->mesh().its), + vol->is_model_part() ? CSGType::Union : CSGType::Difference, + (trafo * vol->get_matrix()).cast()}; + + *out = std::move(part); + ++out; + } + } + } + + if (do_drillholes) { + sla::DrainHoles drainholes = sla::transformed_drainhole_points(mo, trafo); + + for (const sla::DrainHole &dhole : drainholes) { + CSGPart part{std::make_unique( + dhole.to_mesh()), + CSGType::Difference}; + + *out = std::move(part); + ++out; + } + } +} + +}} // namespace Slic3r::csg + +#endif // MODELTOCSGMESH_HPP diff --git a/src/libslic3r/CSGMesh/PerformCSGMeshBooleans.hpp b/src/libslic3r/CSGMesh/PerformCSGMeshBooleans.hpp new file mode 100644 index 000000000..aabe9a2de --- /dev/null +++ b/src/libslic3r/CSGMesh/PerformCSGMeshBooleans.hpp @@ -0,0 +1,205 @@ +#ifndef PERFORMCSGMESHBOOLEANS_HPP +#define PERFORMCSGMESHBOOLEANS_HPP + +#include +#include + +#include "CSGMesh.hpp" + +#include "libslic3r/Execution/ExecutionTBB.hpp" +//#include "libslic3r/Execution/ExecutionSeq.hpp" +#include "libslic3r/MeshBoolean.hpp" + +namespace Slic3r { namespace csg { + +// This method can be overriden when a specific CSGPart type supports caching +// of the voxel grid +template +MeshBoolean::cgal::CGALMeshPtr get_cgalmesh(const CSGPartT &csgpart) +{ + const indexed_triangle_set *its = csg::get_mesh(csgpart); + indexed_triangle_set dummy; + + if (!its) + its = &dummy; + + MeshBoolean::cgal::CGALMeshPtr ret; + + indexed_triangle_set m = *its; + auto tr = get_transform(csgpart); + its_transform(m, get_transform(csgpart), true); + + try { + ret = MeshBoolean::cgal::triangle_mesh_to_cgal(m); + } catch (...) { + // errors are ignored, simply return null + ret = nullptr; + } + + return ret; +} + +namespace detail_cgal { + +using MeshBoolean::cgal::CGALMeshPtr; + +inline void perform_csg(CSGType op, CGALMeshPtr &dst, CGALMeshPtr &src) +{ + if (!dst && op == CSGType::Union && src) { + dst = std::move(src); + return; + } + + if (!dst || !src) + return; + + switch (op) { + case CSGType::Union: + MeshBoolean::cgal::plus(*dst, *src); + break; + case CSGType::Difference: + MeshBoolean::cgal::minus(*dst, *src); + break; + case CSGType::Intersection: + MeshBoolean::cgal::intersect(*dst, *src); + break; + } +} + +template +std::vector get_cgalptrs(Ex policy, const Range &csgrange) +{ + std::vector ret(csgrange.size()); + execution::for_each(policy, size_t(0), csgrange.size(), + [&csgrange, &ret](size_t i) { + auto it = csgrange.begin(); + std::advance(it, i); + auto &csgpart = *it; + ret[i] = get_cgalmesh(csgpart); + }); + + return ret; +} + +} // namespace detail + +// Process the sequence of CSG parts with CGAL. +template +void perform_csgmesh_booleans(MeshBoolean::cgal::CGALMeshPtr &cgalm, + const Range &csgrange) +{ + using MeshBoolean::cgal::CGALMesh; + using MeshBoolean::cgal::CGALMeshPtr; + using namespace detail_cgal; + + struct Frame { + CSGType op; CGALMeshPtr cgalptr; + explicit Frame(CSGType csgop = CSGType::Union) + : op{csgop} + , cgalptr{MeshBoolean::cgal::triangle_mesh_to_cgal(indexed_triangle_set{})} + {} + }; + + std::stack opstack{std::vector{}}; + + opstack.push(Frame{}); + + std::vector cgalmeshes = get_cgalptrs(ex_tbb, csgrange); + + size_t csgidx = 0; + for (auto &csgpart : csgrange) { + + auto op = get_operation(csgpart); + CGALMeshPtr &cgalptr = cgalmeshes[csgidx++]; + + if (get_stack_operation(csgpart) == CSGStackOp::Push) { + opstack.push(Frame{op}); + op = CSGType::Union; + } + + Frame *top = &opstack.top(); + + perform_csg(get_operation(csgpart), top->cgalptr, cgalptr); + + if (get_stack_operation(csgpart) == CSGStackOp::Pop) { + CGALMeshPtr src = std::move(top->cgalptr); + auto popop = opstack.top().op; + opstack.pop(); + CGALMeshPtr &dst = opstack.top().cgalptr; + perform_csg(popop, dst, src); + } + } + + cgalm = std::move(opstack.top().cgalptr); +} + +template +It check_csgmesh_booleans(const Range &csgrange, Visitor &&vfn) +{ + using namespace detail_cgal; + + std::vector cgalmeshes(csgrange.size()); + auto check_part = [&csgrange, &cgalmeshes](size_t i) + { + auto it = csgrange.begin(); + std::advance(it, i); + auto &csgpart = *it; + auto m = get_cgalmesh(csgpart); + + // mesh can be nullptr if this is a stack push or pull + if (!get_mesh(csgpart) && get_stack_operation(csgpart) != CSGStackOp::Continue) { + cgalmeshes[i] = MeshBoolean::cgal::triangle_mesh_to_cgal(indexed_triangle_set{}); + return; + } + + try { + if (!m || MeshBoolean::cgal::empty(*m)) + return; + + if (!MeshBoolean::cgal::does_bound_a_volume(*m)) + return; + + if (MeshBoolean::cgal::does_self_intersect(*m)) + return; + } + catch (...) { return; } + + cgalmeshes[i] = std::move(m); + }; + execution::for_each(ex_tbb, size_t(0), csgrange.size(), check_part); + + It ret = csgrange.end(); + for (size_t i = 0; i < csgrange.size(); ++i) { + if (!cgalmeshes[i]) { + auto it = csgrange.begin(); + std::advance(it, i); + vfn(it); + + if (ret == csgrange.end()) + ret = it; + } + } + + return ret; +} + +template +It check_csgmesh_booleans(const Range &csgrange) +{ + return check_csgmesh_booleans(csgrange, [](auto &) {}); +} + +template +MeshBoolean::cgal::CGALMeshPtr perform_csgmesh_booleans(const Range &csgparts) +{ + auto ret = MeshBoolean::cgal::triangle_mesh_to_cgal(indexed_triangle_set{}); + if (ret) + perform_csgmesh_booleans(ret, csgparts); + + return ret; +} + +} // namespace csg +} // namespace Slic3r + +#endif // PERFORMCSGMESHBOOLEANS_HPP diff --git a/src/libslic3r/CSGMesh/SliceCSGMesh.hpp b/src/libslic3r/CSGMesh/SliceCSGMesh.hpp new file mode 100644 index 000000000..9d7b9a077 --- /dev/null +++ b/src/libslic3r/CSGMesh/SliceCSGMesh.hpp @@ -0,0 +1,131 @@ +#ifndef SLICECSGMESH_HPP +#define SLICECSGMESH_HPP + +#include "CSGMesh.hpp" + +#include + +#include "libslic3r/TriangleMeshSlicer.hpp" +#include "libslic3r/ClipperUtils.hpp" +#include "libslic3r/Execution/ExecutionTBB.hpp" + +namespace Slic3r { namespace csg { + +namespace detail { + +inline void merge_slices(csg::CSGType op, size_t i, + std::vector &target, + std::vector &source) +{ + switch(op) { + case CSGType::Union: + for (ExPolygon &expoly : source[i]) + target[i].emplace_back(std::move(expoly)); + break; + case CSGType::Difference: + target[i] = diff_ex(target[i], source[i]); + break; + case CSGType::Intersection: + target[i] = intersection_ex(target[i], source[i]); + break; + } +} + +inline void collect_nonempty_indices(csg::CSGType op, + const std::vector &slicegrid, + const std::vector &slices, + std::vector &indices) +{ + indices.clear(); + for (size_t i = 0; i < slicegrid.size(); ++i) { + if (op == CSGType::Intersection || !slices[i].empty()) + indices.emplace_back(i); + } +} + +} // namespace detail + +template +std::vector slice_csgmesh_ex( + const Range &csgrange, + const std::vector &slicegrid, + const MeshSlicingParamsEx ¶ms, + const std::function &throw_on_cancel = [] {}) +{ + using namespace detail; + + struct Frame { CSGType op; std::vector slices; }; + + std::stack opstack{std::vector{}}; + + MeshSlicingParamsEx params_cpy = params; + auto trafo = params.trafo; + auto nonempty_indices = reserve_vector(slicegrid.size()); + + opstack.push({CSGType::Union, std::vector(slicegrid.size())}); + + for (const auto &csgpart : csgrange) { + const indexed_triangle_set *its = csg::get_mesh(csgpart); + + auto op = get_operation(csgpart); + + if (get_stack_operation(csgpart) == CSGStackOp::Push) { + opstack.push({op, std::vector(slicegrid.size())}); + op = CSGType::Union; + } + + Frame *top = &opstack.top(); + + if (its) { + params_cpy.trafo = trafo * csg::get_transform(csgpart).template cast(); + std::vector slices = slice_mesh_ex(*its, + slicegrid, params_cpy, + throw_on_cancel); + + assert(slices.size() == slicegrid.size()); + + collect_nonempty_indices(op, slicegrid, slices, nonempty_indices); + + execution::for_each( + ex_tbb, nonempty_indices.begin(), nonempty_indices.end(), + [op, &slices, &top](size_t i) { + merge_slices(op, i, top->slices, slices); + }, execution::max_concurrency(ex_tbb)); + } + + if (get_stack_operation(csgpart) == CSGStackOp::Pop) { + std::vector popslices = std::move(top->slices); + auto popop = opstack.top().op; + opstack.pop(); + std::vector &prev_slices = opstack.top().slices; + + collect_nonempty_indices(popop, slicegrid, popslices, nonempty_indices); + + execution::for_each( + ex_tbb, nonempty_indices.begin(), nonempty_indices.end(), + [&popslices, &prev_slices, popop](size_t i) { + merge_slices(popop, i, prev_slices, popslices); + }, execution::max_concurrency(ex_tbb)); + } + } + + std::vector ret = std::move(opstack.top().slices); + + // TODO: verify if this part can be omitted or not. + execution::for_each(ex_tbb, ret.begin(), ret.end(), [](ExPolygons &slice) { + auto it = std::remove_if(slice.begin(), slice.end(), [](const ExPolygon &p){ + return p.area() < double(SCALED_EPSILON) * double(SCALED_EPSILON); + }); + + // Hopefully, ExPolygons are moved, not copied to new positions + // and that is cheap for expolygons + slice.erase(it, slice.end()); + slice = union_ex(slice); + }, execution::max_concurrency(ex_tbb)); + + return ret; +} + +}} // namespace Slic3r::csg + +#endif // SLICECSGMESH_HPP diff --git a/src/libslic3r/CSGMesh/TriangleMeshAdapter.hpp b/src/libslic3r/CSGMesh/TriangleMeshAdapter.hpp new file mode 100644 index 000000000..81b05b046 --- /dev/null +++ b/src/libslic3r/CSGMesh/TriangleMeshAdapter.hpp @@ -0,0 +1,95 @@ +#ifndef TRIANGLEMESHADAPTER_HPP +#define TRIANGLEMESHADAPTER_HPP + +#include "CSGMesh.hpp" + +#include "libslic3r/TriangleMesh.hpp" + +namespace Slic3r { namespace csg { + +// Provide default overloads for indexed_triangle_set to be usable as a plain +// CSGPart with an implicit union operation + +inline CSGType get_operation(const indexed_triangle_set &part) +{ + return CSGType::Union; +} + +inline CSGStackOp get_stack_operation(const indexed_triangle_set &part) +{ + return CSGStackOp::Continue; +} + +inline const indexed_triangle_set * get_mesh(const indexed_triangle_set &part) +{ + return ∂ +} + +inline Transform3f get_transform(const indexed_triangle_set &part) +{ + return Transform3f::Identity(); +} + +inline CSGType get_operation(const indexed_triangle_set *const part) +{ + return CSGType::Union; +} + +inline CSGStackOp get_stack_operation(const indexed_triangle_set *const part) +{ + return CSGStackOp::Continue; +} + +inline const indexed_triangle_set * get_mesh(const indexed_triangle_set *const part) +{ + return part; +} + +inline Transform3f get_transform(const indexed_triangle_set *const part) +{ + return Transform3f::Identity(); +} + +inline CSGType get_operation(const TriangleMesh &part) +{ + return CSGType::Union; +} + +inline CSGStackOp get_stack_operation(const TriangleMesh &part) +{ + return CSGStackOp::Continue; +} + +inline const indexed_triangle_set * get_mesh(const TriangleMesh &part) +{ + return &part.its; +} + +inline Transform3f get_transform(const TriangleMesh &part) +{ + return Transform3f::Identity(); +} + +inline CSGType get_operation(const TriangleMesh * const part) +{ + return CSGType::Union; +} + +inline CSGStackOp get_stack_operation(const TriangleMesh * const part) +{ + return CSGStackOp::Continue; +} + +inline const indexed_triangle_set * get_mesh(const TriangleMesh * const part) +{ + return &part->its; +} + +inline Transform3f get_transform(const TriangleMesh * const part) +{ + return Transform3f::Identity(); +} + +}} // namespace Slic3r::csg + +#endif // TRIANGLEMESHADAPTER_HPP diff --git a/src/libslic3r/CSGMesh/VoxelizeCSGMesh.hpp b/src/libslic3r/CSGMesh/VoxelizeCSGMesh.hpp new file mode 100644 index 000000000..f64d17b9a --- /dev/null +++ b/src/libslic3r/CSGMesh/VoxelizeCSGMesh.hpp @@ -0,0 +1,116 @@ +#ifndef VOXELIZECSGMESH_HPP +#define VOXELIZECSGMESH_HPP + +#include +#include + +#include "CSGMesh.hpp" +#include "libslic3r/OpenVDBUtils.hpp" +#include "libslic3r/Execution/ExecutionTBB.hpp" + +namespace Slic3r { namespace csg { + +using VoxelizeParams = MeshToGridParams; + +// This method can be overriden when a specific CSGPart type supports caching +// of the voxel grid +template +VoxelGridPtr get_voxelgrid(const CSGPartT &csgpart, VoxelizeParams params) +{ + const indexed_triangle_set *its = csg::get_mesh(csgpart); + VoxelGridPtr ret; + + params.trafo(params.trafo() * csg::get_transform(csgpart)); + + if (its) + ret = mesh_to_grid(*its, params); + + return ret; +} + +namespace detail { + +inline void perform_csg(CSGType op, VoxelGridPtr &dst, VoxelGridPtr &src) +{ + if (!dst || !src) + return; + + switch (op) { + case CSGType::Union: + if (is_grid_empty(*dst) && !is_grid_empty(*src)) + dst = clone(*src); + else + grid_union(*dst, *src); + + break; + case CSGType::Difference: + grid_difference(*dst, *src); + break; + case CSGType::Intersection: + grid_intersection(*dst, *src); + break; + } +} + +} // namespace detail + +template +VoxelGridPtr voxelize_csgmesh(const Range &csgrange, + const VoxelizeParams ¶ms = {}) +{ + using namespace detail; + + VoxelGridPtr ret; + + std::vector grids (csgrange.size()); + + execution::for_each(ex_tbb, size_t(0), csgrange.size(), [&](size_t csgidx) { + if (params.statusfn() && params.statusfn()(-1)) + return; + + auto it = csgrange.begin(); + std::advance(it, csgidx); + auto &csgpart = *it; + grids[csgidx] = get_voxelgrid(csgpart, params); + }, execution::max_concurrency(ex_tbb)); + + size_t csgidx = 0; + struct Frame { CSGType op = CSGType::Union; VoxelGridPtr grid; }; + std::stack opstack{std::vector{}}; + + opstack.push({CSGType::Union, mesh_to_grid({}, params)}); + + for (auto &csgpart : csgrange) { + if (params.statusfn() && params.statusfn()(-1)) + break; + + auto &partgrid = grids[csgidx++]; + + auto op = get_operation(csgpart); + + if (get_stack_operation(csgpart) == CSGStackOp::Push) { + opstack.push({op, mesh_to_grid({}, params)}); + op = CSGType::Union; + } + + Frame *top = &opstack.top(); + + perform_csg(get_operation(csgpart), top->grid, partgrid); + + if (get_stack_operation(csgpart) == CSGStackOp::Pop) { + VoxelGridPtr popgrid = std::move(top->grid); + auto popop = opstack.top().op; + opstack.pop(); + VoxelGridPtr &grid = opstack.top().grid; + perform_csg(popop, grid, popgrid); + } + } + + ret = std::move(opstack.top().grid); + + return ret; +} + +}} // namespace Slic3r::csg + +#endif // VOXELIZECSGMESH_HPP diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index ef3dc32c8..23be26ee7 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -776,7 +776,7 @@ public: static int nil_value() { return std::numeric_limits::max(); } // A scalar is nil, or all values of a vector are nil. bool is_nil() const override { for (auto v : this->values) if (v != nil_value()) return false; return true; } - bool is_nil(size_t idx) const override { return this->values[idx] == nil_value(); } + bool is_nil(size_t idx) const override { return values[idx < this->values.size() ? idx : 0] == nil_value(); } std::string serialize() const override { diff --git a/src/libslic3r/ExPolygon.hpp b/src/libslic3r/ExPolygon.hpp index 98ad9e2e6..83b264803 100644 --- a/src/libslic3r/ExPolygon.hpp +++ b/src/libslic3r/ExPolygon.hpp @@ -32,8 +32,8 @@ public: ExPolygon& operator=(const ExPolygon &other) = default; ExPolygon& operator=(ExPolygon &&other) = default; - Polygon contour; - Polygons holes; + Polygon contour; //CCW + Polygons holes; //CW void clear() { contour.points.clear(); holes.clear(); } void scale(double factor); diff --git a/src/libslic3r/Execution/ExecutionTBB.hpp b/src/libslic3r/Execution/ExecutionTBB.hpp index 2250b8e32..db4ee243f 100644 --- a/src/libslic3r/Execution/ExecutionTBB.hpp +++ b/src/libslic3r/Execution/ExecutionTBB.hpp @@ -53,7 +53,7 @@ public: I to, const T &init, MergeFn &&mergefn, - AccessFn &&access, + AccessFn &&accessfn, size_t granularity = 1 ) { @@ -61,7 +61,7 @@ public: tbb::blocked_range{from, to, granularity}, init, [&](const auto &range, T subinit) { T acc = subinit; - loop_(range, [&](auto &i) { acc = mergefn(acc, access(i)); }); + loop_(range, [&](auto &i) { acc = mergefn(acc, accessfn(i)); }); return acc; }, std::forward(mergefn)); diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index e0e3bdc5d..082cf93cc 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -110,6 +110,11 @@ struct SurfaceFill { SurfaceFillParams params; }; +static inline bool fill_type_monotonic(InfillPattern pattern) +{ + return pattern == ipMonotonic || pattern == ipMonotonicLines; +} + std::vector group_fills(const Layer &layer) { std::vector surface_fills; @@ -138,7 +143,7 @@ std::vector group_fills(const Layer &layer) //FIXME for non-thick bridges, shall we allow a bottom surface pattern? params.pattern = (surface.is_external() && ! is_bridge) ? (surface.is_top() ? region_config.top_fill_pattern.value : region_config.bottom_fill_pattern.value) : - region_config.top_fill_pattern == ipMonotonic ? ipMonotonic : ipRectilinear; + fill_type_monotonic(region_config.top_fill_pattern) ? ipMonotonic : ipRectilinear; } else if (params.density <= 0) continue; @@ -279,7 +284,7 @@ std::vector group_fills(const Layer &layer) if (internal_solid_fill == nullptr) { // Produce another solid fill. params.extruder = layerm.region().extruder(frSolidInfill); - params.pattern = layerm.region().config().top_fill_pattern == ipMonotonic ? ipMonotonic : ipRectilinear; + params.pattern = fill_type_monotonic(layerm.region().config().top_fill_pattern) ? ipMonotonic : ipRectilinear; params.density = 100.f; params.extrusion_role = ExtrusionRole::InternalInfill; params.angle = float(Geometry::deg2rad(layerm.region().config().fill_angle.value)); diff --git a/src/libslic3r/Fill/FillBase.cpp b/src/libslic3r/Fill/FillBase.cpp index b38c5c9f8..6a0f41afb 100644 --- a/src/libslic3r/Fill/FillBase.cpp +++ b/src/libslic3r/Fill/FillBase.cpp @@ -37,6 +37,7 @@ Fill* Fill::new_from_type(const InfillPattern type) case ipRectilinear: return new FillRectilinear(); case ipAlignedRectilinear: return new FillAlignedRectilinear(); case ipMonotonic: return new FillMonotonic(); + case ipMonotonicLines: return new FillMonotonicLines(); case ipLine: return new FillLine(); case ipGrid: return new FillGrid(); case ipTriangles: return new FillTriangles(); diff --git a/src/libslic3r/Fill/FillRectilinear.cpp b/src/libslic3r/Fill/FillRectilinear.cpp index bb93d824b..e27017b03 100644 --- a/src/libslic3r/Fill/FillRectilinear.cpp +++ b/src/libslic3r/Fill/FillRectilinear.cpp @@ -2970,7 +2970,18 @@ Polylines FillMonotonic::fill_surface(const Surface *surface, const FillParams & params2.monotonic = true; Polylines polylines_out; if (! fill_surface_by_lines(surface, params2, 0.f, 0.f, polylines_out)) - BOOST_LOG_TRIVIAL(error) << "FillMonotonous::fill_surface() failed to fill a region."; + BOOST_LOG_TRIVIAL(error) << "FillMonotonic::fill_surface() failed to fill a region."; + return polylines_out; +} + +Polylines FillMonotonicLines::fill_surface(const Surface *surface, const FillParams ¶ms) +{ + FillParams params2 = params; + params2.monotonic = true; + params2.anchor_length_max = 0.0f; + Polylines polylines_out; + if (! fill_surface_by_lines(surface, params2, 0.f, 0.f, polylines_out)) + BOOST_LOG_TRIVIAL(error) << "FillMonotonicLines::fill_surface() failed to fill a region."; return polylines_out; } diff --git a/src/libslic3r/Fill/FillRectilinear.hpp b/src/libslic3r/Fill/FillRectilinear.hpp index 0a6c976ad..f5228440d 100644 --- a/src/libslic3r/Fill/FillRectilinear.hpp +++ b/src/libslic3r/Fill/FillRectilinear.hpp @@ -49,6 +49,15 @@ public: bool no_sort() const override { return true; } }; +class FillMonotonicLines : public FillRectilinear +{ +public: + Fill* clone() const override { return new FillMonotonicLines(*this); } + ~FillMonotonicLines() override = default; + Polylines fill_surface(const Surface *surface, const FillParams ¶ms) override; + bool no_sort() const override { return true; } +}; + class FillGrid : public FillRectilinear { public: diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index ad0d3c43f..ffffd9d31 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -113,26 +113,19 @@ namespace Slic3r { { std::string gcode; - // move to the nearest standby point - if (!this->standby_points.empty()) { - // get current position in print coordinates - Vec3d writer_pos = gcodegen.writer().get_position(); - Point pos = Point::new_scale(writer_pos(0), writer_pos(1)); - - // find standby point - Point standby_point = nearest_point(this->standby_points, pos).first; - - /* We don't call gcodegen.travel_to() because we don't need retraction (it was already - triggered by the caller) nor avoid_crossing_perimeters and also because the coordinates - of the destination point must not be transformed by origin nor current extruder offset. */ - gcode += gcodegen.writer().travel_to_xy(unscale(standby_point), - "move to standby position"); - } - - if (gcodegen.config().standby_temperature_delta.value != 0) { - // we assume that heating is always slower than cooling, so no need to block - gcode += gcodegen.writer().set_temperature - (this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value, false, gcodegen.writer().extruder()->id()); + unsigned int extruder_id = gcodegen.writer().extruder()->id(); + const ConfigOptionIntsNullable& filament_idle_temp = gcodegen.config().idle_temperature; + if (filament_idle_temp.is_nil(extruder_id)) { + // There is no idle temperature defined in filament settings. + // Use the delta value from print config. + if (gcodegen.config().standby_temperature_delta.value != 0) { + // we assume that heating is always slower than cooling, so no need to block + gcode += gcodegen.writer().set_temperature + (this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value, false, extruder_id); + } + } else { + // Use the value from filament settings. That one is absolute, not delta. + gcode += gcodegen.writer().set_temperature(filament_idle_temp.get_at(extruder_id), false, extruder_id); } return gcode; @@ -145,8 +138,7 @@ namespace Slic3r { std::string(); } - int - OozePrevention::_get_temp(GCode& gcodegen) + int OozePrevention::_get_temp(const GCode& gcodegen) const { return (gcodegen.layer() != nullptr && gcodegen.layer()->id() == 0) ? gcodegen.config().first_layer_temperature.get_at(gcodegen.writer().extruder()->id()) @@ -238,8 +230,16 @@ namespace Slic3r { std::string tcr_rotated_gcode = post_process_wipe_tower_moves(tcr, wipe_tower_offset, wipe_tower_rotation); - if (! tcr.priming) { - // Move over the wipe tower. + double current_z = gcodegen.writer().get_position().z(); + if (z == -1.) // in case no specific z was provided, print at current_z pos + z = current_z; + + const bool needs_toolchange = gcodegen.writer().need_toolchange(new_extruder_id); + const bool will_go_down = ! is_approx(z, current_z); + + if (! needs_toolchange || (gcodegen.config().single_extruder_multi_material && ! tcr.priming)) { + // Move over the wipe tower. If this is not single-extruder MM, the first wipe tower move following the + // toolchange will travel there anyway (if there is a toolchange). gcode += gcodegen.retract(); gcodegen.m_avoid_crossing_perimeters.use_external_mp_once(); gcode += gcodegen.travel_to( @@ -248,82 +248,36 @@ namespace Slic3r { "Travel to a Wipe Tower"); gcode += gcodegen.unretract(); } - - double current_z = gcodegen.writer().get_position().z(); - if (z == -1.) // in case no specific z was provided, print at current_z pos - z = current_z; - if (! is_approx(z, current_z)) { + + if (will_go_down) { gcode += gcodegen.writer().retract(); gcode += gcodegen.writer().travel_to_z(z, "Travel down to the last wipe tower layer."); gcode += gcodegen.writer().unretract(); } - - // Process the end filament gcode. - std::string end_filament_gcode_str; - if (gcodegen.writer().extruder() != nullptr) { - // Process the custom end_filament_gcode in case of single_extruder_multi_material. - unsigned int old_extruder_id = gcodegen.writer().extruder()->id(); - const std::string& end_filament_gcode = gcodegen.config().end_filament_gcode.get_at(old_extruder_id); - if (gcodegen.writer().extruder() != nullptr && !end_filament_gcode.empty()) { - end_filament_gcode_str = gcodegen.placeholder_parser_process("end_filament_gcode", end_filament_gcode, old_extruder_id); - check_add_eol(end_filament_gcode_str); - } - } - - // Process the custom toolchange_gcode. If it is empty, provide a simple Tn command to change the filament. - // Otherwise, leave control to the user completely. std::string toolchange_gcode_str; - const std::string& toolchange_gcode = gcodegen.config().toolchange_gcode.value; - if (! toolchange_gcode.empty()) { - DynamicConfig config; - int previous_extruder_id = gcodegen.writer().extruder() ? (int)gcodegen.writer().extruder()->id() : -1; - config.set_key_value("previous_extruder", new ConfigOptionInt(previous_extruder_id)); - config.set_key_value("next_extruder", new ConfigOptionInt((int)new_extruder_id)); - config.set_key_value("layer_num", new ConfigOptionInt(gcodegen.m_layer_index)); - config.set_key_value("layer_z", new ConfigOptionFloat(tcr.print_z)); - config.set_key_value("toolchange_z", new ConfigOptionFloat(z)); - config.set_key_value("max_layer_z", new ConfigOptionFloat(gcodegen.m_max_layer_z)); - toolchange_gcode_str = gcodegen.placeholder_parser_process("toolchange_gcode", toolchange_gcode, new_extruder_id, &config); - check_add_eol(toolchange_gcode_str); + std::string deretraction_str; + if (tcr.priming || (new_extruder_id >= 0 && needs_toolchange)) { + if (gcodegen.config().single_extruder_multi_material) + gcodegen.m_wipe.reset_path(); // We don't want wiping on the ramming lines. + toolchange_gcode_str = gcodegen.set_extruder(new_extruder_id, tcr.print_z); // TODO: toolchange_z vs print_z + if (gcodegen.config().wipe_tower) + deretraction_str = gcodegen.unretract(); } - std::string toolchange_command; - if (tcr.priming || (new_extruder_id >= 0 && gcodegen.writer().need_toolchange(new_extruder_id))) - toolchange_command = gcodegen.writer().toolchange(new_extruder_id); - if (!custom_gcode_changes_tool(toolchange_gcode_str, gcodegen.writer().toolchange_prefix(), new_extruder_id)) - toolchange_gcode_str += toolchange_command; - else { - // We have informed the m_writer about the current extruder_id, we can ignore the generated G-code. - } - gcodegen.placeholder_parser().set("current_extruder", new_extruder_id); + - // Process the start filament gcode. - std::string start_filament_gcode_str; - const std::string& start_filament_gcode = gcodegen.config().start_filament_gcode.get_at(new_extruder_id); - if (!start_filament_gcode.empty()) { - // Process the start_filament_gcode for the active filament only. - DynamicConfig config; - config.set_key_value("layer_num", new ConfigOptionInt(gcodegen.m_layer_index)); - config.set_key_value("layer_z", new ConfigOptionFloat(gcodegen.writer().get_position()(2) - gcodegen.m_config.z_offset.value)); - config.set_key_value("max_layer_z", new ConfigOptionFloat(gcodegen.m_max_layer_z)); - config.set_key_value("filament_extruder_id", new ConfigOptionInt(new_extruder_id)); - start_filament_gcode_str = gcodegen.placeholder_parser_process("start_filament_gcode", start_filament_gcode, new_extruder_id, &config); - check_add_eol(start_filament_gcode_str); - } - // Insert the end filament, toolchange, and start filament gcode into the generated gcode. + // Insert the toolchange and deretraction gcode into the generated gcode. DynamicConfig config; - config.set_key_value("end_filament_gcode", new ConfigOptionString(end_filament_gcode_str)); config.set_key_value("toolchange_gcode", new ConfigOptionString(toolchange_gcode_str)); - config.set_key_value("start_filament_gcode", new ConfigOptionString(start_filament_gcode_str)); + config.set_key_value("deretraction_from_wipe_tower_generator", new ConfigOptionString(deretraction_str)); std::string tcr_gcode, tcr_escaped_gcode = gcodegen.placeholder_parser_process("tcr_rotated_gcode", tcr_rotated_gcode, new_extruder_id, &config); unescape_string_cstyle(tcr_escaped_gcode, tcr_gcode); gcode += tcr_gcode; check_add_eol(toolchange_gcode_str); - // A phony move to the end position at the wipe tower. gcodegen.writer().travel_to_xy(end_pos.cast()); gcodegen.set_last_pos(wipe_tower_point_to_object_point(gcodegen, end_pos)); @@ -898,34 +852,7 @@ namespace DoExport { static void init_ooze_prevention(const Print &print, OozePrevention &ooze_prevention) { - // Calculate wiping points if needed - if (print.config().ooze_prevention.value && ! print.config().single_extruder_multi_material) { - Points skirt_points; - for (const ExtrusionEntity *ee : print.skirt().entities) - for (const ExtrusionPath &path : dynamic_cast(ee)->paths) - append(skirt_points, path.polyline.points); - if (! skirt_points.empty()) { - Polygon outer_skirt = Slic3r::Geometry::convex_hull(skirt_points); - Polygons skirts; - for (unsigned int extruder_id : print.extruders()) { - const Vec2d &extruder_offset = print.config().extruder_offset.get_at(extruder_id); - Polygon s(outer_skirt); - s.translate(Point::new_scale(-extruder_offset(0), -extruder_offset(1))); - skirts.emplace_back(std::move(s)); - } - ooze_prevention.enable = true; - ooze_prevention.standby_points = offset(Slic3r::Geometry::convex_hull(skirts), float(scale_(3.))).front().equally_spaced_points(float(scale_(10.))); - #if 0 - require "Slic3r/SVG.pm"; - Slic3r::SVG::output( - "ooze_prevention.svg", - red_polygons => \@skirts, - polygons => [$outer_skirt], - points => $gcodegen->ooze_prevention->standby_points, - ); - #endif - } - } + ooze_prevention.enable = print.config().ooze_prevention.value && ! print.config().single_extruder_multi_material; } // Fill in print_statistics and return formatted string containing filament statistics to be inserted into G-code comment section. @@ -1263,6 +1190,11 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_placeholder_parser.set("first_layer_print_min", new ConfigOptionFloats({ bbox.min.x(), bbox.min.y() })); m_placeholder_parser.set("first_layer_print_max", new ConfigOptionFloats({ bbox.max.x(), bbox.max.y() })); m_placeholder_parser.set("first_layer_print_size", new ConfigOptionFloats({ bbox.size().x(), bbox.size().y() })); + + std::vector is_extruder_used(print.config().nozzle_diameter.size(), 0); + for (unsigned int extruder_id : print.extruders()) + is_extruder_used[extruder_id] = true; + m_placeholder_parser.set("is_extruder_used", new ConfigOptionBools(is_extruder_used)); } std::string start_gcode = this->placeholder_parser_process("start_gcode", print.config().start_gcode.value, initial_extruder_id); // Set bed temperature if the start G-code does not contain any bed temp control G-codes. @@ -1282,8 +1214,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Set other general things. file.write(this->preamble()); - // Calculate wiping points if needed + // Enable ooze prevention if configured so. DoExport::init_ooze_prevention(print, m_ooze_prevention); + print.throw_if_canceled(); // Collect custom seam data from all objects. @@ -1814,7 +1747,7 @@ void GCode::_print_first_layer_extruder_temperatures(GCodeOutputStream &file, Pr m_writer.set_temperature(temp, wait, first_printing_extruder_id); } else { // Custom G-code does not set the extruder temperature. Do it now. - if (print.config().single_extruder_multi_material.value) { + if (print.config().single_extruder_multi_material.value || m_ooze_prevention.enable) { // Set temperature of the first printing extruder only. int temp = print.config().first_layer_temperature.get_at(first_printing_extruder_id); if (temp > 0) @@ -2136,11 +2069,14 @@ LayerResult GCode::process_layer( // Transition from 1st to 2nd layer. Adjust nozzle temperatures as prescribed by the nozzle dependent // first_layer_temperature vs. temperature settings. for (const Extruder &extruder : m_writer.extruders()) { - if (print.config().single_extruder_multi_material.value && extruder.id() != m_writer.extruder()->id()) + if (print.config().single_extruder_multi_material.value || m_ooze_prevention.enable) { // In single extruder multi material mode, set the temperature for the current extruder only. - continue; + // The same applies when ooze prevention is enabled. + if (extruder.id() != m_writer.extruder()->id()) + continue; + } int temperature = print.config().temperature.get_at(extruder.id()); - if (temperature > 0 && temperature != print.config().first_layer_temperature.get_at(extruder.id())) + if (temperature > 0 && (temperature != print.config().first_layer_temperature.get_at(extruder.id()))) gcode += m_writer.set_temperature(temperature, false, extruder.id()); } gcode += m_writer.set_bed_temperature(print.config().bed_temperature.get_at(first_extruder_id)); @@ -3186,6 +3122,9 @@ std::string GCode::set_extruder(unsigned int extruder_id, double print_z) if (! start_filament_gcode.empty()) { // Process the start_filament_gcode for the filament. DynamicConfig config; + config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); + config.set_key_value("layer_z", new ConfigOptionFloat(this->writer().get_position()(2) - m_config.z_offset.value)); + config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); config.set_key_value("filament_extruder_id", new ConfigOptionInt(int(extruder_id))); gcode += this->placeholder_parser_process("start_filament_gcode", start_filament_gcode, extruder_id, &config); check_add_eol(gcode); @@ -3201,8 +3140,7 @@ std::string GCode::set_extruder(unsigned int extruder_id, double print_z) m_wipe.reset_path(); if (m_writer.extruder() != nullptr) { - // Process the custom end_filament_gcode. set_extruder() is only called if there is no wipe tower - // so it should not be injected twice. + // Process the custom end_filament_gcode. unsigned int old_extruder_id = m_writer.extruder()->id(); const std::string &end_filament_gcode = m_config.end_filament_gcode.get_at(old_extruder_id); if (! end_filament_gcode.empty()) { @@ -3212,8 +3150,7 @@ std::string GCode::set_extruder(unsigned int extruder_id, double print_z) } - // If ooze prevention is enabled, park current extruder in the nearest - // standby point and set it to the standby temperature. + // If ooze prevention is enabled, set current extruder to the standby temperature. if (m_ooze_prevention.enable && m_writer.extruder() != nullptr) gcode += m_ooze_prevention.pre_toolchange(*this); @@ -3257,6 +3194,9 @@ std::string GCode::set_extruder(unsigned int extruder_id, double print_z) if (! start_filament_gcode.empty()) { // Process the start_filament_gcode for the new filament. DynamicConfig config; + config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); + config.set_key_value("layer_z", new ConfigOptionFloat(this->writer().get_position()(2) - m_config.z_offset.value)); + config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); config.set_key_value("filament_extruder_id", new ConfigOptionInt(int(extruder_id))); gcode += this->placeholder_parser_process("start_filament_gcode", start_filament_gcode, extruder_id, &config); check_add_eol(gcode); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 0741d7e37..ee50aefcc 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -39,14 +39,13 @@ struct PrintInstance; class OozePrevention { public: bool enable; - Points standby_points; OozePrevention() : enable(false) {} std::string pre_toolchange(GCode &gcodegen); std::string post_toolchange(GCode &gcodegen); private: - int _get_temp(GCode &gcodegen); + int _get_temp(const GCode &gcodegen) const; }; class Wipe { diff --git a/src/libslic3r/GCode/ExtrusionProcessor.hpp b/src/libslic3r/GCode/ExtrusionProcessor.hpp index 5a13e5484..1525624da 100644 --- a/src/libslic3r/GCode/ExtrusionProcessor.hpp +++ b/src/libslic3r/GCode/ExtrusionProcessor.hpp @@ -122,39 +122,18 @@ std::vector estimate_points_properties(const std::vector

CurvatureEstimator cestim; auto maybe_unscale = [](const P &p) { return SCALED_INPUT ? unscaled(p) : p.template cast(); }; - std::vector

extrusion_points; - { - if (max_line_length <= 0) { - extrusion_points = input_points; - } else { - extrusion_points.reserve(input_points.size() * 2); - for (size_t i = 0; i + 1 < input_points.size(); i++) { - const P &curr = input_points[i]; - const P &next = input_points[i + 1]; - extrusion_points.push_back(curr); - auto len = maybe_unscale(next - curr).squaredNorm(); - double t = sqrt((max_line_length * max_line_length) / len); - size_t new_point_count = 1.0 / (t + EPSILON); - for (size_t j = 1; j < new_point_count + 1; j++) { - extrusion_points.push_back(curr * (1.0 - j * t) + next * (j * t)); - } - } - extrusion_points.push_back(input_points.back()); - } - } - std::vector points; - points.reserve(extrusion_points.size() * (ADD_INTERSECTIONS ? 1.5 : 1)); + points.reserve(input_points.size() * (ADD_INTERSECTIONS ? 1.5 : 1)); { - ExtendedPoint start_point{maybe_unscale(extrusion_points.front())}; + ExtendedPoint start_point{maybe_unscale(input_points.front())}; auto [distance, nearest_line, x] = unscaled_prev_layer.template distance_from_lines_extra(start_point.position.cast()); start_point.distance = distance + boundary_offset; start_point.nearest_prev_layer_line = nearest_line; points.push_back(start_point); } - for (size_t i = 1; i < extrusion_points.size(); i++) { - ExtendedPoint next_point{maybe_unscale(extrusion_points[i])}; + for (size_t i = 1; i < input_points.size(); i++) { + ExtendedPoint next_point{maybe_unscale(input_points[i])}; auto [distance, nearest_line, x] = unscaled_prev_layer.template distance_from_lines_extra(next_point.position.cast()); next_point.distance = distance + boundary_offset; next_point.nearest_prev_layer_line = nearest_line; @@ -172,7 +151,7 @@ std::vector estimate_points_properties(const std::vector

if (PREV_LAYER_BOUNDARY_OFFSET && ADD_INTERSECTIONS) { std::vector new_points; - new_points.reserve(points.size() * 2); + new_points.reserve(points.size()*2); new_points.push_back(points.front()); for (int point_idx = 0; point_idx < int(points.size()) - 1; ++point_idx) { const ExtendedPoint &curr = points[point_idx]; @@ -201,7 +180,30 @@ std::vector estimate_points_properties(const std::vector

} new_points.push_back(next); } - points = std::move(new_points); + points = new_points; + } + + if (max_line_length > 0) { + std::vector new_points; + new_points.reserve(points.size()*2); + { + for (size_t i = 0; i + 1 < points.size(); i++) { + const ExtendedPoint &curr = points[i]; + const ExtendedPoint &next = points[i + 1]; + new_points.push_back(curr); + double len = (next.position - curr.position).squaredNorm(); + double t = sqrt((max_line_length * max_line_length) / len); + size_t new_point_count = 1.0 / t; + for (size_t j = 1; j < new_point_count + 1; j++) { + Vec2d pos = curr.position * (1.0 - j * t) + next.position * (j * t); + auto [p_dist, p_near_l, + p_x] = unscaled_prev_layer.template distance_from_lines_extra(pos.cast()); + new_points.push_back(ExtendedPoint{pos, float(p_dist + boundary_offset), p_near_l}); + } + } + new_points.push_back(points.back()); + } + points = new_points; } for (int point_idx = 0; point_idx < int(points.size()); ++point_idx) { diff --git a/src/libslic3r/GCode/WipeTower.cpp b/src/libslic3r/GCode/WipeTower.cpp index f24311a13..04fab3fcd 100644 --- a/src/libslic3r/GCode/WipeTower.cpp +++ b/src/libslic3r/GCode/WipeTower.cpp @@ -71,6 +71,8 @@ public: return *this; } + WipeTowerWriter& set_position(const Vec2f &pos) { m_current_pos = pos; return *this; } + WipeTowerWriter& set_initial_tool(size_t tool) { m_current_tool = tool; return *this; } WipeTowerWriter& set_z(float z) @@ -802,22 +804,26 @@ void WipeTower::toolchange_Unload( { float xl = cleaning_box.ld.x() + 1.f * m_perimeter_width; float xr = cleaning_box.rd.x() - 1.f * m_perimeter_width; - - const float line_width = m_perimeter_width * m_filpar[m_current_tool].ramming_line_width_multiplicator; // desired ramming line thickness + + const float line_width = m_perimeter_width * m_filpar[m_current_tool].ramming_line_width_multiplicator; // desired ramming line thickness const float y_step = line_width * m_filpar[m_current_tool].ramming_step_multiplicator * m_extra_spacing; // spacing between lines in mm + const Vec2f ramming_start_pos = Vec2f(xl, cleaning_box.ld.y() + m_depth_traversed + y_step/2.f); + writer.append("; CP TOOLCHANGE UNLOAD\n") .change_analyzer_line_width(line_width); unsigned i = 0; // iterates through ramming_speed m_left_to_right = true; // current direction of ramming float remaining = xr - xl ; // keeps track of distance to the next turnaround - float e_done = 0; // measures E move done from each segment - - writer.travel(xl, cleaning_box.ld.y() + m_depth_traversed + y_step/2.f ); // move to starting position + float e_done = 0; // measures E move done from each segment + if (m_semm) + writer.travel(ramming_start_pos); // move to starting position + else + writer.set_position(ramming_start_pos); // if the ending point of the ram would end up in mid air, align it with the end of the wipe tower: - if (m_layer_info > m_plan.begin() && m_layer_info < m_plan.end() && (m_layer_info-1!=m_plan.begin() || !m_adhesion )) { + if (m_semm && (m_layer_info > m_plan.begin() && m_layer_info < m_plan.end() && (m_layer_info-1!=m_plan.begin() || !m_adhesion ))) { // this is y of the center of previous sparse infill border float sparse_beginning_y = 0.f; @@ -849,7 +855,7 @@ void WipeTower::toolchange_Unload( writer.disable_linear_advance(); // now the ramming itself: - while (i < m_filpar[m_current_tool].ramming_speed.size()) + while (m_semm && i < m_filpar[m_current_tool].ramming_speed.size()) { const float x = volume_to_length(m_filpar[m_current_tool].ramming_speed[i] * 0.25f, line_width, m_layer_height); const float e = m_filpar[m_current_tool].ramming_speed[i] * 0.25f / filament_area(); // transform volume per sec to E move; @@ -898,7 +904,7 @@ void WipeTower::toolchange_Unload( // Cooling: const int& number_of_moves = m_filpar[m_current_tool].cooling_moves; - if (number_of_moves > 0) { + if (m_semm && number_of_moves > 0) { const float& initial_speed = m_filpar[m_current_tool].cooling_initial_speed; const float& final_speed = m_filpar[m_current_tool].cooling_final_speed; @@ -916,14 +922,20 @@ void WipeTower::toolchange_Unload( } } - // let's wait is necessary: - writer.wait(m_filpar[m_current_tool].delay); - // we should be at the beginning of the cooling tube again - let's move to parking position: - writer.retract(-m_cooling_tube_length/2.f+m_parking_pos_retraction-m_cooling_tube_retraction, 2000); + if (m_semm) { + // let's wait is necessary: + writer.wait(m_filpar[m_current_tool].delay); + // we should be at the beginning of the cooling tube again - let's move to parking position: + writer.retract(-m_cooling_tube_length/2.f+m_parking_pos_retraction-m_cooling_tube_retraction, 2000); + } // this is to align ramming and future wiping extrusions, so the future y-steps can be uniform from the start: // the perimeter_width will later be subtracted, it is there to not load while moving over just extruded material - writer.travel(end_of_ramming.x(), end_of_ramming.y() + (y_step/m_extra_spacing-m_perimeter_width) / 2.f + m_perimeter_width, 2400.f); + Vec2f pos = Vec2f(end_of_ramming.x(), end_of_ramming.y() + (y_step/m_extra_spacing-m_perimeter_width) / 2.f + m_perimeter_width); + if (m_semm) + writer.travel(pos, 2400.f); + else + writer.set_position(pos); writer.resume_preview() .flush_planner_queue(); @@ -941,7 +953,7 @@ void WipeTower::toolchange_Change( // This is where we want to place the custom gcodes. We will use placeholders for this. // These will be substituted by the actual gcodes when the gcode is generated. - writer.append("[end_filament_gcode]\n"); + //writer.append("[end_filament_gcode]\n"); writer.append("[toolchange_gcode]\n"); // Travel to where we assume we are. Custom toolchange or some special T code handling (parking extruder etc) @@ -952,11 +964,12 @@ void WipeTower::toolchange_Change( .append(std::string("G1 X") + Slic3r::float_to_string_decimal_point(current_pos.x()) + " Y" + Slic3r::float_to_string_decimal_point(current_pos.y()) + never_skip_tag() + "\n"); + writer.append("[deretraction_from_wipe_tower_generator]"); // The toolchange Tn command will be inserted later, only in case that the user does // not provide a custom toolchange gcode. writer.set_tool(new_tool); // This outputs nothing, the writer just needs to know the tool has changed. - writer.append("[start_filament_gcode]\n"); + //writer.append("[start_filament_gcode]\n"); writer.flush_planner_queue(); m_current_tool = new_tool; diff --git a/src/libslic3r/GCode/WipeTower.hpp b/src/libslic3r/GCode/WipeTower.hpp index 397b5ab7d..ab3af507d 100644 --- a/src/libslic3r/GCode/WipeTower.hpp +++ b/src/libslic3r/GCode/WipeTower.hpp @@ -290,7 +290,6 @@ private: // Extruder specific parameters. std::vector m_filpar; - // State of the wipe tower generator. unsigned int m_num_layer_changes = 0; // Layer change counter for the output statistics. unsigned int m_num_tool_changes = 0; // Tool change change counter for the output statistics. diff --git a/src/libslic3r/Geometry.cpp b/src/libslic3r/Geometry.cpp index 7366d2233..441825654 100644 --- a/src/libslic3r/Geometry.cpp +++ b/src/libslic3r/Geometry.cpp @@ -872,18 +872,15 @@ Eigen::Quaterniond rotation_xyz_diff(const Vec3d &rot_xyz_from, const Vec3d &rot } // This should only be called if it is known, that the two rotations only differ in rotation around the Z axis. -double rotation_diff_z(const Vec3d &rot_xyz_from, const Vec3d &rot_xyz_to) +double rotation_diff_z(const Transform3d &trafo_from, const Transform3d &trafo_to) { - const Eigen::AngleAxisd angle_axis(rotation_xyz_diff(rot_xyz_from, rot_xyz_to)); - const Vec3d& axis = angle_axis.axis(); - const double angle = angle_axis.angle(); -#ifndef NDEBUG - if (std::abs(angle) > 1e-8) { - assert(std::abs(axis.x()) < 1e-8); - assert(std::abs(axis.y()) < 1e-8); - } -#endif /* NDEBUG */ - return (axis.z() < 0) ? -angle : angle; + auto m = trafo_to.linear() * trafo_from.linear().inverse(); + assert(std::abs(m.determinant() - 1) < EPSILON); + Vec3d vx = m * Vec3d(1., 0., 0); + // Verify that the linear part of rotation from trafo_from to trafo_to rotates around Z and is unity. + assert(std::abs(std::hypot(vx.x(), vx.y()) - 1.) < 1e-5); + assert(std::abs(vx.z()) < 1e-5); + return atan2(vx.y(), vx.x()); } }} // namespace Slic3r::Geometry diff --git a/src/libslic3r/Geometry.hpp b/src/libslic3r/Geometry.hpp index a5df1e679..0421c0806 100644 --- a/src/libslic3r/Geometry.hpp +++ b/src/libslic3r/Geometry.hpp @@ -470,8 +470,7 @@ public: Transform3d get_mirror_matrix() const; bool is_left_handed() const { - const Vec3d mirror = get_mirror(); - return mirror.x() * mirror.y() * mirror.z() < 0.0; + return m_matrix.affine().determinant() < 0; } #else bool is_scaling_uniform() const { return std::abs(m_scaling_factor.x() - m_scaling_factor.y()) < 1e-8 && std::abs(m_scaling_factor.x() - m_scaling_factor.z()) < 1e-8; } @@ -547,7 +546,7 @@ extern Transform3d transform3d_from_string(const std::string& transform_str); extern Eigen::Quaterniond rotation_xyz_diff(const Vec3d &rot_xyz_from, const Vec3d &rot_xyz_to); // Rotation by Z to align rot_xyz_from to rot_xyz_to. // This should only be called if it is known, that the two rotations only differ in rotation around the Z axis. -extern double rotation_diff_z(const Vec3d &rot_xyz_from, const Vec3d &rot_xyz_to); +extern double rotation_diff_z(const Transform3d &trafo_from, const Transform3d &trafo_to); // Is the angle close to a multiple of 90 degrees? inline bool is_rotation_ninety_degrees(double a) diff --git a/src/libslic3r/Layer.cpp b/src/libslic3r/Layer.cpp index 44fc18f91..9b0004e98 100644 --- a/src/libslic3r/Layer.cpp +++ b/src/libslic3r/Layer.cpp @@ -577,12 +577,22 @@ void Layer::sort_perimeters_into_islands( } if (! sample_set) { // If there is no infill, take a sample of some inner perimeter. - for (uint32_t iperimeter : extrusions.first) - if (const ExtrusionEntity &ee = *this_layer_region.perimeters().entities[iperimeter]; ! ee.role().is_external()) { - sample = ee.first_point(); + for (uint32_t iperimeter : extrusions.first) { + const ExtrusionEntity &ee = *this_layer_region.perimeters().entities[iperimeter]; + if (ee.is_collection()) { + for (const ExtrusionEntity *ee2 : dynamic_cast(ee).entities) + if (! ee2->role().is_external()) { + sample = ee2->first_point(); + sample_set = true; + goto loop_end; + } + } else if (! ee.role().is_external()) { + sample = ee.first_point(); sample_set = true; break; } + } + loop_end: if (! sample_set) { if (! extrusions.second.empty()) { // If there is no inner perimeter, take a sample of some gap fill extrusion. @@ -752,7 +762,9 @@ void Layer::sort_perimeters_into_islands( const PrintRegionConfig ®ion_config = this_layer_region.region().config(); const auto bbox_eps = scaled( EPSILON + print_config.gcode_resolution.value + - (region_config.fuzzy_skin.value == FuzzySkinType::None ? 0. : region_config.fuzzy_skin_thickness.value)); + (region_config.fuzzy_skin.value == FuzzySkinType::None ? 0. : region_config.fuzzy_skin_thickness.value + //FIXME it looks as if Arachne could extend open lines by fuzzy_skin_point_dist, which does not seem right. + + region_config.fuzzy_skin_point_dist.value)); auto point_inside_surface_dist2 = [&lslices = this->lslices, &lslices_ex = this->lslices_ex, bbox_eps] (const size_t lslice_idx, const Point &point) { diff --git a/src/libslic3r/MeshBoolean.cpp b/src/libslic3r/MeshBoolean.cpp index 86b50d156..c7ebcbd19 100644 --- a/src/libslic3r/MeshBoolean.cpp +++ b/src/libslic3r/MeshBoolean.cpp @@ -137,7 +137,8 @@ inline Vec3f to_vec3f(const _EpecMesh::Point& v) return { float(iv.x()), float(iv.y()), float(iv.z()) }; } -template TriangleMesh cgal_to_triangle_mesh(const _Mesh &cgalmesh) +template +indexed_triangle_set cgal_to_indexed_triangle_set(const _Mesh &cgalmesh) { indexed_triangle_set its; its.vertices.reserve(cgalmesh.num_vertices()); @@ -167,7 +168,7 @@ template TriangleMesh cgal_to_triangle_mesh(const _Mesh &cgalmesh) its.indices.emplace_back(facet); } - return TriangleMesh(std::move(its)); + return its; } std::unique_ptr @@ -181,7 +182,12 @@ triangle_mesh_to_cgal(const std::vector &V, TriangleMesh cgal_to_triangle_mesh(const CGALMesh &cgalmesh) { - return cgal_to_triangle_mesh(cgalmesh.m); + return TriangleMesh{cgal_to_indexed_triangle_set(cgalmesh.m)}; +} + +indexed_triangle_set cgal_to_indexed_triangle_set(const CGALMesh &cgalmesh) +{ + return cgal_to_indexed_triangle_set(cgalmesh.m); } // ///////////////////////////////////////////////////////////////////////////// @@ -236,16 +242,28 @@ bool does_self_intersect(const CGALMesh &mesh) { return CGALProc::does_self_inte // Now the public functions for TriangleMesh input: // ///////////////////////////////////////////////////////////////////////////// +template void _mesh_boolean_do(Op &&op, indexed_triangle_set &A, const indexed_triangle_set &B) +{ + CGALMesh meshA; + CGALMesh meshB; + triangle_mesh_to_cgal(A.vertices, A.indices, meshA.m); + triangle_mesh_to_cgal(B.vertices, B.indices, meshB.m); + + _cgal_do(op, meshA, meshB); + + A = cgal_to_indexed_triangle_set(meshA.m); +} + template void _mesh_boolean_do(Op &&op, TriangleMesh &A, const TriangleMesh &B) { CGALMesh meshA; CGALMesh meshB; triangle_mesh_to_cgal(A.its.vertices, A.its.indices, meshA.m); triangle_mesh_to_cgal(B.its.vertices, B.its.indices, meshB.m); - + _cgal_do(op, meshA, meshB); - - A = cgal_to_triangle_mesh(meshA.m); + + A = cgal_to_triangle_mesh(meshA); } void minus(TriangleMesh &A, const TriangleMesh &B) @@ -263,6 +281,21 @@ void intersect(TriangleMesh &A, const TriangleMesh &B) _mesh_boolean_do(_cgal_intersection, A, B); } +void minus(indexed_triangle_set &A, const indexed_triangle_set &B) +{ + _mesh_boolean_do(_cgal_diff, A, B); +} + +void plus(indexed_triangle_set &A, const indexed_triangle_set &B) +{ + _mesh_boolean_do(_cgal_union, A, B); +} + +void intersect(indexed_triangle_set &A, const indexed_triangle_set &B) +{ + _mesh_boolean_do(_cgal_intersection, A, B); +} + bool does_self_intersect(const TriangleMesh &mesh) { CGALMesh cgalm; @@ -282,6 +315,11 @@ bool empty(const CGALMesh &mesh) return mesh.m.is_empty(); } +CGALMeshPtr clone(const CGALMesh &m) +{ + return CGALMeshPtr{new CGALMesh{m}}; +} + } // namespace cgal } // namespace MeshBoolean diff --git a/src/libslic3r/MeshBoolean.hpp b/src/libslic3r/MeshBoolean.hpp index 140c96931..e20425c1c 100644 --- a/src/libslic3r/MeshBoolean.hpp +++ b/src/libslic3r/MeshBoolean.hpp @@ -26,27 +26,35 @@ namespace cgal { struct CGALMesh; struct CGALMeshDeleter { void operator()(CGALMesh *ptr); }; +using CGALMeshPtr = std::unique_ptr; -std::unique_ptr -triangle_mesh_to_cgal(const std::vector &V, - const std::vector &F); +CGALMeshPtr clone(const CGALMesh &m); -inline std::unique_ptr triangle_mesh_to_cgal(const indexed_triangle_set &M) +CGALMeshPtr triangle_mesh_to_cgal( + const std::vector &V, + const std::vector &F); + +inline CGALMeshPtr triangle_mesh_to_cgal(const indexed_triangle_set &M) { return triangle_mesh_to_cgal(M.vertices, M.indices); } -inline std::unique_ptr triangle_mesh_to_cgal(const TriangleMesh &M) +inline CGALMeshPtr triangle_mesh_to_cgal(const TriangleMesh &M) { return triangle_mesh_to_cgal(M.its); } TriangleMesh cgal_to_triangle_mesh(const CGALMesh &cgalmesh); - +indexed_triangle_set cgal_to_indexed_triangle_set(const CGALMesh &cgalmesh); + // Do boolean mesh difference with CGAL bypassing igl. void minus(TriangleMesh &A, const TriangleMesh &B); void plus(TriangleMesh &A, const TriangleMesh &B); void intersect(TriangleMesh &A, const TriangleMesh &B); +void minus(indexed_triangle_set &A, const indexed_triangle_set &B); +void plus(indexed_triangle_set &A, const indexed_triangle_set &B); +void intersect(indexed_triangle_set &A, const indexed_triangle_set &B); + void minus(CGALMesh &A, CGALMesh &B); void plus(CGALMesh &A, CGALMesh &B); void intersect(CGALMesh &A, CGALMesh &B); diff --git a/src/libslic3r/MeshSplitImpl.hpp b/src/libslic3r/MeshSplitImpl.hpp index c9df648fb..0f1fe44c3 100644 --- a/src/libslic3r/MeshSplitImpl.hpp +++ b/src/libslic3r/MeshSplitImpl.hpp @@ -108,6 +108,21 @@ template struct ItsNeighborsWrapper const auto& get_index() const noexcept { return index_ref; } }; +// Can be used as the second argument to its_split to apply a functor on each +// part, instead of collecting them into a container. +template +struct SplitOutputFn { + + Fn fn; + + SplitOutputFn(Fn f): fn{std::move(f)} {} + + SplitOutputFn &operator *() { return *this; } + void operator=(indexed_triangle_set &&its) { fn(std::move(its)); } + void operator=(indexed_triangle_set &its) { fn(its); } + SplitOutputFn& operator++() { return *this; }; +}; + // Splits a mesh into multiple meshes when possible. template void its_split(const Its &m, OutputIt out_it) @@ -155,7 +170,8 @@ void its_split(const Its &m, OutputIt out_it) mesh.indices.emplace_back(new_face); } - out_it = std::move(mesh); + *out_it = std::move(mesh); + ++out_it; } } diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 8829253e4..bdd9bfd70 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -794,6 +794,11 @@ bool ModelObject::is_mm_painted() const return std::any_of(this->volumes.cbegin(), this->volumes.cend(), [](const ModelVolume *mv) { return mv->is_mm_painted(); }); } +bool ModelObject::is_text() const +{ + return this->volumes.size() == 1 && this->volumes[0]->is_text(); +} + void ModelObject::sort_volumes(bool full_sort) { // sort volumes inside the object to order "Model Part, Negative Volume, Modifier, Support Blocker and Support Enforcer. " @@ -1775,7 +1780,7 @@ void ModelObject::bake_xy_rotation_into_meshes(size_t instance_idx) // Adjust the instances. for (size_t i = 0; i < this->instances.size(); ++ i) { ModelInstance &model_instance = *this->instances[i]; - model_instance.set_rotation(Vec3d(0., 0., Geometry::rotation_diff_z(reference_trafo.get_rotation(), model_instance.get_rotation()))); + model_instance.set_rotation(Vec3d(0., 0., Geometry::rotation_diff_z(reference_trafo.get_matrix(), model_instance.get_matrix()))); model_instance.set_scaling_factor(Vec3d(new_scaling_factor, new_scaling_factor, new_scaling_factor)); model_instance.set_mirror(Vec3d(1., 1., 1.)); } @@ -2626,6 +2631,15 @@ bool model_mmu_segmentation_data_changed(const ModelObject& mo, const ModelObjec [](const ModelVolume &mv_old, const ModelVolume &mv_new){ return mv_old.mmu_segmentation_facets.timestamp_matches(mv_new.mmu_segmentation_facets); }); } +bool model_has_parameter_modifiers_in_objects(const Model &model) +{ + for (const auto& model_object : model.objects) + for (const auto& volume : model_object->volumes) + if (volume->is_modifier()) + return true; + return false; +} + bool model_has_multi_part_objects(const Model &model) { for (const ModelObject *model_object : model.objects) diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 0151cf61d..abbd835fe 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -383,7 +383,9 @@ public: bool is_seam_painted() const; // Checks if any of object volume is painted using the multi-material painting gizmo. bool is_mm_painted() const; - + // Checks if object contains just one volume and it's a text + bool is_text() const; + ModelInstance* add_instance(); ModelInstance* add_instance(const ModelInstance &instance); ModelInstance* add_instance(const Vec3d &offset, const Vec3d &scaling_factor, const Vec3d &rotation, const Vec3d &mirror); @@ -797,6 +799,7 @@ public: bool is_support_enforcer() const { return m_type == ModelVolumeType::SUPPORT_ENFORCER; } bool is_support_blocker() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER; } bool is_support_modifier() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER || m_type == ModelVolumeType::SUPPORT_ENFORCER; } + bool is_text() const { return text_configuration.has_value(); } t_model_material_id material_id() const { return m_material_id; } void reset_extra_facets(); void apply_tolerance(); @@ -1387,6 +1390,9 @@ bool model_custom_seam_data_changed(const ModelObject& mo, const ModelObject& mo // The function assumes that volumes list is synchronized. extern bool model_mmu_segmentation_data_changed(const ModelObject& mo, const ModelObject& mo_new); +// If the model has object(s) which contains a modofoer, then it is currently not supported by the SLA mode. +// Either the model cannot be loaded, or a SLA printer has to be activated. +bool model_has_parameter_modifiers_in_objects(const Model& model); // If the model has multi-part objects, then it is currently not supported by the SLA mode. // Either the model cannot be loaded, or a SLA printer has to be activated. bool model_has_multi_part_objects(const Model &model); diff --git a/src/libslic3r/OpenVDBUtils.cpp b/src/libslic3r/OpenVDBUtils.cpp index 2c207bb6a..21409445f 100644 --- a/src/libslic3r/OpenVDBUtils.cpp +++ b/src/libslic3r/OpenVDBUtils.cpp @@ -6,6 +6,7 @@ #pragma warning(push) #pragma warning(disable : 4146) #endif // _MSC_VER +#include #include #ifdef _MSC_VER #pragma warning(pop) @@ -16,14 +17,44 @@ #include #include -//#include "MTUtils.hpp" - namespace Slic3r { +struct VoxelGrid +{ + openvdb::FloatGrid grid; + + mutable std::optional accessor; + + template + VoxelGrid(Args &&...args): grid{std::forward(args)...} {} +}; + +void VoxelGridDeleter::operator()(VoxelGrid *ptr) { delete ptr; } + +// Similarly to std::make_unique() +template +VoxelGridPtr make_voxelgrid(Args &&...args) +{ + VoxelGrid *ptr = nullptr; + try { + ptr = new VoxelGrid(std::forward(args)...); + } catch(...) { + delete ptr; + } + + return VoxelGridPtr{ptr}; +} + +template VoxelGridPtr make_voxelgrid<>(); + +inline Vec3f to_vec3f(const openvdb::Vec3s &v) { return Vec3f{v.x(), v.y(), v.z()}; } +inline Vec3d to_vec3d(const openvdb::Vec3s &v) { return to_vec3f(v).cast(); } +inline Vec3i to_vec3i(const openvdb::Vec3I &v) { return Vec3i{int(v[2]), int(v[1]), int(v[0])}; } + class TriangleMeshDataAdapter { public: const indexed_triangle_set &its; - float voxel_scale; + Transform3d trafo; size_t polygonCount() const { return its.indices.size(); } size_t pointCount() const { return its.vertices.size(); } @@ -35,19 +66,26 @@ public: void getIndexSpacePoint(size_t n, size_t v, openvdb::Vec3d& pos) const { auto vidx = size_t(its.indices[n](Eigen::Index(v))); - Slic3r::Vec3d p = its.vertices[vidx].cast() * voxel_scale; + Slic3r::Vec3d p = trafo * its.vertices[vidx].cast(); pos = {p.x(), p.y(), p.z()}; } - TriangleMeshDataAdapter(const indexed_triangle_set &m, float voxel_sc = 1.f) - : its{m}, voxel_scale{voxel_sc} {}; + TriangleMeshDataAdapter(const indexed_triangle_set &m, const Transform3d tr = Transform3d::Identity()) + : its{m}, trafo{tr} {} }; -openvdb::FloatGrid::Ptr mesh_to_grid(const indexed_triangle_set & mesh, - const openvdb::math::Transform &tr, - float voxel_scale, - float exteriorBandWidth, - float interiorBandWidth) +struct Interrupter +{ + std::function statusfn; + + void start(const char* name = nullptr) { (void)name; } + void end() {} + + inline bool wasInterrupted(int percent = -1) { return statusfn && statusfn(percent); } +}; + +VoxelGridPtr mesh_to_grid(const indexed_triangle_set &mesh, + const MeshToGridParams ¶ms) { // Might not be needed but this is now proven to be working openvdb::initialize(); @@ -55,51 +93,63 @@ openvdb::FloatGrid::Ptr mesh_to_grid(const indexed_triangle_set & mesh, std::vector meshparts = its_split(mesh); auto it = std::remove_if(meshparts.begin(), meshparts.end(), - [](auto &m) { return its_volume(m) < EPSILON; }); + [](auto &m) { + return its_volume(m) < EPSILON; + }); meshparts.erase(it, meshparts.end()); + Transform3d trafo = params.trafo().cast(); + trafo.prescale(params.voxel_scale()); + + Interrupter interrupter{params.statusfn()}; + openvdb::FloatGrid::Ptr grid; for (auto &m : meshparts) { auto subgrid = openvdb::tools::meshToVolume( - TriangleMeshDataAdapter{m, voxel_scale}, tr, 1.f, 1.f); + interrupter, + TriangleMeshDataAdapter{m, trafo}, + openvdb::math::Transform{}, + params.exterior_bandwidth(), + params.interior_bandwidth()); - if (grid && subgrid) openvdb::tools::csgUnion(*grid, *subgrid); - else if (subgrid) grid = std::move(subgrid); + if (interrupter.wasInterrupted()) + break; + + if (grid && subgrid) + openvdb::tools::csgUnion(*grid, *subgrid); + else if (subgrid) + grid = std::move(subgrid); } - if (meshparts.size() > 1) { - // This is needed to avoid various artefacts on multipart meshes. - // TODO: replace with something faster - grid = openvdb::tools::levelSetRebuild(*grid, 0., 1.f, 1.f); - } - if(meshparts.empty()) { + if (interrupter.wasInterrupted()) + return {}; + + if (meshparts.empty()) { // Splitting failed, fall back to hollow the original mesh grid = openvdb::tools::meshToVolume( - TriangleMeshDataAdapter{mesh}, tr, 1.f, 1.f); + interrupter, + TriangleMeshDataAdapter{mesh, trafo}, + openvdb::math::Transform{}, + params.exterior_bandwidth(), + params.interior_bandwidth()); } - constexpr int DilateIterations = 1; + if (interrupter.wasInterrupted()) + return {}; - grid = openvdb::tools::dilateSdf( - *grid, interiorBandWidth, openvdb::tools::NN_FACE_EDGE, - DilateIterations, - openvdb::tools::FastSweepingDomain::SWEEP_LESS_THAN_ISOVALUE); + grid->transform().preScale(1./params.voxel_scale()); + grid->insertMeta("voxel_scale", openvdb::FloatMetadata(params.voxel_scale())); - grid = openvdb::tools::dilateSdf( - *grid, exteriorBandWidth, openvdb::tools::NN_FACE_EDGE, - DilateIterations, - openvdb::tools::FastSweepingDomain::SWEEP_GREATER_THAN_ISOVALUE); + VoxelGridPtr ret = make_voxelgrid(std::move(*grid)); - grid->insertMeta("voxel_scale", openvdb::FloatMetadata(voxel_scale)); - - return grid; + return ret; } -indexed_triangle_set grid_to_mesh(const openvdb::FloatGrid &grid, - double isovalue, - double adaptivity, - bool relaxDisorientedTriangles) +indexed_triangle_set grid_to_mesh(const VoxelGrid &vgrid, + double isovalue, + double adaptivity, + bool relaxDisorientedTriangles) { openvdb::initialize(); @@ -107,51 +157,152 @@ indexed_triangle_set grid_to_mesh(const openvdb::FloatGrid &grid, std::vector triangles; std::vector quads; + auto &grid = vgrid.grid; + openvdb::tools::volumeToMesh(grid, points, triangles, quads, isovalue, adaptivity, relaxDisorientedTriangles); - float scale = 1.; - try { - scale = grid.template metaValue("voxel_scale"); - } catch (...) { } - indexed_triangle_set ret; ret.vertices.reserve(points.size()); ret.indices.reserve(triangles.size() + quads.size() * 2); - for (auto &v : points) ret.vertices.emplace_back(to_vec3f(v) / scale); + for (auto &v : points) ret.vertices.emplace_back(to_vec3f(v) /*/ scale*/); for (auto &v : triangles) ret.indices.emplace_back(to_vec3i(v)); for (auto &quad : quads) { - ret.indices.emplace_back(quad(0), quad(1), quad(2)); - ret.indices.emplace_back(quad(2), quad(3), quad(0)); + ret.indices.emplace_back(quad(2), quad(1), quad(0)); + ret.indices.emplace_back(quad(3), quad(2), quad(0)); } return ret; } -openvdb::FloatGrid::Ptr redistance_grid(const openvdb::FloatGrid &grid, - double iso, - double er, - double ir) +VoxelGridPtr dilate_grid(const VoxelGrid &vgrid, + float exteriorBandWidth, + float interiorBandWidth) { - auto new_grid = openvdb::tools::levelSetRebuild(grid, float(iso), - float(er), float(ir)); + constexpr int DilateIterations = 1; + + openvdb::FloatGrid::Ptr new_grid; + + float scale = get_voxel_scale(vgrid); + + if (interiorBandWidth > 0.f) + new_grid = openvdb::tools::dilateSdf( + vgrid.grid, scale * interiorBandWidth, openvdb::tools::NN_FACE_EDGE, + DilateIterations, + openvdb::tools::FastSweepingDomain::SWEEP_LESS_THAN_ISOVALUE); + + auto &arg = new_grid? *new_grid : vgrid.grid; + + if (exteriorBandWidth > 0.f) + new_grid = openvdb::tools::dilateSdf( + arg, scale * exteriorBandWidth, openvdb::tools::NN_FACE_EDGE, + DilateIterations, + openvdb::tools::FastSweepingDomain::SWEEP_GREATER_THAN_ISOVALUE); + + VoxelGridPtr ret; + + if (new_grid) + ret = make_voxelgrid(std::move(*new_grid)); + else + ret = make_voxelgrid(vgrid.grid); // Copies voxel_scale metadata, if it exists. - new_grid->insertMeta(*grid.deepCopyMeta()); + ret->grid.insertMeta(*vgrid.grid.deepCopyMeta()); - return new_grid; + return ret; } -openvdb::FloatGrid::Ptr redistance_grid(const openvdb::FloatGrid &grid, - double iso) +VoxelGridPtr redistance_grid(const VoxelGrid &vgrid, + float iso, + float er, + float ir) { - auto new_grid = openvdb::tools::levelSetRebuild(grid, float(iso)); + auto new_grid = openvdb::tools::levelSetRebuild(vgrid.grid, iso, er, ir); - // Copies voxel_scale metadata, if it exists. - new_grid->insertMeta(*grid.deepCopyMeta()); + auto ret = make_voxelgrid(std::move(*new_grid)); - return new_grid; + // Copies voxel_scale metadata, if it exists. + ret->grid.insertMeta(*vgrid.grid.deepCopyMeta()); + + return ret; +} + +VoxelGridPtr redistance_grid(const VoxelGrid &vgrid, float iso) +{ + auto new_grid = openvdb::tools::levelSetRebuild(vgrid.grid, iso); + + auto ret = make_voxelgrid(std::move(*new_grid)); + + // Copies voxel_scale metadata, if it exists. + ret->grid.insertMeta(*vgrid.grid.deepCopyMeta()); + + return ret; +} + +void grid_union(VoxelGrid &grid, VoxelGrid &arg) +{ + openvdb::tools::csgUnion(grid.grid, arg.grid); +} + +void grid_difference(VoxelGrid &grid, VoxelGrid &arg) +{ + openvdb::tools::csgDifference(grid.grid, arg.grid); +} + +void grid_intersection(VoxelGrid &grid, VoxelGrid &arg) +{ + openvdb::tools::csgIntersection(grid.grid, arg.grid); +} + +void reset_accessor(const VoxelGrid &vgrid) +{ + vgrid.accessor = vgrid.grid.getConstAccessor(); +} + +double get_distance_raw(const Vec3f &p, const VoxelGrid &vgrid) +{ + if (!vgrid.accessor) + reset_accessor(vgrid); + + auto v = (p).cast(); + auto grididx = vgrid.grid.transform().worldToIndexCellCentered( + {v.x(), v.y(), v.z()}); + + return vgrid.accessor->getValue(grididx) ; +} + +float get_voxel_scale(const VoxelGrid &vgrid) +{ + float scale = 1.; + try { + scale = vgrid.grid.template metaValue("voxel_scale"); + } catch (...) { } + + return scale; +} + +VoxelGridPtr clone(const VoxelGrid &grid) +{ + return make_voxelgrid(grid); +} + +void rescale_grid(VoxelGrid &grid, float scale) +{/* + float old_scale = get_voxel_scale(grid); + + float nscale = scale / old_scale;*/ +// auto tr = openvdb::math::Transform::createLinearTransform(scale); + grid.grid.transform().preScale(scale); + +// grid.grid.insertMeta("voxel_scale", openvdb::FloatMetadata(nscale)); + +// grid.grid.setTransform(tr); +} + +bool is_grid_empty(const VoxelGrid &grid) +{ + return grid.grid.empty(); } } // namespace Slic3r diff --git a/src/libslic3r/OpenVDBUtils.hpp b/src/libslic3r/OpenVDBUtils.hpp index 254ae3583..d4996854c 100644 --- a/src/libslic3r/OpenVDBUtils.hpp +++ b/src/libslic3r/OpenVDBUtils.hpp @@ -3,21 +3,47 @@ #include -#ifdef _MSC_VER -// Suppress warning C4146 in include/gmp.h(2177,31): unary minus operator applied to unsigned type, result still unsigned -#pragma warning(push) -#pragma warning(disable : 4146) -#endif // _MSC_VER -#include -#ifdef _MSC_VER -#pragma warning(pop) -#endif // _MSC_VER - namespace Slic3r { -inline Vec3f to_vec3f(const openvdb::Vec3s &v) { return Vec3f{v.x(), v.y(), v.z()}; } -inline Vec3d to_vec3d(const openvdb::Vec3s &v) { return to_vec3f(v).cast(); } -inline Vec3i to_vec3i(const openvdb::Vec3I &v) { return Vec3i{int(v[0]), int(v[1]), int(v[2])}; } +struct VoxelGrid; +struct VoxelGridDeleter { void operator()(VoxelGrid *ptr); }; +using VoxelGridPtr = std::unique_ptr; + +// This is like std::make_unique for a voxelgrid +template VoxelGridPtr make_voxelgrid(Args &&...args); + +// Default constructed voxelgrid can be obtained this way. +extern template VoxelGridPtr make_voxelgrid<>(); + +void reset_accessor(const VoxelGrid &vgrid); + +double get_distance_raw(const Vec3f &p, const VoxelGrid &interior); + +float get_voxel_scale(const VoxelGrid &grid); + +VoxelGridPtr clone(const VoxelGrid &grid); + +class MeshToGridParams { + Transform3f m_tr = Transform3f::Identity(); + float m_voxel_scale = 1.f; + float m_exteriorBandWidth = 3.0f; + float m_interiorBandWidth = 3.0f; + + std::function m_statusfn; + +public: + MeshToGridParams & trafo(const Transform3f &v) { m_tr = v; return *this; } + MeshToGridParams & voxel_scale(float v) { m_voxel_scale = v; return *this; } + MeshToGridParams & exterior_bandwidth(float v) { m_exteriorBandWidth = v; return *this; } + MeshToGridParams & interior_bandwidth(float v) { m_interiorBandWidth = v; return *this; } + MeshToGridParams & statusfn(std::function fn) { m_statusfn = fn; return *this; } + + const Transform3f& trafo() const noexcept { return m_tr; } + float voxel_scale() const noexcept { return m_voxel_scale; } + float exterior_bandwidth() const noexcept { return m_exteriorBandWidth; } + float interior_bandwidth() const noexcept { return m_interiorBandWidth; } + const std::function& statusfn() const noexcept { return m_statusfn; } +}; // Here voxel_scale defines the scaling of voxels which affects the voxel count. // 1.0 value means a voxel for every unit cube. 2 means the model is scaled to @@ -26,24 +52,32 @@ inline Vec3i to_vec3i(const openvdb::Vec3I &v) { return Vec3i{int(v[0]), int(v[1 // achievable through the Transform parameter. (TODO: or is it?) // The resulting grid will contain the voxel_scale in its metadata under the // "voxel_scale" key to be used in grid_to_mesh function. -openvdb::FloatGrid::Ptr mesh_to_grid(const indexed_triangle_set & mesh, - const openvdb::math::Transform &tr = {}, - float voxel_scale = 1.f, - float exteriorBandWidth = 3.0f, - float interiorBandWidth = 3.0f); +VoxelGridPtr mesh_to_grid(const indexed_triangle_set &mesh, + const MeshToGridParams ¶ms = {}); -indexed_triangle_set grid_to_mesh(const openvdb::FloatGrid &grid, - double isovalue = 0.0, - double adaptivity = 0.0, +indexed_triangle_set grid_to_mesh(const VoxelGrid &grid, + double isovalue = 0.0, + double adaptivity = 0.0, bool relaxDisorientedTriangles = true); -openvdb::FloatGrid::Ptr redistance_grid(const openvdb::FloatGrid &grid, - double iso); +VoxelGridPtr dilate_grid(const VoxelGrid &grid, + float exteriorBandWidth = 3.0f, + float interiorBandWidth = 3.0f); -openvdb::FloatGrid::Ptr redistance_grid(const openvdb::FloatGrid &grid, - double iso, - double ext_range, - double int_range); +VoxelGridPtr redistance_grid(const VoxelGrid &grid, float iso); + +VoxelGridPtr redistance_grid(const VoxelGrid &grid, + float iso, + float ext_range, + float int_range); + +void rescale_grid(VoxelGrid &grid, float scale); + +void grid_union(VoxelGrid &grid, VoxelGrid &arg); +void grid_difference(VoxelGrid &grid, VoxelGrid &arg); +void grid_intersection(VoxelGrid &grid, VoxelGrid &arg); + +bool is_grid_empty(const VoxelGrid &grid); } // namespace Slic3r diff --git a/src/libslic3r/OpenVDBUtilsLegacy.hpp b/src/libslic3r/OpenVDBUtilsLegacy.hpp new file mode 100644 index 000000000..f292af1c8 --- /dev/null +++ b/src/libslic3r/OpenVDBUtilsLegacy.hpp @@ -0,0 +1,100 @@ +#ifndef OPENVDBUTILSLEGACY_HPP +#define OPENVDBUTILSLEGACY_HPP + +#include "libslic3r/TriangleMesh.hpp" + +#ifdef _MSC_VER +// Suppress warning C4146 in OpenVDB: unary minus operator applied to unsigned type, result still unsigned +#pragma warning(push) +#pragma warning(disable : 4146) +#endif // _MSC_VER +#include +#include +#include +#include +#include +#ifdef _MSC_VER +#pragma warning(pop) +#endif // _MSC_VER + +namespace Slic3r { + +openvdb::FloatGrid::Ptr mesh_to_grid(const indexed_triangle_set & mesh, + const openvdb::math::Transform &tr, + float voxel_scale, + float exteriorBandWidth, + float interiorBandWidth) +{ + class TriangleMeshDataAdapter { + public: + const indexed_triangle_set &its; + float voxel_scale; + + size_t polygonCount() const { return its.indices.size(); } + size_t pointCount() const { return its.vertices.size(); } + size_t vertexCount(size_t) const { return 3; } + + // Return position pos in local grid index space for polygon n and vertex v + // The actual mesh will appear to openvdb as scaled uniformly by voxel_size + // And the voxel count per unit volume can be affected this way. + void getIndexSpacePoint(size_t n, size_t v, openvdb::Vec3d& pos) const + { + auto vidx = size_t(its.indices[n](Eigen::Index(v))); + Slic3r::Vec3d p = its.vertices[vidx].cast() * voxel_scale; + pos = {p.x(), p.y(), p.z()}; + } + + TriangleMeshDataAdapter(const indexed_triangle_set &m, float voxel_sc = 1.f) + : its{m}, voxel_scale{voxel_sc} {}; + }; + + // Might not be needed but this is now proven to be working + openvdb::initialize(); + + std::vector meshparts = its_split(mesh); + + auto it = std::remove_if(meshparts.begin(), meshparts.end(), + [](auto &m) { return its_volume(m) < EPSILON; }); + + meshparts.erase(it, meshparts.end()); + + openvdb::FloatGrid::Ptr grid; + for (auto &m : meshparts) { + auto subgrid = openvdb::tools::meshToVolume( + TriangleMeshDataAdapter{m, voxel_scale}, tr, 1.f, 1.f); + + if (grid && subgrid) openvdb::tools::csgUnion(*grid, *subgrid); + else if (subgrid) grid = std::move(subgrid); + } + + if (meshparts.size() > 1) { + // This is needed to avoid various artefacts on multipart meshes. + // TODO: replace with something faster + grid = openvdb::tools::levelSetRebuild(*grid, 0., 1.f, 1.f); + } + if(meshparts.empty()) { + // Splitting failed, fall back to hollow the original mesh + grid = openvdb::tools::meshToVolume( + TriangleMeshDataAdapter{mesh}, tr, 1.f, 1.f); + } + + constexpr int DilateIterations = 1; + + grid = openvdb::tools::dilateSdf( + *grid, interiorBandWidth, openvdb::tools::NN_FACE_EDGE, + DilateIterations, + openvdb::tools::FastSweepingDomain::SWEEP_LESS_THAN_ISOVALUE); + + grid = openvdb::tools::dilateSdf( + *grid, exteriorBandWidth, openvdb::tools::NN_FACE_EDGE, + DilateIterations, + openvdb::tools::FastSweepingDomain::SWEEP_GREATER_THAN_ISOVALUE); + + grid->insertMeta("voxel_scale", openvdb::FloatMetadata(voxel_scale)); + + return grid; +} + +} // namespace Slic3r + +#endif // OPENVDBUTILSLEGACY_HPP diff --git a/src/libslic3r/Optimize/BruteforceOptimizer.hpp b/src/libslic3r/Optimize/BruteforceOptimizer.hpp index 2daef538e..2f6b42224 100644 --- a/src/libslic3r/Optimize/BruteforceOptimizer.hpp +++ b/src/libslic3r/Optimize/BruteforceOptimizer.hpp @@ -13,7 +13,9 @@ template long num_iter(const std::array &idx, size_t gridsz) { long ret = 0; - for (size_t i = 0; i < N; ++i) ret += idx[i] * std::pow(gridsz, i); + for (size_t i = 0; i < N; ++i) + ret += idx[i] * std::pow(gridsz, i); + return ret; } diff --git a/src/libslic3r/Optimize/NLoptOptimizer.hpp b/src/libslic3r/Optimize/NLoptOptimizer.hpp index f5d314046..9e423ff91 100644 --- a/src/libslic3r/Optimize/NLoptOptimizer.hpp +++ b/src/libslic3r/Optimize/NLoptOptimizer.hpp @@ -13,7 +13,7 @@ #include -#include +#include "Optimizer.hpp" namespace Slic3r { namespace opt { @@ -40,69 +40,163 @@ struct IsNLoptAlg> { static const constexpr bool value = true; }; +// NLopt can wrap any of its algorithms to use the augmented lagrangian method +// for deriving an object function from all equality and inequality constraints +// This way one can use algorithms that do not support these constraints natively +template struct NLoptAUGLAG {}; + +template +struct IsNLoptAlg>> { + static const constexpr bool value = true; +}; + +template struct IsNLoptAlg>> { + static const constexpr bool value = true; +}; + template using NLoptOnly = std::enable_if_t::value, T>; +template struct GetNLoptAlg_ { + static constexpr nlopt_algorithm Local = NLOPT_NUM_ALGORITHMS; + static constexpr nlopt_algorithm Global = NLOPT_NUM_ALGORITHMS; + static constexpr bool IsAUGLAG = false; +}; + +template struct GetNLoptAlg_> { + static constexpr nlopt_algorithm Local = NLOPT_NUM_ALGORITHMS; + static constexpr nlopt_algorithm Global = a; + static constexpr bool IsAUGLAG = false; +}; + +template +struct GetNLoptAlg_> { + static constexpr nlopt_algorithm Local = l; + static constexpr nlopt_algorithm Global = g; + static constexpr bool IsAUGLAG = false; +}; + +template constexpr nlopt_algorithm GetNLoptAlg_Global = GetNLoptAlg_>::Global; +template constexpr nlopt_algorithm GetNLoptAlg_Local = GetNLoptAlg_>::Local; +template constexpr bool IsAUGLAG = GetNLoptAlg_>::IsAUGLAG; + +template struct GetNLoptAlg_> { + static constexpr nlopt_algorithm Local = GetNLoptAlg_Local; + static constexpr nlopt_algorithm Global = GetNLoptAlg_Global; + static constexpr bool IsAUGLAG = true; +}; enum class OptDir { MIN, MAX }; // Where to optimize -struct NLopt { // Helper RAII class for nlopt_opt +struct NLoptRAII { // Helper RAII class for nlopt_opt nlopt_opt ptr = nullptr; - template explicit NLopt(A&&...a) + template explicit NLoptRAII(A&&...a) { ptr = nlopt_create(std::forward(a)...); } - NLopt(const NLopt&) = delete; - NLopt(NLopt&&) = delete; - NLopt& operator=(const NLopt&) = delete; - NLopt& operator=(NLopt&&) = delete; + NLoptRAII(const NLoptRAII&) = delete; + NLoptRAII(NLoptRAII&&) = delete; + NLoptRAII& operator=(const NLoptRAII&) = delete; + NLoptRAII& operator=(NLoptRAII&&) = delete; - ~NLopt() { nlopt_destroy(ptr); } + ~NLoptRAII() { nlopt_destroy(ptr); } }; -template class NLoptOpt {}; +// Map a generic function to each argument following the mapping function +template +Fn for_each_argument(Fn &&fn, Args&&...args) +{ + // see https://www.fluentcpp.com/2019/03/05/for_each_arg-applying-a-function-to-each-argument-of-a-function-in-cpp/ + (fn(std::forward(args)),...); -// Optimizers based on NLopt. -template class NLoptOpt> { -protected: + return fn; +} + +// Call fn on each element of the input tuple tup. +template +Fn for_each_in_tuple(Fn fn, Tup &&tup) +{ + auto mpfn = [&fn](auto&...pack) { + for_each_argument(fn, pack...); + }; + + std::apply(mpfn, tup); + + return fn; +} + +// Wrap each element of the tuple tup into a wrapper class W and return +// a new tuple with each element being of type W where T_i is the type of +// i-th element of tup. +template class W, class...Args> +auto wrap_tup(const std::tuple &tup) +{ + return std::tuple...>(tup); +} + +template> +class NLoptOpt { StopCriteria m_stopcr; - OptDir m_dir; + StopCriteria m_loc_stopcr; + OptDir m_dir = OptDir::MIN; - template using TOptData = - std::tuple*, NLoptOpt*, nlopt_opt>; + static constexpr double ConstraintEps = 1e-6; + + template struct OptData { + Fn fn; + NLoptOpt *self = nullptr; + nlopt_opt opt_raw = nullptr; + + OptData(const Fn &f): fn{f} {} + + OptData(const Fn &f, NLoptOpt *s, nlopt_opt nlopt_raw) + : fn{f}, self{s}, opt_raw{nlopt_raw} {} + }; template static double optfunc(unsigned n, const double *params, - double *gradient, - void *data) + double *gradient, void *data) { - assert(n >= N); + assert(n == N); - auto tdata = static_cast*>(data); + auto tdata = static_cast*>(data); - if (std::get<1>(*tdata)->m_stopcr.stop_condition()) - nlopt_force_stop(std::get<2>(*tdata)); + if (tdata->self->m_stopcr.stop_condition()) + nlopt_force_stop(tdata->opt_raw); - auto fnptr = std::get<0>(*tdata); auto funval = to_arr(params); double scoreval = 0.; - using RetT = decltype((*fnptr)(funval)); + using RetT = decltype(tdata->fn(funval)); if constexpr (std::is_convertible_v>) { - ScoreGradient score = (*fnptr)(funval); + ScoreGradient score = tdata->fn(funval); for (size_t i = 0; i < n; ++i) gradient[i] = (*score.gradient)[i]; scoreval = score.score; } else { - scoreval = (*fnptr)(funval); + scoreval = tdata->fn(funval); } return scoreval; } + template + static double constrain_func(unsigned n, const double *params, + double *gradient, void *data) + { + assert(n == N); + + auto tdata = static_cast*>(data); + auto funval = to_arr(params); + + return tdata->fn(funval); + } + template - void set_up(NLopt &nl, const Bounds& bounds) + static void set_up(NLoptRAII &nl, + const Bounds &bounds, + const StopCriteria &stopcr) { std::array lb, ub; @@ -114,23 +208,45 @@ protected: nlopt_set_lower_bounds(nl.ptr, lb.data()); nlopt_set_upper_bounds(nl.ptr, ub.data()); - double abs_diff = m_stopcr.abs_score_diff(); - double rel_diff = m_stopcr.rel_score_diff(); - double stopval = m_stopcr.stop_score(); + double abs_diff = stopcr.abs_score_diff(); + double rel_diff = stopcr.rel_score_diff(); + double stopval = stopcr.stop_score(); if(!std::isnan(abs_diff)) nlopt_set_ftol_abs(nl.ptr, abs_diff); if(!std::isnan(rel_diff)) nlopt_set_ftol_rel(nl.ptr, rel_diff); if(!std::isnan(stopval)) nlopt_set_stopval(nl.ptr, stopval); - if(m_stopcr.max_iterations() > 0) - nlopt_set_maxeval(nl.ptr, m_stopcr.max_iterations()); + if(stopcr.max_iterations() > 0) + nlopt_set_maxeval(nl.ptr, stopcr.max_iterations()); } - template - Result optimize(NLopt &nl, Fn &&fn, const Input &initvals) + template + Result optimize(NLoptRAII &nl, Fn &&fn, const Input &initvals, + const std::tuple &equalities, + const std::tuple &inequalities) { Result r; - TOptData data = std::make_tuple(&fn, this, nl.ptr); + OptData data {fn, this, nl.ptr}; + + auto do_for_each_eq = [this, &nl](auto &arg) { + arg.self = this; + arg.opt_raw = nl.ptr; + using F = decltype(arg.fn); + nlopt_add_equality_constraint (nl.ptr, constrain_func, &arg, ConstraintEps); + }; + + auto do_for_each_ineq = [this, &nl](auto &arg) { + arg.self = this; + arg.opt_raw = nl.ptr; + using F = decltype(arg.fn); + nlopt_add_inequality_constraint (nl.ptr, constrain_func, &arg, ConstraintEps); + }; + + auto eq_data = wrap_tup(equalities); + for_each_in_tuple(do_for_each_eq, eq_data); + + auto ineq_data = wrap_tup(inequalities); + for_each_in_tuple(do_for_each_ineq, ineq_data); switch(m_dir) { case OptDir::MIN: @@ -147,51 +263,81 @@ protected: public: - template - Result optimize(Func&& func, + template + Result optimize(Fn&& f, const Input &initvals, - const Bounds& bounds) + const Bounds& bounds, + const std::tuple &equalities, + const std::tuple &inequalities) { - NLopt nl{alg, N}; - set_up(nl, bounds); + if constexpr (IsAUGLAG) { + NLoptRAII nl_wrap{NLOPT_AUGLAG, N}; + set_up(nl_wrap, bounds, get_criteria()); - return optimize(nl, std::forward(func), initvals); + NLoptRAII nl_glob{GetNLoptAlg_Global, N}; + set_up(nl_glob, bounds, get_criteria()); + nlopt_set_local_optimizer(nl_wrap.ptr, nl_glob.ptr); + + if constexpr (GetNLoptAlg_Local < NLOPT_NUM_ALGORITHMS) { + NLoptRAII nl_loc{GetNLoptAlg_Local, N}; + set_up(nl_loc, bounds, m_loc_stopcr); + nlopt_set_local_optimizer(nl_glob.ptr, nl_loc.ptr); + + return optimize(nl_wrap, std::forward(f), initvals, + equalities, inequalities); + } else { + return optimize(nl_wrap, std::forward(f), initvals, + equalities, inequalities); + } + } else { + NLoptRAII nl_glob{GetNLoptAlg_Global, N}; + set_up(nl_glob, bounds, get_criteria()); + + if constexpr (GetNLoptAlg_Local < NLOPT_NUM_ALGORITHMS) { + NLoptRAII nl_loc{GetNLoptAlg_Local, N}; + set_up(nl_loc, bounds, m_loc_stopcr); + nlopt_set_local_optimizer(nl_glob.ptr, nl_loc.ptr); + + return optimize(nl_glob, std::forward(f), initvals, + equalities, inequalities); + } else { + return optimize(nl_glob, std::forward(f), initvals, + equalities, inequalities); + } + } + + assert(false); + + return {}; } - explicit NLoptOpt(StopCriteria stopcr = {}) : m_stopcr(stopcr) {} + explicit NLoptOpt(const StopCriteria &stopcr_glob = {}) + : m_stopcr(stopcr_glob) + {} void set_criteria(const StopCriteria &cr) { m_stopcr = cr; } const StopCriteria &get_criteria() const noexcept { return m_stopcr; } - void set_dir(OptDir dir) noexcept { m_dir = dir; } + void set_loc_criteria(const StopCriteria &cr) { m_loc_stopcr = cr; } + const StopCriteria &get_loc_criteria() const noexcept { return m_loc_stopcr; } + + void set_dir(OptDir dir) noexcept { m_dir = dir; } void seed(long s) { nlopt_srand(s); } }; -template -class NLoptOpt>: public NLoptOpt> -{ - using Base = NLoptOpt>; -public: - - template - Result optimize(Fn&& f, - const Input &initvals, - const Bounds& bounds) - { - NLopt nl_glob{glob, N}, nl_loc{loc, N}; - - Base::set_up(nl_glob, bounds); - Base::set_up(nl_loc, bounds); - nlopt_set_local_optimizer(nl_glob.ptr, nl_loc.ptr); - - return Base::optimize(nl_glob, std::forward(f), initvals); - } - - explicit NLoptOpt(StopCriteria stopcr = {}) : Base{stopcr} {} +template struct AlgFeatures_ { + static constexpr bool SupportsInequalities = false; + static constexpr bool SupportsEqualities = false; }; } // namespace detail; +template constexpr bool SupportsEqualities = + detail::AlgFeatures_>::SupportsEqualities; + +template constexpr bool SupportsInequalities = + detail::AlgFeatures_>::SupportsInequalities; + // Optimizers based on NLopt. template class Optimizer> { detail::NLoptOpt m_opt; @@ -201,12 +347,24 @@ public: Optimizer& to_max() { m_opt.set_dir(detail::OptDir::MAX); return *this; } Optimizer& to_min() { m_opt.set_dir(detail::OptDir::MIN); return *this; } - template + template Result optimize(Func&& func, const Input &initvals, - const Bounds& bounds) + const Bounds& bounds, + const std::tuple &eq_constraints = {}, + const std::tuple &ineq_constraint = {}) { - return m_opt.optimize(std::forward(func), initvals, bounds); + static_assert(std::tuple_size_v> == 0 + || SupportsEqualities, + "Equality constraints are not supported."); + + static_assert(std::tuple_size_v> == 0 + || SupportsInequalities, + "Inequality constraints are not supported."); + + return m_opt.optimize(std::forward(func), initvals, bounds, + eq_constraints, + ineq_constraint); } explicit Optimizer(StopCriteria stopcr = {}) : m_opt(stopcr) {} @@ -219,14 +377,56 @@ public: const StopCriteria &get_criteria() const { return m_opt.get_criteria(); } void seed(long s) { m_opt.seed(s); } + + void set_loc_criteria(const StopCriteria &cr) { m_opt.set_loc_criteria(cr); } + const StopCriteria &get_loc_criteria() const noexcept { return m_opt.get_loc_criteria(); } }; // Predefinded NLopt algorithms -using AlgNLoptGenetic = detail::NLoptAlgComb; -using AlgNLoptSubplex = detail::NLoptAlg; -using AlgNLoptSimplex = detail::NLoptAlg; -using AlgNLoptDIRECT = detail::NLoptAlg; -using AlgNLoptMLSL = detail::NLoptAlg; +using AlgNLoptGenetic = detail::NLoptAlgComb; +using AlgNLoptSubplex = detail::NLoptAlg; +using AlgNLoptSimplex = detail::NLoptAlg; +using AlgNLoptCobyla = detail::NLoptAlg; +using AlgNLoptDIRECT = detail::NLoptAlg; +using AlgNLoptORIG_DIRECT = detail::NLoptAlg; +using AlgNLoptISRES = detail::NLoptAlg; +using AlgNLoptAGS = detail::NLoptAlg; + +using AlgNLoptMLSL_Subplx = detail::NLoptAlgComb; +using AlgNLoptMLSL_Cobyla = detail::NLoptAlgComb; +using AlgNLoptGenetic_Subplx = detail::NLoptAlgComb; + +// To craft auglag algorithms (constraint support through object function transformation) +using detail::NLoptAUGLAG; + +namespace detail { + +template<> struct AlgFeatures_ { + static constexpr bool SupportsInequalities = true; + static constexpr bool SupportsEqualities = true; +}; + +template<> struct AlgFeatures_ { + static constexpr bool SupportsInequalities = true; + static constexpr bool SupportsEqualities = false; +}; + +template<> struct AlgFeatures_ { + static constexpr bool SupportsInequalities = true; + static constexpr bool SupportsEqualities = false; +}; + +template<> struct AlgFeatures_ { + static constexpr bool SupportsInequalities = true; + static constexpr bool SupportsEqualities = true; +}; + +template struct AlgFeatures_> { + static constexpr bool SupportsInequalities = true; + static constexpr bool SupportsEqualities = true; +}; + +} // namespace detail }} // namespace Slic3r::opt diff --git a/src/libslic3r/Optimize/Optimizer.hpp b/src/libslic3r/Optimize/Optimizer.hpp index bf95d9ee0..6212a5f59 100644 --- a/src/libslic3r/Optimize/Optimizer.hpp +++ b/src/libslic3r/Optimize/Optimizer.hpp @@ -12,6 +12,15 @@ namespace Slic3r { namespace opt { +template +using FloatingOnly = std::enable_if_t::value, O>; + +template> +constexpr T NaN = std::numeric_limits::quiet_NaN(); + +constexpr float NaNf = NaN; +constexpr double NaNd = NaN; + // A type to hold the complete result of the optimization. template struct Result { int resultcode; // Method dependent @@ -79,7 +88,7 @@ public: double stop_score() const { return m_stop_score; } - StopCriteria & max_iterations(double val) + StopCriteria & max_iterations(unsigned val) { m_max_iterations = val; return *this; } @@ -137,16 +146,25 @@ public: // For each dimension an interval (Bound) has to be given marking the bounds // for that dimension. // + // Optionally, some constraints can be given in the form of double(Input) + // functors. The parameters eq_constraints and ineq_constraints can be used + // to add equality and inequality (<= 0) constraints to the optimization. + // Note that it is up the the particular method if it accepts these + // constraints. + // // initvals have to be within the specified bounds, otherwise its undefined // behavior. // // Func can return a score of type double or optionally a ScoreGradient // class to indicate the function gradient for a optimization methods that // make use of the gradient. - template + template Result optimize(Func&& /*func*/, const Input &/*initvals*/, - const Bounds& /*bounds*/) { return {}; } + const Bounds& /*bounds*/, + const std::tuple &eq_constraints = {}, + const std::tuple &ineq_constraint = {} + ) { return {}; } // optional for randomized methods: void seed(long /*s*/) {} diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 206844b7b..d6a4692f3 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -146,6 +146,11 @@ VendorProfile VendorProfile::from_ini(const ptree &tree, const boost::filesystem res.changelog_url = changelog_url->second.data(); } + const auto templates_profile = vendor_section.find("templates_profile"); + if (templates_profile != vendor_section.not_found()) { + res.templates_profile = templates_profile->second.data() == "1"; + } + if (! load_all) { return res; } @@ -200,6 +205,10 @@ VendorProfile VendorProfile::from_ini(const ptree &tree, const boost::filesystem } model.bed_model = section.second.get("bed_model", ""); model.bed_texture = section.second.get("bed_texture", ""); + model.thumbnail = section.second.get("thumbnail", ""); + if (model.thumbnail.empty()) + model.thumbnail = model.id + "_thumbnail.png"; + if (! model.id.empty() && ! model.variants.empty()) res.models.push_back(std::move(model)); } @@ -336,7 +345,8 @@ std::string Preset::label() const bool is_compatible_with_print(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_print, const PresetWithVendorProfile &active_printer) { - if (preset.vendor != nullptr && preset.vendor != active_printer.vendor) + // templates_profile vendor profiles should be decided as same vendor profiles + if (preset.vendor != nullptr && preset.vendor != active_printer.vendor && !preset.vendor->templates_profile) // The current profile has a vendor assigned and it is different from the active print's vendor. return false; auto &condition = preset.preset.compatible_prints_condition(); @@ -358,7 +368,8 @@ bool is_compatible_with_print(const PresetWithVendorProfile &preset, const Prese bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_printer, const DynamicPrintConfig *extra_config) { - if (preset.vendor != nullptr && preset.vendor != active_printer.vendor) + // templates_profile vendor profiles should be decided as same vendor profiles + if (preset.vendor != nullptr && preset.vendor != active_printer.vendor && !preset.vendor->templates_profile) // The current profile has a vendor assigned and it is different from the active print's vendor. return false; auto &condition = preset.preset.compatible_printers_condition(); @@ -439,7 +450,9 @@ static std::vector s_Preset_print_options { "support_material_synchronize_layers", "support_material_angle", "support_material_interface_layers", "support_material_bottom_interface_layers", "support_material_interface_pattern", "support_material_interface_spacing", "support_material_interface_contact_loops", "support_material_contact_distance", "support_material_bottom_contact_distance", - "support_material_buildplate_only", "dont_support_bridges", "thick_bridges", "notes", "complete_objects", "extruder_clearance_radius", + "support_material_buildplate_only", + "support_tree_angle", "support_tree_angle_slow", "support_tree_branch_diameter", "support_tree_branch_diameter_angle", "support_tree_top_rate", "support_tree_tip_diameter", + "dont_support_bridges", "thick_bridges", "notes", "complete_objects", "extruder_clearance_radius", "extruder_clearance_height", "gcode_comments", "gcode_label_objects", "output_filename_format", "post_process", "gcode_substitutions", "perimeter_extruder", "infill_extruder", "solid_infill_extruder", "support_material_extruder", "support_material_interface_extruder", "ooze_prevention", "standby_temperature_delta", "interface_shells", "extrusion_width", "first_layer_extrusion_width", @@ -457,7 +470,7 @@ static std::vector s_Preset_filament_options { "extrusion_multiplier", "filament_density", "filament_cost", "filament_spool_weight", "filament_loading_speed", "filament_loading_speed_start", "filament_load_time", "filament_unloading_speed", "filament_unloading_speed_start", "filament_unload_time", "filament_toolchange_delay", "filament_cooling_moves", "filament_cooling_initial_speed", "filament_cooling_final_speed", "filament_ramming_parameters", "filament_minimal_purge_on_wipe_tower", - "temperature", "first_layer_temperature", "bed_temperature", "first_layer_bed_temperature", "fan_always_on", "cooling", "min_fan_speed", + "temperature", "idle_temperature", "first_layer_temperature", "bed_temperature", "first_layer_bed_temperature", "fan_always_on", "cooling", "min_fan_speed", "max_fan_speed", "bridge_fan_speed", "disable_fan_first_layers", "full_fan_speed_layer", "fan_below_layer_time", "slowdown_below_layer_time", "min_print_speed", "start_filament_gcode", "end_filament_gcode", // Retract overrides @@ -495,14 +508,17 @@ static std::vector s_Preset_sla_print_options { "faded_layers", "supports_enable", "support_tree_type", + "support_head_front_diameter", "support_head_penetration", "support_head_width", "support_pillar_diameter", "support_small_pillar_diameter_percent", "support_max_bridges_on_pillar", + "support_max_weight_on_model", "support_pillar_connection_mode", "support_buildplate_only", + "support_enforcers_only", "support_pillar_widening_factor", "support_base_diameter", "support_base_height", @@ -511,6 +527,25 @@ static std::vector s_Preset_sla_print_options { "support_max_bridge_length", "support_max_pillar_link_distance", "support_object_elevation", + + "branchingsupport_head_front_diameter", + "branchingsupport_head_penetration", + "branchingsupport_head_width", + "branchingsupport_pillar_diameter", + "branchingsupport_small_pillar_diameter_percent", + "branchingsupport_max_bridges_on_pillar", + "branchingsupport_max_weight_on_model", + "branchingsupport_pillar_connection_mode", + "branchingsupport_buildplate_only", + "branchingsupport_pillar_widening_factor", + "branchingsupport_base_diameter", + "branchingsupport_base_height", + "branchingsupport_base_safety_distance", + "branchingsupport_critical_angle", + "branchingsupport_max_bridge_length", + "branchingsupport_max_pillar_link_distance", + "branchingsupport_object_elevation", + "support_points_density_relative", "support_points_minimal_distance", "slice_closing_radius", @@ -1164,6 +1199,7 @@ size_t PresetCollection::update_compatible_internal(const PresetWithVendorProfil if (opt) config.set_key_value("num_extruders", new ConfigOptionInt((int)static_cast(opt)->values.size())); bool some_compatible = false; + std::vector indices_of_template_presets; for (size_t idx_preset = m_num_default_presets; idx_preset < m_presets.size(); ++ idx_preset) { bool selected = idx_preset == m_idx_selected; Preset &preset_selected = m_presets[idx_preset]; @@ -1180,7 +1216,29 @@ size_t PresetCollection::update_compatible_internal(const PresetWithVendorProfil m_idx_selected = size_t(-1); if (selected) preset_selected.is_compatible = preset_edited.is_compatible; + if (preset_edited.vendor && preset_edited.vendor->templates_profile) { + indices_of_template_presets.push_back(idx_preset); + } } + // filter out template profiles where profile with same alias and compability exists + if (!indices_of_template_presets.empty()) { + for (size_t idx_preset = m_num_default_presets; idx_preset < m_presets.size(); ++idx_preset) { + if (m_presets[idx_preset].vendor && !m_presets[idx_preset].vendor->templates_profile && m_presets[idx_preset].is_compatible) { + std::string preset_alias = m_presets[idx_preset].alias; + for (size_t idx_of_template_in_presets : indices_of_template_presets) { + if (m_presets[idx_of_template_in_presets].alias == preset_alias) { + // unselect selected template filament if there is non-template alias compatible + if (idx_of_template_in_presets == m_idx_selected && (unselect_if_incompatible == PresetSelectCompatibleType::Always || unselect_if_incompatible == PresetSelectCompatibleType::OnlyIfWasCompatible)) { + m_idx_selected = size_t(-1); + } + m_presets[idx_of_template_in_presets].is_compatible = false; + break; + } + } + } + } + } + // Update visibility of the default profiles here if the defaults are suppressed, the current profile is not compatible and we don't want to select another compatible profile. if (m_idx_selected >= m_num_default_presets && m_default_suppressed) for (size_t i = 0; i < m_num_default_presets; ++ i) @@ -2078,6 +2136,25 @@ namespace PresetUtils { } return out; } + + bool vendor_profile_has_all_resources(const VendorProfile& vp) + { + namespace fs = boost::filesystem; + + std::string vendor_folder = Slic3r::data_dir() + "/vendor/" + vp.id + "/"; + std::string rsrc_folder = Slic3r::resources_dir() + "/profiles/" + vp.id + "/"; + std::string cache_folder = Slic3r::data_dir() + "/cache/" + vp.id + "/"; + for (const VendorProfile::PrinterModel& model : vp.models) { + for (const std::string& res : { model.bed_texture, model.bed_model, model.thumbnail } ) { + if (! res.empty() + && !fs::exists(fs::path(vendor_folder + res)) + && !fs::exists(fs::path(rsrc_folder + res)) + && !fs::exists(fs::path(cache_folder + res))) + return false; + } + } + return true; + } } // namespace PresetUtils } // namespace Slic3r diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index 74ba2e0f5..9a16a16a9 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -34,6 +34,7 @@ public: Semver config_version; std::string config_update_url; std::string changelog_url; + bool templates_profile { false }; struct PrinterVariant { PrinterVariant() {} @@ -52,6 +53,7 @@ public: // Vendor & Printer Model specific print bed model & texture. std::string bed_model; std::string bed_texture; + std::string thumbnail; PrinterVariant* variant(const std::string &name) { for (auto &v : this->variants) @@ -619,6 +621,7 @@ namespace PresetUtils { const VendorProfile::PrinterModel* system_printer_model(const Preset &preset); std::string system_printer_bed_model(const Preset& preset); std::string system_printer_bed_texture(const Preset& preset); + bool vendor_profile_has_all_resources(const VendorProfile& vp); } // namespace PresetUtils diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index b783c8aeb..899d24ecc 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -159,6 +159,7 @@ void PresetBundle::setup_directories() data_dir, data_dir / "vendor", data_dir / "cache", + data_dir / "cache" / "vendor", data_dir / "shapes", #ifdef SLIC3R_PROFILE_USE_PRESETS_SUBDIR // Store the print/filament/printer presets into a "presets" directory. @@ -1313,11 +1314,11 @@ std::pair PresetBundle::load_configbundle( const VendorProfile *vendor_profile = nullptr; if (flags.has(LoadConfigBundleAttribute::LoadSystem) || flags.has(LoadConfigBundleAttribute::LoadVendorOnly)) { - auto vp = VendorProfile::from_ini(tree, path); - if (vp.models.size() == 0) { + VendorProfile vp = VendorProfile::from_ini(tree, path); + if (vp.models.size() == 0 && !vp.templates_profile) { BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No printer model defined.") % path; return std::make_pair(PresetsConfigSubstitutions{}, 0); - } else if (vp.num_variants() == 0) { + } else if (vp.num_variants() == 0 && !vp.templates_profile) { BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No printer variant defined") % path; return std::make_pair(PresetsConfigSubstitutions{}, 0); } @@ -1359,6 +1360,9 @@ std::pair PresetBundle::load_configbundle( } else if (boost::starts_with(section.first, "filament:")) { presets = &this->filaments; preset_name = section.first.substr(9); + if (vendor_profile && vendor_profile->templates_profile) { + preset_name += " @Template"; + } } else if (boost::starts_with(section.first, "sla_print:")) { presets = &this->sla_prints; preset_name = section.first.substr(10); diff --git a/src/libslic3r/PrincipalComponents2D.cpp b/src/libslic3r/PrincipalComponents2D.cpp index 4b7c3a1da..7bdf79315 100644 --- a/src/libslic3r/PrincipalComponents2D.cpp +++ b/src/libslic3r/PrincipalComponents2D.cpp @@ -3,53 +3,97 @@ namespace Slic3r { -// returns two eigenvectors of the area covered by given polygons. The vectors are sorted by their corresponding eigenvalue, largest first -std::tuple compute_principal_components(const Polygons &polys, const double unscaled_resolution) + + +// returns triangle area, first_moment_of_area_xy, second_moment_of_area_xy, second_moment_of_area_covariance +// none of the values is divided/normalized by area. +// The function computes intgeral over the area of the triangle, with function f(x,y) = x for first moments of area (y is analogous) +// f(x,y) = x^2 for second moment of area +// and f(x,y) = x*y for second moment of area covariance +std::tuple compute_moments_of_area_of_triangle(const Vec2f &a, const Vec2f &b, const Vec2f &c) { - // USING UNSCALED VALUES - const Vec2d pixel_size = Vec2d(unscaled_resolution, unscaled_resolution); - const auto bb = get_extents(polys); - const Vec2i pixel_count = unscaled(bb.size()).cwiseQuotient(pixel_size).cast() + Vec2i::Ones(); + // based on the following guide: + // Denote the vertices of S by a, b, c. Then the map + // g:(u,v)↦a+u(b−a)+v(c−a) , + // which in coordinates appears as + // g:(u,v)↦{x(u,v)y(u,v)=a1+u(b1−a1)+v(c1−a1)=a2+u(b2−a2)+v(c2−a2) ,(1) + // obviously maps S′ bijectively onto S. Therefore the transformation formula for multiple integrals steps into action, and we obtain + // ∫Sf(x,y)d(x,y)=∫S′f(x(u,v),y(u,v))∣∣Jg(u,v)∣∣ d(u,v) . + // In the case at hand the Jacobian determinant is a constant: From (1) we obtain + // Jg(u,v)=det[xuyuxvyv]=(b1−a1)(c2−a2)−(c1−a1)(b2−a2) . + // Therefore we can write + // ∫Sf(x,y)d(x,y)=∣∣Jg∣∣∫10∫1−u0f~(u,v) dv du , + // where f~ denotes the pullback of f to S′: + // f~(u,v):=f(x(u,v),y(u,v)) . + // Don't forget taking the absolute value of Jg! - std::vector lines{}; - for (Line l : to_lines(polys)) { lines.emplace_back(unscaled(l.a), unscaled(l.b)); } - AABBTreeIndirect::Tree<2, double> tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); - auto is_inside = [&](const Vec2d &point) { - size_t nearest_line_index_out = 0; - Vec2d nearest_point_out = Vec2d::Zero(); - auto distance = AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, point, nearest_line_index_out, nearest_point_out); - if (distance < 0) return false; - const Linef &line = lines[nearest_line_index_out]; - Vec2d v1 = line.b - line.a; - Vec2d v2 = point - line.a; - if ((v1.x() * v2.y()) - (v1.y() * v2.x()) > 0.0) { return true; } - return false; - }; + float jacobian_determinant_abs = std::abs((b.x() - a.x()) * (c.y() - a.y()) - (c.x() - a.x()) * (b.y() - a.y())); - double pixel_area = pixel_size.x() * pixel_size.y(); - Vec2d centroid_accumulator = Vec2d::Zero(); - Vec2d second_moment_of_area_accumulator = Vec2d::Zero(); - double second_moment_of_area_covariance_accumulator = 0.0; - double area = 0.0; + // coordinate transform: gx(u,v) = a.x + u * (b.x - a.x) + v * (c.x - a.x) + // coordinate transform: gy(u,v) = a.y + u * (b.y - a.y) + v * (c.y - a.y) + // second moment of area for x: f(x, y) = x^2; + // f(gx(u,v), gy(u,v)) = gx(u,v)^2 = ... (long expanded form) - for (int x = 0; x < pixel_count.x(); x++) { - for (int y = 0; y < pixel_count.y(); y++) { - Vec2d position = unscaled(bb.min) + pixel_size.cwiseProduct(Vec2d{x, y}); - if (is_inside(position)) { - area += pixel_area; - centroid_accumulator += pixel_area * position; - second_moment_of_area_accumulator += pixel_area * position.cwiseProduct(position); - second_moment_of_area_covariance_accumulator += pixel_area * position.x() * position.y(); - } + // result is Int_T func = jacobian_determinant_abs * Int_0^1 Int_0^1-u func(gx(u,v), gy(u,v)) dv du + // integral_0^1 integral_0^(1 - u) (a + u (b - a) + v (c - a))^2 dv du = 1/12 (a^2 + a (b + c) + b^2 + b c + c^2) + + Vec2f second_moment_of_area_xy = jacobian_determinant_abs * + (a.cwiseProduct(a) + b.cwiseProduct(b) + b.cwiseProduct(c) + c.cwiseProduct(c) + + a.cwiseProduct(b + c)) / + 12.0f; + // second moment of area covariance : f(x, y) = x*y; + // f(gx(u,v), gy(u,v)) = gx(u,v)*gy(u,v) = ... (long expanded form) + //(a_1 + u * (b_1 - a_1) + v * (c_1 - a_1)) * (a_2 + u * (b_2 - a_2) + v * (c_2 - a_2)) + // == (a_1 + u (b_1 - a_1) + v (c_1 - a_1)) (a_2 + u (b_2 - a_2) + v (c_2 - a_2)) + + // intermediate result: integral_0^(1 - u) (a_1 + u (b_1 - a_1) + v (c_1 - a_1)) (a_2 + u (b_2 - a_2) + v (c_2 - a_2)) dv = + // 1/6 (u - 1) (-c_1 (u - 1) (a_2 (u - 1) - 3 b_2 u) - c_2 (u - 1) (a_1 (u - 1) - 3 b_1 u + 2 c_1 (u - 1)) + 3 b_1 u (a_2 (u - 1) - 2 + // b_2 u) + a_1 (u - 1) (3 b_2 u - 2 a_2 (u - 1))) result = integral_0^1 1/6 (u - 1) (-c_1 (u - 1) (a_2 (u - 1) - 3 b_2 u) - c_2 (u - + // 1) (a_1 (u - 1) - 3 b_1 u + 2 c_1 (u - 1)) + 3 b_1 u (a_2 (u - 1) - 2 b_2 u) + a_1 (u - 1) (3 b_2 u - 2 a_2 (u - 1))) du = + // 1/24 (a_2 (b_1 + c_1) + a_1 (2 a_2 + b_2 + c_2) + b_2 c_1 + b_1 c_2 + 2 b_1 b_2 + 2 c_1 c_2) + // result is Int_T func = jacobian_determinant_abs * Int_0^1 Int_0^1-u func(gx(u,v), gy(u,v)) dv du + float second_moment_of_area_covariance = jacobian_determinant_abs * (1.0f / 24.0f) * + (a.y() * (b.x() + c.x()) + a.x() * (2.0f * a.y() + b.y() + c.y()) + b.y() * c.x() + + b.x() * c.y() + 2.0f * b.x() * b.y() + 2.0f * c.x() * c.y()); + + float area = jacobian_determinant_abs * 0.5f; + + Vec2f first_moment_of_area_xy = jacobian_determinant_abs * (a + b + c) / 6.0f; + + return {area, first_moment_of_area_xy, second_moment_of_area_xy, second_moment_of_area_covariance}; +}; + +// returns two eigenvectors of the area covered by given polygons. The vectors are sorted by their corresponding eigenvalue, largest first +std::tuple compute_principal_components(const Polygons &polys) +{ + Vec2f centroid_accumulator = Vec2f::Zero(); + Vec2f second_moment_of_area_accumulator = Vec2f::Zero(); + float second_moment_of_area_covariance_accumulator = 0.0f; + float area = 0.0f; + + for (const Polygon &poly : polys) { + Vec2f p0 = unscaled(poly.first_point()).cast(); + for (size_t i = 2; i < poly.points.size(); i++) { + Vec2f p1 = unscaled(poly.points[i - 1]).cast(); + Vec2f p2 = unscaled(poly.points[i]).cast(); + + float sign = cross2(p1 - p0, p2 - p1) > 0 ? 1.0f : -1.0f; + + auto [triangle_area, first_moment_of_area, second_moment_area, + second_moment_of_area_covariance] = compute_moments_of_area_of_triangle(p0, p1, p2); + area += sign * triangle_area; + centroid_accumulator += sign * first_moment_of_area; + second_moment_of_area_accumulator += sign * second_moment_area; + second_moment_of_area_covariance_accumulator += sign * second_moment_of_area_covariance; } } if (area <= 0.0) { - return {Vec2d::Zero(), Vec2d::Zero()}; + return {Vec2f::Zero(), Vec2f::Zero()}; } - Vec2d centroid = centroid_accumulator / area; - Vec2d variance = second_moment_of_area_accumulator / area - centroid.cwiseProduct(centroid); + Vec2f centroid = centroid_accumulator / area; + Vec2f variance = second_moment_of_area_accumulator / area - centroid.cwiseProduct(centroid); double covariance = second_moment_of_area_covariance_accumulator / area - centroid.x() * centroid.y(); #if 0 std::cout << "area : " << area << std::endl; @@ -58,7 +102,7 @@ std::tuple compute_principal_components(const Polygons &polys, con std::cout << "covariance : " << covariance << std::endl; #endif if (abs(covariance) < EPSILON) { - std::tuple result{Vec2d{variance.x(), 0.0}, Vec2d{0.0, variance.y()}}; + std::tuple result{Vec2f{variance.x(), 0.0}, Vec2f{0.0, variance.y()}}; if (variance.y() > variance.x()) { return {std::get<1>(result), std::get<0>(result)}; } else @@ -72,12 +116,12 @@ std::tuple compute_principal_components(const Polygons &polys, con // Eigenvalues are solutions to det(C - lI) = 0, where l is the eigenvalue and I unit matrix // Eigenvector for eigenvalue l is any vector v such that Cv = lv - double eigenvalue_a = 0.5 * (variance.x() + variance.y() + - sqrt((variance.x() - variance.y()) * (variance.x() - variance.y()) + 4 * covariance * covariance)); - double eigenvalue_b = 0.5 * (variance.x() + variance.y() - - sqrt((variance.x() - variance.y()) * (variance.x() - variance.y()) + 4 * covariance * covariance)); - Vec2d eigenvector_a{(eigenvalue_a - variance.y()) / covariance, 1.0}; - Vec2d eigenvector_b{(eigenvalue_b - variance.y()) / covariance, 1.0}; + float eigenvalue_a = 0.5f * (variance.x() + variance.y() + + sqrt((variance.x() - variance.y()) * (variance.x() - variance.y()) + 4.0f * covariance * covariance)); + float eigenvalue_b = 0.5f * (variance.x() + variance.y() - + sqrt((variance.x() - variance.y()) * (variance.x() - variance.y()) + 4.0f * covariance * covariance)); + Vec2f eigenvector_a{(eigenvalue_a - variance.y()) / covariance, 1.0f}; + Vec2f eigenvector_b{(eigenvalue_b - variance.y()) / covariance, 1.0f}; #if 0 std::cout << "eigenvalue_a: " << eigenvalue_a << std::endl; diff --git a/src/libslic3r/PrincipalComponents2D.hpp b/src/libslic3r/PrincipalComponents2D.hpp index 0eccdfcc5..dc8897a7a 100644 --- a/src/libslic3r/PrincipalComponents2D.hpp +++ b/src/libslic3r/PrincipalComponents2D.hpp @@ -9,8 +9,15 @@ namespace Slic3r { +// returns triangle area, first_moment_of_area_xy, second_moment_of_area_xy, second_moment_of_area_covariance +// none of the values is divided/normalized by area. +// The function computes intgeral over the area of the triangle, with function f(x,y) = x for first moments of area (y is analogous) +// f(x,y) = x^2 for second moment of area +// and f(x,y) = x*y for second moment of area covariance +std::tuple compute_moments_of_area_of_triangle(const Vec2f &a, const Vec2f &b, const Vec2f &c); + // returns two eigenvectors of the area covered by given polygons. The vectors are sorted by their corresponding eigenvalue, largest first -std::tuple compute_principal_components(const Polygons &polys, const double unscaled_resolution); +std::tuple compute_principal_components(const Polygons &polys); } diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index e7305234f..468e4c3d1 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -191,6 +191,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "infill_first" || opt_key == "single_extruder_multi_material" || opt_key == "temperature" + || opt_key == "idle_temperature" || opt_key == "wipe_tower" || opt_key == "wipe_tower_width" || opt_key == "wipe_tower_brim_width" @@ -347,7 +348,7 @@ std::vector Print::print_object_ids() const bool Print::has_infinite_skirt() const { - return (m_config.draft_shield == dsEnabled && m_config.skirts > 0) || (m_config.ooze_prevention && this->extruders().size() > 1); + return (m_config.draft_shield == dsEnabled && m_config.skirts > 0)/* || (m_config.ooze_prevention && this->extruders().size() > 1)*/; } bool Print::has_skirt() const @@ -404,7 +405,7 @@ bool Print::sequential_print_horizontal_clearance_valid(const Print& print, Poly } // Make a copy, so it may be rotated for instances. Polygon convex_hull0 = it_convex_hull->second; - const double z_diff = Geometry::rotation_diff_z(model_instance0->get_rotation(), print_object->instances().front().model_instance->get_rotation()); + const double z_diff = Geometry::rotation_diff_z(model_instance0->get_matrix(), print_object->instances().front().model_instance->get_matrix()); if (std::abs(z_diff) > EPSILON) convex_hull0.rotate(z_diff); // Now we check that no instance of convex_hull intersects any of the previously checked object instances. @@ -505,8 +506,8 @@ std::string Print::validate(std::string* warning) const return L("The Wipe Tower is currently only supported for the Marlin, RepRap/Sprinter, RepRapFirmware and Repetier G-code flavors."); if (! m_config.use_relative_e_distances) return L("The Wipe Tower is currently only supported with the relative extruder addressing (use_relative_e_distances=1)."); - if (m_config.ooze_prevention) - return L("Ooze prevention is currently not supported with the wipe tower enabled."); + if (m_config.ooze_prevention && m_config.single_extruder_multi_material) + return L("Ooze prevention is only supported with the wipe tower when 'single_extruder_multi_material' is off."); if (m_config.use_volumetric_e) return L("The Wipe Tower currently does not support volumetric E (use_volumetric_e=0)."); if (m_config.complete_objects && extruders.size() > 1) diff --git a/src/libslic3r/PrintBase.hpp b/src/libslic3r/PrintBase.hpp index 607a7b5a5..4c009cac8 100644 --- a/src/libslic3r/PrintBase.hpp +++ b/src/libslic3r/PrintBase.hpp @@ -740,6 +740,21 @@ public: PrintStateBase::StateWithTimeStamp step_state_with_timestamp(PrintObjectStepEnum step) const { return m_state.state_with_timestamp(step, PrintObjectBase::state_mutex(m_print)); } PrintStateBase::StateWithWarnings step_state_with_warnings(PrintObjectStepEnum step) const { return m_state.state_with_warnings(step, PrintObjectBase::state_mutex(m_print)); } + auto last_completed_step() const + { + static_assert(COUNT > 0, "Step count should be > 0"); + auto s = int(COUNT) - 1; + + std::lock_guard lk(state_mutex(m_print)); + while (s >= 0 && ! is_step_done_unguarded(PrintObjectStepEnum(s))) + --s; + + if (s < 0) + s = COUNT; + + return PrintObjectStepEnum(s); + } + protected: PrintObjectBaseWithState(PrintType *print, ModelObject *model_object) : PrintObjectBase(model_object), m_print(print) {} diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index bc6d81b2d..1d0446ce2 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -96,6 +96,7 @@ CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(FuzzySkinType) static const t_config_enum_values s_keys_map_InfillPattern { { "rectilinear", ipRectilinear }, { "monotonic", ipMonotonic }, + { "monotoniclines", ipMonotonicLines }, { "alignedrectilinear", ipAlignedRectilinear }, { "grid", ipGrid }, { "triangles", ipTriangles }, @@ -181,7 +182,8 @@ CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SLAMaterialSpeed); static inline const t_config_enum_values s_keys_map_SLASupportTreeType = { {"default", int(sla::SupportTreeType::Default)}, - {"branching", int(sla::SupportTreeType::Branching)} + {"branching", int(sla::SupportTreeType::Branching)}, + //TODO: {"organic", int(sla::SupportTreeType::Organic)} }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SLASupportTreeType); @@ -227,6 +229,11 @@ static void assign_printer_technology_to_unknown(t_optiondef_map &options, Print kvp.second.printer_technology = printer_technology; } +// Maximum extruder temperature, bumped to 1500 to support printing of glass. +namespace { + const int max_temp = 1500; +}; + PrintConfigDef::PrintConfigDef() { this->init_common_params(); @@ -768,20 +775,17 @@ void PrintConfigDef::init_fff_params() def->tooltip = L("Fill pattern for top infill. This only affects the top visible layer, and not its adjacent solid shells."); def->cli = "top-fill-pattern|external-fill-pattern|solid-fill-pattern"; def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); - def->enum_values.push_back("rectilinear"); - def->enum_values.push_back("monotonic"); - def->enum_values.push_back("alignedrectilinear"); - def->enum_values.push_back("concentric"); - def->enum_values.push_back("hilbertcurve"); - def->enum_values.push_back("archimedeanchords"); - def->enum_values.push_back("octagramspiral"); - def->enum_labels.push_back(L("Rectilinear")); - def->enum_labels.push_back(L("Monotonic")); - def->enum_labels.push_back(L("Aligned Rectilinear")); - def->enum_labels.push_back(L("Concentric")); - def->enum_labels.push_back(L("Hilbert Curve")); - def->enum_labels.push_back(L("Archimedean Chords")); - def->enum_labels.push_back(L("Octagram Spiral")); + def->set_enum_values({ + { "rectilinear", L("Rectilinear") }, + { "monotonic", L("Monotonic") }, + { "monotoniclines", L("Monotonic Lines") }, + { "alignedrectilinear", L("Aligned Rectilinear") }, + { "concentric", L("Concentric") }, + { "hilbertcurve", L("Hilbert Curve") }, + { "archimedeanchords", L("Archimedean Chords") }, + { "octagramspiral", L("Octagram Spiral") } + }); + // solid_fill_pattern is an obsolete equivalent to top_fill_pattern/bottom_fill_pattern. def->aliases = { "solid_fill_pattern", "external_fill_pattern" }; def->set_default_value(new ConfigOptionEnum(ipMonotonic)); @@ -1973,9 +1977,7 @@ void PrintConfigDef::init_fff_params() def = this->add("ooze_prevention", coBool); def->label = L("Enable"); - def->tooltip = L("This option will drop the temperature of the inactive extruders to prevent oozing. " - "It will enable a tall skirt automatically and move extruders outside such " - "skirt when changing temperatures."); + def->tooltip = L("This option will drop the temperature of the inactive extruders to prevent oozing. "); def->mode = comExpert; def->set_default_value(new ConfigOptionBool(false)); @@ -2476,7 +2478,8 @@ void PrintConfigDef::init_fff_params() def = this->add("standby_temperature_delta", coInt); def->label = L("Temperature variation"); def->tooltip = L("Temperature difference to be applied when an extruder is not active. " - "Enables a full-height \"sacrificial\" skirt on which the nozzles are periodically wiped."); + "The value is not used when 'idle_temperature' in filament settings " + "is defined."); def->sidetext = "∆°C"; def->min = -max_temp; def->max = max_temp; @@ -2875,6 +2878,71 @@ void PrintConfigDef::init_fff_params() def->mode = comExpert; def->set_default_value(new ConfigOptionBool(true)); + def = this->add("support_tree_angle", coFloat); + def->label = L("Tree Support Maximum Branch Angle"); + def->category = L("Support material"); + def->tooltip = L("The maximum angle of the branches, when the branches have to avoid the model. " + "Use a lower angle to make them more vertical and more stable. Use a higher angle to be able to have more reach."); + def->sidetext = L("°"); + def->min = 0; + def->max = 85; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(40)); + + def = this->add("support_tree_angle_slow", coFloat); + def->label = L("Tree Support Preferred Branch Angle"); + def->category = L("Support material"); + def->tooltip = L("The preferred angle of the branches, when they do not have to avoid the model. " + "Use a lower angle to make them more vertical and more stable. Use a higher angle for branches to merge faster."); + def->sidetext = L("°"); + def->min = 10; + def->max = 85; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(25)); + + def = this->add("support_tree_tip_diameter", coFloat); + def->label = L("Tree Support Tip Diameter"); + def->category = L("Support material"); + def->tooltip = L("The diameter of the top of the tip of the branches of tree support."); + def->sidetext = L("mm"); + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.8)); + + def = this->add("support_tree_branch_diameter", coFloat); + def->label = L("Tree Support Branch Diameter"); + def->category = L("Support material"); + def->tooltip = L("The diameter of the thinnest branches of tree support. Thicker branches are more sturdy. " + "Branches towards the base will be thicker than this."); + def->sidetext = L("mm"); + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(2)); + + def = this->add("support_tree_branch_diameter_angle", coFloat); + def->label = L("Tree Support Branch Diameter Angle"); + def->category = L("Support material"); + def->tooltip = L("The angle of the branches' diameter as they gradually become thicker towards the bottom. " + "An angle of 0 will cause the branches to have uniform thickness over their length. " + "A bit of an angle can increase stability of the tree support."); + def->sidetext = L("°"); + def->min = 0; + def->max = 15; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(5)); + + def = this->add("support_tree_top_rate", coPercent); + def->label = L("Tree Support Branch Density"); + def->category = L("Support material"); + def->tooltip = L("Adjusts the density of the support structure used to generate the tips of the branches. " + "A higher value results in better overhangs, but the supports are harder to remove. " + "Use Support Roof for very high values or ensure support density is similarly high at the top."); + def->sidetext = L("%"); + def->min = 5; + def->max_literal = 35; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionPercent(30)); + def = this->add("temperature", coInts); def->label = L("Other layers"); def->tooltip = L("Nozzle temperature for layers after the first one. Set this to zero to disable " @@ -3272,6 +3340,219 @@ void PrintConfigDef::init_extruder_option_keys() assert(std::is_sorted(m_extruder_retract_keys.begin(), m_extruder_retract_keys.end())); } +void PrintConfigDef::init_sla_support_params(const std::string &prefix) +{ + ConfigOptionDef* def; + + constexpr const char * pretext_unavailable = L("Unavailable for this method.\n"); + std::string pretext; + + def = this->add(prefix + "support_head_front_diameter", coFloat); + def->label = L("Pinhead front diameter"); + def->category = L("Supports"); + def->tooltip = L("Diameter of the pointing side of the head"); + def->sidetext = L("mm"); + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.4)); + + def = this->add(prefix + "support_head_penetration", coFloat); + def->label = L("Head penetration"); + def->category = L("Supports"); + def->tooltip = L("How much the pinhead has to penetrate the model surface"); + def->sidetext = L("mm"); + def->mode = comAdvanced; + def->min = 0; + def->set_default_value(new ConfigOptionFloat(0.2)); + + def = this->add(prefix + "support_head_width", coFloat); + def->label = L("Pinhead width"); + def->category = L("Supports"); + def->tooltip = L("Width from the back sphere center to the front sphere center"); + def->sidetext = L("mm"); + def->min = 0; + def->max = 20; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(1.0)); + + def = this->add(prefix + "support_pillar_diameter", coFloat); + def->label = L("Pillar diameter"); + def->category = L("Supports"); + def->tooltip = L("Diameter in mm of the support pillars"); + def->sidetext = L("mm"); + def->min = 0; + def->max = 15; + def->mode = comSimple; + def->set_default_value(new ConfigOptionFloat(1.0)); + + def = this->add(prefix + "support_small_pillar_diameter_percent", coPercent); + def->label = L("Small pillar diameter percent"); + def->category = L("Supports"); + def->tooltip = L("The percentage of smaller pillars compared to the normal pillar diameter " + "which are used in problematic areas where a normal pilla cannot fit."); + def->sidetext = L("%"); + def->min = 1; + def->max = 100; + def->mode = comExpert; + def->set_default_value(new ConfigOptionPercent(50)); + + pretext = ""; + if (prefix == "branching") + pretext = pretext_unavailable; + + def = this->add(prefix + "support_max_bridges_on_pillar", coInt); + def->label = L("Max bridges on a pillar"); + def->tooltip = pretext + L( + "Maximum number of bridges that can be placed on a pillar. Bridges " + "hold support point pinheads and connect to pillars as small branches."); + def->min = 0; + def->max = 50; + def->mode = comExpert; + def->set_default_value(new ConfigOptionInt(prefix == "branching" ? 2 : 3)); + + pretext = ""; + if (prefix.empty()) + pretext = pretext_unavailable; + + def = this->add(prefix + "support_max_weight_on_model", coFloat); + def->label = L("Max weight on model"); + def->category = L("Supports"); + def->tooltip = pretext + L( + "Maximum weight of sub-trees that terminate on the model instead of the print bed. The weight is the sum of the lenghts of all " + "branches emanating from the endpoint."); + def->sidetext = L("mm"); + def->min = 0; + def->mode = comExpert; + def->set_default_value(new ConfigOptionFloat(10.)); + + pretext = ""; + if (prefix == "branching") + pretext = pretext_unavailable; + + def = this->add(prefix + "support_pillar_connection_mode", coEnum); + def->label = L("Pillar connection mode"); + def->tooltip = pretext + L("Controls the bridge type between two neighboring pillars." + " Can be zig-zag, cross (double zig-zag) or dynamic which" + " will automatically switch between the first two depending" + " on the distance of the two pillars."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = ConfigOptionEnum::get_enum_names(); + def->enum_labels = ConfigOptionEnum::get_enum_names(); + def->enum_labels[0] = L("Zig-Zag"); + def->enum_labels[1] = L("Cross"); + def->enum_labels[2] = L("Dynamic"); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(SLAPillarConnectionMode::dynamic)); + + def = this->add(prefix + "support_buildplate_only", coBool); + def->label = L("Support on build plate only"); + def->category = L("Supports"); + def->tooltip = L("Only create support if it lies on a build plate. Don't create support on a print."); + def->mode = comSimple; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add(prefix + "support_pillar_widening_factor", coFloat); + def->label = L("Pillar widening factor"); + def->category = L("Supports"); + + pretext = ""; + if (prefix.empty()) + pretext = pretext_unavailable; + + def->tooltip = pretext + + L("Merging bridges or pillars into another pillars can " + "increase the radius. Zero means no increase, one means " + "full increase. The exact amount of increase is unspecified and can " + "change in the future."); + + def->min = 0; + def->max = 1; + def->mode = comExpert; + def->set_default_value(new ConfigOptionFloat(0.5)); + + def = this->add(prefix + "support_base_diameter", coFloat); + def->label = L("Support base diameter"); + def->category = L("Supports"); + def->tooltip = L("Diameter in mm of the pillar base"); + def->sidetext = L("mm"); + def->min = 0; + def->max = 30; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(4.0)); + + def = this->add(prefix + "support_base_height", coFloat); + def->label = L("Support base height"); + def->category = L("Supports"); + def->tooltip = L("The height of the pillar base cone"); + def->sidetext = L("mm"); + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(1.0)); + + def = this->add(prefix + "support_base_safety_distance", coFloat); + def->label = L("Support base safety distance"); + def->category = L("Supports"); + def->tooltip = L( + "The minimum distance of the pillar base from the model in mm. " + "Makes sense in zero elevation mode where a gap according " + "to this parameter is inserted between the model and the pad."); + def->sidetext = L("mm"); + def->min = 0; + def->max = 10; + def->mode = comExpert; + def->set_default_value(new ConfigOptionFloat(1)); + + def = this->add(prefix + "support_critical_angle", coFloat); + def->label = L("Critical angle"); + def->category = L("Supports"); + def->tooltip = L("The default angle for connecting support sticks and junctions."); + def->sidetext = L("°"); + def->min = 0; + def->max = 90; + def->mode = comExpert; + def->set_default_value(new ConfigOptionFloat(45)); + + def = this->add(prefix + "support_max_bridge_length", coFloat); + def->label = L("Max bridge length"); + def->category = L("Supports"); + def->tooltip = L("The max length of a bridge"); + def->sidetext = L("mm"); + def->min = 0; + def->mode = comAdvanced; + + double default_val = 15.0; + if (prefix == "branching") + default_val = 5.0; + + def->set_default_value(new ConfigOptionFloat(default_val)); + + pretext = ""; + if (prefix == "branching") + pretext = pretext_unavailable; + + def = this->add(prefix + "support_max_pillar_link_distance", coFloat); + def->label = L("Max pillar linking distance"); + def->category = L("Supports"); + def->tooltip = pretext + L("The max distance of two pillars to get linked with each other." + " A zero value will prohibit pillar cascading."); + def->sidetext = L("mm"); + def->min = 0; // 0 means no linking + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(10.0)); + + def = this->add(prefix + "support_object_elevation", coFloat); + def->label = L("Object elevation"); + def->category = L("Supports"); + def->tooltip = L("How much the supports should lift up the supported object. " + "If \"Pad around object\" is enabled, this value is ignored."); + def->sidetext = L("mm"); + def->min = 0; + def->max = 150; // This is the max height of print on SL1 + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(5.0)); +} + void PrintConfigDef::init_sla_params() { ConfigOptionDef* def; @@ -3454,6 +3735,15 @@ void PrintConfigDef::init_sla_params() def->min = 0; def->set_default_value(new ConfigOptionFloat(0.3)); + def = this->add_nullable("idle_temperature", coInts); + def->label = L("Idle temperature"); + def->tooltip = L("Nozzle temperature when the tool is currently not used in multi-tool setups." + "This is only used when 'Ooze prevention is active in Print Settings.'"); + def->sidetext = L("°C"); + def->min = 0; + def->max = max_temp; + def->set_default_value(new ConfigOptionIntsNullable { ConfigOptionIntsNullable::nil_value() }); + def = this->add("bottle_volume", coFloat); def->label = L("Bottle volume"); def->tooltip = L("Bottle volume"); @@ -3613,179 +3903,21 @@ void PrintConfigDef::init_sla_params() def->enum_values = ConfigOptionEnum::get_enum_names(); def->enum_labels = ConfigOptionEnum::get_enum_names(); def->enum_labels[0] = L("Default"); - def->enum_labels[1] = L("Branching"); - def->mode = comAdvanced; + def->enum_labels[1] = L("Branching (experimental)"); + // TODO: def->enum_labels[2] = L("Organic"); + def->mode = comSimple; def->set_default_value(new ConfigOptionEnum(sla::SupportTreeType::Default)); - def = this->add("support_head_front_diameter", coFloat); - def->label = L("Pinhead front diameter"); - def->category = L("Supports"); - def->tooltip = L("Diameter of the pointing side of the head"); - def->sidetext = L("mm"); - def->min = 0; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(0.4)); + init_sla_support_params(""); + init_sla_support_params("branching"); - def = this->add("support_head_penetration", coFloat); - def->label = L("Head penetration"); + def = this->add("support_enforcers_only", coBool); + def->label = L("Support only in enforced regions"); def->category = L("Supports"); - def->tooltip = L("How much the pinhead has to penetrate the model surface"); - def->sidetext = L("mm"); - def->mode = comAdvanced; - def->min = 0; - def->set_default_value(new ConfigOptionFloat(0.2)); - - def = this->add("support_head_width", coFloat); - def->label = L("Pinhead width"); - def->category = L("Supports"); - def->tooltip = L("Width from the back sphere center to the front sphere center"); - def->sidetext = L("mm"); - def->min = 0; - def->max = 20; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(1.0)); - - def = this->add("support_pillar_diameter", coFloat); - def->label = L("Pillar diameter"); - def->category = L("Supports"); - def->tooltip = L("Diameter in mm of the support pillars"); - def->sidetext = L("mm"); - def->min = 0; - def->max = 15; - def->mode = comSimple; - def->set_default_value(new ConfigOptionFloat(1.0)); - - def = this->add("support_small_pillar_diameter_percent", coPercent); - def->label = L("Small pillar diameter percent"); - def->category = L("Supports"); - def->tooltip = L("The percentage of smaller pillars compared to the normal pillar diameter " - "which are used in problematic areas where a normal pilla cannot fit."); - def->sidetext = L("%"); - def->min = 1; - def->max = 100; - def->mode = comExpert; - def->set_default_value(new ConfigOptionPercent(50)); - - def = this->add("support_max_bridges_on_pillar", coInt); - def->label = L("Max bridges on a pillar"); - def->tooltip = L( - "Maximum number of bridges that can be placed on a pillar. Bridges " - "hold support point pinheads and connect to pillars as small branches."); - def->min = 0; - def->max = 50; - def->mode = comExpert; - def->set_default_value(new ConfigOptionInt(3)); - - def = this->add("support_pillar_connection_mode", coEnum); - def->label = L("Pillar connection mode"); - def->tooltip = L("Controls the bridge type between two neighboring pillars." - " Can be zig-zag, cross (double zig-zag) or dynamic which" - " will automatically switch between the first two depending" - " on the distance of the two pillars."); - def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); - def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); - def->enum_values = ConfigOptionEnum::get_enum_names(); - def->enum_labels = ConfigOptionEnum::get_enum_names(); - def->enum_labels[0] = L("Zig-Zag"); - def->enum_labels[1] = L("Cross"); - def->enum_labels[2] = L("Dynamic"); - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionEnum(SLAPillarConnectionMode::dynamic)); - - def = this->add("support_buildplate_only", coBool); - def->label = L("Support on build plate only"); - def->category = L("Supports"); - def->tooltip = L("Only create support if it lies on a build plate. Don't create support on a print."); + def->tooltip = L("Only create support if it lies in a support enforcer."); def->mode = comSimple; def->set_default_value(new ConfigOptionBool(false)); - def = this->add("support_pillar_widening_factor", coFloat); - def->label = L("Pillar widening factor"); - def->category = L("Supports"); - def->tooltip = L( - "Merging bridges or pillars into another pillars can " - "increase the radius. Zero means no increase, one means " - "full increase. The exact amount of increase is unspecified and can " - "change in the future. What is garanteed is that thickness will not " - "exceed \"support_base_diameter\""); - - def->min = 0; - def->max = 1; - def->mode = comExpert; - def->set_default_value(new ConfigOptionFloat(0.15)); - - def = this->add("support_base_diameter", coFloat); - def->label = L("Support base diameter"); - def->category = L("Supports"); - def->tooltip = L("Diameter in mm of the pillar base"); - def->sidetext = L("mm"); - def->min = 0; - def->max = 30; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(4.0)); - - def = this->add("support_base_height", coFloat); - def->label = L("Support base height"); - def->category = L("Supports"); - def->tooltip = L("The height of the pillar base cone"); - def->sidetext = L("mm"); - def->min = 0; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(1.0)); - - def = this->add("support_base_safety_distance", coFloat); - def->label = L("Support base safety distance"); - def->category = L("Supports"); - def->tooltip = L( - "The minimum distance of the pillar base from the model in mm. " - "Makes sense in zero elevation mode where a gap according " - "to this parameter is inserted between the model and the pad."); - def->sidetext = L("mm"); - def->min = 0; - def->max = 10; - def->mode = comExpert; - def->set_default_value(new ConfigOptionFloat(1)); - - def = this->add("support_critical_angle", coFloat); - def->label = L("Critical angle"); - def->category = L("Supports"); - def->tooltip = L("The default angle for connecting support sticks and junctions."); - def->sidetext = L("°"); - def->min = 0; - def->max = 90; - def->mode = comExpert; - def->set_default_value(new ConfigOptionFloat(45)); - - def = this->add("support_max_bridge_length", coFloat); - def->label = L("Max bridge length"); - def->category = L("Supports"); - def->tooltip = L("The max length of a bridge"); - def->sidetext = L("mm"); - def->min = 0; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(15.0)); - - def = this->add("support_max_pillar_link_distance", coFloat); - def->label = L("Max pillar linking distance"); - def->category = L("Supports"); - def->tooltip = L("The max distance of two pillars to get linked with each other." - " A zero value will prohibit pillar cascading."); - def->sidetext = L("mm"); - def->min = 0; // 0 means no linking - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(10.0)); - - def = this->add("support_object_elevation", coFloat); - def->label = L("Object elevation"); - def->category = L("Supports"); - def->tooltip = L("How much the supports should lift up the supported object. " - "If \"Pad around object\" is enabled, this value is ignored."); - def->sidetext = L("mm"); - def->min = 0; - def->max = 150; // This is the max height of print on SL1 - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionFloat(5.0)); - def = this->add("support_points_density_relative", coInt); def->label = L("Support points density"); def->category = L("Supports"); @@ -4392,6 +4524,12 @@ std::string validate(const FullPrintConfig &cfg) assert(opt != nullptr); const ConfigOptionDef *optdef = print_config_def.get(opt_key); assert(optdef != nullptr); + + if (opt->nullable() && opt->is_nil()) { + // Do not check nil values + continue; + } + bool out_of_range = false; switch (opt->type()) { case coFloat: @@ -4753,6 +4891,23 @@ Points get_bed_shape(const PrintConfig &cfg) Points get_bed_shape(const SLAPrinterConfig &cfg) { return to_points(cfg.bed_shape.values); } +std::string get_sla_suptree_prefix(const DynamicPrintConfig &config) +{ + const auto *suptreetype = config.option>("support_tree_type"); + std::string slatree = ""; + if (suptreetype) { + auto ttype = static_cast(suptreetype->getInt()); + switch (ttype) { + case sla::SupportTreeType::Branching: slatree = "branching"; break; + case sla::SupportTreeType::Organic: slatree = "organic"; break; + default: + ; + } + } + + return slatree; +} + } // namespace Slic3r #include diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index a36125a07..6bfc7723d 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -57,7 +57,7 @@ enum class FuzzySkinType { }; enum InfillPattern : int { - ipRectilinear, ipMonotonic, ipAlignedRectilinear, ipGrid, ipTriangles, ipStars, ipCubic, ipLine, ipConcentric, ipHoneycomb, ip3DHoneycomb, + ipRectilinear, ipMonotonic, ipMonotonicLines, ipAlignedRectilinear, ipGrid, ipTriangles, ipStars, ipCubic, ipLine, ipConcentric, ipHoneycomb, ip3DHoneycomb, ipGyroid, ipHilbertCurve, ipArchimedeanChords, ipOctagramSpiral, ipAdaptiveCubic, ipSupportCubic, ipSupportBase, ipLightning, ipCount, @@ -185,6 +185,7 @@ private: void init_fff_params(); void init_extruder_option_keys(); void init_sla_params(); + void init_sla_support_params(const std::string &method_prefix); std::vector m_extruder_option_keys; std::vector m_extruder_retract_keys; @@ -546,6 +547,14 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionInt, support_material_threshold)) ((ConfigOptionBool, support_material_with_sheath)) ((ConfigOptionFloatOrPercent, support_material_xy_spacing)) + // Tree supports + ((ConfigOptionFloat, support_tree_angle)) + ((ConfigOptionFloat, support_tree_angle_slow)) + ((ConfigOptionFloat, support_tree_branch_diameter)) + ((ConfigOptionFloat, support_tree_branch_diameter_angle)) + ((ConfigOptionPercent, support_tree_top_rate)) + ((ConfigOptionFloat, support_tree_tip_diameter)) + // The rest ((ConfigOptionBool, thick_bridges)) ((ConfigOptionFloat, xy_size_compensation)) ((ConfigOptionBool, wipe_into_objects)) @@ -760,6 +769,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionFloatOrPercent, first_layer_height)) ((ConfigOptionFloatOrPercent, first_layer_speed)) ((ConfigOptionInts, first_layer_temperature)) + ((ConfigOptionIntsNullable, idle_temperature)) ((ConfigOptionInts, full_fan_speed_layer)) ((ConfigOptionFloat, infill_acceleration)) ((ConfigOptionBool, infill_first)) @@ -861,6 +871,11 @@ PRINT_CONFIG_CLASS_DEFINE( // Generate only ground facing supports ((ConfigOptionBool, support_buildplate_only)) + ((ConfigOptionFloat, support_max_weight_on_model)) + + // Generate only ground facing supports + ((ConfigOptionBool, support_enforcers_only)) + // TODO: unimplemented at the moment. This coefficient will have an impact // when bridges and pillars are merged. The resulting pillar should be a bit // thicker than the ones merging into it. How much thicker? I don't know @@ -889,6 +904,62 @@ PRINT_CONFIG_CLASS_DEFINE( // and the model object's bounding box bottom. Units in mm. ((ConfigOptionFloat, support_object_elevation))/*= 5.0*/ + + // Branching tree + + // Diameter in mm of the pointing side of the head. + ((ConfigOptionFloat, branchingsupport_head_front_diameter))/*= 0.2*/ + + // How much the pinhead has to penetrate the model surface + ((ConfigOptionFloat, branchingsupport_head_penetration))/*= 0.2*/ + + // Width in mm from the back sphere center to the front sphere center. + ((ConfigOptionFloat, branchingsupport_head_width))/*= 1.0*/ + + // Radius in mm of the support pillars. + ((ConfigOptionFloat, branchingsupport_pillar_diameter))/*= 0.8*/ + + // The percentage of smaller pillars compared to the normal pillar diameter + // which are used in problematic areas where a normal pilla cannot fit. + ((ConfigOptionPercent, branchingsupport_small_pillar_diameter_percent)) + + // How much bridge (supporting another pinhead) can be placed on a pillar. + ((ConfigOptionInt, branchingsupport_max_bridges_on_pillar)) + + // How the pillars are bridged together + ((ConfigOptionEnum, branchingsupport_pillar_connection_mode)) + + // Generate only ground facing supports + ((ConfigOptionBool, branchingsupport_buildplate_only)) + + ((ConfigOptionFloat, branchingsupport_max_weight_on_model)) + + ((ConfigOptionFloat, branchingsupport_pillar_widening_factor)) + + // Radius in mm of the pillar base. + ((ConfigOptionFloat, branchingsupport_base_diameter))/*= 2.0*/ + + // The height of the pillar base cone in mm. + ((ConfigOptionFloat, branchingsupport_base_height))/*= 1.0*/ + + // The minimum distance of the pillar base from the model in mm. + ((ConfigOptionFloat, branchingsupport_base_safety_distance)) /*= 1.0*/ + + // The default angle for connecting support sticks and junctions. + ((ConfigOptionFloat, branchingsupport_critical_angle))/*= 45*/ + + // The max length of a bridge in mm + ((ConfigOptionFloat, branchingsupport_max_bridge_length))/*= 15.0*/ + + // The max distance of two pillars to get cross linked. + ((ConfigOptionFloat, branchingsupport_max_pillar_link_distance)) + + // The elevation in Z direction upwards. This is the space between the pad + // and the model object's bounding box bottom. Units in mm. + ((ConfigOptionFloat, branchingsupport_object_elevation))/*= 5.0*/ + + + /////// Following options influence automatic support points placement: ((ConfigOptionInt, support_points_density_relative)) ((ConfigOptionFloat, support_points_minimal_distance)) @@ -1108,6 +1179,8 @@ Points get_bed_shape(const DynamicPrintConfig &cfg); Points get_bed_shape(const PrintConfig &cfg); Points get_bed_shape(const SLAPrinterConfig &cfg); +std::string get_sla_suptree_prefix(const DynamicPrintConfig &config); + // ModelConfig is a wrapper around DynamicPrintConfig with an addition of a timestamp. // Each change of ModelConfig is tracked by assigning a new timestamp from a global counter. // The counter is used for faster synchronization of the background slicing thread diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index abbf0fb93..82b69baa3 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1,4 +1,6 @@ #include "Exception.hpp" +#include "KDTreeIndirect.hpp" +#include "Point.hpp" #include "Print.hpp" #include "BoundingBox.hpp" #include "ClipperUtils.hpp" @@ -21,14 +23,19 @@ #include "SupportSpotsGenerator.hpp" #include "TriangleSelectorWrapper.hpp" #include "format.hpp" +#include "libslic3r.h" +#include +#include #include #include +#include #include #include #include +#include using namespace std::literals; @@ -69,8 +76,8 @@ PrintObject::PrintObject(Print* print, ModelObject* model_object, const Transfor BoundingBoxf3 bbox = model_object->raw_bounding_box(); Vec3d bbox_center = bbox.center(); // We may need to rotate the bbox / bbox_center from the original instance to the current instance. - double z_diff = Geometry::rotation_diff_z(model_object->instances.front()->get_rotation(), instances.front().model_instance->get_rotation()); - if (std::abs(z_diff) > EPSILON) { + double z_diff = Geometry::rotation_diff_z(model_object->instances.front()->get_matrix(), instances.front().model_instance->get_matrix()); + if (std::abs(z_diff) > EPSILON) { auto z_rot = Eigen::AngleAxisd(z_diff, Vec3d::UnitZ()); bbox = bbox.transformed(Transform3d(z_rot)); bbox_center = (z_rot * bbox_center).eval(); @@ -406,32 +413,58 @@ void PrintObject::ironing() } } - -/* -std::vector problematic_layers = SupportSpotsGenerator::quick_search(this); - if (!problematic_layers.empty()) { - std::cout << "Object needs supports" << std::endl; - this->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, - L("Supportable issues found. Consider enabling supports for this object")); - this->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, - L("Supportable issues found. Consider enabling supports for this object")); - for (size_t index = 0; index < std::min(problematic_layers.size(), size_t(4)); ++index) { - this->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, - format(L("Layer with issues: %1%"), problematic_layers[index] + 1)); - } - } - */ void PrintObject::generate_support_spots() { if (this->set_started(posSupportSpotsSearch)) { BOOST_LOG_TRIVIAL(debug) << "Searching support spots - start"; m_print->set_status(75, L("Searching support spots")); if (!this->shared_regions()->generated_support_points.has_value()) { - PrintTryCancel cancel_func = m_print->make_try_cancel(); - SupportSpotsGenerator::Params params{this->print()->m_config.filament_type.values}; - SupportSpotsGenerator::SupportPoints supp_points = SupportSpotsGenerator::full_search(this, cancel_func, params); + PrintTryCancel cancel_func = m_print->make_try_cancel(); + SupportSpotsGenerator::Params params{this->print()->m_config.filament_type.values, + float(this->print()->m_config.perimeter_acceleration.getFloat()), + this->config().raft_layers.getInt(), this->config().brim_type.value, + float(this->config().brim_width.getFloat())}; + auto [supp_points, partial_objects] = SupportSpotsGenerator::full_search(this, cancel_func, params); this->m_shared_regions->generated_support_points = {this->trafo_centered(), supp_points}; m_print->throw_if_canceled(); + + auto alert_fn = [&](PrintStateBase::WarningLevel level, SupportSpotsGenerator::SupportPointCause cause) { + switch (cause) { + case SupportSpotsGenerator::SupportPointCause::LongBridge: + this->active_step_add_warning(level, L("There are bridges longer than recommended length. Consider adding supports. ") + + (L("Object name")) + ": " + this->model_object()->name); + break; + case SupportSpotsGenerator::SupportPointCause::FloatingBridgeAnchor: + this->active_step_add_warning(level, L("Unsupported bridges will collapse. Supports are needed. ") + (L("Object name")) + + ": " + this->model_object()->name); + break; + case SupportSpotsGenerator::SupportPointCause::FloatingExtrusion: + if (level == PrintStateBase::WarningLevel::CRITICAL) { + this->active_step_add_warning(level, L("Clusters of unsupported extrusions found. Supports are needed. ") + + (L("Object name")) + ": " + this->model_object()->name); + } else { + this->active_step_add_warning(level, L("Some unspported extrusions found. Consider adding supports. ") + + (L("Object name")) + ": " + this->model_object()->name); + } + break; + case SupportSpotsGenerator::SupportPointCause::SeparationFromBed: + this->active_step_add_warning(level, L("Object part may break from the bed. Consider adding brim and/or supports. ") + + (L("Object name")) + ": " + this->model_object()->name); + break; + case SupportSpotsGenerator::SupportPointCause::UnstableFloatingPart: + this->active_step_add_warning(level, L("Floating object parts detected. Supports are needed. ") + (L("Object name")) + + ": " + this->model_object()->name); + break; + case SupportSpotsGenerator::SupportPointCause::WeakObjectPart: + this->active_step_add_warning(level, L("Thin parts of the object may break. Consider adding supports. ") + + (L("Object name")) + ": " + this->model_object()->name); + break; + } + }; + + if (!this->has_support()) { + SupportSpotsGenerator::raise_alerts_for_issues(supp_points, partial_objects, alert_fn); + } } BOOST_LOG_TRIVIAL(debug) << "Searching support spots - end"; this->set_done(posSupportSpotsSearch); @@ -468,7 +501,10 @@ void PrintObject::estimate_curled_extrusions() // Estimate curling of support material and add it to the malformaition lines of each layer float support_flow_width = support_material_flow(this, this->config().layer_height).width(); - SupportSpotsGenerator::Params params{this->print()->m_config.filament_type.values}; + SupportSpotsGenerator::Params params{this->print()->m_config.filament_type.values, + float(this->print()->m_config.perimeter_acceleration.getFloat()), + this->config().raft_layers.getInt(), this->config().brim_type.value, + float(this->config().brim_width.getFloat())}; SupportSpotsGenerator::estimate_supports_malformations(this->support_layers(), support_flow_width, params); SupportSpotsGenerator::estimate_malformations(this->layers(), params); m_print->throw_if_canceled(); @@ -580,6 +616,7 @@ bool PrintObject::invalidate_state_by_config_options( if ( opt_key == "brim_width" || opt_key == "brim_separation" || opt_key == "brim_type") { + steps.emplace_back(posSupportSpotsSearch); // Brim is printed below supports, support invalidates brim and skirt. steps.emplace_back(posSupportMaterial); } else if ( @@ -657,6 +694,12 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "support_material_synchronize_layers" || opt_key == "support_material_threshold" || opt_key == "support_material_with_sheath" + || opt_key == "support_tree_angle" + || opt_key == "support_tree_angle_slow" + || opt_key == "support_tree_branch_diameter" + || opt_key == "support_tree_branch_diameter_angle" + || opt_key == "support_tree_top_rate" + || opt_key == "support_tree_tip_diameter" || opt_key == "raft_expansion" || opt_key == "raft_first_layer_density" || opt_key == "raft_first_layer_expansion" diff --git a/src/libslic3r/QuadricEdgeCollapse.hpp b/src/libslic3r/QuadricEdgeCollapse.hpp index 956ad3511..0043fc24a 100644 --- a/src/libslic3r/QuadricEdgeCollapse.hpp +++ b/src/libslic3r/QuadricEdgeCollapse.hpp @@ -4,6 +4,8 @@ // paper: https://people.eecs.berkeley.edu/~jrs/meshpapers/GarlandHeckbert2.pdf // sum up: https://users.csc.calpoly.edu/~zwood/teaching/csc570/final06/jseeba/ // inspiration: https://github.com/sp4cerat/Fast-Quadric-Mesh-Simplification +#ifndef PRUSASLICER_QUADRIC_EDGE_COLLAPSE_HPP +#define PRUSASLICER_QUADRIC_EDGE_COLLAPSE_HPP #include #include @@ -30,3 +32,5 @@ void its_quadric_edge_collapse( } // namespace Slic3r #endif // slic3r_quadric_edge_collapse_hpp_ + +#endif // PRUSASLICER_QUADRIC_EDGE_COLLAPSE_HPP diff --git a/src/libslic3r/SLA/BranchingTreeSLA.cpp b/src/libslic3r/SLA/BranchingTreeSLA.cpp index 72314c9d8..bb9712616 100644 --- a/src/libslic3r/SLA/BranchingTreeSLA.cpp +++ b/src/libslic3r/SLA/BranchingTreeSLA.cpp @@ -13,20 +13,28 @@ namespace Slic3r { namespace sla { +inline constexpr const auto &beam_ex_policy = ex_tbb; + class BranchingTreeBuilder: public branchingtree::Builder { SupportTreeBuilder &m_builder; const SupportableMesh &m_sm; const branchingtree::PointCloud &m_cloud; + std::vector m_pillars; // to put an index over them + + // cache succesfull ground connections + mutable std::map m_gnd_connections; + mutable execution::SpinningMutex m_gnd_connections_mtx; + // Scaling of the input value 'widening_factor:<0, 1>' to produce resonable // widening behaviour - static constexpr double WIDENING_SCALE = 0.02; + static constexpr double WIDENING_SCALE = 0.05; - double get_radius(const branchingtree::Node &j) + double get_radius(const branchingtree::Node &j) const { double w = WIDENING_SCALE * m_sm.cfg.pillar_widening_factor * j.weight; - return std::min(m_sm.cfg.base_radius_mm, double(j.Rmin) + w); + return double(j.Rmin) + w; } std::vector m_unroutable_pinheads; @@ -78,6 +86,44 @@ class BranchingTreeBuilder: public branchingtree::Builder { }); } + void discard_subtree_rescure(size_t root) + { + // Discard all the support points connecting to this branch. + // As a last resort, try to route child nodes to ground and stop + // traversing if any child branch succeeds. + traverse(m_cloud, root, [this](const branchingtree::Node &node) { + branchingtree::TraverseReturnT ret{true, true}; + + int suppid_parent = m_cloud.get_leaf_id(node.id); + int suppid_left = branchingtree::Node::ID_NONE; + int suppid_right = branchingtree::Node::ID_NONE; + + double glvl = ground_level(m_sm); + branchingtree::Node dst = node; + dst.pos.z() = glvl; + dst.weight += node.pos.z() - glvl; + + if (node.left >= 0 && add_ground_bridge(m_cloud.get(node.left), dst)) + ret.to_left = false; + else + suppid_left = m_cloud.get_leaf_id(node.left); + + if (node.right >= 0 && add_ground_bridge(m_cloud.get(node.right), dst)) + ret.to_right = false; + else + suppid_right = m_cloud.get_leaf_id(node.right); + + if (suppid_parent >= 0) + m_unroutable_pinheads.emplace_back(suppid_parent); + if (suppid_left >= 0) + m_unroutable_pinheads.emplace_back(suppid_left); + if (suppid_right >= 0) + m_unroutable_pinheads.emplace_back(suppid_right); + + return ret; + }); + } + public: BranchingTreeBuilder(SupportTreeBuilder &builder, const SupportableMesh &sm, @@ -98,13 +144,24 @@ public: bool add_mesh_bridge(const branchingtree::Node &from, const branchingtree::Node &to) override; + std::optional suggest_avoidance(const branchingtree::Node &from, + float max_bridge_len) const override; + void report_unroutable(const branchingtree::Node &j) override { - BOOST_LOG_TRIVIAL(error) << "Cannot route junction at " << j.pos.x() - << " " << j.pos.y() << " " << j.pos.z(); + double glvl = ground_level(m_sm); + branchingtree::Node dst = j; + dst.pos.z() = glvl; + dst.weight += j.pos.z() - glvl; + if (add_ground_bridge(j, dst)) + return; + + BOOST_LOG_TRIVIAL(warning) << "Cannot route junction at " << j.pos.x() + << " " << j.pos.y() << " " << j.pos.z(); // Discard all the support points connecting to this branch. - discard_subtree(j.id); + discard_subtree_rescure(j.id); +// discard_subtree(j.id); } const std::vector& unroutable_pinheads() const @@ -113,15 +170,28 @@ public: } bool is_valid() const override { return !m_builder.ctl().stopcondition(); } + + const std::vector & pillars() const { return m_pillars; } + + const GroundConnection *ground_conn(size_t pillar) const + { + const GroundConnection *ret = nullptr; + + auto it = m_gnd_connections.find(m_pillars[pillar].id); + if (it != m_gnd_connections.end()) + ret = &it->second; + + return ret; + } }; bool BranchingTreeBuilder::add_bridge(const branchingtree::Node &from, - const branchingtree::Node &to) + const branchingtree::Node &to) { Vec3d fromd = from.pos.cast(), tod = to.pos.cast(); double fromR = get_radius(from), toR = get_radius(to); Beam beam{Ball{fromd, fromR}, Ball{tod, toR}}; - auto hit = beam_mesh_hit(ex_tbb, m_sm.emesh, beam, + auto hit = beam_mesh_hit(beam_ex_policy , m_sm.emesh, beam, m_sm.cfg.safety_distance_mm); bool ret = hit.distance() > (tod - fromd).norm(); @@ -144,8 +214,8 @@ bool BranchingTreeBuilder::add_merger(const branchingtree::Node &node, Beam beam2{Ball{from2d, closestR}, Ball{tod, mergeR}}; auto sd = m_sm.cfg.safety_distance_mm ; - auto hit1 = beam_mesh_hit(ex_tbb, m_sm.emesh, beam1, sd); - auto hit2 = beam_mesh_hit(ex_tbb, m_sm.emesh, beam2, sd); + auto hit1 = beam_mesh_hit(beam_ex_policy , m_sm.emesh, beam1, sd); + auto hit2 = beam_mesh_hit(beam_ex_policy , m_sm.emesh, beam2, sd); bool ret = hit1.distance() > (tod - from1d).norm() && hit2.distance() > (tod - from2d).norm(); @@ -156,12 +226,31 @@ bool BranchingTreeBuilder::add_merger(const branchingtree::Node &node, bool BranchingTreeBuilder::add_ground_bridge(const branchingtree::Node &from, const branchingtree::Node &to) { - bool ret = search_ground_route(ex_tbb, m_builder, m_sm, - sla::Junction{from.pos.cast(), - get_radius(from)}, - get_radius(to)).first; + bool ret = false; - if (ret) { + namespace bgi = boost::geometry::index; + + auto it = m_gnd_connections.find(from.id); + const GroundConnection *connptr = nullptr; + + if (it == m_gnd_connections.end()) { + sla::Junction j{from.pos.cast(), get_radius(from)}; + Vec3d init_dir = (to.pos - from.pos).cast().normalized(); + + auto conn = deepsearch_ground_connection(beam_ex_policy , m_sm, j, + get_radius(to), init_dir); + + // Remember that this node was tested if can go to ground, don't + // test it with any other destination ground point because + // it is unlikely that search_ground_route would find a better solution + connptr = &(m_gnd_connections[from.id] = conn); + } else { + connptr = &(it->second); + } + + if (connptr && *connptr) { + m_pillars.emplace_back(from); + ret = true; build_subtree(from.id); } @@ -171,17 +260,20 @@ bool BranchingTreeBuilder::add_ground_bridge(const branchingtree::Node &from, bool BranchingTreeBuilder::add_mesh_bridge(const branchingtree::Node &from, const branchingtree::Node &to) { + if (from.weight > m_sm.cfg.max_weight_on_model_support) + return false; + sla::Junction fromj = {from.pos.cast(), get_radius(from)}; auto anchor = m_sm.cfg.ground_facing_only ? std::optional{} : // If no mesh connections are allowed - calculate_anchor_placement(ex_tbb, m_sm, fromj, + calculate_anchor_placement(beam_ex_policy , m_sm, fromj, to.pos.cast()); if (anchor) { sla::Junction toj = {anchor->junction_point(), anchor->r_back_mm}; - auto hit = beam_mesh_hit(ex_tbb, m_sm.emesh, + auto hit = beam_mesh_hit(beam_ex_policy , m_sm.emesh, Beam{{fromj.pos, fromj.r}, {toj.pos, toj.r}}, 0.); if (hit.distance() > distance(fromj.pos, toj.pos)) { @@ -197,6 +289,70 @@ bool BranchingTreeBuilder::add_mesh_bridge(const branchingtree::Node &from, return bool(anchor); } +static std::optional get_avoidance(const GroundConnection &conn, + float maxdist) +{ + std::optional ret; + + if (conn) { + if (conn.path.size() > 1) { + ret = conn.path[1].pos.cast(); + } else { + Vec3f pbeg = conn.path[0].pos.cast(); + Vec3f pend = conn.pillar_base->pos.cast(); + pbeg.z() = std::max(pbeg.z() - maxdist, pend.z()); + ret = pbeg; + } + } + + return ret; +} + +std::optional BranchingTreeBuilder::suggest_avoidance( + const branchingtree::Node &from, float max_bridge_len) const +{ + std::optional ret; + + double glvl = ground_level(m_sm); + branchingtree::Node dst = from; + dst.pos.z() = glvl; + dst.weight += from.pos.z() - glvl; + sla::Junction j{from.pos.cast(), get_radius(from)}; + + auto found_it = m_gnd_connections.end(); + { + std::lock_guard lk{m_gnd_connections_mtx}; + found_it = m_gnd_connections.find(from.id); + } + + if (found_it != m_gnd_connections.end()) { + ret = get_avoidance(found_it->second, max_bridge_len); + } else { + auto conn = deepsearch_ground_connection( + beam_ex_policy , m_sm, j, get_radius(dst), sla::DOWN); + + { + std::lock_guard lk{m_gnd_connections_mtx}; + m_gnd_connections[from.id] = conn; + } + + ret = get_avoidance(conn, max_bridge_len); + } + + return ret; +} + +inline void build_pillars(SupportTreeBuilder &builder, + BranchingTreeBuilder &vbuilder, + const SupportableMesh &sm) +{ + for (size_t pill_id = 0; pill_id < vbuilder.pillars().size(); ++pill_id) { + auto * conn = vbuilder.ground_conn(pill_id); + if (conn) + build_ground_connection(builder, sm, *conn); + } +} + void create_branching_tree(SupportTreeBuilder &builder, const SupportableMesh &sm) { auto coordfn = [&sm](size_t id, size_t dim) { return sm.pts[id].pos(dim); }; @@ -221,7 +377,7 @@ void create_branching_tree(SupportTreeBuilder &builder, const SupportableMesh &s for (auto &h : heads) if (h && h->is_valid()) { leafs.emplace_back(h->junction_point().cast(), h->r_back_mm); - h->id = leafs.size() - 1; + h->id = long(leafs.size() - 1); builder.add_head(h->id, *h); } @@ -240,15 +396,29 @@ void create_branching_tree(SupportTreeBuilder &builder, const SupportableMesh &s props.sampling_radius()); auto bedpts = branchingtree::sample_bed(props.bed_shape(), - props.ground_level(), + float(props.ground_level()), props.sampling_radius()); + for (auto &bp : bedpts) + bp.Rmin = sm.cfg.head_back_radius_mm; + branchingtree::PointCloud nodes{std::move(meshpts), std::move(bedpts), std::move(leafs), props}; BranchingTreeBuilder vbuilder{builder, sm, nodes}; + + execution::for_each(ex_tbb, + size_t(0), + nodes.get_leafs().size(), + [&nodes, &vbuilder](size_t leaf_idx) { + vbuilder.suggest_avoidance(nodes.get_leafs()[leaf_idx], + nodes.properties().max_branch_length()); + }); + branchingtree::build_tree(nodes, vbuilder); + build_pillars(builder, vbuilder, sm); + for (size_t id : vbuilder.unroutable_pinheads()) builder.head(id).invalidate(); diff --git a/src/libslic3r/SLA/DefaultSupportTree.cpp b/src/libslic3r/SLA/DefaultSupportTree.cpp index 9e21fca45..429cd45c6 100644 --- a/src/libslic3r/SLA/DefaultSupportTree.cpp +++ b/src/libslic3r/SLA/DefaultSupportTree.cpp @@ -340,14 +340,17 @@ bool DefaultSupportTree::connect_to_nearpillar(const Head &head, return true; } -bool DefaultSupportTree::create_ground_pillar(const Vec3d &hjp, +bool DefaultSupportTree::create_ground_pillar(const Junction &hjp, const Vec3d &sourcedir, - double radius, long head_id) { auto [ret, pillar_id] = sla::create_ground_pillar(suptree_ex_policy, - m_builder, m_sm, hjp, - sourcedir, radius, radius, + m_builder, + m_sm, + hjp.pos, + sourcedir, + hjp.r, + hjp.r, head_id); if (pillar_id >= 0) // Save the pillar endpoint in the spatial index @@ -362,20 +365,19 @@ void DefaultSupportTree::add_pinheads() // The minimum distance for two support points to remain valid. const double /*constexpr*/ D_SP = 0.1; - // Get the points that are too close to each other and keep only the - // first one + // Get the points that are too close to each other and keep only the + // first one auto aliases = cluster(m_points, D_SP, 2); PtIndices filtered_indices; filtered_indices.reserve(aliases.size()); m_iheads.reserve(aliases.size()); - m_iheadless.reserve(aliases.size()); for(auto& a : aliases) { // Here we keep only the front point of the cluster. filtered_indices.emplace_back(a.front()); } - // calculate the normals to the triangles for filtered points + // calculate the normals to the triangles for filtered points auto nmls = normals(suptree_ex_policy, m_points, m_sm.emesh, m_sm.cfg.head_front_radius_mm, m_thr, filtered_indices); @@ -404,22 +406,22 @@ void DefaultSupportTree::add_pinheads() Vec3d n = nmls.row(Eigen::Index(i)); - // for all normals we generate the spherical coordinates and - // saturate the polar angle to 45 degrees from the bottom then - // convert back to standard coordinates to get the new normal. - // Then we just create a quaternion from the two normals - // (Quaternion::FromTwoVectors) and apply the rotation to the - // arrow head. + // for all normals we generate the spherical coordinates and + // saturate the polar angle to 45 degrees from the bottom then + // convert back to standard coordinates to get the new normal. + // Then we just create a quaternion from the two normals + // (Quaternion::FromTwoVectors) and apply the rotation to the + // arrow head. auto [polar, azimuth] = dir_to_spheric(n); - // skip if the tilt is not sane + // skip if the tilt is not sane if (polar < PI - m_sm.cfg.normal_cutoff_angle) return; - // We saturate the polar angle to 3pi/4 + // We saturate the polar angle to 3pi/4 polar = std::max(polar, PI - m_sm.cfg.bridge_slope); - // save the head (pinpoint) position + // save the head (pinpoint) position Vec3d hp = m_points.row(fidx); double lmin = m_sm.cfg.head_width_mm, lmax = lmin; @@ -428,18 +430,17 @@ void DefaultSupportTree::add_pinheads() lmin = 0., lmax = m_sm.cfg.head_penetration_mm; } - // The distance needed for a pinhead to not collide with model. + // The distance needed for a pinhead to not collide with model. double w = lmin + 2 * back_r + 2 * m_sm.cfg.head_front_radius_mm - m_sm.cfg.head_penetration_mm; double pin_r = double(m_sm.pts[fidx].head_front_radius); - // Reassemble the now corrected normal + // Reassemble the now corrected normal auto nn = spheric_to_dir(polar, azimuth).normalized(); - // check available distance - AABBMesh::hit_result t = pinhead_mesh_intersect(hp, nn, pin_r, - back_r, w); + // check available distance + AABBMesh::hit_result t = pinhead_mesh_intersect(hp, nn, pin_r, back_r, w); if (t.distance() < w) { // Let's try to optimize this angle, there might be a @@ -509,10 +510,10 @@ void DefaultSupportTree::classify() ground_head_indices.reserve(m_iheads.size()); m_iheads_onmodel.reserve(m_iheads.size()); - // First we decide which heads reach the ground and can be full - // pillars and which shall be connected to the model surface (or - // search a suitable path around the surface that leads to the - // ground -- TODO) + // First we decide which heads reach the ground and can be full + // pillars and which shall be connected to the model surface (or + // search a suitable path around the surface that leads to the + // ground -- TODO) for(unsigned i : m_iheads) { m_thr(); @@ -530,10 +531,10 @@ void DefaultSupportTree::classify() m_head_to_ground_scans[i] = hit; } - // We want to search for clusters of points that are far enough - // from each other in the XY plane to not cross their pillar bases - // These clusters of support points will join in one pillar, - // possibly in their centroid support point. + // We want to search for clusters of points that are far enough + // from each other in the XY plane to not cross their pillar bases + // These clusters of support points will join in one pillar, + // possibly in their centroid support point. auto pointfn = [this](unsigned i) { return m_builder.head(i).junction_point(); @@ -559,13 +560,13 @@ void DefaultSupportTree::routing_to_ground() for (auto &cl : m_pillar_clusters) { m_thr(); - // place all the centroid head positions into the index. We - // will query for alternative pillar positions. If a sidehead - // cannot connect to the cluster centroid, we have to search - // for another head with a full pillar. Also when there are two - // elements in the cluster, the centroid is arbitrary and the - // sidehead is allowed to connect to a nearby pillar to - // increase structural stability. + // place all the centroid head positions into the index. We + // will query for alternative pillar positions. If a sidehead + // cannot connect to the cluster centroid, we have to search + // for another head with a full pillar. Also when there are two + // elements in the cluster, the centroid is arbitrary and the + // sidehead is allowed to connect to a nearby pillar to + // increase structural stability. if (cl.empty()) continue; @@ -587,7 +588,7 @@ void DefaultSupportTree::routing_to_ground() Head &h = m_builder.head(hid); - if (!create_ground_pillar(h.junction_point(), h.dir, h.r_back_mm, h.id)) { + if (!create_ground_pillar(h.junction(), h.dir, h.id)) { BOOST_LOG_TRIVIAL(warning) << "Pillar cannot be created for support point id: " << hid; m_iheads_onmodel.emplace_back(h.id); @@ -595,11 +596,11 @@ void DefaultSupportTree::routing_to_ground() } } - // now we will go through the clusters ones again and connect the - // sidepoints with the cluster centroid (which is a ground pillar) - // or a nearby pillar if the centroid is unreachable. + // now we will go through the clusters ones again and connect the + // sidepoints with the cluster centroid (which is a ground pillar) + // or a nearby pillar if the centroid is unreachable. size_t ci = 0; - for (auto cl : m_pillar_clusters) { + for (const std::vector &cl : m_pillar_clusters) { m_thr(); auto cidx = cl_centroids[ci++]; @@ -615,10 +616,9 @@ void DefaultSupportTree::routing_to_ground() if (!connect_to_nearpillar(sidehead, centerpillarID) && !search_pillar_and_connect(sidehead)) { - Vec3d pstart = sidehead.junction_point(); // Vec3d pend = Vec3d{pstart.x(), pstart.y(), gndlvl}; // Could not find a pillar, create one - create_ground_pillar(pstart, sidehead.dir, sidehead.r_back_mm, sidehead.id); + create_ground_pillar(sidehead.junction(), sidehead.dir, sidehead.id); } } } @@ -664,10 +664,10 @@ bool DefaultSupportTree::connect_to_model_body(Head &head) zangle = std::max(zangle, PI/4); double h = std::sin(zangle) * head.fullwidth(); - // The width of the tail head that we would like to have... + // The width of the tail head that we would like to have... h = std::min(hit.distance() - head.r_back_mm, h); - // If this is a mini pillar dont bother with the tail width, can be 0. + // If this is a mini pillar dont bother with the tail width, can be 0. if (head.r_back_mm < m_sm.cfg.head_back_radius_mm) h = std::max(h, 0.); else if (h <= 0.) return false; @@ -773,23 +773,23 @@ void DefaultSupportTree::interconnect_pillars() // Ideally every pillar should be connected with at least one of its // neighbors if that neighbor is within max_pillar_link_distance - // Pillars with height exceeding H1 will require at least one neighbor - // to connect with. Height exceeding H2 require two neighbors. + // Pillars with height exceeding H1 will require at least one neighbor + // to connect with. Height exceeding H2 require two neighbors. double H1 = m_sm.cfg.max_solo_pillar_height_mm; double H2 = m_sm.cfg.max_dual_pillar_height_mm; double d = m_sm.cfg.max_pillar_link_distance_mm; - //A connection between two pillars only counts if the height ratio is - // bigger than 50% + // A connection between two pillars only counts if the height ratio is + // bigger than 50% double min_height_ratio = 0.5; std::set pairs; - // A function to connect one pillar with its neighbors. THe number of - // neighbors is given in the configuration. This function if called - // for every pillar in the pillar index. A pair of pillar will not - // be connected multiple times this is ensured by the 'pairs' set which - // remembers the processed pillar pairs + // A function to connect one pillar with its neighbors. THe number of + // neighbors is given in the configuration. This function if called + // for every pillar in the pillar index. A pair of pillar will not + // be connected multiple times this is ensured by the 'pairs' set which + // remembers the processed pillar pairs auto cascadefn = [this, d, &pairs, min_height_ratio, H1] (const PointIndexEl& el) { @@ -797,10 +797,10 @@ void DefaultSupportTree::interconnect_pillars() const Pillar& pillar = m_builder.pillar(el.second); // actual pillar - // Get the max number of neighbors a pillar should connect to + // Get the max number of neighbors a pillar should connect to unsigned neighbors = m_sm.cfg.pillar_cascade_neighbors; - // connections are already enough for the pillar + // connections are already enough for the pillar if(pillar.links >= neighbors) return; double max_d = d * pillar.r_start / m_sm.cfg.head_back_radius_mm; @@ -809,7 +809,7 @@ void DefaultSupportTree::interconnect_pillars() return distance(e.first, qp) < max_d; }); - // sort the result by distance (have to check if this is needed) + // sort the result by distance (have to check if this is needed) std::sort(qres.begin(), qres.end(), [qp](const PointIndexEl& e1, const PointIndexEl& e2){ return distance(e1.first, qp) < distance(e2.first, qp); @@ -821,23 +821,23 @@ void DefaultSupportTree::interconnect_pillars() auto a = el.second, b = re.second; - // Get unique hash for the given pair (order doesn't matter) + // Get unique hash for the given pair (order doesn't matter) auto hashval = pairhash(a, b); - // Search for the pair amongst the remembered pairs + // Search for the pair amongst the remembered pairs if(pairs.find(hashval) != pairs.end()) continue; const Pillar& neighborpillar = m_builder.pillar(re.second); - // this neighbor is occupied, skip + // this neighbor is occupied, skip if (neighborpillar.links >= neighbors) continue; if (neighborpillar.r_start < pillar.r_start) continue; if(interconnect(pillar, neighborpillar)) { pairs.insert(hashval); - // If the interconnection length between the two pillars is - // less than 50% of the longer pillar's height, don't count + // If the interconnection length between the two pillars is + // less than 50% of the longer pillar's height, don't count if(pillar.height < H1 || neighborpillar.height / pillar.height > min_height_ratio) m_builder.increment_links(pillar); @@ -848,30 +848,30 @@ void DefaultSupportTree::interconnect_pillars() } - // connections are enough for one pillar + // connections are enough for one pillar if(pillar.links >= neighbors) break; } }; - // Run the cascade for the pillars in the index + // Run the cascade for the pillars in the index m_pillar_index.foreach(cascadefn); - // We would be done here if we could allow some pillars to not be - // connected with any neighbors. But this might leave the support tree - // unprintable. - // - // The current solution is to insert additional pillars next to these - // lonely pillars. One or even two additional pillar might get inserted - // depending on the length of the lonely pillar. + // We would be done here if we could allow some pillars to not be + // connected with any neighbors. But this might leave the support tree + // unprintable. + // + // The current solution is to insert additional pillars next to these + // lonely pillars. One or even two additional pillar might get inserted + // depending on the length of the lonely pillar. size_t pillarcount = m_builder.pillarcount(); - // Again, go through all pillars, this time in the whole support tree - // not just the index. + // Again, go through all pillars, this time in the whole support tree + // not just the index. for(size_t pid = 0; pid < pillarcount; pid++) { auto pillar = [this, pid]() { return m_builder.pillar(pid); }; - // Decide how many additional pillars will be needed: + // Decide how many additional pillars will be needed: unsigned needpillars = 0; if (pillar().bridges > m_sm.cfg.max_bridges_on_pillar) @@ -887,17 +887,17 @@ void DefaultSupportTree::interconnect_pillars() needpillars = std::max(pillar().links, needpillars) - pillar().links; if (needpillars == 0) continue; - // Search for new pillar locations: + // Search for new pillar locations: bool found = false; double alpha = 0; // goes to 2Pi double r = 2 * m_sm.cfg.base_radius_mm; Vec3d pillarsp = pillar().startpoint(); - // temp value for starting point detection + // temp value for starting point detection Vec3d sp(pillarsp.x(), pillarsp.y(), pillarsp.z() - r); - // A vector of bool for placement feasbility + // A vector of bool for placement feasbility std::vector canplace(needpillars, false); std::vector spts(needpillars); // vector of starting points @@ -916,12 +916,12 @@ void DefaultSupportTree::interconnect_pillars() s.y() += std::sin(a) * r; spts[n] = s; - // Check the path vertically down + // Check the path vertically down Vec3d check_from = s + Vec3d{0., 0., pillar().r_start}; auto hr = bridge_mesh_intersect(check_from, DOWN, pillar().r_start); Vec3d gndsp{s.x(), s.y(), gnd}; - // If the path is clear, check for pillar base collisions + // If the path is clear, check for pillar base collisions canplace[n] = std::isinf(hr.distance()) && std::sqrt(m_sm.emesh.squared_distance(gndsp)) > min_dist; @@ -930,7 +930,7 @@ void DefaultSupportTree::interconnect_pillars() found = std::all_of(canplace.begin(), canplace.end(), [](bool v) { return v; }); - // 20 angles will be tried... + // 20 angles will be tried... alpha += 0.1 * PI; } diff --git a/src/libslic3r/SLA/DefaultSupportTree.hpp b/src/libslic3r/SLA/DefaultSupportTree.hpp index ff0269978..08990a8df 100644 --- a/src/libslic3r/SLA/DefaultSupportTree.hpp +++ b/src/libslic3r/SLA/DefaultSupportTree.hpp @@ -1,7 +1,7 @@ #ifndef LEGACYSUPPORTTREE_HPP #define LEGACYSUPPORTTREE_HPP -#include "SupportTreeUtils.hpp" +#include "SupportTreeUtilsLegacy.hpp" #include #include @@ -62,7 +62,6 @@ class DefaultSupportTree { PtIndices m_iheads; // support points with pinhead PtIndices m_iheads_onmodel; - PtIndices m_iheadless; // headless support points std::map m_head_to_ground_scans; @@ -176,9 +175,8 @@ class DefaultSupportTree { // jp is the starting junction point which needs to be routed down. // sourcedir is the allowed direction of an optional bridge between the // jp junction and the final pillar. - bool create_ground_pillar(const Vec3d &jp, + bool create_ground_pillar(const Junction &jp, const Vec3d &sourcedir, - double radius, long head_id = SupportTreeNode::ID_UNSET); void add_pillar_base(long pid) diff --git a/src/libslic3r/SLA/Hollowing.cpp b/src/libslic3r/SLA/Hollowing.cpp index 74eb07695..634cde469 100644 --- a/src/libslic3r/SLA/Hollowing.cpp +++ b/src/libslic3r/SLA/Hollowing.cpp @@ -1,20 +1,25 @@ #include #include +#include +#include +#include #include #include #include #include +#include #include #include #include #include #include +#include + +#include #include -#include - #include #include @@ -27,19 +32,17 @@ namespace sla { struct Interior { indexed_triangle_set mesh; - openvdb::FloatGrid::Ptr gridptr; - mutable std::optional accessor; + VoxelGridPtr gridptr; double iso_surface = 0.; double thickness = 0.; - double voxel_scale = 1.; double full_narrowb = 2.; void reset_accessor() const // This resets the accessor and its cache // Not a thread safe call! { if (gridptr) - accessor = gridptr->getConstAccessor(); + Slic3r::reset_accessor(*gridptr); } }; @@ -58,46 +61,43 @@ const indexed_triangle_set &get_mesh(const Interior &interior) return interior.mesh; } -static InteriorPtr generate_interior_verbose(const TriangleMesh & mesh, - const JobController &ctl, - double min_thickness, - double voxel_scale, - double closing_dist) +const VoxelGrid &get_grid(const Interior &interior) { - double offset = voxel_scale * min_thickness; - double D = voxel_scale * closing_dist; - float in_range = 1.1f * float(offset + D); - auto narrowb = 1.; - float out_range = narrowb; + return *interior.gridptr; +} + +VoxelGrid &get_grid(Interior &interior) +{ + return *interior.gridptr; +} + +InteriorPtr generate_interior(const VoxelGrid &vgrid, + const HollowingConfig &hc, + const JobController &ctl) +{ + double voxsc = get_voxel_scale(vgrid); + double offset = hc.min_thickness; // world units + double D = hc.closing_distance; // world units + float in_range = 1.1f * float(offset + D); // world units + float out_range = 1.f / voxsc; // world units + auto narrowb = 1.f; // voxel units (voxel count) if (ctl.stopcondition()) return {}; else ctl.statuscb(0, L("Hollowing")); - auto gridptr = mesh_to_grid(mesh.its, {}, voxel_scale, out_range, in_range); - - assert(gridptr); - - if (!gridptr) { - BOOST_LOG_TRIVIAL(error) << "Returned OpenVDB grid is NULL"; - return {}; - } + auto gridptr = dilate_grid(vgrid, out_range, in_range); if (ctl.stopcondition()) return {}; else ctl.statuscb(30, L("Hollowing")); double iso_surface = D; if (D > EPSILON) { - in_range = narrowb; - gridptr = redistance_grid(*gridptr, -(offset + D), narrowb, in_range); + gridptr = redistance_grid(*gridptr, -(offset + D), narrowb, narrowb); - constexpr int DilateIterations = 1; - - gridptr = openvdb::tools::dilateSdf( - *gridptr, std::ceil(iso_surface), - openvdb::tools::NN_FACE_EDGE_VERTEX, DilateIterations, - openvdb::tools::FastSweepingDomain::SWEEP_GREATER_THAN_ISOVALUE); + gridptr = dilate_grid(*gridptr, 1.1 * std::ceil(iso_surface), 0.f); out_range = iso_surface; + in_range = narrowb / voxsc; } else { iso_surface = -offset; } @@ -109,68 +109,14 @@ static InteriorPtr generate_interior_verbose(const TriangleMesh & mesh, InteriorPtr interior = InteriorPtr{new Interior{}}; interior->mesh = grid_to_mesh(*gridptr, iso_surface, adaptivity); - interior->gridptr = gridptr; + interior->gridptr = std::move(gridptr); if (ctl.stopcondition()) return {}; else ctl.statuscb(100, L("Hollowing")); interior->iso_surface = iso_surface; interior->thickness = offset; - interior->voxel_scale = voxel_scale; - interior->full_narrowb = out_range + in_range; - - return interior; -} - -InteriorPtr generate_interior(const TriangleMesh & mesh, - const HollowingConfig &hc, - const JobController & ctl) -{ - static constexpr double MIN_SAMPLES_IN_WALL = 3.5; - static constexpr double MAX_OVERSAMPL = 8.; - static constexpr double UNIT_VOLUME = 500000; // empiric - - // I can't figure out how to increase the grid resolution through openvdb - // API so the model will be scaled up before conversion and the result - // scaled down. Voxels have a unit size. If I set voxelSize smaller, it - // scales the whole geometry down, and doesn't increase the number of - // voxels. - // - // First an allowed range for voxel scale is determined from an initial - // range of . The final voxel scale is - // then chosen from this range using the 'quality:<0, 1>' parameter. - // The minimum can be lowered if the wall thickness is great enough and - // the maximum is lowered if the model volume very big. - double mesh_vol = its_volume(mesh.its); - double sc_divider = std::max(1.0, (mesh_vol / UNIT_VOLUME)); - double min_oversampl = std::max(MIN_SAMPLES_IN_WALL / hc.min_thickness, 1.); - double max_oversampl_scaled = std::max(min_oversampl, MAX_OVERSAMPL / sc_divider); - auto voxel_scale = min_oversampl + (max_oversampl_scaled - min_oversampl) * hc.quality; - - BOOST_LOG_TRIVIAL(debug) << "Hollowing: max oversampl will be: " << max_oversampl_scaled; - BOOST_LOG_TRIVIAL(debug) << "Hollowing: voxel scale will be: " << voxel_scale; - BOOST_LOG_TRIVIAL(debug) << "Hollowing: mesh volume is: " << mesh_vol; - - InteriorPtr interior = generate_interior_verbose(mesh, ctl, - hc.min_thickness, - voxel_scale, - hc.closing_distance); - - if (interior && !interior->mesh.empty()) { - - // flip normals back... - swap_normals(interior->mesh); - - // simplify mesh lossless - float loss_less_max_error = 2*std::numeric_limits::epsilon(); - its_quadric_edge_collapse(interior->mesh, 0U, &loss_less_max_error); - - its_compactify_vertices(interior->mesh); - its_merge_vertices(interior->mesh); - - // flip normals back... - swap_normals(interior->mesh); - } + interior->full_narrowb = (out_range + in_range) / 2.; return interior; } @@ -179,9 +125,9 @@ indexed_triangle_set DrainHole::to_mesh() const { auto r = double(radius); auto h = double(height); - indexed_triangle_set hole = sla::cylinder(r, h, steps); + indexed_triangle_set hole = its_make_cylinder(r, h); //sla::cylinder(r, h, steps); Eigen::Quaternionf q; - q.setFromTwoVectors(Vec3f{0.f, 0.f, 1.f}, normal); + q.setFromTwoVectors(Vec3f::UnitZ(), normal); for(auto& p : hole.vertices) p = q * p + pos; return hole; @@ -208,7 +154,6 @@ bool DrainHole::is_inside(const Vec3f& pt) const return false; } - // Given a line s+dir*t, find parameter t of intersections with the hole // and the normal (points inside the hole). Outputs through out reference, // returns true if two intersections were found. @@ -331,7 +276,7 @@ void cut_drainholes(std::vector & obj_slices, void hollow_mesh(TriangleMesh &mesh, const HollowingConfig &cfg, int flags) { - InteriorPtr interior = generate_interior(mesh, cfg, JobController{}); + InteriorPtr interior = generate_interior(mesh.its, cfg, JobController{}); if (!interior) return; hollow_mesh(mesh, *interior, flags); @@ -344,7 +289,24 @@ void hollow_mesh(TriangleMesh &mesh, const Interior &interior, int flags) if (flags & hfRemoveInsideTriangles && interior.gridptr) remove_inside_triangles(mesh, interior); - mesh.merge(TriangleMesh{interior.mesh}); + indexed_triangle_set interi = interior.mesh; + sla::swap_normals(interi); + TriangleMesh inter{std::move(interi)}; + + mesh.merge(inter); +} + +void hollow_mesh(indexed_triangle_set &mesh, const Interior &interior, int flags) +{ + if (mesh.empty() || interior.mesh.empty()) return; + + if (flags & hfRemoveInsideTriangles && interior.gridptr) + remove_inside_triangles(mesh, interior); + + indexed_triangle_set interi = interior.mesh; + sla::swap_normals(interi); + + its_merge(mesh, interi); } // Get the distance of p to the interior's zero iso_surface. Interior should @@ -354,13 +316,7 @@ static double get_distance_raw(const Vec3f &p, const Interior &interior) { assert(interior.gridptr); - if (!interior.accessor) interior.reset_accessor(); - - auto v = (p * interior.voxel_scale).cast(); - auto grididx = interior.gridptr->transform().worldToIndexCellCentered( - {v.x(), v.y(), v.z()}); - - return interior.accessor->getValue(grididx) ; + return Slic3r::get_distance_raw(p, *interior.gridptr); } struct TriangleBubble { Vec3f center; double R; }; @@ -369,7 +325,7 @@ struct TriangleBubble { Vec3f center; double R; }; // triangle is too big to be measured. static double get_distance(const TriangleBubble &b, const Interior &interior) { - double R = b.R * interior.voxel_scale; + double R = b.R; double D = 2. * R; double Dst = get_distance_raw(b.center, interior); @@ -379,10 +335,16 @@ static double get_distance(const TriangleBubble &b, const Interior &interior) Dst - interior.iso_surface; } -double get_distance(const Vec3f &p, const Interior &interior) +inline double get_distance(const Vec3f &p, const Interior &interior) { double d = get_distance_raw(p, interior) - interior.iso_surface; - return d / interior.voxel_scale; + return d; +} + +template +FloatingOnly get_distance(const Vec<3, T> &p, const Interior &interior) +{ + return get_distance(Vec3f(p.template cast()), interior); } // A face that can be divided. Stores the indices into the original mesh if its @@ -434,14 +396,14 @@ void divide_triangle(const DivFace &face, Fn &&visitor) divide_triangle(child2, std::forward(visitor)); } -void remove_inside_triangles(TriangleMesh &mesh, const Interior &interior, +void remove_inside_triangles(indexed_triangle_set &mesh, const Interior &interior, const std::vector &exclude_mask) { enum TrPos { posInside, posTouch, posOutside }; - auto &faces = mesh.its.indices; - auto &vertices = mesh.its.vertices; - auto bb = mesh.bounding_box(); + auto &faces = mesh.indices; + auto &vertices = mesh.vertices; + auto bb = bounding_box(mesh); //mesh.bounding_box(); bool use_exclude_mask = faces.size() == exclude_mask.size(); auto is_excluded = [&exclude_mask, use_exclude_mask](size_t face_id) { @@ -477,8 +439,8 @@ void remove_inside_triangles(TriangleMesh &mesh, const Interior &interior, // or not. std::vector to_remove; - MeshMods(const TriangleMesh &mesh): - to_remove(mesh.its.indices.size(), false) {} + MeshMods(const indexed_triangle_set &mesh): + to_remove(mesh.indices.size(), false) {} // Number of triangles that need to be removed. size_t to_remove_cnt() const @@ -500,7 +462,7 @@ void remove_inside_triangles(TriangleMesh &mesh, const Interior &interior, TriangleBubble bubble{facebb.center().cast(), facebb.radius()}; double D = get_distance(bubble, interior); - double R = bubble.R * interior.voxel_scale; + double R = bubble.R; if (std::isnan(D)) // The distance cannot be measured, triangle too big return true; @@ -540,7 +502,8 @@ void remove_inside_triangles(TriangleMesh &mesh, const Interior &interior, const Vec3i &face = faces[face_idx]; // If the triangle is excluded, we need to keep it. - if (is_excluded(face_idx)) return; + if (is_excluded(face_idx)) + return; std::array pts = {vertices[face(0)], vertices[face(1)], vertices[face(2)]}; @@ -548,7 +511,8 @@ void remove_inside_triangles(TriangleMesh &mesh, const Interior &interior, BoundingBoxf3 facebb{pts.begin(), pts.end()}; // Face is certainly outside the cavity - if (!facebb.intersects(bb)) return; + if (!facebb.intersects(bb)) + return; DivFace df{face, pts, long(face_idx)}; @@ -581,8 +545,356 @@ void remove_inside_triangles(TriangleMesh &mesh, const Interior &interior, faces.swap(new_faces); new_faces = {}; - mesh = TriangleMesh{mesh.its}; +// mesh = TriangleMesh{mesh.its}; //FIXME do we want to repair the mesh? Are there duplicate vertices or flipped triangles? } +void remove_inside_triangles(TriangleMesh &mesh, const Interior &interior, + const std::vector &exclude_mask) +{ + remove_inside_triangles(mesh.its, interior, exclude_mask); +} + +struct FaceHash { + + // A 64 bit number's max hex digits + static constexpr size_t MAX_NUM_CHARS = 16; + + // A hash is created for each triangle to be identifiable. The hash uses + // only the triangle's geometric traits, not the index in a particular mesh. + std::unordered_set facehash; + + // Returns the string in reverse, but that is ok for hashing + static std::array to_chars(int64_t val) + { + std::array ret; + + static const constexpr char * Conv = "0123456789abcdef"; + + auto ptr = ret.begin(); + auto uval = static_cast(std::abs(val)); + while (uval) { + *ptr = Conv[uval & 0xf]; + ++ptr; + uval = uval >> 4; + } + if (val < 0) { *ptr = '-'; ++ptr; } + *ptr = '\0'; // C style string ending + + return ret; + } + + static std::string hash(const Vec<3, int64_t> &v) + { + std::string ret; + ret.reserve(3 * MAX_NUM_CHARS); + + for (auto val : v) + ret += to_chars(val).data(); + + return ret; + } + + static std::string facekey(const Vec3i &face, const std::vector &vertices) + { + // Scale to integer to avoid floating points + std::array, 3> pts = { + scaled(vertices[face(0)]), + scaled(vertices[face(1)]), + scaled(vertices[face(2)]) + }; + + // Get the first two sides of the triangle, do a cross product and move + // that vector to the center of the triangle. This encodes all + // information to identify an identical triangle at the same position. + Vec<3, int64_t> a = pts[0] - pts[2], b = pts[1] - pts[2]; + Vec<3, int64_t> c = a.cross(b) + (pts[0] + pts[1] + pts[2]) / 3; + + // Return a concatenated string representation of the coordinates + return hash(c); + } + + FaceHash (const indexed_triangle_set &its): facehash(its.indices.size()) + { + for (Vec3i face : its.indices) { + std::swap(face(0), face(2)); + facehash.insert(facekey(face, its.vertices)); + } + } + + bool find(const std::string &key) + { + auto it = facehash.find(key); + return it != facehash.end(); + } +}; + + +static void exclude_neighbors(const Vec3i &face, + std::vector &mask, + const indexed_triangle_set &its, + const VertexFaceIndex &index, + size_t recursions) +{ + for (int i = 0; i < 3; ++i) { + const auto &neighbors_range = index[face(i)]; + for (size_t fi_n : neighbors_range) { + mask[fi_n] = true; + if (recursions > 0) + exclude_neighbors(its.indices[fi_n], mask, its, index, recursions - 1); + } + } +} + +std::vector create_exclude_mask(const indexed_triangle_set &its, + const Interior &interior, + const std::vector &holes) +{ + FaceHash interior_hash{sla::get_mesh(interior)}; + + std::vector exclude_mask(its.indices.size(), false); + + VertexFaceIndex neighbor_index{its}; + + for (size_t fi = 0; fi < its.indices.size(); ++fi) { + auto &face = its.indices[fi]; + + if (interior_hash.find(FaceHash::facekey(face, its.vertices))) { + exclude_mask[fi] = true; + continue; + } + + if (exclude_mask[fi]) { + exclude_neighbors(face, exclude_mask, its, neighbor_index, 1); + continue; + } + + // Lets deal with the holes. All the triangles of a hole and all the + // neighbors of these triangles need to be kept. The neigbors were + // created by CGAL mesh boolean operation that modified the original + // interior inside the input mesh to contain the holes. + Vec3d tr_center = ( + its.vertices[face(0)] + + its.vertices[face(1)] + + its.vertices[face(2)] + ).cast() / 3.; + + // If the center is more than half a mm inside the interior, + // it cannot possibly be part of a hole wall. + if (sla::get_distance(tr_center, interior) < -0.5) + continue; + + Vec3f U = its.vertices[face(1)] - its.vertices[face(0)]; + Vec3f V = its.vertices[face(2)] - its.vertices[face(0)]; + Vec3f C = U.cross(V); + Vec3f face_normal = C.normalized(); + + for (const sla::DrainHole &dh : holes) { + if (dh.failed) continue; + + Vec3d dhpos = dh.pos.cast(); + Vec3d dhend = dhpos + dh.normal.cast() * dh.height; + + Linef3 holeaxis{dhpos, dhend}; + + double D_hole_center = line_alg::distance_to(holeaxis, tr_center); + double D_hole = std::abs(D_hole_center - dh.radius); + float dot = dh.normal.dot(face_normal); + + // Empiric tolerances for center distance and normals angle. + // For triangles that are part of a hole wall the angle of + // triangle normal and the hole axis is around 90 degrees, + // so the dot product is around zero. + double D_tol = dh.radius / sla::DrainHole::steps; + float normal_angle_tol = 1.f / sla::DrainHole::steps; + + if (D_hole < D_tol && std::abs(dot) < normal_angle_tol) { + exclude_mask[fi] = true; + exclude_neighbors(face, exclude_mask, its, neighbor_index, 1); + } + } + } + + return exclude_mask; +} + +DrainHoles transformed_drainhole_points(const ModelObject &mo, + const Transform3d &trafo) +{ + auto pts = mo.sla_drain_holes; +// const Transform3d& vol_trafo = mo.volumes.front()->get_transformation().get_matrix(); + const Geometry::Transformation trans(trafo /** vol_trafo*/); + const Transform3d& tr = trans.get_matrix(); + for (sla::DrainHole &hl : pts) { + Vec3d pos = hl.pos.cast(); + Vec3d nrm = hl.normal.cast(); + + pos = tr * pos; + nrm = tr * nrm - tr.translation(); + + // Now shift the hole a bit above the object and make it deeper to + // compensate for it. This is to avoid problems when the hole is placed + // on (nearly) flat surface. + pos -= nrm.normalized() * sla::HoleStickOutLength; + + hl.pos = pos.cast(); + hl.normal = nrm.cast(); + hl.height += sla::HoleStickOutLength; + } + + return pts; +} + +double get_voxel_scale(double mesh_volume, const HollowingConfig &hc) +{ + static constexpr double MIN_SAMPLES_IN_WALL = 3.5; + static constexpr double MAX_OVERSAMPL = 8.; + static constexpr double UNIT_VOLUME = 500000; // empiric + + // I can't figure out how to increase the grid resolution through openvdb + // API so the model will be scaled up before conversion and the result + // scaled down. Voxels have a unit size. If I set voxelSize smaller, it + // scales the whole geometry down, and doesn't increase the number of + // voxels. + // + // First an allowed range for voxel scale is determined from an initial + // range of . The final voxel scale is + // then chosen from this range using the 'quality:<0, 1>' parameter. + // The minimum can be lowered if the wall thickness is great enough and + // the maximum is lowered if the model volume very big. + + double sc_divider = std::max(1.0, (mesh_volume / UNIT_VOLUME)); + double min_oversampl = std::max(MIN_SAMPLES_IN_WALL / hc.min_thickness, 1.); + double max_oversampl_scaled = std::max(min_oversampl, MAX_OVERSAMPL / sc_divider); + auto voxel_scale = min_oversampl + (max_oversampl_scaled - min_oversampl) * hc.quality; + + BOOST_LOG_TRIVIAL(debug) << "Hollowing: max oversampl will be: " << max_oversampl_scaled; + BOOST_LOG_TRIVIAL(debug) << "Hollowing: voxel scale will be: " << voxel_scale; + BOOST_LOG_TRIVIAL(debug) << "Hollowing: mesh volume is: " << mesh_volume; + + return voxel_scale; +} + +// The same as its_compactify_vertices, but returns a new mesh, doesn't touch +// the original +static indexed_triangle_set +remove_unconnected_vertices(const indexed_triangle_set &its) +{ + if (its.indices.empty()) {}; + + indexed_triangle_set M; + + std::vector vtransl(its.vertices.size(), -1); + int vcnt = 0; + for (auto &f : its.indices) { + + for (int i = 0; i < 3; ++i) + if (vtransl[size_t(f(i))] < 0) { + + M.vertices.emplace_back(its.vertices[size_t(f(i))]); + vtransl[size_t(f(i))] = vcnt++; + } + + std::array new_f = { + vtransl[size_t(f(0))], + vtransl[size_t(f(1))], + vtransl[size_t(f(2))] + }; + + M.indices.emplace_back(new_f[0], new_f[1], new_f[2]); + } + + return M; +} + +int hollow_mesh_and_drill(indexed_triangle_set &hollowed_mesh, + const Interior &interior, + const DrainHoles &drainholes, + std::function on_hole_fail) +{ + auto tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set( + hollowed_mesh.vertices, + hollowed_mesh.indices + ); + + std::uniform_real_distribution dist(0., float(EPSILON)); + auto holes_mesh_cgal = MeshBoolean::cgal::triangle_mesh_to_cgal({}, {}); + indexed_triangle_set part_to_drill = hollowed_mesh; + + std::mt19937 m_rng{std::random_device{}()}; + + for (size_t i = 0; i < drainholes.size(); ++i) { + sla::DrainHole holept = drainholes[i]; + + holept.normal += Vec3f{dist(m_rng), dist(m_rng), dist(m_rng)}; + holept.normal.normalize(); + holept.pos += Vec3f{dist(m_rng), dist(m_rng), dist(m_rng)}; + indexed_triangle_set m = holept.to_mesh(); + + part_to_drill.indices.clear(); + auto bb = bounding_box(m); + Eigen::AlignedBox ebb{bb.min.cast(), + bb.max.cast()}; + + AABBTreeIndirect::traverse( + tree, + AABBTreeIndirect::intersecting(ebb), + [&part_to_drill, &hollowed_mesh](const auto& node) + { + part_to_drill.indices.emplace_back(hollowed_mesh.indices[node.idx]); + // continue traversal + return true; + }); + + auto cgal_meshpart = MeshBoolean::cgal::triangle_mesh_to_cgal( + remove_unconnected_vertices(part_to_drill)); + + if (MeshBoolean::cgal::does_self_intersect(*cgal_meshpart)) { + on_hole_fail(i); + continue; + } + + auto cgal_hole = MeshBoolean::cgal::triangle_mesh_to_cgal(m); + MeshBoolean::cgal::plus(*holes_mesh_cgal, *cgal_hole); + } + + auto ret = static_cast(HollowMeshResult::Ok); + + if (MeshBoolean::cgal::does_self_intersect(*holes_mesh_cgal)) { + ret |= static_cast(HollowMeshResult::DrillingFailed); + } + + auto hollowed_mesh_cgal = MeshBoolean::cgal::triangle_mesh_to_cgal(hollowed_mesh); + + if (!MeshBoolean::cgal::does_bound_a_volume(*hollowed_mesh_cgal)) { + ret |= static_cast(HollowMeshResult::FaultyMesh); + } + + if (!MeshBoolean::cgal::empty(*holes_mesh_cgal) + && !MeshBoolean::cgal::does_bound_a_volume(*holes_mesh_cgal)) { + ret |= static_cast(HollowMeshResult::FaultyHoles); + } + + // Don't even bother + if (ret & static_cast(HollowMeshResult::DrillingFailed)) + return ret; + + try { + if (!MeshBoolean::cgal::empty(*holes_mesh_cgal)) + MeshBoolean::cgal::minus(*hollowed_mesh_cgal, *holes_mesh_cgal); + + hollowed_mesh = + MeshBoolean::cgal::cgal_to_indexed_triangle_set(*hollowed_mesh_cgal); + + std::vector exclude_mask = + create_exclude_mask(hollowed_mesh, interior, drainholes); + + sla::remove_inside_triangles(hollowed_mesh, interior, exclude_mask); + } catch (const Slic3r::RuntimeError &) { + ret |= static_cast(HollowMeshResult::DrillingFailed); + } + + return ret; +} + }} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/Hollowing.hpp b/src/libslic3r/SLA/Hollowing.hpp index b57513fe7..b6c07aff5 100644 --- a/src/libslic3r/SLA/Hollowing.hpp +++ b/src/libslic3r/SLA/Hollowing.hpp @@ -3,10 +3,14 @@ #include #include +#include #include +#include namespace Slic3r { +class ModelObject; + namespace sla { struct HollowingConfig @@ -28,6 +32,9 @@ using InteriorPtr = std::unique_ptr; indexed_triangle_set & get_mesh(Interior &interior); const indexed_triangle_set &get_mesh(const Interior &interior); +const VoxelGrid & get_grid(const Interior &interior); +VoxelGrid &get_grid(Interior &interior); + struct DrainHole { Vec3f pos; @@ -46,18 +53,18 @@ struct DrainHole DrainHole(const DrainHole& rhs) : DrainHole(rhs.pos, rhs.normal, rhs.radius, rhs.height, rhs.failed) {} - + bool operator==(const DrainHole &sp) const; - + bool operator!=(const DrainHole &sp) const { return !(sp == (*this)); } bool is_inside(const Vec3f& pt) const; bool get_intersections(const Vec3f& s, const Vec3f& dir, std::array, 2>& out) const; - + indexed_triangle_set to_mesh() const; - + template inline void serialize(Archive &ar) { ar(pos, normal, radius, height, failed); @@ -70,26 +77,116 @@ using DrainHoles = std::vector; constexpr float HoleStickOutLength = 1.f; -InteriorPtr generate_interior(const TriangleMesh &mesh, +double get_voxel_scale(double mesh_volume, const HollowingConfig &hc); + +InteriorPtr generate_interior(const VoxelGrid &mesh, const HollowingConfig & = {}, const JobController &ctl = {}); +inline InteriorPtr generate_interior(const indexed_triangle_set &mesh, + const HollowingConfig &hc = {}, + const JobController &ctl = {}) +{ + auto voxel_scale = get_voxel_scale(its_volume(mesh), hc); + auto statusfn = [&ctl](int){ return ctl.stopcondition && ctl.stopcondition(); }; + auto grid = mesh_to_grid(mesh, MeshToGridParams{} + .voxel_scale(voxel_scale) + .exterior_bandwidth(3.f) + .interior_bandwidth(3.f) + .statusfn(statusfn)); + + if (!grid || (ctl.stopcondition && ctl.stopcondition())) + return {}; + +// if (its_is_splittable(mesh)) + grid = redistance_grid(*grid, 0.0f, 3.f, 3.f); + + return grid ? generate_interior(*grid, hc, ctl) : InteriorPtr{}; +} + +template double csgmesh_positive_maxvolume(const Cont &csg) +{ + double mesh_vol = 0; + + bool skip = false; + for (const auto &m : csg) { + auto op = csg::get_operation(m); + auto stackop = csg::get_stack_operation(m); + if (stackop == csg::CSGStackOp::Push && op != csg::CSGType::Union) + skip = true; + + if (!skip && csg::get_mesh(m) && op == csg::CSGType::Union) + mesh_vol = std::max(mesh_vol, + double(its_volume(*(csg::get_mesh(m))))); + + if (stackop == csg::CSGStackOp::Pop) + skip = false; + } + + return mesh_vol; +} + +template +InteriorPtr generate_interior(const Range &csgparts, + const HollowingConfig &hc = {}, + const JobController &ctl = {}) +{ + double mesh_vol = csgmesh_positive_maxvolume(csgparts); + double voxsc = get_voxel_scale(mesh_vol, hc); + + auto params = csg::VoxelizeParams{} + .voxel_scale(voxsc) + .exterior_bandwidth(3.f) + .interior_bandwidth(3.f) + .statusfn([&ctl](int){ return ctl.stopcondition && ctl.stopcondition(); }); + + auto ptr = csg::voxelize_csgmesh(csgparts, params); + + if (!ptr || (ctl.stopcondition && ctl.stopcondition())) + return {}; + + // TODO: figure out issues without the redistance +// if (csgparts.size() > 1 || its_is_splittable(*csg::get_mesh(*csgparts.begin()))) + + ptr = redistance_grid(*ptr, 0.0f, 3.f, 3.f); + + return ptr ? generate_interior(*ptr, hc, ctl) : InteriorPtr{}; +} + // Will do the hollowing void hollow_mesh(TriangleMesh &mesh, const HollowingConfig &cfg, int flags = 0); // Hollowing prepared in "interior", merge with original mesh void hollow_mesh(TriangleMesh &mesh, const Interior &interior, int flags = 0); +// Will do the hollowing +void hollow_mesh(indexed_triangle_set &mesh, const HollowingConfig &cfg, int flags = 0); + +// Hollowing prepared in "interior", merge with original mesh +void hollow_mesh(indexed_triangle_set &mesh, const Interior &interior, int flags = 0); + +enum class HollowMeshResult { + Ok = 0, + FaultyMesh = 1, + FaultyHoles = 2, + DrillingFailed = 4 +}; + +// Return HollowMeshResult codes OR-ed. +int hollow_mesh_and_drill( + indexed_triangle_set &mesh, + const Interior& interior, + const DrainHoles &holes, + std::function on_hole_fail = [](size_t){}); + void remove_inside_triangles(TriangleMesh &mesh, const Interior &interior, const std::vector &exclude_mask = {}); -double get_distance(const Vec3f &p, const Interior &interior); +void remove_inside_triangles(indexed_triangle_set &mesh, const Interior &interior, + const std::vector &exclude_mask = {}); -template -FloatingOnly get_distance(const Vec<3, T> &p, const Interior &interior) -{ - return get_distance(Vec3f(p.template cast()), interior); -} +sla::DrainHoles transformed_drainhole_points(const ModelObject &mo, + const Transform3d &trafo); void cut_drainholes(std::vector & obj_slices, const std::vector &slicegrid, @@ -103,6 +200,16 @@ inline void swap_normals(indexed_triangle_set &its) std::swap(face(0), face(2)); } +// Create exclude mask for triangle removal inside hollowed interiors. +// This is necessary when the interior is already part of the mesh which was +// drilled using CGAL mesh boolean operation. Excluded will be the triangles +// originally part of the interior mesh and triangles that make up the drilled +// hole walls. +std::vector create_exclude_mask( + const indexed_triangle_set &its, + const sla::Interior &interior, + const std::vector &holes); + } // namespace sla } // namespace Slic3r diff --git a/src/libslic3r/SLA/SupportPoint.hpp b/src/libslic3r/SLA/SupportPoint.hpp index 71849a364..cf6dbabba 100644 --- a/src/libslic3r/SLA/SupportPoint.hpp +++ b/src/libslic3r/SLA/SupportPoint.hpp @@ -3,7 +3,11 @@ #include -namespace Slic3r { namespace sla { +namespace Slic3r { + +class ModelObject; + +namespace sla { // An enum to keep track of where the current points on the ModelObject came from. enum class PointsStatus { @@ -62,6 +66,9 @@ struct SupportPoint using SupportPoints = std::vector; +SupportPoints transformed_support_points(const ModelObject &mo, + const Transform3d &trafo); + }} // namespace Slic3r::sla #endif // SUPPORTPOINT_HPP diff --git a/src/libslic3r/SLA/SupportPointGenerator.cpp b/src/libslic3r/SLA/SupportPointGenerator.cpp index 6d159b37b..193333bc6 100644 --- a/src/libslic3r/SLA/SupportPointGenerator.cpp +++ b/src/libslic3r/SLA/SupportPointGenerator.cpp @@ -661,5 +661,17 @@ void SupportPointGenerator::output_expolygons(const ExPolygons& expolys, const s } #endif +SupportPoints transformed_support_points(const ModelObject &mo, + const Transform3d &trafo) +{ + auto spts = mo.sla_support_points; + Transform3f tr = trafo.cast(); + for (sla::SupportPoint& suppt : spts) { + suppt.pos = tr * suppt.pos; + } + + return spts; +} + } // namespace sla } // namespace Slic3r diff --git a/src/libslic3r/SLA/SupportTree.cpp b/src/libslic3r/SLA/SupportTree.cpp index b221c4330..cfafdf7e9 100644 --- a/src/libslic3r/SLA/SupportTree.cpp +++ b/src/libslic3r/SLA/SupportTree.cpp @@ -18,6 +18,8 @@ #include #include +#include + //! macro used to mark string used at localization, //! return same string #define L(s) Slic3r::I18N::translate(s) @@ -30,6 +32,9 @@ indexed_triangle_set create_support_tree(const SupportableMesh &sm, auto builder = make_unique(ctl); if (sm.cfg.enabled) { + Benchmark bench; + bench.start(); + switch (sm.cfg.tree_type) { case SupportTreeType::Default: { create_default_tree(*builder, sm); @@ -39,9 +44,18 @@ indexed_triangle_set create_support_tree(const SupportableMesh &sm, create_branching_tree(*builder, sm); break; } + case SupportTreeType::Organic: { + // TODO + } default:; } + bench.stop(); + + BOOST_LOG_TRIVIAL(info) << "Support tree creation took: " + << bench.getElapsedSec() + << " seconds"; + builder->merge_and_cleanup(); // clean metadata, leave only the meshes. } diff --git a/src/libslic3r/SLA/SupportTree.hpp b/src/libslic3r/SLA/SupportTree.hpp index 0ac29ff7a..53fb16f6e 100644 --- a/src/libslic3r/SLA/SupportTree.hpp +++ b/src/libslic3r/SLA/SupportTree.hpp @@ -50,7 +50,7 @@ struct SupportTreeConfig // when bridges and pillars are merged. The resulting pillar should be a bit // thicker than the ones merging into it. How much thicker? I don't know // but it will be derived from this value. - double pillar_widening_factor = .05; + double pillar_widening_factor = .5; // Radius in mm of the pillar base. double base_radius_mm = 2.0; @@ -76,12 +76,20 @@ struct SupportTreeConfig double pillar_base_safety_distance_mm = 0.5; unsigned max_bridges_on_pillar = 3; - + + double max_weight_on_model_support = 10.f; + double head_fullwidth() const { return 2 * head_front_radius_mm + head_width_mm + 2 * head_back_radius_mm - head_penetration_mm; } + double safety_distance() const { return safety_distance_mm; } + double safety_distance(double r) const + { + return std::min(safety_distance_mm, r * safety_distance_mm / head_back_radius_mm); + } + // ///////////////////////////////////////////////////////////////////////// // Compile time configuration values (candidates for runtime) // ///////////////////////////////////////////////////////////////////////// @@ -89,13 +97,15 @@ struct SupportTreeConfig // The max Z angle for a normal at which it will get completely ignored. static const double constexpr normal_cutoff_angle = 150.0 * M_PI / 180.0; - // The shortest distance of any support structure from the model surface + // The safety gap between a support structure and model body. For support + // struts smaller than head_back_radius, the safety distance is scaled + // down accordingly. see method safety_distance() static const double constexpr safety_distance_mm = 0.5; static const double constexpr max_solo_pillar_height_mm = 15.0; static const double constexpr max_dual_pillar_height_mm = 35.0; - static const double constexpr optimizer_rel_score_diff = 1e-6; - static const unsigned constexpr optimizer_max_iterations = 1000; + static const double constexpr optimizer_rel_score_diff = 1e-10; + static const unsigned constexpr optimizer_max_iterations = 2000; static const unsigned constexpr pillar_cascade_neighbors = 3; }; @@ -108,6 +118,7 @@ struct SupportableMesh SupportPoints pts; SupportTreeConfig cfg; PadConfig pad_cfg; + double zoffset = 0.; explicit SupportableMesh(const indexed_triangle_set &trmsh, const SupportPoints &sp, @@ -115,16 +126,16 @@ struct SupportableMesh : emesh{trmsh}, pts{sp}, cfg{c} {} - explicit SupportableMesh(const AABBMesh &em, - const SupportPoints &sp, - const SupportTreeConfig &c) - : emesh{em}, pts{sp}, cfg{c} - {} +// explicit SupportableMesh(const AABBMesh &em, +// const SupportPoints &sp, +// const SupportTreeConfig &c) +// : emesh{em}, pts{sp}, cfg{c} +// {} }; inline double ground_level(const SupportableMesh &sm) { - double lvl = sm.emesh.ground_level() - + double lvl = sm.zoffset - !bool(sm.pad_cfg.embed_object) * sm.cfg.enabled * sm.cfg.object_elevation_mm + bool(sm.pad_cfg.embed_object) * sm.pad_cfg.wall_thickness_mm; diff --git a/src/libslic3r/SLA/SupportTreeBuilder.hpp b/src/libslic3r/SLA/SupportTreeBuilder.hpp index 29d34ab8e..93fbead99 100644 --- a/src/libslic3r/SLA/SupportTreeBuilder.hpp +++ b/src/libslic3r/SLA/SupportTreeBuilder.hpp @@ -67,25 +67,32 @@ struct SupportTreeNode long id = ID_UNSET; // For identification withing a tree. }; +// A junction connecting bridges and pillars +struct Junction: public SupportTreeNode { + double r = 1; + Vec3d pos; + + Junction(const Vec3d &tr, double r_mm) : r(r_mm), pos(tr) {} +}; + // A pinhead originating from a support point struct Head: public SupportTreeNode { Vec3d dir = DOWN; Vec3d pos = {0, 0, 0}; - + double r_back_mm = 1; double r_pin_mm = 0.5; double width_mm = 2; double penetration_mm = 0.5; - // If there is a pillar connecting to this head, then the id will be set. long pillar_id = ID_UNSET; - + long bridge_id = ID_UNSET; - + inline void invalidate() { id = ID_UNSET; } inline bool is_valid() const { return id >= 0; } - + Head(double r_big_mm, double r_small_mm, double length_mm, @@ -103,21 +110,21 @@ struct Head: public SupportTreeNode { { return real_width() - penetration_mm; } - + + inline Junction junction() const + { + Junction j{pos + (fullwidth() - r_back_mm) * dir, r_back_mm}; + j.id = -this->id; // Remember that this junction is from a head + + return j; + } + inline Vec3d junction_point() const { - return pos + (fullwidth() - r_back_mm) * dir; + return junction().pos; } }; -// A junction connecting bridges and pillars -struct Junction: public SupportTreeNode { - double r = 1; - Vec3d pos; - - Junction(const Vec3d &tr, double r_mm) : r(r_mm), pos(tr) {} -}; - // A straight pillar. Only has an endpoint and a height. No explicit starting // point is given, as it would allow the pillar to be angled. // Some connection info with other primitives can also be tracked. @@ -189,6 +196,10 @@ struct DiffBridge: public Bridge { DiffBridge(const Vec3d &p_s, const Vec3d &p_e, double r_s, double r_e) : Bridge{p_s, p_e, r_s}, end_r{r_e} {} + + DiffBridge(const Junction &j_s, const Junction &j_e) + : Bridge{j_s.pos, j_e.pos, j_s.r}, end_r{j_e.r} + {} }; // This class will hold the support tree parts (not meshes, but logical parts) diff --git a/src/libslic3r/SLA/SupportTreeMesher.cpp b/src/libslic3r/SLA/SupportTreeMesher.cpp index 3f0b6c841..6d91de7e6 100644 --- a/src/libslic3r/SLA/SupportTreeMesher.cpp +++ b/src/libslic3r/SLA/SupportTreeMesher.cpp @@ -165,7 +165,8 @@ indexed_triangle_set halfcone(double baseheight, { assert(steps > 0); - if (baseheight <= 0 || steps <= 0) return {}; + if (baseheight <= 0 || steps <= 0 || (r_bottom <= 0. && r_top <= 0.)) + return {}; indexed_triangle_set base; diff --git a/src/libslic3r/SLA/SupportTreeStrategies.hpp b/src/libslic3r/SLA/SupportTreeStrategies.hpp index 487f43575..2cef1a8a9 100644 --- a/src/libslic3r/SLA/SupportTreeStrategies.hpp +++ b/src/libslic3r/SLA/SupportTreeStrategies.hpp @@ -5,7 +5,7 @@ namespace Slic3r { namespace sla { -enum class SupportTreeType { Default, Branching }; +enum class SupportTreeType { Default, Branching, Organic }; enum class PillarConnectionMode { zigzag, cross, dynamic }; }} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/SupportTreeUtils.hpp b/src/libslic3r/SLA/SupportTreeUtils.hpp index 38515f879..aebd50950 100644 --- a/src/libslic3r/SLA/SupportTreeUtils.hpp +++ b/src/libslic3r/SLA/SupportTreeUtils.hpp @@ -6,10 +6,13 @@ #include #include +#include #include #include #include +#include +#include #include namespace Slic3r { namespace sla { @@ -23,36 +26,12 @@ using Slic3r::opt::AlgNLoptGenetic; using Slic3r::Geometry::dir_to_spheric; using Slic3r::Geometry::spheric_to_dir; -// Helper function for pillar interconnection where pairs of already connected -// pillars should be checked for not to be processed again. This can be done -// in constant time with a set of hash values uniquely representing a pair of -// integers. The order of numbers within the pair should not matter, it has -// the same unique hash. The hash value has to have twice as many bits as the -// arguments need. If the same integral type is used for args and return val, -// make sure the arguments use only the half of the type's bit depth. -template> -IntegerOnly pairhash(I a, I b) -{ - using std::ceil; using std::log2; using std::max; using std::min; - static const auto constexpr Ibits = int(sizeof(I) * CHAR_BIT); - static const auto constexpr DoubleIbits = int(sizeof(DoubleI) * CHAR_BIT); - static const auto constexpr shift = DoubleIbits / 2 < Ibits ? Ibits / 2 : Ibits; - - I g = min(a, b), l = max(a, b); - - // Assume the hash will fit into the output variable - assert((g ? (ceil(log2(g))) : 0) <= shift); - assert((l ? (ceil(log2(l))) : 0) <= shift); - - return (DoubleI(g) << shift) + l; -} - // Give points on a 3D ring with given center, radius and orientation // method based on: // https://math.stackexchange.com/questions/73237/parametric-equation-of-a-circle-in-3d-space template class PointRing { - std::array m_phis; + std::array m_phis; // Two vectors that will be perpendicular to each other and to the // axis. Values for a(X) and a(Y) are now arbitrary, a(Z) is just a @@ -72,7 +51,7 @@ class PointRing { public: - PointRing(const Vec3d &n) : m_phis{linspace_array(0., 2 * PI)} + PointRing(const Vec3d &n) : m_phis{linspace_array(0., 2 * PI)} { // We have to address the case when the direction vector v (same as // dir) is coincident with one of the world axes. In this case two of @@ -91,7 +70,10 @@ public: Vec3d get(size_t idx, const Vec3d &src, double r) const { - double phi = m_phis[idx]; + if (idx == 0) + return src; + + double phi = m_phis[idx - 1]; double sinphi = std::sin(phi); double cosphi = std::cos(phi); @@ -131,31 +113,40 @@ inline StopCriteria get_criteria(const SupportTreeConfig &cfg) // A simple sphere with a center and a radius struct Ball { Vec3d p; double R; }; -struct Beam { // Defines a set of rays displaced along a cone's surface - static constexpr size_t SAMPLES = 8; +template +struct Beam_ { // Defines a set of rays displaced along a cone's surface + static constexpr size_t SAMPLES = Samples; - Vec3d src; - Vec3d dir; + Vec3d src; + Vec3d dir; double r1; double r2; // radius of the beam 1 unit further from src in dir direction - Beam(const Vec3d &s, const Vec3d &d, double R1, double R2): - src{s}, dir{d}, r1{R1}, r2{R2} {}; + Beam_(const Vec3d &s, const Vec3d &d, double R1, double R2) + : src{s}, dir{d}, r1{R1}, r2{R2} {}; - Beam(const Ball &src_ball, const Ball &dst_ball): - src{src_ball.p}, dir{dirv(src_ball.p, dst_ball.p)}, r1{src_ball.R} + Beam_(const Ball &src_ball, const Ball &dst_ball) + : src{src_ball.p}, dir{dirv(src_ball.p, dst_ball.p)}, r1{src_ball.R} { - r2 = src_ball.R + - (dst_ball.R - src_ball.R) / distance(src_ball.p, dst_ball.p); + r2 = src_ball.R; + auto d = distance(src_ball.p, dst_ball.p); + + if (d > EPSILON) + r2 += (dst_ball.R - src_ball.R) / d; } - Beam(const Vec3d &s, const Vec3d &d, double R) + Beam_(const Vec3d &s, const Vec3d &d, double R) : src{s}, dir{d}, r1{R}, r2{R} {} }; -template -Hit beam_mesh_hit(Ex ex, const AABBMesh &mesh, const Beam &beam, double sd) +using Beam = Beam_<>; + +template +Hit beam_mesh_hit(Ex policy, + const AABBMesh &mesh, + const Beam_ &beam, + double sd) { Vec3d src = beam.src; Vec3d dst = src + beam.dir; @@ -164,19 +155,19 @@ Hit beam_mesh_hit(Ex ex, const AABBMesh &mesh, const Beam &beam, double sd) Vec3d D = (dst - src); Vec3d dir = D.normalized(); - PointRing ring{dir}; + PointRing ring{dir}; using Hit = AABBMesh::hit_result; // Hit results - std::array hits; + std::array hits; execution::for_each( - ex, size_t(0), hits.size(), + policy, size_t(0), hits.size(), [&mesh, r_src, r_dst, src, dst, &ring, dir, sd, &hits](size_t i) { Hit &hit = hits[i]; - // Point on the circle on the pin sphere + // Point on the circle on the pin sphere Vec3d p_src = ring.get(i, src, r_src + sd); Vec3d p_dst = ring.get(i, dst, r_dst + sd); Vec3d raydir = (p_dst - p_src).normalized(); @@ -193,7 +184,7 @@ Hit beam_mesh_hit(Ex ex, const AABBMesh &mesh, const Beam &beam, double sd) } } else hit = hr; - }, std::min(execution::max_concurrency(ex), Beam::SAMPLES)); + }, std::min(execution::max_concurrency(policy), RayCount)); return min_hit(hits.begin(), hits.end()); } @@ -208,7 +199,10 @@ Hit pinhead_mesh_hit(Ex ex, double width, double sd) { - static const size_t SAMPLES = 8; + // Support tree generation speed depends heavily on this value. 8 is almost + // ok, but to prevent rare cases of collision, 16 is necessary, which makes + // the algorithm run about 60% longer. + static const size_t SAMPLES = 16; // Move away slightly from the touching point to avoid raycasting on the // inner surface of the mesh. @@ -283,199 +277,12 @@ Hit pinhead_mesh_hit(Ex ex, template Hit pinhead_mesh_hit(Ex ex, - const AABBMesh &mesh, - const Head &head, - double safety_d) + const AABBMesh &mesh, + const Head &head, + double safety_d) { return pinhead_mesh_hit(ex, mesh, head.pos, head.dir, head.r_pin_mm, - head.r_back_mm, head.width_mm, safety_d); -} - -template -std::optional search_widening_path(Ex policy, - const SupportableMesh &sm, - const Vec3d &jp, - const Vec3d &dir, - double radius, - double new_radius) -{ - double w = radius + 2 * sm.cfg.head_back_radius_mm; - double stopval = w + jp.z() - ground_level(sm); - Optimizer solver(get_criteria(sm.cfg).stop_score(stopval)); - - auto [polar, azimuth] = dir_to_spheric(dir); - - double fallback_ratio = radius / sm.cfg.head_back_radius_mm; - - auto oresult = solver.to_max().optimize( - [&policy, &sm, jp, radius, new_radius](const opt::Input<3> &input) { - auto &[plr, azm, t] = input; - - auto d = spheric_to_dir(plr, azm).normalized(); - - auto sd = new_radius * sm.cfg.safety_distance_mm / - sm.cfg.head_back_radius_mm; - - double ret = pinhead_mesh_hit(policy, sm.emesh, jp, d, radius, - new_radius, t, sd) - .distance(); - - Beam beam{jp + t * d, d, new_radius}; - double down = beam_mesh_hit(policy, sm.emesh, beam, sd).distance(); - - if (ret > t && std::isinf(down)) - ret += jp.z() - ground_level(sm); - - return ret; - }, - initvals({polar, azimuth, w}), // start with what we have - bounds({ - {PI - sm.cfg.bridge_slope, PI}, // Must not exceed the slope limit - {-PI, PI}, // azimuth can be a full search - {radius + sm.cfg.head_back_radius_mm, - fallback_ratio * sm.cfg.max_bridge_length_mm} - })); - - if (oresult.score >= stopval) { - polar = std::get<0>(oresult.optimum); - azimuth = std::get<1>(oresult.optimum); - double t = std::get<2>(oresult.optimum); - Vec3d endp = jp + t * spheric_to_dir(polar, azimuth); - - return DiffBridge(jp, endp, radius, sm.cfg.head_back_radius_mm); - } - - return {}; -} - -// This is a proxy function for pillar creation which will mind the gap -// between the pad and the model bottom in zero elevation mode. -// 'pinhead_junctionpt' is the starting junction point which needs to be -// routed down. sourcedir is the allowed direction of an optional bridge -// between the jp junction and the final pillar. -template -std::pair create_ground_pillar( - Ex policy, - SupportTreeBuilder &builder, - const SupportableMesh &sm, - const Vec3d &pinhead_junctionpt, - const Vec3d &sourcedir, - double radius, - double end_radius, - long head_id = SupportTreeNode::ID_UNSET) -{ - Vec3d jp = pinhead_junctionpt, endp = jp, dir = sourcedir; - long pillar_id = SupportTreeNode::ID_UNSET; - bool can_add_base = false, non_head = false; - - double gndlvl = 0.; // The Z level where pedestals should be - double jp_gnd = 0.; // The lowest Z where a junction center can be - double gap_dist = 0.; // The gap distance between the model and the pad - - double r2 = radius + (end_radius - radius) / (jp.z() - ground_level(sm)); - - auto to_floor = [&gndlvl](const Vec3d &p) { return Vec3d{p.x(), p.y(), gndlvl}; }; - - auto eval_limits = [&sm, &radius, &can_add_base, &gndlvl, &gap_dist, &jp_gnd] - (bool base_en = true) - { - can_add_base = base_en && radius >= sm.cfg.head_back_radius_mm; - double base_r = can_add_base ? sm.cfg.base_radius_mm : 0.; - gndlvl = ground_level(sm); - if (!can_add_base) gndlvl -= sm.pad_cfg.wall_thickness_mm; - jp_gnd = gndlvl + (can_add_base ? 0. : sm.cfg.head_back_radius_mm); - gap_dist = sm.cfg.pillar_base_safety_distance_mm + base_r + EPSILON; - }; - - eval_limits(); - - // We are dealing with a mini pillar that's potentially too long - if (radius < sm.cfg.head_back_radius_mm && jp.z() - gndlvl > 20 * radius) - { - std::optional diffbr = - search_widening_path(policy, sm, jp, dir, radius, - sm.cfg.head_back_radius_mm); - - if (diffbr && diffbr->endp.z() > jp_gnd) { - auto &br = builder.add_diffbridge(*diffbr); - if (head_id >= 0) builder.head(head_id).bridge_id = br.id; - endp = diffbr->endp; - radius = diffbr->end_r; - builder.add_junction(endp, radius); - non_head = true; - dir = diffbr->get_dir(); - eval_limits(); - } else return {false, pillar_id}; - } - - if (sm.cfg.object_elevation_mm < EPSILON) - { - // get a suitable direction for the corrector bridge. It is the - // original sourcedir's azimuth but the polar angle is saturated to the - // configured bridge slope. - auto [polar, azimuth] = dir_to_spheric(dir); - polar = PI - sm.cfg.bridge_slope; - Vec3d d = spheric_to_dir(polar, azimuth).normalized(); - auto sd = radius * sm.cfg.safety_distance_mm / sm.cfg.head_back_radius_mm; - double t = beam_mesh_hit(policy, sm.emesh, Beam{endp, d, radius, r2}, sd).distance(); - double tmax = std::min(sm.cfg.max_bridge_length_mm, t); - t = 0.; - - double zd = endp.z() - jp_gnd; - double tmax2 = zd / std::sqrt(1 - sm.cfg.bridge_slope * sm.cfg.bridge_slope); - tmax = std::min(tmax, tmax2); - - Vec3d nexp = endp; - double dlast = 0.; - while (((dlast = std::sqrt(sm.emesh.squared_distance(to_floor(nexp)))) < gap_dist || - !std::isinf(beam_mesh_hit(policy, sm.emesh, Beam{nexp, DOWN, radius, r2}, sd).distance())) && - t < tmax) - { - t += radius; - nexp = endp + t * d; - } - - if (dlast < gap_dist && can_add_base) { - nexp = endp; - t = 0.; - can_add_base = false; - eval_limits(can_add_base); - - zd = endp.z() - jp_gnd; - tmax2 = zd / std::sqrt(1 - sm.cfg.bridge_slope * sm.cfg.bridge_slope); - tmax = std::min(tmax, tmax2); - - while (((dlast = std::sqrt(sm.emesh.squared_distance(to_floor(nexp)))) < gap_dist || - !std::isinf(beam_mesh_hit(policy, sm.emesh, Beam{nexp, DOWN, radius}, sd).distance())) && t < tmax) { - t += radius; - nexp = endp + t * d; - } - } - - // Could not find a path to avoid the pad gap - if (dlast < gap_dist) return {false, pillar_id}; - - if (t > 0.) { // Need to make additional bridge - const Bridge& br = builder.add_bridge(endp, nexp, radius); - if (head_id >= 0) builder.head(head_id).bridge_id = br.id; - - builder.add_junction(nexp, radius); - endp = nexp; - non_head = true; - } - } - - Vec3d gp = to_floor(endp); - double h = endp.z() - gp.z(); - - pillar_id = head_id >= 0 && !non_head ? builder.add_pillar(head_id, h) : - builder.add_pillar(gp, h, radius, end_radius); - - if (can_add_base) - builder.add_pillar_base(pillar_id, sm.cfg.base_height_mm, - sm.cfg.base_radius_mm); - - return {true, pillar_id}; + head.r_back_mm, head.width_mm, safety_d); } inline double distance(const SupportPoint &a, const SupportPoint &b) @@ -530,13 +337,13 @@ bool optimize_pinhead_placement(Ex policy, double back_r = head.r_back_mm; - // skip if the tilt is not sane + // skip if the tilt is not sane if (polar < PI - m.cfg.normal_cutoff_angle) return false; - // We saturate the polar angle to 3pi/4 + // We saturate the polar angle to 3pi/4 polar = std::max(polar, PI - m.cfg.bridge_slope); - // save the head (pinpoint) position + // save the head (pinpoint) position Vec3d hp = head.pos; double lmin = m.cfg.head_width_mm, lmax = lmin; @@ -545,28 +352,26 @@ bool optimize_pinhead_placement(Ex policy, lmin = 0., lmax = m.cfg.head_penetration_mm; } - // The distance needed for a pinhead to not collide with model. + // The distance needed for a pinhead to not collide with model. double w = lmin + 2 * back_r + 2 * m.cfg.head_front_radius_mm - m.cfg.head_penetration_mm; double pin_r = head.r_pin_mm; - // Reassemble the now corrected normal + // Reassemble the now corrected normal auto nn = spheric_to_dir(polar, azimuth).normalized(); - double sd = back_r * m.cfg.safety_distance_mm / - m.cfg.head_back_radius_mm; + double sd = m.cfg.safety_distance(back_r); - // check available distance - Hit t = pinhead_mesh_hit(policy, m.emesh, hp, nn, pin_r, back_r, w, - sd); + // check available distance + Hit t = pinhead_mesh_hit(policy, m.emesh, hp, nn, pin_r, back_r, w, sd); if (t.distance() < w) { // Let's try to optimize this angle, there might be a // viable normal that doesn't collide with the model // geometry and its very close to the default. - Optimizer solver(get_criteria(m.cfg).stop_score(w).max_iterations(100)); + Optimizer solver(get_criteria(m.cfg).stop_score(w).max_iterations(100)); solver.seed(0); // we want deterministic behavior auto oresult = solver.to_max().optimize( @@ -627,7 +432,7 @@ std::optional calculate_pinhead_placement(Ex policy, }; if (optimize_pinhead_placement(policy, sm, head)) { - head.id = suppt_idx; + head.id = long(suppt_idx); return head; } @@ -635,81 +440,335 @@ std::optional calculate_pinhead_placement(Ex policy, return {}; } -template -std::pair connect_to_ground(Ex policy, - SupportTreeBuilder &builder, - const SupportableMesh &sm, - const Junction &j, - const Vec3d &dir, - double end_r) +struct GroundConnection { + // Currently, a ground connection will contain at most 2 additional junctions + // which will not require any allocations. If I come up with an algo that + // can produce a route to ground with more junctions, this design will be + // able to handle that. + static constexpr size_t MaxExpectedJunctions = 3; + + boost::container::small_vector path; + std::optional pillar_base; + + operator bool() const { return pillar_base.has_value() && !path.empty(); } +}; + +inline long build_ground_connection(SupportTreeBuilder &builder, + const SupportableMesh &sm, + const GroundConnection &conn) { - auto hjp = j.pos; - double r = j.r; - auto sd = r * sm.cfg.safety_distance_mm / sm.cfg.head_back_radius_mm; - double r2 = j.r + (end_r - j.r) / (j.pos.z() - ground_level(sm)); + long ret = SupportTreeNode::ID_UNSET; - double t = beam_mesh_hit(policy, sm.emesh, Beam{hjp, dir, r, r2}, sd).distance(); - double d = 0, tdown = 0; - t = std::min(t, sm.cfg.max_bridge_length_mm * r / sm.cfg.head_back_radius_mm); + if (!conn) + return ret; - while (d < t && - !std::isinf(tdown = beam_mesh_hit(policy, sm.emesh, - Beam{hjp + d * dir, DOWN, r, r2}, sd) - .distance())) { - d += r; + auto it = conn.path.begin(); + auto itnx = std::next(it); + + while (itnx != conn.path.end()) { + builder.add_diffbridge(*it, *itnx); + builder.add_junction(*itnx); + ++it; ++itnx; } - if(!std::isinf(tdown)) - return {false, SupportTreeNode::ID_UNSET}; + auto gp = conn.path.back().pos; + gp.z() = ground_level(sm); + double h = conn.path.back().pos.z() - gp.z(); - Vec3d endp = hjp + d * dir; - auto ret = create_ground_pillar(policy, builder, sm, endp, dir, r, end_r); + if (conn.pillar_base->r_top < sm.cfg.head_back_radius_mm) { + h += sm.pad_cfg.wall_thickness_mm; + gp.z() -= sm.pad_cfg.wall_thickness_mm; + } - if (ret.second >= 0) { - builder.add_bridge(hjp, endp, r); - builder.add_junction(endp, r); +// TODO: does not work yet +// if (conn.path.back().id < 0) { +// // this is a head +// long head_id = std::abs(conn.path.back().id); +// ret = builder.add_pillar(head_id, h); +// } else + + ret = builder.add_pillar(gp, h, conn.path.back().r, conn.pillar_base->r_top); + + if (conn.pillar_base->r_top >= sm.cfg.head_back_radius_mm) + builder.add_pillar_base(ret, conn.pillar_base->height, conn.pillar_base->r_bottom); + + return ret; +} + +template +constexpr bool IsWideningFn = std::is_invocable_r_v; + +// A widening function can determine how many ray samples should a beam contain +// (see in beam_mesh_hit) +template struct BeamSamples { static constexpr size_t Value = 8; }; +template constexpr size_t BeamSamplesV = BeamSamples>::Value; + +// To use with check_ground_route, full will check the bridge and the pillar, +// PillarOnly checks only the pillar for collisions. +enum class GroundRouteCheck { Full, PillarOnly }; + +// Returns the collision point with mesh if there is a collision or a ground point, +// given a source point with a direction of a potential avoidance bridge and +// a bridge length. +template> > +Vec3d check_ground_route( + Ex policy, + const SupportableMesh &sm, + const Junction &source, // source location + const Vec3d &dir, // direction of the bridge from the source + double bridge_len, // lenght of the avoidance bridge + WideningFn &&wideningfn, // Widening strategy + GroundRouteCheck type = GroundRouteCheck::Full + ) +{ + static const constexpr auto Samples = BeamSamplesV; + + Vec3d ret; + + const auto sd = sm.cfg.safety_distance(source.r); + const auto gndlvl = ground_level(sm); + + // Intersection of the suggested bridge with ground plane. If the bridge + // spans below ground, stop it at ground level. + double t = (gndlvl - source.pos.z()) / dir.z(); + bridge_len = std::min(t, bridge_len); + + Vec3d bridge_end = source.pos + bridge_len * dir; + + double down_l = bridge_end.z() - gndlvl; + double bridge_r = wideningfn(Ball{source.pos, source.r}, dir, bridge_len); + double brhit_dist = 0.; + + if (bridge_len > EPSILON && type == GroundRouteCheck::Full) { + // beam_mesh_hit with a zero lenght bridge is invalid + + Beam_ bridgebeam{Ball{source.pos, source.r}, + Ball{bridge_end, bridge_r}}; + + auto brhit = beam_mesh_hit(policy, sm.emesh, bridgebeam, sd); + brhit_dist = brhit.distance(); + } else { + brhit_dist = bridge_len; + } + + if (brhit_dist < bridge_len) { + ret = (source.pos + brhit_dist * dir); + } else if (down_l > 0.) { + // check if pillar can be placed below + auto gp = Vec3d{bridge_end.x(), bridge_end.y(), gndlvl}; + double end_radius = wideningfn( + Ball{bridge_end, bridge_r}, DOWN, bridge_end.z() - gndlvl); + + Beam_ gndbeam {{bridge_end, bridge_r}, {gp, end_radius}}; + auto gndhit = beam_mesh_hit(policy, sm.emesh, gndbeam, sd); + double gnd_hit_d = std::min(gndhit.distance(), down_l + EPSILON); + + if (source.r >= sm.cfg.head_back_radius_mm && gndhit.distance() > down_l && sm.cfg.object_elevation_mm < EPSILON) { + // Dealing with zero elevation mode, to not route pillars + // into the gap between the optional pad and the model + double gap = std::sqrt(sm.emesh.squared_distance(gp)); + double base_r = std::max(sm.cfg.base_radius_mm, end_radius); + double min_gap = sm.cfg.pillar_base_safety_distance_mm + base_r; + + if (gap < min_gap) { + gnd_hit_d = down_l - min_gap + gap; + } + } + + ret = Vec3d{bridge_end.x(), bridge_end.y(), bridge_end.z() - gnd_hit_d}; + } else { + ret = bridge_end; } return ret; } -template -std::pair search_ground_route(Ex policy, - SupportTreeBuilder &builder, - const SupportableMesh &sm, - const Junction &j, - double end_radius, - const Vec3d &init_dir = DOWN) +// Searching a ground connection from an arbitrary source point. +// Currently, the result will contain one avoidance bridge (at most) and a +// pillar to the ground, if it's feasible +template> > +GroundConnection deepsearch_ground_connection( + Ex policy, + const SupportableMesh &sm, + const Junction &source, + WideningFn &&wideningfn, + const Vec3d &init_dir = DOWN) { - double downdst = j.pos.z() - ground_level(sm); + constexpr unsigned MaxIterationsGlobal = 5000; + constexpr unsigned MaxIterationsLocal = 100; + constexpr double RelScoreDiff = 0.05; - auto res = connect_to_ground(policy, builder, sm, j, init_dir, end_radius); - if (res.first) - return res; + const auto gndlvl = ground_level(sm); - // Optimize bridge direction: - // Straight path failed so we will try to search for a suitable - // direction out of the cavity. - auto [polar, azimuth] = dir_to_spheric(init_dir); + // The used solver (AlgNLoptMLSL_Subplx search method) is composed of a global (MLSL) + // and a local (Subplex) search method. Criteria can be set in a way that + // local searches are quick and less accurate. The global method will only + // consider the max iteration number and the stop score (Z level <= ground) - Optimizer solver(get_criteria(sm.cfg).stop_score(1e6)); - solver.seed(0); // we want deterministic behavior + auto criteria = get_criteria(sm.cfg); // get defaults from cfg + criteria.max_iterations(MaxIterationsGlobal); + criteria.abs_score_diff(NaNd); + criteria.rel_score_diff(NaNd); + criteria.stop_score(gndlvl); - auto sd = j.r * sm.cfg.safety_distance_mm / sm.cfg.head_back_radius_mm; - auto oresult = solver.to_max().optimize( - [&j, sd, &policy, &sm, &downdst, &end_radius](const opt::Input<2> &input) { - auto &[plr, azm] = input; - Vec3d n = spheric_to_dir(plr, azm).normalized(); - Beam beam{Ball{j.pos, j.r}, Ball{j.pos + downdst * n, end_radius}}; - return beam_mesh_hit(policy, sm.emesh, beam, sd).distance(); - }, - initvals({polar, azimuth}), // let's start with what we have - bounds({ {PI - sm.cfg.bridge_slope, PI}, {-PI, PI} }) - ); + auto criteria_loc = criteria; + criteria_loc.max_iterations(MaxIterationsLocal); + criteria_loc.abs_score_diff(EPSILON); + criteria_loc.rel_score_diff(RelScoreDiff); - Vec3d bridgedir = spheric_to_dir(oresult.optimum).normalized(); + Optimizer solver(criteria); + solver.set_loc_criteria(criteria_loc); + solver.seed(0); // require repeatability - return connect_to_ground(policy, builder, sm, j, bridgedir, end_radius); + // functor returns the z height of collision point, given a polar and + // azimuth angles as bridge direction and bridge length. The route is + // traced from source, through this bridge and an attached pillar. If there + // is a collision with the mesh, the Z height is returned. Otherwise the + // z level of ground is returned. + auto z_fn = [&](const opt::Input<3> &input) { + // solver suggests polar, azimuth and bridge length values: + auto &[plr, azm, bridge_len] = input; + + Vec3d n = spheric_to_dir(plr, azm); + + Vec3d hitpt = check_ground_route(policy, sm, source, n, bridge_len, wideningfn); + + return hitpt.z(); + }; + + // Calculate the initial direction of the search by + // saturating the polar angle to max tilt defined in config + auto [plr_init, azm_init] = dir_to_spheric(init_dir); + plr_init = std::max(plr_init, PI - sm.cfg.bridge_slope); + + auto bound_constraints = + bounds({ + {PI - sm.cfg.bridge_slope, PI}, // bounds for polar angle + {-PI, PI}, // bounds for azimuth + {0., sm.cfg.max_bridge_length_mm} // bounds bridge length + }); + + // The optimizer can navigate fairly well on the mesh surface, finding + // lower and lower Z coordinates as collision points. MLSL is not a local + // search method, so it should not be trapped in a local minima. Eventually, + // this search should arrive at a ground location. + auto oresult = solver.to_min().optimize( + z_fn, + initvals({plr_init, azm_init, 0.}), + bound_constraints + ); + + GroundConnection conn; + + // Extract and apply the result + auto [plr, azm, bridge_l] = oresult.optimum; + Vec3d n = spheric_to_dir(plr, azm); + assert(std::abs(n.norm() - 1.) < EPSILON); + + double t = (gndlvl - source.pos.z()) / n.z(); + bridge_l = std::min(t, bridge_l); + + // Now the optimizer gave a possible route to ground with a bridge direction + // and length. This length can be shortened further by brute-force queries + // of free route straigt down for a possible pillar. + // NOTE: This requirement could be incorporated into the optimization as a + // constraint, but it would not find quickly enough an accurate solution, + // and it would be very hard to define a stop score which is very useful in + // terminating the search as soon as the ground is found. + double l = 0., l_max = bridge_l; + double zlvl = std::numeric_limits::infinity(); + while(zlvl > gndlvl && l <= l_max) { + + zlvl = check_ground_route(policy, sm, source, n, l, wideningfn, + GroundRouteCheck::PillarOnly).z(); + + if (zlvl <= gndlvl) + bridge_l = l; + + l += source.r; + } + + Vec3d bridge_end = source.pos + bridge_l * n; + Vec3d gp{bridge_end.x(), bridge_end.y(), gndlvl}; + + double bridge_r = wideningfn(Ball{source.pos, source.r}, n, bridge_l); + double down_l = bridge_end.z() - gndlvl; + double end_radius = wideningfn(Ball{bridge_end, bridge_r}, DOWN, down_l); + double base_r = std::max(sm.cfg.base_radius_mm, end_radius); + + // Even if the search was not succesful, the result is populated by the + // source and the last best result of the optimization. + conn.path.emplace_back(source); + if (bridge_l > EPSILON) + conn.path.emplace_back(Junction{bridge_end, bridge_r}); + + // The resulting ground connection is only valid if the pillar base is set. + // At this point it will only be set if the search was succesful. + if (z_fn(opt::Input<3>({plr, azm, bridge_l})) <= gndlvl) + conn.pillar_base = + Pedestal{gp, sm.cfg.base_height_mm, base_r, end_radius}; + + return conn; +} + +// Ground route search with a predefined end radius +template +GroundConnection deepsearch_ground_connection(Ex policy, + const SupportableMesh &sm, + const Junction &source, + double end_radius, + const Vec3d &init_dir = DOWN) +{ + double gndlvl = ground_level(sm); + auto wfn = [end_radius, gndlvl](const Ball &src, const Vec3d &dir, double len) { + if (len < EPSILON) + return src.R; + + Vec3d dst = src.p + len * dir; + double widening = end_radius - src.R; + double zlen = dst.z() - gndlvl; + double full_len = len + zlen; + double r = src.R + widening * len / full_len; + + return r; + }; + + static_assert(IsWideningFn, "Not a widening function"); + + return deepsearch_ground_connection(policy, sm, source, wfn, init_dir); +} + +struct DefaultWideningModel { + static constexpr double WIDENING_SCALE = 0.02; + const SupportableMesh &sm; + + double operator()(const Ball &src, const Vec3d & /*dir*/, double len) { + static_assert(IsWideningFn, + "DefaultWideningModel is not a widening function"); + + double w = WIDENING_SCALE * sm.cfg.pillar_widening_factor * len; + return std::max(src.R, sm.cfg.head_back_radius_mm) + w; + }; +}; + +template<> struct BeamSamples { + static constexpr size_t Value = 16; +}; + +template +GroundConnection deepsearch_ground_connection(Ex policy, + const SupportableMesh &sm, + const Junction &source, + const Vec3d &init_dir = DOWN) +{ + return deepsearch_ground_connection(policy, sm, source, + DefaultWideningModel{sm}, init_dir); } template @@ -729,8 +788,7 @@ bool optimize_anchor_placement(Ex policy, double lmax = std::min(sm.cfg.head_width_mm, distance(from.pos, anchor.pos) - 2 * from.r); - double sd = anchor.r_back_mm * sm.cfg.safety_distance_mm / - sm.cfg.head_back_radius_mm; + double sd = sm.cfg.safety_distance(anchor.r_back_mm); Optimizer solver(get_criteria(sm.cfg) .stop_score(anchor.fullwidth()) @@ -796,6 +854,92 @@ std::optional calculate_anchor_placement(Ex policy, return anchor; } +inline bool is_outside_support_cone(const Vec3f &supp, + const Vec3f &pt, + float angle) +{ + using namespace Slic3r; + + Vec3d D = (pt - supp).cast(); + double dot_sq = -D.z() * std::abs(-D.z()); + + return dot_sq < + D.squaredNorm() * std::cos(angle) * std::abs(std::cos(angle)); +} + +inline // TODO: should be in a cpp +std::optional find_merge_pt(const Vec3f &A, + const Vec3f &B, + float critical_angle) +{ + // The idea is that A and B both have their support cones. But searching + // for the intersection of these support cones is difficult and its enough + // to reduce this problem to 2D and search for the intersection of two + // rays that merge somewhere between A and B. The 2D plane is a vertical + // slice of the 3D scene where the 2D Y axis is equal to the 3D Z axis and + // the 2D X axis is determined by the XY direction of the AB vector. + // + // Z^ + // | A * + // | . . B * + // | . . . . + // | . . . . + // | . x . + // -------------------> XY + + // Determine the transformation matrix for the 2D projection: + Vec3f diff = {B.x() - A.x(), B.y() - A.y(), 0.f}; + Vec3f dir = diff.normalized(); // TODO: avoid normalization + + Eigen::Matrix tr2D; + tr2D.row(0) = Vec3f{dir.x(), dir.y(), dir.z()}; + tr2D.row(1) = Vec3f{0.f, 0.f, 1.f}; + + // Transform the 2 vectors A and B into 2D vector 'a' and 'b'. Here we can + // omit 'a', pretend that its the origin and use BA as the vector b. + Vec2f b = tr2D * (B - A); + + // Get the square sine of the ray emanating from 'a' towards 'b'. This ray might + // exceed the allowed angle but that is corrected subsequently. + // The sign of the original sine is also needed, hence b.y is multiplied by + // abs(b.y) + float b_sqn = b.squaredNorm(); + float sin2sig_a = b_sqn > EPSILON ? (b.y() * std::abs(b.y())) / b_sqn : 0.f; + + // sine2 from 'b' to 'a' is the opposite of sine2 from a to b + float sin2sig_b = -sin2sig_a; + + // Derive the allowed angles from the given critical angle. + // critical_angle is measured from the horizontal X axis. + // The rays need to go downwards which corresponds to negative angles + + float sincrit = std::sin(critical_angle); // sine of the critical angle + float sin2crit = -sincrit * sincrit; // signed sine squared + sin2sig_a = std::min(sin2sig_a, sin2crit); // Do the angle saturation of both rays + sin2sig_b = std::min(sin2sig_b, sin2crit); // + float sin2_a = std::abs(sin2sig_a); // Get cosine squared values + float sin2_b = std::abs(sin2sig_b); + float cos2_a = 1.f - sin2_a; + float cos2_b = 1.f - sin2_b; + + // Derive the new direction vectors. This is by square rooting the sin2 + // and cos2 values and restoring the original signs + Vec2f Da = {std::copysign(std::sqrt(cos2_a), b.x()), std::copysign(std::sqrt(sin2_a), sin2sig_a)}; + Vec2f Db = {-std::copysign(std::sqrt(cos2_b), b.x()), std::copysign(std::sqrt(sin2_b), sin2sig_b)}; + + // Determine where two rays ([0, 0], Da), (b, Db) intersect. + // Based on + // https://stackoverflow.com/questions/27459080/given-two-points-and-two-direction-vectors-find-the-point-where-they-intersect + // One ray is emanating from (0, 0) so the formula is simplified + double t1 = (Db.y() * b.x() - b.y() * Db.x()) / + (Da.x() * Db.y() - Da.y() * Db.x()); + + Vec2f mp = t1 * Da; + Vec3f Mp = A + tr2D.transpose() * mp; + + return t1 >= 0.f ? Mp : Vec3f{}; +} + }} // namespace Slic3r::sla #endif // SLASUPPORTTREEUTILS_H diff --git a/src/libslic3r/SLA/SupportTreeUtilsLegacy.hpp b/src/libslic3r/SLA/SupportTreeUtilsLegacy.hpp new file mode 100644 index 000000000..b504d82fb --- /dev/null +++ b/src/libslic3r/SLA/SupportTreeUtilsLegacy.hpp @@ -0,0 +1,300 @@ +#ifndef SUPPORTTREEUTILSLEGACY_HPP +#define SUPPORTTREEUTILSLEGACY_HPP + +#include "SupportTreeUtils.hpp" + +// Old functions are gathered here that are used in DefaultSupportTree +// to maintain functionality that was well tested. + +namespace Slic3r { namespace sla { + +// Helper function for pillar interconnection where pairs of already connected +// pillars should be checked for not to be processed again. This can be done +// in constant time with a set of hash values uniquely representing a pair of +// integers. The order of numbers within the pair should not matter, it has +// the same unique hash. The hash value has to have twice as many bits as the +// arguments need. If the same integral type is used for args and return val, +// make sure the arguments use only the half of the type's bit depth. +template> +IntegerOnly pairhash(I a, I b) +{ + using std::ceil; using std::log2; using std::max; using std::min; + static const auto constexpr Ibits = int(sizeof(I) * CHAR_BIT); + static const auto constexpr DoubleIbits = int(sizeof(DoubleI) * CHAR_BIT); + static const auto constexpr shift = DoubleIbits / 2 < Ibits ? Ibits / 2 : Ibits; + + I g = min(a, b), l = max(a, b); + + // Assume the hash will fit into the output variable + assert((g ? (ceil(log2(g))) : 0) <= shift); + assert((l ? (ceil(log2(l))) : 0) <= shift); + + return (DoubleI(g) << shift) + l; +} + +template +std::optional search_widening_path(Ex policy, + const SupportableMesh &sm, + const Vec3d &jp, + const Vec3d &dir, + double radius, + double new_radius) +{ + double w = radius + 2 * sm.cfg.head_back_radius_mm; + double stopval = w + jp.z() - ground_level(sm); + Optimizer solver(get_criteria(sm.cfg).stop_score(stopval)); + + auto [polar, azimuth] = dir_to_spheric(dir); + + double fallback_ratio = radius / sm.cfg.head_back_radius_mm; + + auto oresult = solver.to_max().optimize( + [&policy, &sm, jp, radius, new_radius](const opt::Input<3> &input) { + auto &[plr, azm, t] = input; + + auto d = spheric_to_dir(plr, azm).normalized(); + + auto sd = sm.cfg.safety_distance(new_radius); + + double ret = pinhead_mesh_hit(policy, sm.emesh, jp, d, radius, + new_radius, t, sd) + .distance(); + + Beam beam{jp + t * d, d, new_radius}; + double down = beam_mesh_hit(policy, sm.emesh, beam, sd).distance(); + + if (ret > t && std::isinf(down)) + ret += jp.z() - ground_level(sm); + + return ret; + }, + initvals({polar, azimuth, w}), // start with what we have + bounds({ + {PI - sm.cfg.bridge_slope, PI}, // Must not exceed the slope limit + {-PI, PI}, // azimuth can be a full search + {radius + sm.cfg.head_back_radius_mm, + fallback_ratio * sm.cfg.max_bridge_length_mm} + })); + + if (oresult.score >= stopval) { + polar = std::get<0>(oresult.optimum); + azimuth = std::get<1>(oresult.optimum); + double t = std::get<2>(oresult.optimum); + Vec3d endp = jp + t * spheric_to_dir(polar, azimuth); + + return DiffBridge(jp, endp, radius, sm.cfg.head_back_radius_mm); + } + + return {}; +} + +// This is a proxy function for pillar creation which will mind the gap +// between the pad and the model bottom in zero elevation mode. +// 'pinhead_junctionpt' is the starting junction point which needs to be +// routed down. sourcedir is the allowed direction of an optional bridge +// between the jp junction and the final pillar. +template +std::pair create_ground_pillar( + Ex policy, + SupportTreeBuilder &builder, + const SupportableMesh &sm, + const Vec3d &pinhead_junctionpt, + const Vec3d &sourcedir, + double radius, + double end_radius, + long head_id = SupportTreeNode::ID_UNSET) +{ + Vec3d jp = pinhead_junctionpt, endp = jp, dir = sourcedir; + long pillar_id = SupportTreeNode::ID_UNSET; + bool can_add_base = false, non_head = false; + + double gndlvl = 0.; // The Z level where pedestals should be + double jp_gnd = 0.; // The lowest Z where a junction center can be + double gap_dist = 0.; // The gap distance between the model and the pad + + double r2 = radius + (end_radius - radius) / (jp.z() - ground_level(sm)); + + auto to_floor = [&gndlvl](const Vec3d &p) { return Vec3d{p.x(), p.y(), gndlvl}; }; + + auto eval_limits = [&sm, &radius, &can_add_base, &gndlvl, &gap_dist, &jp_gnd] + (bool base_en = true) + { + can_add_base = base_en && radius >= sm.cfg.head_back_radius_mm; + double base_r = can_add_base ? sm.cfg.base_radius_mm : 0.; + gndlvl = ground_level(sm); + if (!can_add_base) gndlvl -= sm.pad_cfg.wall_thickness_mm; + jp_gnd = gndlvl + (can_add_base ? 0. : sm.cfg.head_back_radius_mm); + gap_dist = sm.cfg.pillar_base_safety_distance_mm + base_r + EPSILON; + }; + + eval_limits(); + + // We are dealing with a mini pillar that's potentially too long + if (radius < sm.cfg.head_back_radius_mm && jp.z() - gndlvl > 20 * radius) + { + std::optional diffbr = + search_widening_path(policy, sm, jp, dir, radius, + sm.cfg.head_back_radius_mm); + + if (diffbr && diffbr->endp.z() > jp_gnd) { + auto &br = builder.add_diffbridge(*diffbr); + if (head_id >= 0) builder.head(head_id).bridge_id = br.id; + endp = diffbr->endp; + radius = diffbr->end_r; + builder.add_junction(endp, radius); + non_head = true; + dir = diffbr->get_dir(); + eval_limits(); + } else return {false, pillar_id}; + } + + if (sm.cfg.object_elevation_mm < EPSILON) + { + // get a suitable direction for the corrector bridge. It is the + // original sourcedir's azimuth but the polar angle is saturated to the + // configured bridge slope. + auto [polar, azimuth] = dir_to_spheric(dir); + polar = PI - sm.cfg.bridge_slope; + Vec3d d = spheric_to_dir(polar, azimuth).normalized(); + auto sd = radius * sm.cfg.safety_distance_mm / sm.cfg.head_back_radius_mm; + double t = beam_mesh_hit(policy, sm.emesh, Beam{endp, d, radius, r2}, sd).distance(); + double tmax = std::min(sm.cfg.max_bridge_length_mm, t); + t = 0.; + + double zd = endp.z() - jp_gnd; + double tmax2 = zd / std::sqrt(1 - sm.cfg.bridge_slope * sm.cfg.bridge_slope); + tmax = std::min(tmax, tmax2); + + Vec3d nexp = endp; + double dlast = 0.; + while (((dlast = std::sqrt(sm.emesh.squared_distance(to_floor(nexp)))) < gap_dist || + !std::isinf(beam_mesh_hit(policy, sm.emesh, Beam{nexp, DOWN, radius, r2}, sd).distance())) && + t < tmax) + { + t += radius; + nexp = endp + t * d; + } + + if (dlast < gap_dist && can_add_base) { + nexp = endp; + t = 0.; + can_add_base = false; + eval_limits(can_add_base); + + zd = endp.z() - jp_gnd; + tmax2 = zd / std::sqrt(1 - sm.cfg.bridge_slope * sm.cfg.bridge_slope); + tmax = std::min(tmax, tmax2); + + while (((dlast = std::sqrt(sm.emesh.squared_distance(to_floor(nexp)))) < gap_dist || + !std::isinf(beam_mesh_hit(policy, sm.emesh, Beam{nexp, DOWN, radius}, sd).distance())) && t < tmax) { + t += radius; + nexp = endp + t * d; + } + } + + // Could not find a path to avoid the pad gap + if (dlast < gap_dist) return {false, pillar_id}; + + if (t > 0.) { // Need to make additional bridge + const Bridge& br = builder.add_bridge(endp, nexp, radius); + if (head_id >= 0) builder.head(head_id).bridge_id = br.id; + + builder.add_junction(nexp, radius); + endp = nexp; + non_head = true; + } + } + + Vec3d gp = to_floor(endp); + double h = endp.z() - gp.z(); + + pillar_id = head_id >= 0 && !non_head ? builder.add_pillar(head_id, h) : + builder.add_pillar(gp, h, radius, end_radius); + + if (can_add_base) + builder.add_pillar_base(pillar_id, sm.cfg.base_height_mm, + sm.cfg.base_radius_mm); + + return {true, pillar_id}; +} + +template +std::pair connect_to_ground(Ex policy, + SupportTreeBuilder &builder, + const SupportableMesh &sm, + const Junction &j, + const Vec3d &dir, + double end_r) +{ + auto hjp = j.pos; + double r = j.r; + auto sd = r * sm.cfg.safety_distance_mm / sm.cfg.head_back_radius_mm; + double r2 = j.r + (end_r - j.r) / (j.pos.z() - ground_level(sm)); + + double t = beam_mesh_hit(policy, sm.emesh, Beam{hjp, dir, r, r2}, sd).distance(); + double d = 0, tdown = 0; + t = std::min(t, sm.cfg.max_bridge_length_mm * r / sm.cfg.head_back_radius_mm); + + while (d < t && + !std::isinf(tdown = beam_mesh_hit(policy, sm.emesh, + Beam{hjp + d * dir, DOWN, r, r2}, sd) + .distance())) { + d += r; + } + + if(!std::isinf(tdown)) + return {false, SupportTreeNode::ID_UNSET}; + + Vec3d endp = hjp + d * dir; + auto ret = create_ground_pillar(policy, builder, sm, endp, dir, r, end_r); + + if (ret.second >= 0) { + builder.add_bridge(hjp, endp, r); + builder.add_junction(endp, r); + } + + return ret; +} + +template +std::pair search_ground_route(Ex policy, + SupportTreeBuilder &builder, + const SupportableMesh &sm, + const Junction &j, + double end_radius, + const Vec3d &init_dir = DOWN) +{ + double downdst = j.pos.z() - ground_level(sm); + + auto res = connect_to_ground(policy, builder, sm, j, init_dir, end_radius); + if (res.first) + return res; + + // Optimize bridge direction: + // Straight path failed so we will try to search for a suitable + // direction out of the cavity. + auto [polar, azimuth] = dir_to_spheric(init_dir); + + Optimizer solver(get_criteria(sm.cfg).stop_score(1e6)); + solver.seed(0); // we want deterministic behavior + + auto sd = j.r * sm.cfg.safety_distance_mm / sm.cfg.head_back_radius_mm; + auto oresult = solver.to_max().optimize( + [&j, sd, &policy, &sm, &downdst, &end_radius](const opt::Input<2> &input) { + auto &[plr, azm] = input; + Vec3d n = spheric_to_dir(plr, azm).normalized(); + Beam beam{Ball{j.pos, j.r}, Ball{j.pos + downdst * n, end_radius}}; + return beam_mesh_hit(policy, sm.emesh, beam, sd).distance(); + }, + initvals({polar, azimuth}), // let's start with what we have + bounds({ {PI - sm.cfg.bridge_slope, PI}, {-PI, PI} }) + ); + + Vec3d bridgedir = spheric_to_dir(oresult.optimum).normalized(); + + return connect_to_ground(policy, builder, sm, j, bridgedir, end_radius); +} + +}} // namespace Slic3r::sla + +#endif // SUPPORTTREEUTILSLEGACY_HPP diff --git a/src/libslic3r/SLAPrint.cpp b/src/libslic3r/SLAPrint.cpp index d9b8e33df..ffae989d4 100644 --- a/src/libslic3r/SLAPrint.cpp +++ b/src/libslic3r/SLAPrint.cpp @@ -1,13 +1,9 @@ #include "SLAPrint.hpp" #include "SLAPrintSteps.hpp" +#include "CSGMesh/CSGMeshCopy.hpp" +#include "CSGMesh/PerformCSGMeshBooleans.hpp" -#include "Format/SL1.hpp" -#include "Format/SL1_SVG.hpp" -#include "Format/pwmx.hpp" - -#include "ClipperUtils.hpp" #include "Geometry.hpp" -#include "MTUtils.hpp" #include "Thread.hpp" #include @@ -41,31 +37,66 @@ bool is_zero_elevation(const SLAPrintObjectConfig &c) sla::SupportTreeConfig make_support_cfg(const SLAPrintObjectConfig& c) { sla::SupportTreeConfig scfg; - + scfg.enabled = c.supports_enable.getBool(); scfg.tree_type = c.support_tree_type.value; - scfg.head_front_radius_mm = 0.5*c.support_head_front_diameter.getFloat(); - double pillar_r = 0.5 * c.support_pillar_diameter.getFloat(); - scfg.head_back_radius_mm = pillar_r; - scfg.head_fallback_radius_mm = - 0.01 * c.support_small_pillar_diameter_percent.getFloat() * pillar_r; - scfg.head_penetration_mm = c.support_head_penetration.getFloat(); - scfg.head_width_mm = c.support_head_width.getFloat(); - scfg.object_elevation_mm = is_zero_elevation(c) ? - 0. : c.support_object_elevation.getFloat(); - scfg.bridge_slope = c.support_critical_angle.getFloat() * PI / 180.0 ; - scfg.max_bridge_length_mm = c.support_max_bridge_length.getFloat(); - scfg.max_pillar_link_distance_mm = c.support_max_pillar_link_distance.getFloat(); - scfg.pillar_connection_mode = c.support_pillar_connection_mode.value; - scfg.ground_facing_only = c.support_buildplate_only.getBool(); - scfg.pillar_widening_factor = c.support_pillar_widening_factor.getFloat(); - scfg.base_radius_mm = 0.5*c.support_base_diameter.getFloat(); - scfg.base_height_mm = c.support_base_height.getFloat(); - scfg.pillar_base_safety_distance_mm = - c.support_base_safety_distance.getFloat() < EPSILON ? - scfg.safety_distance_mm : c.support_base_safety_distance.getFloat(); - - scfg.max_bridges_on_pillar = unsigned(c.support_max_bridges_on_pillar.getInt()); + + switch(scfg.tree_type) { + case sla::SupportTreeType::Default: { + scfg.head_front_radius_mm = 0.5*c.support_head_front_diameter.getFloat(); + double pillar_r = 0.5 * c.support_pillar_diameter.getFloat(); + scfg.head_back_radius_mm = pillar_r; + scfg.head_fallback_radius_mm = + 0.01 * c.support_small_pillar_diameter_percent.getFloat() * pillar_r; + scfg.head_penetration_mm = c.support_head_penetration.getFloat(); + scfg.head_width_mm = c.support_head_width.getFloat(); + scfg.object_elevation_mm = is_zero_elevation(c) ? + 0. : c.support_object_elevation.getFloat(); + scfg.bridge_slope = c.support_critical_angle.getFloat() * PI / 180.0 ; + scfg.max_bridge_length_mm = c.support_max_bridge_length.getFloat(); + scfg.max_pillar_link_distance_mm = c.support_max_pillar_link_distance.getFloat(); + scfg.pillar_connection_mode = c.support_pillar_connection_mode.value; + scfg.ground_facing_only = c.support_buildplate_only.getBool(); + scfg.pillar_widening_factor = c.support_pillar_widening_factor.getFloat(); + scfg.base_radius_mm = 0.5*c.support_base_diameter.getFloat(); + scfg.base_height_mm = c.support_base_height.getFloat(); + scfg.pillar_base_safety_distance_mm = + c.support_base_safety_distance.getFloat() < EPSILON ? + scfg.safety_distance_mm : c.support_base_safety_distance.getFloat(); + + scfg.max_bridges_on_pillar = unsigned(c.support_max_bridges_on_pillar.getInt()); + scfg.max_weight_on_model_support = c.support_max_weight_on_model.getFloat(); + break; + } + case sla::SupportTreeType::Branching: + [[fallthrough]]; + case sla::SupportTreeType::Organic:{ + scfg.head_front_radius_mm = 0.5*c.branchingsupport_head_front_diameter.getFloat(); + double pillar_r = 0.5 * c.branchingsupport_pillar_diameter.getFloat(); + scfg.head_back_radius_mm = pillar_r; + scfg.head_fallback_radius_mm = + 0.01 * c.branchingsupport_small_pillar_diameter_percent.getFloat() * pillar_r; + scfg.head_penetration_mm = c.branchingsupport_head_penetration.getFloat(); + scfg.head_width_mm = c.branchingsupport_head_width.getFloat(); + scfg.object_elevation_mm = is_zero_elevation(c) ? + 0. : c.branchingsupport_object_elevation.getFloat(); + scfg.bridge_slope = c.branchingsupport_critical_angle.getFloat() * PI / 180.0 ; + scfg.max_bridge_length_mm = c.branchingsupport_max_bridge_length.getFloat(); + scfg.max_pillar_link_distance_mm = c.branchingsupport_max_pillar_link_distance.getFloat(); + scfg.pillar_connection_mode = c.branchingsupport_pillar_connection_mode.value; + scfg.ground_facing_only = c.branchingsupport_buildplate_only.getBool(); + scfg.pillar_widening_factor = c.branchingsupport_pillar_widening_factor.getFloat(); + scfg.base_radius_mm = 0.5*c.branchingsupport_base_diameter.getFloat(); + scfg.base_height_mm = c.branchingsupport_base_height.getFloat(); + scfg.pillar_base_safety_distance_mm = + c.branchingsupport_base_safety_distance.getFloat() < EPSILON ? + scfg.safety_distance_mm : c.branchingsupport_base_safety_distance.getFloat(); + + scfg.max_bridges_on_pillar = unsigned(c.branchingsupport_max_bridges_on_pillar.getInt()); + scfg.max_weight_on_model_support = c.branchingsupport_max_weight_on_model.getFloat(); + break; + } + } return scfg; } @@ -159,14 +190,13 @@ static std::vector sla_instances(const ModelObject &mo std::vector instances; assert(! model_object.instances.empty()); if (! model_object.instances.empty()) { - Vec3d rotation0 = model_object.instances.front()->get_rotation(); - rotation0(2) = 0.; + const Transform3d& trafo0 = model_object.instances.front()->get_matrix(); for (ModelInstance *model_instance : model_object.instances) if (model_instance->is_printable()) { instances.emplace_back( model_instance->id(), Point::new_scale(model_instance->get_offset(X), model_instance->get_offset(Y)), - float(Geometry::rotation_diff_z(rotation0, model_instance->get_rotation()))); + float(Geometry::rotation_diff_z(trafo0, model_instance->get_matrix()))); } } return instances; @@ -388,7 +418,12 @@ SLAPrint::ApplyStatus SLAPrint::apply(const Model &model, DynamicPrintConfig con if (it_print_object_status != print_object_status.end() && it_print_object_status->id != model_object.id()) it_print_object_status = print_object_status.end(); // Check whether a model part volume was added or removed, their transformations or order changed. - bool model_parts_differ = model_volume_list_changed(model_object, model_object_new, ModelVolumeType::MODEL_PART); + bool model_parts_differ = + model_volume_list_changed(model_object, model_object_new, + {ModelVolumeType::MODEL_PART, + ModelVolumeType::NEGATIVE_VOLUME, + ModelVolumeType::SUPPORT_ENFORCER, + ModelVolumeType::SUPPORT_BLOCKER}); bool sla_trafo_differs = model_object.instances.empty() != model_object_new.instances.empty() || (! model_object.instances.empty() && @@ -597,7 +632,7 @@ void SLAPrint::process() // We want to first process all objects... std::vector level1_obj_steps = { - slaposHollowing, slaposDrillHoles, slaposObjectSlice, slaposSupportPoints, slaposSupportTree, slaposPad + slaposAssembly, slaposHollowing, slaposDrillHoles, slaposObjectSlice, slaposSupportPoints, slaposSupportTree, slaposPad }; // and then slice all supports to allow preview to be displayed ASAP @@ -793,12 +828,6 @@ bool SLAPrint::is_step_done(SLAPrintObjectStep step) const SLAPrintObject::SLAPrintObject(SLAPrint *print, ModelObject *model_object) : Inherited(print, model_object) - , m_transformed_rmesh([this](TriangleMesh &obj) { - obj = m_model_object->raw_mesh(); - if (!obj.empty()) { - obj.transform(m_trafo); - } - }) {} SLAPrintObject::~SLAPrintObject() {} @@ -827,6 +856,7 @@ bool SLAPrintObject::invalidate_state_by_config_options(const std::vectorinvalidate_all_steps(); + } else if (step == slaposHollowing) { + invalidated |= invalidated |= this->invalidate_steps({ slaposDrillHoles, slaposObjectSlice, slaposSupportPoints, slaposSupportTree, slaposPad, slaposSliceSupports }); } else if (step == slaposDrillHoles) { invalidated |= this->invalidate_steps({ slaposObjectSlice, slaposSupportPoints, slaposSupportTree, slaposPad, slaposSliceSupports }); invalidated |= m_print->invalidate_step(slapsMergeSlicesAndEval); @@ -907,7 +959,7 @@ bool SLAPrintObject::invalidate_step(SLAPrintObjectStep step) bool SLAPrintObject::invalidate_all_steps() { - return Inherited::invalidate_all_steps() | m_print->invalidate_all_steps(); + return Inherited::invalidate_all_steps() || m_print->invalidate_all_steps(); } double SLAPrintObject::get_elevation() const { @@ -998,113 +1050,71 @@ const ExPolygons &SliceRecord::get_slice(SliceOrigin o) const return idx >= v.size() ? EMPTY_SLICE : v[idx]; } -bool SLAPrintObject::has_mesh(SLAPrintObjectStep step) const -{ - switch (step) { - case slaposDrillHoles: - return m_hollowing_data && !m_hollowing_data->hollow_mesh_with_holes.empty(); - case slaposSupportTree: - return ! this->support_mesh().empty(); - case slaposPad: - return ! this->pad_mesh().empty(); - default: - return false; - } -} - -TriangleMesh SLAPrintObject::get_mesh(SLAPrintObjectStep step) const -{ - switch (step) { - case slaposSupportTree: - return this->support_mesh(); - case slaposPad: - return this->pad_mesh(); - case slaposDrillHoles: - if (m_hollowing_data) - return get_mesh_to_print(); - [[fallthrough]]; - default: - return TriangleMesh(); - } -} - const TriangleMesh& SLAPrintObject::support_mesh() const { - if(m_config.supports_enable.getBool() && m_supportdata) + if (m_config.supports_enable.getBool() && + is_step_done(slaposSupportTree) && + m_supportdata) return m_supportdata->tree_mesh; - + return EMPTY_MESH; } const TriangleMesh& SLAPrintObject::pad_mesh() const { - if(m_config.pad_enable.getBool() && m_supportdata) + if(m_config.pad_enable.getBool() && is_step_done(slaposPad) && m_supportdata) return m_supportdata->pad_mesh; return EMPTY_MESH; } -const indexed_triangle_set &SLAPrintObject::hollowed_interior_mesh() const +const std::shared_ptr & +SLAPrintObject::get_mesh_to_print() const { - if (m_hollowing_data && m_hollowing_data->interior && - m_config.hollowing_enable.getBool()) - return sla::get_mesh(*m_hollowing_data->interior); - - return EMPTY_TRIANGLE_SET; + int s = last_completed_step(); + + while (s > 0 && ! m_preview_meshes[s]) + --s; + + return m_preview_meshes[s]; } -const TriangleMesh &SLAPrintObject::transformed_mesh() const { - // we need to transform the raw mesh... - // currently all the instances share the same x and y rotation and scaling - // so we have to extract those from e.g. the first instance and apply to the - // raw mesh. This is also true for the support points. - // BUT: when the support structure is spawned for each instance than it has - // to omit the X, Y rotation and scaling as those have been already applied - // or apply an inverse transformation on the support structure after it - // has been created. +std::vector SLAPrintObject::get_parts_to_slice() const +{ + return get_parts_to_slice(slaposCount); +} - return m_transformed_rmesh.get(); +std::vector +SLAPrintObject::get_parts_to_slice(SLAPrintObjectStep untilstep) const +{ + auto laststep = last_completed_step(); + SLAPrintObjectStep s = std::min(untilstep, laststep); + + if (s == slaposCount) + return {}; + + std::vector ret; + + for (int step = 0; step < s; ++step) { + auto r = m_mesh_to_slice.equal_range(SLAPrintObjectStep(step)); + csg::copy_csgrange_shallow(Range{r.first, r.second}, std::back_inserter(ret)); + } + + return ret; } sla::SupportPoints SLAPrintObject::transformed_support_points() const { - assert(m_model_object != nullptr); - auto spts = m_model_object->sla_support_points; - const Transform3d& vol_trafo = m_model_object->volumes.front()->get_transformation().get_matrix(); - const Transform3f& tr = (trafo() * vol_trafo).cast(); - for (sla::SupportPoint& suppt : spts) { - suppt.pos = tr * suppt.pos; - } - - return spts; + assert(model_object()); + + return sla::transformed_support_points(*model_object(), trafo()); } sla::DrainHoles SLAPrintObject::transformed_drainhole_points() const { - assert(m_model_object != nullptr); - auto pts = m_model_object->sla_drain_holes; - const Transform3d& vol_trafo = m_model_object->volumes.front()->get_transformation().get_matrix(); - const Geometry::Transformation trans(trafo() * vol_trafo); - const Transform3f& tr = trans.get_matrix().cast(); - const Vec3f sc = trans.get_scaling_factor().cast(); - for (sla::DrainHole &hl : pts) { - hl.pos = tr * hl.pos; - hl.normal = tr * hl.normal - tr.translation(); + assert(model_object()); - // The normal scales as a covector (and we must also - // undo the damage already done). - hl.normal = Vec3f(hl.normal(0)/(sc(0)*sc(0)), - hl.normal(1)/(sc(1)*sc(1)), - hl.normal(2)/(sc(2)*sc(2))); - - // Now shift the hole a bit above the object and make it deeper to - // compensate for it. This is to avoid problems when the hole is placed - // on (nearly) flat surface. - hl.pos -= hl.normal.normalized() * sla::HoleStickOutLength; - hl.height += sla::HoleStickOutLength; - } - - return pts; + return sla::transformed_drainhole_points(*model_object(), trafo()); } DynamicConfig SLAPrintStatistics::config() const @@ -1161,4 +1171,17 @@ void SLAPrint::StatusReporter::operator()(SLAPrint & p, p.set_status(int(std::round(st)), msg, flags); } +namespace csg { + +MeshBoolean::cgal::CGALMeshPtr get_cgalmesh(const CSGPartForStep &part) +{ + if (!part.cgalcache && csg::get_mesh(part)) { + part.cgalcache = csg::get_cgalmesh(static_cast(part)); + } + + return part.cgalcache? clone(*part.cgalcache) : nullptr; +} + +} // namespace csg + } // namespace Slic3r diff --git a/src/libslic3r/SLAPrint.hpp b/src/libslic3r/SLAPrint.hpp index 2ebb7cc2f..bd8424ca7 100644 --- a/src/libslic3r/SLAPrint.hpp +++ b/src/libslic3r/SLAPrint.hpp @@ -3,16 +3,18 @@ #include #include +#include + #include "PrintBase.hpp" -#include "SLA/RasterBase.hpp" #include "SLA/SupportTree.hpp" #include "Point.hpp" -#include "MTUtils.hpp" -#include "Zipper.hpp" #include "Format/SLAArchiveWriter.hpp" #include "GCode/ThumbnailData.hpp" +#include "libslic3r/CSGMesh/CSGMesh.hpp" +#include "libslic3r/MeshBoolean.hpp" +#include "libslic3r/OpenVDBUtils.hpp" -#include "libslic3r/Execution/ExecutionTBB.hpp" +#include namespace Slic3r { @@ -23,6 +25,7 @@ enum SLAPrintStep : unsigned int { }; enum SLAPrintObjectStep : unsigned int { + slaposAssembly, slaposHollowing, slaposDrillHoles, slaposObjectSlice, @@ -45,10 +48,47 @@ using _SLAPrintObjectBase = enum SliceOrigin { soSupport, soModel }; +} // namespace Slic3r + +namespace Slic3r { + +// Each sla object step can hold a collection of csg operations on the +// sla model to be sliced. Currently, Assembly step adds negative and positive +// volumes, hollowing adds the negative interior, drilling adds the hole cylinders. +// They need to be processed in this specific order. If CSGPartForStep instances +// are put into a multiset container the key being the sla step, +// iterating over the container will maintain the correct order of csg operations. +struct CSGPartForStep : public csg::CSGPart +{ + SLAPrintObjectStep key; + mutable MeshBoolean::cgal::CGALMeshPtr cgalcache; + + CSGPartForStep(SLAPrintObjectStep k, CSGPart &&p = {}) + : key{k}, CSGPart{std::move(p)} + {} + + CSGPartForStep &operator=(CSGPart &&part) + { + this->its_ptr = std::move(part.its_ptr); + this->operation = part.operation; + + return *this; + } + + bool operator<(const CSGPartForStep &other) const { return key < other.key; } +}; + +namespace csg { + +MeshBoolean::cgal::CGALMeshPtr get_cgalmesh(const CSGPartForStep &part); + +} // namespace csg + class SLAPrintObject : public _SLAPrintObjectBase { private: // Prevents erroneous use by other classes. using Inherited = _SLAPrintObjectBase; + using CSGContainer = std::multiset; public: @@ -72,31 +112,20 @@ public: }; const std::vector& instances() const { return m_instances; } - bool has_mesh(SLAPrintObjectStep step) const; - TriangleMesh get_mesh(SLAPrintObjectStep step) const; - // Get a support mesh centered around origin in XY, and with zero rotation around Z applied. // Support mesh is only valid if this->is_step_done(slaposSupportTree) is true. const TriangleMesh& support_mesh() const; // Get a pad mesh centered around origin in XY, and with zero rotation around Z applied. // Support mesh is only valid if this->is_step_done(slaposPad) is true. const TriangleMesh& pad_mesh() const; - - // Ready after this->is_step_done(slaposDrillHoles) is true - const indexed_triangle_set &hollowed_interior_mesh() const; // Get the mesh that is going to be printed with all the modifications // like hollowing and drilled holes. - const TriangleMesh & get_mesh_to_print() const { - return (m_hollowing_data && is_step_done(slaposDrillHoles)) ? m_hollowing_data->hollow_mesh_with_holes_trimmed : transformed_mesh(); - } + const std::shared_ptr& get_mesh_to_print() const; - const TriangleMesh & get_mesh_to_slice() const { - return (m_hollowing_data && is_step_done(slaposDrillHoles)) ? m_hollowing_data->hollow_mesh_with_holes : transformed_mesh(); - } + std::vector get_parts_to_slice() const; - // This will return the transformed mesh which is cached - const TriangleMesh& transformed_mesh() const; + std::vector get_parts_to_slice(SLAPrintObjectStep step) const; sla::SupportPoints transformed_support_points() const; sla::DrainHoles transformed_drainhole_points() const; @@ -275,7 +304,8 @@ protected: { m_config.apply_only(other, keys, ignore_nonexistent); } void set_trafo(const Transform3d& trafo, bool left_handed) { - m_transformed_rmesh.invalidate([this, &trafo, left_handed](){ m_trafo = trafo; m_left_handed = left_handed; }); + m_trafo = trafo; + m_left_handed = left_handed; } template inline void set_instances(InstVec&& instances) { m_instances = std::forward(instances); } @@ -306,9 +336,6 @@ private: std::vector m_model_height_levels; - // Caching the transformed (m_trafo) raw mesh of the object - mutable CachedObject m_transformed_rmesh; - struct SupportData { sla::SupportableMesh input; // the input @@ -318,6 +345,10 @@ private: inline SupportData(const TriangleMesh &t) : input{t.its, {}, {}} {} + + inline SupportData(const indexed_triangle_set &t) + : input{t, {}, {}} + {} void create_support_tree(const sla::JobController &ctl) { @@ -329,16 +360,32 @@ private: pad_mesh = TriangleMesh{sla::create_pad(input, tree_mesh.its, ctl)}; } }; - - std::unique_ptr m_supportdata; - + + std::unique_ptr m_supportdata; + + // Holds CSG operations for the printed object, prioritized by print steps. + CSGContainer m_mesh_to_slice; + + auto mesh_to_slice(SLAPrintObjectStep s) const + { + auto r = m_mesh_to_slice.equal_range(s); + + return Range{r.first, r.second}; + } + + auto mesh_to_slice() const { return range(m_mesh_to_slice); } + + // Holds the preview of the object to be printed (as it will look like with + // all its holes and cavities, negatives and positive volumes unified. + // Essentially this should be a m_mesh_to_slice after the CSG operations + // or an approximation of that. + std::array, SLAPrintObjectStep::slaposCount + 1> m_preview_meshes; + class HollowingData { public: sla::InteriorPtr interior; - mutable TriangleMesh hollow_mesh_with_holes; // caching the complete hollowed mesh - mutable TriangleMesh hollow_mesh_with_holes_trimmed; }; std::unique_ptr m_hollowing_data; @@ -421,6 +468,11 @@ public: const PrintObjects& objects() const { return m_objects; } // PrintObject by its ObjectID, to be used to uniquely bind slicing warnings to their source PrintObjects // in the notification center. + const SLAPrintObject* get_print_object_by_model_object_id(ObjectID object_id) const { + auto it = std::find_if(m_objects.begin(), m_objects.end(), + [object_id](const SLAPrintObject* obj) { return obj->model_object()->id() == object_id; }); + return (it == m_objects.end()) ? nullptr : *it; + } const SLAPrintObject* get_object(ObjectID object_id) const { auto it = std::find_if(m_objects.begin(), m_objects.end(), [object_id](const SLAPrintObject *obj) { return obj->id() == object_id; }); diff --git a/src/libslic3r/SLAPrintSteps.cpp b/src/libslic3r/SLAPrintSteps.cpp index bed4f0249..92260deec 100644 --- a/src/libslic3r/SLAPrintSteps.cpp +++ b/src/libslic3r/SLAPrintSteps.cpp @@ -14,8 +14,17 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include +//#include #include @@ -32,18 +41,20 @@ namespace Slic3r { namespace { const std::array OBJ_STEP_LEVELS = { - 10, // slaposHollowing, - 10, // slaposDrillHoles - 10, // slaposObjectSlice, - 20, // slaposSupportPoints, - 10, // slaposSupportTree, - 10, // slaposPad, - 30, // slaposSliceSupports, + 13, // slaposAssembly + 13, // slaposHollowing, + 13, // slaposDrillHoles + 13, // slaposObjectSlice, + 13, // slaposSupportPoints, + 13, // slaposSupportTree, + 11, // slaposPad, + 11, // slaposSliceSupports, }; std::string OBJ_STEP_LABELS(size_t idx) { switch (idx) { + case slaposAssembly: return L("Assembling model from parts"); case slaposHollowing: return L("Hollowing model"); case slaposDrillHoles: return L("Drilling holes into model."); case slaposObjectSlice: return L("Slicing model"); @@ -76,7 +87,6 @@ std::string PRINT_STEP_LABELS(size_t idx) SLAPrint::Steps::Steps(SLAPrint *print) : m_print{print} - , m_rng{std::random_device{}()} , objcount{m_print->m_objects.size()} , ilhd{m_print->m_material_config.initial_layer_height.getFloat()} , ilh{float(ilhd)} @@ -119,9 +129,228 @@ void SLAPrint::Steps::apply_printer_corrections(SLAPrintObject &po, SliceOrigin } } +template bool is_all_positive(const Cont &csgmesh) +{ + bool is_all_pos = + std::all_of(csgmesh.begin(), + csgmesh.end(), + [](auto &part) { + return csg::get_operation(part) == csg::CSGType::Union; + }); + + return is_all_pos; +} + +template +static indexed_triangle_set csgmesh_merge_positive_parts(const Cont &csgmesh) +{ + indexed_triangle_set m; + for (auto &csgpart : csgmesh) { + auto op = csg::get_operation(csgpart); + const indexed_triangle_set * pmesh = csg::get_mesh(csgpart); + if (pmesh && op == csg::CSGType::Union) { + indexed_triangle_set mcpy = *pmesh; + its_transform(mcpy, csg::get_transform(csgpart), true); + its_merge(m, mcpy); + } + } + + return m; +} + +indexed_triangle_set SLAPrint::Steps::generate_preview_vdb( + SLAPrintObject &po, SLAPrintObjectStep step) +{ + // Empirical upper limit to not get excessive performance hit + constexpr double MaxPreviewVoxelScale = 12.; + + // update preview mesh + double vscale = std::min(MaxPreviewVoxelScale, + 1. / po.m_config.layer_height.getFloat()); + + auto voxparams = csg::VoxelizeParams{} + .voxel_scale(vscale) + .exterior_bandwidth(1.f) + .interior_bandwidth(1.f); + + voxparams.statusfn([&po](int){ + return po.m_print->cancel_status() != CancelStatus::NOT_CANCELED; + }); + + auto r = range(po.m_mesh_to_slice); + auto grid = csg::voxelize_csgmesh(r, voxparams); + auto m = grid ? grid_to_mesh(*grid, 0., 0.01) : indexed_triangle_set{}; + float loss_less_max_error = 1e-6; + its_quadric_edge_collapse(m, 0U, &loss_less_max_error); + + return m; +} + +void SLAPrint::Steps::generate_preview(SLAPrintObject &po, SLAPrintObjectStep step) +{ + Benchmark bench; + + bench.start(); + + auto r = range(po.m_mesh_to_slice); + auto m = indexed_triangle_set{}; + + bool handled = false; + + if (is_all_positive(r)) { + m = csgmesh_merge_positive_parts(r); + handled = true; + } else if (csg::check_csgmesh_booleans(r) == r.end()) { + auto cgalmeshptr = csg::perform_csgmesh_booleans(r); + if (cgalmeshptr) { + m = MeshBoolean::cgal::cgal_to_indexed_triangle_set(*cgalmeshptr); + handled = true; + } else { + BOOST_LOG_TRIVIAL(warning) << "CSG mesh is not egligible for proper CGAL booleans!"; + } + } else { + // Normal cgal processing failed. If there are no negative volumes, + // the hollowing can be tried with the old algorithm which didn't handled volumes. + // If that fails for any of the drillholes, the voxelization fallback is + // used. + + bool is_pure_model = is_all_positive(po.mesh_to_slice(slaposAssembly)); + bool can_hollow = po.m_hollowing_data && po.m_hollowing_data->interior && + !sla::get_mesh(*po.m_hollowing_data->interior).empty(); + + + bool hole_fail = false; + if (step == slaposHollowing && is_pure_model) { + if (can_hollow) { + m = csgmesh_merge_positive_parts(r); + sla::hollow_mesh(m, *po.m_hollowing_data->interior, + sla::hfRemoveInsideTriangles); + } + + handled = true; + } else if (step == slaposDrillHoles && is_pure_model) { + if (po.m_model_object->sla_drain_holes.empty()) { + // Get the last printable preview + auto &meshp = po.get_mesh_to_print(); + if (meshp) + m = *(meshp); + + handled = true; + } else if (can_hollow) { + m = csgmesh_merge_positive_parts(r); + sla::hollow_mesh(m, *po.m_hollowing_data->interior); + sla::DrainHoles drainholes = po.transformed_drainhole_points(); + + auto ret = sla::hollow_mesh_and_drill( + m, *po.m_hollowing_data->interior, drainholes, + [/*&po, &drainholes, */&hole_fail](size_t i) + { + hole_fail = /*drainholes[i].failed = + po.model_object()->sla_drain_holes[i].failed =*/ true; + }); + + if (ret & static_cast(sla::HollowMeshResult::FaultyMesh)) { + po.active_step_add_warning( + PrintStateBase::WarningLevel::NON_CRITICAL, + L("Mesh to be hollowed is not suitable for hollowing (does not " + "bound a volume).")); + } + + if (ret & static_cast(sla::HollowMeshResult::FaultyHoles)) { + po.active_step_add_warning( + PrintStateBase::WarningLevel::NON_CRITICAL, + L("Unable to drill the current configuration of holes into the " + "model.")); + } + + handled = true; + + if (ret & static_cast(sla::HollowMeshResult::DrillingFailed)) { + po.active_step_add_warning( + PrintStateBase::WarningLevel::NON_CRITICAL, L( + "Drilling holes into the mesh failed. " + "This is usually caused by broken model. Try to fix it first.")); + + handled = false; + } + + if (hole_fail) { + po.active_step_add_warning(PrintStateBase::WarningLevel::NON_CRITICAL, + L("Failed to drill some holes into the model")); + + handled = false; + } + } + } + } + + if (!handled) { // Last resort to voxelization. + po.active_step_add_warning(PrintStateBase::WarningLevel::NON_CRITICAL, + L("Can't perform full mesh booleans! " + "Some parts of the print will be previewed with approximated meshes. " + "This does not affect the quality of slices or the physical print in any way.")); + m = generate_preview_vdb(po, step); + } + + po.m_preview_meshes[step] = + std::make_shared(std::move(m)); + + for (size_t i = size_t(step) + 1; i < slaposCount; ++i) + { + po.m_preview_meshes[i] = {}; + } + + bench.stop(); + + if (!m.empty()) + BOOST_LOG_TRIVIAL(trace) << "Preview gen took: " << bench.getElapsedSec(); + else + BOOST_LOG_TRIVIAL(error) << "Preview failed!"; + + using namespace std::string_literals; + + report_status(-2, "Reload preview from step "s + std::to_string(int(step)), SlicingStatus::RELOAD_SLA_PREVIEW); +} + +static inline +void clear_csg(std::multiset &s, SLAPrintObjectStep step) +{ + auto r = s.equal_range(step); + s.erase(r.first, r.second); +} + +struct csg_inserter { + std::multiset &m; + SLAPrintObjectStep key; + + csg_inserter &operator*() { return *this; } + void operator=(csg::CSGPart &&part) + { + part.its_ptr.convert_unique_to_shared(); + m.emplace(key, std::move(part)); + } + csg_inserter& operator++() { return *this; } +}; + +void SLAPrint::Steps::mesh_assembly(SLAPrintObject &po) +{ + po.m_mesh_to_slice.clear(); + po.m_supportdata.reset(); + po.m_hollowing_data.reset(); + + csg::model_to_csgmesh(*po.model_object(), po.trafo(), + csg_inserter{po.m_mesh_to_slice, slaposAssembly}, + csg::mpartsPositive | csg::mpartsNegative | csg::mpartsDoSplits); + + generate_preview(po, slaposAssembly); +} + void SLAPrint::Steps::hollow_model(SLAPrintObject &po) { po.m_hollowing_data.reset(); + po.m_supportdata.reset(); + clear_csg(po.m_mesh_to_slice, slaposDrillHoles); + clear_csg(po.m_mesh_to_slice, slaposHollowing); if (! po.m_config.hollowing_enable.getBool()) { BOOST_LOG_TRIVIAL(info) << "Skipping hollowing step!"; @@ -134,348 +363,104 @@ void SLAPrint::Steps::hollow_model(SLAPrintObject &po) double quality = po.m_config.hollowing_quality.getFloat(); double closing_d = po.m_config.hollowing_closing_distance.getFloat(); sla::HollowingConfig hlwcfg{thickness, quality, closing_d}; + sla::JobController ctl; + ctl.stopcondition = [this]() { return canceled(); }; + ctl.cancelfn = [this]() { throw_if_canceled(); }; - sla::InteriorPtr interior = generate_interior(po.transformed_mesh(), hlwcfg); + sla::InteriorPtr interior = + generate_interior(po.mesh_to_slice(), hlwcfg, ctl); if (!interior || sla::get_mesh(*interior).empty()) BOOST_LOG_TRIVIAL(warning) << "Hollowed interior is empty!"; else { po.m_hollowing_data.reset(new SLAPrintObject::HollowingData()); po.m_hollowing_data->interior = std::move(interior); - } -} -struct FaceHash { + indexed_triangle_set &m = sla::get_mesh(*po.m_hollowing_data->interior); - // A 64 bit number's max hex digits - static constexpr size_t MAX_NUM_CHARS = 16; + if (!m.empty()) { + // simplify mesh lossless + float loss_less_max_error = 2*std::numeric_limits::epsilon(); + its_quadric_edge_collapse(m, 0U, &loss_less_max_error); - // A hash is created for each triangle to be identifiable. The hash uses - // only the triangle's geometric traits, not the index in a particular mesh. - std::unordered_set facehash; - - // Returns the string in reverse, but that is ok for hashing - static std::array to_chars(int64_t val) - { - std::array ret; - - static const constexpr char * Conv = "0123456789abcdef"; - - auto ptr = ret.begin(); - auto uval = static_cast(std::abs(val)); - while (uval) { - *ptr = Conv[uval & 0xf]; - ++ptr; - uval = uval >> 4; - } - if (val < 0) { *ptr = '-'; ++ptr; } - *ptr = '\0'; // C style string ending - - return ret; - } - - static std::string hash(const Vec<3, int64_t> &v) - { - std::string ret; - ret.reserve(3 * MAX_NUM_CHARS); - - for (auto val : v) - ret += to_chars(val).data(); - - return ret; - } - - static std::string facekey(const Vec3i &face, const std::vector &vertices) - { - // Scale to integer to avoid floating points - std::array, 3> pts = { - scaled(vertices[face(0)]), - scaled(vertices[face(1)]), - scaled(vertices[face(2)]) - }; - - // Get the first two sides of the triangle, do a cross product and move - // that vector to the center of the triangle. This encodes all - // information to identify an identical triangle at the same position. - Vec<3, int64_t> a = pts[0] - pts[2], b = pts[1] - pts[2]; - Vec<3, int64_t> c = a.cross(b) + (pts[0] + pts[1] + pts[2]) / 3; - - // Return a concatenated string representation of the coordinates - return hash(c); - } - - FaceHash (const indexed_triangle_set &its): facehash(its.indices.size()) - { - for (const Vec3i &face : its.indices) - facehash.insert(facekey(face, its.vertices)); - } - - bool find(const std::string &key) - { - auto it = facehash.find(key); - return it != facehash.end(); - } -}; - -static void exclude_neighbors(const Vec3i &face, - std::vector &mask, - const indexed_triangle_set &its, - const VertexFaceIndex &index, - size_t recursions) -{ - for (int i = 0; i < 3; ++i) { - const auto &neighbors_range = index[face(i)]; - for (size_t fi_n : neighbors_range) { - mask[fi_n] = true; - if (recursions > 0) - exclude_neighbors(its.indices[fi_n], mask, its, index, recursions - 1); - } - } -} - -// Create exclude mask for triangle removal inside hollowed interiors. -// This is necessary when the interior is already part of the mesh which was -// drilled using CGAL mesh boolean operation. Excluded will be the triangles -// originally part of the interior mesh and triangles that make up the drilled -// hole walls. -static std::vector create_exclude_mask( - const indexed_triangle_set &its, - const sla::Interior &interior, - const std::vector &holes) -{ - FaceHash interior_hash{sla::get_mesh(interior)}; - - std::vector exclude_mask(its.indices.size(), false); - - VertexFaceIndex neighbor_index{its}; - - for (size_t fi = 0; fi < its.indices.size(); ++fi) { - auto &face = its.indices[fi]; - - if (interior_hash.find(FaceHash::facekey(face, its.vertices))) { - exclude_mask[fi] = true; - continue; + its_compactify_vertices(m); + its_merge_vertices(m); } - if (exclude_mask[fi]) { - exclude_neighbors(face, exclude_mask, its, neighbor_index, 1); - continue; - } + // Put the interior into the target mesh as a negative + po.m_mesh_to_slice + .emplace(slaposHollowing, + csg::CSGPart{std::make_shared(m), + csg::CSGType::Difference}); - // Lets deal with the holes. All the triangles of a hole and all the - // neighbors of these triangles need to be kept. The neigbors were - // created by CGAL mesh boolean operation that modified the original - // interior inside the input mesh to contain the holes. - Vec3d tr_center = ( - its.vertices[face(0)] + - its.vertices[face(1)] + - its.vertices[face(2)] - ).cast() / 3.; - - // If the center is more than half a mm inside the interior, - // it cannot possibly be part of a hole wall. - if (sla::get_distance(tr_center, interior) < -0.5) - continue; - - Vec3f U = its.vertices[face(1)] - its.vertices[face(0)]; - Vec3f V = its.vertices[face(2)] - its.vertices[face(0)]; - Vec3f C = U.cross(V); - Vec3f face_normal = C.normalized(); - - for (const sla::DrainHole &dh : holes) { - if (dh.failed) continue; - - Vec3d dhpos = dh.pos.cast(); - Vec3d dhend = dhpos + dh.normal.cast() * dh.height; - - Linef3 holeaxis{dhpos, dhend}; - - double D_hole_center = line_alg::distance_to(holeaxis, tr_center); - double D_hole = std::abs(D_hole_center - dh.radius); - float dot = dh.normal.dot(face_normal); - - // Empiric tolerances for center distance and normals angle. - // For triangles that are part of a hole wall the angle of - // triangle normal and the hole axis is around 90 degrees, - // so the dot product is around zero. - double D_tol = dh.radius / sla::DrainHole::steps; - float normal_angle_tol = 1.f / sla::DrainHole::steps; - - if (D_hole < D_tol && std::abs(dot) < normal_angle_tol) { - exclude_mask[fi] = true; - exclude_neighbors(face, exclude_mask, its, neighbor_index, 1); - } - } + generate_preview(po, slaposHollowing); } - - return exclude_mask; -} - -static indexed_triangle_set -remove_unconnected_vertices(const indexed_triangle_set &its) -{ - if (its.indices.empty()) {}; - - indexed_triangle_set M; - - std::vector vtransl(its.vertices.size(), -1); - int vcnt = 0; - for (auto &f : its.indices) { - - for (int i = 0; i < 3; ++i) - if (vtransl[size_t(f(i))] < 0) { - - M.vertices.emplace_back(its.vertices[size_t(f(i))]); - vtransl[size_t(f(i))] = vcnt++; - } - - std::array new_f = { - vtransl[size_t(f(0))], - vtransl[size_t(f(1))], - vtransl[size_t(f(2))] - }; - - M.indices.emplace_back(new_f[0], new_f[1], new_f[2]); - } - - return M; } // Drill holes into the hollowed/original mesh. void SLAPrint::Steps::drill_holes(SLAPrintObject &po) { - bool needs_drilling = ! po.m_model_object->sla_drain_holes.empty(); - bool is_hollowed = - (po.m_hollowing_data && po.m_hollowing_data->interior && - !sla::get_mesh(*po.m_hollowing_data->interior).empty()); + po.m_supportdata.reset(); + clear_csg(po.m_mesh_to_slice, slaposDrillHoles); - if (! is_hollowed && ! needs_drilling) { - // In this case we can dump any data that might have been - // generated on previous runs. - po.m_hollowing_data.reset(); - return; - } + csg::model_to_csgmesh(*po.model_object(), po.trafo(), + csg_inserter{po.m_mesh_to_slice, slaposDrillHoles}, + csg::mpartsDrillHoles); - if (! po.m_hollowing_data) - po.m_hollowing_data.reset(new SLAPrintObject::HollowingData()); + generate_preview(po, slaposDrillHoles); - // Hollowing and/or drilling is active, m_hollowing_data is valid. + // Release the data, won't be needed anymore, takes huge amount of ram + if (po.m_hollowing_data && po.m_hollowing_data->interior) + po.m_hollowing_data->interior.reset(); +} - // Regenerate hollowed mesh, even if it was there already. It may contain - // holes that are no longer on the frontend. - TriangleMesh &hollowed_mesh = po.m_hollowing_data->hollow_mesh_with_holes; - hollowed_mesh = po.transformed_mesh(); - if (is_hollowed) - sla::hollow_mesh(hollowed_mesh, *po.m_hollowing_data->interior); - - TriangleMesh &mesh_view = po.m_hollowing_data->hollow_mesh_with_holes_trimmed; - - if (! needs_drilling) { - mesh_view = po.transformed_mesh(); - - if (is_hollowed) - sla::hollow_mesh(mesh_view, *po.m_hollowing_data->interior, - sla::hfRemoveInsideTriangles); - - BOOST_LOG_TRIVIAL(info) << "Drilling skipped (no holes)."; - return; - } - - BOOST_LOG_TRIVIAL(info) << "Drilling drainage holes."; - sla::DrainHoles drainholes = po.transformed_drainhole_points(); - - auto tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set( - hollowed_mesh.its.vertices, - hollowed_mesh.its.indices - ); - - std::uniform_real_distribution dist(0., float(EPSILON)); - auto holes_mesh_cgal = MeshBoolean::cgal::triangle_mesh_to_cgal({}, {}); - indexed_triangle_set part_to_drill = hollowed_mesh.its; - - bool hole_fail = false; - for (size_t i = 0; i < drainholes.size(); ++i) { - sla::DrainHole holept = drainholes[i]; - - holept.normal += Vec3f{dist(m_rng), dist(m_rng), dist(m_rng)}; - holept.normal.normalize(); - holept.pos += Vec3f{dist(m_rng), dist(m_rng), dist(m_rng)}; - indexed_triangle_set m = holept.to_mesh(); - - part_to_drill.indices.clear(); - auto bb = bounding_box(m); - Eigen::AlignedBox ebb{bb.min.cast(), - bb.max.cast()}; - - AABBTreeIndirect::traverse( - tree, - AABBTreeIndirect::intersecting(ebb), - [&part_to_drill, &hollowed_mesh](const auto& node) - { - part_to_drill.indices.emplace_back(hollowed_mesh.its.indices[node.idx]); - // continue traversal - return true; - }); - - auto cgal_meshpart = MeshBoolean::cgal::triangle_mesh_to_cgal( - remove_unconnected_vertices(part_to_drill)); - - if (MeshBoolean::cgal::does_self_intersect(*cgal_meshpart)) { - BOOST_LOG_TRIVIAL(error) << "Failed to drill hole"; - - hole_fail = drainholes[i].failed = - po.model_object()->sla_drain_holes[i].failed = true; - - continue; +template +static std::vector slice_volumes( + const ModelVolumePtrs &volumes, + const std::vector &slice_grid, + const Transform3d &trafo, + const MeshSlicingParamsEx &slice_params, + Pred &&predicate) +{ + indexed_triangle_set mesh; + for (const ModelVolume *vol : volumes) { + if (predicate(vol)) { + indexed_triangle_set vol_mesh = vol->mesh().its; + its_transform(vol_mesh, trafo * vol->get_matrix()); + its_merge(mesh, vol_mesh); } - - auto cgal_hole = MeshBoolean::cgal::triangle_mesh_to_cgal(m); - MeshBoolean::cgal::plus(*holes_mesh_cgal, *cgal_hole); } - if (MeshBoolean::cgal::does_self_intersect(*holes_mesh_cgal)) - throw Slic3r::SlicingError(L("Too many overlapping holes.")); + std::vector out; - auto hollowed_mesh_cgal = MeshBoolean::cgal::triangle_mesh_to_cgal(hollowed_mesh); - - if (!MeshBoolean::cgal::does_bound_a_volume(*hollowed_mesh_cgal)) { - po.active_step_add_warning( - PrintStateBase::WarningLevel::NON_CRITICAL, - L("Mesh to be hollowed is not suitable for hollowing (does not " - "bound a volume).")); + if (!mesh.empty()) { + out = slice_mesh_ex(mesh, slice_grid, slice_params); } - if (!MeshBoolean::cgal::empty(*holes_mesh_cgal) - && !MeshBoolean::cgal::does_bound_a_volume(*holes_mesh_cgal)) { - po.active_step_add_warning( - PrintStateBase::WarningLevel::NON_CRITICAL, - L("Unable to drill the current configuration of holes into the " - "model.")); + return out; +} + +template BoundingBoxf3 csgmesh_positive_bb(const Cont &csg) +{ + // Calculate the biggest possible bounding box of the mesh to be sliced + // from all the positive parts that it contains. + BoundingBoxf3 bb3d; + + bool skip = false; + for (const auto &m : csg) { + auto op = csg::get_operation(m); + auto stackop = csg::get_stack_operation(m); + if (stackop == csg::CSGStackOp::Push && op != csg::CSGType::Union) + skip = true; + + if (!skip && csg::get_mesh(m) && op == csg::CSGType::Union) + bb3d.merge(bounding_box(*csg::get_mesh(m), csg::get_transform(m))); + + if (stackop == csg::CSGStackOp::Pop) + skip = false; } - try { - if (!MeshBoolean::cgal::empty(*holes_mesh_cgal)) - MeshBoolean::cgal::minus(*hollowed_mesh_cgal, *holes_mesh_cgal); - - hollowed_mesh = MeshBoolean::cgal::cgal_to_triangle_mesh(*hollowed_mesh_cgal); - mesh_view = hollowed_mesh; - - if (is_hollowed) { - auto &interior = *po.m_hollowing_data->interior; - std::vector exclude_mask = - create_exclude_mask(mesh_view.its, interior, drainholes); - - sla::remove_inside_triangles(mesh_view, interior, exclude_mask); - } - } catch (const Slic3r::RuntimeError &) { - throw Slic3r::SlicingError(L( - "Drilling holes into the mesh failed. " - "This is usually caused by broken model. Try to fix it first.")); - } - - if (hole_fail) - po.active_step_add_warning(PrintStateBase::WarningLevel::NON_CRITICAL, - L("Failed to drill some holes into the model")); + return bb3d; } // The slicing will be performed on an imaginary 1D grid which starts from @@ -488,14 +473,17 @@ void SLAPrint::Steps::drill_holes(SLAPrintObject &po) // same imaginary grid (the height vector argument to TriangleMeshSlicer). void SLAPrint::Steps::slice_model(SLAPrintObject &po) { - const TriangleMesh &mesh = po.get_mesh_to_slice(); + // The first mesh in the csg sequence is assumed to be a positive part + assert(po.m_mesh_to_slice.empty() || + csg::get_operation(*po.m_mesh_to_slice.begin()) == csg::CSGType::Union); + + auto bb3d = csgmesh_positive_bb(po.m_mesh_to_slice); // We need to prepare the slice index... double lhd = m_print->m_objects.front()->m_config.layer_height.getFloat(); float lh = float(lhd); coord_t lhs = scaled(lhd); - auto && bb3d = mesh.bounding_box(); double minZ = bb3d.min(Z) - po.get_elevation(); double maxZ = bb3d.max(Z); auto minZf = float(minZ); @@ -537,27 +525,8 @@ void SLAPrint::Steps::slice_model(SLAPrintObject &po) } auto thr = [this]() { m_print->throw_if_canceled(); }; auto &slice_grid = po.m_model_height_levels; - po.m_model_slices = slice_mesh_ex(mesh.its, slice_grid, params, thr); - sla::Interior *interior = po.m_hollowing_data ? - po.m_hollowing_data->interior.get() : - nullptr; - - if (interior && ! sla::get_mesh(*interior).empty()) { - indexed_triangle_set interiormesh = sla::get_mesh(*interior); - sla::swap_normals(interiormesh); - params.mode = MeshSlicingParams::SlicingMode::Regular; - - std::vector interior_slices = slice_mesh_ex(interiormesh, slice_grid, params, thr); - - execution::for_each( - ex_tbb, size_t(0), interior_slices.size(), - [&po, &interior_slices](size_t i) { - const ExPolygons &slice = interior_slices[i]; - po.m_model_slices[i] = diff_ex(po.m_model_slices[i], slice); - }, - execution::max_concurrency(ex_tbb)); - } + po.m_model_slices = slice_csgmesh_ex(po.mesh_to_slice(), slice_grid, params, thr); auto mit = slindex_it; for (size_t id = 0; @@ -569,10 +538,67 @@ void SLAPrint::Steps::slice_model(SLAPrintObject &po) // We apply the printer correction offset here. apply_printer_corrections(po, soModel); - if(po.m_config.supports_enable.getBool() || po.m_config.pad_enable.getBool()) - { - po.m_supportdata.reset(new SLAPrintObject::SupportData(po.get_mesh_to_print())); +// po.m_preview_meshes[slaposObjectSlice] = po.get_mesh_to_print(); +// report_status(-2, "", SlicingStatus::RELOAD_SLA_PREVIEW); +} + + +struct SuppPtMask { + const std::vector &blockers; + const std::vector &enforcers; + bool enforcers_only = false; +}; + +static void filter_support_points_by_modifiers(sla::SupportPoints &pts, + const SuppPtMask &mask, + const std::vector &slice_grid) +{ + assert((mask.blockers.empty() || mask.blockers.size() == slice_grid.size()) && + (mask.enforcers.empty() || mask.enforcers.size() == slice_grid.size())); + + auto new_pts = reserve_vector(pts.size()); + + for (size_t i = 0; i < pts.size(); ++i) { + const sla::SupportPoint &sp = pts[i]; + Point sp2d = scaled(to_2d(sp.pos)); + + auto it = std::lower_bound(slice_grid.begin(), slice_grid.end(), sp.pos.z()); + if (it != slice_grid.end()) { + size_t idx = std::distance(slice_grid.begin(), it); + bool is_enforced = false; + if (idx < mask.enforcers.size()) { + for (size_t enf_idx = 0; + !is_enforced && enf_idx < mask.enforcers[idx].size(); + ++enf_idx) + { + if (mask.enforcers[idx][enf_idx].contains(sp2d)) + is_enforced = true; + } + } + + bool is_blocked = false; + if (!is_enforced) { + if (!mask.enforcers_only) { + if (idx < mask.blockers.size()) { + for (size_t blk_idx = 0; + !is_blocked && blk_idx < mask.blockers[idx].size(); + ++blk_idx) + { + if (mask.blockers[idx][blk_idx].contains(sp2d)) + is_blocked = true; + } + } + } else { + is_blocked = true; + } + } + + if (!is_blocked) + new_pts.emplace_back(sp); + } } + + pts.swap(new_pts); } // In this step we check the slices, identify island and cover them with @@ -582,8 +608,15 @@ void SLAPrint::Steps::support_points(SLAPrintObject &po) // If supports are disabled, we can skip the model scan. if(!po.m_config.supports_enable.getBool()) return; - if (!po.m_supportdata) - po.m_supportdata.reset(new SLAPrintObject::SupportData(po.get_mesh_to_print())); + if (!po.m_supportdata) { + auto &meshp = po.get_mesh_to_print(); + assert(meshp); + po.m_supportdata = + std::make_unique(*meshp); + } + + po.m_supportdata->input.zoffset = csgmesh_positive_bb(po.m_mesh_to_slice) + .min.z(); const ModelObject& mo = *po.m_model_object; @@ -598,11 +631,6 @@ void SLAPrint::Steps::support_points(SLAPrintObject &po) // calculate heights of slices (slices are calculated already) const std::vector& heights = po.m_model_height_levels; - // Tell the mesh where drain holes are. Although the points are - // calculated on slices, the algorithm then raycasts the points - // so they actually lie on the mesh. -// po.m_supportdata->emesh.load_holes(po.transformed_drainhole_points()); - throw_if_canceled(); sla::SupportPointGenerator::Config config; const SLAPrintObjectConfig& cfg = po.config(); @@ -630,8 +658,28 @@ void SLAPrint::Steps::support_points(SLAPrintObject &po) heights, config, [this]() { throw_if_canceled(); }, statuscb); // Now let's extract the result. - const std::vector& points = auto_supports.output(); + std::vector& points = auto_supports.output(); throw_if_canceled(); + + MeshSlicingParamsEx params; + params.closing_radius = float(po.config().slice_closing_radius.value); + std::vector blockers = + slice_volumes(po.model_object()->volumes, + po.m_model_height_levels, po.trafo(), params, + [](const ModelVolume *vol) { + return vol->is_support_blocker(); + }); + + std::vector enforcers = + slice_volumes(po.model_object()->volumes, + po.m_model_height_levels, po.trafo(), params, + [](const ModelVolume *vol) { + return vol->is_support_enforcer(); + }); + + SuppPtMask mask{blockers, enforcers, po.config().support_enforcers_only.getBool()}; + filter_support_points_by_modifiers(points, mask, po.m_model_height_levels); + po.m_supportdata->input.pts = points; BOOST_LOG_TRIVIAL(debug) @@ -653,23 +701,17 @@ void SLAPrint::Steps::support_tree(SLAPrintObject &po) { if(!po.m_supportdata) return; -// sla::PadConfig pcfg = make_pad_cfg(po.m_config); - -// if (pcfg.embed_object) -// po.m_supportdata->emesh.ground_level_offset(pcfg.wall_thickness_mm); - // If the zero elevation mode is engaged, we have to filter out all the // points that are on the bottom of the object if (is_zero_elevation(po.config())) { remove_bottom_points(po.m_supportdata->input.pts, float( - po.m_supportdata->input.emesh.ground_level() + + po.m_supportdata->input.zoffset + EPSILON)); } po.m_supportdata->input.cfg = make_support_cfg(po.m_config); po.m_supportdata->input.pad_cfg = make_pad_cfg(po.m_config); -// po.m_supportdata->emesh.load_holes(po.transformed_drainhole_points()); // scaling for the sub operations double d = objectstep_scale * OBJ_STEP_LEVELS[slaposSupportTree] / 100.0; @@ -713,6 +755,13 @@ void SLAPrint::Steps::generate_pad(SLAPrintObject &po) { // repeated) if(po.m_config.pad_enable.getBool()) { + if (!po.m_supportdata) { + auto &meshp = po.get_mesh_to_print(); + assert(meshp); + po.m_supportdata = + std::make_unique(*meshp); + } + // Get the distilled pad configuration from the config // (Again, despite it was retrieved in the previous step. Note that // on a param change event, the previous step might not be executed @@ -720,16 +769,6 @@ void SLAPrint::Steps::generate_pad(SLAPrintObject &po) { sla::PadConfig pcfg = make_pad_cfg(po.m_config); po.m_supportdata->input.pad_cfg = pcfg; -// if (!po.m_config.supports_enable.getBool() || pcfg.embed_object) { -// // No support (thus no elevation) or zero elevation mode -// // we sometimes call it "builtin pad" is enabled so we will -// // get a sample from the bottom of the mesh and use it for pad -// // creation. -// sla::pad_blueprint(trmesh.its, bp, float(pad_h), -// float(po.m_config.layer_height.getFloat()), -// [this](){ throw_if_canceled(); }); -// } - sla::JobController ctl; ctl.stopcondition = [this]() { return canceled(); }; ctl.cancelfn = [this]() { throw_if_canceled(); }; @@ -1162,6 +1201,7 @@ double SLAPrint::Steps::progressrange(SLAPrintStep step) const void SLAPrint::Steps::execute(SLAPrintObjectStep step, SLAPrintObject &obj) { switch(step) { + case slaposAssembly: mesh_assembly(obj); break; case slaposHollowing: hollow_model(obj); break; case slaposDrillHoles: drill_holes(obj); break; case slaposObjectSlice: slice_model(obj); break; diff --git a/src/libslic3r/SLAPrintSteps.hpp b/src/libslic3r/SLAPrintSteps.hpp index 19b64d4a9..32d10a424 100644 --- a/src/libslic3r/SLAPrintSteps.hpp +++ b/src/libslic3r/SLAPrintSteps.hpp @@ -14,40 +14,43 @@ class SLAPrint::Steps { private: SLAPrint *m_print = nullptr; - std::mt19937 m_rng; - -public: + +public: // where the per object operations start and end - static const constexpr unsigned min_objstatus = 0; - static const constexpr unsigned max_objstatus = 50; - + static const constexpr unsigned min_objstatus = 0; + static const constexpr unsigned max_objstatus = 70; + private: const size_t objcount; - + // shortcut to initial layer height const double ilhd; const float ilh; const coord_t ilhs; - + // the coefficient that multiplies the per object status values which // are set up for <0, 100>. They need to be scaled into the whole process const double objectstep_scale; - + template void report_status(Args&&...args) { m_print->m_report_status(*m_print, std::forward(args)...); } - + double current_status() const { return m_print->m_report_status.status(); } void throw_if_canceled() const { m_print->throw_if_canceled(); } bool canceled() const { return m_print->canceled(); } void initialize_printer_input(); - + void apply_printer_corrections(SLAPrintObject &po, SliceOrigin o); - + + void generate_preview(SLAPrintObject &po, SLAPrintObjectStep step); + indexed_triangle_set generate_preview_vdb(SLAPrintObject &po, SLAPrintObjectStep step); + public: explicit Steps(SLAPrint *print); - + + void mesh_assembly(SLAPrintObject &po); void hollow_model(SLAPrintObject &po); void drill_holes (SLAPrintObject &po); void slice_model(SLAPrintObject& po); @@ -55,20 +58,20 @@ public: void support_tree(SLAPrintObject& po); void generate_pad(SLAPrintObject& po); void slice_supports(SLAPrintObject& po); - + void merge_slices_and_eval_stats(); void rasterize(); - + void execute(SLAPrintObjectStep step, SLAPrintObject &obj); void execute(SLAPrintStep step); - + static std::string label(SLAPrintObjectStep step); static std::string label(SLAPrintStep step); - + double progressrange(SLAPrintObjectStep step) const; double progressrange(SLAPrintStep step) const; }; -} +} // namespace Slic3r #endif // SLAPRINTSTEPS_HPP diff --git a/src/libslic3r/SlicesToTriangleMesh.cpp b/src/libslic3r/SlicesToTriangleMesh.cpp index 969fa8dac..9e290d472 100644 --- a/src/libslic3r/SlicesToTriangleMesh.cpp +++ b/src/libslic3r/SlicesToTriangleMesh.cpp @@ -9,6 +9,8 @@ #include #include +#include + namespace Slic3r { // Same as walls() but with identical higher and lower polygons. @@ -52,7 +54,8 @@ indexed_triangle_set slices_to_mesh( Layers layers(slices.size()); size_t len = slices.size() - 1; - tbb::parallel_for(size_t(0), len, [&slices, &layers, &grid](size_t i) { + auto threads_cnt = execution::max_concurrency(ex_tbb); + execution::for_each(ex_tbb, size_t(0), len, [&slices, &layers, &grid](size_t i) { const ExPolygons &upper = slices[i + 1]; const ExPolygons &lower = slices[i]; @@ -64,14 +67,15 @@ indexed_triangle_set slices_to_mesh( its_merge(layers[i], triangulate_expolygons_3d(free_top, grid[i], NORMALS_UP)); its_merge(layers[i], triangulate_expolygons_3d(overhang, grid[i], NORMALS_DOWN)); its_merge(layers[i], straight_walls(upper, grid[i], grid[i + 1])); - }); + }, threads_cnt); auto merge_fn = []( const indexed_triangle_set &a, const indexed_triangle_set &b ) { indexed_triangle_set res{a}; its_merge(res, b); return res; }; auto ret = execution::reduce(ex_tbb, layers.begin(), layers.end(), - indexed_triangle_set{}, merge_fn); + indexed_triangle_set{}, merge_fn, + threads_cnt); its_merge(ret, triangulate_expolygons_3d(slices.front(), zmin, NORMALS_DOWN)); its_merge(ret, straight_walls(slices.front(), zmin, grid.front())); @@ -80,9 +84,14 @@ indexed_triangle_set slices_to_mesh( // FIXME: these repairs do not fix the mesh entirely. There will be cracks // in the output. It is very hard to do the meshing in a way that does not // leave errors. - its_merge_vertices(ret); - its_remove_degenerate_faces(ret); - its_compactify_vertices(ret); + int num_mergedv = its_merge_vertices(ret); + BOOST_LOG_TRIVIAL(debug) << "Merged vertices count: " << num_mergedv; + + int remcnt = its_remove_degenerate_faces(ret); + BOOST_LOG_TRIVIAL(debug) << "Removed degenerate faces count: " << remcnt; + + int num_erasedv = its_compactify_vertices(ret); + BOOST_LOG_TRIVIAL(debug) << "Erased vertices count: " << num_erasedv; return ret; } diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index eaf7dd57d..3820aa4d5 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -1,5 +1,6 @@ #include "SupportSpotsGenerator.hpp" +#include "BoundingBox.hpp" #include "ExPolygon.hpp" #include "ExtrusionEntity.hpp" #include "ExtrusionEntityCollection.hpp" @@ -7,8 +8,10 @@ #include "Line.hpp" #include "Point.hpp" #include "Polygon.hpp" +#include "PrincipalComponents2D.hpp" #include "Print.hpp" #include "PrintBase.hpp" +#include "PrintConfig.hpp" #include "Tesselate.hpp" #include "libslic3r.h" #include "tbb/parallel_for.h" @@ -21,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -66,7 +70,7 @@ public: float len; const ExtrusionEntity *origin_entity; - bool support_point_generated = false; + std::optional support_point_generated = {}; float form_quality = 1.0f; float curled_up_height = 0.0f; @@ -79,10 +83,6 @@ auto get_b(ExtrusionLine &&l) { return l.b; } namespace SupportSpotsGenerator { -SupportPoint::SupportPoint(const Vec3f &position, float force, float spot_radius, const Vec2f &direction) - : position(position), force(force), spot_radius(spot_radius), direction(direction) -{} - using LD = AABBTreeLines::LinesDistancer; struct SupportGridFilter @@ -117,13 +117,14 @@ public: size_t to_cell_index(const Vec3i &cell_coords) const { +#ifdef DETAILED_DEBUG_LOGS assert(cell_coords.x() >= 0); assert(cell_coords.x() < cell_count.x()); assert(cell_coords.y() >= 0); assert(cell_coords.y() < cell_count.y()); assert(cell_coords.z() >= 0); assert(cell_coords.z() < cell_count.z()); - +#endif return cell_coords.z() * cell_count.x() * cell_count.y() + cell_coords.y() * cell_count.x() + cell_coords.x(); } @@ -237,13 +238,67 @@ std::vector check_extrusion_entity_stability(const ExtrusionEntit checked_lines_out.insert(checked_lines_out.end(), tmp.begin(), tmp.end()); } return checked_lines_out; + } else if (entity->role().is_bridge() && !entity->role().is_perimeter()) { + // pure bridges are handled separately, beacuse we need to align the forward and backward direction support points + if (entity->length() < scale_(params.min_distance_to_allow_local_supports)) { + return {}; + } + const float flow_width = get_flow_width(layer_region, entity->role()); + std::vector annotated_points = estimate_points_properties(entity->as_polyline().points, + prev_layer_boundary, flow_width, + params.bridge_distance); + + std::vector lines_out; + lines_out.reserve(annotated_points.size()); + float bridged_distance = 0.0f; + + std::optional bridging_dir{}; + + for (size_t i = 0; i < annotated_points.size(); ++i) { + ExtendedPoint &curr_point = annotated_points[i]; + ExtendedPoint &prev_point = i > 0 ? annotated_points[i - 1] : annotated_points[i - 1]; + + SupportPointCause potential_cause = std::abs(curr_point.curvature) > 0.1 ? SupportPointCause::FloatingBridgeAnchor : + SupportPointCause::LongBridge; + float line_len = i > 0 ? ((annotated_points[i - 1].position - curr_point.position).norm()) : 0.0f; + Vec2d line_dir = (curr_point.position - prev_point.position).normalized(); + + ExtrusionLine line_out{i > 0 ? annotated_points[i - 1].position.cast() : curr_point.position.cast(), + curr_point.position.cast(), line_len, entity}; + + float max_bridge_len = std::max(params.support_points_interface_radius * 2.0f, + params.bridge_distance / + ((1.0f + std::abs(curr_point.curvature)) * (1.0f + std::abs(curr_point.curvature)) * + (1.0f + std::abs(curr_point.curvature)))); + + if (!bridging_dir.has_value() && curr_point.distance > flow_width && line_len > params.bridge_distance * 0.6) { + bridging_dir = (prev_point.position - curr_point.position).normalized(); + } + + if (curr_point.distance > flow_width && potential_cause == SupportPointCause::LongBridge && bridging_dir.has_value() && + bridging_dir->dot(line_dir) < 0.8) { // skip backward direction of bridge - supported by forward points enough + bridged_distance += line_len; + } else if (curr_point.distance > flow_width) { + bridged_distance += line_len; + if (bridged_distance > max_bridge_len) { + bridged_distance = 0.0f; + line_out.support_point_generated = potential_cause; + } + } else { + bridged_distance = 0.0f; + } + + lines_out.push_back(line_out); + } + return lines_out; + } else { // single extrusion path, with possible varying parameters if (entity->length() < scale_(params.min_distance_to_allow_local_supports)) { return {}; } const float flow_width = get_flow_width(layer_region, entity->role()); - + // Compute only unsigned distance - prev_layer_lines can contain unconnected paths, thus the sign of the distance is unreliable std::vector annotated_points = estimate_points_properties(entity->as_polyline().points, prev_layer_lines, flow_width, params.bridge_distance); @@ -259,27 +314,51 @@ std::vector check_extrusion_entity_stability(const ExtrusionEntit curr_point.position.cast(), line_len, entity}; const ExtrusionLine nearest_prev_layer_line = prev_layer_lines.get_lines().size() > 0 ? - prev_layer_lines.get_line(curr_point.nearest_prev_layer_line) : - ExtrusionLine{}; + prev_layer_lines.get_line(curr_point.nearest_prev_layer_line) : + ExtrusionLine{}; + // correctify the distance sign using slice polygons float sign = (prev_layer_boundary.distance_from_lines(curr_point.position) + 0.5f * flow_width) < 0.0f ? -1.0f : 1.0f; curr_point.distance *= sign; - float max_bridge_len = params.bridge_distance / - ((1.0f + std::abs(curr_point.curvature)) * (1.0f + std::abs(curr_point.curvature))); + SupportPointCause potential_cause = SupportPointCause::FloatingExtrusion; + if (bridged_distance + line_len > params.bridge_distance * 0.8 && std::abs(curr_point.curvature) < 0.1) { + potential_cause = SupportPointCause::FloatingExtrusion; + } + + float max_bridge_len = std::max(params.support_points_interface_radius * 2.0f, + params.bridge_distance / + ((1.0f + std::abs(curr_point.curvature)) * (1.0f + std::abs(curr_point.curvature)) * + (1.0f + std::abs(curr_point.curvature)))); if (curr_point.distance > 2.0f * flow_width) { line_out.form_quality = 0.8f; bridged_distance += line_len; if (bridged_distance > max_bridge_len) { - line_out.support_point_generated = true; + std::cout << "Problem found A: " << std::endl; + std::cout << "bridged_distance: " << bridged_distance << std::endl; + std::cout << "max_bridge_len: " << max_bridge_len << std::endl; + std::cout << "line_out.form_quality: " << line_out.form_quality << std::endl; + std::cout << "curr_point.distance: " << curr_point.distance << std::endl; + std::cout << "curr_point.curvature: " << curr_point.curvature << std::endl; + std::cout << "flow_width: " << flow_width << std::endl; + + line_out.support_point_generated = potential_cause; bridged_distance = 0.0f; } } else if (curr_point.distance > flow_width * (1.0 + std::clamp(curr_point.curvature, -0.30f, 0.20f))) { bridged_distance += line_len; line_out.form_quality = nearest_prev_layer_line.form_quality - 0.3f; if (line_out.form_quality < 0 && bridged_distance > max_bridge_len) { - line_out.support_point_generated = true; + std::cout << "Problem found B: " << std::endl; + std::cout << "bridged_distance: " << bridged_distance << std::endl; + std::cout << "max_bridge_len: " << max_bridge_len << std::endl; + std::cout << "line_out.form_quality: " << line_out.form_quality << std::endl; + std::cout << "curr_point.distance: " << curr_point.distance << std::endl; + std::cout << "curr_point.curvature: " << curr_point.curvature << std::endl; + std::cout << "flow_width: " << flow_width << std::endl; + + line_out.support_point_generated = potential_cause; line_out.form_quality = 0.5f; bridged_distance = 0.0f; } @@ -297,84 +376,39 @@ std::vector check_extrusion_entity_stability(const ExtrusionEntit } } -// returns triangle area, first_moment_of_area_xy, second_moment_of_area_xy, second_moment_of_area_covariance -// none of the values is divided/normalized by area. -// The function computes intgeral over the area of the triangle, with function f(x,y) = x for first moments of area (y is analogous) -// f(x,y) = x^2 for second moment of area -// and f(x,y) = x*y for second moment of area covariance -std::tuple compute_triangle_moments_of_area(const Vec2f &a, const Vec2f &b, const Vec2f &c) -{ - // based on the following guide: - // Denote the vertices of S by a, b, c. Then the map - // g:(u,v)↦a+u(b−a)+v(c−a) , - // which in coordinates appears as - // g:(u,v)↦{x(u,v)y(u,v)=a1+u(b1−a1)+v(c1−a1)=a2+u(b2−a2)+v(c2−a2) ,(1) - // obviously maps S′ bijectively onto S. Therefore the transformation formula for multiple integrals steps into action, and we obtain - // ∫Sf(x,y)d(x,y)=∫S′f(x(u,v),y(u,v))∣∣Jg(u,v)∣∣ d(u,v) . - // In the case at hand the Jacobian determinant is a constant: From (1) we obtain - // Jg(u,v)=det[xuyuxvyv]=(b1−a1)(c2−a2)−(c1−a1)(b2−a2) . - // Therefore we can write - // ∫Sf(x,y)d(x,y)=∣∣Jg∣∣∫10∫1−u0f~(u,v) dv du , - // where f~ denotes the pullback of f to S′: - // f~(u,v):=f(x(u,v),y(u,v)) . - // Don't forget taking the absolute value of Jg! - - float jacobian_determinant_abs = std::abs((b.x() - a.x()) * (c.y() - a.y()) - (c.x() - a.x()) * (b.y() - a.y())); - - // coordinate transform: gx(u,v) = a.x + u * (b.x - a.x) + v * (c.x - a.x) - // coordinate transform: gy(u,v) = a.y + u * (b.y - a.y) + v * (c.y - a.y) - // second moment of area for x: f(x, y) = x^2; - // f(gx(u,v), gy(u,v)) = gx(u,v)^2 = ... (long expanded form) - - // result is Int_T func = jacobian_determinant_abs * Int_0^1 Int_0^1-u func(gx(u,v), gy(u,v)) dv du - // integral_0^1 integral_0^(1 - u) (a + u (b - a) + v (c - a))^2 dv du = 1/12 (a^2 + a (b + c) + b^2 + b c + c^2) - - Vec2f second_moment_of_area_xy = jacobian_determinant_abs * - (a.cwiseProduct(a) + b.cwiseProduct(b) + b.cwiseProduct(c) + c.cwiseProduct(c) + - a.cwiseProduct(b + c)) / - 12.0f; - // second moment of area covariance : f(x, y) = x*y; - // f(gx(u,v), gy(u,v)) = gx(u,v)*gy(u,v) = ... (long expanded form) - //(a_1 + u * (b_1 - a_1) + v * (c_1 - a_1)) * (a_2 + u * (b_2 - a_2) + v * (c_2 - a_2)) - // == (a_1 + u (b_1 - a_1) + v (c_1 - a_1)) (a_2 + u (b_2 - a_2) + v (c_2 - a_2)) - - // intermediate result: integral_0^(1 - u) (a_1 + u (b_1 - a_1) + v (c_1 - a_1)) (a_2 + u (b_2 - a_2) + v (c_2 - a_2)) dv = - // 1/6 (u - 1) (-c_1 (u - 1) (a_2 (u - 1) - 3 b_2 u) - c_2 (u - 1) (a_1 (u - 1) - 3 b_1 u + 2 c_1 (u - 1)) + 3 b_1 u (a_2 (u - 1) - 2 - // b_2 u) + a_1 (u - 1) (3 b_2 u - 2 a_2 (u - 1))) result = integral_0^1 1/6 (u - 1) (-c_1 (u - 1) (a_2 (u - 1) - 3 b_2 u) - c_2 (u - - // 1) (a_1 (u - 1) - 3 b_1 u + 2 c_1 (u - 1)) + 3 b_1 u (a_2 (u - 1) - 2 b_2 u) + a_1 (u - 1) (3 b_2 u - 2 a_2 (u - 1))) du = - // 1/24 (a_2 (b_1 + c_1) + a_1 (2 a_2 + b_2 + c_2) + b_2 c_1 + b_1 c_2 + 2 b_1 b_2 + 2 c_1 c_2) - // result is Int_T func = jacobian_determinant_abs * Int_0^1 Int_0^1-u func(gx(u,v), gy(u,v)) dv du - float second_moment_of_area_covariance = jacobian_determinant_abs * (1.0f / 24.0f) * - (a.y() * (b.x() + c.x()) + a.x() * (2.0f * a.y() + b.y() + c.y()) + b.y() * c.x() + - b.x() * c.y() + 2.0f * b.x() * b.y() + 2.0f * c.x() * c.y()); - - float area = jacobian_determinant_abs * 0.5f; - - Vec2f first_moment_of_area_xy = jacobian_determinant_abs * (a + b + c) / 6.0f; - - return {area, first_moment_of_area_xy, second_moment_of_area_xy, second_moment_of_area_covariance}; -}; - SliceConnection estimate_slice_connection(size_t slice_idx, const Layer *layer) { SliceConnection connection; const LayerSlice &slice = layer->lslices_ex[slice_idx]; - ExPolygon slice_poly = layer->lslices[slice_idx]; + Polygons slice_polys = to_polygons(layer->lslices[slice_idx]); + BoundingBox slice_bb = get_extents(slice_polys); const Layer *lower_layer = layer->lower_layer; - ExPolygons below_polys{}; - for (const auto &link : slice.overlaps_below) { below_polys.push_back(lower_layer->lslices[link.slice_idx]); } - ExPolygons overlap = intersection_ex({slice_poly}, below_polys); + ExPolygons below{}; + for (const auto &link : slice.overlaps_below) { below.push_back(lower_layer->lslices[link.slice_idx]); } + Polygons below_polys = to_polygons(below); - std::vector triangles = triangulate_expolygons_2f(overlap); - for (size_t idx = 0; idx < triangles.size(); idx += 3) { - auto [area, first_moment_of_area, second_moment_area, - second_moment_of_area_covariance] = compute_triangle_moments_of_area(triangles[idx], triangles[idx + 1], triangles[idx + 2]); - connection.area += area; - connection.centroid_accumulator += Vec3f(first_moment_of_area.x(), first_moment_of_area.y(), layer->print_z * area); - connection.second_moment_of_area_accumulator += second_moment_area; - connection.second_moment_of_area_covariance_accumulator += second_moment_of_area_covariance; + BoundingBox below_bb = get_extents(below_polys); + + Polygons overlap = intersection(ClipperUtils::clip_clipper_polygons_with_subject_bbox(slice_polys, below_bb), + ClipperUtils::clip_clipper_polygons_with_subject_bbox(below_polys, slice_bb)); + + for (const Polygon &poly : overlap) { + Vec2f p0 = unscaled(poly.first_point()).cast(); + for (size_t i = 2; i < poly.points.size(); i++) { + Vec2f p1 = unscaled(poly.points[i - 1]).cast(); + Vec2f p2 = unscaled(poly.points[i]).cast(); + + float sign = cross2(p1 - p0, p2 - p1) > 0 ? 1.0f : -1.0f; + + auto [area, first_moment_of_area, second_moment_area, + second_moment_of_area_covariance] = compute_moments_of_area_of_triangle(p0, p1, p2); + connection.area += sign * area; + connection.centroid_accumulator += sign * Vec3f(first_moment_of_area.x(), first_moment_of_area.y(), layer->print_z * area); + connection.second_moment_of_area_accumulator += sign * second_moment_area; + connection.second_moment_of_area_covariance_accumulator += sign * second_moment_of_area_covariance; + } } return connection; @@ -389,11 +423,13 @@ public: Vec3f sticking_centroid_accumulator = Vec3f::Zero(); Vec2f sticking_second_moment_of_area_accumulator = Vec2f::Zero(); float sticking_second_moment_of_area_covariance_accumulator{}; + bool connected_to_bed = false; ObjectPart() = default; void add(const ObjectPart &other) { + this->connected_to_bed = this->connected_to_bed || other.connected_to_bed; this->volume_centroid_accumulator += other.volume_centroid_accumulator; this->volume += other.volume; this->sticking_area += other.sticking_area; @@ -456,7 +492,7 @@ public: return elastic_section_modulus; } - float is_stable_while_extruding(const SliceConnection &connection, + std::tuple is_stable_while_extruding(const SliceConnection &connection, const ExtrusionLine &extruded_line, const Vec3f &extreme_point, float layer_z, @@ -474,7 +510,7 @@ public: // section for bed calculations { - if (this->sticking_area < EPSILON) return 1.0f; + if (this->sticking_area < EPSILON) return {1.0f, SupportPointCause::UnstableFloatingPart}; Vec3f bed_centroid = this->sticking_centroid_accumulator / this->sticking_area; float bed_yield_torque = -compute_elastic_section_modulus(line_dir, extreme_point, this->sticking_centroid_accumulator, @@ -503,7 +539,7 @@ public: #ifdef DETAILED_DEBUG_LOGS BOOST_LOG_TRIVIAL(debug) << "bed_centroid: " << bed_centroid.x() << " " << bed_centroid.y() << " " << bed_centroid.z(); BOOST_LOG_TRIVIAL(debug) << "SSG: bed_yield_torque: " << bed_yield_torque; - BOOST_LOG_TRIVIAL(debug) << "SSG: bed_weight_arm: " << bed_weight_arm; + BOOST_LOG_TRIVIAL(debug) << "SSG: bed_weight_arm: " << bed_weight_arm_len; BOOST_LOG_TRIVIAL(debug) << "SSG: bed_weight_torque: " << bed_weight_torque; BOOST_LOG_TRIVIAL(debug) << "SSG: bed_movement_arm: " << bed_movement_arm; BOOST_LOG_TRIVIAL(debug) << "SSG: bed_movement_torque: " << bed_movement_torque; @@ -515,16 +551,19 @@ public: BOOST_LOG_TRIVIAL(debug) << "SSG: total_torque: " << bed_total_torque << " layer_z: " << layer_z; #endif - if (bed_total_torque > 0) return bed_total_torque / bed_conflict_torque_arm; + if (bed_total_torque > 0) { + return {bed_total_torque / bed_conflict_torque_arm, + (this->connected_to_bed ? SupportPointCause::SeparationFromBed : SupportPointCause::UnstableFloatingPart)}; + } } // section for weak connection calculations { - if (connection.area < EPSILON) return 1.0f; + if (connection.area < EPSILON) return {1.0f, SupportPointCause::UnstableFloatingPart}; Vec3f conn_centroid = connection.centroid_accumulator / connection.area; - if (layer_z - conn_centroid.z() < 3.0f) { return -1.0f; } + if (layer_z - conn_centroid.z() < 3.0f) { return {-1.0f, SupportPointCause::WeakObjectPart}; } float conn_yield_torque = compute_elastic_section_modulus(line_dir, extreme_point, connection.centroid_accumulator, connection.second_moment_of_area_accumulator, connection.second_moment_of_area_covariance_accumulator, @@ -532,7 +571,7 @@ public: params.material_yield_strength; float conn_weight_arm = (conn_centroid.head<2>() - mass_centroid.head<2>()).norm(); - float conn_weight_torque = conn_weight_arm * weight * (conn_centroid.z() / layer_z); + float conn_weight_torque = conn_weight_arm * weight * (1.0f - conn_centroid.z() / layer_z) * (1.0f - conn_centroid.z() / layer_z); float conn_movement_arm = std::max(0.0f, mass_centroid.z() - conn_centroid.z()); float conn_movement_torque = movement_force * conn_movement_arm; @@ -554,18 +593,20 @@ public: BOOST_LOG_TRIVIAL(debug) << "SSG: total_torque: " << conn_total_torque << " layer_z: " << layer_z; #endif - return conn_total_torque / conn_conflict_torque_arm; + return {conn_total_torque / conn_conflict_torque_arm, SupportPointCause::WeakObjectPart}; } } }; // return new object part and actual area covered by extrusions -std::tuple build_object_part_from_slice(const LayerSlice &slice, const Layer *layer) +std::tuple build_object_part_from_slice(const size_t &slice_idx, const Layer *layer, const Params& params) { ObjectPart new_object_part; float area_covered_by_extrusions = 0; + const LayerSlice& slice = layer->lslices_ex.at(slice_idx); - auto add_extrusions_to_object = [&new_object_part, &area_covered_by_extrusions](const ExtrusionEntity *e, const LayerRegion *region) { + auto add_extrusions_to_object = [&new_object_part, &area_covered_by_extrusions, ¶ms](const ExtrusionEntity *e, + const LayerRegion *region) { float flow_width = get_flow_width(region, e->role()); const Layer *l = region->layer(); float slice_z = l->slice_z; @@ -577,8 +618,9 @@ std::tuple build_object_part_from_slice(const LayerSlice &sli new_object_part.volume += volume; new_object_part.volume_centroid_accumulator += to_3d(Vec2f((line.a + line.b) / 2.0f), slice_z) * volume; - if (l->bottom_z() < EPSILON) { // layer attached on bed - float sticking_area = line.len * flow_width; + if (l->id() == params.raft_layers_count) { // layer attached on bed/raft + new_object_part.connected_to_bed = true; + float sticking_area = line.len * flow_width; new_object_part.sticking_area += sticking_area; Vec2f middle = Vec2f((line.a + line.b) / 2.0f); new_object_part.sticking_centroid_accumulator += sticking_area * to_3d(middle, slice_z); @@ -619,6 +661,49 @@ std::tuple build_object_part_from_slice(const LayerSlice &sli } } + // BRIM HANDLING + if (layer->id() == params.raft_layers_count && params.raft_layers_count == 0 && params.brim_type != BrimType::btNoBrim) { + // TODO: The algorithm here should take into account that multiple slices may have coliding Brim areas and the final brim area is + // smaller, + // thus has lower adhesion. For now this effect will be neglected. + ExPolygon slice_poly = layer->lslices[slice_idx]; + ExPolygons brim; + if (params.brim_type == BrimType::btOuterAndInner || params.brim_type == BrimType::btOuterOnly) { + Polygon brim_hole = slice_poly.contour; + brim_hole.reverse(); + brim.push_back(ExPolygon{expand(slice_poly.contour, scale_(params.brim_width)).front(), brim_hole}); + } + if (params.brim_type == BrimType::btOuterAndInner || params.brim_type == BrimType::btInnerOnly) { + Polygons brim_contours = slice_poly.holes; + polygons_reverse(brim_contours); + for (const Polygon &brim_contour : brim_contours) { + Polygons brim_holes = shrink({brim_contour}, scale_(params.brim_width)); + polygons_reverse(brim_holes); + ExPolygon inner_brim{brim_contour}; + inner_brim.holes = brim_holes; + brim.push_back(inner_brim); + } + } + + for (const Polygon &poly : to_polygons(brim)) { + Vec2f p0 = unscaled(poly.first_point()).cast(); + for (size_t i = 2; i < poly.points.size(); i++) { + Vec2f p1 = unscaled(poly.points[i - 1]).cast(); + Vec2f p2 = unscaled(poly.points[i]).cast(); + + float sign = cross2(p1 - p0, p2 - p1) > 0 ? 1.0f : -1.0f; + + auto [area, first_moment_of_area, second_moment_area, + second_moment_of_area_covariance] = compute_moments_of_area_of_triangle(p0, p1, p2); + new_object_part.sticking_area += sign * area; + new_object_part.sticking_centroid_accumulator += sign * Vec3f(first_moment_of_area.x(), first_moment_of_area.y(), + layer->print_z * area); + new_object_part.sticking_second_moment_of_area_accumulator += sign * second_moment_area; + new_object_part.sticking_second_moment_of_area_covariance_accumulator += sign * second_moment_of_area_covariance; + } + } + } + return {new_object_part, area_covered_by_extrusions}; } @@ -661,11 +746,12 @@ public: } }; -SupportPoints check_stability(const PrintObject *po, const PrintTryCancel& cancel_func, const Params ¶ms) +std::tuple check_stability(const PrintObject *po, const PrintTryCancel &cancel_func, const Params ¶ms) { - SupportPoints supp_points{}; + SupportPoints supp_points{}; SupportGridFilter supports_presence_grid(po, params.min_distance_between_support_points); ActiveObjectParts active_object_parts{}; + PartialObjects partial_objects{}; LD prev_layer_ext_perim_lines; std::unordered_map prev_slice_idx_to_object_part_mapping; @@ -673,6 +759,14 @@ SupportPoints check_stability(const PrintObject *po, const PrintTryCancel& cance std::unordered_map prev_slice_idx_to_weakest_connection; std::unordered_map next_slice_idx_to_weakest_connection; + auto remember_partial_object = [&active_object_parts, &partial_objects](size_t object_part_id) { + auto object_part = active_object_parts.access(object_part_id); + if (object_part.volume > EPSILON) { + partial_objects.emplace_back(object_part.volume_centroid_accumulator / object_part.volume, object_part.volume, + object_part.connected_to_bed); + } + }; + for (size_t layer_idx = 0; layer_idx < po->layer_count(); ++layer_idx) { cancel_func(); const Layer *layer = po->get_layer(layer_idx); @@ -681,7 +775,7 @@ SupportPoints check_stability(const PrintObject *po, const PrintTryCancel& cance for (size_t slice_idx = 0; slice_idx < layer->lslices_ex.size(); ++slice_idx) { const LayerSlice &slice = layer->lslices_ex.at(slice_idx); - auto [new_part, covered_area] = build_object_part_from_slice(slice, layer); + auto [new_part, covered_area] = build_object_part_from_slice(slice_idx, layer, params); SliceConnection connection_to_below = estimate_slice_connection(slice_idx, layer); #ifdef DETAILED_DEBUG_LOGS @@ -710,7 +804,10 @@ SupportPoints check_stability(const PrintObject *po, const PrintTryCancel& cance final_part_id = *parts_ids.begin(); for (size_t part_id : parts_ids) { - if (final_part_id != part_id) { active_object_parts.merge(part_id, final_part_id); } + if (final_part_id != part_id) { + remember_partial_object(part_id); + active_object_parts.merge(part_id, final_part_id); + } } } auto estimate_conn_strength = [bottom_z](const SliceConnection &conn) { @@ -771,18 +868,26 @@ SupportPoints check_stability(const PrintObject *po, const PrintTryCancel& cance // Function that is used when new support point is generated. It will update the ObjectPart stability, weakest conneciton info, // and the support presence grid and add the point to the issues. auto reckon_new_support_point = [&part, &weakest_conn, &supp_points, &supports_presence_grid, ¶ms, - &layer_idx](const Vec3f &support_point, float force, const Vec2f &dir) { + &layer_idx](SupportPointCause cause, const Vec3f &support_point, float force, + const Vec2f &dir) { + // if position is taken and point is for global stability (force > 0) or we are too close to the bed, do not add + // This allows local support points (e.g. bridging) to be generated densely if ((supports_presence_grid.position_taken(support_point) && force > 0) || layer_idx <= 1) { return; } + float area = params.support_points_interface_radius * params.support_points_interface_radius * float(PI); - part.add_support_point(support_point, area); + // add the stability effect of the point only if the spot is not taken, so that the densely created local support points do + // not add unrealistic amount of stability to the object (due to overlaping of local support points) + if (!(supports_presence_grid.position_taken(support_point))) { + part.add_support_point(support_point, area); + } float radius = params.support_points_interface_radius; - supp_points.emplace_back(support_point, force, radius, dir); - if (force > 0) { - supports_presence_grid.take_position(support_point); - } + supp_points.emplace_back(cause, support_point, force, radius, dir); + supports_presence_grid.take_position(support_point); + + // The support point also increases the stability of the weakest connection of the object, which should be reflected if (weakest_conn.area > EPSILON) { // Do not add it to the weakest connection if it is not valid - does not exist weakest_conn.area += area; weakest_conn.centroid_accumulator += support_point * area; @@ -801,9 +906,11 @@ SupportPoints check_stability(const PrintObject *po, const PrintTryCancel& cance const ExtrusionEntity *entity = fill_region->fills().entities[fill_idx]; if (entity->role() == ExtrusionRole::BridgeInfill) { for (const ExtrusionLine &bridge : - check_extrusion_entity_stability(entity, fill_region, prev_layer_ext_perim_lines,prev_layer_boundary, params)) { - if (bridge.support_point_generated) { - reckon_new_support_point(create_support_point_position(bridge.b), -EPSILON, Vec2f::Zero()); + check_extrusion_entity_stability(entity, fill_region, prev_layer_ext_perim_lines, prev_layer_boundary, + params)) { + if (bridge.support_point_generated.has_value()) { + reckon_new_support_point(*bridge.support_point_generated, create_support_point_position(bridge.b), + -EPSILON, Vec2f::Zero()); } } } @@ -816,10 +923,13 @@ SupportPoints check_stability(const PrintObject *po, const PrintTryCancel& cance std::vector perims = check_extrusion_entity_stability(entity, perimeter_region, prev_layer_ext_perim_lines,prev_layer_boundary, params); for (const ExtrusionLine &perim : perims) { - if (perim.support_point_generated) { - reckon_new_support_point(create_support_point_position(perim.b), -EPSILON, Vec2f::Zero()); + if (perim.support_point_generated.has_value()) { + reckon_new_support_point(*perim.support_point_generated, create_support_point_position(perim.b), -EPSILON, + Vec2f::Zero()); + } + if (perim.is_external_perimeter()) { + current_slice_ext_perims_lines.push_back(perim); } - if (perim.is_external_perimeter()) { current_slice_ext_perims_lines.push_back(perim); } } } } @@ -828,7 +938,8 @@ SupportPoints check_stability(const PrintObject *po, const PrintTryCancel& cance float unchecked_dist = params.min_distance_between_support_points + 1.0f; for (const ExtrusionLine &line : current_slice_ext_perims_lines) { - if ((unchecked_dist + line.len < params.min_distance_between_support_points && line.curled_up_height < 0.3f) || line.len < EPSILON) { + if ((unchecked_dist + line.len < params.min_distance_between_support_points && line.curled_up_height < 0.3f) || + line.len < EPSILON) { unchecked_dist += line.len; } else { unchecked_dist = line.len; @@ -836,8 +947,10 @@ SupportPoints check_stability(const PrintObject *po, const PrintTryCancel& cance auto [dist, nidx, nearest_point] = current_slice_lines_distancer.distance_from_lines_extra(pivot_site_search_point); Vec3f support_point = create_support_point_position(nearest_point); - auto force = part.is_stable_while_extruding(weakest_conn, line, support_point, bottom_z, params); - if (force > 0) { reckon_new_support_point(support_point, force, (line.b - line.a).normalized()); } + auto [force, cause] = part.is_stable_while_extruding(weakest_conn, line, support_point, bottom_z, params); + if (force > 0) { + reckon_new_support_point(cause, support_point, force, (line.b - line.a).normalized()); + } } } current_layer_ext_perims_lines.insert(current_layer_ext_perims_lines.end(), current_slice_ext_perims_lines.begin(), @@ -845,11 +958,16 @@ SupportPoints check_stability(const PrintObject *po, const PrintTryCancel& cance } // slice iterations prev_layer_ext_perim_lines = LD(current_layer_ext_perims_lines); } // layer iterations - return supp_points; + + for (const auto& active_obj_pair : prev_slice_idx_to_object_part_mapping) { + remember_partial_object(active_obj_pair.second); + } + + return {supp_points, partial_objects}; } #ifdef DEBUG_FILES -void debug_export(SupportPoints support_points, std::string file_name) +void debug_export(const SupportPoints& support_points,const PartialObjects& objects, std::string file_name) { Slic3r::CNumericLocalesSetter locales_setter; { @@ -860,13 +978,27 @@ void debug_export(SupportPoints support_points, std::string file_name) } for (size_t i = 0; i < support_points.size(); ++i) { - if (support_points[i].force <= 0) { - fprintf(fp, "v %f %f %f %f %f %f\n", support_points[i].position(0), support_points[i].position(1), - support_points[i].position(2), 0.0, 1.0, 0.0); - } else { - fprintf(fp, "v %f %f %f %f %f %f\n", support_points[i].position(0), support_points[i].position(1), - support_points[i].position(2), 1.0, 0.0, 0.0); + Vec3f color{1.0f, 1.0f, 1.0f}; + switch (support_points[i].cause) { + case SupportPointCause::FloatingBridgeAnchor: color = {0.863281f, 0.109375f, 0.113281f}; break; //RED + case SupportPointCause::LongBridge: color = {0.960938f, 0.90625f, 0.0625f}; break; // YELLOW + case SupportPointCause::FloatingExtrusion: color = {0.921875f, 0.515625f, 0.101563f}; break; // ORANGE + case SupportPointCause::SeparationFromBed: color = {0.0f, 1.0f, 0.0}; break; // GREEN + case SupportPointCause::UnstableFloatingPart: color = {0.105469f, 0.699219f, 0.84375f}; break; // BLUE + case SupportPointCause::WeakObjectPart: color = {0.609375f, 0.210938f, 0.621094f}; break; // PURPLE } + + fprintf(fp, "v %f %f %f %f %f %f\n", support_points[i].position(0), support_points[i].position(1), + support_points[i].position(2), color[0], color[1], color[2]); + } + + for (size_t i = 0; i < objects.size(); ++i) { + Vec3f color{1.0f, 0.0f, 1.0f}; + if (objects[i].connected_to_bed) { + color = {1.0f, 0.0f, 0.0f}; + } + fprintf(fp, "v %f %f %f %f %f %f\n", objects[i].centroid(0), objects[i].centroid(1), objects[i].centroid(2), color[0], + color[1], color[2]); } fclose(fp); @@ -874,17 +1006,15 @@ void debug_export(SupportPoints support_points, std::string file_name) } #endif -// std::vector quick_search(const PrintObject *po, const Params ¶ms) { -// return {}; -// } -SupportPoints full_search(const PrintObject *po, const PrintTryCancel& cancel_func, const Params ¶ms) +std::tuple full_search(const PrintObject *po, const PrintTryCancel& cancel_func, const Params ¶ms) { - SupportPoints supp_points = check_stability(po, cancel_func, params); + auto results = check_stability(po, cancel_func, params); #ifdef DEBUG_FILES - debug_export(supp_points, "issues"); + auto [supp_points, objects] = results; + debug_export(supp_points, objects, "issues"); #endif - return supp_points; + return results; } void estimate_supports_malformations(SupportLayerPtrs &layers, float flow_width, const Params ¶ms) @@ -973,6 +1103,9 @@ void estimate_malformations(LayerPtrs &layers, const Params ¶ms) std::vector current_layer_lines; for (const LayerRegion *layer_region : l->regions()) { for (const ExtrusionEntity *extrusion : layer_region->perimeters().flatten().entities) { + + if (!extrusion->role().is_external_perimeter()) continue; + Points extrusion_pts; extrusion->collect_points(extrusion_pts); float flow_width = get_flow_width(layer_region, extrusion->role()); @@ -1028,5 +1161,84 @@ void estimate_malformations(LayerPtrs &layers, const Params ¶ms) #endif } +void raise_alerts_for_issues(const SupportPoints &support_points, + PartialObjects &partial_objects, + std::function alert_fn) +{ + for (const SupportPoint &sp : support_points) { + if (sp.cause == SupportPointCause::SeparationFromBed) { + alert_fn(PrintStateBase::WarningLevel::NON_CRITICAL, SupportPointCause::SeparationFromBed); + break; + } + } + + std::reverse(partial_objects.begin(), partial_objects.end()); + std::sort(partial_objects.begin(), partial_objects.end(), + [](const PartialObject &left, const PartialObject &right) { return left.volume > right.volume; }); + + float max_volume_part = partial_objects.front().volume; + for (const PartialObject &p : partial_objects) { + if (p.volume > max_volume_part / 500.0f && !p.connected_to_bed) { + alert_fn(PrintStateBase::WarningLevel::CRITICAL, SupportPointCause::UnstableFloatingPart); + return; + } + } + + for (const SupportPoint &sp : support_points) { + if (sp.cause == SupportPointCause::UnstableFloatingPart) { + alert_fn(PrintStateBase::WarningLevel::CRITICAL, SupportPointCause::UnstableFloatingPart); + return; + } + } + + std::vector ext_supp_points{}; + ext_supp_points.reserve(support_points.size()); + for (const SupportPoint &sp : support_points) { + switch (sp.cause) { + case SupportPointCause::FloatingBridgeAnchor: + case SupportPointCause::FloatingExtrusion: ext_supp_points.push_back(sp); break; + default: break; + } + } + + auto coord_fn = [&ext_supp_points](size_t idx, size_t dim) { return ext_supp_points[idx].position[dim]; }; + KDTreeIndirect<3, float, decltype(coord_fn)> ext_points_tree{coord_fn, ext_supp_points.size()}; + for (const SupportPoint &sp : ext_supp_points) { + auto cluster = find_nearby_points(ext_points_tree, sp.position, 3.0); + int score = 0; + bool floating_bridge = false; + for (size_t idx : cluster) { + score += ext_supp_points[idx].cause == SupportPointCause::FloatingBridgeAnchor ? 3 : 1; + floating_bridge = floating_bridge || ext_supp_points[idx].cause == SupportPointCause::FloatingBridgeAnchor; + } + if (score > 5) { + if (floating_bridge) { + alert_fn(PrintStateBase::WarningLevel::CRITICAL, SupportPointCause::FloatingBridgeAnchor); + } else { + alert_fn(PrintStateBase::WarningLevel::CRITICAL, SupportPointCause::FloatingExtrusion); + } + return; + } + } + + if (ext_supp_points.size() > 5) { + alert_fn(PrintStateBase::WarningLevel::NON_CRITICAL, SupportPointCause::FloatingExtrusion); + } + + for (const SupportPoint &sp : support_points) { + if (sp.cause == SupportPointCause::LongBridge) { + alert_fn(PrintStateBase::WarningLevel::NON_CRITICAL, SupportPointCause::LongBridge); + break; + } + } + + for (const SupportPoint &sp : support_points) { + if (sp.cause == SupportPointCause::WeakObjectPart) { + alert_fn(PrintStateBase::WarningLevel::NON_CRITICAL, SupportPointCause::WeakObjectPart); + break; + } + } +} + } // namespace SupportSpotsGenerator } // namespace Slic3r diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index 1c07c1381..23fb5dad0 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -4,43 +4,52 @@ #include "Layer.hpp" #include "Line.hpp" #include "PrintBase.hpp" +#include "PrintConfig.hpp" #include +#include #include namespace Slic3r { namespace SupportSpotsGenerator { -struct Params { - Params(const std::vector &filament_types) { +struct Params +{ + Params( + const std::vector &filament_types, float max_acceleration, int raft_layers_count, BrimType brim_type, float brim_width) + : max_acceleration(max_acceleration), raft_layers_count(raft_layers_count), brim_type(brim_type), brim_width(brim_width) + { if (filament_types.size() > 1) { BOOST_LOG_TRIVIAL(warning) - << "SupportSpotsGenerator does not currently handle different materials properly, only first will be used"; + << "SupportSpotsGenerator does not currently handle different materials properly, only first will be used"; } if (filament_types.empty() || filament_types[0].empty()) { - BOOST_LOG_TRIVIAL(error) - << "SupportSpotsGenerator error: empty filament_type"; + BOOST_LOG_TRIVIAL(error) << "SupportSpotsGenerator error: empty filament_type"; filament_type = std::string("PLA"); } else { filament_type = filament_types[0]; - BOOST_LOG_TRIVIAL(debug) - << "SupportSpotsGenerator: applying filament type: " << filament_type; + BOOST_LOG_TRIVIAL(debug) << "SupportSpotsGenerator: applying filament type: " << filament_type; } } // the algorithm should use the following units for all computations: distance [mm], mass [g], time [s], force [g*mm/s^2] - const float bridge_distance = 12.0f; //mm + const float bridge_distance = 12.0f; // mm + const float max_acceleration; // mm/s^2 ; max acceleration of object in XY -- should be applicable only to printers with bed slinger, + // however we do not have such info yet. The force is usually small anyway, so not such a big deal to include it everytime + const int raft_layers_count; + std::string filament_type; + + BrimType brim_type; + const float brim_width; + const std::pair malformation_distance_factors = std::pair { 0.4, 1.2 }; const float max_curled_height_factor = 10.0f; const float min_distance_between_support_points = 3.0f; //mm const float support_points_interface_radius = 1.5f; // mm - const float connections_min_considerable_area = 1.5f; //mm^2 const float min_distance_to_allow_local_supports = 1.0f; //mm - std::string filament_type; const float gravity_constant = 9806.65f; // mm/s^2; gravity acceleration on Earth's surface, algorithm assumes that printer is in upwards position. - const float max_acceleration = 9 * 1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY (NOTE: The max hit is received by the object in the jerk phase, so the usual machine limits are too low) const double filament_density = 1.25e-3f; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important const double material_yield_strength = 33.0f * 1e6f; // (g*mm/s^2)/mm^2; 33 MPa is yield strength of ABS, which has the lowest yield strength from common materials. const float standard_extruder_conflict_force = 20.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); @@ -48,10 +57,16 @@ struct Params { // MPa * 1e^6 = (g*mm/s^2)/mm^2 = g/(mm*s^2); yield strength of the bed surface double get_bed_adhesion_yield_strength() const { + if (raft_layers_count > 0) { + return get_support_spots_adhesion_strength() * 2.0; + } + if (filament_type == "PLA") { return 0.018 * 1e6; } else if (filament_type == "PET" || filament_type == "PETG") { return 0.3 * 1e6; + } else if (filament_type == "ABS" || filament_type == "ASA") { + return 0.1 * 1e6; //TODO do measurements } else { //PLA default value - defensive approach, PLA has quite low adhesion return 0.018 * 1e6; } @@ -63,11 +78,51 @@ struct Params { } }; -struct SupportPoint { - SupportPoint(const Vec3f &position, float force, float spot_radius, const Vec2f &direction); +enum class SupportPointCause { + LongBridge, // point generated on bridge and straight perimeter extrusion longer than the allowed length + FloatingBridgeAnchor, // point generated on unsupported bridge endpoint + FloatingExtrusion, // point generated on extrusion that does not hold on its own + SeparationFromBed, // point generated for object parts that are connected to the bed, but the area is too small and there is a risk of separation (brim may help) + UnstableFloatingPart, // point generated for object parts not connected to the bed, holded only by the other support points (brim will not help here) + WeakObjectPart // point generated when some part of the object is too weak to hold the upper part and may break (imagine hourglass) + }; + +// The support points can be sorted into two groups +// 1. Local extrusion support for extrusions that are printed in the air and would not +// withstand on their own (too long bridges, sharp turns in large overhang, concave bridge holes, etc.) +// These points have negative force (-EPSILON) and Vec2f::Zero() direction +// The algorithm still expects that these points will be supported and accounts for them in the global stability check. +// 2. Global stability support points are generated at each spot, where the algorithm detects that extruding the current line +// may cause separation of the object part from the bed and/or its support spots or crack in the weak connection of the object parts. +// The generated point's direction is the estimated falling direction of the object part, and the force is equal to te difference +// between forces that destabilize the object (extruder conflicts with curled filament, weight if instable center of mass, bed movements etc) +// and forces that stabilize the object (bed adhesion, other support spots adhesion, weight if stable center of mass). +// Note that the force is only the difference - the amount needed to stabilize the object again. +struct SupportPoint +{ + SupportPoint(SupportPointCause cause, const Vec3f &position, float force, float spot_radius, const Vec2f &direction) + : cause(cause), position(position), force(force), spot_radius(spot_radius), direction(direction) + {} + + bool is_local_extrusion_support() const + { + return cause == SupportPointCause::LongBridge || cause == SupportPointCause::FloatingExtrusion; + } + bool is_global_object_support() const { return !is_local_extrusion_support(); } + + SupportPointCause cause; // reason why this support point was generated. Used for the user alerts + // position is in unscaled coords. The z coordinate is aligned with the layers bottom_z coordiantes Vec3f position; + // force that destabilizes the object to the point of falling/breaking. g*mm/s^2 units + // It is valid only for global_object_support. For local extrusion support points, the force is -EPSILON + // values gathered from large XL model: Min : 0 | Max : 18713800 | Average : 1361186 | Median : 329103 + // For reference 18713800 is weight of 1.8 Kg object, 329103 is weight of 0.03 Kg + // The final sliced object weight was approx 0.5 Kg float force; + // Expected spot size. The support point strength is calculated from the area defined by this value. + // Currently equal to the support_points_interface_radius parameter above float spot_radius; + // direction of the fall of the object (z part is neglected) Vec2f direction; }; @@ -77,13 +132,28 @@ struct Malformations { std::vector layers; //for each layer }; -// std::vector quick_search(const PrintObject *po, const Params ¶ms); -SupportPoints full_search(const PrintObject *po, const PrintTryCancel& cancel_func, const Params ¶ms); +struct PartialObject +{ + PartialObject(Vec3f centroid, float volume, bool connected_to_bed) + : centroid(centroid), volume(volume), connected_to_bed(connected_to_bed) + {} -void estimate_supports_malformations(std::vector &layers, float supports_flow_width, const Params ¶ms); -void estimate_malformations(std::vector &layers, const Params ¶ms); + Vec3f centroid; + float volume; + bool connected_to_bed; +}; -} // namespace SupportSpotsGenerator -} +using PartialObjects = std::vector; + +std::tuple full_search(const PrintObject *po, const PrintTryCancel& cancel_func, const Params ¶ms); + +void estimate_supports_malformations(std::vector &layers, float supports_flow_width, const Params ¶ms); +void estimate_malformations(std::vector &layers, const Params ¶ms); + +void raise_alerts_for_issues(const SupportPoints &support_points, + PartialObjects &partial_objects, + std::function alert_fn); + +}} // namespace Slic3r::SupportSpotsGenerator #endif /* SRC_LIBSLIC3R_SUPPORTABLEISSUESSEARCH_HPP_ */ diff --git a/src/libslic3r/TreeModelVolumes.cpp b/src/libslic3r/TreeModelVolumes.cpp index b416db634..63b0ab7a3 100644 --- a/src/libslic3r/TreeModelVolumes.cpp +++ b/src/libslic3r/TreeModelVolumes.cpp @@ -18,6 +18,8 @@ #include "PrintConfig.hpp" #include "Utils.hpp" +#include + #include #include @@ -26,6 +28,8 @@ namespace Slic3r::FFFTreeSupport { +using namespace std::literals; + // or warning // had to use a define beacuse the macro processing inside macro BOOST_LOG_TRIVIAL() #define error_level_not_in_cache error @@ -50,6 +54,7 @@ TreeSupportMeshGroupSettings::TreeSupportMeshGroupSettings(const PrintObject &pr this->layer_height = scaled(config.layer_height.value); this->resolution = scaled(print_config.gcode_resolution.value); + // Arache feature this->min_feature_size = scaled(config.min_feature_size.value); // +1 makes the threshold inclusive this->support_angle = 0.5 * M_PI - std::clamp((config.support_material_threshold + 1) * M_PI / 180., 0., 0.5 * M_PI); @@ -84,6 +89,14 @@ TreeSupportMeshGroupSettings::TreeSupportMeshGroupSettings(const PrintObject &pr // this->minimum_support_area = // this->minimum_bottom_area = // this->support_offset = +// this->support_tree_branch_distance = 2.5 * line_width ?? + this->support_tree_angle = std::clamp(config.support_tree_angle * M_PI / 180., 0., 0.5 * M_PI - EPSILON); + this->support_tree_angle_slow = std::clamp(config.support_tree_angle_slow * M_PI / 180., 0., this->support_tree_angle - EPSILON); + this->support_tree_branch_diameter = scaled(config.support_tree_branch_diameter.value); + this->support_tree_branch_diameter_angle = std::clamp(config.support_tree_branch_diameter_angle * M_PI / 180., 0., 0.5 * M_PI - EPSILON); + this->support_tree_top_rate = config.support_tree_top_rate.value; // percent +// this->support_tree_tip_diameter = this->support_line_width; + this->support_tree_tip_diameter = std::clamp(scaled(config.support_tree_tip_diameter.value), 0, this->support_tree_branch_diameter); } //FIXME Machine border is currently ignored. @@ -336,12 +349,20 @@ const Polygons& TreeModelVolumes::getCollision(const coord_t orig_radius, LayerI return (*result).get(); if (m_precalculated) { BOOST_LOG_TRIVIAL(error_level_not_in_cache) << "Had to calculate collision at radius " << radius << " and layer " << layer_idx << ", but precalculate was called. Performance may suffer!"; - tree_supports_show_error("Not precalculated Collision requested.", false); + tree_supports_show_error("Not precalculated Collision requested."sv, false); } const_cast(this)->calculateCollision(radius, layer_idx); return getCollision(orig_radius, layer_idx, min_xy_dist); } +// Get a collision area at a given layer for a radius that is a lower or equial to the key radius. +// It is expected that the collision area is precalculated for a given layer at least for the radius zero. +// Used for pushing tree supports away from object during the final Organic optimization step. +std::optional>> TreeModelVolumes::get_collision_lower_bound_area(LayerIndex layer_id, coord_t max_radius) const +{ + return m_collision_cache.get_lower_bound_area({ max_radius, layer_id }); +} + // Private. Only called internally by calculateAvoidance() and calculateAvoidanceToModel(), radius is already snapped to grid. const Polygons& TreeModelVolumes::getCollisionHolefree(coord_t radius, LayerIndex layer_idx) const { @@ -351,7 +372,7 @@ const Polygons& TreeModelVolumes::getCollisionHolefree(coord_t radius, LayerInde return (*result).get(); if (m_precalculated) { BOOST_LOG_TRIVIAL(error_level_not_in_cache) << "Had to calculate collision holefree at radius " << radius << " and layer " << layer_idx << ", but precalculate was called. Performance may suffer!"; - tree_supports_show_error("Not precalculated Holefree Collision requested.", false); + tree_supports_show_error("Not precalculated Holefree Collision requested."sv, false); } const_cast(this)->calculateCollisionHolefree({ radius, layer_idx }); return getCollisionHolefree(radius, layer_idx); @@ -375,10 +396,10 @@ const Polygons& TreeModelVolumes::getAvoidance(const coord_t orig_radius, LayerI if (m_precalculated) { if (to_model) { BOOST_LOG_TRIVIAL(error_level_not_in_cache) << "Had to calculate Avoidance to model at radius " << radius << " and layer " << layer_idx << ", but precalculate was called. Performance may suffer!"; - tree_supports_show_error("Not precalculated Avoidance(to model) requested.", false); + tree_supports_show_error("Not precalculated Avoidance(to model) requested."sv, false); } else { BOOST_LOG_TRIVIAL(error_level_not_in_cache) << "Had to calculate Avoidance at radius " << radius << " and layer " << layer_idx << ", but precalculate was called. Performance may suffer!"; - tree_supports_show_error("Not precalculated Avoidance(to buildplate) requested.", false); + tree_supports_show_error("Not precalculated Avoidance(to buildplate) requested."sv, false); } } const_cast(this)->calculateAvoidance({ radius, layer_idx }, ! to_model, to_model); @@ -396,7 +417,7 @@ const Polygons& TreeModelVolumes::getPlaceableAreas(const coord_t orig_radius, L return (*result).get(); if (m_precalculated) { BOOST_LOG_TRIVIAL(error_level_not_in_cache) << "Had to calculate Placeable Areas at radius " << radius << " and layer " << layer_idx << ", but precalculate was called. Performance may suffer!"; - tree_supports_show_error("Not precalculated Placeable areas requested.", false); + tree_supports_show_error("Not precalculated Placeable areas requested."sv, false); } const_cast(this)->calculatePlaceables(radius, layer_idx); return getPlaceableAreas(orig_radius, layer_idx); @@ -422,7 +443,7 @@ const Polygons& TreeModelVolumes::getWallRestriction(const coord_t orig_radius, tree_supports_show_error( min_xy_dist ? "Not precalculated Wall restriction of minimum xy distance requested )." : - "Not precalculated Wall restriction requested )." + "Not precalculated Wall restriction requested )."sv , false); } const_cast(this)->calculateWallRestrictions({ radius, layer_idx }); @@ -561,7 +582,8 @@ void TreeModelVolumes::calculateCollisionHolefree(const std::vector(0, max_layer + 1, keys.size()), [&](const tbb::blocked_range &range) { - RadiusLayerPolygonCacheData data; + std::vector> data; + data.reserve(range.size() * keys.size()); for (LayerIndex layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) { for (RadiusLayerPair key : keys) if (layer_idx <= key.second) { @@ -572,10 +594,10 @@ void TreeModelVolumes::calculateCollisionHolefree(const std::vector 0); // this union is important as otherwise holes(in form of lines that will increase to holes in a later step) can get unioned onto the area. - data[RadiusLayerPair(radius, layer_idx)] = polygons_simplify( - offset(union_ex(getCollision(m_increase_until_radius, layer_idx, false)), + data.emplace_back(RadiusLayerPair(radius, layer_idx), polygons_simplify( + offset(union_ex(this->getCollision(m_increase_until_radius, layer_idx, false)), 5 - increase_radius_ceil, ClipperLib::jtRound, m_min_resolution), - m_min_resolution); + m_min_resolution)); } } m_collision_cache_holefree.insert(std::move(data)); @@ -819,13 +841,25 @@ coord_t TreeModelVolumes::ceilRadius(const coord_t radius) const return out; } +void TreeModelVolumes::RadiusLayerPolygonCache::allocate_layers(size_t num_layers) +{ + if (num_layers > m_data.size()) { + if (num_layers > m_data.capacity()) + reserve_power_of_2(m_data, num_layers); + m_data.resize(num_layers, {}); + } +} + // For debugging purposes, sorted by layer index, then by radius. std::vector>> TreeModelVolumes::RadiusLayerPolygonCache::sorted() const { std::vector>> out; - for (auto it = this->data.begin(); it != this->data.end(); ++ it) - out.emplace_back(it->first, it->second); - std::sort(out.begin(), out.end(), [](auto &l, auto &r){ return l.first.second < r.first.second || (l.first.second == r.first.second) && l.first.first < r.first.first; }); + for (auto &layer : m_data) { + auto layer_idx = LayerIndex(&layer - m_data.data()); + for (auto &radius_polygons : layer) + out.emplace_back(std::make_pair(radius_polygons.first, layer_idx), radius_polygons.second); + } + assert(std::is_sorted(out.begin(), out.end(), [](auto &l, auto &r){ return l.first.second < r.first.second || (l.first.second == r.first.second) && l.first.first < r.first.first; })); return out; } diff --git a/src/libslic3r/TreeModelVolumes.hpp b/src/libslic3r/TreeModelVolumes.hpp index 0ced2f422..0c3c4cc8a 100644 --- a/src/libslic3r/TreeModelVolumes.hpp +++ b/src/libslic3r/TreeModelVolumes.hpp @@ -11,7 +11,6 @@ #include #include -#include #include @@ -43,7 +42,7 @@ struct TreeSupportMeshGroupSettings { // the print will be less accurate, but the g-code will be smaller. Maximum Deviation is a limit for Maximum Resolution, // so if the two conflict the Maximum Deviation will always be held true. coord_t resolution { scaled(0.025) }; - // Minimum Feature Size (aka minimum line width) + // Minimum Feature Size (aka minimum line width) - Arachne specific // Minimum thickness of thin features. Model features that are thinner than this value will not be printed, while features thicker // than the Minimum Feature Size will be widened to the Minimum Wall Line Width. coord_t min_feature_size { scaled(0.1) }; @@ -183,7 +182,7 @@ struct TreeSupportMeshGroupSettings { // 5%-35% double support_tree_top_rate { 15. }; // Tree Support Tip Diameter - // The diameter of the top of the tip of the branches of tree support." + // The diameter of the top of the tip of the branches of tree support. // minimum: min_wall_line_width, minimum warning: min_wall_line_width+0.05, maximum_value: support_tree_branch_diameter, value: support_line_width coord_t support_tree_tip_diameter { scaled(0.4) }; @@ -238,6 +237,11 @@ public: */ const Polygons& getCollision(const coord_t radius, LayerIndex layer_idx, bool min_xy_dist) const; + // Get a collision area at a given layer for a radius that is a lower or equial to the key radius. + // It is expected that the collision area is precalculated for a given layer at least for the radius zero. + // Used for pushing tree supports away from object during the final Organic optimization step. + std::optional>> get_collision_lower_bound_area(LayerIndex layer_id, coord_t max_radius) const; + /*! * \brief Provides the areas that have to be avoided by the tree's branches * in order to reach the build plate. @@ -306,36 +310,36 @@ private: * \brief Convenience typedef for the keys to the caches */ using RadiusLayerPair = std::pair; - using RadiusLayerPolygonCacheData = std::unordered_map>; class RadiusLayerPolygonCache { + // Map from radius to Polygons. Cache of one layer collision regions. + using LayerData = std::map; + // Vector of layers, at each layer map of radius to Polygons. + // Reference to Polygons returned shall be stable to insertion. + using Layers = std::vector; public: RadiusLayerPolygonCache() = default; - RadiusLayerPolygonCache(RadiusLayerPolygonCache &&rhs) : data(std::move(rhs.data)) {} - RadiusLayerPolygonCache& operator=(RadiusLayerPolygonCache &&rhs) { data = std::move(rhs.data); return *this; } + RadiusLayerPolygonCache(RadiusLayerPolygonCache &&rhs) : m_data(std::move(rhs.m_data)) {} + RadiusLayerPolygonCache& operator=(RadiusLayerPolygonCache &&rhs) { m_data = std::move(rhs.m_data); return *this; } RadiusLayerPolygonCache(const RadiusLayerPolygonCache&) = delete; RadiusLayerPolygonCache& operator=(const RadiusLayerPolygonCache&) = delete; - void insert(RadiusLayerPolygonCacheData &&in) { - std::lock_guard guard(this->mutex); - for (auto& d : in) - this->data.emplace(d.first, std::move(d.second)); - } void insert(std::vector> &&in) { - std::lock_guard guard(this->mutex); - for (auto& d : in) - this->data.emplace(d.first, std::move(d.second)); + std::lock_guard guard(m_mutex); + for (auto &d : in) + this->get_allocate_layer_data(d.first.second).emplace(d.first.first, std::move(d.second)); } // by layer void insert(std::vector> &&in, coord_t radius) { - std::lock_guard guard(this->mutex); + std::lock_guard guard(m_mutex); for (auto &d : in) - this->data.emplace(RadiusLayerPair{ radius, d.first }, std::move(d.second)); + this->get_allocate_layer_data(d.first).emplace(radius, std::move(d.second)); } void insert(std::vector &&in, coord_t first_layer_idx, coord_t radius) { - std::lock_guard guard(this->mutex); + std::lock_guard guard(m_mutex); + allocate_layers(first_layer_idx + in.size()); for (auto &d : in) - this->data.emplace(RadiusLayerPair{ radius, first_layer_idx ++ }, std::move(d)); + m_data[first_layer_idx ++].emplace(radius, std::move(d)); } /*! * \brief Checks a cache for a given RadiusLayerPair and returns it if it is found @@ -343,11 +347,30 @@ private: * \return A wrapped optional reference of the requested area (if it was found, an empty optional if nothing was found) */ std::optional> getArea(const TreeModelVolumes::RadiusLayerPair &key) const { - std::lock_guard guard(this->mutex); - const auto it = this->data.find(key); - return it == this->data.end() ? + std::lock_guard guard(m_mutex); + if (key.second >= m_data.size()) + return std::optional>{}; + const auto &layer = m_data[key.second]; + auto it = layer.find(key.first); + return it == layer.end() ? std::optional>{} : std::optional>{ it->second }; } + // Get a collision area at a given layer for a radius that is a lower or equial to the key radius. + std::optional>> get_lower_bound_area(const TreeModelVolumes::RadiusLayerPair &key) const { + std::lock_guard guard(m_mutex); + if (key.second >= m_data.size()) + return {}; + const auto &layer = m_data[key.second]; + if (layer.empty()) + return {}; + auto it = layer.lower_bound(key.first); + if (it == layer.end() || it->first != key.first) { + if (it == layer.begin()) + return {}; + -- it; + } + return std::make_pair(it->first, std::reference_wrapper(it->second)); + } /*! * \brief Get the highest already calculated layer in the cache. * \param radius The radius for which the highest already calculated layer has to be found. @@ -356,22 +379,27 @@ private: * \return A wrapped optional reference of the requested area (if it was found, an empty optional if nothing was found) */ LayerIndex getMaxCalculatedLayer(coord_t radius) const { - std::lock_guard guard(this->mutex); - int max_layer = -1; - // the placeable on model areas do not exist on layer 0, as there can not be model below it. As such it may be possible that layer 1 is available, but layer 0 does not exist. - if (this->data.find({ radius, 1 }) != this->data.end()) - max_layer = 1; - while (this->data.count(TreeModelVolumes::RadiusLayerPair(radius, max_layer + 1)) > 0) - ++ max_layer; - return max_layer; + std::lock_guard guard(m_mutex); + auto layer_idx = LayerIndex(m_data.size()) - 1; + for (; layer_idx > 0; -- layer_idx) + if (const auto &layer = m_data[layer_idx]; layer.find(radius) != layer.end()) + break; + // The placeable on model areas do not exist on layer 0, as there can not be model below it. As such it may be possible that layer 1 is available, but layer 0 does not exist. + return layer_idx == 0 ? -1 : layer_idx; } // For debugging purposes, sorted by layer index, then by radius. [[nodiscard]] std::vector>> sorted() const; private: - RadiusLayerPolygonCacheData data; - mutable std::mutex mutex; + LayerData& get_allocate_layer_data(LayerIndex layer_idx) { + allocate_layers(layer_idx + 1); + return m_data[layer_idx]; + } + void allocate_layers(size_t num_layers); + + Layers m_data; + mutable std::mutex m_mutex; }; diff --git a/src/libslic3r/TreeSupport.cpp b/src/libslic3r/TreeSupport.cpp index 1f0aa00b2..9b373312f 100644 --- a/src/libslic3r/TreeSupport.cpp +++ b/src/libslic3r/TreeSupport.cpp @@ -8,6 +8,7 @@ #include "TreeSupport.hpp" #include "AABBTreeIndirect.hpp" +#include "AABBTreeLines.hpp" #include "BuildVolume.hpp" #include "ClipperUtils.hpp" #include "EdgeGrid.hpp" @@ -20,7 +21,7 @@ #include "MutablePolygon.hpp" #include "SupportMaterial.hpp" #include "TriangleMeshSlicer.hpp" -#include "OpenVDBUtils.hpp" +#include "OpenVDBUtilsLegacy.hpp" #include #include @@ -40,6 +41,10 @@ #include #include +#if defined(TREE_SUPPORT_SHOW_ERRORS) && defined(_WIN32) + #define TREE_SUPPORT_SHOW_ERRORS_WIN32 +#endif + namespace Slic3r { @@ -58,6 +63,7 @@ enum class LineStatus using LineInformation = std::vector>; using LineInformations = std::vector; +using namespace std::literals; static inline void validate_range(const Point &pt) { @@ -103,16 +109,16 @@ static inline void validate_range(const LineInformations &lines) static inline void check_self_intersections(const Polygons &polygons, const std::string_view message) { -#ifdef _WIN32 +#ifdef TREE_SUPPORT_SHOW_ERRORS_WIN32 if (!intersecting_edges(polygons).empty()) ::MessageBoxA(nullptr, (std::string("TreeSupport infill self intersections: ") + std::string(message)).c_str(), "Bug detected!", MB_OK | MB_SYSTEMMODAL | MB_SETFOREGROUND | MB_ICONWARNING); -#endif // _WIN32 +#endif // TREE_SUPPORT_SHOW_ERRORS_WIN32 } static inline void check_self_intersections(const ExPolygon &expoly, const std::string_view message) { -#ifdef _WIN32 +#ifdef TREE_SUPPORT_SHOW_ERRORS_WIN32 check_self_intersections(to_polygons(expoly), message); -#endif // _WIN32 +#endif // TREE_SUPPORT_SHOW_ERRORS_WIN32 } static constexpr const auto tiny_area_threshold = sqr(scaled(0.001)); @@ -191,22 +197,21 @@ static std::vector>> group_me } #endif -//todo Remove! Only relevant for public BETA! static bool inline g_showed_critical_error = false; static bool inline g_showed_performance_warning = false; -void tree_supports_show_error(std::string message, bool critical) +void tree_supports_show_error(std::string_view message, bool critical) { // todo Remove! ONLY FOR PUBLIC BETA!! -#if defined(_WIN32) && defined(TREE_SUPPORT_SHOW_ERRORS) +#ifdef TREE_SUPPORT_SHOW_ERRORS_WIN32 static bool showed_critical = false; static bool showed_performance = false; auto bugtype = std::string(critical ? " This is a critical bug. It may cause missing or malformed branches.\n" : "This bug should only decrease performance.\n"); bool show = (critical && !g_showed_critical_error) || (!critical && !g_showed_performance_warning); (critical ? g_showed_critical_error : g_showed_performance_warning) = true; if (show) - MessageBoxA(nullptr, std::string("TreeSupport_2 MOD detected an error while generating the tree support.\nPlease report this back to me with profile and model.\nRevision 5.0\n" + message + "\n" + bugtype).c_str(), + MessageBoxA(nullptr, std::string("TreeSupport_2 MOD detected an error while generating the tree support.\nPlease report this back to me with profile and model.\nRevision 5.0\n" + std::string(message) + "\n" + bugtype).c_str(), "Bug detected!", MB_OK | MB_SYSTEMMODAL | MB_SETFOREGROUND | MB_ICONWARNING); -#endif // WIN32 +#endif // TREE_SUPPORT_SHOW_ERRORS_WIN32 } [[nodiscard]] static const std::vector generate_overhangs(const PrintObject &print_object) @@ -572,7 +577,7 @@ static std::optional> polyline_sample_next_point_at_dis // In case a fixpoint is encountered, better aggressively overcompensate so the code does not become stuck here... BOOST_LOG_TRIVIAL(warning) << "Tree Support: Encountered a fixpoint in polyline_sample_next_point_at_distance. This is expected to happen if the distance (currently " << next_distance << ") is smaller than 100"; - tree_supports_show_error("Encountered issue while placing tips. Some tips may be missing.", true); + tree_supports_show_error("Encountered issue while placing tips. Some tips may be missing."sv, true); if (next_distance > 2 * current_distance) // This case should never happen, but better safe than sorry. break; @@ -644,14 +649,6 @@ static std::optional> polyline_sample_next_point_at_dis append(lines, to_polylines(polygons)); return lines; #else -#ifdef _WIN32 - // Max dimensions for MK3 -// if (! BoundingBox(Point::new_scale(-170., -170.), Point::new_scale(170., 170.)).contains(get_extents(polygon))) - // Max dimensions for XL - if (! BoundingBox(Point::new_scale(-250., -250.), Point::new_scale(250., 250.)).contains(get_extents(polygon))) - ::MessageBoxA(nullptr, "TreeSupport infill kravsky", "Bug detected!", MB_OK | MB_SYSTEMMODAL | MB_SETFOREGROUND | MB_ICONWARNING); -#endif // _WIN32 - const Flow &flow = roof ? support_params.support_material_interface_flow : support_params.support_material_flow; std::unique_ptr filler = std::unique_ptr(Fill::new_from_type(roof ? support_params.interface_fill_pattern : support_params.base_fill_pattern)); FillParams fill_params; @@ -670,20 +667,20 @@ static std::optional> polyline_sample_next_point_at_dis for (ExPolygon &expoly : union_ex(polygon)) { // The surface type does not matter. assert(area(expoly) > 0.); -#ifdef _WIN32 +#ifdef TREE_SUPPORT_SHOW_ERRORS_WIN32 if (area(expoly) <= 0.) ::MessageBoxA(nullptr, "TreeSupport infill negative area", "Bug detected!", MB_OK | MB_SYSTEMMODAL | MB_SETFOREGROUND | MB_ICONWARNING); -#endif // _WIN32 +#endif // TREE_SUPPORT_SHOW_ERRORS_WIN32 assert(intersecting_edges(to_polygons(expoly)).empty()); check_self_intersections(expoly, "generate_support_infill_lines"); Surface surface(stInternal, std::move(expoly)); try { Polylines pl = filler->fill_surface(&surface, fill_params); assert(pl.empty() || get_extents(surface.expolygon).inflated(SCALED_EPSILON).contains(get_extents(pl))); -#ifdef _WIN32 +#ifdef TREE_SUPPORT_SHOW_ERRORS_WIN32 if (! pl.empty() && ! get_extents(surface.expolygon).inflated(SCALED_EPSILON).contains(get_extents(pl))) ::MessageBoxA(nullptr, "TreeSupport infill failure", "Bug detected!", MB_OK | MB_SYSTEMMODAL | MB_SETFOREGROUND | MB_ICONWARNING); -#endif // _WIN32 +#endif // TREE_SUPPORT_SHOW_ERRORS_WIN32 append(out, std::move(pl)); } catch (InfillFailedException &) { } @@ -759,7 +756,7 @@ static std::optional> polyline_sample_next_point_at_dis return do_final_difference ? diff(ret, collision_trimmed()) : union_(ret); if (safe_step_size < 0 || last_step_offset_without_check < 0) { BOOST_LOG_TRIVIAL(error) << "Offset increase got invalid parameter!"; - tree_supports_show_error("Negative offset distance... How did you manage this ?", true); + tree_supports_show_error("Negative offset distance... How did you manage this ?"sv, true); return do_final_difference ? diff(ret, collision_trimmed()) : union_(ret); } @@ -805,6 +802,14 @@ static double layer_z(const SlicingParameters &slicing_params, const size_t laye { return slicing_params.object_print_z_min + slicing_params.first_object_layer_height + layer_idx * slicing_params.layer_height; } +static LayerIndex layer_idx_ceil(const SlicingParameters &slicing_params, const double z) +{ + return LayerIndex(ceil((z - slicing_params.object_print_z_min - slicing_params.first_object_layer_height) / slicing_params.layer_height)); +} +static LayerIndex layer_idx_floor(const SlicingParameters &slicing_params, const double z) +{ + return LayerIndex(floor((z - slicing_params.object_print_z_min - slicing_params.first_object_layer_height) / slicing_params.layer_height)); +} static inline SupportGeneratorLayer& layer_initialize( SupportGeneratorLayer &layer_new, @@ -951,7 +956,7 @@ static void generate_initial_areas( bool safe_radius = p.second == LineStatus::TO_BP_SAFE || p.second == LineStatus::TO_MODEL_GRACIOUS_SAFE; if (!mesh_config.support_rests_on_model && !to_bp) { BOOST_LOG_TRIVIAL(warning) << "Tried to add an invalid support point"; - tree_supports_show_error("Unable to add tip. Some overhang may not be supported correctly.", true); + tree_supports_show_error("Unable to add tip. Some overhang may not be supported correctly."sv, true); return; } Polygons circle{ base_circle }; @@ -1517,7 +1522,7 @@ static Point move_inside_if_outside(const Polygons &polygons, Point from, int di if (area(check_layer_data) < tiny_area_threshold) { BOOST_LOG_TRIVIAL(error) << "Lost area by doing catch up from " << ceil_radius_before << " to radius " << volumes.ceilRadius(config.getCollisionRadius(current_elem), settings.use_min_distance); - tree_supports_show_error("Area lost catching up radius. May not cause visible malformation.", true); + tree_supports_show_error("Area lost catching up radius. May not cause visible malformation."sv, true); } } } @@ -1783,7 +1788,7 @@ static void increase_areas_one_layer( "Parent " << &parent << ": Radius: " << config.getCollisionRadius(parent.state) << " at layer: " << layer_idx << " NextTarget: " << parent.state.layer_idx << " Distance to top: " << parent.state.distance_to_top << " Elephant foot increases " << parent.state.elephant_foot_increases << " use_min_xy_dist " << parent.state.use_min_xy_dist << " to buildplate " << parent.state.to_buildplate << " gracious " << parent.state.to_model_gracious << " safe " << parent.state.can_use_safe_radius << " until move " << parent.state.dont_move_until; - tree_supports_show_error("Potentially lost branch!", true); + tree_supports_show_error("Potentially lost branch!"sv, true); } else result = increase_single_area(volumes, config, settings, layer_idx, parent, settings.increase_speed == slow_speed ? offset_slow : offset_fast, to_bp_data, to_model_data, inc_wo_collision, 0, mergelayer); @@ -2270,7 +2275,7 @@ static void create_layer_pathing(const TreeModelVolumes &volumes, const TreeSupp if (elem.areas.to_bp_areas.empty() && elem.areas.to_model_areas.empty()) { if (area(elem.areas.influence_areas) < tiny_area_threshold) { BOOST_LOG_TRIVIAL(error) << "Insert Error of Influence area bypass on layer " << layer_idx - 1; - tree_supports_show_error("Insert error of area after bypassing merge.\n", true); + tree_supports_show_error("Insert error of area after bypassing merge.\n"sv, true); } // Move the area to output. this_layer.emplace_back(elem.state, std::move(elem.parents), std::move(elem.areas.influence_areas)); @@ -2303,7 +2308,7 @@ static void create_layer_pathing(const TreeModelVolumes &volumes, const TreeSupp Polygons new_area = safe_union(elem.areas.influence_areas); if (area(new_area) < tiny_area_threshold) { BOOST_LOG_TRIVIAL(error) << "Insert Error of Influence area on layer " << layer_idx - 1 << ". Origin of " << elem.parents.size() << " areas. Was to bp " << elem.state.to_buildplate; - tree_supports_show_error("Insert error of area after merge.\n", true); + tree_supports_show_error("Insert error of area after merge.\n"sv, true); } this_layer.emplace_back(elem.state, std::move(elem.parents), std::move(new_area)); } @@ -2331,7 +2336,7 @@ static void set_points_on_areas(const SupportElement &elem, SupportElements *lay // Based on the branch center point of the current layer, the point on the next (further up) layer is calculated. if (! elem.state.result_on_layer_is_set()) { BOOST_LOG_TRIVIAL(error) << "Uninitialized support element"; - tree_supports_show_error("Uninitialized support element. A branch may be missing.\n", true); + tree_supports_show_error("Uninitialized support element. A branch may be missing.\n"sv, true); return; } @@ -2394,7 +2399,7 @@ static void set_to_model_contact_to_model_gracious( // Could not find valid placement, even though it should exist => error handling if (last_successfull_layer == nullptr) { BOOST_LOG_TRIVIAL(warning) << "No valid placement found for to model gracious element on layer " << first_elem.state.layer_idx; - tree_supports_show_error("Could not fine valid placement on model! Just placing it down anyway. Could cause floating branches.", true); + tree_supports_show_error("Could not fine valid placement on model! Just placing it down anyway. Could cause floating branches."sv, true); first_elem.state.to_model_gracious = false; set_to_model_contact_simple(first_elem); } else { @@ -2494,7 +2499,7 @@ static void create_nodes_from_area( if (elem.state.to_buildplate) { BOOST_LOG_TRIVIAL(error) << "Uninitialized Influence area targeting " << elem.state.target_position.x() << "," << elem.state.target_position.y() << ") " "at target_height: " << elem.state.target_height << " layer: " << layer_idx; - tree_supports_show_error("Uninitialized support element! A branch could be missing or exist partially.", true); + tree_supports_show_error("Uninitialized support element! A branch could be missing or exist partially."sv, true); } // we dont need to remove yet the parents as they will have a lower dtt and also no result_on_layer set elem.state.deleted = true; @@ -2537,6 +2542,7 @@ static void create_nodes_from_area( double radius_increase = config.getRadius(elem.state) - config.getRadius(parent.state); assert(radius_increase >= 0); double shift = (elem.state.result_on_layer - parent.state.result_on_layer).cast().norm(); + //FIXME this assert fails a lot. Is it correct? assert(shift < radius_increase + 2. * config.maximum_move_distance_slow); } } @@ -2561,6 +2567,7 @@ static void create_nodes_from_area( double radius_increase = config.getRadius(elem.state) - config.getRadius(parent.state); assert(radius_increase >= 0); double shift = (elem.state.result_on_layer - parent.state.result_on_layer).cast().norm(); + //FIXME this assert fails a lot. Is it correct? assert(shift < radius_increase + 2. * config.maximum_move_distance_slow); } } @@ -3382,6 +3389,306 @@ static void extrude_branch( } #endif +// #define TREE_SUPPORT_ORGANIC_NUDGE_NEW 1 + +#ifdef TREE_SUPPORT_ORGANIC_NUDGE_NEW +// New version using per layer AABB trees of lines for nudging spheres away from an object. +static void organic_smooth_branches_avoid_collisions( + const PrintObject &print_object, + const TreeModelVolumes &volumes, + const TreeSupportSettings &config, + std::vector &move_bounds, + const std::vector> &elements_with_link_down, + const std::vector &linear_data_layers) +{ + struct LayerCollisionCache { + coord_t min_element_radius{ std::numeric_limits::max() }; + bool min_element_radius_known() const { return this->min_element_radius != std::numeric_limits::max(); } + coord_t collision_radius{ 0 }; + std::vector lines; + AABBTreeIndirect::Tree<2, double> aabbtree_lines; + bool empty() const { return this->lines.empty(); } + }; + std::vector layer_collision_cache; + layer_collision_cache.reserve(1024); + const SlicingParameters &slicing_params = print_object.slicing_parameters(); + for (const std::pair& element : elements_with_link_down) { + LayerIndex layer_idx = element.first->state.layer_idx; + if (size_t num_layers = layer_idx + 1; num_layers > layer_collision_cache.size()) { + if (num_layers > layer_collision_cache.capacity()) + reserve_power_of_2(layer_collision_cache, num_layers); + layer_collision_cache.resize(num_layers, {}); + } + auto& l = layer_collision_cache[layer_idx]; + l.min_element_radius = std::min(l.min_element_radius, config.getRadius(element.first->state)); + } + for (LayerIndex layer_idx = 0; layer_idx < LayerIndex(layer_collision_cache.size()); ++layer_idx) + if (LayerCollisionCache& l = layer_collision_cache[layer_idx]; !l.min_element_radius_known()) + l.min_element_radius = 0; + else { + //FIXME + l.min_element_radius = 0; + std::optional>> res = volumes.get_collision_lower_bound_area(layer_idx, l.min_element_radius); + assert(res.has_value()); + l.collision_radius = res->first; + Lines alines = to_lines(res->second.get()); + l.lines.reserve(alines.size()); + for (const Line &line : alines) + l.lines.push_back({ unscaled(line.a), unscaled(line.b) }); + l.aabbtree_lines = AABBTreeLines::build_aabb_tree_over_indexed_lines(l.lines); + } + + struct CollisionSphere { + const SupportElement& element; + int element_below_id; + const bool locked; + float radius; + // Current position, when nudged away from the collision. + Vec3f position; + // Previous position, for Laplacian smoothing. + Vec3f prev_position; + // + Vec3f last_collision; + double last_collision_depth; + // Minimum Z for which the sphere collision will be evaluated. + // Limited by the minimum sloping angle and by the bottom of the tree. + float min_z{ -std::numeric_limits::max() }; + // Maximum Z for which the sphere collision will be evaluated. + // Limited by the minimum sloping angle and by the tip of the current branch. + float max_z{ std::numeric_limits::max() }; + uint32_t layer_begin; + uint32_t layer_end; + }; + + std::vector collision_spheres; + collision_spheres.reserve(elements_with_link_down.size()); + for (const std::pair &element_with_link : elements_with_link_down) { + const SupportElement &element = *element_with_link.first; + const int link_down = element_with_link.second; + collision_spheres.push_back({ + element, + link_down, + // locked + element.parents.empty() || (link_down == -1 && element.state.layer_idx > 0), + unscaled(config.getRadius(element.state)), + // 3D position + to_3d(unscaled(element.state.result_on_layer), float(layer_z(slicing_params, element.state.layer_idx))) + }); + // Update min_z coordinate to min_z of the tree below. + CollisionSphere &collision_sphere = collision_spheres.back(); + if (link_down != -1) { + const size_t offset_below = linear_data_layers[element.state.layer_idx - 1]; + collision_sphere.min_z = collision_spheres[offset_below + link_down].min_z; + } else + collision_sphere.min_z = collision_sphere.position.z(); + } + // Update max_z by propagating max_z from the tips of the branches. + for (int collision_sphere_id = int(collision_spheres.size()) - 1; collision_sphere_id >= 0; -- collision_sphere_id) { + CollisionSphere &collision_sphere = collision_spheres[collision_sphere_id]; + if (collision_sphere.element.parents.empty()) + // Tip + collision_sphere.max_z = collision_sphere.position.z(); + else { + // Below tip + const size_t offset_above = linear_data_layers[collision_sphere.element.state.layer_idx + 1]; + for (auto iparent : collision_sphere.element.parents) { + float parent_z = collision_spheres[offset_above + iparent].max_z; +// collision_sphere.max_z = collision_sphere.max_z == std::numeric_limits::max() ? parent_z : std::max(collision_sphere.max_z, parent_z); + collision_sphere.max_z = std::min(collision_sphere.max_z, parent_z); + } + } + } + // Update min_z / max_z to limit the search Z span of a given sphere for collision detection. + for (CollisionSphere &collision_sphere : collision_spheres) { + //FIXME limit the collision span by the tree slope. + collision_sphere.min_z = std::max(collision_sphere.min_z, collision_sphere.position.z() - collision_sphere.radius); + collision_sphere.max_z = std::min(collision_sphere.max_z, collision_sphere.position.z() + collision_sphere.radius); + collision_sphere.layer_begin = std::min(collision_sphere.element.state.layer_idx, layer_idx_ceil(slicing_params, collision_sphere.min_z)); + collision_sphere.layer_end = std::max(collision_sphere.element.state.layer_idx, layer_idx_floor(slicing_params, collision_sphere.max_z)) + 1; + } + + static constexpr const double collision_extra_gap = 0.1; + static constexpr const double max_nudge_collision_avoidance = 0.2; + static constexpr const double max_nudge_smoothing = 0.2; + static constexpr const size_t num_iter = 100; // 1000; + for (size_t iter = 0; iter < num_iter; ++ iter) { + // Back up prev position before Laplacian smoothing. + for (CollisionSphere &collision_sphere : collision_spheres) + collision_sphere.prev_position = collision_sphere.position; + std::atomic num_moved{ 0 }; + tbb::parallel_for(tbb::blocked_range(0, collision_spheres.size()), + [&collision_spheres, &layer_collision_cache, &slicing_params, &move_bounds, &linear_data_layers, &num_moved](const tbb::blocked_range range) { + for (size_t collision_sphere_id = range.begin(); collision_sphere_id < range.end(); ++ collision_sphere_id) + if (CollisionSphere &collision_sphere = collision_spheres[collision_sphere_id]; ! collision_sphere.locked) { + // Calculate collision of multiple 2D layers against a collision sphere. + collision_sphere.last_collision_depth = - std::numeric_limits::max(); + for (uint32_t layer_id = collision_sphere.layer_begin; layer_id != collision_sphere.layer_end; ++ layer_id) { + double dz = (layer_id - collision_sphere.element.state.layer_idx) * slicing_params.layer_height; + if (double r2 = sqr(collision_sphere.radius) - sqr(dz); r2 > 0) { + if (const LayerCollisionCache &layer_collision_cache_item = layer_collision_cache[layer_id]; ! layer_collision_cache.empty()) { + size_t hit_idx_out; + Vec2d hit_point_out; + double dist = sqrt(AABBTreeLines::squared_distance_to_indexed_lines( + layer_collision_cache_item.lines, layer_collision_cache_item.aabbtree_lines, Vec2d(to_2d(collision_sphere.position).cast()), + hit_idx_out, hit_point_out, r2)); + double collision_depth = sqrt(r2) - dist; + if (collision_depth > collision_sphere.last_collision_depth) { + collision_sphere.last_collision_depth = collision_depth; + collision_sphere.last_collision = to_3d(hit_point_out.cast(), float(layer_z(slicing_params, layer_id))); + } + } + } + } + if (collision_sphere.last_collision_depth > 0) { + // Collision detected to be removed. + // Nudge the circle center away from the collision. + if (collision_sphere.last_collision_depth > EPSILON) + // a little bit of hysteresis to detect end of + ++ num_moved; + // Shift by maximum 2mm. + double nudge_dist = std::min(std::max(0., collision_sphere.last_collision_depth + collision_extra_gap), max_nudge_collision_avoidance); + Vec2d nudge_vector = (to_2d(collision_sphere.position) - to_2d(collision_sphere.last_collision)).cast().normalized() * nudge_dist; + collision_sphere.position.head<2>() += (nudge_vector * nudge_dist).cast(); + } + // Laplacian smoothing + Vec2d avg{ 0, 0 }; + const SupportElements &above = move_bounds[collision_sphere.element.state.layer_idx + 1]; + const size_t offset_above = linear_data_layers[collision_sphere.element.state.layer_idx + 1]; + double weight = 0.; + for (auto iparent : collision_sphere.element.parents) { + double w = collision_sphere.radius; + avg += w * to_2d(collision_spheres[offset_above + iparent].prev_position.cast()); + weight += w; + } + if (collision_sphere.element_below_id != -1) { + const size_t offset_below = linear_data_layers[collision_sphere.element.state.layer_idx - 1]; + const double w = weight; // config.getRadius(move_bounds[element.state.layer_idx - 1][below].state); + avg += w * to_2d(collision_spheres[offset_below + collision_sphere.element_below_id].prev_position.cast()); + weight += w; + } + avg /= weight; + static constexpr const double smoothing_factor = 0.5; + Vec2d old_pos = to_2d(collision_sphere.position).cast(); + Vec2d new_pos = (1. - smoothing_factor) * old_pos + smoothing_factor * avg; + Vec2d shift = new_pos - old_pos; + double nudge_dist_max = shift.norm(); + // Shift by maximum 1mm, less than the collision avoidance factor. + double nudge_dist = std::min(std::max(0., nudge_dist_max), max_nudge_smoothing); + collision_sphere.position.head<2>() += (shift.normalized() * nudge_dist).cast(); + } + }); + // printf("iteration: %d, moved: %d\n", int(iter), int(num_moved)); + if (num_moved == 0) + break; + } + + for (size_t i = 0; i < collision_spheres.size(); ++ i) + elements_with_link_down[i].first->state.result_on_layer = scaled(to_2d(collision_spheres[i].position)); +} +#else // TREE_SUPPORT_ORGANIC_NUDGE_NEW +// Old version using OpenVDB, works but it is extremely slow for complex meshes. +static void organic_smooth_branches_avoid_collisions( + const PrintObject &print_object, + const TreeModelVolumes &volumes, + const TreeSupportSettings &config, + std::vector &move_bounds, + const std::vector> &elements_with_link_down, + const std::vector &linear_data_layers) +{ + TriangleMesh mesh = print_object.model_object()->raw_mesh(); + mesh.transform(print_object.trafo_centered()); + double scale = 10.; + openvdb::FloatGrid::Ptr grid = mesh_to_grid(mesh.its, openvdb::math::Transform{}, scale, 0., 0.); + std::unique_ptr> closest_surface_point = openvdb::tools::ClosestSurfacePoint::create(*grid); + std::vector pts, prev, projections; + std::vector distances; + for (const std::pair& element : elements_with_link_down) { + Vec3d pt = to_3d(unscaled(element.first->state.result_on_layer), layer_z(print_object.slicing_parameters(), element.first->state.layer_idx)) * scale; + pts.push_back({ pt.x(), pt.y(), pt.z() }); + } + + const double collision_extra_gap = 1. * scale; + const double max_nudge_collision_avoidance = 2. * scale; + const double max_nudge_smoothing = 1. * scale; + + static constexpr const size_t num_iter = 100; // 1000; + for (size_t iter = 0; iter < num_iter; ++ iter) { + prev = pts; + projections = pts; + distances.assign(pts.size(), std::numeric_limits::max()); + closest_surface_point->searchAndReplace(projections, distances); + size_t num_moved = 0; + for (size_t i = 0; i < projections.size(); ++ i) { + const SupportElement &element = *elements_with_link_down[i].first; + const int below = elements_with_link_down[i].second; + const bool locked = below == -1 && element.state.layer_idx > 0; + if (! locked && pts[i] != projections[i]) { + // Nudge the circle center away from the collision. + Vec3d v{ projections[i].x() - pts[i].x(), projections[i].y() - pts[i].y(), projections[i].z() - pts[i].z() }; + double depth = v.norm(); + assert(std::abs(distances[i] - depth) < EPSILON); + double radius = unscaled(config.getRadius(element.state)) * scale; + if (depth < radius) { + // Collision detected to be removed. + ++ num_moved; + double dxy = sqrt(sqr(radius) - sqr(v.z())); + double nudge_dist_max = dxy - std::hypot(v.x(), v.y()) + //FIXME 1mm gap + + collision_extra_gap; + // Shift by maximum 2mm. + double nudge_dist = std::min(std::max(0., nudge_dist_max), max_nudge_collision_avoidance); + Vec2d nudge_v = to_2d(v).normalized() * (- nudge_dist); + pts[i].x() += nudge_v.x(); + pts[i].y() += nudge_v.y(); + } + } + // Laplacian smoothing + if (! locked && ! element.parents.empty()) { + Vec2d avg{ 0, 0 }; + const SupportElements &above = move_bounds[element.state.layer_idx + 1]; + const size_t offset_above = linear_data_layers[element.state.layer_idx + 1]; + double weight = 0.; + for (auto iparent : element.parents) { + double w = config.getRadius(above[iparent].state); + avg.x() += w * prev[offset_above + iparent].x(); + avg.y() += w * prev[offset_above + iparent].y(); + weight += w; + } + size_t cnt = element.parents.size(); + if (below != -1) { + const size_t offset_below = linear_data_layers[element.state.layer_idx - 1]; + const double w = weight; // config.getRadius(move_bounds[element.state.layer_idx - 1][below].state); + avg.x() += w * prev[offset_below + below].x(); + avg.y() += w * prev[offset_below + below].y(); + ++ cnt; + weight += w; + } + //avg /= double(cnt); + avg /= weight; + static constexpr const double smoothing_factor = 0.5; + Vec2d old_pos{ pts[i].x(), pts[i].y() }; + Vec2d new_pos = (1. - smoothing_factor) * old_pos + smoothing_factor * avg; + Vec2d shift = new_pos - old_pos; + double nudge_dist_max = shift.norm(); + // Shift by maximum 1mm, less than the collision avoidance factor. + double nudge_dist = std::min(std::max(0., nudge_dist_max), max_nudge_smoothing); + Vec2d nudge_v = shift.normalized() * nudge_dist; + pts[i].x() += nudge_v.x(); + pts[i].y() += nudge_v.y(); + } + } +// printf("iteration: %d, moved: %d\n", int(iter), int(num_moved)); + if (num_moved == 0) + break; + } + + for (size_t i = 0; i < projections.size(); ++ i) { + elements_with_link_down[i].first->state.result_on_layer.x() = scaled(pts[i].x()) / scale; + elements_with_link_down[i].first->state.result_on_layer.y() = scaled(pts[i].y()) / scale; + } +} +#endif // TREE_SUPPORT_ORGANIC_NUDGE_NEW + static void draw_branches( PrintObject &print_object, const TreeModelVolumes &volumes, @@ -3396,8 +3703,6 @@ static void draw_branches( { static int irun = 0; - const SlicingParameters& slicing_params = print_object.slicing_parameters(); - // All SupportElements are put into a layer independent storage to improve parallelization. std::vector> elements_with_link_down; std::vector linear_data_layers; @@ -3436,102 +3741,7 @@ static void draw_branches( } } - std::unique_ptr> closest_surface_point; - { - TriangleMesh mesh = print_object.model_object()->raw_mesh(); - mesh.transform(print_object.trafo_centered()); - double scale = 10.; - openvdb::FloatGrid::Ptr grid = mesh_to_grid(mesh.its, {}, scale, 0., 0.); - closest_surface_point = openvdb::tools::ClosestSurfacePoint::create(*grid); - std::vector pts, prev, projections; - std::vector distances; - for (const std::pair &element : elements_with_link_down) { - Vec3d pt = to_3d(unscaled(element.first->state.result_on_layer), layer_z(slicing_params, element.first->state.layer_idx)) * scale; - pts.push_back({ pt.x(), pt.y(), pt.z() }); - } - - const double collision_extra_gap = 1. * scale; - const double max_nudge_collision_avoidance = 2. * scale; - const double max_nudge_smoothing = 1. * scale; - - static constexpr const size_t num_iter = 100; // 1000; - for (size_t iter = 0; iter < num_iter; ++ iter) { - prev = pts; - projections = pts; - distances.assign(pts.size(), std::numeric_limits::max()); - closest_surface_point->searchAndReplace(projections, distances); - size_t num_moved = 0; - for (size_t i = 0; i < projections.size(); ++ i) { - const SupportElement &element = *elements_with_link_down[i].first; - const int below = elements_with_link_down[i].second; - const bool locked = below == -1 && element.state.layer_idx > 0; - if (! locked && pts[i] != projections[i]) { - // Nudge the circle center away from the collision. - Vec3d v{ projections[i].x() - pts[i].x(), projections[i].y() - pts[i].y(), projections[i].z() - pts[i].z() }; - double depth = v.norm(); - assert(std::abs(distances[i] - depth) < EPSILON); - double radius = unscaled(config.getRadius(element.state)) * scale; - if (depth < radius) { - // Collision detected to be removed. - ++ num_moved; - double dxy = sqrt(sqr(radius) - sqr(v.z())); - double nudge_dist_max = dxy - std::hypot(v.x(), v.y()) - //FIXME 1mm gap - + collision_extra_gap; - // Shift by maximum 2mm. - double nudge_dist = std::min(std::max(0., nudge_dist_max), max_nudge_collision_avoidance); - Vec2d nudge_v = to_2d(v).normalized() * (- nudge_dist); - pts[i].x() += nudge_v.x(); - pts[i].y() += nudge_v.y(); - } - } - // Laplacian smoothing - if (! locked && ! element.parents.empty()) { - Vec2d avg{ 0, 0 }; - const SupportElements &above = move_bounds[element.state.layer_idx + 1]; - const size_t offset_above = linear_data_layers[element.state.layer_idx + 1]; - double weight = 0.; - for (auto iparent : element.parents) { - double w = config.getRadius(above[iparent].state); - avg.x() += w * prev[offset_above + iparent].x(); - avg.y() += w * prev[offset_above + iparent].y(); - weight += w; - } - size_t cnt = element.parents.size(); - if (below != -1) { - const size_t offset_below = linear_data_layers[element.state.layer_idx - 1]; - const double w = weight; // config.getRadius(move_bounds[element.state.layer_idx - 1][below].state); - avg.x() += w * prev[offset_below + below].x(); - avg.y() += w * prev[offset_below + below].y(); - ++ cnt; - weight += w; - } - //avg /= double(cnt); - avg /= weight; - static constexpr const double smoothing_factor = 0.5; - Vec2d old_pos{ pts[i].x(), pts[i].y() }; - Vec2d new_pos = (1. - smoothing_factor) * old_pos + smoothing_factor * avg; - Vec2d shift = new_pos - old_pos; - double nudge_dist_max = shift.norm(); - // Shift by maximum 1mm, less than the collision avoidance factor. - double nudge_dist = std::min(std::max(0., nudge_dist_max), max_nudge_smoothing); - Vec2d nudge_v = shift.normalized() * nudge_dist; - pts[i].x() += nudge_v.x(); - pts[i].y() += nudge_v.y(); - } - } - printf("iteration: %d, moved: %d\n", int(iter), int(num_moved)); - if (num_moved == 0) - break; - } - -#if 1 - for (size_t i = 0; i < projections.size(); ++ i) { - elements_with_link_down[i].first->state.result_on_layer.x() = scaled(pts[i].x()) / scale; - elements_with_link_down[i].first->state.result_on_layer.y() = scaled(pts[i].y()) / scale; - } -#endif - } + organic_smooth_branches_avoid_collisions(print_object, volumes, config, move_bounds, elements_with_link_down, linear_data_layers); std::vector support_layer_storage(move_bounds.size()); std::vector support_roof_storage(move_bounds.size()); @@ -3543,6 +3753,7 @@ static void draw_branches( // Traverse all nodes, generate tubes. // Traversal stack with nodes and thier current parent + const SlicingParameters &slicing_params = print_object.slicing_parameters(); std::vector path; indexed_triangle_set cummulative_mesh; indexed_triangle_set partial_mesh; diff --git a/src/libslic3r/TreeSupport.hpp b/src/libslic3r/TreeSupport.hpp index 0d5e967d9..b0ab46891 100644 --- a/src/libslic3r/TreeSupport.hpp +++ b/src/libslic3r/TreeSupport.hpp @@ -17,7 +17,7 @@ #include "BoundingBox.hpp" #include "Utils.hpp" - #define TREE_SUPPORT_SHOW_ERRORS +// #define TREE_SUPPORT_SHOW_ERRORS #ifdef SLIC3R_TREESUPPORTS_PROGRESS // The various stages of the process can be weighted differently in the progress bar. @@ -576,8 +576,7 @@ public: } }; -// todo Remove! ONLY FOR PUBLIC BETA!! -void tree_supports_show_error(std::string message, bool critical); +void tree_supports_show_error(std::string_view message, bool critical); } // namespace FFFTreeSupport diff --git a/src/libslic3r/TriangleMesh.hpp b/src/libslic3r/TriangleMesh.hpp index 26701bdf5..6473f7783 100644 --- a/src/libslic3r/TriangleMesh.hpp +++ b/src/libslic3r/TriangleMesh.hpp @@ -269,12 +269,18 @@ inline int its_triangle_edge_index(const stl_triangle_vertex_indices &triangle_i using its_triangle = std::array; +inline its_triangle its_triangle_vertices(const indexed_triangle_set &its, + const Vec3i &face) +{ + return {its.vertices[face(0)], + its.vertices[face(1)], + its.vertices[face(2)]}; +} + inline its_triangle its_triangle_vertices(const indexed_triangle_set &its, size_t face_id) { - return {its.vertices[its.indices[face_id](0)], - its.vertices[its.indices[face_id](1)], - its.vertices[its.indices[face_id](2)]}; + return its_triangle_vertices(its, its.indices[face_id]); } inline stl_normal its_unnormalized_normal(const indexed_triangle_set &its, @@ -346,6 +352,22 @@ inline BoundingBoxf3 bounding_box(const indexed_triangle_set& its) return {bmin.cast(), bmax.cast()}; } +inline BoundingBoxf3 bounding_box(const indexed_triangle_set& its, const Transform3f &tr) +{ + if (its.vertices.empty()) + return {}; + + Vec3f bmin = tr * its.vertices.front(), bmax = tr * its.vertices.front(); + + for (const Vec3f &p : its.vertices) { + Vec3f pp = tr * p; + bmin = pp.cwiseMin(bmin); + bmax = pp.cwiseMax(bmax); + } + + return {bmin.cast(), bmax.cast()}; +} + } // Serialization through the Cereal library diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index 00de10fc4..815d6ff55 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -176,6 +176,21 @@ template size_t next_highest_power_of_2(T v, return next_highest_power_of_2(uint32_t(v)); } +template void reserve_power_of_2(VectorType &vector, size_t n) +{ + vector.reserve(next_highest_power_of_2(n)); +} + +template void reserve_more(VectorType &vector, size_t n) +{ + vector.reserve(vector.size() + n); +} + +template void reserve_more_power_of_2(VectorType &vector, size_t n) +{ + vector.reserve(next_highest_power_of_2(vector.size() + n)); +} + template inline INDEX_TYPE prev_idx_modulo(INDEX_TYPE idx, const INDEX_TYPE count) { diff --git a/src/libslic3r/libslic3r.h b/src/libslic3r/libslic3r.h index d5a21cf21..2b96994fa 100644 --- a/src/libslic3r/libslic3r.h +++ b/src/libslic3r/libslic3r.h @@ -351,10 +351,15 @@ public: Range(It b, It e) : from(std::move(b)), to(std::move(e)) {} // Some useful container-like methods... - inline size_t size() const { return end() - begin(); } - inline bool empty() const { return size() == 0; } + inline size_t size() const { return std::distance(from, to); } + inline bool empty() const { return from == to; } }; +template auto range(Cont &&cont) +{ + return Range{std::begin(cont), std::end(cont)}; +} + template> constexpr T NaN = std::numeric_limits::quiet_NaN(); @@ -380,6 +385,8 @@ inline IntegerOnly fast_round_up(double a) return a == 0.49999999999999994 ? I(0) : I(floor(a + 0.5)); } +template using SamePair = std::pair; + } // namespace Slic3r #endif // _libslic3r_h_ diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 236aedf66..453d7eeb5 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -39,6 +39,8 @@ set(SLIC3R_GUI_SOURCES GUI/Gizmos/GLGizmosCommon.hpp GUI/Gizmos/GLGizmoBase.cpp GUI/Gizmos/GLGizmoBase.hpp + GUI/Gizmos/GLGizmoSlaBase.cpp + GUI/Gizmos/GLGizmoSlaBase.hpp GUI/Gizmos/GLGizmoEmboss.cpp GUI/Gizmos/GLGizmoEmboss.hpp GUI/Gizmos/GLGizmoMove.cpp diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index d83890a40..185ebb743 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -476,51 +476,6 @@ int GLVolumeCollection::load_object_volume( return int(this->volumes.size() - 1); } -// Load SLA auxiliary GLVolumes (for support trees or pad). -// This function produces volumes for multiple instances in a single shot, -// as some object specific mesh conversions may be expensive. -void GLVolumeCollection::load_object_auxiliary( - const SLAPrintObject* print_object, - int obj_idx, - // pairs of - const std::vector>& instances, - SLAPrintObjectStep milestone, - // Timestamp of the last change of the milestone - size_t timestamp) -{ - assert(print_object->is_step_done(milestone)); - Transform3d mesh_trafo_inv = print_object->trafo().inverse(); - // Get the support mesh. - TriangleMesh mesh = print_object->get_mesh(milestone); - mesh.transform(mesh_trafo_inv); - // Convex hull is required for out of print bed detection. - TriangleMesh convex_hull = mesh.convex_hull_3d(); - for (const std::pair& instance_idx : instances) { - const ModelInstance& model_instance = *print_object->model_object()->instances[instance_idx.first]; - this->volumes.emplace_back(new GLVolume((milestone == slaposPad) ? GLVolume::SLA_PAD_COLOR : GLVolume::SLA_SUPPORT_COLOR)); - GLVolume& v = *this->volumes.back(); -#if ENABLE_SMOOTH_NORMALS - v.model.init_from(mesh, true); -#else - v.model.init_from(mesh); - v.model.set_color((milestone == slaposPad) ? GLVolume::SLA_PAD_COLOR : GLVolume::SLA_SUPPORT_COLOR); - v.mesh_raycaster = std::make_unique(std::make_shared(mesh)); -#endif // ENABLE_SMOOTH_NORMALS - v.composite_id = GLVolume::CompositeID(obj_idx, -int(milestone), (int)instance_idx.first); - v.geometry_id = std::pair(timestamp, model_instance.id().id); - // Create a copy of the convex hull mesh for each instance. Use a move operator on the last instance. - if (&instance_idx == &instances.back()) - v.set_convex_hull(std::move(convex_hull)); - else - v.set_convex_hull(convex_hull); - v.is_modifier = false; - v.shader_outside_printer_detection_enabled = (milestone == slaposSupportTree); - v.set_instance_transformation(model_instance.get_transformation()); - // Leave the volume transformation at identity. - // v.set_volume_transformation(model_volume->get_transformation()); - } -} - #if ENABLE_OPENGL_ES int GLVolumeCollection::load_wipe_tower_preview( float pos_x, float pos_y, float width, float depth, float height, diff --git a/src/slic3r/GUI/3DScene.hpp b/src/slic3r/GUI/3DScene.hpp index 674c3ce82..4c19927ef 100644 --- a/src/slic3r/GUI/3DScene.hpp +++ b/src/slic3r/GUI/3DScene.hpp @@ -181,7 +181,7 @@ public: bool force_neutral_color : 1; // Whether or not to force rendering of sinking contours bool force_sinking_contours : 1; - }; + }; // this gets instantiated automatically in the parent struct // Is mouse or rectangle selection over this object to select/deselect it ? EHoverState hover; @@ -416,16 +416,6 @@ public: int volume_idx, int instance_idx); - // Load SLA auxiliary GLVolumes (for support trees or pad). - void load_object_auxiliary( - const SLAPrintObject* print_object, - int obj_idx, - // pairs of - const std::vector>& instances, - SLAPrintObjectStep milestone, - // Timestamp of the last change of the milestone - size_t timestamp); - #if ENABLE_OPENGL_ES int load_wipe_tower_preview( float pos_x, float pos_y, float width, float depth, float height, float rotation_angle, bool size_unknown, float brim_width, TriangleMesh* out_mesh = nullptr); diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 7f8e70e6c..d442a8646 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -374,22 +374,43 @@ void ConfigManipulation::toggle_print_sla_options(DynamicPrintConfig* config) bool is_default_tree = treetype == sla::SupportTreeType::Default; bool is_branching_tree = treetype == sla::SupportTreeType::Branching; - toggle_field("support_head_front_diameter", supports_en); - toggle_field("support_head_penetration", supports_en); - toggle_field("support_head_width", supports_en); - toggle_field("support_pillar_diameter", supports_en); - toggle_field("support_small_pillar_diameter_percent", supports_en); + toggle_field("support_tree_type", supports_en); + + toggle_field("support_head_front_diameter", supports_en && is_default_tree); + toggle_field("support_head_penetration", supports_en && is_default_tree); + toggle_field("support_head_width", supports_en && is_default_tree); + toggle_field("support_pillar_diameter", supports_en && is_default_tree); + toggle_field("support_small_pillar_diameter_percent", supports_en && is_default_tree); toggle_field("support_max_bridges_on_pillar", supports_en && is_default_tree); toggle_field("support_pillar_connection_mode", supports_en && is_default_tree); - toggle_field("support_tree_type", supports_en); - toggle_field("support_buildplate_only", supports_en); - toggle_field("support_base_diameter", supports_en); - toggle_field("support_base_height", supports_en); - toggle_field("support_base_safety_distance", supports_en); - toggle_field("support_critical_angle", supports_en); - toggle_field("support_max_bridge_length", supports_en); + toggle_field("support_buildplate_only", supports_en && is_default_tree); + toggle_field("support_base_diameter", supports_en && is_default_tree); + toggle_field("support_base_height", supports_en && is_default_tree); + toggle_field("support_base_safety_distance", supports_en && is_default_tree); + toggle_field("support_critical_angle", supports_en && is_default_tree); + toggle_field("support_max_bridge_length", supports_en && is_default_tree); + toggle_field("support_enforcers_only", supports_en); toggle_field("support_max_pillar_link_distance", supports_en && is_default_tree); - toggle_field("support_pillar_widening_factor", supports_en && is_branching_tree); + toggle_field("support_pillar_widening_factor", false); + toggle_field("support_max_weight_on_model", false); + + toggle_field("branchingsupport_head_front_diameter", supports_en && is_branching_tree); + toggle_field("branchingsupport_head_penetration", supports_en && is_branching_tree); + toggle_field("branchingsupport_head_width", supports_en && is_branching_tree); + toggle_field("branchingsupport_pillar_diameter", supports_en && is_branching_tree); + toggle_field("branchingsupport_small_pillar_diameter_percent", supports_en && is_branching_tree); + toggle_field("branchingsupport_max_bridges_on_pillar", false); + toggle_field("branchingsupport_pillar_connection_mode", false); + toggle_field("branchingsupport_buildplate_only", supports_en && is_branching_tree); + toggle_field("branchingsupport_base_diameter", supports_en && is_branching_tree); + toggle_field("branchingsupport_base_height", supports_en && is_branching_tree); + toggle_field("branchingsupport_base_safety_distance", supports_en && is_branching_tree); + toggle_field("branchingsupport_critical_angle", supports_en && is_branching_tree); + toggle_field("branchingsupport_max_bridge_length", supports_en && is_branching_tree); + toggle_field("branchingsupport_max_pillar_link_distance", false); + toggle_field("branchingsupport_pillar_widening_factor", supports_en && is_branching_tree); + toggle_field("branchingsupport_max_weight_on_model", supports_en && is_branching_tree); + toggle_field("support_points_density_relative", supports_en); toggle_field("support_points_minimal_distance", supports_en); @@ -406,7 +427,8 @@ void ConfigManipulation::toggle_print_sla_options(DynamicPrintConfig* config) bool zero_elev = config->opt_bool("pad_around_object") && pad_en; - toggle_field("support_object_elevation", supports_en && !zero_elev); + toggle_field("support_object_elevation", supports_en && is_default_tree && !zero_elev); + toggle_field("branchingsupport_object_elevation", supports_en && is_branching_tree && !zero_elev); toggle_field("pad_object_gap", zero_elev); toggle_field("pad_around_object_everywhere", zero_elev); toggle_field("pad_object_connector_stride", zero_elev); diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index ef6f53bf0..224c0e30b 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -1,3360 +1,3512 @@ -// FIXME: extract absolute units -> em - -#include "ConfigWizard_private.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef WIN32 -#include -#include -#include -#endif // WIN32 - -#ifdef _MSW_DARK_MODE -#include -#endif // _MSW_DARK_MODE - -#include "libslic3r/Platform.hpp" -#include "libslic3r/Utils.hpp" -#include "libslic3r/Config.hpp" -#include "libslic3r/libslic3r.h" -#include "libslic3r/Model.hpp" -#include "libslic3r/Color.hpp" -#include "GUI.hpp" -#include "GUI_App.hpp" -#include "GUI_Utils.hpp" -#include "GUI_ObjectManipulation.hpp" -#include "Field.hpp" -#include "DesktopIntegrationDialog.hpp" -#include "slic3r/Config/Snapshot.hpp" -#include "slic3r/Utils/PresetUpdater.hpp" -#include "format.hpp" -#include "MsgDialog.hpp" -#include "UnsavedChangesDialog.hpp" -#include "slic3r/Utils/AppUpdater.hpp" - -#if defined(__linux__) && defined(__WXGTK3__) -#define wxLinux_gtk3 true -#else -#define wxLinux_gtk3 false -#endif //defined(__linux__) && defined(__WXGTK3__) - -namespace Slic3r { -namespace GUI { - - -using Config::Snapshot; -using Config::SnapshotDB; - - -// Configuration data structures extensions needed for the wizard - -bool Bundle::load(fs::path source_path, bool ais_in_resources, bool ais_prusa_bundle) -{ - this->preset_bundle = std::make_unique(); - this->is_in_resources = ais_in_resources; - this->is_prusa_bundle = ais_prusa_bundle; - - std::string path_string = source_path.string(); - // Throw when parsing invalid configuration. Only valid configuration is supposed to be provided over the air. - auto [config_substitutions, presets_loaded] = preset_bundle->load_configbundle( - path_string, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable); - UNUSED(config_substitutions); - // No substitutions shall be reported when loading a system config bundle, no substitutions are allowed. - assert(config_substitutions.empty()); - auto first_vendor = preset_bundle->vendors.begin(); - if (first_vendor == preset_bundle->vendors.end()) { - BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % path_string; - return false; - } - if (presets_loaded == 0) { - BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No profile loaded.") % path_string; - return false; - } - - BOOST_LOG_TRIVIAL(trace) << boost::format("Vendor bundle: `%1%`: %2% profiles loaded.") % path_string % presets_loaded; - this->vendor_profile = &first_vendor->second; - return true; -} - -Bundle::Bundle(Bundle &&other) - : preset_bundle(std::move(other.preset_bundle)) - , vendor_profile(other.vendor_profile) - , is_in_resources(other.is_in_resources) - , is_prusa_bundle(other.is_prusa_bundle) -{ - other.vendor_profile = nullptr; -} - -BundleMap BundleMap::load() -{ - BundleMap res; - - const auto vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / "vendor").make_preferred(); - const auto rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); - - auto prusa_bundle_path = (vendor_dir / PresetBundle::PRUSA_BUNDLE).replace_extension(".ini"); - auto prusa_bundle_rsrc = false; - if (! boost::filesystem::exists(prusa_bundle_path)) { - prusa_bundle_path = (rsrc_vendor_dir / PresetBundle::PRUSA_BUNDLE).replace_extension(".ini"); - prusa_bundle_rsrc = true; - } - { - Bundle prusa_bundle; - if (prusa_bundle.load(std::move(prusa_bundle_path), prusa_bundle_rsrc, true)) - res.emplace(PresetBundle::PRUSA_BUNDLE, std::move(prusa_bundle)); - } - - // Load the other bundles in the datadir/vendor directory - // and then additionally from resources/profiles. - bool is_in_resources = false; - for (auto dir : { &vendor_dir, &rsrc_vendor_dir }) { - for (const auto &dir_entry : boost::filesystem::directory_iterator(*dir)) { - if (Slic3r::is_ini_file(dir_entry)) { - std::string id = dir_entry.path().stem().string(); // stem() = filename() without the trailing ".ini" part - - // Don't load this bundle if we've already loaded it. - if (res.find(id) != res.end()) { continue; } - - Bundle bundle; - if (bundle.load(dir_entry.path(), is_in_resources)) - res.emplace(std::move(id), std::move(bundle)); - } - } - - is_in_resources = true; - } - - return res; -} - -Bundle& BundleMap::prusa_bundle() -{ - auto it = find(PresetBundle::PRUSA_BUNDLE); - if (it == end()) { - throw Slic3r::RuntimeError("ConfigWizard: Internal error in BundleMap: PRUSA_BUNDLE not loaded"); - } - - return it->second; -} - -const Bundle& BundleMap::prusa_bundle() const -{ - return const_cast(this)->prusa_bundle(); -} - - -// Printer model picker GUI control - -struct PrinterPickerEvent : public wxEvent -{ - std::string vendor_id; - std::string model_id; - std::string variant_name; - bool enable; - - PrinterPickerEvent(wxEventType eventType, int winid, std::string vendor_id, std::string model_id, std::string variant_name, bool enable) - : wxEvent(winid, eventType) - , vendor_id(std::move(vendor_id)) - , model_id(std::move(model_id)) - , variant_name(std::move(variant_name)) - , enable(enable) - {} - - virtual wxEvent *Clone() const - { - return new PrinterPickerEvent(*this); - } -}; - -wxDEFINE_EVENT(EVT_PRINTER_PICK, PrinterPickerEvent); - -const std::string PrinterPicker::PRINTER_PLACEHOLDER = "printer_placeholder.png"; - -PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxString title, size_t max_cols, const AppConfig &appconfig, const ModelFilter &filter) - : wxPanel(parent) - , vendor_id(vendor.id) - , width(0) -{ - wxGetApp().UpdateDarkUI(this); - const auto &models = vendor.models; - - auto *sizer = new wxBoxSizer(wxVERTICAL); - - const auto font_title = GetFont().MakeBold().Scaled(1.3f); - const auto font_name = GetFont().MakeBold(); - const auto font_alt_nozzle = GetFont().Scaled(0.9f); - - // wxGrid appends widgets by rows, but we need to construct them in columns. - // These vectors are used to hold the elements so that they can be appended in the right order. - std::vector titles; - std::vector bitmaps; - std::vector variants_panels; - - int max_row_width = 0; - int current_row_width = 0; - - bool is_variants = false; - - for (const auto &model : models) { - if (! filter(model)) { continue; } - - wxBitmap bitmap; - int bitmap_width = 0; - auto load_bitmap = [](const wxString& bitmap_file, wxBitmap& bitmap, int& bitmap_width)->bool { - if (wxFileExists(bitmap_file)) { - bitmap.LoadFile(bitmap_file, wxBITMAP_TYPE_PNG); - bitmap_width = bitmap.GetWidth(); - return true; - } - return false; - }; - if (!load_bitmap(GUI::from_u8(Slic3r::data_dir() + "/vendor/" + vendor.id + "/" + model.id + "_thumbnail.png"), bitmap, bitmap_width)) { - if (!load_bitmap(GUI::from_u8(Slic3r::resources_dir() + "/profiles/" + vendor.id + "/" + model.id + "_thumbnail.png"), bitmap, bitmap_width)) { - BOOST_LOG_TRIVIAL(warning) << boost::format("Can't find bitmap file `%1%` for vendor `%2%`, printer `%3%`, using placeholder icon instead") - % (Slic3r::resources_dir() + "/profiles/" + vendor.id + "/" + model.id + "_thumbnail.png") - % vendor.id - % model.id; - load_bitmap(Slic3r::var(PRINTER_PLACEHOLDER), bitmap, bitmap_width); - } - } - auto *title = new wxStaticText(this, wxID_ANY, from_u8(model.name), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); - title->SetFont(font_name); - const int wrap_width = std::max((int)MODEL_MIN_WRAP, bitmap_width); - title->Wrap(wrap_width); - - current_row_width += wrap_width; - if (titles.size() % max_cols == max_cols - 1) { - max_row_width = std::max(max_row_width, current_row_width); - current_row_width = 0; - } - - titles.push_back(title); - - auto *bitmap_widget = new wxStaticBitmap(this, wxID_ANY, bitmap); - bitmaps.push_back(bitmap_widget); - - auto *variants_panel = new wxPanel(this); - wxGetApp().UpdateDarkUI(variants_panel); - auto *variants_sizer = new wxBoxSizer(wxVERTICAL); - variants_panel->SetSizer(variants_sizer); - const auto model_id = model.id; - - for (size_t i = 0; i < model.variants.size(); i++) { - const auto &variant = model.variants[i]; - - const auto label = model.technology == ptFFF - ? from_u8((boost::format("%1% %2% %3%") % variant.name % _utf8(L("mm")) % _utf8(L("nozzle"))).str()) - : from_u8(model.name); - - if (i == 1) { - auto *alt_label = new wxStaticText(variants_panel, wxID_ANY, _L("Alternate nozzles:")); - alt_label->SetFont(font_alt_nozzle); - variants_sizer->Add(alt_label, 0, wxBOTTOM, 3); - is_variants = true; - } - - auto *cbox = new Checkbox(variants_panel, label, model_id, variant.name); - i == 0 ? cboxes.push_back(cbox) : cboxes_alt.push_back(cbox); - - const bool enabled = appconfig.get_variant(vendor.id, model_id, variant.name); - cbox->SetValue(enabled); - - variants_sizer->Add(cbox, 0, wxBOTTOM, 3); - - cbox->Bind(wxEVT_CHECKBOX, [this, cbox](wxCommandEvent &event) { - on_checkbox(cbox, event.IsChecked()); - }); - } - - variants_panels.push_back(variants_panel); - } - - width = std::max(max_row_width, current_row_width); - - const size_t cols = std::min(max_cols, titles.size()); - - auto *printer_grid = new wxFlexGridSizer(cols, 0, 20); - printer_grid->SetFlexibleDirection(wxVERTICAL | wxHORIZONTAL); - - if (titles.size() > 0) { - const size_t odd_items = titles.size() % cols; - - for (size_t i = 0; i < titles.size() - odd_items; i += cols) { - for (size_t j = i; j < i + cols; j++) { printer_grid->Add(bitmaps[j], 0, wxBOTTOM, 20); } - for (size_t j = i; j < i + cols; j++) { printer_grid->Add(titles[j], 0, wxBOTTOM, 3); } - for (size_t j = i; j < i + cols; j++) { printer_grid->Add(variants_panels[j]); } - - // Add separator space to multiliners - if (titles.size() > cols) { - for (size_t j = i; j < i + cols; j++) { printer_grid->Add(1, 30); } - } - } - if (odd_items > 0) { - const size_t rem = titles.size() - odd_items; - - for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(bitmaps[i], 0, wxBOTTOM, 20); } - for (size_t i = 0; i < cols - odd_items; i++) { printer_grid->AddSpacer(1); } - for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(titles[i], 0, wxBOTTOM, 3); } - for (size_t i = 0; i < cols - odd_items; i++) { printer_grid->AddSpacer(1); } - for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(variants_panels[i]); } - } - } - - auto *title_sizer = new wxBoxSizer(wxHORIZONTAL); - if (! title.IsEmpty()) { - auto *title_widget = new wxStaticText(this, wxID_ANY, title); - title_widget->SetFont(font_title); - title_sizer->Add(title_widget); - } - title_sizer->AddStretchSpacer(); - - if (titles.size() > 1 || is_variants) { - // It only makes sense to add the All / None buttons if there's multiple printers - // All Standard button is added when there are more variants for at least one printer - auto *sel_all_std = new wxButton(this, wxID_ANY, titles.size() > 1 ? _L("All standard") : _L("Standard")); - auto *sel_all = new wxButton(this, wxID_ANY, _L("All")); - auto *sel_none = new wxButton(this, wxID_ANY, _L("None")); - if (is_variants) - sel_all_std->Bind(wxEVT_BUTTON, [this](const wxCommandEvent& event) { this->select_all(true, false); }); - sel_all->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->select_all(true, true); }); - sel_none->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->select_all(false); }); - if (is_variants) - title_sizer->Add(sel_all_std, 0, wxRIGHT, BTN_SPACING); - title_sizer->Add(sel_all, 0, wxRIGHT, BTN_SPACING); - title_sizer->Add(sel_none); - - wxGetApp().UpdateDarkUI(sel_all_std); - wxGetApp().UpdateDarkUI(sel_all); - wxGetApp().UpdateDarkUI(sel_none); - - // fill button indexes used later for buttons rescaling - if (is_variants) - m_button_indexes = { sel_all_std->GetId(), sel_all->GetId(), sel_none->GetId() }; - else { - sel_all_std->Destroy(); - m_button_indexes = { sel_all->GetId(), sel_none->GetId() }; - } - } - - sizer->Add(title_sizer, 0, wxEXPAND | wxBOTTOM, BTN_SPACING); - sizer->Add(printer_grid); - - SetSizer(sizer); -} - -PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxString title, size_t max_cols, const AppConfig &appconfig) - : PrinterPicker(parent, vendor, std::move(title), max_cols, appconfig, [](const VendorProfile::PrinterModel&) { return true; }) -{} - -void PrinterPicker::select_all(bool select, bool alternates) -{ - for (const auto &cb : cboxes) { - if (cb->GetValue() != select) { - cb->SetValue(select); - on_checkbox(cb, select); - } - } - - if (! select) { alternates = false; } - - for (const auto &cb : cboxes_alt) { - if (cb->GetValue() != alternates) { - cb->SetValue(alternates); - on_checkbox(cb, alternates); - } - } -} - -void PrinterPicker::select_one(size_t i, bool select) -{ - if (i < cboxes.size() && cboxes[i]->GetValue() != select) { - cboxes[i]->SetValue(select); - on_checkbox(cboxes[i], select); - } -} - -bool PrinterPicker::any_selected() const -{ - for (const auto &cb : cboxes) { - if (cb->GetValue()) { return true; } - } - - for (const auto &cb : cboxes_alt) { - if (cb->GetValue()) { return true; } - } - - return false; -} - -std::set PrinterPicker::get_selected_models() const -{ - std::set ret_set; - - for (const auto& cb : cboxes) - if (cb->GetValue()) - ret_set.emplace(cb->model); - - for (const auto& cb : cboxes_alt) - if (cb->GetValue()) - ret_set.emplace(cb->model); - - return ret_set; -} - -void PrinterPicker::on_checkbox(const Checkbox *cbox, bool checked) -{ - PrinterPickerEvent evt(EVT_PRINTER_PICK, GetId(), vendor_id, cbox->model, cbox->variant, checked); - AddPendingEvent(evt); -} - - -// Wizard page base - -ConfigWizardPage::ConfigWizardPage(ConfigWizard *parent, wxString title, wxString shortname, unsigned indent) - : wxPanel(parent->p->hscroll) - , parent(parent) - , shortname(std::move(shortname)) - , indent(indent) -{ - wxGetApp().UpdateDarkUI(this); - - auto *sizer = new wxBoxSizer(wxVERTICAL); - - auto *text = new wxStaticText(this, wxID_ANY, std::move(title), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); - const auto font = GetFont().MakeBold().Scaled(1.5); - text->SetFont(font); - sizer->Add(text, 0, wxALIGN_LEFT, 0); - sizer->AddSpacer(10); - - content = new wxBoxSizer(wxVERTICAL); - sizer->Add(content, 1, wxEXPAND); - - SetSizer(sizer); - - // There is strange layout on Linux with GTK3, - // see https://github.com/prusa3d/PrusaSlicer/issues/5103 and https://github.com/prusa3d/PrusaSlicer/issues/4861 - // So, non-active pages will be hidden later, on wxEVT_SHOW, after completed Layout() for all pages - if (!wxLinux_gtk3) - this->Hide(); - - Bind(wxEVT_SIZE, [this](wxSizeEvent &event) { - this->Layout(); - event.Skip(); - }); -} - -ConfigWizardPage::~ConfigWizardPage() {} - -wxStaticText* ConfigWizardPage::append_text(wxString text) -{ - auto *widget = new wxStaticText(this, wxID_ANY, text, wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); - widget->Wrap(WRAP_WIDTH); - widget->SetMinSize(wxSize(WRAP_WIDTH, -1)); - append(widget); - return widget; -} - -void ConfigWizardPage::append_spacer(int space) -{ - // FIXME: scaling - content->AddSpacer(space); -} - -// Wizard pages - -PageWelcome::PageWelcome(ConfigWizard *parent) - : ConfigWizardPage(parent, from_u8((boost::format( -#ifdef __APPLE__ - _utf8(L("Welcome to the %s Configuration Assistant")) -#else - _utf8(L("Welcome to the %s Configuration Wizard")) -#endif - ) % SLIC3R_APP_NAME).str()), _L("Welcome")) - , welcome_text(append_text(from_u8((boost::format( - _utf8(L("Hello, welcome to %s! This %s helps you with the initial configuration; just a few settings and you will be ready to print."))) - % SLIC3R_APP_NAME - % _utf8(ConfigWizard::name())).str()) - )) - , cbox_reset(append( - new wxCheckBox(this, wxID_ANY, _L("Remove user profiles (a snapshot will be taken beforehand)")) - )) - , cbox_integrate(append( - new wxCheckBox(this, wxID_ANY, _L("Perform desktop integration (Sets this binary to be searchable by the system).")) - )) -{ - welcome_text->Hide(); - cbox_reset->Hide(); - cbox_integrate->Hide(); -} - -void PageWelcome::set_run_reason(ConfigWizard::RunReason run_reason) -{ - const bool data_empty = run_reason == ConfigWizard::RR_DATA_EMPTY; - welcome_text->Show(data_empty); - cbox_reset->Show(!data_empty); -#if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) - if (!DesktopIntegrationDialog::is_integrated()) - cbox_integrate->Show(true); - else - cbox_integrate->Hide(); -#else - cbox_integrate->Hide(); -#endif -} - - -PagePrinters::PagePrinters(ConfigWizard *parent, - wxString title, - wxString shortname, - const VendorProfile &vendor, - unsigned indent, - Technology technology) - : ConfigWizardPage(parent, std::move(title), std::move(shortname), indent) - , technology(technology) - , install(false) // only used for 3rd party vendors -{ - enum { - COL_SIZE = 200, - }; - - AppConfig *appconfig = &this->wizard_p()->appconfig_new; - - const auto families = vendor.families(); - for (const auto &family : families) { - const auto filter = [&](const VendorProfile::PrinterModel &model) { - return ((model.technology == ptFFF && technology & T_FFF) - || (model.technology == ptSLA && technology & T_SLA)) - && model.family == family; - }; - - if (std::find_if(vendor.models.begin(), vendor.models.end(), filter) == vendor.models.end()) { - continue; - } - - const auto picker_title = family.empty() ? wxString() : from_u8((boost::format(_utf8(L("%s Family"))) % family).str()); - auto *picker = new PrinterPicker(this, vendor, picker_title, MAX_COLS, *appconfig, filter); - - picker->Bind(EVT_PRINTER_PICK, [this, appconfig](const PrinterPickerEvent &evt) { - appconfig->set_variant(evt.vendor_id, evt.model_id, evt.variant_name, evt.enable); - wizard_p()->on_printer_pick(this, evt); - }); - - append(new StaticLine(this)); - - append(picker); - printer_pickers.push_back(picker); - has_printers = true; - } - -} - -void PagePrinters::select_all(bool select, bool alternates) -{ - for (auto picker : printer_pickers) { - picker->select_all(select, alternates); - } -} - -int PagePrinters::get_width() const -{ - return std::accumulate(printer_pickers.begin(), printer_pickers.end(), 0, - [](int acc, const PrinterPicker *picker) { return std::max(acc, picker->get_width()); }); -} - -bool PagePrinters::any_selected() const -{ - for (const auto *picker : printer_pickers) { - if (picker->any_selected()) { return true; } - } - - return false; -} - -std::set PagePrinters::get_selected_models() -{ - std::set ret_set; - - for (const auto *picker : printer_pickers) - { - std::set tmp_models = picker->get_selected_models(); - ret_set.insert(tmp_models.begin(), tmp_models.end()); - } - - return ret_set; -} - -void PagePrinters::set_run_reason(ConfigWizard::RunReason run_reason) -{ - if (is_primary_printer_page - && (run_reason == ConfigWizard::RR_DATA_EMPTY || run_reason == ConfigWizard::RR_DATA_LEGACY) - && printer_pickers.size() > 0 - && printer_pickers[0]->vendor_id == PresetBundle::PRUSA_BUNDLE) { - printer_pickers[0]->select_one(0, true); - } -} - - -const std::string PageMaterials::EMPTY; - -PageMaterials::PageMaterials(ConfigWizard *parent, Materials *materials, wxString title, wxString shortname, wxString list1name) - : ConfigWizardPage(parent, std::move(title), std::move(shortname)) - , materials(materials) - , list_printer(new StringList(this, wxLB_MULTIPLE)) - , list_type(new StringList(this)) - , list_vendor(new StringList(this)) - , list_profile(new PresetList(this)) -{ - append_spacer(VERTICAL_SPACING); - - const int em = parent->em_unit(); - const int list_h = 30*em; - - - list_printer->SetMinSize(wxSize(23*em, list_h)); - list_type->SetMinSize(wxSize(13*em, list_h)); - list_vendor->SetMinSize(wxSize(13*em, list_h)); - list_profile->SetMinSize(wxSize(23*em, list_h)); - - - - grid = new wxFlexGridSizer(4, em/2, em); - grid->AddGrowableCol(3, 1); - grid->AddGrowableRow(1, 1); - - grid->Add(new wxStaticText(this, wxID_ANY, _L("Printer:"))); - grid->Add(new wxStaticText(this, wxID_ANY, list1name)); - grid->Add(new wxStaticText(this, wxID_ANY, _L("Vendor:"))); - grid->Add(new wxStaticText(this, wxID_ANY, _L("Profile:"))); - - grid->Add(list_printer, 0, wxEXPAND); - grid->Add(list_type, 0, wxEXPAND); - grid->Add(list_vendor, 0, wxEXPAND); - grid->Add(list_profile, 1, wxEXPAND); - - auto *btn_sizer = new wxBoxSizer(wxHORIZONTAL); - auto *sel_all = new wxButton(this, wxID_ANY, _L("All")); - auto *sel_none = new wxButton(this, wxID_ANY, _L("None")); - btn_sizer->Add(sel_all, 0, wxRIGHT, em / 2); - btn_sizer->Add(sel_none); - - wxGetApp().UpdateDarkUI(list_printer); - wxGetApp().UpdateDarkUI(list_type); - wxGetApp().UpdateDarkUI(list_vendor); - wxGetApp().UpdateDarkUI(sel_all); - wxGetApp().UpdateDarkUI(sel_none); - - grid->Add(new wxBoxSizer(wxHORIZONTAL)); - grid->Add(new wxBoxSizer(wxHORIZONTAL)); - grid->Add(new wxBoxSizer(wxHORIZONTAL)); - grid->Add(btn_sizer, 0, wxALIGN_RIGHT); - - append(grid, 1, wxEXPAND); - - append_spacer(VERTICAL_SPACING); - - html_window = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, - wxSize(60 * em, 20 * em), wxHW_SCROLLBAR_AUTO); - append(html_window, 0, wxEXPAND); - - list_printer->Bind(wxEVT_LISTBOX, [this](wxCommandEvent& evt) { - update_lists(list_type->GetSelection(), list_vendor->GetSelection(), evt.GetInt()); - }); - list_type->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { - update_lists(list_type->GetSelection(), list_vendor->GetSelection()); - }); - list_vendor->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { - update_lists(list_type->GetSelection(), list_vendor->GetSelection()); - }); - - list_profile->Bind(wxEVT_CHECKLISTBOX, [this](wxCommandEvent &evt) { select_material(evt.GetInt()); }); - list_profile->Bind(wxEVT_LISTBOX, [this](wxCommandEvent& evt) { on_material_highlighted(evt.GetInt()); }); - - sel_all->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { select_all(true); }); - sel_none->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { select_all(false); }); - /* - Bind(wxEVT_PAINT, [this](wxPaintEvent& evt) {on_paint();}); - - list_profile->Bind(wxEVT_MOTION, [this](wxMouseEvent& evt) { on_mouse_move_on_profiles(evt); }); - list_profile->Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& evt) { on_mouse_enter_profiles(evt); }); - list_profile->Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& evt) { on_mouse_leave_profiles(evt); }); - */ - reload_presets(); - set_compatible_printers_html_window(std::vector(), false); -} -void PageMaterials::on_paint() -{ -} -void PageMaterials::on_mouse_move_on_profiles(wxMouseEvent& evt) -{ - const wxClientDC dc(list_profile); - const wxPoint pos = evt.GetLogicalPosition(dc); - int item = list_profile->HitTest(pos); - on_material_hovered(item); -} -void PageMaterials::on_mouse_enter_profiles(wxMouseEvent& evt) -{} -void PageMaterials::on_mouse_leave_profiles(wxMouseEvent& evt) -{ - on_material_hovered(-1); -} -void PageMaterials::reload_presets() -{ - clear(); - - list_printer->append(_L("(All)"), &EMPTY); - //list_printer->SetLabelMarkup("bald"); - for (const Preset* printer : materials->printers) { - list_printer->append(printer->name, &printer->name); - } - sort_list_data(list_printer, true, false); - if (list_printer->GetCount() > 0) { - list_printer->SetSelection(0); - sel_printers_prev.Clear(); - sel_type_prev = wxNOT_FOUND; - sel_vendor_prev = wxNOT_FOUND; - update_lists(0, 0, 0); - } - - presets_loaded = true; -} - -void PageMaterials::set_compatible_printers_html_window(const std::vector& printer_names, bool all_printers) -{ - const auto bgr_clr = -#if defined(__APPLE__) - html_window->GetParent()->GetBackgroundColour(); -#else -#if defined(_WIN32) - wxGetApp().get_window_default_clr(); -#else - wxSystemSettings::GetColour(wxSYS_COLOUR_MENU); -#endif -#endif - const auto text_clr = wxGetApp().get_label_clr_default(); - const auto bgr_clr_str = encode_color(ColorRGB(bgr_clr.Red(), bgr_clr.Green(), bgr_clr.Blue())); - const auto text_clr_str = encode_color(ColorRGB(text_clr.Red(), text_clr.Green(), text_clr.Blue())); - wxString first_line = format_wxstr(_L("%1% marked with * are not compatible with some installed printers."), materials->technology == T_FFF ? _L("Filaments") : _L("SLA materials")); - wxString text; - if (all_printers) { - wxString second_line = format_wxstr(_L("All installed printers are compatible with the selected %1%."), materials->technology == T_FFF ? _L("filament") : _L("SLA material")); - text = wxString::Format( - "" - "" - "" - "" - "" - "%s

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

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

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

%s" + "" + "" + , bgr_clr_str + , text_clr_str + , first_line + , second_line); + for (size_t i = 0; i < printer_names.size(); ++i) + { + text += wxString::Format("", boost::nowide::widen(printer_names[i])); + if (i % 3 == 2) { + text += wxString::Format( + "" + ""); + } + } + text += wxString::Format( + "" + "
%s
" + "
" + "
" + "" + "" + ); + } + } + + + wxFont font = get_default_font_for_dpi(this, get_dpi_for_window(this)); + const int fs = font.GetPointSize(); + int size[] = { fs,fs,fs,fs,fs,fs,fs }; + html_window->SetFonts(font.GetFaceName(), font.GetFaceName(), size); + html_window->SetPage(text); +} + +void PageMaterials::clear_compatible_printers_label() +{ + set_compatible_printers_html_window(std::vector(), false); +} + +void PageMaterials::on_material_hovered(int sel_material) +{ + +} + +void PageMaterials::on_material_highlighted(int sel_material) +{ + if (sel_material == last_hovered_item) + return; + if (sel_material == -1) { + clear_compatible_printers_label(); + return; + } + last_hovered_item = sel_material; + std::vector tabs; + tabs.push_back(std::string()); + tabs.push_back(std::string()); + tabs.push_back(std::string()); + //selected material string + std::string material_name = list_profile->get_data(sel_material); + // get material preset + const std::vector matching_materials = materials->get_presets_by_alias(material_name); + if (matching_materials.empty()) + { + clear_compatible_printers_label(); + return; + } + //find matching printers + std::vector names; + for (const Preset* printer : materials->printers) { + for (const Preset* material : matching_materials) { + if (material->vendor && material->vendor->templates_profile) + continue; + if (is_compatible_with_printer(PresetWithVendorProfile(*material, material->vendor), PresetWithVendorProfile(*printer, printer->vendor))) { + names.push_back(printer->name); + break; + } + } + } + set_compatible_printers_html_window(names, names.size() == materials->printers.size()); +} + +void PageMaterials::update_lists(int sel_type, int sel_vendor, int last_selected_printer/* = -1*/) +{ + wxWindowUpdateLocker freeze_guard(this); + (void)freeze_guard; + + wxArrayInt sel_printers; + int sel_printers_count = list_printer->GetSelections(sel_printers); + + bool templates_available = list_printer->size() > 1 && list_printer->get_data(1) == TEMPLATES; + + // Does our wxWidgets version support operator== for wxArrayInt ? + // https://github.com/prusa3d/PrusaSlicer/issues/5152#issuecomment-787208614 +#if wxCHECK_VERSION(3, 1, 1) + if (sel_printers != sel_printers_prev) { +#else + auto are_equal = [](const wxArrayInt& arr_first, const wxArrayInt& arr_second) { + if (arr_first.GetCount() != arr_second.GetCount()) + return false; + for (size_t i = 0; i < arr_first.GetCount(); i++) + if (arr_first[i] != arr_second[i]) + return false; + return true; + }; + if (!are_equal(sel_printers, sel_printers_prev)) { +#endif + template_shown = false; + // Refresh type list + list_type->Clear(); + list_type->append(_L("(All)"), &EMPTY); + if (sel_printers_count > 1) { + // If all is selected with other printers + // unselect "all" or all printers depending on last value + // same with "templates" + if (sel_printers[0] == 0 && sel_printers_count > 1) { + if (last_selected_printer == 0) { + list_printer->SetSelection(wxNOT_FOUND); + list_printer->SetSelection(0); + } else { + list_printer->SetSelection(0, false); + sel_printers_count = list_printer->GetSelections(sel_printers); + } + } + if (materials->technology == T_FFF && templates_available && (sel_printers[0] == 1 || sel_printers[1] == 1) && sel_printers_count > 1) { + if (last_selected_printer == 1) { + list_printer->SetSelection(wxNOT_FOUND); + list_printer->SetSelection(1); + } + else if (last_selected_printer != 0) { + list_printer->SetSelection(1, false); + sel_printers_count = list_printer->GetSelections(sel_printers); + } + } + } + if (sel_printers_count > 0 && sel_printers[0] != 0 && ((materials->technology == T_FFF && templates_available && sel_printers[0] != 1) || materials->technology != T_FFF || !templates_available)) { + for (int i = 0; i < sel_printers_count; i++) { + const std::string& printer_name = list_printer->get_data(sel_printers[i]); + const Preset* printer = nullptr; + for (const Preset* it : materials->printers) { + if (it->name == printer_name) { + printer = it; + break; + } + } + materials->filter_presets(printer, printer_name, EMPTY, EMPTY, [this](const Preset* p) { + const std::string& type = this->materials->get_type(p); + if (list_type->find(type) == wxNOT_FOUND) { + list_type->append(type, &type); + } + }); + } + } + else if (sel_printers_count > 0 && last_selected_printer == 0) { + //clear selection except "ALL" + list_printer->SetSelection(wxNOT_FOUND); + list_printer->SetSelection(0); + sel_printers_count = list_printer->GetSelections(sel_printers); + + materials->filter_presets(nullptr, EMPTY, EMPTY, EMPTY, [this](const Preset* p) { + const std::string& type = this->materials->get_type(p); + if (list_type->find(type) == wxNOT_FOUND) { + list_type->append(type, &type); + } + }); + } + else if (materials->technology == T_FFF && templates_available && sel_printers_count > 0 && last_selected_printer == 1) { + //clear selection except "TEMPLATES" + list_printer->SetSelection(wxNOT_FOUND); + list_printer->SetSelection(1); + sel_printers_count = list_printer->GetSelections(sel_printers); + template_shown = true; + materials->filter_presets(nullptr, TEMPLATES, EMPTY, EMPTY, + [this](const Preset* p) { + const std::string& type = this->materials->get_type(p); + if (list_type->find(type) == wxNOT_FOUND) { + list_type->append(type, &type); + } + }); + } + sort_list_data(list_type, true, true); + + sel_printers_prev = sel_printers; + sel_type = 0; + sel_type_prev = wxNOT_FOUND; + list_type->SetSelection(sel_type); + list_profile->Clear(); + } + + if (sel_type != sel_type_prev) { + // Refresh vendor list + + // XXX: The vendor list is created with quadratic complexity here, + // but the number of vendors is going to be very small this shouldn't be a problem. + + list_vendor->Clear(); + list_vendor->append(_L("(All)"), &EMPTY); + if (sel_printers_count != 0 && sel_type != wxNOT_FOUND) { + const std::string& type = list_type->get_data(sel_type); + // find printer preset + for (int i = 0; i < sel_printers_count; i++) { + const std::string& printer_name = list_printer->get_data(sel_printers[i]); + const Preset* printer = nullptr; + for (const Preset* it : materials->printers) { + if (it->name == printer_name) { + printer = it; + break; + } + } + materials->filter_presets(printer, printer_name, type, EMPTY, [this](const Preset* p) { + const std::string& vendor = this->materials->get_vendor(p); + if (list_vendor->find(vendor) == wxNOT_FOUND) { + list_vendor->append(vendor, &vendor); + } + }); + } + sort_list_data(list_vendor, true, false); + } + + sel_type_prev = sel_type; + sel_vendor = 0; + sel_vendor_prev = wxNOT_FOUND; + list_vendor->SetSelection(sel_vendor); + list_profile->Clear(); + } + + if (sel_vendor != sel_vendor_prev) { + // Refresh material list + + list_profile->Clear(); + clear_compatible_printers_label(); + if (sel_printers_count != 0 && sel_type != wxNOT_FOUND && sel_vendor != wxNOT_FOUND) { + const std::string& type = list_type->get_data(sel_type); + const std::string& vendor = list_vendor->get_data(sel_vendor); + // first printer preset + std::vector to_list; + for (int i = 0; i < sel_printers_count; i++) { + const std::string& printer_name = list_printer->get_data(sel_printers[i]); + const Preset* printer = nullptr; + for (const Preset* it : materials->printers) { + if (it->name == printer_name) { + printer = it; + break; + } + } + materials->filter_presets(printer, printer_name, type, vendor, [this, &to_list](const Preset* p) { + const std::string& section = materials->appconfig_section(); + bool checked = wizard_p()->appconfig_new.has(section, p->name); + bool was_checked = false; + + int cur_i = list_profile->find(p->alias); + if (cur_i == wxNOT_FOUND) { + cur_i = list_profile->append(p->alias + (materials->get_omnipresent(p) || template_shown ? "" : " *"), &p->alias); + to_list.emplace_back(p->alias, materials->get_omnipresent(p), checked); + } + else { + was_checked = list_profile->IsChecked(cur_i); + to_list[cur_i].checked = checked || was_checked; + } + list_profile->Check(cur_i, checked || was_checked); + + /* Update preset selection in config. + * If one preset from aliases bundle is selected, + * than mark all presets with this aliases as selected + * */ + if (checked && !was_checked) + wizard_p()->update_presets_in_config(section, p->alias, true); + else if (!checked && was_checked) + wizard_p()->appconfig_new.set(section, p->name, "1"); + }); + } + sort_list_data(list_profile, to_list); + } + + sel_vendor_prev = sel_vendor; + } + wxGetApp().UpdateDarkUI(list_profile); +} + +void PageMaterials::sort_list_data(StringList* list, bool add_All_item, bool material_type_ordering) +{ +// get data from list +// sort data +// first should be +// then prusa profiles +// then the rest +// in alphabetical order + + std::vector> prusa_profiles; + std::vector> other_profiles; + bool add_TEMPLATES_item = false; + for (int i = 0 ; i < list->size(); ++i) { + const std::string& data = list->get_data(i); + if (data == EMPTY) // do not sort item + continue; + if (data == TEMPLATES) {// do not sort item + add_TEMPLATES_item = true; + continue; + } + if (!material_type_ordering && data.find("Prusa") != std::string::npos) + prusa_profiles.push_back(data); + else + other_profiles.push_back(data); + } + if(material_type_ordering) { + + const ConfigOptionDef* def = print_config_def.get("filament_type"); + std::vectorenum_values = def->enum_values; + size_t end_of_sorted = 0; + for (size_t vals = 0; vals < enum_values.size(); vals++) { + for (size_t profs = end_of_sorted; profs < other_profiles.size(); profs++) + { + // find instead compare because PET vs PETG + if (other_profiles[profs].get().find(enum_values[vals]) != std::string::npos) { + //swap + if(profs != end_of_sorted) { + std::reference_wrapper aux = other_profiles[end_of_sorted]; + other_profiles[end_of_sorted] = other_profiles[profs]; + other_profiles[profs] = aux; + } + end_of_sorted++; + break; + } + } + } + } else { + std::sort(prusa_profiles.begin(), prusa_profiles.end(), [](std::reference_wrapper a, std::reference_wrapper b) { + return a.get() < b.get(); + }); + std::sort(other_profiles.begin(), other_profiles.end(), [](std::reference_wrapper a, std::reference_wrapper b) { + return a.get() < b.get(); + }); + } + + list->Clear(); + if (add_All_item) + list->append(_L("(All)"), &EMPTY); + if (materials->technology == T_FFF && add_TEMPLATES_item) + list->append(_L("(Templates)"), &TEMPLATES); + for (const auto& item : prusa_profiles) + list->append(item, &const_cast(item.get())); + for (const auto& item : other_profiles) + list->append(item, &const_cast(item.get())); + +} + +void PageMaterials::sort_list_data(PresetList* list, const std::vector& data) +{ + // sort data + // then prusa profiles + // then the rest + // in alphabetical order + std::vector prusa_profiles; + std::vector other_profiles; + //for (int i = 0; i < data.size(); ++i) { + for (const auto& item : data) { + const std::string& name = item.name; + if (name.find("Prusa") != std::string::npos) + prusa_profiles.emplace_back(item); + else + other_profiles.emplace_back(item); + } + std::sort(prusa_profiles.begin(), prusa_profiles.end(), [](ProfilePrintData a, ProfilePrintData b) { + return a.name.get() < b.name.get(); + }); + std::sort(other_profiles.begin(), other_profiles.end(), [](ProfilePrintData a, ProfilePrintData b) { + return a.name.get() < b.name.get(); + }); + list->Clear(); + for (size_t i = 0; i < prusa_profiles.size(); ++i) { + list->append(std::string(prusa_profiles[i].name) + (prusa_profiles[i].omnipresent || template_shown ? "" : " *"), &const_cast(prusa_profiles[i].name.get())); + list->Check(i, prusa_profiles[i].checked); + } + for (size_t i = 0; i < other_profiles.size(); ++i) { + list->append(std::string(other_profiles[i].name) + (other_profiles[i].omnipresent || template_shown ? "" : " *"), &const_cast(other_profiles[i].name.get())); + list->Check(i + prusa_profiles.size(), other_profiles[i].checked); + } +} + +void PageMaterials::select_material(int i) +{ + const bool checked = list_profile->IsChecked(i); + + const std::string& alias_key = list_profile->get_data(i); + if (checked && template_shown && !notification_shown) { + notification_shown = true; + wxString message = _L("You have selected template filament. Please note that these filaments are available for all printers but are NOT certain to be compatible with your printer. Do you still wish to have this filament selected?\n(This message won't be displayed again.)"); + MessageDialog msg(this, message, _L("Notice"), wxYES_NO); + if (msg.ShowModal() == wxID_NO) { + list_profile->Check(i, false); + return; + } + } + wizard_p()->update_presets_in_config(materials->appconfig_section(), alias_key, checked); +} + +void PageMaterials::select_all(bool select) +{ + wxWindowUpdateLocker freeze_guard(this); + (void)freeze_guard; + + for (unsigned i = 0; i < list_profile->GetCount(); i++) { + const bool current = list_profile->IsChecked(i); + if (current != select) { + list_profile->Check(i, select); + select_material(i); + } + } +} + +void PageMaterials::clear() +{ + list_printer->Clear(); + list_type->Clear(); + list_vendor->Clear(); + list_profile->Clear(); + sel_printers_prev.Clear(); + sel_type_prev = wxNOT_FOUND; + sel_vendor_prev = wxNOT_FOUND; + presets_loaded = false; +} + +void PageMaterials::on_activate() +{ + if (! presets_loaded) { + wizard_p()->update_materials(materials->technology); + reload_presets(); + } + first_paint = true; +} + + +const char *PageCustom::default_profile_name = "My Settings"; + +PageCustom::PageCustom(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Custom Printer Setup"), _L("Custom Printer")) +{ + cb_custom = new wxCheckBox(this, wxID_ANY, _L("Define a custom printer profile")); + auto *label = new wxStaticText(this, wxID_ANY, _L("Custom profile name:")); + + wxBoxSizer* profile_name_sizer = new wxBoxSizer(wxVERTICAL); + profile_name_editor = new SavePresetDialog::Item{ this, profile_name_sizer, default_profile_name }; + profile_name_editor->Enable(false); + + cb_custom->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &) { + profile_name_editor->Enable(custom_wanted()); + wizard_p()->on_custom_setup(custom_wanted()); + }); + + append(cb_custom); + append(label); + append(profile_name_sizer); +} + +PageUpdate::PageUpdate(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Automatic updates"), _L("Updates")) + , version_check(true) + , preset_update(true) +{ + const AppConfig *app_config = wxGetApp().app_config; + auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + boldfont.SetWeight(wxFONTWEIGHT_BOLD); + + auto *box_slic3r = new wxCheckBox(this, wxID_ANY, _L("Check for application updates")); + box_slic3r->SetValue(app_config->get("notify_release") != "none"); + append(box_slic3r); + append_text(wxString::Format(_L( + "If enabled, %s checks for new application versions online. When a new version becomes available, " + "a notification is displayed at the next application startup (never during program usage). " + "This is only a notification mechanisms, no automatic installation is done."), SLIC3R_APP_NAME)); + + append_spacer(VERTICAL_SPACING); + + auto *box_presets = new wxCheckBox(this, wxID_ANY, _L("Update built-in Presets automatically")); + box_presets->SetValue(app_config->get("preset_update") == "1"); + append(box_presets); + append_text(wxString::Format(_L( + "If enabled, %s downloads updates of built-in system presets in the background." + "These updates are downloaded into a separate temporary location." + "When a new preset version becomes available it is offered at application startup."), SLIC3R_APP_NAME)); + const auto text_bold = _L("Updates are never applied without user's consent and never overwrite user's customized settings."); + auto *label_bold = new wxStaticText(this, wxID_ANY, text_bold); + label_bold->SetFont(boldfont); + label_bold->Wrap(WRAP_WIDTH); + append(label_bold); + append_text(_L("Additionally a backup snapshot of the whole configuration is created before an update is applied.")); + + box_slic3r->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->version_check = event.IsChecked(); }); + box_presets->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->preset_update = event.IsChecked(); }); +} + +namespace DownloaderUtils +{ +#ifdef _WIN32 + + wxString get_downloads_path() + { + wxString ret; + PWSTR path = NULL; + HRESULT hr = SHGetKnownFolderPath(FOLDERID_Downloads, 0, NULL, &path); + if (SUCCEEDED(hr)) { + ret = wxString(path); + } + CoTaskMemFree(path); + return ret; + } + +#elif __APPLE__ + wxString get_downloads_path() + { + // call objective-c implementation + return wxString::FromUTF8(get_downloads_path_mac()); + } +#else + wxString get_downloads_path() + { + wxString command = "xdg-user-dir DOWNLOAD"; + wxArrayString output; + GUI::desktop_execute_get_result(command, output); + if (output.GetCount() > 0) { + return output[0]; + } + return wxString(); + } + +#endif + +Worker::Worker(wxWindow* parent) +: wxBoxSizer(wxHORIZONTAL) +, m_parent(parent) +{ + m_input_path = new wxTextCtrl(m_parent, wxID_ANY); + set_path_name(get_app_config()->get("url_downloader_dest")); + + auto* path_label = new wxStaticText(m_parent, wxID_ANY, _L("Download path") + ":"); + + this->Add(path_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + this->Add(m_input_path, 1, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 5); + + auto* button_path = new wxButton(m_parent, wxID_ANY, _L("Browse")); + this->Add(button_path, 0, wxEXPAND | wxTOP | wxLEFT, 5); + button_path->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { + boost::filesystem::path chosen_dest(boost::nowide::narrow(m_input_path->GetValue())); + + wxDirDialog dialog(m_parent, L("Choose folder:"), chosen_dest.string() ); + if (dialog.ShowModal() == wxID_OK) + this->m_input_path->SetValue(dialog.GetPath()); + }); + + for (wxSizerItem* item : this->GetChildren()) + if (item->IsWindow()) { + wxWindow* win = item->GetWindow(); + wxGetApp().UpdateDarkUI(win); + } +} + +void Worker::set_path_name(wxString path) +{ + if (path.empty()) + path = boost::nowide::widen(get_app_config()->get("url_downloader_dest")); + + if (path.empty()) { + // What should be default path? Each system has Downloads folder, that could be good one. + // Other would be program location folder - not so good: access rights, apple bin is inside bundle... + // default_path = boost::dll::program_location().parent_path().string(); + path = get_downloads_path(); + } + + m_input_path->SetValue(path); +} + +void Worker::set_path_name(const std::string& name) +{ + if (!m_input_path) + return; + + set_path_name(boost::nowide::widen(name)); +} + +} // DownLoader + +PageDownloader::PageDownloader(ConfigWizard* parent) + : ConfigWizardPage(parent, _L("Downloads from URL"), _L("Downloads")) +{ + const AppConfig* app_config = wxGetApp().app_config; + auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + boldfont.SetWeight(wxFONTWEIGHT_BOLD); + + append_spacer(VERTICAL_SPACING); + + auto* box_allow_downloads = new wxCheckBox(this, wxID_ANY, _L("Allow build-in downloader")); + // TODO: Do we want it like this? The downloader is allowed for very first time the wizard is run. + bool box_allow_value = (app_config->has("downloader_url_registered") ? app_config->get("downloader_url_registered") == "1" : true); + box_allow_downloads->SetValue(box_allow_value); + append(box_allow_downloads); + + append_text(wxString::Format(_L( + "If enabled, %s registers to start on custom URL on www.printables.com." + " You will be able to use button with %s logo to open models in this %s." + " The model will be downloaded into folder you choose bellow." + ), SLIC3R_APP_NAME, SLIC3R_APP_NAME, SLIC3R_APP_NAME)); + +#ifdef __linux__ + append_text(wxString::Format(_L( + "On Linux systems the process of registration also creates desktop integration files for this version of application." + ))); +#endif + + box_allow_downloads->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) { this->downloader->allow(event.IsChecked()); }); + + downloader = new DownloaderUtils::Worker(this); + append(downloader); + downloader->allow(box_allow_value); +} + +bool PageDownloader::on_finish_downloader() const +{ + return downloader->on_finish(); +} + +bool DownloaderUtils::Worker::perform_register() +{ + //boost::filesystem::path chosen_dest/*(path_text_ctrl->GetValue());*/(boost::nowide::narrow(path_text_ctrl->GetValue())); + boost::filesystem::path chosen_dest (GUI::format(path_name())); + boost::system::error_code ec; + if (chosen_dest.empty() || !boost::filesystem::is_directory(chosen_dest, ec) || ec) { + std::string err_msg = GUI::format("%1%\n\n%2%",_L("Chosen directory for downloads does not Exists.") ,chosen_dest.string()); + BOOST_LOG_TRIVIAL(error) << err_msg; + show_error(m_parent, err_msg); + return false; + } + BOOST_LOG_TRIVIAL(info) << "Downloader registration: Directory for downloads: " << chosen_dest.string(); + wxGetApp().app_config->set("url_downloader_dest", chosen_dest.string()); +#ifdef _WIN32 + // Registry key creation for "prusaslicer://" URL + + boost::filesystem::path binary_path(boost::filesystem::canonical(boost::dll::program_location())); + // the path to binary needs to be correctly saved in string with respect to localized characters + wxString wbinary = wxString::FromUTF8(binary_path.string()); + std::string binary_string = (boost::format("%1%") % wbinary).str(); + BOOST_LOG_TRIVIAL(info) << "Downloader registration: Path of binary: " << binary_string; + + //std::string key_string = "\"" + binary_string + "\" \"-u\" \"%1\""; + //std::string key_string = "\"" + binary_string + "\" \"%1\""; + std::string key_string = "\"" + binary_string + "\" \"--single-instance\" \"%1\""; + + wxRegKey key_first(wxRegKey::HKCU, "Software\\Classes\\prusaslicer"); + wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\prusaslicer\\shell\\open\\command"); + if (!key_first.Exists()) { + key_first.Create(false); + } + key_first.SetValue("URL Protocol", ""); + + if (!key_full.Exists()) { + key_full.Create(false); + } + //key_full = "\"C:\\Program Files\\Prusa3D\\PrusaSlicer\\prusa-slicer-console.exe\" \"%1\""; + key_full = key_string; +#elif __APPLE__ + // Apple registers for custom url in info.plist thus it has to be already registered since build. + // The url will always trigger opening of prusaslicer and we have to check that user has allowed it. (GUI_App::MacOpenURL is the triggered method) +#else + // the performation should be called later during desktop integration + perform_registration_linux = true; +#endif + return true; +} + +void DownloaderUtils::Worker::deregister() +{ +#ifdef _WIN32 + std::string key_string = ""; + wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\prusaslicer\\shell\\open\\command"); + if (!key_full.Exists()) { + return; + } + key_full = key_string; +#elif __APPLE__ + // TODO +#else + BOOST_LOG_TRIVIAL(debug) << "DesktopIntegrationDialog::undo_downloader_registration"; + DesktopIntegrationDialog::undo_downloader_registration(); + perform_registration_linux = false; +#endif +} + +bool DownloaderUtils::Worker::on_finish() { + AppConfig* app_config = wxGetApp().app_config; + bool ac_value = app_config->get("downloader_url_registered") == "1"; + BOOST_LOG_TRIVIAL(debug) << "PageDownloader::on_finish_downloader ac_value " << ac_value << " downloader_checked " << downloader_checked; + if (ac_value && downloader_checked) { + // already registered but we need to do it again + if (!perform_register()) + return false; + app_config->set("downloader_url_registered", "1"); + } else if (!ac_value && downloader_checked) { + // register + if (!perform_register()) + return false; + app_config->set("downloader_url_registered", "1"); + } else if (ac_value && !downloader_checked) { + // deregister, downloads are banned now + deregister(); + app_config->set("downloader_url_registered", "0"); + } /*else if (!ac_value && !downloader_checked) { + // not registered and we dont want to do it + // do not deregister as other instance might be registered + } */ + return true; +} + + +PageReloadFromDisk::PageReloadFromDisk(ConfigWizard* parent) + : ConfigWizardPage(parent, _L("Reload from disk"), _L("Reload from disk")) + , full_pathnames(false) +{ + auto* box_pathnames = new wxCheckBox(this, wxID_ANY, _L("Export full pathnames of models and parts sources into 3mf and amf files")); + box_pathnames->SetValue(wxGetApp().app_config->get("export_sources_full_pathnames") == "1"); + append(box_pathnames); + append_text(_L( + "If enabled, allows the Reload from disk command to automatically find and load the files when invoked.\n" + "If not enabled, the Reload from disk command will ask to select each file using an open file dialog." + )); + + box_pathnames->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) { this->full_pathnames = event.IsChecked(); }); +} + +#ifdef _WIN32 +PageFilesAssociation::PageFilesAssociation(ConfigWizard* parent) + : ConfigWizardPage(parent, _L("Files association"), _L("Files association")) +{ + cb_3mf = new wxCheckBox(this, wxID_ANY, _L("Associate .3mf files to PrusaSlicer")); + cb_stl = new wxCheckBox(this, wxID_ANY, _L("Associate .stl files to PrusaSlicer")); +// cb_gcode = new wxCheckBox(this, wxID_ANY, _L("Associate .gcode files to PrusaSlicer G-code Viewer")); + + append(cb_3mf); + append(cb_stl); +// append(cb_gcode); +} +#endif // _WIN32 + +PageMode::PageMode(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("View mode"), _L("View mode")) +{ + append_text(_L("PrusaSlicer's user interfaces comes in three variants:\nSimple, Advanced, and Expert.\n" + "The Simple mode shows only the most frequently used settings relevant for regular 3D printing. " + "The other two offer progressively more sophisticated fine-tuning, " + "they are suitable for advanced and expert users, respectively.")); + + radio_simple = new wxRadioButton(this, wxID_ANY, _L("Simple mode")); + radio_advanced = new wxRadioButton(this, wxID_ANY, _L("Advanced mode")); + radio_expert = new wxRadioButton(this, wxID_ANY, _L("Expert mode")); + + std::string mode { "simple" }; + wxGetApp().app_config->get("", "view_mode", mode); + + if (mode == "advanced") { radio_advanced->SetValue(true); } + else if (mode == "expert") { radio_expert->SetValue(true); } + else { radio_simple->SetValue(true); } + + append(radio_simple); + append(radio_advanced); + append(radio_expert); + + append_text("\n" + _L("The size of the object can be specified in inches")); + check_inch = new wxCheckBox(this, wxID_ANY, _L("Use inches")); + check_inch->SetValue(wxGetApp().app_config->get("use_inches") == "1"); + append(check_inch); + + on_activate(); +} + +void PageMode::serialize_mode(AppConfig *app_config) const +{ + std::string mode = ""; + + if (radio_simple->GetValue()) { mode = "simple"; } + if (radio_advanced->GetValue()) { mode = "advanced"; } + if (radio_expert->GetValue()) { mode = "expert"; } + + app_config->set("view_mode", mode); + app_config->set("use_inches", check_inch->GetValue() ? "1" : "0"); +} + +PageVendors::PageVendors(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Other Vendors"), _L("Other Vendors")) +{ + const AppConfig &appconfig = this->wizard_p()->appconfig_new; + + append_text(wxString::Format(_L("Pick another vendor supported by %s"), SLIC3R_APP_NAME) + ":"); + + auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + boldfont.SetWeight(wxFONTWEIGHT_BOLD); + + for (const auto &pair : wizard_p()->bundles) { + const VendorProfile *vendor = pair.second.vendor_profile; + if (vendor->id == PresetBundle::PRUSA_BUNDLE) { continue; } + if (vendor && vendor->templates_profile) + continue; + + auto *cbox = new wxCheckBox(this, wxID_ANY, vendor->name); + cbox->Bind(wxEVT_CHECKBOX, [=](wxCommandEvent &event) { + wizard_p()->on_3rdparty_install(vendor, cbox->IsChecked()); + }); + + const auto &vendors = appconfig.vendors(); + const bool enabled = vendors.find(pair.first) != vendors.end(); + if (enabled) { + cbox->SetValue(true); + + auto pages = wizard_p()->pages_3rdparty.find(vendor->id); + wxCHECK_RET(pages != wizard_p()->pages_3rdparty.end(), "Internal error: 3rd party vendor printers page not created"); + + for (PagePrinters* page : { pages->second.first, pages->second.second }) + if (page) page->install = true; + } + + append(cbox); + } +} + +PageFirmware::PageFirmware(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Firmware Type"), _L("Firmware"), 1) + , gcode_opt(*print_config_def.get("gcode_flavor")) + , gcode_picker(nullptr) +{ + append_text(_L("Choose the type of firmware used by your printer.")); + append_text(_(gcode_opt.tooltip)); + + wxArrayString choices; + choices.Alloc(gcode_opt.enum_labels.size()); + for (const auto &label : gcode_opt.enum_labels) { + choices.Add(label); + } + + gcode_picker = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices); + wxGetApp().UpdateDarkUI(gcode_picker); + const auto &enum_values = gcode_opt.enum_values; + auto needle = enum_values.cend(); + if (gcode_opt.default_value) { + needle = std::find(enum_values.cbegin(), enum_values.cend(), gcode_opt.default_value->serialize()); + } + if (needle != enum_values.cend()) { + gcode_picker->SetSelection(needle - enum_values.cbegin()); + } else { + gcode_picker->SetSelection(0); + } + + append(gcode_picker); +} + +void PageFirmware::apply_custom_config(DynamicPrintConfig &config) +{ + auto sel = gcode_picker->GetSelection(); + if (sel >= 0 && (size_t)sel < gcode_opt.enum_labels.size()) { + auto *opt = new ConfigOptionEnum(static_cast(sel)); + config.set_key_value("gcode_flavor", opt); + } +} + +static void focus_event(wxFocusEvent& e, wxTextCtrl* ctrl, double def_value) +{ + e.Skip(); + wxString str = ctrl->GetValue(); + + const char dec_sep = is_decimal_separator_point() ? '.' : ','; + const char dec_sep_alt = dec_sep == '.' ? ',' : '.'; + // Replace the first incorrect separator in decimal number. + bool was_replaced = str.Replace(dec_sep_alt, dec_sep, false) != 0; + + double val = 0.0; + if (!str.ToDouble(&val)) { + if (val == 0.0) + val = def_value; + ctrl->SetValue(double_to_string(val)); + show_error(nullptr, _L("Invalid numeric input.")); + // On Windows, this SetFocus creates an invisible marker. + //ctrl->SetFocus(); + } + else if (was_replaced) + ctrl->SetValue(double_to_string(val)); +} + +class DiamTextCtrl : public wxTextCtrl +{ +public: + DiamTextCtrl(wxWindow* parent) + { +#ifdef _WIN32 + long style = wxBORDER_SIMPLE; +#else + long style = 0; +#endif + Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(Field::def_width_thinner() * wxGetApp().em_unit(), wxDefaultCoord), style); + wxGetApp().UpdateDarkUI(this); + } + ~DiamTextCtrl() {} +}; + +PageBedShape::PageBedShape(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Bed Shape and Size"), _L("Bed Shape"), 1) + , shape_panel(new BedShapePanel(this)) +{ + append_text(_L("Set the shape of your printer's bed.")); + + shape_panel->build_panel(*wizard_p()->custom_config->option("bed_shape"), + *wizard_p()->custom_config->option("bed_custom_texture"), + *wizard_p()->custom_config->option("bed_custom_model")); + + append(shape_panel); +} + +void PageBedShape::apply_custom_config(DynamicPrintConfig &config) +{ + const std::vector& points = shape_panel->get_shape(); + const std::string& custom_texture = shape_panel->get_custom_texture(); + const std::string& custom_model = shape_panel->get_custom_model(); + config.set_key_value("bed_shape", new ConfigOptionPoints(points)); + config.set_key_value("bed_custom_texture", new ConfigOptionString(custom_texture)); + config.set_key_value("bed_custom_model", new ConfigOptionString(custom_model)); +} + +PageBuildVolume::PageBuildVolume(ConfigWizard* parent) + : ConfigWizardPage(parent, _L("Build Volume"), _L("Build Volume"), 1) + , build_volume(new DiamTextCtrl(this)) +{ + append_text(_L("Set vertical size of your printer.")); + + wxString value = "200"; + build_volume->SetValue(value); + + build_volume->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { + double def_value = 200.0; + double max_value = 1200.0; + e.Skip(); + wxString str = build_volume->GetValue(); + + const char dec_sep = is_decimal_separator_point() ? '.' : ','; + const char dec_sep_alt = dec_sep == '.' ? ',' : '.'; + // Replace the first incorrect separator in decimal number. + bool was_replaced = str.Replace(dec_sep_alt, dec_sep, false) != 0; + + double val = 0.0; + if (!str.ToDouble(&val)) { + val = def_value; + build_volume->SetValue(double_to_string(val)); + show_error(nullptr, _L("Invalid numeric input.")); + //build_volume->SetFocus(); + } else if (val < 0.0) { + val = def_value; + build_volume->SetValue(double_to_string(val)); + show_error(nullptr, _L("Invalid numeric input.")); + //build_volume->SetFocus(); + } else if (val > max_value) { + val = max_value; + build_volume->SetValue(double_to_string(val)); + show_error(nullptr, _L("Invalid numeric input.")); + //build_volume->SetFocus(); + } else if (was_replaced) + build_volume->SetValue(double_to_string(val)); + }, build_volume->GetId()); + + auto* sizer_volume = new wxFlexGridSizer(3, 5, 5); + auto* text_volume = new wxStaticText(this, wxID_ANY, _L("Max print height:")); + auto* unit_volume = new wxStaticText(this, wxID_ANY, _L("mm")); + sizer_volume->AddGrowableCol(0, 1); + sizer_volume->Add(text_volume, 0, wxALIGN_CENTRE_VERTICAL); + sizer_volume->Add(build_volume); + sizer_volume->Add(unit_volume, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_volume); +} + +void PageBuildVolume::apply_custom_config(DynamicPrintConfig& config) +{ + double val = 0.0; + build_volume->GetValue().ToDouble(&val); + auto* opt_volume = new ConfigOptionFloat(val); + config.set_key_value("max_print_height", opt_volume); +} + +PageDiameters::PageDiameters(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Filament and Nozzle Diameters"), _L("Print Diameters"), 1) + , diam_nozzle(new DiamTextCtrl(this)) + , diam_filam (new DiamTextCtrl(this)) +{ + auto *default_nozzle = print_config_def.get("nozzle_diameter")->get_default_value(); + wxString value = double_to_string(default_nozzle != nullptr && default_nozzle->size() > 0 ? default_nozzle->get_at(0) : 0.5); + diam_nozzle->SetValue(value); + + auto *default_filam = print_config_def.get("filament_diameter")->get_default_value(); + value = double_to_string(default_filam != nullptr && default_filam->size() > 0 ? default_filam->get_at(0) : 3.0); + diam_filam->SetValue(value); + + diam_nozzle->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { focus_event(e, diam_nozzle, 0.5); }, diam_nozzle->GetId()); + diam_filam ->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { focus_event(e, diam_filam , 3.0); }, diam_filam->GetId()); + + append_text(_L("Enter the diameter of your printer's hot end nozzle.")); + + auto *sizer_nozzle = new wxFlexGridSizer(3, 5, 5); + auto *text_nozzle = new wxStaticText(this, wxID_ANY, _L("Nozzle Diameter:")); + auto *unit_nozzle = new wxStaticText(this, wxID_ANY, _L("mm")); + sizer_nozzle->AddGrowableCol(0, 1); + sizer_nozzle->Add(text_nozzle, 0, wxALIGN_CENTRE_VERTICAL); + sizer_nozzle->Add(diam_nozzle); + sizer_nozzle->Add(unit_nozzle, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_nozzle); + + append_spacer(VERTICAL_SPACING); + + append_text(_L("Enter the diameter of your filament.")); + append_text(_L("Good precision is required, so use a caliper and do multiple measurements along the filament, then compute the average.")); + + auto *sizer_filam = new wxFlexGridSizer(3, 5, 5); + auto *text_filam = new wxStaticText(this, wxID_ANY, _L("Filament Diameter:")); + auto *unit_filam = new wxStaticText(this, wxID_ANY, _L("mm")); + sizer_filam->AddGrowableCol(0, 1); + sizer_filam->Add(text_filam, 0, wxALIGN_CENTRE_VERTICAL); + sizer_filam->Add(diam_filam, 0, wxALIGN_CENTRE_VERTICAL); + sizer_filam->Add(unit_filam, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_filam); +} + +void PageDiameters::apply_custom_config(DynamicPrintConfig &config) +{ + double val = 0.0; + diam_nozzle->GetValue().ToDouble(&val); + auto *opt_nozzle = new ConfigOptionFloats(1, val); + config.set_key_value("nozzle_diameter", opt_nozzle); + + val = 0.0; + diam_filam->GetValue().ToDouble(&val); + auto * opt_filam = new ConfigOptionFloats(1, val); + config.set_key_value("filament_diameter", opt_filam); + + auto set_extrusion_width = [&config, opt_nozzle](const char *key, double dmr) { + char buf[64]; // locales don't matter here (sprintf/atof) + sprintf(buf, "%.2lf", dmr * opt_nozzle->values.front() / 0.4); + config.set_key_value(key, new ConfigOptionFloatOrPercent(atof(buf), false)); + }; + + set_extrusion_width("support_material_extrusion_width", 0.35); + set_extrusion_width("top_infill_extrusion_width", 0.40); + set_extrusion_width("first_layer_extrusion_width", 0.42); + + set_extrusion_width("extrusion_width", 0.45); + set_extrusion_width("perimeter_extrusion_width", 0.45); + set_extrusion_width("external_perimeter_extrusion_width", 0.45); + set_extrusion_width("infill_extrusion_width", 0.45); + set_extrusion_width("solid_infill_extrusion_width", 0.45); +} + +class SpinCtrlDouble: public wxSpinCtrlDouble +{ +public: + SpinCtrlDouble(wxWindow* parent) + { +#ifdef _WIN32 + long style = wxSP_ARROW_KEYS | wxBORDER_SIMPLE; +#else + long style = wxSP_ARROW_KEYS; +#endif + Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, style); +#ifdef _WIN32 + wxGetApp().UpdateDarkUI(this->GetText()); +#endif + this->Refresh(); + } + ~SpinCtrlDouble() {} +}; + +PageTemperatures::PageTemperatures(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Nozzle and Bed Temperatures"), _L("Temperatures"), 1) + , spin_extr(new SpinCtrlDouble(this)) + , spin_bed (new SpinCtrlDouble(this)) +{ + spin_extr->SetIncrement(5.0); + const auto &def_extr = *print_config_def.get("temperature"); + spin_extr->SetRange(def_extr.min, def_extr.max); + auto *default_extr = def_extr.get_default_value(); + spin_extr->SetValue(default_extr != nullptr && default_extr->size() > 0 ? default_extr->get_at(0) : 200); + + spin_bed->SetIncrement(5.0); + const auto &def_bed = *print_config_def.get("bed_temperature"); + spin_bed->SetRange(def_bed.min, def_bed.max); + auto *default_bed = def_bed.get_default_value(); + spin_bed->SetValue(default_bed != nullptr && default_bed->size() > 0 ? default_bed->get_at(0) : 0); + + append_text(_L("Enter the temperature needed for extruding your filament.")); + append_text(_L("A rule of thumb is 160 to 230 °C for PLA, and 215 to 250 °C for ABS.")); + + auto *sizer_extr = new wxFlexGridSizer(3, 5, 5); + auto *text_extr = new wxStaticText(this, wxID_ANY, _L("Extrusion Temperature:")); + auto *unit_extr = new wxStaticText(this, wxID_ANY, _L("°C")); + sizer_extr->AddGrowableCol(0, 1); + sizer_extr->Add(text_extr, 0, wxALIGN_CENTRE_VERTICAL); + sizer_extr->Add(spin_extr); + sizer_extr->Add(unit_extr, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_extr); + + append_spacer(VERTICAL_SPACING); + + append_text(_L("Enter the bed temperature needed for getting your filament to stick to your heated bed.")); + append_text(_L("A rule of thumb is 60 °C for PLA and 110 °C for ABS. Leave zero if you have no heated bed.")); + + auto *sizer_bed = new wxFlexGridSizer(3, 5, 5); + auto *text_bed = new wxStaticText(this, wxID_ANY, _L("Bed Temperature:")); + auto *unit_bed = new wxStaticText(this, wxID_ANY, _L("°C")); + sizer_bed->AddGrowableCol(0, 1); + sizer_bed->Add(text_bed, 0, wxALIGN_CENTRE_VERTICAL); + sizer_bed->Add(spin_bed); + sizer_bed->Add(unit_bed, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_bed); +} + +void PageTemperatures::apply_custom_config(DynamicPrintConfig &config) +{ + auto *opt_extr = new ConfigOptionInts(1, spin_extr->GetValue()); + config.set_key_value("temperature", opt_extr); + auto *opt_extr1st = new ConfigOptionInts(1, spin_extr->GetValue()); + config.set_key_value("first_layer_temperature", opt_extr1st); + auto *opt_bed = new ConfigOptionInts(1, spin_bed->GetValue()); + config.set_key_value("bed_temperature", opt_bed); + auto *opt_bed1st = new ConfigOptionInts(1, spin_bed->GetValue()); + config.set_key_value("first_layer_bed_temperature", opt_bed1st); +} + + +// Index + +ConfigWizardIndex::ConfigWizardIndex(wxWindow *parent) + : wxPanel(parent) + , bg(ScalableBitmap(parent, "PrusaSlicer_192px_transparent.png", 192)) + , bullet_black(ScalableBitmap(parent, "bullet_black.png")) + , bullet_blue(ScalableBitmap(parent, "bullet_blue.png")) + , bullet_white(ScalableBitmap(parent, "bullet_white.png")) + , item_active(NO_ITEM) + , item_hover(NO_ITEM) + , last_page((size_t)-1) +{ +#ifndef __WXOSX__ + SetDoubleBuffered(true);// SetDoubleBuffered exists on Win and Linux/GTK, but is missing on OSX +#endif //__WXOSX__ + SetMinSize(bg.GetSize()); + + const wxSize size = GetTextExtent("m"); + em_w = size.x; + em_h = size.y; + + Bind(wxEVT_PAINT, &ConfigWizardIndex::on_paint, this); + Bind(wxEVT_SIZE, [this](wxEvent& e) { e.Skip(); Refresh(); }); + Bind(wxEVT_MOTION, &ConfigWizardIndex::on_mouse_move, this); + + Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent &evt) { + if (item_hover != -1) { + item_hover = -1; + Refresh(); + } + evt.Skip(); + }); + + Bind(wxEVT_LEFT_UP, [this](wxMouseEvent &evt) { + if (item_hover >= 0) { go_to(item_hover); } + }); +} + +wxDECLARE_EVENT(EVT_INDEX_PAGE, wxCommandEvent); + +void ConfigWizardIndex::add_page(ConfigWizardPage *page) +{ + last_page = items.size(); + items.emplace_back(Item { page->shortname, page->indent, page }); + Refresh(); +} + +void ConfigWizardIndex::add_label(wxString label, unsigned indent) +{ + items.emplace_back(Item { std::move(label), indent, nullptr }); + Refresh(); +} + +ConfigWizardPage* ConfigWizardIndex::active_page() const +{ + if (item_active >= items.size()) { return nullptr; } + + return items[item_active].page; +} + +void ConfigWizardIndex::go_prev() +{ + // Search for a preceiding item that is a page (not a label, ie. page != nullptr) + + if (item_active == NO_ITEM) { return; } + + for (size_t i = item_active; i > 0; i--) { + if (items[i - 1].page != nullptr) { + go_to(i - 1); + return; + } + } +} + +void ConfigWizardIndex::go_next() +{ + // Search for a next item that is a page (not a label, ie. page != nullptr) + + if (item_active == NO_ITEM) { return; } + + for (size_t i = item_active + 1; i < items.size(); i++) { + if (items[i].page != nullptr) { + go_to(i); + return; + } + } +} + +// This one actually performs the go-to op +void ConfigWizardIndex::go_to(size_t i) +{ + if (i != item_active + && i < items.size() + && items[i].page != nullptr) { + auto *new_active = items[i].page; + auto *former_active = active_page(); + if (former_active != nullptr) { + former_active->Hide(); + } + + item_active = i; + new_active->Show(); + + wxCommandEvent evt(EVT_INDEX_PAGE, GetId()); + AddPendingEvent(evt); + + Refresh(); + + new_active->on_activate(); + } +} + +void ConfigWizardIndex::go_to(const ConfigWizardPage *page) +{ + if (page == nullptr) { return; } + + for (size_t i = 0; i < items.size(); i++) { + if (items[i].page == page) { + go_to(i); + return; + } + } +} + +void ConfigWizardIndex::clear() +{ + auto *former_active = active_page(); + if (former_active != nullptr) { former_active->Hide(); } + + items.clear(); + item_active = NO_ITEM; +} + +void ConfigWizardIndex::on_paint(wxPaintEvent & evt) +{ + const auto size = GetClientSize(); + if (size.GetHeight() == 0 || size.GetWidth() == 0) { return; } + + wxPaintDC dc(this); + + const auto bullet_w = bullet_black.GetWidth(); + const auto bullet_h = bullet_black.GetHeight(); + const int yoff_icon = bullet_h < em_h ? (em_h - bullet_h) / 2 : 0; + const int yoff_text = bullet_h > em_h ? (bullet_h - em_h) / 2 : 0; + const int yinc = item_height(); + + int index_width = 0; + + unsigned y = 0; + for (size_t i = 0; i < items.size(); i++) { + const Item& item = items[i]; + unsigned x = em_w/2 + item.indent * em_w; + + if (i == item_active || (item_hover >= 0 && i == (size_t)item_hover)) { + dc.DrawBitmap(bullet_blue.get_bitmap(), x, y + yoff_icon, false); + } + else if (i < item_active) { dc.DrawBitmap(bullet_black.get_bitmap(), x, y + yoff_icon, false); } + else if (i > item_active) { dc.DrawBitmap(bullet_white.get_bitmap(), x, y + yoff_icon, false); } + + x += + bullet_w + em_w/2; + const auto text_size = dc.GetTextExtent(item.label); + dc.SetTextForeground(wxGetApp().get_label_clr_default()); + dc.DrawText(item.label, x, y + yoff_text); + + y += yinc; + index_width = std::max(index_width, (int)x + text_size.x); + } + + //draw logo + if (int y = size.y - bg.GetHeight(); y>=0) { + dc.DrawBitmap(bg.get_bitmap(), 0, y, false); + index_width = std::max(index_width, bg.GetWidth() + em_w / 2); + } + + if (GetMinSize().x < index_width) { + CallAfter([this, index_width]() { + SetMinSize(wxSize(index_width, GetMinSize().y)); + Refresh(); + }); + } +} + +void ConfigWizardIndex::on_mouse_move(wxMouseEvent &evt) +{ + const wxClientDC dc(this); + const wxPoint pos = evt.GetLogicalPosition(dc); + + const ssize_t item_hover_new = pos.y / item_height(); + + if (item_hover_new < ssize_t(items.size()) && item_hover_new != item_hover) { + item_hover = item_hover_new; + Refresh(); + } + + evt.Skip(); +} + +void ConfigWizardIndex::msw_rescale() +{ + const wxSize size = GetTextExtent("m"); + em_w = size.x; + em_h = size.y; + + SetMinSize(bg.GetSize()); + + Refresh(); +} + + +// Materials + +const std::string Materials::UNKNOWN = "(Unknown)"; + +void Materials::push(const Preset *preset) +{ + presets.emplace_back(preset); + types.insert(technology & T_FFF + ? Materials::get_filament_type(preset) + : Materials::get_material_type(preset)); +} + +void Materials::add_printer(const Preset* preset) +{ + printers.insert(preset); +} + +void Materials::clear() +{ + presets.clear(); + types.clear(); + printers.clear(); + compatibility_counter.clear(); +} + +const std::string& Materials::appconfig_section() const +{ + return (technology & T_FFF) ? AppConfig::SECTION_FILAMENTS : AppConfig::SECTION_MATERIALS; +} + +const std::string& Materials::get_type(const Preset *preset) const +{ + return (technology & T_FFF) ? get_filament_type(preset) : get_material_type(preset); +} + +const std::string& Materials::get_vendor(const Preset *preset) const +{ + return (technology & T_FFF) ? get_filament_vendor(preset) : get_material_vendor(preset); +} + +const std::string& Materials::get_filament_type(const Preset *preset) +{ + const auto *opt = preset->config.opt("filament_type"); + if (opt != nullptr && opt->values.size() > 0) { + return opt->values[0]; + } else { + return UNKNOWN; + } +} + +const std::string& Materials::get_filament_vendor(const Preset *preset) +{ + const auto *opt = preset->config.opt("filament_vendor"); + return opt != nullptr ? opt->value : UNKNOWN; +} + +const std::string& Materials::get_material_type(const Preset *preset) +{ + const auto *opt = preset->config.opt("material_type"); + if (opt != nullptr) { + return opt->value; + } else { + return UNKNOWN; + } +} + +const std::string& Materials::get_material_vendor(const Preset *preset) +{ + const auto *opt = preset->config.opt("material_vendor"); + return opt != nullptr ? opt->value : UNKNOWN; +} + +// priv + +static const std::unordered_map> legacy_preset_map {{ + { "Original Prusa i3 MK2.ini", std::make_pair("MK2S", "0.4") }, + { "Original Prusa i3 MK2 MM Single Mode.ini", std::make_pair("MK2SMM", "0.4") }, + { "Original Prusa i3 MK2 MM Single Mode 0.6 nozzle.ini", std::make_pair("MK2SMM", "0.6") }, + { "Original Prusa i3 MK2 MultiMaterial.ini", std::make_pair("MK2SMM", "0.4") }, + { "Original Prusa i3 MK2 MultiMaterial 0.6 nozzle.ini", std::make_pair("MK2SMM", "0.6") }, + { "Original Prusa i3 MK2 0.25 nozzle.ini", std::make_pair("MK2S", "0.25") }, + { "Original Prusa i3 MK2 0.6 nozzle.ini", std::make_pair("MK2S", "0.6") }, + { "Original Prusa i3 MK3.ini", std::make_pair("MK3", "0.4") }, +}}; + +void ConfigWizard::priv::load_pages() +{ + wxWindowUpdateLocker freeze_guard(q); + (void)freeze_guard; + + const ConfigWizardPage *former_active = index->active_page(); + + index->clear(); + + index->add_page(page_welcome); + + // Printers + if (!only_sla_mode) + index->add_page(page_fff); + index->add_page(page_msla); + if (!only_sla_mode) { + index->add_page(page_vendors); + for (const auto &pages : pages_3rdparty) { + for ( PagePrinters* page : { pages.second.first, pages.second.second }) + if (page && page->install) + index->add_page(page); + } + + index->add_page(page_custom); + if (page_custom->custom_wanted()) { + index->add_page(page_firmware); + index->add_page(page_bed); + index->add_page(page_bvolume); + index->add_page(page_diams); + index->add_page(page_temps); + } + + // Filaments & Materials + if (any_fff_selected) { index->add_page(page_filaments); } + // Filaments page if only custom printer is selected + const AppConfig* app_config = wxGetApp().app_config; + if (!any_fff_selected && (custom_printer_selected || custom_printer_in_bundle) && (app_config->get("no_templates") == "0")) { + update_materials(T_ANY); + index->add_page(page_filaments); + } + } + if (any_sla_selected) { index->add_page(page_sla_materials); } + + // there should to be selected at least one printer + btn_finish->Enable(any_fff_selected || any_sla_selected || custom_printer_selected || custom_printer_in_bundle); + + index->add_page(page_update); + index->add_page(page_downloader); + index->add_page(page_reload_from_disk); +#ifdef _WIN32 + index->add_page(page_files_association); +#endif // _WIN32 + index->add_page(page_mode); + + index->go_to(former_active); // Will restore the active item/page if possible + + q->Layout(); +// This Refresh() is needed to avoid ugly artifacts after printer selection, when no one vendor was selected from the very beginnig + q->Refresh(); +} + +void ConfigWizard::priv::init_dialog_size() +{ + // Clamp the Wizard size based on screen dimensions + + const auto idx = wxDisplay::GetFromWindow(q); + wxDisplay display(idx != wxNOT_FOUND ? idx : 0u); + + const auto disp_rect = display.GetClientArea(); + wxRect window_rect( + disp_rect.x + disp_rect.width / 20, + disp_rect.y + disp_rect.height / 20, + 9*disp_rect.width / 10, + 9*disp_rect.height / 10); + + const int width_hint = index->GetSize().GetWidth() + std::max(90 * em(), (only_sla_mode ? page_msla->get_width() : page_fff->get_width()) + 30 * em()); // XXX: magic constant, I found no better solution + if (width_hint < window_rect.width) { + window_rect.x += (window_rect.width - width_hint) / 2; + window_rect.width = width_hint; + } + + q->SetSize(window_rect); +} + +void ConfigWizard::priv::load_vendors() +{ + bundles = BundleMap::load(); + + // Load up the set of vendors / models / variants the user has had enabled up till now + AppConfig *app_config = wxGetApp().app_config; + if (! app_config->legacy_datadir()) { + appconfig_new.set_vendors(*app_config); + } else { + // In case of legacy datadir, try to guess the preference based on the printer preset files that are present + const auto printer_dir = fs::path(Slic3r::data_dir()) / "printer"; + for (auto &dir_entry : boost::filesystem::directory_iterator(printer_dir)) + if (Slic3r::is_ini_file(dir_entry)) { + auto needle = legacy_preset_map.find(dir_entry.path().filename().string()); + if (needle == legacy_preset_map.end()) { continue; } + + const auto &model = needle->second.first; + const auto &variant = needle->second.second; + appconfig_new.set_variant("PrusaResearch", model, variant, true); + } + } + + for (const auto& printer : wxGetApp().preset_bundle->printers) { + if (!printer.is_default && !printer.is_system && printer.is_visible) { + custom_printer_in_bundle = true; + break; + } + } + + // Initialize the is_visible flag in printer Presets + for (auto &pair : bundles) { + pair.second.preset_bundle->load_installed_printers(appconfig_new); + } + + // Copy installed filaments and SLA material names from app_config to appconfig_new + // while resolving current names of profiles, which were renamed in the meantime. + for (PrinterTechnology technology : { ptFFF, ptSLA }) { + const std::string §ion_name = (technology == ptFFF) ? AppConfig::SECTION_FILAMENTS : AppConfig::SECTION_MATERIALS; + std::map section_new; + if (app_config->has_section(section_name)) { + const std::map §ion_old = app_config->get_section(section_name); + for (const auto& material_name_and_installed : section_old) + if (material_name_and_installed.second == "1") { + // Material is installed. Resolve it in bundles. + size_t num_found = 0; + const std::string &material_name = material_name_and_installed.first; + for (auto &bundle : bundles) { + const PresetCollection &materials = bundle.second.preset_bundle->materials(technology); + const Preset *preset = materials.find_preset(material_name); + if (preset == nullptr) { + // Not found. Maybe the material preset is there, bu it was was renamed? + const std::string *new_name = materials.get_preset_name_renamed(material_name); + if (new_name != nullptr) + preset = materials.find_preset(*new_name); + } + if (preset != nullptr) { + // Materal preset was found, mark it as installed. + section_new[preset->name] = "1"; + ++ num_found; + } + } + if (num_found == 0) + BOOST_LOG_TRIVIAL(error) << boost::format("Profile %1% was not found in installed vendor Preset Bundles.") % material_name; + else if (num_found > 1) + BOOST_LOG_TRIVIAL(error) << boost::format("Profile %1% was found in %2% vendor Preset Bundles.") % material_name % num_found; + } + } + appconfig_new.set_section(section_name, section_new); + }; +} + +void ConfigWizard::priv::add_page(ConfigWizardPage *page) +{ + const int proportion = (page->shortname == _L("Filaments")) || (page->shortname == _L("SLA Materials")) ? 1 : 0; + hscroll_sizer->Add(page, proportion, wxEXPAND); + all_pages.push_back(page); +} + +void ConfigWizard::priv::enable_next(bool enable) +{ + btn_next->Enable(enable); + btn_finish->Enable(enable); +} + +void ConfigWizard::priv::set_start_page(ConfigWizard::StartPage start_page) +{ + switch (start_page) { + case ConfigWizard::SP_PRINTERS: + index->go_to(page_fff); + btn_next->SetFocus(); + break; + case ConfigWizard::SP_FILAMENTS: + index->go_to(page_filaments); + btn_finish->SetFocus(); + break; + case ConfigWizard::SP_MATERIALS: + index->go_to(page_sla_materials); + btn_finish->SetFocus(); + break; + default: + index->go_to(page_welcome); + btn_next->SetFocus(); + break; + } +} + +void ConfigWizard::priv::create_3rdparty_pages() +{ + for (const auto &pair : bundles) { + const VendorProfile *vendor = pair.second.vendor_profile; + if (vendor->id == PresetBundle::PRUSA_BUNDLE) { continue; } + + bool is_fff_technology = false; + bool is_sla_technology = false; + + for (auto& model: vendor->models) + { + if (!is_fff_technology && model.technology == ptFFF) + is_fff_technology = true; + if (!is_sla_technology && model.technology == ptSLA) + is_sla_technology = true; + } + + PagePrinters* pageFFF = nullptr; + PagePrinters* pageSLA = nullptr; + + if (is_fff_technology) { + pageFFF = new PagePrinters(q, vendor->name + " " +_L("FFF Technology Printers"), vendor->name+" FFF", *vendor, 1, T_FFF); + add_page(pageFFF); + } + + if (is_sla_technology) { + pageSLA = new PagePrinters(q, vendor->name + " " + _L("SLA Technology Printers"), vendor->name+" MSLA", *vendor, 1, T_SLA); + add_page(pageSLA); + } + + pages_3rdparty.insert({vendor->id, {pageFFF, pageSLA}}); + } +} + +void ConfigWizard::priv::set_run_reason(RunReason run_reason) +{ + this->run_reason = run_reason; + for (auto &page : all_pages) { + page->set_run_reason(run_reason); + } +} + +void ConfigWizard::priv::update_materials(Technology technology) +{ + if ((any_fff_selected || custom_printer_in_bundle || custom_printer_selected) && (technology & T_FFF)) { + filaments.clear(); + aliases_fff.clear(); + // Iterate filaments in all bundles + for (const auto &pair : bundles) { + for (const auto &filament : pair.second.preset_bundle->filaments) { + // Check if filament is already added + if (filaments.containts(&filament)) + continue; + // Iterate printers in all bundles + for (const auto &printer : pair.second.preset_bundle->printers) { + if (!printer.is_visible || printer.printer_technology() != ptFFF) + continue; + // Filter out inapplicable printers + if (is_compatible_with_printer(PresetWithVendorProfile(filament, filament.vendor), PresetWithVendorProfile(printer, printer.vendor))) { + if (!filaments.containts(&filament)) { + filaments.push(&filament); + if (!filament.alias.empty()) + aliases_fff[filament.alias].insert(filament.name); + } + filaments.add_printer(&printer); + } + } + // template filament bundle has no printers - filament would be never added + if(pair.second.vendor_profile&& pair.second.vendor_profile->templates_profile && pair.second.preset_bundle->printers.begin() == pair.second.preset_bundle->printers.end()) + { + if (!filaments.containts(&filament)) { + filaments.push(&filament); + if (!filament.alias.empty()) + aliases_fff[filament.alias].insert(filament.name); + } + } + } + } + // count compatible printers + for (const auto& preset : filaments.presets) { + // skip template filaments + if (preset->vendor && preset->vendor->templates_profile) + continue; + + const auto filter = [preset](const std::pair element) { + return preset->alias == element.first; + }; + if (std::find_if(filaments.compatibility_counter.begin(), filaments.compatibility_counter.end(), filter) != filaments.compatibility_counter.end()) { + continue; + } + // find all aliases (except templates) + std::vector idx_with_same_alias; + for (size_t i = 0; i < filaments.presets.size(); ++i) { + if (preset->alias == filaments.presets[i]->alias && ((filaments.presets[i]->vendor && !filaments.presets[i]->vendor->templates_profile) || !filaments.presets[i]->vendor)) + idx_with_same_alias.push_back(i); + } + // check compatibility with each printer + size_t counter = 0; + for (const auto& printer : filaments.printers) { + if (!(*printer).is_visible || (*printer).printer_technology() != ptFFF) + continue; + bool compatible = false; + // Test other materials with same alias + for (size_t i = 0; i < idx_with_same_alias.size() && !compatible; ++i) { + const Preset& prst = *(filaments.presets[idx_with_same_alias[i]]); + const Preset& prntr = *printer; + if (is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) { + compatible = true; + break; + } + } + if (compatible) + counter++; + } + filaments.compatibility_counter.emplace_back(preset->alias, counter); + } + } + + if (any_sla_selected && (technology & T_SLA)) { + sla_materials.clear(); + aliases_sla.clear(); + + // Iterate SLA materials in all bundles + for (const auto &pair : bundles) { + for (const auto &material : pair.second.preset_bundle->sla_materials) { + // Check if material is already added + if (sla_materials.containts(&material)) + continue; + // Iterate printers in all bundles + // For now, we only allow the profiles to be compatible with another profiles inside the same bundle. + for (const auto& printer : pair.second.preset_bundle->printers) { + if(!printer.is_visible || printer.printer_technology() != ptSLA) + continue; + // Filter out inapplicable printers + if (is_compatible_with_printer(PresetWithVendorProfile(material, nullptr), PresetWithVendorProfile(printer, nullptr))) { + // Check if material is already added + if(!sla_materials.containts(&material)) { + sla_materials.push(&material); + if (!material.alias.empty()) + aliases_sla[material.alias].insert(material.name); + } + sla_materials.add_printer(&printer); + } + } + } + } + // count compatible printers + for (const auto& preset : sla_materials.presets) { + + const auto filter = [preset](const std::pair element) { + return preset->alias == element.first; + }; + if (std::find_if(sla_materials.compatibility_counter.begin(), sla_materials.compatibility_counter.end(), filter) != sla_materials.compatibility_counter.end()) { + continue; + } + std::vector idx_with_same_alias; + for (size_t i = 0; i < sla_materials.presets.size(); ++i) { + if(preset->alias == sla_materials.presets[i]->alias) + idx_with_same_alias.push_back(i); + } + size_t counter = 0; + for (const auto& printer : sla_materials.printers) { + if (!(*printer).is_visible || (*printer).printer_technology() != ptSLA) + continue; + bool compatible = false; + // Test otrher materials with same alias + for (size_t i = 0; i < idx_with_same_alias.size() && !compatible; ++i) { + const Preset& prst = *(sla_materials.presets[idx_with_same_alias[i]]); + const Preset& prntr = *printer; + if (is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) { + compatible = true; + break; + } + } + if (compatible) + counter++; + } + sla_materials.compatibility_counter.emplace_back(preset->alias, counter); + } + } +} + +void ConfigWizard::priv::on_custom_setup(const bool custom_wanted) +{ + custom_printer_selected = custom_wanted; + load_pages(); +} + +void ConfigWizard::priv::on_printer_pick(PagePrinters *page, const PrinterPickerEvent &evt) +{ + if (check_sla_selected() != any_sla_selected || + check_fff_selected() != any_fff_selected) { + any_fff_selected = check_fff_selected(); + any_sla_selected = check_sla_selected(); + + load_pages(); + } + + // Update the is_visible flag on relevant printer profiles + for (auto &pair : bundles) { + if (pair.first != evt.vendor_id) { continue; } + + for (auto &preset : pair.second.preset_bundle->printers) { + if (preset.config.opt_string("printer_model") == evt.model_id + && preset.config.opt_string("printer_variant") == evt.variant_name) { + preset.is_visible = evt.enable; + } + } + + // When a printer model is picked, but there is no material installed compatible with this printer model, + // install default materials for selected printer model silently. + check_and_install_missing_materials(page->technology, evt.model_id); + } + + if (page->technology & T_FFF) { + page_filaments->clear(); + } else if (page->technology & T_SLA) { + page_sla_materials->clear(); + } +} + +void ConfigWizard::priv::select_default_materials_for_printer_model(const VendorProfile::PrinterModel &printer_model, Technology technology) +{ + PageMaterials* page_materials = technology & T_FFF ? page_filaments : page_sla_materials; + for (const std::string& material : printer_model.default_materials) + appconfig_new.set(page_materials->materials->appconfig_section(), material, "1"); +} + +void ConfigWizard::priv::select_default_materials_for_printer_models(Technology technology, const std::set &printer_models) +{ + PageMaterials *page_materials = technology & T_FFF ? page_filaments : page_sla_materials; + const std::string &appconfig_section = page_materials->materials->appconfig_section(); + + // Following block was unnecessary. Its enough to iterate printer_models once. Not for every vendor printer page. + // Filament is selected on same page for all printers of same technology. + /* + auto select_default_materials_for_printer_page = [this, appconfig_section, printer_models, technology](PagePrinters *page_printers, Technology technology) + { + const std::string vendor_id = page_printers->get_vendor_id(); + for (auto& pair : bundles) + if (pair.first == vendor_id) + for (const VendorProfile::PrinterModel *printer_model : printer_models) + for (const std::string &material : printer_model->default_materials) + appconfig_new.set(appconfig_section, material, "1"); + }; + + PagePrinters* page_printers = technology & T_FFF ? page_fff : page_msla; + select_default_materials_for_printer_page(page_printers, technology); + + for (const auto& printer : pages_3rdparty) + { + page_printers = technology & T_FFF ? printer.second.first : printer.second.second; + if (page_printers) + select_default_materials_for_printer_page(page_printers, technology); + } + */ + + // Iterate printer_models and select default materials. If none available -> msg to user. + std::vector models_without_default; + for (const VendorProfile::PrinterModel* printer_model : printer_models) { + if (printer_model->default_materials.empty()) { + models_without_default.emplace_back(printer_model); + } else { + for (const std::string& material : printer_model->default_materials) + appconfig_new.set(appconfig_section, material, "1"); + } + } + + if (!models_without_default.empty()) { + std::string printer_names = "\n\n"; + for (const VendorProfile::PrinterModel* printer_model : models_without_default) { + printer_names += printer_model->name + "\n"; + } + printer_names += "\n\n"; + std::string message = (technology & T_FFF ? + GUI::format(_L("Following printer profiles has no default filament: %1%Please select one manually."), printer_names) : + GUI::format(_L("Following printer profiles has no default material: %1%Please select one manually."), printer_names)); + MessageDialog msg(q, message, _L("Notice"), wxOK); + msg.ShowModal(); + } + + update_materials(technology); + ((technology & T_FFF) ? page_filaments : page_sla_materials)->reload_presets(); +} + +void ConfigWizard::priv::on_3rdparty_install(const VendorProfile *vendor, bool install) +{ + auto it = pages_3rdparty.find(vendor->id); + wxCHECK_RET(it != pages_3rdparty.end(), "Internal error: GUI page not found for 3rd party vendor profile"); + + for (PagePrinters* page : { it->second.first, it->second.second }) + if (page) { + if (page->install && !install) + page->select_all(false); + page->install = install; + // if some 3rd vendor is selected, select first printer for them + if (install) + page->printer_pickers[0]->select_one(0, true); + page->Layout(); + } + + load_pages(); +} + +bool ConfigWizard::priv::on_bnt_finish() +{ + wxBusyCursor wait; + + if (!page_downloader->on_finish_downloader()) { + index->go_to(page_downloader); + return false; + } + /* When Filaments or Sla Materials pages are activated, + * materials for this pages are automaticaly updated and presets are reloaded. + * + * But, if _Finish_ button was clicked without activation of those pages + * (for example, just some printers were added/deleted), + * than last changes wouldn't be updated for filaments/materials. + * SO, do that before close of Wizard + */ + update_materials(T_ANY); + if (any_fff_selected) + page_filaments->reload_presets(); + if (any_sla_selected) + page_sla_materials->reload_presets(); + + // theres no need to check that filament is selected if we have only custom printer + if (custom_printer_selected && !any_fff_selected && !any_sla_selected) return true; + // check, that there is selected at least one filament/material + return check_and_install_missing_materials(T_ANY); +} + +// This allmighty method verifies, whether there is at least a single compatible filament or SLA material installed +// for each Printer preset of each Printer Model installed. +// +// In case only_for_model_id is set, then the test is done for that particular printer model only, and the default materials are installed silently. +// Otherwise the user is quieried whether to install the missing default materials or not. +// +// Return true if the tested Printer Models already had materials installed. +// Return false if there were some Printer Models with missing materials, independent from whether the defaults were installed for these +// respective Printer Models or not. +bool ConfigWizard::priv::check_and_install_missing_materials(Technology technology, const std::string &only_for_model_id) +{ + // Walk over all installed Printer presets and verify whether there is a filament or SLA material profile installed at the same PresetBundle, + // which is compatible with it. + const auto printer_models_missing_materials = [this, only_for_model_id](PrinterTechnology technology, const std::string §ion) + { + const std::map &appconfig_presets = appconfig_new.has_section(section) ? appconfig_new.get_section(section) : std::map(); + std::set printer_models_without_material; + for (const auto &pair : bundles) { + const PresetCollection &materials = pair.second.preset_bundle->materials(technology); + for (const auto &printer : pair.second.preset_bundle->printers) { + if (printer.is_visible && printer.printer_technology() == technology) { + const VendorProfile::PrinterModel *printer_model = PresetUtils::system_printer_model(printer); + assert(printer_model != nullptr); + if ((only_for_model_id.empty() || only_for_model_id == printer_model->id) && + printer_models_without_material.find(printer_model) == printer_models_without_material.end()) { + bool has_material = false; + for (const auto& preset : appconfig_presets) { + if (preset.second == "1") { + const Preset *material = materials.find_preset(preset.first, false); + if (material != nullptr && is_compatible_with_printer(PresetWithVendorProfile(*material, nullptr), PresetWithVendorProfile(printer, nullptr))) { + has_material = true; + break; + } + + // find if preset.first is part of the templates profile (up is searching if preset.first is part of printer vendor preset) + for (const auto& bp : bundles) { + if (!bp.second.preset_bundle->vendors.empty() && bp.second.preset_bundle->vendors.begin()->second.templates_profile) { + const PresetCollection& template_materials = bp.second.preset_bundle->materials(technology); + const Preset* template_material = template_materials.find_preset(preset.first, false); + if (template_material && is_compatible_with_printer(PresetWithVendorProfile(*template_material, &bp.second.preset_bundle->vendors.begin()->second), PresetWithVendorProfile(printer, nullptr))) { + has_material = true; + break; + } + } + } + if (has_material) + break; + + } + } + if (! has_material) + printer_models_without_material.insert(printer_model); + } + } + } + } + // todo: just workaround so template_profile_selected wont get to false after this function is called for SLA + // this will work unltil there are no SLA template filaments + if (technology == ptFFF) { + // template_profile_selected check + template_profile_selected = false; + for (const auto& bp : bundles) { + if (!bp.second.preset_bundle->vendors.empty() && bp.second.preset_bundle->vendors.begin()->second.templates_profile) { + for (const auto& preset : appconfig_presets) { + const PresetCollection& template_materials = bp.second.preset_bundle->materials(technology); + const Preset* template_material = template_materials.find_preset(preset.first, false); + if (template_material){ + template_profile_selected = true; + break; + } + } + if (template_profile_selected) { + break; + } + } + } + } + assert(printer_models_without_material.empty() || only_for_model_id.empty() || only_for_model_id == (*printer_models_without_material.begin())->id); + return printer_models_without_material; + }; + + const auto ask_and_select_default_materials = [this](const wxString &message, const std::set &printer_models, Technology technology) + { + //wxMessageDialog msg(q, message, _L("Notice"), wxYES_NO); + MessageDialog msg(q, message, _L("Notice"), wxYES_NO); + if (msg.ShowModal() == wxID_YES) + select_default_materials_for_printer_models(technology, printer_models); + }; + + const auto printer_model_list = [](const std::set &printer_models) -> wxString { + wxString out; + for (const VendorProfile::PrinterModel *printer_model : printer_models) { + wxString name = from_u8(printer_model->name); + out += "\t\t"; + out += name; + out += "\n"; + } + return out; + }; + + if (any_fff_selected && (technology & T_FFF)) { + std::set printer_models_without_material = printer_models_missing_materials(ptFFF, AppConfig::SECTION_FILAMENTS); + if (! printer_models_without_material.empty()) { + if (only_for_model_id.empty()) + ask_and_select_default_materials( + _L("The following FFF printer models have no filament selected:") + + "\n\n\t" + + printer_model_list(printer_models_without_material) + + "\n\n\t" + + _L("Do you want to select default filaments for these FFF printer models?"), + printer_models_without_material, + T_FFF); + else + select_default_materials_for_printer_model(**printer_models_without_material.begin(), T_FFF); + return false; + } + } + + if (any_sla_selected && (technology & T_SLA)) { + std::set printer_models_without_material = printer_models_missing_materials(ptSLA, AppConfig::SECTION_MATERIALS); + if (! printer_models_without_material.empty()) { + if (only_for_model_id.empty()) + ask_and_select_default_materials( + _L("The following SLA printer models have no materials selected:") + + "\n\n\t" + + printer_model_list(printer_models_without_material) + + "\n\n\t" + + _L("Do you want to select default SLA materials for these printer models?"), + printer_models_without_material, + T_SLA); + else + select_default_materials_for_printer_model(**printer_models_without_material.begin(), T_SLA); + return false; + } + } + + return true; +} + +static std::set get_new_added_presets(const std::map& old_data, const std::map& new_data) +{ + auto get_aliases = [](const std::map& data) { + std::set old_aliases; + for (auto item : data) { + const std::string& name = item.first; + size_t pos = name.find("@"); + old_aliases.emplace(pos == std::string::npos ? name : name.substr(0, pos-1)); + } + return old_aliases; + }; + + std::set old_aliases = get_aliases(old_data); + std::set new_aliases = get_aliases(new_data); + std::set diff; + std::set_difference(new_aliases.begin(), new_aliases.end(), old_aliases.begin(), old_aliases.end(), std::inserter(diff, diff.begin())); + + return diff; +} + +static std::string get_first_added_preset(const std::map& old_data, const std::map& new_data) +{ + std::set diff = get_new_added_presets(old_data, new_data); + if (diff.empty()) + return std::string(); + return *diff.begin(); +} + +bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater, bool& apply_keeped_changes) +{ + wxString header, caption = _L("Configuration is edited in ConfigWizard"); + const auto enabled_vendors = appconfig_new.vendors(); + const auto enabled_vendors_old = app_config->vendors(); + + bool suppress_sla_printer = model_has_multi_part_objects(wxGetApp().model()); + PrinterTechnology preferred_pt = ptAny; + auto get_preferred_printer_technology = [enabled_vendors, enabled_vendors_old, suppress_sla_printer](const std::string& bundle_name, const Bundle& bundle) { + const auto config = enabled_vendors.find(bundle_name); + PrinterTechnology pt = ptAny; + if (config != enabled_vendors.end()) { + for (const auto& model : bundle.vendor_profile->models) { + if (const auto model_it = config->second.find(model.id); + model_it != config->second.end() && model_it->second.size() > 0) { + pt = model.technology; + const auto config_old = enabled_vendors_old.find(bundle_name); + if (config_old == enabled_vendors_old.end() || config_old->second.find(model.id) == config_old->second.end()) { + // if preferred printer model has SLA printer technology it's important to check the model for multi-part state + if (pt == ptSLA && suppress_sla_printer) + continue; + return pt; + } + + if (const auto model_it_old = config_old->second.find(model.id); + model_it_old == config_old->second.end() || model_it_old->second != model_it->second) { + // if preferred printer model has SLA printer technology it's important to check the model for multi-part state + if (pt == ptSLA && suppress_sla_printer) + continue; + return pt; + } + } + } + } + return pt; + }; + // Prusa printers are considered first, then 3rd party. + if (preferred_pt = get_preferred_printer_technology("PrusaResearch", bundles.prusa_bundle()); + preferred_pt == ptAny || (preferred_pt == ptSLA && suppress_sla_printer)) { + for (const auto& bundle : bundles) { + if (bundle.second.is_prusa_bundle) { continue; } + if (PrinterTechnology pt = get_preferred_printer_technology(bundle.first, bundle.second); pt == ptAny) + continue; + else if (preferred_pt == ptAny) + preferred_pt = pt; + if(!(preferred_pt == ptAny || (preferred_pt == ptSLA && suppress_sla_printer))) + break; + } + } + + if (preferred_pt == ptSLA && !wxGetApp().may_switch_to_SLA_preset(caption)) + return false; + + bool check_unsaved_preset_changes = page_welcome->reset_user_profile(); + if (check_unsaved_preset_changes) + header = _L("All user presets will be deleted."); + int act_btns = ActionButtons::KEEP; + if (!check_unsaved_preset_changes) + act_btns |= ActionButtons::SAVE; + + // Install bundles from resources or cache / vendor if needed: + std::vector install_bundles; + for (const auto &pair : bundles) { + if (pair.second.location == BundleLocation::IN_VENDOR) { continue; } + + if (pair.second.is_prusa_bundle) { + // Always install Prusa bundle, because it has a lot of filaments/materials + // likely to be referenced by other profiles. + install_bundles.emplace_back(pair.first); + continue; + } + + const auto vendor = enabled_vendors.find(pair.first); + if (vendor == enabled_vendors.end()) { + // vendor not found + // if templates vendor and needs to be installed, add it + // then continue + if (template_profile_selected && pair.second.vendor_profile && pair.second.vendor_profile->templates_profile) + install_bundles.emplace_back(pair.first); + continue; + } + + size_t size_sum = 0; + for (const auto &model : vendor->second) { size_sum += model.second.size(); } + + if (size_sum > 0) { + // This vendor needs to be installed + install_bundles.emplace_back(pair.first); + } + } + if (!check_unsaved_preset_changes) + if ((check_unsaved_preset_changes = install_bundles.size() > 0)) + header = _L_PLURAL("A new vendor was installed and one of its printers will be activated", "New vendors were installed and one of theirs printers will be activated", install_bundles.size()); + +#ifdef __linux__ + // Desktop integration on Linux + BOOST_LOG_TRIVIAL(debug) << "ConfigWizard::priv::apply_config integrate_desktop" << page_welcome->integrate_desktop() << " perform_registration_linux " << page_downloader->downloader->get_perform_registration_linux(); + if (page_welcome->integrate_desktop() || page_downloader->downloader->get_perform_registration_linux()) + DesktopIntegrationDialog::perform_desktop_integration(page_downloader->downloader->get_perform_registration_linux()); +#endif + + // Decide whether to create snapshot based on run_reason and the reset profile checkbox + bool snapshot = true; + Snapshot::Reason snapshot_reason = Snapshot::SNAPSHOT_UPGRADE; + switch (run_reason) { + case ConfigWizard::RR_DATA_EMPTY: + snapshot = false; + break; + case ConfigWizard::RR_DATA_LEGACY: + snapshot = true; + break; + case ConfigWizard::RR_DATA_INCOMPAT: + // In this case snapshot has already been taken by + // PresetUpdater with the appropriate reason + snapshot = false; + break; + case ConfigWizard::RR_USER: + snapshot = page_welcome->reset_user_profile(); + snapshot_reason = Snapshot::SNAPSHOT_USER; + break; + } + + if (snapshot && ! take_config_snapshot_cancel_on_error(*app_config, snapshot_reason, "", _u8L("Do you want to continue changing the configuration?"))) + return false; + + if (check_unsaved_preset_changes && + !wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + + if (install_bundles.size() > 0) { + // Install bundles from resources or cache / vendor. + // Don't create snapshot - we've already done that above if applicable. + + bool install_result = updater->install_bundles_rsrc_or_cache_vendor(std::move(install_bundles), false); + if (!install_result) + return false; + } else { + BOOST_LOG_TRIVIAL(info) << "No bundles need to be installed from resources or cache / vendor"; + } + + if (page_welcome->reset_user_profile()) { + BOOST_LOG_TRIVIAL(info) << "Resetting user profiles..."; + preset_bundle->reset(true); + } + + std::string preferred_model; + std::string preferred_variant; + auto get_preferred_printer_model = [enabled_vendors, enabled_vendors_old, preferred_pt](const std::string& bundle_name, const Bundle& bundle, std::string& variant) { + const auto config = enabled_vendors.find(bundle_name); + if (config == enabled_vendors.end()) + return std::string(); + for (const auto& model : bundle.vendor_profile->models) { + if (const auto model_it = config->second.find(model.id); + model_it != config->second.end() && model_it->second.size() > 0 && + preferred_pt == model.technology) { + variant = *model_it->second.begin(); + const auto config_old = enabled_vendors_old.find(bundle_name); + if (config_old == enabled_vendors_old.end()) + return model.id; + const auto model_it_old = config_old->second.find(model.id); + if (model_it_old == config_old->second.end()) + return model.id; + else if (model_it_old->second != model_it->second) { + for (const auto& var : model_it->second) + if (model_it_old->second.find(var) == model_it_old->second.end()) { + variant = var; + return model.id; + } + } + } + } + if (!variant.empty()) + variant.clear(); + return std::string(); + }; + // Prusa printers are considered first, then 3rd party. + if (preferred_model = get_preferred_printer_model("PrusaResearch", bundles.prusa_bundle(), preferred_variant); + preferred_model.empty()) { + for (const auto& bundle : bundles) { + if (bundle.second.is_prusa_bundle) { continue; } + if (preferred_model = get_preferred_printer_model(bundle.first, bundle.second, preferred_variant); + !preferred_model.empty()) + break; + } + } + + // if unsaved changes was not cheched till this moment + if (!check_unsaved_preset_changes) { + if ((check_unsaved_preset_changes = !preferred_model.empty())) { + header = _L("A new Printer was installed and it will be activated."); + if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + } + else if ((check_unsaved_preset_changes = enabled_vendors_old != enabled_vendors)) { + header = _L("Some Printers were uninstalled."); + if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + } + } + + std::string first_added_filament, first_added_sla_material; + auto get_first_added_material_preset = [this, app_config](const std::string& section_name, std::string& first_added_preset) { + if (appconfig_new.has_section(section_name)) { + // get first of new added preset names + const std::map& old_presets = app_config->has_section(section_name) ? app_config->get_section(section_name) : std::map(); + first_added_preset = get_first_added_preset(old_presets, appconfig_new.get_section(section_name)); + } + }; + get_first_added_material_preset(AppConfig::SECTION_FILAMENTS, first_added_filament); + get_first_added_material_preset(AppConfig::SECTION_MATERIALS, first_added_sla_material); + + // if unsaved changes was not cheched till this moment + if (!check_unsaved_preset_changes) { + if ((check_unsaved_preset_changes = !first_added_filament.empty() || !first_added_sla_material.empty())) { + header = !first_added_filament.empty() ? + _L("A new filament was installed and it will be activated.") : + _L("A new SLA material was installed and it will be activated."); + if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + } + else { + auto changed = [app_config, &appconfig_new = std::as_const(this->appconfig_new)](const std::string& section_name) { + return (app_config->has_section(section_name) ? app_config->get_section(section_name) : std::map()) != appconfig_new.get_section(section_name); + }; + bool is_filaments_changed = changed(AppConfig::SECTION_FILAMENTS); + bool is_sla_materials_changed = changed(AppConfig::SECTION_MATERIALS); + if ((check_unsaved_preset_changes = is_filaments_changed || is_sla_materials_changed)) { + header = is_filaments_changed ? _L("Some filaments were uninstalled.") : _L("Some SLA materials were uninstalled."); + if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + } + } + } + + // apply materials in app_config + for (const std::string& section_name : {AppConfig::SECTION_FILAMENTS, AppConfig::SECTION_MATERIALS}) + app_config->set_section(section_name, appconfig_new.get_section(section_name)); + + app_config->set_vendors(appconfig_new); + + app_config->set("notify_release", page_update->version_check ? "all" : "none"); + app_config->set("preset_update", page_update->preset_update ? "1" : "0"); + app_config->set("export_sources_full_pathnames", page_reload_from_disk->full_pathnames ? "1" : "0"); + +#ifdef _WIN32 + app_config->set("associate_3mf", page_files_association->associate_3mf() ? "1" : "0"); + app_config->set("associate_stl", page_files_association->associate_stl() ? "1" : "0"); +// app_config->set("associate_gcode", page_files_association->associate_gcode() ? "1" : "0"); + + if (wxGetApp().is_editor()) { + if (page_files_association->associate_3mf()) + wxGetApp().associate_3mf_files(); + if (page_files_association->associate_stl()) + wxGetApp().associate_stl_files(); + } +// else { +// if (page_files_association->associate_gcode()) +// wxGetApp().associate_gcode_files(); +// } +#endif // _WIN32 + + page_mode->serialize_mode(app_config); + + if (check_unsaved_preset_changes) + preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem, + {preferred_model, preferred_variant, first_added_filament, first_added_sla_material}); + + if (!only_sla_mode && page_custom->custom_wanted() && page_custom->is_valid_profile_name()) { + // if unsaved changes was not cheched till this moment + if (!check_unsaved_preset_changes && + !wxGetApp().check_and_keep_current_preset_changes(caption, _L("Custom printer was installed and it will be activated."), act_btns, &apply_keeped_changes)) + return false; + + page_firmware->apply_custom_config(*custom_config); + page_bed->apply_custom_config(*custom_config); + page_bvolume->apply_custom_config(*custom_config); + page_diams->apply_custom_config(*custom_config); + page_temps->apply_custom_config(*custom_config); + + copy_bed_model_and_texture_if_needed(*custom_config); + + const std::string profile_name = page_custom->profile_name(); + preset_bundle->load_config_from_wizard(profile_name, *custom_config); + } + + // Update the selections from the compatibilty. + preset_bundle->export_selections(*app_config); + + return true; +} +void ConfigWizard::priv::update_presets_in_config(const std::string& section, const std::string& alias_key, bool add) +{ + const PresetAliases& aliases = section == AppConfig::SECTION_FILAMENTS ? aliases_fff : aliases_sla; + + auto update = [this, add](const std::string& s, const std::string& key) { + assert(! s.empty()); + if (add) + appconfig_new.set(s, key, "1"); + else + appconfig_new.erase(s, key); + }; + + // add or delete presets had a same alias + auto it = aliases.find(alias_key); + if (it != aliases.end()) + for (const std::string& name : it->second) + update(section, name); +} + +bool ConfigWizard::priv::check_fff_selected() +{ + bool ret = page_fff->any_selected(); + for (const auto& printer: pages_3rdparty) + if (printer.second.first) // FFF page + ret |= printer.second.first->any_selected(); + return ret; +} + +bool ConfigWizard::priv::check_sla_selected() +{ + bool ret = page_msla->any_selected(); + for (const auto& printer: pages_3rdparty) + if (printer.second.second) // SLA page + ret |= printer.second.second->any_selected(); + return ret; +} + + +// Public + +ConfigWizard::ConfigWizard(wxWindow *parent) + : DPIDialog(parent, wxID_ANY, wxString(SLIC3R_APP_NAME) + " - " + _(name()), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) + , p(new priv(this)) +{ + this->SetFont(wxGetApp().normal_font()); + + p->load_vendors(); + p->custom_config.reset(DynamicPrintConfig::new_from_defaults_keys({ + "gcode_flavor", "bed_shape", "bed_custom_texture", "bed_custom_model", "nozzle_diameter", "filament_diameter", "temperature", "bed_temperature", + })); + + p->index = new ConfigWizardIndex(this); + + auto *vsizer = new wxBoxSizer(wxVERTICAL); + auto *topsizer = new wxBoxSizer(wxHORIZONTAL); + auto* hline = new StaticLine(this); + p->btnsizer = new wxBoxSizer(wxHORIZONTAL); + + // Initially we _do not_ SetScrollRate in order to figure out the overall width of the Wizard without scrolling. + // Later, we compare that to the size of the current screen and set minimum width based on that (see below). + p->hscroll = new wxScrolledWindow(this); + p->hscroll_sizer = new wxBoxSizer(wxHORIZONTAL); + p->hscroll->SetSizer(p->hscroll_sizer); + + topsizer->Add(p->index, 0, wxEXPAND); + topsizer->AddSpacer(INDEX_MARGIN); + topsizer->Add(p->hscroll, 1, wxEXPAND); + + p->btn_sel_all = new wxButton(this, wxID_ANY, _L("Select all standard printers")); + p->btnsizer->Add(p->btn_sel_all); + + p->btn_prev = new wxButton(this, wxID_ANY, _L("< &Back")); + p->btn_next = new wxButton(this, wxID_ANY, _L("&Next >")); + p->btn_finish = new wxButton(this, wxID_APPLY, _L("&Finish")); + p->btn_cancel = new wxButton(this, wxID_CANCEL, _L("Cancel")); // Note: The label needs to be present, otherwise we get accelerator bugs on Mac + p->btnsizer->AddStretchSpacer(); + p->btnsizer->Add(p->btn_prev, 0, wxLEFT, BTN_SPACING); + p->btnsizer->Add(p->btn_next, 0, wxLEFT, BTN_SPACING); + p->btnsizer->Add(p->btn_finish, 0, wxLEFT, BTN_SPACING); + p->btnsizer->Add(p->btn_cancel, 0, wxLEFT, BTN_SPACING); + + wxGetApp().UpdateDarkUI(p->btn_sel_all); + wxGetApp().UpdateDarkUI(p->btn_prev); + wxGetApp().UpdateDarkUI(p->btn_next); + wxGetApp().UpdateDarkUI(p->btn_finish); + wxGetApp().UpdateDarkUI(p->btn_cancel); + + const auto prusa_it = p->bundles.find("PrusaResearch"); + wxCHECK_RET(prusa_it != p->bundles.cend(), "Vendor PrusaResearch not found"); + const VendorProfile *vendor_prusa = prusa_it->second.vendor_profile; + + p->add_page(p->page_welcome = new PageWelcome(this)); + + + p->page_fff = new PagePrinters(this, _L("Prusa FFF Technology Printers"), "Prusa FFF", *vendor_prusa, 0, T_FFF); + p->only_sla_mode = !p->page_fff->has_printers; + if (!p->only_sla_mode) { + p->add_page(p->page_fff); + p->page_fff->is_primary_printer_page = true; + } + + + p->page_msla = new PagePrinters(this, _L("Prusa MSLA Technology Printers"), "Prusa MSLA", *vendor_prusa, 0, T_SLA); + p->add_page(p->page_msla); + if (p->only_sla_mode) { + p->page_msla->is_primary_printer_page = true; + } + + if (!p->only_sla_mode) { + // Pages for 3rd party vendors + p->create_3rdparty_pages(); // Needs to be done _before_ creating PageVendors + p->add_page(p->page_vendors = new PageVendors(this)); + p->add_page(p->page_custom = new PageCustom(this)); + p->custom_printer_selected = p->page_custom->custom_wanted(); + } + + p->any_sla_selected = p->check_sla_selected(); + p->any_fff_selected = ! p->only_sla_mode && p->check_fff_selected(); + + p->update_materials(T_ANY); + if (!p->only_sla_mode) + p->add_page(p->page_filaments = new PageMaterials(this, &p->filaments, + _L("Filament Profiles Selection"), _L("Filaments"), _L("Type:") )); + + p->add_page(p->page_sla_materials = new PageMaterials(this, &p->sla_materials, + _L("SLA Material Profiles Selection") + " ", _L("SLA Materials"), _L("Type:") )); + + + p->add_page(p->page_update = new PageUpdate(this)); + p->add_page(p->page_downloader = new PageDownloader(this)); + p->add_page(p->page_reload_from_disk = new PageReloadFromDisk(this)); +#ifdef _WIN32 + p->add_page(p->page_files_association = new PageFilesAssociation(this)); +#endif // _WIN32 + p->add_page(p->page_mode = new PageMode(this)); + p->add_page(p->page_firmware = new PageFirmware(this)); + p->add_page(p->page_bed = new PageBedShape(this)); + p->add_page(p->page_bvolume = new PageBuildVolume(this)); + p->add_page(p->page_diams = new PageDiameters(this)); + p->add_page(p->page_temps = new PageTemperatures(this)); + + p->load_pages(); + p->index->go_to(size_t{0}); + + vsizer->Add(topsizer, 1, wxEXPAND | wxALL, DIALOG_MARGIN); + vsizer->Add(hline, 0, wxEXPAND | wxLEFT | wxRIGHT, VERTICAL_SPACING); + vsizer->Add(p->btnsizer, 0, wxEXPAND | wxALL, DIALOG_MARGIN); + + SetSizer(vsizer); + SetSizerAndFit(vsizer); + + // We can now enable scrolling on hscroll + p->hscroll->SetScrollRate(30, 30); + + on_window_geometry(this, [this]() { + p->init_dialog_size(); + }); + + p->btn_prev->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { this->p->index->go_prev(); }); + + p->btn_next->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) + { + // check, that there is selected at least one filament/material + ConfigWizardPage* active_page = this->p->index->active_page(); + if (// Leaving the filaments or SLA materials page and + (active_page == p->page_filaments || active_page == p->page_sla_materials) && + // some Printer models had no filament or SLA material selected. + ! p->check_and_install_missing_materials(dynamic_cast(active_page)->materials->technology)) + // In that case don't leave the page and the function above queried the user whether to install default materials. + return; + this->p->index->go_next(); + }); + + p->btn_finish->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) + { + if (p->on_bnt_finish()) + this->EndModal(wxID_OK); + }); + + p->btn_sel_all->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { + p->any_sla_selected = true; + p->load_pages(); + p->page_fff->select_all(true, false); + p->page_msla->select_all(true, false); + p->index->go_to(p->page_mode); + }); + + p->index->Bind(EVT_INDEX_PAGE, [this](const wxCommandEvent &) { + const bool is_last = p->index->active_is_last(); + p->btn_next->Show(! is_last); + if (is_last) + p->btn_finish->SetFocus(); + + Layout(); + }); + + if (wxLinux_gtk3) + this->Bind(wxEVT_SHOW, [this, vsizer](const wxShowEvent& e) { + ConfigWizardPage* active_page = p->index->active_page(); + if (!active_page) + return; + for (auto page : p->all_pages) + if (page != active_page) + page->Hide(); + // update best size for the dialog after hiding of the non-active pages + vsizer->SetSizeHints(this); + // set initial dialog size + p->init_dialog_size(); + }); +} + +ConfigWizard::~ConfigWizard() {} + +bool ConfigWizard::run(RunReason reason, StartPage start_page) +{ + BOOST_LOG_TRIVIAL(info) << boost::format("Running ConfigWizard, reason: %1%, start_page: %2%") % reason % start_page; + + GUI_App &app = wxGetApp(); + + p->set_run_reason(reason); + p->set_start_page(start_page); + + if (ShowModal() == wxID_OK) { + bool apply_keeped_changes = false; + if (! p->apply_config(app.app_config, app.preset_bundle, app.preset_updater, apply_keeped_changes)) + return false; + + if (apply_keeped_changes) + app.apply_keeped_preset_modifications(); + + app.app_config->set_legacy_datadir(false); + app.update_mode(); + app.obj_manipul()->update_ui_from_settings(); + BOOST_LOG_TRIVIAL(info) << "ConfigWizard applied"; + return true; + } else { + BOOST_LOG_TRIVIAL(info) << "ConfigWizard cancelled"; + return false; + } +} + +const wxString& ConfigWizard::name(const bool from_menu/* = false*/) +{ + // A different naming convention is used for the Wizard on Windows & GTK vs. OSX. + // Note: Don't call _() macro here. + // This function just return the current name according to the OS. + // Translation is implemented inside GUI_App::add_config_menu() +#if __APPLE__ + static const wxString config_wizard_name = L("Configuration Assistant"); + static const wxString config_wizard_name_menu = L("Configuration &Assistant"); +#else + static const wxString config_wizard_name = L("Configuration Wizard"); + static const wxString config_wizard_name_menu = L("Configuration &Wizard"); +#endif + return from_menu ? config_wizard_name_menu : config_wizard_name; +} + +void ConfigWizard::on_dpi_changed(const wxRect &suggested_rect) +{ + p->index->msw_rescale(); + + const int em = em_unit(); + + msw_buttons_rescale(this, em, { wxID_APPLY, + wxID_CANCEL, + p->btn_sel_all->GetId(), + p->btn_next->GetId(), + p->btn_prev->GetId() }); + + for (auto printer_picker: p->page_fff->printer_pickers) + msw_buttons_rescale(this, em, printer_picker->get_button_indexes()); + + p->init_dialog_size(); + + Refresh(); +} + +void ConfigWizard::on_sys_color_changed() +{ + wxGetApp().UpdateDlgDarkUI(this); + Refresh(); +} + +} +} diff --git a/src/slic3r/GUI/ConfigWizard_private.hpp b/src/slic3r/GUI/ConfigWizard_private.hpp index a8ac09d9b..17c9fb25f 100644 --- a/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/src/slic3r/GUI/ConfigWizard_private.hpp @@ -60,18 +60,25 @@ enum Technology { T_ANY = ~0, }; +enum BundleLocation{ + IN_VENDOR, + IN_ARCHIVE, + IN_RESOURCES +}; + struct Bundle { std::unique_ptr preset_bundle; VendorProfile* vendor_profile{ nullptr }; - bool is_in_resources{ false }; + //bool is_in_resources{ false }; + BundleLocation location; bool is_prusa_bundle{ false }; Bundle() = default; Bundle(Bundle&& other); // Returns false if not loaded. Reason for that is logged as boost::log error. - bool load(fs::path source_path, bool is_in_resources, bool is_prusa_bundle = false); + bool load(fs::path source_path, BundleLocation location, bool is_prusa_bundle = false); const std::string& vendor_id() const { return vendor_profile->id; } }; @@ -84,74 +91,8 @@ struct BundleMap : std::map const Bundle& prusa_bundle() const; }; -struct Materials -{ - Technology technology; - // use vector for the presets to purpose of save of presets sorting in the bundle - std::vector presets; - // String is alias of material, size_t number of compatible counters - std::vector> compatibility_counter; - std::set types; - std::set printers; +struct Materials; - Materials(Technology technology) : technology(technology) {} - - void push(const Preset *preset); - void add_printer(const Preset* preset); - void clear(); - bool containts(const Preset *preset) const { - //return std::find(presets.begin(), presets.end(), preset) != presets.end(); - return std::find_if(presets.begin(), presets.end(), - [preset](const Preset* element) { return element == preset; }) != presets.end(); - - } - - bool get_omnipresent(const Preset* preset) { - return get_printer_counter(preset) == printers.size(); - } - - const std::vector get_presets_by_alias(const std::string name) { - std::vector ret_vec; - for (auto it = presets.begin(); it != presets.end(); ++it) { - if ((*it)->alias == name) - ret_vec.push_back((*it)); - } - return ret_vec; - } - - - - size_t get_printer_counter(const Preset* preset) { - for (auto it : compatibility_counter) { - if (it.first == preset->alias) - return it.second; - } - return 0; - } - - const std::string& appconfig_section() const; - const std::string& get_type(const Preset *preset) const; - const std::string& get_vendor(const Preset *preset) const; - - template void filter_presets(const Preset* printer, const std::string& type, const std::string& vendor, F cb) { - for (auto preset : presets) { - const Preset& prst = *(preset); - const Preset& prntr = *printer; - if ((printer == nullptr || is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) && - (type.empty() || get_type(preset) == type) && - (vendor.empty() || get_vendor(preset) == vendor)) { - - cb(preset); - } - } - } - - static const std::string UNKNOWN; - static const std::string& get_filament_type(const Preset *preset); - static const std::string& get_filament_vendor(const Preset *preset); - static const std::string& get_material_type(const Preset *preset); - static const std::string& get_material_vendor(const Preset *preset); -}; struct PrinterPickerEvent; @@ -344,6 +285,10 @@ struct PageMaterials: ConfigWizardPage std::string empty_printers_label; bool first_paint = { false }; static const std::string EMPTY; + static const std::string TEMPLATES; + // notify user first time they choose template profile + bool template_shown = { false }; + bool notification_shown = { false }; int last_hovered_item = { -1 } ; PageMaterials(ConfigWizard *parent, Materials *materials, wxString title, wxString shortname, wxString list1name); @@ -368,6 +313,82 @@ struct PageMaterials: ConfigWizardPage virtual void on_activate() override; }; +struct Materials +{ + Technology technology; + // use vector for the presets to purpose of save of presets sorting in the bundle + std::vector presets; + // String is alias of material, size_t number of compatible counters + std::vector> compatibility_counter; + std::set types; + std::set printers; + + Materials(Technology technology) : technology(technology) {} + + void push(const Preset* preset); + void add_printer(const Preset* preset); + void clear(); + bool containts(const Preset* preset) const { + //return std::find(presets.begin(), presets.end(), preset) != presets.end(); + return std::find_if(presets.begin(), presets.end(), + [preset](const Preset* element) { return element == preset; }) != presets.end(); + + } + + bool get_omnipresent(const Preset* preset) { + return get_printer_counter(preset) == printers.size(); + } + + const std::vector get_presets_by_alias(const std::string name) { + std::vector ret_vec; + for (auto it = presets.begin(); it != presets.end(); ++it) { + if ((*it)->alias == name) + ret_vec.push_back((*it)); + } + return ret_vec; + } + + + + size_t get_printer_counter(const Preset* preset) { + for (auto it : compatibility_counter) { + if (it.first == preset->alias) + return it.second; + } + return 0; + } + + const std::string& appconfig_section() const; + const std::string& get_type(const Preset* preset) const; + const std::string& get_vendor(const Preset* preset) const; + + template void filter_presets(const Preset* printer, const std::string& printer_name, const std::string& type, const std::string& vendor, F cb) { + for (auto preset : presets) { + const Preset& prst = *(preset); + const Preset& prntr = *printer; + if (((printer == nullptr && printer_name == PageMaterials::EMPTY) || (printer != nullptr && is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor)))) && + (type.empty() || get_type(preset) == type) && + (vendor.empty() || get_vendor(preset) == vendor) && + prst.vendor && !prst.vendor->templates_profile) { + + cb(preset); + } + else if ((printer == nullptr && printer_name == PageMaterials::TEMPLATES) && prst.vendor && prst.vendor->templates_profile && + (type.empty() || get_type(preset) == type) && + (vendor.empty() || get_vendor(preset) == vendor)) { + cb(preset); + } + } + } + + static const std::string UNKNOWN; + static const std::string& get_filament_type(const Preset* preset); + static const std::string& get_filament_vendor(const Preset* preset); + static const std::string& get_material_type(const Preset* preset); + static const std::string& get_material_vendor(const Preset* preset); +}; + + struct PageCustom: ConfigWizardPage { PageCustom(ConfigWizard *parent); @@ -608,9 +629,11 @@ struct ConfigWizard::priv std::unique_ptr custom_config; // Backing for custom printer definition bool any_fff_selected; // Used to decide whether to display Filaments page bool any_sla_selected; // Used to decide whether to display SLA Materials page - bool custom_printer_selected { false }; + bool custom_printer_selected { false }; // New custom printer is requested + bool custom_printer_in_bundle { false }; // Older custom printer already exists when wizard starts // Set to true if there are none FFF printers on the main FFF page. If true, only SLA printers are shown (not even custum printers) bool only_sla_mode { false }; + bool template_profile_selected { false }; // This bool has one purpose - to tell that template profile should be installed if its not (because it cannot be added to appconfig) wxScrolledWindow *hscroll = nullptr; wxBoxSizer *hscroll_sizer = nullptr; diff --git a/src/slic3r/GUI/DownloaderFileGet.cpp b/src/slic3r/GUI/DownloaderFileGet.cpp index deac45ad5..d81261296 100644 --- a/src/slic3r/GUI/DownloaderFileGet.cpp +++ b/src/slic3r/GUI/DownloaderFileGet.cpp @@ -119,8 +119,8 @@ void FileGet::priv::get_perform() wxString temp_path_wstring(m_tmp_path.wstring()); - std::cout << "dest_path: " << dest_path.string() << std::endl; - std::cout << "m_tmp_path: " << m_tmp_path.string() << std::endl; + //std::cout << "dest_path: " << dest_path.string() << std::endl; + //std::cout << "m_tmp_path: " << m_tmp_path.string() << std::endl; BOOST_LOG_TRIVIAL(info) << GUI::format("Starting download from %1% to %2%. Temp path is %3%",m_url, dest_path, m_tmp_path); @@ -163,6 +163,8 @@ void FileGet::priv::get_perform() m_stopped = true; fclose(file); cancel = true; + if (m_written == 0) + std::remove(m_tmp_path.string().c_str()); wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_PAUSED); evt->SetInt(m_id); m_evt_handler->QueueEvent(evt); diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp index 419d48d5b..4e087c98f 100644 --- a/src/slic3r/GUI/Field.cpp +++ b/src/slic3r/GUI/Field.cpp @@ -762,28 +762,26 @@ void SpinCtrl::BUILD() { if (m_opt.width >= 0) size.SetWidth(m_opt.width*m_em_unit); wxString text_value = wxString(""); - int default_value = 0; + int default_value = UNDEF_VALUE; switch (m_opt.type) { case coInt: default_value = m_opt.default_value->getInt(); - text_value = wxString::Format(_T("%i"), default_value); break; case coInts: { - const ConfigOptionInts *vec = m_opt.get_default_value(); - if (vec == nullptr || vec->empty()) break; - for (size_t id = 0; id < vec->size(); ++id) - { - default_value = vec->get_at(id); - text_value += wxString::Format(_T("%i"), default_value); - } + default_value = m_opt.get_default_value()->get_at(m_opt_idx); + if (m_opt.nullable) + m_last_meaningful_value = default_value == ConfigOptionIntsNullable::nil_value() ? static_cast(m_opt.max) : default_value; break; } default: break; } + if (default_value != UNDEF_VALUE) + text_value = wxString::Format(_T("%i"), default_value); + const int min_val = m_opt.min == -FLT_MAX #ifdef __WXOSX__ // We will forcibly set the input value for SpinControl, since the value @@ -882,6 +880,50 @@ void SpinCtrl::BUILD() { window = dynamic_cast(temp); } +void SpinCtrl::set_value(const boost::any& value, bool change_event/* = false*/) +{ + m_disable_change_event = !change_event; + tmp_value = boost::any_cast(value); + m_value = value; + if (m_opt.nullable) { + const bool m_is_na_val = tmp_value == ConfigOptionIntsNullable::nil_value(); + if (m_is_na_val) + dynamic_cast(window)->SetValue(na_value()); + else { + m_last_meaningful_value = value; + dynamic_cast(window)->SetValue(tmp_value); + } + } + else + dynamic_cast(window)->SetValue(tmp_value); + m_disable_change_event = false; +} + +void SpinCtrl::set_last_meaningful_value() +{ + const int val = boost::any_cast(m_last_meaningful_value); + dynamic_cast(window)->SetValue(val); + tmp_value = val; + propagate_value(); +} + +void SpinCtrl::set_na_value() +{ + dynamic_cast(window)->SetValue(na_value()); + m_value = ConfigOptionIntsNullable::nil_value(); + propagate_value(); +} + +boost::any& SpinCtrl::get_value() +{ + wxSpinCtrl* spin = static_cast(window); + if (spin->GetTextValue() == na_value()) + return m_value; + + int value = spin->GetValue(); + return m_value = value; +} + void SpinCtrl::propagate_value() { // check if value was really changed diff --git a/src/slic3r/GUI/Field.hpp b/src/slic3r/GUI/Field.hpp index eaa4fe481..73af0ef53 100644 --- a/src/slic3r/GUI/Field.hpp +++ b/src/slic3r/GUI/Field.hpp @@ -330,24 +330,18 @@ public: void BUILD() override; /// Propagate value from field to the OptionGroupe and Config after kill_focus/ENTER void propagate_value() ; - +/* void set_value(const std::string& value, bool change_event = false) { m_disable_change_event = !change_event; dynamic_cast(window)->SetValue(value); m_disable_change_event = false; } - void set_value(const boost::any& value, bool change_event = false) override { - m_disable_change_event = !change_event; - tmp_value = boost::any_cast(value); - m_value = value; - dynamic_cast(window)->SetValue(tmp_value); - m_disable_change_event = false; - } +*/ + void set_value(const boost::any& value, bool change_event = false) override; + void set_last_meaningful_value() override; + void set_na_value() override; - boost::any& get_value() override { - int value = static_cast(window)->GetValue(); - return m_value = value; - } + boost::any& get_value() override; void msw_rescale() override; diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index fdfb6bfe8..84a628572 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -3182,14 +3182,10 @@ void GCodeViewer::render_shells() if (shader == nullptr) return; -// glsafe(::glDepthMask(GL_FALSE)); - shader->start_using(); const Camera& camera = wxGetApp().plater()->get_camera(); m_shells.volumes.render(GLVolumeCollection::ERenderType::Transparent, true, camera.get_view_matrix(), camera.get_projection_matrix()); shader->stop_using(); - -// glsafe(::glDepthMask(GL_TRUE)); } void GCodeViewer::render_legend(float& legend_height) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index d8a005b34..0adfdb92e 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1515,12 +1515,12 @@ void GLCanvas3D::render() _render_background(); _render_objects(GLVolumeCollection::ERenderType::Opaque); - if (!m_main_toolbar.is_enabled()) - _render_gcode(); _render_sla_slices(); _render_selection(); if (is_looking_downward) _render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), false, true); + if (!m_main_toolbar.is_enabled()) + _render_gcode(); _render_objects(GLVolumeCollection::ERenderType::Transparent); _render_sequential_clearance(); @@ -1823,15 +1823,6 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re size_t volume_idx; }; - // SLA steps to pull the preview meshes for. - typedef std::array SLASteps; - SLASteps sla_steps = { slaposDrillHoles, slaposSupportTree, slaposPad }; - struct SLASupportState { - std::array::value> step; - }; - // State of the sla_steps for all SLAPrintObjects. - std::vector sla_support_state; - std::vector instance_ids_selected; std::vector map_glvolume_old_to_new(m_volumes.volumes.size(), size_t(-1)); std::vector deleted_volumes; @@ -1856,33 +1847,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re } } } - if (printer_technology == ptSLA) { - const SLAPrint* sla_print = this->sla_print(); -#ifndef NDEBUG - // Verify that the SLAPrint object is synchronized with m_model. - check_model_ids_equal(*m_model, sla_print->model()); -#endif /* NDEBUG */ - sla_support_state.reserve(sla_print->objects().size()); - for (const SLAPrintObject* print_object : sla_print->objects()) { - SLASupportState state; - for (size_t istep = 0; istep < sla_steps.size(); ++istep) { - state.step[istep] = print_object->step_state_with_timestamp(sla_steps[istep]); - if (state.step[istep].is_done()) { - if (!print_object->has_mesh(sla_steps[istep])) - // Consider the Done step without a valid mesh as invalid for the purpose - // of mesh visualization. - state.step[istep].state = PrintStateBase::State::Fresh; - else if (sla_steps[istep] != slaposDrillHoles) - for (const ModelInstance* model_instance : print_object->model_object()->instances) - // Only the instances, which are currently printable, will have the SLA support structures kept. - // The instances outside the print bed will have the GLVolumes of their support structures released. - if (model_instance->is_printable()) - aux_volume_state.emplace_back(state.step[istep].timestamp, model_instance->id()); - } - } - sla_support_state.emplace_back(state); - } - } + std::sort(model_volume_state.begin(), model_volume_state.end(), model_volume_state_lower); std::sort(aux_volume_state.begin(), aux_volume_state.end(), model_volume_state_lower); // Release all ModelVolume based GLVolumes not found in the current Model. Find the GLVolume of a hollowed mesh. @@ -1996,112 +1961,6 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re } } } - if (printer_technology == ptSLA) { - size_t idx = 0; - const SLAPrint *sla_print = this->sla_print(); - std::vector shift_zs(m_model->objects.size(), 0); - double relative_correction_z = sla_print->relative_correction().z(); - if (relative_correction_z <= EPSILON) - relative_correction_z = 1.; - for (const SLAPrintObject *print_object : sla_print->objects()) { - SLASupportState &state = sla_support_state[idx ++]; - const ModelObject *model_object = print_object->model_object(); - // Find an index of the ModelObject - int object_idx; - // There may be new SLA volumes added to the scene for this print_object. - // Find the object index of this print_object in the Model::objects list. - auto it = std::find(sla_print->model().objects.begin(), sla_print->model().objects.end(), model_object); - assert(it != sla_print->model().objects.end()); - object_idx = it - sla_print->model().objects.begin(); - // Cache the Z offset to be applied to all volumes with this object_idx. - shift_zs[object_idx] = print_object->get_current_elevation() / relative_correction_z; - // Collect indices of this print_object's instances, for which the SLA support meshes are to be added to the scene. - // pairs of - std::vector> instances[std::tuple_size::value]; - for (size_t print_instance_idx = 0; print_instance_idx < print_object->instances().size(); ++ print_instance_idx) { - const SLAPrintObject::Instance &instance = print_object->instances()[print_instance_idx]; - // Find index of ModelInstance corresponding to this SLAPrintObject::Instance. - auto it = std::find_if(model_object->instances.begin(), model_object->instances.end(), - [&instance](const ModelInstance *mi) { return mi->id() == instance.instance_id; }); - assert(it != model_object->instances.end()); - int instance_idx = it - model_object->instances.begin(); - for (size_t istep = 0; istep < sla_steps.size(); ++ istep) - if (sla_steps[istep] == slaposDrillHoles) { - // Hollowing is a special case, where the mesh from the backend is being loaded into the 1st volume of an instance, - // not into its own GLVolume. - // There shall always be such a GLVolume allocated. - ModelVolumeState key(model_object->volumes.front()->id(), instance.instance_id); - auto it = std::lower_bound(model_volume_state.begin(), model_volume_state.end(), key, model_volume_state_lower); - assert(it != model_volume_state.end() && it->geometry_id == key.geometry_id); - assert(!it->new_geometry()); - GLVolume &volume = *m_volumes.volumes[it->volume_idx]; - if (! volume.offsets.empty() && state.step[istep].timestamp != volume.offsets.front()) { - // The backend either produced a new hollowed mesh, or it invalidated the one that the front end has seen. - volume.model.reset(); - if (state.step[istep].is_done()) { - TriangleMesh mesh = print_object->get_mesh(slaposDrillHoles); - assert(! mesh.empty()); - - // sla_trafo does not contain volume trafo. To get a mesh to create - // a new volume from, we have to apply vol trafo inverse separately. - const ModelObject& mo = *m_model->objects[volume.object_idx()]; - Transform3d trafo = sla_print->sla_trafo(mo) - * mo.volumes.front()->get_transformation().get_matrix(); - mesh.transform(trafo.inverse()); -#if ENABLE_SMOOTH_NORMALS - volume.model.init_from(mesh, true); -#else - volume.model.init_from(mesh); - volume.mesh_raycaster = std::make_unique(std::make_shared(mesh)); -#endif // ENABLE_SMOOTH_NORMALS - } - else { - // Reload the original volume. -#if ENABLE_SMOOTH_NORMALS - volume.model.init_from(m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh(), true); -#else - const TriangleMesh& new_mesh = m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh(); - volume.model.init_from(new_mesh); - volume.mesh_raycaster = std::make_unique(std::make_shared(new_mesh)); -#endif // ENABLE_SMOOTH_NORMALS - } - } - //FIXME it is an ugly hack to write the timestamp into the "offsets" field to not have to add another member variable - // to the GLVolume. We should refactor GLVolume significantly, so that the GLVolume will not contain member variables - // of various concenrs (model vs. 3D print path). - volume.offsets = { state.step[istep].timestamp }; - } - else if (state.step[istep].is_done()) { - // Check whether there is an existing auxiliary volume to be updated, or a new auxiliary volume to be created. - ModelVolumeState key(state.step[istep].timestamp, instance.instance_id.id); - auto it = std::lower_bound(aux_volume_state.begin(), aux_volume_state.end(), key, model_volume_state_lower); - assert(it != aux_volume_state.end() && it->geometry_id == key.geometry_id); - if (it->new_geometry()) { - // This can be an SLA support structure that should not be rendered (in case someone used undo - // to revert to before it was generated). If that's the case, we should not generate anything. - if (model_object->sla_points_status != sla::PointsStatus::NoPoints) - instances[istep].emplace_back(std::pair(instance_idx, print_instance_idx)); - else - shift_zs[object_idx] = 0.; - } - else { - // Recycling an old GLVolume. Update the Object/Instance indices into the current Model. - m_volumes.volumes[it->volume_idx]->composite_id = GLVolume::CompositeID(object_idx, m_volumes.volumes[it->volume_idx]->volume_idx(), instance_idx); - m_volumes.volumes[it->volume_idx]->set_instance_transformation(model_object->instances[instance_idx]->get_transformation()); - } - } - } - - for (size_t istep = 0; istep < sla_steps.size(); ++istep) - if (!instances[istep].empty()) - m_volumes.load_object_auxiliary(print_object, object_idx, instances[istep], sla_steps[istep], state.step[istep].timestamp); - } - - // Shift-up all volumes of the object so that it has the right elevation with respect to the print bed - for (GLVolume* volume : m_volumes.volumes) - if (volume->object_idx() < (int)m_model->objects.size() && m_model->objects[volume->object_idx()]->instances[volume->instance_idx()]->is_printable()) - volume->set_sla_shift_z(shift_zs[volume->object_idx()]); - } if (printer_technology == ptFFF && m_config->has("nozzle_diameter")) { // Should the wipe tower be visualized ? @@ -2145,7 +2004,12 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re else m_selection.volumes_changed(map_glvolume_old_to_new); - m_gizmos.update_data(); + // The current gizmo may be not supported by the current printer technology + // after the user changes printer. + // Check if it is still activable before to call update_data() method. + const GLGizmoBase* gizmo = m_gizmos.get_current(); + if (gizmo != nullptr && gizmo->is_activable()) + m_gizmos.update_data(); m_gizmos.refresh_on_off_state(); // Update the toolbar @@ -2212,6 +2076,12 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re raycaster->set_active(v->is_active); } + for (GLVolume* volume : m_volumes.volumes) + if (volume->object_idx() < (int)m_model->objects.size() && m_model->objects[volume->object_idx()]->instances[volume->instance_idx()]->is_printable()) { + if (volume->is_modifier && m_model->objects[volume->object_idx()]->volumes[volume->volume_idx()]->is_modifier()) + volume->is_active = printer_technology != ptSLA; + } + // refresh gizmo elements raycasters for picking GLGizmoBase* curr_gizmo = m_gizmos.get_current(); if (curr_gizmo != nullptr) @@ -3583,7 +3453,7 @@ void GLCanvas3D::do_move(const std::string& snapshot_type) #else model_object->instances[instance_idx]->set_offset(v->get_instance_offset()); #endif // ENABLE_WORLD_COORDINATE - else if (selection_mode == Selection::Volume) + else if (volume_idx >= 0 && selection_mode == Selection::Volume) #if ENABLE_WORLD_COORDINATE model_object->volumes[volume_idx]->set_transformation(v->get_volume_transformation()); #else @@ -3763,7 +3633,7 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type) model_object->instances[instance_idx]->set_offset(v->get_instance_offset()); #endif // ENABLE_WORLD_COORDINATE } - else if (selection_mode == Selection::Volume) { + else if (selection_mode == Selection::Volume && volume_idx >= 0) { #if ENABLE_WORLD_COORDINATE model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); model_object->volumes[volume_idx]->set_transformation(v->get_volume_transformation()); @@ -4880,7 +4750,7 @@ bool GLCanvas3D::_init_main_toolbar() }; item.left.action_callback = GLToolbarItem::Default_Action_Callback; item.visibility_callback = GLToolbarItem::Default_Visibility_Callback; - item.enabling_callback = GLToolbarItem::Default_Enabling_Callback; + item.enabling_callback = [this]()->bool { return m_gizmos.get_current_type() == GLGizmosManager::Undefined; }; if (!m_main_toolbar.add_item(item)) return false; @@ -6803,7 +6673,7 @@ void GLCanvas3D::_load_sla_shells() return; auto add_volume = [this](const SLAPrintObject &object, int volume_id, const SLAPrintObject::Instance& instance, - const TriangleMesh& mesh, const ColorRGBA& color, bool outside_printer_detection_enabled) { + const indexed_triangle_set& mesh, const ColorRGBA& color, bool outside_printer_detection_enabled) { m_volumes.volumes.emplace_back(new GLVolume(color)); GLVolume& v = *m_volumes.volumes.back(); #if ENABLE_SMOOTH_NORMALS @@ -6816,29 +6686,31 @@ void GLCanvas3D::_load_sla_shells() v.set_instance_offset(unscale(instance.shift.x(), instance.shift.y(), 0.0)); v.set_instance_rotation({ 0.0, 0.0, (double)instance.rotation }); v.set_instance_mirror(X, object.is_left_handed() ? -1. : 1.); - v.set_convex_hull(mesh.convex_hull_3d()); + v.set_convex_hull(TriangleMesh{its_convex_hull(mesh)}); }; // adds objects' volumes - for (const SLAPrintObject* obj : print->objects()) - if (obj->is_step_done(slaposSliceSupports)) { - unsigned int initial_volumes_count = (unsigned int)m_volumes.volumes.size(); - for (const SLAPrintObject::Instance& instance : obj->instances()) { - add_volume(*obj, 0, instance, obj->get_mesh_to_print(), GLVolume::MODEL_COLOR[0], true); + for (const SLAPrintObject* obj : print->objects()) { + unsigned int initial_volumes_count = (unsigned int)m_volumes.volumes.size(); + for (const SLAPrintObject::Instance& instance : obj->instances()) { + std::shared_ptr m = obj->get_mesh_to_print(); + if (m && !m->empty()) { + add_volume(*obj, 0, instance, *m, GLVolume::MODEL_COLOR[0], true); // Set the extruder_id and volume_id to achieve the same color as in the 3D scene when // through the update_volumes_colors_by_extruder() call. m_volumes.volumes.back()->extruder_id = obj->model_object()->volumes.front()->extruder_id(); - if (obj->is_step_done(slaposSupportTree) && obj->has_mesh(slaposSupportTree)) - add_volume(*obj, -int(slaposSupportTree), instance, obj->support_mesh(), GLVolume::SLA_SUPPORT_COLOR, true); - if (obj->is_step_done(slaposPad) && obj->has_mesh(slaposPad)) - add_volume(*obj, -int(slaposPad), instance, obj->pad_mesh(), GLVolume::SLA_PAD_COLOR, false); - } - double shift_z = obj->get_current_elevation(); - for (unsigned int i = initial_volumes_count; i < m_volumes.volumes.size(); ++ i) { - // apply shift z - m_volumes.volumes[i]->set_sla_shift_z(shift_z); + if (auto &tree_mesh = obj->support_mesh().its; !tree_mesh.empty()) + add_volume(*obj, -int(slaposSupportTree), instance, tree_mesh, GLVolume::SLA_SUPPORT_COLOR, true); + if (auto &pad_mesh = obj->pad_mesh().its; !pad_mesh.empty()) + add_volume(*obj, -int(slaposPad), instance, pad_mesh, GLVolume::SLA_PAD_COLOR, false); } } + double shift_z = obj->get_current_elevation(); + for (unsigned int i = initial_volumes_count; i < m_volumes.volumes.size(); ++ i) { + // apply shift z + m_volumes.volumes[i]->set_sla_shift_z(shift_z); + } + } update_volumes_colors_by_extruder(); } @@ -7228,5 +7100,21 @@ void GLCanvas3D::GizmoHighlighter::blink() invalidate(); } +const ModelVolume *get_model_volume(const GLVolume &v, const Model &model) +{ + const ModelVolume * ret = nullptr; + + if (model.objects.size() < v.object_idx()) { + if (v.object_idx() < model.objects.size()) { + const ModelObject *obj = model.objects[v.object_idx()]; + if (v.volume_idx() < obj->volumes.size()) { + ret = obj->volumes[v.volume_idx()]; + } + } + } + + return ret; +} + } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 7b5a1084c..198ecb1d8 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -1046,6 +1046,8 @@ private: float get_overlay_window_width() { return LayersEditing::get_overlay_window_width(); } }; +const ModelVolume * get_model_volume(const GLVolume &v, const Model &model); + } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/GLShadersManager.cpp b/src/slic3r/GUI/GLShadersManager.cpp index 804d48971..e0ee02a98 100644 --- a/src/slic3r/GUI/GLShadersManager.cpp +++ b/src/slic3r/GUI/GLShadersManager.cpp @@ -65,6 +65,8 @@ std::pair GLShadersManager::init() valid &= append_shader("toolpaths_cog", { prefix + "toolpaths_cog.vs", prefix + "toolpaths_cog.fs" }); // used to render bed axes and model, selection hints, gcode sequential view marker model, preview shells, options in gcode preview valid &= append_shader("gouraud_light", { prefix + "gouraud_light.vs", prefix + "gouraud_light.fs" }); + // extend "gouraud_light" by adding clipping, used in sla gizmos + valid &= append_shader("gouraud_light_clip", { prefix + "gouraud_light_clip.vs", prefix + "gouraud_light_clip.fs" }); // used to render printbed valid &= append_shader("printbed", { prefix + "printbed.vs", prefix + "printbed.fs" }); // used to render options in gcode preview diff --git a/src/slic3r/GUI/GLTexture.cpp b/src/slic3r/GUI/GLTexture.cpp index ef9a8ef20..93e96a642 100644 --- a/src/slic3r/GUI/GLTexture.cpp +++ b/src/slic3r/GUI/GLTexture.cpp @@ -375,10 +375,29 @@ void GLTexture::render_sub_texture(unsigned int tex_id, float left, float right, glsafe(::glDisable(GL_BLEND)); } +static bool to_squared_power_of_two(const std::string& filename, int max_size_px, int& w, int& h) +{ + auto is_power_of_two = [](int v) { return v != 0 && (v & (v - 1)) == 0; }; + auto upper_power_of_two = [](int v) { v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; }; + + int new_w = std::max(w, h); + if (!is_power_of_two(new_w)) + new_w = upper_power_of_two(new_w); + + while (new_w > max_size_px) { + new_w /= 2; + } + + const int new_h = new_w; + const bool ret = (new_w != w || new_h != h); + w = new_w; + h = new_h; + return ret; +} + bool GLTexture::load_from_png(const std::string& filename, bool use_mipmaps, ECompressionType compression_type, bool apply_anisotropy) { const bool compression_enabled = (compression_type != None) && OpenGLManager::are_compressed_textures_supported(); - const bool use_compressor = compression_enabled && OpenGLManager::use_manually_generated_mipmaps(); // Load a PNG with an alpha channel. wxImage image; @@ -392,6 +411,11 @@ bool GLTexture::load_from_png(const std::string& filename, bool use_mipmaps, ECo bool requires_rescale = false; + if (use_mipmaps && compression_enabled && OpenGLManager::force_power_of_two_textures()) { + if (to_squared_power_of_two(boost::filesystem::path(filename).filename().string(), OpenGLManager::get_gl_info().get_max_tex_size(), m_width, m_height)) + requires_rescale = true; + } + if (compression_enabled && compression_type == MultiThreaded) { // the stb_dxt compression library seems to like only texture sizes which are a multiple of 4 int width_rem = m_width % 4; @@ -448,7 +472,7 @@ bool GLTexture::load_from_png(const std::string& filename, bool use_mipmaps, ECo } if (compression_enabled) { - if (compression_type == SingleThreaded || !use_compressor) + if (compression_type == SingleThreaded) glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, (GLsizei)m_width, (GLsizei)m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)data.data())); else { // initializes the texture on GPU @@ -460,7 +484,7 @@ bool GLTexture::load_from_png(const std::string& filename, bool use_mipmaps, ECo else glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)m_width, (GLsizei)m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)data.data())); - if (use_mipmaps && OpenGLManager::use_manually_generated_mipmaps()) { + if (use_mipmaps) { // we manually generate mipmaps because glGenerateMipmap() function is not reliable on all graphics cards int lod_w = m_width; int lod_h = m_height; @@ -507,10 +531,6 @@ bool GLTexture::load_from_png(const std::string& filename, bool use_mipmaps, ECo glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)); } } - else if (use_mipmaps && !OpenGLManager::use_manually_generated_mipmaps()) { - glsafe(::glGenerateMipmap(GL_TEXTURE_2D)); - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)); - } else { glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0)); @@ -522,7 +542,7 @@ bool GLTexture::load_from_png(const std::string& filename, bool use_mipmaps, ECo m_source = filename; - if (use_compressor && compression_type == MultiThreaded) + if (compression_type == MultiThreaded) // start asynchronous compression m_compressor.start_compressing(); @@ -532,7 +552,6 @@ bool GLTexture::load_from_png(const std::string& filename, bool use_mipmaps, ECo bool GLTexture::load_from_svg(const std::string& filename, bool use_mipmaps, bool compress, bool apply_anisotropy, unsigned int max_size_px) { const bool compression_enabled = compress && OpenGLManager::are_compressed_textures_supported(); - const bool use_compressor = compression_enabled && OpenGLManager::use_manually_generated_mipmaps(); NSVGimage* image = BitmapCache::nsvgParseFromFileWithReplace(filename.c_str(), "px", 96.0f, {}); if (image == nullptr) { @@ -540,11 +559,17 @@ bool GLTexture::load_from_svg(const std::string& filename, bool use_mipmaps, boo return false; } - float scale = (float)max_size_px / std::max(image->width, image->height); + const float scale = (float)max_size_px / std::max(image->width, image->height); m_width = (int)(scale * image->width); m_height = (int)(scale * image->height); + if (use_mipmaps && compression_enabled && OpenGLManager::force_power_of_two_textures()) + to_squared_power_of_two(boost::filesystem::path(filename).filename().string(), max_size_px, m_width, m_height); + + float scale_w = (float)m_width / image->width; + float scale_h = (float)m_height / image->height; + if (compression_enabled) { // the stb_dxt compression library seems to like only texture sizes which are a multiple of 4 int width_rem = m_width % 4; @@ -574,7 +599,7 @@ bool GLTexture::load_from_svg(const std::string& filename, bool use_mipmaps, boo // creates the temporary buffer only once, with max size, and reuse it for all the levels, if generating mipmaps std::vector data(n_pixels * 4, 0); - nsvgRasterize(rast, image, 0, 0, scale, data.data(), m_width, m_height, m_width * 4); + nsvgRasterizeXY(rast, image, 0, 0, scale_w, scale_h, data.data(), m_width, m_height, m_width * 4); // sends data to gpu glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1)); @@ -588,19 +613,15 @@ bool GLTexture::load_from_svg(const std::string& filename, bool use_mipmaps, boo } if (compression_enabled) { - if (use_compressor) { - // initializes the texture on GPU - glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, (GLsizei)m_width, (GLsizei)m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0)); - // and send the uncompressed data to the compressor - m_compressor.add_level((unsigned int)m_width, (unsigned int)m_height, data); - } - else - glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, (GLsizei)m_width, (GLsizei)m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)data.data())); + // initializes the texture on GPU + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, (GLsizei)m_width, (GLsizei)m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0)); + // and send the uncompressed data to the compressor + m_compressor.add_level((unsigned int)m_width, (unsigned int)m_height, data); } else glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)m_width, (GLsizei)m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)data.data())); - if (use_mipmaps && OpenGLManager::use_manually_generated_mipmaps()) { + if (use_mipmaps) { // we manually generate mipmaps because glGenerateMipmap() function is not reliable on all graphics cards int lod_w = m_width; int lod_h = m_height; @@ -610,11 +631,12 @@ bool GLTexture::load_from_svg(const std::string& filename, bool use_mipmaps, boo lod_w = std::max(lod_w / 2, 1); lod_h = std::max(lod_h / 2, 1); - scale /= 2.0f; + scale_w /= 2.0f; + scale_h /= 2.0f; data.resize(lod_w * lod_h * 4); - nsvgRasterize(rast, image, 0, 0, scale, data.data(), lod_w, lod_h, lod_w * 4); + nsvgRasterizeXY(rast, image, 0, 0, scale_w, scale_h, data.data(), lod_w, lod_h, lod_w * 4); if (compression_enabled) { // initializes the texture on GPU glsafe(::glTexImage2D(GL_TEXTURE_2D, level, GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, (GLsizei)lod_w, (GLsizei)lod_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0)); @@ -630,10 +652,6 @@ bool GLTexture::load_from_svg(const std::string& filename, bool use_mipmaps, boo glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)); } } - else if (use_mipmaps && !OpenGLManager::use_manually_generated_mipmaps()) { - glsafe(::glGenerateMipmap(GL_TEXTURE_2D)); - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)); - } else { glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0)); @@ -645,7 +663,7 @@ bool GLTexture::load_from_svg(const std::string& filename, bool use_mipmaps, boo m_source = filename; - if (use_compressor) + if (compression_enabled) // start asynchronous compression m_compressor.start_compressing(); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 5a2562aab..e75f8836d 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -1,3385 +1,3449 @@ -#include "libslic3r/Technologies.hpp" -#include "GUI_App.hpp" -#include "GUI_Init.hpp" -#include "GUI_ObjectList.hpp" -#include "GUI_ObjectManipulation.hpp" -#include "GUI_Factories.hpp" -#include "format.hpp" -#include "I18N.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "libslic3r/Utils.hpp" -#include "libslic3r/Model.hpp" -#include "libslic3r/I18N.hpp" -#include "libslic3r/PresetBundle.hpp" -#include "libslic3r/Color.hpp" - -#include "GUI.hpp" -#include "GUI_Utils.hpp" -#include "3DScene.hpp" -#include "MainFrame.hpp" -#include "Plater.hpp" -#include "GLCanvas3D.hpp" - -#include "../Utils/PresetUpdater.hpp" -#include "../Utils/PrintHost.hpp" -#include "../Utils/Process.hpp" -#include "../Utils/MacDarkMode.hpp" -#include "../Utils/AppUpdater.hpp" -#include "../Utils/WinRegistry.hpp" -#include "slic3r/Config/Snapshot.hpp" -#include "ConfigSnapshotDialog.hpp" -#include "FirmwareDialog.hpp" -#include "Preferences.hpp" -#include "Tab.hpp" -#include "SysInfoDialog.hpp" -#include "KBShortcutsDialog.hpp" -#include "UpdateDialogs.hpp" -#include "Mouse3DController.hpp" -#include "RemovableDriveManager.hpp" -#include "InstanceCheck.hpp" -#include "NotificationManager.hpp" -#include "UnsavedChangesDialog.hpp" -#include "SavePresetDialog.hpp" -#include "PrintHostDialogs.hpp" -#include "DesktopIntegrationDialog.hpp" -#include "SendSystemInfoDialog.hpp" -#include "Downloader.hpp" - -#include "BitmapCache.hpp" -#include "Notebook.hpp" - -#ifdef __WXMSW__ -#include -#include -#ifdef _MSW_DARK_MODE -#include -#endif // _MSW_DARK_MODE -#endif -#ifdef _WIN32 -#include -#endif - -#if ENABLE_THUMBNAIL_GENERATOR_DEBUG -#include -#include -#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG - -// Needed for forcing menu icons back under gtk2 and gtk3 -#if defined(__WXGTK20__) || defined(__WXGTK3__) - #include -#endif - -using namespace std::literals; - -namespace Slic3r { -namespace GUI { - -class MainFrame; - -class SplashScreen : public wxSplashScreen -{ -public: - SplashScreen(const wxBitmap& bitmap, long splashStyle, int milliseconds, wxPoint pos = wxDefaultPosition) - : wxSplashScreen(bitmap, splashStyle, milliseconds, static_cast(wxGetApp().mainframe), wxID_ANY, wxDefaultPosition, wxDefaultSize, -#ifdef __APPLE__ - wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR | wxSTAY_ON_TOP -#else - wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR -#endif // !__APPLE__ - ) - { - wxASSERT(bitmap.IsOk()); - -// int init_dpi = get_dpi_for_window(this); - this->SetPosition(pos); - // The size of the SplashScreen can be hanged after its moving to another display - // So, update it from a bitmap size - this->SetClientSize(bitmap.GetWidth(), bitmap.GetHeight()); - this->CenterOnScreen(); -// int new_dpi = get_dpi_for_window(this); - -// m_scale = (float)(new_dpi) / (float)(init_dpi); - m_main_bitmap = bitmap; - -// scale_bitmap(m_main_bitmap, m_scale); - - // init constant texts and scale fonts - init_constant_text(); - - // this font will be used for the action string - m_action_font = m_constant_text.credits_font.Bold(); - - // draw logo and constant info text - Decorate(m_main_bitmap); - } - - void SetText(const wxString& text) - { - set_bitmap(m_main_bitmap); - if (!text.empty()) { - wxBitmap bitmap(m_main_bitmap); - - wxMemoryDC memDC; - memDC.SelectObject(bitmap); - - memDC.SetFont(m_action_font); - memDC.SetTextForeground(wxColour(237, 107, 33)); - memDC.DrawText(text, int(m_scale * 60), m_action_line_y_position); - - memDC.SelectObject(wxNullBitmap); - set_bitmap(bitmap); -#ifdef __WXOSX__ - // without this code splash screen wouldn't be updated under OSX - wxYield(); -#endif - } - } - - static wxBitmap MakeBitmap(wxBitmap bmp) - { - if (!bmp.IsOk()) - return wxNullBitmap; - - // create dark grey background for the splashscreen - // It will be 5/3 of the weight of the bitmap - int width = lround((double)5 / 3 * bmp.GetWidth()); - int height = bmp.GetHeight(); - - wxImage image(width, height); - unsigned char* imgdata_ = image.GetData(); - for (int i = 0; i < width * height; ++i) { - *imgdata_++ = 51; - *imgdata_++ = 51; - *imgdata_++ = 51; - } - - wxBitmap new_bmp(image); - - wxMemoryDC memDC; - memDC.SelectObject(new_bmp); - memDC.DrawBitmap(bmp, width - bmp.GetWidth(), 0, true); - - return new_bmp; - } - - void Decorate(wxBitmap& bmp) - { - if (!bmp.IsOk()) - return; - - // draw text to the box at the left of the splashscreen. - // this box will be 2/5 of the weight of the bitmap, and be at the left. - int width = lround(bmp.GetWidth() * 0.4); - - // load bitmap for logo - BitmapCache bmp_cache; - int logo_size = lround(width * 0.25); - wxBitmap logo_bmp = *bmp_cache.load_svg(wxGetApp().logo_name(), logo_size, logo_size); - - wxCoord margin = int(m_scale * 20); - - wxRect banner_rect(wxPoint(0, logo_size), wxPoint(width, bmp.GetHeight())); - banner_rect.Deflate(margin, 2 * margin); - - // use a memory DC to draw directly onto the bitmap - wxMemoryDC memDc(bmp); - - // draw logo - memDc.DrawBitmap(logo_bmp, margin, margin, true); - - // draw the (white) labels inside of our black box (at the left of the splashscreen) - memDc.SetTextForeground(wxColour(255, 255, 255)); - - memDc.SetFont(m_constant_text.title_font); - memDc.DrawLabel(m_constant_text.title, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); - - int title_height = memDc.GetTextExtent(m_constant_text.title).GetY(); - banner_rect.SetTop(banner_rect.GetTop() + title_height); - banner_rect.SetHeight(banner_rect.GetHeight() - title_height); - - memDc.SetFont(m_constant_text.version_font); - memDc.DrawLabel(m_constant_text.version, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); - int version_height = memDc.GetTextExtent(m_constant_text.version).GetY(); - - memDc.SetFont(m_constant_text.credits_font); - memDc.DrawLabel(m_constant_text.credits, banner_rect, wxALIGN_BOTTOM | wxALIGN_LEFT); - int credits_height = memDc.GetMultiLineTextExtent(m_constant_text.credits).GetY(); - int text_height = memDc.GetTextExtent("text").GetY(); - - // calculate position for the dynamic text - int logo_and_header_height = margin + logo_size + title_height + version_height; - m_action_line_y_position = logo_and_header_height + 0.5 * (bmp.GetHeight() - margin - credits_height - logo_and_header_height - text_height); - } - -private: - wxBitmap m_main_bitmap; - wxFont m_action_font; - int m_action_line_y_position; - float m_scale {1.0}; - - struct ConstantText - { - wxString title; - wxString version; - wxString credits; - - wxFont title_font; - wxFont version_font; - wxFont credits_font; - - void init(wxFont init_font) - { - // title - title = wxGetApp().is_editor() ? SLIC3R_APP_NAME : GCODEVIEWER_APP_NAME; - - // dynamically get the version to display - version = _L("Version") + " " + std::string(SLIC3R_VERSION); - - // credits infornation - credits = title + " " + - _L("is based on Slic3r by Alessandro Ranellucci and the RepRap community.") + "\n" + - _L("Developed by Prusa Research.") + "\n\n" + - title + " " + _L("is licensed under the") + " " + _L("GNU Affero General Public License, version 3") + ".\n\n" + - _L("Contributions by Vojtech Bubnik, Enrico Turri, Oleksandra Iushchenko, Tamas Meszaros, Lukas Matena, Vojtech Kral, David Kocik and numerous others.") + "\n\n" + - _L("Artwork model by Creative Tools"); - - title_font = version_font = credits_font = init_font; - } - } - m_constant_text; - - void init_constant_text() - { - m_constant_text.init(get_default_font(this)); - - // As default we use a system font for current display. - // Scale fonts in respect to banner width - - int text_banner_width = lround(0.4 * m_main_bitmap.GetWidth()) - roundl(m_scale * 50); // banner_width - margins - - float title_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.title).GetX(); - scale_font(m_constant_text.title_font, title_font_scale > 3.5f ? 3.5f : title_font_scale); - - float version_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.version).GetX(); - scale_font(m_constant_text.version_font, version_font_scale > 2.f ? 2.f : version_font_scale); - - // The width of the credits information string doesn't respect to the banner width some times. - // So, scale credits_font in the respect to the longest string width - int longest_string_width = word_wrap_string(m_constant_text.credits); - float font_scale = (float)text_banner_width / longest_string_width; - scale_font(m_constant_text.credits_font, font_scale); - } - - void set_bitmap(wxBitmap& bmp) - { - m_window->SetBitmap(bmp); - m_window->Refresh(); - m_window->Update(); - } - - void scale_bitmap(wxBitmap& bmp, float scale) - { - if (scale == 1.0) - return; - - wxImage image = bmp.ConvertToImage(); - if (!image.IsOk() || image.GetWidth() == 0 || image.GetHeight() == 0) - return; - - int width = int(scale * image.GetWidth()); - int height = int(scale * image.GetHeight()); - image.Rescale(width, height, wxIMAGE_QUALITY_BILINEAR); - - bmp = wxBitmap(std::move(image)); - } - - void scale_font(wxFont& font, float scale) - { -#ifdef __WXMSW__ - // Workaround for the font scaling in respect to the current active display, - // not for the primary display, as it's implemented in Font.cpp - // See https://github.com/wxWidgets/wxWidgets/blob/master/src/msw/font.cpp - // void wxNativeFontInfo::SetFractionalPointSize(float pointSizeNew) - wxNativeFontInfo nfi= *font.GetNativeFontInfo(); - float pointSizeNew = wxDisplay(this).GetScaleFactor() * scale * font.GetPointSize(); - nfi.lf.lfHeight = nfi.GetLogFontHeightAtPPI(pointSizeNew, get_dpi_for_window(this)); - nfi.pointSize = pointSizeNew; - font = wxFont(nfi); -#else - font.Scale(scale); -#endif //__WXMSW__ - } - - // wrap a string for the strings no longer then 55 symbols - // return extent of the longest string - int word_wrap_string(wxString& input) - { - size_t line_len = 55;// count of symbols in one line - int idx = -1; - size_t cur_len = 0; - - wxString longest_sub_string; - auto get_longest_sub_string = [input](wxString &longest_sub_str, size_t cur_len, size_t i) { - if (cur_len > longest_sub_str.Len()) - longest_sub_str = input.SubString(i - cur_len + 1, i); - }; - - for (size_t i = 0; i < input.Len(); i++) - { - cur_len++; - if (input[i] == ' ') - idx = i; - if (input[i] == '\n') - { - get_longest_sub_string(longest_sub_string, cur_len, i); - idx = -1; - cur_len = 0; - } - if (cur_len >= line_len && idx >= 0) - { - get_longest_sub_string(longest_sub_string, cur_len, i); - input[idx] = '\n'; - cur_len = i - static_cast(idx); - } - } - - return GetTextExtent(longest_sub_string).GetX(); - } -}; - - -#ifdef __linux__ -bool static check_old_linux_datadir(const wxString& app_name) { - // If we are on Linux and the datadir does not exist yet, look into the old - // location where the datadir was before version 2.3. If we find it there, - // tell the user that he might wanna migrate to the new location. - // (https://github.com/prusa3d/PrusaSlicer/issues/2911) - // To be precise, the datadir should exist, it is created when single instance - // lock happens. Instead of checking for existence, check the contents. - - namespace fs = boost::filesystem; - - std::string new_path = Slic3r::data_dir(); - - wxString dir; - if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) - dir = wxFileName::GetHomeDir() + wxS("/.config"); - std::string default_path = (dir + "/" + app_name).ToUTF8().data(); - - if (new_path != default_path) { - // This happens when the user specifies a custom --datadir. - // Do not show anything in that case. - return true; - } - - fs::path data_dir = fs::path(new_path); - if (! fs::is_directory(data_dir)) - return true; // This should not happen. - - int file_count = std::distance(fs::directory_iterator(data_dir), fs::directory_iterator()); - - if (file_count <= 1) { // just cache dir with an instance lock - std::string old_path = wxStandardPaths::Get().GetUserDataDir().ToUTF8().data(); - - if (fs::is_directory(old_path)) { - wxString msg = from_u8((boost::format(_u8L("Starting with %1% 2.3, configuration " - "directory on Linux has changed (according to XDG Base Directory Specification) to \n%2%.\n\n" - "This directory did not exist yet (maybe you run the new version for the first time).\nHowever, " - "an old %1% configuration directory was detected in \n%3%.\n\n" - "Consider moving the contents of the old directory to the new location in order to access " - "your profiles, etc.\nNote that if you decide to downgrade %1% in future, it will use the old " - "location again.\n\n" - "What do you want to do now?")) % SLIC3R_APP_NAME % new_path % old_path).str()); - wxString caption = from_u8((boost::format(_u8L("%s - BREAKING CHANGE")) % SLIC3R_APP_NAME).str()); - RichMessageDialog dlg(nullptr, msg, caption, wxYES_NO); - dlg.SetYesNoLabels(_L("Quit, I will move my data now"), _L("Start the application")); - if (dlg.ShowModal() != wxID_NO) - return false; - } - } else { - // If the new directory exists, be silent. The user likely already saw the message. - } - return true; -} -#endif - -#ifdef _WIN32 -#if 0 // External Updater is replaced with AppUpdater.cpp -static bool run_updater_win() -{ - // find updater exe - boost::filesystem::path path_updater = boost::dll::program_location().parent_path() / "prusaslicer-updater.exe"; - // run updater. Original args: /silent -restartapp prusa-slicer.exe -startappfirst - std::string msg; - bool res = create_process(path_updater, L"/silent", msg); - if (!res) - BOOST_LOG_TRIVIAL(error) << msg; - return res; -} -#endif // 0 -#endif // _WIN32 - -struct FileWildcards { - std::string_view title; - std::vector file_extensions; -}; - - - -static const FileWildcards file_wildcards_by_type[FT_SIZE] = { - /* FT_STL */ { "STL files"sv, { ".stl"sv } }, - /* FT_OBJ */ { "OBJ files"sv, { ".obj"sv } }, - /* FT_OBJECT */ { "Object files"sv, { ".stl"sv, ".obj"sv } }, - /* FT_STEP */ { "STEP files"sv, { ".stp"sv, ".step"sv } }, - /* FT_AMF */ { "AMF files"sv, { ".amf"sv, ".zip.amf"sv, ".xml"sv } }, - /* FT_3MF */ { "3MF files"sv, { ".3mf"sv } }, - /* FT_GCODE */ { "G-code files"sv, { ".gcode"sv, ".gco"sv, ".g"sv, ".ngc"sv } }, - /* FT_MODEL */ { "Known files"sv, { ".stl"sv, ".obj"sv, ".3mf"sv, ".amf"sv, ".zip.amf"sv, ".xml"sv, ".step"sv, ".stp"sv } }, - /* FT_PROJECT */ { "Project files"sv, { ".3mf"sv, ".amf"sv, ".zip.amf"sv } }, - /* FT_FONTS */ { "Font files"sv, { ".ttc"sv, ".ttf"sv } }, - /* FT_GALLERY */ { "Known files"sv, { ".stl"sv, ".obj"sv } }, - - /* FT_INI */ { "INI files"sv, { ".ini"sv } }, - /* FT_SVG */ { "SVG files"sv, { ".svg"sv } }, - - /* FT_TEX */ { "Texture"sv, { ".png"sv, ".svg"sv } }, - - /* FT_SL1 */ { "Masked SLA files"sv, { ".sl1"sv, ".sl1s"sv, ".pwmx"sv } }, -}; - -#if ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR -wxString file_wildcards(FileType file_type) -{ - const FileWildcards& data = file_wildcards_by_type[file_type]; - std::string title; - std::string mask; - - // Generate cumulative first item - for (const std::string_view& ext : data.file_extensions) { - if (title.empty()) { - title = "*"; - title += ext; - mask = title; - } - else { - title += ", *"; - title += ext; - mask += ";*"; - mask += ext; - } - mask += ";*"; - mask += boost::to_upper_copy(std::string(ext)); - } - - wxString ret = GUI::format_wxstr("%s (%s)|%s", data.title, title, mask); - - // Adds an item for each of the extensions - if (data.file_extensions.size() > 1) { - for (const std::string_view& ext : data.file_extensions) { - title = "*"; - title += ext; - ret += GUI::format_wxstr("|%s (%s)|%s", data.title, title, title); - } - } - - return ret; -} -#else -// This function produces a Win32 file dialog file template mask to be consumed by wxWidgets on all platforms. -// The function accepts a custom extension parameter. If the parameter is provided, the custom extension -// will be added as a fist to the list. This is important for a "file save" dialog on OSX, which strips -// an extension from the provided initial file name and substitutes it with the default extension (the first one in the template). -wxString file_wildcards(FileType file_type, const std::string &custom_extension) -{ - const FileWildcards& data = file_wildcards_by_type[file_type]; - std::string title; - std::string mask; - std::string custom_ext_lower; - - if (! custom_extension.empty()) { - // Generate an extension into the title mask and into the list of extensions. - custom_ext_lower = boost::to_lower_copy(custom_extension); - const std::string custom_ext_upper = boost::to_upper_copy(custom_extension); - if (custom_ext_lower == custom_extension) { - // Add a lower case version. - title = std::string("*") + custom_ext_lower; - mask = title; - // Add an upper case version. - mask += ";*"; - mask += custom_ext_upper; - } else if (custom_ext_upper == custom_extension) { - // Add an upper case version. - title = std::string("*") + custom_ext_upper; - mask = title; - // Add a lower case version. - mask += ";*"; - mask += custom_ext_lower; - } else { - // Add the mixed case version only. - title = std::string("*") + custom_extension; - mask = title; - } - } - - for (const std::string_view &ext : data.file_extensions) - // Only add an extension if it was not added first as the custom extension. - if (ext != custom_ext_lower) { - if (title.empty()) { - title = "*"; - title += ext; - mask = title; - } else { - title += ", *"; - title += ext; - mask += ";*"; - mask += ext; - } - mask += ";*"; - mask += boost::to_upper_copy(std::string(ext)); - } - - return GUI::format_wxstr("%s (%s)|%s", data.title, title, mask); -} -#endif // ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR - -static std::string libslic3r_translate_callback(const char *s) { return wxGetTranslation(wxString(s, wxConvUTF8)).utf8_str().data(); } - -#ifdef WIN32 -#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) -static void register_win32_dpi_event() -{ - enum { WM_DPICHANGED_ = 0x02e0 }; - - wxWindow::MSWRegisterMessageHandler(WM_DPICHANGED_, [](wxWindow *win, WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam) { - const int dpi = wParam & 0xffff; - const auto rect = reinterpret_cast(lParam); - const wxRect wxrect(wxPoint(rect->top, rect->left), wxPoint(rect->bottom, rect->right)); - - DpiChangedEvent evt(EVT_DPI_CHANGED_SLICER, dpi, wxrect); - win->GetEventHandler()->AddPendingEvent(evt); - - return true; - }); -} -#endif // !wxVERSION_EQUAL_OR_GREATER_THAN - -static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 }; - -static void register_win32_device_notification_event() -{ - wxWindow::MSWRegisterMessageHandler(WM_DEVICECHANGE, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { - // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. - auto main_frame = dynamic_cast(win); - auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); - if (plater == nullptr) - // Maybe some other top level window like a dialog or maybe a pop-up menu? - return true; - PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam; - switch (wParam) { - case DBT_DEVICEARRIVAL: - if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) - plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); - else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { - PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; -// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) { -// printf("DBT_DEVICEARRIVAL %d - Media has arrived: %ws\n", msg_count, lpdbi->dbcc_name); - if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) - plater->GetEventHandler()->AddPendingEvent(HIDDeviceAttachedEvent(EVT_HID_DEVICE_ATTACHED, boost::nowide::narrow(lpdbi->dbcc_name))); - } - break; - case DBT_DEVICEREMOVECOMPLETE: - if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) - plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); - else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { - PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; -// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) -// printf("DBT_DEVICEARRIVAL %d - Media was removed: %ws\n", msg_count, lpdbi->dbcc_name); - if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) - plater->GetEventHandler()->AddPendingEvent(HIDDeviceDetachedEvent(EVT_HID_DEVICE_DETACHED, boost::nowide::narrow(lpdbi->dbcc_name))); - } - break; - default: - break; - } - return true; - }); - - wxWindow::MSWRegisterMessageHandler(MainFrame::WM_USER_MEDIACHANGED, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { - // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. - auto main_frame = dynamic_cast(win); - auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); - if (plater == nullptr) - // Maybe some other top level window like a dialog or maybe a pop-up menu? - return true; - wchar_t sPath[MAX_PATH]; - if (lParam == SHCNE_MEDIAINSERTED || lParam == SHCNE_MEDIAREMOVED) { - struct _ITEMIDLIST* pidl = *reinterpret_cast(wParam); - if (! SHGetPathFromIDList(pidl, sPath)) { - BOOST_LOG_TRIVIAL(error) << "MediaInserted: SHGetPathFromIDList failed"; - return false; - } - } - switch (lParam) { - case SHCNE_MEDIAINSERTED: - { - //printf("SHCNE_MEDIAINSERTED %S\n", sPath); - plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); - break; - } - case SHCNE_MEDIAREMOVED: - { - //printf("SHCNE_MEDIAREMOVED %S\n", sPath); - plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); - break; - } - default: -// printf("Unknown\n"); - break; - } - return true; - }); - - wxWindow::MSWRegisterMessageHandler(WM_INPUT, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { - auto main_frame = dynamic_cast(Slic3r::GUI::find_toplevel_parent(win)); - auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); -// if (wParam == RIM_INPUTSINK && plater != nullptr && main_frame->IsActive()) { - if (wParam == RIM_INPUT && plater != nullptr && main_frame->IsActive()) { - RAWINPUT raw; - UINT rawSize = sizeof(RAWINPUT); - ::GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &raw, &rawSize, sizeof(RAWINPUTHEADER)); - if (raw.header.dwType == RIM_TYPEHID && plater->get_mouse3d_controller().handle_raw_input_win32(raw.data.hid.bRawData, raw.data.hid.dwSizeHid)) - return true; - } - return false; - }); - - wxWindow::MSWRegisterMessageHandler(WM_COPYDATA, [](wxWindow* win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { - COPYDATASTRUCT* copy_data_structure = { 0 }; - copy_data_structure = (COPYDATASTRUCT*)lParam; - if (copy_data_structure->dwData == 1) { - LPCWSTR arguments = (LPCWSTR)copy_data_structure->lpData; - Slic3r::GUI::wxGetApp().other_instance_message_handler()->handle_message(boost::nowide::narrow(arguments)); - } - return true; - }); -} -#endif // WIN32 - -static void generic_exception_handle() -{ - // Note: Some wxWidgets APIs use wxLogError() to report errors, eg. wxImage - // - see https://docs.wxwidgets.org/3.1/classwx_image.html#aa249e657259fe6518d68a5208b9043d0 - // - // wxLogError typically goes around exception handling and display an error dialog some time - // after an error is logged even if exception handling and OnExceptionInMainLoop() take place. - // This is why we use wxLogError() here as well instead of a custom dialog, because it accumulates - // errors if multiple have been collected and displays just one error message for all of them. - // Otherwise we would get multiple error messages for one missing png, for example. - // - // If a custom error message window (or some other solution) were to be used, it would be necessary - // to turn off wxLogError() usage in wx APIs, most notably in wxImage - // - see https://docs.wxwidgets.org/trunk/classwx_image.html#aa32e5d3507cc0f8c3330135bc0befc6a - - try { - throw; - } catch (const std::bad_alloc& ex) { - // bad_alloc in main thread is most likely fatal. Report immediately to the user (wxLogError would be delayed) - // and terminate the app so it is at least certain to happen now. - wxString errmsg = wxString::Format(_L("%s has encountered an error. It was likely caused by running out of memory. " - "If you are sure you have enough RAM on your system, this may also be a bug and we would " - "be glad if you reported it.\n\nThe application will now terminate."), SLIC3R_APP_NAME); - wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Fatal error"), wxOK | wxICON_ERROR); - BOOST_LOG_TRIVIAL(error) << boost::format("std::bad_alloc exception: %1%") % ex.what(); - std::terminate(); - } catch (const boost::io::bad_format_string& ex) { - wxString errmsg = _L("PrusaSlicer has encountered a localization error. " - "Please report to PrusaSlicer team, what language was active and in which scenario " - "this issue happened. Thank you.\n\nThe application will now terminate."); - wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Critical error"), wxOK | wxICON_ERROR); - BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); - std::terminate(); - throw; - } catch (const std::exception& ex) { - wxLogError(format_wxstr(_L("Internal error: %1%"), ex.what())); - BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); - throw; - } -} - -void GUI_App::post_init() -{ - assert(initialized()); - if (! this->initialized()) - throw Slic3r::RuntimeError("Calling post_init() while not yet initialized"); - - if (this->is_gcode_viewer()) { - if (! this->init_params->input_files.empty()) - this->plater()->load_gcode(wxString::FromUTF8(this->init_params->input_files[0].c_str())); - } - else if (this->init_params->start_downloader) { - start_download(this->init_params->download_url); - } else { - if (! this->init_params->preset_substitutions.empty()) - show_substitutions_info(this->init_params->preset_substitutions); - -#if 0 - // Load the cummulative config over the currently active profiles. - //FIXME if multiple configs are loaded, only the last one will have an effect. - // We need to decide what to do about loading of separate presets (just print preset, just filament preset etc). - // As of now only the full configs are supported here. - if (!m_print_config.empty()) - this->gui->mainframe->load_config(m_print_config); -#endif - if (! this->init_params->load_configs.empty()) - // Load the last config to give it a name at the UI. The name of the preset may be later - // changed by loading an AMF or 3MF. - //FIXME this is not strictly correct, as one may pass a print/filament/printer profile here instead of a full config. - this->mainframe->load_config_file(this->init_params->load_configs.back()); - // If loading a 3MF file, the config is loaded from the last one. - if (!this->init_params->input_files.empty()) { - const std::vector res = this->plater()->load_files(this->init_params->input_files, true, true); - if (!res.empty() && this->init_params->input_files.size() == 1) { - // Update application titlebar when opening a project file - const std::string& filename = this->init_params->input_files.front(); - if (boost::algorithm::iends_with(filename, ".amf") || - boost::algorithm::iends_with(filename, ".amf.xml") || - boost::algorithm::iends_with(filename, ".3mf")) - this->plater()->set_project_filename(from_u8(filename)); - } - if (this->init_params->delete_after_load) { - for (const std::string& p : this->init_params->input_files) { - boost::system::error_code ec; - boost::filesystem::remove(boost::filesystem::path(p), ec); - if (ec) { - BOOST_LOG_TRIVIAL(error) << ec.message(); - } - } - } - } - if (! this->init_params->extra_config.empty()) - this->mainframe->load_config(this->init_params->extra_config); - } - - // show "Did you know" notification - if (app_config->get("show_hints") == "1" && ! is_gcode_viewer()) - plater_->get_notification_manager()->push_hint_notification(true); - - // The extra CallAfter() is needed because of Mac, where this is the only way - // to popup a modal dialog on start without screwing combo boxes. - // This is ugly but I honestly found no better way to do it. - // Neither wxShowEvent nor wxWindowCreateEvent work reliably. - if (this->preset_updater) { // G-Code Viewer does not initialize preset_updater. - if (! this->check_updates(false)) - // Configuration is not compatible and reconfigure was refused by the user. Application is closing. - return; - CallAfter([this] { - bool cw_showed = this->config_wizard_startup(); - this->preset_updater->sync(preset_bundle); - this->app_version_check(false); - if (! cw_showed) { - // The CallAfter is needed as well, without it, GL extensions did not show. - // Also, we only want to show this when the wizard does not, so the new user - // sees something else than "we want something" on the first start. - show_send_system_info_dialog_if_needed(); - } - }); - } - - // Set PrusaSlicer version and save to PrusaSlicer.ini or PrusaSlicerGcodeViewer.ini. - app_config->set("version", SLIC3R_VERSION); - app_config->save(); - -#ifdef _WIN32 - // Sets window property to mainframe so other instances can indentify it. - OtherInstanceMessageHandler::init_windows_properties(mainframe, m_instance_hash_int); -#endif //WIN32 -} - -IMPLEMENT_APP(GUI_App) - -GUI_App::GUI_App(EAppMode mode) - : wxApp() - , m_app_mode(mode) - , m_em_unit(10) - , m_imgui(new ImGuiWrapper()) - , m_removable_drive_manager(std::make_unique()) - , m_other_instance_message_handler(std::make_unique()) - , m_downloader(std::make_unique()) -{ - //app config initializes early becasuse it is used in instance checking in PrusaSlicer.cpp - this->init_app_config(); - // init app downloader after path to datadir is set - m_app_updater = std::make_unique(); -} - -GUI_App::~GUI_App() -{ - if (app_config != nullptr) - delete app_config; - - if (preset_bundle != nullptr) - delete preset_bundle; - - if (preset_updater != nullptr) - delete preset_updater; -} - -// If formatted for github, plaintext with OpenGL extensions enclosed into

. -// Otherwise HTML formatted for the system info dialog. -std::string GUI_App::get_gl_info(bool for_github) -{ - return OpenGLManager::get_gl_info().to_string(for_github); -} - -wxGLContext* GUI_App::init_glcontext(wxGLCanvas& canvas) -{ -#if ENABLE_GL_CORE_PROFILE -#if ENABLE_OPENGL_DEBUG_OPTION - return m_opengl_mgr.init_glcontext(canvas, init_params != nullptr ? init_params->opengl_version : std::make_pair(0, 0), - init_params != nullptr ? init_params->opengl_debug : false); -#else - return m_opengl_mgr.init_glcontext(canvas, init_params != nullptr ? init_params->opengl_version : std::make_pair(0, 0)); -#endif // ENABLE_OPENGL_DEBUG_OPTION -#else - return m_opengl_mgr.init_glcontext(canvas); -#endif // ENABLE_GL_CORE_PROFILE -} - -bool GUI_App::init_opengl() -{ -#ifdef __linux__ - bool status = m_opengl_mgr.init_gl(); - m_opengl_initialized = true; - return status; -#else - return m_opengl_mgr.init_gl(); -#endif -} - -// gets path to PrusaSlicer.ini, returns semver from first line comment -static boost::optional parse_semver_from_ini(std::string path) -{ - std::ifstream stream(path); - std::stringstream buffer; - buffer << stream.rdbuf(); - std::string body = buffer.str(); - size_t start = body.find("PrusaSlicer "); - if (start == std::string::npos) - return boost::none; - body = body.substr(start + 12); - size_t end = body.find_first_of(" \n"); - if (end < body.size()) - body.resize(end); - return Semver::parse(body); -} - -void GUI_App::init_app_config() -{ - // Profiles for the alpha are stored into the PrusaSlicer-alpha directory to not mix with the current release. - -// SetAppName(SLIC3R_APP_KEY); - SetAppName(SLIC3R_APP_KEY "-alpha"); -// SetAppName(SLIC3R_APP_KEY "-beta"); - - -// SetAppDisplayName(SLIC3R_APP_NAME); - - // Set the Slic3r data directory at the Slic3r XS module. - // Unix: ~/ .Slic3r - // Windows : "C:\Users\username\AppData\Roaming\Slic3r" or "C:\Documents and Settings\username\Application Data\Slic3r" - // Mac : "~/Library/Application Support/Slic3r" - - if (data_dir().empty()) { - #ifndef __linux__ - set_data_dir(wxStandardPaths::Get().GetUserDataDir().ToUTF8().data()); - #else - // Since version 2.3, config dir on Linux is in ${XDG_CONFIG_HOME}. - // https://github.com/prusa3d/PrusaSlicer/issues/2911 - wxString dir; - if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) - dir = wxFileName::GetHomeDir() + wxS("/.config"); - set_data_dir((dir + "/" + GetAppName()).ToUTF8().data()); - #endif - } else { - m_datadir_redefined = true; - } - - if (!app_config) - app_config = new AppConfig(is_editor() ? AppConfig::EAppMode::Editor : AppConfig::EAppMode::GCodeViewer); - - // load settings - m_app_conf_exists = app_config->exists(); - if (m_app_conf_exists) { - std::string error = app_config->load(); - if (!error.empty()) { - // Error while parsing config file. We'll customize the error message and rethrow to be displayed. - if (is_editor()) { - throw Slic3r::RuntimeError( - _u8L("Error parsing PrusaSlicer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error. Your user profiles will not be affected.") + - "\n\n" + app_config->config_path() + "\n\n" + error); - } - else { - throw Slic3r::RuntimeError( - _u8L("Error parsing PrusaGCodeViewer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error.") + - "\n\n" + app_config->config_path() + "\n\n" + error); - } - } - } -} - -// returns old config path to copy from if such exists, -// returns an empty string if such config path does not exists or if it cannot be loaded. -std::string GUI_App::check_older_app_config(Semver current_version, bool backup) -{ - std::string older_data_dir_path; - - // If the config folder is redefined - do not check - if (m_datadir_redefined) - return {}; - - // find other version app config (alpha / beta / release) - std::string config_path = app_config->config_path(); - boost::filesystem::path parent_file_path(config_path); - std::string filename = parent_file_path.filename().string(); - parent_file_path.remove_filename().remove_filename(); - - std::vector candidates; - - if (SLIC3R_APP_KEY "-alpha" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-alpha" / filename); - if (SLIC3R_APP_KEY "-beta" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-beta" / filename); - if (SLIC3R_APP_KEY != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY / filename); - - Semver last_semver = current_version; - for (const auto& candidate : candidates) { - if (boost::filesystem::exists(candidate)) { - // parse - boost::optionalother_semver = parse_semver_from_ini(candidate.string()); - if (other_semver && *other_semver > last_semver) { - last_semver = *other_semver; - older_data_dir_path = candidate.parent_path().string(); - } - } - } - if (older_data_dir_path.empty()) - return {}; - BOOST_LOG_TRIVIAL(info) << "last app config file used: " << older_data_dir_path; - // ask about using older data folder - - InfoDialog msg(nullptr - , format_wxstr(_L("You are opening %1% version %2%."), SLIC3R_APP_NAME, SLIC3R_VERSION) - , backup ? - format_wxstr(_L( - "The active configuration was created by %1% %2%," - "\nwhile a newer configuration was found in %3%" - "\ncreated by %1% %4%." - "\n\nShall the newer configuration be imported?" - "\nIf so, your active configuration will be backed up before importing the new configuration." - ) - , SLIC3R_APP_NAME, current_version.to_string(), older_data_dir_path, last_semver.to_string()) - : format_wxstr(_L( - "An existing configuration was found in %3%" - "\ncreated by %1% %2%." - "\n\nShall this configuration be imported?" - ) - , SLIC3R_APP_NAME, last_semver.to_string(), older_data_dir_path) - , true, wxYES_NO); - - if (backup) { - msg.SetButtonLabel(wxID_YES, _L("Import")); - msg.SetButtonLabel(wxID_NO, _L("Don't import")); - } - - if (msg.ShowModal() == wxID_YES) { - std::string snapshot_id; - if (backup) { - const Config::Snapshot* snapshot{ nullptr }; - if (! GUI::Config::take_config_snapshot_cancel_on_error(*app_config, Config::Snapshot::SNAPSHOT_USER, "", - _u8L("Continue and import newer configuration?"), &snapshot)) - return {}; - if (snapshot) { - // Save snapshot ID before loading the alternate AppConfig, as loading the alternate AppConfig may fail. - snapshot_id = snapshot->id; - assert(! snapshot_id.empty()); - app_config->set("on_snapshot", snapshot_id); - } else - BOOST_LOG_TRIVIAL(error) << "Failed to take congiguration snapshot"; - } - - // load app config from older file - std::string error = app_config->load((boost::filesystem::path(older_data_dir_path) / filename).string()); - if (!error.empty()) { - // Error while parsing config file. We'll customize the error message and rethrow to be displayed. - if (is_editor()) { - throw Slic3r::RuntimeError( - _u8L("Error parsing PrusaSlicer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error. Your user profiles will not be affected.") + - "\n\n" + app_config->config_path() + "\n\n" + error); - } - else { - throw Slic3r::RuntimeError( - _u8L("Error parsing PrusaGCodeViewer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error.") + - "\n\n" + app_config->config_path() + "\n\n" + error); - } - } - if (!snapshot_id.empty()) - app_config->set("on_snapshot", snapshot_id); - m_app_conf_exists = true; - return older_data_dir_path; - } - return {}; -} - -void GUI_App::init_single_instance_checker(const std::string &name, const std::string &path) -{ - BOOST_LOG_TRIVIAL(debug) << "init wx instance checker " << name << " "<< path; - m_single_instance_checker = std::make_unique(boost::nowide::widen(name), boost::nowide::widen(path)); -} - -bool GUI_App::OnInit() -{ - try { - return on_init_inner(); - } catch (const std::exception&) { - generic_exception_handle(); - return false; - } -} - -bool GUI_App::on_init_inner() -{ - // Set initialization of image handlers before any UI actions - See GH issue #7469 - wxInitAllImageHandlers(); - -#if defined(_WIN32) && ! defined(_WIN64) - // Win32 32bit build. - if (wxPlatformInfo::Get().GetArchName().substr(0, 2) == "64") { - RichMessageDialog dlg(nullptr, - _L("You are running a 32 bit build of PrusaSlicer on 64-bit Windows." - "\n32 bit build of PrusaSlicer will likely not be able to utilize all the RAM available in the system." - "\nPlease download and install a 64 bit build of PrusaSlicer from https://www.prusa3d.cz/prusaslicer/." - "\nDo you wish to continue?"), - "PrusaSlicer", wxICON_QUESTION | wxYES_NO); - if (dlg.ShowModal() != wxID_YES) - return false; - } -#endif // _WIN64 - - // Forcing back menu icons under gtk2 and gtk3. Solution is based on: - // https://docs.gtk.org/gtk3/class.Settings.html - // see also https://docs.wxwidgets.org/3.0/classwx_menu_item.html#a2b5d6bcb820b992b1e4709facbf6d4fb - // TODO: Find workaround for GTK4 -#if defined(__WXGTK20__) || defined(__WXGTK3__) - g_object_set (gtk_settings_get_default (), "gtk-menu-images", TRUE, NULL); -#endif - - // Verify resources path - const wxString resources_dir = from_u8(Slic3r::resources_dir()); - wxCHECK_MSG(wxDirExists(resources_dir), false, - wxString::Format("Resources path does not exist or is not a directory: %s", resources_dir)); - -#ifdef __linux__ - if (! check_old_linux_datadir(GetAppName())) { - std::cerr << "Quitting, user chose to move their data to new location." << std::endl; - return false; - } -#endif - - // Enable this to get the default Win32 COMCTRL32 behavior of static boxes. -// wxSystemOptions::SetOption("msw.staticbox.optimized-paint", 0); - // Enable this to disable Windows Vista themes for all wxNotebooks. The themes seem to lead to terrible - // performance when working on high resolution multi-display setups. -// wxSystemOptions::SetOption("msw.notebook.themed-background", 0); - -// Slic3r::debugf "wxWidgets version %s, Wx version %s\n", wxVERSION_STRING, wxVERSION; - - // !!! Initialization of UI settings as a language, application color mode, fonts... have to be done before first UI action. - // Like here, before the show InfoDialog in check_older_app_config() - - // If load_language() fails, the application closes. - load_language(wxString(), true); -#ifdef _MSW_DARK_MODE - bool init_dark_color_mode = app_config->get("dark_color_mode") == "1"; - bool init_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; - NppDarkMode::InitDarkMode(init_dark_color_mode, init_sys_menu_enabled); -#endif - // initialize label colors and fonts - init_ui_colours(); - init_fonts(); - - std::string older_data_dir_path; - if (m_app_conf_exists) { - if (app_config->orig_version().valid() && app_config->orig_version() < *Semver::parse(SLIC3R_VERSION)) - // Only copying configuration if it was saved with a newer slicer than the one currently running. - older_data_dir_path = check_older_app_config(app_config->orig_version(), true); - } else { - // No AppConfig exists, fresh install. Always try to copy from an alternate location, don't make backup of the current configuration. - older_data_dir_path = check_older_app_config(Semver(), false); - } - -#ifdef _MSW_DARK_MODE - // app_config can be updated in check_older_app_config(), so check if dark_color_mode and sys_menu_enabled was changed - if (bool new_dark_color_mode = app_config->get("dark_color_mode") == "1"; - init_dark_color_mode != new_dark_color_mode) { - NppDarkMode::SetDarkMode(new_dark_color_mode); - init_ui_colours(); - update_ui_colours_from_appconfig(); - } - if (bool new_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; - init_sys_menu_enabled != new_sys_menu_enabled) - NppDarkMode::SetSystemMenuForApp(new_sys_menu_enabled); -#endif - - if (is_editor()) { - std::string msg = Http::tls_global_init(); - std::string ssl_cert_store = app_config->get("tls_accepted_cert_store_location"); - bool ssl_accept = app_config->get("tls_cert_store_accepted") == "yes" && ssl_cert_store == Http::tls_system_cert_store(); - - if (!msg.empty() && !ssl_accept) { - RichMessageDialog - dlg(nullptr, - wxString::Format(_L("%s\nDo you want to continue?"), msg), - "PrusaSlicer", wxICON_QUESTION | wxYES_NO); - dlg.ShowCheckBox(_L("Remember my choice")); - if (dlg.ShowModal() != wxID_YES) return false; - - app_config->set("tls_cert_store_accepted", - dlg.IsCheckBoxChecked() ? "yes" : "no"); - app_config->set("tls_accepted_cert_store_location", - dlg.IsCheckBoxChecked() ? Http::tls_system_cert_store() : ""); - } - } - - SplashScreen* scrn = nullptr; - if (app_config->get("show_splash_screen") == "1") { - // make a bitmap with dark grey banner on the left side - wxBitmap bmp = SplashScreen::MakeBitmap(wxBitmap(from_u8(var(is_editor() ? "splashscreen.jpg" : "splashscreen-gcodepreview.jpg")), wxBITMAP_TYPE_JPEG)); - - // Detect position (display) to show the splash screen - // Now this position is equal to the mainframe position - wxPoint splashscreen_pos = wxDefaultPosition; - bool default_splashscreen_pos = true; - if (app_config->has("window_mainframe") && app_config->get("restore_win_position") == "1") { - auto metrics = WindowMetrics::deserialize(app_config->get("window_mainframe")); - default_splashscreen_pos = metrics == boost::none; - if (!default_splashscreen_pos) - splashscreen_pos = metrics->get_rect().GetPosition(); - } - - if (!default_splashscreen_pos) { - // workaround for crash related to the positioning of the window on secondary monitor - get_app_config()->set("restore_win_position", "crashed_at_splashscreen_pos"); - get_app_config()->save(); - } - - // create splash screen with updated bmp - scrn = new SplashScreen(bmp.IsOk() ? bmp : get_bmp_bundle("PrusaSlicer", 400)->GetPreferredBitmapSizeAtScale(1.0), - wxSPLASH_CENTRE_ON_SCREEN | wxSPLASH_TIMEOUT, 4000, splashscreen_pos); - - if (!default_splashscreen_pos) - // revert "restore_win_position" value if application wasn't crashed - get_app_config()->set("restore_win_position", "1"); -#ifndef __linux__ - wxYield(); -#endif - scrn->SetText(_L("Loading configuration")+ dots); - } - - preset_bundle = new PresetBundle(); - - // just checking for existence of Slic3r::data_dir is not enough : it may be an empty directory - // supplied as argument to --datadir; in that case we should still run the wizard - preset_bundle->setup_directories(); - - if (! older_data_dir_path.empty()) { - preset_bundle->import_newer_configs(older_data_dir_path); - app_config->save(); - } - - if (is_editor()) { -#ifdef __WXMSW__ - if (app_config->get("associate_3mf") == "1") - associate_3mf_files(); - if (app_config->get("associate_stl") == "1") - associate_stl_files(); -#endif // __WXMSW__ - - preset_updater = new PresetUpdater(); - Bind(EVT_SLIC3R_VERSION_ONLINE, &GUI_App::on_version_read, this); - Bind(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, [this](const wxCommandEvent& evt) { - if (this->plater_ != nullptr && app_config->get("notify_release") == "all") { - std::string evt_string = into_u8(evt.GetString()); - if (*Semver::parse(SLIC3R_VERSION) < *Semver::parse(evt_string)) { - auto notif_type = (evt_string.find("beta") != std::string::npos ? NotificationType::NewBetaAvailable : NotificationType::NewAlphaAvailable); - this->plater_->get_notification_manager()->push_notification( notif_type - , NotificationManager::NotificationLevel::ImportantNotificationLevel - , Slic3r::format(_u8L("New prerelease version %1% is available."), evt_string) - , _u8L("See Releases page.") - , [](wxEvtHandler* evnthndlr) {wxGetApp().open_browser_with_warning_dialog("https://github.com/prusa3d/PrusaSlicer/releases"); return true; } - ); - } - } - }); - Bind(EVT_SLIC3R_APP_DOWNLOAD_PROGRESS, [this](const wxCommandEvent& evt) { - //lm:This does not force a render. The progress bar only updateswhen the mouse is moved. - if (this->plater_ != nullptr) - this->plater_->get_notification_manager()->set_download_progress_percentage((float)std::stoi(into_u8(evt.GetString())) / 100.f ); - }); - - Bind(EVT_SLIC3R_APP_DOWNLOAD_FAILED, [this](const wxCommandEvent& evt) { - if (this->plater_ != nullptr) - this->plater_->get_notification_manager()->close_notification_of_type(NotificationType::AppDownload); - if(!evt.GetString().IsEmpty()) - show_error(nullptr, evt.GetString()); - }); - - Bind(EVT_SLIC3R_APP_OPEN_FAILED, [](const wxCommandEvent& evt) { - show_error(nullptr, evt.GetString()); - }); - - } - else { -#ifdef __WXMSW__ - if (app_config->get("associate_gcode") == "1") - associate_gcode_files(); -#endif // __WXMSW__ - } - - // Suppress the '- default -' presets. - preset_bundle->set_default_suppressed(app_config->get("no_defaults") == "1"); - try { - // Enable all substitutions (in both user and system profiles), but log the substitutions in user profiles only. - // If there are substitutions in system profiles, then a "reconfigure" event shall be triggered, which will force - // installation of a compatible system preset, thus nullifying the system preset substitutions. - init_params->preset_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSystemSilent); - } catch (const std::exception &ex) { - show_error(nullptr, ex.what()); - } - -#ifdef WIN32 -#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) - register_win32_dpi_event(); -#endif // !wxVERSION_EQUAL_OR_GREATER_THAN - register_win32_device_notification_event(); -#endif // WIN32 - - // Let the libslic3r know the callback, which will translate messages on demand. - Slic3r::I18N::set_translate_callback(libslic3r_translate_callback); - - // application frame - if (scrn && is_editor()) - scrn->SetText(_L("Preparing settings tabs") + dots); - - mainframe = new MainFrame(); - // hide settings tabs after first Layout - if (is_editor()) - mainframe->select_tab(size_t(0)); - - sidebar().obj_list()->init_objects(); // propagate model objects to object list -// update_mode(); // !!! do that later - SetTopWindow(mainframe); - - plater_->init_notification_manager(); - - m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); - - if (is_gcode_viewer()) { - mainframe->update_layout(); - if (plater_ != nullptr) - // ensure the selected technology is ptFFF - plater_->set_printer_technology(ptFFF); - } - else - load_current_presets(); - - // Save the active profiles as a "saved into project". - update_saved_preset_from_current_preset(); - - if (plater_ != nullptr) { - // Save the names of active presets and project specific config into ProjectDirtyStateManager. - plater_->reset_project_dirty_initial_presets(); - // Update Project dirty state, update application title bar. - plater_->update_project_dirty_from_presets(); - } - - mainframe->Show(true); - - obj_list()->set_min_height(); - - update_mode(); // update view mode after fix of the object_list size - -#ifdef __APPLE__ - other_instance_message_handler()->bring_instance_forward(); -#endif //__APPLE__ - - Bind(wxEVT_IDLE, [this](wxIdleEvent& event) - { - if (! plater_) - return; - - this->obj_manipul()->update_if_dirty(); - - // An ugly solution to GH #5537 in which GUI_App::init_opengl (normally called from events wxEVT_PAINT - // and wxEVT_SET_FOCUS before GUI_App::post_init is called) wasn't called before GUI_App::post_init and OpenGL wasn't initialized. -#ifdef __linux__ - if (! m_post_initialized && m_opengl_initialized) { -#else - if (! m_post_initialized) { -#endif - m_post_initialized = true; -#ifdef WIN32 - this->mainframe->register_win32_callbacks(); -#endif - this->post_init(); - } - - if (m_post_initialized && app_config->dirty() && app_config->get("autosave") == "1") - app_config->save(); - }); - - m_initialized = true; - - if (const std::string& crash_reason = app_config->get("restore_win_position"); - boost::starts_with(crash_reason,"crashed")) - { - wxString preferences_item = _L("Restore window position on start"); - InfoDialog dialog(nullptr, - _L("PrusaSlicer started after a crash"), - format_wxstr(_L("PrusaSlicer crashed last time when attempting to set window position.\n" - "We are sorry for the inconvenience, it unfortunately happens with certain multiple-monitor setups.\n" - "More precise reason for the crash: \"%1%\".\n" - "For more information see our GitHub issue tracker: \"%2%\" and \"%3%\"\n\n" - "To avoid this problem, consider disabling \"%4%\" in \"Preferences\". " - "Otherwise, the application will most likely crash again next time."), - "" + from_u8(crash_reason) + "", - "#2939", - "#5573", - "" + preferences_item + ""), - true, wxYES_NO); - - dialog.SetButtonLabel(wxID_YES, format_wxstr(_L("Disable \"%1%\""), preferences_item)); - dialog.SetButtonLabel(wxID_NO, format_wxstr(_L("Leave \"%1%\" enabled") , preferences_item)); - - auto answer = dialog.ShowModal(); - if (answer == wxID_YES) - app_config->set("restore_win_position", "0"); - else if (answer == wxID_NO) - app_config->set("restore_win_position", "1"); - app_config->save(); - } - - return true; -} - -unsigned GUI_App::get_colour_approx_luma(const wxColour &colour) -{ - double r = colour.Red(); - double g = colour.Green(); - double b = colour.Blue(); - - return std::round(std::sqrt( - r * r * .241 + - g * g * .691 + - b * b * .068 - )); -} - -bool GUI_App::dark_mode() -{ -#if __APPLE__ - // The check for dark mode returns false positive on 10.12 and 10.13, - // which allowed setting dark menu bar and dock area, which is - // is detected as dark mode. We must run on at least 10.14 where the - // proper dark mode was first introduced. - return wxPlatformInfo::Get().CheckOSVersion(10, 14) && mac_dark_mode(); -#else - if (wxGetApp().app_config->has("dark_color_mode")) - return wxGetApp().app_config->get("dark_color_mode") == "1"; - return check_dark_mode(); -#endif -} - -const wxColour GUI_App::get_label_default_clr_system() -{ - return dark_mode() ? wxColour(115, 220, 103) : wxColour(26, 132, 57); -} - -const wxColour GUI_App::get_label_default_clr_modified() -{ - return dark_mode() ? wxColour(253, 111, 40) : wxColour(252, 77, 1); -} - -const std::vector GUI_App::get_mode_default_palette() -{ - return { "#7DF028", "#FFDC00", "#E70000" }; -} - -void GUI_App::init_ui_colours() -{ - m_color_label_modified = get_label_default_clr_modified(); - m_color_label_sys = get_label_default_clr_system(); - m_mode_palette = get_mode_default_palette(); - - bool is_dark_mode = dark_mode(); -#ifdef _WIN32 - m_color_label_default = is_dark_mode ? wxColour(250, 250, 250): wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); - m_color_highlight_label_default = is_dark_mode ? wxColour(230, 230, 230): wxSystemSettings::GetColour(/*wxSYS_COLOUR_HIGHLIGHTTEXT*/wxSYS_COLOUR_WINDOWTEXT); - m_color_highlight_default = is_dark_mode ? wxColour(78, 78, 78) : wxSystemSettings::GetColour(wxSYS_COLOUR_3DLIGHT); - m_color_hovered_btn_label = is_dark_mode ? wxColour(253, 111, 40) : wxColour(252, 77, 1); - m_color_default_btn_label = is_dark_mode ? wxColour(255, 181, 100): wxColour(203, 61, 0); - m_color_selected_btn_bg = is_dark_mode ? wxColour(95, 73, 62) : wxColour(228, 220, 216); -#else - m_color_label_default = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); -#endif - m_color_window_default = is_dark_mode ? wxColour(43, 43, 43) : wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); -} - -void GUI_App::update_ui_colours_from_appconfig() -{ - // load label colors - if (app_config->has("label_clr_sys")) { - auto str = app_config->get("label_clr_sys"); - if (!str.empty()) - m_color_label_sys = wxColour(str); - } - - if (app_config->has("label_clr_modified")) { - auto str = app_config->get("label_clr_modified"); - if (!str.empty()) - m_color_label_modified = wxColour(str); - } - - // load mode markers colors - if (app_config->has("mode_palette")) { - const auto colors = app_config->get("mode_palette"); - if (!colors.empty()) { - m_mode_palette.clear(); - if (!unescape_strings_cstyle(colors, m_mode_palette)) - m_mode_palette = get_mode_default_palette(); - } - } -} - -void GUI_App::update_label_colours() -{ - for (Tab* tab : tabs_list) - tab->update_label_colours(); -} - -#ifdef _WIN32 -static bool is_focused(HWND hWnd) -{ - HWND hFocusedWnd = ::GetFocus(); - return hFocusedWnd && hWnd == hFocusedWnd; -} - -static bool is_default(wxWindow* win) -{ - wxTopLevelWindow* tlw = find_toplevel_parent(win); - if (!tlw) - return false; - - return win == tlw->GetDefaultItem(); -} -#endif - -void GUI_App::UpdateDarkUI(wxWindow* window, bool highlited/* = false*/, bool just_font/* = false*/) -{ -#ifdef _WIN32 - bool is_focused_button = false; - bool is_default_button = false; - if (wxButton* btn = dynamic_cast(window)) { - if (!(btn->GetWindowStyle() & wxNO_BORDER)) { - btn->SetWindowStyle(btn->GetWindowStyle() | wxNO_BORDER); - highlited = true; - } - // button marking - { - auto mark_button = [this, btn, highlited](const bool mark) { - if (btn->GetLabel().IsEmpty()) - btn->SetBackgroundColour(mark ? m_color_selected_btn_bg : highlited ? m_color_highlight_default : m_color_window_default); - else - btn->SetForegroundColour(mark ? m_color_hovered_btn_label : (is_default(btn) ? m_color_default_btn_label : m_color_label_default)); - btn->Refresh(); - btn->Update(); - }; - - // hovering - btn->Bind(wxEVT_ENTER_WINDOW, [mark_button](wxMouseEvent& event) { mark_button(true); event.Skip(); }); - btn->Bind(wxEVT_LEAVE_WINDOW, [mark_button, btn](wxMouseEvent& event) { mark_button(is_focused(btn->GetHWND())); event.Skip(); }); - // focusing - btn->Bind(wxEVT_SET_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(true); event.Skip(); }); - btn->Bind(wxEVT_KILL_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(false); event.Skip(); }); - - is_focused_button = is_focused(btn->GetHWND()); - is_default_button = is_default(btn); - if (is_focused_button || is_default_button) - mark_button(is_focused_button); - } - } - else if (wxTextCtrl* text = dynamic_cast(window)) { - if (text->GetBorder() != wxBORDER_SIMPLE) - text->SetWindowStyle(text->GetWindowStyle() | wxBORDER_SIMPLE); - } - else if (wxCheckListBox* list = dynamic_cast(window)) { - list->SetWindowStyle(list->GetWindowStyle() | wxBORDER_SIMPLE); - list->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); - for (size_t i = 0; i < list->GetCount(); i++) - if (wxOwnerDrawn* item = list->GetItem(i)) { - item->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); - item->SetTextColour(m_color_label_default); - } - return; - } - else if (dynamic_cast(window)) - window->SetWindowStyle(window->GetWindowStyle() | wxBORDER_SIMPLE); - - if (!just_font) - window->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); - if (!is_focused_button && !is_default_button) - window->SetForegroundColour(m_color_label_default); -#endif -} - -// recursive function for scaling fonts for all controls in Window -#ifdef _WIN32 -static void update_dark_children_ui(wxWindow* window, bool just_buttons_update = false) -{ - bool is_btn = dynamic_cast(window) != nullptr; - if (!(just_buttons_update && !is_btn)) - wxGetApp().UpdateDarkUI(window, is_btn); - - auto children = window->GetChildren(); - for (auto child : children) { - update_dark_children_ui(child); - } -} -#endif - -// Note: Don't use this function for Dialog contains ScalableButtons -void GUI_App::UpdateDlgDarkUI(wxDialog* dlg, bool just_buttons_update/* = false*/) -{ -#ifdef _WIN32 - update_dark_children_ui(dlg, just_buttons_update); -#endif -} -void GUI_App::UpdateDVCDarkUI(wxDataViewCtrl* dvc, bool highlited/* = false*/) -{ -#ifdef _WIN32 - UpdateDarkUI(dvc, highlited ? dark_mode() : false); -#ifdef _MSW_DARK_MODE - dvc->RefreshHeaderDarkMode(&m_normal_font); -#endif //_MSW_DARK_MODE - if (dvc->HasFlag(wxDV_ROW_LINES)) - dvc->SetAlternateRowColour(m_color_highlight_default); - if (dvc->GetBorder() != wxBORDER_SIMPLE) - dvc->SetWindowStyle(dvc->GetWindowStyle() | wxBORDER_SIMPLE); -#endif -} - -void GUI_App::UpdateAllStaticTextDarkUI(wxWindow* parent) -{ -#ifdef _WIN32 - wxGetApp().UpdateDarkUI(parent); - - auto children = parent->GetChildren(); - for (auto child : children) { - if (dynamic_cast(child)) - child->SetForegroundColour(m_color_label_default); - } -#endif -} - -void GUI_App::init_fonts() -{ - m_small_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - m_bold_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold(); - m_normal_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - -#ifdef __WXMAC__ - m_small_font.SetPointSize(11); - m_bold_font.SetPointSize(13); -#endif /*__WXMAC__*/ - - // wxSYS_OEM_FIXED_FONT and wxSYS_ANSI_FIXED_FONT use the same as - // DEFAULT in wxGtk. Use the TELETYPE family as a work-around - m_code_font = wxFont(wxFontInfo().Family(wxFONTFAMILY_TELETYPE)); - m_code_font.SetPointSize(m_normal_font.GetPointSize()); -} - -void GUI_App::update_fonts(const MainFrame *main_frame) -{ - /* Only normal and bold fonts are used for an application rescale, - * because of under MSW small and normal fonts are the same. - * To avoid same rescaling twice, just fill this values - * from rescaled MainFrame - */ - if (main_frame == nullptr) - main_frame = this->mainframe; - m_normal_font = main_frame->normal_font(); - m_small_font = m_normal_font; - m_bold_font = main_frame->normal_font().Bold(); - m_link_font = m_bold_font.Underlined(); - m_em_unit = main_frame->em_unit(); - m_code_font.SetPointSize(m_normal_font.GetPointSize()); -} - -void GUI_App::set_label_clr_modified(const wxColour& clr) -{ - if (m_color_label_modified == clr) - return; - m_color_label_modified = clr; - const std::string str = encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); - app_config->set("label_clr_modified", str); - app_config->save(); -} - -void GUI_App::set_label_clr_sys(const wxColour& clr) -{ - if (m_color_label_sys == clr) - return; - m_color_label_sys = clr; - const std::string str = encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); - app_config->set("label_clr_sys", str); - app_config->save(); -} - -const std::string& GUI_App::get_mode_btn_color(int mode_id) -{ - assert(0 <= mode_id && size_t(mode_id) < m_mode_palette.size()); - return m_mode_palette[mode_id]; -} - -std::vector GUI_App::get_mode_palette() -{ - return { wxColor(m_mode_palette[0]), - wxColor(m_mode_palette[1]), - wxColor(m_mode_palette[2]) }; -} - -void GUI_App::set_mode_palette(const std::vector& palette) -{ - bool save = false; - - for (size_t mode = 0; mode < palette.size(); ++mode) { - const wxColour& clr = palette[mode]; - std::string color_str = clr == wxTransparentColour ? std::string("") : encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); - if (m_mode_palette[mode] != color_str) { - m_mode_palette[mode] = color_str; - save = true; - } - } - - if (save) { - mainframe->update_mode_markers(); - app_config->set("mode_palette", escape_strings_cstyle(m_mode_palette)); - app_config->save(); - } -} - -bool GUI_App::tabs_as_menu() const -{ - return app_config->get("tabs_as_menu") == "1"; // || dark_mode(); -} - -wxSize GUI_App::get_min_size() const -{ - return wxSize(76*m_em_unit, 49 * m_em_unit); -} - -float GUI_App::toolbar_icon_scale(const bool is_limited/* = false*/) const -{ -#ifdef __APPLE__ - const float icon_sc = 1.0f; // for Retina display will be used its own scale -#else - const float icon_sc = m_em_unit*0.1f; -#endif // __APPLE__ - - const std::string& use_val = app_config->get("use_custom_toolbar_size"); - const std::string& val = app_config->get("custom_toolbar_size"); - const std::string& auto_val = app_config->get("auto_toolbar_size"); - - if (val.empty() || auto_val.empty() || use_val.empty()) - return icon_sc; - - int int_val = use_val == "0" ? 100 : atoi(val.c_str()); - // correct value in respect to auto_toolbar_size - int_val = std::min(atoi(auto_val.c_str()), int_val); - - if (is_limited && int_val < 50) - int_val = 50; - - return 0.01f * int_val * icon_sc; -} - -void GUI_App::set_auto_toolbar_icon_scale(float scale) const -{ -#ifdef __APPLE__ - const float icon_sc = 1.0f; // for Retina display will be used its own scale -#else - const float icon_sc = m_em_unit * 0.1f; -#endif // __APPLE__ - - long int_val = std::min(int(std::lround(scale / icon_sc * 100)), 100); - std::string val = std::to_string(int_val); - - app_config->set("auto_toolbar_size", val); -} - -// check user printer_presets for the containing information about "Print Host upload" -void GUI_App::check_printer_presets() -{ - std::vector preset_names = PhysicalPrinter::presets_with_print_host_information(preset_bundle->printers); - if (preset_names.empty()) - return; - - wxString msg_text = _L("You have the following presets with saved options for \"Print Host upload\"") + ":"; - for (const std::string& preset_name : preset_names) - msg_text += "\n \"" + from_u8(preset_name) + "\","; - msg_text.RemoveLast(); - msg_text += "\n\n" + _L("But since this version of PrusaSlicer we don't show this information in Printer Settings anymore.\n" - "Settings will be available in physical printers settings.") + "\n\n" + - _L("By default new Printer devices will be named as \"Printer N\" during its creation.\n" - "Note: This name can be changed later from the physical printers settings"); - - //wxMessageDialog(nullptr, msg_text, _L("Information"), wxOK | wxICON_INFORMATION).ShowModal(); - MessageDialog(nullptr, msg_text, _L("Information"), wxOK | wxICON_INFORMATION).ShowModal(); - - preset_bundle->physical_printers.load_printers_from_presets(preset_bundle->printers); -} - -void GUI_App::recreate_GUI(const wxString& msg_name) -{ - m_is_recreating_gui = true; - - mainframe->shutdown(); - - wxProgressDialog dlg(msg_name, msg_name, 100, nullptr, wxPD_AUTO_HIDE); - dlg.Pulse(); - dlg.Update(10, _L("Recreating") + dots); - - MainFrame *old_main_frame = mainframe; - mainframe = new MainFrame(); - if (is_editor()) - // hide settings tabs after first Layout - mainframe->select_tab(size_t(0)); - // Propagate model objects to object list. - sidebar().obj_list()->init_objects(); - SetTopWindow(mainframe); - - dlg.Update(30, _L("Recreating") + dots); - old_main_frame->Destroy(); - - dlg.Update(80, _L("Loading of current presets") + dots); - m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); - load_current_presets(); - mainframe->Show(true); - - dlg.Update(90, _L("Loading of a mode view") + dots); - - obj_list()->set_min_height(); - update_mode(); - - // #ys_FIXME_delete_after_testing Do we still need this ? -// CallAfter([]() { -// // Run the config wizard, don't offer the "reset user profile" checkbox. -// config_wizard_startup(true); -// }); - - m_is_recreating_gui = false; -} - -void GUI_App::system_info() -{ - SysInfoDialog dlg; - dlg.ShowModal(); -} - -void GUI_App::keyboard_shortcuts() -{ - KBShortcutsDialog dlg; - dlg.ShowModal(); -} - -// static method accepting a wxWindow object as first parameter -bool GUI_App::catch_error(std::function cb, - // wxMessageDialog* message_dialog, - const std::string& err /*= ""*/) -{ - if (!err.empty()) { - if (cb) - cb(); - // if (message_dialog) - // message_dialog->(err, "Error", wxOK | wxICON_ERROR); - show_error(/*this*/nullptr, err); - return true; - } - return false; -} - -// static method accepting a wxWindow object as first parameter -void fatal_error(wxWindow* parent) -{ - show_error(parent, ""); - // exit 1; // #ys_FIXME -} - -#ifdef _WIN32 - -#ifdef _MSW_DARK_MODE -static void update_scrolls(wxWindow* window) -{ - wxWindowList::compatibility_iterator node = window->GetChildren().GetFirst(); - while (node) - { - wxWindow* win = node->GetData(); - if (dynamic_cast(win) || - dynamic_cast(win) || - dynamic_cast(win)) - NppDarkMode::SetDarkExplorerTheme(win->GetHWND()); - - update_scrolls(win); - node = node->GetNext(); - } -} -#endif //_MSW_DARK_MODE - - -#ifdef _MSW_DARK_MODE -void GUI_App::force_menu_update() -{ - NppDarkMode::SetSystemMenuForApp(app_config->get("sys_menu_enabled") == "1"); -} -#endif //_MSW_DARK_MODE - -void GUI_App::force_colors_update() -{ -#ifdef _MSW_DARK_MODE - NppDarkMode::SetDarkMode(app_config->get("dark_color_mode") == "1"); - if (WXHWND wxHWND = wxToolTip::GetToolTipCtrl()) - NppDarkMode::SetDarkExplorerTheme((HWND)wxHWND); - NppDarkMode::SetDarkTitleBar(mainframe->GetHWND()); - NppDarkMode::SetDarkTitleBar(mainframe->m_settings_dialog.GetHWND()); -#endif //_MSW_DARK_MODE - m_force_colors_update = true; -} -#endif //_WIN32 - -// Called after the Preferences dialog is closed and the program settings are saved. -// Update the UI based on the current preferences. -void GUI_App::update_ui_from_settings() -{ - update_label_colours(); -#ifdef _WIN32 - // Upadte UI colors before Update UI from settings - if (m_force_colors_update) { - m_force_colors_update = false; - mainframe->force_color_changed(); - mainframe->diff_dialog.force_color_changed(); - mainframe->preferences_dialog->force_color_changed(); - mainframe->printhost_queue_dlg()->force_color_changed(); -#ifdef _MSW_DARK_MODE - update_scrolls(mainframe); - if (mainframe->is_dlg_layout()) { - // update for tabs bar - UpdateDarkUI(&mainframe->m_settings_dialog); - mainframe->m_settings_dialog.Fit(); - mainframe->m_settings_dialog.Refresh(); - // update scrollbars - update_scrolls(&mainframe->m_settings_dialog); - } -#endif //_MSW_DARK_MODE - } -#endif - mainframe->update_ui_from_settings(); -} - -void GUI_App::persist_window_geometry(wxTopLevelWindow *window, bool default_maximized) -{ - const std::string name = into_u8(window->GetName()); - - window->Bind(wxEVT_CLOSE_WINDOW, [=](wxCloseEvent &event) { - window_pos_save(window, name); - event.Skip(); - }); - - window_pos_restore(window, name, default_maximized); - - on_window_geometry(window, [=]() { - window_pos_sanitize(window); - }); -} - -void GUI_App::load_project(wxWindow *parent, wxString& input_file) const -{ - input_file.Clear(); - wxFileDialog dialog(parent ? parent : GetTopWindow(), - _L("Choose one file (3MF/AMF):"), - app_config->get_last_dir(), "", - file_wildcards(FT_PROJECT), wxFD_OPEN | wxFD_FILE_MUST_EXIST); - - if (dialog.ShowModal() == wxID_OK) - input_file = dialog.GetPath(); -} - -void GUI_App::import_model(wxWindow *parent, wxArrayString& input_files) const -{ - input_files.Clear(); - wxFileDialog dialog(parent ? parent : GetTopWindow(), - _L("Choose one or more files (STL/3MF/STEP/OBJ/AMF/PRUSA):"), - from_u8(app_config->get_last_dir()), "", - file_wildcards(FT_MODEL), wxFD_OPEN | wxFD_MULTIPLE | wxFD_FILE_MUST_EXIST); - - if (dialog.ShowModal() == wxID_OK) - dialog.GetPaths(input_files); -} - -void GUI_App::load_gcode(wxWindow* parent, wxString& input_file) const -{ - input_file.Clear(); - wxFileDialog dialog(parent ? parent : GetTopWindow(), - _L("Choose one file (GCODE/.GCO/.G/.ngc/NGC):"), - app_config->get_last_dir(), "", - file_wildcards(FT_GCODE), wxFD_OPEN | wxFD_FILE_MUST_EXIST); - - if (dialog.ShowModal() == wxID_OK) - input_file = dialog.GetPath(); -} - -bool GUI_App::switch_language() -{ - if (select_language()) { - recreate_GUI(_L("Changing of an application language") + dots); - return true; - } else { - return false; - } -} - -#ifdef __linux__ -static const wxLanguageInfo* linux_get_existing_locale_language(const wxLanguageInfo* language, - const wxLanguageInfo* system_language) -{ - constexpr size_t max_len = 50; - char path[max_len] = ""; - std::vector locales; - const std::string lang_prefix = into_u8(language->CanonicalName.BeforeFirst('_')); - - // Call locale -a so we can parse the output to get the list of available locales - // We expect lines such as "en_US.utf8". Pick ones starting with the language code - // we are switching to. Lines with different formatting will be removed later. - FILE* fp = popen("locale -a", "r"); - if (fp != NULL) { - while (fgets(path, max_len, fp) != NULL) { - std::string line(path); - line = line.substr(0, line.find('\n')); - if (boost::starts_with(line, lang_prefix)) - locales.push_back(line); - } - pclose(fp); - } - - // locales now contain all candidates for this language. - // Sort them so ones containing anything about UTF-8 are at the end. - std::sort(locales.begin(), locales.end(), [](const std::string& a, const std::string& b) - { - auto has_utf8 = [](const std::string & s) { - auto S = boost::to_upper_copy(s); - return S.find("UTF8") != std::string::npos || S.find("UTF-8") != std::string::npos; - }; - return ! has_utf8(a) && has_utf8(b); - }); - - // Remove the suffix behind a dot, if there is one. - for (std::string& s : locales) - s = s.substr(0, s.find(".")); - - // We just hope that dear Linux "locale -a" returns country codes - // in ISO 3166-1 alpha-2 code (two letter) format. - // https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes - // To be sure, remove anything not looking as expected - // (any number of lowercase letters, underscore, two uppercase letters). - locales.erase(std::remove_if(locales.begin(), - locales.end(), - [](const std::string& s) { - return ! std::regex_match(s, - std::regex("^[a-z]+_[A-Z]{2}$")); - }), - locales.end()); - - if (system_language) { - // Is there a candidate matching a country code of a system language? Move it to the end, - // while maintaining the order of matches, so that the best match ends up at the very end. - std::string system_country = "_" + into_u8(system_language->CanonicalName.AfterFirst('_')).substr(0, 2); - int cnt = locales.size(); - for (int i = 0; i < cnt; ++i) - if (locales[i].find(system_country) != std::string::npos) { - locales.emplace_back(std::move(locales[i])); - locales[i].clear(); - } - } - - // Now try them one by one. - for (auto it = locales.rbegin(); it != locales.rend(); ++ it) - if (! it->empty()) { - const std::string &locale = *it; - const wxLanguageInfo* lang = wxLocale::FindLanguageInfo(from_u8(locale)); - if (wxLocale::IsAvailable(lang->Language)) - return lang; - } - return language; -} -#endif - -int GUI_App::GetSingleChoiceIndex(const wxString& message, - const wxString& caption, - const wxArrayString& choices, - int initialSelection) -{ -#ifdef _WIN32 - wxSingleChoiceDialog dialog(nullptr, message, caption, choices); - wxGetApp().UpdateDlgDarkUI(&dialog); - - dialog.SetSelection(initialSelection); - return dialog.ShowModal() == wxID_OK ? dialog.GetSelection() : -1; -#else - return wxGetSingleChoiceIndex(message, caption, choices, initialSelection); -#endif -} - -// select language from the list of installed languages -bool GUI_App::select_language() -{ - wxArrayString translations = wxTranslations::Get()->GetAvailableTranslations(SLIC3R_APP_KEY); - std::vector language_infos; - language_infos.emplace_back(wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH)); - for (size_t i = 0; i < translations.GetCount(); ++ i) { - const wxLanguageInfo *langinfo = wxLocale::FindLanguageInfo(translations[i]); - if (langinfo != nullptr) - language_infos.emplace_back(langinfo); - } - sort_remove_duplicates(language_infos); - std::sort(language_infos.begin(), language_infos.end(), [](const wxLanguageInfo* l, const wxLanguageInfo* r) { return l->Description < r->Description; }); - - wxArrayString names; - names.Alloc(language_infos.size()); - - // Some valid language should be selected since the application start up. - const wxLanguage current_language = wxLanguage(m_wxLocale->GetLanguage()); - int init_selection = -1; - int init_selection_alt = -1; - int init_selection_default = -1; - for (size_t i = 0; i < language_infos.size(); ++ i) { - if (wxLanguage(language_infos[i]->Language) == current_language) - // The dictionary matches the active language and country. - init_selection = i; - else if ((language_infos[i]->CanonicalName.BeforeFirst('_') == m_wxLocale->GetCanonicalName().BeforeFirst('_')) || - // if the active language is Slovak, mark the Czech language as active. - (language_infos[i]->CanonicalName.BeforeFirst('_') == "cs" && m_wxLocale->GetCanonicalName().BeforeFirst('_') == "sk")) - // The dictionary matches the active language, it does not necessarily match the country. - init_selection_alt = i; - if (language_infos[i]->CanonicalName.BeforeFirst('_') == "en") - // This will be the default selection if the active language does not match any dictionary. - init_selection_default = i; - names.Add(language_infos[i]->Description); - } - if (init_selection == -1) - // This is the dictionary matching the active language. - init_selection = init_selection_alt; - if (init_selection != -1) - // This is the language to highlight in the choice dialog initially. - init_selection_default = init_selection; - - const long index = GetSingleChoiceIndex(_L("Select the language"), _L("Language"), names, init_selection_default); - // Try to load a new language. - if (index != -1 && (init_selection == -1 || init_selection != index)) { - const wxLanguageInfo *new_language_info = language_infos[index]; - if (this->load_language(new_language_info->CanonicalName, false)) { - // Save language at application config. - // Which language to save as the selected dictionary language? - // 1) Hopefully the language set to wxTranslations by this->load_language(), but that API is weird and we don't want to rely on its - // stability in the future: - // wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); - // 2) Current locale language may not match the dictionary name, see GH issue #3901 - // m_wxLocale->GetCanonicalName() - // 3) new_language_info->CanonicalName is a safe bet. It points to a valid dictionary name. - app_config->set("translation_language", new_language_info->CanonicalName.ToUTF8().data()); - app_config->save(); - return true; - } - } - - return false; -} - -// Load gettext translation files and activate them at the start of the application, -// based on the "translation_language" key stored in the application config. -bool GUI_App::load_language(wxString language, bool initial) -{ - if (initial) { - // There is a static list of lookup path prefixes in wxWidgets. Add ours. - wxFileTranslationsLoader::AddCatalogLookupPathPrefix(from_u8(localization_dir())); - // Get the active language from PrusaSlicer.ini, or empty string if the key does not exist. - language = app_config->get("translation_language"); - if (! language.empty()) - BOOST_LOG_TRIVIAL(trace) << boost::format("translation_language provided by PrusaSlicer.ini: %1%") % language; - - // Get the system language. - { - const wxLanguage lang_system = wxLanguage(wxLocale::GetSystemLanguage()); - if (lang_system != wxLANGUAGE_UNKNOWN) { - m_language_info_system = wxLocale::GetLanguageInfo(lang_system); - BOOST_LOG_TRIVIAL(trace) << boost::format("System language detected (user locales and such): %1%") % m_language_info_system->CanonicalName.ToUTF8().data(); - } - } - { - // Allocating a temporary locale will switch the default wxTranslations to its internal wxTranslations instance. - wxLocale temp_locale; -#ifdef __WXOSX__ - // ysFIXME - temporary workaround till it isn't fixed in wxWidgets: - // Use English as an initial language, because of under OSX it try to load "inappropriate" language for wxLANGUAGE_DEFAULT. - // For example in our case it's trying to load "en_CZ" and as a result PrusaSlicer catch warning message. - // But wxWidgets guys work on it. - temp_locale.Init(wxLANGUAGE_ENGLISH); -#else - temp_locale.Init(); -#endif // __WXOSX__ - // Set the current translation's language to default, otherwise GetBestTranslation() may not work (see the wxWidgets source code). - wxTranslations::Get()->SetLanguage(wxLANGUAGE_DEFAULT); - // Let the wxFileTranslationsLoader enumerate all translation dictionaries for PrusaSlicer - // and try to match them with the system specific "preferred languages". - // There seems to be a support for that on Windows and OSX, while on Linuxes the code just returns wxLocale::GetSystemLanguage(). - // The last parameter gets added to the list of detected dictionaries. This is a workaround - // for not having the English dictionary. Let's hope wxWidgets of various versions process this call the same way. - wxString best_language = wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); - if (! best_language.IsEmpty()) { - m_language_info_best = wxLocale::FindLanguageInfo(best_language); - BOOST_LOG_TRIVIAL(trace) << boost::format("Best translation language detected (may be different from user locales): %1%") % m_language_info_best->CanonicalName.ToUTF8().data(); - } - #ifdef __linux__ - wxString lc_all; - if (wxGetEnv("LC_ALL", &lc_all) && ! lc_all.IsEmpty()) { - // Best language returned by wxWidgets on Linux apparently does not respect LC_ALL. - // Disregard the "best" suggestion in case LC_ALL is provided. - m_language_info_best = nullptr; - } - #endif - } - } - - const wxLanguageInfo *language_info = language.empty() ? nullptr : wxLocale::FindLanguageInfo(language); - if (! language.empty() && (language_info == nullptr || language_info->CanonicalName.empty())) { - // Fix for wxWidgets issue, where the FindLanguageInfo() returns locales with undefined ANSII code (wxLANGUAGE_KONKANI or wxLANGUAGE_MANIPURI). - language_info = nullptr; - BOOST_LOG_TRIVIAL(error) << boost::format("Language code \"%1%\" is not supported") % language.ToUTF8().data(); - } - - if (language_info != nullptr && language_info->LayoutDirection == wxLayout_RightToLeft) { - BOOST_LOG_TRIVIAL(trace) << boost::format("The following language code requires right to left layout, which is not supported by PrusaSlicer: %1%") % language_info->CanonicalName.ToUTF8().data(); - language_info = nullptr; - } - - if (language_info == nullptr) { - // PrusaSlicer does not support the Right to Left languages yet. - if (m_language_info_system != nullptr && m_language_info_system->LayoutDirection != wxLayout_RightToLeft) - language_info = m_language_info_system; - if (m_language_info_best != nullptr && m_language_info_best->LayoutDirection != wxLayout_RightToLeft) - language_info = m_language_info_best; - if (language_info == nullptr) - language_info = wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH_US); - } - - BOOST_LOG_TRIVIAL(trace) << boost::format("Switching wxLocales to %1%") % language_info->CanonicalName.ToUTF8().data(); - - // Alternate language code. - wxLanguage language_dict = wxLanguage(language_info->Language); - if (language_info->CanonicalName.BeforeFirst('_') == "sk") { - // Slovaks understand Czech well. Give them the Czech translation. - language_dict = wxLANGUAGE_CZECH; - BOOST_LOG_TRIVIAL(trace) << "Using Czech dictionaries for Slovak language"; - } - - // Select language for locales. This language may be different from the language of the dictionary. - if (language_info == m_language_info_best || language_info == m_language_info_system) { - // The current language matches user's default profile exactly. That's great. - } else if (m_language_info_best != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_best->CanonicalName.BeforeFirst('_')) { - // Use whatever the operating system recommends, if it the language code of the dictionary matches the recommended language. - // This allows a Swiss guy to use a German dictionary without forcing him to German locales. - language_info = m_language_info_best; - } else if (m_language_info_system != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_system->CanonicalName.BeforeFirst('_')) - language_info = m_language_info_system; - -#ifdef __linux__ - // If we can't find this locale , try to use different one for the language - // instead of just reporting that it is impossible to switch. - if (! wxLocale::IsAvailable(language_info->Language)) { - std::string original_lang = into_u8(language_info->CanonicalName); - language_info = linux_get_existing_locale_language(language_info, m_language_info_system); - BOOST_LOG_TRIVIAL(trace) << boost::format("Can't switch language to %1% (missing locales). Using %2% instead.") - % original_lang % language_info->CanonicalName.ToUTF8().data(); - } -#endif - - if (! wxLocale::IsAvailable(language_info->Language)) { - // Loading the language dictionary failed. - wxString message = "Switching PrusaSlicer to language " + language_info->CanonicalName + " failed."; -#if !defined(_WIN32) && !defined(__APPLE__) - // likely some linux system - message += "\nYou may need to reconfigure the missing locales, likely by running the \"locale-gen\" and \"dpkg-reconfigure locales\" commands.\n"; -#endif - if (initial) - message + "\n\nApplication will close."; - wxMessageBox(message, "PrusaSlicer - Switching language failed", wxOK | wxICON_ERROR); - if (initial) - std::exit(EXIT_FAILURE); - else - return false; - } - - // Release the old locales, create new locales. - //FIXME wxWidgets cause havoc if the current locale is deleted. We just forget it causing memory leaks for now. - m_wxLocale.release(); - m_wxLocale = Slic3r::make_unique(); - m_wxLocale->Init(language_info->Language); - // Override language at the active wxTranslations class (which is stored in the active m_wxLocale) - // to load possibly different dictionary, for example, load Czech dictionary for Slovak language. - wxTranslations::Get()->SetLanguage(language_dict); - { - // UKR Localization specific workaround till the wxWidgets doesn't fixed: - // From wxWidgets 3.1.6 calls setlocation(0, wxInfoLanguage->LocaleTag), see (https://github.com/prusa3d/wxWidgets/commit/deef116a09748796711d1e3509965ee208dcdf0b#diff-7de25e9a71c4dce61bbf76492c589623d5b93fd1bb105ceaf0662075d15f4472), - // where LocaleTag is a Tag of locale in BCP 47 - like notation. - // For Ukrainian Language LocaleTag == "uk". - // But setlocale(0, "uk") returns "English_United Kingdom.1252" instead of "uk", - // and, as a result, locales are set to English_United Kingdom - - if (language_info->CanonicalName == "uk") - setlocale(0, language_info->GetCanonicalWithRegion().data()); - } - m_wxLocale->AddCatalog(SLIC3R_APP_KEY); - m_imgui->set_language(into_u8(language_info->CanonicalName)); - //FIXME This is a temporary workaround, the correct solution is to switch to "C" locale during file import / export only. - //wxSetlocale(LC_NUMERIC, "C"); - Preset::update_suffix_modified((" (" + _L("modified") + ")").ToUTF8().data()); - return true; -} - -Tab* GUI_App::get_tab(Preset::Type type) -{ - for (Tab* tab: tabs_list) - if (tab->type() == type) - return tab->completed() ? tab : nullptr; // To avoid actions with no-completed Tab - return nullptr; -} - -ConfigOptionMode GUI_App::get_mode() -{ - if (!app_config->has("view_mode")) - return comSimple; - - const auto mode = app_config->get("view_mode"); - return mode == "expert" ? comExpert : - mode == "simple" ? comSimple : comAdvanced; -} - -void GUI_App::save_mode(const /*ConfigOptionMode*/int mode) -{ - const std::string mode_str = mode == comExpert ? "expert" : - mode == comSimple ? "simple" : "advanced"; - app_config->set("view_mode", mode_str); - app_config->save(); - update_mode(); -} - -// Update view mode according to selected menu -void GUI_App::update_mode() -{ - sidebar().update_mode(); - -#ifdef _WIN32 //_MSW_DARK_MODE - if (!wxGetApp().tabs_as_menu()) - dynamic_cast(mainframe->m_tabpanel)->UpdateMode(); -#endif - - for (auto tab : tabs_list) - tab->update_mode(); - - plater()->update_menus(); - plater()->canvas3D()->update_gizmos_on_off_state(); -} - -void GUI_App::add_config_menu(wxMenuBar *menu) -{ - auto local_menu = new wxMenu(); - wxWindowID config_id_base = wxWindow::NewControlId(int(ConfigMenuCnt)); - - const auto config_wizard_name = _(ConfigWizard::name(true)); - const auto config_wizard_tooltip = from_u8((boost::format(_utf8(L("Run %s"))) % config_wizard_name).str()); - // Cmd+, is standard on OS X - what about other operating systems? - if (is_editor()) { - local_menu->Append(config_id_base + ConfigMenuWizard, config_wizard_name + dots, config_wizard_tooltip); - local_menu->Append(config_id_base + ConfigMenuSnapshots, _L("&Configuration Snapshots") + dots, _L("Inspect / activate configuration snapshots")); - local_menu->Append(config_id_base + ConfigMenuTakeSnapshot, _L("Take Configuration &Snapshot"), _L("Capture a configuration snapshot")); - local_menu->Append(config_id_base + ConfigMenuUpdateConf, _L("Check for Configuration Updates"), _L("Check for configuration updates")); - local_menu->Append(config_id_base + ConfigMenuUpdateApp, _L("Check for Application Updates"), _L("Check for new version of application")); -#if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) - //if (DesktopIntegrationDialog::integration_possible()) - local_menu->Append(config_id_base + ConfigMenuDesktopIntegration, _L("Desktop Integration"), _L("Desktop Integration")); -#endif //(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) - local_menu->AppendSeparator(); - } - local_menu->Append(config_id_base + ConfigMenuPreferences, _L("&Preferences") + dots + -#ifdef __APPLE__ - "\tCtrl+,", -#else - "\tCtrl+P", -#endif - _L("Application preferences")); - wxMenu* mode_menu = nullptr; - if (is_editor()) { - local_menu->AppendSeparator(); - mode_menu = new wxMenu(); - mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeSimple, _L("Simple"), _L("Simple View Mode")); -// mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _L("Advanced"), _L("Advanced View Mode")); - mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _CTX(L_CONTEXT("Advanced", "Mode"), "Mode"), _L("Advanced View Mode")); - mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeExpert, _L("Expert"), _L("Expert View Mode")); - Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comSimple) evt.Check(true); }, config_id_base + ConfigMenuModeSimple); - Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comAdvanced) evt.Check(true); }, config_id_base + ConfigMenuModeAdvanced); - Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comExpert) evt.Check(true); }, config_id_base + ConfigMenuModeExpert); - - local_menu->AppendSubMenu(mode_menu, _L("Mode"), wxString::Format(_L("%s View Mode"), SLIC3R_APP_NAME)); - } - local_menu->AppendSeparator(); - local_menu->Append(config_id_base + ConfigMenuLanguage, _L("&Language")); - if (is_editor()) { - local_menu->AppendSeparator(); - local_menu->Append(config_id_base + ConfigMenuFlashFirmware, _L("Flash Printer &Firmware"), _L("Upload a firmware image into an Arduino based printer")); - // TODO: for when we're able to flash dictionaries - // local_menu->Append(config_id_base + FirmwareMenuDict, _L("Flash Language File"), _L("Upload a language dictionary file into a Prusa printer")); - } - - local_menu->Bind(wxEVT_MENU, [this, config_id_base](wxEvent &event) { - switch (event.GetId() - config_id_base) { - case ConfigMenuWizard: - run_wizard(ConfigWizard::RR_USER); - break; - case ConfigMenuUpdateConf: - check_updates(true); - break; - case ConfigMenuUpdateApp: - app_version_check(true); - break; -#ifdef __linux__ - case ConfigMenuDesktopIntegration: - show_desktop_integration_dialog(); - break; -#endif - case ConfigMenuTakeSnapshot: - // Take a configuration snapshot. - if (wxString action_name = _L("Taking a configuration snapshot"); - check_and_save_current_preset_changes(action_name, _L("Some presets are modified and the unsaved changes will not be captured by the configuration snapshot."), false, true)) { - wxTextEntryDialog dlg(nullptr, action_name, _L("Snapshot name")); - UpdateDlgDarkUI(&dlg); - - // set current normal font for dialog children, - // because of just dlg.SetFont(normal_font()) has no result; - for (auto child : dlg.GetChildren()) - child->SetFont(normal_font()); - - if (dlg.ShowModal() == wxID_OK) - if (const Config::Snapshot *snapshot = Config::take_config_snapshot_report_error( - *app_config, Config::Snapshot::SNAPSHOT_USER, dlg.GetValue().ToUTF8().data()); - snapshot != nullptr) - app_config->set("on_snapshot", snapshot->id); - } - break; - case ConfigMenuSnapshots: - if (check_and_save_current_preset_changes(_L("Loading a configuration snapshot"), "", false)) { - std::string on_snapshot; - if (Config::SnapshotDB::singleton().is_on_snapshot(*app_config)) - on_snapshot = app_config->get("on_snapshot"); - ConfigSnapshotDialog dlg(Slic3r::GUI::Config::SnapshotDB::singleton(), on_snapshot); - dlg.ShowModal(); - if (!dlg.snapshot_to_activate().empty()) { - if (! Config::SnapshotDB::singleton().is_on_snapshot(*app_config) && - ! Config::take_config_snapshot_cancel_on_error(*app_config, Config::Snapshot::SNAPSHOT_BEFORE_ROLLBACK, "", - GUI::format(_L("Continue to activate a configuration snapshot %1%?"), dlg.snapshot_to_activate()))) - break; - try { - app_config->set("on_snapshot", Config::SnapshotDB::singleton().restore_snapshot(dlg.snapshot_to_activate(), *app_config).id); - // Enable substitutions, log both user and system substitutions. There should not be any substitutions performed when loading system - // presets because compatibility of profiles shall be verified using the min_slic3r_version keys in config index, but users - // are known to be creative and mess with the config files in various ways. - if (PresetsConfigSubstitutions all_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::Enable); - ! all_substitutions.empty()) - show_substitutions_info(all_substitutions); - - // Load the currently selected preset into the GUI, update the preset selection box. - load_current_presets(); - } catch (std::exception &ex) { - GUI::show_error(nullptr, _L("Failed to activate configuration snapshot.") + "\n" + into_u8(ex.what())); - } - } - } - break; - case ConfigMenuPreferences: - { - open_preferences(); - break; - } - case ConfigMenuLanguage: - { - /* Before change application language, let's check unsaved changes on 3D-Scene - * and draw user's attention to the application restarting after a language change - */ - { - // the dialog needs to be destroyed before the call to switch_language() - // or sometimes the application crashes into wxDialogBase() destructor - // so we put it into an inner scope - wxString title = is_editor() ? wxString(SLIC3R_APP_NAME) : wxString(GCODEVIEWER_APP_NAME); - title += " - " + _L("Language selection"); - //wxMessageDialog dialog(nullptr, - MessageDialog dialog(nullptr, - _L("Switching the language will trigger application restart.\n" - "You will lose content of the plater.") + "\n\n" + - _L("Do you want to proceed?"), - title, - wxICON_QUESTION | wxOK | wxCANCEL); - if (dialog.ShowModal() == wxID_CANCEL) - return; - } - - switch_language(); - break; - } - case ConfigMenuFlashFirmware: - FirmwareDialog::run(mainframe); - break; - default: - break; - } - }); - - using std::placeholders::_1; - - if (mode_menu != nullptr) { - auto modfn = [this](int mode, wxCommandEvent&) { if (get_mode() != mode) save_mode(mode); }; - mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comSimple, _1), config_id_base + ConfigMenuModeSimple); - mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comAdvanced, _1), config_id_base + ConfigMenuModeAdvanced); - mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comExpert, _1), config_id_base + ConfigMenuModeExpert); - } - - menu->Append(local_menu, _L("&Configuration")); -} - -void GUI_App::open_preferences(const std::string& highlight_option /*= std::string()*/, const std::string& tab_name/*= std::string()*/) -{ - mainframe->preferences_dialog->show(highlight_option, tab_name); - - if (mainframe->preferences_dialog->recreate_GUI()) - recreate_GUI(_L("Restart application") + dots); - -#if ENABLE_GCODE_LINES_ID_IN_H_SLIDER - if (dlg.seq_top_layer_only_changed() || dlg.seq_seq_top_gcode_indices_changed()) -#else - if (mainframe->preferences_dialog->seq_top_layer_only_changed()) -#endif // ENABLE_GCODE_LINES_ID_IN_H_SLIDER - this->plater_->refresh_print(); - -#ifdef _WIN32 - if (is_editor()) { - if (app_config->get("associate_3mf") == "1") - associate_3mf_files(); - if (app_config->get("associate_stl") == "1") - associate_stl_files(); - } - else { - if (app_config->get("associate_gcode") == "1") - associate_gcode_files(); - } -#endif // _WIN32 - - if (mainframe->preferences_dialog->settings_layout_changed()) { - // hide full main_sizer for mainFrame - mainframe->GetSizer()->Show(false); - mainframe->update_layout(); - mainframe->select_tab(size_t(0)); - } -} - -bool GUI_App::has_unsaved_preset_changes() const -{ - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (const Tab* const tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology) && tab->saved_preset_is_dirty()) - return true; - } - return false; -} - -bool GUI_App::has_current_preset_changes() const -{ - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (const Tab* const tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) - return true; - } - return false; -} - -void GUI_App::update_saved_preset_from_current_preset() -{ - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (Tab* tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology)) - tab->update_saved_preset_from_current_preset(); - } -} - -std::vector GUI_App::get_active_preset_collections() const -{ - std::vector ret; - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (const Tab* tab : tabs_list) - if (tab->supports_printer_technology(printer_technology)) - ret.push_back(tab->get_presets()); - return ret; -} - -// To notify the user whether he is aware that some preset changes will be lost, -// UnsavedChangesDialog: "Discard / Save / Cancel" -// This is called when: -// - Close Application & Current project isn't saved -// - Load Project & Current project isn't saved -// - Undo / Redo with change of print technologie -// - Loading snapshot -// - Loading config_file/bundle -// UnsavedChangesDialog: "Don't save / Save / Cancel" -// This is called when: -// - Exporting config_bundle -// - Taking snapshot -bool GUI_App::check_and_save_current_preset_changes(const wxString& caption, const wxString& header, bool remember_choice/* = true*/, bool dont_save_insted_of_discard/* = false*/) -{ - if (has_current_preset_changes()) { - const std::string app_config_key = remember_choice ? "default_action_on_close_application" : ""; - int act_buttons = ActionButtons::SAVE; - if (dont_save_insted_of_discard) - act_buttons |= ActionButtons::DONT_SAVE; - UnsavedChangesDialog dlg(caption, header, app_config_key, act_buttons); - std::string act = app_config_key.empty() ? "none" : wxGetApp().app_config->get(app_config_key); - if (act == "none" && dlg.ShowModal() == wxID_CANCEL) - return false; - - if (dlg.save_preset()) // save selected changes - { - for (const std::pair& nt : dlg.get_names_and_types()) - preset_bundle->save_changes_for_preset(nt.first, nt.second, dlg.get_unselected_options(nt.second)); - - load_current_presets(false); - - // if we saved changes to the new presets, we should to - // synchronize config.ini with the current selections. - preset_bundle->export_selections(*app_config); - - MessageDialog(nullptr, dlg.msg_success_saved_modifications(dlg.get_names_and_types().size())).ShowModal(); - } - } - - return true; -} - -void GUI_App::apply_keeped_preset_modifications() -{ - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (Tab* tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology)) - tab->apply_config_from_cache(); - } - load_current_presets(false); -} - -// This is called when creating new project or load another project -// OR close ConfigWizard -// to ask the user what should we do with unsaved changes for presets. -// New Project => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Cancel" -// => Current project isn't saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" -// Close ConfigWizard => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" -// Note: no_nullptr postponed_apply_of_keeped_changes indicates that thie function is called after ConfigWizard is closed -bool GUI_App::check_and_keep_current_preset_changes(const wxString& caption, const wxString& header, int action_buttons, bool* postponed_apply_of_keeped_changes/* = nullptr*/) -{ - if (has_current_preset_changes()) { - bool is_called_from_configwizard = postponed_apply_of_keeped_changes != nullptr; - - const std::string app_config_key = is_called_from_configwizard ? "" : "default_action_on_new_project"; - UnsavedChangesDialog dlg(caption, header, app_config_key, action_buttons); - std::string act = app_config_key.empty() ? "none" : wxGetApp().app_config->get(app_config_key); - if (act == "none" && dlg.ShowModal() == wxID_CANCEL) - return false; - - auto reset_modifications = [this, is_called_from_configwizard]() { - if (is_called_from_configwizard) - return; // no need to discared changes. It will be done fromConfigWizard closing - - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (const Tab* const tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) - tab->m_presets->discard_current_changes(); - } - load_current_presets(false); - }; - - if (dlg.discard()) - reset_modifications(); - else // save selected changes - { - const auto& preset_names_and_types = dlg.get_names_and_types(); - if (dlg.save_preset()) { - for (const std::pair& nt : preset_names_and_types) - preset_bundle->save_changes_for_preset(nt.first, nt.second, dlg.get_unselected_options(nt.second)); - - // if we saved changes to the new presets, we should to - // synchronize config.ini with the current selections. - preset_bundle->export_selections(*app_config); - - wxString text = dlg.msg_success_saved_modifications(preset_names_and_types.size()); - if (!is_called_from_configwizard) - text += "\n\n" + _L("For new project all modifications will be reseted"); - - MessageDialog(nullptr, text).ShowModal(); - reset_modifications(); - } - else if (dlg.transfer_changes() && (dlg.has_unselected_options() || is_called_from_configwizard)) { - // execute this part of code only if not all modifications are keeping to the new project - // OR this function is called when ConfigWizard is closed and "Keep modifications" is selected - for (const std::pair& nt : preset_names_and_types) { - Preset::Type type = nt.second; - Tab* tab = get_tab(type); - std::vector selected_options = dlg.get_selected_options(type); - if (type == Preset::TYPE_PRINTER) { - auto it = std::find(selected_options.begin(), selected_options.end(), "extruders_count"); - if (it != selected_options.end()) { - // erase "extruders_count" option from the list - selected_options.erase(it); - // cache the extruders count - static_cast(tab)->cache_extruder_cnt(); - } - } - tab->cache_config_diff(selected_options); - if (!is_called_from_configwizard) - tab->m_presets->discard_current_changes(); - } - if (is_called_from_configwizard) - *postponed_apply_of_keeped_changes = true; - else - apply_keeped_preset_modifications(); - } - } - } - - return true; -} - -bool GUI_App::can_load_project() -{ - int saved_project = plater()->save_project_if_dirty(_L("Loading a new project while the current project is modified.")); - if (saved_project == wxID_CANCEL || - (plater()->is_project_dirty() && saved_project == wxID_NO && - !check_and_save_current_preset_changes(_L("Project is loading"), _L("Opening new project while some presets are unsaved.")))) - return false; - return true; -} - -bool GUI_App::check_print_host_queue() -{ - wxString dirty; - std::vector> jobs; - // Get ongoing jobs from dialog - mainframe->m_printhost_queue_dlg->get_active_jobs(jobs); - if (jobs.empty()) - return true; - // Show dialog - wxString job_string = wxString(); - for (const auto& job : jobs) { - job_string += format_wxstr(" %1% : %2% \n", job.first, job.second); - } - wxString message; - message += _(L("The uploads are still ongoing")) + ":\n\n" + job_string +"\n" + _(L("Stop them and continue anyway?")); - //wxMessageDialog dialog(mainframe, - MessageDialog dialog(mainframe, - message, - wxString(SLIC3R_APP_NAME) + " - " + _(L("Ongoing uploads")), - wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT); - if (dialog.ShowModal() == wxID_YES) - return true; - - // TODO: If already shown, bring forward - mainframe->m_printhost_queue_dlg->Show(); - return false; -} - -bool GUI_App::checked_tab(Tab* tab) -{ - bool ret = true; - if (find(tabs_list.begin(), tabs_list.end(), tab) == tabs_list.end()) - ret = false; - return ret; -} - -// Update UI / Tabs to reflect changes in the currently loaded presets -void GUI_App::load_current_presets(bool check_printer_presets_ /*= true*/) -{ - // check printer_presets for the containing information about "Print Host upload" - // and create physical printer from it, if any exists - if (check_printer_presets_) - check_printer_presets(); - - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - this->plater()->set_printer_technology(printer_technology); - for (Tab *tab : tabs_list) - if (tab->supports_printer_technology(printer_technology)) { - if (tab->type() == Preset::TYPE_PRINTER) { - static_cast(tab)->update_pages(); - // Mark the plater to update print bed by tab->load_current_preset() from Plater::on_config_change(). - this->plater()->force_print_bed_update(); - } - tab->load_current_preset(); - } -} - -bool GUI_App::OnExceptionInMainLoop() -{ - generic_exception_handle(); - return false; -} - -#ifdef __APPLE__ -// This callback is called from wxEntry()->wxApp::CallOnInit()->NSApplication run -// that is, before GUI_App::OnInit(), so we have a chance to switch GUI_App -// to a G-code viewer. -void GUI_App::OSXStoreOpenFiles(const wxArrayString &fileNames) -{ - size_t num_gcodes = 0; - for (const wxString &filename : fileNames) - if (is_gcode_file(into_u8(filename))) - ++ num_gcodes; - if (fileNames.size() == num_gcodes) { - // Opening PrusaSlicer by drag & dropping a G-Code onto PrusaSlicer icon in Finder, - // just G-codes were passed. Switch to G-code viewer mode. - m_app_mode = EAppMode::GCodeViewer; - unlock_lockfile(get_instance_hash_string() + ".lock", data_dir() + "/cache/"); - if(app_config != nullptr) - delete app_config; - app_config = nullptr; - init_app_config(); - } - wxApp::OSXStoreOpenFiles(fileNames); -} -// wxWidgets override to get an event on open files. -void GUI_App::MacOpenFiles(const wxArrayString &fileNames) -{ - std::vector files; - std::vector gcode_files; - std::vector non_gcode_files; - for (const auto& filename : fileNames) { - if (is_gcode_file(into_u8(filename))) - gcode_files.emplace_back(filename); - else { - files.emplace_back(into_u8(filename)); - non_gcode_files.emplace_back(filename); - } - } - if (m_app_mode == EAppMode::GCodeViewer) { - // Running in G-code viewer. - // Load the first G-code into the G-code viewer. - // Or if no G-codes, send other files to slicer. - if (! gcode_files.empty()) { - if (m_post_initialized) - this->plater()->load_gcode(gcode_files.front()); - else - this->init_params->input_files = { into_u8(gcode_files.front()) }; - } - if (!non_gcode_files.empty()) - start_new_slicer(non_gcode_files, true); - } else { - if (! files.empty()) { - if (m_post_initialized) { - wxArrayString input_files; - for (size_t i = 0; i < non_gcode_files.size(); ++i) - input_files.push_back(non_gcode_files[i]); - this->plater()->load_files(input_files); - } else { - for (const auto &f : non_gcode_files) - this->init_params->input_files.emplace_back(into_u8(f)); - } - } - for (const wxString &filename : gcode_files) - start_new_gcodeviewer(&filename); - } -} - -void GUI_App::MacOpenURL(const wxString& url) -{ - if (app_config && app_config->get("downloader_url_registered") != "1") - { - BOOST_LOG_TRIVIAL(error) << "Recieved command to open URL, but it is not allowed in app configuration. URL: " << url; - return; - } - start_download(boost::nowide::narrow(url)); -} - -#endif /* __APPLE */ - -Sidebar& GUI_App::sidebar() -{ - return plater_->sidebar(); -} - -ObjectManipulation* GUI_App::obj_manipul() -{ - // If this method is called before plater_ has been initialized, return nullptr (to avoid a crash) - return (plater_ != nullptr) ? sidebar().obj_manipul() : nullptr; -} - -ObjectSettings* GUI_App::obj_settings() -{ - return sidebar().obj_settings(); -} - -ObjectList* GUI_App::obj_list() -{ - return sidebar().obj_list(); -} - -ObjectLayers* GUI_App::obj_layers() -{ - return sidebar().obj_layers(); -} - -Plater* GUI_App::plater() -{ - return plater_; -} - -const Plater* GUI_App::plater() const -{ - return plater_; -} - -Model& GUI_App::model() -{ - return plater_->model(); -} -wxBookCtrlBase* GUI_App::tab_panel() const -{ - return mainframe->m_tabpanel; -} - -NotificationManager* GUI_App::notification_manager() -{ - return plater_->get_notification_manager(); -} - -GalleryDialog* GUI_App::gallery_dialog() -{ - return mainframe->gallery_dialog(); -} - -Downloader* GUI_App::downloader() -{ - return m_downloader.get(); -} - -// extruders count from selected printer preset -int GUI_App::extruders_cnt() const -{ - const Preset& preset = preset_bundle->printers.get_selected_preset(); - return preset.printer_technology() == ptSLA ? 1 : - preset.config.option("nozzle_diameter")->values.size(); -} - -// extruders count from edited printer preset -int GUI_App::extruders_edited_cnt() const -{ - const Preset& preset = preset_bundle->printers.get_edited_preset(); - return preset.printer_technology() == ptSLA ? 1 : - preset.config.option("nozzle_diameter")->values.size(); -} - -wxString GUI_App::current_language_code_safe() const -{ - // Translate the language code to a code, for which Prusa Research maintains translations. - const std::map mapping { - { "cs", "cs_CZ", }, - { "sk", "cs_CZ", }, - { "de", "de_DE", }, - { "es", "es_ES", }, - { "fr", "fr_FR", }, - { "it", "it_IT", }, - { "ja", "ja_JP", }, - { "ko", "ko_KR", }, - { "pl", "pl_PL", }, - { "uk", "uk_UA", }, - { "zh", "zh_CN", }, - { "ru", "ru_RU", }, - }; - wxString language_code = this->current_language_code().BeforeFirst('_'); - auto it = mapping.find(language_code); - if (it != mapping.end()) - language_code = it->second; - else - language_code = "en_US"; - return language_code; -} - -void GUI_App::open_web_page_localized(const std::string &http_address) -{ - open_browser_with_warning_dialog(http_address + "&lng=" + this->current_language_code_safe(), nullptr, false); -} - -// If we are switching from the FFF-preset to the SLA, we should to control the printed objects if they have a part(s). -// Because of we can't to print the multi-part objects with SLA technology. -bool GUI_App::may_switch_to_SLA_preset(const wxString& caption) -{ - if (model_has_multi_part_objects(model())) { - show_info(nullptr, - _L("It's impossible to print multi-part object(s) with SLA technology.") + "\n\n" + - _L("Please check your object list before preset changing."), - caption); - return false; - } - if (model_has_connectors(model())) { - show_info(nullptr, - _L("SLA technology doesn't support cut with connectors") + "\n\n" + - _L("Please check your object list before preset changing."), - caption); - return false; - } - return true; -} - -bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage start_page) -{ - wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); - - if (reason == ConfigWizard::RR_USER) { - if (preset_updater->config_update(app_config->orig_version(), PresetUpdater::UpdateParams::FORCED_BEFORE_WIZARD) == PresetUpdater::R_ALL_CANCELED) - return false; - } - - auto wizard = new ConfigWizard(mainframe); - const bool res = wizard->run(reason, start_page); - - if (res) { - load_current_presets(); - - // #ysFIXME - delete after testing: This part of code looks redundant. All checks are inside ConfigWizard::priv::apply_config() - if (preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA) - may_switch_to_SLA_preset(_L("Configuration is editing from ConfigWizard")); - } - - return res; -} - -void GUI_App::show_desktop_integration_dialog() -{ -#ifdef __linux__ - //wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); - DesktopIntegrationDialog dialog(mainframe); - dialog.ShowModal(); -#endif //__linux__ -} - -#if ENABLE_THUMBNAIL_GENERATOR_DEBUG -void GUI_App::gcode_thumbnails_debug() -{ - const std::string BEGIN_MASK = "; thumbnail begin"; - const std::string END_MASK = "; thumbnail end"; - std::string gcode_line; - bool reading_image = false; - unsigned int width = 0; - unsigned int height = 0; - - wxFileDialog dialog(GetTopWindow(), _L("Select a gcode file:"), "", "", "G-code files (*.gcode)|*.gcode;*.GCODE;", wxFD_OPEN | wxFD_FILE_MUST_EXIST); - if (dialog.ShowModal() != wxID_OK) - return; - - std::string in_filename = into_u8(dialog.GetPath()); - std::string out_path = boost::filesystem::path(in_filename).remove_filename().append(L"thumbnail").string(); - - boost::nowide::ifstream in_file(in_filename.c_str()); - std::vector rows; - std::string row; - if (in_file.good()) - { - while (std::getline(in_file, gcode_line)) - { - if (in_file.good()) - { - if (boost::starts_with(gcode_line, BEGIN_MASK)) - { - reading_image = true; - gcode_line = gcode_line.substr(BEGIN_MASK.length() + 1); - std::string::size_type x_pos = gcode_line.find('x'); - std::string width_str = gcode_line.substr(0, x_pos); - width = (unsigned int)::atoi(width_str.c_str()); - std::string height_str = gcode_line.substr(x_pos + 1); - height = (unsigned int)::atoi(height_str.c_str()); - row.clear(); - } - else if (reading_image && boost::starts_with(gcode_line, END_MASK)) - { - std::string out_filename = out_path + std::to_string(width) + "x" + std::to_string(height) + ".png"; - boost::nowide::ofstream out_file(out_filename.c_str(), std::ios::binary); - if (out_file.good()) - { - std::string decoded; - decoded.resize(boost::beast::detail::base64::decoded_size(row.size())); - decoded.resize(boost::beast::detail::base64::decode((void*)&decoded[0], row.data(), row.size()).first); - - out_file.write(decoded.c_str(), decoded.size()); - out_file.close(); - } - - reading_image = false; - width = 0; - height = 0; - rows.clear(); - } - else if (reading_image) - row += gcode_line.substr(2); - } - } - - in_file.close(); - } -} -#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG - -void GUI_App::window_pos_save(wxTopLevelWindow* window, const std::string &name) -{ - if (name.empty()) { return; } - const auto config_key = (boost::format("window_%1%") % name).str(); - - WindowMetrics metrics = WindowMetrics::from_window(window); - app_config->set(config_key, metrics.serialize()); - app_config->save(); -} - -void GUI_App::window_pos_restore(wxTopLevelWindow* window, const std::string &name, bool default_maximized) -{ - if (name.empty()) { return; } - const auto config_key = (boost::format("window_%1%") % name).str(); - - if (! app_config->has(config_key)) { - window->Maximize(default_maximized); - return; - } - - auto metrics = WindowMetrics::deserialize(app_config->get(config_key)); - if (! metrics) { - window->Maximize(default_maximized); - return; - } - - const wxRect& rect = metrics->get_rect(); - - if (app_config->get("restore_win_position") == "1") { - // workaround for crash related to the positioning of the window on secondary monitor - app_config->set("restore_win_position", (boost::format("crashed_at_%1%_pos") % name).str()); - app_config->save(); - window->SetPosition(rect.GetPosition()); - - // workaround for crash related to the positioning of the window on secondary monitor - app_config->set("restore_win_position", (boost::format("crashed_at_%1%_size") % name).str()); - app_config->save(); - window->SetSize(rect.GetSize()); - - // revert "restore_win_position" value if application wasn't crashed - app_config->set("restore_win_position", "1"); - app_config->save(); - } - else - window->CenterOnScreen(); - - window->Maximize(metrics->get_maximized()); -} - -void GUI_App::window_pos_sanitize(wxTopLevelWindow* window) -{ - /*unsigned*/int display_idx = wxDisplay::GetFromWindow(window); - wxRect display; - if (display_idx == wxNOT_FOUND) { - display = wxDisplay(0u).GetClientArea(); - window->Move(display.GetTopLeft()); - } else { - display = wxDisplay(display_idx).GetClientArea(); - } - - auto metrics = WindowMetrics::from_window(window); - metrics.sanitize_for_display(display); - if (window->GetScreenRect() != metrics.get_rect()) { - window->SetSize(metrics.get_rect()); - } -} - -bool GUI_App::config_wizard_startup() -{ - if (!m_app_conf_exists || preset_bundle->printers.only_default_printers()) { - run_wizard(ConfigWizard::RR_DATA_EMPTY); - return true; - } else if (get_app_config()->legacy_datadir()) { - // Looks like user has legacy pre-vendorbundle data directory, - // explain what this is and run the wizard - - MsgDataLegacy dlg; - dlg.ShowModal(); - - run_wizard(ConfigWizard::RR_DATA_LEGACY); - return true; - } - return false; -} - -bool GUI_App::check_updates(const bool verbose) -{ - PresetUpdater::UpdateResult updater_result; - try { - updater_result = preset_updater->config_update(app_config->orig_version(), verbose ? PresetUpdater::UpdateParams::SHOW_TEXT_BOX : PresetUpdater::UpdateParams::SHOW_NOTIFICATION); - if (updater_result == PresetUpdater::R_INCOMPAT_EXIT) { - mainframe->Close(); - // Applicaiton is closing. - return false; - } - else if (updater_result == PresetUpdater::R_INCOMPAT_CONFIGURED) { - m_app_conf_exists = true; - } - else if (verbose && updater_result == PresetUpdater::R_NOOP) { - MsgNoUpdates dlg; - dlg.ShowModal(); - } - } - catch (const std::exception & ex) { - show_error(nullptr, ex.what()); - } - // Applicaiton will continue. - return true; -} - -bool GUI_App::open_browser_with_warning_dialog(const wxString& url, wxWindow* parent/* = nullptr*/, bool force_remember_choice /*= true*/, int flags/* = 0*/) -{ - bool launch = true; - - // warning dialog containes a "Remember my choice" checkbox - std::string option_key = "suppress_hyperlinks"; - if (force_remember_choice || app_config->get(option_key).empty()) { - if (app_config->get(option_key).empty()) { - RichMessageDialog dialog(parent, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); - dialog.ShowCheckBox(_L("Remember my choice")); - auto answer = dialog.ShowModal(); - launch = answer == wxID_YES; - if (dialog.IsCheckBoxChecked()) { - wxString preferences_item = _L("Suppress to open hyperlink in browser"); - wxString msg = - _L("PrusaSlicer will remember your choice.") + "\n\n" + - _L("You will not be asked about it again on hyperlinks hovering.") + "\n\n" + - format_wxstr(_L("Visit \"Preferences\" and check \"%1%\"\nto changes your choice."), preferences_item); - - MessageDialog msg_dlg(parent, msg, _L("PrusaSlicer: Don't ask me again"), wxOK | wxCANCEL | wxICON_INFORMATION); - if (msg_dlg.ShowModal() == wxID_CANCEL) - return false; - app_config->set(option_key, answer == wxID_NO ? "1" : "0"); - } - } - if (launch) - launch = app_config->get(option_key) != "1"; - } - // warning dialog doesn't containe a "Remember my choice" checkbox - // and will be shown only when "Suppress to open hyperlink in browser" is ON. - else if (app_config->get(option_key) == "1") { - MessageDialog dialog(parent, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); - launch = dialog.ShowModal() == wxID_YES; - } - - return launch && wxLaunchDefaultBrowser(url, flags); -} - -// static method accepting a wxWindow object as first parameter -// void warning_catcher{ -// my($self, $message_dialog) = @_; -// return sub{ -// my $message = shift; -// return if $message = ~/ GLUquadricObjPtr | Attempt to free unreferenced scalar / ; -// my @params = ($message, 'Warning', wxOK | wxICON_WARNING); -// $message_dialog -// ? $message_dialog->(@params) -// : Wx::MessageDialog->new($self, @params)->ShowModal; -// }; -// } - -// Do we need this function??? -// void GUI_App::notify(message) { -// auto frame = GetTopWindow(); -// // try harder to attract user attention on OS X -// if (!frame->IsActive()) -// frame->RequestUserAttention(defined(__WXOSX__/*&Wx::wxMAC */)? wxUSER_ATTENTION_ERROR : wxUSER_ATTENTION_INFO); -// -// // There used to be notifier using a Growl application for OSX, but Growl is dead. -// // The notifier also supported the Linux X D - bus notifications, but that support was broken. -// //TODO use wxNotificationMessage ? -// } - - -#ifdef __WXMSW__ -void GUI_App::associate_3mf_files() -{ - associate_file_type(L".3mf", L"Prusa.Slicer.1", L"PrusaSlicer", true); -} - -void GUI_App::associate_stl_files() -{ - associate_file_type(L".stl", L"Prusa.Slicer.1", L"PrusaSlicer", true); -} - -void GUI_App::associate_gcode_files() -{ - associate_file_type(L".gcode", L"PrusaSlicer.GCodeViewer.1", L"PrusaSlicerGCodeViewer", true); -} -#endif // __WXMSW__ - -void GUI_App::on_version_read(wxCommandEvent& evt) -{ - app_config->set("version_online", into_u8(evt.GetString())); - app_config->save(); - std::string opt = app_config->get("notify_release"); - if (this->plater_ == nullptr || (opt != "all" && opt != "release")) { - return; - } - if (*Semver::parse(SLIC3R_VERSION) >= *Semver::parse(into_u8(evt.GetString()))) { - return; - } - // notification - /* - this->plater_->get_notification_manager()->push_notification(NotificationType::NewAppAvailable - , NotificationManager::NotificationLevel::ImportantNotificationLevel - , Slic3r::format(_u8L("New release version %1% is available."), evt.GetString()) - , _u8L("See Download page.") - , [](wxEvtHandler* evnthndlr) {wxGetApp().open_web_page_localized("https://www.prusa3d.com/slicerweb"); return true; } - ); - */ - // updater - // read triggered_by_user that was set when calling GUI_App::app_version_check - app_updater(m_app_updater->get_triggered_by_user()); -} - -void GUI_App::app_updater(bool from_user) -{ - DownloadAppData app_data = m_app_updater->get_app_data(); - - if (from_user && (!app_data.version || *app_data.version <= *Semver::parse(SLIC3R_VERSION))) - { - BOOST_LOG_TRIVIAL(info) << "There is no newer version online."; - MsgNoAppUpdates no_update_dialog; - no_update_dialog.ShowModal(); - return; - - } - - assert(!app_data.url.empty()); - assert(!app_data.target_path.empty()); - - // dialog with new version info - AppUpdateAvailableDialog dialog(*Semver::parse(SLIC3R_VERSION), *app_data.version); - auto dialog_result = dialog.ShowModal(); - // checkbox "do not show again" - if (dialog.disable_version_check()) { - app_config->set("notify_release", "none"); - } - // Doesn't wish to update - if (dialog_result != wxID_OK) { - return; - } - // dialog with new version download (installer or app dependent on system) including path selection - AppUpdateDownloadDialog dwnld_dlg(*app_data.version, app_data.target_path); - dialog_result = dwnld_dlg.ShowModal(); - // Doesn't wish to download - if (dialog_result != wxID_OK) { - return; - } - app_data.target_path =dwnld_dlg.get_download_path(); - - // start download - this->plater_->get_notification_manager()->push_download_progress_notification(_utf8("Download"), std::bind(&AppUpdater::cancel_callback, this->m_app_updater.get())); - app_data.start_after = dwnld_dlg.run_after_download(); - m_app_updater->set_app_data(std::move(app_data)); - m_app_updater->sync_download(); -} - -void GUI_App::app_version_check(bool from_user) -{ - if (from_user) { - if (m_app_updater->get_download_ongoing()) { - MessageDialog msgdlg(nullptr, _L("Download of new version is already ongoing. Do you wish to continue?"), _L("Notice"), wxYES_NO); - if (msgdlg.ShowModal() != wxID_YES) - return; - } - } - std::string version_check_url = app_config->version_check_url(); - m_app_updater->sync_version(version_check_url, from_user); -} - -void GUI_App::start_download(std::string url) -{ - if (!plater_) { - BOOST_LOG_TRIVIAL(error) << "Could not start URL download: plater is nullptr."; - return; - } - //lets always init so if the download dest folder was changed, new dest is used - boost::filesystem::path dest_folder(app_config->get("url_downloader_dest")); - if (dest_folder.empty() || !boost::filesystem::is_directory(dest_folder)) { - std::string msg = _utf8("Could not start URL download. Destination folder is not set. Please choose destination folder in Configuration Wizard."); - BOOST_LOG_TRIVIAL(error) << msg; - show_error(nullptr, msg); - return; - } - m_downloader->init(dest_folder); - m_downloader->start_download(url); -} - -} // GUI -} //Slic3r +#include "libslic3r/Technologies.hpp" +#include "GUI_App.hpp" +#include "GUI_Init.hpp" +#include "GUI_ObjectList.hpp" +#include "GUI_ObjectManipulation.hpp" +#include "GUI_Factories.hpp" +#include "format.hpp" +#include "I18N.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "libslic3r/Utils.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/I18N.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Color.hpp" + +#include "GUI.hpp" +#include "GUI_Utils.hpp" +#include "3DScene.hpp" +#include "MainFrame.hpp" +#include "Plater.hpp" +#include "GLCanvas3D.hpp" + +#include "../Utils/PresetUpdater.hpp" +#include "../Utils/PrintHost.hpp" +#include "../Utils/Process.hpp" +#include "../Utils/MacDarkMode.hpp" +#include "../Utils/AppUpdater.hpp" +#include "../Utils/WinRegistry.hpp" +#include "slic3r/Config/Snapshot.hpp" +#include "ConfigSnapshotDialog.hpp" +#include "FirmwareDialog.hpp" +#include "Preferences.hpp" +#include "Tab.hpp" +#include "SysInfoDialog.hpp" +#include "KBShortcutsDialog.hpp" +#include "UpdateDialogs.hpp" +#include "Mouse3DController.hpp" +#include "RemovableDriveManager.hpp" +#include "InstanceCheck.hpp" +#include "NotificationManager.hpp" +#include "UnsavedChangesDialog.hpp" +#include "SavePresetDialog.hpp" +#include "PrintHostDialogs.hpp" +#include "DesktopIntegrationDialog.hpp" +#include "SendSystemInfoDialog.hpp" +#include "Downloader.hpp" + +#include "BitmapCache.hpp" +#include "Notebook.hpp" + +#ifdef __WXMSW__ +#include +#include +#ifdef _MSW_DARK_MODE +#include +#endif // _MSW_DARK_MODE +#endif +#ifdef _WIN32 +#include +#endif + +#if ENABLE_THUMBNAIL_GENERATOR_DEBUG +#include +#include +#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG + +// Needed for forcing menu icons back under gtk2 and gtk3 +#if defined(__WXGTK20__) || defined(__WXGTK3__) + #include +#endif + +using namespace std::literals; + +namespace Slic3r { +namespace GUI { + +class MainFrame; + +class SplashScreen : public wxSplashScreen +{ +public: + SplashScreen(const wxBitmap& bitmap, long splashStyle, int milliseconds, wxPoint pos = wxDefaultPosition) + : wxSplashScreen(bitmap, splashStyle, milliseconds, static_cast(wxGetApp().mainframe), wxID_ANY, wxDefaultPosition, wxDefaultSize, +#ifdef __APPLE__ + wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR | wxSTAY_ON_TOP +#else + wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR +#endif // !__APPLE__ + ) + { + wxASSERT(bitmap.IsOk()); + +// int init_dpi = get_dpi_for_window(this); + this->SetPosition(pos); + // The size of the SplashScreen can be hanged after its moving to another display + // So, update it from a bitmap size + this->SetClientSize(bitmap.GetWidth(), bitmap.GetHeight()); + this->CenterOnScreen(); +// int new_dpi = get_dpi_for_window(this); + +// m_scale = (float)(new_dpi) / (float)(init_dpi); + m_main_bitmap = bitmap; + +// scale_bitmap(m_main_bitmap, m_scale); + + // init constant texts and scale fonts + init_constant_text(); + + // this font will be used for the action string + m_action_font = m_constant_text.credits_font.Bold(); + + // draw logo and constant info text + Decorate(m_main_bitmap); + } + + void SetText(const wxString& text) + { + set_bitmap(m_main_bitmap); + if (!text.empty()) { + wxBitmap bitmap(m_main_bitmap); + + wxMemoryDC memDC; + memDC.SelectObject(bitmap); + + memDC.SetFont(m_action_font); + memDC.SetTextForeground(wxColour(237, 107, 33)); + memDC.DrawText(text, int(m_scale * 60), m_action_line_y_position); + + memDC.SelectObject(wxNullBitmap); + set_bitmap(bitmap); +#ifdef __WXOSX__ + // without this code splash screen wouldn't be updated under OSX + wxYield(); +#endif + } + } + + static wxBitmap MakeBitmap(wxBitmap bmp) + { + if (!bmp.IsOk()) + return wxNullBitmap; + + // create dark grey background for the splashscreen + // It will be 5/3 of the weight of the bitmap + int width = lround((double)5 / 3 * bmp.GetWidth()); + int height = bmp.GetHeight(); + + wxImage image(width, height); + unsigned char* imgdata_ = image.GetData(); + for (int i = 0; i < width * height; ++i) { + *imgdata_++ = 51; + *imgdata_++ = 51; + *imgdata_++ = 51; + } + + wxBitmap new_bmp(image); + + wxMemoryDC memDC; + memDC.SelectObject(new_bmp); + memDC.DrawBitmap(bmp, width - bmp.GetWidth(), 0, true); + + return new_bmp; + } + + void Decorate(wxBitmap& bmp) + { + if (!bmp.IsOk()) + return; + + // draw text to the box at the left of the splashscreen. + // this box will be 2/5 of the weight of the bitmap, and be at the left. + int width = lround(bmp.GetWidth() * 0.4); + + // load bitmap for logo + BitmapCache bmp_cache; + int logo_size = lround(width * 0.25); + wxBitmap logo_bmp = *bmp_cache.load_svg(wxGetApp().logo_name(), logo_size, logo_size); + + wxCoord margin = int(m_scale * 20); + + wxRect banner_rect(wxPoint(0, logo_size), wxPoint(width, bmp.GetHeight())); + banner_rect.Deflate(margin, 2 * margin); + + // use a memory DC to draw directly onto the bitmap + wxMemoryDC memDc(bmp); + + // draw logo + memDc.DrawBitmap(logo_bmp, margin, margin, true); + + // draw the (white) labels inside of our black box (at the left of the splashscreen) + memDc.SetTextForeground(wxColour(255, 255, 255)); + + memDc.SetFont(m_constant_text.title_font); + memDc.DrawLabel(m_constant_text.title, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); + + int title_height = memDc.GetTextExtent(m_constant_text.title).GetY(); + banner_rect.SetTop(banner_rect.GetTop() + title_height); + banner_rect.SetHeight(banner_rect.GetHeight() - title_height); + + memDc.SetFont(m_constant_text.version_font); + memDc.DrawLabel(m_constant_text.version, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); + int version_height = memDc.GetTextExtent(m_constant_text.version).GetY(); + + memDc.SetFont(m_constant_text.credits_font); + memDc.DrawLabel(m_constant_text.credits, banner_rect, wxALIGN_BOTTOM | wxALIGN_LEFT); + int credits_height = memDc.GetMultiLineTextExtent(m_constant_text.credits).GetY(); + int text_height = memDc.GetTextExtent("text").GetY(); + + // calculate position for the dynamic text + int logo_and_header_height = margin + logo_size + title_height + version_height; + m_action_line_y_position = logo_and_header_height + 0.5 * (bmp.GetHeight() - margin - credits_height - logo_and_header_height - text_height); + } + +private: + wxBitmap m_main_bitmap; + wxFont m_action_font; + int m_action_line_y_position; + float m_scale {1.0}; + + struct ConstantText + { + wxString title; + wxString version; + wxString credits; + + wxFont title_font; + wxFont version_font; + wxFont credits_font; + + void init(wxFont init_font) + { + // title + title = wxGetApp().is_editor() ? SLIC3R_APP_NAME : GCODEVIEWER_APP_NAME; + + // dynamically get the version to display + version = _L("Version") + " " + std::string(SLIC3R_VERSION); + + // credits infornation + credits = title + " " + + _L("is based on Slic3r by Alessandro Ranellucci and the RepRap community.") + "\n" + + _L("Developed by Prusa Research.") + "\n\n" + + title + " " + _L("is licensed under the") + " " + _L("GNU Affero General Public License, version 3") + ".\n\n" + + _L("Contributions by Vojtech Bubnik, Enrico Turri, Oleksandra Iushchenko, Tamas Meszaros, Lukas Matena, Vojtech Kral, David Kocik and numerous others.") + "\n\n" + + _L("Artwork model by Creative Tools"); + + title_font = version_font = credits_font = init_font; + } + } + m_constant_text; + + void init_constant_text() + { + m_constant_text.init(get_default_font(this)); + + // As default we use a system font for current display. + // Scale fonts in respect to banner width + + int text_banner_width = lround(0.4 * m_main_bitmap.GetWidth()) - roundl(m_scale * 50); // banner_width - margins + + float title_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.title).GetX(); + scale_font(m_constant_text.title_font, title_font_scale > 3.5f ? 3.5f : title_font_scale); + + float version_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.version).GetX(); + scale_font(m_constant_text.version_font, version_font_scale > 2.f ? 2.f : version_font_scale); + + // The width of the credits information string doesn't respect to the banner width some times. + // So, scale credits_font in the respect to the longest string width + int longest_string_width = word_wrap_string(m_constant_text.credits); + float font_scale = (float)text_banner_width / longest_string_width; + scale_font(m_constant_text.credits_font, font_scale); + } + + void set_bitmap(wxBitmap& bmp) + { + m_window->SetBitmap(bmp); + m_window->Refresh(); + m_window->Update(); + } + + void scale_bitmap(wxBitmap& bmp, float scale) + { + if (scale == 1.0) + return; + + wxImage image = bmp.ConvertToImage(); + if (!image.IsOk() || image.GetWidth() == 0 || image.GetHeight() == 0) + return; + + int width = int(scale * image.GetWidth()); + int height = int(scale * image.GetHeight()); + image.Rescale(width, height, wxIMAGE_QUALITY_BILINEAR); + + bmp = wxBitmap(std::move(image)); + } + + void scale_font(wxFont& font, float scale) + { +#ifdef __WXMSW__ + // Workaround for the font scaling in respect to the current active display, + // not for the primary display, as it's implemented in Font.cpp + // See https://github.com/wxWidgets/wxWidgets/blob/master/src/msw/font.cpp + // void wxNativeFontInfo::SetFractionalPointSize(float pointSizeNew) + wxNativeFontInfo nfi= *font.GetNativeFontInfo(); + float pointSizeNew = wxDisplay(this).GetScaleFactor() * scale * font.GetPointSize(); + nfi.lf.lfHeight = nfi.GetLogFontHeightAtPPI(pointSizeNew, get_dpi_for_window(this)); + nfi.pointSize = pointSizeNew; + font = wxFont(nfi); +#else + font.Scale(scale); +#endif //__WXMSW__ + } + + // wrap a string for the strings no longer then 55 symbols + // return extent of the longest string + int word_wrap_string(wxString& input) + { + size_t line_len = 55;// count of symbols in one line + int idx = -1; + size_t cur_len = 0; + + wxString longest_sub_string; + auto get_longest_sub_string = [input](wxString &longest_sub_str, size_t cur_len, size_t i) { + if (cur_len > longest_sub_str.Len()) + longest_sub_str = input.SubString(i - cur_len + 1, i); + }; + + for (size_t i = 0; i < input.Len(); i++) + { + cur_len++; + if (input[i] == ' ') + idx = i; + if (input[i] == '\n') + { + get_longest_sub_string(longest_sub_string, cur_len, i); + idx = -1; + cur_len = 0; + } + if (cur_len >= line_len && idx >= 0) + { + get_longest_sub_string(longest_sub_string, cur_len, i); + input[idx] = '\n'; + cur_len = i - static_cast(idx); + } + } + + return GetTextExtent(longest_sub_string).GetX(); + } +}; + + +#ifdef __linux__ +bool static check_old_linux_datadir(const wxString& app_name) { + // If we are on Linux and the datadir does not exist yet, look into the old + // location where the datadir was before version 2.3. If we find it there, + // tell the user that he might wanna migrate to the new location. + // (https://github.com/prusa3d/PrusaSlicer/issues/2911) + // To be precise, the datadir should exist, it is created when single instance + // lock happens. Instead of checking for existence, check the contents. + + namespace fs = boost::filesystem; + + std::string new_path = Slic3r::data_dir(); + + wxString dir; + if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) + dir = wxFileName::GetHomeDir() + wxS("/.config"); + std::string default_path = (dir + "/" + app_name).ToUTF8().data(); + + if (new_path != default_path) { + // This happens when the user specifies a custom --datadir. + // Do not show anything in that case. + return true; + } + + fs::path data_dir = fs::path(new_path); + if (! fs::is_directory(data_dir)) + return true; // This should not happen. + + int file_count = std::distance(fs::directory_iterator(data_dir), fs::directory_iterator()); + + if (file_count <= 1) { // just cache dir with an instance lock + std::string old_path = wxStandardPaths::Get().GetUserDataDir().ToUTF8().data(); + + if (fs::is_directory(old_path)) { + wxString msg = from_u8((boost::format(_u8L("Starting with %1% 2.3, configuration " + "directory on Linux has changed (according to XDG Base Directory Specification) to \n%2%.\n\n" + "This directory did not exist yet (maybe you run the new version for the first time).\nHowever, " + "an old %1% configuration directory was detected in \n%3%.\n\n" + "Consider moving the contents of the old directory to the new location in order to access " + "your profiles, etc.\nNote that if you decide to downgrade %1% in future, it will use the old " + "location again.\n\n" + "What do you want to do now?")) % SLIC3R_APP_NAME % new_path % old_path).str()); + wxString caption = from_u8((boost::format(_u8L("%s - BREAKING CHANGE")) % SLIC3R_APP_NAME).str()); + RichMessageDialog dlg(nullptr, msg, caption, wxYES_NO); + dlg.SetYesNoLabels(_L("Quit, I will move my data now"), _L("Start the application")); + if (dlg.ShowModal() != wxID_NO) + return false; + } + } else { + // If the new directory exists, be silent. The user likely already saw the message. + } + return true; +} +#endif + +#ifdef _WIN32 +#if 0 // External Updater is replaced with AppUpdater.cpp +static bool run_updater_win() +{ + // find updater exe + boost::filesystem::path path_updater = boost::dll::program_location().parent_path() / "prusaslicer-updater.exe"; + // run updater. Original args: /silent -restartapp prusa-slicer.exe -startappfirst + std::string msg; + bool res = create_process(path_updater, L"/silent", msg); + if (!res) + BOOST_LOG_TRIVIAL(error) << msg; + return res; +} +#endif // 0 +#endif // _WIN32 + +struct FileWildcards { + std::string_view title; + std::vector file_extensions; +}; + + + +static const FileWildcards file_wildcards_by_type[FT_SIZE] = { + /* FT_STL */ { "STL files"sv, { ".stl"sv } }, + /* FT_OBJ */ { "OBJ files"sv, { ".obj"sv } }, + /* FT_OBJECT */ { "Object files"sv, { ".stl"sv, ".obj"sv } }, + /* FT_STEP */ { "STEP files"sv, { ".stp"sv, ".step"sv } }, + /* FT_AMF */ { "AMF files"sv, { ".amf"sv, ".zip.amf"sv, ".xml"sv } }, + /* FT_3MF */ { "3MF files"sv, { ".3mf"sv } }, + /* FT_GCODE */ { "G-code files"sv, { ".gcode"sv, ".gco"sv, ".g"sv, ".ngc"sv } }, + /* FT_MODEL */ { "Known files"sv, { ".stl"sv, ".obj"sv, ".3mf"sv, ".amf"sv, ".zip.amf"sv, ".xml"sv, ".step"sv, ".stp"sv } }, + /* FT_PROJECT */ { "Project files"sv, { ".3mf"sv, ".amf"sv, ".zip.amf"sv } }, + /* FT_FONTS */ { "Font files"sv, { ".ttc"sv, ".ttf"sv } }, + /* FT_GALLERY */ { "Known files"sv, { ".stl"sv, ".obj"sv } }, + + /* FT_INI */ { "INI files"sv, { ".ini"sv } }, + /* FT_SVG */ { "SVG files"sv, { ".svg"sv } }, + + /* FT_TEX */ { "Texture"sv, { ".png"sv, ".svg"sv } }, + + /* FT_SL1 */ { "Masked SLA files"sv, { ".sl1"sv, ".sl1s"sv, ".pwmx"sv } }, +}; + +#if ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR +wxString file_wildcards(FileType file_type) +{ + const FileWildcards& data = file_wildcards_by_type[file_type]; + std::string title; + std::string mask; + + // Generate cumulative first item + for (const std::string_view& ext : data.file_extensions) { + if (title.empty()) { + title = "*"; + title += ext; + mask = title; + } + else { + title += ", *"; + title += ext; + mask += ";*"; + mask += ext; + } + mask += ";*"; + mask += boost::to_upper_copy(std::string(ext)); + } + + wxString ret = GUI::format_wxstr("%s (%s)|%s", data.title, title, mask); + + // Adds an item for each of the extensions + if (data.file_extensions.size() > 1) { + for (const std::string_view& ext : data.file_extensions) { + title = "*"; + title += ext; + ret += GUI::format_wxstr("|%s (%s)|%s", data.title, title, title); + } + } + + return ret; +} +#else +// This function produces a Win32 file dialog file template mask to be consumed by wxWidgets on all platforms. +// The function accepts a custom extension parameter. If the parameter is provided, the custom extension +// will be added as a fist to the list. This is important for a "file save" dialog on OSX, which strips +// an extension from the provided initial file name and substitutes it with the default extension (the first one in the template). +wxString file_wildcards(FileType file_type, const std::string &custom_extension) +{ + const FileWildcards& data = file_wildcards_by_type[file_type]; + std::string title; + std::string mask; + std::string custom_ext_lower; + + if (! custom_extension.empty()) { + // Generate an extension into the title mask and into the list of extensions. + custom_ext_lower = boost::to_lower_copy(custom_extension); + const std::string custom_ext_upper = boost::to_upper_copy(custom_extension); + if (custom_ext_lower == custom_extension) { + // Add a lower case version. + title = std::string("*") + custom_ext_lower; + mask = title; + // Add an upper case version. + mask += ";*"; + mask += custom_ext_upper; + } else if (custom_ext_upper == custom_extension) { + // Add an upper case version. + title = std::string("*") + custom_ext_upper; + mask = title; + // Add a lower case version. + mask += ";*"; + mask += custom_ext_lower; + } else { + // Add the mixed case version only. + title = std::string("*") + custom_extension; + mask = title; + } + } + + for (const std::string_view &ext : data.file_extensions) + // Only add an extension if it was not added first as the custom extension. + if (ext != custom_ext_lower) { + if (title.empty()) { + title = "*"; + title += ext; + mask = title; + } else { + title += ", *"; + title += ext; + mask += ";*"; + mask += ext; + } + mask += ";*"; + mask += boost::to_upper_copy(std::string(ext)); + } + + return GUI::format_wxstr("%s (%s)|%s", data.title, title, mask); +} +#endif // ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR + +static std::string libslic3r_translate_callback(const char *s) { return wxGetTranslation(wxString(s, wxConvUTF8)).utf8_str().data(); } + +#ifdef WIN32 +#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) +static void register_win32_dpi_event() +{ + enum { WM_DPICHANGED_ = 0x02e0 }; + + wxWindow::MSWRegisterMessageHandler(WM_DPICHANGED_, [](wxWindow *win, WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam) { + const int dpi = wParam & 0xffff; + const auto rect = reinterpret_cast(lParam); + const wxRect wxrect(wxPoint(rect->top, rect->left), wxPoint(rect->bottom, rect->right)); + + DpiChangedEvent evt(EVT_DPI_CHANGED_SLICER, dpi, wxrect); + win->GetEventHandler()->AddPendingEvent(evt); + + return true; + }); +} +#endif // !wxVERSION_EQUAL_OR_GREATER_THAN + +static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 }; + +static void register_win32_device_notification_event() +{ + wxWindow::MSWRegisterMessageHandler(WM_DEVICECHANGE, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. + auto main_frame = dynamic_cast(win); + auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); + if (plater == nullptr) + // Maybe some other top level window like a dialog or maybe a pop-up menu? + return true; + PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam; + switch (wParam) { + case DBT_DEVICEARRIVAL: + if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) + plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); + else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { + PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; +// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) { +// printf("DBT_DEVICEARRIVAL %d - Media has arrived: %ws\n", msg_count, lpdbi->dbcc_name); + if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) + plater->GetEventHandler()->AddPendingEvent(HIDDeviceAttachedEvent(EVT_HID_DEVICE_ATTACHED, boost::nowide::narrow(lpdbi->dbcc_name))); + } + break; + case DBT_DEVICEREMOVECOMPLETE: + if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) + plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); + else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { + PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; +// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) +// printf("DBT_DEVICEARRIVAL %d - Media was removed: %ws\n", msg_count, lpdbi->dbcc_name); + if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) + plater->GetEventHandler()->AddPendingEvent(HIDDeviceDetachedEvent(EVT_HID_DEVICE_DETACHED, boost::nowide::narrow(lpdbi->dbcc_name))); + } + break; + default: + break; + } + return true; + }); + + wxWindow::MSWRegisterMessageHandler(MainFrame::WM_USER_MEDIACHANGED, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. + auto main_frame = dynamic_cast(win); + auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); + if (plater == nullptr) + // Maybe some other top level window like a dialog or maybe a pop-up menu? + return true; + wchar_t sPath[MAX_PATH]; + if (lParam == SHCNE_MEDIAINSERTED || lParam == SHCNE_MEDIAREMOVED) { + struct _ITEMIDLIST* pidl = *reinterpret_cast(wParam); + if (! SHGetPathFromIDList(pidl, sPath)) { + BOOST_LOG_TRIVIAL(error) << "MediaInserted: SHGetPathFromIDList failed"; + return false; + } + } + switch (lParam) { + case SHCNE_MEDIAINSERTED: + { + //printf("SHCNE_MEDIAINSERTED %S\n", sPath); + plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); + break; + } + case SHCNE_MEDIAREMOVED: + { + //printf("SHCNE_MEDIAREMOVED %S\n", sPath); + plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); + break; + } + default: +// printf("Unknown\n"); + break; + } + return true; + }); + + wxWindow::MSWRegisterMessageHandler(WM_INPUT, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + auto main_frame = dynamic_cast(Slic3r::GUI::find_toplevel_parent(win)); + auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); +// if (wParam == RIM_INPUTSINK && plater != nullptr && main_frame->IsActive()) { + if (wParam == RIM_INPUT && plater != nullptr && main_frame->IsActive()) { + RAWINPUT raw; + UINT rawSize = sizeof(RAWINPUT); + ::GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &raw, &rawSize, sizeof(RAWINPUTHEADER)); + if (raw.header.dwType == RIM_TYPEHID && plater->get_mouse3d_controller().handle_raw_input_win32(raw.data.hid.bRawData, raw.data.hid.dwSizeHid)) + return true; + } + return false; + }); + + wxWindow::MSWRegisterMessageHandler(WM_COPYDATA, [](wxWindow* win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + COPYDATASTRUCT* copy_data_structure = { 0 }; + copy_data_structure = (COPYDATASTRUCT*)lParam; + if (copy_data_structure->dwData == 1) { + LPCWSTR arguments = (LPCWSTR)copy_data_structure->lpData; + Slic3r::GUI::wxGetApp().other_instance_message_handler()->handle_message(boost::nowide::narrow(arguments)); + } + return true; + }); +} +#endif // WIN32 + +static void generic_exception_handle() +{ + // Note: Some wxWidgets APIs use wxLogError() to report errors, eg. wxImage + // - see https://docs.wxwidgets.org/3.1/classwx_image.html#aa249e657259fe6518d68a5208b9043d0 + // + // wxLogError typically goes around exception handling and display an error dialog some time + // after an error is logged even if exception handling and OnExceptionInMainLoop() take place. + // This is why we use wxLogError() here as well instead of a custom dialog, because it accumulates + // errors if multiple have been collected and displays just one error message for all of them. + // Otherwise we would get multiple error messages for one missing png, for example. + // + // If a custom error message window (or some other solution) were to be used, it would be necessary + // to turn off wxLogError() usage in wx APIs, most notably in wxImage + // - see https://docs.wxwidgets.org/trunk/classwx_image.html#aa32e5d3507cc0f8c3330135bc0befc6a + + try { + throw; + } catch (const std::bad_alloc& ex) { + // bad_alloc in main thread is most likely fatal. Report immediately to the user (wxLogError would be delayed) + // and terminate the app so it is at least certain to happen now. + wxString errmsg = wxString::Format(_L("%s has encountered an error. It was likely caused by running out of memory. " + "If you are sure you have enough RAM on your system, this may also be a bug and we would " + "be glad if you reported it.\n\nThe application will now terminate."), SLIC3R_APP_NAME); + wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Fatal error"), wxOK | wxICON_ERROR); + BOOST_LOG_TRIVIAL(error) << boost::format("std::bad_alloc exception: %1%") % ex.what(); + std::terminate(); + } catch (const boost::io::bad_format_string& ex) { + wxString errmsg = _L("PrusaSlicer has encountered a localization error. " + "Please report to PrusaSlicer team, what language was active and in which scenario " + "this issue happened. Thank you.\n\nThe application will now terminate."); + wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Critical error"), wxOK | wxICON_ERROR); + BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); + std::terminate(); + throw; + } catch (const std::exception& ex) { + wxLogError(format_wxstr(_L("Internal error: %1%"), ex.what())); + BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); + throw; + } +} + +void GUI_App::post_init() +{ + assert(initialized()); + if (! this->initialized()) + throw Slic3r::RuntimeError("Calling post_init() while not yet initialized"); + + if (this->is_gcode_viewer()) { + if (! this->init_params->input_files.empty()) + this->plater()->load_gcode(wxString::FromUTF8(this->init_params->input_files[0].c_str())); + } + else if (this->init_params->start_downloader) { + start_download(this->init_params->download_url); + } else { + if (! this->init_params->preset_substitutions.empty()) + show_substitutions_info(this->init_params->preset_substitutions); + +#if 0 + // Load the cummulative config over the currently active profiles. + //FIXME if multiple configs are loaded, only the last one will have an effect. + // We need to decide what to do about loading of separate presets (just print preset, just filament preset etc). + // As of now only the full configs are supported here. + if (!m_print_config.empty()) + this->gui->mainframe->load_config(m_print_config); +#endif + if (! this->init_params->load_configs.empty()) + // Load the last config to give it a name at the UI. The name of the preset may be later + // changed by loading an AMF or 3MF. + //FIXME this is not strictly correct, as one may pass a print/filament/printer profile here instead of a full config. + this->mainframe->load_config_file(this->init_params->load_configs.back()); + // If loading a 3MF file, the config is loaded from the last one. + if (!this->init_params->input_files.empty()) { +#if 1 // #ysFIXME_delete_after_test_of + wxArrayString fns; + for (const std::string& name : this->init_params->input_files) + fns.Add(from_u8(name)); + if (plater()->load_files(fns) && this->init_params->input_files.size() == 1) { +#else + const std::vector res = this->plater()->load_files(this->init_params->input_files, true, true); + if (!res.empty() && this->init_params->input_files.size() == 1) { +#endif + // Update application titlebar when opening a project file + const std::string& filename = this->init_params->input_files.front(); + if (boost::algorithm::iends_with(filename, ".amf") || + boost::algorithm::iends_with(filename, ".amf.xml") || + boost::algorithm::iends_with(filename, ".3mf")) + this->plater()->set_project_filename(from_u8(filename)); + } + if (this->init_params->delete_after_load) { + for (const std::string& p : this->init_params->input_files) { + boost::system::error_code ec; + boost::filesystem::remove(boost::filesystem::path(p), ec); + if (ec) { + BOOST_LOG_TRIVIAL(error) << ec.message(); + } + } + } + } + if (! this->init_params->extra_config.empty()) + this->mainframe->load_config(this->init_params->extra_config); + } + + // show "Did you know" notification + if (app_config->get("show_hints") == "1" && ! is_gcode_viewer()) + plater_->get_notification_manager()->push_hint_notification(true); + + // The extra CallAfter() is needed because of Mac, where this is the only way + // to popup a modal dialog on start without screwing combo boxes. + // This is ugly but I honestly found no better way to do it. + // Neither wxShowEvent nor wxWindowCreateEvent work reliably. + if (this->preset_updater) { // G-Code Viewer does not initialize preset_updater. + if (! this->check_updates(false)) + // Configuration is not compatible and reconfigure was refused by the user. Application is closing. + return; + CallAfter([this] { + // preset_updater->sync downloads profile updates on background so it must begin after config wizard finished. + bool cw_showed = this->config_wizard_startup(); + this->preset_updater->sync(preset_bundle); + if (! cw_showed) { + // The CallAfter is needed as well, without it, GL extensions did not show. + // Also, we only want to show this when the wizard does not, so the new user + // sees something else than "we want something" on the first start. + show_send_system_info_dialog_if_needed(); + } + // app version check is asynchronous and triggers blocking dialog window, better call it last + this->app_version_check(false); + }); + } + + // Set PrusaSlicer version and save to PrusaSlicer.ini or PrusaSlicerGcodeViewer.ini. + app_config->set("version", SLIC3R_VERSION); + app_config->save(); + +#ifdef _WIN32 + // Sets window property to mainframe so other instances can indentify it. + OtherInstanceMessageHandler::init_windows_properties(mainframe, m_instance_hash_int); +#endif //WIN32 +} + +IMPLEMENT_APP(GUI_App) + +GUI_App::GUI_App(EAppMode mode) + : wxApp() + , m_app_mode(mode) + , m_em_unit(10) + , m_imgui(new ImGuiWrapper()) + , m_removable_drive_manager(std::make_unique()) + , m_other_instance_message_handler(std::make_unique()) + , m_downloader(std::make_unique()) +{ + //app config initializes early becasuse it is used in instance checking in PrusaSlicer.cpp + this->init_app_config(); + // init app downloader after path to datadir is set + m_app_updater = std::make_unique(); +} + +GUI_App::~GUI_App() +{ + if (app_config != nullptr) + delete app_config; + + if (preset_bundle != nullptr) + delete preset_bundle; + + if (preset_updater != nullptr) + delete preset_updater; +} + +// If formatted for github, plaintext with OpenGL extensions enclosed into
. +// Otherwise HTML formatted for the system info dialog. +std::string GUI_App::get_gl_info(bool for_github) +{ + return OpenGLManager::get_gl_info().to_string(for_github); +} + +wxGLContext* GUI_App::init_glcontext(wxGLCanvas& canvas) +{ +#if ENABLE_GL_CORE_PROFILE +#if ENABLE_OPENGL_DEBUG_OPTION + return m_opengl_mgr.init_glcontext(canvas, init_params != nullptr ? init_params->opengl_version : std::make_pair(0, 0), + init_params != nullptr ? init_params->opengl_debug : false); +#else + return m_opengl_mgr.init_glcontext(canvas, init_params != nullptr ? init_params->opengl_version : std::make_pair(0, 0)); +#endif // ENABLE_OPENGL_DEBUG_OPTION +#else + return m_opengl_mgr.init_glcontext(canvas); +#endif // ENABLE_GL_CORE_PROFILE +} + +bool GUI_App::init_opengl() +{ +#ifdef __linux__ + bool status = m_opengl_mgr.init_gl(); + m_opengl_initialized = true; + return status; +#else + return m_opengl_mgr.init_gl(); +#endif +} + +// gets path to PrusaSlicer.ini, returns semver from first line comment +static boost::optional parse_semver_from_ini(std::string path) +{ + std::ifstream stream(path); + std::stringstream buffer; + buffer << stream.rdbuf(); + std::string body = buffer.str(); + size_t start = body.find("PrusaSlicer "); + if (start == std::string::npos) + return boost::none; + body = body.substr(start + 12); + size_t end = body.find_first_of(" \n"); + if (end < body.size()) + body.resize(end); + return Semver::parse(body); +} + +void GUI_App::init_app_config() +{ + // Profiles for the alpha are stored into the PrusaSlicer-alpha directory to not mix with the current release. + +// SetAppName(SLIC3R_APP_KEY); + SetAppName(SLIC3R_APP_KEY "-alpha"); +// SetAppName(SLIC3R_APP_KEY "-beta"); + + +// SetAppDisplayName(SLIC3R_APP_NAME); + + // Set the Slic3r data directory at the Slic3r XS module. + // Unix: ~/ .Slic3r + // Windows : "C:\Users\username\AppData\Roaming\Slic3r" or "C:\Documents and Settings\username\Application Data\Slic3r" + // Mac : "~/Library/Application Support/Slic3r" + + if (data_dir().empty()) { + #ifndef __linux__ + set_data_dir(wxStandardPaths::Get().GetUserDataDir().ToUTF8().data()); + #else + // Since version 2.3, config dir on Linux is in ${XDG_CONFIG_HOME}. + // https://github.com/prusa3d/PrusaSlicer/issues/2911 + wxString dir; + if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) + dir = wxFileName::GetHomeDir() + wxS("/.config"); + set_data_dir((dir + "/" + GetAppName()).ToUTF8().data()); + #endif + } else { + m_datadir_redefined = true; + } + + if (!app_config) + app_config = new AppConfig(is_editor() ? AppConfig::EAppMode::Editor : AppConfig::EAppMode::GCodeViewer); + + // load settings + m_app_conf_exists = app_config->exists(); + if (m_app_conf_exists) { + std::string error = app_config->load(); + if (!error.empty()) { + // Error while parsing config file. We'll customize the error message and rethrow to be displayed. + if (is_editor()) { + throw Slic3r::RuntimeError( + _u8L("Error parsing PrusaSlicer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error. Your user profiles will not be affected.") + + "\n\n" + app_config->config_path() + "\n\n" + error); + } + else { + throw Slic3r::RuntimeError( + _u8L("Error parsing PrusaGCodeViewer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error.") + + "\n\n" + app_config->config_path() + "\n\n" + error); + } + } + } +} + +// returns old config path to copy from if such exists, +// returns an empty string if such config path does not exists or if it cannot be loaded. +std::string GUI_App::check_older_app_config(Semver current_version, bool backup) +{ + std::string older_data_dir_path; + + // If the config folder is redefined - do not check + if (m_datadir_redefined) + return {}; + + // find other version app config (alpha / beta / release) + std::string config_path = app_config->config_path(); + boost::filesystem::path parent_file_path(config_path); + std::string filename = parent_file_path.filename().string(); + parent_file_path.remove_filename().remove_filename(); + + std::vector candidates; + + if (SLIC3R_APP_KEY "-alpha" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-alpha" / filename); + if (SLIC3R_APP_KEY "-beta" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-beta" / filename); + if (SLIC3R_APP_KEY != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY / filename); + + Semver last_semver = current_version; + for (const auto& candidate : candidates) { + if (boost::filesystem::exists(candidate)) { + // parse + boost::optionalother_semver = parse_semver_from_ini(candidate.string()); + if (other_semver && *other_semver > last_semver) { + last_semver = *other_semver; + older_data_dir_path = candidate.parent_path().string(); + } + } + } + if (older_data_dir_path.empty()) + return {}; + BOOST_LOG_TRIVIAL(info) << "last app config file used: " << older_data_dir_path; + // ask about using older data folder + + InfoDialog msg(nullptr + , format_wxstr(_L("You are opening %1% version %2%."), SLIC3R_APP_NAME, SLIC3R_VERSION) + , backup ? + format_wxstr(_L( + "The active configuration was created by %1% %2%," + "\nwhile a newer configuration was found in %3%" + "\ncreated by %1% %4%." + "\n\nShall the newer configuration be imported?" + "\nIf so, your active configuration will be backed up before importing the new configuration." + ) + , SLIC3R_APP_NAME, current_version.to_string(), older_data_dir_path, last_semver.to_string()) + : format_wxstr(_L( + "An existing configuration was found in %3%" + "\ncreated by %1% %2%." + "\n\nShall this configuration be imported?" + ) + , SLIC3R_APP_NAME, last_semver.to_string(), older_data_dir_path) + , true, wxYES_NO); + + if (backup) { + msg.SetButtonLabel(wxID_YES, _L("Import")); + msg.SetButtonLabel(wxID_NO, _L("Don't import")); + } + + if (msg.ShowModal() == wxID_YES) { + std::string snapshot_id; + if (backup) { + const Config::Snapshot* snapshot{ nullptr }; + if (! GUI::Config::take_config_snapshot_cancel_on_error(*app_config, Config::Snapshot::SNAPSHOT_USER, "", + _u8L("Continue and import newer configuration?"), &snapshot)) + return {}; + if (snapshot) { + // Save snapshot ID before loading the alternate AppConfig, as loading the alternate AppConfig may fail. + snapshot_id = snapshot->id; + assert(! snapshot_id.empty()); + app_config->set("on_snapshot", snapshot_id); + } else + BOOST_LOG_TRIVIAL(error) << "Failed to take congiguration snapshot"; + } + + // load app config from older file + std::string error = app_config->load((boost::filesystem::path(older_data_dir_path) / filename).string()); + if (!error.empty()) { + // Error while parsing config file. We'll customize the error message and rethrow to be displayed. + if (is_editor()) { + throw Slic3r::RuntimeError( + _u8L("Error parsing PrusaSlicer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error. Your user profiles will not be affected.") + + "\n\n" + app_config->config_path() + "\n\n" + error); + } + else { + throw Slic3r::RuntimeError( + _u8L("Error parsing PrusaGCodeViewer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error.") + + "\n\n" + app_config->config_path() + "\n\n" + error); + } + } + if (!snapshot_id.empty()) + app_config->set("on_snapshot", snapshot_id); + m_app_conf_exists = true; + return older_data_dir_path; + } + return {}; +} + +void GUI_App::init_single_instance_checker(const std::string &name, const std::string &path) +{ + BOOST_LOG_TRIVIAL(debug) << "init wx instance checker " << name << " "<< path; + m_single_instance_checker = std::make_unique(boost::nowide::widen(name), boost::nowide::widen(path)); +} + +bool GUI_App::OnInit() +{ + try { + return on_init_inner(); + } catch (const std::exception&) { + generic_exception_handle(); + return false; + } +} + +bool GUI_App::on_init_inner() +{ + // Set initialization of image handlers before any UI actions - See GH issue #7469 + wxInitAllImageHandlers(); + +#if defined(_WIN32) && ! defined(_WIN64) + // Win32 32bit build. + if (wxPlatformInfo::Get().GetArchName().substr(0, 2) == "64") { + RichMessageDialog dlg(nullptr, + _L("You are running a 32 bit build of PrusaSlicer on 64-bit Windows." + "\n32 bit build of PrusaSlicer will likely not be able to utilize all the RAM available in the system." + "\nPlease download and install a 64 bit build of PrusaSlicer from https://www.prusa3d.cz/prusaslicer/." + "\nDo you wish to continue?"), + "PrusaSlicer", wxICON_QUESTION | wxYES_NO); + if (dlg.ShowModal() != wxID_YES) + return false; + } +#endif // _WIN64 + + // Forcing back menu icons under gtk2 and gtk3. Solution is based on: + // https://docs.gtk.org/gtk3/class.Settings.html + // see also https://docs.wxwidgets.org/3.0/classwx_menu_item.html#a2b5d6bcb820b992b1e4709facbf6d4fb + // TODO: Find workaround for GTK4 +#if defined(__WXGTK20__) || defined(__WXGTK3__) + g_object_set (gtk_settings_get_default (), "gtk-menu-images", TRUE, NULL); +#endif + + // Verify resources path + const wxString resources_dir = from_u8(Slic3r::resources_dir()); + wxCHECK_MSG(wxDirExists(resources_dir), false, + wxString::Format("Resources path does not exist or is not a directory: %s", resources_dir)); + +#ifdef __linux__ + if (! check_old_linux_datadir(GetAppName())) { + std::cerr << "Quitting, user chose to move their data to new location." << std::endl; + return false; + } +#endif + + // Enable this to get the default Win32 COMCTRL32 behavior of static boxes. +// wxSystemOptions::SetOption("msw.staticbox.optimized-paint", 0); + // Enable this to disable Windows Vista themes for all wxNotebooks. The themes seem to lead to terrible + // performance when working on high resolution multi-display setups. +// wxSystemOptions::SetOption("msw.notebook.themed-background", 0); + +// Slic3r::debugf "wxWidgets version %s, Wx version %s\n", wxVERSION_STRING, wxVERSION; + + // !!! Initialization of UI settings as a language, application color mode, fonts... have to be done before first UI action. + // Like here, before the show InfoDialog in check_older_app_config() + + // If load_language() fails, the application closes. + load_language(wxString(), true); +#ifdef _MSW_DARK_MODE + bool init_dark_color_mode = app_config->get("dark_color_mode") == "1"; + bool init_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; + NppDarkMode::InitDarkMode(init_dark_color_mode, init_sys_menu_enabled); +#endif + // initialize label colors and fonts + init_ui_colours(); + init_fonts(); + + std::string older_data_dir_path; + if (m_app_conf_exists) { + if (app_config->orig_version().valid() && app_config->orig_version() < *Semver::parse(SLIC3R_VERSION)) + // Only copying configuration if it was saved with a newer slicer than the one currently running. + older_data_dir_path = check_older_app_config(app_config->orig_version(), true); + } else { + // No AppConfig exists, fresh install. Always try to copy from an alternate location, don't make backup of the current configuration. + older_data_dir_path = check_older_app_config(Semver(), false); + } + +#ifdef _MSW_DARK_MODE + // app_config can be updated in check_older_app_config(), so check if dark_color_mode and sys_menu_enabled was changed + if (bool new_dark_color_mode = app_config->get("dark_color_mode") == "1"; + init_dark_color_mode != new_dark_color_mode) { + NppDarkMode::SetDarkMode(new_dark_color_mode); + init_ui_colours(); + update_ui_colours_from_appconfig(); + } + if (bool new_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; + init_sys_menu_enabled != new_sys_menu_enabled) + NppDarkMode::SetSystemMenuForApp(new_sys_menu_enabled); +#endif + + if (is_editor()) { + std::string msg = Http::tls_global_init(); + std::string ssl_cert_store = app_config->get("tls_accepted_cert_store_location"); + bool ssl_accept = app_config->get("tls_cert_store_accepted") == "yes" && ssl_cert_store == Http::tls_system_cert_store(); + + if (!msg.empty() && !ssl_accept) { + RichMessageDialog + dlg(nullptr, + wxString::Format(_L("%s\nDo you want to continue?"), msg), + "PrusaSlicer", wxICON_QUESTION | wxYES_NO); + dlg.ShowCheckBox(_L("Remember my choice")); + if (dlg.ShowModal() != wxID_YES) return false; + + app_config->set("tls_cert_store_accepted", + dlg.IsCheckBoxChecked() ? "yes" : "no"); + app_config->set("tls_accepted_cert_store_location", + dlg.IsCheckBoxChecked() ? Http::tls_system_cert_store() : ""); + } + } + + SplashScreen* scrn = nullptr; + if (app_config->get("show_splash_screen") == "1") { + // make a bitmap with dark grey banner on the left side + wxBitmap bmp = SplashScreen::MakeBitmap(wxBitmap(from_u8(var(is_editor() ? "splashscreen.jpg" : "splashscreen-gcodepreview.jpg")), wxBITMAP_TYPE_JPEG)); + + // Detect position (display) to show the splash screen + // Now this position is equal to the mainframe position + wxPoint splashscreen_pos = wxDefaultPosition; + bool default_splashscreen_pos = true; + if (app_config->has("window_mainframe") && app_config->get("restore_win_position") == "1") { + auto metrics = WindowMetrics::deserialize(app_config->get("window_mainframe")); + default_splashscreen_pos = metrics == boost::none; + if (!default_splashscreen_pos) + splashscreen_pos = metrics->get_rect().GetPosition(); + } + + if (!default_splashscreen_pos) { + // workaround for crash related to the positioning of the window on secondary monitor + get_app_config()->set("restore_win_position", "crashed_at_splashscreen_pos"); + get_app_config()->save(); + } + + // create splash screen with updated bmp + scrn = new SplashScreen(bmp.IsOk() ? bmp : get_bmp_bundle("PrusaSlicer", 400)->GetPreferredBitmapSizeAtScale(1.0), + wxSPLASH_CENTRE_ON_SCREEN | wxSPLASH_TIMEOUT, 4000, splashscreen_pos); + + if (!default_splashscreen_pos) + // revert "restore_win_position" value if application wasn't crashed + get_app_config()->set("restore_win_position", "1"); +#ifndef __linux__ + wxYield(); +#endif + scrn->SetText(_L("Loading configuration")+ dots); + } + + preset_bundle = new PresetBundle(); + + // just checking for existence of Slic3r::data_dir is not enough : it may be an empty directory + // supplied as argument to --datadir; in that case we should still run the wizard + preset_bundle->setup_directories(); + + if (! older_data_dir_path.empty()) { + preset_bundle->import_newer_configs(older_data_dir_path); + app_config->save(); + } + + if (is_editor()) { +#ifdef __WXMSW__ + if (app_config->get("associate_3mf") == "1") + associate_3mf_files(); + if (app_config->get("associate_stl") == "1") + associate_stl_files(); +#endif // __WXMSW__ + + preset_updater = new PresetUpdater(); + Bind(EVT_SLIC3R_VERSION_ONLINE, &GUI_App::on_version_read, this); + Bind(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, [this](const wxCommandEvent& evt) { + if (this->plater_ != nullptr && (m_app_updater->get_triggered_by_user() || app_config->get("notify_release") == "all")) { + std::string evt_string = into_u8(evt.GetString()); + if (*Semver::parse(SLIC3R_VERSION) < *Semver::parse(evt_string)) { + auto notif_type = (evt_string.find("beta") != std::string::npos ? NotificationType::NewBetaAvailable : NotificationType::NewAlphaAvailable); + this->plater_->get_notification_manager()->push_version_notification( notif_type + , NotificationManager::NotificationLevel::ImportantNotificationLevel + , Slic3r::format(_u8L("New prerelease version %1% is available."), evt_string) + , _u8L("See Releases page.") + , [](wxEvtHandler* evnthndlr) {wxGetApp().open_browser_with_warning_dialog("https://github.com/prusa3d/PrusaSlicer/releases"); return true; } + ); + } + } + }); + Bind(EVT_SLIC3R_APP_DOWNLOAD_PROGRESS, [this](const wxCommandEvent& evt) { + //lm:This does not force a render. The progress bar only updateswhen the mouse is moved. + if (this->plater_ != nullptr) + this->plater_->get_notification_manager()->set_download_progress_percentage((float)std::stoi(into_u8(evt.GetString())) / 100.f ); + }); + + Bind(EVT_SLIC3R_APP_DOWNLOAD_FAILED, [this](const wxCommandEvent& evt) { + if (this->plater_ != nullptr) + this->plater_->get_notification_manager()->close_notification_of_type(NotificationType::AppDownload); + if(!evt.GetString().IsEmpty()) + show_error(nullptr, evt.GetString()); + }); + + Bind(EVT_SLIC3R_APP_OPEN_FAILED, [](const wxCommandEvent& evt) { + show_error(nullptr, evt.GetString()); + }); + + } + else { +#ifdef __WXMSW__ + if (app_config->get("associate_gcode") == "1") + associate_gcode_files(); +#endif // __WXMSW__ + } + + std::string delayed_error_load_presets; + // Suppress the '- default -' presets. + preset_bundle->set_default_suppressed(app_config->get("no_defaults") == "1"); + try { + // Enable all substitutions (in both user and system profiles), but log the substitutions in user profiles only. + // If there are substitutions in system profiles, then a "reconfigure" event shall be triggered, which will force + // installation of a compatible system preset, thus nullifying the system preset substitutions. + init_params->preset_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSystemSilent); + } catch (const std::exception &ex) { + delayed_error_load_presets = ex.what(); + } + +#ifdef WIN32 +#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) + register_win32_dpi_event(); +#endif // !wxVERSION_EQUAL_OR_GREATER_THAN + register_win32_device_notification_event(); +#endif // WIN32 + + // Let the libslic3r know the callback, which will translate messages on demand. + Slic3r::I18N::set_translate_callback(libslic3r_translate_callback); + + // application frame + if (scrn && is_editor()) + scrn->SetText(_L("Preparing settings tabs") + dots); + + if (!delayed_error_load_presets.empty()) + show_error(nullptr, delayed_error_load_presets); + + mainframe = new MainFrame(); + // hide settings tabs after first Layout + if (is_editor()) + mainframe->select_tab(size_t(0)); + + sidebar().obj_list()->init_objects(); // propagate model objects to object list +// update_mode(); // !!! do that later + SetTopWindow(mainframe); + + plater_->init_notification_manager(); + + m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); + + if (is_gcode_viewer()) { + mainframe->update_layout(); + if (plater_ != nullptr) + // ensure the selected technology is ptFFF + plater_->set_printer_technology(ptFFF); + } + else + load_current_presets(); + + // Save the active profiles as a "saved into project". + update_saved_preset_from_current_preset(); + + if (plater_ != nullptr) { + // Save the names of active presets and project specific config into ProjectDirtyStateManager. + plater_->reset_project_dirty_initial_presets(); + // Update Project dirty state, update application title bar. + plater_->update_project_dirty_from_presets(); + } + + mainframe->Show(true); + + obj_list()->set_min_height(); + + update_mode(); // update view mode after fix of the object_list size + +#ifdef __APPLE__ + other_instance_message_handler()->bring_instance_forward(); +#endif //__APPLE__ + + Bind(wxEVT_IDLE, [this](wxIdleEvent& event) + { + if (! plater_) + return; + + this->obj_manipul()->update_if_dirty(); + + // An ugly solution to GH #5537 in which GUI_App::init_opengl (normally called from events wxEVT_PAINT + // and wxEVT_SET_FOCUS before GUI_App::post_init is called) wasn't called before GUI_App::post_init and OpenGL wasn't initialized. +#ifdef __linux__ + if (! m_post_initialized && m_opengl_initialized) { +#else + if (! m_post_initialized) { +#endif + m_post_initialized = true; +#ifdef WIN32 + this->mainframe->register_win32_callbacks(); +#endif + this->post_init(); + } + + if (m_post_initialized && app_config->dirty() && app_config->get("autosave") == "1") + app_config->save(); + }); + + m_initialized = true; + + if (const std::string& crash_reason = app_config->get("restore_win_position"); + boost::starts_with(crash_reason,"crashed")) + { + wxString preferences_item = _L("Restore window position on start"); + InfoDialog dialog(nullptr, + _L("PrusaSlicer started after a crash"), + format_wxstr(_L("PrusaSlicer crashed last time when attempting to set window position.\n" + "We are sorry for the inconvenience, it unfortunately happens with certain multiple-monitor setups.\n" + "More precise reason for the crash: \"%1%\".\n" + "For more information see our GitHub issue tracker: \"%2%\" and \"%3%\"\n\n" + "To avoid this problem, consider disabling \"%4%\" in \"Preferences\". " + "Otherwise, the application will most likely crash again next time."), + "" + from_u8(crash_reason) + "", + "#2939", + "#5573", + "" + preferences_item + ""), + true, wxYES_NO); + + dialog.SetButtonLabel(wxID_YES, format_wxstr(_L("Disable \"%1%\""), preferences_item)); + dialog.SetButtonLabel(wxID_NO, format_wxstr(_L("Leave \"%1%\" enabled") , preferences_item)); + + auto answer = dialog.ShowModal(); + if (answer == wxID_YES) + app_config->set("restore_win_position", "0"); + else if (answer == wxID_NO) + app_config->set("restore_win_position", "1"); + app_config->save(); + } + + return true; +} + +unsigned GUI_App::get_colour_approx_luma(const wxColour &colour) +{ + double r = colour.Red(); + double g = colour.Green(); + double b = colour.Blue(); + + return std::round(std::sqrt( + r * r * .241 + + g * g * .691 + + b * b * .068 + )); +} + +bool GUI_App::dark_mode() +{ +#if __APPLE__ + // The check for dark mode returns false positive on 10.12 and 10.13, + // which allowed setting dark menu bar and dock area, which is + // is detected as dark mode. We must run on at least 10.14 where the + // proper dark mode was first introduced. + return wxPlatformInfo::Get().CheckOSVersion(10, 14) && mac_dark_mode(); +#else + if (wxGetApp().app_config->has("dark_color_mode")) + return wxGetApp().app_config->get("dark_color_mode") == "1"; + return check_dark_mode(); +#endif +} + +const wxColour GUI_App::get_label_default_clr_system() +{ + return dark_mode() ? wxColour(115, 220, 103) : wxColour(26, 132, 57); +} + +const wxColour GUI_App::get_label_default_clr_modified() +{ + return dark_mode() ? wxColour(253, 111, 40) : wxColour(252, 77, 1); +} + +const std::vector GUI_App::get_mode_default_palette() +{ + return { "#7DF028", "#FFDC00", "#E70000" }; +} + +void GUI_App::init_ui_colours() +{ + m_color_label_modified = get_label_default_clr_modified(); + m_color_label_sys = get_label_default_clr_system(); + m_mode_palette = get_mode_default_palette(); + + bool is_dark_mode = dark_mode(); +#ifdef _WIN32 + m_color_label_default = is_dark_mode ? wxColour(250, 250, 250): wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); + m_color_highlight_label_default = is_dark_mode ? wxColour(230, 230, 230): wxSystemSettings::GetColour(/*wxSYS_COLOUR_HIGHLIGHTTEXT*/wxSYS_COLOUR_WINDOWTEXT); + m_color_highlight_default = is_dark_mode ? wxColour(78, 78, 78) : wxSystemSettings::GetColour(wxSYS_COLOUR_3DLIGHT); + m_color_hovered_btn_label = is_dark_mode ? wxColour(253, 111, 40) : wxColour(252, 77, 1); + m_color_default_btn_label = is_dark_mode ? wxColour(255, 181, 100): wxColour(203, 61, 0); + m_color_selected_btn_bg = is_dark_mode ? wxColour(95, 73, 62) : wxColour(228, 220, 216); +#else + m_color_label_default = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); +#endif + m_color_window_default = is_dark_mode ? wxColour(43, 43, 43) : wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); +} + +void GUI_App::update_ui_colours_from_appconfig() +{ + // load label colors + if (app_config->has("label_clr_sys")) { + auto str = app_config->get("label_clr_sys"); + if (!str.empty()) + m_color_label_sys = wxColour(str); + } + + if (app_config->has("label_clr_modified")) { + auto str = app_config->get("label_clr_modified"); + if (!str.empty()) + m_color_label_modified = wxColour(str); + } + + // load mode markers colors + if (app_config->has("mode_palette")) { + const auto colors = app_config->get("mode_palette"); + if (!colors.empty()) { + m_mode_palette.clear(); + if (!unescape_strings_cstyle(colors, m_mode_palette)) + m_mode_palette = get_mode_default_palette(); + } + } +} + +void GUI_App::update_label_colours() +{ + for (Tab* tab : tabs_list) + tab->update_label_colours(); +} + +#ifdef _WIN32 +static bool is_focused(HWND hWnd) +{ + HWND hFocusedWnd = ::GetFocus(); + return hFocusedWnd && hWnd == hFocusedWnd; +} + +static bool is_default(wxWindow* win) +{ + wxTopLevelWindow* tlw = find_toplevel_parent(win); + if (!tlw) + return false; + + return win == tlw->GetDefaultItem(); +} +#endif + +void GUI_App::UpdateDarkUI(wxWindow* window, bool highlited/* = false*/, bool just_font/* = false*/) +{ +#ifdef _WIN32 + bool is_focused_button = false; + bool is_default_button = false; + if (wxButton* btn = dynamic_cast(window)) { + if (!(btn->GetWindowStyle() & wxNO_BORDER)) { + btn->SetWindowStyle(btn->GetWindowStyle() | wxNO_BORDER); + highlited = true; + } + // button marking + { + auto mark_button = [this, btn, highlited](const bool mark) { + if (btn->GetLabel().IsEmpty()) + btn->SetBackgroundColour(mark ? m_color_selected_btn_bg : highlited ? m_color_highlight_default : m_color_window_default); + else + btn->SetForegroundColour(mark ? m_color_hovered_btn_label : (is_default(btn) ? m_color_default_btn_label : m_color_label_default)); + btn->Refresh(); + btn->Update(); + }; + + // hovering + btn->Bind(wxEVT_ENTER_WINDOW, [mark_button](wxMouseEvent& event) { mark_button(true); event.Skip(); }); + btn->Bind(wxEVT_LEAVE_WINDOW, [mark_button, btn](wxMouseEvent& event) { mark_button(is_focused(btn->GetHWND())); event.Skip(); }); + // focusing + btn->Bind(wxEVT_SET_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(true); event.Skip(); }); + btn->Bind(wxEVT_KILL_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(false); event.Skip(); }); + + is_focused_button = is_focused(btn->GetHWND()); + is_default_button = is_default(btn); + if (is_focused_button || is_default_button) + mark_button(is_focused_button); + } + } + else if (wxTextCtrl* text = dynamic_cast(window)) { + if (text->GetBorder() != wxBORDER_SIMPLE) + text->SetWindowStyle(text->GetWindowStyle() | wxBORDER_SIMPLE); + } + else if (wxCheckListBox* list = dynamic_cast(window)) { + list->SetWindowStyle(list->GetWindowStyle() | wxBORDER_SIMPLE); + list->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); + for (size_t i = 0; i < list->GetCount(); i++) + if (wxOwnerDrawn* item = list->GetItem(i)) { + item->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); + item->SetTextColour(m_color_label_default); + } + return; + } + else if (dynamic_cast(window)) + window->SetWindowStyle(window->GetWindowStyle() | wxBORDER_SIMPLE); + + if (!just_font) + window->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); + if (!is_focused_button && !is_default_button) + window->SetForegroundColour(m_color_label_default); +#endif +} + +// recursive function for scaling fonts for all controls in Window +#ifdef _WIN32 +static void update_dark_children_ui(wxWindow* window, bool just_buttons_update = false) +{ + bool is_btn = dynamic_cast(window) != nullptr; + if (!(just_buttons_update && !is_btn)) + wxGetApp().UpdateDarkUI(window, is_btn); + + auto children = window->GetChildren(); + for (auto child : children) { + update_dark_children_ui(child); + } +} +#endif + +// Note: Don't use this function for Dialog contains ScalableButtons +void GUI_App::UpdateDlgDarkUI(wxDialog* dlg, bool just_buttons_update/* = false*/) +{ +#ifdef _WIN32 + update_dark_children_ui(dlg, just_buttons_update); +#endif +} +void GUI_App::UpdateDVCDarkUI(wxDataViewCtrl* dvc, bool highlited/* = false*/) +{ +#ifdef _WIN32 + UpdateDarkUI(dvc, highlited ? dark_mode() : false); +#ifdef _MSW_DARK_MODE + dvc->RefreshHeaderDarkMode(&m_normal_font); +#endif //_MSW_DARK_MODE + if (dvc->HasFlag(wxDV_ROW_LINES)) + dvc->SetAlternateRowColour(m_color_highlight_default); + if (dvc->GetBorder() != wxBORDER_SIMPLE) + dvc->SetWindowStyle(dvc->GetWindowStyle() | wxBORDER_SIMPLE); +#endif +} + +void GUI_App::UpdateAllStaticTextDarkUI(wxWindow* parent) +{ +#ifdef _WIN32 + wxGetApp().UpdateDarkUI(parent); + + auto children = parent->GetChildren(); + for (auto child : children) { + if (dynamic_cast(child)) + child->SetForegroundColour(m_color_label_default); + } +#endif +} + +void GUI_App::init_fonts() +{ + m_small_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + m_bold_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold(); + m_normal_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + +#ifdef __WXMAC__ + m_small_font.SetPointSize(11); + m_bold_font.SetPointSize(13); +#endif /*__WXMAC__*/ + + // wxSYS_OEM_FIXED_FONT and wxSYS_ANSI_FIXED_FONT use the same as + // DEFAULT in wxGtk. Use the TELETYPE family as a work-around + m_code_font = wxFont(wxFontInfo().Family(wxFONTFAMILY_TELETYPE)); + m_code_font.SetPointSize(m_normal_font.GetPointSize()); +} + +void GUI_App::update_fonts(const MainFrame *main_frame) +{ + /* Only normal and bold fonts are used for an application rescale, + * because of under MSW small and normal fonts are the same. + * To avoid same rescaling twice, just fill this values + * from rescaled MainFrame + */ + if (main_frame == nullptr) + main_frame = this->mainframe; + m_normal_font = main_frame->normal_font(); + m_small_font = m_normal_font; + m_bold_font = main_frame->normal_font().Bold(); + m_link_font = m_bold_font.Underlined(); + m_em_unit = main_frame->em_unit(); + m_code_font.SetPointSize(m_normal_font.GetPointSize()); +} + +void GUI_App::set_label_clr_modified(const wxColour& clr) +{ + if (m_color_label_modified == clr) + return; + m_color_label_modified = clr; + const std::string str = encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); + app_config->set("label_clr_modified", str); + app_config->save(); +} + +void GUI_App::set_label_clr_sys(const wxColour& clr) +{ + if (m_color_label_sys == clr) + return; + m_color_label_sys = clr; + const std::string str = encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); + app_config->set("label_clr_sys", str); + app_config->save(); +} + +const std::string& GUI_App::get_mode_btn_color(int mode_id) +{ + assert(0 <= mode_id && size_t(mode_id) < m_mode_palette.size()); + return m_mode_palette[mode_id]; +} + +std::vector GUI_App::get_mode_palette() +{ + return { wxColor(m_mode_palette[0]), + wxColor(m_mode_palette[1]), + wxColor(m_mode_palette[2]) }; +} + +void GUI_App::set_mode_palette(const std::vector& palette) +{ + bool save = false; + + for (size_t mode = 0; mode < palette.size(); ++mode) { + const wxColour& clr = palette[mode]; + std::string color_str = clr == wxTransparentColour ? std::string("") : encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); + if (m_mode_palette[mode] != color_str) { + m_mode_palette[mode] = color_str; + save = true; + } + } + + if (save) { + mainframe->update_mode_markers(); + app_config->set("mode_palette", escape_strings_cstyle(m_mode_palette)); + app_config->save(); + } +} + +bool GUI_App::tabs_as_menu() const +{ + return app_config->get("tabs_as_menu") == "1"; // || dark_mode(); +} + +wxSize GUI_App::get_min_size() const +{ + return wxSize(76*m_em_unit, 49 * m_em_unit); +} + +float GUI_App::toolbar_icon_scale(const bool is_limited/* = false*/) const +{ +#ifdef __APPLE__ + const float icon_sc = 1.0f; // for Retina display will be used its own scale +#else + const float icon_sc = m_em_unit*0.1f; +#endif // __APPLE__ + + const std::string& use_val = app_config->get("use_custom_toolbar_size"); + const std::string& val = app_config->get("custom_toolbar_size"); + const std::string& auto_val = app_config->get("auto_toolbar_size"); + + if (val.empty() || auto_val.empty() || use_val.empty()) + return icon_sc; + + int int_val = use_val == "0" ? 100 : atoi(val.c_str()); + // correct value in respect to auto_toolbar_size + int_val = std::min(atoi(auto_val.c_str()), int_val); + + if (is_limited && int_val < 50) + int_val = 50; + + return 0.01f * int_val * icon_sc; +} + +void GUI_App::set_auto_toolbar_icon_scale(float scale) const +{ +#ifdef __APPLE__ + const float icon_sc = 1.0f; // for Retina display will be used its own scale +#else + const float icon_sc = m_em_unit * 0.1f; +#endif // __APPLE__ + + long int_val = std::min(int(std::lround(scale / icon_sc * 100)), 100); + std::string val = std::to_string(int_val); + + app_config->set("auto_toolbar_size", val); +} + +// check user printer_presets for the containing information about "Print Host upload" +void GUI_App::check_printer_presets() +{ + std::vector preset_names = PhysicalPrinter::presets_with_print_host_information(preset_bundle->printers); + if (preset_names.empty()) + return; + + wxString msg_text = _L("You have the following presets with saved options for \"Print Host upload\"") + ":"; + for (const std::string& preset_name : preset_names) + msg_text += "\n \"" + from_u8(preset_name) + "\","; + msg_text.RemoveLast(); + msg_text += "\n\n" + _L("But since this version of PrusaSlicer we don't show this information in Printer Settings anymore.\n" + "Settings will be available in physical printers settings.") + "\n\n" + + _L("By default new Printer devices will be named as \"Printer N\" during its creation.\n" + "Note: This name can be changed later from the physical printers settings"); + + //wxMessageDialog(nullptr, msg_text, _L("Information"), wxOK | wxICON_INFORMATION).ShowModal(); + MessageDialog(nullptr, msg_text, _L("Information"), wxOK | wxICON_INFORMATION).ShowModal(); + + preset_bundle->physical_printers.load_printers_from_presets(preset_bundle->printers); +} + +void GUI_App::recreate_GUI(const wxString& msg_name) +{ + m_is_recreating_gui = true; + + mainframe->shutdown(); + + wxProgressDialog dlg(msg_name, msg_name, 100, nullptr, wxPD_AUTO_HIDE); + dlg.Pulse(); + dlg.Update(10, _L("Recreating") + dots); + + MainFrame *old_main_frame = mainframe; + mainframe = new MainFrame(); + if (is_editor()) + // hide settings tabs after first Layout + mainframe->select_tab(size_t(0)); + // Propagate model objects to object list. + sidebar().obj_list()->init_objects(); + SetTopWindow(mainframe); + + dlg.Update(30, _L("Recreating") + dots); + old_main_frame->Destroy(); + + dlg.Update(80, _L("Loading of current presets") + dots); + m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); + load_current_presets(); + mainframe->Show(true); + + dlg.Update(90, _L("Loading of a mode view") + dots); + + obj_list()->set_min_height(); + update_mode(); + + // #ys_FIXME_delete_after_testing Do we still need this ? +// CallAfter([]() { +// // Run the config wizard, don't offer the "reset user profile" checkbox. +// config_wizard_startup(true); +// }); + + m_is_recreating_gui = false; +} + +void GUI_App::system_info() +{ + SysInfoDialog dlg; + dlg.ShowModal(); +} + +void GUI_App::keyboard_shortcuts() +{ + KBShortcutsDialog dlg; + dlg.ShowModal(); +} + +// static method accepting a wxWindow object as first parameter +bool GUI_App::catch_error(std::function cb, + // wxMessageDialog* message_dialog, + const std::string& err /*= ""*/) +{ + if (!err.empty()) { + if (cb) + cb(); + // if (message_dialog) + // message_dialog->(err, "Error", wxOK | wxICON_ERROR); + show_error(/*this*/nullptr, err); + return true; + } + return false; +} + +// static method accepting a wxWindow object as first parameter +void fatal_error(wxWindow* parent) +{ + show_error(parent, ""); + // exit 1; // #ys_FIXME +} + +#ifdef _WIN32 + +#ifdef _MSW_DARK_MODE +static void update_scrolls(wxWindow* window) +{ + wxWindowList::compatibility_iterator node = window->GetChildren().GetFirst(); + while (node) + { + wxWindow* win = node->GetData(); + if (dynamic_cast(win) || + dynamic_cast(win) || + dynamic_cast(win)) + NppDarkMode::SetDarkExplorerTheme(win->GetHWND()); + + update_scrolls(win); + node = node->GetNext(); + } +} +#endif //_MSW_DARK_MODE + + +#ifdef _MSW_DARK_MODE +void GUI_App::force_menu_update() +{ + NppDarkMode::SetSystemMenuForApp(app_config->get("sys_menu_enabled") == "1"); +} +#endif //_MSW_DARK_MODE + +void GUI_App::force_colors_update() +{ +#ifdef _MSW_DARK_MODE + NppDarkMode::SetDarkMode(app_config->get("dark_color_mode") == "1"); + if (WXHWND wxHWND = wxToolTip::GetToolTipCtrl()) + NppDarkMode::SetDarkExplorerTheme((HWND)wxHWND); + NppDarkMode::SetDarkTitleBar(mainframe->GetHWND()); + NppDarkMode::SetDarkTitleBar(mainframe->m_settings_dialog.GetHWND()); +#endif //_MSW_DARK_MODE + m_force_colors_update = true; +} +#endif //_WIN32 + +// Called after the Preferences dialog is closed and the program settings are saved. +// Update the UI based on the current preferences. +void GUI_App::update_ui_from_settings() +{ + update_label_colours(); +#ifdef _WIN32 + // Upadte UI colors before Update UI from settings + if (m_force_colors_update) { + m_force_colors_update = false; + mainframe->force_color_changed(); + mainframe->diff_dialog.force_color_changed(); + mainframe->preferences_dialog->force_color_changed(); + mainframe->printhost_queue_dlg()->force_color_changed(); +#ifdef _MSW_DARK_MODE + update_scrolls(mainframe); + if (mainframe->is_dlg_layout()) { + // update for tabs bar + UpdateDarkUI(&mainframe->m_settings_dialog); + mainframe->m_settings_dialog.Fit(); + mainframe->m_settings_dialog.Refresh(); + // update scrollbars + update_scrolls(&mainframe->m_settings_dialog); + } +#endif //_MSW_DARK_MODE + } +#endif + mainframe->update_ui_from_settings(); +} + +void GUI_App::persist_window_geometry(wxTopLevelWindow *window, bool default_maximized) +{ + const std::string name = into_u8(window->GetName()); + + window->Bind(wxEVT_CLOSE_WINDOW, [=](wxCloseEvent &event) { + window_pos_save(window, name); + event.Skip(); + }); + + window_pos_restore(window, name, default_maximized); + + on_window_geometry(window, [=]() { + window_pos_sanitize(window); + }); +} + +void GUI_App::load_project(wxWindow *parent, wxString& input_file) const +{ + input_file.Clear(); + wxFileDialog dialog(parent ? parent : GetTopWindow(), + _L("Choose one file (3MF/AMF):"), + app_config->get_last_dir(), "", + file_wildcards(FT_PROJECT), wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + if (dialog.ShowModal() == wxID_OK) + input_file = dialog.GetPath(); +} + +void GUI_App::import_model(wxWindow *parent, wxArrayString& input_files) const +{ + input_files.Clear(); + wxFileDialog dialog(parent ? parent : GetTopWindow(), + _L("Choose one or more files (STL/3MF/STEP/OBJ/AMF/PRUSA):"), + from_u8(app_config->get_last_dir()), "", + file_wildcards(FT_MODEL), wxFD_OPEN | wxFD_MULTIPLE | wxFD_FILE_MUST_EXIST); + + if (dialog.ShowModal() == wxID_OK) + dialog.GetPaths(input_files); +} + +void GUI_App::load_gcode(wxWindow* parent, wxString& input_file) const +{ + input_file.Clear(); + wxFileDialog dialog(parent ? parent : GetTopWindow(), + _L("Choose one file (GCODE/.GCO/.G/.ngc/NGC):"), + app_config->get_last_dir(), "", + file_wildcards(FT_GCODE), wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + if (dialog.ShowModal() == wxID_OK) + input_file = dialog.GetPath(); +} + +bool GUI_App::switch_language() +{ + if (select_language()) { + recreate_GUI(_L("Changing of an application language") + dots); + return true; + } else { + return false; + } +} + +#ifdef __linux__ +static const wxLanguageInfo* linux_get_existing_locale_language(const wxLanguageInfo* language, + const wxLanguageInfo* system_language) +{ + constexpr size_t max_len = 50; + char path[max_len] = ""; + std::vector locales; + const std::string lang_prefix = into_u8(language->CanonicalName.BeforeFirst('_')); + + // Call locale -a so we can parse the output to get the list of available locales + // We expect lines such as "en_US.utf8". Pick ones starting with the language code + // we are switching to. Lines with different formatting will be removed later. + FILE* fp = popen("locale -a", "r"); + if (fp != NULL) { + while (fgets(path, max_len, fp) != NULL) { + std::string line(path); + line = line.substr(0, line.find('\n')); + if (boost::starts_with(line, lang_prefix)) + locales.push_back(line); + } + pclose(fp); + } + + // locales now contain all candidates for this language. + // Sort them so ones containing anything about UTF-8 are at the end. + std::sort(locales.begin(), locales.end(), [](const std::string& a, const std::string& b) + { + auto has_utf8 = [](const std::string & s) { + auto S = boost::to_upper_copy(s); + return S.find("UTF8") != std::string::npos || S.find("UTF-8") != std::string::npos; + }; + return ! has_utf8(a) && has_utf8(b); + }); + + // Remove the suffix behind a dot, if there is one. + for (std::string& s : locales) + s = s.substr(0, s.find(".")); + + // We just hope that dear Linux "locale -a" returns country codes + // in ISO 3166-1 alpha-2 code (two letter) format. + // https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes + // To be sure, remove anything not looking as expected + // (any number of lowercase letters, underscore, two uppercase letters). + locales.erase(std::remove_if(locales.begin(), + locales.end(), + [](const std::string& s) { + return ! std::regex_match(s, + std::regex("^[a-z]+_[A-Z]{2}$")); + }), + locales.end()); + + if (system_language) { + // Is there a candidate matching a country code of a system language? Move it to the end, + // while maintaining the order of matches, so that the best match ends up at the very end. + std::string system_country = "_" + into_u8(system_language->CanonicalName.AfterFirst('_')).substr(0, 2); + int cnt = locales.size(); + for (int i = 0; i < cnt; ++i) + if (locales[i].find(system_country) != std::string::npos) { + locales.emplace_back(std::move(locales[i])); + locales[i].clear(); + } + } + + // Now try them one by one. + for (auto it = locales.rbegin(); it != locales.rend(); ++ it) + if (! it->empty()) { + const std::string &locale = *it; + const wxLanguageInfo* lang = wxLocale::FindLanguageInfo(from_u8(locale)); + if (wxLocale::IsAvailable(lang->Language)) + return lang; + } + return language; +} +#endif + +int GUI_App::GetSingleChoiceIndex(const wxString& message, + const wxString& caption, + const wxArrayString& choices, + int initialSelection) +{ +#ifdef _WIN32 + wxSingleChoiceDialog dialog(nullptr, message, caption, choices); + wxGetApp().UpdateDlgDarkUI(&dialog); + + dialog.SetSelection(initialSelection); + return dialog.ShowModal() == wxID_OK ? dialog.GetSelection() : -1; +#else + return wxGetSingleChoiceIndex(message, caption, choices, initialSelection); +#endif +} + +// select language from the list of installed languages +bool GUI_App::select_language() +{ + wxArrayString translations = wxTranslations::Get()->GetAvailableTranslations(SLIC3R_APP_KEY); + std::vector language_infos; + language_infos.emplace_back(wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH)); + for (size_t i = 0; i < translations.GetCount(); ++ i) { + const wxLanguageInfo *langinfo = wxLocale::FindLanguageInfo(translations[i]); + if (langinfo != nullptr) + language_infos.emplace_back(langinfo); + } + sort_remove_duplicates(language_infos); + std::sort(language_infos.begin(), language_infos.end(), [](const wxLanguageInfo* l, const wxLanguageInfo* r) { return l->Description < r->Description; }); + + wxArrayString names; + names.Alloc(language_infos.size()); + + // Some valid language should be selected since the application start up. + const wxLanguage current_language = wxLanguage(m_wxLocale->GetLanguage()); + int init_selection = -1; + int init_selection_alt = -1; + int init_selection_default = -1; + for (size_t i = 0; i < language_infos.size(); ++ i) { + if (wxLanguage(language_infos[i]->Language) == current_language) + // The dictionary matches the active language and country. + init_selection = i; + else if ((language_infos[i]->CanonicalName.BeforeFirst('_') == m_wxLocale->GetCanonicalName().BeforeFirst('_')) || + // if the active language is Slovak, mark the Czech language as active. + (language_infos[i]->CanonicalName.BeforeFirst('_') == "cs" && m_wxLocale->GetCanonicalName().BeforeFirst('_') == "sk")) + // The dictionary matches the active language, it does not necessarily match the country. + init_selection_alt = i; + if (language_infos[i]->CanonicalName.BeforeFirst('_') == "en") + // This will be the default selection if the active language does not match any dictionary. + init_selection_default = i; + names.Add(language_infos[i]->Description); + } + if (init_selection == -1) + // This is the dictionary matching the active language. + init_selection = init_selection_alt; + if (init_selection != -1) + // This is the language to highlight in the choice dialog initially. + init_selection_default = init_selection; + + const long index = GetSingleChoiceIndex(_L("Select the language"), _L("Language"), names, init_selection_default); + // Try to load a new language. + if (index != -1 && (init_selection == -1 || init_selection != index)) { + const wxLanguageInfo *new_language_info = language_infos[index]; + if (this->load_language(new_language_info->CanonicalName, false)) { + // Save language at application config. + // Which language to save as the selected dictionary language? + // 1) Hopefully the language set to wxTranslations by this->load_language(), but that API is weird and we don't want to rely on its + // stability in the future: + // wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); + // 2) Current locale language may not match the dictionary name, see GH issue #3901 + // m_wxLocale->GetCanonicalName() + // 3) new_language_info->CanonicalName is a safe bet. It points to a valid dictionary name. + app_config->set("translation_language", new_language_info->CanonicalName.ToUTF8().data()); + app_config->save(); + return true; + } + } + + return false; +} + +// Load gettext translation files and activate them at the start of the application, +// based on the "translation_language" key stored in the application config. +bool GUI_App::load_language(wxString language, bool initial) +{ + if (initial) { + // There is a static list of lookup path prefixes in wxWidgets. Add ours. + wxFileTranslationsLoader::AddCatalogLookupPathPrefix(from_u8(localization_dir())); + // Get the active language from PrusaSlicer.ini, or empty string if the key does not exist. + language = app_config->get("translation_language"); + if (! language.empty()) + BOOST_LOG_TRIVIAL(trace) << boost::format("translation_language provided by PrusaSlicer.ini: %1%") % language; + + // Get the system language. + { + const wxLanguage lang_system = wxLanguage(wxLocale::GetSystemLanguage()); + if (lang_system != wxLANGUAGE_UNKNOWN) { + m_language_info_system = wxLocale::GetLanguageInfo(lang_system); + BOOST_LOG_TRIVIAL(trace) << boost::format("System language detected (user locales and such): %1%") % m_language_info_system->CanonicalName.ToUTF8().data(); + } + } + { + // Allocating a temporary locale will switch the default wxTranslations to its internal wxTranslations instance. + wxLocale temp_locale; +#ifdef __WXOSX__ + // ysFIXME - temporary workaround till it isn't fixed in wxWidgets: + // Use English as an initial language, because of under OSX it try to load "inappropriate" language for wxLANGUAGE_DEFAULT. + // For example in our case it's trying to load "en_CZ" and as a result PrusaSlicer catch warning message. + // But wxWidgets guys work on it. + temp_locale.Init(wxLANGUAGE_ENGLISH); +#else + temp_locale.Init(); +#endif // __WXOSX__ + // Set the current translation's language to default, otherwise GetBestTranslation() may not work (see the wxWidgets source code). + wxTranslations::Get()->SetLanguage(wxLANGUAGE_DEFAULT); + // Let the wxFileTranslationsLoader enumerate all translation dictionaries for PrusaSlicer + // and try to match them with the system specific "preferred languages". + // There seems to be a support for that on Windows and OSX, while on Linuxes the code just returns wxLocale::GetSystemLanguage(). + // The last parameter gets added to the list of detected dictionaries. This is a workaround + // for not having the English dictionary. Let's hope wxWidgets of various versions process this call the same way. + wxString best_language = wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); + if (! best_language.IsEmpty()) { + m_language_info_best = wxLocale::FindLanguageInfo(best_language); + BOOST_LOG_TRIVIAL(trace) << boost::format("Best translation language detected (may be different from user locales): %1%") % m_language_info_best->CanonicalName.ToUTF8().data(); + } + #ifdef __linux__ + wxString lc_all; + if (wxGetEnv("LC_ALL", &lc_all) && ! lc_all.IsEmpty()) { + // Best language returned by wxWidgets on Linux apparently does not respect LC_ALL. + // Disregard the "best" suggestion in case LC_ALL is provided. + m_language_info_best = nullptr; + } + #endif + } + } + + const wxLanguageInfo *language_info = language.empty() ? nullptr : wxLocale::FindLanguageInfo(language); + if (! language.empty() && (language_info == nullptr || language_info->CanonicalName.empty())) { + // Fix for wxWidgets issue, where the FindLanguageInfo() returns locales with undefined ANSII code (wxLANGUAGE_KONKANI or wxLANGUAGE_MANIPURI). + language_info = nullptr; + BOOST_LOG_TRIVIAL(error) << boost::format("Language code \"%1%\" is not supported") % language.ToUTF8().data(); + } + + if (language_info != nullptr && language_info->LayoutDirection == wxLayout_RightToLeft) { + BOOST_LOG_TRIVIAL(trace) << boost::format("The following language code requires right to left layout, which is not supported by PrusaSlicer: %1%") % language_info->CanonicalName.ToUTF8().data(); + language_info = nullptr; + } + + if (language_info == nullptr) { + // PrusaSlicer does not support the Right to Left languages yet. + if (m_language_info_system != nullptr && m_language_info_system->LayoutDirection != wxLayout_RightToLeft) + language_info = m_language_info_system; + if (m_language_info_best != nullptr && m_language_info_best->LayoutDirection != wxLayout_RightToLeft) + language_info = m_language_info_best; + if (language_info == nullptr) + language_info = wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH_US); + } + + BOOST_LOG_TRIVIAL(trace) << boost::format("Switching wxLocales to %1%") % language_info->CanonicalName.ToUTF8().data(); + + // Alternate language code. + wxLanguage language_dict = wxLanguage(language_info->Language); + if (language_info->CanonicalName.BeforeFirst('_') == "sk") { + // Slovaks understand Czech well. Give them the Czech translation. + language_dict = wxLANGUAGE_CZECH; + BOOST_LOG_TRIVIAL(trace) << "Using Czech dictionaries for Slovak language"; + } + + // Select language for locales. This language may be different from the language of the dictionary. + if (language_info == m_language_info_best || language_info == m_language_info_system) { + // The current language matches user's default profile exactly. That's great. + } else if (m_language_info_best != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_best->CanonicalName.BeforeFirst('_')) { + // Use whatever the operating system recommends, if it the language code of the dictionary matches the recommended language. + // This allows a Swiss guy to use a German dictionary without forcing him to German locales. + language_info = m_language_info_best; + } else if (m_language_info_system != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_system->CanonicalName.BeforeFirst('_')) + language_info = m_language_info_system; + +#ifdef __linux__ + // If we can't find this locale , try to use different one for the language + // instead of just reporting that it is impossible to switch. + if (! wxLocale::IsAvailable(language_info->Language)) { + std::string original_lang = into_u8(language_info->CanonicalName); + language_info = linux_get_existing_locale_language(language_info, m_language_info_system); + BOOST_LOG_TRIVIAL(trace) << boost::format("Can't switch language to %1% (missing locales). Using %2% instead.") + % original_lang % language_info->CanonicalName.ToUTF8().data(); + } +#endif + + if (! wxLocale::IsAvailable(language_info->Language)) { + // Loading the language dictionary failed. + wxString message = "Switching PrusaSlicer to language " + language_info->CanonicalName + " failed."; +#if !defined(_WIN32) && !defined(__APPLE__) + // likely some linux system + message += "\nYou may need to reconfigure the missing locales, likely by running the \"locale-gen\" and \"dpkg-reconfigure locales\" commands.\n"; +#endif + if (initial) + message + "\n\nApplication will close."; + wxMessageBox(message, "PrusaSlicer - Switching language failed", wxOK | wxICON_ERROR); + if (initial) + std::exit(EXIT_FAILURE); + else + return false; + } + + // Release the old locales, create new locales. + //FIXME wxWidgets cause havoc if the current locale is deleted. We just forget it causing memory leaks for now. + m_wxLocale.release(); + m_wxLocale = Slic3r::make_unique(); + m_wxLocale->Init(language_info->Language); + // Override language at the active wxTranslations class (which is stored in the active m_wxLocale) + // to load possibly different dictionary, for example, load Czech dictionary for Slovak language. + wxTranslations::Get()->SetLanguage(language_dict); + { + // UKR Localization specific workaround till the wxWidgets doesn't fixed: + // From wxWidgets 3.1.6 calls setlocation(0, wxInfoLanguage->LocaleTag), see (https://github.com/prusa3d/wxWidgets/commit/deef116a09748796711d1e3509965ee208dcdf0b#diff-7de25e9a71c4dce61bbf76492c589623d5b93fd1bb105ceaf0662075d15f4472), + // where LocaleTag is a Tag of locale in BCP 47 - like notation. + // For Ukrainian Language LocaleTag == "uk". + // But setlocale(0, "uk") returns "English_United Kingdom.1252" instead of "uk", + // and, as a result, locales are set to English_United Kingdom + + if (language_info->CanonicalName == "uk") + setlocale(0, language_info->GetCanonicalWithRegion().data()); + } + m_wxLocale->AddCatalog(SLIC3R_APP_KEY); + m_imgui->set_language(into_u8(language_info->CanonicalName)); + //FIXME This is a temporary workaround, the correct solution is to switch to "C" locale during file import / export only. + //wxSetlocale(LC_NUMERIC, "C"); + Preset::update_suffix_modified((" (" + _L("modified") + ")").ToUTF8().data()); + return true; +} + +Tab* GUI_App::get_tab(Preset::Type type) +{ + for (Tab* tab: tabs_list) + if (tab->type() == type) + return tab->completed() ? tab : nullptr; // To avoid actions with no-completed Tab + return nullptr; +} + +ConfigOptionMode GUI_App::get_mode() +{ + if (!app_config->has("view_mode")) + return comSimple; + + const auto mode = app_config->get("view_mode"); + return mode == "expert" ? comExpert : + mode == "simple" ? comSimple : comAdvanced; +} + +bool GUI_App::save_mode(const /*ConfigOptionMode*/int mode) +{ + const std::string mode_str = mode == comExpert ? "expert" : + mode == comSimple ? "simple" : "advanced"; + + auto can_switch_to_simple = [](Model& model) { + for (const ModelObject* model_object : model.objects) + if (model_object->volumes.size() > 1) { + for (size_t i = 1; i < model_object->volumes.size(); ++i) + if (!model_object->volumes[i]->is_support_modifier()) + return false; + } + return true; + }; + + if (mode == comSimple && !can_switch_to_simple(model())) { + show_info(nullptr, + _L("Simple mode supports manipulation with single-part object(s)\n" + "or object(s) with support modifiers only.") + "\n\n" + + _L("Please check your object list before mode changing."), + _L("Change application mode")); + return false; + } + app_config->set("view_mode", mode_str); + app_config->save(); + update_mode(); + return true; +} + +// Update view mode according to selected menu +void GUI_App::update_mode() +{ + sidebar().update_mode(); + +#ifdef _WIN32 //_MSW_DARK_MODE + if (!wxGetApp().tabs_as_menu()) + dynamic_cast(mainframe->m_tabpanel)->UpdateMode(); +#endif + + for (auto tab : tabs_list) + tab->update_mode(); + + plater()->update_menus(); + plater()->canvas3D()->update_gizmos_on_off_state(); +} + +void GUI_App::add_config_menu(wxMenuBar *menu) +{ + auto local_menu = new wxMenu(); + wxWindowID config_id_base = wxWindow::NewControlId(int(ConfigMenuCnt)); + + const auto config_wizard_name = _(ConfigWizard::name(true)); + const auto config_wizard_tooltip = from_u8((boost::format(_utf8(L("Run %s"))) % config_wizard_name).str()); + // Cmd+, is standard on OS X - what about other operating systems? + if (is_editor()) { + local_menu->Append(config_id_base + ConfigMenuWizard, config_wizard_name + dots, config_wizard_tooltip); + local_menu->Append(config_id_base + ConfigMenuSnapshots, _L("&Configuration Snapshots") + dots, _L("Inspect / activate configuration snapshots")); + local_menu->Append(config_id_base + ConfigMenuTakeSnapshot, _L("Take Configuration &Snapshot"), _L("Capture a configuration snapshot")); + local_menu->Append(config_id_base + ConfigMenuUpdateConf, _L("Check for Configuration Updates"), _L("Check for configuration updates")); + local_menu->Append(config_id_base + ConfigMenuUpdateApp, _L("Check for Application Updates"), _L("Check for new version of application")); +#if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) + //if (DesktopIntegrationDialog::integration_possible()) + local_menu->Append(config_id_base + ConfigMenuDesktopIntegration, _L("Desktop Integration"), _L("Desktop Integration")); +#endif //(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) + local_menu->AppendSeparator(); + } + local_menu->Append(config_id_base + ConfigMenuPreferences, _L("&Preferences") + dots + +#ifdef __APPLE__ + "\tCtrl+,", +#else + "\tCtrl+P", +#endif + _L("Application preferences")); + wxMenu* mode_menu = nullptr; + if (is_editor()) { + local_menu->AppendSeparator(); + mode_menu = new wxMenu(); + mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeSimple, _L("Simple"), _L("Simple View Mode")); +// mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _L("Advanced"), _L("Advanced View Mode")); + mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _CTX(L_CONTEXT("Advanced", "Mode"), "Mode"), _L("Advanced View Mode")); + mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeExpert, _L("Expert"), _L("Expert View Mode")); + Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comSimple) evt.Check(true); }, config_id_base + ConfigMenuModeSimple); + Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comAdvanced) evt.Check(true); }, config_id_base + ConfigMenuModeAdvanced); + Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comExpert) evt.Check(true); }, config_id_base + ConfigMenuModeExpert); + + local_menu->AppendSubMenu(mode_menu, _L("Mode"), wxString::Format(_L("%s View Mode"), SLIC3R_APP_NAME)); + } + local_menu->AppendSeparator(); + local_menu->Append(config_id_base + ConfigMenuLanguage, _L("&Language")); + if (is_editor()) { + local_menu->AppendSeparator(); + local_menu->Append(config_id_base + ConfigMenuFlashFirmware, _L("Flash Printer &Firmware"), _L("Upload a firmware image into an Arduino based printer")); + // TODO: for when we're able to flash dictionaries + // local_menu->Append(config_id_base + FirmwareMenuDict, _L("Flash Language File"), _L("Upload a language dictionary file into a Prusa printer")); + } + + local_menu->Bind(wxEVT_MENU, [this, config_id_base](wxEvent &event) { + switch (event.GetId() - config_id_base) { + case ConfigMenuWizard: + run_wizard(ConfigWizard::RR_USER); + break; + case ConfigMenuUpdateConf: + check_updates(true); + break; + case ConfigMenuUpdateApp: + app_version_check(true); + break; +#ifdef __linux__ + case ConfigMenuDesktopIntegration: + show_desktop_integration_dialog(); + break; +#endif + case ConfigMenuTakeSnapshot: + // Take a configuration snapshot. + if (wxString action_name = _L("Taking a configuration snapshot"); + check_and_save_current_preset_changes(action_name, _L("Some presets are modified and the unsaved changes will not be captured by the configuration snapshot."), false, true)) { + wxTextEntryDialog dlg(nullptr, action_name, _L("Snapshot name")); + UpdateDlgDarkUI(&dlg); + + // set current normal font for dialog children, + // because of just dlg.SetFont(normal_font()) has no result; + for (auto child : dlg.GetChildren()) + child->SetFont(normal_font()); + + if (dlg.ShowModal() == wxID_OK) + if (const Config::Snapshot *snapshot = Config::take_config_snapshot_report_error( + *app_config, Config::Snapshot::SNAPSHOT_USER, dlg.GetValue().ToUTF8().data()); + snapshot != nullptr) + app_config->set("on_snapshot", snapshot->id); + } + break; + case ConfigMenuSnapshots: + if (check_and_save_current_preset_changes(_L("Loading a configuration snapshot"), "", false)) { + std::string on_snapshot; + if (Config::SnapshotDB::singleton().is_on_snapshot(*app_config)) + on_snapshot = app_config->get("on_snapshot"); + ConfigSnapshotDialog dlg(Slic3r::GUI::Config::SnapshotDB::singleton(), on_snapshot); + dlg.ShowModal(); + if (!dlg.snapshot_to_activate().empty()) { + if (! Config::SnapshotDB::singleton().is_on_snapshot(*app_config) && + ! Config::take_config_snapshot_cancel_on_error(*app_config, Config::Snapshot::SNAPSHOT_BEFORE_ROLLBACK, "", + GUI::format(_L("Continue to activate a configuration snapshot %1%?"), dlg.snapshot_to_activate()))) + break; + try { + app_config->set("on_snapshot", Config::SnapshotDB::singleton().restore_snapshot(dlg.snapshot_to_activate(), *app_config).id); + // Enable substitutions, log both user and system substitutions. There should not be any substitutions performed when loading system + // presets because compatibility of profiles shall be verified using the min_slic3r_version keys in config index, but users + // are known to be creative and mess with the config files in various ways. + if (PresetsConfigSubstitutions all_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::Enable); + ! all_substitutions.empty()) + show_substitutions_info(all_substitutions); + + // Load the currently selected preset into the GUI, update the preset selection box. + load_current_presets(); + } catch (std::exception &ex) { + GUI::show_error(nullptr, _L("Failed to activate configuration snapshot.") + "\n" + into_u8(ex.what())); + } + } + } + break; + case ConfigMenuPreferences: + { + open_preferences(); + break; + } + case ConfigMenuLanguage: + { + /* Before change application language, let's check unsaved changes on 3D-Scene + * and draw user's attention to the application restarting after a language change + */ + { + // the dialog needs to be destroyed before the call to switch_language() + // or sometimes the application crashes into wxDialogBase() destructor + // so we put it into an inner scope + wxString title = is_editor() ? wxString(SLIC3R_APP_NAME) : wxString(GCODEVIEWER_APP_NAME); + title += " - " + _L("Language selection"); + //wxMessageDialog dialog(nullptr, + MessageDialog dialog(nullptr, + _L("Switching the language will trigger application restart.\n" + "You will lose content of the plater.") + "\n\n" + + _L("Do you want to proceed?"), + title, + wxICON_QUESTION | wxOK | wxCANCEL); + if (dialog.ShowModal() == wxID_CANCEL) + return; + } + + switch_language(); + break; + } + case ConfigMenuFlashFirmware: + FirmwareDialog::run(mainframe); + break; + default: + break; + } + }); + + using std::placeholders::_1; + + if (mode_menu != nullptr) { + auto modfn = [this](int mode, wxCommandEvent&) { if (get_mode() != mode) save_mode(mode); }; + mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comSimple, _1), config_id_base + ConfigMenuModeSimple); + mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comAdvanced, _1), config_id_base + ConfigMenuModeAdvanced); + mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comExpert, _1), config_id_base + ConfigMenuModeExpert); + } + + menu->Append(local_menu, _L("&Configuration")); +} + +void GUI_App::open_preferences(const std::string& highlight_option /*= std::string()*/, const std::string& tab_name/*= std::string()*/) +{ + mainframe->preferences_dialog->show(highlight_option, tab_name); + + if (mainframe->preferences_dialog->recreate_GUI()) + recreate_GUI(_L("Restart application") + dots); + +#if ENABLE_GCODE_LINES_ID_IN_H_SLIDER + if (dlg.seq_top_layer_only_changed() || dlg.seq_seq_top_gcode_indices_changed()) +#else + if (mainframe->preferences_dialog->seq_top_layer_only_changed()) +#endif // ENABLE_GCODE_LINES_ID_IN_H_SLIDER + this->plater_->refresh_print(); + +#ifdef _WIN32 + if (is_editor()) { + if (app_config->get("associate_3mf") == "1") + associate_3mf_files(); + if (app_config->get("associate_stl") == "1") + associate_stl_files(); + } + else { + if (app_config->get("associate_gcode") == "1") + associate_gcode_files(); + } +#endif // _WIN32 + + if (mainframe->preferences_dialog->settings_layout_changed()) { + // hide full main_sizer for mainFrame + mainframe->GetSizer()->Show(false); + mainframe->update_layout(); + mainframe->select_tab(size_t(0)); + } +} + +bool GUI_App::has_unsaved_preset_changes() const +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* const tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology) && tab->saved_preset_is_dirty()) + return true; + } + return false; +} + +bool GUI_App::has_current_preset_changes() const +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* const tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) + return true; + } + return false; +} + +void GUI_App::update_saved_preset_from_current_preset() +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (Tab* tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology)) + tab->update_saved_preset_from_current_preset(); + } +} + +std::vector GUI_App::get_active_preset_collections() const +{ + std::vector ret; + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* tab : tabs_list) + if (tab->supports_printer_technology(printer_technology)) + ret.push_back(tab->get_presets()); + return ret; +} + +// To notify the user whether he is aware that some preset changes will be lost, +// UnsavedChangesDialog: "Discard / Save / Cancel" +// This is called when: +// - Close Application & Current project isn't saved +// - Load Project & Current project isn't saved +// - Undo / Redo with change of print technologie +// - Loading snapshot +// - Loading config_file/bundle +// UnsavedChangesDialog: "Don't save / Save / Cancel" +// This is called when: +// - Exporting config_bundle +// - Taking snapshot +bool GUI_App::check_and_save_current_preset_changes(const wxString& caption, const wxString& header, bool remember_choice/* = true*/, bool dont_save_insted_of_discard/* = false*/) +{ + if (has_current_preset_changes()) { + const std::string app_config_key = remember_choice ? "default_action_on_close_application" : ""; + int act_buttons = ActionButtons::SAVE; + if (dont_save_insted_of_discard) + act_buttons |= ActionButtons::DONT_SAVE; + UnsavedChangesDialog dlg(caption, header, app_config_key, act_buttons); + std::string act = app_config_key.empty() ? "none" : wxGetApp().app_config->get(app_config_key); + if (act == "none" && dlg.ShowModal() == wxID_CANCEL) + return false; + + if (dlg.save_preset()) // save selected changes + { + for (const std::pair& nt : dlg.get_names_and_types()) + preset_bundle->save_changes_for_preset(nt.first, nt.second, dlg.get_unselected_options(nt.second)); + + load_current_presets(false); + + // if we saved changes to the new presets, we should to + // synchronize config.ini with the current selections. + preset_bundle->export_selections(*app_config); + + MessageDialog(nullptr, dlg.msg_success_saved_modifications(dlg.get_names_and_types().size())).ShowModal(); + } + } + + return true; +} + +void GUI_App::apply_keeped_preset_modifications() +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (Tab* tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology)) + tab->apply_config_from_cache(); + } + load_current_presets(false); +} + +// This is called when creating new project or load another project +// OR close ConfigWizard +// to ask the user what should we do with unsaved changes for presets. +// New Project => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Cancel" +// => Current project isn't saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" +// Close ConfigWizard => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" +// Note: no_nullptr postponed_apply_of_keeped_changes indicates that thie function is called after ConfigWizard is closed +bool GUI_App::check_and_keep_current_preset_changes(const wxString& caption, const wxString& header, int action_buttons, bool* postponed_apply_of_keeped_changes/* = nullptr*/) +{ + if (has_current_preset_changes()) { + bool is_called_from_configwizard = postponed_apply_of_keeped_changes != nullptr; + + const std::string app_config_key = is_called_from_configwizard ? "" : "default_action_on_new_project"; + UnsavedChangesDialog dlg(caption, header, app_config_key, action_buttons); + std::string act = app_config_key.empty() ? "none" : wxGetApp().app_config->get(app_config_key); + if (act == "none" && dlg.ShowModal() == wxID_CANCEL) + return false; + + auto reset_modifications = [this, is_called_from_configwizard]() { + if (is_called_from_configwizard) + return; // no need to discared changes. It will be done fromConfigWizard closing + + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* const tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) + tab->m_presets->discard_current_changes(); + } + load_current_presets(false); + }; + + if (dlg.discard()) + reset_modifications(); + else // save selected changes + { + const auto& preset_names_and_types = dlg.get_names_and_types(); + if (dlg.save_preset()) { + for (const std::pair& nt : preset_names_and_types) + preset_bundle->save_changes_for_preset(nt.first, nt.second, dlg.get_unselected_options(nt.second)); + + // if we saved changes to the new presets, we should to + // synchronize config.ini with the current selections. + preset_bundle->export_selections(*app_config); + + wxString text = dlg.msg_success_saved_modifications(preset_names_and_types.size()); + if (!is_called_from_configwizard) + text += "\n\n" + _L("For new project all modifications will be reseted"); + + MessageDialog(nullptr, text).ShowModal(); + reset_modifications(); + } + else if (dlg.transfer_changes() && (dlg.has_unselected_options() || is_called_from_configwizard)) { + // execute this part of code only if not all modifications are keeping to the new project + // OR this function is called when ConfigWizard is closed and "Keep modifications" is selected + for (const std::pair& nt : preset_names_and_types) { + Preset::Type type = nt.second; + Tab* tab = get_tab(type); + std::vector selected_options = dlg.get_selected_options(type); + if (type == Preset::TYPE_PRINTER) { + auto it = std::find(selected_options.begin(), selected_options.end(), "extruders_count"); + if (it != selected_options.end()) { + // erase "extruders_count" option from the list + selected_options.erase(it); + // cache the extruders count + static_cast(tab)->cache_extruder_cnt(); + } + } + tab->cache_config_diff(selected_options); + if (!is_called_from_configwizard) + tab->m_presets->discard_current_changes(); + } + if (is_called_from_configwizard) + *postponed_apply_of_keeped_changes = true; + else + apply_keeped_preset_modifications(); + } + } + } + + return true; +} + +bool GUI_App::can_load_project() +{ + int saved_project = plater()->save_project_if_dirty(_L("Loading a new project while the current project is modified.")); + if (saved_project == wxID_CANCEL || + (plater()->is_project_dirty() && saved_project == wxID_NO && + !check_and_save_current_preset_changes(_L("Project is loading"), _L("Opening new project while some presets are unsaved.")))) + return false; + return true; +} + +bool GUI_App::check_print_host_queue() +{ + wxString dirty; + std::vector> jobs; + // Get ongoing jobs from dialog + mainframe->m_printhost_queue_dlg->get_active_jobs(jobs); + if (jobs.empty()) + return true; + // Show dialog + wxString job_string = wxString(); + for (const auto& job : jobs) { + job_string += format_wxstr(" %1% : %2% \n", job.first, job.second); + } + wxString message; + message += _(L("The uploads are still ongoing")) + ":\n\n" + job_string +"\n" + _(L("Stop them and continue anyway?")); + //wxMessageDialog dialog(mainframe, + MessageDialog dialog(mainframe, + message, + wxString(SLIC3R_APP_NAME) + " - " + _(L("Ongoing uploads")), + wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT); + if (dialog.ShowModal() == wxID_YES) + return true; + + // TODO: If already shown, bring forward + mainframe->m_printhost_queue_dlg->Show(); + return false; +} + +bool GUI_App::checked_tab(Tab* tab) +{ + bool ret = true; + if (find(tabs_list.begin(), tabs_list.end(), tab) == tabs_list.end()) + ret = false; + return ret; +} + +// Update UI / Tabs to reflect changes in the currently loaded presets +void GUI_App::load_current_presets(bool check_printer_presets_ /*= true*/) +{ + // check printer_presets for the containing information about "Print Host upload" + // and create physical printer from it, if any exists + if (check_printer_presets_) + check_printer_presets(); + + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + this->plater()->set_printer_technology(printer_technology); + for (Tab *tab : tabs_list) + if (tab->supports_printer_technology(printer_technology)) { + if (tab->type() == Preset::TYPE_PRINTER) { + static_cast(tab)->update_pages(); + // Mark the plater to update print bed by tab->load_current_preset() from Plater::on_config_change(). + this->plater()->force_print_bed_update(); + } + tab->load_current_preset(); + } +} + +bool GUI_App::OnExceptionInMainLoop() +{ + generic_exception_handle(); + return false; +} + +#ifdef __APPLE__ +// This callback is called from wxEntry()->wxApp::CallOnInit()->NSApplication run +// that is, before GUI_App::OnInit(), so we have a chance to switch GUI_App +// to a G-code viewer. +void GUI_App::OSXStoreOpenFiles(const wxArrayString &fileNames) +{ + size_t num_gcodes = 0; + for (const wxString &filename : fileNames) + if (is_gcode_file(into_u8(filename))) + ++ num_gcodes; + if (fileNames.size() == num_gcodes) { + // Opening PrusaSlicer by drag & dropping a G-Code onto PrusaSlicer icon in Finder, + // just G-codes were passed. Switch to G-code viewer mode. + m_app_mode = EAppMode::GCodeViewer; + unlock_lockfile(get_instance_hash_string() + ".lock", data_dir() + "/cache/"); + if(app_config != nullptr) + delete app_config; + app_config = nullptr; + init_app_config(); + } + wxApp::OSXStoreOpenFiles(fileNames); +} +// wxWidgets override to get an event on open files. +void GUI_App::MacOpenFiles(const wxArrayString &fileNames) +{ + std::vector files; + std::vector gcode_files; + std::vector non_gcode_files; + for (const auto& filename : fileNames) { + if (is_gcode_file(into_u8(filename))) + gcode_files.emplace_back(filename); + else { + files.emplace_back(into_u8(filename)); + non_gcode_files.emplace_back(filename); + } + } + if (m_app_mode == EAppMode::GCodeViewer) { + // Running in G-code viewer. + // Load the first G-code into the G-code viewer. + // Or if no G-codes, send other files to slicer. + if (! gcode_files.empty()) { + if (m_post_initialized) + this->plater()->load_gcode(gcode_files.front()); + else + this->init_params->input_files = { into_u8(gcode_files.front()) }; + } + if (!non_gcode_files.empty()) + start_new_slicer(non_gcode_files, true); + } else { + if (! files.empty()) { + if (m_post_initialized) { + wxArrayString input_files; + for (size_t i = 0; i < non_gcode_files.size(); ++i) + input_files.push_back(non_gcode_files[i]); + this->plater()->load_files(input_files); + } else { + for (const auto &f : non_gcode_files) + this->init_params->input_files.emplace_back(into_u8(f)); + } + } + for (const wxString &filename : gcode_files) + start_new_gcodeviewer(&filename); + } +} + +void GUI_App::MacOpenURL(const wxString& url) +{ + if (app_config && app_config->get("downloader_url_registered") != "1") + { + BOOST_LOG_TRIVIAL(error) << "Recieved command to open URL, but it is not allowed in app configuration. URL: " << url; + return; + } + start_download(boost::nowide::narrow(url)); +} + +#endif /* __APPLE */ + +Sidebar& GUI_App::sidebar() +{ + return plater_->sidebar(); +} + +ObjectManipulation* GUI_App::obj_manipul() +{ + // If this method is called before plater_ has been initialized, return nullptr (to avoid a crash) + return (plater_ != nullptr) ? sidebar().obj_manipul() : nullptr; +} + +ObjectSettings* GUI_App::obj_settings() +{ + return sidebar().obj_settings(); +} + +ObjectList* GUI_App::obj_list() +{ + return sidebar().obj_list(); +} + +ObjectLayers* GUI_App::obj_layers() +{ + return sidebar().obj_layers(); +} + +Plater* GUI_App::plater() +{ + return plater_; +} + +const Plater* GUI_App::plater() const +{ + return plater_; +} + +Model& GUI_App::model() +{ + return plater_->model(); +} +wxBookCtrlBase* GUI_App::tab_panel() const +{ + return mainframe->m_tabpanel; +} + +NotificationManager* GUI_App::notification_manager() +{ + return plater_->get_notification_manager(); +} + +GalleryDialog* GUI_App::gallery_dialog() +{ + return mainframe->gallery_dialog(); +} + +Downloader* GUI_App::downloader() +{ + return m_downloader.get(); +} + +// extruders count from selected printer preset +int GUI_App::extruders_cnt() const +{ + const Preset& preset = preset_bundle->printers.get_selected_preset(); + return preset.printer_technology() == ptSLA ? 1 : + preset.config.option("nozzle_diameter")->values.size(); +} + +// extruders count from edited printer preset +int GUI_App::extruders_edited_cnt() const +{ + const Preset& preset = preset_bundle->printers.get_edited_preset(); + return preset.printer_technology() == ptSLA ? 1 : + preset.config.option("nozzle_diameter")->values.size(); +} + +wxString GUI_App::current_language_code_safe() const +{ + // Translate the language code to a code, for which Prusa Research maintains translations. + const std::map mapping { + { "cs", "cs_CZ", }, + { "sk", "cs_CZ", }, + { "de", "de_DE", }, + { "es", "es_ES", }, + { "fr", "fr_FR", }, + { "it", "it_IT", }, + { "ja", "ja_JP", }, + { "ko", "ko_KR", }, + { "pl", "pl_PL", }, + { "uk", "uk_UA", }, + { "zh", "zh_CN", }, + { "ru", "ru_RU", }, + }; + wxString language_code = this->current_language_code().BeforeFirst('_'); + auto it = mapping.find(language_code); + if (it != mapping.end()) + language_code = it->second; + else + language_code = "en_US"; + return language_code; +} + +void GUI_App::open_web_page_localized(const std::string &http_address) +{ + open_browser_with_warning_dialog(http_address + "&lng=" + this->current_language_code_safe(), nullptr, false); +} + +// If we are switching from the FFF-preset to the SLA, we should to control the printed objects if they have a part(s). +// Because of we can't to print the multi-part objects with SLA technology. +bool GUI_App::may_switch_to_SLA_preset(const wxString& caption) +{ + if (model_has_parameter_modifiers_in_objects(model())) { + show_info(nullptr, + _L("It's impossible to print object(s) which contains parameter modifiers with SLA technology.") + "\n\n" + + _L("Please check your object list before preset changing."), + caption); + return false; + } +/* + if (model_has_multi_part_objects(model())) { + show_info(nullptr, + _L("It's impossible to print multi-part object(s) with SLA technology.") + "\n\n" + + _L("Please check your object list before preset changing."), + caption); + return false; + } + if (model_has_connectors(model())) { + show_info(nullptr, + _L("SLA technology doesn't support cut with connectors") + "\n\n" + + _L("Please check your object list before preset changing."), + caption); + return false; + } +*/ + return true; +} + +bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage start_page) +{ + wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); + + if (reason == ConfigWizard::RR_USER) { + // Cancel sync before starting wizard to prevent two downloads at same time + preset_updater->cancel_sync(); + preset_updater->update_index_db(); + if (preset_updater->config_update(app_config->orig_version(), PresetUpdater::UpdateParams::FORCED_BEFORE_WIZARD) == PresetUpdater::R_ALL_CANCELED) + return false; + } + + auto wizard = new ConfigWizard(mainframe); + const bool res = wizard->run(reason, start_page); + + if (res) { + load_current_presets(); + + // #ysFIXME - delete after testing: This part of code looks redundant. All checks are inside ConfigWizard::priv::apply_config() + if (preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA) + may_switch_to_SLA_preset(_L("Configuration is editing from ConfigWizard")); + } + + return res; +} + +void GUI_App::show_desktop_integration_dialog() +{ +#ifdef __linux__ + //wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); + DesktopIntegrationDialog dialog(mainframe); + dialog.ShowModal(); +#endif //__linux__ +} + +#if ENABLE_THUMBNAIL_GENERATOR_DEBUG +void GUI_App::gcode_thumbnails_debug() +{ + const std::string BEGIN_MASK = "; thumbnail begin"; + const std::string END_MASK = "; thumbnail end"; + std::string gcode_line; + bool reading_image = false; + unsigned int width = 0; + unsigned int height = 0; + + wxFileDialog dialog(GetTopWindow(), _L("Select a gcode file:"), "", "", "G-code files (*.gcode)|*.gcode;*.GCODE;", wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dialog.ShowModal() != wxID_OK) + return; + + std::string in_filename = into_u8(dialog.GetPath()); + std::string out_path = boost::filesystem::path(in_filename).remove_filename().append(L"thumbnail").string(); + + boost::nowide::ifstream in_file(in_filename.c_str()); + std::vector rows; + std::string row; + if (in_file.good()) + { + while (std::getline(in_file, gcode_line)) + { + if (in_file.good()) + { + if (boost::starts_with(gcode_line, BEGIN_MASK)) + { + reading_image = true; + gcode_line = gcode_line.substr(BEGIN_MASK.length() + 1); + std::string::size_type x_pos = gcode_line.find('x'); + std::string width_str = gcode_line.substr(0, x_pos); + width = (unsigned int)::atoi(width_str.c_str()); + std::string height_str = gcode_line.substr(x_pos + 1); + height = (unsigned int)::atoi(height_str.c_str()); + row.clear(); + } + else if (reading_image && boost::starts_with(gcode_line, END_MASK)) + { + std::string out_filename = out_path + std::to_string(width) + "x" + std::to_string(height) + ".png"; + boost::nowide::ofstream out_file(out_filename.c_str(), std::ios::binary); + if (out_file.good()) + { + std::string decoded; + decoded.resize(boost::beast::detail::base64::decoded_size(row.size())); + decoded.resize(boost::beast::detail::base64::decode((void*)&decoded[0], row.data(), row.size()).first); + + out_file.write(decoded.c_str(), decoded.size()); + out_file.close(); + } + + reading_image = false; + width = 0; + height = 0; + rows.clear(); + } + else if (reading_image) + row += gcode_line.substr(2); + } + } + + in_file.close(); + } +} +#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG + +void GUI_App::window_pos_save(wxTopLevelWindow* window, const std::string &name) +{ + if (name.empty()) { return; } + const auto config_key = (boost::format("window_%1%") % name).str(); + + WindowMetrics metrics = WindowMetrics::from_window(window); + app_config->set(config_key, metrics.serialize()); + app_config->save(); +} + +void GUI_App::window_pos_restore(wxTopLevelWindow* window, const std::string &name, bool default_maximized) +{ + if (name.empty()) { return; } + const auto config_key = (boost::format("window_%1%") % name).str(); + + if (! app_config->has(config_key)) { + window->Maximize(default_maximized); + return; + } + + auto metrics = WindowMetrics::deserialize(app_config->get(config_key)); + if (! metrics) { + window->Maximize(default_maximized); + return; + } + + const wxRect& rect = metrics->get_rect(); + + if (app_config->get("restore_win_position") == "1") { + // workaround for crash related to the positioning of the window on secondary monitor + app_config->set("restore_win_position", (boost::format("crashed_at_%1%_pos") % name).str()); + app_config->save(); + window->SetPosition(rect.GetPosition()); + + // workaround for crash related to the positioning of the window on secondary monitor + app_config->set("restore_win_position", (boost::format("crashed_at_%1%_size") % name).str()); + app_config->save(); + window->SetSize(rect.GetSize()); + + // revert "restore_win_position" value if application wasn't crashed + app_config->set("restore_win_position", "1"); + app_config->save(); + } + else + window->CenterOnScreen(); + + window->Maximize(metrics->get_maximized()); +} + +void GUI_App::window_pos_sanitize(wxTopLevelWindow* window) +{ + /*unsigned*/int display_idx = wxDisplay::GetFromWindow(window); + wxRect display; + if (display_idx == wxNOT_FOUND) { + display = wxDisplay(0u).GetClientArea(); + window->Move(display.GetTopLeft()); + } else { + display = wxDisplay(display_idx).GetClientArea(); + } + + auto metrics = WindowMetrics::from_window(window); + metrics.sanitize_for_display(display); + if (window->GetScreenRect() != metrics.get_rect()) { + window->SetSize(metrics.get_rect()); + } +} + +bool GUI_App::config_wizard_startup() +{ + if (!m_app_conf_exists || preset_bundle->printers.only_default_printers()) { + run_wizard(ConfigWizard::RR_DATA_EMPTY); + return true; + } else if (get_app_config()->legacy_datadir()) { + // Looks like user has legacy pre-vendorbundle data directory, + // explain what this is and run the wizard + + MsgDataLegacy dlg; + dlg.ShowModal(); + + run_wizard(ConfigWizard::RR_DATA_LEGACY); + return true; + } + return false; +} + +bool GUI_App::check_updates(const bool verbose) +{ + PresetUpdater::UpdateResult updater_result; + try { + preset_updater->update_index_db(); + updater_result = preset_updater->config_update(app_config->orig_version(), verbose ? PresetUpdater::UpdateParams::SHOW_TEXT_BOX : PresetUpdater::UpdateParams::SHOW_NOTIFICATION); + if (updater_result == PresetUpdater::R_INCOMPAT_EXIT) { + mainframe->Close(); + // Applicaiton is closing. + return false; + } + else if (updater_result == PresetUpdater::R_INCOMPAT_CONFIGURED) { + m_app_conf_exists = true; + } + else if (verbose && updater_result == PresetUpdater::R_NOOP) { + MsgNoUpdates dlg; + dlg.ShowModal(); + } + } + catch (const std::exception & ex) { + show_error(nullptr, ex.what()); + } + // Applicaiton will continue. + return true; +} + +bool GUI_App::open_browser_with_warning_dialog(const wxString& url, wxWindow* parent/* = nullptr*/, bool force_remember_choice /*= true*/, int flags/* = 0*/) +{ + bool launch = true; + + // warning dialog containes a "Remember my choice" checkbox + std::string option_key = "suppress_hyperlinks"; + if (force_remember_choice || app_config->get(option_key).empty()) { + if (app_config->get(option_key).empty()) { + RichMessageDialog dialog(parent, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); + dialog.ShowCheckBox(_L("Remember my choice")); + auto answer = dialog.ShowModal(); + launch = answer == wxID_YES; + if (dialog.IsCheckBoxChecked()) { + wxString preferences_item = _L("Suppress to open hyperlink in browser"); + wxString msg = + _L("PrusaSlicer will remember your choice.") + "\n\n" + + _L("You will not be asked about it again on hyperlinks hovering.") + "\n\n" + + format_wxstr(_L("Visit \"Preferences\" and check \"%1%\"\nto changes your choice."), preferences_item); + + MessageDialog msg_dlg(parent, msg, _L("PrusaSlicer: Don't ask me again"), wxOK | wxCANCEL | wxICON_INFORMATION); + if (msg_dlg.ShowModal() == wxID_CANCEL) + return false; + app_config->set(option_key, answer == wxID_NO ? "1" : "0"); + } + } + if (launch) + launch = app_config->get(option_key) != "1"; + } + // warning dialog doesn't containe a "Remember my choice" checkbox + // and will be shown only when "Suppress to open hyperlink in browser" is ON. + else if (app_config->get(option_key) == "1") { + MessageDialog dialog(parent, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); + launch = dialog.ShowModal() == wxID_YES; + } + + return launch && wxLaunchDefaultBrowser(url, flags); +} + +// static method accepting a wxWindow object as first parameter +// void warning_catcher{ +// my($self, $message_dialog) = @_; +// return sub{ +// my $message = shift; +// return if $message = ~/ GLUquadricObjPtr | Attempt to free unreferenced scalar / ; +// my @params = ($message, 'Warning', wxOK | wxICON_WARNING); +// $message_dialog +// ? $message_dialog->(@params) +// : Wx::MessageDialog->new($self, @params)->ShowModal; +// }; +// } + +// Do we need this function??? +// void GUI_App::notify(message) { +// auto frame = GetTopWindow(); +// // try harder to attract user attention on OS X +// if (!frame->IsActive()) +// frame->RequestUserAttention(defined(__WXOSX__/*&Wx::wxMAC */)? wxUSER_ATTENTION_ERROR : wxUSER_ATTENTION_INFO); +// +// // There used to be notifier using a Growl application for OSX, but Growl is dead. +// // The notifier also supported the Linux X D - bus notifications, but that support was broken. +// //TODO use wxNotificationMessage ? +// } + + +#ifdef __WXMSW__ +void GUI_App::associate_3mf_files() +{ + associate_file_type(L".3mf", L"Prusa.Slicer.1", L"PrusaSlicer", true); +} + +void GUI_App::associate_stl_files() +{ + associate_file_type(L".stl", L"Prusa.Slicer.1", L"PrusaSlicer", true); +} + +void GUI_App::associate_gcode_files() +{ + associate_file_type(L".gcode", L"PrusaSlicer.GCodeViewer.1", L"PrusaSlicerGCodeViewer", true); +} +#endif // __WXMSW__ + +void GUI_App::on_version_read(wxCommandEvent& evt) +{ + app_config->set("version_online", into_u8(evt.GetString())); + app_config->save(); + std::string opt = app_config->get("notify_release"); + if (this->plater_ == nullptr || (!m_app_updater->get_triggered_by_user() && opt != "all" && opt != "release")) { + BOOST_LOG_TRIVIAL(info) << "Version online: " << evt.GetString() << ". User does not wish to be notified."; + return; + } + if (*Semver::parse(SLIC3R_VERSION) >= *Semver::parse(into_u8(evt.GetString()))) { + if (m_app_updater->get_triggered_by_user()) + { + std::string text = (*Semver::parse(into_u8(evt.GetString())) == Semver()) + ? Slic3r::format(_u8L("Check for application update has failed.")) + : Slic3r::format(_u8L("No new version is available. Latest release version is %1%."), evt.GetString()); + + this->plater_->get_notification_manager()->push_version_notification(NotificationType::NoNewReleaseAvailable + , NotificationManager::NotificationLevel::RegularNotificationLevel + , text + , std::string() + , std::function() + ); + } + return; + } + // notification + /* + this->plater_->get_notification_manager()->push_notification(NotificationType::NewAppAvailable + , NotificationManager::NotificationLevel::ImportantNotificationLevel + , Slic3r::format(_u8L("New release version %1% is available."), evt.GetString()) + , _u8L("See Download page.") + , [](wxEvtHandler* evnthndlr) {wxGetApp().open_web_page_localized("https://www.prusa3d.com/slicerweb"); return true; } + ); + */ + // updater + // read triggered_by_user that was set when calling GUI_App::app_version_check + app_updater(m_app_updater->get_triggered_by_user()); +} + +void GUI_App::app_updater(bool from_user) +{ + DownloadAppData app_data = m_app_updater->get_app_data(); + + if (from_user && (!app_data.version || *app_data.version <= *Semver::parse(SLIC3R_VERSION))) + { + BOOST_LOG_TRIVIAL(info) << "There is no newer version online."; + MsgNoAppUpdates no_update_dialog; + no_update_dialog.ShowModal(); + return; + + } + + assert(!app_data.url.empty()); + assert(!app_data.target_path.empty()); + + // dialog with new version info + AppUpdateAvailableDialog dialog(*Semver::parse(SLIC3R_VERSION), *app_data.version, from_user); + auto dialog_result = dialog.ShowModal(); + // checkbox "do not show again" + if (dialog.disable_version_check()) { + app_config->set("notify_release", "none"); + } + // Doesn't wish to update + if (dialog_result != wxID_OK) { + return; + } + // dialog with new version download (installer or app dependent on system) including path selection + AppUpdateDownloadDialog dwnld_dlg(*app_data.version, app_data.target_path); + dialog_result = dwnld_dlg.ShowModal(); + // Doesn't wish to download + if (dialog_result != wxID_OK) { + return; + } + if (dwnld_dlg.get_download_path().parent_path().empty() || !boost::filesystem::exists(dwnld_dlg.get_download_path().parent_path())) { + show_error(nullptr,GUI::format_wxstr(_L("Download can't proceed. Target directory doesn't exists: %1%"), dwnld_dlg.get_download_path().parent_path().string())); + return; + } + app_data.target_path =dwnld_dlg.get_download_path(); + + // start download + this->plater_->get_notification_manager()->push_download_progress_notification(_utf8("Download"), std::bind(&AppUpdater::cancel_callback, this->m_app_updater.get())); + app_data.start_after = dwnld_dlg.run_after_download(); + m_app_updater->set_app_data(std::move(app_data)); + m_app_updater->sync_download(); +} + +void GUI_App::app_version_check(bool from_user) +{ + if (from_user) { + if (m_app_updater->get_download_ongoing()) { + MessageDialog msgdlg(nullptr, _L("Download of new version is already ongoing. Do you wish to continue?"), _L("Notice"), wxYES_NO); + if (msgdlg.ShowModal() != wxID_YES) + return; + } + } + std::string version_check_url = app_config->version_check_url(); + m_app_updater->sync_version(version_check_url, from_user); +} + +void GUI_App::start_download(std::string url) +{ + if (!plater_) { + BOOST_LOG_TRIVIAL(error) << "Could not start URL download: plater is nullptr."; + return; + } + //lets always init so if the download dest folder was changed, new dest is used + boost::filesystem::path dest_folder(app_config->get("url_downloader_dest")); + if (dest_folder.empty() || !boost::filesystem::is_directory(dest_folder)) { + std::string msg = _utf8("Could not start URL download. Destination folder is not set. Please choose destination folder in Configuration Wizard."); + BOOST_LOG_TRIVIAL(error) << msg; + show_error(nullptr, msg); + return; + } + m_downloader->init(dest_folder); + m_downloader->start_download(url); +} + +} // GUI +} //Slic3r diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 81382d092..70d72d5e3 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -263,7 +263,7 @@ public: Tab* get_tab(Preset::Type type); ConfigOptionMode get_mode(); - void save_mode(const /*ConfigOptionMode*/int mode) ; + bool save_mode(const /*ConfigOptionMode*/int mode) ; void update_mode(); void add_config_menu(wxMenuBar *menu); diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index d71a128b0..c06947565 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -44,7 +44,6 @@ static bool is_improper_category(const std::string& category, const int extruder (!is_object_settings && category == "Support material"); } - //------------------------------------- // SettingsFactory //------------------------------------- @@ -155,22 +154,22 @@ wxBitmapBundle* SettingsFactory::get_category_bitmap(const std::string& category //------------------------------------- // Note: id accords to type of the sub-object (adding volume), so sequence of the menu items is important -const std::vector> MenuFactory::ADD_VOLUME_MENU_ITEMS { -// menu_item Name menu_item bitmap name - {L("Add part"), "add_part" }, // ~ModelVolumeType::MODEL_PART - {L("Add negative volume"), "add_negative" }, // ~ModelVolumeType::NEGATIVE_VOLUME - {L("Add modifier"), "add_modifier"}, // ~ModelVolumeType::PARAMETER_MODIFIER - {L("Add support blocker"), "support_blocker"}, // ~ModelVolumeType::SUPPORT_BLOCKER - {L("Add support enforcer"), "support_enforcer"}, // ~ModelVolumeType::SUPPORT_ENFORCER -}; +static const constexpr std::array, 5> ADD_VOLUME_MENU_ITEMS = {{ + // menu_item Name menu_item bitmap name + {L("Add part"), "add_part" }, // ~ModelVolumeType::MODEL_PART + {L("Add negative volume"), "add_negative" }, // ~ModelVolumeType::NEGATIVE_VOLUME + {L("Add modifier"), "add_modifier"}, // ~ModelVolumeType::PARAMETER_MODIFIER + {L("Add support blocker"), "support_blocker"}, // ~ModelVolumeType::SUPPORT_BLOCKER + {L("Add support enforcer"), "support_enforcer"}, // ~ModelVolumeType::SUPPORT_ENFORCER +}}; // Note: id accords to type of the sub-object (adding volume), so sequence of the menu items is important -const std::vector> MenuFactory::TEXT_VOLUME_ICONS { +static const constexpr std::array, 3> TEXT_VOLUME_ICONS {{ // menu_item Name menu_item bitmap name {L("Add text"), "add_text_part"}, // ~ModelVolumeType::MODEL_PART {L("Add negative text"), "add_text_negative" }, // ~ModelVolumeType::NEGATIVE_VOLUME {L("Add text modifier"), "add_text_modifier"}, // ~ModelVolumeType::PARAMETER_MODIFIER -}; +}}; static Plater* plater() { @@ -466,19 +465,19 @@ void MenuFactory::append_menu_item_delete(wxMenu* menu) } -wxMenu* MenuFactory::append_submenu_add_generic(wxMenu* menu, ModelVolumeType type) { +wxMenu* MenuFactory::append_submenu_add_generic(wxMenu* menu, ModelVolumeType type) +{ auto sub_menu = new wxMenu; const ConfigOptionMode mode = wxGetApp().get_mode(); - if (mode == comExpert && type != ModelVolumeType::INVALID || - mode == comAdvanced && type == ModelVolumeType::MODEL_PART) { + if (type != ModelVolumeType::INVALID && mode > comSimple) { append_menu_item(sub_menu, wxID_ANY, _L("Load") + " " + dots, "", [type](wxCommandEvent&) { obj_list()->load_subobject(type); }, "", menu); sub_menu->AppendSeparator(); } - if (!(type == ModelVolumeType::MODEL_PART && mode == comAdvanced)) + //if (!(type == ModelVolumeType::MODEL_PART && mode == comAdvanced)) for (auto& item : { L("Box"), L("Cylinder"), L("Sphere"), L("Slab") }) { if (type == ModelVolumeType::INVALID && strncmp(item, "Slab", 4) == 0) @@ -528,13 +527,18 @@ void MenuFactory::append_menu_item_add_text(wxMenu* menu, ModelVolumeType type, ) { wxString item_name = is_submenu_item ? "" : _(ADD_VOLUME_MENU_ITEMS[int(type)].first) + ": "; item_name += _L("Text"); + menu->AppendSeparator(); const std::string icon_name = is_submenu_item ? "" : ADD_VOLUME_MENU_ITEMS[int(type)].second; append_menu_item(menu, wxID_ANY, item_name, "", add_text, icon_name, menu); } } -void MenuFactory::append_menu_items_add_volume(wxMenu* menu) +void MenuFactory::append_menu_items_add_volume(MenuType menu_type) { + wxMenu* menu = menu_type == mtObjectFFF ? &m_object_menu : menu_type == mtObjectSLA ? &m_sla_object_menu : nullptr; + if (!menu) + return; + // Update "add" items(delete old & create new) items popupmenu for (auto& item : ADD_VOLUME_MENU_ITEMS) { const wxString item_name = _(item.first); @@ -570,9 +574,11 @@ void MenuFactory::append_menu_items_add_volume(wxMenu* menu) return; } - int type = 0; - for (auto& item : ADD_VOLUME_MENU_ITEMS) { - wxMenu* sub_menu = append_submenu_add_generic(menu, ModelVolumeType(type++)); + for (size_t type = 0; type < ADD_VOLUME_MENU_ITEMS.size(); type++) { + auto& item = ADD_VOLUME_MENU_ITEMS[type]; + if (menu_type == mtObjectSLA && ModelVolumeType(type) == ModelVolumeType::PARAMETER_MODIFIER) + continue; + wxMenu* sub_menu = append_submenu_add_generic(menu, ModelVolumeType(type)); append_submenu(menu, sub_menu, wxID_ANY, _(item.first), "", item.second, [type]() { bool can_add = type < size_t(ModelVolumeType::PARAMETER_MODIFIER) ? !obj_list()->is_selected_object_cut() : true; @@ -580,7 +586,8 @@ void MenuFactory::append_menu_items_add_volume(wxMenu* menu) }, m_parent); } - append_menu_item_layers_editing(menu); + if (menu_type == mtObjectFFF) + append_menu_item_layers_editing(menu); } wxMenuItem* MenuFactory::append_menu_item_layers_editing(wxMenu* menu) @@ -631,7 +638,7 @@ wxMenuItem* MenuFactory::append_menu_item_settings(wxMenu* menu_) // If there are selected more then one instance but not all of them // don't add settings menu items const Selection& selection = get_selection(); - if ((selection.is_multiple_full_instance() && !selection.is_single_full_object()) || + if ((selection.is_multiple_full_instance() && !selection.is_single_full_object()) || (printer_technology() == ptSLA && selection.is_single_volume()) || selection.is_multiple_volume() || selection.is_mixed()) // more than one volume(part) is selected on the scene return nullptr; @@ -1038,37 +1045,26 @@ void MenuFactory::create_common_object_menu(wxMenu* menu) append_menu_item_fix_through_netfabb(menu); append_menu_item_simplify(menu); append_menu_items_mirror(menu); + + append_menu_items_split(menu); + menu->AppendSeparator(); } -void MenuFactory::create_object_menu() +void MenuFactory::append_menu_items_split(wxMenu *menu) { - create_common_object_menu(&m_object_menu); wxMenu* split_menu = new wxMenu(); if (!split_menu) return; append_menu_item(split_menu, wxID_ANY, _L("To objects"), _L("Split the selected object into individual objects"), - [](wxCommandEvent&) { plater()->split_object(); }, "split_object_SMALL", &m_object_menu, + [](wxCommandEvent&) { plater()->split_object(); }, "split_object_SMALL", menu, []() { return plater()->can_split(true); }, m_parent); append_menu_item(split_menu, wxID_ANY, _L("To parts"), _L("Split the selected object into individual parts"), - [](wxCommandEvent&) { plater()->split_volume(); }, "split_parts_SMALL", &m_object_menu, + [](wxCommandEvent&) { plater()->split_volume(); }, "split_parts_SMALL", menu, []() { return plater()->can_split(false); }, m_parent); - append_submenu(&m_object_menu, split_menu, wxID_ANY, _L("Split"), _L("Split the selected object"), "", + append_submenu(menu, split_menu, wxID_ANY, _L("Split"), _L("Split the selected object"), "", []() { return plater()->can_split(true); }, m_parent); - m_object_menu.AppendSeparator(); - - // "Height range Modifier" and "Add (volumes)" menu items will be added later in append_menu_items_add_volume() -} - -void MenuFactory::create_sla_object_menu() -{ - create_common_object_menu(&m_sla_object_menu); - append_menu_item(&m_sla_object_menu, wxID_ANY, _L("Split"), _L("Split the selected object into individual objects"), - [](wxCommandEvent&) { plater()->split_object(); }, "split_object_SMALL", nullptr, - []() { return plater()->can_split(true); }, m_parent); - - m_sla_object_menu.AppendSeparator(); } void MenuFactory::append_immutable_part_menu_items(wxMenu* menu) @@ -1130,8 +1126,8 @@ void MenuFactory::init(wxWindow* parent) m_parent = parent; create_default_menu(); - create_object_menu(); - create_sla_object_menu(); + create_common_object_menu(&m_object_menu); + create_common_object_menu(&m_sla_object_menu); create_part_menu(); create_text_part_menu(); create_instance_menu(); @@ -1140,7 +1136,7 @@ void MenuFactory::init(wxWindow* parent) void MenuFactory::update() { update_default_menu(); - update_object_menu(); + update_objects_menu(); } wxMenu* MenuFactory::default_menu() @@ -1277,9 +1273,10 @@ void MenuFactory::update_menu_items_instance_manipulation(MenuType type) } } -void MenuFactory::update_object_menu() +void MenuFactory::update_objects_menu() { - append_menu_items_add_volume(&m_object_menu); + append_menu_items_add_volume(mtObjectFFF); + append_menu_items_add_volume(mtObjectSLA); } void MenuFactory::update_default_menu() diff --git a/src/slic3r/GUI/GUI_Factories.hpp b/src/slic3r/GUI/GUI_Factories.hpp index ed27655be..515311d0d 100644 --- a/src/slic3r/GUI/GUI_Factories.hpp +++ b/src/slic3r/GUI/GUI_Factories.hpp @@ -33,8 +33,6 @@ struct SettingsFactory class MenuFactory { public: - static const std::vector> ADD_VOLUME_MENU_ITEMS; - static const std::vector> TEXT_VOLUME_ICONS; static std::vector get_volume_bitmaps(); static std::vector get_text_volume_bitmaps(); @@ -43,7 +41,7 @@ public: void init(wxWindow* parent); void update(); - void update_object_menu(); + void update_objects_menu(); void update_default_menu(); void sys_color_changed(); @@ -81,8 +79,6 @@ private: void create_default_menu(); void create_common_object_menu(wxMenu *menu); - void create_object_menu(); - void create_sla_object_menu(); void append_immutable_part_menu_items(wxMenu* menu); void append_mutable_part_menu_items(wxMenu* menu); void create_part_menu(); @@ -91,7 +87,7 @@ private: wxMenu* append_submenu_add_generic(wxMenu* menu, ModelVolumeType type); void append_menu_item_add_text(wxMenu* menu, ModelVolumeType type, bool is_submenu_item = true); - void append_menu_items_add_volume(wxMenu* menu); + void append_menu_items_add_volume(MenuType type); wxMenuItem* append_menu_item_layers_editing(wxMenu* menu); wxMenuItem* append_menu_item_settings(wxMenu* menu); wxMenuItem* append_menu_item_change_type(wxMenu* menu); @@ -114,6 +110,7 @@ private: void append_menu_item_edit_text(wxMenu *menu); void append_menu_items_instance_manipulation(wxMenu *menu); void update_menu_items_instance_manipulation(MenuType type); + void append_menu_items_split(wxMenu *menu); }; }} diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 954f61813..81ec956ee 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -226,6 +226,21 @@ ObjectList::ObjectList(wxWindow* parent) : wxDataViewItem item; wxDataViewColumn* col; this->HitTest(this->get_mouse_position_in_control(), item, col); + + // if there is text item to editing, than edit just a name without Text marker + if (auto type = m_objects_model->GetItemType(item); + type & (itObject | itVolume) && col->GetModelColumn() == colName) { + if (ModelObject* obj = object(m_objects_model->GetObjectIdByItem(item))) { + if (type == itObject && obj->is_text()) + m_objects_model->SetName(from_u8(obj->name), item); + else if (type == itVolume && obj->volumes[m_objects_model->GetVolumeIdByItem(item)]->is_text()) { + // we cant rename text parts + event.StopPropagation(); + return; + } + } + } + this->EditItem(item, col); event.StopPropagation(); }); @@ -640,6 +655,11 @@ void ObjectList::update_extruder_in_config(const wxDataViewItem& item) wxGetApp().plater()->update(); } +static wxString get_item_name(const std::string& name, const bool is_text_volume) +{ + return (is_text_volume ? _L("Text") + " - " : "") + from_u8(name); +} + void ObjectList::update_name_in_model(const wxDataViewItem& item) const { const int obj_idx = m_objects_model->GetObjectIdByItem(item); @@ -652,8 +672,11 @@ void ObjectList::update_name_in_model(const wxDataViewItem& item) const if (m_objects_model->GetItemType(item) & itObject) { obj->name = into_u8(m_objects_model->GetName(item)); // if object has just one volume, rename this volume too - if (obj->volumes.size() == 1 && !obj->volumes[0]->text_configuration.has_value()) + if (obj->is_text()) { obj->volumes[0]->name = obj->name; + //update object name with text marker in ObjectList + m_objects_model->SetName(get_item_name(obj->name, true), item); + } return; } @@ -662,28 +685,32 @@ void ObjectList::update_name_in_model(const wxDataViewItem& item) const // Renaming of the text volume is suppressed // So, revert the name in object list - if (obj->volumes[volume_id]->text_configuration.has_value()) { - m_objects_model->SetName(from_u8(obj->volumes[volume_id]->name), item); + if (obj->volumes[volume_id]->is_text()) { + m_objects_model->SetName(get_item_name(obj->volumes[volume_id]->name, true), item); return; } - obj->volumes[volume_id]->name = m_objects_model->GetName(item).ToUTF8().data(); + obj->volumes[volume_id]->name = into_u8(m_objects_model->GetName(item)); } void ObjectList::update_name_in_list(int obj_idx, int vol_idx) const { if (obj_idx < 0) return; wxDataViewItem item = GetSelection(); - if (!item || !(m_objects_model->GetItemType(item) & (itVolume | itObject))) + auto type = m_objects_model->GetItemType(item); + if (!item || !(type & (itVolume | itObject))) return; - wxString new_name = from_u8(object(obj_idx)->volumes[vol_idx]->name); + ModelObject* obj = object(obj_idx); + const bool is_text_volume = type == itVolume ? obj->volumes[vol_idx]->is_text() : obj->is_text(); + const wxString new_name = get_item_name(object(obj_idx)->volumes[vol_idx]->name, is_text_volume); + if (new_name.IsEmpty() || m_objects_model->GetName(item) == new_name) return; m_objects_model->SetName(new_name, item); // if object has just one volume, rename object too - if (ModelObject* obj = object(obj_idx); obj->volumes.size() == 1) + if (obj->volumes.size() == 1) obj->name = obj->volumes.front()->name; } @@ -2089,13 +2116,13 @@ bool ObjectList::del_subobject_from_object(const int obj_idx, const int idx, con object->delete_volume(idx); if (object->volumes.size() == 1) { + wxDataViewItem obj_item = m_objects_model->GetItemById(obj_idx); const auto last_volume = object->volumes[0]; if (!last_volume->config.empty()) { object->config.apply(last_volume->config); last_volume->config.reset(); // update extruder color in ObjectList - wxDataViewItem obj_item = m_objects_model->GetItemById(obj_idx); if (obj_item) { wxString extruder = object->config.has("extruder") ? wxString::Format("%d", object->config.extruder()) : _L("default"); m_objects_model->SetExtruder(extruder, obj_item); @@ -2103,6 +2130,9 @@ bool ObjectList::del_subobject_from_object(const int obj_idx, const int idx, con // add settings to the object, if it has them add_settings_item(obj_item, &object->config.get()); } + + if (last_volume->is_text()) + m_objects_model->SetName(get_item_name(/*last_volume*/object->name, true), obj_item); } } else if (type == itInstance) { @@ -2598,7 +2628,7 @@ void ObjectList::delete_all_connectors_for_object(int obj_idx) bool ObjectList::can_merge_to_multipart_object() const { - if (printer_technology() == ptSLA || has_selected_cut_object()) + if (has_selected_cut_object()) return false; wxDataViewItemArray sels; @@ -3008,16 +3038,23 @@ wxDataViewItemArray ObjectList::add_volumes_to_object_in_list(size_t obj_idx, st const ModelObject* object = (*m_objects)[obj_idx]; // add volumes to the object if (can_add_volumes_to_object(object)) { + if (object->volumes.size() > 1) { + wxString obj_item_name = from_u8(object->name); + if (m_objects_model->GetName(object_item) != obj_item_name) + m_objects_model->SetName(obj_item_name, object_item); + } + int volume_idx{ -1 }; for (const ModelVolume* volume : object->volumes) { ++volume_idx; - if (object->is_cut() && volume->is_cut_connector()) + if ((object->is_cut() && volume->is_cut_connector()) || + (printer_technology() == ptSLA && volume->type() == ModelVolumeType::PARAMETER_MODIFIER)) continue; const wxDataViewItem& vol_item = m_objects_model->AddVolumeChild(object_item, - from_u8(volume->name), + get_item_name(volume->name, volume->is_text()), volume_idx, volume->type(), - volume->text_configuration.has_value(), + volume->is_text(), get_warning_icon_name(volume->mesh().stats()), extruder2str(volume->config.has("extruder") ? volume->config.extruder() : 0)); add_settings_item(vol_item, &volume->config.get()); @@ -3035,7 +3072,7 @@ wxDataViewItemArray ObjectList::add_volumes_to_object_in_list(size_t obj_idx, st void ObjectList::add_object_to_list(size_t obj_idx, bool call_selection_changed) { auto model_object = (*m_objects)[obj_idx]; - const wxString& item_name = from_u8(model_object->name); + const wxString& item_name = get_item_name(model_object->name, model_object->is_text()); const auto item = m_objects_model->AddObject(item_name, extruder2str(model_object->config.has("extruder") ? model_object->config.extruder() : 0), get_warning_icon_name(model_object->mesh().stats()), @@ -4281,17 +4318,35 @@ void ObjectList::change_part_type() } const bool is_cut_object = obj->is_cut(); - wxArrayString names; - if (!is_cut_object) - for (const wxString& type : { _L("Part"), _L("Negative Volume") }) - names.Add(type); - names.Add(_L("Modifier")); - if (!volume->text_configuration.has_value()) - for (const wxString& name : { _L("Support Blocker"), _L("Support Enforcer") }) + wxArrayString names; + std::vector types; + types.reserve(5); + if (!is_cut_object) { + for (const wxString& name : { _L("Part"), _L("Negative Volume") }) names.Add(name); + for (const ModelVolumeType type_id : { ModelVolumeType::MODEL_PART, ModelVolumeType::NEGATIVE_VOLUME }) + types.emplace_back(type_id); + } + + if (printer_technology() != ptSLA) { + names.Add(_L("Modifier")); + types.emplace_back(ModelVolumeType::PARAMETER_MODIFIER); + } + + if (!volume->text_configuration.has_value()) { + for (const wxString& name : { _L("Support Blocker"), _L("Support Enforcer") }) + names.Add(name); + for (const ModelVolumeType type_id : { ModelVolumeType::SUPPORT_BLOCKER, ModelVolumeType::SUPPORT_ENFORCER }) + types.emplace_back(type_id); + } + + int selection = 0; + if (auto it = std::find(types.begin(), types.end(), type); it != types.end()) + selection = it - types.begin(); + + auto choice = wxGetApp().GetSingleChoiceIndex(_L("Type:"), _L("Select type of part"), names, selection); + const auto new_type = choice >= 0 ? types[choice] : ModelVolumeType::INVALID; - const int type_shift = is_cut_object ? 2 : 0; - auto new_type = ModelVolumeType(type_shift + wxGetApp().GetSingleChoiceIndex(_L("Type:"), _L("Select type of part"), names, int(type) - type_shift)); if (new_type == type || new_type == ModelVolumeType::INVALID) return; @@ -4376,8 +4431,9 @@ void ObjectList::update_object_list_by_printer_technology() m_objects_model->GetChildren(wxDataViewItem(nullptr), object_items); for (auto& object_item : object_items) { + const int obj_idx = m_objects_model->GetObjectIdByItem(object_item); // update custom supports info - update_info_items(m_objects_model->GetObjectIdByItem(object_item), &sel); + update_info_items(obj_idx, &sel); // Update Settings Item for object update_settings_item_and_selection(object_item, sel); @@ -4385,10 +4441,26 @@ void ObjectList::update_object_list_by_printer_technology() // Update settings for Volumes wxDataViewItemArray all_object_subitems; m_objects_model->GetChildren(object_item, all_object_subitems); + + bool was_selected_some_subitem = false; for (auto item : all_object_subitems) - if (m_objects_model->GetItemType(item) & itVolume) - // update settings for volume - update_settings_item_and_selection(item, sel); + if (m_objects_model->GetItemType(item) & itVolume) { + if (sel.Index(item) != wxNOT_FOUND) { + sel.Remove(item); + was_selected_some_subitem = true; + } + else if (const wxDataViewItem vol_settings_item = m_objects_model->GetSettingsItem(item); + sel.Index(vol_settings_item) != wxNOT_FOUND) { + sel.Remove(vol_settings_item); + was_selected_some_subitem = true; + break; + } + } + if (was_selected_some_subitem) + sel.Add(object_item); + + // Update volumes list in respect to the print mode + add_volumes_to_object_in_list(obj_idx); // Update Layers Items wxDataViewItem layers_item = m_objects_model->GetLayerRootItem(object_item); @@ -4430,6 +4502,8 @@ void ObjectList::update_object_list_by_printer_technology() // restore selection: SetSelections(sel); m_prevent_canvas_selection_update = false; + + update_selections_on_canvas(); } void ObjectList::instances_to_separated_object(const int obj_idx, const std::set& inst_idxs) @@ -4525,11 +4599,18 @@ void ObjectList::split_instances() void ObjectList::rename_item() { const wxDataViewItem item = GetSelection(); - if (!item || !(m_objects_model->GetItemType(item) & (itVolume | itObject))) + auto type = m_objects_model->GetItemType(item); + if (!item || !(type & (itVolume | itObject))) return ; - const wxString new_name = wxGetTextFromUser(_(L("Enter new name"))+":", _(L("Renaming")), - m_objects_model->GetName(item), this); + wxString input_name = m_objects_model->GetName(item); + if (ModelObject* obj = object(m_objects_model->GetObjectIdByItem(item))) { + // if there is text item to editing, than edit just a name without Text marker + if (type == itObject && obj->is_text()) + input_name = from_u8(obj->name); + } + + const wxString new_name = wxGetTextFromUser(_L("Enter new name")+":", _L("Renaming"), input_name, this); if (new_name.IsEmpty()) return; diff --git a/src/slic3r/GUI/GUI_ObjectManipulation.cpp b/src/slic3r/GUI/GUI_ObjectManipulation.cpp index 41f4cf060..d737a7be4 100644 --- a/src/slic3r/GUI/GUI_ObjectManipulation.cpp +++ b/src/slic3r/GUI/GUI_ObjectManipulation.cpp @@ -944,6 +944,9 @@ void ObjectManipulation::update_reset_buttons_visibility() if (selection.is_single_full_instance()) { #if ENABLE_WORLD_COORDINATE + const Geometry::Transformation& trafo = volume->get_instance_transformation(); + rotation = trafo.get_rotation_matrix(); + scale = trafo.get_scaling_factor_matrix(); const Selection::IndicesList& idxs = selection.get_volume_idxs(); for (unsigned int id : idxs) { const Geometry::Transformation world_trafo(selection.get_volume(id)->world_matrix()); @@ -1215,8 +1218,8 @@ void ObjectManipulation::change_scale_value(int axis, double value) const Selection& selection = wxGetApp().plater()->canvas3D()->get_selection(); Vec3d ref_scale = m_cache.scale; if (selection.is_single_volume_or_modifier()) { - if (is_local_coordinates()) - ref_scale = 100.0 * Vec3d::Ones(); + scale = scale.cwiseQuotient(ref_scale); + ref_scale = Vec3d::Ones(); } else if (selection.is_single_full_instance()) { scale = scale.cwiseQuotient(ref_scale); @@ -1257,8 +1260,6 @@ void ObjectManipulation::change_size_value(int axis, double value) Vec3d ref_size = m_cache.size; #if ENABLE_WORLD_COORDINATE if (selection.is_single_volume_or_modifier()) { - if (is_local_coordinates()) - ref_size = selection.get_first_volume()->bounding_box().size(); size = size.cwiseQuotient(ref_size); ref_size = Vec3d::Ones(); #else diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index 3e5822cfd..4235bf941 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -1669,12 +1669,10 @@ void GLGizmoCut3D::render_cut_plane_input_window(CutConnectors &connectors) reset_cut_plane(); m_imgui->disabled_end(); - if (wxGetApp().plater()->printer_technology() == ptFFF) { - m_imgui->disabled_begin(!m_keep_upper || !m_keep_lower); - if (m_imgui->button(_L("Add/Edit connectors"))) - set_connectors_editing(true); - m_imgui->disabled_end(); - } + m_imgui->disabled_begin(!m_keep_upper || !m_keep_lower); + if (m_imgui->button(_L("Add/Edit connectors"))) + set_connectors_editing(true); + m_imgui->disabled_end(); ImGui::Separator(); @@ -1804,7 +1802,7 @@ void GLGizmoCut3D::render_input_window_warning() const { if (m_is_contour_changed) return; - if (wxGetApp().plater()->printer_technology() == ptFFF && m_has_invalid_connector) { + if (m_has_invalid_connector) { wxString out = wxString(ImGui::WarningMarkerSmall) + _L("Invalid connectors detected") + ":"; if (m_info_stats.outside_cut_contour > size_t(0)) out += "\n - " + format_wxstr(_L_PLURAL("%1$d connector is out of cut contour", "%1$d connectors are out of cut contour", m_info_stats.outside_cut_contour), diff --git a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp index df22dde10..62d3c9a8c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp @@ -2134,10 +2134,10 @@ void GLGizmoEmboss::draw_model_type() else if (type != negative) ImGui::SetTooltip("%s", _u8L("Click to change part type into negative volume.").c_str()); } - ImGui::SameLine(); // In simple mode are not modifiers - if (wxGetApp().get_mode() != ConfigOptionMode::comSimple) { + if (wxGetApp().plater()->printer_technology() != ptSLA && wxGetApp().get_mode() != ConfigOptionMode::comSimple) { + ImGui::SameLine(); if (ImGui::RadioButton(_u8L("Modifier").c_str(), type == modifier)) new_type = modifier; else if (ImGui::IsItemHovered()) { @@ -3664,7 +3664,7 @@ DataBase priv::create_emboss_data_base(const std::string &text, StyleManager& st text_fixed = text; // copy std::replace(text_fixed.begin(), text_fixed.end(), '\n', ' '); } - return _u8L("Text") + " - " + ((contain_enter) ? text_fixed : text); + return ((contain_enter) ? text_fixed : text); }; auto create_configuration = [&]() -> TextConfiguration { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 6e12527d5..ec993fcd4 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -43,8 +43,8 @@ bool GLGizmoFdmSupports::on_init() { m_shortcut_key = WXK_CONTROL_L; - m_desc["auto_generate"] = _L("Auto-generate supports"); - m_desc["generating"] = _L("Generating supports..."); + m_desc["autopaint"] = _L("Automatic painting"); + m_desc["painting"] = _L("painting..."); m_desc["clipping_of_view"] = _L("Clipping of view") + ": "; m_desc["reset_direction"] = _L("Reset direction"); m_desc["cursor_size"] = _L("Brush size") + ": "; @@ -160,9 +160,9 @@ void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_l ImGui::Separator(); if (waiting_for_autogenerated_supports) { - m_imgui->text(m_desc.at("generating")); + m_imgui->text(m_desc.at("painting")); } else { - bool generate = m_imgui->button(m_desc.at("auto_generate")); + bool generate = m_imgui->button(m_desc.at("autopaint")); if (generate) auto_generate(); } @@ -525,12 +525,12 @@ void GLGizmoFdmSupports::auto_generate() }); MessageDialog dlg(GUI::wxGetApp().plater(), - _L("Autogeneration will erase all currently painted areas.") + "\n\n" + + _L("Automatic painting will erase all currently painted areas.") + "\n\n" + _L("Are you sure you want to do it?") + "\n", _L("Warning"), wxICON_WARNING | wxYES | wxNO); if (not_painted || dlg.ShowModal() == wxID_YES) { - Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Autogenerate support points")); + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Automatic painting support points")); int mesh_id = -1.0f; for (ModelVolume *mv : mo->volumes) { if (mv->is_model_part()) { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp index 99994f7b6..fc54622e8 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp @@ -1,6 +1,6 @@ +#include "libslic3r/libslic3r.h" #include "GLGizmoHollow.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" -#include "slic3r/GUI/Camera.hpp" #include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp" #include @@ -10,15 +10,15 @@ #include "slic3r/GUI/GUI_ObjectList.hpp" #include "slic3r/GUI/Plater.hpp" #include "libslic3r/PresetBundle.hpp" +#include "libslic3r/SLAPrint.hpp" #include "libslic3r/Model.hpp" - namespace Slic3r { namespace GUI { GLGizmoHollow::GLGizmoHollow(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) - : GLGizmoBase(parent, icon_filename, sprite_id) + : GLGizmoSlaBase(parent, icon_filename, sprite_id, slaposAssembly) { } @@ -53,12 +53,20 @@ void GLGizmoHollow::data_changed() reload_cache(); m_old_mo_id = mo->id(); } - if (m_c->hollowed_mesh() && m_c->hollowed_mesh()->get_hollowed_mesh()) - m_holes_in_drilled_mesh = mo->sla_drain_holes; - if (m_raycasters.empty()) - on_register_raycasters_for_picking(); + + const SLAPrintObject* po = m_c->selection_info()->print_object(); + std::shared_ptr preview_mesh_ptr = po->get_mesh_to_print(); + if (po != nullptr && (!preview_mesh_ptr || preview_mesh_ptr->empty())) + reslice_until_step(slaposAssembly); + + update_volumes(); + + if (m_hole_raycasters.empty()) + register_hole_raycasters_for_picking(); else - update_raycasters_for_picking_transform(); + update_hole_raycasters_for_picking_transform(); + + m_c->instances_hider()->set_hide_full_scene(true); } } @@ -80,39 +88,25 @@ void GLGizmoHollow::on_render() glsafe(::glEnable(GL_BLEND)); glsafe(::glEnable(GL_DEPTH_TEST)); - if (selection.is_from_single_instance()) - render_points(selection); + render_volumes(); + render_points(selection); m_selection_rectangle.render(m_parent); m_c->object_clipper()->render_cut(); - m_c->supports_clipper()->render_cut(); glsafe(::glDisable(GL_BLEND)); } void GLGizmoHollow::on_register_raycasters_for_picking() { - assert(m_raycasters.empty()); - - init_cylinder_model(); - - set_sla_auxiliary_volumes_picking_state(false); - - const CommonGizmosDataObjects::SelectionInfo* info = m_c->selection_info(); - if (info != nullptr && !info->model_object()->sla_drain_holes.empty()) { - const sla::DrainHoles& drain_holes = info->model_object()->sla_drain_holes; - for (int i = 0; i < (int)drain_holes.size(); ++i) { - m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, i, *m_cylinder.mesh_raycaster)); - } - update_raycasters_for_picking_transform(); - } + register_hole_raycasters_for_picking(); + register_volume_raycasters_for_picking(); } void GLGizmoHollow::on_unregister_raycasters_for_picking() { - m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo); - m_raycasters.clear(); - set_sla_auxiliary_volumes_picking_state(true); + unregister_hole_raycasters_for_picking(); + unregister_volume_raycasters_for_picking(); } void GLGizmoHollow::render_points(const Selection& selection) @@ -124,11 +118,17 @@ void GLGizmoHollow::render_points(const Selection& selection) shader->start_using(); ScopeGuard guard([shader]() { shader->stop_using(); }); - const GLVolume* vol = selection.get_first_volume(); - const Transform3d trafo = vol->world_matrix(); + auto *inst = m_c->selection_info()->model_instance(); + if (!inst) + return; + + double shift_z = m_c->selection_info()->print_object()->get_current_elevation(); + Transform3d trafo(inst->get_transformation().get_matrix()); + trafo.translation()(2) += shift_z; + const Geometry::Transformation transformation{trafo}; #if ENABLE_WORLD_COORDINATE - const Transform3d instance_scaling_matrix_inverse = vol->get_instance_transformation().get_scaling_factor_matrix().inverse(); + const Transform3d instance_scaling_matrix_inverse = transformation.get_scaling_factor_matrix().inverse(); #else const Transform3d instance_scaling_matrix_inverse = vol->get_instance_transformation().get_matrix(true, true, false, true).inverse(); #endif // ENABLE_WORLD_COORDINATE @@ -145,26 +145,21 @@ void GLGizmoHollow::render_points(const Selection& selection) const bool point_selected = m_selected[i]; const bool clipped = is_mesh_point_clipped(drain_hole.pos.cast()); - m_raycasters[i]->set_active(!clipped); + m_hole_raycasters[i]->set_active(!clipped); if (clipped) continue; // First decide about the color of the point. - if (size_t(m_hover_id) == i) - render_color = ColorRGBA::CYAN(); - else if (m_c->hollowed_mesh() && - i < m_c->hollowed_mesh()->get_drainholes().size() && - m_c->hollowed_mesh()->get_drainholes()[i].failed) { - render_color = { 1.0f, 0.0f, 0.0f, 0.5f }; - } - else - render_color = point_selected ? ColorRGBA(1.0f, 0.3f, 0.3f, 0.5f) : ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f); + if (size_t(m_hover_id) == i) + render_color = ColorRGBA::CYAN(); + else + render_color = point_selected ? ColorRGBA(1.0f, 0.3f, 0.3f, 0.5f) : ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f); m_cylinder.model.set_color(render_color); // Inverse matrix of the instance scaling is applied so that the mark does not scale with the object. const Transform3d hole_matrix = Geometry::translation_transform(drain_hole.pos.cast()) * instance_scaling_matrix_inverse; - if (vol->is_left_handed()) + if (transformation.is_left_handed()) glsafe(::glFrontFace(GL_CW)); // Matrices set, we can render the point mark now. @@ -178,7 +173,7 @@ void GLGizmoHollow::render_points(const Selection& selection) shader->set_uniform("view_normal_matrix", view_normal_matrix); m_cylinder.model.render(); - if (vol->is_left_handed()) + if (transformation.is_left_handed()) glsafe(::glFrontFace(GL_CCW)); } } @@ -198,56 +193,6 @@ bool GLGizmoHollow::is_mesh_point_clipped(const Vec3d& point) const return m_c->object_clipper()->get_clipping_plane()->is_point_clipped(transformed_point); } - - -// Unprojects the mouse position on the mesh and saves hit point and normal of the facet into pos_and_normal -// Return false if no intersection was found, true otherwise. -bool GLGizmoHollow::unproject_on_mesh(const Vec2d& mouse_pos, std::pair& pos_and_normal) -{ - if (! m_c->raycaster()->raycaster()) - return false; - - const Camera& camera = wxGetApp().plater()->get_camera(); - const Selection& selection = m_parent.get_selection(); - const GLVolume* volume = selection.get_first_volume(); - Geometry::Transformation trafo = volume->get_instance_transformation() * volume->get_volume_transformation(); - trafo.set_offset(trafo.get_offset() + Vec3d(0., 0., m_c->selection_info()->get_sla_shift())); - - double clp_dist = m_c->object_clipper()->get_position(); - const ClippingPlane* clp = m_c->object_clipper()->get_clipping_plane(); - - // The raycaster query - Vec3f hit; - Vec3f normal; - if (m_c->raycaster()->raycaster()->unproject_on_mesh( - mouse_pos, - trafo.get_matrix(), - camera, - hit, - normal, - clp_dist != 0. ? clp : nullptr)) - { - if (m_c->hollowed_mesh() && m_c->hollowed_mesh()->get_hollowed_mesh()) { - // in this case the raycaster sees the hollowed and drilled mesh. - // if the point lies on the surface created by the hole, we want - // to ignore it. - for (const sla::DrainHole& hole : m_holes_in_drilled_mesh) { - sla::DrainHole outer(hole); - outer.radius *= 1.001f; - outer.height *= 1.001f; - if (outer.is_inside(hit)) - return false; - } - } - - // Return both the point and the facet normal. - pos_and_normal = std::make_pair(hit, normal); - return true; - } - else - return false; -} - // Following function is called from GLCanvas3D to inform the gizmo about a mouse/keyboard event. // The gizmo has an opportunity to react - if it does, it should return true so that the Canvas3D is // aware that the event was reacted to and stops trying to make different sense of it. If the gizmo @@ -261,9 +206,8 @@ bool GLGizmoHollow::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_pos // left down with shift - show the selection rectangle: if (action == SLAGizmoEventType::LeftDown && (shift_down || alt_down || control_down)) { if (m_hover_id == -1) { - if (shift_down || alt_down) { + if (shift_down || alt_down) m_selection_rectangle.start_dragging(mouse_position, shift_down ? GLSelectionRectangle::EState::Select : GLSelectionRectangle::EState::Deselect); - } } else { if (m_selected[m_hover_id]) @@ -295,8 +239,8 @@ bool GLGizmoHollow::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_pos assert(m_selected.size() == mo->sla_drain_holes.size()); m_parent.set_as_dirty(); m_wait_for_up_event = true; - on_unregister_raycasters_for_picking(); - on_register_raycasters_for_picking(); + unregister_hole_raycasters_for_picking(); + register_hole_raycasters_for_picking(); } else return false; @@ -416,13 +360,14 @@ void GLGizmoHollow::delete_selected_points() } } - on_unregister_raycasters_for_picking(); - on_register_raycasters_for_picking(); + unregister_hole_raycasters_for_picking(); + register_hole_raycasters_for_picking(); select_point(NoPoints); } bool GLGizmoHollow::on_mouse(const wxMouseEvent &mouse_event) { + if (!is_input_enabled()) return true; if (mouse_event.Moving()) return false; if (use_grabbers(mouse_event)) return true; @@ -485,41 +430,49 @@ bool GLGizmoHollow::on_mouse(const wxMouseEvent &mouse_event) return false; } -void GLGizmoHollow::hollow_mesh(bool postpone_error_messages) +void GLGizmoHollow::register_hole_raycasters_for_picking() { - wxGetApp().CallAfter([this, postpone_error_messages]() { - wxGetApp().plater()->reslice_SLA_hollowing( - *m_c->selection_info()->model_object(), postpone_error_messages); - }); -} + assert(m_hole_raycasters.empty()); -void GLGizmoHollow::set_sla_auxiliary_volumes_picking_state(bool state) -{ - std::vector>* raycasters = m_parent.get_raycasters_for_picking(SceneRaycaster::EType::Volume); - if (raycasters != nullptr) { - const Selection& selection = m_parent.get_selection(); - const Selection::IndicesList ids = selection.get_volume_idxs(); - for (unsigned int id : ids) { - const GLVolume* v = selection.get_volume(id); - if (v->is_sla_pad() || v->is_sla_support()) { - auto it = std::find_if(raycasters->begin(), raycasters->end(), [v](std::shared_ptr item) { return item->get_raycaster() == v->mesh_raycaster.get(); }); - if (it != raycasters->end()) - (*it)->set_active(state); - } + init_cylinder_model(); + + const CommonGizmosDataObjects::SelectionInfo* info = m_c->selection_info(); + if (info != nullptr && !info->model_object()->sla_drain_holes.empty()) { + const sla::DrainHoles& drain_holes = info->model_object()->sla_drain_holes; + for (int i = 0; i < (int)drain_holes.size(); ++i) { + m_hole_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, i, *m_cylinder.mesh_raycaster, Transform3d::Identity())); } + update_hole_raycasters_for_picking_transform(); } } -void GLGizmoHollow::update_raycasters_for_picking_transform() +void GLGizmoHollow::unregister_hole_raycasters_for_picking() +{ + for (size_t i = 0; i < m_hole_raycasters.size(); ++i) { + m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, i); + } + m_hole_raycasters.clear(); +} + +void GLGizmoHollow::update_hole_raycasters_for_picking_transform() { const CommonGizmosDataObjects::SelectionInfo* info = m_c->selection_info(); if (info != nullptr) { const sla::DrainHoles& drain_holes = info->model_object()->sla_drain_holes; if (!drain_holes.empty()) { - assert(!m_raycasters.empty()); + assert(!m_hole_raycasters.empty()); const GLVolume* vol = m_parent.get_selection().get_first_volume(); - const Transform3d instance_scaling_matrix_inverse = vol->get_instance_transformation().get_scaling_factor_matrix().inverse(); + Geometry::Transformation transformation(vol->get_instance_transformation()); + + auto *inst = m_c->selection_info()->model_instance(); + if (inst && m_c->selection_info() && m_c->selection_info()->print_object()) { + double shift_z = m_c->selection_info()->print_object()->get_current_elevation(); + auto trafo = inst->get_transformation().get_matrix(); + trafo.translation()(2) += shift_z; + transformation.set_matrix(trafo); + } + const Transform3d instance_scaling_matrix_inverse = transformation.get_scaling_factor_matrix().inverse(); for (size_t i = 0; i < drain_holes.size(); ++i) { const sla::DrainHole& drain_hole = drain_holes[i]; @@ -527,9 +480,9 @@ void GLGizmoHollow::update_raycasters_for_picking_transform() Eigen::Quaterniond q; q.setFromTwoVectors(Vec3d::UnitZ(), instance_scaling_matrix_inverse * (-drain_hole.normal).cast()); const Eigen::AngleAxisd aa(q); - const Transform3d matrix = vol->world_matrix() * hole_matrix * Transform3d(aa.toRotationMatrix()) * + const Transform3d matrix = transformation.get_matrix() * hole_matrix * Transform3d(aa.toRotationMatrix()) * Geometry::translation_transform(-drain_hole.height * Vec3d::UnitZ()) * Geometry::scale_transform(Vec3d(drain_hole.radius, drain_hole.radius, drain_hole.height + sla::HoleStickOutLength)); - m_raycasters[i]->set_transform(matrix); + m_hole_raycasters[i]->set_transform(matrix); } } } @@ -626,9 +579,11 @@ RENDER_AGAIN: float window_width = minimal_slider_width + std::max({settings_sliders_left, clipping_slider_left, diameter_slider_left}); window_width = std::max(window_width, button_preview_width); + m_imgui->disabled_begin(!is_input_enabled()); + if (m_imgui->button(m_desc["preview"])) - hollow_mesh(); - + reslice_until_step(slaposDrillHoles); + bool config_changed = false; ImGui::Separator(); @@ -643,7 +598,10 @@ RENDER_AGAIN: } } - m_imgui->disabled_begin(! m_enable_hollowing); + m_imgui->disabled_end(); + + m_imgui->disabled_begin(!is_input_enabled() || !m_enable_hollowing); + ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc.at("offset")); ImGui::SameLine(settings_sliders_left, m_imgui->get_item_spacing().x); @@ -686,7 +644,7 @@ RENDER_AGAIN: mo->config.set("hollowing_min_thickness", m_offset_stash); mo->config.set("hollowing_quality", m_quality_stash); mo->config.set("hollowing_closing_distance", m_closing_d_stash); - Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Hollowing parameter change"))); + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Hollowing parameter change")); } mo->config.set("hollowing_min_thickness", offset); mo->config.set("hollowing_quality", quality); @@ -709,12 +667,15 @@ RENDER_AGAIN: if (m_new_hole_radius * 2.f > diameter_upper_cap) m_new_hole_radius = diameter_upper_cap / 2.f; ImGui::AlignTextToFramePadding(); + + m_imgui->disabled_begin(!is_input_enabled()); + m_imgui->text(m_desc.at("hole_diameter")); ImGui::SameLine(diameter_slider_left, m_imgui->get_item_spacing().x); ImGui::PushItemWidth(window_width - diameter_slider_left); - float diam = 2.f * m_new_hole_radius; m_imgui->slider_float("##hole_diameter", &diam, 1.f, 25.f, "%.1f mm", 1.f, false); + // Let's clamp the value (which could have been entered by keyboard) to a larger range // than the slider. This allows entering off-scale values and still protects against //complete non-sense. @@ -725,9 +686,13 @@ RENDER_AGAIN: bool deactivated = m_imgui->get_last_slider_status().deactivated_after_edit; ImGui::AlignTextToFramePadding(); + m_imgui->text(m_desc["hole_depth"]); ImGui::SameLine(diameter_slider_left, m_imgui->get_item_spacing().x); m_imgui->slider_float("##hole_depth", &m_new_hole_height, 0.f, 10.f, "%.1f mm", 1.f, false); + + m_imgui->disabled_end(); + // Same as above: m_new_hole_height = std::clamp(m_new_hole_height, 0.f, 100.f); @@ -763,24 +728,24 @@ RENDER_AGAIN: break; } } - Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Change drainage hole diameter"))); + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Change drainage hole diameter")); m_new_hole_radius = backup_rad; m_new_hole_height = backup_hei; mo->sla_drain_holes = new_holes; } } - m_imgui->disabled_begin(m_selection_empty); + m_imgui->disabled_begin(!is_input_enabled() || m_selection_empty); remove_selected = m_imgui->button(m_desc.at("remove_selected")); m_imgui->disabled_end(); - m_imgui->disabled_begin(mo->sla_drain_holes.empty()); + m_imgui->disabled_begin(!is_input_enabled() || mo->sla_drain_holes.empty()); remove_all = m_imgui->button(m_desc.at("remove_all")); m_imgui->disabled_end(); // Following is rendered in both editing and non-editing mode: - // m_imgui->text(""); ImGui::Separator(); + m_imgui->disabled_begin(!is_input_enabled()); if (m_c->object_clipper()->get_position() == 0.f) { ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc.at("clipping_of_view")); @@ -799,16 +764,7 @@ RENDER_AGAIN: if (m_imgui->slider_float("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f")) m_c->object_clipper()->set_position_by_ratio(clp_dist, true); - // make sure supports are shown/hidden as appropriate - bool show_sups = m_c->instances_hider()->are_supports_shown(); - if (m_imgui->checkbox(m_desc["show_supports"], show_sups)) { - m_c->instances_hider()->show_supports(show_sups); - if (show_sups) - // ensure supports and pad are disabled from picking even when they are visible - set_sla_auxiliary_volumes_picking_state(false); - force_refresh = true; - } - + m_imgui->disabled_end(); m_imgui->end(); @@ -841,7 +797,7 @@ bool GLGizmoHollow::on_is_activable() const const Selection& selection = m_parent.get_selection(); if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA - || !selection.is_from_single_instance()) + || !selection.is_single_full_instance()) return false; // Check that none of the selected volumes is outside. Only SLA auxiliaries (supports) are allowed outside. @@ -863,26 +819,17 @@ std::string GLGizmoHollow::on_get_name() const return _u8L("Hollow and drill"); } - -CommonGizmosDataID GLGizmoHollow::on_get_requirements() const -{ - return CommonGizmosDataID( - int(CommonGizmosDataID::SelectionInfo) - | int(CommonGizmosDataID::InstancesHider) - | int(CommonGizmosDataID::Raycaster) - | int(CommonGizmosDataID::HollowedMesh) - | int(CommonGizmosDataID::ObjectClipper) - | int(CommonGizmosDataID::SupportsClipper)); -} - - void GLGizmoHollow::on_set_state() { if (m_state == m_old_state) return; - if (m_state == Off && m_old_state != Off) // the gizmo was just turned Off + if (m_state == Off && m_old_state != Off) { + // the gizmo was just turned Off m_parent.post_event(SimpleEvent(EVT_GLCANVAS_FORCE_UPDATE)); + m_c->instances_hider()->set_hide_full_scene(false); + } + m_old_state = m_state; } @@ -997,6 +944,9 @@ void GLGizmoHollow::reload_cache() void GLGizmoHollow::on_set_hover_id() { + if (m_c->selection_info()->model_object() == nullptr) + return; + if (int(m_c->selection_info()->model_object()->sla_drain_holes.size()) <= m_hover_id) m_hover_id = -1; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoHollow.hpp b/src/slic3r/GUI/Gizmos/GLGizmoHollow.hpp index ea88c4291..abfb2503f 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoHollow.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoHollow.hpp @@ -1,7 +1,7 @@ #ifndef slic3r_GLGizmoHollow_hpp_ #define slic3r_GLGizmoHollow_hpp_ -#include "GLGizmoBase.hpp" +#include "GLGizmoSlaBase.hpp" #include "slic3r/GUI/GLSelectionRectangle.hpp" #include @@ -20,12 +20,8 @@ namespace GUI { enum class SLAGizmoEventType : unsigned char; class Selection; -class GLGizmoHollow : public GLGizmoBase +class GLGizmoHollow : public GLGizmoSlaBase { -private: - bool unproject_on_mesh(const Vec2d& mouse_pos, std::pair& pos_and_normal); - - public: GLGizmoHollow(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); void data_changed() override; @@ -51,14 +47,14 @@ protected: private: void render_points(const Selection& selection); - void hollow_mesh(bool postpone_error_messages = false); - void set_sla_auxiliary_volumes_picking_state(bool state); - void update_raycasters_for_picking_transform(); + void register_hole_raycasters_for_picking(); + void unregister_hole_raycasters_for_picking(); + void update_hole_raycasters_for_picking_transform(); ObjectID m_old_mo_id = -1; PickingModel m_cylinder; - std::vector> m_raycasters; + std::vector> m_hole_raycasters; float m_new_hole_radius = 2.f; // Size of a new hole. float m_new_hole_height = 6.f; @@ -106,7 +102,6 @@ protected: void on_stop_dragging() override; void on_dragging(const UpdateData &data) override; void on_render_input_window(float x, float y, float bottom_limit) override; - virtual CommonGizmosDataID on_get_requirements() const override; std::string on_get_name() const override; bool on_is_activable() const override; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.cpp new file mode 100644 index 000000000..444e3bf46 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.cpp @@ -0,0 +1,180 @@ +#include "libslic3r/libslic3r.h" +#include "GLGizmoSlaBase.hpp" +#include "slic3r/GUI/Camera.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp" + +namespace Slic3r { +namespace GUI { + +static const ColorRGBA DISABLED_COLOR = ColorRGBA::DARK_GRAY(); +static const int VOLUME_RAYCASTERS_BASE_ID = (int)SceneRaycaster::EIdBase::Gizmo; + +GLGizmoSlaBase::GLGizmoSlaBase(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id, SLAPrintObjectStep min_step) +: GLGizmoBase(parent, icon_filename, sprite_id) +, m_min_sla_print_object_step((int)min_step) +{} + +void GLGizmoSlaBase::reslice_until_step(SLAPrintObjectStep step, bool postpone_error_messages) +{ + wxGetApp().CallAfter([this, step, postpone_error_messages]() { + wxGetApp().plater()->reslice_SLA_until_step(step, *m_c->selection_info()->model_object(), postpone_error_messages); + }); +} + +CommonGizmosDataID GLGizmoSlaBase::on_get_requirements() const +{ + return CommonGizmosDataID( + int(CommonGizmosDataID::SelectionInfo) + | int(CommonGizmosDataID::InstancesHider) + | int(CommonGizmosDataID::Raycaster) + | int(CommonGizmosDataID::ObjectClipper)); +} + +void GLGizmoSlaBase::update_volumes() +{ + m_volumes.clear(); + unregister_volume_raycasters_for_picking(); + + const ModelObject* mo = m_c->selection_info()->model_object(); + if (mo == nullptr) + return; + + const SLAPrintObject* po = m_c->selection_info()->print_object(); + if (po == nullptr) + return; + + m_input_enabled = false; + + TriangleMesh backend_mesh; + std::shared_ptr preview_mesh_ptr = po->get_mesh_to_print(); + if (preview_mesh_ptr) + backend_mesh = TriangleMesh{*preview_mesh_ptr}; + + if (!backend_mesh.empty()) { + // The backend has generated a valid mesh. Use it + backend_mesh.transform(po->trafo().inverse()); + m_volumes.volumes.emplace_back(new GLVolume()); + GLVolume* new_volume = m_volumes.volumes.back(); + new_volume->model.init_from(backend_mesh); + new_volume->set_instance_transformation(po->model_object()->instances[m_parent.get_selection().get_instance_idx()]->get_transformation()); + new_volume->set_sla_shift_z(po->get_current_elevation()); + new_volume->mesh_raycaster = std::make_unique(backend_mesh); + m_input_enabled = last_completed_step(*m_c->selection_info()->print_object()->print()) >= m_min_sla_print_object_step; + if (m_input_enabled) + new_volume->selected = true; // to set the proper color + else + new_volume->set_color(DISABLED_COLOR); + } + + if (m_volumes.volumes.empty()) { + // No valid mesh found in the backend. Use the selection to duplicate the volumes + const Selection& selection = m_parent.get_selection(); + const Selection::IndicesList& idxs = selection.get_volume_idxs(); + for (unsigned int idx : idxs) { + const GLVolume* v = selection.get_volume(idx); + if (!v->is_modifier) { + m_volumes.volumes.emplace_back(new GLVolume()); + GLVolume* new_volume = m_volumes.volumes.back(); + const TriangleMesh& mesh = mo->volumes[v->volume_idx()]->mesh(); + new_volume->model.init_from(mesh); + new_volume->set_instance_transformation(v->get_instance_transformation()); + new_volume->set_volume_transformation(v->get_volume_transformation()); + new_volume->set_sla_shift_z(v->get_sla_shift_z()); + new_volume->set_color(DISABLED_COLOR); + new_volume->mesh_raycaster = std::make_unique(mesh); + } + } + } + + register_volume_raycasters_for_picking(); +} + +void GLGizmoSlaBase::render_volumes() +{ + GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light_clip"); + if (shader == nullptr) + return; + + shader->start_using(); + shader->set_uniform("emission_factor", 0.0f); + const Camera& camera = wxGetApp().plater()->get_camera(); + + ClippingPlane clipping_plane = (m_c->object_clipper()->get_position() == 0.0) ? ClippingPlane::ClipsNothing() : *m_c->object_clipper()->get_clipping_plane(); + clipping_plane.set_normal(-clipping_plane.get_normal()); + m_volumes.set_clipping_plane(clipping_plane.get_data()); + + m_volumes.render(GLVolumeCollection::ERenderType::Opaque, false, camera.get_view_matrix(), camera.get_projection_matrix()); + shader->stop_using(); + +} + +void GLGizmoSlaBase::register_volume_raycasters_for_picking() +{ + for (size_t i = 0; i < m_volumes.volumes.size(); ++i) { + const GLVolume* v = m_volumes.volumes[i]; + m_volume_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, VOLUME_RAYCASTERS_BASE_ID + (int)i, *v->mesh_raycaster, v->world_matrix())); + } +} + +void GLGizmoSlaBase::unregister_volume_raycasters_for_picking() +{ + for (size_t i = 0; i < m_volume_raycasters.size(); ++i) { + m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, VOLUME_RAYCASTERS_BASE_ID + (int)i); + } + m_volume_raycasters.clear(); +} + +int GLGizmoSlaBase::last_completed_step(const SLAPrint& sla) +{ + int step = -1; + for (int i = 0; i < (int)SLAPrintObjectStep::slaposCount; ++i) { + if (sla.is_step_done((SLAPrintObjectStep)i)) + ++step; + } + return step; +} + +// Unprojects the mouse position on the mesh and saves hit point and normal of the facet into pos_and_normal +// Return false if no intersection was found, true otherwise. +bool GLGizmoSlaBase::unproject_on_mesh(const Vec2d& mouse_pos, std::pair& pos_and_normal) +{ + if (m_c->raycaster()->raycasters().size() != 1) + return false; + if (!m_c->raycaster()->raycaster()) + return false; + if (m_volumes.volumes.empty()) + return false; + + auto *inst = m_c->selection_info()->model_instance(); + if (!inst) + return false; + + Transform3d trafo = m_volumes.volumes.front()->world_matrix(); + if (m_c->selection_info() && m_c->selection_info()->print_object()) { + double shift_z = m_c->selection_info()->print_object()->get_current_elevation(); + trafo = inst->get_transformation().get_matrix(); + trafo.translation()(2) += shift_z; + } + + // The raycaster query + Vec3f hit; + Vec3f normal; + if (m_c->raycaster()->raycaster()->unproject_on_mesh( + mouse_pos, + trafo/*m_volumes.volumes.front()->world_matrix()*/, + wxGetApp().plater()->get_camera(), + hit, + normal, + m_c->object_clipper()->get_position() != 0.0 ? m_c->object_clipper()->get_clipping_plane() : nullptr)) { + // Return both the point and the facet normal. + pos_and_normal = std::make_pair(hit, normal); + return true; + } + return false; +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.hpp new file mode 100644 index 000000000..9b5e9f469 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.hpp @@ -0,0 +1,57 @@ +#ifndef slic3r_GLGizmoSlaBase_hpp_ +#define slic3r_GLGizmoSlaBase_hpp_ + +#include "GLGizmoBase.hpp" +#include "slic3r/GUI/3DScene.hpp" +#include "slic3r/GUI/SceneRaycaster.hpp" +#include "libslic3r/SLAPrint.hpp" +#include "libslic3r/Point.hpp" + +#include +#include +#include + +namespace Slic3r { + +class SLAPrint; + +namespace GUI { + +class GLCanvas3D; + +class GLGizmoSlaBase : public GLGizmoBase +{ +public: + GLGizmoSlaBase(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id, SLAPrintObjectStep min_step); + + void reslice_until_step(SLAPrintObjectStep step, bool postpone_error_messages = false); + +protected: + virtual CommonGizmosDataID on_get_requirements() const override; + + void update_volumes(); + void render_volumes(); + + void register_volume_raycasters_for_picking(); + void unregister_volume_raycasters_for_picking(); + + bool is_input_enabled() const { return m_input_enabled; } + int get_min_sla_print_object_step() const { return m_min_sla_print_object_step; } + + static int last_completed_step(const SLAPrint& sla); + + bool unproject_on_mesh(const Vec2d& mouse_pos, std::pair& pos_and_normal); + + const GLVolumeCollection &volumes() const { return m_volumes; } + +private: + GLVolumeCollection m_volumes; + bool m_input_enabled{ false }; + int m_min_sla_print_object_step{ -1 }; + std::vector> m_volume_raycasters; +}; + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_GLGizmoSlaBase_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp index c22cdf606..e7ea945e3 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp @@ -1,8 +1,6 @@ +#include "libslic3r/libslic3r.h" // Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro. #include "GLGizmoSlaSupports.hpp" -#include "slic3r/GUI/GLCanvas3D.hpp" -#include "slic3r/GUI/Camera.hpp" -#include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp" #include "slic3r/GUI/MainFrame.hpp" #include "slic3r/Utils/UndoRedo.hpp" @@ -12,11 +10,9 @@ #include #include -#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI_ObjectSettings.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" -#include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/NotificationManager.hpp" #include "slic3r/GUI/MsgDialog.hpp" #include "libslic3r/PresetBundle.hpp" @@ -29,7 +25,7 @@ namespace Slic3r { namespace GUI { GLGizmoSlaSupports::GLGizmoSlaSupports(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) - : GLGizmoBase(parent, icon_filename, sprite_id) + : GLGizmoSlaBase(parent, icon_filename, sprite_id, slaposDrillHoles) {} bool GLGizmoSlaSupports::on_init() @@ -63,19 +59,28 @@ void GLGizmoSlaSupports::data_changed() disable_editing_mode(); reload_cache(); m_old_mo_id = mo->id(); - m_c->instances_hider()->show_supports(true); } // If we triggered autogeneration before, check backend and fetch results if they are there if (mo) { + m_c->instances_hider()->set_hide_full_scene(true); + const SLAPrintObject* po = m_c->selection_info()->print_object(); + const int required_step = get_min_sla_print_object_step(); + if (po != nullptr && last_completed_step(*po->print()) < required_step) + reslice_until_step((SLAPrintObjectStep)required_step, false); + + update_volumes(); + if (mo->sla_points_status == sla::PointsStatus::Generating) get_data_from_backend(); - if (m_raycasters.empty()) - on_register_raycasters_for_picking(); + if (m_point_raycasters.empty()) + register_point_raycasters_for_picking(); else - update_raycasters_for_picking_transform(); + update_point_raycasters_for_picking_transform(); } + +// m_parent.toggle_model_objects_visibility(false); } @@ -92,8 +97,6 @@ void GLGizmoSlaSupports::on_render() m_cone.model.init_from(its); m_cone.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); } - if (!m_cylinder.is_initialized()) - m_cylinder.init_from(its_make_cylinder(1.0, 1.0, double(PI) / 12.0)); ModelObject* mo = m_c->selection_info()->model_object(); const Selection& selection = m_parent.get_selection(); @@ -109,35 +112,25 @@ void GLGizmoSlaSupports::on_render() glsafe(::glEnable(GL_BLEND)); glsafe(::glEnable(GL_DEPTH_TEST)); - if (selection.is_from_single_instance()) - render_points(selection); + render_volumes(); + render_points(selection); m_selection_rectangle.render(m_parent); m_c->object_clipper()->render_cut(); - m_c->supports_clipper()->render_cut(); glsafe(::glDisable(GL_BLEND)); } void GLGizmoSlaSupports::on_register_raycasters_for_picking() { - assert(m_raycasters.empty()); - set_sla_auxiliary_volumes_picking_state(false); - - if (m_editing_mode && !m_editing_cache.empty()) { - for (size_t i = 0; i < m_editing_cache.size(); ++i) { - m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, i, *m_sphere.mesh_raycaster), - m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, i, *m_cone.mesh_raycaster)); - } - update_raycasters_for_picking_transform(); - } + register_point_raycasters_for_picking(); + register_volume_raycasters_for_picking(); } void GLGizmoSlaSupports::on_unregister_raycasters_for_picking() { - m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo); - m_raycasters.clear(); - set_sla_auxiliary_volumes_picking_state(true); + unregister_point_raycasters_for_picking(); + unregister_volume_raycasters_for_picking(); } void GLGizmoSlaSupports::render_points(const Selection& selection) @@ -145,10 +138,7 @@ void GLGizmoSlaSupports::render_points(const Selection& selection) const size_t cache_size = m_editing_mode ? m_editing_cache.size() : m_normal_cache.size(); const bool has_points = (cache_size != 0); - const bool has_holes = (! m_c->hollowed_mesh()->get_hollowed_mesh() - && ! m_c->selection_info()->model_object()->sla_drain_holes.empty()); - - if (! has_points && ! has_holes) + if (!has_points) return; GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); @@ -158,8 +148,15 @@ void GLGizmoSlaSupports::render_points(const Selection& selection) shader->start_using(); ScopeGuard guard([shader]() { shader->stop_using(); }); - const GLVolume* vol = selection.get_first_volume(); - const Geometry::Transformation transformation(vol->world_matrix()); + auto *inst = m_c->selection_info()->model_instance(); + if (!inst) + return; + + double shift_z = m_c->selection_info()->print_object()->get_current_elevation(); + Transform3d trafo = inst->get_transformation().get_matrix(); + trafo.translation()(2) += shift_z; + const Geometry::Transformation transformation{trafo}; + #if ENABLE_WORLD_COORDINATE const Transform3d instance_scaling_matrix_inverse = transformation.get_scaling_factor_matrix().inverse(); #else @@ -175,9 +172,9 @@ void GLGizmoSlaSupports::render_points(const Selection& selection) const bool point_selected = m_editing_mode ? m_editing_cache[i].selected : false; const bool clipped = is_mesh_point_clipped(support_point.pos.cast()); - if (!m_raycasters.empty()) { - m_raycasters[i].first->set_active(!clipped); - m_raycasters[i].second->set_active(!clipped); + if (i < m_point_raycasters.size()) { + m_point_raycasters[i].first->set_active(!clipped); + m_point_raycasters[i].second->set_active(!clipped); } if (clipped) continue; @@ -207,7 +204,7 @@ void GLGizmoSlaSupports::render_points(const Selection& selection) // Inverse matrix of the instance scaling is applied so that the mark does not scale with the object. const Transform3d support_matrix = Geometry::translation_transform(support_point.pos.cast()) * instance_scaling_matrix_inverse; - if (vol->is_left_handed()) + if (transformation.is_left_handed()) glsafe(::glFrontFace(GL_CW)); // Matrices set, we can render the point mark now. @@ -220,7 +217,7 @@ void GLGizmoSlaSupports::render_points(const Selection& selection) Eigen::Quaterniond q; q.setFromTwoVectors(Vec3d::UnitZ(), instance_scaling_matrix_inverse * m_editing_cache[i].normal.cast()); const Eigen::AngleAxisd aa(q); - const Transform3d model_matrix = vol->world_matrix() * support_matrix * Transform3d(aa.toRotationMatrix()) * + const Transform3d model_matrix = transformation.get_matrix() * support_matrix * Transform3d(aa.toRotationMatrix()) * Geometry::translation_transform((CONE_HEIGHT + support_point.head_front_radius * RenderPointScale) * Vec3d::UnitZ()) * Geometry::rotation_transform({ double(PI), 0.0, 0.0 }) * Geometry::scale_transform({ CONE_RADIUS, CONE_RADIUS, CONE_HEIGHT }); @@ -231,49 +228,17 @@ void GLGizmoSlaSupports::render_points(const Selection& selection) } const double radius = (double)support_point.head_front_radius * RenderPointScale; - const Transform3d model_matrix = vol->world_matrix() * support_matrix * Geometry::scale_transform(radius); + const Transform3d model_matrix = transformation.get_matrix() * support_matrix * Geometry::scale_transform(radius); shader->set_uniform("view_model_matrix", view_matrix * model_matrix); const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); shader->set_uniform("view_normal_matrix", view_normal_matrix); m_sphere.model.render(); - if (vol->is_left_handed()) + if (transformation.is_left_handed()) glsafe(::glFrontFace(GL_CCW)); } - - // Now render the drain holes: - if (has_holes) { - render_color = { 0.7f, 0.7f, 0.7f, 0.7f }; - m_cylinder.set_color(render_color); - shader->set_uniform("emission_factor", 0.5f); - for (const sla::DrainHole& drain_hole : m_c->selection_info()->model_object()->sla_drain_holes) { - if (is_mesh_point_clipped(drain_hole.pos.cast())) - continue; - - const Transform3d hole_matrix = Geometry::translation_transform(drain_hole.pos.cast()) * instance_scaling_matrix_inverse; - - if (vol->is_left_handed()) - glsafe(::glFrontFace(GL_CW)); - - // Matrices set, we can render the point mark now. - Eigen::Quaterniond q; - q.setFromTwoVectors(Vec3d::UnitZ(), instance_scaling_matrix_inverse * (-drain_hole.normal).cast()); - const Eigen::AngleAxisd aa(q); - const Transform3d model_matrix = vol->world_matrix() * hole_matrix * Transform3d(aa.toRotationMatrix()) * - Geometry::translation_transform(-drain_hole.height * Vec3d::UnitZ()) * Geometry::scale_transform(Vec3d(drain_hole.radius, drain_hole.radius, drain_hole.height + sla::HoleStickOutLength)); - shader->set_uniform("view_model_matrix", view_matrix * model_matrix); - const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); - shader->set_uniform("view_normal_matrix", view_normal_matrix); - m_cylinder.render(); - - if (vol->is_left_handed()) - glsafe(::glFrontFace(GL_CCW)); - } - } } - - bool GLGizmoSlaSupports::is_mesh_point_clipped(const Vec3d& point) const { if (m_c->object_clipper()->get_position() == 0.) @@ -289,59 +254,6 @@ bool GLGizmoSlaSupports::is_mesh_point_clipped(const Vec3d& point) const return m_c->object_clipper()->get_clipping_plane()->is_point_clipped(transformed_point); } - - -// Unprojects the mouse position on the mesh and saves hit point and normal of the facet into pos_and_normal -// Return false if no intersection was found, true otherwise. -bool GLGizmoSlaSupports::unproject_on_mesh(const Vec2d& mouse_pos, std::pair& pos_and_normal) -{ - if (! m_c->raycaster()->raycaster()) - return false; - - const Camera& camera = wxGetApp().plater()->get_camera(); - const Selection& selection = m_parent.get_selection(); - const GLVolume* volume = selection.get_first_volume(); - Geometry::Transformation trafo = volume->get_instance_transformation() * volume->get_volume_transformation(); - trafo.set_offset(trafo.get_offset() + Vec3d(0., 0., m_c->selection_info()->get_sla_shift())); - - double clp_dist = m_c->object_clipper()->get_position(); - const ClippingPlane* clp = m_c->object_clipper()->get_clipping_plane(); - - // The raycaster query - Vec3f hit; - Vec3f normal; - if (m_c->raycaster()->raycaster()->unproject_on_mesh( - mouse_pos, - trafo.get_matrix(), - camera, - hit, - normal, - clp_dist != 0. ? clp : nullptr)) - { - // Check whether the hit is in a hole - bool in_hole = false; - // In case the hollowed and drilled mesh is available, we can allow - // placing points in holes, because they should never end up - // on surface that's been drilled away. - if (! m_c->hollowed_mesh()->get_hollowed_mesh()) { - sla::DrainHoles drain_holes = m_c->selection_info()->model_object()->sla_drain_holes; - for (const sla::DrainHole& hole : drain_holes) { - if (hole.is_inside(hit)) { - in_hole = true; - break; - } - } - } - if (! in_hole) { - // Return both the point and the facet normal. - pos_and_normal = std::make_pair(hit, normal); - return true; - } - } - - return false; -} - // Following function is called from GLCanvas3D to inform the gizmo about a mouse/keyboard event. // The gizmo has an opportunity to react - if it does, it should return true so that the Canvas3D is // aware that the event was reacted to and stops trying to make different sense of it. If the gizmo @@ -386,8 +298,8 @@ bool GLGizmoSlaSupports::gizmo_event(SLAGizmoEventType action, const Vec2d& mous m_editing_cache.emplace_back(sla::SupportPoint(pos_and_normal.first, m_new_point_head_diameter/2.f, false), false, pos_and_normal.second); m_parent.set_as_dirty(); m_wait_for_up_event = true; - on_unregister_raycasters_for_picking(); - on_register_raycasters_for_picking(); + unregister_point_raycasters_for_picking(); + register_point_raycasters_for_picking(); } else return false; @@ -426,7 +338,7 @@ bool GLGizmoSlaSupports::gizmo_event(SLAGizmoEventType action, const Vec2d& mous points_inside.emplace_back((trafo.get_matrix().cast() * (m_editing_cache[idx].support_point.pos + m_editing_cache[idx].normal)).cast()); for (size_t idx : m_c->raycaster()->raycaster()->get_unobscured_idxs( - trafo, wxGetApp().plater()->get_camera(), points_inside, + trafo, wxGetApp().plater()->get_camera(), points_inside, m_c->object_clipper()->get_clipping_plane())) { if (idx >= orig_pts_num) // this is a cone-base, get index of point it belongs to @@ -543,9 +455,8 @@ void GLGizmoSlaSupports::delete_selected_points(bool force) } } - on_unregister_raycasters_for_picking(); - on_register_raycasters_for_picking(); - + unregister_point_raycasters_for_picking(); + register_point_raycasters_for_picking(); select_point(NoPoints); } @@ -644,8 +555,7 @@ RENDER_AGAIN: float win_h = ImGui::GetWindowHeight(); y = std::min(y, bottom_limit - win_h); ImGui::SetWindowPos(ImVec2(x, y), ImGuiCond_Always); - if ((last_h != win_h) || (last_y != y)) - { + if (last_h != win_h || last_y != y) { // ask canvas for another frame to render the window in the correct position m_imgui->set_requires_extra_frame(); if (last_h != win_h) @@ -676,6 +586,7 @@ RENDER_AGAIN: if (m_new_point_head_diameter > diameter_upper_cap) m_new_point_head_diameter = diameter_upper_cap; ImGui::AlignTextToFramePadding(); + m_imgui->text(m_desc.at("head_diameter")); ImGui::SameLine(diameter_slider_left); ImGui::PushItemWidth(window_width - diameter_slider_left); @@ -736,6 +647,8 @@ RENDER_AGAIN: } } else { // not in editing mode: + m_imgui->disabled_begin(!is_input_enabled()); + ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc.at("minimal_distance")); ImGui::SameLine(settings_sliders_left); @@ -785,7 +698,9 @@ RENDER_AGAIN: if (m_imgui->button(m_desc.at("manual_editing"))) switch_to_editing_mode(); - m_imgui->disabled_begin(m_normal_cache.empty()); + m_imgui->disabled_end(); + + m_imgui->disabled_begin(!is_input_enabled() || m_normal_cache.empty()); remove_all = m_imgui->button(m_desc.at("remove_all")); m_imgui->disabled_end(); @@ -798,6 +713,7 @@ RENDER_AGAIN: // Following is rendered in both editing and non-editing mode: + m_imgui->disabled_begin(!is_input_enabled()); ImGui::Separator(); if (m_c->object_clipper()->get_position() == 0.f) { ImGui::AlignTextToFramePadding(); @@ -817,7 +733,6 @@ RENDER_AGAIN: if (m_imgui->slider_float("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f")) m_c->object_clipper()->set_position_by_ratio(clp_dist, true); - if (m_imgui->button("?")) { wxGetApp().CallAfter([]() { SlaGizmoHelpDialog help_dlg; @@ -825,6 +740,8 @@ RENDER_AGAIN: }); } + m_imgui->disabled_end(); + m_imgui->end(); if (remove_selected || remove_all) { @@ -857,7 +774,7 @@ bool GLGizmoSlaSupports::on_is_activable() const const Selection& selection = m_parent.get_selection(); if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA - || !selection.is_from_single_instance()) + || !selection.is_single_full_instance()) return false; // Check that none of the selected volumes is outside. Only SLA auxiliaries (supports) are allowed outside. @@ -879,19 +796,6 @@ std::string GLGizmoSlaSupports::on_get_name() const return _u8L("SLA Support Points"); } -CommonGizmosDataID GLGizmoSlaSupports::on_get_requirements() const -{ - return CommonGizmosDataID( - int(CommonGizmosDataID::SelectionInfo) - | int(CommonGizmosDataID::InstancesHider) - | int(CommonGizmosDataID::Raycaster) - | int(CommonGizmosDataID::HollowedMesh) - | int(CommonGizmosDataID::ObjectClipper) - | int(CommonGizmosDataID::SupportsClipper)); -} - - - void GLGizmoSlaSupports::ask_about_changes_call_after(std::function on_yes, std::function on_no) { wxGetApp().CallAfter([on_yes, on_no]() { @@ -931,6 +835,9 @@ void GLGizmoSlaSupports::on_set_state() disable_editing_mode(); // so it is not active next time the gizmo opens m_old_mo_id = -1; } + + if (m_state == Off) + m_c->instances_hider()->set_hide_full_scene(false); } m_old_state = m_state; } @@ -1076,7 +983,7 @@ void GLGizmoSlaSupports::editing_mode_apply_changes() mo->sla_support_points.clear(); mo->sla_support_points = m_normal_cache; - reslice_SLA_supports(); + reslice_until_step(slaposPad); } } @@ -1108,15 +1015,9 @@ bool GLGizmoSlaSupports::has_backend_supports() const return false; } -void GLGizmoSlaSupports::reslice_SLA_supports(bool postpone_error_messages) const +bool GLGizmoSlaSupports::on_mouse(const wxMouseEvent &mouse_event) { - wxGetApp().CallAfter([this, postpone_error_messages]() { - wxGetApp().plater()->reslice_SLA_supports( - *m_c->selection_info()->model_object(), postpone_error_messages); - }); -} - -bool GLGizmoSlaSupports::on_mouse(const wxMouseEvent &mouse_event){ + if (!is_input_enabled()) return true; if (mouse_event.Moving()) return false; if (!mouse_event.ShiftDown() && !mouse_event.AltDown() && use_grabbers(mouse_event)) return true; @@ -1183,7 +1084,7 @@ void GLGizmoSlaSupports::get_data_from_backend() if (po->model_object()->id() == mo->id()) { m_normal_cache.clear(); const std::vector& points = po->get_support_points(); - auto mat = (po->trafo() * po->model_object()->volumes.front()->get_transformation().get_matrix()).inverse().cast(); + auto mat = po->trafo().inverse().cast(); for (unsigned int i=0; isla_points_status != sla::PointsStatus::UserModified || m_normal_cache.empty() || dlg.ShowModal() == wxID_YES) { Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Autogenerate support points")); - wxGetApp().CallAfter([this]() { reslice_SLA_supports(); }); + wxGetApp().CallAfter([this]() { reslice_until_step(slaposPad); }); mo->sla_points_status = sla::PointsStatus::Generating; } } @@ -1224,9 +1125,7 @@ void GLGizmoSlaSupports::switch_to_editing_mode() for (const sla::SupportPoint& sp : m_normal_cache) m_editing_cache.emplace_back(sp); select_point(NoPoints); - on_register_raycasters_for_picking(); - - m_c->instances_hider()->show_supports(false); + register_point_raycasters_for_picking(); m_parent.set_as_dirty(); } @@ -1236,9 +1135,8 @@ void GLGizmoSlaSupports::disable_editing_mode() if (m_editing_mode) { m_editing_mode = false; wxGetApp().plater()->leave_gizmos_stack(); - m_c->instances_hider()->show_supports(true); m_parent.set_as_dirty(); - on_unregister_raycasters_for_picking(); + unregister_point_raycasters_for_picking(); } wxGetApp().plater()->get_notification_manager()->close_notification_of_type(NotificationType::QuitSLAManualMode); } @@ -1257,49 +1155,63 @@ bool GLGizmoSlaSupports::unsaved_changes() const return false; } -void GLGizmoSlaSupports::set_sla_auxiliary_volumes_picking_state(bool state) +void GLGizmoSlaSupports::register_point_raycasters_for_picking() { - std::vector>* raycasters = m_parent.get_raycasters_for_picking(SceneRaycaster::EType::Volume); - if (raycasters != nullptr) { - const Selection& selection = m_parent.get_selection(); - const Selection::IndicesList ids = selection.get_volume_idxs(); - for (unsigned int id : ids) { - const GLVolume* v = selection.get_volume(id); - if (v->is_sla_pad() || v->is_sla_support()) { - auto it = std::find_if(raycasters->begin(), raycasters->end(), [v](std::shared_ptr item) { return item->get_raycaster() == v->mesh_raycaster.get(); }); - if (it != raycasters->end()) - (*it)->set_active(state); - } + assert(m_point_raycasters.empty()); + + if (m_editing_mode && !m_editing_cache.empty()) { + for (size_t i = 0; i < m_editing_cache.size(); ++i) { + m_point_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, i, *m_sphere.mesh_raycaster, Transform3d::Identity()), + m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, i, *m_cone.mesh_raycaster, Transform3d::Identity())); } + update_point_raycasters_for_picking_transform(); } } -void GLGizmoSlaSupports::update_raycasters_for_picking_transform() +void GLGizmoSlaSupports::unregister_point_raycasters_for_picking() { - if (!m_editing_cache.empty()) { - assert(!m_raycasters.empty()); + for (size_t i = 0; i < m_point_raycasters.size(); ++i) { + m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, i); + } + m_point_raycasters.clear(); +} - const GLVolume* vol = m_parent.get_selection().get_first_volume(); - const Geometry::Transformation transformation(vol->world_matrix()); - const Transform3d instance_scaling_matrix_inverse = transformation.get_scaling_factor_matrix().inverse(); - for (size_t i = 0; i < m_editing_cache.size(); ++i) { - const Transform3d support_matrix = Geometry::translation_transform(m_editing_cache[i].support_point.pos.cast()) * instance_scaling_matrix_inverse; +void GLGizmoSlaSupports::update_point_raycasters_for_picking_transform() +{ + if (m_editing_cache.empty()) + return; - if (m_editing_cache[i].normal == Vec3f::Zero()) - m_c->raycaster()->raycaster()->get_closest_point(m_editing_cache[i].support_point.pos, &m_editing_cache[i].normal); + assert(!m_point_raycasters.empty()); - Eigen::Quaterniond q; - q.setFromTwoVectors(Vec3d::UnitZ(), instance_scaling_matrix_inverse * m_editing_cache[i].normal.cast()); - const Eigen::AngleAxisd aa(q); - const Transform3d cone_matrix = vol->world_matrix() * support_matrix * Transform3d(aa.toRotationMatrix()) * - Geometry::translation_transform((CONE_HEIGHT + m_editing_cache[i].support_point.head_front_radius * RenderPointScale) * Vec3d::UnitZ()) * - Geometry::rotation_transform({ double(PI), 0.0, 0.0 }) * Geometry::scale_transform({ CONE_RADIUS, CONE_RADIUS, CONE_HEIGHT }); - m_raycasters[i].second->set_transform(cone_matrix); + const GLVolume* vol = m_parent.get_selection().get_first_volume(); + Geometry::Transformation transformation(vol->world_matrix()); - const double radius = (double)m_editing_cache[i].support_point.head_front_radius * RenderPointScale; - const Transform3d sphere_matrix = vol->world_matrix() * support_matrix * Geometry::scale_transform(radius); - m_raycasters[i].first->set_transform(sphere_matrix); - } + auto *inst = m_c->selection_info()->model_instance(); + if (inst && m_c->selection_info() && m_c->selection_info()->print_object()) { + double shift_z = m_c->selection_info()->print_object()->get_current_elevation(); + auto trafo = inst->get_transformation().get_matrix(); + trafo.translation()(2) += shift_z; + transformation.set_matrix(trafo); + } + + const Transform3d instance_scaling_matrix_inverse = transformation.get_scaling_factor_matrix().inverse(); + for (size_t i = 0; i < m_editing_cache.size(); ++i) { + const Transform3d support_matrix = Geometry::translation_transform(m_editing_cache[i].support_point.pos.cast()) * instance_scaling_matrix_inverse; + + if (m_editing_cache[i].normal == Vec3f::Zero()) + m_c->raycaster()->raycaster()->get_closest_point(m_editing_cache[i].support_point.pos, &m_editing_cache[i].normal); + + Eigen::Quaterniond q; + q.setFromTwoVectors(Vec3d::UnitZ(), instance_scaling_matrix_inverse * m_editing_cache[i].normal.cast()); + const Eigen::AngleAxisd aa(q); + const Transform3d cone_matrix = transformation.get_matrix() * support_matrix * Transform3d(aa.toRotationMatrix()) * + Geometry::assemble_transform((CONE_HEIGHT + m_editing_cache[i].support_point.head_front_radius * RenderPointScale) * Vec3d::UnitZ(), + Vec3d(PI, 0.0, 0.0), Vec3d(CONE_RADIUS, CONE_RADIUS, CONE_HEIGHT)); + m_point_raycasters[i].second->set_transform(cone_matrix); + + const double radius = (double)m_editing_cache[i].support_point.head_front_radius * RenderPointScale; + const Transform3d sphere_matrix = transformation.get_matrix() * support_matrix * Geometry::scale_transform(radius); + m_point_raycasters[i].first->set_transform(sphere_matrix); } } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp index e8238c888..578858b5b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp @@ -1,7 +1,7 @@ #ifndef slic3r_GLGizmoSlaSupports_hpp_ #define slic3r_GLGizmoSlaSupports_hpp_ -#include "GLGizmoBase.hpp" +#include "GLGizmoSlaBase.hpp" #include "slic3r/GUI/GLSelectionRectangle.hpp" #include "libslic3r/SLA/SupportPoint.hpp" @@ -19,13 +19,10 @@ namespace GUI { class Selection; enum class SLAGizmoEventType : unsigned char; -class GLGizmoSlaSupports : public GLGizmoBase +class GLGizmoSlaSupports : public GLGizmoSlaBase { private: - - bool unproject_on_mesh(const Vec2d& mouse_pos, std::pair& pos_and_normal); - - const float RenderPointScale = 1.f; + static constexpr float RenderPointScale = 1.f; class CacheEntry { public: @@ -65,7 +62,6 @@ public: bool is_in_editing_mode() const override { return m_editing_mode; } bool is_selection_rectangle_dragging() const override { return m_selection_rectangle.is_dragging(); } bool has_backend_supports() const; - void reslice_SLA_supports(bool postpone_error_messages = false) const; bool wants_enter_leave_snapshots() const override { return true; } std::string get_gizmo_entering_text() const override { return _u8L("Entering SLA support points"); } @@ -86,8 +82,9 @@ private: void render_points(const Selection& selection); bool unsaved_changes() const; - void set_sla_auxiliary_volumes_picking_state(bool state); - void update_raycasters_for_picking_transform(); + void register_point_raycasters_for_picking(); + void unregister_point_raycasters_for_picking(); + void update_point_raycasters_for_picking_transform(); bool m_lock_unique_islands = false; bool m_editing_mode = false; // Is editing mode active? @@ -102,8 +99,7 @@ private: PickingModel m_sphere; PickingModel m_cone; - std::vector, std::shared_ptr>> m_raycasters; - GLModel m_cylinder; + std::vector, std::shared_ptr>> m_point_raycasters; // This map holds all translated description texts, so they can be easily referenced during layout calculations // etc. When language changes, GUI is recreated and this class constructed again, so the change takes effect. @@ -138,8 +134,7 @@ private: protected: void on_set_state() override; - void on_set_hover_id() override - { + void on_set_hover_id() override { if (! m_editing_mode || (int)m_editing_cache.size() <= m_hover_id) m_hover_id = -1; } @@ -151,7 +146,6 @@ protected: std::string on_get_name() const override; bool on_is_activable() const override; bool on_is_selectable() const override; - virtual CommonGizmosDataID on_get_requirements() const override; void on_load(cereal::BinaryInputArchive& ar) override; void on_save(cereal::BinaryOutputArchive& ar) const override; }; diff --git a/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp b/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp index 0e1d5ae0c..a8dc16c55 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp @@ -23,7 +23,7 @@ CommonGizmosDataPool::CommonGizmosDataPool(GLCanvas3D* canvas) using c = CommonGizmosDataID; m_data[c::SelectionInfo].reset( new SelectionInfo(this)); m_data[c::InstancesHider].reset( new InstancesHider(this)); - m_data[c::HollowedMesh].reset( new HollowedMesh(this)); +// m_data[c::HollowedMesh].reset( new HollowedMesh(this)); m_data[c::Raycaster].reset( new Raycaster(this)); m_data[c::ObjectClipper].reset( new ObjectClipper(this)); m_data[c::SupportsClipper].reset( new SupportsClipper(this)); @@ -59,13 +59,6 @@ InstancesHider* CommonGizmosDataPool::instances_hider() const return inst_hider->is_valid() ? inst_hider : nullptr; } -HollowedMesh* CommonGizmosDataPool::hollowed_mesh() const -{ - HollowedMesh* hol_mesh = dynamic_cast(m_data.at(CommonGizmosDataID::HollowedMesh).get()); - assert(hol_mesh); - return hol_mesh->is_valid() ? hol_mesh : nullptr; -} - Raycaster* CommonGizmosDataPool::raycaster() const { Raycaster* rc = dynamic_cast(m_data.at(CommonGizmosDataID::Raycaster).get()); @@ -117,15 +110,16 @@ bool CommonGizmosDataPool::check_dependencies(CommonGizmosDataID required) const void SelectionInfo::on_update() { const Selection& selection = get_pool()->get_canvas()->get_selection(); + + m_model_object = nullptr; + m_print_object = nullptr; + if (selection.is_single_full_instance()) { m_model_object = selection.get_model()->objects[selection.get_object_idx()]; - m_model_volume = nullptr; - m_z_shift = selection.get_first_volume()->get_sla_shift_z(); - } - else { - m_model_object = nullptr; - if (selection.is_single_volume()) - m_model_volume = selection.get_model()->objects[selection.get_object_idx()]->volumes[selection.get_first_volume()->volume_idx()]; + if (m_model_object) + m_print_object = get_pool()->get_canvas()->sla_print()->get_print_object_by_model_object_id(m_model_object->id()); + + m_z_shift = m_print_object ? m_print_object->get_current_elevation() : selection.get_first_volume()->get_sla_shift_z(); } } @@ -135,6 +129,13 @@ void SelectionInfo::on_release() m_model_volume = nullptr; } +ModelInstance *SelectionInfo::model_instance() const +{ + int inst_idx = get_active_instance(); + return inst_idx < int(m_model_object->instances.size()) ? + m_model_object->instances[get_active_instance()] : nullptr; +} + int SelectionInfo::get_active_instance() const { return get_pool()->get_canvas()->get_selection().get_instance_idx(); @@ -152,8 +153,10 @@ void InstancesHider::on_update() if (mo && active_inst != -1) { canvas->toggle_model_objects_visibility(false); - canvas->toggle_model_objects_visibility(true, mo, active_inst); - canvas->toggle_sla_auxiliaries_visibility(m_show_supports, mo, active_inst); + if (!m_hide_full_scene) { + canvas->toggle_model_objects_visibility(true, mo, active_inst); + canvas->toggle_sla_auxiliaries_visibility(false, mo, active_inst); + } canvas->set_use_clipping_planes(true); // Some objects may be sinking, do not show whatever is below the bed. canvas->set_clipping_plane(0, ClippingPlane(Vec3d::UnitZ(), -SINKING_Z_THRESHOLD)); @@ -169,7 +172,7 @@ void InstancesHider::on_update() for (const TriangleMesh* mesh : meshes) { m_clippers.emplace_back(new MeshClipper); m_clippers.back()->set_plane(ClippingPlane(-Vec3d::UnitZ(), -SINKING_Z_THRESHOLD)); - m_clippers.back()->set_mesh(*mesh); + m_clippers.back()->set_mesh(mesh->its); } m_old_meshes = meshes; } @@ -186,9 +189,10 @@ void InstancesHider::on_release() m_clippers.clear(); } -void InstancesHider::show_supports(bool show) { - if (m_show_supports != show) { - m_show_supports = show; +void InstancesHider::set_hide_full_scene(bool hide) +{ + if (m_hide_full_scene != hide) { + m_hide_full_scene = hide; on_update(); } } @@ -236,81 +240,6 @@ void InstancesHider::render_cut() const } - -void HollowedMesh::on_update() -{ - const ModelObject* mo = get_pool()->selection_info()->model_object(); - bool is_sla = wxGetApp().preset_bundle->printers.get_selected_preset().printer_technology() == ptSLA; - if (! mo || ! is_sla) - return; - - const GLCanvas3D* canvas = get_pool()->get_canvas(); - const PrintObjects& print_objects = canvas->sla_print()->objects(); - const SLAPrintObject* print_object = (m_print_object_idx >= 0 && m_print_object_idx < int(print_objects.size())) - ? print_objects[m_print_object_idx] - : nullptr; - - // Find the respective SLAPrintObject. - if (m_print_object_idx < 0 || m_print_objects_count != int(print_objects.size())) { - m_print_objects_count = print_objects.size(); - m_print_object_idx = -1; - for (const SLAPrintObject* po : print_objects) { - ++m_print_object_idx; - if (po->model_object()->id() == mo->id()) { - print_object = po; - break; - } - } - } - - // If there is a valid SLAPrintObject, check state of Hollowing step. - if (print_object) { - if (print_object->is_step_done(slaposDrillHoles) && print_object->has_mesh(slaposDrillHoles)) { - size_t timestamp = print_object->step_state_with_timestamp(slaposDrillHoles).timestamp; - if (timestamp > m_old_hollowing_timestamp) { - const TriangleMesh& backend_mesh = print_object->get_mesh_to_slice(); - if (! backend_mesh.empty()) { - m_hollowed_mesh_transformed.reset(new TriangleMesh(backend_mesh)); - Transform3d trafo_inv = (canvas->sla_print()->sla_trafo(*mo) * print_object->model_object()->volumes.front()->get_transformation().get_matrix()).inverse(); - m_hollowed_mesh_transformed->transform(trafo_inv); - m_drainholes = print_object->model_object()->sla_drain_holes; - m_old_hollowing_timestamp = timestamp; - - indexed_triangle_set interior = print_object->hollowed_interior_mesh(); - its_flip_triangles(interior); - m_hollowed_interior_transformed = std::make_unique(std::move(interior)); - m_hollowed_interior_transformed->transform(trafo_inv); - } - else { - m_hollowed_mesh_transformed.reset(nullptr); - } - } - } - else - m_hollowed_mesh_transformed.reset(nullptr); - } -} - - -void HollowedMesh::on_release() -{ - m_hollowed_mesh_transformed.reset(); - m_old_hollowing_timestamp = 0; - m_print_object_idx = -1; -} - - -const TriangleMesh* HollowedMesh::get_hollowed_mesh() const -{ - return m_hollowed_mesh_transformed.get(); -} - -const TriangleMesh* HollowedMesh::get_hollowed_interior() const -{ - return m_hollowed_interior_transformed.get(); -} - - void Raycaster::on_update() { wxBusyCursor wait; @@ -327,20 +256,33 @@ void Raycaster::on_update() mvs = mo->volumes; std::vector meshes; - if (mvs.size() == 1) { - assert(mvs.front()->is_model_part()); - const HollowedMesh* hollowed_mesh_tracker = get_pool()->hollowed_mesh(); - if (hollowed_mesh_tracker && hollowed_mesh_tracker->get_hollowed_mesh()) - meshes.push_back(hollowed_mesh_tracker->get_hollowed_mesh()); - } - if (meshes.empty()) { - for (const ModelVolume* v : mvs) { - if (v->is_model_part()) - meshes.push_back(&v->mesh()); + bool force_raycaster_regeneration = false; + if (wxGetApp().preset_bundle->printers.get_selected_preset().printer_technology() == ptSLA) { + // For sla printers we use the mesh generated by the backend + std::shared_ptr preview_mesh_ptr; + const SLAPrintObject* po = get_pool()->selection_info()->print_object(); + if (po) + preview_mesh_ptr = po->get_mesh_to_print(); + + if (preview_mesh_ptr) + m_sla_mesh_cache = TriangleMesh{*preview_mesh_ptr}; + + if (!m_sla_mesh_cache.empty()) { + m_sla_mesh_cache.transform(po->trafo().inverse()); + meshes.emplace_back(&m_sla_mesh_cache); + force_raycaster_regeneration = true; } } - if (meshes != m_old_meshes) { + if (meshes.empty()) { + const std::vector& mvs = mo->volumes; + for (const ModelVolume* mv : mvs) { + if (mv->is_model_part()) + meshes.push_back(&mv->mesh()); + } + } + + if (force_raycaster_regeneration || meshes != m_old_meshes) { m_raycasters.clear(); for (const TriangleMesh* mesh : meshes) m_raycasters.emplace_back(new MeshRaycaster(std::make_shared(*mesh))); @@ -362,8 +304,9 @@ std::vector Raycaster::raycasters() const return mrcs; } +} // namespace GUI - +namespace GUI { void ObjectClipper::on_update() @@ -374,24 +317,42 @@ void ObjectClipper::on_update() // which mesh should be cut? std::vector meshes; - bool has_hollowed = get_pool()->hollowed_mesh() && get_pool()->hollowed_mesh()->get_hollowed_mesh(); - if (has_hollowed) - meshes.push_back(get_pool()->hollowed_mesh()->get_hollowed_mesh()); + std::vector trafos; + bool force_clipper_regeneration = false; - if (meshes.empty()) - for (const ModelVolume* mv : mo->volumes) - meshes.push_back(&mv->mesh()); - - if (meshes != m_old_meshes) { - m_clippers.clear(); - for (const TriangleMesh* mesh : meshes) { - m_clippers.emplace_back(new MeshClipper); - m_clippers.back()->set_mesh(*mesh); + std::unique_ptr mc; + Geometry::Transformation mc_tr; + if (wxGetApp().preset_bundle->printers.get_selected_preset().printer_technology() == ptSLA) { + // For sla printers we use the mesh generated by the backend + const SLAPrintObject* po = get_pool()->selection_info()->print_object(); + if (po) { + auto partstoslice = po->get_parts_to_slice(); + if (! partstoslice.empty()) { + mc = std::make_unique(); + mc->set_mesh(range(partstoslice)); + mc_tr = Geometry::Transformation{po->trafo().inverse().cast()}; + } } - m_old_meshes = meshes; + } - if (has_hollowed) - m_clippers.front()->set_negative_mesh(*get_pool()->hollowed_mesh()->get_hollowed_interior()); + if (!mc && meshes.empty()) { + for (const ModelVolume* mv : mo->volumes) { + meshes.emplace_back(&mv->mesh()); + trafos.emplace_back(mv->get_transformation()); + } + } + + if (mc || force_clipper_regeneration || meshes != m_old_meshes) { + m_clippers.clear(); + for (size_t i = 0; i < meshes.size(); ++i) { + m_clippers.emplace_back(new MeshClipper, trafos[i]); + m_clippers.back().first->set_mesh(meshes[i]->its); + } + m_old_meshes = std::move(meshes); + + if (mc) { + m_clippers.emplace_back(std::move(mc), mc_tr); + } m_active_inst_bb_radius = mo->instance_bounding_box(get_pool()->selection_info()->get_active_instance()).radius(); @@ -413,37 +374,27 @@ void ObjectClipper::render_cut() const if (m_clp_ratio == 0.) return; const SelectionInfo* sel_info = get_pool()->selection_info(); - int sel_instance_idx = sel_info->get_active_instance(); - if (sel_instance_idx < 0) - return; - const ModelObject* mo = sel_info->model_object(); - const Geometry::Transformation inst_trafo = mo->instances[sel_instance_idx]->get_transformation(); + const Geometry::Transformation inst_trafo = sel_info->model_object()->instances[sel_info->get_active_instance()]->get_transformation(); - size_t clipper_id = 0; - for (const ModelVolume* mv : mo->volumes) { - const Geometry::Transformation vol_trafo = mv->get_transformation(); - Geometry::Transformation trafo = inst_trafo * vol_trafo; - trafo.set_offset(trafo.get_offset() + Vec3d(0., 0., sel_info->get_sla_shift())); - - auto& clipper = m_clippers[clipper_id]; - clipper->set_plane(*m_clp); - clipper->set_transformation(trafo); - clipper->set_limiting_plane(ClippingPlane(Vec3d::UnitZ(), -SINKING_Z_THRESHOLD)); - clipper->render_cut({ 1.0f, 0.37f, 0.0f, 1.0f }); - clipper->render_contour({ 1.f, 1.f, 1.f, 1.f}); - - ++clipper_id; + for (auto& clipper : m_clippers) { + Geometry::Transformation trafo = inst_trafo * clipper.second; + trafo.set_offset(trafo.get_offset() + Vec3d(0., 0., sel_info->get_sla_shift())); + clipper.first->set_plane(*m_clp); + clipper.first->set_transformation(trafo); + clipper.first->set_limiting_plane(ClippingPlane(Vec3d::UnitZ(), -SINKING_Z_THRESHOLD)); + clipper.first->render_cut({ 1.0f, 0.37f, 0.0f, 1.0f }); + clipper.first->render_contour({ 1.f, 1.f, 1.f, 1.f }); } } bool ObjectClipper::is_projection_inside_cut(const Vec3d& point) const { - return m_clp_ratio != 0. && std::any_of(m_clippers.begin(), m_clippers.end(), [point](const std::unique_ptr& cl) { return cl->is_projection_inside_cut(point); }); + return m_clp_ratio != 0. && std::any_of(m_clippers.begin(), m_clippers.end(), [point](const auto& cl) { return cl.first->is_projection_inside_cut(point); }); } bool ObjectClipper::has_valid_contour() const { - return m_clp_ratio != 0. && std::any_of(m_clippers.begin(), m_clippers.end(), [](const std::unique_ptr& cl) { return cl->has_valid_contour(); }); + return m_clp_ratio != 0. && std::any_of(m_clippers.begin(), m_clippers.end(), [](const auto& cl) { return cl.first->has_valid_contour(); }); } void ObjectClipper::set_position_by_ratio(double pos, bool keep_normal) @@ -481,13 +432,13 @@ void ObjectClipper::set_behavior(bool hide_clipped, bool fill_cut, double contou { m_hide_clipped = hide_clipped; for (auto& clipper : m_clippers) - clipper->set_behaviour(fill_cut, contour_width); + clipper.first->set_behaviour(fill_cut, contour_width); } void ObjectClipper::pass_mouse_click(const Vec3d& pt) { for (auto& clipper : m_clippers) - clipper->pass_mouse_click(pt); + clipper.first->pass_mouse_click(pt); } std::vector ObjectClipper::get_disabled_contours() const @@ -532,7 +483,7 @@ void SupportsClipper::on_update() // The timestamp has changed. m_clipper.reset(new MeshClipper); // The mesh should already have the shared vertices calculated. - m_clipper->set_mesh(print_object->support_mesh()); + m_clipper->set_mesh(print_object->support_mesh().its); m_old_timestamp = timestamp; } } @@ -553,7 +504,6 @@ void SupportsClipper::render_cut() const { const CommonGizmosDataObjects::ObjectClipper* ocl = get_pool()->object_clipper(); if (ocl->get_position() == 0. - || ! get_pool()->instances_hider()->are_supports_shown() || ! m_clipper) return; diff --git a/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp b/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp index c1b605726..21e1bb73c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp @@ -5,11 +5,12 @@ #include #include "slic3r/GUI/MeshUtils.hpp" -#include "libslic3r/SLA/Hollowing.hpp" namespace Slic3r { class ModelObject; +class ModelInstance; +class SLAPrintObject; class ModelVolume; namespace GUI { @@ -64,7 +65,6 @@ enum class CommonGizmosDataID { None = 0, SelectionInfo = 1 << 0, InstancesHider = 1 << 1, - HollowedMesh = 1 << 2, Raycaster = 1 << 3, ObjectClipper = 1 << 4, SupportsClipper = 1 << 5, @@ -86,7 +86,7 @@ public: // Getters for the data that need to be accessed from the gizmos directly. CommonGizmosDataObjects::SelectionInfo* selection_info() const; CommonGizmosDataObjects::InstancesHider* instances_hider() const; - CommonGizmosDataObjects::HollowedMesh* hollowed_mesh() const; +// CommonGizmosDataObjects::HollowedMesh* hollowed_mesh() const; CommonGizmosDataObjects::Raycaster* raycaster() const; CommonGizmosDataObjects::ObjectClipper* object_clipper() const; CommonGizmosDataObjects::SupportsClipper* supports_clipper() const; @@ -158,8 +158,10 @@ public: // Returns a non-null pointer if the selection is a single full instance ModelObject* model_object() const { return m_model_object; } + const SLAPrintObject *print_object() const { return m_print_object; } // Returns a non-null pointer if the selection is a single volume ModelVolume* model_volume() const { return m_model_volume; } + ModelInstance *model_instance() const; int get_active_instance() const; float get_sla_shift() const { return m_z_shift; } @@ -169,6 +171,7 @@ protected: private: ModelObject* m_model_object = nullptr; + const SLAPrintObject *m_print_object = nullptr; ModelVolume* m_model_volume = nullptr; // int m_active_inst = -1; float m_z_shift = 0.f; @@ -185,8 +188,7 @@ public: CommonGizmosDataID get_dependencies() const override { return CommonGizmosDataID::SelectionInfo; } #endif // NDEBUG - void show_supports(bool show); - bool are_supports_shown() const { return m_show_supports; } + void set_hide_full_scene(bool hide); void render_cut() const; protected: @@ -194,42 +196,13 @@ protected: void on_release() override; private: - bool m_show_supports = false; + bool m_hide_full_scene{ false }; std::vector m_old_meshes; std::vector> m_clippers; }; -class HollowedMesh : public CommonGizmosDataBase -{ -public: - explicit HollowedMesh(CommonGizmosDataPool* cgdp) - : CommonGizmosDataBase(cgdp) {} -#ifndef NDEBUG - CommonGizmosDataID get_dependencies() const override { return CommonGizmosDataID::SelectionInfo; } -#endif // NDEBUG - - const sla::DrainHoles &get_drainholes() const { return m_drainholes; } - - const TriangleMesh* get_hollowed_mesh() const; - const TriangleMesh* get_hollowed_interior() const; - -protected: - void on_update() override; - void on_release() override; - -private: - std::unique_ptr m_hollowed_mesh_transformed; - std::unique_ptr m_hollowed_interior_transformed; - size_t m_old_hollowing_timestamp = 0; - int m_print_object_idx = -1; - int m_print_objects_count = 0; - sla::DrainHoles m_drainholes; -}; - - - class Raycaster : public CommonGizmosDataBase { public: @@ -249,6 +222,8 @@ protected: private: std::vector> m_raycasters; std::vector m_old_meshes; + // Used to store the sla mesh coming from the backend + TriangleMesh m_sla_mesh_cache; }; @@ -283,7 +258,9 @@ protected: private: std::vector m_old_meshes; - std::vector> m_clippers; + // Used to store the sla mesh coming from the backend + TriangleMesh m_sla_mesh_cache; + std::vector, Geometry::Transformation>> m_clippers; std::unique_ptr m_clp; double m_clp_ratio = 0.; double m_active_inst_bb_radius = 0.; diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 0a229caf9..001074a2c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -657,7 +657,7 @@ void GLGizmosManager::update_after_undo_redo(const UndoRedo::Snapshot& snapshot) m_serializing = false; if (m_current == SlaSupports && snapshot.snapshot_data.flags & UndoRedo::SnapshotData::RECALCULATE_SLA_SUPPORTS) - dynamic_cast(m_gizmos[SlaSupports].get())->reslice_SLA_supports(true); + dynamic_cast(m_gizmos[SlaSupports].get())->reslice_until_step(slaposPad, true); } void GLGizmosManager::render_background(float left, float top, float right, float bottom, float border_w, float border_h) const diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 93baac448..8a4bee03e 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -943,7 +943,7 @@ bool MainFrame::can_export_supports() const const PrintObjects& objects = m_plater->sla_print().objects(); for (const SLAPrintObject* object : objects) { - if (object->has_mesh(slaposPad) || object->has_mesh(slaposSupportTree)) + if (!object->support_mesh().empty()) { can_export = true; break; diff --git a/src/slic3r/GUI/MeshUtils.cpp b/src/slic3r/GUI/MeshUtils.cpp index 428a83a64..bb93afc8d 100644 --- a/src/slic3r/GUI/MeshUtils.cpp +++ b/src/slic3r/GUI/MeshUtils.cpp @@ -5,12 +5,14 @@ #include "libslic3r/TriangleMeshSlicer.hpp" #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/CSGMesh/SliceCSGMesh.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/Camera.hpp" #include "slic3r/GUI/CameraUtils.hpp" + #include #include @@ -50,22 +52,38 @@ void MeshClipper::set_limiting_plane(const ClippingPlane& plane) -void MeshClipper::set_mesh(const TriangleMesh& mesh) +void MeshClipper::set_mesh(const indexed_triangle_set& mesh) { - if (m_mesh != &mesh) { + if (m_mesh.get() != &mesh) { m_mesh = &mesh; m_result.reset(); } } -void MeshClipper::set_negative_mesh(const TriangleMesh& mesh) +void MeshClipper::set_mesh(AnyPtr &&ptr) { - if (m_negative_mesh != &mesh) { + if (m_mesh.get() != ptr.get()) { + m_mesh = std::move(ptr); + m_result.reset(); + } +} + +void MeshClipper::set_negative_mesh(const indexed_triangle_set& mesh) +{ + if (m_negative_mesh.get() != &mesh) { m_negative_mesh = &mesh; m_result.reset(); } } +void MeshClipper::set_negative_mesh(AnyPtr &&ptr) +{ + if (m_negative_mesh.get() != ptr.get()) { + m_negative_mesh = std::move(ptr); + m_result.reset(); + } +} + void MeshClipper::set_transformation(const Geometry::Transformation& trafo) @@ -173,13 +191,21 @@ void MeshClipper::recalculate_triangles() MeshSlicingParams slicing_params; slicing_params.trafo.rotate(Eigen::Quaternion::FromTwoVectors(up, Vec3d::UnitZ())); - ExPolygons expolys = union_ex(slice_mesh(m_mesh->its, height_mesh, slicing_params)); + ExPolygons expolys; - if (m_negative_mesh && !m_negative_mesh->empty()) { - const ExPolygons neg_expolys = union_ex(slice_mesh(m_negative_mesh->its, height_mesh, slicing_params)); - expolys = diff_ex(expolys, neg_expolys); + if (m_csgmesh.empty()) { + if (m_mesh) + expolys = union_ex(slice_mesh(*m_mesh, height_mesh, slicing_params)); + + if (m_negative_mesh && !m_negative_mesh->empty()) { + const ExPolygons neg_expolys = union_ex(slice_mesh(*m_negative_mesh, height_mesh, slicing_params)); + expolys = diff_ex(expolys, neg_expolys); + } + } else { + expolys = std::move(csg::slice_csgmesh_ex(range(m_csgmesh), {height_mesh}, MeshSlicingParamsEx{slicing_params}).front()); } + // Triangulate and rotate the cut into world coords: Eigen::Quaterniond q; q.setFromTwoVectors(Vec3d::UnitZ(), up); diff --git a/src/slic3r/GUI/MeshUtils.hpp b/src/slic3r/GUI/MeshUtils.hpp index 7c07b97c6..b10fa5ff8 100644 --- a/src/slic3r/GUI/MeshUtils.hpp +++ b/src/slic3r/GUI/MeshUtils.hpp @@ -5,6 +5,8 @@ #include "libslic3r/Geometry.hpp" #include "libslic3r/TriangleMesh.hpp" #include "libslic3r/AABBMesh.hpp" +#include "libslic3r/CSGMesh/TriangleMeshAdapter.hpp" +#include "libslic3r/CSGMesh/CSGMeshCopy.hpp" #include "admesh/stl.h" #include "slic3r/GUI/GLModel.hpp" @@ -14,7 +16,6 @@ #include namespace Slic3r { - namespace GUI { struct Camera; @@ -88,9 +89,25 @@ public: // Which mesh to cut. MeshClipper remembers const * to it, caller // must make sure that it stays valid. - void set_mesh(const TriangleMesh& mesh); + void set_mesh(const indexed_triangle_set& mesh); + void set_mesh(AnyPtr &&ptr); - void set_negative_mesh(const TriangleMesh &mesh); + void set_negative_mesh(const indexed_triangle_set &mesh); + void set_negative_mesh(AnyPtr &&ptr); + + template + void set_mesh(const Range &csgrange, bool copy_meshes = false) + { + if (! csg::is_same(range(m_csgmesh), csgrange)) { + m_csgmesh.clear(); + if (copy_meshes) + csg::copy_csgrange_deep(csgrange, std::back_inserter(m_csgmesh)); + else + csg::copy_csgrange_shallow(csgrange, std::back_inserter(m_csgmesh)); + + m_result.reset(); + } + } // Inform the MeshClipper about the transformation that transforms the mesh // into world coordinates. @@ -110,8 +127,10 @@ private: void recalculate_triangles(); Geometry::Transformation m_trafo; - const TriangleMesh* m_mesh = nullptr; - const TriangleMesh* m_negative_mesh = nullptr; + AnyPtr m_mesh; + AnyPtr m_negative_mesh; + std::vector m_csgmesh; + ClippingPlane m_plane; ClippingPlane m_limiting_plane = ClippingPlane::ClipsNothing(); @@ -138,13 +157,17 @@ private: class MeshRaycaster { public: explicit MeshRaycaster(std::shared_ptr mesh) - : m_mesh(mesh) - , m_emesh(*mesh, true) // calculate epsilon for triangle-ray intersection from an average edge length - , m_normals(its_face_normals(mesh->its)) + : m_mesh(std::move(mesh)) + , m_emesh(*m_mesh, true) // calculate epsilon for triangle-ray intersection from an average edge length + , m_normals(its_face_normals(m_mesh->its)) { - assert(m_mesh != nullptr); + assert(m_mesh); } + explicit MeshRaycaster(const TriangleMesh &mesh) + : MeshRaycaster(std::make_unique(mesh)) + {} + // DEPRICATED - use CameraUtils::ray_from_screen_pos static void line_from_mouse_pos(const Vec2d& mouse_pos, const Transform3d& trafo, const Camera& camera, Vec3d& point, Vec3d& direction); diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index e6f9d952a..e8fd68c81 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -2246,6 +2246,29 @@ void NotificationManager::push_simplify_suggestion_notification(const std::stri notification->object_id = object_id; push_notification_data(std::move(notification), 0); } +void NotificationManager::push_version_notification(NotificationType type, NotificationLevel level, const std::string& text, const std::string& hypertext, std::function callback) +{ + assert (type == NotificationType::NewAlphaAvailable + || type == NotificationType::NewBetaAvailable + || type == NotificationType::NoNewReleaseAvailable); + + for (std::unique_ptr& notification : m_pop_notifications) { + // NoNewReleaseAvailable must not show if alfa / beta is on. + NotificationType nttype = notification->get_type(); + if (type == NotificationType::NoNewReleaseAvailable + && (notification->get_type() == NotificationType::NewAlphaAvailable + || notification->get_type() == NotificationType::NewBetaAvailable)) { + return; + } + // NoNewReleaseAvailable must close if alfa / beta is being push. + if (notification->get_type() == NotificationType::NoNewReleaseAvailable + && (type == NotificationType::NewAlphaAvailable + || type == NotificationType::NewBetaAvailable)) { + notification->close(); + } + } + push_notification(type, level, text, hypertext, callback); +} void NotificationManager::close_notification_of_type(const NotificationType type) { for (std::unique_ptr ¬ification : m_pop_notifications) { diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 7cd77a304..b3a39a936 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -56,6 +56,7 @@ enum class NotificationType // Like NewAppAvailable but with text and link for alpha / bet release NewAlphaAvailable, NewBetaAvailable, + NoNewReleaseAvailable, // Notification on the start of PrusaSlicer, when updates of system profiles are detected. // Contains a hyperlink to execute installation of the new system profiles. PresetUpdateAvailable, @@ -191,6 +192,9 @@ public: // Object warning with ObjectID, closes when object is deleted. ID used is of object not print like in slicing warning. void push_simplify_suggestion_notification(const std::string& text, ObjectID object_id, const std::string& hypertext = "", std::function callback = std::function()); + // Could be either NewAlphaAvailable, NewBetaAvailable or NoNewReleaseAvailable - this function only makes sure only 1 is visible. + void push_version_notification(NotificationType type, NotificationLevel level, const std::string& text, const std::string& hypertext, + std::function callback); // Close object warnings, whose ObjectID is not in the list. // living_oids is expected to be sorted. void remove_simplify_suggestion_of_released_objects(const std::vector& living_oids); diff --git a/src/slic3r/GUI/OpenGLManager.cpp b/src/slic3r/GUI/OpenGLManager.cpp index 8959553e6..ecf0c5790 100644 --- a/src/slic3r/GUI/OpenGLManager.cpp +++ b/src/slic3r/GUI/OpenGLManager.cpp @@ -200,7 +200,6 @@ std::string OpenGLManager::GLInfo::to_string(bool for_github) const out << b_start << "Renderer: " << b_end << m_renderer << line_end; out << b_start << "GLSL version: " << b_end << m_glsl_version << line_end; out << b_start << "Textures compression: " << b_end << (are_compressed_textures_supported() ? "Enabled" : "Disabled") << line_end; - out << b_start << "Textures mipmap generation: " << b_end << (use_manually_generated_mipmaps() ? "Manual" : "Automatic") << line_end; { #if ENABLE_GL_CORE_PROFILE @@ -257,7 +256,7 @@ std::vector OpenGLManager::GLInfo::get_extensions_list() const OpenGLManager::GLInfo OpenGLManager::s_gl_info; bool OpenGLManager::s_compressed_textures_supported = false; -bool OpenGLManager::s_use_manually_generated_mipmaps = true; +bool OpenGLManager::s_force_power_of_two_textures = false; OpenGLManager::EMultisampleState OpenGLManager::s_multisample = OpenGLManager::EMultisampleState::Unknown; OpenGLManager::EFramebufferType OpenGLManager::s_framebuffers_type = OpenGLManager::EFramebufferType::Unknown; @@ -415,17 +414,16 @@ bool OpenGLManager::init_gl() // texture of the bed (see: https://github.com/prusa3d/PrusaSlicer/issues/8417). // It seems that this issue only triggers when mipmaps are generated manually // (combined with a texture compression) with texture size not being power of two. - // When mipmaps are generated through OpenGL function glGenerateMipmap() the driver works fine. + // When mipmaps are generated through OpenGL function glGenerateMipmap() the driver works fine, + // but the mipmap generation is quite slow on some machines. // There is no an easy way to detect the driver version without using Win32 API because the strings returned by OpenGL // have no standardized format, only some of them contain the driver version. - // Until we do not know that driver will be fixed (if ever) we force the use of glGenerateMipmap() on all cards + // Until we do not know that driver will be fixed (if ever) we force the use of power of two textures on all cards // containing the string 'Radeon' in the string returned by glGetString(GL_RENDERER) const auto& gl_info = OpenGLManager::get_gl_info(); - if (boost::contains(gl_info.get_vendor(), "ATI Technologies Inc.") && boost::contains(gl_info.get_renderer(), "Radeon")) { - s_use_manually_generated_mipmaps = false; - BOOST_LOG_TRIVIAL(debug) << "Mipmapping through OpenGL was enabled."; - } -#endif + if (boost::contains(gl_info.get_vendor(), "ATI Technologies Inc.") && boost::contains(gl_info.get_renderer(), "Radeon")) + s_force_power_of_two_textures = true; +#endif // _WIN32 } return true; diff --git a/src/slic3r/GUI/OpenGLManager.hpp b/src/slic3r/GUI/OpenGLManager.hpp index 6b947deb5..5e2bdc316 100644 --- a/src/slic3r/GUI/OpenGLManager.hpp +++ b/src/slic3r/GUI/OpenGLManager.hpp @@ -110,7 +110,7 @@ private: static OSInfo s_os_info; #endif //__APPLE__ static bool s_compressed_textures_supported; - static bool s_use_manually_generated_mipmaps; + static bool s_force_power_of_two_textures; static EMultisampleState s_multisample; static EFramebufferType s_framebuffers_type; @@ -139,7 +139,7 @@ public: static EFramebufferType get_framebuffers_type() { return s_framebuffers_type; } static wxGLCanvas* create_wxglcanvas(wxWindow& parent); static const GLInfo& get_gl_info() { return s_gl_info; } - static bool use_manually_generated_mipmaps() { return s_use_manually_generated_mipmaps; } + static bool force_power_of_two_textures() { return s_force_power_of_two_textures; } private: #if ENABLE_GL_CORE_PROFILE || ENABLE_OPENGL_ES diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 1ab23b344..c966f942b 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -553,10 +553,19 @@ FreqChangedParams::FreqChangedParams(wxWindow* parent) : const bool supports_enable = selection == _("None") ? false : true; new_conf.set_key_value("supports_enable", new ConfigOptionBool(supports_enable)); - if (selection == _("Everywhere")) - new_conf.set_key_value("support_buildplate_only", new ConfigOptionBool(false)); - else if (selection == _("Support on build plate only")) - new_conf.set_key_value("support_buildplate_only", new ConfigOptionBool(true)); + std::string treetype = get_sla_suptree_prefix(new_conf); + + if (selection == _("Everywhere")) { + new_conf.set_key_value(treetype + "support_buildplate_only", new ConfigOptionBool(false)); + new_conf.set_key_value("support_enforcers_only", new ConfigOptionBool(false)); + } + else if (selection == _("Support on build plate only")) { + new_conf.set_key_value(treetype + "support_buildplate_only", new ConfigOptionBool(true)); + new_conf.set_key_value("support_enforcers_only", new ConfigOptionBool(false)); + } + else if (selection == _("For support enforcers only")) { + new_conf.set_key_value("support_enforcers_only", new ConfigOptionBool(true)); + } } tab->load_config(new_conf); @@ -567,8 +576,6 @@ FreqChangedParams::FreqChangedParams(wxWindow* parent) : ConfigOptionDef support_def_sla = support_def; support_def_sla.set_default_value(new ConfigOptionStrings{ "None" }); - assert(support_def_sla.enum_labels[2] == L("For support enforcers only")); - support_def_sla.enum_labels.erase(support_def_sla.enum_labels.begin() + 2); option = Option(support_def_sla, "support"); option.opt.full_width = true; line.append_option(option); @@ -695,10 +702,8 @@ void Sidebar::priv::show_preset_comboboxes() for (size_t i = 0; i < 4; ++i) sizer_presets->Show(i, !showSLA); - for (size_t i = 4; i < 8; ++i) { - if (sizer_presets->IsShown(i) != showSLA) - sizer_presets->Show(i, showSLA); - } + for (size_t i = 4; i < 8; ++i) + sizer_presets->Show(i, showSLA); frequently_changed_parameters->Show(!showSLA); @@ -812,8 +817,11 @@ Sidebar::Sidebar(Plater *parent) auto *sizer_presets = this->p->sizer_presets; auto *sizer_filaments = this->p->sizer_filaments; + // Hide controls, which will be shown/hidden in respect to the printer technology + text->Show(preset_type == Preset::TYPE_PRINTER); sizer_presets->Add(text, 0, wxALIGN_LEFT | wxEXPAND | wxRIGHT, 4); if (! filament) { + combo_and_btn_sizer->ShowItems(preset_type == Preset::TYPE_PRINTER); sizer_presets->Add(combo_and_btn_sizer, 0, wxEXPAND | #ifdef __WXGTK3__ wxRIGHT, margin_5); @@ -828,6 +836,7 @@ Sidebar::Sidebar(Plater *parent) wxBOTTOM, 1); #endif // __WXGTK3__ (*combo)->set_extruder_idx(0); + sizer_filaments->ShowItems(false); sizer_presets->Add(sizer_filaments, 1, wxEXPAND); } }; @@ -1080,7 +1089,10 @@ void Sidebar::update_presets(Preset::Type preset_type) case Preset::TYPE_PRINTER: { update_all_preset_comboboxes(); - p->show_preset_comboboxes(); + // CallAfter is really needed here to correct layout of the preset comboboxes, + // when printer technology is changed during a project loading AND/OR switching the application mode. + // Otherwise, some of comboboxes are invisible + CallAfter([this]() { p->show_preset_comboboxes(); }); break; } @@ -1537,7 +1549,6 @@ void Sidebar::update_mode() p->object_list->unselect_objects(); p->object_list->update_selections(); -// p->object_list->update_object_menu(); Layout(); } @@ -2653,10 +2664,10 @@ std::vector Plater::priv::load_files(const std::vector& input_ } if ((wxGetApp().get_mode() == comSimple) && (type_3mf || type_any_amf) && model_has_advanced_features(model)) { MessageDialog msg_dlg(q, _L("This file cannot be loaded in a simple mode. Do you want to switch to an advanced mode?")+"\n", - _L("Detected advanced data"), wxICON_WARNING | wxYES | wxCANCEL); - if (msg_dlg.ShowModal() == wxID_YES) { - Slic3r::GUI::wxGetApp().save_mode(comAdvanced); - view3D->set_as_dirty(); + _L("Detected advanced data"), wxICON_WARNING | wxOK | wxCANCEL); + if (msg_dlg.ShowModal() == wxID_OK) { + if (wxGetApp().save_mode(comAdvanced)) + view3D->set_as_dirty(); } else return obj_idxs; @@ -2673,17 +2684,6 @@ std::vector Plater::priv::load_files(const std::vector& input_ model_object->ensure_on_bed(is_project_file); } - // check multi-part object adding for the SLA-printing - if (printer_technology == ptSLA) { - for (auto obj : model.objects) - if ( obj->volumes.size()>1 ) { - Slic3r::GUI::show_error(nullptr, - format_wxstr(_L("You can't to add the object(s) from %s because of one or some of them is(are) multi-part"), - from_path(filename))); - return obj_idxs; - } - } - if (one_by_one) { if ((type_3mf && !is_project_file) || (type_any_amf && !type_zip_amf)) model.center_instances_around_point(this->bed.build_volume().bed_center()); @@ -4404,6 +4404,7 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) } else { show_error(q, message.first, message.second); notification_manager->set_slicing_progress_hidden(); + notification_manager->stop_delayed_notifications_of_type(NotificationType::ExportOngoing); } } else notification_manager->push_slicing_error_notification(message.first); @@ -4525,9 +4526,9 @@ void Plater::priv::on_right_click(RBtnEvent& evt) // so this selection should be updated before menu creation wxGetApp().obj_list()->update_selections(); - if (printer_technology == ptSLA) - menu = menus.sla_object_menu(); - else { +// if (printer_technology == ptSLA) +// menu = menus.sla_object_menu(); +// else { const Selection& selection = get_selection(); // show "Object menu" for each one or several FullInstance instead of FullObject const bool is_some_full_instances = selection.is_single_full_instance() || @@ -4539,12 +4540,12 @@ void Plater::priv::on_right_click(RBtnEvent& evt) const bool is_part = selection.is_single_volume() || selection.is_single_modifier(); #endif // ENABLE_WORLD_COORDINATE if (is_some_full_instances) - menu = menus.object_menu(); + menu = printer_technology == ptSLA ? menus.sla_object_menu() : menus.object_menu(); else if (is_part) menu = selection.is_single_text() ? menus.text_part_menu() : menus.part_menu(); else menu = menus.multi_selection_menu(); - } +// } } if (q != nullptr && menu) { @@ -5013,14 +5014,13 @@ bool Plater::priv::can_split_to_objects() const bool Plater::priv::can_split_to_volumes() const { - return (printer_technology != ptSLA) && q->can_split(false); + return q->can_split(false); } bool Plater::priv::can_arrange() const { if (model.objects.empty() || !m_worker.is_idle()) return false; - if (q->canvas3D()->get_gizmos_manager().get_current_type() == GLGizmosManager::Emboss) return false; - return true; + return q->canvas3D()->get_gizmos_manager().get_current_type() == GLGizmosManager::Undefined; } bool Plater::priv::can_layers_editing() const @@ -5304,8 +5304,8 @@ void Plater::priv::update_after_undo_redo(const UndoRedo::Snapshot& snapshot, bo if (wxGetApp().get_mode() == comSimple && model_has_advanced_features(this->model)) { // If the user jumped to a snapshot that require user interface with advanced features, switch to the advanced mode without asking. // There is a little risk of surprising the user, as he already must have had the advanced or expert mode active for such a snapshot to be taken. - Slic3r::GUI::wxGetApp().save_mode(comAdvanced); - view3D->set_as_dirty(); + if (wxGetApp().save_mode(comAdvanced)) + view3D->set_as_dirty(); } // this->update() above was called with POSTPONE_VALIDATION_ERROR_MESSAGE, so that if an error message was generated when updating the back end, it would not open immediately, @@ -6015,7 +6015,9 @@ protected: ProjectDropDialog::ProjectDropDialog(const std::string& filename) : DPIDialog(static_cast(wxGetApp().mainframe), wxID_ANY, - from_u8((boost::format(_utf8(L("%s - Drop project file"))) % SLIC3R_APP_NAME).str()), wxDefaultPosition, +// #ysFIXME_delete_after_test_of_6377 +// from_u8((boost::format(_utf8(L("%s - Drop project file"))) % SLIC3R_APP_NAME).str()), wxDefaultPosition, + from_u8((boost::format(_utf8(L("%s - Load project file"))) % SLIC3R_APP_NAME).str()), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE) { SetFont(wxGetApp().normal_font()); @@ -6123,7 +6125,7 @@ bool Plater::load_files(const wxArrayString& filenames, bool delete_after_load/* std::string filename = (*it).filename().string(); if (boost::algorithm::iends_with(filename, ".3mf") || boost::algorithm::iends_with(filename, ".amf")) { ProjectDropDialog::LoadType load_type = ProjectDropDialog::LoadType::Unknown; - if (!model().objects.empty()) { +// if (!model().objects.empty()) { // #ysFIXME_delete_after_test_of_6377 if ((boost::algorithm::iends_with(filename, ".3mf") && !is_project_3mf(it->string())) || (boost::algorithm::iends_with(filename, ".amf") && !boost::algorithm::iends_with(filename, ".zip.amf"))) load_type = ProjectDropDialog::LoadType::LoadGeometry; @@ -6140,9 +6142,11 @@ bool Plater::load_files(const wxArrayString& filenames, bool delete_after_load/* load_type = static_cast(std::clamp(std::stoi(wxGetApp().app_config->get("drop_project_action")), static_cast(ProjectDropDialog::LoadType::OpenProject), static_cast(ProjectDropDialog::LoadType::LoadConfig))); } +/* // #ysFIXME_delete_after_test_of_6377 } else load_type = ProjectDropDialog::LoadType::OpenProject; +*/ if (load_type == ProjectDropDialog::LoadType::Unknown) return false; @@ -6555,7 +6559,7 @@ void Plater::export_stl_obj(bool extended, bool selection_only) return; // Following lambda generates a combined mesh for export with normals pointing outwards. - auto mesh_to_export = [](const ModelObject& mo, int instance_id) { + auto mesh_to_export_fff = [](const ModelObject& mo, int instance_id) { TriangleMesh mesh; for (const ModelVolume* v : mo.volumes) if (v->is_model_part()) { @@ -6577,93 +6581,65 @@ void Plater::export_stl_obj(bool extended, bool selection_only) return mesh; }; - TriangleMesh mesh; - if (p->printer_technology == ptFFF) { - if (selection_only) { - const ModelObject* model_object = p->model.objects[obj_idx]; - if (selection.get_mode() == Selection::Instance) - mesh = mesh_to_export(*model_object, (selection.is_single_full_object() && model_object->instances.size() > 1) ? -1 : selection.get_instance_idx()); - else { - const GLVolume* volume = selection.get_first_volume(); - mesh = model_object->volumes[volume->volume_idx()]->mesh(); - mesh.transform(volume->get_volume_transformation().get_matrix(), true); - } + auto mesh_to_export_sla = [&, this](const ModelObject& mo, int instance_id) { + TriangleMesh mesh; - if (!selection.is_single_full_object() || model_object->instances.size() == 1) - mesh.translate(-model_object->origin_translation.cast()); - } + const SLAPrintObject *object = this->p->sla_print.get_print_object_by_model_object_id(mo.id()); + + if (auto m = object->get_mesh_to_print(); !m || m->empty()) + mesh = mesh_to_export_fff(mo, instance_id); else { - for (const ModelObject* o : p->model.objects) { - mesh.merge(mesh_to_export(*o, -1)); - } - } - } - else { - // This is SLA mode, all objects have only one volume. - // However, we must have a look at the backend to load - // hollowed mesh and/or supports - - const PrintObjects& objects = p->sla_print.objects(); - for (const SLAPrintObject* object : objects) { - const ModelObject* model_object = object->model_object(); - if (selection_only) { - if (model_object->id() != p->model.objects[obj_idx]->id()) - continue; - } const Transform3d mesh_trafo_inv = object->trafo().inverse(); const bool is_left_handed = object->is_left_handed(); - TriangleMesh pad_mesh; - const bool has_pad_mesh = extended && object->has_mesh(slaposPad); - if (has_pad_mesh) { - pad_mesh = object->get_mesh(slaposPad); - pad_mesh.transform(mesh_trafo_inv); - } + auto pad_mesh = extended? object->pad_mesh() : TriangleMesh{}; + pad_mesh.transform(mesh_trafo_inv); + + auto supports_mesh = extended ? object->support_mesh() : TriangleMesh{}; + supports_mesh.transform(mesh_trafo_inv); - TriangleMesh supports_mesh; - const bool has_supports_mesh = extended && object->has_mesh(slaposSupportTree); - if (has_supports_mesh) { - supports_mesh = object->get_mesh(slaposSupportTree); - supports_mesh.transform(mesh_trafo_inv); - } const std::vector& obj_instances = object->instances(); for (const SLAPrintObject::Instance& obj_instance : obj_instances) { - auto it = std::find_if(model_object->instances.begin(), model_object->instances.end(), - [&obj_instance](const ModelInstance *mi) { return mi->id() == obj_instance.instance_id; }); - assert(it != model_object->instances.end()); + auto it = std::find_if(object->model_object()->instances.begin(), object->model_object()->instances.end(), + [&obj_instance](const ModelInstance *mi) { return mi->id() == obj_instance.instance_id; }); + assert(it != object->model_object()->instances.end()); - if (it != model_object->instances.end()) { + if (it != object->model_object()->instances.end()) { const bool one_inst_only = selection_only && ! selection.is_single_full_object(); - const int instance_idx = it - model_object->instances.begin(); + const int instance_idx = it - object->model_object()->instances.begin(); const Transform3d& inst_transform = one_inst_only - ? Transform3d::Identity() - : object->model_object()->instances[instance_idx]->get_transformation().get_matrix(); + ? Transform3d::Identity() + : object->model_object()->instances[instance_idx]->get_transformation().get_matrix(); TriangleMesh inst_mesh; - if (has_pad_mesh) { + if (!pad_mesh.empty()) { TriangleMesh inst_pad_mesh = pad_mesh; inst_pad_mesh.transform(inst_transform, is_left_handed); inst_mesh.merge(inst_pad_mesh); } - if (has_supports_mesh) { + if (!supports_mesh.empty()) { TriangleMesh inst_supports_mesh = supports_mesh; inst_supports_mesh.transform(inst_transform, is_left_handed); inst_mesh.merge(inst_supports_mesh); } - TriangleMesh inst_object_mesh = object->get_mesh_to_slice(); + std::shared_ptr m = object->get_mesh_to_print(); + TriangleMesh inst_object_mesh; + if (m) + inst_object_mesh = TriangleMesh{*m}; + inst_object_mesh.transform(mesh_trafo_inv); inst_object_mesh.transform(inst_transform, is_left_handed); inst_mesh.merge(inst_object_mesh); - // ensure that the instance lays on the bed + // ensure that the instance lays on the bed inst_mesh.translate(0.0f, 0.0f, -inst_mesh.bounding_box().min.z()); - // merge instance with global mesh + // merge instance with global mesh mesh.merge(inst_mesh); if (one_inst_only) @@ -6671,6 +6647,36 @@ void Plater::export_stl_obj(bool extended, bool selection_only) } } } + + return mesh; + }; + + std::function + mesh_to_export; + + if (p->printer_technology == ptFFF ) + mesh_to_export = mesh_to_export_fff; + else + mesh_to_export = mesh_to_export_sla; + + TriangleMesh mesh; + if (selection_only) { + const ModelObject* model_object = p->model.objects[obj_idx]; + if (selection.get_mode() == Selection::Instance) + mesh = mesh_to_export(*model_object, (selection.is_single_full_object() && model_object->instances.size() > 1) ? -1 : selection.get_instance_idx()); + else { + const GLVolume* volume = selection.get_first_volume(); + mesh = model_object->volumes[volume->volume_idx()]->mesh(); + mesh.transform(volume->get_volume_transformation().get_matrix(), true); + } + + if (!selection.is_single_full_object() || model_object->instances.size() == 1) + mesh.translate(-model_object->origin_translation.cast()); + } + else { + for (const ModelObject* o : p->model.objects) { + mesh.merge(mesh_to_export(*o, -1)); + } } if (path.EndsWith(".stl")) @@ -6845,16 +6851,6 @@ void Plater::reslice() p->preview->reload_print(!clean_gcode_toolpaths); } -void Plater::reslice_SLA_supports(const ModelObject &object, bool postpone_error_messages) -{ - reslice_SLA_until_step(slaposPad, object, postpone_error_messages); -} - -void Plater::reslice_SLA_hollowing(const ModelObject &object, bool postpone_error_messages) -{ - reslice_SLA_until_step(slaposDrillHoles, object, postpone_error_messages); -} - void Plater::reslice_until_step_inner(int step, const ModelObject &object, bool postpone_error_messages) { //FIXME Don't reslice if export of G-code or sending to OctoPrint is running. diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index e254428b5..d6f2d9b79 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -271,8 +271,6 @@ public: void export_toolpaths_to_obj() const; void reslice(); void reslice_FFF_until_step(PrintObjectStep step, const ModelObject &object, bool postpone_error_messages = false); - void reslice_SLA_supports(const ModelObject &object, bool postpone_error_messages = false); - void reslice_SLA_hollowing(const ModelObject &object, bool postpone_error_messages = false); void reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject &object, bool postpone_error_messages = false); void clear_before_change_mesh(int obj_idx); diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 106937c46..3830ba75e 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -89,6 +89,13 @@ void PreferencesDialog::show(const std::string& highlight_opt_key /*= std::strin m_custom_toolbar_size = atoi(get_app_config()->get("custom_toolbar_size").c_str()); m_use_custom_toolbar_size = get_app_config()->get("use_custom_toolbar_size") == "1"; + // set Field for notify_release to its value + if (m_optgroup_gui && m_optgroup_gui->get_field("notify_release") != nullptr) { + boost::any val = s_keys_map_NotifyReleaseMode.at(wxGetApp().app_config->get("notify_release")); + m_optgroup_gui->get_field("notify_release")->set_value(val, false); + } + + if (wxGetApp().is_editor()) { auto app_config = get_app_config(); @@ -301,6 +308,11 @@ void PreferencesDialog::build() L("Suppress \" - default - \" presets in the Print / Filament / Printer selections once there are any other valid presets available."), app_config->get("no_defaults") == "1"); + append_bool_option(m_optgroup_general, "no_templates", + L("Suppress \" Template \" filament presets"), + L("Suppress \" Template \" filament presets in configuration wizard and sidebar visibility."), + app_config->get("no_templates") == "1"); + append_bool_option(m_optgroup_general, "show_incompatible_presets", L("Show incompatible print and filament presets"), L("When checked, the print and filament presets are shown in the preset editor " @@ -310,8 +322,14 @@ void PreferencesDialog::build() m_optgroup_general->append_separator(); append_bool_option(m_optgroup_general, "show_drop_project_dialog", +#if 1 // #ysFIXME_delete_after_test_of_6377 + L("Show load project dialog"), + L("When checked, whenever dragging and dropping a project file on the application or open it from a browser, " + "shows a dialog asking to select the action to take on the file to load."), +#else L("Show drop project dialog"), L("When checked, whenever dragging and dropping a project file on the application, shows a dialog asking to select the action to take on the file to load."), +#endif app_config->get("show_drop_project_dialog") == "1"); append_bool_option(m_optgroup_general, "single_instance", @@ -692,6 +710,8 @@ void PreferencesDialog::accept(wxEvent&) DesktopIntegrationDialog::perform_desktop_integration(true); #endif // __linux__ + bool update_filament_sidebar = (m_values.find("no_templates") != m_values.end()); + std::vector options_to_recreate_GUI = { "no_defaults", "tabs_as_menu", "sys_menu_enabled" }; for (const std::string& option : options_to_recreate_GUI) { @@ -761,6 +781,9 @@ void PreferencesDialog::accept(wxEvent&) wxGetApp().update_ui_from_settings(); clear_cache(); + + if (update_filament_sidebar) + wxGetApp().plater()->sidebar().update_presets(Preset::Type::TYPE_FILAMENT); } void PreferencesDialog::revert(wxEvent&) diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index c4d0fc670..d25f5adfa 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -798,7 +798,7 @@ void PlaterPresetComboBox::show_edit_menu() wxString PlaterPresetComboBox::get_preset_name(const Preset& preset) { - std::string name = preset.alias.empty() ? preset.name : preset.alias; + std::string name = preset.alias.empty() ? preset.name : (preset.vendor && preset.vendor->templates_profile ? preset.name : preset.alias); return from_u8(name + suffix(preset)); } @@ -836,6 +836,7 @@ void PlaterPresetComboBox::update() null_icon_width = (wide_icons ? 3 : 2) * norm_icon_width + thin_space_icon_width + wide_space_icon_width; std::map nonsys_presets; + std::map template_presets; wxString selected_user_preset; wxString tooltip; @@ -883,10 +884,18 @@ void PlaterPresetComboBox::update() const std::string name = preset.alias.empty() ? preset.name : preset.alias; if (preset.is_default || preset.is_system) { - Append(get_preset_name(preset), *bmp); - validate_selection(is_selected); - if (is_selected) - tooltip = from_u8(preset.name); + if (preset.vendor && preset.vendor->templates_profile) { + template_presets.emplace(get_preset_name(preset), bmp); + if (is_selected) { + selected_user_preset = get_preset_name(preset); + tooltip = from_u8(preset.name); + } + } else { + Append(get_preset_name(preset), *bmp); + validate_selection(is_selected); + if (is_selected) + tooltip = from_u8(preset.name); + } } else { @@ -899,6 +908,8 @@ void PlaterPresetComboBox::update() if (i + 1 == m_collection->num_default_presets()) set_label_marker(Append(separator(L("System presets")), NullBitmapBndl())); } + + if (!nonsys_presets.empty()) { set_label_marker(Append(separator(L("User presets")), NullBitmapBndl())); @@ -908,6 +919,15 @@ void PlaterPresetComboBox::update() } } + const AppConfig* app_config = wxGetApp().app_config; + if (!template_presets.empty() && app_config->get("no_templates") == "0") { + set_label_marker(Append(separator(L("Template presets")), wxNullBitmap)); + for (std::map::iterator it = template_presets.begin(); it != template_presets.end(); ++it) { + Append(it->first, *it->second); + validate_selection(it->first == selected_user_preset); + } + } + if (m_type == Preset::TYPE_PRINTER) { // add Physical printers, if any exists @@ -1046,6 +1066,8 @@ void TabPresetComboBox::update() const std::deque& presets = m_collection->get_presets(); std::map> nonsys_presets; + std::map> template_presets; + wxString selected = ""; if (!presets.front().is_visible) set_label_marker(Append(separator(L("System presets")), NullBitmapBndl())); @@ -1078,11 +1100,19 @@ void TabPresetComboBox::update() auto bmp = get_bmp(bitmap_key, main_icon_name, "lock_closed", is_enabled, preset.is_compatible, preset.is_system || preset.is_default); assert(bmp); - if (preset.is_default || preset.is_system) { - int item_id = Append(get_preset_name(preset), *bmp); - if (!is_enabled) - set_label_marker(item_id, LABEL_ITEM_DISABLED); - validate_selection(i == idx_selected); + if (preset.is_default || preset.is_system) { + if (preset.vendor && preset.vendor->templates_profile) { + template_presets.emplace(get_preset_name(preset), std::pair(bmp, is_enabled)); + if (i == idx_selected) + selected = get_preset_name(preset); + } else { + int item_id = Append(get_preset_name(preset), *bmp); + if (!is_enabled) + set_label_marker(item_id, LABEL_ITEM_DISABLED); + validate_selection(i == idx_selected); + } + + } else { @@ -1094,6 +1124,7 @@ void TabPresetComboBox::update() if (i + 1 == m_collection->num_default_presets()) set_label_marker(Append(separator(L("System presets")), NullBitmapBndl())); } + if (!nonsys_presets.empty()) { set_label_marker(Append(separator(L("User presets")), NullBitmapBndl())); @@ -1105,7 +1136,19 @@ void TabPresetComboBox::update() validate_selection(it->first == selected); } } - + + const AppConfig* app_config = wxGetApp().app_config; + if (!template_presets.empty() && app_config->get("no_templates") == "0") { + set_label_marker(Append(separator(L("Template presets")), wxNullBitmap)); + for (std::map>::iterator it = template_presets.begin(); it != template_presets.end(); ++it) { + int item_id = Append(it->first, *it->second.first); + bool is_enabled = it->second.second; + if (!is_enabled) + set_label_marker(item_id, LABEL_ITEM_DISABLED); + validate_selection(it->first == selected); + } + } + if (m_type == Preset::TYPE_PRINTER) { // add Physical printers, if any exists diff --git a/src/slic3r/GUI/SavePresetDialog.cpp b/src/slic3r/GUI/SavePresetDialog.cpp index d9ee5f58c..d4c2ba58c 100644 --- a/src/slic3r/GUI/SavePresetDialog.cpp +++ b/src/slic3r/GUI/SavePresetDialog.cpp @@ -285,17 +285,17 @@ void SavePresetDialog::Item::Enable(bool enable /*= true*/) // SavePresetDialog //----------------------------------------------- -SavePresetDialog::SavePresetDialog(wxWindow* parent, Preset::Type type, std::string suffix) +SavePresetDialog::SavePresetDialog(wxWindow* parent, Preset::Type type, std::string suffix, bool template_filament) : DPIDialog(parent, wxID_ANY, _L("Save preset"), wxDefaultPosition, wxSize(45 * wxGetApp().em_unit(), 5 * wxGetApp().em_unit()), wxDEFAULT_DIALOG_STYLE | wxICON_WARNING | wxRESIZE_BORDER) { - build(std::vector{type}, suffix); + build(std::vector{type}, suffix, template_filament); } -SavePresetDialog::SavePresetDialog(wxWindow* parent, std::vector types, std::string suffix, PresetBundle* preset_bundle/* = nullptr*/) +SavePresetDialog::SavePresetDialog(wxWindow* parent, std::vector types, std::string suffix, bool template_filament/* =false*/, PresetBundle* preset_bundle/* = nullptr*/) : DPIDialog(parent, wxID_ANY, _L("Save presets"), wxDefaultPosition, wxSize(45 * wxGetApp().em_unit(), 5 * wxGetApp().em_unit()), wxDEFAULT_DIALOG_STYLE | wxICON_WARNING | wxRESIZE_BORDER), m_preset_bundle(preset_bundle) { - build(types, suffix); + build(types, suffix, template_filament); } SavePresetDialog::SavePresetDialog(wxWindow* parent, Preset::Type type, bool rename, const wxString& info_line_extention) @@ -312,7 +312,7 @@ SavePresetDialog::~SavePresetDialog() } } -void SavePresetDialog::build(std::vector types, std::string suffix) +void SavePresetDialog::build(std::vector types, std::string suffix, bool template_filament) { #if defined(__WXMSW__) // ys_FIXME! temporary workaround for correct font scaling @@ -341,6 +341,15 @@ void SavePresetDialog::build(std::vector types, std::string suffix btnOK->Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { evt.Enable(enable_ok_btn()); }); topSizer->Add(m_presets_sizer, 0, wxEXPAND | wxALL, BORDER_W); + + // Add checkbox for Template filament saving + if (template_filament && types.size() == 1 && *types.begin() == Preset::Type::TYPE_FILAMENT) { + m_template_filament_checkbox = new wxCheckBox(this, wxID_ANY, _L("Save as profile derived from current printer only.")); + wxBoxSizer* check_sizer = new wxBoxSizer(wxVERTICAL); + check_sizer->Add(m_template_filament_checkbox); + topSizer->Add(check_sizer, 0, wxEXPAND | wxALL, BORDER_W); + } + topSizer->Add(btns, 0, wxEXPAND | wxALL, BORDER_W); SetSizer(topSizer); @@ -371,6 +380,15 @@ std::string SavePresetDialog::get_name(Preset::Type type) return ""; } +bool SavePresetDialog::get_template_filament_checkbox() +{ + if (m_template_filament_checkbox) + { + return m_template_filament_checkbox->GetValue(); + } + return false; +} + bool SavePresetDialog::enable_ok_btn() const { for (const Item* item : m_items) diff --git a/src/slic3r/GUI/SavePresetDialog.hpp b/src/slic3r/GUI/SavePresetDialog.hpp index e34ed9e5d..3088d457f 100644 --- a/src/slic3r/GUI/SavePresetDialog.hpp +++ b/src/slic3r/GUI/SavePresetDialog.hpp @@ -76,6 +76,7 @@ private: wxStaticText* m_label {nullptr}; wxBoxSizer* m_radio_sizer {nullptr}; ActionType m_action {UndefAction}; + wxCheckBox* m_template_filament_checkbox {nullptr}; std::string m_ph_printer_name; std::string m_old_preset_name; @@ -88,8 +89,8 @@ public: const wxString& get_info_line_extention() { return m_info_line_extention; } - SavePresetDialog(wxWindow* parent, Preset::Type type, std::string suffix = ""); - SavePresetDialog(wxWindow* parent, std::vector types, std::string suffix = "", PresetBundle* preset_bundle = nullptr); + SavePresetDialog(wxWindow* parent, Preset::Type type, std::string suffix = "", bool template_filament = false); + SavePresetDialog(wxWindow* parent, std::vector types, std::string suffix = "", bool template_filament = false, PresetBundle* preset_bundle = nullptr); SavePresetDialog(wxWindow* parent, Preset::Type type, bool rename, const wxString& info_line_extention); ~SavePresetDialog() override; @@ -105,12 +106,13 @@ public: bool Layout() override; bool is_for_rename() { return m_use_for_rename; } + bool get_template_filament_checkbox(); protected: void on_dpi_changed(const wxRect& suggested_rect) override; void on_sys_color_changed() override {} private: - void build(std::vector types, std::string suffix = ""); + void build(std::vector types, std::string suffix = "", bool template_filament = false); void update_physical_printers(const std::string& preset_name); void accept(); }; diff --git a/src/slic3r/GUI/SceneRaycaster.cpp b/src/slic3r/GUI/SceneRaycaster.cpp index 619e26042..1e8bb0c82 100644 --- a/src/slic3r/GUI/SceneRaycaster.cpp +++ b/src/slic3r/GUI/SceneRaycaster.cpp @@ -86,7 +86,7 @@ void SceneRaycaster::remove_raycaster(std::shared_ptr item) } } -SceneRaycaster::HitResult SceneRaycaster::hit(const Vec2d& mouse_pos, const Camera& camera, const ClippingPlane* clipping_plane) +SceneRaycaster::HitResult SceneRaycaster::hit(const Vec2d& mouse_pos, const Camera& camera, const ClippingPlane* clipping_plane) const { double closest_hit_squared_distance = std::numeric_limits::max(); auto is_closest = [&closest_hit_squared_distance](const Camera& camera, const Vec3f& hit) { @@ -98,14 +98,14 @@ SceneRaycaster::HitResult SceneRaycaster::hit(const Vec2d& mouse_pos, const Came }; #if ENABLE_RAYCAST_PICKING_DEBUG - m_last_hit.reset(); + const_cast*>(&m_last_hit)->reset(); #endif // ENABLE_RAYCAST_PICKING_DEBUG HitResult ret; auto test_raycasters = [this, is_closest, clipping_plane](EType type, const Vec2d& mouse_pos, const Camera& camera, HitResult& ret) { const ClippingPlane* clip_plane = (clipping_plane != nullptr && type == EType::Volume) ? clipping_plane : nullptr; - std::vector>* raycasters = get_raycasters(type); + const std::vector>* raycasters = get_raycasters(type); const Vec3f camera_forward = camera.get_dir_forward().cast(); HitResult current_hit = { type }; for (std::shared_ptr item : *raycasters) { @@ -140,7 +140,7 @@ SceneRaycaster::HitResult SceneRaycaster::hit(const Vec2d& mouse_pos, const Came ret.raycaster_id = decode_id(ret.type, ret.raycaster_id); #if ENABLE_RAYCAST_PICKING_DEBUG - m_last_hit = ret; + *const_cast*>(&m_last_hit) = ret; #endif // ENABLE_RAYCAST_PICKING_DEBUG return ret; } @@ -212,6 +212,20 @@ std::vector>* SceneRaycaster::get_raycasters return ret; } +const std::vector>* SceneRaycaster::get_raycasters(EType type) const +{ + const std::vector>* ret = nullptr; + switch (type) + { + case EType::Bed: { ret = &m_bed; break; } + case EType::Volume: { ret = &m_volumes; break; } + case EType::Gizmo: { ret = &m_gizmos; break; } + default: { break; } + } + assert(ret != nullptr); + return ret; +} + int SceneRaycaster::base_id(EType type) { switch (type) diff --git a/src/slic3r/GUI/SceneRaycaster.hpp b/src/slic3r/GUI/SceneRaycaster.hpp index 3119e6d1e..df44b1701 100644 --- a/src/slic3r/GUI/SceneRaycaster.hpp +++ b/src/slic3r/GUI/SceneRaycaster.hpp @@ -87,10 +87,11 @@ public: void remove_raycaster(std::shared_ptr item); std::vector>* get_raycasters(EType type); + const std::vector>* get_raycasters(EType type) const; void set_gizmos_on_top(bool value) { m_gizmos_on_top = value; } - HitResult hit(const Vec2d& mouse_pos, const Camera& camera, const ClippingPlane* clipping_plane = nullptr); + HitResult hit(const Vec2d& mouse_pos, const Camera& camera, const ClippingPlane* clipping_plane = nullptr) const; #if ENABLE_RAYCAST_PICKING_DEBUG void render_hit(const Camera& camera); diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index fda11421f..5678008d3 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -62,18 +62,18 @@ Selection::VolumeCache::VolumeCache(const Geometry::Transformation& volume_trans bool Selection::Clipboard::is_sla_compliant() const { - if (m_mode == Selection::Volume) - return false; +// if (m_mode == Selection::Volume) +// return false; - for (const ModelObject* o : m_model->objects) { - if (o->is_multiparts()) - return false; +// for (const ModelObject* o : m_model->objects) { +// if (o->is_multiparts()) +// return false; - for (const ModelVolume* v : o->volumes) { - if (v->is_modifier()) - return false; - } - } +// for (const ModelVolume* v : o->volumes) { +// if (v->is_modifier()) +// return false; +// } +// } return true; } @@ -157,6 +157,11 @@ void Selection::add(unsigned int volume_idx, bool as_single_selection, bool chec return; const GLVolume* volume = (*m_volumes)[volume_idx]; + + if (wxGetApp().plater()->printer_technology() == ptSLA && volume->is_modifier && + m_model->objects[volume->object_idx()]->volumes[volume->volume_idx()]->is_modifier()) + return; + // wipe tower is already selected if (is_wipe_tower() && volume->is_wipe_tower) return; @@ -482,8 +487,14 @@ void Selection::instances_changed(const std::vector &instance_ids_select assert(m_valid); assert(m_mode == Instance); m_list.clear(); + + const PrinterTechnology pt = wxGetApp().plater()->printer_technology(); + for (unsigned int volume_idx = 0; volume_idx < (unsigned int)m_volumes->size(); ++ volume_idx) { const GLVolume *volume = (*m_volumes)[volume_idx]; + if (pt == ptSLA && volume->is_modifier && + m_model->objects[volume->object_idx()]->volumes[volume->volume_idx()]->is_modifier()) + continue; auto it = std::lower_bound(instance_ids_selected.begin(), instance_ids_selected.end(), volume->geometry_id.second); if (it != instance_ids_selected.end() && *it == volume->geometry_id.second) this->do_add_volume(volume_idx); @@ -571,13 +582,13 @@ bool Selection::is_from_single_object() const bool Selection::is_sla_compliant() const { - if (m_mode == Volume) - return false; +// if (m_mode == Volume) +// return false; - for (unsigned int i : m_list) { - if ((*m_volumes)[i]->is_modifier) - return false; - } +// for (unsigned int i : m_list) { +// if ((*m_volumes)[i]->is_modifier) +// return false; +// } return true; } @@ -961,7 +972,7 @@ void Selection::translate(const Vec3d& displacement, TransformationType transfor transform_instance_relative(v, volume_data, transformation_type, Geometry::translation_transform(displacement), m_cache.dragging_center); } else { - if (transformation_type.local()) { + if (transformation_type.local() && transformation_type.absolute()) { const Geometry::Transformation& vol_trafo = volume_data.get_volume_transform(); const Geometry::Transformation& inst_trafo = volume_data.get_instance_transform(); v.set_volume_offset(vol_trafo.get_offset() + inst_trafo.get_scaling_factor_matrix().inverse() * vol_trafo.get_rotation_matrix() * displacement); @@ -1063,7 +1074,7 @@ void Selection::rotate(const Vec3d& rotation, TransformationType transformation_ transform_volume_relative(v, volume_data, transformation_type, rotation_matrix, m_cache.dragging_center); } else { - if (transformation_type.local()) { + if (transformation_type.local() && transformation_type.absolute()) { const Geometry::Transformation& vol_trafo = volume_data.get_volume_transform(); Matrix3d vol_rotation, vol_scale; vol_trafo.get_matrix().computeRotationScaling(&vol_rotation, &vol_scale); @@ -1453,46 +1464,13 @@ void Selection::mirror(Axis axis) void Selection::scale_and_translate(const Vec3d& scale, const Vec3d& translation, TransformationType transformation_type) { if (!m_valid) - return; - - Vec3d relative_scale = scale; + return; for (unsigned int i : m_list) { GLVolume& v = *(*m_volumes)[i]; const VolumeCache& volume_data = m_cache.volumes_data[i]; const Geometry::Transformation& inst_trafo = volume_data.get_instance_transform(); - if (transformation_type.absolute()) { - // convert from absolute scaling to relative scaling - BoundingBoxf3 original_box; - BoundingBoxf3 reference_box = m_box.get_bounding_box(); - if (m_mode == Instance) { - if (is_single_full_instance()) { - if (transformation_type.world()) - original_box = get_full_unscaled_instance_bounding_box(); - else - original_box = get_full_unscaled_instance_local_bounding_box(); - } - else - original_box = get_bounding_box(); - } - else { - if (!is_single_volume_or_modifier()) - original_box = get_bounding_box(); - else if (transformation_type.world()) - original_box = get_bounding_box(); - else if (transformation_type.instance()) - original_box = v.transformed_convex_hull_bounding_box(volume_data.get_volume_transform().get_matrix()); - else { - original_box = v.bounding_box(); - reference_box = v.bounding_box().transformed(volume_data.get_volume_transform().get_scaling_factor_matrix()); - } - transformation_type.set_relative(); - } - - relative_scale = original_box.size().cwiseProduct(scale).cwiseQuotient(reference_box.size()); - } - if (m_mode == Instance) { if (transformation_type.instance()) { const Vec3d world_inst_pivot = m_cache.dragging_center - inst_trafo.get_offset(); @@ -1500,39 +1478,40 @@ void Selection::scale_and_translate(const Vec3d& scale, const Vec3d& translation Matrix3d inst_rotation, inst_scale; inst_trafo.get_matrix().computeRotationScaling(&inst_rotation, &inst_scale); const Transform3d offset_trafo = Geometry::translation_transform(inst_trafo.get_offset() + inst_rotation * translation); - const Transform3d scale_trafo = Transform3d(inst_scale) * Geometry::scale_transform(relative_scale); + const Transform3d scale_trafo = Transform3d(inst_scale) * Geometry::scale_transform(scale); v.set_instance_transformation(Geometry::translation_transform(world_inst_pivot) * offset_trafo * Transform3d(inst_rotation) * scale_trafo * Geometry::translation_transform(-local_inst_pivot)); } else - transform_instance_relative(v, volume_data, transformation_type, Geometry::translation_transform(translation) * Geometry::scale_transform(relative_scale), m_cache.dragging_center); + transform_instance_relative(v, volume_data, transformation_type, Geometry::translation_transform(translation) * Geometry::scale_transform(scale), m_cache.dragging_center); } else { if (!is_single_volume_or_modifier()) { assert(transformation_type.world()); - transform_volume_relative(v, volume_data, transformation_type, Geometry::translation_transform(translation) * Geometry::scale_transform(relative_scale), m_cache.dragging_center); + transform_volume_relative(v, volume_data, transformation_type, Geometry::translation_transform(translation) * Geometry::scale_transform(scale), m_cache.dragging_center); } else { - if (transformation_type.local()) { - const Geometry::Transformation& vol_trafo = volume_data.get_volume_transform(); - Matrix3d vol_rotation, vol_scale; - vol_trafo.get_matrix().computeRotationScaling(&vol_rotation, &vol_scale); - const Transform3d offset_trafo = Geometry::translation_transform(vol_trafo.get_offset() + vol_rotation * translation); - const Transform3d scale_trafo = Transform3d(vol_scale) * Geometry::scale_transform(relative_scale); - v.set_volume_transformation(offset_trafo * Transform3d(vol_rotation) * scale_trafo); - } - else { - transformation_type.set_independent(); - transform_volume_relative(v, volume_data, transformation_type, Geometry::translation_transform(translation) * Geometry::scale_transform(relative_scale), m_cache.dragging_center); - } + if (transformation_type.local() && transformation_type.absolute()) { + const Geometry::Transformation& vol_trafo = volume_data.get_volume_transform(); + Matrix3d vol_rotation, vol_scale; + vol_trafo.get_matrix().computeRotationScaling(&vol_rotation, &vol_scale); + const Transform3d offset_trafo = Geometry::translation_transform(vol_trafo.get_offset() + vol_rotation * translation); + const Transform3d scale_trafo = Transform3d(vol_scale) * Geometry::scale_transform(scale); + v.set_volume_transformation(offset_trafo * Transform3d(vol_rotation) * scale_trafo); + } + else { + transformation_type.set_independent(); + transformation_type.set_relative(); + transform_volume_relative(v, volume_data, transformation_type, Geometry::translation_transform(translation) * Geometry::scale_transform(scale), m_cache.dragging_center); + } } } } #if !DISABLE_INSTANCES_SYNCH if (m_mode == Instance) - synchronize_unselected_instances(SyncRotationType::NONE); + synchronize_unselected_instances(SyncRotationType::NONE); else if (m_mode == Volume) - synchronize_unselected_volumes(); + synchronize_unselected_volumes(); #endif // !DISABLE_INSTANCES_SYNCH ensure_on_bed(); @@ -2072,9 +2051,16 @@ std::vector Selection::get_volume_idxs_from_object(unsigned int ob { std::vector idxs; + const PrinterTechnology pt = wxGetApp().plater()->printer_technology(); + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { - if ((*m_volumes)[i]->object_idx() == (int)object_idx) + const GLVolume* v = (*m_volumes)[i]; + if (v->object_idx() == (int)object_idx) { + if (pt == ptSLA && v->is_modifier && + m_model->objects[object_idx]->volumes[v->volume_idx()]->is_modifier()) + continue; idxs.push_back(i); + } } return idxs; @@ -2084,8 +2070,13 @@ std::vector Selection::get_volume_idxs_from_instance(unsigned int { std::vector idxs; + const PrinterTechnology pt = wxGetApp().plater()->printer_technology(); + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { const GLVolume* v = (*m_volumes)[i]; + const ModelVolume *mv = get_model_volume(*v, *m_model); + if (pt == ptSLA && v->is_modifier && mv && mv->is_modifier()) + continue; if (v->object_idx() == (int)object_idx && v->instance_idx() == (int)instance_idx) idxs.push_back(i); } @@ -2861,6 +2852,16 @@ void Selection::render_debug_window() const } #endif // ENABLE_WORLD_COORDINATE_DEBUG +static bool is_left_handed(const Transform3d::ConstLinearPart& m) +{ + return m.determinant() < 0; +} + +static bool is_left_handed(const Transform3d& m) +{ + return is_left_handed(m.linear()); +} + #ifndef NDEBUG static bool is_rotation_xy_synchronized(const Vec3d &rot_xyz_from, const Vec3d &rot_xyz_to) { @@ -2875,6 +2876,7 @@ static bool is_rotation_xy_synchronized(const Vec3d &rot_xyz_from, const Vec3d & return std::abs(axis.x()) < 1e-8 && std::abs(axis.y()) < 1e-8 && std::abs(std::abs(axis.z()) - 1.) < 1e-8; } +#if 0 static void verify_instances_rotation_synchronized(const Model &model, const GLVolumePtrs &volumes) { for (int idx_object = 0; idx_object < int(model.objects.size()); ++idx_object) { @@ -2896,8 +2898,103 @@ static void verify_instances_rotation_synchronized(const Model &model, const GLV } } } +#endif + +static bool is_rotation_xy_synchronized(const Transform3d::ConstLinearPart &trafo_from, const Transform3d::ConstLinearPart &trafo_to) +{ + auto rot = trafo_to * trafo_from.inverse(); + static constexpr const double eps = EPSILON; + return + // Looks like a rotation around Z: block(0..1, 0..1) + no change of Z component. + is_approx(rot(0, 0), rot(1, 1), eps) && + is_approx(rot(0, 1), - rot(1, 0), eps) && + is_approx(rot(2, 2), 1., eps) && + // Rest should be zeros. + is_approx(rot(0, 2), 0., eps) && + is_approx(rot(1, 2), 0., eps) && + is_approx(rot(2, 0), 0., eps) && + is_approx(rot(2, 1), 0., eps) && + // Determinant equals 1 + is_approx(rot.determinant(), 1., eps) && + // and finally the rotated X and Y axes shall be perpendicular. + is_approx(rot(0, 0) * rot(0, 1) + rot(1, 0) * rot(1, 1), 0., eps); +} + +static bool is_rotation_xy_synchronized(const Transform3d& trafo_from, const Transform3d& trafo_to) +{ + return is_rotation_xy_synchronized(trafo_from.linear(), trafo_to.linear()); +} + +static void verify_instances_rotation_synchronized(const Model &model, const GLVolumePtrs &volumes) +{ + for (int idx_object = 0; idx_object < int(model.objects.size()); ++idx_object) { + int idx_volume_first = -1; + for (int i = 0; i < (int)volumes.size(); ++i) { + if (volumes[i]->object_idx() == idx_object) { + idx_volume_first = i; + break; + } + } + assert(idx_volume_first != -1); // object without instances? + if (idx_volume_first == -1) + continue; + const Transform3d::ConstLinearPart &rotation0 = volumes[idx_volume_first]->get_instance_transformation().get_matrix().linear(); + for (int i = idx_volume_first + 1; i < (int)volumes.size(); ++i) + if (volumes[i]->object_idx() == idx_object) { + const Transform3d::ConstLinearPart &rotation = volumes[i]->get_instance_transformation().get_matrix().linear(); + assert(is_rotation_xy_synchronized(rotation, rotation0)); + } + } +} + #endif /* NDEBUG */ +#if ENABLE_WORLD_COORDINATE +void Selection::synchronize_unselected_instances(SyncRotationType sync_rotation_type) +{ + std::set done; // prevent processing volumes twice + done.insert(m_list.begin(), m_list.end()); + for (unsigned int i : m_list) { + if (done.size() == m_volumes->size()) + break; + const GLVolume* volume_i = (*m_volumes)[i]; + if (volume_i->is_wipe_tower) + continue; + + const int object_idx = volume_i->object_idx(); + const int instance_idx = volume_i->instance_idx(); + const Transform3d& curr_inst_trafo_i = volume_i->get_instance_transformation().get_matrix(); + const bool curr_inst_left_handed = is_left_handed(curr_inst_trafo_i); + const Transform3d& old_inst_trafo_i = m_cache.volumes_data[i].get_instance_transform().get_matrix(); + bool mirrored = is_left_handed(curr_inst_trafo_i) != is_left_handed(old_inst_trafo_i); +// bool mirrored = curr_inst_trafo_i.linear().determinant() * old_inst_trafo_i.linear().determinant() < 0; + + // Process unselected instances. + for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { + if (done.size() == m_volumes->size()) + break; + if (done.find(j) != done.end()) + continue; + GLVolume* volume_j = (*m_volumes)[j]; + if (volume_j->object_idx() != object_idx || volume_j->instance_idx() == instance_idx) + continue; + const Transform3d& old_inst_trafo_j = m_cache.volumes_data[j].get_instance_transform().get_matrix(); + assert(is_rotation_xy_synchronized(old_inst_trafo_i, old_inst_trafo_j)); + Transform3d new_inst_trafo_j = volume_j->get_instance_transformation().get_matrix(); + if (sync_rotation_type != SyncRotationType::NONE || mirrored) + new_inst_trafo_j.linear() = (old_inst_trafo_j.linear() * old_inst_trafo_i.linear().inverse()) * curr_inst_trafo_i.linear(); + if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA) + new_inst_trafo_j.translation().z() = curr_inst_trafo_i.translation().z(); + assert(is_rotation_xy_synchronized(curr_inst_trafo_i, new_inst_trafo_j)); + volume_j->set_instance_transformation(new_inst_trafo_j); + done.insert(j); + } + } +#ifndef NDEBUG + verify_instances_rotation_synchronized(*m_model, *m_volumes); +#endif /* NDEBUG */ +} +#else void Selection::synchronize_unselected_instances(SyncRotationType sync_rotation_type) { std::set done; // prevent processing volumes twice @@ -2913,17 +3010,9 @@ void Selection::synchronize_unselected_instances(SyncRotationType sync_rotation_ const int object_idx = volume_i->object_idx(); const int instance_idx = volume_i->instance_idx(); -#if ENABLE_WORLD_COORDINATE - const Geometry::Transformation& curr_inst_trafo_i = volume_i->get_instance_transformation(); - const Vec3d curr_inst_rotation_i = curr_inst_trafo_i.get_rotation(); - const Vec3d& curr_inst_scaling_factor_i = curr_inst_trafo_i.get_scaling_factor(); - const Vec3d& curr_inst_mirror_i = curr_inst_trafo_i.get_mirror(); - const Vec3d old_inst_rotation_i = m_cache.volumes_data[i].get_instance_transform().get_rotation(); -#else const Vec3d& rotation = volume_i->get_instance_rotation(); const Vec3d& scaling_factor = volume_i->get_instance_scaling_factor(); const Vec3d& mirror = volume_i->get_instance_mirror(); -#endif // ENABLE_WORLD_COORDINATE // Process unselected instances. for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { @@ -2937,64 +3026,27 @@ void Selection::synchronize_unselected_instances(SyncRotationType sync_rotation_ if (volume_j->object_idx() != object_idx || volume_j->instance_idx() == instance_idx) continue; -#if ENABLE_WORLD_COORDINATE - const Vec3d old_inst_rotation_j = m_cache.volumes_data[j].get_instance_transform().get_rotation(); - assert(is_rotation_xy_synchronized(old_inst_rotation_i, old_inst_rotation_j)); - const Geometry::Transformation& curr_inst_trafo_j = volume_j->get_instance_transformation(); - const Vec3d curr_inst_rotation_j = curr_inst_trafo_j.get_rotation(); - Vec3d new_inst_offset_j = curr_inst_trafo_j.get_offset(); - Vec3d new_inst_rotation_j = curr_inst_rotation_j; -#else assert(is_rotation_xy_synchronized(m_cache.volumes_data[i].get_instance_rotation(), m_cache.volumes_data[j].get_instance_rotation())); -#endif // ENABLE_WORLD_COORDINATE switch (sync_rotation_type) { case SyncRotationType::NONE: { // z only rotation -> synch instance z // The X,Y rotations should be synchronized from start to end of the rotation. -#if ENABLE_WORLD_COORDINATE - assert(is_rotation_xy_synchronized(curr_inst_rotation_i, curr_inst_rotation_j)); - if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA) - new_inst_offset_j.z() = curr_inst_trafo_i.get_offset().z(); -#else assert(is_rotation_xy_synchronized(rotation, volume_j->get_instance_rotation())); if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA) volume_j->set_instance_offset(Z, volume_i->get_instance_offset().z()); -#endif // ENABLE_WORLD_COORDINATE break; } case SyncRotationType::GENERAL: { // generic rotation -> update instance z with the delta of the rotation. -#if ENABLE_WORLD_COORDINATE - const double z_diff = Geometry::rotation_diff_z(old_inst_rotation_i, old_inst_rotation_j); - new_inst_rotation_j = curr_inst_rotation_i + z_diff * Vec3d::UnitZ(); -#else const double z_diff = Geometry::rotation_diff_z(m_cache.volumes_data[i].get_instance_rotation(), m_cache.volumes_data[j].get_instance_rotation()); volume_j->set_instance_rotation({ rotation.x(), rotation.y(), rotation.z() + z_diff }); -#endif // ENABLE_WORLD_COORDINATE break; } -#if ENABLE_WORLD_COORDINATE - case SyncRotationType::FULL: { - // generic rotation -> update instance z with the delta of the rotation. - const Eigen::AngleAxisd angle_axis(Geometry::rotation_xyz_diff(curr_inst_rotation_i, old_inst_rotation_j)); - const Vec3d& axis = angle_axis.axis(); - const double z_diff = (std::abs(axis.x()) > EPSILON || std::abs(axis.y()) > EPSILON) ? - angle_axis.angle() * axis.z() : Geometry::rotation_diff_z(curr_inst_rotation_i, old_inst_rotation_j); - - new_inst_rotation_j = curr_inst_rotation_i + z_diff * Vec3d::UnitZ(); - break; - } -#endif // ENABLE_WORLD_COORDINATE } -#if ENABLE_WORLD_COORDINATE - volume_j->set_instance_transformation(Geometry::assemble_transform(new_inst_offset_j, new_inst_rotation_j, - curr_inst_scaling_factor_i, curr_inst_mirror_i)); -#else volume_j->set_instance_scaling_factor(scaling_factor); volume_j->set_instance_mirror(mirror); -#endif // ENABLE_WORLD_COORDINATE done.insert(j); } @@ -3004,6 +3056,7 @@ void Selection::synchronize_unselected_instances(SyncRotationType sync_rotation_ verify_instances_rotation_synchronized(*m_model, *m_volumes); #endif /* NDEBUG */ } +#endif // ENABLE_WORLD_COORDINATE void Selection::synchronize_unselected_volumes() { @@ -3130,7 +3183,12 @@ bool Selection::is_from_fully_selected_instance(unsigned int volume_idx) const return false; unsigned int count = (unsigned int)std::count_if(m_list.begin(), m_list.end(), SameInstance(object_idx, volume->instance_idx(), *m_volumes)); - return count == (unsigned int)m_model->objects[object_idx]->volumes.size(); + + PrinterTechnology pt = wxGetApp().plater()->printer_technology(); + const ModelVolumePtrs& volumes = m_model->objects[object_idx]->volumes; + const unsigned int vol_cnt = (unsigned int)std::count_if(volumes.begin(), volumes.end(), [pt](const ModelVolume* volume) { return pt == ptFFF || !volume->is_modifier(); }); + + return count == vol_cnt; } void Selection::paste_volumes_from_clipboard() @@ -3270,6 +3328,8 @@ void Selection::transform_volume_relative(GLVolume& volume, const VolumeCache& v const Transform3d trafo = Geometry::translation_transform(inst_pivot) * transform * Geometry::translation_transform(-inst_pivot); volume.set_volume_transformation(trafo * vol_trafo.get_matrix()); } + else if (transformation_type.local()) + volume.set_volume_transformation(vol_trafo.get_matrix() * transform); else assert(false); } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 0b0cfc8ae..3a69bcee1 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1038,12 +1038,15 @@ void Tab::load_key_value(const std::string& opt_key, const boost::any& value, bo static wxString support_combo_value_for_config(const DynamicPrintConfig &config, bool is_fff) { + std::string slatree = is_fff ? "" : get_sla_suptree_prefix(config); + const std::string support = is_fff ? "support_material" : "supports_enable"; - const std::string buildplate_only = is_fff ? "support_material_buildplate_only" : "support_buildplate_only"; + const std::string buildplate_only = is_fff ? "support_material_buildplate_only" : slatree + "support_buildplate_only"; + return ! config.opt_bool(support) ? _("None") : - (is_fff && !config.opt_bool("support_material_auto")) ? + ((is_fff && !config.opt_bool("support_material_auto")) || (!is_fff && config.opt_bool("support_enforcers_only"))) ? _("For support enforcers only") : (config.opt_bool(buildplate_only) ? _("Support on build plate only") : _("Everywhere")); @@ -1082,7 +1085,7 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) if (is_fff ? (opt_key == "support_material" || opt_key == "support_material_auto" || opt_key == "support_material_buildplate_only") : - (opt_key == "supports_enable" || opt_key == "support_buildplate_only")) + (opt_key == "supports_enable" || opt_key == "support_tree_type" || opt_key == get_sla_suptree_prefix(*m_config) + "support_buildplate_only" || opt_key == "support_enforcers_only")) og_freq_chng_params->set_value("support", support_combo_value_for_config(*m_config, is_fff)); if (! is_fff && (opt_key == "pad_enable" || opt_key == "pad_around_object")) @@ -1523,6 +1526,14 @@ void TabPrint::build() optgroup->append_single_option_line("dont_support_bridges", category_path + "dont-support-bridges"); optgroup->append_single_option_line("support_material_synchronize_layers", category_path + "synchronize-with-object-layers"); + optgroup = page->new_optgroup(L("Tree supports")); + optgroup->append_single_option_line("support_tree_angle", category_path + "tree_angle"); + optgroup->append_single_option_line("support_tree_angle_slow", category_path + "tree_angle_slow"); + optgroup->append_single_option_line("support_tree_branch_diameter", category_path + "tree_branch_diameter"); + optgroup->append_single_option_line("support_tree_branch_diameter_angle", category_path + "tree_branch_diameter_angle"); + optgroup->append_single_option_line("support_tree_tip_diameter", category_path + "tree_tip_diameter"); + optgroup->append_single_option_line("support_tree_top_rate", category_path + "tree_top_rate"); + page = add_options_page(L("Speed"), "time"); optgroup = page->new_optgroup(L("Speed for print moves")); optgroup->append_single_option_line("perimeter_speed"); @@ -1815,45 +1826,59 @@ static void validate_custom_gcode_cb(Tab* tab, const wxString& title, const t_co tab->on_value_change(opt_key, value); } +void TabFilament::create_line_with_near_label_widget(ConfigOptionsGroupShp optgroup, const std::string& opt_key, int opt_index/* = 0*/) +{ + Line line {"",""}; + if (opt_key == "filament_retract_lift_above" || opt_key == "filament_retract_lift_below") { + Option opt = optgroup->get_option(opt_key); + opt.opt.label = opt.opt.full_label; + line = optgroup->create_single_option_line(opt); + } + else + line = optgroup->create_single_option_line(optgroup->get_option(opt_key)); + + line.near_label_widget = [this, optgroup_wk = ConfigOptionsGroupWkp(optgroup), opt_key, opt_index](wxWindow* parent) { + wxCheckBox* check_box = new wxCheckBox(parent, wxID_ANY, ""); + + check_box->Bind(wxEVT_CHECKBOX, [optgroup_wk, opt_key, opt_index](wxCommandEvent& evt) { + const bool is_checked = evt.IsChecked(); + if (auto optgroup_sh = optgroup_wk.lock(); optgroup_sh) { + if (Field *field = optgroup_sh->get_fieldc(opt_key, opt_index); field != nullptr) { + field->toggle(is_checked); + if (is_checked) + field->set_last_meaningful_value(); + else + field->set_na_value(); + } + } + }, check_box->GetId()); + + m_overrides_options[opt_key] = check_box; + return check_box; + }; + + optgroup->append_line(line); +} + +void TabFilament::update_line_with_near_label_widget(ConfigOptionsGroupShp optgroup, const std::string& opt_key, int opt_index/* = 0*/, bool is_checked/* = true*/) +{ + if (!m_overrides_options[opt_key]) + return; + m_overrides_options[opt_key]->Enable(is_checked); + + is_checked &= !m_config->option(opt_key)->is_nil(); + m_overrides_options[opt_key]->SetValue(is_checked); + + Field* field = optgroup->get_fieldc(opt_key, opt_index); + if (field != nullptr) + field->toggle(is_checked); +} + void TabFilament::add_filament_overrides_page() { PageShp page = add_options_page(L("Filament Overrides"), "wrench"); ConfigOptionsGroupShp optgroup = page->new_optgroup(L("Retraction")); - auto append_single_option_line = [optgroup, this](const std::string& opt_key, int opt_index) - { - Line line {"",""}; - if (opt_key == "filament_retract_lift_above" || opt_key == "filament_retract_lift_below") { - Option opt = optgroup->get_option(opt_key); - opt.opt.label = opt.opt.full_label; - line = optgroup->create_single_option_line(opt); - } - else - line = optgroup->create_single_option_line(optgroup->get_option(opt_key)); - - line.near_label_widget = [this, optgroup_wk = ConfigOptionsGroupWkp(optgroup), opt_key, opt_index](wxWindow* parent) { - wxCheckBox* check_box = new wxCheckBox(parent, wxID_ANY, ""); - - check_box->Bind(wxEVT_CHECKBOX, [optgroup_wk, opt_key, opt_index](wxCommandEvent& evt) { - const bool is_checked = evt.IsChecked(); - if (auto optgroup_sh = optgroup_wk.lock(); optgroup_sh) { - if (Field *field = optgroup_sh->get_fieldc(opt_key, opt_index); field != nullptr) { - field->toggle(is_checked); - if (is_checked) - field->set_last_meaningful_value(); - else - field->set_na_value(); - } - } - }, check_box->GetId()); - - m_overrides_options[opt_key] = check_box; - return check_box; - }; - - optgroup->append_line(line); - }; - const int extruder_idx = 0; // #ys_FIXME for (const std::string opt_key : { "filament_retract_length", @@ -1868,7 +1893,7 @@ void TabFilament::add_filament_overrides_page() "filament_wipe", "filament_retract_before_wipe" }) - append_single_option_line(opt_key, extruder_idx); + create_line_with_near_label_widget(optgroup, opt_key, extruder_idx); } void TabFilament::update_filament_overrides_page() @@ -1903,14 +1928,7 @@ void TabFilament::update_filament_overrides_page() for (const std::string& opt_key : opt_keys) { bool is_checked = opt_key=="filament_retract_length" ? true : have_retract_length; - m_overrides_options[opt_key]->Enable(is_checked); - - is_checked &= !m_config->option(opt_key)->is_nil(); - m_overrides_options[opt_key]->SetValue(is_checked); - - Field* field = optgroup->get_fieldc(opt_key, extruder_idx); - if (field != nullptr) - field->toggle(is_checked); + update_line_with_near_label_widget(optgroup, opt_key, extruder_idx, is_checked); } } @@ -1941,6 +1959,9 @@ void TabFilament::build() }; optgroup = page->new_optgroup(L("Temperature")); + + create_line_with_near_label_widget(optgroup, "idle_temperature"); + Line line = { L("Nozzle"), "" }; line.append_option(optgroup->get_option("first_layer_temperature")); line.append_option(optgroup->get_option("temperature")); @@ -2131,6 +2152,14 @@ void TabFilament::toggle_options() if (m_active_page->title() == "Filament Overrides") update_filament_overrides_page(); + + if (m_active_page->title() == "Filament") { + Page* page = m_active_page; + + const auto og_it = std::find_if(page->m_optgroups.begin(), page->m_optgroups.end(), [](const ConfigOptionsGroupShp og) { return og->title == "Temperature"; }); + if (og_it != page->m_optgroups.end()) + update_line_with_near_label_widget(*og_it, "idle_temperature"); + } } void TabFilament::update() @@ -3712,11 +3741,25 @@ void Tab::save_preset(std::string name /*= ""*/, bool detach) // focus currently.is there anything better than this ? //! m_treectrl->OnSetFocus(); + auto& old_preset = m_presets->get_edited_preset(); + bool from_template = false; + std::string edited_printer; + if (m_type == Preset::TYPE_FILAMENT && old_preset.vendor && old_preset.vendor->templates_profile) + { + //TODO: is this really the best way to get "printer_model" option of currently edited printer? + edited_printer = wxGetApp().preset_bundle->printers.get_edited_preset().config.opt("printer_model")->serialize(); + if (!edited_printer.empty()) + from_template = true; + + } + if (name.empty()) { - SavePresetDialog dlg(m_parent, m_type, detach ? _u8L("Detached") : ""); + SavePresetDialog dlg(m_parent, m_type, detach ? _u8L("Detached") : "", from_template); if (dlg.ShowModal() != wxID_OK) return; name = dlg.get_name(); + if (from_template) + from_template = dlg.get_template_filament_checkbox(); } if (detach && m_type == Preset::TYPE_PRINTER) @@ -3728,6 +3771,19 @@ void Tab::save_preset(std::string name /*= ""*/, bool detach) if (detach && m_type == Preset::TYPE_PRINTER) wxGetApp().mainframe->on_config_changed(m_config); + // Update compatible printers + if (from_template && !edited_printer.empty()) { + auto& new_preset = m_presets->get_edited_preset(); + std::string cond = new_preset.compatible_printers_condition(); + if (!cond.empty()) + cond += " and "; + cond += "printer_model == \""+edited_printer+"\""; + new_preset.config.set("compatible_printers_condition", cond); + new_preset.save(); + m_presets->save_current_preset(name, detach); + load_current_preset(); + } + // Mark the print & filament enabled if they are compatible with the currently selected preset. // If saving the preset changes compatibility with other presets, keep the now incompatible dependent presets selected, however with a "red flag" icon showing that they are no more compatible. m_preset_bundle->update_compatible(PresetSelectCompatibleType::Never); @@ -4821,6 +4877,60 @@ void TabSLAMaterial::update() wxGetApp().mainframe->on_config_changed(m_config); } +static void add_options_into_line(ConfigOptionsGroupShp &optgroup, + const std::vector> &prefixes, + const std::string &optkey) +{ + auto opt = optgroup->get_option(prefixes.front().first + optkey); + Line line{ opt.opt.label, "" }; + line.full_width = 1; + for (auto &prefix : prefixes) { + opt = optgroup->get_option(prefix.first + optkey); + opt.opt.label = prefix.second; + opt.opt.width = 12; // TODO + line.append_option(opt); + } + optgroup->append_line(line); +} + +void TabSLAPrint::build_sla_support_params(const std::vector> &prefixes, + const Slic3r::GUI::PageShp &page) +{ + + auto optgroup = page->new_optgroup(L("Support head")); + add_options_into_line(optgroup, prefixes, "support_head_front_diameter"); + add_options_into_line(optgroup, prefixes, "support_head_penetration"); + add_options_into_line(optgroup, prefixes, "support_head_width"); + + optgroup = page->new_optgroup(L("Support pillar")); + add_options_into_line(optgroup, prefixes, "support_pillar_diameter"); + add_options_into_line(optgroup, prefixes, "support_small_pillar_diameter_percent"); + add_options_into_line(optgroup, prefixes, "support_max_bridges_on_pillar"); + + add_options_into_line(optgroup, prefixes, "support_pillar_connection_mode"); + add_options_into_line(optgroup, prefixes, "support_buildplate_only"); + add_options_into_line(optgroup, prefixes, "support_pillar_widening_factor"); + add_options_into_line(optgroup, prefixes, "support_max_weight_on_model"); + add_options_into_line(optgroup, prefixes, "support_base_diameter"); + add_options_into_line(optgroup, prefixes, "support_base_height"); + add_options_into_line(optgroup, prefixes, "support_base_safety_distance"); + + // Mirrored parameter from Pad page for toggling elevation on the same page + add_options_into_line(optgroup, prefixes, "support_object_elevation"); + + Line line{ "", "" }; + line.full_width = 1; + line.widget = [this](wxWindow* parent) { + return description_line_widget(parent, &m_support_object_elevation_description_line); + }; + optgroup->append_line(line); + + optgroup = page->new_optgroup(L("Connection of the support sticks and junctions")); + add_options_into_line(optgroup, prefixes, "support_critical_angle"); + add_options_into_line(optgroup, prefixes, "support_max_bridge_length"); + add_options_into_line(optgroup, prefixes, "support_max_pillar_link_distance"); +} + void TabSLAPrint::build() { m_presets = &m_preset_bundle->sla_prints; @@ -4833,41 +4943,14 @@ void TabSLAPrint::build() optgroup->append_single_option_line("faded_layers"); page = add_options_page(L("Supports"), "support"/*"sla_supports"*/); + optgroup = page->new_optgroup(L("Supports")); optgroup->append_single_option_line("supports_enable"); optgroup->append_single_option_line("support_tree_type"); - - optgroup = page->new_optgroup(L("Support head")); - optgroup->append_single_option_line("support_head_front_diameter"); - optgroup->append_single_option_line("support_head_penetration"); - optgroup->append_single_option_line("support_head_width"); - - optgroup = page->new_optgroup(L("Support pillar")); - optgroup->append_single_option_line("support_pillar_diameter"); - optgroup->append_single_option_line("support_small_pillar_diameter_percent"); - optgroup->append_single_option_line("support_max_bridges_on_pillar"); + optgroup->append_single_option_line("support_enforcers_only"); - optgroup->append_single_option_line("support_pillar_connection_mode"); - optgroup->append_single_option_line("support_buildplate_only"); - optgroup->append_single_option_line("support_pillar_widening_factor"); - optgroup->append_single_option_line("support_base_diameter"); - optgroup->append_single_option_line("support_base_height"); - optgroup->append_single_option_line("support_base_safety_distance"); - - // Mirrored parameter from Pad page for toggling elevation on the same page - optgroup->append_single_option_line("support_object_elevation"); + build_sla_support_params({{"", "Default"}, {"branching", "Branching"}}, page); - Line line{ "", "" }; - line.full_width = 1; - line.widget = [this](wxWindow* parent) { - return description_line_widget(parent, &m_support_object_elevation_description_line); - }; - optgroup->append_line(line); - - optgroup = page->new_optgroup(L("Connection of the support sticks and junctions")); - optgroup->append_single_option_line("support_critical_angle"); - optgroup->append_single_option_line("support_max_bridge_length"); - optgroup->append_single_option_line("support_max_pillar_link_distance"); optgroup = page->new_optgroup(L("Automatic generation")); optgroup->append_single_option_line("support_points_density_relative"); diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index f5dd4c522..b131761e6 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -339,7 +339,7 @@ public: void on_roll_back_value(const bool to_sys = false); - PageShp add_options_page(const wxString& title, const std::string& icon, bool is_extruder_pages = false); + PageShp add_options_page(const wxString& title, const std::string& icon, bool is_extruder_pages = false); static wxString translate_category(const wxString& title, Preset::Type preset_type); virtual void OnActivate(); @@ -436,6 +436,8 @@ private: ogStaticText* m_volumetric_speed_description_line {nullptr}; ogStaticText* m_cooling_description_line {nullptr}; + void create_line_with_near_label_widget(ConfigOptionsGroupShp optgroup, const std::string &opt_key, int opt_index = 0); + void update_line_with_near_label_widget(ConfigOptionsGroupShp optgroup, const std::string &opt_key, int opt_index = 0, bool is_checked = true); void add_filament_overrides_page(); void update_filament_overrides_page(); void update_volumetric_flow_preset_hints(); @@ -526,6 +528,12 @@ public: class TabSLAPrint : public Tab { + // Methods are a vector of method prefix -> method label pairs + // method prefix is the prefix whith which all the config values are prefixed + // for a particular method. The label is the friendly name for the method + void build_sla_support_params(const std::vector> &methods, + const Slic3r::GUI::PageShp &page); + public: TabSLAPrint(wxBookCtrlBase* parent) : Tab(parent, _(L("Print Settings")), Slic3r::Preset::TYPE_SLA_PRINT) {} diff --git a/src/slic3r/GUI/UpdateDialogs.cpp b/src/slic3r/GUI/UpdateDialogs.cpp index e8edd5798..7640f3220 100644 --- a/src/slic3r/GUI/UpdateDialogs.cpp +++ b/src/slic3r/GUI/UpdateDialogs.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include "libslic3r/libslic3r.h" #include "libslic3r/Utils.hpp" @@ -93,7 +94,7 @@ bool MsgUpdateSlic3r::disable_version_check() const wxSize AppUpdateAvailableDialog::AUAD_size; // AppUpdater -AppUpdateAvailableDialog::AppUpdateAvailableDialog(const Semver& ver_current, const Semver& ver_online) +AppUpdateAvailableDialog::AppUpdateAvailableDialog(const Semver& ver_current, const Semver& ver_online, bool from_user) : MsgDialog(nullptr, _(L("App Update available")), wxString::Format(_(L("New version of %s is available.\nDo you wish to download it?")), SLIC3R_APP_NAME)) { auto* versions = new wxFlexGridSizer(1, 0, VERT_SPACING); @@ -104,8 +105,10 @@ AppUpdateAvailableDialog::AppUpdateAvailableDialog(const Semver& ver_current, co content_sizer->Add(versions); content_sizer->AddSpacer(VERT_SPACING); - cbox = new wxCheckBox(this, wxID_ANY, _(L("Don't notify about new releases any more"))); - content_sizer->Add(cbox); + if(!from_user) { + cbox = new wxCheckBox(this, wxID_ANY, _(L("Don't notify about new releases any more"))); + content_sizer->Add(cbox); + } content_sizer->AddSpacer(VERT_SPACING); AUAD_size = content_sizer->GetSize(); @@ -125,6 +128,8 @@ AppUpdateAvailableDialog::~AppUpdateAvailableDialog() {} bool AppUpdateAvailableDialog::disable_version_check() const { + if (!cbox) + return false; return cbox->GetValue(); } @@ -143,13 +148,14 @@ AppUpdateDownloadDialog::AppUpdateDownloadDialog( const Semver& ver_online, boos #endif content_sizer->AddSpacer(VERT_SPACING); content_sizer->AddSpacer(VERT_SPACING); - content_sizer->Add(new wxStaticText(this, wxID_ANY, _(L("Target path:")))); + content_sizer->Add(new wxStaticText(this, wxID_ANY, _(L("Target directory:")))); content_sizer->AddSpacer(VERT_SPACING); - txtctrl_path = new wxTextCtrl(this, wxID_ANY, path.wstring()); + txtctrl_path = new wxTextCtrl(this, wxID_ANY, GUI::format_wxstr(path.parent_path().string())); + filename = GUI::format_wxstr(path.filename().string()); content_sizer->Add(txtctrl_path, 1, wxEXPAND); content_sizer->AddSpacer(VERT_SPACING); - wxButton* btn = new wxButton(this, wxID_ANY, _L("Select path")); + wxButton* btn = new wxButton(this, wxID_ANY, _L("Select directory")); content_sizer->Add(btn/*, 1, wxEXPAND*/); // button to open file dialog @@ -161,13 +167,18 @@ AppUpdateDownloadDialog::AppUpdateDownloadDialog( const Semver& ver_online, boos wxString wxext = boost::nowide::widen(extension); wildcard = GUI::format_wxstr("%1% Files (*.%2%)|*.%2%", wxext.Upper(), wxext); } - wxFileDialog save_dlg( + boost::system::error_code ec; + boost::filesystem::path dir = boost::filesystem::absolute(boost::filesystem::path(GUI::format(txtctrl_path->GetValue())), ec); + if (ec) + dir = GUI::format(txtctrl_path->GetValue()); + wxDirDialog save_dlg( this - , _L("Save as:") - , txtctrl_path->GetValue() - , boost::nowide::widen(AppUpdater::get_filename_from_url(txtctrl_path->GetValue().ToUTF8().data())) + , _L("Select directory:") + , GUI::format_wxstr(dir.string()) + /* + , filename //boost::nowide::widen(AppUpdater::get_filename_from_url(txtctrl_path->GetValue().ToUTF8().data())) , wildcard - , wxFD_SAVE | wxFD_OVERWRITE_PROMPT + , wxFD_SAVE | wxFD_OVERWRITE_PROMPT*/ ); if (save_dlg.ShowModal() == wxID_OK) { txtctrl_path->SetValue(save_dlg.GetPath()); @@ -181,8 +192,46 @@ AppUpdateDownloadDialog::AppUpdateDownloadDialog( const Semver& ver_online, boos if (auto* btn_ok = get_button(wxID_OK); btn_ok != NULL) { btn_ok->SetLabel(_L("Download")); btn_ok->Bind(wxEVT_BUTTON, ([this, path](wxCommandEvent& e){ - if (boost::filesystem::exists(boost::filesystem::path(txtctrl_path->GetValue().ToUTF8().data()))) { - MessageDialog msgdlg(nullptr, GUI::format_wxstr(_L("File %1% already exists. Do you wish to overwrite it?"), txtctrl_path->GetValue()),_L("Notice"), wxYES_NO); + boost::system::error_code ec; + std::string input = GUI::into_u8(txtctrl_path->GetValue()); + boost::filesystem::path dir = boost::filesystem::absolute(boost::filesystem::path(input), ec); + if (ec) + dir = boost::filesystem::path(input); + bool show_change = (dir.string() != input); + boost::filesystem::path path = dir / GUI::format(filename); + ec.clear(); + if (dir.string().empty()) { + MessageDialog msgdlg(nullptr, _L("Directory path is empty."), _L("Notice"), wxOK); + msgdlg.ShowModal(); + return; + } + ec.clear(); + if (!boost::filesystem::exists(dir, ec) || !boost::filesystem::is_directory(dir,ec) || ec) { + ec.clear(); + if (!boost::filesystem::exists(dir.parent_path(), ec) || !boost::filesystem::is_directory(dir.parent_path(), ec) || ec) { + MessageDialog msgdlg(nullptr, _L("Directory path is incorrect."), _L("Notice"), wxOK); + msgdlg.ShowModal(); + return; + } + show_change = false; + MessageDialog msgdlg(nullptr, GUI::format_wxstr(_L("Directory %1% doesn't exists. Do you wish to create it?"), dir.string()), _L("Notice"), wxYES_NO); + if (msgdlg.ShowModal() != wxID_YES) + return; + ec.clear(); + if(!boost::filesystem::create_directory(dir, ec) || ec) { + MessageDialog msgdlg(nullptr, _L("Failed to create directory."), _L("Notice"), wxOK); + msgdlg.ShowModal(); + return; + } + } + if (boost::filesystem::exists(path)) { + show_change = false; + MessageDialog msgdlg(nullptr, GUI::format_wxstr(_L("File %1% already exists. Do you wish to overwrite it?"), path.string()),_L("Notice"), wxYES_NO); + if (msgdlg.ShowModal() != wxID_YES) + return; + } + if (show_change) { + MessageDialog msgdlg(nullptr, GUI::format_wxstr(_L("Download path is %1%. Do you wish to continue?"), path.string()), _L("Notice"), wxYES_NO); if (msgdlg.ShowModal() != wxID_YES) return; } @@ -207,7 +256,12 @@ bool AppUpdateDownloadDialog::run_after_download() const boost::filesystem::path AppUpdateDownloadDialog::get_download_path() const { - return boost::filesystem::path(txtctrl_path->GetValue().ToUTF8().data()); + boost::system::error_code ec; + std::string input = GUI::format(txtctrl_path->GetValue()); + boost::filesystem::path dir = boost::filesystem::absolute(boost::filesystem::path(input), ec); + if (ec) + dir = boost::filesystem::path(input); + return dir / GUI::format(filename); } // MsgUpdateConfig diff --git a/src/slic3r/GUI/UpdateDialogs.hpp b/src/slic3r/GUI/UpdateDialogs.hpp index 2eb4ff8d4..deedd9a50 100644 --- a/src/slic3r/GUI/UpdateDialogs.hpp +++ b/src/slic3r/GUI/UpdateDialogs.hpp @@ -42,7 +42,7 @@ private: class AppUpdateAvailableDialog : public MsgDialog { public: - AppUpdateAvailableDialog(const Semver& ver_current, const Semver& ver_online); + AppUpdateAvailableDialog(const Semver& ver_current, const Semver& ver_online, bool from_user); AppUpdateAvailableDialog(AppUpdateAvailableDialog&&) = delete; AppUpdateAvailableDialog(const AppUpdateAvailableDialog&) = delete; AppUpdateAvailableDialog& operator=(AppUpdateAvailableDialog&&) = delete; @@ -53,7 +53,7 @@ public: bool disable_version_check() const; static wxSize AUAD_size; private: - wxCheckBox* cbox; + wxCheckBox* cbox {nullptr}; }; class AppUpdateDownloadDialog : public MsgDialog @@ -73,6 +73,7 @@ public: private: wxCheckBox* cbox_run; wxTextCtrl* txtctrl_path; + wxString filename; }; // Confirmation dialog informing about configuration update. Lists updated bundles & their versions. diff --git a/src/slic3r/GUI/wxExtensions.cpp b/src/slic3r/GUI/wxExtensions.cpp index bd2949cbf..e528191aa 100644 --- a/src/slic3r/GUI/wxExtensions.cpp +++ b/src/slic3r/GUI/wxExtensions.cpp @@ -747,9 +747,11 @@ ModeSizer::ModeSizer(wxWindow *parent, int hgap/* = 0*/) : { SetFlexibleDirection(wxHORIZONTAL); - auto modebtnfn = [](wxCommandEvent &event, int mode_id) { - Slic3r::GUI::wxGetApp().save_mode(mode_id); - event.Skip(); + auto modebtnfn = [this](wxCommandEvent &event, int mode_id) { + if (Slic3r::GUI::wxGetApp().save_mode(mode_id)) + event.Skip(); + else + SetMode(Slic3r::GUI::wxGetApp().get_mode()); }; m_mode_btns.reserve(3); diff --git a/src/slic3r/Utils/AppUpdater.cpp b/src/slic3r/Utils/AppUpdater.cpp index a17adea8a..d054db4b5 100644 --- a/src/slic3r/Utils/AppUpdater.cpp +++ b/src/slic3r/Utils/AppUpdater.cpp @@ -13,6 +13,7 @@ #include "slic3r/GUI/format.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/I18N.hpp" #include "slic3r/Utils/Http.hpp" #include "libslic3r/Utils.hpp" @@ -36,7 +37,7 @@ namespace { std::string msg; bool res = GUI::create_process(path, std::wstring(), msg); if (!res) { - std::string full_message = GUI::format("Running downloaded instaler of %1% has failed:\n%2%", SLIC3R_APP_NAME, msg); + std::string full_message = GUI::format(_utf8("Running downloaded instaler of %1% has failed:\n%2%"), SLIC3R_APP_NAME, msg); BOOST_LOG_TRIVIAL(error) << full_message; // lm: maybe UI error msg? // dk: bellow. (maybe some general show error evt would be better?) wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); evt->SetString(full_message); @@ -170,8 +171,10 @@ bool AppUpdater::priv::http_get_file(const std::string& url, size_t size_limit, // progress function returns true as success (to continue) cancel = (m_cancel ? true : !progress_fn(std::move(progress))); if (cancel) { - error_message = GUI::format("Error getting: `%1%`: Download was canceled.", //lm:typo //dk: am i blind? :) - url); + // Lets keep error_message empty here - if there is need to show error dialog, the message will be probably shown by whatever caused the cancel. + /* + error_message = GUI::format(_utf8("Error getting: `%1%`: Download was canceled."), url); + */ BOOST_LOG_TRIVIAL(debug) << "AppUpdater::priv::http_get_file message: "<< error_message; } }) @@ -200,9 +203,33 @@ boost::filesystem::path AppUpdater::priv::download_file(const DownloadAppData& d assert(!dest_path.empty()); if (dest_path.empty()) { - BOOST_LOG_TRIVIAL(error) << "Download from " << data.url << " could not start. Destination path is empty."; + std::string line1 = GUI::format(_utf8("Internal download error for url %1%:"), data.url); + std::string line2 = _utf8("Destination path is empty."); + std::string message = GUI::format("%1%\n%2%", line1, line2); + BOOST_LOG_TRIVIAL(error) << message; + wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); + evt->SetString(message); + GUI::wxGetApp().QueueEvent(evt); return boost::filesystem::path(); } + + boost::filesystem::path tmp_path = dest_path; + tmp_path += format(".%1%%2%", get_current_pid(), ".download"); + FILE* file; + wxString temp_path_wstring(tmp_path.wstring()); + file = fopen(temp_path_wstring.c_str(), "wb"); + assert(file != NULL); + if (file == NULL) { + std::string line1 = GUI::format(_utf8("Download from %1% couldn't start:"), data.url); + std::string line2 = GUI::format(_utf8("Can't create file at %1%."), tmp_path.string()); + std::string message = GUI::format("%1%\n%2%", line1, line2); + BOOST_LOG_TRIVIAL(error) << message; + wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); + evt->SetString(message); + GUI::wxGetApp().QueueEvent(evt); + return boost::filesystem::path(); + } + std::string error_message; bool res = http_get_file(data.url, 130 * 1024 * 1024 //2.4.0 windows installer is 65MB //lm:I don't know, but larger. The binaries will grow. // dk: changed to 130, to have 100% more space. We should put this information into version file. // on_progress @@ -216,12 +243,12 @@ boost::filesystem::path AppUpdater::priv::download_file(const DownloadAppData& d GUI::wxGetApp().QueueEvent(evt); return false; } else if (progress.dltotal > 0 && progress.dltotal < expected_size) { - //lm:When will this happen? Is that not an error? // dk: It is possible error, but we cannot know until the download is finished. Somehow the total size can grow during the download. + // This is possible error, but we cannot know until the download is finished. Somehow the total size can grow during the download. BOOST_LOG_TRIVIAL(info) << GUI::format("Downloading new %1% has incorrect size. The download will continue. \nExpected size: %2%\nDownload size: %3%", SLIC3R_APP_NAME, expected_size, progress.dltotal); } // progress event size_t gui_progress = progress.dltotal > 0 ? 100 * progress.dlnow / progress.dltotal : 0; - BOOST_LOG_TRIVIAL(error) << "App download " << gui_progress << "% " << progress.dlnow << " of " << progress.dltotal; + BOOST_LOG_TRIVIAL(debug) << "App download " << gui_progress << "% " << progress.dlnow << " of " << progress.dltotal; if (last_gui_progress < gui_progress && (last_gui_progress != 0 || gui_progress != 100)) { last_gui_progress = gui_progress; wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_PROGRESS); @@ -231,26 +258,26 @@ boost::filesystem::path AppUpdater::priv::download_file(const DownloadAppData& d return true; } // on_complete - , [dest_path, expected_size](std::string body, std::string& error_message){ + , [&file, dest_path, tmp_path, expected_size](std::string body, std::string& error_message){ // Size check. Does always 1 char == 1 byte? size_t body_size = body.size(); if (body_size != expected_size) { - //lm:UI message? // dk: changed. Now it propagates to UI. - error_message = GUI::format("Downloaded file has wrong size. Expected size: %1% Downloaded size: %2%", expected_size, body_size); + error_message = GUI::format(_utf8("Downloaded file has wrong size. Expected size: %1% Downloaded size: %2%"), expected_size, body_size); + return false; + } + if (file == NULL) { + error_message = GUI::format(_utf8("Can't create file at %1%."), tmp_path.string()); return false; } - boost::filesystem::path tmp_path = dest_path; - tmp_path += format(".%1%%2%", get_current_pid(), ".download"); try { - boost::nowide::fstream file(tmp_path.string(), std::ios::out | std::ios::binary | std::ios::trunc); - file.write(body.c_str(), body.size()); - file.close(); + fwrite(body.c_str(), 1, body.size(), file); + fclose(file); boost::filesystem::rename(tmp_path, dest_path); } - catch (const std::exception&) + catch (const std::exception& e) { - error_message = GUI::format("Failed to write and move %1% to %2%", tmp_path, dest_path); + error_message = GUI::format(_utf8("Failed to write to file or to move %1% to %2%:\n%3%"), tmp_path, dest_path, e.what()); return false; } return true; @@ -259,16 +286,19 @@ boost::filesystem::path AppUpdater::priv::download_file(const DownloadAppData& d ); if (!res) { - if (m_cancel) - { - BOOST_LOG_TRIVIAL(info) << error_message; //lm:Is this an error? // dk: changed to info + if (m_cancel) { + BOOST_LOG_TRIVIAL(info) << error_message; wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); // FAILED with empty msg only closes progress notification GUI::wxGetApp().QueueEvent(evt); } else { - std::string message = GUI::format("Downloading new %1% has failed:\n%2%", SLIC3R_APP_NAME, error_message); - BOOST_LOG_TRIVIAL(error) << message; + std::string message = (error_message.empty() + ? std::string() + : GUI::format(_utf8("Downloading new %1% has failed:\n%2%"), SLIC3R_APP_NAME, error_message)); wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); - evt->SetString(message); + if (!message.empty()) { + BOOST_LOG_TRIVIAL(error) << message; + evt->SetString(message); + } GUI::wxGetApp().QueueEvent(evt); } return boost::filesystem::path(); @@ -323,6 +353,11 @@ void AppUpdater::priv::parse_version_string(const std::string& body) return; #endif // 0 BOOST_LOG_TRIVIAL(error) << "Could not find property tree in version file. Checking for application update has failed."; + // Lets send event with current version, this way if user triggered this check, it will notify him about no new version online. + std::string version = Semver().to_string(); + wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_VERSION_ONLINE); + evt->SetString(GUI::from_u8(version)); + GUI::wxGetApp().QueueEvent(evt); return; } std::string tree_string = body.substr(start); @@ -358,10 +393,10 @@ void AppUpdater::priv::parse_version_string(const std::string& body) if (data.first == "url") { new_data.url = data.second.data(); new_data.target_path = m_default_dest_folder / AppUpdater::get_filename_from_url(new_data.url); - BOOST_LOG_TRIVIAL(error) << format("parsing version string: url: %1%", new_data.url); + BOOST_LOG_TRIVIAL(info) << format("parsing version string: url: %1%", new_data.url); } else if (data.first == "size"){ new_data.size = std::stoi(data.second.data()); - BOOST_LOG_TRIVIAL(error) << format("parsing version string: expected size: %1%", new_data.size); + BOOST_LOG_TRIVIAL(info) << format("parsing version string: expected size: %1%", new_data.size); } } } diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index f4863ff20..cd3935ea0 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -12,14 +12,17 @@ #include #include #include +#include #include #include +#include #include "libslic3r/libslic3r.h" #include "libslic3r/format.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/PresetBundle.hpp" +#include "libslic3r/miniz_extension.hpp" #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/UpdateDialogs.hpp" @@ -49,7 +52,7 @@ namespace Slic3r { static const char *INDEX_FILENAME = "index.idx"; static const char *TMP_EXTENSION = ".download"; - +namespace { void copy_file_fix(const fs::path &source, const fs::path &target) { BOOST_LOG_TRIVIAL(debug) << format("PresetUpdater: Copying %1% -> %2%", source, target); @@ -66,7 +69,21 @@ void copy_file_fix(const fs::path &source, const fs::path &target) static constexpr const auto perms = fs::owner_read | fs::owner_write | fs::group_read | fs::others_read; fs::permissions(target, perms); } - +std::string escape_string_url(const std::string& unescaped) +{ + std::string ret_val; + CURL* curl = curl_easy_init(); + if (curl) { + char* decoded = curl_easy_escape(curl, unescaped.c_str(), unescaped.size()); + if (decoded) { + ret_val = std::string(decoded); + curl_free(decoded); + } + curl_easy_cleanup(curl); + } + return ret_val; +} +} struct Update { fs::path source; @@ -144,6 +161,7 @@ struct PresetUpdater::priv std::string version_check_url; fs::path cache_path; + fs::path cache_vendor_path; fs::path rsrc_path; fs::path vendor_path; @@ -155,19 +173,25 @@ struct PresetUpdater::priv priv(); - void set_download_prefs(AppConfig *app_config); + void set_download_prefs(const AppConfig *app_config); bool get_file(const std::string &url, const fs::path &target_path) const; void prune_tmps() const; - void sync_config(const VendorMap vendors); + void sync_config(const VendorMap vendors, const std::string& index_archive_url); void check_install_indices() const; Updates get_config_updates(const Semver& old_slic3r_version) const; bool perform_updates(Updates &&updates, bool snapshot = true) const; void set_waiting_updates(Updates u); + // checks existence and downloads resource to cache + void get_missing_resource(const std::string& vendor, const std::string& filename, const std::string& url) const; + // checks existence and downloads resource to vendor or copy from cache to vendor + void get_or_copy_missing_resource(const std::string& vendor, const std::string& filename, const std::string& url) const; + void update_index_db(); }; PresetUpdater::priv::priv() : cache_path(fs::path(Slic3r::data_dir()) / "cache") + , cache_vendor_path(cache_path / "vendor") , rsrc_path(fs::path(resources_dir()) / "profiles") , vendor_path(fs::path(Slic3r::data_dir()) / "vendor") , cancel(false) @@ -179,8 +203,13 @@ PresetUpdater::priv::priv() index_db = Index::load_db(); } +void PresetUpdater::priv::update_index_db() +{ + index_db = Index::load_db(); +} + // Pull relevant preferences from AppConfig -void PresetUpdater::priv::set_download_prefs(AppConfig *app_config) +void PresetUpdater::priv::set_download_prefs(const AppConfig *app_config) { enabled_version_check = app_config->get("notify_release") != "none"; version_check_url = app_config->version_check_url(); @@ -232,46 +261,208 @@ void PresetUpdater::priv::prune_tmps() const } } +void PresetUpdater::priv::get_missing_resource(const std::string& vendor, const std::string& filename, const std::string& url) const +{ + if (filename.empty() || vendor.empty()) + return; + + if (!boost::starts_with(url, "http://files.prusa3d.com/wp-content/uploads/repository/") && + !boost::starts_with(url, "https://files.prusa3d.com/wp-content/uploads/repository/")) + { + throw Slic3r::CriticalException(GUI::format("URL outside prusa3d.com network: %1%", url)); + } + + std::string escaped_filename = escape_string_url(filename); + const fs::path file_in_vendor(vendor_path / (vendor + "/" + filename)); + const fs::path file_in_rsrc(rsrc_path / (vendor + "/" + filename)); + const fs::path file_in_cache(cache_path / (vendor + "/" + filename)); + + if (fs::exists(file_in_vendor)) { // Already in vendor. No need to do anything. + BOOST_LOG_TRIVIAL(info) << "Resource " << vendor << " / " << filename << " found in vendor folder. No need to download."; + return; + } + if (fs::exists(file_in_rsrc)) { // In resources dir since installation. No need to do anything. + BOOST_LOG_TRIVIAL(info) << "Resource " << vendor << " / " << filename << " found in resources folder. No need to download."; + return; + } + if (fs::exists(file_in_cache)) { // In cache/venodr_name/ dir. No need to do anything. + BOOST_LOG_TRIVIAL(info) << "Resource " << vendor << " / " << filename << " found in cache folder. No need to download."; + return; + } + + BOOST_LOG_TRIVIAL(info) << "Resources check could not find " << vendor << " / " << filename << " bed texture. Downloading."; + + const auto resource_url = format("%1%%2%%3%", url, url.back() == '/' ? "" : "/", escaped_filename); // vendor should already be in url + + if (!fs::exists(file_in_cache.parent_path())) + fs::create_directory(file_in_cache.parent_path()); + + get_file(resource_url, file_in_cache); + return; +} + +void PresetUpdater::priv::get_or_copy_missing_resource(const std::string& vendor, const std::string& filename, const std::string& url) const +{ + if (filename.empty() || vendor.empty()) + return; + + std::string escaped_filename = escape_string_url(filename); + const fs::path file_in_vendor(vendor_path / (vendor + "/" + filename)); + const fs::path file_in_rsrc(rsrc_path / (vendor + "/" + filename)); + const fs::path file_in_cache(cache_path / (vendor + "/" + filename)); + + if (fs::exists(file_in_vendor)) { // Already in vendor. No need to do anything. + BOOST_LOG_TRIVIAL(info) << "Resource " << vendor << " / " << filename << " found in vendor folder. No need to download."; + return; + } + if (fs::exists(file_in_rsrc)) { // In resources dir since installation. No need to do anything. + BOOST_LOG_TRIVIAL(info) << "Resource " << vendor << " / " << filename << " found in resources folder. No need to download."; + return; + } + if (!fs::exists(file_in_cache)) { // No file to copy. Download it to straight to the vendor dir. + if (!boost::starts_with(url, "http://files.prusa3d.com/wp-content/uploads/repository/") && + !boost::starts_with(url, "https://files.prusa3d.com/wp-content/uploads/repository/")) + { + throw Slic3r::CriticalException(GUI::format("URL outside prusa3d.com network: %1%", url)); + } + BOOST_LOG_TRIVIAL(info) << "Downloading resources missing in cache directory: " << vendor << " / " << filename; + + const auto resource_url = format("%1%%2%%3%", url, url.back() == '/' ? "" : "/", escaped_filename); // vendor should already be in url + + if (!fs::exists(file_in_vendor.parent_path())) + fs::create_directory(file_in_vendor.parent_path()); + + get_file(resource_url, file_in_vendor); + return; + } + + if (!fs::exists(file_in_vendor.parent_path())) // create vendor_name dir in vendor + fs::create_directory(file_in_vendor.parent_path()); + + BOOST_LOG_TRIVIAL(debug) << "Copiing: " << file_in_cache << " to " << file_in_vendor; + copy_file_fix(file_in_cache, file_in_vendor); +} + // Download vendor indices. Also download new bundles if an index indicates there's a new one available. // Both are saved in cache. -void PresetUpdater::priv::sync_config(const VendorMap vendors) +void PresetUpdater::priv::sync_config(const VendorMap vendors, const std::string& index_archive_url) { BOOST_LOG_TRIVIAL(info) << "Syncing configuration cache"; if (!enabled_config_update) { return; } - // Donwload vendor preset bundles + // Download profiles archive zip + // dk: Do we want to return here on error? Or skip archive dwnld and unzip and work with previous run state cache / vendor? I think return. + // Any error here also doesnt show any info in UI. Do we want maybe notification? + fs::path archive_path(cache_path / "vendor_indices.zip"); + if (index_archive_url.empty()) { + BOOST_LOG_TRIVIAL(error) << "Downloading profile archive failed - url has no value."; + return; + } + BOOST_LOG_TRIVIAL(info) << "Downloading vedor profiles archive zip from " << index_archive_url; + //check if idx_url is leading to our site + if (!boost::starts_with(index_archive_url, "http://files.prusa3d.com/wp-content/uploads/repository/") && + !boost::starts_with(index_archive_url, "https://files.prusa3d.com/wp-content/uploads/repository/")) + { + BOOST_LOG_TRIVIAL(error) << "Unsafe url path for vedor profiles archive zip. Download is rejected."; + return; + } + if (!get_file(index_archive_url, archive_path)) { + BOOST_LOG_TRIVIAL(error) << "Download of vedor profiles archive zip failed."; + return; + } + if (cancel) { + return; + } + + enum class VendorStatus + { + IN_ARCHIVE, + IN_CACHE, + NEW_VERSION, + INSTALLED + }; + + std::vector> vendors_with_status; + // Unzip archive to cache / vendor + mz_zip_archive archive; + mz_zip_zero_struct(&archive); + if (!open_zip_reader(&archive, archive_path.string())) { + BOOST_LOG_TRIVIAL(error) << "Couldn't open zipped bundle."; + return; + } else { + mz_uint num_entries = mz_zip_reader_get_num_files(&archive); + // loop the entries + mz_zip_archive_file_stat stat; + for (mz_uint i = 0; i < num_entries; ++i) { + if (mz_zip_reader_file_stat(&archive, i, &stat)) { + std::string name(stat.m_filename); + if (stat.m_uncomp_size > 0) { + std::string buffer((size_t)stat.m_uncomp_size, 0); + mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); + if (res == 0) { + BOOST_LOG_TRIVIAL(error) << "Failed to unzip " << stat.m_filename; + continue; + } + // create file from buffer + fs::path tmp_path(cache_vendor_path / (name + ".tmp")); + if (!fs::exists(tmp_path.parent_path())) { + BOOST_LOG_TRIVIAL(error) << "Failed to unzip file " << name << ". Directories are not supported. Skipping file."; + continue; + } + fs::path target_path(cache_vendor_path / name); + fs::fstream file(tmp_path, std::ios::out | std::ios::binary | std::ios::trunc); + file.write(buffer.c_str(), buffer.size()); + file.close(); + boost::system::error_code ec; + bool exists = fs::exists(tmp_path, ec); + if(!exists || ec) { + BOOST_LOG_TRIVIAL(error) << "Failed to find unzipped file at " << tmp_path << ". Terminating Preset updater synchorinzation." ; + close_zip_reader(&archive); + return; + } + fs::rename(tmp_path, target_path, ec); + if (ec) { + BOOST_LOG_TRIVIAL(error) << "Failed to rename unzipped file at " << tmp_path << ". Terminating Preset updater synchorinzation. Error message: " << ec.message(); + close_zip_reader(&archive); + return; + } + // TODO: what if unexpected happens here (folder inside zip) - crash! + + if (name.substr(name.size() - 3) == "idx") + vendors_with_status.emplace_back(name.substr(0, name.size() - 4), VendorStatus::IN_ARCHIVE); // asume for now its only in archive - if not, it will change later. + } + } + } + close_zip_reader(&archive); + } + + // Update vendor preset bundles if in Vendor // Over all indices from the cache directory: for (auto &index : index_db) { - if (cancel) { return; } + if (cancel) { + return; + } + auto archive_it = std::find_if(vendors_with_status.begin(), vendors_with_status.end(), + [&index](const std::pair& element) { return element.first == index.vendor(); }); + //assert(archive_it != vendors_with_status.end()); // this would mean there is a index for vendor that is missing in recently downloaded archive const auto vendor_it = vendors.find(index.vendor()); if (vendor_it == vendors.end()) { - BOOST_LOG_TRIVIAL(warning) << "No such vendor: " << index.vendor(); + // Not installed vendor yet we need to check missing thumbnails (of new printers) + BOOST_LOG_TRIVIAL(debug) << "No such vendor: " << index.vendor(); + if (archive_it != vendors_with_status.end()) + archive_it->second = VendorStatus::IN_CACHE; continue; } + if (archive_it != vendors_with_status.end()) + archive_it->second = VendorStatus::INSTALLED; + const VendorProfile &vendor = vendor_it->second; - if (vendor.config_update_url.empty()) { - BOOST_LOG_TRIVIAL(info) << "Vendor has no config_update_url: " << vendor.name; - continue; - } - - // Download a fresh index - BOOST_LOG_TRIVIAL(info) << "Downloading index for vendor: " << vendor.name; - const auto idx_url = vendor.config_update_url + "/" + INDEX_FILENAME; const std::string idx_path = (cache_path / (vendor.id + ".idx")).string(); - const std::string idx_path_temp = idx_path + "-update"; - //check if idx_url is leading to our site - if (! boost::starts_with(idx_url, "http://files.prusa3d.com/wp-content/uploads/repository/") && - ! boost::starts_with(idx_url, "https://files.prusa3d.com/wp-content/uploads/repository/")) - { - BOOST_LOG_TRIVIAL(warning) << "unsafe url path for vendor \"" << vendor.name << "\" rejected: " << idx_url; - continue; - } - if (!get_file(idx_url, idx_path_temp)) { continue; } - if (cancel) { return; } - + const std::string idx_path_temp = (cache_vendor_path / (vendor.id + ".idx")).string(); + // Load the fresh index up { Index new_index; @@ -282,10 +473,11 @@ void PresetUpdater::priv::sync_config(const VendorMap vendors) continue; } if (new_index.version() < index.version()) { - BOOST_LOG_TRIVIAL(warning) << format("The downloaded index %1% for vendor %2% is older than the active one. Ignoring the downloaded index.", idx_path_temp, vendor.name); + BOOST_LOG_TRIVIAL(info) << format("The downloaded index %1% for vendor %2% is older than the active one. Ignoring the downloaded index.", idx_path_temp, vendor.name); continue; } - Slic3r::rename_file(idx_path_temp, idx_path); + copy_file_fix(idx_path_temp, idx_path); + //if we rename path we need to change it in Index object too or create the object again //index = std::move(new_index); try { @@ -315,12 +507,265 @@ void PresetUpdater::priv::sync_config(const VendorMap vendors) if (vendor.config_version >= recommended) { continue; } - // Download a fresh bundle + // vendors that are checked here, doesnt need to be checked again later + if (archive_it != vendors_with_status.end()) + archive_it->second = VendorStatus::NEW_VERSION; + + // Download recomended ini to cache + const auto path_in_cache = cache_path / (vendor.id + ".ini"); BOOST_LOG_TRIVIAL(info) << "Downloading new bundle for vendor: " << vendor.name; const auto bundle_url = format("%1%/%2%.ini", vendor.config_update_url, recommended.to_string()); const auto bundle_path = cache_path / (vendor.id + ".ini"); - if (! get_file(bundle_url, bundle_path)) { continue; } - if (cancel) { return; } + if (!get_file(bundle_url, bundle_path)) + continue; + if (cancel) + return; + // vp is fully loaded to get all resources + VendorProfile vp; + try { + vp = VendorProfile::from_ini(bundle_path, true); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.id, bundle_path, e.what()); + continue; + } + // check the fresh bundle for missing resources + // for that, the ini file must be parsed (done above) + for (const auto& model : vp.models) { + for (const std::string& res : { model.bed_texture, model.bed_model, model.thumbnail/*id +"_thumbnail.png"*/} ) { + if (! res.empty()) { + try + { + get_missing_resource(vp.id, res, vendor.config_update_url); + } + catch (const std::exception& e) + { + BOOST_LOG_TRIVIAL(error) << "Failed to get " << res << " for " << vp.id << " " << model.id << ": " << e.what(); + } + + } + if (cancel) + return; + } + } + } + // Download missing thumbnails for not-installed vendors. + //for (const std::string& vendor : vendors_only_in_archive) + for (const std::pair& vendor : vendors_with_status) { + if (vendor.second == VendorStatus::IN_ARCHIVE) { + // index in archive and not in cache and not installed vendor + + const auto idx_path_in_archive = cache_vendor_path / (vendor.first + ".idx"); + const auto ini_path_in_archive = cache_vendor_path / (vendor.first + ".ini"); + if (!fs::exists(idx_path_in_archive)) + continue; + Index index; + try { + index.load(idx_path_in_archive); + } + catch (const std::exception& /* err */) { + BOOST_LOG_TRIVIAL(error) << format("Could not load downloaded index %1% for vendor %2%: invalid index?", idx_path_in_archive, vendor.first); + continue; + } + const auto recommended_it = index.recommended(); + if (recommended_it == index.end()) { + BOOST_LOG_TRIVIAL(error) << format("No recommended version for vendor: %1%, invalid index? (%2%)", vendor.first, idx_path_in_archive); + continue; + } + const auto recommended = recommended_it->config_version; + if (!fs::exists(ini_path_in_archive)){ + // Download recommneded to vendor - we do not have any existing ini file so we have to use hardcoded url. + const std::string fixed_url = GUI::wxGetApp().app_config->profile_folder_url(); + const auto bundle_url = format("%1%/%2%/%3%.ini", fixed_url, vendor.first, recommended.to_string()); + if (!get_file(bundle_url, ini_path_in_archive)) + continue; + } else { + // check existing ini version + // then download recommneded to vendor if needed + VendorProfile vp; + try { + vp = VendorProfile::from_ini(ini_path_in_archive, true); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.first, ini_path_in_archive, e.what()); + continue; + } + if (vp.config_version != recommended) { + const std::string fixed_url = GUI::wxGetApp().app_config->profile_folder_url(); + const auto bundle_url = format("%1%/%2%/%3%.ini", fixed_url, vendor.first, recommended.to_string()); + if (!get_file(bundle_url, ini_path_in_archive)) + continue; + } + } + // check missing thumbnails + VendorProfile vp; + try { + vp = VendorProfile::from_ini(ini_path_in_archive, true); + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.first, ini_path_in_archive, e.what()); + continue; + } + for (const auto& model : vp.models) { + if (!model.thumbnail.empty()) { + try + { + get_missing_resource(vp.id, model.thumbnail, vp.config_update_url); + } + catch (const std::exception& e) + { + BOOST_LOG_TRIVIAL(error) << "Failed to get " << model.thumbnail << " for " << vp.id << " " << model.id << ": " << e.what(); + } + } + if (cancel) + return; + } + } else if (vendor.second == VendorStatus::IN_CACHE) { + // find those where archive index recommends other version than index in cache and get it if not present + const auto idx_path_in_archive = cache_vendor_path / (vendor.first + ".idx"); + const auto ini_path_in_archive = cache_vendor_path / (vendor.first + ".ini"); + const auto idx_path_in_cache = cache_path / (vendor.first + ".idx"); + + if (!fs::exists(idx_path_in_archive) || !fs::exists(idx_path_in_cache)) + continue; + + // Compare index in cache and recetly downloaded one as part of zip archive + Index index_cache; + try { + index_cache.load(idx_path_in_cache); + } + catch (const std::exception& /* err */) { + BOOST_LOG_TRIVIAL(error) << format("Could not load downloaded index %1% for vendor %2%: invalid index?", idx_path_in_cache, vendor.first); + continue; + } + const auto recommended_it_cache = index_cache.recommended(); + if (recommended_it_cache == index_cache.end()) { + BOOST_LOG_TRIVIAL(error) << format("No recommended version for vendor: %1%, invalid index? (%2%)", vendor.first, idx_path_in_cache); + continue; + } + const auto recommended_cache = recommended_it_cache->config_version; + + Index index_archive; + try { + index_archive.load(idx_path_in_archive); + } + catch (const std::exception& /* err */) { + BOOST_LOG_TRIVIAL(error) << format("Could not load downloaded index %1% for vendor %2%: invalid index?", idx_path_in_archive, vendor.first); + continue; + } + const auto recommended_it_archive = index_archive.recommended(); + if (recommended_it_archive == index_archive.end()) { + BOOST_LOG_TRIVIAL(error) << format("No recommended version for vendor: %1%, invalid index? (%2%)", vendor.first, idx_path_in_archive); + continue; + } + const auto recommended_archive = recommended_it_archive->config_version; + + if (recommended_archive <= recommended_cache) { + // There isn't more recent recomended version online. This vendor is also not istalled. + // Thus only .ini is in resources and came with installation. + // And we expect all resources are present. + continue; + } + + // Download new .ini if needed. So next time user runs Wizard, most recent profiles are shown & installed. + if (!fs::exists(ini_path_in_archive) || fs::is_empty(ini_path_in_archive)) { + // download recommneded to vendor + const fs::path ini_path_in_rsrc = rsrc_path / (vendor.first + ".ini"); + if (!fs::exists(ini_path_in_rsrc)) { + // THIS SHOULD NOT HAPPEN + continue; + } + // Get download path from existing ini. + VendorProfile vp; + try { + vp = VendorProfile::from_ini(ini_path_in_rsrc, false); + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.first, ini_path_in_rsrc, e.what()); + continue; + } + const auto bundle_url = format("%1%/%2%.ini", vp.config_update_url, recommended_archive.to_string()); + if (!get_file(bundle_url, ini_path_in_archive)) { + BOOST_LOG_TRIVIAL(error) << format("Failed to open vendor .ini file when checking missing resources: %1%", ini_path_in_rsrc); + continue; + } + } else { + // Check existing ini version. + // Then download recommneded to vendor if needed. + VendorProfile vp; + try { + vp = VendorProfile::from_ini(ini_path_in_archive, false); + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.first, ini_path_in_archive, e.what()); + continue; + } + if (vp.config_version != recommended_archive) { + const auto bundle_url = format("%1%/%2%.ini", vp.config_update_url, recommended_archive.to_string()); + if (!get_file(bundle_url, ini_path_in_archive)) { + BOOST_LOG_TRIVIAL(error) << format("Failed to open vendor .ini file when checking missing resources: %1%", ini_path_in_archive); + continue; + } + } + } + + if (!fs::exists(ini_path_in_archive)) { + BOOST_LOG_TRIVIAL(error) << "Resources check failed to find ini file for vendor: " << vendor.first; + continue; + } + // check missing thumbnails + VendorProfile vp; + try { + vp = VendorProfile::from_ini(ini_path_in_archive, true); + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.first, ini_path_in_archive, e.what()); + continue; + } + for (const auto& model : vp.models) { + if (!model.thumbnail.empty()) { + try + { + get_missing_resource(vp.id, model.thumbnail, vp.config_update_url); + } + catch (const std::exception& e) + { + BOOST_LOG_TRIVIAL(error) << "Failed to get " << model.thumbnail << " for " << vp.id << " " << model.id << ": " << e.what(); + } + } + if (cancel) + return; + } + } else if (vendor.second == VendorStatus::INSTALLED || vendor.second == VendorStatus::NEW_VERSION) { + // Installed vendors need to check that no resource is missing. Do this only for files in vendor folder (not in resorces) + // VendorStatus::NEW_VERSION might seem like a mistake here since files are downloaded when preparing update higher in this function. + // But this is a check for ini file in vendor where resources might be still missing since last update. + const auto path_in_vendor = vendor_path / (vendor.first + ".ini"); + if(!fs::exists(path_in_vendor)) + continue; + VendorProfile vp; + try { + vp = VendorProfile::from_ini(path_in_vendor, true); + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.first, path_in_vendor, e.what()); + continue; + } + for (const auto& model : vp.models) { + for (const std::string& res : { model.bed_texture, model.bed_model, model.thumbnail }) { + if (!model.thumbnail.empty()) { + try + { + get_or_copy_missing_resource(vp.id, res, vp.config_update_url); + } + catch (const std::exception& e) + { + BOOST_LOG_TRIVIAL(error) << "Failed to get " << model.thumbnail << " for " << vp.id << " " << model.id << ": " << e.what(); + } + } + if (cancel) + return; + } + } + } } } @@ -370,8 +815,14 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version } // Perform a basic load and check the version of the installed preset bundle. - auto vp = VendorProfile::from_ini(bundle_path, false); - + VendorProfile vp; + try { + vp = VendorProfile::from_ini(bundle_path, false); + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", idx.vendor(), bundle_path, e.what()); + continue; + } // Getting a recommended version from the latest index, wich may have been downloaded // from the internet, or installed / updated from the installation resources. auto recommended = idx.recommended(); @@ -401,7 +852,7 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version bool current_not_supported = false; //if slcr is incompatible but situation is not downgrade, we do forced updated and this bool is information to do it if (ver_current_found && !ver_current->is_current_slic3r_supported()){ - if(ver_current->is_current_slic3r_downgrade()) { + if (ver_current->is_current_slic3r_downgrade()) { // "Reconfigure" situation. BOOST_LOG_TRIVIAL(warning) << "Current Slic3r incompatible with installed bundle: " << bundle_path.string(); updates.incompats.emplace_back(std::move(bundle_path), *ver_current, vp.name); @@ -443,10 +894,13 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version fs::path bundle_path_idx_to_install; if (fs::exists(path_in_cache)) { try { - VendorProfile new_vp = VendorProfile::from_ini(path_in_cache, false); + VendorProfile new_vp = VendorProfile::from_ini(path_in_cache, true); if (new_vp.config_version == recommended->config_version) { // The config bundle from the cache directory matches the recommended version of the index from the cache directory. // This is the newest known recommended config. Use it. + if (!PresetUtils::vendor_profile_has_all_resources(new_vp)) { + BOOST_LOG_TRIVIAL(warning) << "Some resources are missing for update of vedor " << new_vp.id; + } new_update = Update(std::move(path_in_cache), std::move(bundle_path), *recommended, vp.name, vp.changelog_url, current_not_supported); // and install the config index from the cache into vendor's directory. bundle_path_idx_to_install = idx.path(); @@ -564,6 +1018,9 @@ bool PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) cons BOOST_LOG_TRIVIAL(info) << format("Performing %1% updates", updates.updates.size()); + wxProgressDialog progress_dialog(_L("Installing profiles"), _L("Installing profiles") , 100, nullptr, wxPD_AUTO_HIDE); + progress_dialog.Pulse(); + for (const auto &update : updates.updates) { BOOST_LOG_TRIVIAL(info) << '\t' << update; @@ -601,12 +1058,40 @@ bool PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) cons for (const auto &name : bundle.obsolete_presets.sla_prints) { obsolete_remover("sla_print", name); } for (const auto &name : bundle.obsolete_presets.sla_materials/*filaments*/) { obsolete_remover("sla_material", name); } for (const auto &name : bundle.obsolete_presets.printers) { obsolete_remover("printer", name); } + + // check if any resorces of installed bundle are missing. If so, new ones should be already downloaded at cache/vendor_id/ + VendorProfile vp; + try { + vp = VendorProfile::from_ini(update.target, true); + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1%, message: %2%", update.target, e.what()); + continue; + } + progress_dialog.Update(1, GUI::format_wxstr(_L("Downloading resources for %1%."),vp.id)); + progress_dialog.Pulse(); + for (const auto& model : vp.models) { + for (const std::string& resource : { model.bed_texture, model.bed_model, model.thumbnail }) { + if (resource.empty()) + continue; + try + { + get_or_copy_missing_resource(vp.id, resource, vp.config_update_url); + } + catch (const std::exception& e) + { + BOOST_LOG_TRIVIAL(error) << "Failed to prepare " << resource << " for " << vp.id << " " << model.id << ": " << e.what(); + } + } + } } + + progress_dialog.Destroy(); } return true; } - + void PresetUpdater::priv::set_waiting_updates(Updates u) { waiting_updates = u; @@ -630,7 +1115,7 @@ PresetUpdater::~PresetUpdater() } } -void PresetUpdater::sync(PresetBundle *preset_bundle) +void PresetUpdater::sync(const PresetBundle *preset_bundle) { p->set_download_prefs(GUI::wxGetApp().app_config); if (!p->enabled_version_check && !p->enabled_config_update) { return; } @@ -639,13 +1124,24 @@ void PresetUpdater::sync(PresetBundle *preset_bundle) // Unfortunatelly as of C++11, it needs to be copied again // into the closure (but perhaps the compiler can elide this). VendorMap vendors = preset_bundle->vendors; + std::string index_archive_url = GUI::wxGetApp().app_config->index_archive_url(); - p->thread = std::thread([this, vendors]() { + p->thread = std::thread([this, vendors, index_archive_url]() { this->p->prune_tmps(); - this->p->sync_config(std::move(vendors)); + this->p->sync_config(std::move(vendors), index_archive_url); }); } +void PresetUpdater::cancel_sync() +{ + if (p && p->thread.joinable()) { + // This will stop transfers being done by the thread, if any. + // Cancelling takes some time, but should complete soon enough. + p->cancel = true; + p->thread.join(); + } +} + void PresetUpdater::slic3r_update_notify() { if (! p->enabled_version_check) @@ -810,7 +1306,7 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3 return R_NOOP; } -bool PresetUpdater::install_bundles_rsrc(std::vector bundles, bool snapshot) const +bool PresetUpdater::install_bundles_rsrc_or_cache_vendor(std::vector bundles, bool snapshot) const { Updates updates; @@ -818,8 +1314,66 @@ bool PresetUpdater::install_bundles_rsrc(std::vector bundles, bool for (const auto &bundle : bundles) { auto path_in_rsrc = (p->rsrc_path / bundle).replace_extension(".ini"); + auto path_in_cache_vendor = (p->cache_vendor_path / bundle).replace_extension(".ini"); auto path_in_vendors = (p->vendor_path / bundle).replace_extension(".ini"); - updates.updates.emplace_back(std::move(path_in_rsrc), std::move(path_in_vendors), Version(), "", ""); + + bool is_in_rsrc = fs::exists(path_in_rsrc); + bool is_in_cache_vendor = fs::exists(path_in_cache_vendor) && !fs::is_empty(path_in_cache_vendor); + + // find if in cache vendor is newer version than in resources + if (is_in_cache_vendor) { + Semver version_cache = Semver::zero(); + try { + auto vp_cache = VendorProfile::from_ini(path_in_cache_vendor, false); + version_cache = vp_cache.config_version; + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1%, message: %2%", path_in_cache_vendor, e.what()); + // lets use file in resources + if (is_in_rsrc) { + updates.updates.emplace_back(std::move(path_in_rsrc), std::move(path_in_vendors), Version(), "", ""); + } + continue; + } + + Semver version_rsrc = Semver::zero(); + try { + if (is_in_rsrc) { + auto vp = VendorProfile::from_ini(path_in_rsrc, false); + version_rsrc = vp.config_version; + } + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1%, message: %2%", path_in_rsrc, e.what()); + continue; + } + + if (!is_in_rsrc || version_cache > version_rsrc) { + // in case we are installing from cache / vendor. we should also copy index to cache + // This needs to be done now bcs the current one would be missing this version on next start + // dk: Should we copy it to vendor dir too? + auto path_idx_cache_vendor(path_in_cache_vendor); + path_idx_cache_vendor.replace_extension(".idx"); + auto path_idx_cache = (p->cache_path / bundle).replace_extension(".idx"); + // DK: do this during perform_updates() too? + if (fs::exists(path_idx_cache_vendor)) + copy_file_fix(path_idx_cache_vendor, path_idx_cache); + else // Should we dialog this? + BOOST_LOG_TRIVIAL(error) << GUI::format(_L("Couldn't locate idx file %1% when performing updates."), path_idx_cache_vendor.string()); + updates.updates.emplace_back(std::move(path_in_cache_vendor), std::move(path_in_vendors), Version(), "", ""); + + } else { + if (is_in_rsrc) + updates.updates.emplace_back(std::move(path_in_rsrc), std::move(path_in_vendors), Version(), "", ""); + } + } else { + if (! is_in_rsrc) { + // This should not happen. Instead of an assert, make it crash in Release mode too. + BOOST_LOG_TRIVIAL(error) << "Internal error in PresetUpdater! Terminating the application."; + std::terminate(); + } + updates.updates.emplace_back(std::move(path_in_rsrc), std::move(path_in_vendors), Version(), "", ""); + } } return p->perform_updates(std::move(updates), snapshot); @@ -857,4 +1411,9 @@ bool PresetUpdater::version_check_enabled() const return p->enabled_version_check; } +void PresetUpdater::update_index_db() +{ + p->update_index_db(); +} + } diff --git a/src/slic3r/Utils/PresetUpdater.hpp b/src/slic3r/Utils/PresetUpdater.hpp index 974a7dcfa..85875e5a3 100644 --- a/src/slic3r/Utils/PresetUpdater.hpp +++ b/src/slic3r/Utils/PresetUpdater.hpp @@ -26,7 +26,8 @@ public: ~PresetUpdater(); // If either version check or config updating is enabled, get the appropriate data in the background and cache it. - void sync(PresetBundle *preset_bundle); + void sync(const PresetBundle *preset_bundle); + void cancel_sync(); // If version check is enabled, check if chaced online slic3r version is newer, notify if so. void slic3r_update_notify(); @@ -52,9 +53,11 @@ public: // Providing old slic3r version upgrade profiles on upgrade of an application even in case // that the config index installed from the Internet is equal to the index contained in the installation package. UpdateResult config_update(const Semver &old_slic3r_version, UpdateParams params) const; + + void update_index_db(); - // "Update" a list of bundles from resources (behaves like an online update). - bool install_bundles_rsrc(std::vector bundles, bool snapshot = true) const; + // "Update" a list of bundles from resources or cache/vendor (behaves like an online update). + bool install_bundles_rsrc_or_cache_vendor(std::vector bundles, bool snapshot = true) const; void on_update_notification_confirm(); diff --git a/tests/data/U_overhang.obj b/tests/data/U_overhang.obj new file mode 100644 index 000000000..70453e58f --- /dev/null +++ b/tests/data/U_overhang.obj @@ -0,0 +1,76 @@ +#### +# +# OBJ File Generated by Meshlab +# +#### +# Object U_overhang.obj +# +# Vertices: 16 +# Faces: 28 +# +#### +vn 1.570797 1.570796 1.570796 +v 10.000000 10.000000 11.000000 +vn 4.712389 1.570796 -1.570796 +v 10.000000 1.000000 10.000000 +vn 1.570796 1.570796 -1.570796 +v 10.000000 10.000000 10.000000 +vn 1.570796 -1.570796 1.570796 +v 10.000000 0.000000 11.000000 +vn 4.712389 1.570796 1.570796 +v 10.000000 1.000000 1.000000 +vn 1.570797 1.570796 -1.570796 +v 10.000000 10.000000 0.000000 +vn 1.570796 1.570796 1.570796 +v 10.000000 10.000000 1.000000 +vn 1.570796 -1.570796 -1.570796 +v 10.000000 0.000000 0.000000 +vn -1.570796 1.570796 1.570796 +v 0.000000 10.000000 1.000000 +vn -4.712389 1.570796 1.570796 +v 0.000000 1.000000 1.000000 +vn -1.570796 -1.570796 -1.570796 +v 0.000000 0.000000 0.000000 +vn -1.570797 1.570796 -1.570796 +v 0.000000 10.000000 0.000000 +vn -4.712389 1.570796 -1.570796 +v 0.000000 1.000000 10.000000 +vn -1.570797 1.570796 1.570796 +v 0.000000 10.000000 11.000000 +vn -1.570796 1.570796 -1.570796 +v 0.000000 10.000000 10.000000 +vn -1.570796 -1.570796 1.570796 +v 0.000000 0.000000 11.000000 +# 16 vertices, 0 vertices normals + +f 1//1 2//2 3//3 +f 2//2 4//4 5//5 +f 4//4 2//2 1//1 +f 5//5 6//6 7//7 +f 5//5 8//8 6//6 +f 8//8 5//5 4//4 +f 9//9 5//5 7//7 +f 5//5 9//9 10//10 +f 11//11 6//6 8//8 +f 6//6 11//11 12//12 +f 12//12 10//10 9//9 +f 10//10 11//11 13//13 +f 11//11 10//10 12//12 +f 13//13 14//14 15//15 +f 13//13 16//16 14//14 +f 16//16 13//13 11//11 +f 6//6 9//9 7//7 +f 9//9 6//6 12//12 +f 11//11 4//4 16//16 +f 4//4 11//11 8//8 +f 13//13 3//3 2//2 +f 3//3 13//13 15//15 +f 5//5 13//13 2//2 +f 13//13 5//5 10//10 +f 14//14 4//4 1//1 +f 4//4 14//14 16//16 +f 3//3 14//14 1//1 +f 14//14 3//3 15//15 +# 28 faces, 0 coords texture + +# End of File diff --git a/tests/fff_print/test_multi.cpp b/tests/fff_print/test_multi.cpp index 03c7f644b..90f311c39 100644 --- a/tests/fff_print/test_multi.cpp +++ b/tests/fff_print/test_multi.cpp @@ -108,18 +108,18 @@ SCENARIO("Ooze prevention", "[Multi]") Polygon convex_hull = Geometry::convex_hull(extrusion_points); - THEN("all nozzles are outside skirt at toolchange") { - Points t; - sort_remove_duplicates(toolchange_points); - size_t inside = 0; - for (const auto &point : toolchange_points) - for (const Vec2d &offset : print_config.extruder_offset.values) { - Point p = point + scaled(offset); - if (convex_hull.contains(p)) - ++ inside; - } - REQUIRE(inside == 0); - } + // THEN("all nozzles are outside skirt at toolchange") { + // Points t; + // sort_remove_duplicates(toolchange_points); + // size_t inside = 0; + // for (const auto &point : toolchange_points) + // for (const Vec2d &offset : print_config.extruder_offset.values) { + // Point p = point + scaled(offset); + // if (convex_hull.contains(p)) + // ++ inside; + // } + // REQUIRE(inside == 0); + // } #if 0 require "Slic3r/SVG.pm"; diff --git a/tests/sla_print/CMakeLists.txt b/tests/sla_print/CMakeLists.txt index 24e9552c5..2a800cc50 100644 --- a/tests/sla_print/CMakeLists.txt +++ b/tests/sla_print/CMakeLists.txt @@ -4,6 +4,7 @@ add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp sla_test_utils.hpp sla_test_utils.cpp sla_supptgen_tests.cpp sla_raycast_tests.cpp + sla_supptreeutils_tests.cpp sla_archive_readwrite_tests.cpp) # mold linker for successful linking needs also to link TBB library and link it before libslic3r. diff --git a/tests/sla_print/sla_print_tests.cpp b/tests/sla_print/sla_print_tests.cpp index 8ea91d57a..a733e77cc 100644 --- a/tests/sla_print/sla_print_tests.cpp +++ b/tests/sla_print/sla_print_tests.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -32,13 +31,6 @@ const char *const SUPPORT_TEST_MODELS[] = { } // namespace -TEST_CASE("Pillar pairhash should be unique", "[SLASupportGeneration]") { - test_pairhash(); - test_pairhash(); - test_pairhash(); - test_pairhash(); -} - TEST_CASE("Support point generator should be deterministic if seeded", "[SLASupportGeneration], [SLAPointGen]") { TriangleMesh mesh = load_model("A_upsidedown.obj"); @@ -173,106 +165,6 @@ TEST_CASE("DefaultSupports::FloorSupportsDoNotPierceModel", "[SLASupportGenerati // for (auto &fname: SUPPORT_TEST_MODELS) test_supports(fname, supportcfg); //} -bool is_outside_support_cone(const Vec3f &supp, const Vec3f &pt, float angle) -{ - Vec3d D = (pt - supp).cast(); - double dot_sq = -D.z() * std::abs(-D.z()); - - return dot_sq < - D.squaredNorm() * std::cos(angle) * std::abs(std::cos(angle)); -} - -TEST_CASE("BranchingSupports::MergePointFinder", "[SLASupportGeneration][Branching]") { - SECTION("Identical points have the same merge point") { - Vec3f a{0.f, 0.f, 0.f}, b = a; - auto slope = float(PI / 4.); - - auto mergept = branchingtree::find_merge_pt(a, b, slope); - - REQUIRE(bool(mergept)); - REQUIRE((*mergept - b).norm() < EPSILON); - REQUIRE((*mergept - a).norm() < EPSILON); - } - - // ^ Z - // | a * - // | - // | b * <= mergept - SECTION("Points at different heights have the lower point as mergepoint") { - Vec3f a{0.f, 0.f, 0.f}, b = {0.f, 0.f, -1.f}; - auto slope = float(PI / 4.); - - auto mergept = branchingtree::find_merge_pt(a, b, slope); - - REQUIRE(bool(mergept)); - REQUIRE((*mergept - b).squaredNorm() < 2 * EPSILON); - } - - // -|---------> X - // a b - // * * - // * <= mergept - SECTION("Points at different X have mergept in the middle at lower Z") { - Vec3f a{0.f, 0.f, 0.f}, b = {1.f, 0.f, 0.f}; - auto slope = float(PI / 4.); - - auto mergept = branchingtree::find_merge_pt(a, b, slope); - - REQUIRE(bool(mergept)); - - // Distance of mergept should be equal from both input points - float D = std::abs((*mergept - b).squaredNorm() - (*mergept - a).squaredNorm()); - - REQUIRE(D < EPSILON); - REQUIRE(!is_outside_support_cone(a, *mergept, slope)); - REQUIRE(!is_outside_support_cone(b, *mergept, slope)); - } - - // -|---------> Y - // a b - // * * - // * <= mergept - SECTION("Points at different Y have mergept in the middle at lower Z") { - Vec3f a{0.f, 0.f, 0.f}, b = {0.f, 1.f, 0.f}; - auto slope = float(PI / 4.); - - auto mergept = branchingtree::find_merge_pt(a, b, slope); - - REQUIRE(bool(mergept)); - - // Distance of mergept should be equal from both input points - float D = std::abs((*mergept - b).squaredNorm() - (*mergept - a).squaredNorm()); - - REQUIRE(D < EPSILON); - REQUIRE(!is_outside_support_cone(a, *mergept, slope)); - REQUIRE(!is_outside_support_cone(b, *mergept, slope)); - } - - SECTION("Points separated by less than critical angle have the lower point as mergept") { - Vec3f a{-1.f, -1.f, -1.f}, b = {-1.5f, -1.5f, -2.f}; - auto slope = float(PI / 4.); - - auto mergept = branchingtree::find_merge_pt(a, b, slope); - - REQUIRE(bool(mergept)); - REQUIRE((*mergept - b).norm() < 2 * EPSILON); - } - - // -|----------------------------> Y - // a b - // * * <= mergept * - // - SECTION("Points at same height have mergepoint in the middle if critical angle is zero ") { - Vec3f a{-1.f, -1.f, -1.f}, b = {-1.5f, -1.5f, -1.f}; - auto slope = EPSILON; - - auto mergept = branchingtree::find_merge_pt(a, b, slope); - - REQUIRE(bool(mergept)); - Vec3f middle = (b + a) / 2.; - REQUIRE((*mergept - middle).norm() < 4 * EPSILON); - } -} TEST_CASE("BranchingSupports::ElevatedSupportsDoNotPierceModel", "[SLASupportGeneration][Branching]") { diff --git a/tests/sla_print/sla_supptreeutils_tests.cpp b/tests/sla_print/sla_supptreeutils_tests.cpp new file mode 100644 index 000000000..c8abbb831 --- /dev/null +++ b/tests/sla_print/sla_supptreeutils_tests.cpp @@ -0,0 +1,377 @@ +#include +#include + +#include + +#include "libslic3r/Execution/ExecutionSeq.hpp" +#include "libslic3r/SLA/SupportTreeUtils.hpp" +#include "libslic3r/SLA/SupportTreeUtilsLegacy.hpp" + +// Test pair hash for 'nums' random number pairs. +template void test_pairhash() +{ + const constexpr size_t nums = 1000; + I A[nums] = {0}, B[nums] = {0}; + std::unordered_set CH; + std::unordered_map> ints; + + std::random_device rd; + std::mt19937 gen(rd()); + + const I Ibits = int(sizeof(I) * CHAR_BIT); + const II IIbits = int(sizeof(II) * CHAR_BIT); + + int bits = IIbits / 2 < Ibits ? Ibits / 2 : Ibits; + if (std::is_signed::value) bits -= 1; + const I Imin = 0; + const I Imax = I(std::pow(2., bits) - 1); + + std::uniform_int_distribution dis(Imin, Imax); + + for (size_t i = 0; i < nums;) { + I a = dis(gen); + if (CH.find(a) == CH.end()) { CH.insert(a); A[i] = a; ++i; } + } + + for (size_t i = 0; i < nums;) { + I b = dis(gen); + if (CH.find(b) == CH.end()) { CH.insert(b); B[i] = b; ++i; } + } + + for (size_t i = 0; i < nums; ++i) { + I a = A[i], b = B[i]; + + REQUIRE(a != b); + + II hash_ab = Slic3r::sla::pairhash(a, b); + II hash_ba = Slic3r::sla::pairhash(b, a); + REQUIRE(hash_ab == hash_ba); + + auto it = ints.find(hash_ab); + + if (it != ints.end()) { + REQUIRE(( + (it->second.first == a && it->second.second == b) || + (it->second.first == b && it->second.second == a) + )); + } else + ints[hash_ab] = std::make_pair(a, b); + } +} + +TEST_CASE("Pillar pairhash should be unique", "[suptreeutils]") { + test_pairhash(); + test_pairhash(); + test_pairhash(); + test_pairhash(); +} + +static void eval_ground_conn(const Slic3r::sla::GroundConnection &conn, + const Slic3r::sla::SupportableMesh &sm, + const Slic3r::sla::Junction &j, + double end_r, + const std::string &stl_fname = "output.stl") +{ + using namespace Slic3r; + +//#ifndef NDEBUG + + sla::SupportTreeBuilder builder; + + if (!conn) + builder.add_junction(j); + + sla::build_ground_connection(builder, sm, conn); + + indexed_triangle_set mesh = *sm.emesh.get_triangle_mesh(); + its_merge(mesh, builder.merged_mesh()); + + its_write_stl_ascii(stl_fname.c_str(), "stl_fname", mesh); +//#endif + + REQUIRE(bool(conn)); + + // The route should include the source and one avoidance junction. + REQUIRE(conn.path.size() == 2); + + // Check if the radius increases with each node + REQUIRE(conn.path.front().r < conn.path.back().r); + REQUIRE(conn.path.back().r < conn.pillar_base->r_top); + + // The end radius and the pillar base's upper radius should match + REQUIRE(conn.pillar_base->r_top == Approx(end_r)); +} + +TEST_CASE("Pillar search dumb case", "[suptreeutils]") { + using namespace Slic3r; + + constexpr double FromR = 0.5; + auto j = sla::Junction{Vec3d::Zero(), FromR}; + + SECTION("with empty mesh") { + sla::SupportableMesh sm{indexed_triangle_set{}, + sla::SupportPoints{}, + sla::SupportTreeConfig{}}; + + constexpr double EndR = 1.; + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndR, sla::DOWN); + + REQUIRE(conn); +// REQUIRE(conn.path.size() == 1); + REQUIRE(conn.pillar_base->pos.z() == Approx(ground_level(sm))); + } + + SECTION("with zero R source and destination") { + sla::SupportableMesh sm{indexed_triangle_set{}, + sla::SupportPoints{}, + sla::SupportTreeConfig{}}; + + j.r = 0.; + constexpr double EndR = 0.; + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndR, sla::DOWN); + + REQUIRE(conn); +// REQUIRE(conn.path.size() == 1); + REQUIRE(conn.pillar_base->pos.z() == Approx(ground_level(sm))); + REQUIRE(conn.pillar_base->r_top == Approx(0.)); + } + + SECTION("with zero init direction") { + sla::SupportableMesh sm{indexed_triangle_set{}, + sla::SupportPoints{}, + sla::SupportTreeConfig{}}; + + constexpr double EndR = 1.; + Vec3d init_dir = Vec3d::Zero(); + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndR, init_dir); + + REQUIRE(conn); +// REQUIRE(conn.path.size() == 1); + REQUIRE(conn.pillar_base->pos.z() == Approx(ground_level(sm))); + } +} + +TEST_CASE("Avoid disk below junction", "[suptreeutils]") +{ + // In this test there will be a disk mesh with some radius, centered at + // (0, 0, 0) and above the disk, a junction from which the support pillar + // should be routed. The algorithm needs to find an avoidance route. + + using namespace Slic3r; + + constexpr double FromRadius = .5; + constexpr double EndRadius = 1.; + constexpr double CylRadius = 4.; + constexpr double CylHeight = 1.; + + sla::SupportTreeConfig cfg; + + indexed_triangle_set disk = its_make_cylinder(CylRadius, CylHeight); + + // 2.5 * CyRadius height should be enough to be able to insert a bridge + // with 45 degree tilt above the disk. + sla::Junction j{Vec3d{0., 0., 2.5 * CylRadius}, FromRadius}; + + sla::SupportableMesh sm{disk, sla::SupportPoints{}, cfg}; + + SECTION("with elevation") { + + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_tbb, sm, j, EndRadius, sla::DOWN); + + eval_ground_conn(conn, sm, j, EndRadius, "disk.stl"); + + // Check if the avoidance junction is indeed outside of the disk barrier's + // edge. + auto p = conn.path.back().pos; + double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y()); + REQUIRE(pR + FromRadius > CylRadius); + } + + SECTION("without elevation") { + sm.cfg.object_elevation_mm = 0.; + + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_tbb, sm, j, EndRadius, sla::DOWN); + + eval_ground_conn(conn, sm, j, EndRadius, "disk_ze.stl"); + + // Check if the avoidance junction is indeed outside of the disk barrier's + // edge. + auto p = conn.path.back().pos; + double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y()); + REQUIRE(pR + FromRadius > CylRadius); + } +} + +TEST_CASE("Avoid disk below junction with barrier on the side", "[suptreeutils]") +{ + // In this test there will be a disk mesh with some radius, centered at + // (0, 0, 0) and above the disk, a junction from which the support pillar + // should be routed. The algorithm needs to find an avoidance route. + + using namespace Slic3r; + + constexpr double FromRadius = .5; + constexpr double EndRadius = 1.; + constexpr double CylRadius = 4.; + constexpr double CylHeight = 1.; + constexpr double JElevX = 2.5; + + sla::SupportTreeConfig cfg; + + indexed_triangle_set disk = its_make_cylinder(CylRadius, CylHeight); + indexed_triangle_set wall = its_make_cube(1., 2 * CylRadius, JElevX * CylRadius); + its_translate(wall, Vec3f{float(FromRadius), -float(CylRadius), 0.f}); + its_merge(disk, wall); + + // 2.5 * CyRadius height should be enough to be able to insert a bridge + // with 45 degree tilt above the disk. + sla::Junction j{Vec3d{0., 0., JElevX * CylRadius}, FromRadius}; + + sla::SupportableMesh sm{disk, sla::SupportPoints{}, cfg}; + + SECTION("with elevation") { + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndRadius, sla::DOWN); + + eval_ground_conn(conn, sm, j, EndRadius, "disk_with_barrier.stl"); + + // Check if the avoidance junction is indeed outside of the disk barrier's + // edge. + auto p = conn.path.back().pos; + double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y()); + REQUIRE(pR + FromRadius > CylRadius); + } + + SECTION("without elevation") { + sm.cfg.object_elevation_mm = 0.; + + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndRadius, sla::DOWN); + + eval_ground_conn(conn, sm, j, EndRadius, "disk_with_barrier_ze.stl"); + + // Check if the avoidance junction is indeed outside of the disk barrier's + // edge. + auto p = conn.path.back().pos; + double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y()); + REQUIRE(pR + FromRadius > CylRadius); + } +} + +TEST_CASE("Find ground route just above ground", "[suptreeutils]") { + using namespace Slic3r; + + sla::SupportTreeConfig cfg; + cfg.object_elevation_mm = 0.; + + sla::Junction j{Vec3d{0., 0., 2. * cfg.head_back_radius_mm}, cfg.head_back_radius_mm}; + + sla::SupportableMesh sm{{}, sla::SupportPoints{}, cfg}; + + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, Geometry::spheric_to_dir(3 * PI/ 4, PI)); + + REQUIRE(conn); + + REQUIRE(conn.pillar_base->pos.z() >= Approx(ground_level(sm))); +} + +TEST_CASE("BranchingSupports::MergePointFinder", "[suptreeutils]") { + using namespace Slic3r; + + SECTION("Identical points have the same merge point") { + Vec3f a{0.f, 0.f, 0.f}, b = a; + auto slope = float(PI / 4.); + + auto mergept = sla::find_merge_pt(a, b, slope); + + REQUIRE(bool(mergept)); + REQUIRE((*mergept - b).norm() < EPSILON); + REQUIRE((*mergept - a).norm() < EPSILON); + } + + // ^ Z + // | a * + // | + // | b * <= mergept + SECTION("Points at different heights have the lower point as mergepoint") { + Vec3f a{0.f, 0.f, 0.f}, b = {0.f, 0.f, -1.f}; + auto slope = float(PI / 4.); + + auto mergept = sla::find_merge_pt(a, b, slope); + + REQUIRE(bool(mergept)); + REQUIRE((*mergept - b).squaredNorm() < 2 * EPSILON); + } + + // -|---------> X + // a b + // * * + // * <= mergept + SECTION("Points at different X have mergept in the middle at lower Z") { + Vec3f a{0.f, 0.f, 0.f}, b = {1.f, 0.f, 0.f}; + auto slope = float(PI / 4.); + + auto mergept = sla::find_merge_pt(a, b, slope); + + REQUIRE(bool(mergept)); + + // Distance of mergept should be equal from both input points + float D = std::abs((*mergept - b).squaredNorm() - (*mergept - a).squaredNorm()); + + REQUIRE(D < EPSILON); + REQUIRE(!sla::is_outside_support_cone(a, *mergept, slope)); + REQUIRE(!sla::is_outside_support_cone(b, *mergept, slope)); + } + + // -|---------> Y + // a b + // * * + // * <= mergept + SECTION("Points at different Y have mergept in the middle at lower Z") { + Vec3f a{0.f, 0.f, 0.f}, b = {0.f, 1.f, 0.f}; + auto slope = float(PI / 4.); + + auto mergept = sla::find_merge_pt(a, b, slope); + + REQUIRE(bool(mergept)); + + // Distance of mergept should be equal from both input points + float D = std::abs((*mergept - b).squaredNorm() - (*mergept - a).squaredNorm()); + + REQUIRE(D < EPSILON); + REQUIRE(!sla::is_outside_support_cone(a, *mergept, slope)); + REQUIRE(!sla::is_outside_support_cone(b, *mergept, slope)); + } + + SECTION("Points separated by less than critical angle have the lower point as mergept") { + Vec3f a{-1.f, -1.f, -1.f}, b = {-1.5f, -1.5f, -2.f}; + auto slope = float(PI / 4.); + + auto mergept = sla::find_merge_pt(a, b, slope); + + REQUIRE(bool(mergept)); + REQUIRE((*mergept - b).norm() < 2 * EPSILON); + } + + // -|----------------------------> Y + // a b + // * * <= mergept * + // + SECTION("Points at same height have mergepoint in the middle if critical angle is zero ") { + Vec3f a{-1.f, -1.f, -1.f}, b = {-1.5f, -1.5f, -1.f}; + auto slope = EPSILON; + + auto mergept = sla::find_merge_pt(a, b, slope); + + REQUIRE(bool(mergept)); + Vec3f middle = (b + a) / 2.; + REQUIRE((*mergept - middle).norm() < 4 * EPSILON); + } +} + diff --git a/tests/sla_print/sla_test_utils.cpp b/tests/sla_print/sla_test_utils.cpp index 69feea31f..f294abccb 100644 --- a/tests/sla_print/sla_test_utils.cpp +++ b/tests/sla_print/sla_test_utils.cpp @@ -101,7 +101,7 @@ void test_supports(const std::string &obj_filename, REQUIRE_FALSE(mesh.empty()); if (hollowingcfg.enabled) { - sla::InteriorPtr interior = sla::generate_interior(mesh, hollowingcfg); + sla::InteriorPtr interior = sla::generate_interior(mesh.its, hollowingcfg); REQUIRE(interior); mesh.merge(TriangleMesh{sla::get_mesh(*interior)}); } @@ -118,7 +118,7 @@ void test_supports(const std::string &obj_filename, // Create the special index-triangle mesh with spatial indexing which // is the input of the support point and support mesh generators - AABBMesh emesh{mesh}; + sla::SupportableMesh sm{mesh.its, {}, supportcfg}; #ifdef SLIC3R_HOLE_RAYCASTER if (hollowingcfg.enabled) @@ -130,23 +130,23 @@ void test_supports(const std::string &obj_filename, // Create the support point generator sla::SupportPointGenerator::Config autogencfg; autogencfg.head_diameter = float(2 * supportcfg.head_front_radius_mm); - sla::SupportPointGenerator point_gen{emesh, autogencfg, [] {}, [](int) {}}; + sla::SupportPointGenerator point_gen{sm.emesh, autogencfg, [] {}, [](int) {}}; point_gen.seed(0); // Make the test repeatable point_gen.execute(out.model_slices, out.slicegrid); // Get the calculated support points. - std::vector support_points = point_gen.output(); + sm.pts = point_gen.output(); int validityflags = ASSUME_NO_REPAIR; // If there is no elevation, support points shall be removed from the // bottom of the object. if (std::abs(supportcfg.object_elevation_mm) < EPSILON) { - sla::remove_bottom_points(support_points, zmin + supportcfg.base_height_mm); + sla::remove_bottom_points(sm.pts, zmin + supportcfg.base_height_mm); } else { // Should be support points at least on the bottom of the model - REQUIRE_FALSE(support_points.empty()); + REQUIRE_FALSE(sm.pts.empty()); // Also the support mesh should not be empty. validityflags |= ASSUME_NO_EMPTY; @@ -154,7 +154,6 @@ void test_supports(const std::string &obj_filename, // Generate the actual support tree sla::SupportTreeBuilder treebuilder; - sla::SupportableMesh sm{emesh, support_points, supportcfg}; switch (sm.cfg.tree_type) { case sla::SupportTreeType::Default: { @@ -182,6 +181,18 @@ void test_supports(const std::string &obj_filename, if (std::abs(supportcfg.object_elevation_mm) < EPSILON) allowed_zmin = zmin - 2 * supportcfg.head_back_radius_mm; +#ifndef NDEBUG + if (!(obb.min.z() >= Approx(allowed_zmin)) || !(obb.max.z() <= Approx(zmax))) + { + indexed_triangle_set its; + treebuilder.retrieve_full_mesh(its); + TriangleMesh m{its}; + m.merge(mesh); + m.WriteOBJFile((Catch::getResultCapture().getCurrentTestName() + "_" + + obj_filename).c_str()); + } +#endif + REQUIRE(obb.min.z() >= Approx(allowed_zmin)); REQUIRE(obb.max.z() <= Approx(zmax)); diff --git a/tests/sla_print/sla_test_utils.hpp b/tests/sla_print/sla_test_utils.hpp index 187a72d54..103b2f66a 100644 --- a/tests/sla_print/sla_test_utils.hpp +++ b/tests/sla_print/sla_test_utils.hpp @@ -14,14 +14,12 @@ #include "libslic3r/TriangleMesh.hpp" #include "libslic3r/SLA/Pad.hpp" #include "libslic3r/SLA/SupportTreeBuilder.hpp" -#include "libslic3r/SLA/SupportTreeUtils.hpp" #include "libslic3r/SLA/SupportPointGenerator.hpp" #include "libslic3r/SLA/AGGRaster.hpp" #include "libslic3r/SLA/ConcaveHull.hpp" #include "libslic3r/MTUtils.hpp" #include "libslic3r/SVG.hpp" -#include "libslic3r/Format/OBJ.hpp" using namespace Slic3r; @@ -111,58 +109,6 @@ inline void test_support_model_collision( test_support_model_collision(obj_filename, input_supportcfg, hcfg, {}); } -// Test pair hash for 'nums' random number pairs. -template void test_pairhash() -{ - const constexpr size_t nums = 1000; - I A[nums] = {0}, B[nums] = {0}; - std::unordered_set CH; - std::unordered_map> ints; - - std::random_device rd; - std::mt19937 gen(rd()); - - const I Ibits = int(sizeof(I) * CHAR_BIT); - const II IIbits = int(sizeof(II) * CHAR_BIT); - - int bits = IIbits / 2 < Ibits ? Ibits / 2 : Ibits; - if (std::is_signed::value) bits -= 1; - const I Imin = 0; - const I Imax = I(std::pow(2., bits) - 1); - - std::uniform_int_distribution dis(Imin, Imax); - - for (size_t i = 0; i < nums;) { - I a = dis(gen); - if (CH.find(a) == CH.end()) { CH.insert(a); A[i] = a; ++i; } - } - - for (size_t i = 0; i < nums;) { - I b = dis(gen); - if (CH.find(b) == CH.end()) { CH.insert(b); B[i] = b; ++i; } - } - - for (size_t i = 0; i < nums; ++i) { - I a = A[i], b = B[i]; - - REQUIRE(a != b); - - II hash_ab = sla::pairhash(a, b); - II hash_ba = sla::pairhash(b, a); - REQUIRE(hash_ab == hash_ba); - - auto it = ints.find(hash_ab); - - if (it != ints.end()) { - REQUIRE(( - (it->second.first == a && it->second.second == b) || - (it->second.first == b && it->second.second == a) - )); - } else - ints[hash_ab] = std::make_pair(a, b); - } -} - // SLA Raster test utils: using TPixel = uint8_t;