diff --git a/.github/workflows/check_profiles.yml b/.github/workflows/check_profiles.yml index 422513442e..4167cc705b 100644 --- a/.github/workflows/check_profiles.yml +++ b/.github/workflows/check_profiles.yml @@ -13,6 +13,7 @@ on: # both it and its tests live in scripts/, so a PR touching only those must still run # this workflow. - 'resources/printers/**' + - 'resources/info/legacy_compatible_printers.json' - 'scripts/**' # orca_profile_tool.py reads the variant key sets from PrintConfig.cpp, and its # tests the obsolete keys, so a PR changing either must be checked against the profiles. @@ -59,6 +60,21 @@ jobs: python3 ./scripts/orca_profile_tool.py check 2>&1 | tee ${{ runner.temp }}/profile_tool.log exit ${PIPESTATUS[0]} + # The app migrates user filament presets made against 2.4.2 back to the printers their + # parent listed then, from resources/info/legacy_compatible_printers.json. A PR that + # renames a filament preset or narrows its compatible_printers changes what that map + # must say, so it has to regenerate it. + - name: Check the legacy compatible printers map + id: legacy_compat + continue-on-error: true + run: | + set -o pipefail + base="${{ runner.temp }}/v2.4.2" + git fetch --no-tags --depth=1 origin refs/tags/v2.4.2:refs/tags/v2.4.2 + mkdir -p "$base" + git archive v2.4.2 resources/profiles | tar -x -C "$base" + python3 ./scripts/orca_profile_tool.py legacy-compat --base "$base/resources/profiles" --check 2>&1 | tee ${{ runner.temp }}/legacy_compat.log + # download - name: Download working-directory: ${{ github.workspace }} @@ -217,7 +233,7 @@ jobs: echo "${{ github.event.pull_request.number }}" > ${{ runner.temp }}/profile-check-results/pr_number.txt - name: Prepare comment artifact - if: ${{ always() && github.event_name == 'pull_request' && (steps.profile_tool.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }} + if: ${{ always() && github.event_name == 'pull_request' && (steps.profile_tool.outcome == 'failure' || steps.legacy_compat.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }} run: | { # Marker matched by check_profiles_comment.yml to delete prior comments. @@ -234,6 +250,17 @@ jobs: echo "" fi + if [ "${{ steps.legacy_compat.outcome }}" = "failure" ]; then + echo "### Legacy Compatible Printers Map Out of Date" + echo "" + echo '```' + head -c 30000 ${{ runner.temp }}/legacy_compat.log || echo "No output captured" + echo '```' + echo "" + echo 'Regenerate it: `git archive v2.4.2 resources/profiles | tar -x -C /tmp/v2.4.2`, then `python3 scripts/orca_profile_tool.py legacy-compat --base /tmp/v2.4.2/resources/profiles`.' + echo "" + fi + if [ "${{ steps.validate_system.outcome }}" = "failure" ]; then echo "### System Profile Validation Failed" echo "" @@ -283,7 +310,7 @@ jobs: retention-days: 1 - name: Fail if any check failed - if: ${{ always() && (steps.profile_tool.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }} + if: ${{ always() && (steps.profile_tool.outcome == 'failure' || steps.legacy_compat.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }} run: | echo "One or more profile checks failed; see the step logs above." echo 'Reproduce the whole run locally with scripts/check_profile.sh (scripts\check_profile.bat on Windows).' diff --git a/resources/info/legacy_compatible_printers.json b/resources/info/legacy_compatible_printers.json new file mode 100644 index 0000000000..3d99065c87 --- /dev/null +++ b/resources/info/legacy_compatible_printers.json @@ -0,0 +1,795 @@ +{ + "presets": { + "Anycubic Generic ABS": [ + "Anycubic Kobra 3 0.4 nozzle" + ], + "Anycubic Generic TPU": [ + "Anycubic Kobra 3 0.4 nozzle" + ], + "Bambu ABS @BBL P1P": [ + "Bambu Lab P1P 0.6 nozzle" + ], + "Bambu ABS @BBL X1C": [ + "Bambu Lab P1S 0.4 nozzle", + "Bambu Lab P1S 0.6 nozzle", + "Bambu Lab X1 Carbon 0.6 nozzle" + ], + "Bambu ASA @BBL X2D": [ + "Bambu Lab X2D 0.8 nozzle" + ], + "Bambu PA6-CF @BBL H2D": [ + "Bambu Lab H2D 0.6 nozzle", + "Bambu Lab H2D 0.8 nozzle" + ], + "Bambu PA6-CF @BBL H2DP": [ + "Bambu Lab H2D Pro 0.6 nozzle", + "Bambu Lab H2D Pro 0.8 nozzle" + ], + "Bambu PA6-CF @BBL H2S": [ + "Bambu Lab H2S 0.6 nozzle", + "Bambu Lab H2S 0.8 nozzle" + ], + "Bambu PA6-CF @BBL P2S": [ + "Bambu Lab P2S 0.6 nozzle", + "Bambu Lab P2S 0.8 nozzle" + ], + "Bambu PA6-CF @BBL X1C": [ + "Bambu Lab P1P 0.6 nozzle", + "Bambu Lab P1P 0.8 nozzle", + "Bambu Lab P1S 0.6 nozzle", + "Bambu Lab P1S 0.8 nozzle", + "Bambu Lab X1 Carbon 0.6 nozzle", + "Bambu Lab X1 Carbon 0.8 nozzle" + ], + "Bambu PA6-CF @BBL X2D": [ + "Bambu Lab X2D 0.8 nozzle" + ], + "Bambu PAHT-CF @BBL H2D": [ + "Bambu Lab H2D 0.6 nozzle", + "Bambu Lab H2D 0.8 nozzle" + ], + "Bambu PAHT-CF @BBL H2DP": [ + "Bambu Lab H2D Pro 0.6 nozzle", + "Bambu Lab H2D Pro 0.8 nozzle" + ], + "Bambu PAHT-CF @BBL H2S": [ + "Bambu Lab H2S 0.6 nozzle", + "Bambu Lab H2S 0.8 nozzle" + ], + "Bambu PAHT-CF @BBL P1P": [ + "Bambu Lab P1P 0.6 nozzle", + "Bambu Lab P1P 0.8 nozzle" + ], + "Bambu PAHT-CF @BBL P2S": [ + "Bambu Lab P2S 0.6 nozzle", + "Bambu Lab P2S 0.8 nozzle" + ], + "Bambu PAHT-CF @BBL X1C": [ + "Bambu Lab P1S 0.6 nozzle", + "Bambu Lab P1S 0.8 nozzle", + "Bambu Lab X1 Carbon 0.6 nozzle", + "Bambu Lab X1 Carbon 0.8 nozzle" + ], + "Bambu PAHT-CF @BBL X2D": [ + "Bambu Lab X2D 0.8 nozzle" + ], + "Bambu PC @BBL P1P": [ + "Bambu Lab P1P 0.6 nozzle" + ], + "Bambu PETG @0.2 nozzle": [ + "Qidi Q1 Pro 0.2 nozzle" + ], + "Bambu PETG Basic @BBL H2D 0.6 nozzle": [ + "Bambu Lab H2D 0.8 nozzle" + ], + "Bambu PETG Basic @BBL H2DP 0.6 nozzle": [ + "Bambu Lab H2D Pro 0.8 nozzle" + ], + "Bambu PETG Basic @BBL H2S": [ + "Bambu Lab H2S 0.6 nozzle", + "Bambu Lab H2S 0.8 nozzle" + ], + "Bambu PETG Basic @BBL P2S 0.6 nozzle": [ + "Bambu Lab P2S 0.8 nozzle" + ], + "Bambu PETG Basic @BBL X2D": [ + "Bambu Lab X2D 0.8 nozzle" + ], + "Bambu PETG HF @BBL P2S 0.6 nozzle": [ + "Bambu Lab P2S 0.8 nozzle" + ], + "Bambu PETG HF @BBL X1C": [ + "Bambu Lab P1P 0.4 nozzle", + "Bambu Lab P1S 0.4 nozzle" + ], + "Bambu PETG HF @BBL X1C 0.8 nozzle": [ + "Bambu Lab X1 Carbon 0.6 nozzle" + ], + "Bambu PETG HF @BBL X2D": [ + "Bambu Lab X2D 0.8 nozzle" + ], + "Bambu PETG Translucent @BBL H2D 0.6 nozzle": [ + "Bambu Lab H2D 0.8 nozzle" + ], + "Bambu PETG Translucent @BBL H2DP 0.6 nozzle": [ + "Bambu Lab H2D Pro 0.8 nozzle" + ], + "Bambu PETG Translucent @BBL H2S": [ + "Bambu Lab H2S 0.6 nozzle", + "Bambu Lab H2S 0.8 nozzle" + ], + "Bambu PETG Translucent @BBL P2S 0.6 nozzle": [ + "Bambu Lab P2S 0.8 nozzle" + ], + "Bambu PETG Translucent @BBL X1C 0.8 nozzle": [ + "Bambu Lab P1P 0.6 nozzle", + "Bambu Lab P1S 0.6 nozzle", + "Bambu Lab X1 Carbon 0.6 nozzle" + ], + "Bambu PETG-CF @BBL H2D 0.6 nozzle": [ + "Bambu Lab H2D 0.8 nozzle" + ], + "Bambu PETG-CF @BBL H2DP 0.6 nozzle": [ + "Bambu Lab H2D Pro 0.8 nozzle" + ], + "Bambu PETG-CF @BBL H2S": [ + "Bambu Lab H2S 0.6 nozzle", + "Bambu Lab H2S 0.8 nozzle" + ], + "Bambu PETG-CF @BBL P2S 0.6 nozzle": [ + "Bambu Lab P2S 0.8 nozzle" + ], + "Bambu PLA Basic @BBL P1P": [ + "Bambu Lab P1P 0.6 nozzle" + ], + "Bambu PLA Basic @BBL X1C": [ + "Bambu Lab P1S 0.4 nozzle", + "Bambu Lab P1S 0.6 nozzle", + "Bambu Lab X1 Carbon 0.6 nozzle" + ], + "Bambu PLA Basic @BBL X2D": [ + "Bambu Lab X2D 0.8 nozzle" + ], + "Bambu PLA Dynamic @BBL P1P": [ + "Bambu Lab P1P 0.6 nozzle" + ], + "Bambu PLA Dynamic @BBL X1C": [ + "Bambu Lab P1S 0.6 nozzle", + "Bambu Lab X1 Carbon 0.6 nozzle" + ], + "Bambu PLA Galaxy @BBL P1P": [ + "Bambu Lab P1P 0.6 nozzle" + ], + "Bambu PLA Galaxy @BBL X1C": [ + "Bambu Lab P1S 0.6 nozzle", + "Bambu Lab X1 Carbon 0.6 nozzle" + ], + "Bambu PLA Glow @BBL P1P": [ + "Bambu Lab P1P 0.6 nozzle" + ], + "Bambu PLA Glow @BBL X1C": [ + "Bambu Lab P1S 0.6 nozzle", + "Bambu Lab X1 Carbon 0.6 nozzle" + ], + "Bambu PLA Lite @BBL H2D": [ + "Bambu Lab H2D 0.6 nozzle" + ], + "Bambu PLA Lite @BBL H2S": [ + "Bambu Lab H2S 0.6 nozzle", + "Bambu Lab H2S 0.8 nozzle" + ], + "Bambu PLA Lite @BBL P1P": [ + "Bambu Lab P1P 0.6 nozzle" + ], + "Bambu PLA Lite @BBL X1C": [ + "Bambu Lab P1S 0.6 nozzle", + "Bambu Lab X1 Carbon 0.6 nozzle" + ], + "Bambu PLA Lite @BBL X2D": [ + "Bambu Lab X2D 0.8 nozzle" + ], + "Bambu PLA Marble @BBL P1P": [ + "Bambu Lab P1P 0.6 nozzle", + "Bambu Lab P1P 0.8 nozzle" + ], + "Bambu PLA Marble @BBL X1C": [ + "Bambu Lab P1S 0.6 nozzle", + "Bambu Lab X1 Carbon 0.6 nozzle" + ], + "Bambu PLA Matte @BBL P1P": [ + "Bambu Lab P1P 0.6 nozzle" + ], + "Bambu PLA Matte @BBL X1C": [ + "Bambu Lab P1S 0.4 nozzle", + "Bambu Lab P1S 0.6 nozzle", + "Bambu Lab X1 Carbon 0.6 nozzle" + ], + "Bambu PLA Matte @BBL X2D": [ + "Bambu Lab X2D 0.8 nozzle" + ], + "Bambu PLA Metal @BBL P1P": [ + "Bambu Lab P1P 0.6 nozzle" + ], + "Bambu PLA Metal @BBL X1C": [ + "Bambu Lab P1S 0.6 nozzle", + "Bambu Lab X1 Carbon 0.6 nozzle" + ], + "Bambu PLA Silk+ @BBL H2D": [ + "Bambu Lab H2D 0.6 nozzle", + "Bambu Lab H2D 0.8 nozzle" + ], + "Bambu PLA Silk+ @BBL H2DP": [ + "Bambu Lab H2D Pro 0.6 nozzle", + "Bambu Lab H2D Pro 0.8 nozzle" + ], + "Bambu PLA Silk+ @BBL H2S": [ + "Bambu Lab H2S 0.6 nozzle", + "Bambu Lab H2S 0.8 nozzle" + ], + "Bambu PLA Silk+ @BBL P1P": [ + "Bambu Lab P1P 0.6 nozzle" + ], + "Bambu PLA Silk+ @BBL X1C": [ + "Bambu Lab P1S 0.4 nozzle", + "Bambu Lab P1S 0.6 nozzle", + "Bambu Lab X1 Carbon 0.6 nozzle" + ], + "Bambu PLA Silk+ @BBL X2D": [ + "Bambu Lab X2D 0.8 nozzle" + ], + "Bambu PLA Sparkle @BBL P1P": [ + "Bambu Lab P1P 0.6 nozzle" + ], + "Bambu PLA Sparkle @BBL X1C": [ + "Bambu Lab P1S 0.6 nozzle", + "Bambu Lab X1 Carbon 0.6 nozzle" + ], + "Bambu PLA Tough+ @BBL P1P": [ + "Bambu Lab P1P 0.6 nozzle" + ], + "Bambu PLA Tough+ @BBL X1C": [ + "Bambu Lab X1 Carbon 0.6 nozzle" + ], + "Bambu PLA Tough+ @BBL X2D": [ + "Bambu Lab X2D 0.8 nozzle" + ], + "Bambu PLA Translucent @BBL H2D 0.8 nozzle": [ + "Bambu Lab H2D 0.6 nozzle" + ], + "Bambu PLA Translucent @BBL H2DP 0.8 nozzle": [ + "Bambu Lab H2D Pro 0.6 nozzle" + ], + "Bambu PLA Translucent @BBL H2S": [ + "Bambu Lab H2S 0.6 nozzle", + "Bambu Lab H2S 0.8 nozzle" + ], + "Bambu PLA Wood @BBL P1P": [ + "Bambu Lab P1P 0.6 nozzle" + ], + "Bambu PLA Wood @BBL X1C": [ + "Bambu Lab P1S 0.6 nozzle", + "Bambu Lab X1 Carbon 0.6 nozzle" + ], + "Bambu PLA-CF @BBL H2D 0.6 nozzle": [ + "Bambu Lab H2D 0.8 nozzle" + ], + "Bambu PLA-CF @BBL H2DP 0.6 nozzle": [ + "Bambu Lab H2D Pro 0.8 nozzle" + ], + "Bambu PLA-CF @BBL H2S": [ + "Bambu Lab H2S 0.6 nozzle", + "Bambu Lab H2S 0.8 nozzle" + ], + "Bambu PLA-CF @BBL P2S 0.6 nozzle": [ + "Bambu Lab P2S 0.8 nozzle" + ], + "Bambu PLA-CF @BBL X2D": [ + "Bambu Lab X2D 0.8 nozzle" + ], + "Bambu PPA-CF @BBL X1C": [ + "Bambu Lab P1P 0.6 nozzle", + "Bambu Lab P1P 0.8 nozzle", + "Bambu Lab P1S 0.6 nozzle", + "Bambu Lab P1S 0.8 nozzle", + "Bambu Lab X1 Carbon 0.6 nozzle", + "Bambu Lab X1 Carbon 0.8 nozzle" + ], + "Bambu PVA @BBL H2D": [ + "Bambu Lab H2D 0.6 nozzle", + "Bambu Lab H2D 0.8 nozzle" + ], + "Bambu PVA @BBL H2DP": [ + "Bambu Lab H2D Pro 0.6 nozzle", + "Bambu Lab H2D Pro 0.8 nozzle" + ], + "Bambu PVA @BBL H2S": [ + "Bambu Lab H2S 0.6 nozzle", + "Bambu Lab H2S 0.8 nozzle" + ], + "Bambu PVA @BBL P2S": [ + "Bambu Lab P2S 0.6 nozzle", + "Bambu Lab P2S 0.8 nozzle" + ], + "Bambu PVA @BBL X2D": [ + "Bambu Lab X2D 0.8 nozzle" + ], + "Bambu Support For PLA @BBL H2D": [ + "Bambu Lab H2D 0.6 nozzle", + "Bambu Lab H2D 0.8 nozzle" + ], + "Bambu Support For PLA @BBL H2DP": [ + "Bambu Lab H2D Pro 0.6 nozzle", + "Bambu Lab H2D Pro 0.8 nozzle" + ], + "Bambu Support For PLA @BBL H2S": [ + "Bambu Lab H2S 0.6 nozzle", + "Bambu Lab H2S 0.8 nozzle" + ], + "Bambu Support For PLA @BBL P2S": [ + "Bambu Lab P2S 0.6 nozzle", + "Bambu Lab P2S 0.8 nozzle" + ], + "Bambu Support For PLA @BBL X2D": [ + "Bambu Lab X2D 0.8 nozzle" + ], + "Bambu Support For PLA/PETG @BBL H2D": [ + "Bambu Lab H2D 0.6 nozzle", + "Bambu Lab H2D 0.8 nozzle" + ], + "Bambu Support For PLA/PETG @BBL H2DP": [ + "Bambu Lab H2D Pro 0.6 nozzle", + "Bambu Lab H2D Pro 0.8 nozzle" + ], + "Bambu Support For PLA/PETG @BBL H2S": [ + "Bambu Lab H2S 0.6 nozzle", + "Bambu Lab H2S 0.8 nozzle" + ], + "Bambu Support For PLA/PETG @BBL P2S": [ + "Bambu Lab P2S 0.6 nozzle", + "Bambu Lab P2S 0.8 nozzle" + ], + "Bambu Support For PLA/PETG @BBL X2D": [ + "Bambu Lab X2D 0.8 nozzle" + ], + "Bambu Support for ABS @BBL H2D": [ + "Bambu Lab H2D 0.6 nozzle", + "Bambu Lab H2D 0.8 nozzle" + ], + "Bambu Support for ABS @BBL H2DP": [ + "Bambu Lab H2D Pro 0.6 nozzle", + "Bambu Lab H2D Pro 0.8 nozzle" + ], + "Bambu Support for ABS @BBL H2S": [ + "Bambu Lab H2S 0.6 nozzle", + "Bambu Lab H2S 0.8 nozzle" + ], + "Bambu Support for ABS @BBL P2S": [ + "Bambu Lab P2S 0.6 nozzle", + "Bambu Lab P2S 0.8 nozzle" + ], + "Bambu Support for ABS @BBL X2D": [ + "Bambu Lab X2D 0.8 nozzle" + ], + "Bambu TPU 90A @BBL H2D": [ + "Bambu Lab H2D 0.6 nozzle", + "Bambu Lab H2D 0.8 nozzle" + ], + "Bambu TPU 90A @BBL H2DP": [ + "Bambu Lab H2D Pro 0.6 nozzle", + "Bambu Lab H2D Pro 0.8 nozzle" + ], + "Bambu TPU 95A HF @BBL H2D": [ + "Bambu Lab H2D 0.4 nozzle", + "Bambu Lab H2D 0.8 nozzle" + ], + "Bambu TPU 95A HF @BBL H2DP": [ + "Bambu Lab H2D Pro 0.4 nozzle", + "Bambu Lab H2D Pro 0.8 nozzle" + ], + "Bambu TPU 95A HF @BBL H2S": [ + "Bambu Lab H2S 0.6 nozzle", + "Bambu Lab H2S 0.8 nozzle" + ], + "Bambu TPU 95A HF @BBL P2S": [ + "Bambu Lab P2S 0.6 nozzle", + "Bambu Lab P2S 0.8 nozzle" + ], + "Bambu TPU 95A HF @BBL X2D": [ + "Bambu Lab X2D 0.8 nozzle" + ], + "Bambu TPU for AMS @BBL H2D": [ + "Bambu Lab H2D 0.6 nozzle", + "Bambu Lab H2D 0.8 nozzle" + ], + "Bambu TPU for AMS @BBL H2DP": [ + "Bambu Lab H2D Pro 0.6 nozzle", + "Bambu Lab H2D Pro 0.8 nozzle" + ], + "Bambu TPU for AMS @BBL H2S": [ + "Bambu Lab H2S 0.6 nozzle", + "Bambu Lab H2S 0.8 nozzle" + ], + "Bambu TPU for AMS @BBL P1P": [ + "Bambu Lab P1P 0.6 nozzle" + ], + "Bambu TPU for AMS @BBL P2S": [ + "Bambu Lab P2S 0.6 nozzle", + "Bambu Lab P2S 0.8 nozzle" + ], + "Bambu TPU for AMS @BBL X1C": [ + "Bambu Lab P1S 0.6 nozzle", + "Bambu Lab X1 Carbon 0.6 nozzle" + ], + "Bambu TPU for AMS @BBL X2D": [ + "Bambu Lab X2D 0.8 nozzle" + ], + "Blocks Generic ABS": [ + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ], + "Blocks Generic ASA": [ + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ], + "Blocks Generic ASA-CF": [ + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ], + "Blocks Generic PA": [ + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ], + "Blocks Generic PA-CF": [ + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ], + "Blocks Generic PC": [ + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ], + "Blocks Generic PETG": [ + "BLOCKS RD50 V2 0.4 nozzle", + "BLOCKS RD50 V2 0.6 nozzle", + "BLOCKS RD50 V2 0.8 nozzle", + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ], + "Blocks Generic PLA": [ + "BLOCKS RD50 V2 0.4 nozzle", + "BLOCKS RD50 V2 0.6 nozzle", + "BLOCKS RD50 V2 0.8 nozzle", + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ], + "Blocks Generic PLA-CF": [ + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ], + "Blocks Generic PVA": [ + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ], + "Blocks Generic TPU": [ + "BLOCKS RD50 V2 0.4 nozzle", + "BLOCKS RD50 V2 0.6 nozzle", + "BLOCKS RD50 V2 0.8 nozzle", + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ], + "Creality Generic ABS": [ + "Creality CR-10 SE 0.2 nozzle", + "Creality CR-10 SE 0.4 nozzle", + "Creality CR-10 SE 0.6 nozzle", + "Creality CR-10 SE 0.8 nozzle", + "Creality CR-6 SE 0.2 nozzle", + "Creality CR-6 SE 0.4 nozzle", + "Creality CR-6 SE 0.6 nozzle", + "Creality CR-6 SE 0.8 nozzle", + "Creality CR-M4 0.4 nozzle", + "Creality Ender-3 V2 0.4 nozzle" + ], + "Creality Generic ABS @Ender-3V3-all": [ + "Creality Ender-3 V3 Plus 0.4 nozzle", + "Creality Ender-3 V3 Plus 0.6 nozzle" + ], + "Creality Generic ABS @Hi-all": [ + "Creality Hi 0.4 nozzle" + ], + "Creality Generic ABS @K1-all": [ + "Creality K1 SE 0.4 nozzle", + "Creality K1C 0.4 nozzle" + ], + "Creality Generic ABS @K2-all": [ + "Creality K2 0.4 nozzle", + "Creality K2 Plus 0.4 nozzle", + "Creality K2 Plus 0.6 nozzle", + "Creality K2 Plus 0.8 nozzle", + "Creality K2 Pro 0.4 nozzle" + ], + "Creality Generic ASA @Ender-3V3-all": [ + "Creality Ender-3 V3 Plus 0.4 nozzle", + "Creality Ender-3 V3 Plus 0.6 nozzle" + ], + "Creality Generic ASA @K1-all": [ + "Creality K1 SE 0.4 nozzle", + "Creality K1C 0.4 nozzle" + ], + "Creality Generic ASA @K2-all": [ + "Creality K2 0.4 nozzle", + "Creality K2 Plus 0.4 nozzle", + "Creality K2 Pro 0.4 nozzle" + ], + "Creality Generic PA-CF @Ender-3V3-all": [ + "Creality Ender-3 V3 Plus 0.4 nozzle", + "Creality Ender-3 V3 Plus 0.6 nozzle" + ], + "Creality Generic PA-CF @K1-all": [ + "Creality K1C 0.4 nozzle" + ], + "Creality Generic PA-CF @K2-all": [ + "Creality K2 Plus 0.4 nozzle", + "Creality K2 Plus 0.6 nozzle", + "Creality K2 Plus 0.8 nozzle" + ], + "Creality Generic PC @K1-all": [ + "Creality K1C 0.4 nozzle" + ], + "Creality Generic PETG": [ + "Creality CR-6 SE 0.2 nozzle", + "Creality CR-6 SE 0.4 nozzle", + "Creality CR-6 SE 0.6 nozzle", + "Creality CR-6 SE 0.8 nozzle", + "Creality CR-M4 0.4 nozzle" + ], + "Creality Generic PETG @Ender-3V3-all": [ + "Creality Ender-3 V3 0.4 nozzle", + "Creality Ender-3 V3 0.6 nozzle", + "Creality Ender-3 V3 Plus 0.4 nozzle", + "Creality Ender-3 V3 Plus 0.6 nozzle", + "Creality Ender-3 V3 SE 0.2 nozzle", + "Creality Ender-3 V3 SE 0.4 nozzle", + "Creality Ender-3 V3 SE 0.6 nozzle", + "Creality Ender-3 V3 SE 0.8 nozzle" + ], + "Creality Generic PETG @Hi-all": [ + "Creality Hi 0.4 nozzle" + ], + "Creality Generic PETG @K1-all": [ + "Creality K1 SE 0.4 nozzle", + "Creality K1C 0.4 nozzle" + ], + "Creality Generic PETG @K2-all": [ + "Creality K2 0.4 nozzle", + "Creality K2 Plus 0.4 nozzle", + "Creality K2 Plus 0.6 nozzle", + "Creality K2 Plus 0.8 nozzle", + "Creality K2 Pro 0.4 nozzle" + ], + "Creality Generic PLA": [ + "Creality CR-10 SE 0.2 nozzle", + "Creality CR-10 SE 0.4 nozzle", + "Creality CR-10 SE 0.6 nozzle", + "Creality CR-10 SE 0.8 nozzle", + "Creality CR-6 SE 0.2 nozzle", + "Creality CR-6 SE 0.4 nozzle", + "Creality CR-6 SE 0.6 nozzle", + "Creality CR-6 SE 0.8 nozzle", + "Creality CR-M4 0.4 nozzle" + ], + "Creality Generic PLA @Ender-3V3-all": [ + "Creality Ender-3 V3 0.4 nozzle", + "Creality Ender-3 V3 0.6 nozzle", + "Creality Ender-3 V3 Plus 0.4 nozzle", + "Creality Ender-3 V3 Plus 0.6 nozzle" + ], + "Creality Generic PLA @K1-all": [ + "Creality K1 SE 0.4 nozzle", + "Creality K1C 0.4 nozzle" + ], + "Creality Generic PLA @K2-all": [ + "Creality K2 0.4 nozzle", + "Creality K2 Plus 0.4 nozzle", + "Creality K2 Plus 0.6 nozzle", + "Creality K2 Plus 0.8 nozzle", + "Creality K2 Pro 0.4 nozzle" + ], + "Creality Generic PLA-CF @K1-all": [ + "Creality K1 SE 0.4 nozzle", + "Creality K1C 0.4 nozzle" + ], + "Creality Generic PLA-CF @K2-all": [ + "Creality K2 0.4 nozzle", + "Creality K2 Plus 0.4 nozzle", + "Creality K2 Pro 0.4 nozzle" + ], + "Creality Generic TPU": [ + "Creality CR-10 SE 0.2 nozzle", + "Creality CR-10 SE 0.4 nozzle", + "Creality CR-10 SE 0.6 nozzle", + "Creality CR-10 SE 0.8 nozzle" + ], + "Creality Generic TPU @Ender-3V3-all": [ + "Creality Ender-3 V3 0.4 nozzle", + "Creality Ender-3 V3 0.6 nozzle", + "Creality Ender-3 V3 Plus 0.4 nozzle", + "Creality Ender-3 V3 Plus 0.6 nozzle", + "Creality Ender-3 V3 SE 0.2 nozzle", + "Creality Ender-3 V3 SE 0.4 nozzle", + "Creality Ender-3 V3 SE 0.6 nozzle", + "Creality Ender-3 V3 SE 0.8 nozzle" + ], + "Creality Generic TPU @K1-all": [ + "Creality K1 SE 0.4 nozzle", + "Creality K1C 0.4 nozzle" + ], + "Creality Generic TPU @K2-all": [ + "Creality K2 0.4 nozzle", + "Creality K2 Plus 0.4 nozzle", + "Creality K2 Pro 0.4 nozzle" + ], + "Cubicon ABS @base": [ + "Cubicon xCeler-Plus 0.4 nozzle" + ], + "Cubicon ABS-A100 @base": [ + "Cubicon xCeler-Plus 0.4 nozzle" + ], + "Cubicon ABSk @base": [ + "Cubicon xCeler-Plus 0.4 nozzle" + ], + "Cubicon PA-CF @base": [ + "Cubicon xCeler-Plus 0.4 nozzle" + ], + "Cubicon PC @base": [ + "Cubicon xCeler-Plus 0.4 nozzle" + ], + "Cubicon PETG @base": [ + "Cubicon xCeler-Plus 0.4 nozzle" + ], + "Cubicon PLA @base": [ + "Cubicon xCeler-Plus 0.4 nozzle" + ], + "Cubicon PLA+ @base": [ + "Cubicon xCeler-Plus 0.4 nozzle" + ], + "Cubicon PLAi21 @base": [ + "Cubicon xCeler-Plus 0.4 nozzle" + ], + "Flashforge Generic ABS @G3U": [ + "Flashforge Guider 3 Ultra 0.6 Nozzle" + ], + "Flashforge Generic ASA @G3U": [ + "Flashforge Guider 3 Ultra 0.6 Nozzle" + ], + "Flashforge Generic HIPS": [ + "Flashforge Guider 3 Ultra 0.6 Nozzle" + ], + "Flashforge Generic PETG @G3U": [ + "Flashforge Guider 3 Ultra 0.6 Nozzle", + "Flashforge Guider 3 Ultra 0.8 Nozzle" + ], + "Flashforge Generic PETG-CF @G3U": [ + "Flashforge Guider 3 Ultra 0.6 Nozzle", + "Flashforge Guider 3 Ultra 0.8 Nozzle" + ], + "Flashforge Generic PLA @G3U": [ + "Flashforge Guider 3 Ultra 0.6 Nozzle", + "Flashforge Guider 3 Ultra 0.8 Nozzle" + ], + "Flashforge Generic PLA-CF @G3U": [ + "Flashforge Guider 3 Ultra 0.6 Nozzle", + "Flashforge Guider 3 Ultra 0.8 Nozzle" + ], + "Flashforge HS PLA": [ + "Flashforge AD5X 0.4 nozzle", + "Flashforge AD5X 0.6 nozzle", + "Flashforge AD5X 0.8 nozzle" + ], + "Flashforge PETG-CF": [ + "Flashforge AD5X 0.4 nozzle", + "Flashforge AD5X 0.6 nozzle", + "Flashforge AD5X 0.8 nozzle" + ], + "Flashforge PLA-CF": [ + "Flashforge AD5X 0.4 nozzle", + "Flashforge AD5X 0.6 nozzle", + "Flashforge AD5X 0.8 nozzle" + ], + "Flashforge TPU 95A": [ + "Flashforge AD5X 0.4 nozzle", + "Flashforge AD5X 0.6 nozzle", + "Flashforge AD5X 0.8 nozzle" + ], + "FusRock Generic PAHT-CF": [ + "Flashforge Guider 3 Ultra 0.6 Nozzle" + ], + "FusRock Generic PET-CF": [ + "Flashforge Guider 3 Ultra 0.6 Nozzle" + ], + "Generic PETG HF @BBL X1C": [ + "Bambu Lab P1S 0.4 nozzle", + "Bambu Lab P1S 0.6 nozzle", + "Bambu Lab X1 0.4 nozzle", + "Bambu Lab X1 0.6 nozzle", + "Bambu Lab X1 0.8 nozzle" + ], + "Generic PLA @Hi-all": [ + "Creality Hi 0.4 nozzle", + "Creality Hi 0.6 nozzle" + ], + "HATCHBOX PETG @0.2 nozzle": [ + "Qidi Q1 Pro 0.2 nozzle" + ], + "Prusa Generic PA": [ + "Prusa MINIIS 0.25 nozzle", + "Prusa MINIIS 0.4 nozzle", + "Prusa MINIIS 0.6 nozzle", + "Prusa MINIIS 0.8 nozzle" + ], + "Prusa Generic PA-CF": [ + "Prusa MINIIS 0.25 nozzle", + "Prusa MINIIS 0.4 nozzle", + "Prusa MINIIS 0.6 nozzle", + "Prusa MINIIS 0.8 nozzle" + ], + "Prusa Generic PLA-CF": [ + "Prusa MINIIS 0.25 nozzle", + "Prusa MINIIS 0.4 nozzle", + "Prusa MINIIS 0.6 nozzle", + "Prusa MINIIS 0.8 nozzle" + ], + "QIDI PETG Basic @0.2 nozzle": [ + "Qidi Q1 Pro 0.2 nozzle" + ], + "QIDI PETG Rapido @0.2 nozzle": [ + "Qidi Q1 Pro 0.2 nozzle" + ], + "QIDI PETG Translucent @0.2 nozzle": [ + "Qidi Q1 Pro 0.2 nozzle" + ], + "Sovol Zero PETG HS Nozzle": [ + "Sovol Zero 0.4 nozzle" + ], + "Sovol Zero PLA Basic HS Nozzle": [ + "Sovol Zero 0.4 nozzle" + ], + "Sovol Zero PLA Silk HS Nozzle": [ + "Sovol Zero 0.4 nozzle" + ], + "re3D Greengate rPETG": [ + "re3D GigabotX 2 1.75 nozzle", + "re3D GigabotX 2 XLT 1.75 nozzle", + "re3D TerabotX 2 1.75 nozzle" + ], + "re3D PC": [ + "re3D Gigabot 4 0.8 nozzle", + "re3D Gigabot 4 XLT 0.8 nozzle", + "re3D Terabot 4 0.8 nozzle" + ], + "re3D PETG": [ + "re3D Gigabot 4 0.8 nozzle", + "re3D Gigabot 4 XLT 0.8 nozzle", + "re3D Terabot 4 0.8 nozzle" + ], + "re3D PLA": [ + "re3D Gigabot 4 0.8 nozzle", + "re3D Gigabot 4 XLT 0.8 nozzle", + "re3D Terabot 4 0.8 nozzle" + ], + "re3D rPP": [ + "re3D GigabotX 2 1.75 nozzle", + "re3D GigabotX 2 XLT 1.75 nozzle", + "re3D TerabotX 2 1.75 nozzle" + ] + } +} diff --git a/scripts/check_profile.ps1 b/scripts/check_profile.ps1 index c016e8f299..c38d31167e 100644 --- a/scripts/check_profile.ps1 +++ b/scripts/check_profile.ps1 @@ -5,11 +5,13 @@ .DESCRIPTION The Windows counterpart of scripts/check_profile.sh, and kept deliberately close to it. - Runs the same five checks, in the same order, with the same validator flags, and with the + Runs the same six checks, in the same order, with the same validator flags, and with the same semantics: every check runs even after an earlier one fails (the workflow's continue-on-error), then the script exits non-zero once at the end. profile_tool scripts/orca_profile_tool.py check + legacy_compat scripts/orca_profile_tool.py legacy-compat --check against + the v2.4.2 profiles validate_system validator -p -l validate_slice validator -p -s -l validate_filament_subtypes validator -p -l -f @@ -20,8 +22,9 @@ never sees a profile no .json indexes, a preset name two files claim, or a file normalize and update-index would still rewrite. - Everything that has to be downloaded - the profile validator and the custom-preset fixture - archives - lands under a per-user cache directory (%LOCALAPPDATA%\orca-profile-check) and + Everything that has to be downloaded - the profile validator, the custom-preset fixture + archives and the v2.4.2 profile tree - lands under a per-user cache directory + (%LOCALAPPDATA%\orca-profile-check) and is reused on the next run. Being outside the checkout, that directory is shared by every worktree on the machine. It also holds one log per check plus a copy of the comment CI would post on the PR. @@ -48,7 +51,8 @@ check it cannot narrow is validate_slice for a vendor that ships no printers; the summary reports that one as skipped, and naming it explicitly still runs it. profile_tool keeps its two cross-vendor checks (setting_id and filament_id) tree-wide, so a scoped run can still - fail on another vendor's files. + fail on another vendor's files, and legacy_compat is tree-wide too: the map it checks covers + every vendor. .PARAMETER Validator OrcaSlicer_profile_validator.exe to use; also $env:ORCA_PROFILE_VALIDATOR. Default: the local @@ -116,7 +120,7 @@ $HostArch = switch ($HostArch) { default { 'x86' } } -$AllChecks = @('profile_tool', 'validate_system', 'validate_slice', 'validate_filament_subtypes', 'validate_custom') +$AllChecks = @('profile_tool', 'legacy_compat', 'validate_system', 'validate_slice', 'validate_filament_subtypes', 'validate_custom') $script:LogWriter = $null $script:Python = '' @@ -439,6 +443,43 @@ function Expand-VendorPresets([string] $Zip, [string] $Tree, [string] $Prefix) { return $kept } +# The profile tree of the release resources\info\legacy_compatible_printers.json is generated +# against. The tag is immutable, so the extracted tree is cached in the work dir and -Refresh +# re-extracts it. A checkout without the tag (a shallow clone, a fork that never fetched tags) +# gets it from upstream, as CI does. git archive writes a file rather than a pipe, which +# PowerShell would re-encode, and tar.exe ships with Windows 10 and later. +$LegacyBaseTag = 'v2.4.2' +$LegacyBaseUrl = 'https://github.com/OrcaSlicer/OrcaSlicer.git' + +function Get-LegacyBaseProfiles { + $ErrorActionPreference = 'Continue' + $base = Join-Path (Join-Path $WorkDir 'legacy-base') $LegacyBaseTag + $profiles = Join-Path (Join-Path $base 'resources') 'profiles' + if ($Refresh -or -not (Test-Path -LiteralPath $profiles -PathType Container)) { + & git -C $RepoRoot rev-parse -q --verify "refs/tags/$LegacyBaseTag^{commit}" 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host "fetching tag $LegacyBaseTag from $LegacyBaseUrl ..." + & git -C $RepoRoot fetch --no-tags $LegacyBaseUrl "refs/tags/${LegacyBaseTag}:refs/tags/${LegacyBaseTag}" + if ($LASTEXITCODE -ne 0) { throw "cannot fetch tag $LegacyBaseTag" } + } + # Extracted beside its final place and moved there whole, so an interrupted extraction is + # never mistaken for the tree on the next run. + $partial = "$base.partial" + foreach ($dir in $base, $partial) { + if (Test-Path -LiteralPath $dir) { Remove-Item -LiteralPath $dir -Recurse -Force } + } + New-Item -ItemType Directory -Force -Path $partial | Out-Null + $archive = Join-Path $partial 'profiles.tar' + & git -C $RepoRoot archive -o $archive $LegacyBaseTag resources/profiles + if ($LASTEXITCODE -ne 0) { throw "cannot archive resources/profiles of $LegacyBaseTag" } + & tar -xf $archive -C $partial + if ($LASTEXITCODE -ne 0) { throw "cannot extract $archive" } + Remove-Item -LiteralPath $archive -Force + Move-Item -LiteralPath $partial -Destination $base + } + return $profiles +} + # ---------------------------------------------------------------------------- checks $CheckBodies = @{ @@ -447,6 +488,14 @@ $CheckBodies = @{ Invoke-Tool -Exe (Resolve-Python) -Arguments (@((Join-Path $RepoRoot 'scripts\orca_profile_tool.py'), 'check', '--profiles', $ProfilesDir) + $VendorPyArgs) } + # The map says which printers a user filament preset made against 2.4.2 can be migrated + # back to, so a profile change that renames a filament preset or narrows its + # compatible_printers has to regenerate it (the CI comment gives the command). + legacy_compat = { + $base = Get-LegacyBaseProfiles + Invoke-Tool -Exe (Resolve-Python) -Arguments @((Join-Path $RepoRoot 'scripts\orca_profile_tool.py'), 'legacy-compat', '--profiles', $ProfilesDir, '--base', $base, '--check') + } + validate_system = { Invoke-Tool -Exe $Validator -Arguments (@('-p', $ProfilesDir) + $VendorArgs + @('-l', "$LogLevel")) } @@ -581,6 +630,7 @@ $CheckBodies = @{ # Heading CI puts above this check's log in the PR comment. $CommentHeadings = @{ profile_tool = '### Profile Check Failed (orca_profile_tool.py)' + legacy_compat = '### Legacy Compatible Printers Map Out of Date' validate_system = '### System Profile Validation Failed' validate_slice = '### Slice Validation Failed (custom g-code expansion)' validate_filament_subtypes = '### Filament Subtype Validation Failed' @@ -627,7 +677,7 @@ try { [Console]::OutputEncoding = New-Object Text.UTF8Encoding($false) Push-UserPresets - if ($Checks | Where-Object { $_ -ne 'profile_tool' }) { $Validator = Resolve-Validator } + if ($Checks | Where-Object { $_ -ne 'profile_tool' -and $_ -ne 'legacy_compat' }) { $Validator = Resolve-Validator } # An empty printer set is a failure to the sweep, so validate_slice is recorded as skipped # rather than run for a vendor that ships no printers (the filament-only OrcaFilamentLibrary); diff --git a/scripts/check_profile.sh b/scripts/check_profile.sh index a6d7ca3b2e..9eddf71d3a 100755 --- a/scripts/check_profile.sh +++ b/scripts/check_profile.sh @@ -2,12 +2,13 @@ # # Local twin of the "Check profiles" CI job (.github/workflows/check_profiles.yml). # -# Runs the same five checks, in the same order, with the same validator flags, and with the +# Runs the same six checks, in the same order, with the same validator flags, and with the # same semantics: every check runs even after an earlier one fails (the workflow's # continue-on-error), then the script exits non-zero once at the end. # -# Everything that has to be downloaded - the profile validator and the custom-preset fixture -# archives - lands under a per-user cache directory and is reused on the next run. Being outside +# Everything that has to be downloaded - the profile validator, the custom-preset fixture +# archives and the v2.4.2 profile tree - lands under a per-user cache directory and is reused on +# the next run. Being outside # the checkout, that directory is shared by every worktree on the machine. It also holds one log # per check plus a copy of the comment CI would post on the PR. # @@ -58,7 +59,7 @@ LOG_LEVEL=2 PREFER_DOWNLOAD=0 REFRESH=0 -ALL_CHECKS=(profile_tool validate_system validate_slice validate_filament_subtypes validate_custom) +ALL_CHECKS=(profile_tool legacy_compat validate_system validate_slice validate_filament_subtypes validate_custom) CHECKS=() # "pass|fail" per check that ran, plus "skipwhy" for one a vendor # scope left out; a string rather than an array because bash 3.2 (still the /bin/bash on macOS) @@ -73,6 +74,8 @@ Usage: scripts/check_profile.sh [OPTION]... [CHECK]... Checks (default: all, in this order): profile_tool scripts/orca_profile_tool.py check + legacy_compat scripts/orca_profile_tool.py legacy-compat --check against the + v2.4.2 profiles validate_system validator -p -l validate_slice validator -p -s -l validate_filament_subtypes validator -p -l -f @@ -100,7 +103,8 @@ Note: --vendor narrows validate_custom too, by keeping only that vendor's preset fixture tree. The one check it cannot narrow is validate_slice for a vendor that ships no printers; the summary reports that one as skipped, and naming it explicitly still runs it. profile_tool keeps its two cross-vendor checks (setting_id and filament_id) tree-wide, so a -scoped run can still fail on another vendor's files. +scoped run can still fail on another vendor's files, and legacy_compat is tree-wide too: the map +it checks covers every vendor. EOF } @@ -379,6 +383,44 @@ check_profile_tool() { python3 "${REPO_ROOT}/scripts/orca_profile_tool.py" check --profiles "${PROFILES_DIR}" --vendor "${VENDOR}" } +# The profile tree of the release resources/info/legacy_compatible_printers.json is generated +# against. The tag is immutable, so the extracted tree is cached in the work dir and --refresh +# re-extracts it. A checkout without the tag (a shallow clone, a fork that never fetched tags) +# gets it from upstream, as CI does. +LEGACY_BASE_TAG="v2.4.2" +LEGACY_BASE_URL="https://github.com/OrcaSlicer/OrcaSlicer.git" + +legacy_base_profiles() { + local base="${WORK_DIR}/legacy-base/${LEGACY_BASE_TAG}" + if [ "${REFRESH}" -eq 1 ] || [ ! -d "${base}/resources/profiles" ]; then + if ! git -C "${REPO_ROOT}" rev-parse -q --verify "refs/tags/${LEGACY_BASE_TAG}^{commit}" >/dev/null; then + msg "fetching tag ${LEGACY_BASE_TAG} from ${LEGACY_BASE_URL} ..." + git -C "${REPO_ROOT}" fetch --no-tags "${LEGACY_BASE_URL}" \ + "refs/tags/${LEGACY_BASE_TAG}:refs/tags/${LEGACY_BASE_TAG}" || return 1 + fi + # Extracted beside its final place and moved there whole, so an interrupted extraction is + # never mistaken for the tree on the next run. + local partial="${base}.partial" + rm -rf "${base}" "${partial}" + mkdir -p "${partial}" || return 1 + if ! git -C "${REPO_ROOT}" archive "${LEGACY_BASE_TAG}" resources/profiles | tar -x -C "${partial}"; then + rm -rf "${partial}" + return 1 + fi + mv "${partial}" "${base}" || return 1 + fi + printf '%s\n' "${base}/resources/profiles" +} + +# The map says which printers a user filament preset made against 2.4.2 can be migrated back +# to, so a profile change that renames a filament preset or narrows its compatible_printers has +# to regenerate it (the CI comment gives the command). +check_legacy_compat() { + local base + base="$(legacy_base_profiles)" || return 1 + python3 "${REPO_ROOT}/scripts/orca_profile_tool.py" legacy-compat --profiles "${PROFILES_DIR}" --base "${base}" --check +} + check_validate_system() { "${VALIDATOR}" -p "${PROFILES_DIR}" -v "${VENDOR}" -l "${LOG_LEVEL}" } @@ -563,6 +605,7 @@ EOF comment_heading() { case "$1" in profile_tool) echo "### Profile Check Failed (orca_profile_tool.py)" ;; + legacy_compat) echo "### Legacy Compatible Printers Map Out of Date" ;; validate_system) echo "### System Profile Validation Failed" ;; validate_slice) echo "### Slice Validation Failed (custom g-code expansion)" ;; validate_filament_subtypes) echo "### Filament Subtype Validation Failed" ;; diff --git a/scripts/orca_profile_tool.py b/scripts/orca_profile_tool.py index ec94eb7653..f1a428cdfe 100755 --- a/scripts/orca_profile_tool.py +++ b/scripts/orca_profile_tool.py @@ -11,6 +11,7 @@ commands: normalize rewrite profile files into their canonical shape trim delete profile files no .json list references update-index regenerate the *_list sections of .json + legacy-compat write the printers user presets can be migrated back to options shared by several commands: --vendor VENDOR act on one vendor bundle only; repeatable, empty means all @@ -24,6 +25,9 @@ After adding, renaming or deleting profile files, run: normalize -> update-index -> generate-id -> check normalize supplies missing types; update-index registers presets before id generation. +After renaming filament presets or narrowing their compatible_printers, also +rerun legacy-compat --base <2.4.2 profiles> so user presets made against 2.4.2 +can be migrated back to their printers; CI runs it with --check. Use trim only for deliberate cleanup, previewed with --dry-run: it judges against the current index and can delete newly added, unindexed presets. @@ -3072,6 +3076,139 @@ def fix_variant_arrays(profiles_dir=PROFILES_DIR, vendors=None, dry_run=False, s return len(changed), errors +# --------------------------------------------------------------------------- +# legacy-compat +# --------------------------------------------------------------------------- + +LEGACY_COMPAT_PATH = os.path.normpath( + os.path.join(SCRIPTS_DIR, "..", "resources", "info", "legacy_compatible_printers.json")) + + +def _renamed_from(data): + """A preset's renamed_from names. Profiles write one ';'-separated string, as the + loader's unescape_strings_cstyle reads it; none of them quote or escape a name.""" + value = data.get("renamed_from") or [] + return [n for v in _as_list(value) for n in v.split(";") if n] + + +def _flatten_presets(profiles_dir): + """Every bundle's composed filament and printer presets, for legacy-compat. + + Returns (filaments, printers): filaments maps (vendor, name) to a dict with the + resolved compatible_printers, filament_id, extruder variants, renamed_from and + whether it is instantiated; printers maps each instantiated printer name to a + dict with its renamed_from and extruder variants. instantiation is read from the + preset's own file, as the loader requires every file to state it. + """ + def instantiated(data): + return str(data.get("instantiation", "")).lower() == "true" + + ofl = load_vendor_configs(profiles_dir, OFL)["filament"] + filaments, printers = {}, {} + for vendor in list_vendor_names(profiles_dir): + configs = load_vendor_configs(profiles_dir, vendor) + cache = {} + for name, (_rel, data) in configs["filament"].items(): + config = compose_config(name, configs["filament"], ofl, cache, vendor == OFL) + filaments[(vendor, name)] = { + "compatible_printers": set(_as_list(config.get("compatible_printers", ([],))[0])), + "filament_id": config.get("filament_id", (None,))[0], + "variants": set(_as_list(config.get("filament_extruder_variant", ([],))[0])), + "instantiation": instantiated(data), + "renamed_from": _renamed_from(data), + } + cache = {} + for name, (_rel, data) in configs["machine"].items(): + if instantiated(data): + config = compose_config(name, configs["machine"], {}, cache) + printers[name] = { + "renamed_from": _renamed_from(data), + "variants": set(_as_list(config.get("printer_extruder_variant", ([],))[0])), + } + return filaments, printers + + +def legacy_compatible_printers(base_dir, profiles_dir=PROFILES_DIR): + """Printers each filament preset of the base release covered but the current + preset it loads as no longer lists, kept only where a current preset with + the same filament_id still covers that printer. + + User presets store only their diff, so they inherit their parent's + compatible_printers; when a profile change narrows that list, a user preset + made against the base release disappears from printers it was used on. The + app offers to write these printers into such user presets. Requiring a same-id + successor leaves out printers a filament was deliberately taken off, and + requiring a shared extruder variant keeps a printer from picking up another + variant's values. + + Keyed by the base release's preset name, which is what such a user preset's + inherits still says. One current preset can stand in for several old ones + through renamed_from, each with its own old list, so the key tells them + apart. An old name resolves as the loader resolves an inherits: to the + current selectable preset of that name, else to one listing it in + renamed_from. A preset that is not instantiated never enters the loader's + collection, so it does not stop the renamed_from lookup. Printer names are + translated through the printers' renamed_from. Returns + {old preset name: sorted printer names}. + """ + old_filaments, _ = _flatten_presets(base_dir) + new_filaments, new_printers = _flatten_presets(profiles_dir) + renamed_printer = {old: new for new, rec in new_printers.items() for old in rec["renamed_from"]} + + by_name, by_old_name = {}, {} + for (vendor, name), rec in new_filaments.items(): + if rec["instantiation"]: + by_name.setdefault(name, []).append((vendor, rec)) + for old_name in rec["renamed_from"]: + by_old_name.setdefault(old_name, []).append((vendor, rec)) + + covering = {} # filament_id -> printers some current instantiated preset covers + for rec in new_filaments.values(): + if rec["instantiation"] and rec["filament_id"]: + covering.setdefault(rec["filament_id"], set()).update(rec["compatible_printers"]) + + def shares_variant(filament, printer): + variants = new_printers[printer]["variants"] + return not filament["variants"] or not variants or bool(filament["variants"] & variants) + + result = {} + for (vendor, name), old in sorted(old_filaments.items()): + # Only a selectable preset can be a user preset's parent. + if not old["instantiation"]: + continue + candidates = by_name.get(name) or by_old_name.get(name, []) + same_vendor = [r for v, r in candidates if v == vendor] + rec = (same_vendor or [r for _, r in candidates] or [None])[0] + # An empty list means every printer, so nothing can have been lost. + if rec is None or not rec["compatible_printers"]: + continue + lost = {renamed_printer.get(p, p) for p in old["compatible_printers"]} - rec["compatible_printers"] + lost &= covering.get(rec["filament_id"], set()) & set(new_printers) + lost = {p for p in lost if shares_variant(rec, p)} + if lost: + result.setdefault(name, set()).update(lost) + return {name: sorted(printers) for name, printers in result.items()} + + +def run_legacy_compat(base_dir, profiles_dir, output, check): + presets = legacy_compatible_printers(base_dir, profiles_dir) + if check: + try: + with open(output, encoding="utf-8") as f: + current = json.load(f).get("presets") + except (OSError, ValueError, AttributeError): + current = None + if current != presets: + print_error(f"{output} is out of date; rerun legacy-compat --base {base_dir}") + return 1 + print_success(f"{output} is up to date") + return 0 + with open(output, "w", encoding="utf-8", newline="\n") as f: + f.write(json.dumps({"presets": presets}, indent=4, sort_keys=True, ensure_ascii=False) + "\n") + print_info(f"wrote {len(presets)} presets / {sum(map(len, presets.values()))} printers to {output}") + return 0 + + # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- @@ -3093,11 +3230,17 @@ examples: preview resizing the variant arrays check rejects in that bundle orca_profile_tool.py check --strict --vendor BBL also hold every preset's inherited variant arrays to its own width + orca_profile_tool.py legacy-compat --base /tmp/v2.4.2/resources/profiles + rewrite resources/info/legacy_compatible_printers.json against 2.4.2's + profiles (git archive v2.4.2 resources/profiles | tar -x -C /tmp/v2.4.2) after adding, renaming or deleting profile files, run in this order: normalize -> update-index -> generate-id -> check normalize supplies missing types; update-index registers presets before id generation. +After renaming filament presets or narrowing their compatible_printers, also +rerun legacy-compat --base <2.4.2 profiles> so user presets made against 2.4.2 +can be migrated back to their printers; CI runs it with --check. Use trim only for deliberate cleanup, previewed with --dry-run: it judges against the current index and can delete newly added, unindexed presets. """ @@ -3227,6 +3370,23 @@ def build_parser(): "Use trim only for deliberate unindexed-file cleanup, previewed with\n" "--dry-run; it can also delete newly authored presets.") + legacy_cmd = add( + "legacy-compat", [profiles_opt], + "write the printers user presets can be migrated back to", + "Compare the tree with an older release's profiles and write, per filament\n" + "preset of that release, the printers it covered then that the preset it now\n" + "loads as no longer lists, where a preset with the same filament_id still\n" + "covers them. The app offers to write these into user presets that inherit\n" + "that preset by its old name and do not set compatible_printers, so a\n" + "narrowed list does not hide them on printers they were made for.") + legacy_cmd.add_argument("--base", required=True, metavar="DIR", + help="the older release's resources/profiles directory") + legacy_cmd.add_argument("--output", default=LEGACY_COMPAT_PATH, metavar="FILE", + help="map to write (default: resources/info/" + "legacy_compatible_printers.json)") + legacy_cmd.add_argument("--check", action="store_true", + help="fail if the map differs from what --base implies; write nothing") + return parser @@ -3260,6 +3420,9 @@ def main(argv=None): return run_generate_id(profiles_dir, vendors, args.filament_id, args.setting_id, args.dry_run) + if args.command == "legacy-compat": + return run_legacy_compat(args.base, profiles_dir, args.output, args.check) + if args.command == "fix-variant": _changed, errors = fix_variant_arrays(profiles_dir, vendors, dry_run=args.dry_run, strict=args.strict) diff --git a/scripts/tests/test_profile_tool.py b/scripts/tests/test_profile_tool.py index d32dbd144e..167951d609 100644 --- a/scripts/tests/test_profile_tool.py +++ b/scripts/tests/test_profile_tool.py @@ -1258,6 +1258,118 @@ class TestVariantNames(TreeCase): self.assertIn('holds the legacy variant "Direct Drive Normal"', out) +# --------------------------------------------------------------------------- +# legacy-compat +# --------------------------------------------------------------------------- + +class TestLegacyCompat(unittest.TestCase): + """A base release and the current tree, each with one vendor.""" + + def setUp(self): + self.base, self.cur = Tree(), Tree() + self.addCleanup(self.base.cleanup) + self.addCleanup(self.cur.cleanup) + + @staticmethod + def printer(tree, name, **extra): + tree.write("V", f"machine/{name}.json", + {"type": "machine", "name": name, "instantiation": "true", **extra}) + tree.index("V", "machine", name, f"machine/{name}.json") + + @staticmethod + def filament(tree, name, printers, filament_id="OFpetg01", **extra): + tree.write("V", f"filament/{name}.json", + {"type": "filament", "name": name, "instantiation": "true", + "filament_id": filament_id, "compatible_printers": printers, **extra}) + tree.index("V", "filament", name, f"filament/{name}.json") + + def generate(self): + return apt.legacy_compatible_printers(self.base.profiles, self.cur.profiles) + + def test_renamed_and_narrowed_preset_keeps_printers_a_same_id_sibling_covers(self): + for tree in (self.base, self.cur): + for p in ("A 0.4", "A 0.6", "B 0.4"): + self.printer(tree, p) + self.filament(self.base, "PETG @A-all", ["A 0.4", "A 0.6", "B 0.4"]) + self.filament(self.cur, "PETG @V A-all", ["A 0.6"], renamed_from="PETG @A-all;PETG A-all") + self.filament(self.cur, "PETG @A 0.4", ["A 0.4"]) + self.assertEqual(self.generate(), {"PETG @A-all": ["A 0.4"]}) + + def test_each_old_name_gets_back_only_its_own_list(self): + # The current "PC @0.4" existed in the base and also replaces the split "PC". + for tree in (self.base, self.cur): + for p in ("A 0.4", "A 0.8"): + self.printer(tree, p) + self.filament(self.base, "PC", ["A 0.4", "A 0.8"]) + self.filament(self.base, "PC @0.4", ["A 0.4"]) + self.filament(self.cur, "PC @0.4", ["A 0.4"], renamed_from="PC") + self.filament(self.cur, "PC @0.8", ["A 0.8"]) + self.assertEqual(self.generate(), {"PC": ["A 0.8"]}) + + def test_old_name_now_a_base_preset_resolves_through_renamed_from(self): + for tree in (self.base, self.cur): + for p in ("I 0.4", "Plus 0.4"): + self.printer(tree, p) + self.filament(self.base, "PLA @base", ["I 0.4", "Plus 0.4"]) + self.filament(self.cur, "PLA @base", [], instantiation="false") + self.filament(self.cur, "PLA @I 0.4", ["I 0.4"], renamed_from="PLA @base") + self.filament(self.cur, "PLA @Plus 0.4", ["Plus 0.4"]) + self.assertEqual(self.generate(), {"PLA @base": ["Plus 0.4"]}) + + def test_printer_without_a_successor_is_left_out(self): + for tree in (self.base, self.cur): + for p in ("A 0.4", "A 0.6"): + self.printer(tree, p) + self.filament(self.base, "PETG @A", ["A 0.4", "A 0.6"]) + self.filament(self.cur, "PETG @A", ["A 0.6"]) + self.filament(self.cur, "Other @A", ["A 0.4"], filament_id="OFother1") + self.assertEqual(self.generate(), {}) + + def test_printer_names_follow_printer_renames(self): + self.printer(self.base, "Old 0.4") + self.printer(self.base, "A 0.6") + self.printer(self.cur, "New 0.4", renamed_from="Old 0.4") + self.printer(self.cur, "A 0.6") + self.filament(self.base, "PETG @A", ["Old 0.4", "A 0.6"]) + self.filament(self.cur, "PETG @A", ["A 0.6"]) + self.filament(self.cur, "PETG @New", ["New 0.4"]) + self.assertEqual(self.generate(), {"PETG @A": ["New 0.4"]}) + + def test_printer_without_a_shared_extruder_variant_is_left_out(self): + for tree in (self.base, self.cur): + self.printer(tree, "Direct 0.4", printer_extruder_variant=["Direct Drive Standard"]) + self.printer(tree, "Bowden 0.4", printer_extruder_variant=["Bowden Standard"]) + self.filament(self.base, "PETG @all", ["Direct 0.4", "Bowden 0.4"]) + self.filament(self.cur, "PETG @all", ["Direct 0.4"], + filament_extruder_variant=["Direct Drive Standard"]) + self.filament(self.cur, "PETG @Bowden", ["Bowden 0.4"]) + self.assertEqual(self.generate(), {}) + + def test_preset_now_listing_no_printer_is_skipped(self): + for tree in (self.base, self.cur): + self.printer(tree, "A 0.4") + self.filament(self.base, "PETG @A", ["A 0.4"]) + self.filament(self.cur, "PETG @A", []) + self.assertEqual(self.generate(), {}) + + def test_check_passes_on_a_current_map_and_fails_on_a_stale_one(self): + for tree in (self.base, self.cur): + for p in ("A 0.4", "A 0.6"): + self.printer(tree, p) + self.filament(self.base, "PETG @A", ["A 0.4", "A 0.6"]) + self.filament(self.cur, "PETG @A", ["A 0.6"]) + self.filament(self.cur, "PETG @A 0.4", ["A 0.4"]) + output = os.path.join(self.cur.dir, "map.json") + argv = ["legacy-compat", "--base", self.base.profiles, "--profiles", self.cur.profiles, + "--output", output] + with contextlib.redirect_stdout(io.StringIO()): + self.assertEqual(apt.main(argv), 0) + self.assertEqual(apt.main(argv + ["--check"]), 0) + with open(output, "w", encoding="utf-8") as f: + json.dump({"presets": {}}, f) + self.assertEqual(apt.main(argv + ["--check"]), 1) + + class TestDispatch(TreeCase): def test_each_command_reaches_its_own_writer(self): self.t.write("V", "filament/A.json", {"type": "filament", "name": "A"}) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 2a75b0f060..eb30763ce6 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -82,6 +82,7 @@ #include "LifecycleEvents.hpp" #include "Utils.hpp" #include "InstanceLock.hpp" +#include "nlohmann/json.hpp" #include #include "PlaceholderParser.hpp" @@ -2758,6 +2759,118 @@ void PresetCollection::update_after_user_presets_loaded() return; } +// A user preset file as written: it holds only the keys the preset sets itself, and an inherits that names a +// renamed parent by the old name the preset was made against. Throws if the file can not be read or parsed. +static json read_user_preset_file(const std::string &file) +{ + boost::nowide::ifstream in(file); + return json::parse(in); +} + +std::vector PresetCollection::legacy_user_filament_presets(const std::map> &printers_by_parent) +{ + auto has_entry = [&printers_by_parent](const std::string &name) { return printers_by_parent.count(name) > 0; }; + std::vector legacy_presets; + // The preset sync thread can add or delete presets meanwhile. + lock(); + for (const Preset &preset : m_presets) { + if (!preset.is_user() || preset.inherits().empty()) + continue; + const Preset *parent = get_preset_parent(preset); + if (parent == nullptr || !parent->is_system) + continue; + // An empty list already covers every printer. + const std::vector &own = preset.config.option("compatible_printers")->values; + if (own.empty()) + continue; + // The parent's name in the older release is its current name or one in its renamed_from, so only the + // files of presets whose parent has an entry under one of those are read. + if (!has_entry(parent->name) && std::none_of(parent->renamed_from.begin(), parent->renamed_from.end(), has_entry)) + continue; + LegacyUserFilamentPreset legacy{preset.name, parent->name, {}, {}, preset.file, own}; + try { + const json j = read_user_preset_file(preset.file); + // A preset whose file sets compatible_printers, even to an empty list, was given its printers on + // purpose and keeps exactly those. + if (j.contains("compatible_printers")) + continue; + legacy.inherits = j.value(BBL_JSON_KEY_INHERITS, std::string()); + } catch (const std::exception &err) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": can not read " << preset.file << ": " << err.what(); + continue; + } + auto it = printers_by_parent.find(legacy.inherits); + if (it == printers_by_parent.end()) + continue; + for (const std::string &printer : it->second) + if (std::find(own.begin(), own.end(), printer) == own.end()) { + legacy.printers.push_back(printer); + legacy.compatible_printers.push_back(printer); + } + if (!legacy.printers.empty()) + legacy_presets.push_back(std::move(legacy)); + } + unlock(); + return legacy_presets; +} + +bool PresetCollection::write_legacy_user_filament_preset(const LegacyUserFilamentPreset &legacy) +{ + // Only compatible_printers changes in the file. Saving the whole preset would also rewrite an inherits that + // names a renamed parent by its old name, the only name older releases know it by. + try { + json j = read_user_preset_file(legacy.file); + // Changed by hand or by another instance since the preset was found: a list of its own stands, and + // another parent would need other printers. + if (j.contains("compatible_printers") || j.value(BBL_JSON_KEY_INHERITS, std::string()) != legacy.inherits) + return false; + j["compatible_printers"] = legacy.compatible_printers; + if (const std::error_code ec = write_file_atomically(legacy.file, j.dump(1, '\t') + "\n")) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to save " << legacy.file << ": " << ec.message(); + return false; + } + } catch (const std::exception &err) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not migrate " << legacy.file << ": " << err.what(); + return false; + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": user preset " << legacy.preset << " gets " << legacy.printers.size() + << " printer(s) its parent listed in an older release"; + return true; +} + +void PresetCollection::apply_legacy_user_filament_presets(const std::vector &written) +{ + lock(); + for (const LegacyUserFilamentPreset &legacy : written) { + auto it = this->find_preset_internal(legacy.preset); + if (it == m_presets.end() || it->name != legacy.preset) + continue; // deleted by the preset sync thread meanwhile + std::vector &saved = it->config.option("compatible_printers", true)->values; + std::vector &edited = m_edited_preset.config.option("compatible_printers", true)->values; + // Added to the list the preset holds now rather than replacing it: the cloud sync may have given the + // preset a newer list since it was found, and that list is still to be written out. + std::vector printers = saved; + for (const std::string &printer : legacy.printers) + if (std::find(printers.begin(), printers.end(), printer) == printers.end()) + printers.push_back(printer); + // A change to the list pending in the settings tab stays pending. Without one, the edited copy and the + // snapshot unsaved changes are measured against take the written list, so nothing reads as modified. + if (m_idx_selected == static_cast(it - m_presets.begin()) && edited == saved) { + edited = printers; + if (m_saved_preset.name == it->name) + m_saved_preset.config.option("compatible_printers", true)->values = printers; + } + saved = printers; + // Push the change to the cloud copy, as an edit in the settings tab would. A sync already pending for + // the preset (a download waiting to be saved, a deletion) keeps its state. + if (!it->setting_id.empty() && it->sync_info.empty()) { + it->sync_info = "update"; + it->save_info(); + } + } + unlock(); +} + //BBS: validate_preset bool PresetCollection::validate_preset(const std::string &preset_name, std::string &inherit_name) { diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index f06c8f4224..98e7e62e90 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -551,6 +551,20 @@ struct PresetConfigSubstitutions { // PrusaSlicer and reading the user Print / Filament / Printer profiles. using PresetsConfigSubstitutions = std::vector; +// A user filament preset made against an older release that no longer shows on printers its system parent +// listed then, and those printers. Migrating it writes them into the preset's own compatible_printers. +struct LegacyUserFilamentPreset { + std::string preset; + std::string parent; + // The parent's name as the preset file states it: the name the older release knew it by. + std::string inherits; + // The printers the preset lost: what the prompt lists. + std::vector printers; + std::string file; + // The list the file gets: the parent's current printers plus the lost ones. + std::vector compatible_printers; +}; + // Collections of presets of the same type (one of the Print, Filament or Printer type). class PresetCollection { @@ -601,6 +615,18 @@ public: void save_user_presets(const std::string& dir_path, const std::string& type, std::map& need_to_delete_list); bool load_user_preset(std::string name, std::map preset_values, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule, const PresetOrigin &load_origin = PresetOrigin(PresetOrigin::Kind::User)); void update_after_user_presets_loaded(); + // User presets store only their diff, so a user preset inherits its system parent's compatible_printers + // and disappears from a printer once a profile update narrows that list. Lists the user presets whose + // files do not set compatible_printers, and the printers their parent covered in an older release but + // no longer lists. printers_by_parent is keyed by the parent's name in that release, which is the name + // the preset file's inherits still holds. + std::vector legacy_user_filament_presets(const std::map> &printers_by_parent); + // Writes legacy.compatible_printers into the preset's file and keeps every other key. The caller holds the + // user presets lock. False if the file has gained a compatible_printers key or another parent since the + // preset was found, or could not be written. + static bool write_legacy_user_filament_preset(const LegacyUserFilamentPreset &legacy); + // Gives the loaded presets the lists their files were just given, and marks cloud copies for upload. + void apply_legacy_user_filament_presets(const std::vector &written); //BBS: get user presets int get_user_presets(PresetBundle *preset_bundle, std::vector &result_presets); void set_sync_info_and_save(std::string name, std::string setting_id, std::string syncinfo, long long update_time, const std::string& user_id); diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 0fd0b57e8d..a817ebdde9 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,8 @@ #include "ParallelResolve.hpp" #include "Preset.hpp" #include "PresetCacheFormat.hpp" +#include "Time.hpp" +#include "Zipper.hpp" #include "PrintConfig.hpp" #include "PublishSettings.hpp" #include "FilamentMixer.hpp" @@ -96,6 +99,21 @@ using json = nlohmann::json; namespace Slic3r { +// Per system filament preset of an older release, by its name then, the printers it listed that the preset it +// now loads as does not (resources/info/legacy_compatible_printers.json, written by orca_profile_tool.py +// legacy-compat). Empty if the file is missing or malformed. +static std::map> legacy_compatible_printers() +{ + const fs::path path = fs::path(resources_dir()) / "info" / "legacy_compatible_printers.json"; + try { + boost::nowide::ifstream in(path.string()); + return json::parse(in).at("presets").get>>(); + } catch (const std::exception &err) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not read " << path.string() << ": " << err.what(); + return {}; + } +} + // Project-level options imported from a loaded 3MF into project_config. s_project_options_published // below is the reduced subset that crosses over in "published" 3MF mode; keep both in sync. // s_project_options_published additionally carries wipe_tower_rotation_angle, which normal @@ -1045,6 +1063,73 @@ bool PresetBundle::backup_user_folder() const } } +bool PresetBundle::backup_user_folder_to_zip(const std::string &zip_path) const +{ + const fs::path user_dir = fs::path(data_dir()) / PRESET_USER_DIR; + BOOST_LOG_TRIVIAL(info) << "Backing up user folder to: " << zip_path; + try { + Zipper zipper(zip_path); + // Directories get their own entries, which extract_archive_confined() needs to recreate them. + zipper.add_entry(std::string(PRESET_USER_DIR) + "/", nullptr, 0); + // The loader follows symlinked folders, and migration writes the presets in them. + for (const fs::directory_entry &entry : fs::recursive_directory_iterator(user_dir, fs::directory_options::follow_directory_symlink)) { + const std::string name = (fs::path(PRESET_USER_DIR) / entry.path().lexically_relative(user_dir)).generic_string(); + if (fs::is_directory(entry.status())) { + zipper.add_entry(name + "/", nullptr, 0); + continue; + } + if (!fs::is_regular_file(entry.status())) + continue; + boost::nowide::ifstream in(entry.path().string(), std::ios::binary); + const std::string data((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + if (!in.good() && !in.eof()) + throw Slic3r::RuntimeError("can not read " + entry.path().string()); + zipper.add_entry(name, data.data(), data.size()); + } + zipper.finalize(); + return true; + } catch (const std::exception &ex) { + BOOST_LOG_TRIVIAL(error) << "Exception during user folder backup: " << ex.what(); + boost::system::error_code ec; + fs::remove(zip_path, ec); + return false; + } +} + +std::vector PresetBundle::legacy_user_filament_presets() +{ + return filaments.legacy_user_filament_presets(legacy_compatible_printers()); +} + +PresetBundle::LegacyUserFilamentPresetMigration PresetBundle::migrate_legacy_user_filament_presets(const std::vector &legacy_presets) +{ + LegacyUserFilamentPresetMigration result; + std::vector written; + const std::string zip_path = data_dir() + "/filament_preset_migration_" + + Utils::time2str(Utils::TimeZone::utc, Utils::TimeFormat::iso8601Z) + "_backup.zip"; + // Before the lock: zipping a large folder takes longer than a guard may be held. + if (!backup_user_folder_to_zip(zip_path)) + return result; + result.backup = zip_path; + { + // One hold for every write, so a lock another instance keeps is one outcome rather than a failure per + // preset (the lock backs off for a while after a miss). Held without the collection mutex, the order + // InstanceLock requires; each write re-reads its file, so a save that slipped in before is respected. + const std::string lock_path = user_presets_lock_path(); + InstanceLock instance_lock(lock_path); + if (!lock_path.empty() && !instance_lock.locked()) + return result; + result.locked = true; + for (const LegacyUserFilamentPreset &legacy : legacy_presets) + if (PresetCollection::write_legacy_user_filament_preset(legacy)) + written.push_back(legacy); + else + result.failed.push_back(legacy.preset); + } + filaments.apply_legacy_user_filament_presets(written); + return result; +} + std::optional PresetBundle::get_filament_by_filament_id(const std::string& filament_id, const std::string& printer_name) const { if (filament_id.empty()) diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index c6f79b741e..8da0e0e196 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -376,6 +376,21 @@ public: bool use_bbl_device_tab(); bool backup_user_folder() const; + // Writes the user preset folder into a zip archive at zip_path, its entries under "user/". + bool backup_user_folder_to_zip(const std::string &zip_path) const; + + // User filament presets made against an older release that no longer show on printers their parent + // listed then (resources/info/legacy_compatible_printers.json). + std::vector legacy_user_filament_presets(); + // Backs up the user preset folder into a zip in data_dir(), then writes each preset's printers into its file + // under one hold of the user presets lock. Nothing is written when the backup failed (backup empty) or the + // lock could not be taken (locked false); otherwise failed names the presets whose files could not be written. + struct LegacyUserFilamentPresetMigration { + bool locked{false}; + std::string backup; + std::vector failed; + }; + LegacyUserFilamentPresetMigration migrate_legacy_user_filament_presets(const std::vector &legacy_presets); //BBS: project embedded preset logic PresetsConfigSubstitutions load_project_embedded_presets(std::vector project_presets, ForwardCompatibilitySubstitutionRule substitution_rule); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index c62ec5a3bc..7f4ddfadb7 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -1109,6 +1109,8 @@ void GUI_App::post_init() } this->check_new_version_sf(); + if (!cw_showed) + this->check_legacy_user_filament_presets(false); const auto cloud_provider = get_printer_cloud_provider(); if (is_user_login(cloud_provider) && !app_config->get_stealth_mode()) { // this->check_privacy_version(0); @@ -6817,6 +6819,71 @@ void GUI_App::reload_settings() } } +void GUI_App::check_legacy_user_filament_presets(bool by_user) +{ + if (!by_user && app_config->get_bool("legacy_user_filament_presets_dont_remind")) + return; + const std::vector legacy_presets = preset_bundle->legacy_user_filament_presets(); + if (legacy_presets.empty()) { + if (by_user) + MessageDialog(mainframe, _L("All your filament presets are available on the printers they were made for."), + _L("Migrate Filament Presets"), wxOK | wxICON_INFORMATION).ShowModal(); + return; + } + + wxString message = _L("OrcaSlicer 2.5.0 restructured many system filament presets to fix incorrect profiles and prepare " + "for future features. As a result, the presets below no longer show on some printers they were made " + "for. Sorry for the trouble; this is a one-time fix.\n\n" + "Add those printers back to your presets now? Your presets are backed up first. You can also do this " + "later from Help > Migrate Filament Presets.") + "\n"; + for (const LegacyUserFilamentPreset &legacy : legacy_presets) { + message += "\n" + format_wxstr(_L("%1% (based on %2%)"), legacy.preset, legacy.parent); + for (const std::string &printer : legacy.printers) + message += "\n " + from_u8(printer); + } + RichMessageDialog dlg(mainframe, message, _L("Migrate Filament Presets"), wxYES | wxNO | wxICON_QUESTION); + if (!by_user) + dlg.ShowCheckBox(_L("Don't remind me again")); + const bool migrate = dlg.ShowModal() == wxID_YES; + if (dlg.IsCheckBoxChecked()) + app_config->set_bool("legacy_user_filament_presets_dont_remind", true); + if (!migrate) + return; + + const PresetBundle::LegacyUserFilamentPresetMigration migration = preset_bundle->migrate_legacy_user_filament_presets(legacy_presets); + const std::string &backup = migration.backup; + const std::vector &failed = migration.failed; + if (backup.empty()) { + show_error(mainframe, _L("Your presets could not be backed up, so they were not migrated.")); + return; + } + if (!migration.locked) { + // The lock is also refused for a while after a miss, and when the lock file itself cannot be opened. + show_error(mainframe, _L("The user presets folder could not be locked, so nothing was migrated. Another OrcaSlicer " + "instance may be saving presets; try again later from Help > Migrate Filament Presets.")); + return; + } + if (failed.size() == legacy_presets.size()) { + show_error(mainframe, format_wxstr(_L("Your filament presets could not be migrated. A backup of your presets was saved to:\n%1%"), backup)); + return; + } + preset_bundle->update_compatible(PresetSelectCompatibleType::Never); + mainframe->update_side_preset_ui(); + for (auto tab : tabs_list) { + tab->reload_config(); + tab->update_changed_ui(); + } + + message = format_wxstr(_L("Your filament presets were migrated. Select them again in the filament list where you used them.\n\n" + "A backup of your presets was saved to:\n%1%"), backup); + if (!failed.empty()) { + message += "\n\n" + _L("These presets could not be migrated:"); + for (const std::string &name : failed) + message += "\n" + from_u8(name); + } + MessageDialog(mainframe, message, _L("Migrate Filament Presets"), wxOK | (failed.empty() ? wxICON_INFORMATION : wxICON_WARNING)).ShowModal(); +} + //BBS reload when logout void GUI_App::remove_user_presets() { diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index e03c3d280a..0dcdc8e36e 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -567,6 +567,10 @@ public: void check_update(bool show_tips, int by_user); void check_new_version(bool show_tips = false, int by_user = 0); void check_new_version_sf(bool show_tips = false, int by_user = 0); + // Offers to migrate user filament presets made against an older release that lost printers their parent + // listed then. At startup it asks on every start that finds such presets, unless the user chose not to be + // reminded; by_user also reports when there is nothing to migrate. + void check_legacy_user_filament_presets(bool by_user); bool process_network_msg(std::string dev_id, std::string msg); void request_new_version(int by_user); void enter_force_upgrade(); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 2c6e147f06..a06ee98bbf 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -2828,6 +2828,10 @@ wxMenu* MainFrame::generate_help_menu() append_menu_item(helpMenu, wxID_ANY, _L("Show Configuration Folder"), _L("Show Configuration Folder"), [](wxCommandEvent&) { Slic3r::GUI::desktop_open_datadir_folder(); }); + append_menu_item(helpMenu, wxID_ANY, _L("Migrate Filament Presets"), + _L("Bring back filament presets that a profile update removed from printers they were made for"), + [](wxCommandEvent&) { wxGetApp().check_legacy_user_filament_presets(true); }); + helpMenu->AppendSeparator(); // Troubleshoot center diff --git a/src/slic3r/GUI/NativeCommands.cpp b/src/slic3r/GUI/NativeCommands.cpp index 334f2aad9b..4da288d1ea 100644 --- a/src/slic3r/GUI/NativeCommands.cpp +++ b/src/slic3r/GUI/NativeCommands.cpp @@ -589,6 +589,10 @@ std::vector build_command_catalog() Slic3r::GUI::desktop_open_datadir_folder(); return AppActionRunResult{AppActionRunResult::Level::Success}; }); + add("help_migrate_filament_presets", L("Migrate Filament Presets"), L("Help"), [](const std::string&) { + wxGetApp().check_legacy_user_filament_presets(true); + return AppActionRunResult{AppActionRunResult::Level::Success}; + }); add("help_troubleshoot", L("Troubleshoot Center"), L("Help"), [](const std::string&) { wxGetApp().troubleshoot(); return AppActionRunResult{AppActionRunResult::Level::Success}; diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index ae10cc7924..2bc87ba51b 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -38,6 +38,7 @@ #include "libslic3r/Config.hpp" #include #include +#include #include "libslic3r/libslic3r.h" #include "libslic3r/PublishSettings.hpp" #include @@ -7261,3 +7262,233 @@ TEST_CASE("A user filament whose derived compatible printer cannot be written ba CHECK(read_file(file) == before); } #endif + +namespace { + +// A filament collection that exposes the rename map update, which PresetBundle runs after loading system presets. +struct RenameTestFilaments : public PresetCollection +{ + RenameTestFilaments() + : PresetCollection(Preset::TYPE_FILAMENT, Preset::filament_options(), + static_cast(FullPrintConfig::defaults())) + {} + using PresetCollection::update_map_system_profile_renamed; +}; + +// A system filament preset that lists the given printers and is known by the given former names. +void add_system_filament(PresetCollection &filaments, const std::string &name, std::vector printers, + std::vector renamed_from = {}) +{ + add_inmemory_preset(filaments, name).is_system = true; + filaments.find_preset(name)->config.option("compatible_printers", true)->values = std::move(printers); + set_renamed_from(filaments, name, std::move(renamed_from)); +} + +// A user filament preset file as the settings tab saves it: only its diff, so unless extra sets +// compatible_printers, the printer list comes from the parent. +fs::path write_user_filament(const fs::path &dir, const std::string &name, const std::string &inherits, const std::string &extra = {}) +{ + const fs::path file = dir / PRESET_FILAMENT_NAME / (name + ".json"); + fs::create_directories(file.parent_path()); + std::ofstream(file.string()) << R"({"type":"filament","name":")" << name << R"(","from":"User","version":"1.0.0","inherits":")" + << inherits << '"' << extra << '}'; + return file; +} + +void load_user_filaments(PresetCollection &filaments, const fs::path &dir) +{ + PresetsConfigSubstitutions substitutions; + filaments.load_presets(dir.string(), PRESET_FILAMENT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::EnableSilent); +} + +const std::string k1_parent = "Generic PETG @Creality K1-all"; +const std::string k1_old_parent = "Creality Generic PETG @K1-all"; + +} // namespace + +TEST_CASE("User filaments made against an older release that lost printers their narrowed system parent listed are found", "[Preset][Bundle]") +{ + ScopedTemporaryDir temp_dir; + RenameTestFilaments filaments; + add_system_filament(filaments, k1_parent, { "Creality K1C 0.6 nozzle" }, { k1_old_parent }); + filaments.update_map_system_profile_renamed(); + write_user_filament(temp_dir.path(), "Old PETG", k1_old_parent); + write_user_filament(temp_dir.path(), "New PETG", k1_parent); + write_user_filament(temp_dir.path(), "Own list PETG", k1_old_parent, R"(,"compatible_printers":["Creality K1C 0.6 nozzle"])"); + write_user_filament(temp_dir.path(), "Narrowed PETG", k1_old_parent, R"(,"compatible_printers":["Creality K1C 0.8 nozzle"])"); + write_user_filament(temp_dir.path(), "Empty list PETG", k1_old_parent, R"(,"compatible_printers":[])"); + load_user_filaments(filaments, temp_dir.path()); + + // The map is keyed by the parent's old name, which only the file still holds: a preset made against the + // parent's current name is left alone, as is one whose file sets compatible_printers at all, whether to the + // parent's list, to other printers or to an empty list. A printer the parent still lists is not reported again. + const std::vector legacy_presets = + filaments.legacy_user_filament_presets({ { k1_old_parent, { "Creality K1C 0.4 nozzle", "Creality K1C 0.6 nozzle" } } }); + REQUIRE(legacy_presets.size() == 1); + CHECK(legacy_presets[0].preset == "Old PETG"); + CHECK(legacy_presets[0].parent == k1_parent); + CHECK(legacy_presets[0].inherits == k1_old_parent); + CHECK(legacy_presets[0].printers == std::vector{ "Creality K1C 0.4 nozzle" }); + CHECK(legacy_presets[0].file == (temp_dir.path() / PRESET_FILAMENT_NAME / "Old PETG.json").string()); + CHECK(legacy_presets[0].compatible_printers == std::vector{ "Creality K1C 0.6 nozzle", "Creality K1C 0.4 nozzle" }); +} + +TEST_CASE("Migrating a user filament backs up the user folder to a zip and writes the legacy printers to its file", "[Preset][Bundle]") +{ + ScopedTemporaryDir temp_dir; + ScopedDataDir data(temp_dir.path()); + const fs::path user_dir = temp_dir.path() / PRESET_USER_DIR / "default"; + const fs::path file = write_user_filament(user_dir, "My PETG", k1_parent); + const std::string original = read_file(file); + PresetBundle bundle; + add_system_filament(bundle.filaments, k1_parent, { "Creality K1C 0.6 nozzle" }); + load_user_filaments(bundle.filaments, user_dir); + const std::map> map{ { k1_parent, { "Creality K1C 0.4 nozzle" } } }; + const std::vector legacy_presets = bundle.filaments.legacy_user_filament_presets(map); + REQUIRE(legacy_presets.size() == 1); + + const PresetBundle::LegacyUserFilamentPresetMigration migration = bundle.migrate_legacy_user_filament_presets(legacy_presets); + const std::string &backup = migration.backup; + CHECK(migration.locked); + REQUIRE_FALSE(backup.empty()); + CHECK(std::regex_match(fs::path(backup).filename().string(), std::regex("filament_preset_migration_\\d{8}T\\d{6}Z_backup\\.zip"))); + CHECK(migration.failed.empty()); + const fs::path extracted = temp_dir.path() / "extracted"; + REQUIRE(extract_archive_confined(backup, extracted.string())); + CHECK(read_file(extracted / PRESET_USER_DIR / "default" / PRESET_FILAMENT_NAME / "My PETG.json") == original); + + // Reloaded from disk, the preset carries both printers itself and needs no further migration. + PresetBundle reloaded; + add_system_filament(reloaded.filaments, k1_parent, { "Creality K1C 0.6 nozzle" }); + load_user_filaments(reloaded.filaments, user_dir); + const std::vector expected{ "Creality K1C 0.6 nozzle", "Creality K1C 0.4 nozzle" }; + CHECK(reloaded.filaments.find_preset("My PETG")->config.option("compatible_printers")->values == expected); + CHECK(reloaded.filaments.legacy_user_filament_presets(map).empty()); + // The file now sets its own list, so the migration found earlier no longer applies to it. + CHECK(reloaded.migrate_legacy_user_filament_presets(legacy_presets).failed == std::vector{ "My PETG" }); +} + +TEST_CASE("Migrating a user filament keeps the old name of its renamed parent", "[Preset][Bundle]") +{ + ScopedTemporaryDir temp_dir; + RenameTestFilaments filaments; + add_system_filament(filaments, k1_parent, { "Creality K1C 0.6 nozzle" }, { k1_old_parent }); + filaments.update_map_system_profile_renamed(); + const fs::path file = write_user_filament(temp_dir.path(), "My PETG", k1_old_parent); + load_user_filaments(filaments, temp_dir.path()); + + const std::vector legacy_presets = + filaments.legacy_user_filament_presets({ { k1_old_parent, { "Creality K1C 0.4 nozzle" } } }); + REQUIRE(legacy_presets.size() == 1); + REQUIRE(PresetCollection::write_legacy_user_filament_preset(legacy_presets[0])); + // An older release knows the parent only by its old name. + const std::string saved = read_file(file); + CHECK(saved.find("\"inherits\": \"" + k1_old_parent + "\"") != std::string::npos); + CHECK(saved.find("Creality K1C 0.4 nozzle") != std::string::npos); +} + +TEST_CASE("Migrating a user filament leaves a file another instance changed after it was found", "[Preset][Bundle]") +{ + ScopedTemporaryDir temp_dir; + RenameTestFilaments filaments; + add_system_filament(filaments, k1_parent, { "Creality K1C 0.6 nozzle" }); + const fs::path file = write_user_filament(temp_dir.path(), "My PETG", k1_parent); + load_user_filaments(filaments, temp_dir.path()); + const std::vector legacy_presets = + filaments.legacy_user_filament_presets({ { k1_parent, { "Creality K1C 0.4 nozzle" } } }); + REQUIRE(legacy_presets.size() == 1); + CHECK(legacy_presets[0].inherits == k1_parent); + + SECTION("to set its own printers") { + write_user_filament(temp_dir.path(), "My PETG", k1_parent, R"(,"compatible_printers":["Creality K1C 0.8 nozzle"])"); + } + SECTION("to inherit another parent") { + write_user_filament(temp_dir.path(), "My PETG", "Generic PETG @System"); + } + const std::string edited = read_file(file); + CHECK_FALSE(PresetCollection::write_legacy_user_filament_preset(legacy_presets[0])); + CHECK(read_file(file) == edited); +} + +TEST_CASE("Migrating user filaments backs up and writes nothing while it can not take the user presets lock", "[Preset][Bundle]") +{ + ScopedTemporaryDir temp_dir; + ScopedDataDir data(temp_dir.path()); + const fs::path user_dir = temp_dir.path() / PRESET_USER_DIR / "default"; + const fs::path file = write_user_filament(user_dir, "My PETG", k1_parent); + const std::string original = read_file(file); + PresetBundle bundle; + add_system_filament(bundle.filaments, k1_parent, { "Creality K1C 0.6 nozzle" }); + load_user_filaments(bundle.filaments, user_dir); + const std::vector legacy_presets = + bundle.filaments.legacy_user_filament_presets({ { k1_parent, { "Creality K1C 0.4 nozzle" } } }); + REQUIRE(legacy_presets.size() == 1); + + // A folder in the lock file's place can not be opened as the lock; a fresh data dir, since loading has + // already opened the lock file of the first one. The backup is taken before the lock, then one hold covers + // every write, so a lock held elsewhere is one outcome, not a failure per preset. + const fs::path other_data = temp_dir.path() / "data"; + fs::create_directories(other_data / PRESET_USER_DIR); + ScopedDataDir data_for_migration(other_data); + fs::create_directories(fs::path(user_presets_lock_path())); + const PresetBundle::LegacyUserFilamentPresetMigration migration = bundle.migrate_legacy_user_filament_presets(legacy_presets); + CHECK_FALSE(migration.backup.empty()); + CHECK_FALSE(migration.locked); + CHECK(migration.failed.empty()); + CHECK(read_file(file) == original); +} + +TEST_CASE("Applying a migration adds the printers to the list the preset holds now and keeps a pending cloud sync", "[Preset][Bundle]") +{ + ScopedTemporaryDir temp_dir; + RenameTestFilaments filaments; + add_system_filament(filaments, k1_parent, { "Creality K1C 0.6 nozzle" }); + write_user_filament(temp_dir.path(), "My PETG", k1_parent); + load_user_filaments(filaments, temp_dir.path()); + const std::vector legacy_presets = + filaments.legacy_user_filament_presets({ { k1_parent, { "Creality K1C 0.4 nozzle" } } }); + REQUIRE(legacy_presets.size() == 1); + + // Between detection and the write, the cloud sync gave the preset a newer list that is still to be saved. + Preset *preset = filaments.find_preset("My PETG", false, true); + preset->config.option("compatible_printers", true)->values = { "Creality K1C 0.8 nozzle" }; + preset->setting_id = "GFSA01"; + preset->sync_info = "save"; + filaments.apply_legacy_user_filament_presets(legacy_presets); + CHECK(preset->config.option("compatible_printers")->values == + std::vector{ "Creality K1C 0.8 nozzle", "Creality K1C 0.4 nozzle" }); + CHECK(preset->sync_info == "save"); +} + +TEST_CASE("Migrating the selected user filament keeps a printer list edit pending in the settings tab", "[Preset][Bundle]") +{ + ScopedTemporaryDir temp_dir; + RenameTestFilaments filaments; + add_system_filament(filaments, k1_parent, { "Creality K1C 0.6 nozzle" }); + write_user_filament(temp_dir.path(), "My PETG", k1_parent); + load_user_filaments(filaments, temp_dir.path()); + REQUIRE(filaments.select_preset_by_name("My PETG", true)); + const std::vector legacy_presets = + filaments.legacy_user_filament_presets({ { k1_parent, { "Creality K1C 0.4 nozzle" } } }); + REQUIRE(legacy_presets.size() == 1); + const std::vector migrated{ "Creality K1C 0.6 nozzle", "Creality K1C 0.4 nozzle" }; + auto edited_printers = [&filaments]() -> std::vector & { + return filaments.get_edited_preset().config.option("compatible_printers", true)->values; + }; + + std::vector expected_edited = migrated; + bool expected_dirty = false; + SECTION("an unedited list follows the saved one") {} + SECTION("an edited list stays as edited") { + edited_printers() = { "Creality K1C 0.8 nozzle" }; + expected_edited = edited_printers(); + expected_dirty = true; + } + REQUIRE(PresetCollection::write_legacy_user_filament_preset(legacy_presets[0])); + filaments.apply_legacy_user_filament_presets(legacy_presets); + // find_preset() returns the edited copy of the selected preset unless asked for the real one. + CHECK(filaments.find_preset("My PETG", false, true)->config.option("compatible_printers")->values == migrated); + CHECK(edited_printers() == expected_edited); + // What marks the project as having unsaved preset changes. + CHECK(filaments.saved_is_dirty() == expected_dirty); +} diff --git a/tests/slic3rutils/test_action_source.cpp b/tests/slic3rutils/test_action_source.cpp index 520c8118fc..b58898a698 100644 --- a/tests/slic3rutils/test_action_source.cpp +++ b/tests/slic3rutils/test_action_source.cpp @@ -255,7 +255,7 @@ TEST_CASE("Native command catalog includes the Help and developer-mode commands" const Slic3r::GUI::NativeCommand* first = find("help_keyboard_shortcuts"); REQUIRE(first != nullptr); - for (const char* key : {"help_setup_wizard", "help_open_config_folder", "help_troubleshoot", "help_network_test", + for (const char* key : {"help_setup_wizard", "help_open_config_folder", "help_migrate_filament_presets", "help_troubleshoot", "help_network_test", "help_benchmark_3d_scene", "help_tip_of_the_day", "help_check_updates", "help_about", "open_wiki", "open_youtube"}) { const Slic3r::GUI::NativeCommand* c = find(key);