diff --git a/.gitattributes b/.gitattributes index 4cab1f4d26..472a0b2d1f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,12 @@ # Set the default behavior, in case people don't have core.autocrlf set. * text=auto + +# Shell scripts are run by Git Bash on Windows CI, which cannot read a script +# with CRLF line endings: it fails on the first line. Windows checkouts default +# to core.autocrlf=true, so keep these LF whatever the platform. +*.sh text eol=lf + +# Batch files are read by cmd.exe, which tracks a byte offset into the file to +# resume after `call :label`. With LF endings that offset can land wrong and the +# label lookup fails, so keep these CRLF whatever the platform. +*.bat text eol=crlf diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 1b3ba3f407..63f74a069e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -32,9 +32,17 @@ body: attributes: label: OrcaSlicer Version description: Which version of Orca Slicer are you running? You can see the full version in `Help` -> `About Orca Slicer`. - placeholder: e.g. 1.9.0 + placeholder: e.g. 2.5.0 validations: required: true + - type: input + id: working_version + attributes: + label: Regression compared to a previous version + description: Did it work in a previous version? + placeholder: e.g. 2.3.2 + validations: + required: false - type: dropdown id: os_type attributes: diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml index 1c392e4bc6..0ab9cfe41d 100644 --- a/.github/workflows/build_all.yml +++ b/.github/workflows/build_all.yml @@ -14,6 +14,7 @@ on: - 'localization/**' - 'resources/**' - ".github/workflows/build_*.yml" + - 'scripts/build_preset_cache.*' - 'scripts/flatpak/**' - 'scripts/msix/**' - 'tests/**' @@ -32,7 +33,10 @@ on: - 'build_linux.sh' - 'build_release_vs.bat' - 'build_release_vs2022.bat' + - 'build_win.bat' + - 'scripts/test_build_win.ps1' - 'build_release_macos.sh' + - 'scripts/build_preset_cache.*' - 'scripts/flatpak/**' - 'scripts/msix/**' - 'tests/**' @@ -54,6 +58,22 @@ concurrency: jobs: + # build_win.bat ships a test suite. Run it before the Windows builds. + check_build_script: + name: Windows build script tests + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + lfs: 'false' + + # Windows PowerShell rather than pwsh: the suite drives build_win.bat + # through cmd, and the two differ in how they quote native arguments. + - name: Run the build script test suite + shell: powershell + run: .\scripts\test_build_win.ps1 + build_linux: strategy: fail-fast: false @@ -79,14 +99,16 @@ jobs: # SELF_HOSTED skips arm64 (the self-hosted Windows server is x64-only). matrix: include: ${{ fromJSON(vars.SELF_HOSTED - && '[{"arch":"x64","os":"orca-win-server"}]' - || '[{"arch":"x64","os":"windows-latest"},{"arch":"arm64","os":"windows-11-arm"}]') }} + && '[{"arch":"x64","os":"orca-win-server","compiler":"clang"}]' + || '[{"arch":"x64","os":"windows-latest","compiler":"clang"},{"arch":"arm64","os":"windows-11-arm","compiler":"clang"}]') }} + needs: check_build_script # Don't run scheduled builds on forks: - if: ${{ !cancelled() && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }} + if: ${{ !cancelled() && needs.check_build_script.result == 'success' && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }} uses: ./.github/workflows/build_check_cache.yml with: os: ${{ matrix.os }} arch: ${{ matrix.arch }} + compiler: ${{ matrix.compiler }} build-deps-only: ${{ inputs.build-deps-only || false }} force-build: ${{ github.event_name == 'schedule' }} secrets: inherit @@ -257,20 +279,24 @@ jobs: echo "date=$(date +'%Y%m%d')" >> $GITHUB_ENV echo "git_commit_hash=$git_commit_hash" >> $GITHUB_ENV shell: bash + - name: Compute the flatpak-builder cache key + id: fp_cache_key + run: echo "key=flatpak-builder-${{ matrix.variant.arch }}-${{ hashFiles('deps/**', 'scripts/flatpak/com.orcaslicer.OrcaSlicer.yml', 'scripts/flatpak/make_deps_tar.sh') }}" >> "$GITHUB_OUTPUT" + shell: bash # Manage flatpak-builder cache externally so PRs restore but never upload - name: Restore flatpak-builder cache if: github.event_name == 'pull_request' uses: actions/cache/restore@v6 with: path: .flatpak-builder - key: flatpak-builder-${{ matrix.variant.arch }}-${{ github.event.pull_request.base.sha }} + key: ${{ steps.fp_cache_key.outputs.key }} restore-keys: flatpak-builder-${{ matrix.variant.arch }}- - name: Save/restore flatpak-builder cache if: github.event_name != 'pull_request' uses: actions/cache@v6 with: path: .flatpak-builder - key: flatpak-builder-${{ matrix.variant.arch }}-${{ github.sha }} + key: ${{ steps.fp_cache_key.outputs.key }} restore-keys: flatpak-builder-${{ matrix.variant.arch }}- - name: Disable debug info for faster CI builds run: | @@ -282,6 +308,12 @@ jobs: sed -i "/name: OrcaSlicer/{n;s|buildsystem: simple|buildsystem: simple\n build-options:\n env:\n git_commit_hash: \"$git_commit_hash\"|}" \ scripts/flatpak/com.orcaslicer.OrcaSlicer.yml shell: bash + - name: Check the manifest keeps orca_deps cacheable + run: ./scripts/flatpak/check_manifest_cacheable.sh + shell: bash + - name: Pack deps/ for the Flatpak manifest + run: ./scripts/flatpak/make_deps_tar.sh + shell: bash - uses: flatpak/flatpak-github-actions/flatpak-builder@master with: bundle: OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak diff --git a/.github/workflows/build_check_cache.yml b/.github/workflows/build_check_cache.yml index b5c5b28b95..91ea51ac60 100644 --- a/.github/workflows/build_check_cache.yml +++ b/.github/workflows/build_check_cache.yml @@ -9,6 +9,10 @@ on: arch: required: false type: string + compiler: + required: false + type: string + default: msvc build-deps-only: required: false type: boolean @@ -33,10 +37,10 @@ jobs: - name: set outputs id: set_outputs env: - # Keep macOS/Windows cache keys architecture-specific. amd64 Linux passes - # no arch (key stays 'linux-clang', preserving the existing cache); - # aarch64 gets its own 'linux-clang-aarch64' key. - cache-os: ${{ runner.os == 'macOS' && format('macos-{0}', inputs.arch) || (runner.os == 'Windows' && format('windows-{0}', inputs.arch) || format('linux-clang{0}', inputs.arch && format('-{0}', inputs.arch) || '')) }} + # Anything that changes how the tree is built belongs in the key, or a job + # restores one it cannot use. Linux amd64 passes no arch deliberately, so + # 'linux-clang' keeps the cache it already has. + cache-os: ${{ runner.os == 'macOS' && format('macos-{0}', inputs.arch) || (runner.os == 'Windows' && format('windows-{0}-{1}', inputs.arch, inputs.compiler) || format('linux-clang{0}', inputs.arch && format('-{0}', inputs.arch) || '')) }} # ARM64 builds use the build-arm64 tree (see build_release_vs.bat); x64/other use build. dep-folder-name: ${{ runner.os == 'macOS' && format('/{0}', inputs.arch) || (runner.os == 'Windows' && inputs.arch == 'arm64') && '-arm64/OrcaSlicer_dep' || '/OrcaSlicer_dep' }} output-cmd: ${{ runner.os == 'Windows' && '$env:GITHUB_OUTPUT' || '"$GITHUB_OUTPUT"'}} @@ -62,6 +66,7 @@ jobs: valid-cache: ${{ needs.check_cache.outputs.valid-cache == 'true' }} os: ${{ inputs.os }} arch: ${{ inputs.arch }} + compiler: ${{ inputs.compiler }} build-deps-only: ${{ inputs.build-deps-only }} force-build: ${{ inputs.force-build }} secrets: inherit diff --git a/.github/workflows/build_deps.yml b/.github/workflows/build_deps.yml index 9927f5bef6..f7082a8a06 100644 --- a/.github/workflows/build_deps.yml +++ b/.github/workflows/build_deps.yml @@ -16,6 +16,10 @@ on: arch: required: false type: string + compiler: + required: false + type: string + default: msvc build-deps-only: required: false type: boolean @@ -135,11 +139,22 @@ jobs: choco install strawberryperl } $arch = "${{ inputs.arch }}" + # -l selects clang-cl and -x Ninja; together they build the deps with clang. + $clang = "${{ inputs.compiler }}" -eq "clang" + $flags = if ($clang) { "-l", "-x" } else { @() } + if ($clang) { + # OpenSSL builds with nmake, which needs a VC environment. + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + $vs = & $vswhere -latest -property installationPath + $devArch = if ($arch -eq "arm64") { "arm64" } else { "amd64" } + Import-Module "$vs\Common7\Tools\Microsoft.VisualStudio.DevShell.dll" + Enter-VsDevShell -VsInstallPath $vs -SkipAutomaticLocation -DevCmdArguments "-arch=$devArch" + } if ($arch -eq "arm64") { - .\build_release_vs.bat deps arm64 + .\build_release_vs.bat deps arm64 @flags .\build_release_vs.bat pack arm64 } else { - .\build_release_vs.bat deps + .\build_release_vs.bat deps @flags .\build_release_vs.bat pack } shell: pwsh @@ -149,9 +164,9 @@ jobs: working-directory: ${{ github.workspace }} run: | if [ -z "${{ vars.SELF_HOSTED }}" ]; then - brew install automake texinfo libtool + brew install automake texinfo libtool pkgconf yasm nasm fi - ./build_release_macos.sh -dx ${{ !vars.SELF_HOSTED && '-1' || '' }} -a ${{ inputs.arch }} -t 10.15 + ./build_release_macos.sh -dx ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -a ${{ inputs.arch }} -t 10.15 (cd "${{ github.workspace }}/deps/build/${{ inputs.arch }}" && \ find . -mindepth 1 -maxdepth 1 ! -name 'OrcaSlicer_dep' -exec rm -rf {} +) @@ -204,4 +219,5 @@ jobs: cache-path: ${{ inputs.cache-path }} os: ${{ inputs.os }} arch: ${{ inputs.arch }} + compiler: ${{ inputs.compiler }} secrets: inherit diff --git a/.github/workflows/build_orca.yml b/.github/workflows/build_orca.yml index a7652c3bd6..8e1e28db13 100644 --- a/.github/workflows/build_orca.yml +++ b/.github/workflows/build_orca.yml @@ -13,6 +13,10 @@ on: arch: required: false type: string + compiler: + required: false + type: string + default: msvc macos-combine-only: required: false type: boolean @@ -145,7 +149,7 @@ jobs: env: ORCA_TESTS_BUILD_ONLY: ${{ inputs.arch == 'arm64' && '1' || '' }} run: | - ./build_release_macos.sh -s -n -x ${{ !vars.SELF_HOSTED && '-1' || '' }} -a ${{ inputs.arch }} -t 10.15 ${{ inputs.arch == 'arm64' && '-T' || '' }} + ./build_release_macos.sh -s -n -x ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -a ${{ inputs.arch }} -t 10.15 ${{ inputs.arch == 'arm64' && '-T' || '' }} - name: Pack unit tests mac if: runner.os == 'macOS' && !inputs.macos-combine-only && inputs.arch == 'arm64' @@ -162,6 +166,14 @@ jobs: retention-days: 5 if-no-files-found: error + - name: Build system preset cache (macOS) + if: runner.os == 'macOS' && !inputs.macos-combine-only + working-directory: ${{ github.workspace }} + shell: bash + # The bundle was already packed from resources/, so the caches have to be + # installed into it here; the source tree keeps its JSONs for later jobs. + run: ./scripts/build_preset_cache.sh -b build/${{ inputs.arch }} build/${{ inputs.arch }}/OrcaSlicer/OrcaSlicer.app/Contents/Resources/profiles + - name: Pack macOS app bundle ${{ inputs.arch }} if: runner.os == 'macOS' && !inputs.macos-combine-only working-directory: ${{ github.workspace }} @@ -196,7 +208,7 @@ jobs: if: runner.os == 'macOS' && inputs.macos-combine-only working-directory: ${{ github.workspace }} run: | - ./build_release_macos.sh -u -x ${{ !vars.SELF_HOSTED && '-1' || '' }} -a universal -t 10.15 + ./build_release_macos.sh -u -x ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -a universal -t 10.15 # Thanks to RaySajuuk, it's working now - name: Sign app and notary @@ -387,9 +399,27 @@ jobs: # "tests" builds the unit tests too; the unit_tests_windows_* jobs run them. run: | $arch = "${{ inputs.arch }}" - if ($arch -eq "arm64") { .\build_release_vs.bat slicer arm64 tests } else { .\build_release_vs.bat slicer tests } + # -l selects clang-cl and -x Ninja; together they build the slicer with clang. + $clang = "${{ inputs.compiler }}" -eq "clang" + $flags = if ($clang) { "-l", "-x" } else { @() } + if ($clang) { + # Build against the same VC toolchain and SDK as the dependencies. + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + $vs = & $vswhere -latest -property installationPath + $devArch = if ($arch -eq "arm64") { "arm64" } else { "amd64" } + Import-Module "$vs\Common7\Tools\Microsoft.VisualStudio.DevShell.dll" + Enter-VsDevShell -VsInstallPath $vs -SkipAutomaticLocation -DevCmdArguments "-arch=$devArch" + } + if ($arch -eq "arm64") { .\build_release_vs.bat slicer arm64 @flags tests } else { .\build_release_vs.bat slicer @flags tests } shell: pwsh + - name: Build system preset cache (Windows) + if: runner.os == 'Windows' + shell: cmd + # Shipped into both the already-installed tree (portable zip, MSIX) and + # the checkout cpack re-installs from when it builds the NSIS installer. + run: scripts\build_preset_cache.bat --prune-source "%BUILD_DIR%" "resources\profiles" "%BUILD_DIR%\OrcaSlicer\resources\profiles" + - name: Pack unit tests Win if: runner.os == 'Windows' working-directory: ${{ github.workspace }} @@ -539,6 +569,20 @@ jobs: retention-days: 5 if-no-files-found: error + - name: Build system preset cache (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + # Both were packed from resources/ before the caches existed, so the + # AppImage is unpacked first and the caches shipped into it and into + # the package tree; the source tree keeps its JSONs for later steps. + appimage=$(find build -maxdepth 1 -name "OrcaSlicer_Linux_AppImage*.AppImage" | head -1) + chmod +x "$appimage" + "$appimage" --appimage-extract + ./scripts/build_preset_cache.sh -b build build/package/resources/profiles squashfs-root/resources/profiles + appimagetool=$(find build -name "appimagetool.AppImage" | head -1) + ARCH=$(uname -m) "$appimagetool" --appimage-extract-and-run squashfs-root "$appimage" + rm -rf squashfs-root # Ship the freshly-built validator so slice_check_linux (build_all.yml) # can slice-sweep the shipped profiles with this PR's engine. Taken from # the aarch64 leg so the sweep also exercises the arm build; x86_64 on diff --git a/.github/workflows/check_profiles.yml b/.github/workflows/check_profiles.yml index 59c92e3ec0..db3ae6c4e8 100644 --- a/.github/workflows/check_profiles.yml +++ b/.github/workflows/check_profiles.yml @@ -1,8 +1,12 @@ name: Check profiles on: pull_request: + # release/* is included because pr-merge-bot.yml lets delegates merge into + # it, and it gates on this workflow's result. Without it a delegated merge + # into a release branch would run no profile validation at all. branches: - main + - release/* paths: - 'resources/profiles/**' - ".github/workflows/check_profiles.yml" @@ -20,6 +24,8 @@ permissions: jobs: check_profiles: + # This job name is the check-run name pr-merge-bot.yml requires before a + # delegated merge. Renaming it silently disables that gate. name: Check profiles runs-on: ubuntu-24.04 steps: diff --git a/.github/workflows/pr-label-bot.yml b/.github/workflows/pr-label-bot.yml index 1214db0776..fe74841831 100644 --- a/.github/workflows/pr-label-bot.yml +++ b/.github/workflows/pr-label-bot.yml @@ -32,13 +32,21 @@ jobs: } const allowedLabels = [ + // kind of change + 'crash', 'bug-fix', 'enhancement', - 'Localization', - 'profile', 'QoL', + 'optimization', + // area 'UI/UX', - 'dependencies' + 'profile', + 'Localization', + // infrastructure + 'build', + 'test', + 'dependencies', + 'documentation' ]; const pr = context.payload.pull_request; const labelsList = `${allowedLabels @@ -182,13 +190,21 @@ jobs: } const allowedLabels = [ + // kind of change + 'crash', 'bug-fix', 'enhancement', - 'Localization', - 'profile', 'QoL', + 'optimization', + // area 'UI/UX', - 'dependencies' + 'profile', + 'Localization', + // infrastructure + 'build', + 'test', + 'dependencies', + 'documentation' ]; const issue = context.payload.issue; diff --git a/.github/workflows/pr-merge-bot.yml b/.github/workflows/pr-merge-bot.yml new file mode 100644 index 0000000000..9b5ff2dfaf --- /dev/null +++ b/.github/workflows/pr-merge-bot.yml @@ -0,0 +1,510 @@ +name: PR Merge Bot + +# Merges a pull request on request from a delegated vendor profile maintainer. +# The merge is performed by this workflow's GITHUB_TOKEN, so a delegate needs no +# repository access. +# +# Commands, posted as a comment on the PR: +# /bot merge squash-merge the PR +# /bot merge --dry-run report the verdict without merging +# +# Merges only when the commenter holds a grant covering every changed path, the +# PR targets main or release/*, and CI is green on the head commit. Otherwise it +# comments naming the files that fell outside the grant. +# +# Grants come from the FOLDER_MERGERS variable in the `merge-delegation` +# environment: one per line, `account: path`, `#` comments and blank lines +# allowed. Paths may contain spaces. A vendor takes two grants, the folder and +# its sibling bundle JSON: +# +# # Acme profiles +# vendor-maintainer: resources/profiles/Acme/ +# vendor-maintainer: resources/profiles/Acme.json +# +# Edit the grant list (environment scope, so admin only): +# gh variable set FOLDER_MERGERS --env merge-delegation --body "$(cat folder-mergers.txt)" +# gh variable get FOLDER_MERGERS --env merge-delegation +# +# Stop all merging without touching this file: +# gh variable set MERGE_BOT_DRY_RUN --body true + +on: + issue_comment: + types: + - created + +# One merge attempt per PR at a time, so two quick comments cannot race. +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number }} + cancel-in-progress: false + +jobs: + merge: + # Skips the job unless a PR comment mentions the command. + if: >- + github.repository == 'OrcaSlicer/OrcaSlicer' + && github.event.issue.pull_request != null + && contains(github.event.comment.body, '/bot merge') + permissions: + contents: write # pulls.merge + pull-requests: write # pulls.merge + issues: write # feedback comment + reactions + actions: write # re-dispatch build_all.yml after the merge + runs-on: ubuntu-latest + timeout-minutes: 10 + # Supplies FOLDER_MERGERS. Must carry no protection rules, or every + # delegated merge would wait for a human reviewer. + environment: merge-delegation + steps: + - name: Merge PR on behalf of a folder delegate + uses: actions/github-script@v9 + env: + # Read as env vars, never interpolated into the script body. + FOLDER_MERGERS: ${{ vars.FOLDER_MERGERS }} + MERGE_BOT_DRY_RUN: ${{ vars.MERGE_BOT_DRY_RUN }} + with: + script: | + function isPermissionDenied(error) { + return error && error.status === 403 && /Resource not accessible by integration/i.test(error.message || ''); + } + + const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + + const MARKER = ''; + // No grant may reach outside this root. + const DELEGATABLE_ROOT = 'resources/profiles/'; + const ALLOWED_BASE_BRANCH = /^(?:main|release\/.+)$/; + const MERGE_METHOD = 'squash'; + const REQUIRED_CHECK = 'Check profiles'; // job name in check_profiles.yml + const MAX_CHANGED_FILES = 500; // policy cap, well under listFiles' 3000 + const LISTFILES_CAP = 3000; + const MAX_REPORTED_FILES = 12; + const MERGEABLE_ATTEMPTS = 5; + const MERGEABLE_DELAY_MS = 2000; + const OK_CONCLUSIONS = new Set(['success', 'neutral', 'skipped']); + const REGULAR_FILE_MODES = new Set(['100644', '100755']); + + // Paths refused whatever the grants say. Checked before grants, so + // delegating a new root means removing it from this list too. + const DENIED_PATTERNS = [ + /^\.github\//, + /(^|\/)\.git(attributes|modules|ignore|config)$/, + /^(?:src|deps|deps_src|tests|tools|cmake|sandboxes|scripts|docs?|localization|bbl)\//, + /(^|\/)cmakelists\.txt$/, + /\.cmake$/, + /^build_[^/]*\.(?:sh|bat)$/, + /^version\.inc$/, + // Executables, including those inside the delegatable root. + /\.(?:sh|bash|bat|cmd|ps1|py|js|mjs|cjs|ts|rb|pl|php)$/ + ]; + + function parseGrants(raw) { + // GitHub login: 1-39 chars, alphanumerics with single interior hyphens. + const loginPattern = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/; + const grantsByLogin = new Map(); + const problems = []; + + (raw || '').split(/\r?\n/).forEach((rawLine, index) => { + const line = rawLine.trim(); + if (!line || line.startsWith('#')) { + return; + } + + // Splits on the first colon only, so paths may contain ':' and spaces. + const separator = line.indexOf(':'); + if (separator === -1) { + problems.push(`line ${index + 1}: expected \`account: path\``); + return; + } + + const login = line.slice(0, separator).trim().replace(/^@/, ''); + const path = line.slice(separator + 1).trim().replace(/\/+$/, ''); + + if (!loginPattern.test(login)) { + problems.push(`line ${index + 1}: \`${login}\` is not a valid GitHub account name`); + return; + } + if (/[\\*?\u0000-\u001f\u007f]/.test(path) || path.split('/').includes('..') || path.includes('//')) { + problems.push(`line ${index + 1}: invalid path (no globs, \`..\`, \`//\`, backslashes or control characters)`); + return; + } + // Rejects anything outside the root, and the bare root itself. + if (!path.startsWith(DELEGATABLE_ROOT) || path.length <= DELEGATABLE_ROOT.length) { + problems.push(`line ${index + 1}: \`${path}\` is not inside \`${DELEGATABLE_ROOT}\``); + return; + } + + const key = login.toLowerCase(); + grantsByLogin.set(key, (grantsByLogin.get(key) || []).concat(path)); + }); + + return { grantsByLogin, problems }; + } + + function isDenied(path) { + if (/[\\\u0000-\u001f\u007f]/.test(path) || path.startsWith('/') || path.split('/').includes('..')) { + return true; + } + + const normalized = path.normalize('NFKC').toLowerCase(); + return DENIED_PATTERNS.some((pattern) => pattern.test(normalized)); + } + + // Byte-exact match on directory boundaries, so a grant of + // `.../Acme` covers neither `.../Acme Labs/x.json` nor `.../Acme.json`. + function isGranted(path, grants) { + return grants.some((grant) => path === grant || path.startsWith(`${grant}/`)); + } + + // Both endpoints of a rename; both must satisfy the grant. + function pathsFor(file) { + return [file.filename, file.previous_filename].filter(Boolean); + } + + function formatList(items) { + const unique = [...new Set(items)]; + const shown = unique.slice(0, MAX_REPORTED_FILES).map((item) => `- \`${item}\``); + if (unique.length > MAX_REPORTED_FILES) { + shown.push(`- …and ${unique.length - MAX_REPORTED_FILES} more`); + } + return shown.join('\n'); + } + + const { owner, repo } = context.repo; + const issue = context.payload.issue; + const comment = context.payload.comment; + + if (!issue.pull_request) { + core.info('Ignoring comment that is not on a pull request.'); + return; + } + // Ignores a comment whose sender is not its author. + if (context.payload.action !== 'created' || context.payload.sender.login !== comment.user.login) { + core.warning('Ignoring comment whose sender does not match its author.'); + return; + } + if (comment.user.type !== 'User') { + core.info('Ignoring bot-authored command.'); + return; + } + + const commandLine = (comment.body || '') + .split('\n') + .map((line) => line.trim()) + .find((line) => /^\/bot\s+merge\b/i.test(line)); + + if (!commandLine) { + core.info('No /bot merge command found.'); + return; + } + + const commenter = comment.user.login; + const { grantsByLogin, problems } = parseGrants(process.env.FOLDER_MERGERS); + const grants = grantsByLogin.get(commenter.toLowerCase()) || []; + + for (const problem of problems) { + core.warning(`FOLDER_MERGERS ${problem}`); + } + + // Says nothing to accounts with no grant, so it cannot be used to spam. + if (!grants.length) { + core.info(`Ignoring /bot merge from @${commenter}: not listed in FOLDER_MERGERS.`); + return; + } + + // Warns instead of failing when the token cannot post feedback. + async function bestEffort(call, warning) { + try { + await call(); + } catch (error) { + if (isPermissionDenied(error)) { + core.warning(warning); + return; + } + + throw error; + } + } + + const react = (content) => bestEffort( + () => github.rest.reactions.createForIssueComment({ owner, repo, comment_id: comment.id, content }), + `Cannot add the "${content}" reaction because the token cannot write.`); + + const say = (body) => bestEffort( + () => github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body: `${MARKER}\n${body}` }), + 'Cannot post a comment because the token cannot write comments.'); + + // Declines the command: warns in the log, reacts, explains on the PR. + async function refuse(reason) { + const configNote = problems.length + ? `\n\n\`FOLDER_MERGERS\` also has problems a maintainer needs to fix:\n${problems.map((problem) => `- ${problem}`).join('\n')}` + : ''; + const grantsNote = `\n\n
Your current grants\n\n${formatList(grants)}\n\n
`; + + core.warning(`Refused /bot merge from @${commenter}: ${reason}`); + await react('-1'); + await say(`@${commenter} I can't merge this PR: ${reason}${configNote}${grantsNote}`); + } + + await react('eyes'); + + const args = (commandLine.match(/^\/bot\s+merge\s*(.*)$/i)[1] || '').trim().split(/\s+/).filter(Boolean); + const unknownArgs = args.filter((arg) => arg.toLowerCase() !== '--dry-run'); + const dryRun = String(process.env.MERGE_BOT_DRY_RUN || '').toLowerCase() === 'true' + || unknownArgs.length !== args.length; + + if (unknownArgs.length) { + return refuse( + `I don't understand ${unknownArgs.map((arg) => `\`${arg}\``).join(', ')}. ` + + 'Usage: `/bot merge` or `/bot merge --dry-run`.' + ); + } + + // Refuses everything while the grant list is malformed. + if (problems.length) { + return refuse( + 'the `FOLDER_MERGERS` grant list has malformed lines, so I refuse every merge until it is fixed.' + ); + } + + let { data: pr } = await github.rest.pulls.get({ + owner, + repo, + pull_number: issue.number + }); + + if (pr.merged) { + return refuse('it is already merged.'); + } + if (pr.state !== 'open') { + return refuse(`its state is \`${pr.state}\`, not \`open\`.`); + } + if (pr.draft) { + return refuse('it is still a draft. Mark it ready for review first.'); + } + if (!ALLOWED_BASE_BRANCH.test(pr.base.ref)) { + return refuse(`it targets \`${pr.base.ref}\`. Delegated merges are only allowed into \`main\` and \`release/*\`.`); + } + + // ---- folder scope ---- + const files = await github.paginate(github.rest.pulls.listFiles, { + owner, + repo, + pull_number: pr.number, + per_page: 100 + }); + + if (!files.length) { + return refuse('it changes no files, so there is nothing to verify or merge.'); + } + // Refuses when the file list is truncated or disagrees with the PR. + if (files.length >= LISTFILES_CAP || files.length !== pr.changed_files) { + return refuse( + `it reports ${pr.changed_files} changed files but the API listed ${files.length}, ` + + 'so the file list is truncated and I cannot verify the folder scope. A maintainer must merge this one.' + ); + } + if (pr.changed_files > MAX_CHANGED_FILES) { + return refuse(`it changes ${pr.changed_files} files; delegated merges are capped at ${MAX_CHANGED_FILES}.`); + } + + const deniedFiles = []; + const outsideFiles = []; + + for (const file of files) { + for (const path of pathsFor(file)) { + if (isDenied(path)) { + deniedFiles.push(path); + } else if (!isGranted(path, grants)) { + outsideFiles.push(path); + } + } + } + + if (deniedFiles.length) { + core.error(`@${commenter} attempted a delegated merge touching protected paths: ${deniedFiles.join(', ')}`); + return refuse( + 'it touches paths that are never delegatable, whatever the grants say ' + + `(CI, build, scripts or executable files):\n\n${formatList(deniedFiles)}\n\nA maintainer should look at this before it goes any further.` + ); + } + if (outsideFiles.length) { + return refuse( + `${outsideFiles.length} changed path(s) fall outside your grants:\n\n${formatList(outsideFiles)}\n\n` + + 'A vendor needs both grants: `resources/profiles//` **and** `resources/profiles/.json`.' + ); + } + + // ---- file modes: rejects symlinks and submodules ---- + // Fetches the delegatable subtree only; listFiles does not report modes. + const headSha = pr.head.sha; + const { data: tree } = await github.rest.git.getTree({ + owner, + repo, + tree_sha: `${headSha}:${DELEGATABLE_ROOT.replace(/\/$/, '')}`, + recursive: 'true' + }); + + if (tree.truncated) { + return refuse('the git tree is too large to verify file modes. A maintainer must merge this one.'); + } + + // Entry paths are subtree-relative. + const modesByPath = new Map(tree.tree.map((entry) => [`${DELEGATABLE_ROOT}${entry.path}`, entry.mode])); + const irregularFiles = files + .filter((file) => file.status !== 'removed') + .map((file) => [file.filename, modesByPath.get(file.filename)]) + .filter(([, mode]) => !REGULAR_FILE_MODES.has(mode)) + .map(([path, mode]) => `${path} (mode ${mode || 'missing'})`); + + if (irregularFiles.length) { + core.error(`@${commenter} attempted a delegated merge with non-regular files: ${irregularFiles.join(', ')}`); + return refuse( + `it adds symlinks, submodules or files I cannot verify:\n\n${formatList(irregularFiles)}\n\nA maintainer should look at this before it goes any further.` + ); + } + + // ---- mergeability: waits for GitHub to compute it ---- + for (let attempt = 0; pr.mergeable === null && attempt < MERGEABLE_ATTEMPTS; attempt += 1) { + core.info(`Mergeability not computed yet; retrying in ${MERGEABLE_DELAY_MS}ms.`); + await sleep(MERGEABLE_DELAY_MS); + ({ data: pr } = await github.rest.pulls.get({ + owner, + repo, + pull_number: pr.number + })); + } + + if (pr.mergeable === null) { + return refuse('GitHub is still working out whether it can be merged. Try `/bot merge` again in a minute.'); + } + if (!pr.mergeable) { + return refuse(`it is not mergeable (\`${pr.mergeable_state}\`) - most likely a conflict with \`${pr.base.ref}\`.`); + } + + // ---- CI on the head commit ---- + const checkRuns = await github.paginate(github.rest.checks.listForRef, { + owner, + repo, + ref: headSha, + filter: 'latest', + per_page: 100 + }); + const pendingChecks = checkRuns.filter((run) => run.status !== 'completed'); + const failedChecks = checkRuns.filter((run) => run.status === 'completed' && !OK_CONCLUSIONS.has(run.conclusion)); + + if (pendingChecks.length) { + return refuse( + `${pendingChecks.length} check(s) are still running on \`${headSha.slice(0, 7)}\`:\n\n` + + `${formatList(pendingChecks.map((run) => run.name))}\n\nRe-run \`/bot merge\` once they finish.` + ); + } + if (failedChecks.length) { + return refuse( + `${failedChecks.length} check(s) are not green on \`${headSha.slice(0, 7)}\`:\n\n` + + formatList(failedChecks.map((run) => `${run.name} (${run.conclusion})`)) + ); + } + + const { data: combined } = await github.rest.repos.getCombinedStatusForRef({ + owner, + repo, + ref: headSha + }); + // total_count 0 only means there are no legacy statuses. + if (combined.total_count > 0 && combined.state !== 'success') { + return refuse( + `the combined commit status on \`${headSha.slice(0, 7)}\` is \`${combined.state}\`:\n\n` + + formatList(combined.statuses.filter((status) => status.state !== 'success') + .map((status) => `${status.context} (${status.state})`)) + ); + } + + // Requires the check to have actually run, not merely to have not failed. + const requiredCheck = checkRuns.find((run) => + run.name === REQUIRED_CHECK && + run.app && run.app.slug === 'github-actions' && + run.status === 'completed' && OK_CONCLUSIONS.has(run.conclusion)); + + if (!requiredCheck) { + return refuse( + `the \`${REQUIRED_CHECK}\` check has not succeeded on \`${headSha.slice(0, 7)}\`. ` + + 'If it never ran, a maintainer needs to approve the workflow run first.' + ); + } + + const scopeSummary = `${files.length} file(s), all within:\n${formatList(grants)}`; + + if (dryRun) { + core.info('Dry run: every gate passed, not merging.'); + await react('+1'); + await say( + `@${commenter} **dry run** - this PR passes every gate and I *would* squash-merge it ` + + `at \`${headSha.slice(0, 7)}\`.\n\nVerified scope: ${scopeSummary}` + ); + return; + } + + // ---- re-validate, then merge ---- + // An unchanged head SHA means the verified file list still holds. + const { data: fresh } = await github.rest.pulls.get({ + owner, + repo, + pull_number: pr.number + }); + + if (fresh.head.sha !== headSha || fresh.base.ref !== pr.base.ref || fresh.state !== 'open' || fresh.merged || fresh.draft) { + return refuse('it changed while I was checking it. Nothing was merged - re-run `/bot merge`.'); + } + + let merged; + try { + // Pinned to the verified head: a moved head fails with 409. + ({ data: merged } = await github.rest.pulls.merge({ + owner, + repo, + pull_number: pr.number, + sha: headSha, + merge_method: MERGE_METHOD, + commit_title: `${pr.title} (#${pr.number})`, + commit_message: + `Merged by /bot merge on behalf of @${commenter} (id ${comment.user.id}).\n` + + `Grants: ${grants.join(', ')}\nHead: ${headSha}\n` + })); + } catch (error) { + const hint = { + 403: 'the workflow token cannot write to the repository.', + 405: 'GitHub refused the merge - branch protection, a required review or check, a newly added CODEOWNERS file, or squash merging being disabled.', + 409: `the head commit moved after I verified it (was \`${headSha.slice(0, 7)}\`).`, + 422: 'GitHub rejected the merge as invalid.' + }[error.status]; + + if (!hint) { + throw error; + } + + await refuse(`${hint}\n\n> ${error.message}\n\nNothing was merged.`); + core.setFailed(`Delegated merge failed: ${error.status} ${error.message}`); + return; + } + + core.info(`Merged #${pr.number} as ${merged.sha}.`); + await react('rocket'); + await say( + `@${commenter} squash-merged into \`${pr.base.ref}\` as ${merged.sha}.\n\nVerified scope: ${scopeSummary}` + ); + + // ---- re-kick the build ---- + // A GITHUB_TOKEN merge fires no push event, so build_all.yml would + // otherwise never see these files. + try { + await github.rest.actions.createWorkflowDispatch({ + owner, + repo, + workflow_id: 'build_all.yml', + ref: pr.base.ref + }); + core.info(`Dispatched build_all.yml on ${pr.base.ref}.`); + } catch (error) { + core.warning(`Merged successfully, but dispatching build_all.yml failed: ${error.message}`); + } diff --git a/.gitignore b/.gitignore index 916c7207b7..d994d922c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ Build Build.bat /build*/ +/out/ CMakeLists.txt.user +CMakeUserPresets.json **/CMakeLists.txt.autosave deps/build* MYMETA.json @@ -49,3 +51,5 @@ internal_docs/ # Python bytecode __pycache__/ *.pyc +*.opc +/.test/ diff --git a/AGENTS.md b/AGENTS.md index fbc624b958..236aa54c05 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,6 +56,7 @@ ctest --test-dir ./tests/fff_print - Add helper functions or utilities only when existing code cannot reasonably be reused. Avoid duplication. - Keep code concise and clear. Manually simplify AI generated bloated codes before review. - Include targeted tests or documented verification for behavior changes, especially in slicing logic, profiles, formats, and GUI defaults. +- For profile changes (`resources/profiles//**`), check that `version` in the sibling `resources/profiles/.json` was bumped. - For translation changes (`localization/i18n/**/*.po`), check that recurring terms match the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/guides/localization_glossary.md) for that language. ## Localization & translations diff --git a/CMakeLists.txt b/CMakeLists.txt index fc688b35df..78dd2586da 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,6 +59,13 @@ if (APPLE) message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}") endif () +# Keep MSVC's default /W3 out of CMAKE__FLAGS so it can be applied to our own +# targets only. Silencing a bundled target would otherwise override a warning level, +# which cl reports as D9025 for every file it compiles. +if (POLICY CMP0092) + cmake_policy(SET CMP0092 NEW) +endif () + project(OrcaSlicer) # Backward compatibility for old CMake versions @@ -88,33 +95,6 @@ else () add_compile_definitions("$<$:WXINSPECTOR_DISABLE>") endif () -find_package(Git) -if(DEFINED ENV{git_commit_hash} AND NOT "$ENV{git_commit_hash}" STREQUAL "") - message(STATUS "Specified git commit hash: $ENV{git_commit_hash}") - if(GIT_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git") - # Convert the given hash to short hash - execute_process( - COMMAND ${GIT_EXECUTABLE} rev-parse --short "$ENV{git_commit_hash}" - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - OUTPUT_VARIABLE GIT_COMMIT_HASH - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - else() - # No .git directory (e.g., Flatpak sandbox) — truncate directly - string(SUBSTRING "$ENV{git_commit_hash}" 0 7 GIT_COMMIT_HASH) - endif() - add_definitions("-DGIT_COMMIT_HASH=\"${GIT_COMMIT_HASH}\"") -elseif(GIT_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git") - # Check current Git commit hash - execute_process( - COMMAND ${GIT_EXECUTABLE} log -1 --format=%h - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - OUTPUT_VARIABLE GIT_COMMIT_HASH - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - add_definitions("-DGIT_COMMIT_HASH=\"${GIT_COMMIT_HASH}\"") -endif() - if(DEFINED ENV{SLIC3R_STATIC}) set(SLIC3R_STATIC_INITIAL $ENV{SLIC3R_STATIC}) else() @@ -126,6 +106,8 @@ option(SLIC3R_GUI "Compile OrcaSlicer with GUI components (OpenGL, option(SLIC3R_FHS "Assume OrcaSlicer is to be installed in a FHS directory structure" 0) option(SLIC3R_PROFILE "Compile OrcaSlicer with an invasive Shiny profiler" 0) option(SLIC3R_PCH "Use precompiled headers" 1) +option(SLIC3R_WARNINGS "Emit compiler warnings for OrcaSlicer sources" 1) +option(SLIC3R_BUNDLED_WARNINGS "Emit compiler warnings for bundled third-party sources" 0) option(SLIC3R_MSVC_COMPILE_PARALLEL "Compile on Visual Studio in parallel" 1) option(SLIC3R_MSVC_PDB "Generate PDB files on MSVC in Release mode" 1) option(SLIC3R_ASAN "Enable ASan on Clang and GCC" 0) @@ -289,6 +271,8 @@ if (APPLE) SET(CMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.orcaslicer.OrcaSlicer") message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}") +elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(CMAKE_INSTALL_RPATH "$ORIGIN") endif () # Proposal for C++ unit tests and sandboxes @@ -335,17 +319,38 @@ if (MSVC AND CMAKE_CXX_COMPILER_ID STREQUAL Clang) # clang-cl can interpret SYSTEM header paths if -imsvc is used set(CMAKE_INCLUDE_SYSTEM_FLAG_CXX "-imsvc") - - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall \ - -Wno-old-style-cast -Wno-reserved-id-macro -Wno-c++98-compat-pedantic") else () set(IS_CLANG_CL FALSE) endif () if (MSVC) - if (SLIC3R_MSVC_COMPILE_PARALLEL AND NOT IS_CLANG_CL) + # CMP0092 only applies when the cache is created; an existing tree keeps its /W3, + # which a silenced bundled target would then override (D9025, once per file). + string(REGEX REPLACE "/W[0-4]" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") + string(REGEX REPLACE "/W[0-4]" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + + # /MP only matters for the VS generators, where CMake turns it into the + # MultiProcessorCompilation property. Ninja parallelises on its own, and + # clang-cl warns "argument unused" if the flag reaches it. + if (SLIC3R_MSVC_COMPILE_PARALLEL AND CMAKE_GENERATOR MATCHES "Visual Studio") add_compile_options(/MP) endif () + + # Parse lambdas the way the standard says, as clang and GCC already do. Without it + # MSVC keeps its legacy lambda processor under /std:c++17 and rejects reading a + # constexpr constant inside a lambda that does not capture it (C3493), which no + # other compiler requires. Implied by /std:c++20 and /permissive-, so it is only + # needed while we are on C++17. clang-cl is conforming already and does not take + # the flag. Requires VS2019 16.8 or newer. + if (NOT IS_CLANG_CL) + # cl.exe only warns (D9002) about an unknown /Zc: option, so without this the + # flag would be dropped and the first lambda reading a constexpr constant would + # fail with C3493 far from the cause. + if (MSVC_VERSION LESS 1928) + message(FATAL_ERROR "Visual Studio 2019 16.8 (MSVC 19.28) or newer is required; detected MSVC ${MSVC_VERSION}.") + endif () + add_compile_options(/Zc:lambda) + endif () # /bigobj (Increase Number of Sections in .Obj file) # error C3859: virtual memory range for PCH exceeded; please recompile with a command line option of '-Zm90' or greater # Generate symbols at every build target, even for the release. @@ -523,8 +528,15 @@ if (CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUXX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fext-numeric-literals" ) endif() -if (NOT MSVC AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang")) - if (NOT MINGW) +if ((NOT MSVC OR IS_CLANG_CL) AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang")) + if (IS_CLANG_CL) + # clang-cl reads -Wall as MSVC /Wall, which clang maps to -Weverything. /W4 is + # its -Wall -Wextra and, unlike /clang:-Wall, is ordered with the -Wno-* below + # instead of after them. The -Wextra-only warnings are dropped again so the set + # matches what -Wall gives the GNU/Clang builds. + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4" ) + add_compile_options(-Wno-unused-parameter -Wno-ignored-qualifiers -Wno-missing-field-initializers) + elseif (NOT MINGW) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall" ) endif () set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-reorder" ) @@ -1044,6 +1056,10 @@ function(orcaslicer_copy_dlls target config postfix output_dlls) ${CMAKE_PREFIX_PATH}/bin/occt/TKXDESTEP.dll ${CMAKE_PREFIX_PATH}/bin/occt/TKXSBase.dll ${CMAKE_PREFIX_PATH}/bin/freetype.dll + ${CMAKE_PREFIX_PATH}/bin/avcodec-61.dll + ${CMAKE_PREFIX_PATH}/bin/swresample-5.dll + ${CMAKE_PREFIX_PATH}/bin/swscale-8.dll + ${CMAKE_PREFIX_PATH}/bin/avutil-59.dll DESTINATION ${_out_dir}) set(${output_dlls} @@ -1079,15 +1095,110 @@ function(orcaslicer_copy_dlls target config postfix output_dlls) ${_out_dir}/TKXSBase.dll ${_out_dir}/freetype.dll - + ${_out_dir}/avcodec-61.dll + ${_out_dir}/swresample-5.dll + ${_out_dir}/swscale-8.dll + ${_out_dir}/avutil-59.dll PARENT_SCOPE ) endfunction() +function(orcaslicer_copy_sos target config postfix output_sos) + + get_property(_is_multi GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) + get_target_property(_alt_out_dir ${target} RUNTIME_OUTPUT_DIRECTORY) + + if (_alt_out_dir) + set(_out_dir "${_alt_out_dir}") + elseif (_is_multi) + set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}/${config}") + else () + set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}") + endif () + + file(COPY ${CMAKE_PREFIX_PATH}/lib/libavcodec.so + ${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61 + ${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61.3.100 + ${CMAKE_PREFIX_PATH}/lib/libavutil.so + ${CMAKE_PREFIX_PATH}/lib/libavutil.so.59 + ${CMAKE_PREFIX_PATH}/lib/libavutil.so.59.8.100 + ${CMAKE_PREFIX_PATH}/lib/libswscale.so + ${CMAKE_PREFIX_PATH}/lib/libswscale.so.8 + ${CMAKE_PREFIX_PATH}/lib/libswscale.so.8.1.100 + ${CMAKE_PREFIX_PATH}/lib/libswresample.so + ${CMAKE_PREFIX_PATH}/lib/libswresample.so.5 + ${CMAKE_PREFIX_PATH}/lib/libswresample.so.5.1.100 + DESTINATION ${_out_dir}) + + set(${output_sos} + ${_out_dir}/libavcodec.so + ${_out_dir}/libavcodec.so.61 + ${_out_dir}/libavcodec.so.61.3.100 + ${_out_dir}/libavutil.so + ${_out_dir}/libavutil.so.59 + ${_out_dir}/libavutil.so.59.8.100 + ${_out_dir}/libswscale.so + ${_out_dir}/libswscale.so.8 + ${_out_dir}/libswscale.so.8.1.100 + ${_out_dir}/libswresample.so + ${_out_dir}/libswresample.so.5 + ${_out_dir}/libswresample.so.5.1.100 + PARENT_SCOPE + ) +endfunction() + +# Bundled sources set their own warning flags, and a plain -Wall there means /Wall +# (= -Weverything) under clang-cl. Target options are applied after the ones a target +# set on itself, so these win. Targets are discovered rather than listed so a newly +# bundled library needs no maintenance here. +function(orcaslicer_silence_third_party_warnings _dir) + get_property(_subdirs DIRECTORY "${_dir}" PROPERTY SUBDIRECTORIES) + foreach (_subdir IN LISTS _subdirs) + orcaslicer_silence_third_party_warnings("${_subdir}") + endforeach () + get_property(_targets DIRECTORY "${_dir}" PROPERTY BUILDSYSTEM_TARGETS) + foreach (_target IN LISTS _targets) + get_target_property(_type ${_target} TYPE) + if (NOT _type STREQUAL "INTERFACE_LIBRARY" AND NOT _type STREQUAL "UTILITY") + if (MSVC AND NOT IS_CLANG_CL) + # Drop any level the target set for itself, or -w overrides it and cl + # reports D9025 once per file. + get_target_property(_opts ${_target} COMPILE_OPTIONS) + if (_opts) + string(REGEX REPLACE "/W[0-4]|/Wall" "" _opts "${_opts}") + string(REGEX REPLACE ";;+" ";" _opts "${_opts}") + set_target_properties(${_target} PROPERTIES COMPILE_OPTIONS "${_opts}") + endif () + # CMake maps a level into the VS generator's WarningLevel element, while a + # bare -w stays on the command line and trips D9025 there, once per file. + target_compile_options(${_target} PRIVATE /W0) + else () + target_compile_options(${_target} PRIVATE -w) + endif () + endif () + endforeach () +endfunction() + # libslic3r, OrcaSlicer GUI and the OrcaSlicer executable. add_subdirectory(deps_src) + +if (NOT SLIC3R_BUNDLED_WARNINGS) + orcaslicer_silence_third_party_warnings("${CMAKE_CURRENT_SOURCE_DIR}/deps_src") +endif () + +# Warning level for the targets added below: our sources, plus glad and libvgcode, +# which are vendored but live under src/. The deps_src libraries were configured just +# above. CMP0092 left MSVC without a default level, so it is set here. +if (NOT SLIC3R_WARNINGS) + add_compile_options(-w) +elseif (MSVC AND NOT IS_CLANG_CL) + # /we4715 is C4715, no return from a non-void function, matching the + # -Werror=return-type the GNU/Clang builds apply. + add_compile_options(/W3 /we4715) +endif () + add_subdirectory(src) set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT OrcaSlicer_app_gui) @@ -1099,6 +1210,10 @@ endif() if(BUILD_TESTS) add_subdirectory(tests) + if (NOT SLIC3R_BUNDLED_WARNINGS) + # Catch2 is vendored under tests/ and sets its own warning flags too. + orcaslicer_silence_third_party_warnings("${CMAKE_CURRENT_SOURCE_DIR}/tests/catch2") + endif () endif() if (NOT WIN32 AND NOT APPLE) @@ -1143,6 +1258,20 @@ else () endif() endif () +if (CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(LIBRARY_FILES + ${LIBDIR_BIN}/libavcodec.so.61 + ${LIBDIR_BIN}/libavcodec.so.61.3.100 + ${LIBDIR_BIN}/libavutil.so.59 + ${LIBDIR_BIN}/libavutil.so.59.8.100 + ${LIBDIR_BIN}/libswresample.so.5 + ${LIBDIR_BIN}/libswresample.so.5.1.100 + ${LIBDIR_BIN}/libswscale.so.8 + ${LIBDIR_BIN}/libswscale.so.8.1.100 + ) + install(FILES ${LIBRARY_FILES} DESTINATION "${CMAKE_INSTALL_PREFIX}/bin") +endif () + install(FILES ${CMAKE_SOURCE_DIR}/LICENSE.txt DESTINATION ".") configure_file(${LIBDIR}/dev-utils/platform/unix/fhs.hpp.in ${LIBDIR_BIN}/dev-utils/platform/unix/fhs.hpp) diff --git a/build_flatpak.sh b/build_flatpak.sh index b805bb3422..12ccac372b 100755 --- a/build_flatpak.sh +++ b/build_flatpak.sh @@ -260,6 +260,9 @@ if [[ ! -f "./scripts/flatpak/com.orcaslicer.OrcaSlicer.yml" ]]; then exit 1 fi +echo -e "${YELLOW}Packing deps/ for the manifest...${NC}" +./scripts/flatpak/make_deps_tar.sh + # Build the Flatpak echo -e "${YELLOW}Building Flatpak package...${NC}" echo -e "This may take a while (30+ minutes depending on your system)..." diff --git a/build_linux.sh b/build_linux.sh index 72ea742f1a..6d65a10e41 100755 --- a/build_linux.sh +++ b/build_linux.sh @@ -567,6 +567,8 @@ if [[ -n "${BUILD_ORCA}" ]] || [[ -n "${BUILD_TESTS}" ]] ; then print_and_run cmake --build $BUILD_DIR --config "${BUILD_CONFIG}" --target OrcaSlicer echo "Building OrcaSlicer_profile_validator .." print_and_run cmake --build $BUILD_DIR --config "${BUILD_CONFIG}" --target OrcaSlicer_profile_validator + echo "Building generate_system_cache ..." + print_and_run cmake --build $BUILD_DIR --config "${BUILD_CONFIG}" --target generate_system_cache ./scripts/run_gettext.sh fi if [[ -n "${BUILD_TESTS}" ]] ; then diff --git a/build_release_macos.sh b/build_release_macos.sh index 2fd7b26cd8..b1de5df2e4 100755 --- a/build_release_macos.sh +++ b/build_release_macos.sh @@ -4,7 +4,7 @@ set -e set -o pipefail SECONDS=0 -while getopts ":dpa:snt:xbc:i:1Tuh" opt; do +while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do case "${opt}" in d ) export BUILD_TARGET="deps" @@ -38,8 +38,8 @@ while getopts ":dpa:snt:xbc:i:1Tuh" opt; do i ) export CMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH:+$CMAKE_IGNORE_PREFIX_PATH;}$OPTARG" ;; - 1 ) - export CMAKE_BUILD_PARALLEL_LEVEL=1 + j ) + export CMAKE_BUILD_PARALLEL_LEVEL="$OPTARG" ;; T ) export BUILD_TESTS="1" @@ -58,7 +58,7 @@ while getopts ":dpa:snt:xbc:i:1Tuh" opt; do echo " -b: Build without reconfiguring CMake" echo " -c: Set CMake build configuration, default is Release" echo " -i: Add a prefix to ignore during CMake dependency discovery (repeatable), defaults to /opt/local:/usr/local:/opt/homebrew" - echo " -1: Use single job for building" + echo " -j: Set the number of parallel build jobs (CMAKE_BUILD_PARALLEL_LEVEL)" echo " -T: Build and run tests (set ORCA_TESTS_BUILD_ONLY=1 to build without running)" exit 0 ;; diff --git a/build_release_vs.bat b/build_release_vs.bat index 3288beda4c..3842f1d198 100644 --- a/build_release_vs.bat +++ b/build_release_vs.bat @@ -20,6 +20,18 @@ for %%a in (%*) do ( if "%%a"=="-x" set USE_NINJA=1 ) +@REM Check for clang-cl option (-l). Combined with -x it also builds the deps with +@REM clang-cl; on the Visual Studio generator it applies to the slicer only, because +@REM the dependency sub-builds have no toolset to inherit and stay on MSVC. +set CLANG_ARG= +set TOOLSET_ARG= +for %%a in (%*) do ( + if "%%a"=="-l" ( + set CLANG_ARG=-DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl + set TOOLSET_ARG=-T ClangCL + ) +) + @REM Check for unit-tests option ("tests") set BUILD_TESTS=OFF for %%a in (%*) do ( @@ -74,7 +86,7 @@ if "%VS_MAJOR%"=="16" ( set CMAKE_GENERATOR="Visual Studio 18 2026" ) else ( echo Error: Unsupported Visual Studio version: %VS_MAJOR% - echo Supported versions: VS2019 (16.x^), VS2022 (17.x^), VS2026 (18.x^) + echo Supported versions: VS2019 (16.8+^), VS2022 (17.x^), VS2026 (18.x^) exit /b 1 ) @@ -127,12 +139,13 @@ if "%1"=="slicer" ( GOTO :slicer ) echo "building deps.." +if defined CLANG_ARG if "%USE_NINJA%"=="0" echo Note: -l needs -x for the dependencies; building them with MSVC. echo on REM Set minimum CMake policy to avoid <3.5 errors set CMAKE_POLICY_VERSION_MINIMUM=3.5 if "%USE_NINJA%"=="1" ( - cmake ../ -G %CMAKE_GENERATOR% -DCMAKE_BUILD_TYPE=%build_type% + cmake ../ -G %CMAKE_GENERATOR% %CLANG_ARG% -DCMAKE_BUILD_TYPE=%build_type% cmake --build . --config %build_type% --target deps ) else ( cmake ../ -G %CMAKE_GENERATOR% -A %arch% -DCMAKE_BUILD_TYPE=%build_type% @@ -151,10 +164,10 @@ cd %build_dir% echo on set CMAKE_POLICY_VERSION_MINIMUM=3.5 if "%USE_NINJA%"=="1" ( - cmake .. -G %CMAKE_GENERATOR% -DORCA_TOOLS=ON %SIG_FLAG% -DBUILD_TESTS=%BUILD_TESTS% -DCMAKE_BUILD_TYPE=%build_type% - cmake --build . --config %build_type% --target ALL_BUILD + cmake .. -G %CMAKE_GENERATOR% %CLANG_ARG% -DORCA_TOOLS=ON %SIG_FLAG% -DBUILD_TESTS=%BUILD_TESTS% -DCMAKE_BUILD_TYPE=%build_type% + cmake --build . --config %build_type% --target all ) else ( - cmake .. -G %CMAKE_GENERATOR% -A %arch% -DORCA_TOOLS=ON %SIG_FLAG% -DBUILD_TESTS=%BUILD_TESTS% -DCMAKE_BUILD_TYPE=%build_type% + cmake .. -G %CMAKE_GENERATOR% -A %arch% %TOOLSET_ARG% -DORCA_TOOLS=ON %SIG_FLAG% -DBUILD_TESTS=%BUILD_TESTS% -DCMAKE_BUILD_TYPE=%build_type% cmake --build . --config %build_type% --target ALL_BUILD -- -m ) @echo off diff --git a/build_win.bat b/build_win.bat new file mode 100644 index 0000000000..c447d8a71f --- /dev/null +++ b/build_win.bat @@ -0,0 +1,1333 @@ +@echo off + +REM OrcaSlicer build script for Windows. Run with -h for the options. + +REM =========================================================================== +REM Script setup +REM =========================================================================== + +setlocal enableDelayedExpansion + +REM A manually set errorlevel shadows the real one, and cmd then stops +REM updating it after each command. Clear it, inside the scope above so +REM the caller keeps whatever it had. +set errorlevel= + +REM Everything here is relative to the repository root: deps/, build/ and +REM the 7z in tools/. Work from there whatever directory this was called +REM from. setlocal above restores the caller directory on exit. +cd /d "%~dp0" +set "WP=%CD%" +set "script_name=%~nx0" + +set argdefn=0 + +REM Both macros stop on a non-zero errorlevel; error_check also says so. +REM +REM error_check jumps to :die rather than calling exit /b where it stands. +REM Every use of it is inside a parenthesised block, and an exit /b there +REM ends the script but leaves the process exit code at zero, so a failed +REM build reported success. goto unwinds the block first. +set "repeat_error=if not ^!errorlevel^! == 0 exit /b ^!errorlevel^!" +set "error_check=if not ^!errorlevel^! == 0 (set rc=^!errorlevel^!& goto :die)" + +set VSWHERE="%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" + +REM =========================================================================== +REM Command line options +REM =========================================================================== + +call :add_section "Actions" +call :add_arg build_deps bool d deps "Download and build the dependencies, needed before -s" +call :add_arg build_slicer bool s slicer "Build OrcaSlicer" +call :add_arg build_tests bool "" tests "Build the unit tests" +call :add_arg run_tests bool "" run-tests "Build the unit tests and run them" +call :add_arg pack_deps bool p pack "Bundle the built dependencies into a zip file" +call :add_arg install_deps bool u install-deps "Install or update CMake, Perl and Git with WinGet" +call :add_arg install_vs string "" install-vs "Also install Visual Studio: buildtools or ide" +call :add_arg kill_jobs bool k kill-jobs "Kill any running build and compiler processes" +call :add_arg print_help bool h help "Print this help message" + +call :add_section "Build configuration" +call :add_arg config string "" config "release, debug, relwithdebinfo or minsizerel (default: release)" +call :add_arg target_arch string "" arch "x64 or arm64 (default: the host architecture)" +call :add_arg slicer_asan bool a asan "Build the slicer with ASAN enabled" + +call :add_section "Toolchain" +call :add_arg use_clang_cl bool l clang-cl "Use clang-cl as the compiler" +call :add_arg use_msvc bool "" msvc "Use cl as the compiler (default)" +call :add_arg use_ninja bool x ninja "Use the Ninja Multi-Config generator" +call :add_arg use_msbuild bool "" msbuild "Use the Visual Studio generator (default)" +call :add_arg vs_version string "" vs "Visual Studio release: 2019, 2022 or 2026 (default: autodetect)" +call :add_arg clang_path string "" clang-path "Path to clang-cl.exe, requires -x (default: the one from Visual Studio)" + +call :add_section "How much gets rebuilt" +call :add_arg slicer_target string "" slicer-target "Build one slicer target instead of all, e.g. libslic3r" +call :add_arg deps_target string t deps-target "Build one dependency instead of all, e.g. dep_Boost" +call :add_arg no_configure bool "" no-configure "Build the existing tree without configuring" +call :add_arg no_gettext bool "" no-gettext "Skip regenerating the translations" +call :add_arg install_slicer bool i install "Install into the build tree's OrcaSlicer folder" +call :add_arg jobs string j jobs "Limit the build to N parallel jobs" +call :add_arg clean bool c clean "Remove the trees this run builds, deps with -d, slicer with -s" + +call :add_section "Paths and extra arguments" +call :add_arg deps_dir string "" deps-dir "Dependency tree to build in or use, instead of the one named for this build" +call :add_arg slicer_dir string "" build-dir "Slicer build directory, instead of the one named for this build" +call :add_arg deps_args rawstring "" deps-args "Extra arguments for the deps configure, quoted" +call :add_arg slicer_args rawstring "" slicer-args "Extra arguments for the slicer configure, quoted" + +call :add_section "Diagnostics" +call :add_arg verbose bool v verbose "Show the compiler command lines" +call :add_arg dry_run bool D dry-run "Print the commands instead of running them" + +REM =========================================================================== +REM Lookup tables +REM =========================================================================== + +REM Known build configurations: the CMake build type, the directory to build +REM in, and the dependency tree to build against. Adding one means adding all +REM three entries in its group. The name is matched case-insensitively because +REM batch variable names are, so --config Debug finds cfg_type_debug. +REM +REM Release, RelWithDebInfo and MinSizeRel all link the /MD dependencies, so +REM they share one tree rather than each paying for its own. Debug needs /MDd +REM and cannot. deps/CMakeLists.txt makes the same split: only Debug turns on +REM DEP_DEBUG, while RelWithDebInfo just adds debug info to a release build. +set "cfg_type_release=Release" +set "cfg_dir_release=build" +set "cfg_dep_release=build" + +set "cfg_type_debug=Debug" +set "cfg_dir_debug=build-dbg" +set "cfg_dep_debug=build-dbg" + +set "cfg_type_relwithdebinfo=RelWithDebInfo" +set "cfg_dir_relwithdebinfo=build-dbginfo" +set "cfg_dep_relwithdebinfo=build" + +set "cfg_type_minsizerel=MinSizeRel" +set "cfg_dir_minsizerel=build-minsize" +set "cfg_dep_minsizerel=build" + +set "cfg_default=release" + +REM Known Visual Studio releases: the generator name, the suffix in the +REM WinGet id, and the major version vswhere and msbuild report. Adding a +REM release means adding all three entries in its group. +set "vs_gen_2019=Visual Studio 16 2019" +set "vs_winget_2019=.2019" +set "vs_year_16=2019" + +set "vs_gen_2022=Visual Studio 17 2022" +set "vs_winget_2022=.2022" +set "vs_year_17=2022" + +REM 2026 is the unversioned WinGet id: there is no Microsoft.VisualStudio.2026. +set "vs_gen_2026=Visual Studio 18 2026" +set "vs_winget_2026=" +set "vs_year_18=2026" + +set "vs_default=2026" + +REM What --install-vs asks WinGet for. +set "vs_edition_buildtools=BuildTools" +set "vs_edition_ide=Community" + +REM =========================================================================== +REM Command line handling +REM =========================================================================== + +call :handle_args %* +%error_check% + +if "%debugscript%" == "ON" ( + set /A range_end = %argdefn% - 1 + for /L %%i in (0, 1, !range_end!) do ( + call :echo_var !argdefs[%%i].VARIABLE_NAME! + ) +) + +if "%~1" == "" ( + set print_help=ON + goto :before_print_help +) + +set "all_args=%*" +if "%all_args:"=%" == "" ( + set print_help=ON +) + +:before_print_help + +if "%print_help%" == "ON" ( + call :print_help_msg + exit /b 0 +) + +REM Say so before the first + line scrolls past. +if "%dry_run%" == "ON" echo Dry run: printing commands without running them. + +if "%kill_jobs%" == "ON" ( + echo Stopping build processes. + call :kill_image MSBuild.exe + call :kill_image ninja.exe + call :kill_image cl.exe + call :kill_image clang-cl.exe + exit /b 0 +) + +REM Neither test option can happen without building the slicer, so asking for +REM one asks for that, unless another action was already named. +if "%build_deps%%build_slicer%%pack_deps%%install_deps%%install_vs%" == "" ( + if "%build_tests%" == "ON" set "build_slicer=ON" + if "%run_tests%" == "ON" set "build_slicer=ON" +) + +REM Options like --config or -j only shape a build. Without one of the actions +REM there is nothing for them to shape, so say so rather than resolving a whole +REM build and reporting it took no time. The block above may have added one. +if "%build_deps%%build_slicer%%pack_deps%%install_deps%%install_vs%" == "" ( + echo Nothing to do. Pick an action: -d, -s, -p or -u. Run -h for the full list. + exit /b 1 +) + +REM =========================================================================== +REM Visual Studio and target architecture +REM =========================================================================== + +REM Asking for Visual Studio is asking to install prerequisites. Resolve the +REM edition here rather than testing whether the name is defined: `if defined` +REM stops at the first space, so "ide " would pass and then expand to nothing. +set "vs_edition=" +if not "%install_vs%" == "" ( + set "install_deps=ON" + set "vs_edition=!vs_edition_%install_vs%!" + if "!vs_edition!" == "" ( + echo Unknown Visual Studio edition "%install_vs%". Known editions: buildtools, ide. + exit /b 1 + ) +) +REM Autodetection overwrites vs_version, so snapshot whether it was pinned. +set "vs_pinned=%vs_version%" + +if not "%vs_version%" == "" if "%use_ninja%" == "ON" ( + echo --vs and --ninja select different generators. + exit /b 1 +) +REM install(TARGETS OrcaSlicer) carries no OPTIONAL, so installing a tree +REM whose executable was never built fails. Say so before the build starts. +if "%build_slicer%" == "ON" if "%install_slicer%" == "ON" if not "%slicer_target%" == "" if /I not "%slicer_target%" == "OrcaSlicer" ( + echo --install needs the executable, but --slicer-target names "%slicer_target%". + exit /b 1 +) +if not "%vs_version%" == "" if "!vs_gen_%vs_version%!" == "" ( + echo Unknown Visual Studio release "%vs_version%". Known releases: 2019, 2022, 2026. + exit /b 1 +) + +call :autodetect_vs +%error_check% + +REM autodetect leaves this empty when it finds nothing usable. +if "%vs_version%" == "" set "vs_version=%vs_default%" + +REM Default to the host CPU. PROCESSOR_ARCHITEW6432 covers a 32-bit shell on +REM a 64-bit OS, where PROCESSOR_ARCHITECTURE reads x86. +set arch=x64 +if /I "%PROCESSOR_ARCHITECTURE%" == "ARM64" set arch=ARM64 +if /I "%PROCESSOR_ARCHITEW6432%" == "ARM64" set arch=ARM64 +if not "%target_arch%" == "" ( + if /I "%target_arch%" == "arm64" ( + set arch=ARM64 + ) else ( + if /I "%target_arch%" == "x64" ( + set arch=x64 + ) else ( + echo Unknown architecture "%target_arch%". Expected x64 or arm64. + exit /b 1 + ) + ) +) + +REM =========================================================================== +REM Installing prerequisites +REM =========================================================================== + +if "%install_deps%" == "ON" ( + where winget >nul 2>nul + if not !errorlevel! == 0 ( + echo WinGet was not found + exit /b 1 + ) + REM Keep going after one failure so the rest still get installed, then + REM name the ones that did not rather than claiming they all did. + set "install_failed=" + set "winget_args=-e --source=winget" + if not "%install_vs%" == "" ( + set "vs_year=!vs_winget_%vs_version%!" + set "ide_component_flag=" + if /I "%install_vs%" == "ide" set "ide_component_flag=Microsoft.VisualStudio.Component.VC.CoreIde" + REM Two components: the clang-cl compiler itself, and the MSBuild + REM toolset that lets the Visual Studio generator drive it. One + REM install covers both x64 and ARM64. + set "clang_cl_flag=" + if "%use_clang_cl%" == "ON" set "clang_cl_flag=Microsoft.VisualStudio.Component.VC.Llvm.Clang Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset" + REM The x64 tools build the host tooling either way; targeting ARM64 + REM needs its own toolset on top of them. + set "arm64_tools_flag=" + if /I "%arch%" == "ARM64" set "arm64_tools_flag=Microsoft.VisualStudio.Component.VC.Tools.ARM64" + call :print_and_run winget install !winget_args! --id=Microsoft.VisualStudio!vs_year!.!vs_edition! --force --custom "--add !ide_component_flag! Microsoft.VisualStudio.Component.VC.Tools.x86.x64 !arm64_tools_flag! Microsoft.VisualStudio.Component.VC.CMake.Project Microsoft.VisualStudio.Component.Windows11SDK.22621 !clang_cl_flag!" + call :note_failed "Visual Studio" !errorlevel! + ) + + REM CMake 4 dropped pre-3.5 policy support and ships incomplete ASM_ARMASM + REM linker modules, which breaks Boost.Context on ARM64. CI pins the same way. + set "cmake_version_flag=" + if /I "%arch%" == "ARM64" set "cmake_version_flag=--version 3.31.8" + call :print_and_run winget install !winget_args! --id=Kitware.CMake !cmake_version_flag! + call :note_failed CMake !errorlevel! + call :print_and_run winget install !winget_args! --id=StrawberryPerl.StrawberryPerl + call :note_failed Perl !errorlevel! + call :print_and_run winget install !winget_args! --id=Git.Git + call :note_failed Git !errorlevel! + + if defined install_failed ( + set "die_reason=Failed to install:!install_failed!" + set rc=1 + goto :die + ) + + REM Flags to repeat back in the deferred build command. + set "next_flags=" + if "%use_clang_cl%" == "ON" set "next_flags=!next_flags! -l" + if "%use_ninja%" == "ON" set "next_flags=!next_flags! -x" + if not "%target_arch%" == "" set "next_flags=!next_flags! --arch %target_arch%" + + REM Name the build that was deferred, not a fuller one. + set "next_actions=" + if "%build_deps%" == "ON" set "next_actions=!next_actions!d" + if "%build_slicer%" == "ON" set "next_actions=!next_actions!s" + if "%pack_deps%" == "ON" set "next_actions=!next_actions!p" + if "!next_actions!" == "" set "next_actions=ds" + + echo. + echo ------------------------------------------------------------- + REM A dry run installed nothing, so do not report that it did. + if "%dry_run%" == "ON" ( + echo Dry run: nothing was installed. + ) else ( + echo Installed the prerequisites. + ) + echo. + echo Next + REM The new PATH cannot reach this shell, so the build runs in the next one. + echo Restart this shell so the new PATH takes effect, then + echo build_win.bat -!next_actions!!next_flags! + echo ------------------------------------------------------------- + exit /b 0 +) + +REM =========================================================================== +REM Resolving the build +REM =========================================================================== + +REM A value ending in a backslash escapes the closing quote when it is +REM spliced into a command line: -B "D:\tree\" reaches cmake as D:\tree" +REM and swallows the argument after it. No path here needs one. +call :trim_slash deps_dir +call :trim_slash slicer_dir +call :trim_slash clang_path + +REM Naming a specific clang-cl is naming clang-cl. Doing it before the +REM conflict check below means --clang-path with --msvc is caught there. +if not "%clang_path%" == "" set "use_clang_cl=ON" +if not "%clang_path%" == "" if not exist "%clang_path%" ( + echo No clang-cl at "%clang_path%". + exit /b 1 +) +REM A trailing backslash matches directories only, and a directory would +REM otherwise reach CMake as the compiler. +if not "%clang_path%" == "" if exist "%clang_path%\" ( + echo "%clang_path%" is a directory. Name clang-cl.exe itself. + exit /b 1 +) + +REM Naming a default explicitly is fine; naming both sides is not. +if "%use_clang_cl%" == "ON" if "%use_msvc%" == "ON" ( + echo --clang-cl and --msvc select different compilers. + exit /b 1 +) +if "%use_ninja%" == "ON" if "%use_msbuild%" == "ON" ( + echo --ninja and --msbuild select different generators. + exit /b 1 +) + +set "generator=!vs_gen_%vs_version%!" +if "%use_ninja%" == "ON" ( + set "generator=Ninja Multi-Config" + call :setup_dev_env + %error_check% + set "using_ninja=ON" +) + +call :resolve_clang_cl +%repeat_error% + +if "%using_ninja%" == "ON" ( + if "%use_clang_cl%" == "ON" ( + REM Quoted, because the resolved path has spaces in it. + set "gen_args=-DCMAKE_C_COMPILER="!clang_exe!" -DCMAKE_CXX_COMPILER="!clang_exe!"" + ) +) else ( + set "gen_args=-A !arch!" + if "%use_clang_cl%" == "ON" ( + set "gen_args=!gen_args! -T ClangCL" + ) +) + +if not "%clang_path%" == "" if not "%using_ninja%" == "ON" ( + echo --clang-path needs the Ninja generator; add -x. The Visual Studio + echo generator builds clang-cl through its own ClangCL toolset. + exit /b 1 +) + +REM Ninja prints [123/456] by default, which says nothing about how far +REM along it is or how long is left. %p is a percentage, %w and %W are +REM elapsed and remaining, but those two arrived in ninja 1.12 and an +REM unknown placeholder is fatal rather than a warning, so an older ninja +REM would abort the build instead of degrading. Ask which one is on PATH. +REM Keep %p first inside the brackets: VS Code reads the first digits% +REM it finds there to drive its progress bar. +if "%using_ninja%" == "ON" if not defined NINJA_STATUS ( + set "nj_major=0" + set "nj_minor=0" + for /f "tokens=1,2 delims=." %%a in ('ninja --version 2^>nul') do ( + set "nj_major=%%a" + set "nj_minor=%%b" + ) + set "NINJA_STATUS=[%%s/%%t %%p :: %%e] " + if !nj_major! GTR 1 set "NINJA_STATUS=[%%f/%%t %%p :: %%w / %%W] " + if !nj_major! EQU 1 if !nj_minor! GEQ 12 set "NINJA_STATUS=[%%f/%%t %%p :: %%w / %%W] " +) +if "%verbose%" == "ON" if defined NINJA_STATUS echo Ninja progress format: !NINJA_STATUS! + +if "%deps_target%" == "" ( + set "deps_target=deps" +) + +REM Only the builds need CMake. -p zips an existing tree with 7z. +if "%build_deps%%build_slicer%" == "" goto :cmake_ready + +cmake --version >nul 2>nul +if not !errorlevel! == 0 ( + echo CMake was not found. Have you installed the system dependencies? + exit /b 1 +) + +REM Strawberry Perl ships a c/bin full of GNU tools, and the top-level +REM CMakeLists refuses to configure when it precedes CMake on PATH. Put a +REM real CMake first, skipping any hit from Strawberry's own cmake.exe, or +REM this pins exactly the order it is meant to undo. The findstr needle +REM must not end in a backslash, which escapes the quote and matches nothing. +set "cmake_bin=" +for /f "delims=" %%i in ('where cmake 2^>nul') do ( + if not defined cmake_bin ( + echo %%~dpi| findstr /i /c:"\Strawberry\c\bin" >nul || set "cmake_bin=%%~dpi" + ) +) +if defined cmake_bin set "PATH=%cmake_bin%;%PATH%" +:cmake_ready + +if "%config%" == "" set "config=%cfg_default%" + +REM Judge what the lookup returned rather than whether the name is defined. +REM "release " passed `if defined` and then expanded to nothing, leaving an +REM empty build directory for --clean to remove. +set "build_type=!cfg_type_%config%!" +set "tree_name=!cfg_dir_%config%!" +set "dep_name=!cfg_dep_%config%!" +set "bad_config=" +if "!build_type!" == "" set "bad_config=ON" +if "!tree_name!" == "" set "bad_config=ON" +if "!dep_name!" == "" set "bad_config=ON" +if defined bad_config ( + echo Unknown configuration "%config%". Known configurations: release, debug, relwithdebinfo, minsizerel. + exit /b 1 +) + +REM A tree is only good for the configuration, compiler and architecture it +REM was made with. Change one underneath it and CMake resets its cache and +REM carries on, leaving ExternalProject stamps from the old toolchain and +REM sub-builds that fail scattershot on things that build fine from scratch. +REM So name the tree for all three. MSVC x64 keeps the historical names. +if "%use_clang_cl%" == "ON" set "tree_name=!tree_name!-clang" +if /I "%arch%" == "ARM64" set "tree_name=!tree_name!-arm64" +if "%use_clang_cl%" == "ON" set "dep_name=!dep_name!-clang" +if /I "%arch%" == "ARM64" set "dep_name=!dep_name!-arm64" + +set "build_dir=!tree_name!" +if not "%slicer_dir%" == "" set "build_dir=%slicer_dir%" + +REM Resolve it once. --build-dir may arrive absolute, relative, or with +REM forward slashes, and anything that prints the directory has to show a +REM real path rather than one glued onto the repository root. +for %%p in ("!build_dir!") do set "build_full=%%~fp" +echo Configuration: %build_type%, %arch% +if not "%clang_exe%" == "" echo Compiler: %clang_exe% + +set "SIG_FLAG=" +if defined ORCA_UPDATER_SIG_KEY set "SIG_FLAG=-DORCA_UPDATER_SIG_KEY=%ORCA_UPDATER_SIG_KEY%" + +set "TESTS_FLAG=-DBUILD_TESTS=OFF" +if "%build_tests%" == "ON" set "TESTS_FLAG=-DBUILD_TESTS=ON" +if "%run_tests%" == "ON" set "TESTS_FLAG=-DBUILD_TESTS=ON" + +set "SLICER_TARGET_FLAG=" +if not "%slicer_target%" == "" set "SLICER_TARGET_FLAG=--target %slicer_target%" + +set "DEP_TREE=deps/!dep_name!" +set "DEP_TREE_PACK=%WP%\deps\!dep_name!" +set "DEP_TREE_FLAG=" +if not "%deps_dir%" == "" ( + set "DEP_TREE=%deps_dir%" + set "DEP_TREE_PACK=%deps_dir%" + set "DEP_TREE_FLAG=-DDEP_BUILD_DIR="%deps_dir%"" +) + +REM CMakeLists derives DEP_BUILD_DIR from the build directory NAME, which is +REM wrong whenever the build tree and the dependency tree are named +REM differently, as they are for every configuration that shares the release +REM dependencies. Name it outright once the two stop matching. +if "!DEP_TREE_FLAG!" == "" if not "%build_dir%" == "!dep_name!" ( + set "DEP_TREE_FLAG=-DDEP_BUILD_DIR="%WP%\deps\!dep_name!"" +) + +set "VERBOSE_FLAG=" +if "%verbose%" == "ON" set "VERBOSE_FLAG=--verbose" + +REM The two generators count differently. +REM +REM Ninja counts compilers, so -j is exact. The deps superbuild reads +REM CMAKE_BUILD_PARALLEL_LEVEL for the sub-builds it drives. +REM +REM MSBuild turns -j into /m, which counts projects, not compilers: /MP still +REM runs one cl per core inside each one, so -j 1 would not give one +REM compiler. CL_MPCount sets the /MP degree instead, and MSBuild reads +REM properties from the environment, so nested deps builds inherit it. /m +REM stays at its default of one project at a time. +set "JOBS_FLAG=" +if "%jobs%" == "" goto :jobs_ready +echo %jobs%| findstr /r /c:"^[1-9][0-9]*$" >nul +if errorlevel 1 ( + echo Invalid --jobs value "%jobs%". Expected a positive integer. + exit /b 1 +) +if "%using_ninja%" == "ON" ( + set "JOBS_FLAG=-j %jobs%" + set "CMAKE_BUILD_PARALLEL_LEVEL=%jobs%" +) else ( + set "CL_MPCount=%jobs%" +) +echo Parallel jobs: %jobs% +:jobs_ready + +REM The flags that reproduce this run, for the commands the summary suggests. +REM Kept in three parts because they are not all relevant everywhere: a deps +REM retry has no use for --build-dir, and the bundle line names its own tree. +set "recall_tc=" +if "%use_clang_cl%" == "ON" set "recall_tc=!recall_tc! -l" +if "%using_ninja%" == "ON" set "recall_tc=!recall_tc! -x" +if /I not "%config%" == "%cfg_default%" set "recall_tc=!recall_tc! --config %config%" +if not "%target_arch%" == "" set "recall_tc=!recall_tc! --arch %target_arch%" +if not "%vs_pinned%" == "" set "recall_tc=!recall_tc! --vs %vs_pinned%" +if not "%clang_path%" == "" set "recall_tc=!recall_tc! --clang-path "%clang_path%"" + +set "recall_i=" +if "%install_slicer%" == "ON" set "recall_i= -i" +set "recall_dd=" +if not "%deps_dir%" == "" set "recall_dd= --deps-dir "%deps_dir%"" +set "recall_bd=" +if not "%slicer_dir%" == "" set "recall_bd= --build-dir "%slicer_dir%"" +set "recall=!recall_tc!!recall_i!!recall_dd!!recall_bd!" + +REM =========================================================================== +REM Running the build +REM =========================================================================== + +REM CMake 4 refuses a pre-3.5 policy version; several deps still ask for one. +set CMAKE_POLICY_VERSION_MINIMUM=3.5 + +set _START_TIME=%TIME% + +if "%build_deps%" == "ON" ( + REM Which stage is running, so a failure can name it and suggest a + REM retry scoped to it rather than to the whole run. + set "stage=d" + echo Building the dependencies... + + if "%clean%" == "ON" ( + call :clean_tree "!DEP_TREE!" + %error_check% + ) + + if not "%no_configure%" == "ON" ( + call :print_and_run cmake -S deps -B "!DEP_TREE!" -G "%generator%" %gen_args% -DCMAKE_BUILD_TYPE=%build_type% !deps_args! %ORCA_DEPS_CMAKE_ARGS% + %error_check% + ) + + call :print_and_run cmake --build "!DEP_TREE!" --config %build_type% --target %deps_target% %JOBS_FLAG% %VERBOSE_FLAG% + %error_check% +) + +if "%pack_deps%" == "ON" ( + set "stage=p" + setlocal ENABLEDELAYEDEXPANSION + call :print_and_run cd /d "!DEP_TREE_PACK!" + %error_check% + REM date /t prints in the machine locale and its field order varies by + REM region, which is how the inherited parse produced YYYYDDMM. Ask for + REM an unambiguous stamp instead. powershell.exe lives under + REM System32\WindowsPowerShell rather than System32, so a trimmed PATH + REM cannot find it and the stamp comes back empty. + set "ps=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" + set "build_date=" + for /f %%d in ('!ps! -NoProfile -Command "Get-Date -Format yyyyMMdd"') do set "build_date=%%d" + if "!build_date!" == "" ( + set "die_reason=Could not read the date from !ps!." + set rc=1 + goto :die + ) + + REM A bundle is only good for what built it, so it carries the same parts + REM as the dependency tree. Release x64 on cl keeps the plain name. + set "dep_flavour=!cfg_dep_%config%!" + set "dep_flavour=!dep_flavour:build=!" + set "dep_variant=%arch%" + if "%use_clang_cl%" == "ON" set "dep_variant=!dep_variant!-clang" + set "dep_variant=!dep_variant!!dep_flavour!" + echo Packing the dependencies: OrcaSlicer_dep_win-!dep_variant!_!build_date!.zip + + REM tools\7z.exe loads its codecs from a 7z.dll, and the repo does not + REM carry one, so it can only archive on a machine that has 7-Zip + REM installed. Windows has shipped bsdtar in System32 since 10 1803 and + REM it writes an ordinary deflate zip, so fall back to that rather than + REM failing on a machine that has everything it needs. + set "zipper=" + if exist "%WP%\tools\7z.dll" set "zipper=%WP%/tools/7z.exe a" + if not defined zipper if exist "%SystemRoot%\System32\tar.exe" set "zipper=%SystemRoot%\System32\tar.exe -a -c -f" + if not defined zipper ( + set "die_reason=No archiver. tools\7z.exe needs a 7z.dll, and this Windows has no System32\tar.exe." + set "die_hint=Install 7-Zip, or copy its 7z.dll into tools\." + set rc=1 + goto :die + ) + + call :print_and_run !zipper! OrcaSlicer_dep_win-!dep_variant!_!build_date!.zip OrcaSlicer_dep + %error_check% + REM endlocal is about to discard the name, so carry the path out with it. + for %%z in ("!DEP_TREE_PACK!\OrcaSlicer_dep_win-!dep_variant!_!build_date!.zip") do endlocal & set "bundle=%%~fz" +) + +if "%build_slicer%" == "ON" ( + set "stage=s" + echo Building OrcaSlicer... + + if "%clean%" == "ON" ( + call :clean_tree "%build_dir%" + %error_check% + ) + + if "%slicer_asan%" == "ON" ( + set "slicer_args=!slicer_args! -DSLIC3R_ASAN=ON" + ) + + REM Configuring against a tree that was never built fails deep inside + REM package resolution. Name it here instead. Skipped when -d is about to + REM build it in this same run, and under a dry run, which configures + REM nothing and must not depend on what happens to be on the machine. + if not "%dry_run%" == "ON" if not "%no_configure%" == "ON" if not "%build_deps%" == "ON" if not exist "!DEP_TREE!\OrcaSlicer_dep\usr\local\" ( + for %%p in ("!DEP_TREE!") do set "die_reason=Dependencies not found at %%~fp" + set "die_hint=Build them with build_win.bat -d!recall!." + REM Only worth suggesting to someone who did not name a tree. + if "%deps_dir%" == "" set "die_hint=Build them with build_win.bat -d!recall!, or point --deps-dir at an existing tree." + set rc=1 + goto :die + ) + + if not "%no_configure%" == "ON" ( + call :print_and_run cmake -B "%build_dir%" -G "%generator%" %gen_args% -DORCA_TOOLS=ON %SIG_FLAG% %TESTS_FLAG% %DEP_TREE_FLAG% -DCMAKE_BUILD_TYPE=%build_type% !slicer_args! %ORCA_SLICER_CMAKE_ARGS% + %error_check% + ) + + call :print_and_run cmake --build "%build_dir%" --config %build_type% %SLICER_TARGET_FLAG% %JOBS_FLAG% %VERBOSE_FLAG% + %error_check% + + if "%run_tests%" == "ON" ( + call :print_and_run ctest --test-dir "%build_dir%/tests" -C %build_type% --output-on-failure + %error_check% + ) + + if not "%no_gettext%" == "ON" ( + call :print_and_run call scripts/run_gettext.bat + %error_check% + ) + + if "%install_slicer%" == "ON" ( + call :print_and_run cmake --build "%build_dir%" --target install --config %build_type% + %error_check% + ) +) + +REM Elapsed wall clock. The 1%%a-100 trick strips a leading zero, which set /A +REM would otherwise read as octal, and a negative total means the build ran +REM past midnight. +for /f "tokens=1-3 delims=:.," %%a in ("%_START_TIME: =0%") do set /a "_start_s=(1%%a-100)*3600+(1%%b-100)*60+(1%%c-100)" +for /f "tokens=1-3 delims=:.," %%a in ("%TIME: =0%") do set /a "_end_s=(1%%a-100)*3600+(1%%b-100)*60+(1%%c-100)" +set /a "_elapsed=_end_s - _start_s" +if %_elapsed% lss 0 set /a "_elapsed+=86400" +set /a "_hours=_elapsed / 3600" +set /a "_remainder=_elapsed - _hours * 3600" +set /a "_mins=_remainder / 60" +set /a "_secs=_remainder - _mins * 60" +call :summary +exit /b 0 + +REM Reached only by error_check, from the top level, where exit /b works. +REM The hash block is what CMakeLists already uses for a build that cannot +REM continue, so it means the same thing here. +:die +echo. +echo ############################################################# +REM The paths that set die_reason had no command fail, so last_cmd there +REM names one that succeeded. +if defined die_reason echo !die_reason! +if not defined die_reason if defined last_cmd echo Failed: !last_cmd! +if defined die_hint echo !die_hint! +echo Exit code %rc%. +REM -v only makes the build verbose, so it has nothing to offer a configure +REM that failed before any compiler ran. +set "failed_configure=" +if "!last_cmd:~0,9!" == "cmake -B " set "failed_configure=ON" +if "!last_cmd:~0,9!" == "cmake -S " set "failed_configure=ON" +REM Scoped to the stage that failed. Offering -c for the whole run would +REM discard a dependency tree that was not at fault, and neither flag does +REM anything for a failed pack. A named reason already carries its own advice. +if "!stage!" == "d" set "recall=!recall_tc!!recall_dd!" +if not defined die_reason if defined stage if not "!stage!" == "p" ( + echo. + echo Try + if not defined failed_configure echo build_win.bat -!stage!!recall! -v show the failing compiler command line + echo build_win.bat -!stage!!recall! -c discard that tree and configure from scratch +) +echo ############################################################# +exit /b %rc% + +REM =========================================================================== +REM Function definitions +REM =========================================================================== + +REM summary - what was produced, and what to do with it next. A dry run says +REM so rather than claiming the files exist, but every line below that is +REM worked out the same way in either run. +:summary + for %%p in ("!DEP_TREE!") do set "dep_full=%%~fp" + REM The binary only leaves the build tree when it is installed. + set "slicer_exe=%build_dir%\src\%build_type%\orca-slicer.exe" + if "%install_slicer%" == "ON" set "slicer_exe=%build_dir%\OrcaSlicer\orca-slicer.exe" + for %%p in ("!slicer_exe!") do set "slicer_full=%%~fp" + REM The 2026 generator writes OrcaSlicer.slnx, the releases before it + REM OrcaSlicer.sln. A file already there wins, in case an older CMake + REM configured the build. + set "solution=OrcaSlicer.sln" + if "%vs_version%" == "2026" set "solution=OrcaSlicer.slnx" + if exist "!build_full!\OrcaSlicer.sln" set "solution=OrcaSlicer.sln" + if exist "!build_full!\OrcaSlicer.slnx" set "solution=OrcaSlicer.slnx" + + REM Naming a target builds it and its dependencies, not its dependents, + REM so only a full build or the executable's own target relinks. + set "linked=ON" + if not "%slicer_target%" == "" set "linked=" + if /I "%slicer_target%" == "OrcaSlicer" set "linked=ON" + + echo. + echo ------------------------------------------------------------- + if "%dry_run%" == "ON" ( + echo Dry run: nothing was built. A real run would report: + ) else ( + echo Build completed in %_hours%h %_mins%m %_secs%s + ) + + if "%build_deps%" == "ON" echo Dependencies !dep_full! + if "%build_slicer%" == "ON" if "%linked%" == "ON" echo OrcaSlicer !slicer_full! + if "%build_slicer%" == "ON" if not "%linked%" == "ON" echo Target %slicer_target% + if "%build_slicer%" == "ON" if not "%using_ninja%" == "ON" echo Solution %build_full%\!solution! + if "%pack_deps%" == "ON" if defined bundle echo Bundle !bundle! + + echo. + echo Next + if "%build_slicer%" == "ON" ( + if "%linked%" == "ON" echo Run it !slicer_exe! + if not "%using_ninja%" == "ON" echo Open in Visual Studio %build_dir%\!solution! + if "%linked%" == "ON" echo Rebuild after edits build_win.bat -s!recall! --no-configure + if not "%linked%" == "ON" echo Relink the binary build_win.bat -s!recall! --no-configure + if "%linked%" == "ON" if "%using_ninja%" == "ON" echo Rebuild one target build_win.bat -s!recall! --no-configure --slicer-target libslic3r + if "%run_tests%" == "ON" echo Re-run the tests ctest --test-dir %build_dir%/tests -C %build_type% --output-on-failure + ) else ( + if "%build_deps%" == "ON" echo Build the slicer build_win.bat -s!recall! + ) + if "%pack_deps%" == "ON" echo Share the bundle unzip it elsewhere, then build_win.bat -s!recall_tc! --deps-dir ^ + echo ------------------------------------------------------------- + exit /b 0 + +:debug_msg + if "%debugscript%" == "ON" echo %* + exit /b 0 + +REM get_str_len -> length in %ret% +:get_str_len + setlocal + set "in=%~1" + for /L %%i in (0, 1, 100) do ( + if "!in:~%%i,1!" == "" ( + set out=%%i + goto :break_str_len + ) + ) + + echo error in get_str_len: string is too long + endlocal + exit /b 1 + + :break_str_len + endlocal & set ret=%out% + exit /b 0 + +:print_help_msg + setlocal + + REM Measure the widest flag column. Section headers have no flags and + REM must not widen it. + set flags= + set max_len=0 + set /A range_end = %argdefn% - 1 + for /L %%i in (0, 1, %range_end%) do ( + if "!argdefs[%%i].TYPE!" == "section" ( + set "flags[%%i]=" + ) else ( + set str_placeholder= + if not "!argdefs[%%i].TYPE!" == "bool" ( + set "str_placeholder= " + ) + + REM The placeholder goes on the long form only. Repeating it on + REM the short one says nothing extra and costs eight columns. + set "flag_str=" + if not "!argdefs[%%i].SHORT_FLAG!" == "" ( + set "flag_str=-!argdefs[%%i].SHORT_FLAG!" + ) + + if not "!argdefs[%%i].LONG_FLAG!" == "" ( + if "!flag_str!" == "" ( + REM No short flag: pad so the long one lines up with the others. + set "flag_str= " + ) else ( + set "flag_str=!flag_str!, " + ) + set "flag_str=!flag_str!--!argdefs[%%i].LONG_FLAG!!str_placeholder!" + ) else ( + set "flag_str=!flag_str!!str_placeholder!" + ) + set "flag_str=!flag_str! " + set "flags[%%i]=!flag_str!" + call :get_str_len "!flag_str!" + if !ret! GTR !max_len! ( + set max_len=!ret! + ) + ) + ) + + set padding= + for /L %%i in (0, 1, %max_len%) do set "padding=!padding! " + + echo Builds OrcaSlicer and its dependencies on Windows. + echo. + echo Usage: %script_name% [options] + set /A range_end = %argdefn% - 1 + for /L %%i in (0, 1, !range_end!) do ( + if "!argdefs[%%i].TYPE!" == "section" ( + echo. + echo !argdefs[%%i].HELP_TEXT!: + ) else ( + set "flag=!flags[%%i]!%padding%" + echo !flag:~0,%max_len%!!argdefs[%%i].HELP_TEXT! + ) + ) + echo. + echo Examples: + echo %script_name% --install-vs ide Set up a new machine, then restart the shell + echo %script_name% -ds Build the dependencies, then the slicer + echo %script_name% -s -l -x Rebuild the slicer with clang-cl and Ninja + echo %script_name% -s --no-configure -j 8 Rebuild quickly while iterating + echo %script_name% -s --slicer-target glad Compile one target to check the toolchain + echo %script_name% -l -x --run-tests Test that toolchain's build, not the default one + echo. + echo Environment: + echo ORCA_DEPS_CMAKE_ARGS Extra arguments for the deps configure + echo ORCA_SLICER_CMAKE_ARGS Extra arguments for the slicer configure + echo ORCA_UPDATER_SIG_KEY Update signing key baked into the slicer + echo git_commit_hash Revision to stamp into the build, as CI does + echo NINJA_STATUS Ninja progress format, if you want your own + echo debugscript Set to ON to trace this script + echo. + echo set ORCA_SLICER_CMAKE_ARGS=-DSLIC3R_PCH=OFF -DSLIC3R_MSVC_PDB=OFF + echo $env:ORCA_SLICER_CMAKE_ARGS = '-DSLIC3R_PCH=OFF' (PowerShell) + echo. + echo --deps-args and --slicer-args cannot carry a value with spaces; use + echo these instead. Neither form supports a value containing an ampersand. + endlocal + exit /b 0 + +REM add_section +:add_section + set argdefs[%argdefn%].VARIABLE_NAME= + set argdefs[%argdefn%].TYPE=section + set argdefs[%argdefn%].SHORT_FLAG= + set argdefs[%argdefn%].LONG_FLAG= + set argdefs[%argdefn%].HELP_TEXT=%~1 + set /A argdefn+=1 + exit /b 0 + +REM add_arg <variable> <type:bool,string,rawstring> <short> <long> <help> +:add_arg + set argdefs[%argdefn%].VARIABLE_NAME=%~1 + set argdefs[%argdefn%].TYPE=%~2 + set argdefs[%argdefn%].SHORT_FLAG=%~3 + set argdefs[%argdefn%].LONG_FLAG=%~4 + set argdefs[%argdefn%].HELP_TEXT=%~5 + + REM Start every option unset, so "defined" means the user gave it. + set %~1= + + if "%debugscript%" == "ON" ( + echo add_arg VARIABLE_NAME: !argdefs[%argdefn%].VARIABLE_NAME! + echo add_arg TYPE: !argdefs[%argdefn%].TYPE! + echo add_arg SHORT_FLAG: !argdefs[%argdefn%].SHORT_FLAG! + echo add_arg LONG_FLAG: !argdefs[%argdefn%].LONG_FLAG! + echo add_arg HELP_TEXT: !argdefs[%argdefn%].HELP_TEXT! + ) + + set /A argdefn+=1 + + exit /b 0 + +REM find_arg <type:short,long> <flag> -> index in %ret% +:find_arg + setlocal + call :debug_msg starting function: find_arg "%~1" "%~2" + + set type= + if /I "%~1" == "short" set type=SHORT + if /I "%~1" == "long" set type=LONG + if not defined type ( + endlocal + set ret= + exit /b 1 + ) + + call :debug_msg find_arg type=%type% + set /A range_end = %argdefn% - 1 + for /L %%i in (0, 1, %range_end%) do ( + if "!argdefs[%%i].%type%_FLAG!" == "%~2" ( + set idx=%%i + goto :find_arg_cont + ) + ) + + echo Error in find_arg: Failed to find arg "%~2" + call :print_help_msg + + endlocal + set ret= + exit /b 1 + + :find_arg_cont + call :debug_msg find_arg: found at %idx% + endlocal & ( + set ret=%idx% + ) + exit /b 0 + +REM set_arg <index> [<value>] +:set_arg + call :debug_msg starting function: set_arg "%~1" "%~2" + + if "%~1" == "" ( + echo Error in set_arg: no index provided + exit /b 1 + ) + + setlocal + if /I "!argdefs[%~1].TYPE!" == "bool" ( + call :debug_msg set_arg: setting bool type to ON + set val=ON + ) else ( + set "val=%~2" + set quote_char=" + REM Delayed expansion throughout: %val% here would be the value from the + REM previous call, since the whole block is parsed before it runs. + if "!val:~0,1!" == "!quote_char!" ( + if "!val:~-1,1!" == "!quote_char!" ( + set "val=!val:~1,-1!" + ) + ) + + call :debug_msg set_arg: setting string type to %~2 + ) + set var_name=!argdefs[%~1].VARIABLE_NAME! + + endlocal & ( + REM Set variable in parent scope + set "%var_name%=%val%" + + REM Add variable to finalize command + set "finalize_cmd=%finalize_cmd% & set "%var_name%=%val%"" + ) + + exit /b 0 + +REM get_arg_type <index> -> type in %ret% +:get_arg_type + call :debug_msg starting function get_arg_type "%~1" + setlocal + set type=!argdefs[%~1].TYPE! + endlocal & set ret=%type% + exit /b 0 + +REM echo_var <variable> +:echo_var + echo %~1=!%~1! + exit /b 0 + +:autodetect_vs + REM Nothing to detect once the release is pinned, or under Ninja. + if not "%vs_version%" == "" exit /b 0 + if "%use_ninja%" == "ON" exit /b 0 + + setlocal + + %VSWHERE% -nologo >nul 2>nul + if not !errorlevel! == 0 ( + REM vswhere is not in its usual place; try msbuild instead. + goto :msbuild_check + ) + + echo Detecting Visual Studio version using vswhere... + for /f "tokens=1 delims=." %%i in ('%VSWHERE% -nologo -products * -latest -property catalog_productDisplayVersion') do ( + set "VS_MAJOR=%%i" + goto :version_found + ) + + :msbuild_check + where msbuild >nul 2>nul + if not !errorlevel! == 0 ( + REM No msbuild either; leave the release empty and let the default apply. + endlocal + exit /b 0 + ) + + echo Detecting Visual Studio version using msbuild... + + REM The version line varies by release, so try two patterns for it. + set VS_MAJOR= + for /f "tokens=*" %%i in ('msbuild -version 2^>^&1 ^| findstr /r "^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"') do ( + for /f "tokens=1 delims=." %%a in ("%%i") do set VS_MAJOR=%%a + set MSBUILD_OUTPUT=%%i + goto :version_found + ) + + REM The same pattern unanchored, for releases that print a banner first. + if "%VS_MAJOR%"=="" ( + for /f "tokens=*" %%i in ('msbuild -version 2^>^&1 ^| findstr /r "[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"') do ( + for /f "tokens=1 delims=." %%a in ("%%i") do set VS_MAJOR=%%a + set MSBUILD_OUTPUT=%%i + goto :version_found + ) + ) + + :version_found + set "detected=!vs_year_%VS_MAJOR%!" + if not defined detected ( + echo Error: Unsupported Visual Studio major version: %VS_MAJOR% + endlocal + exit /b 1 + ) + echo Detected Visual Studio %VS_MAJOR% ^(!detected!^) + endlocal & set "vs_version=%detected%" + + exit /b 0 + +:setup_dev_env + REM A dry run runs nothing, and VsDevCmd costs seconds. + if "%dry_run%" == "ON" exit /b 0 + set "dev_arch=x64" + if /I "!arch!" == "ARM64" set "dev_arch=arm64" + %VSWHERE% -nologo >nul 2>nul + if not !errorlevel! == 0 ( + REM vswhere is not in its usual place; use the environment as it stands. + exit /b 0 + ) + + for /f "tokens=*" %%i in ('%VSWHERE% -nologo -products * -latest -property resolvedInstallationPath') do ( + set "VS_PATH=%%i" + goto :vs_path_found + ) + exit /b 0 + + :vs_path_found + call "%VS_PATH%\Common7\Tools\VsDevCmd.bat" -arch=!dev_arch! >nul 2>nul + set "VS_PATH=" + + exit /b 0 + +REM resolve_clang_cl - set clang_exe to the clang-cl a Ninja build uses. +REM VsDevCmd appends the Visual Studio LLVM directory to the end of PATH, +REM so a standalone LLVM already there shadows it. Name a full path. +:resolve_clang_cl + set "clang_exe=" + if not "%using_ninja%" == "ON" exit /b 0 + if not "%use_clang_cl%" == "ON" exit /b 0 + REM Only a configure uses it, so -p and --no-configure need none. + if "%no_configure%" == "ON" exit /b 0 + if "%build_deps%%build_slicer%" == "" exit /b 0 + + REM --clang-path wins. Forward slashes either way, so CMake does not + REM read a backslash as an escape. + if not "%clang_path%" == "" ( + set "clang_exe=%clang_path:\=/%" + exit /b 0 + ) + + setlocal + + REM Keyed on the host, not %arch%, because the x64 compiler + REM cross-compiles to ARM64. + set "llvm_host=x64" + if /I "%PROCESSOR_ARCHITECTURE%" == "ARM64" set "llvm_host=ARM64" + + set "found=" + %VSWHERE% -nologo >nul 2>nul + if !errorlevel! == 0 ( + for /f "tokens=*" %%i in ('%VSWHERE% -nologo -products * -latest -property resolvedInstallationPath') do ( + if exist "%%i\VC\Tools\Llvm\!llvm_host!\bin\clang-cl.exe" ( + set "found=%%i\VC\Tools\Llvm\!llvm_host!\bin\clang-cl.exe" + ) + ) + ) + + REM No clang toolset in Visual Studio. where lists every match, and the + REM first is the one PATH would resolve. + if "!found!" == "" ( + for /f "tokens=*" %%i in ('where clang-cl.exe 2^>nul') do ( + if "!found!" == "" set "found=%%i" + ) + if not "!found!" == "" echo Visual Studio has no clang-cl; using !found! from PATH. + ) + + if "!found!" == "" ( + echo No clang-cl found. Add the C++ Clang Compiler component with + echo %script_name% --install-vs ide -l + echo or name a standalone one with --clang-path. + endlocal + exit /b 1 + ) + + endlocal & set "clang_exe=%found:\=/%" + + exit /b 0 + +REM clean_tree <path> - remove a build tree, refusing anything that is not +REM one. Nothing here should ever fire; it is a floor under a bug that +REM produced a path far shorter than it looks. +:clean_tree + setlocal + set "target=%~1" + if not defined target ( + echo Refusing to clean: no directory to remove. + exit /b 1 + ) + + REM Resolve first, because "." and "deps/.." are shorter than they read. + for %%p in ("!target!") do set "full=%%~fp" + if "!full:~1!" == ":\" ( + echo Refusing to clean "!full!": that is a drive root. + exit /b 1 + ) + if /I "!full!" == "%WP%" ( + echo Refusing to clean "!full!": that is the repository itself. + exit /b 1 + ) + + call :print_and_run rmdir /S /Q "!full!" + exit /b !errorlevel! + +REM note_failed <name> <code> - record a failed install. Two winget codes +REM mean the tool is already there, which is what -u is for: +REM -1978335135 (0x8A150061) winget declined: a version is already there +REM -1978335189 (0x8A15002B) installed and already the newest version +:note_failed + if "%~2" == "0" exit /b 0 + if "%~2" == "-1978335135" exit /b 0 + if "%~2" == "-1978335189" exit /b 0 + set "install_failed=%install_failed% %~1" + exit /b 0 + +REM trim_slash <variable> - drop a trailing backslash from a path value. +REM A drive root keeps its own: "C:\" is not the same place as "C:". +:trim_slash + set "trim_value=!%~1!" + if not defined trim_value exit /b 0 + if not "!trim_value:~-1!" == "\" exit /b 0 + if "!trim_value:~-2!" == ":\" exit /b 0 + set "%~1=!trim_value:~0,-1!" + exit /b 0 + +REM kill_image <name> - stop every process of one image. The count is printed +REM first because taskkill can take a while and says nothing until it returns. +REM Never fails, so one stubborn image cannot stop the rest. +:kill_image + setlocal + REM A dry run does not count, so it prints the same on every machine. + if "%dry_run%" == "ON" ( + call :print_and_run taskkill /F /IM %~1 + endlocal + exit /b 0 + ) + set "img_count=0" + for /f "tokens=1" %%a in ('tasklist /nh /fi "IMAGENAME eq %~1" 2^>nul') do ( + if /I "%%a" == "%~1" set /a img_count+=1 + ) + if "!img_count!" == "0" ( + echo %~1: none running + endlocal + exit /b 0 + ) + echo %~1: stopping !img_count! + call :print_and_run taskkill /F /IM %~1 + endlocal + exit /b 0 + +REM print_and_run <command...> +:print_and_run + echo + %* + REM Recorded before the command runs. A set afterwards would clear the + REM errorlevel the next line returns, reporting every failure as a pass. + set "last_cmd=%*" + if not "%dry_run%" == "ON" ( + %* + exit /b !errorlevel! + ) + exit /b 0 + +REM handle_args <args...> +:handle_args + call :debug_msg starting function handle_args "%*" + if "%~1" == "" exit /b 0 + + setlocal + set arg=%~1 + set finalize_cmd= + + call :debug_msg Begin handling arg "%arg%" + + if not "%arg:~0,2%" == "--" goto :HAL_check_short + + call :debug_msg Processing long arg + + call :find_arg long %arg:~2% + %repeat_error% + set idx=%ret% + + call :get_arg_type %idx% + %repeat_error% + set type=%ret% + + if /I not "%type%" == "bool" ( + set "string_val=%~2" + if not defined string_val ( + echo Error in handle_args_loop: The option "%arg:~2%" requires a value + exit /b 1 + ) + + REM rawstring options carry cmake arguments, which start with a dash, so + REM only plain string options reject a value that looks like an option. + if /I "%type%" == "string" ( + if "!string_val:~0,1!" == "-" ( + echo Error in handle_args_loop: The option "%arg:~2%" requires a value. "!string_val!" looks like another option. + exit /b 1 + ) + ) + shift + ) + + call :set_arg %idx% "%string_val%" + %repeat_error% + goto :handle_args_loop_reset + + :HAL_check_short + if not "%arg:~0,1%" == "-" ( + echo Unknown argument: %arg% + call :print_help_msg + exit /b 1 + ) + + call :debug_msg processing short args + set /A charidx=1 + :short_arg_loop + if "!arg:~%charidx%,1!" == "" goto :handle_args_loop_reset + + call :find_arg short !arg:~%charidx%,1! + %repeat_error% + set idx=%ret% + + call :get_arg_type %idx% + %repeat_error% + set type=%ret% + + set /A start_idx = %charidx% + 1 + if /I not "%type%" == "bool" ( + REM A value may be attached (-j8) or be the next argument (-j 8). + set remaining=!arg:~%start_idx%! + if defined remaining ( + set string_val=!remaining! + ) else ( + set "string_val=%~2" + shift + ) + if not defined string_val ( + echo Error in handle_args_loop: The option "!arg:~%charidx%,1!" requires a value + exit /b 1 + ) + + REM As in the long-option branch above. + if /I "%type%" == "string" ( + if "!string_val:~0,1!" == "-" ( + echo Error in handle_args_loop: The option "!arg:~%charidx%,1!" requires a value. "!string_val!" looks like another option. + exit /b 1 + ) + ) + call :set_arg !idx! "!string_val!" + %repeat_error% + goto :handle_args_loop_reset + ) + + call :set_arg %idx% + %repeat_error% + set /A charidx+=1 + goto :short_arg_loop + + :handle_args_loop_reset + REM endlocal drops this iteration scope; finalize_cmd re-applies the options + REM it set, in the caller scope. + endlocal %finalize_cmd% + shift + goto :handle_args diff --git a/deps/Assimp/Assimp.cmake b/deps/Assimp/Assimp.cmake new file mode 100644 index 0000000000..9b973a55d5 --- /dev/null +++ b/deps/Assimp/Assimp.cmake @@ -0,0 +1,43 @@ +if(CMAKE_VERSION VERSION_LESS 3.22) + set(_assimp_url "https://github.com/assimp/assimp/archive/refs/tags/v5.3.1.tar.gz") + set(_assimp_hash "SHA256=a07666be71afe1ad4bc008c2336b7c688aca391271188eb9108d0c6db1be53f1") +else() + set(_assimp_url "https://github.com/assimp/assimp/archive/refs/tags/v5.4.3.tar.gz") + set(_assimp_hash "SHA256=66dfbaee288f2bc43172440a55d0235dfc7bf885dda6435c038e8000e79582cb") +endif() + +# Assimp's bundled zlib (contrib/zlib) is too old to compile against the modern +# macOS SDK: its zutil.h takes the classic-Mac branch under TARGET_OS_MAC and +# does `#define fdopen(fd,mode) NULL`, which then clobbers the SDK's real +# `fdopen` prototype in <stdio.h> and breaks the build. On macOS use the system +# zlib (already found by find_package(ZLIB) in deps-unix-common) instead. +if(APPLE) + set(_assimp_build_zlib "-DASSIMP_BUILD_ZLIB=OFF") +else() + set(_assimp_build_zlib "-DASSIMP_BUILD_ZLIB=ON") +endif() + +orcaslicer_add_cmake_project(Assimp + URL ${_assimp_url} + URL_HASH ${_assimp_hash} + CMAKE_ARGS + # Assimp's ccache support sets the global RULE_LAUNCH_COMPILE, which breaks + # the Ninja RC rule. The superbuild forwards CMAKE_<LANG>_COMPILER_LAUNCHER. + -DASSIMP_BUILD_USE_CCACHE=OFF + -DASSIMP_BUILD_TESTS=OFF + -DASSIMP_BUILD_SAMPLES=OFF + -DASSIMP_BUILD_ASSIMP_TOOLS=OFF + -DASSIMP_INSTALL_PDB=OFF + -DASSIMP_NO_EXPORT=ON + -DASSIMP_BUILD_ALL_IMPORTERS_BY_DEFAULT=OFF + -DASSIMP_BUILD_GLTF_IMPORTER=ON + -DASSIMP_BUILD_OBJ_IMPORTER=ON + -DASSIMP_BUILD_FBX_IMPORTER=ON + ${_assimp_build_zlib} + -DASSIMP_WARNINGS_AS_ERRORS=OFF + -DBUILD_WITH_STATIC_CRT=OFF +) + +if (MSVC) + add_debug_dep(dep_Assimp) +endif () diff --git a/deps/Boost/Boost.cmake b/deps/Boost/Boost.cmake index bdd801857e..08b62b9fb8 100644 --- a/deps/Boost/Boost.cmake +++ b/deps/Boost/Boost.cmake @@ -24,6 +24,13 @@ if (MSVC AND DEP_DEBUG) set(_options "FORWARD_CONFIG") endif () +# Boost.Container's bundled dlmalloc passes int* where the Win32 Interlocked API +# takes volatile long*; cl compiles that with a warning, clang errors out. +set(_boost_c_flags_line "") +if (MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang") + set(_boost_c_flags_line "-DCMAKE_C_FLAGS:STRING=-Wno-incompatible-pointer-types") +endif () + orcaslicer_add_cmake_project(Boost ${_options} URL "https://github.com/boostorg/boost/releases/download/boost-1.84.0/boost-1.84.0.tar.gz" @@ -38,6 +45,7 @@ orcaslicer_add_cmake_project(Boost "${_context_abi_line}" "${_context_arch_line}" "${_context_impl_line}" + "${_boost_c_flags_line}" ) -set(DEP_Boost_DEPENDS ZLIB) \ No newline at end of file +set(DEP_Boost_DEPENDS ZLIB) diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 39cc5de182..ed3af70d03 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -157,8 +157,16 @@ endif () function(orcaslicer_add_cmake_project projectname) cmake_parse_arguments(P_ARGS "FORWARD_CONFIG" "INSTALL_DIR;BUILD_COMMAND;INSTALL_COMMAND" "CMAKE_ARGS" ${ARGN}) + # MSVC is true for clang-cl as well, so the sub-build toolchain has to key on the + # generator. A non-Visual-Studio superbuild passes its own generator down, and with + # it the CMAKE_C_COMPILER / CMAKE_CXX_COMPILER forwarded below. + set(_dep_msvc_gen FALSE) + if (MSVC AND CMAKE_GENERATOR MATCHES "Visual Studio") + set(_dep_msvc_gen TRUE) + endif () + set(_configs_line -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}) - if (_is_multi OR MSVC) + if (_is_multi OR _dep_msvc_gen) if (P_ARGS_FORWARD_CONFIG) set(_configs_line -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}) elseif (ORCA_INCLUDE_DEBUG_INFO AND NOT DEP_DEBUG) @@ -174,7 +182,7 @@ function(orcaslicer_add_cmake_project projectname) set(_target_config "Release") endif() - if (MSVC) + if (_dep_msvc_gen) set(_gen CMAKE_GENERATOR "${DEP_MSVC_GEN}" CMAKE_GENERATOR_PLATFORM "${DEP_PLATFORM}") else() set(_gen "") @@ -182,7 +190,7 @@ function(orcaslicer_add_cmake_project projectname) if ($ENV{CMAKE_BUILD_PARALLEL_LEVEL}) set(_build_j "") # assume environment will control --build parallel setting - elseif(MSVC) + elseif(_dep_msvc_gen) set(_build_j "/m") else() set(_build_j "-j${NPROC}") @@ -367,6 +375,9 @@ include(libnoise/libnoise.cmake) include(Draco/Draco.cmake) +include(FFMPEG/FFMPEG.cmake) +include(Assimp/Assimp.cmake) + # I *think* 1.1 is used for *just* md5 hashing? # 3.1 has everything in the right place, but the md5 funcs used are deprecated @@ -448,6 +459,8 @@ set(_dep_list dep_libnoise dep_python3 dep_wxInspector + dep_FFMPEG + dep_Assimp ) if (MSVC) diff --git a/deps/CURL/CURL.cmake b/deps/CURL/CURL.cmake index a5ae1b9d00..3d649dfdea 100644 --- a/deps/CURL/CURL.cmake +++ b/deps/CURL/CURL.cmake @@ -56,6 +56,18 @@ else() set(_curl_static ON) endif() +# curl 7.75's configure probes and code rely on C laxness cl allows but clang +# errors on (implicit function declarations, int* vs u_long* in ioctlsocket), +# which flips probe results and misconfigures nonblock.c into the AmigaOS +# IoctlSocket branch. Relax both diagnostics so the probes behave like cl, and +# pin the camel-case probes off since they only "pass" by implicit declaration. +set(_curl_c_flags_line "") +set(_curl_probe_overrides "") +if (MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang") + set(_curl_c_flags_line "-DCMAKE_C_FLAGS:STRING=-Wno-implicit-function-declaration -Wno-incompatible-pointer-types") + set(_curl_probe_overrides -DHAVE_IOCTLSOCKET_CAMEL=0 -DHAVE_IOCTLSOCKET_CAMEL_FIONBIO=0) +endif () + orcaslicer_add_cmake_project(CURL # GIT_REPOSITORY https://github.com/curl/curl.git # GIT_TAG curl-7_75_0 @@ -69,6 +81,8 @@ orcaslicer_add_cmake_project(CURL -DBUILD_CURL_EXE:BOOL=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCURL_STATICLIB=${_curl_static} + "${_curl_c_flags_line}" + ${_curl_probe_overrides} ${_curl_platform_flags} ) diff --git a/deps/Eigen/Eigen.cmake b/deps/Eigen/Eigen.cmake index 599976debb..2a9cc7105c 100644 --- a/deps/Eigen/Eigen.cmake +++ b/deps/Eigen/Eigen.cmake @@ -7,5 +7,20 @@ orcaslicer_add_cmake_project(Eigen URL https://gitlab.com/libeigen/eigen/-/archive/5.0.1/eigen-5.0.1.zip URL_HASH SHA256=0dbb1f9e3aaad66f352c03227d8c983f6f0b49e0b07e71a7300f4abcc01aee12 CMAKE_ARGS "${_eigen_extra_flags}" + # Only the headers are consumed here. Everything below builds nothing we + # use, and all three enable_language(Fortran): test/CMakeLists.txt:9, + # lapack/CMakeLists.txt:6 and blas/testing/CMakeLists.txt:2. They default + # to ON because the dependency configures as its own top-level project. + # + # Whether that probe is harmless depends on what CMake finds. The Visual + # Studio generator supports no Fortran, so it finds nothing; clang-cl sits + # next to the LLVM toolset's flang, which works. MSVC with Ninja finds + # Strawberry Perl's MinGW gfortran instead, which the deps build already + # requires for OpenSSL, and hands it the MSVC-style /machine:x64 that + # MinGW's ld reads as a missing input file. The configure dies there and + # takes the rest of the superbuild with it. + -DEIGEN_BUILD_TESTING=OFF + -DEIGEN_BUILD_BLAS=OFF + -DEIGEN_BUILD_LAPACK=OFF DEPENDS dep_Boost dep_GMP dep_MPFR ) diff --git a/deps/FFMPEG/FFMPEG.cmake b/deps/FFMPEG/FFMPEG.cmake new file mode 100644 index 0000000000..38e952bbdd --- /dev/null +++ b/deps/FFMPEG/FFMPEG.cmake @@ -0,0 +1,87 @@ +set(_conf_cmd ./configure) + +if (MSVC) + set(_source_dir "${CMAKE_BINARY_DIR}/dep_FFMPEG-prefix/src/dep_FFMPEG") + + set(PREBUILD_URL_arm64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-07-17-14-28/ffmpeg-n7.0.3-31-g9b6ffd74b5-winarm64-orca-shared-7.0.zip") + set(PREBUILD_HASH_arm64 "12f4140279f2f8469885e1b5b2e8be9d788882914c21523cacd56989f3548054") + set(PREBUILD_URL_x64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-07-17-14-28/ffmpeg-n7.0.3-31-g9b6ffd74b5-win64-orca-shared-7.0.zip") + set(PREBUILD_HASH_x64 "e65916020ddb9ef84b2666dfbcbfc9b1d67f69d15b4a66db53754637bf2d498c") + + ExternalProject_Add(dep_FFMPEG + URL ${PREBUILD_URL_${DEPS_ARCH}} + URL_HASH SHA256=${PREBUILD_HASH_${DEPS_ARCH}} + DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND + COMMAND ${CMAKE_COMMAND} -E copy_directory "${_source_dir}/bin" "${DESTDIR}/bin" + COMMAND ${CMAKE_COMMAND} -E copy_directory "${_source_dir}/lib" "${DESTDIR}/lib" + COMMAND ${CMAKE_COMMAND} -E copy_directory "${_source_dir}/include" "${DESTDIR}/include" + ) + +else () + if (APPLE) + set(_minos_cmd + "--extra-cflags=-mmacosx-version-min=${DEP_OSX_TARGET}" + "--extra-ldflags=-mmacosx-version-min=${DEP_OSX_TARGET}" + ) + # Static FFmpeg: nothing to bundle into the .app, no rpath handling. + # Disable the VideoToolbox/AudioToolbox HW-accel paths: the player decodes + # in software (swscale), and the auto-detected HW objects would drag in + # system frameworks that the static libs would then depend on. + set(_link_cmd --enable-static --disable-shared --disable-videotoolbox --disable-audiotoolbox) + if (IS_CROSS_COMPILE) + set(_cross_cmd --enable-cross-compile) + set(_pic_cmd --enable-pic) + if (${CMAKE_SYSTEM_PROCESSOR} MATCHES "x86_64") + set(_arch_cmd --arch=arm64) + set(_cc_cmd "--cc=clang -arch arm64") + else() + set(_arch_cmd --arch=x86_64) + set(_cc_cmd "--cc=clang -arch x86_64") + endif() + endif() + else () + set(_link_cmd --enable-shared) + endif () + + set(_build_j -j) + if(DEFINED ENV{CMAKE_BUILD_PARALLEL_LEVEL}) + set(_build_j "-j$ENV{CMAKE_BUILD_PARALLEL_LEVEL}") + endif() + + ExternalProject_Add(dep_FFMPEG + URL https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n7.0.3.tar.gz + URL_HASH SHA256=DEEDCABE339165214A3637DF4C86A507AEF0D793CF8774FF68735F4737E8DDBC + DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG + CONFIGURE_COMMAND ${_conf_cmd} + ${_cross_cmd} + ${_pic_cmd} + ${_arch_cmd} + ${_cc_cmd} + "--prefix=${DESTDIR}" + ${_link_cmd} + ${_minos_cmd} + --disable-doc + --enable-small + --disable-outdevs + --disable-filters + --enable-filter=*null*,afade,*fifo,*format,*resample,aeval,allrgb,allyuv,atempo,pan,*bars,color,*key,crop,draw*,eq*,framerate,*_qsv,*_vaapi,*v4l2*,hw*,scale,volume,test* + --disable-protocols + --enable-protocol=file,fd,pipe,rtp,udp + --disable-muxers + --enable-muxer=rtp + --disable-encoders + --disable-decoders + --enable-decoder=*aac*,h264*,mp3*,mjpeg,rv* + --disable-demuxers + --enable-demuxer=h264,mp3,mov + --disable-zlib + --disable-avdevice + BUILD_IN_SOURCE ON + BUILD_COMMAND make ${_build_j} + INSTALL_COMMAND make install + ) + +endif() diff --git a/deps/OCCT/0001-OCCT-fix.patch b/deps/OCCT/0001-OCCT-fix.patch index 27f5db7e0f..d251cc7ab6 100644 --- a/deps/OCCT/0001-OCCT-fix.patch +++ b/deps/OCCT/0001-OCCT-fix.patch @@ -1,3 +1,20 @@ +diff --git a/adm/cmake/occt_defs_flags.cmake b/adm/cmake/occt_defs_flags.cmake +index 00000000..00000001 100644 +--- a/adm/cmake/occt_defs_flags.cmake ++++ b/adm/cmake/occt_defs_flags.cmake +@@ -134,7 +134,11 @@ + set (CMAKE_CXX_FLAGS "-std=c++0x ${CMAKE_CXX_FLAGS}") + endif() + # Optimize size of binaries +- set (CMAKE_SHARED_LINKER_FLAGS "-Wl,-s ${CMAKE_SHARED_LINKER_FLAGS}") ++ # clang-cl reports the Clang compiler ID, and OCCT builds shared on Windows, ++ # where the MSVC-style linker gets this flag as an argument it does not know. ++ if (NOT WIN32) ++ set (CMAKE_SHARED_LINKER_FLAGS "-Wl,-s ${CMAKE_SHARED_LINKER_FLAGS}") ++ endif() + elseif(MINGW) + add_definitions(-D_WIN32_WINNT=0x0601) + # _WIN32_WINNT=0x0601 (use Windows 7 SDK) diff --git a/CMakeLists.txt b/CMakeLists.txt index d98acc0f..28eb8eb4 100644 --- a/CMakeLists.txt @@ -168,6 +185,32 @@ index d98acc0f..28eb8eb4 100644 endforeach() if (BUILD_SAMPLES_QT) +diff --git a/adm/cmake/occt_macros.cmake b/adm/cmake/occt_macros.cmake +index 224c96b1..8c94a1c5 100644 +--- a/adm/cmake/occt_macros.cmake ++++ b/adm/cmake/occt_macros.cmake +@@ -608,7 +608,7 @@ macro (OCCT_INSERT_CODE_FOR_TARGET) + install(CODE "if (\"\${CMAKE_INSTALL_CONFIG_NAME}\" MATCHES \"^([Rr][Ee][Ll][Ee][Aa][Ss][Ee])$\") + set (OCCT_INSTALL_BIN_LETTER \"\") + elseif (\"\${CMAKE_INSTALL_CONFIG_NAME}\" MATCHES \"^([Rr][Ee][Ll][Ww][Ii][Tt][Hh][Dd][Ee][Bb][Ii][Nn][Ff][Oo])$\") +- set (OCCT_INSTALL_BIN_LETTER \"i\") ++ set (OCCT_INSTALL_BIN_LETTER \"\") + elseif (\"\${CMAKE_INSTALL_CONFIG_NAME}\" MATCHES \"^([Dd][Ee][Bb][Uu][Gg])$\") + set (OCCT_INSTALL_BIN_LETTER \"d\") + endif()") +diff --git a/adm/cmake/occt_toolkit.cmake b/adm/cmake/occt_toolkit.cmake +index 550e0e2f..7ac1a3b8 100644 +--- a/adm/cmake/occt_toolkit.cmake ++++ b/adm/cmake/occt_toolkit.cmake +@@ -241,7 +241,7 @@ + else() + set (aReleasePdbConf) + endif() +- install (FILES ${CMAKE_BINARY_DIR}/${OS_WITH_BIT}/${COMPILER}/bin\${OCCT_INSTALL_BIN_LETTER}/${PROJECT_NAME}.pdb ++ install (FILES $<TARGET_PDB_FILE:${PROJECT_NAME}> + CONFIGURATIONS Debug ${aReleasePdbConf} RelWithDebInfo + DESTINATION "${INSTALL_DIR_BIN}\${OCCT_INSTALL_BIN_LETTER}") + endif() diff --git a/src/Font/Font_FTFont.cxx b/src/Font/Font_FTFont.cxx index 5ae9899f..0a17372b 100644 --- a/src/Font/Font_FTFont.cxx diff --git a/deps/OCCT/OCCT.cmake b/deps/OCCT/OCCT.cmake index b4cd9facd2..62bfcf8e76 100644 --- a/deps/OCCT/OCCT.cmake +++ b/deps/OCCT/OCCT.cmake @@ -1,3 +1,10 @@ +# clang-cl cannot emit IGESAppli_GeneralModule.cxx on ARM64 +# (llvm/llvm-project#62081). cl and clang-cl share an ABI. +set(_occt_compiler_args "") +if ("${DEPS_ARCH}" STREQUAL "arm64" AND CMAKE_CXX_COMPILER_ID STREQUAL Clang) + set(_occt_compiler_args -DCMAKE_C_COMPILER:STRING=cl -DCMAKE_CXX_COMPILER:STRING=cl) +endif () + if(WIN32) set(library_build_type "Shared") else() @@ -31,6 +38,7 @@ orcaslicer_add_cmake_project(OCCT -DBUILD_MODULE_ModelingAlgorithms=OFF -DBUILD_MODULE_ModelingData=OFF -DBUILD_MODULE_Visualization=OFF + ${_occt_compiler_args} ) # add_dependencies(dep_OCCT ${FREETYPE_PKG}) diff --git a/deps/OpenCV/OpenCV.cmake b/deps/OpenCV/OpenCV.cmake index d8c7cbac3b..0290b3f47a 100644 --- a/deps/OpenCV/OpenCV.cmake +++ b/deps/OpenCV/OpenCV.cmake @@ -10,6 +10,13 @@ else () set(_options "") endif () +# carotene is OpenCV's ARM NEON HAL. It uses M_PI without _USE_MATH_DEFINES +# and does not compile with clang-cl. +set(_disable_carotene "") +if ("${DEPS_ARCH}" STREQUAL "arm64" AND CMAKE_CXX_COMPILER_ID STREQUAL Clang) + set(_disable_carotene "-DWITH_CAROTENE=OFF") +endif () + if (IN_GIT_REPO) set(OpenCV_DIRECTORY_FLAG --directory ${BINARY_DIR_REL}/dep_OpenCV-prefix/src/dep_OpenCV) endif () @@ -83,5 +90,6 @@ orcaslicer_add_cmake_project(OpenCV -DWITH_PROTOBUF=OFF -DWITH_WIN32UI=OFF -DHAVE_WIN32UI=FALSE + ${_disable_carotene} ) diff --git a/deps/OpenSSL/OpenSSL.cmake b/deps/OpenSSL/OpenSSL.cmake index 21a49b91b8..2fb4b51757 100644 --- a/deps/OpenSSL/OpenSSL.cmake +++ b/deps/OpenSSL/OpenSSL.cmake @@ -6,7 +6,7 @@ if(DEFINED OPENSSL_ARCH) set(_cross_arch ${OPENSSL_ARCH}) else() if(WIN32) - if("${CMAKE_GENERATOR_PLATFORM}" STREQUAL "ARM64") + if("${DEPS_ARCH}" STREQUAL "arm64") set(_cross_arch "VC-WIN64-ARM") else() set(_cross_arch "VC-WIN64A") @@ -17,10 +17,20 @@ else() endif() if(WIN32) - set(_conf_cmd perl Configure ) + set(_openssl_msvc_env CC=cl CXX=cl RC=rc CL=/FS) + # OpenSSL's perl Configure honors the CC environment variable, but the + # VC-WIN64A makefile only works with cl (an unquoted clang-cl path with + # spaces, e.g. exported by CLion, silently produces no .obj files and the + # lib step fails with LNK1181). Pin the upstream toolchain. + # Keep rc.exe resolved from the MSVC developer environment as well. The + # absolute Windows SDK path contains spaces and OpenSSL 1.1.1 writes it to + # the generated nmake file without quoting, which skips .res generation. + # /FS serializes access to OpenSSL's shared generated PDB when cl is + # driven through nmake from a Ninja configure step. + set(_conf_cmd ${CMAKE_COMMAND} -E env ${_openssl_msvc_env} perl Configure ) set(_cross_comp_prefix_line "") - set(_make_cmd nmake) - set(_install_cmd nmake install_sw ) + set(_make_cmd ${CMAKE_COMMAND} -E env ${_openssl_msvc_env} nmake) + set(_install_cmd ${CMAKE_COMMAND} -E env ${_openssl_msvc_env} nmake install_sw ) else() if(APPLE) set(_conf_cmd export MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} && ./Configure -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}) @@ -52,6 +62,14 @@ ExternalProject_Add(dep_OpenSSL CONFIGURE_COMMAND ${_conf_cmd} ${_cross_arch} "--openssldir=${DESTDIR}" "--prefix=${DESTDIR}" + # OpenSSL's linux-x86_64 target sets multilib=64, so it installs to + # <prefix>/lib64 while every other dep uses <prefix>/lib. CPython's + # --with-openssl only ever emits -L<dir>/lib, so it misses the bundled + # static libs and silently links the system OpenSSL instead -- which, + # against 1.1.1w headers, leaves _ssl.so with an undefined + # SSL_get_peer_certificate (removed in OpenSSL 3.x). Pin libdir so the + # prefix stays single-layout. + "--libdir=lib" ${_cross_comp_prefix_line} no-shared no-asm diff --git a/deps/PNG/PNG.cmake b/deps/PNG/PNG.cmake index 332fdfe274..a73e96a0c9 100644 --- a/deps/PNG/PNG.cmake +++ b/deps/PNG/PNG.cmake @@ -1,6 +1,10 @@ if (APPLE) # Only disable NEON extension for Apple ARM builds, leave it enabled for Raspberry PI. set(_disable_neon_extension "-DPNG_ARM_NEON=off") +elseif ("${DEPS_ARCH}" STREQUAL "arm64" AND CMAKE_CXX_COMPILER_ID STREQUAL Clang) + # libpng's CMake ignores PNG_ARM_NEON on Windows ARM64 and skips the NEON + # sources, but pngpriv.h enables NEON anyway. + set(_disable_neon_extension "-DCMAKE_C_FLAGS=/DWIN32 /D_WINDOWS /DPNG_ARM_NEON_OPT=0") else () set(_disable_neon_extension "") endif () diff --git a/deps/TBB/MSVC.cmake b/deps/TBB/MSVC.cmake new file mode 100644 index 0000000000..d7984bff80 --- /dev/null +++ b/deps/TBB/MSVC.cmake @@ -0,0 +1,98 @@ +# Copyright (c) 2020-2021 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set(TBB_LINK_DEF_FILE_FLAG ${CMAKE_LINK_DEF_FILE_FLAG}) +set(TBB_DEF_FILE_PREFIX win${TBB_ARCH}) + +# Workaround for CMake issue https://gitlab.kitware.com/cmake/cmake/issues/18317. +# TODO: consider use of CMP0092 CMake policy. +string(REGEX REPLACE "/W[0-4]" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + +set(TBB_WARNING_LEVEL $<$<BOOL:${TBB_STRICT}>:/W4> $<$<BOOL:${TBB_STRICT}>:/WX>) + +# Warning suppression C4324: structure was padded due to alignment specifier +set(TBB_WARNING_SUPPRESS /wd4324) +set(TBB_TEST_COMPILE_FLAGS /bigobj) + +if (MSVC_VERSION LESS_EQUAL 1900) + # Warning suppression C4503 for VS2015 and earlier: + # decorated name length exceeded, name was truncated. + # More info can be found at + # https://docs.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-1-c4503 + set(TBB_TEST_COMPILE_FLAGS ${TBB_TEST_COMPILE_FLAGS} /wd4503) +endif() + +set(TBB_LIB_COMPILE_FLAGS -D_CRT_SECURE_NO_WARNINGS /GS) +set(TBB_COMMON_COMPILE_FLAGS /volatile:iso /FS /EHsc) + +# Ignore /WX set through add_compile_options() or added to CMAKE_CXX_FLAGS if TBB_STRICT is disabled. +if (NOT TBB_STRICT AND COMMAND tbb_remove_compile_flag) + tbb_remove_compile_flag(/WX) +endif() + +if (WINDOWS_STORE OR TBB_WINDOWS_DRIVER) + set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} /D_WIN32_WINNT=0x0A00) + set(TBB_COMMON_LINK_FLAGS -NODEFAULTLIB:kernel32.lib -INCREMENTAL:NO) + set(TBB_COMMON_LINK_LIBS OneCore.lib) +endif() + +if (WINDOWS_STORE) + if (NOT CMAKE_SYSTEM_VERSION EQUAL 10.0) + message(FATAL_ERROR "CMAKE_SYSTEM_VERSION must be equal to 10.0") + endif() + set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} /ZW /ZW:nostdlib) + # CMake define this extra lib, remove it for this build type + string(REGEX REPLACE "WindowsApp.lib" "" CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES}") + + if (TBB_NO_APPCONTAINER) + set(TBB_LIB_LINK_FLAGS ${TBB_LIB_LINK_FLAGS} -APPCONTAINER:NO) + endif() +endif() + +if (TBB_WINDOWS_DRIVER) + # Since this is universal driver disable this variable + set(CMAKE_SYSTEM_PROCESSOR "") + # CMake define list additional libs, remove it for this build type + set(CMAKE_CXX_STANDARD_LIBRARIES "") + set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} /D _UNICODE /DUNICODE /DWINAPI_FAMILY=WINAPI_FAMILY_APP /D__WRL_NO_DEFAULT_LIB__) +endif() + +if (NOT DEFINED TBB_ENABLE_IPO) + if (DEFINED CMAKE_INTERPROCEDURAL_OPTIMIZATION) + set(TBB_ENABLE_IPO ${CMAKE_INTERPROCEDURAL_OPTIMIZATION}) + else() + set(TBB_ENABLE_IPO ON) + endif() +endif() + +if (TBB_ENABLE_IPO) + if (CMAKE_CXX_COMPILER_ID MATCHES "(Clang|IntelLLVM)") + if (CMAKE_SYSTEM_PROCESSOR MATCHES "(x86|AMD64)") + set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} -mrtm -mwaitpkg) + endif() + set(TBB_OPENMP_NO_LINK_FLAG TRUE) + set(TBB_IPO_COMPILE_FLAGS $<$<NOT:$<CONFIG:Debug>>:-flto>) + else() + set(TBB_IPO_COMPILE_FLAGS $<$<NOT:$<CONFIG:Debug>>:/GL>) + set(TBB_IPO_LINK_FLAGS $<$<NOT:$<CONFIG:Debug>>:-LTCG> $<$<NOT:$<CONFIG:Debug>>:-INCREMENTAL:NO>) + endif() +else() + if (CMAKE_CXX_COMPILER_ID MATCHES "(Clang|IntelLLVM)" AND CMAKE_SYSTEM_PROCESSOR MATCHES "(x86|AMD64)") + set(TBB_COMMON_COMPILE_FLAGS ${TBB_COMMON_COMPILE_FLAGS} -mrtm -mwaitpkg) + endif() + set(TBB_IPO_COMPILE_FLAGS "") + set(TBB_IPO_LINK_FLAGS "") +endif() + +set(TBB_OPENMP_FLAG /openmp) diff --git a/deps/TBB/TBB.cmake b/deps/TBB/TBB.cmake index 9b1452d33e..dac2ed63e6 100644 --- a/deps/TBB/TBB.cmake +++ b/deps/TBB/TBB.cmake @@ -1,4 +1,6 @@ -if (FLATPAK AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") +if (MSVC) + set(_patch_command ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/MSVC.cmake ./cmake/compilers/MSVC.cmake) +elseif (FLATPAK AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") set(_patch_command ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/GNU.cmake ./cmake/compilers/GNU.cmake) else() set(_patch_command "") @@ -13,6 +15,8 @@ orcaslicer_add_cmake_project( -DTBB_BUILD_SHARED=OFF -DTBB_BUILD_TESTS=OFF -DTBB_TEST=OFF + -DTBB_ENABLE_IPO=OFF + -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_DEBUG_POSTFIX=_debug ) diff --git a/deps/python3/python3.cmake b/deps/python3/python3.cmake index e93045fe45..d926c7e888 100644 --- a/deps/python3/python3.cmake +++ b/deps/python3/python3.cmake @@ -53,12 +53,9 @@ if(WIN32) set(_python_pcbuild_output_dir win32) endif() + # pybind11 undefines _DEBUG around Python.h so a debug build links the + # release python3xx.lib; Py_DEBUG could not load release plugin modules. set(_python_pcbuild_config Release) - set(_python_layout_debug OFF) - if(DEFINED DEP_DEBUG AND DEP_DEBUG) - set(_python_pcbuild_config Debug) - set(_python_layout_debug ON) - endif() # CPython's PCbuild needs a 64-bit-hosted toolchain: find_msbuild.bat picks the # 32-bit Bin\MSBuild.exe, whose x86 cl.exe/link.exe run out of address space @@ -101,7 +98,6 @@ if(WIN32) -DPYTHON_BUILD_DIR=<SOURCE_DIR>/PCbuild/${_python_pcbuild_output_dir} -DPYTHON_DEST_DIR=${DESTDIR}/libpython -DPYTHON_LAYOUT_ARCH=${_python_layout_arch} - -DPYTHON_DEBUG=${_python_layout_debug} -P ${CMAKE_CURRENT_LIST_DIR}/stage_windows.cmake ) elseif(APPLE) diff --git a/deps/python3/stage_windows.cmake b/deps/python3/stage_windows.cmake index b127f06b68..758e0be9f3 100644 --- a/deps/python3/stage_windows.cmake +++ b/deps/python3/stage_windows.cmake @@ -9,9 +9,6 @@ foreach(_var PYTHON_SOURCE_DIR PYTHON_BUILD_DIR PYTHON_DEST_DIR PYTHON_LAYOUT_AR endforeach() set(_python_exe "${PYTHON_BUILD_DIR}/python.exe") -if(PYTHON_DEBUG) - set(_python_exe "${PYTHON_BUILD_DIR}/python_d.exe") -endif() if(NOT EXISTS "${_python_exe}") message(FATAL_ERROR "Built Python executable not found: ${_python_exe}") @@ -49,22 +46,11 @@ endif() set(_required_files "${PYTHON_DEST_DIR}/Lib/encodings/__init__.py" "${PYTHON_DEST_DIR}/include/Python.h" + "${PYTHON_DEST_DIR}/python.exe" + "${PYTHON_DEST_DIR}/python${_python_abi}.dll" + "${PYTHON_DEST_DIR}/libs/python${_python_abi}.lib" ) -if(PYTHON_DEBUG) - list(APPEND _required_files - "${PYTHON_DEST_DIR}/python_d.exe" - "${PYTHON_DEST_DIR}/python${_python_abi}_d.dll" - "${PYTHON_DEST_DIR}/libs/python${_python_abi}_d.lib" - ) -else() - list(APPEND _required_files - "${PYTHON_DEST_DIR}/python.exe" - "${PYTHON_DEST_DIR}/python${_python_abi}.dll" - "${PYTHON_DEST_DIR}/libs/python${_python_abi}.lib" - ) -endif() - foreach(_required_file IN LISTS _required_files) if(NOT EXISTS "${_required_file}") message(FATAL_ERROR "Staged Python file missing: ${_required_file}") diff --git a/deps/wxInspector/wxInspector.cmake b/deps/wxInspector/wxInspector.cmake index 97c3810809..4a5b28407f 100644 --- a/deps/wxInspector/wxInspector.cmake +++ b/deps/wxInspector/wxInspector.cmake @@ -1,3 +1,26 @@ +# wxInspector finds wxWidgets through CMake's FindwxWidgets module, which only +# searches lib/vc*_lib because _WX_TOOL is hardcoded to "vc". A superbuild driven +# by clang-cl installs wxWidgets into lib/clang_x64_lib, so hand the module the +# directory wxWidgets actually used, derived the same way wxWidgetsConfig.cmake +# derives it. +set(_wxinspector_wx_hints "") +if (MSVC) + if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + set(_wx_compiler_prefix "clang") + else () + set(_wx_compiler_prefix "vc") + endif () + set(_wx_arch_suffix "") + if (CMAKE_GENERATOR_PLATFORM AND NOT CMAKE_GENERATOR_PLATFORM STREQUAL "Win32") + string(TOLOWER "_${CMAKE_GENERATOR_PLATFORM}" _wx_arch_suffix) + elseif (CMAKE_SIZEOF_VOID_P EQUAL 8) + set(_wx_arch_suffix "_x64") + endif () + set(_wxinspector_wx_hints + "-DwxWidgets_ROOT_DIR=${DESTDIR}" + "-DwxWidgets_LIB_DIR=${DESTDIR}/lib/${_wx_compiler_prefix}${_wx_arch_suffix}_lib") +endif () + orcaslicer_add_cmake_project( wxInspector URL https://github.com/Noisyfox/wxInspector/archive/refs/tags/v1.0.0.zip @@ -6,6 +29,7 @@ orcaslicer_add_cmake_project( CMAKE_ARGS -DCMAKE_CXX_FLAGS="-DwxDEBUG_LEVEL=0" -DCMAKE_POSITION_INDEPENDENT_CODE=ON + ${_wxinspector_wx_hints} ) if (MSVC) diff --git a/deps_src/clipper2/CMakeLists.txt b/deps_src/clipper2/CMakeLists.txt index c604002da7..86c9a9efab 100644 --- a/deps_src/clipper2/CMakeLists.txt +++ b/deps_src/clipper2/CMakeLists.txt @@ -37,7 +37,11 @@ target_include_directories(Clipper2 ) if (WIN32) - target_compile_options(Clipper2 PRIVATE /W4 /WX) + if (MSVC AND NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + target_compile_options(Clipper2 PRIVATE /W4 /WX) + elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + target_compile_options(Clipper2 PRIVATE /W4) + endif() else() target_compile_options(Clipper2 PRIVATE -Wall -Wextra -Wpedantic -Werror) target_link_libraries(Clipper2 PUBLIC -lm) diff --git a/deps_src/imgui/imgui_draw.cpp b/deps_src/imgui/imgui_draw.cpp index 913a551fa0..d88bc79904 100644 --- a/deps_src/imgui/imgui_draw.cpp +++ b/deps_src/imgui/imgui_draw.cpp @@ -2856,6 +2856,7 @@ const ImWchar* ImFontAtlas::GetGlyphRangesDefault() { 0x0020, 0x00FF, // Basic Latin + Latin Supplement 0x2000, 0x206F, // General Punctuation + 0x2103, 0x2103, // ℃ Celsius symbol 0x3000, 0x30FF, // CJK Symbols and Punctuations, Hiragana, Katakana 0x31F0, 0x31FF, // Katakana Phonetic Extensions 0xFF00, 0xFFEF, // Half-width characters diff --git a/deps_src/imgui/imstb_textedit.h b/deps_src/imgui/imstb_textedit.h index 7644670975..3733bb2fa9 100644 --- a/deps_src/imgui/imstb_textedit.h +++ b/deps_src/imgui/imstb_textedit.h @@ -465,6 +465,57 @@ static void stb_textedit_click(STB_TEXTEDIT_STRING *str, STB_TexteditState *stat STB_TEXTEDIT_LAYOUTROW(&r, str, 0); y = r.ymin; } + else + { + // In multi-line mode, clamp y to stay within the text vertical bounds. + // This lets the click still land at a valid location if the mouse is slightly + // above or below the text. + StbTexteditRow r; + int n = STB_TEXTEDIT_STRINGLEN(str); + int i = 0; + float base_y = 0, y_min, y_max; + + // Get the first row to establish y_min and start the iteration + STB_TEXTEDIT_LAYOUTROW(&r, str, 0); + if (r.num_chars <= 0) + { + state->cursor = 0; + state->select_start = state->cursor; + state->select_end = state->cursor; + state->has_preferred_x = 0; + return; + } + y_min = r.ymin; + y_max = base_y + r.ymax; + i = r.num_chars; + base_y += r.baseline_y_delta; + + // Walk the remaining rows to find the bottom of the last row + while (i < n) + { + STB_TEXTEDIT_LAYOUTROW(&r, str, i); + if (r.num_chars <= 0) + break; + y_max = base_y + r.ymax; + i += r.num_chars; + base_y += r.baseline_y_delta; + } + + // If the text ends with a newline, account for the empty trailing line + // so the cursor can be placed on it + if (n > 0 && STB_TEXTEDIT_GETCHAR(str, n - 1) == STB_TEXTEDIT_NEWLINE) + { + STB_TEXTEDIT_LAYOUTROW(&r, str, n); + y_max = base_y + r.ymax; + } + + // Subtract half the last line height to avoid rounding issues when the mouse + // is just barely below the last line (keep cursor on the last line, not after the text) + y_max -= (r.ymax - r.ymin) * 0.5f; + + if (y < y_min) y = y_min; + if (y > y_max) y = y_max; + } state->cursor = stb_text_locate_coord(str, x, y); state->select_start = state->cursor; @@ -485,6 +536,50 @@ static void stb_textedit_drag(STB_TEXTEDIT_STRING *str, STB_TexteditState *state STB_TEXTEDIT_LAYOUTROW(&r, str, 0); y = r.ymin; } + else + { + // In multi-line mode, clamp y to stay within the text vertical bounds. + // This lets the drag keep working if the mouse goes off the top or bottom of the text. + StbTexteditRow r; + int n = STB_TEXTEDIT_STRINGLEN(str); + int i = 0; + float base_y = 0, y_min, y_max; + + // Get the first row to establish y_min and start the iteration + STB_TEXTEDIT_LAYOUTROW(&r, str, 0); + if (r.num_chars <= 0) + return; + y_min = r.ymin; + y_max = base_y + r.ymax; + i = r.num_chars; + base_y += r.baseline_y_delta; + + // Walk the remaining rows to find the bottom of the last row + while (i < n) + { + STB_TEXTEDIT_LAYOUTROW(&r, str, i); + if (r.num_chars <= 0) + break; + y_max = base_y + r.ymax; + i += r.num_chars; + base_y += r.baseline_y_delta; + } + + // If the text ends with a newline, account for the empty trailing line + // so the cursor can be placed on it + if (n > 0 && STB_TEXTEDIT_GETCHAR(str, n - 1) == STB_TEXTEDIT_NEWLINE) + { + STB_TEXTEDIT_LAYOUTROW(&r, str, n); + y_max = base_y + r.ymax; + } + + // Subtract half the last line height to avoid rounding issues when the mouse + // is just barely below the last line (keep cursor on the last line, not after the text) + y_max -= (r.ymax - r.ymin) * 0.5f; + + if (y < y_min) y = y_min; + if (y > y_max) y = y_max; + } if (state->select_start == state->select_end) state->select_start = state->cursor; diff --git a/deps_src/miniz/CMakeLists.txt b/deps_src/miniz/CMakeLists.txt index e02d8a4885..7e060a180f 100644 --- a/deps_src/miniz/CMakeLists.txt +++ b/deps_src/miniz/CMakeLists.txt @@ -11,6 +11,8 @@ add_library(miniz_static STATIC if(${CMAKE_C_COMPILER_ID} STREQUAL "GNU") target_compile_definitions(miniz_static PRIVATE _GNU_SOURCE) +elseif (CMAKE_C_COMPILER_ID STREQUAL "Clang" AND CMAKE_C_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + target_compile_options(miniz_static PRIVATE /clang:-Wno-error=incompatible-pointer-types) endif() target_link_libraries(miniz INTERFACE miniz_static) diff --git a/docs/HLSD/preset-cache.md b/docs/HLSD/preset-cache.md new file mode 100644 index 0000000000..6e693dbd6f --- /dev/null +++ b/docs/HLSD/preset-cache.md @@ -0,0 +1,402 @@ +# System Preset Cache — High Level Design + +## Why it exists + +OrcaSlicer ships tens of thousands of system preset JSON files. Every launch used to +parse all of them: read each vendor profile, walk its machine, process and filament +sub-files, resolve inheritance, and build the preset collections from scratch. That +parse dominated startup, and it produced the same result every time, because system +presets only change when the app is updated or a profile update is installed. + +The preset cache replaces that parse with a read. Each vendor's presets are serialized +once — at build time, in CI — into a single binary file the app reads in one pass. The +read replaces the file walk and the JSON parsing, which is where the time went; +resolving inheritance and registering the presets still runs at load, through the same +code the JSON path uses, so the result is the parse's result without the parse. + +The cache is **only ever an optimization**. Every rule below exists to guarantee that a +cache is either provably equivalent to parsing the JSONs, or rejected. There is no +"mostly right" cache. + +## The unit is one vendor + +A cache covers exactly one vendor. `BBL.opc` sits beside `BBL.json` and holds +everything `BBL.json` and the `BBL/` sub-file tree would have produced. + +Per-vendor granularity is what makes the system practical: + +- A vendor whose profile is bumped invalidates only its own cache. The other 60-odd + vendors keep theirs — even when the bumped vendor is the shared Orca filament + library everyone else inherits from. +- The setup wizard, which loads vendors one at a time, gets the same speedup as + startup without a second code path. +- A vendor with no cache, or a broken one, costs only that vendor a parse. + +A cache holds *system* presets only. User presets, project settings and modified +presets are never serialized — they have their own storage and their own lifecycle. + +## Where the files live + +| Location | Contents on a shipped build | Role | +|---|---|---| +| `resources/profiles/` | `<vendor>.opc` alone — the profile and its preset JSONs both pruned | What the app ships with; what installing copies from, and the only thing it is read for | +| `<data_dir>/system/` | `<vendor>.opc` alone, or `<vendor>.json` + `<vendor>/` after an update | What the user has installed | +| `<data_dir>/system/` (dev build) | `<vendor>.json` + `<vendor>/` + `<vendor>.opc` written at runtime | A developer tree caches as it parses | +| `<data_dir>/cache/wizard_profile_data.json` | The wizard's derived vendor catalog plus the stamps it was built from | Written and read by the setup wizard only; never shipped (see "The wizard's profile-data cache") | + +Two forms of the same vendor therefore exist, and the system's central rule is that +**a vendor's cache is the whole of it**. Where a cache ships or is installed, no profile +and no preset JSONs sit beside it: the cache carries the presets, the vendor profile, +and the version stamp that says which release it came from. A vendor is "installed" if +either form is present *and usable*, and its installed version is read from whichever +form a load would serve. + +What stays beside the caches in `resources/profiles/` is everything that is not a +preset: each vendor's directory of printer thumbnails, cover images, bed models and +hotend meshes, which are read from disk by path and were never part of the cache. Files +that are not vendors at all, `blacklist.json` chief among them, are untouched. + +The alternative — shipping both and treating the cache as a sidecar — was rejected. It +doubles the installed size, and it creates a class of bug where the two disagree and +the app's behavior depends on which one a given code path happened to read. + +## What a cache file is + +A fixed-size header followed by one binary stream. + +The header carries a magic number, the cache format version, the payload size and a +CRC32 of the payload. It exists so that a truncated download, a half-written file or a +file from an entirely different program is rejected in microseconds, before anything +tries to interpret it. + +The payload opens with the stamps that decide whether the cache may be used at all — +format version, vendor name, vendor version — then a dictionary, and then the vendor's +data: its vendor profile, three lists of preset entries (process, filament, machine), +and the count of errors the original parse hit. + +Each entry is one preset **in source form**: what its JSON sub-file states and nothing +that resolving it derives — the preset's own config diff, the name of the preset it +inherits, and the parse metadata (name, sub-path, description, instantiation, setting +and filament ids, renames). Non-instantiated base presets are stored too; the children +that inherit from them cannot resolve without them. + +**The payload names its own keys.** The dictionary holds the distinct `opt_key`s the +file uses, the `ConfigOptionType` each was written as, and the distinct enum *value +names*; an option in an entry's config is then a `uint16` index into that dictionary +plus its value. Names are written once per file rather than once per occurrence, and a +reader resolves the dictionary against this build's `print_config_def` once, after +which reading an option is a vector index. + +This is what makes the cache survive config-schema drift. The alternative — keying an +option by its `serialization_key_ordinal`, the position `ConfigDef::add` assigns by +declaration order at static init — cannot: inserting one option into the middle of +`PrintConfig.cpp` shifts every later ordinal, and the lookup on the way back in then +*succeeds on the wrong option*, silently, wherever the two share a type. Because a +name-keyed payload instead drops the individual options this build cannot place, the +file as a whole stays readable, and there is no schema fingerprint — no checksum over +the option schema that would reject every cache on every release. An option this build +no longer defines, or now defines with a different type, gets exactly what it gets from +a JSON profile: read, dropped, and the rest of the preset loads. + +The ordinal-keyed cereal hooks in `PrintConfig.hpp` are untouched — they are also the +undo/redo wire format, where the process cannot change underneath them. The cache has +its own serialization in `PresetCacheFormat.{hpp,cpp}`. + +Three deliberate choices in the layout: + +- **Stamps come first**, so the question "what version is this vendor installed at?" + can be answered by reading the first kilobyte. The updater asks that question for + every vendor on every launch; reading tens of megabytes to answer it would give back + the startup time the cache saved. The dictionary sits behind them, ahead of the + entries, so a reader that does go on resolves it once and then indexes. +- **Nothing inherited is baked in.** A filament preset that inherits from the shared + library is stored as its own diff plus its parent's name, and the parent is looked up + when the entry is installed, against whatever library is loaded then. A cache + therefore carries no other vendor's values, and no other vendor's update — the + library's included — can make it stale. +- **Nothing derived is stored.** Default presets, flattened configs, aliases and + lookup maps are all reconstructed at load by the same code the JSON path runs, and + state that path never fills (obsolete-preset lists) is not stored either. This keeps + the cache a record of the vendor's data, not a memory image of the program's state. + +## When a cache may be used + +A cache is accepted only if every gate below passes. Any failure means "parse the +JSONs instead" — never a hard error, never a partial load. + +**1. Integrity.** Magic number, a declared body size that is exactly the rest of the +file, CRC32 over the payload. The size is checked against the file's real length before +anything is allocated on the strength of it, so an eight-byte field in an unauthenticated +file cannot ask for a gigabyte. + +**2. Cache format version.** A single integer bumped by hand whenever the binary layout +changes in a way nothing else would catch: reordering or retyping a hand-written +serialized field, or changing what the cache's own stamps mean. Config-schema drift is +explicitly *not* such a change — the dictionary handles it — so this no longer moves +every release. + +**3. Vendor identity and version.** The cache names the vendor it holds and the profile +version it was built from. It is accepted only if that version is at least as new as +the profile now on disk. Where no profile sits beside the cache — the shipped, +cache-only form — the comparison is skipped, because nothing on disk can be newer than +a cache that is the installation. + +**4. Every entry installs.** Entries are installed as they are read, and an entry that +cannot be — typically one that inherits a parent the currently loaded filament library +no longer provides — rejects the whole cache, never just the entry. A partial vendor is +not a vendor. + +There is deliberately no stamp for the shared filament library. A cache stores its +filaments' inheritance by name and resolves it at load, so a library update changes +what a cache load *produces*, never whether the cache is *valid* — the same file yields +the updated result. This matters most on a shipped build, where a vendor is its cache +and nothing else: a profile update that delivered only the library would otherwise have +stranded every other vendor with a cache it invalidated and no JSONs to fall back on. + +A vendor profile with no parsable version is never cached and never served from a +cache. There would be no way to tell later whether the cache had gone stale, and a +cache nothing can invalidate is worse than no cache. + +## How a vendor is loaded + +Vendors load in a fixed order, because filament inheritance crosses exactly one +boundary: any vendor's filament may inherit from the shared Orca filament library, +and nothing else reaches across vendors. The library therefore goes first, alone; +every other vendor follows in parallel, resolving against it; and the results are +merged in a stable order: + +```mermaid +flowchart LR + lib["1 · OrcaFilamentLibrary<br/>loaded first, synchronously"] --> par["2 · every other vendor in parallel,<br/>each into its own bundle, filaments<br/>resolving against the loaded library"] --> merge["3 · bundles merged into one,<br/>sequentially, in stable vendor order"] +``` + +Whether a vendor comes from its cache or from a parse changes nothing in that +order — both produce the same bundle, so cached and parsed vendors mix freely in +one startup. + +**A vendor is loaded from where it is installed and nowhere else.** For startup that +is `<data_dir>/system/`; resources reaches the app by being *installed* into that +directory first, never by being loaded from. (The setup wizard is the one caller with +a different notion of "where": it also shows vendors the user has not installed, and +loads those from `resources/profiles` — see "The wizard's profile-data cache".) There +is one lookup tier and one parse source: + +``` +load vendor V from <data_dir>/system: + system/V.opc passes CACHE_VERSION + size + CRC + vendor name + version gate? + yes -> serve from it + no -> parse system/V.json, then write system/V.opc back +``` + +The same decision drawn out — "the gates" are the four acceptance checks above: + +```mermaid +flowchart TB + start["load vendor V from a directory dir<br/>— normally <data_dir>/system/"] + start --> stamp["installed version = version of dir/V.json<br/>— or ∞ with no profile there,<br/>the cache then being the installation"] + stamp --> g1{"dir/V.opc<br/>passes all four gates?"} + g1 -- "yes" --> hit(["served from the<br/>installed cache"]) + g1 -- "no" --> pd["parse the JSONs in dir"] + pd --> ver{"profile version<br/>parsable?"} + ver -- "yes" --> save(["loaded; dir/V.opc written back —<br/>the next load takes the top path"]) + ver -- "no" --> raw(["loaded, never cached"]) +``` + +A second tier into `resources/profiles/` used to sit between those two, and a parse +fallback to the same place behind them. Both existed only because an installed cache +died on every app upgrade, when the schema fingerprint rejected it; with the fingerprint +gone there is nothing for them to rescue. They also had a cost: on a developer tree the +shipped cache answered first, so the profile in `<data_dir>/system/` was never parsed +and its cache was never written back. + +Serving from a cache is not a memory-image restore. The entries are deserialized and +then installed one by one — inheritance resolved against the presets installed before +them and the currently loaded filament library, configs flattened onto the collection +defaults, validated and registered — by the same function the JSON path calls straight +after parsing a sub-file. The two paths share everything below the parse, which is what +makes a cache-loaded bundle indistinguishable from a JSON-loaded one by construction +rather than by test coverage. Installation also rebuilds each preset's file path from +the local data directory, so a shipped cache never carries the generating machine's +paths. + +App upgrades work because a cache normally survives one. Only a deliberate +`CACHE_VERSION` bump makes an installed cache unreadable, and that is handled at +install time rather than at load: a vendor whose cache this build cannot read counts +as **not installed**, so the updater lays down a working copy on the next launch (see +below). A vendor that still has its profile JSONs beside the cache is simply parsed +and re-cached. + +If a parse does happen and the vendor's profile carries a version, the app writes the +cache back beside where it looked for the vendor. That is how a developer build warms +itself up on second launch, and how a vendor delivered by a profile update becomes +cached without waiting for the next release. + +## The wizard's profile-data cache + +The setup wizard's printer and filament pages want every vendor in one bundle — the +installed ones *and* the shipped ones the user has not installed yet, because the +wizard is where installing is chosen. Its set therefore spans two directories: +`<data_dir>/system/` for installed vendors (shadowing resources on a name collision), +`resources/profiles` for the rest, each vendor loaded from its own directory. + +What the wizard actually consumes from that bundle is one derived JSON — the model / +machine / filament / process catalog its web pages render — and that JSON is a pure +function of the vendor set: each vendor's name and version, in load order. A profile +change requires a version bump, so name and version determine a vendor's content +wherever its copy sits; which directory served it is deliberately **not** stamped, +and installing or removing a copy at an unchanged version leaves the cache valid. So +the wizard caches the *derived JSON*, not another form of the inputs: +`<data_dir>/cache/wizard_profile_data.json` holds the stamp list and the catalog. On +open, the wizard computes the current stamps (one version peek per vendor) and, when +they match, serves the catalog from the file — no bundle built, no preset installed. +Caching bundle inputs instead was tried and measured: rebuilding the bundle from +per-vendor caches costs ~2 s of preset installation whatever feeds it, so only +skipping the rebuild entirely wins. + +Any change to the set — a vendor added, removed or updated, or its cache-only +`.opc` replaced by a newer one — changes the stamps and retires the whole file; +the wizard then rebuilds the bundle vendor by vendor (per-vendor caches serving where +they cover) and writes the catalog back. Selections, region and per-open decorations +are applied downstream of the cache either way, so a served catalog is +indistinguishable from a rebuilt one. Nothing ships this file and the updater never +touches it; it is a locally written artifact, re-derived whenever stale, written +through a temp file and rename so half a cache is never readable. + +The cache lives under `<data_dir>/cache/`, not beside the vendors: everything that +scans `<data_dir>/system/` treats any `.opc` there as a vendor, so a non-vendor +cache file must not sit in that directory. Relatedly, the stamp reader is hardened: +`read_cache_stamps` validates the cache version before reading anything +variable-length and bounds the stamp strings' lengths, so a reader pointed at a +foreign or damaged `.opc` rejects it cleanly instead of aborting on a garbage +64-bit allocation. + +## How a vendor is installed + +Installing copies from `resources/profiles/` into `<data_dir>/system/`. A shipped build +offers only a cache and a source tree only JSONs, but a partially-generated tree can +have both, at different versions, so the installer picks the form that ships at the +**newer version** and installs only that one: + +- Cache newer or equal, and readable → copy the `.opc`, verify the *copy* is one this + build can read, and only then delete any profile and vendor directory a previous + install left behind, so nothing can shadow it. +- Profile newer, or the cache unreadable or absent → copy the profile and the vendor's + preset JSONs exactly as the app did before caches existed, and delete any stale `.opc` + once the profile is safely in place. + +One vendor that cannot be installed is one vendor missing, not a reason to leave the +rest uninstalled: the installer skips it, records the failure, and carries on with the +batch. A vendor whose cache arrives unreadable falls back to installing its profile, +which is decided by reading the copy rather than by the kilobyte peek that chose the +form. + +**"Installed" means present and usable.** Where the cache is the whole of a vendor's +installation, a `.opc` this build cannot read is not an installation — counted as one, +the vendor would be stranded with nothing to load and the updater would never repair +it. The installed version is likewise whichever form a load would actually serve: the +cache's stamp while it covers the profile beside it, the profile's own version once it +does not. + +The result is that only one form of a vendor is ever present, and it is the newest one +the build has. This matters most for the update check, which compares what is installed +against what installing *would* lay down: if those two disagreed about which form +counts, a vendor could reinstall on every launch forever, or silently never update. + +Profile updates delivered over the air always arrive as JSONs, and they win — an +updated vendor's real profile lands in the data directory, the installed cache beside it +is older and gets rejected, and the vendor is parsed and re-cached. An update that touches only +the filament library needs nothing more: every other vendor's cache stays valid and +simply resolves against the new library on its next load. + +## How the caches are produced + +Cache generation is a build step, not something a user ever runs. + +One script per platform does the whole job, and CI calls it once on each. It builds a +small dev-utility that loads a profiles directory exactly as the app would, with cache +writing enabled, dropping a `<vendor>.opc` beside every vendor profile it parses; then +it copies those caches into each packaged application it was pointed at and deletes +every preset JSON they replace — the vendor's own profile included. Only a vendor that +actually has a cache is pruned, so a vendor the generator skipped keeps its JSONs and is +simply parsed at startup. + +Caches are generated into the checkout's own `resources/profiles`, because that is what +cpack re-installs from when it builds the NSIS installer — so that directory is also a +prune target in CI. Pruning it deletes the checkout's preset JSONs, which is a packaging +step, not something a build should do to a working tree by surprise: the Windows script +refuses that target unless given `--prune-source`, and CI passes it. + +Generation runs after the build, in the same job, so the caches ship with a build that +can read them. + +The flatpak differs only in where the script is called from. Nothing outside +flatpak-builder ever builds it, so there is no packaged tree for the workflow to point +the script at afterwards: the manifest runs it as a build step instead, against the +profiles the install has already copied into `/app`. + +## Behavior when things go wrong + +The system is designed so that no cache problem is fatal: + +- **Corrupt, truncated or foreign file** — rejected at the header, vendor parsed. A + cache is written to a temp file beside its target and moved into place, so a write + that dies partway leaves the previous cache intact rather than a truncated one. +- **An option this build no longer has, or now types differently** — that option alone + is dropped, exactly as a JSON profile's would be. The preset and the file load. +- **Cache from a build with a different cache layout** — rejected on `CACHE_VERSION`. + A vendor with JSONs beside it is parsed and re-cached; a cache-only vendor reads as + not installed and the updater reinstalls it. +- **Stale cache** — rejected on the vendor version stamp, vendor parsed and re-cached. +- **Failure part-way through loading** — a deserialization error, or any entry that + fails to install — rejects the whole cache, and the bundle is reset to a clean state + before falling back, so a half-loaded cache can never leak into the parsed result. +- **A vendor that can be neither read nor parsed** — logged, and left out. The setup + wizard drops that vendor from its list and opens with the rest; startup records the + error alongside the vendors that did load. One broken vendor never takes the app down. + +The one genuine limit: on a shipped build a vendor is its cache and nothing else, so a +rejected cache has nothing to fall back to for that vendor. This is by design — the +alternative is shipping every preset twice — and it is why the acceptance gates are +conservative and why CI generates the caches with the same build that ships them. The +recovery path is a profile update, which delivers real JSONs. + +It also means nothing may quietly assume a `<vendor>.json` exists. Discovery, version +checks and the update decision all read whichever form is present, and a code path that +enumerates only `*.json` will find no vendors at all in a packaged build. + +## Maintenance rules + +- **Adding, removing, retyping or reordering a config option** needs nothing. The + payload names its keys and its enum values, so an option a cache carries and this + build does not is dropped; one this build has and the cache does not is simply + absent, as it would be from a JSON that predates it. +- **Changing a hand-written `serialize()`** — `VendorProfile` or its nested types — or + the `CachedPreset` field list — written and read by `visit_entry` in + `PresetCacheFormat.cpp`, one list for the save, the load and the name peek alike — or + the cache's own layout or stamps, requires bumping `CACHE_VERSION` by hand. +- **The dictionary indexes with a `uint16`**, so `print_config_def` may hold at most + 65535 options and one cache at most 65535 distinct enum value names. + `CacheDictionary::save` throws past that, which surfaces when CI generates the + caches rather than on a user's machine. +- **Bumping `CACHE_VERSION` is safe without a resources fallback** because + `is_vendor_installed` means *present and usable*: cache-only vendors read as not + installed after a bump, and the updater reinstalls them from resources. +- **Bumping a vendor profile's version** invalidates that vendor's cache and nothing + else — the filament library's included. Other vendors' caches resolve against the + new library the next time they load. +- **Caches are never committed.** They are build artifacts, generated per build, + ignored by git. + +## Where this lives in the tree + +| Area | Files | +|---|---| +| Everything about the bytes on disk — the dictionary, one config's wire format, the file framing and stamps, entry serialization, `VendorCacheFile` save/load/peeks | `src/libslic3r/PresetCacheFormat.{hpp,cpp}` | +| Serve-or-parse decision, installing cache entries into a bundle, cache write-back | `src/libslic3r/PresetBundle.{hpp,cpp}` | +| Vendor profile serialization | `src/libslic3r/Preset.hpp` | +| Vendor discovery, installed/shipped versions, installation | `src/libslic3r/utils.cpp` (declared in `Utils.hpp`) | +| Update and reinstall decisions | `src/slic3r/Utils/PresetUpdater.cpp` | +| Setup wizard and printer-selection dialog | `src/slic3r/GUI/ConfigWizard.cpp`, `src/slic3r/GUI/WebGuideDialog.cpp` | +| Generator tool | `src/dev-utils/generate_system_cache.cpp` | +| Build and packaging script | `scripts/build_preset_cache.{sh,bat}` | +| Tests | `tests/libslic3r/test_vendor_cache.cpp` | diff --git a/docs/superpowers/plans/2026-08-14-ffmpeg-player-macos.md b/docs/superpowers/plans/2026-08-14-ffmpeg-player-macos.md new file mode 100644 index 0000000000..811626fc8b --- /dev/null +++ b/docs/superpowers/plans/2026-08-14-ffmpeg-player-macos.md @@ -0,0 +1,376 @@ +# macOS FFmpeg Media Player Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make macOS use the same FFmpeg-based media player (`wxMediaCtrl3` + `AVVideoDecoder`) as Windows/Linux, linking the static FFmpeg libraries from the deps build, and remove the old `wxMediaCtrl2.mm` BambuPlayer-based player. + +**Architecture:** The new player is platform-neutral C++ already used on Linux/Windows. Enabling it on macOS is pure build wiring: compile `wxMediaCtrl3.cpp` + `AVVideoDecoder.cpp` on macOS, drop the `__WXMAC__` alias that redirects `wxMediaCtrl3` to the old `wxMediaCtrl2`, and link static FFmpeg (`libavcodec.a`/`libswscale.a`/`libavutil.a`) from the deps install. The Bambu stream API is dlsym'd at runtime from the network plugin (`libBambuSource.dylib`), which already exports it — no plugin changes needed. Rendering reuses the existing `wxImage` → `DrawBitmap` paint path (same as Linux). + +**Tech Stack:** C++17, wxWidgets, CMake, FFmpeg 7.0.3 (libavcodec/libswscale/libavutil), macOS (Xcode generator), `deps/` ExternalProject build system. + +## Global Constraints + +- Branch: `dev/ffmpeg-player-macos`. Commit after every task. +- **Linux and Windows builds must not change** — the FFmpeg deps flag change is guarded by `APPLE`; Linux keeps `--enable-shared`, Windows keeps its prebuilt DLL zips. +- Static FFmpeg only on macOS: deps produce `libavcodec.a`/`libswscale.a`/`libavutil.a`; the app links those explicitly — the app binary must have **no** `libav*` dylib references (`otool -L` check). +- Follow existing code style: PascalCase classes, snake_case functions, C++17. +- No changes to `StatusPanel.cpp`, `MediaPlayCtrl.*`, or the BambuTunnel interface — the app already creates `wxMediaCtrl3` and uses only its public interface. +- The player cannot be unit-tested (hardware/plugin-dependent GUI code); verification is build-level, link-level, and manual runtime on a Mac. +- `localization/i18n/list.txt` references only `wxMediaCtrl2.cpp` (Win/Linux, stays) — no translation-list changes needed. +- Build dirs on the dev machine: main app = `build_arm64/` (Xcode generator, multi-config), deps = `deps/build/arm64/` (Unix Makefiles). App target name: `OrcaSlicer`. Substitute your own configured build dirs where noted. + +--- + +### Task 1: Enable wxMediaCtrl3 on macOS and link static FFmpeg + +**Files:** +- Modify: `src/slic3r/GUI/wxMediaCtrl3.h` (lines 18–22: the `#ifdef __WXMAC__` alias branch) +- Modify: `src/slic3r/GUI/wxMediaCtrl3.cpp:13` (uncomment the event define) +- Modify: `src/slic3r/GUI/wxMediaCtrl2.cpp:101` (remove the event define) +- Modify: `src/slic3r/CMakeLists.txt` (APPLE source list ~lines 779–792; FFmpeg link block ~lines 905–910) + +**Interfaces:** +- Consumes: nothing new (all classes already exist). +- Produces: `wxMediaCtrl3` class compiled on macOS with the same interface as Linux/Windows — `Load(wxURI)`, `Play()`, `Stop()`, `SetIdleImage(wxString)`, `GetState()`, `GetLastError()`, `GetVideoSize()`, event `EVT_MEDIA_CTRL_STAT` defined once in the lib (from `wxMediaCtrl3.cpp`). + +- [ ] **Step 1: Remove the macOS alias in wxMediaCtrl3.h** + +Current (lines 16–23 of `src/slic3r/GUI/wxMediaCtrl3.h`): + +```cpp +void wxMediaCtrl_OnSize(wxWindow * ctrl, wxSize const & videoSize, int width, int height); + +#ifdef __WXMAC__ + +#include "wxMediaCtrl2.h" +#define wxMediaCtrl3 wxMediaCtrl2 + +#else + +#define BAMBU_DYNAMIC +``` + +New: + +```cpp +void wxMediaCtrl_OnSize(wxWindow * ctrl, wxSize const & videoSize, int width, int height); + +#define BAMBU_DYNAMIC +``` + +Also remove the matching `#endif` that closed the `#else` branch (the one before the final `#endif /* wxMediaCtrl3_h */`), so the file's `#ifndef`/`#endif` guard pair stays balanced. + +- [ ] **Step 2: Move the EVT_MEDIA_CTRL_STAT definition into wxMediaCtrl3.cpp** + +In `src/slic3r/GUI/wxMediaCtrl3.cpp:13`, uncomment: + +```cpp +//wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent); +``` + +becomes: + +```cpp +wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent); +``` + +In `src/slic3r/GUI/wxMediaCtrl2.cpp:101`, delete: + +```cpp +wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent); +``` + +(One definition total in the lib — `MediaPlayCtrl.cpp:59` binds this event on the media ctrl.) + +- [ ] **Step 3: Update the APPLE source list in CMakeLists.txt** + +In `src/slic3r/CMakeLists.txt`, the APPLE branch (currently compiles `wxMediaCtrl2.mm`, which becomes dead on macOS): + +```cmake + GUI/wxMediaCtrl2.mm + GUI/wxMediaCtrl2.h + GUI/wxMediaCtrl3.h + ) +``` + +becomes: + +```cmake + GUI/AVVideoDecoder.cpp + GUI/AVVideoDecoder.hpp + GUI/wxMediaCtrl3.cpp + GUI/wxMediaCtrl3.h + ) +``` + +(The `else ()` branch — Win/Linux — stays exactly as it is.) + +- [ ] **Step 4: Link static FFmpeg on macOS** + +In `src/slic3r/CMakeLists.txt`, the FFmpeg block (currently `if (NOT APPLE)`): + +```cmake +if (NOT APPLE) + pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET + libavcodec + libswscale + libavutil + ) + target_link_libraries(libslic3r_gui PkgConfig::LIBAV) +endif() +``` + +becomes: + +```cmake +if (APPLE) + # Static FFmpeg from the deps install: nothing to bundle into the .app, + # no rpath/install_name handling. Order matters: avcodec -> swscale -> avutil. + find_library(LIBAVCODEC_LIBRARY NAMES libavcodec.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) + find_library(LIBSWSCALE_LIBRARY NAMES libswscale.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) + find_library(LIBAVUTIL_LIBRARY NAMES libavutil.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) + target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY}) + target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include) +else () + pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET + libavcodec + libswscale + libavutil + ) + target_link_libraries(libslic3r_gui PkgConfig::LIBAV) +endif() +``` + +The deps install (`${CMAKE_PREFIX_PATH}/lib`) already contains the three `.a` files from the existing arm64 deps build — no deps rebuild needed for this task. + +- [ ] **Step 5: Reconfigure and build the app** + +Run (Xcode generator; `cmake` re-runs automatically on build): + +```bash +cmake --build build_arm64 --config RelWithDebInfo --target OrcaSlicer +``` + +Expected: configure succeeds (no `pkg_check_modules` errors on macOS, `find_library` finds all three `.a` files), compile succeeds (`wxMediaCtrl3.cpp` and `AVVideoDecoder.cpp` compile on macOS without changes), link succeeds. + +If CMake complains that `wxMediaCtrl3.h` is included but not in the source list or similar IDE-only warnings — ignore; headers in the list are cosmetic. + +- [ ] **Step 6: Verify no dynamic FFmpeg dependency** + +```bash +otool -L build_arm64/src/RelWithDebInfo/OrcaSlicer.app/Contents/MacOS/OrcaSlicer | grep -i "libav" || echo "OK: no dynamic FFmpeg" +``` + +Expected: prints `OK: no dynamic FFmpeg` (empty grep output). This is the whole point of static linking — nothing to bundle into the `.app`. + +- [ ] **Step 7: Quick sanity — macOS unit tests still pass** + +```bash +ctest --test-dir build_arm64/tests/libslic3r --output-on-failure +``` + +Expected: passes (add `-C RelWithDebInfo` if the multi-config generator requires it). If no tests were built in this build dir, build target `tests` first (`cmake --build build_arm64 --config RelWithDebInfo --target tests`). + +- [ ] **Step 8: Commit** + +```bash +git add src/slic3r/CMakeLists.txt src/slic3r/GUI/wxMediaCtrl3.h src/slic3r/GUI/wxMediaCtrl3.cpp src/slic3r/GUI/wxMediaCtrl2.cpp +git commit -m "feat: use FFmpeg media player on macOS with static FFmpeg" +``` + +--- + +### Task 2: Static-only FFmpeg in the macOS deps build + +**Files:** +- Modify: `deps/FFMPEG/FFMPEG.cmake` (non-MSVC branch, APPLE section and CONFIGURE_COMMAND) + +**Interfaces:** +- Consumes: nothing. +- Produces: a deps install on macOS containing only `libavcodec.a`, `libswscale.a`, `libavutil.a` (+ headers) — no `libav*` dylibs, so no bundling/rpath machinery is ever needed on macOS. Linux and Windows output are unchanged. + +- [ ] **Step 1: Add the static flag variable** + +In `deps/FFMPEG/FFMPEG.cmake`, inside the non-MSVC `else ()` branch, in the existing `if (APPLE)` block: + +```cmake + if (APPLE) + set(_minos_cmd + "CFLAGS=-mmacosx-version-min=${DEP_OSX_TARGET}" + "LDFLAGS=-mmacosx-version-min=${DEP_OSX_TARGET}" + ) +``` + +add after the `_minos_cmd` set: + +```cmake + # Static FFmpeg: nothing to bundle into the .app, no rpath handling. + # Shared flags must come AFTER --enable-shared below so they win. + set(_link_cmd --enable-static --disable-shared) +``` + +and add a matching `else ()` after the `if (IS_CROSS_COMPILE) ... endif()` block inside that `if (APPLE)`, so non-Apple Unix keeps shared: + +```cmake + else () + set(_link_cmd --enable-shared) + endif () +``` + +(If the existing `if (IS_CROSS_COMPILE)` block is the last thing inside `if (APPLE)`, the new `else ()` closes the `if (APPLE)` itself.) + +- [ ] **Step 2: Use the variable in CONFIGURE_COMMAND** + +In the `ExternalProject_Add(dep_FFMPEG ...)` configure command: + +```cmake + "--prefix=${DESTDIR}" + --enable-shared +``` + +becomes: + +```cmake + "--prefix=${DESTDIR}" + --enable-shared + ${_link_cmd} +``` + +Order matters: `--enable-shared` comes first, then `--enable-static --disable-shared` (APPLE) or `--enable-shared` (Linux) — the last flag wins in FFmpeg configure. + +- [ ] **Step 3: Rebuild the FFmpeg dep (slow — several minutes, run in background)** + +The changed CONFIGURE_COMMAND invalidates the ExternalProject stamp, so this re-configures and rebuilds FFmpeg: + +```bash +cmake --build deps/build/arm64 --target dep_FFMPEG +``` + +For a fully clean static-only check (removes the previous shared build tree, which can leave stale `.dylib` files behind in the in-source build): + +```bash +rm -rf deps/build/arm64/dep_FFMPEG-prefix +cmake --build deps/build/arm64 --target dep_FFMPEG +``` + +- [ ] **Step 4: Verify the artifacts** + +```bash +ls deps/build/arm64/dep_FFMPEG-prefix/src/dep_FFMPEG/libavcodec/*.a +ls deps/build/arm64/dep_FFMPEG-prefix/src/dep_FFMPEG/libavcodec/*.dylib 2>/dev/null || echo "OK: no dylibs" +``` + +Expected: `libavcodec.a` present, second command prints `OK: no dylibs`. Check `libavutil` and `libswscale` the same way. + +- [ ] **Step 5: Verify the app still links against the static libs** + +```bash +cmake --build build_arm64 --config RelWithDebInfo --target OrcaSlicer +otool -L build_arm64/src/RelWithDebInfo/OrcaSlicer.app/Contents/MacOS/OrcaSlicer | grep -i "libav" || echo "OK: no dynamic FFmpeg" +``` + +Expected: build succeeds, `OK: no dynamic FFmpeg`. + +- [ ] **Step 6: Commit** + +```bash +git add deps/FFMPEG/FFMPEG.cmake +git commit -m "build: build static-only FFmpeg for macOS deps" +``` + +--- + +### Task 3: Remove the old macOS player + +**Files:** +- Delete: `src/slic3r/GUI/wxMediaCtrl2.mm` +- Delete: `src/slic3r/GUI/BambuPlayer/BambuPlayer.h` (and the empty `BambuPlayer/` dir) +- Modify: `src/slic3r/GUI/wxMediaCtrl2.h` (remove the `#ifdef __WXMAC__` section, lines 22–60) + +**Interfaces:** +- Consumes: Task 1 (macOS no longer references `wxMediaCtrl2` — nothing includes `wxMediaCtrl2.h` on macOS anymore; `wxMediaCtrl2` is never instantiated on any platform). +- Produces: a clean tree where the old BambuPlayer-based player is gone from macOS. The `BambuPlayer` ObjC class itself remains inside the network plugin (external prebuilt binary) — only the GUI-side consumer is removed. + +- [ ] **Step 1: Delete the old player files** + +```bash +git rm src/slic3r/GUI/wxMediaCtrl2.mm +git rm src/slic3r/GUI/BambuPlayer/BambuPlayer.h +rmdir src/slic3r/GUI/BambuPlayer 2>/dev/null || true +``` + +- [ ] **Step 2: Strip the __WXMAC__ section from wxMediaCtrl2.h** + +In `src/slic3r/GUI/wxMediaCtrl2.h`, remove the entire macOS branch of the `#ifdef __WXMAC__` guard — from `#ifdef __WXMAC__` (line 22) through the closing `};` of the mac class (line 60), and the `#else` marker — leaving only the non-mac `class wxMediaCtrl2 : public wxMediaCtrl { ... };` definition followed by the final `#endif /* wxMediaCtrl2_h */`. The resulting file keeps its `#ifndef`/`#endif` include guard pair balanced. + +The file stays on disk because Win/Linux compile `wxMediaCtrl2.cpp`, which includes it. + +- [ ] **Step 3: Grep for leftover references** + +```bash +grep -rn "wxMediaCtrl2.mm\|BambuPlayer/BambuPlayer.h\|BambuPlayer" src/slic3r --include="*.cpp" --include="*.h" --include="*.mm" --include="*.txt" +``` + +Expected: no hits in `src/slic3r/GUI` (ignore `localization/i18n/list.txt:196`, which lists the Win/Linux `wxMediaCtrl2.cpp` and stays). + +- [ ] **Step 4: Rebuild the app** + +```bash +cmake --build build_arm64 --config RelWithDebInfo --target OrcaSlicer +``` + +Expected: configure + compile + link succeed with the deleted files gone. + +- [ ] **Step 5: Commit** + +```bash +git add -A src/slic3r/GUI +git commit -m "refactor: remove old BambuPlayer-based media player from macOS" +``` + +--- + +### Task 4: Runtime verification on hardware + +**Files:** none — manual verification. + +**Interfaces:** consumes all prior tasks. Final gate: the new player must actually stream on a Mac. + +- [ ] **Step 1: Launch the freshly built app** + +```bash +open build_arm64/src/RelWithDebInfo/OrcaSlicer.app +``` + +Expected: app launches normally; no crash in the network/device subsystem. + +- [ ] **Step 2: Load the network plugin and open the Device tab** + +Log in / ensure the network plugin (`libBambuSource.dylib`) loads, select a printer, open the Device tab (camera monitoring panel). + +Expected: the camera preview area shows the idle image initially (no crash — this exercises `wxMediaCtrl3::SetIdleImage` and the `wxImage` load path on macOS for the first time). + +- [ ] **Step 3: Start the stream and watch it render** + +Click play / wait for `MediaPlayCtrl` to start the stream. + +Expected: live video renders in the panel. Check the console/log output (`BOOST_LOG` goes to the terminal if run from it, or check the log file): +- `stat_log ...` lines appear (the `EVT_MEDIA_CTRL_STAT` path is live — proves the Bambu C API dlsym worked from `libBambuSource.dylib`); +- no repeated decode/error messages like `AVVideoDecoder: ...` or `can not find function ...` (proves `StaticBambuLib::get` resolved all Bambu functions); +- Stop/Play toggle works; idle image reappears on stop; +- window resize keeps aspect ratio (exercises `DoSetSize`/`adjust_frame_size`/`paintEvent`). + +- [ ] **Step 4: Confirm the old player is really gone** + +Expected: nothing in the logs references `BambuPlayer` (the ObjC class is no longer dlsym'd); the video path is entirely `wxMediaCtrl3` + `AVVideoDecoder`. + +If a printer is unavailable, at minimum verify Steps 1–2 (launch + idle image) and note in the PR that live-stream verification needs hardware. + +- [ ] **Step 5: Final review pass** + +```bash +git log --oneline -6 +git show --stat HEAD # and each of the three task commits +``` + +Expected: the last 4 commits are the design doc + the 3 implementation tasks (each task commit touches only its listed files). Review the diff for scope: no Linux/Windows changes beyond the two `EVT_MEDIA_CTRL_STAT` lines in Task 1, no `StatusPanel`/`MediaPlayCtrl` changes. diff --git a/docs/superpowers/specs/2026-08-14-ffmpeg-player-macos-design.md b/docs/superpowers/specs/2026-08-14-ffmpeg-player-macos-design.md new file mode 100644 index 0000000000..812a7bf735 --- /dev/null +++ b/docs/superpowers/specs/2026-08-14-ffmpeg-player-macos-design.md @@ -0,0 +1,110 @@ +# FFmpeg Media Player for macOS — Design + +Date: 2026-08-14 +Branch: `dev/ffmpeg-player-macos` + +## Problem + +The branch's new FFmpeg-based media player (`wxMediaCtrl3` + `AVVideoDecoder`) is used on +Windows and Linux, but macOS still runs the old player: `wxMediaCtrl2.mm`, an ObjC +`BambuPlayer` class dlsym'd from the Bambu network plugin that renders via CALayer. +On macOS, `wxMediaCtrl3` is currently aliased to `wxMediaCtrl2` and FFmpeg is not linked +into the app at all. + +Goal: make macOS use the same FFmpeg player as Windows/Linux, linking the **static** +FFmpeg libraries from the deps build instead of dynamic ones. + +## Current state (verified) + +- New player (Win/Linux): `GUI/wxMediaCtrl3.cpp` + `GUI/AVVideoDecoder.cpp`. Decodes with + FFmpeg (libavcodec/libswscale/libavutil), renders frames into `wxImage` (non-Windows) / + `wxBitmap` (Windows) drawn in a `paintEvent`, feeds via the `Bambu_*` C API + (`BambuTunnel.h`, `BAMBU_DYNAMIC`) dlsym'd from the network plugin through + `StaticBambuLib::get()` (`GUI/Printer/PrinterFileSystem.cpp`, compiled on all platforms). +- Old player (macOS): `GUI/wxMediaCtrl2.mm` uses the ObjC `BambuPlayer` class found via + `dlsym(module, "OBJC_CLASS_$_BambuPlayer")` in `libBambuSource.dylib`. +- The macOS network plugin `libBambuSource.dylib` already exports the full Bambu C API + (verified with `nm`), so the new player needs zero plugin changes. +- FFmpeg linking in `src/slic3r/CMakeLists.txt` is guarded by `if (NOT APPLE)` — + macOS currently does not link FFmpeg. +- `deps/FFMPEG/FFMPEG.cmake`: non-MSVC branch builds FFmpeg from source with + `--enable-shared`. The existing arm64 deps build on the dev machine happened to be + configured with both static and shared enabled, so `libavcodec.a` / `libswscale.a` / + `libavutil.a` are already present at + `deps/build/arm64/OrcaSlicer_dep/usr/local/lib/`. +- `EVT_MEDIA_CTRL_STAT` is `wxDEFINE_EVENT`'d in `wxMediaCtrl2.cpp` (Win/Linux) and + `wxMediaCtrl2.mm` (macOS); the define in `wxMediaCtrl3.cpp` is commented out. +- `wxMediaCtrl2` is never instantiated anywhere on any platform — dead code. +- `StatusPanel` already creates `wxMediaCtrl3`; `MediaPlayCtrl` only uses the + `wxMediaCtrl3` interface (`Load/Play/Stop/GetState/GetVideoSize/GetLastError/SetIdleImage`), + so no UI-side changes are needed. + +## Approach (approved) + +**Reuse the shared player on macOS.** Compile the existing `wxMediaCtrl3.cpp` + +`AVVideoDecoder.cpp` on macOS so all three platforms run one implementation. +Rendering uses the existing `wxImage` → `DrawBitmap` paint path, identical to Linux. +Known trade-off: frames are scaled to the widget's logical (1x) size, so Retina is +slightly soft compared to the old CALayer player. Accepted for now; a Retina-aware +scaling follow-up is possible later. + +Rejected alternative: a native CGImage/CALayer renderer for macOS — faster and +Retina-crisp, but adds a second render implementation to maintain. + +## Changes + +### 1. Enable the FFmpeg player on macOS (source) + +- `GUI/wxMediaCtrl3.h`: remove the `#ifdef __WXMAC__` branch (lines 18–22) that aliases + `wxMediaCtrl3` → `wxMediaCtrl2`. macOS then compiles the real `wxMediaCtrl3` class, + including the `BAMBU_DYNAMIC` BambuTunnel path used on Linux. +- Event symbol fix: move `wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent)` into + `wxMediaCtrl3.cpp` (uncomment the existing line) and remove it from + `wxMediaCtrl2.cpp`. One definition total in the lib; all three platforms resolve it. + +### 2. Static FFmpeg linking (deps + app) + +- `deps/FFMPEG/FFMPEG.cmake`: in the non-MSVC branch, pass + `--disable-shared --enable-static` when `APPLE`. Linux keeps `--enable-shared`; + Windows keeps its prebuilt shared DLL zips. Fresh macOS deps builds install only + `libavcodec.a` / `libswscale.a` / `libavutil.a` — no dylibs to bundle, no + rpath/install_name handling. (The existing local arm64 deps build already contains + the `.a` files, so no deps rebuild is strictly needed to try the change locally, + but a fresh CI deps build must produce them.) +- `src/slic3r/CMakeLists.txt`: + - APPLE branch of `SLIC3R_GUI_SOURCES`: add `GUI/wxMediaCtrl3.cpp`, + `GUI/wxMediaCtrl3.h`, `GUI/AVVideoDecoder.cpp`, `GUI/AVVideoDecoder.hpp`; + remove `GUI/wxMediaCtrl2.mm` and `GUI/wxMediaCtrl2.h` (the `.h` stays on + disk for the Win/Linux build of `wxMediaCtrl2.cpp`, but nothing on macOS + includes it after this change). + - Add an APPLE mirror of the `NOT APPLE` FFmpeg block: `find_library` for + `libavcodec.a`, `libswscale.a`, `libavutil.a` under `${CMAKE_PREFIX_PATH}/lib` + with `NO_DEFAULT_PATH`, link them (order avcodec → swscale → avutil), and add + `${CMAKE_PREFIX_PATH}/include` as a SYSTEM include directory. Deps are built with + `--disable-zlib` and no external codecs, so the three static libs link cleanly. + +### 3. Remove the old player + +- Delete `GUI/wxMediaCtrl2.mm` and `GUI/BambuPlayer/BambuPlayer.h` (header used only + by the `.mm`; the real `BambuPlayer` lives inside the network plugin). +- Remove the now-dead `__WXMAC__` section of `GUI/wxMediaCtrl2.h`. +- `wxMediaCtrl2.cpp` (Win/Linux) stays in the build as-is (dead but harmless; out of + scope to remove on this branch). + +### 4. Verification + +- Build on macOS: `cmake --build build_arm64` (or `build/arm64`). +- Confirm no dynamic FFmpeg dependency: `otool -L` on the app binary shows no `libav*` + dylib references. +- Runtime: with the network plugin loaded, the Device tab camera preview streams via + the FFmpeg player (check the device page / `MediaPlayCtrl`). +- macOS `ctest` still passes — static linking means no test-executable `.so` copying + hacks (unlike the Linux shared-lib setup). + +## Out of scope + +- Linux (shared libs, AppImage/flatpak bundling) and Windows (prebuilt DLL zips) + keep their current FFmpeg setup. +- Retina-aware frame scaling / native CGImage rendering (follow-up if visual quality + is judged insufficient). +- Audio streaming (neither player plays audio in this UI path). diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index e83112d989..db62fb4443 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -2762,6 +2762,9 @@ msgstr "" msgid "Merge with" msgstr "" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "" @@ -3039,6 +3042,9 @@ msgstr "" msgid "Merge parts to an object" msgstr "" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + msgid "Add layers" msgstr "" @@ -4452,6 +4458,20 @@ msgstr "" msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "" +#, possible-c-format, possible-boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4483,7 +4503,7 @@ msgid "" "The value will be reset to 0." msgstr "" -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "" msgid "" @@ -4514,13 +4534,13 @@ msgid "" msgstr "" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" @@ -4533,6 +4553,12 @@ msgid "" "No - Disable Arachne Wall Generator and set [Displacement] mode of the Fuzzy Skin" msgstr "" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "" @@ -4784,6 +4810,12 @@ msgstr "" msgid "Calibration error" msgstr "" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + msgid "Network unavailable" msgstr "" @@ -5615,7 +5647,7 @@ msgstr "" msgid "Size:" msgstr "" -#, possible-c-format, possible-boost-format +#, possible-boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "" @@ -5790,6 +5822,9 @@ msgstr "" msgid "Project" msgstr "" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "" @@ -5922,10 +5957,10 @@ msgstr "" msgid "Load a model" msgstr "" -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "" -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "" msgid "Import Configs" @@ -7374,6 +7409,9 @@ msgstr "" msgid "The %s nozzle can not print %s." msgstr "" +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, possible-boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "" @@ -7494,12 +7532,36 @@ msgstr "" msgid "Set filaments to use" msgstr "" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "" msgid "Pellets" msgstr "" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + #, possible-c-format, possible-boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "" @@ -7634,7 +7696,7 @@ msgstr "" msgid "Customized Preset" msgstr "" -msgid "Component name(s) inside step file not in UTF8 format!" +msgid "Component name(s) inside step file not in UTF-8 format!" msgstr "" msgid "Because of unsupported text encoding, garbage characters may appear!" @@ -7656,7 +7718,7 @@ msgstr "" #, possible-c-format, possible-boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" msgid "Object too small" @@ -7671,6 +7733,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "" @@ -7780,20 +7848,20 @@ msgstr "" msgid "Replaced with 3D files from directory:\n" msgstr "" -#, possible-boost-format -msgid "✖ Skipped %1%: same file.\n" +#, possible-c-format, possible-boost-format +msgid "✖ Skipped %s: same file.\n" msgstr "" -#, possible-boost-format -msgid "✖ Skipped %1%: file does not exist.\n" +#, possible-c-format, possible-boost-format +msgid "✖ Skipped %s: file does not exist.\n" msgstr "" -#, possible-boost-format -msgid "✖ Skipped %1%: failed to replace.\n" +#, possible-c-format, possible-boost-format +msgid "✖ Skipped %s: failed to replace.\n" msgstr "" -#, possible-boost-format -msgid "✔ Replaced %1%.\n" +#, possible-c-format, possible-boost-format +msgid "✔ Replaced %s.\n" msgstr "" msgid "Replaced volumes" @@ -7865,6 +7933,18 @@ msgstr "" msgid "Sync now" msgstr "" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "" @@ -8472,6 +8552,15 @@ msgstr "" msgid "Pop up to select filament grouping mode" msgstr "" +msgid "Visible plugin pages" +msgstr "" + +msgid "pages" +msgstr "" + +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "" + msgid "Behaviour" msgstr "" @@ -8797,6 +8886,14 @@ msgstr "" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "" +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + msgid "Experimental Features" msgstr "" @@ -8997,6 +9094,9 @@ msgstr "" msgid "First layer filament sequence" msgstr "" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "" @@ -9052,9 +9152,21 @@ msgstr "" msgid "Preset Inside Project" msgstr "" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + msgid "Detach from parent" msgstr "" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "" @@ -9732,20 +9844,6 @@ msgstr "" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "" - -msgid "Adjust to the set range automatically?\n" -msgstr "" - -msgid "Adjust" -msgstr "" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "" @@ -9931,6 +10029,9 @@ msgstr "" msgid "Setting Overrides" msgstr "" +msgid "Retraction when switching material" +msgstr "" + msgid "Basic information" msgstr "" @@ -10057,6 +10158,12 @@ msgstr "" msgid "Printable space" msgstr "" +msgid "Printer Agent" +msgstr "" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "" + #. TRN: The first argument is the parameter's name; the second argument is its value. #, possible-boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10179,9 +10286,6 @@ msgstr "" msgid "Z-Hop" msgstr "" -msgid "Retraction when switching material" -msgstr "" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -10279,11 +10383,11 @@ msgstr "" msgid "No modifications need to be copied." msgstr "" -msgid "Copy paramters" +msgid "Copy parameters" msgstr "" #, possible-c-format, possible-boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "" #, possible-c-format, possible-boost-format @@ -10757,27 +10861,6 @@ msgstr "" msgid "Please choose the filament colour" msgstr "" -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "" - -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "" - -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "" - -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "" - -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "" - -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "" - -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "" - msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "" @@ -11445,6 +11528,9 @@ msgstr "" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr "" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "" @@ -11457,6 +11543,9 @@ msgstr "" msgid "No extrusions under current settings." msgstr "" +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "" @@ -11493,6 +11582,9 @@ msgstr "" msgid "Variable layer height is not supported with Organic supports." msgstr "" +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "" @@ -11740,9 +11832,6 @@ msgstr "" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "" -msgid "Printer Agent" -msgstr "" - msgid "Select the network agent implementation for printer communication." msgstr "" @@ -11824,7 +11913,7 @@ msgstr "" msgid "Other layers" msgstr "" -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." msgstr "" msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." @@ -12279,9 +12368,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "" -msgid "Brim width" -msgstr "" - msgid "This is the distance from the model to the outermost brim line." msgstr "" @@ -12347,6 +12433,12 @@ msgid "" "0 to deactivate." msgstr "" +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "" @@ -12905,6 +12997,7 @@ msgstr "" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "" +msgctxt "second" msgid "s" msgstr "" @@ -13198,6 +13291,48 @@ msgstr "" msgid "Support material is commonly used to print supports and support interfaces." msgstr "" +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "" @@ -13359,6 +13494,12 @@ msgstr "" msgid "Gyroid" msgstr "" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "" @@ -13839,6 +13980,12 @@ msgstr "" msgid "Klipper" msgstr "" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "" @@ -14333,6 +14480,7 @@ msgstr "" msgid "The allowed maximum output force of Y axis" msgstr "" +msgctxt "Newton" msgid "N" msgstr "" @@ -14342,6 +14490,7 @@ msgstr "" msgid "The machine bed mass load of Y axis" msgstr "" +msgctxt "gram" msgid "g" msgstr "" @@ -14610,7 +14759,7 @@ msgstr "" msgid "Reduce infill retraction" msgstr "" -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." msgstr "" msgid "This option will drop the temperature of the inactive extruders to prevent oozing." @@ -14766,7 +14915,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" @@ -14800,10 +14949,16 @@ msgstr "" msgid "Retraction distance when extruder change" msgstr "" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "" msgid "Z-hop lower boundary" @@ -14893,6 +15048,9 @@ msgstr "" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "" +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "" @@ -15278,6 +15436,12 @@ msgstr "" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "" +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "" @@ -15754,6 +15918,12 @@ msgid "" "Setting a value in the retract amount before wipe setting below will perform any excess retraction before the wipe, else it will be performed after." msgstr "" +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "" @@ -16781,6 +16951,9 @@ msgstr "" msgid "The supplied file couldn't be read because it's empty." msgstr "" +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "" @@ -18111,17 +18284,17 @@ msgstr "" msgid "Only display the filament names with changes to filament presets." msgstr "" -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "" msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" msgid "Please select at least one printer or filament." @@ -18253,9 +18426,6 @@ msgstr "" msgid "Print Host upload" msgstr "" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "" - msgid "Select a Flashforge printer" msgstr "" @@ -18339,7 +18509,7 @@ msgstr "" msgid "We need information for diagnosing source of the issue. Check wiki page for detailed guide." msgstr "" -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "" msgid "Any additional visual examples like images or screen recordings might be helpful while reporting the issue." @@ -18381,7 +18551,7 @@ msgstr "" msgid "Stored logs" msgstr "" -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "" msgid "Profiles" @@ -18446,7 +18616,7 @@ msgstr "" msgid "Authorizing..." msgstr "" -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "" msgid "Could not parse server response." @@ -18884,7 +19054,7 @@ msgstr "" msgid "Enable smart filament assign: Assign one filament to multiple nozzles to maximize savings" msgstr "" -msgid "Fila Saving" +msgid "File Saving" msgstr "" msgid "Don't remind me again" @@ -19087,9 +19257,6 @@ msgstr "" msgid "User canceled." msgstr "" -msgid "Head diameter" -msgstr "" - msgid "Max angle" msgstr "" @@ -19247,6 +19414,9 @@ msgstr "" msgid "NO RAMMING AT ALL" msgstr "" +msgid "s" +msgstr "" + msgid "Volumetric speed" msgstr "" diff --git a/localization/i18n/ca/OrcaSlicer_ca.po b/localization/i18n/ca/OrcaSlicer_ca.po index ab80943ca0..2b42f300ac 100644 --- a/localization/i18n/ca/OrcaSlicer_ca.po +++ b/localization/i18n/ca/OrcaSlicer_ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "PO-Revision-Date: 2025-03-15 10:55+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -3015,6 +3015,10 @@ msgstr "Editar" msgid "Merge with" msgstr "Fusiona amb" +# AI Translated +msgid "Decompose Color" +msgstr "Descompondre el color" + msgid "Delete this filament" msgstr "Elimina aquest filament" @@ -3316,6 +3320,10 @@ msgstr "Muntatge" msgid "Merge parts to an object" msgstr "Fusionar les peces en un objecte" +# AI Translated +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "Utilitzar una alçada de capa variable juntament amb la subcapa de color mixt pot reduir la qualitat de la barreja de colors." + # AI Translated msgid "Add layers" msgstr "Afegir capes" @@ -4828,6 +4836,23 @@ msgstr "La temperatura actual de la cambra és superior a la temperatura segura msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "La temperatura mínima de la cambra (%d℃) és superior a la temperatura objectiu de la cambra (%d℃). El valor mínim és el llindar a partir del qual comença la impressió mentre la cambra continua escalfant-se cap a l'objectiu, de manera que no l'hauria de superar. Es limitarà al valor objectiu." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "L'alçada de capa és massa petita. S'establirà al mínim (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "L'alçada de capa està fora dels límits establerts a Configuració de la Impressora -> Extrusora -> Límits d'alçada de la capa, això pot causar problemes de qualitat d'impressió." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Voleu ajustar-la automàticament al límit (%g mm)?" + +msgid "Adjust" +msgstr "Ajustar" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4876,7 +4901,7 @@ msgstr "" "\n" "El valor es restablirà a 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "La paret addicional alternativa no funciona bé quan assegurar el gruix de la closca vertical està establert a Tot." msgid "" @@ -4922,7 +4947,7 @@ msgstr "" "NO - Mantenir l'alçada de la capa de suport independent" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height ha de ser més petit que layer_height.\n" @@ -4930,7 +4955,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "La profunditat de bloqueig ha de ser menor que la profunditat de la pell.\n" @@ -4948,6 +4973,13 @@ msgstr "" "Sí - Activa el generador de parets Arachne\n" "No - Desactiva el generador de parets Arachne i estableix el mode [Desplaçament] de la pell difusa" +# AI Translated +msgid "Brim ear radius" +msgstr "Radi de l'orella de la Vora d'Adherència" + +msgid "Brim width" +msgstr "Ample de la Vora d'Adherència" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "El mode espiral només funciona quan els bucles de paret són 1, el suport està desactivat, la detecció d'acumulació per sondeig està desactivada, les capes de la coberta superior són 0, la densitat de farciment dispers és 0 i el tipus de timelapse és tradicional." @@ -5202,6 +5234,14 @@ msgstr "No s'ha pogut generar el gcode cali" msgid "Calibration error" msgstr "Error de calibratge" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Aquesta impressora no està configurada amb el maquinari que necessita aquest control." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Aquest control no és compatible amb aquesta impressora." + # AI Translated msgid "Network unavailable" msgstr "Xarxa no disponible" @@ -6067,7 +6107,7 @@ msgstr "Volum:" msgid "Size:" msgstr "Mida:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "S'han trobat conflictes de rutes gcode a la capa %d, Z = %.2lfmm. Si us plau, separeu els objectes conflictius més lluny ( %s <-> %s )." @@ -6248,6 +6288,10 @@ msgstr "Multidispositiu" msgid "Project" msgstr "Projecte" +# AI Translated +msgid "Device (Web)" +msgstr "Dispositiu (Web)" + msgid "Yes" msgstr "Sí" @@ -6383,10 +6427,10 @@ msgstr "Importar 3MF STL/STEP/SVG/OBJ/AMF" msgid "Load a model" msgstr "Carregar un model" -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "Importar fitxer ZIP" -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "Carregar models continguts dins d'un arxiu zip" msgid "Import Configs" @@ -7920,6 +7964,10 @@ msgstr "Personalitzar la placa actual" msgid "The %s nozzle can not print %s." msgstr "El broquet %s no pot imprimir %s." +# AI Translated +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "Imprimir filament de color mixt en una impressora d'un sol extrusor requereix canvis de filament i purgues freqüents, cosa que pot augmentar considerablement el malbaratament i el risc d'obstrucció del broquet o del conducte de residus." + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "No es recomana barrejar %1% amb %2% en la impressió.\n" @@ -8045,6 +8093,26 @@ msgstr "Sincronitzar la llista de filaments des d'AMS" msgid "Set filaments to use" msgstr "Configurar els filaments a utilitzar" +# AI Translated +msgid "Add Mixed Filament" +msgstr "Afegir filament mixt" + +# AI Translated +msgid "Mixed Filament" +msgstr "Filament mixt" + +# AI Translated +msgid "Remove last mixed filament" +msgstr "Eliminar l'últim filament mixt" + +# AI Translated +msgid "Add mixed filament" +msgstr "Afegir filament mixt" + +# AI Translated +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "El filament mixt té components no vàlids o incoherents. Torneu a editar les entrades afectades." + msgid "Search plate, object and part." msgstr "Cercar placa, objecte i peça." @@ -8052,6 +8120,18 @@ msgstr "Cercar placa, objecte i peça." msgid "Pellets" msgstr "Pèl·lets" +# AI Translated +msgid "Mixed filament has broken component references" +msgstr "El filament mixt té referències de components trencades" + +# AI Translated +msgid "Edit / Delete / Merge" +msgstr "Editar / Esborrar / Fusionar" + +# AI Translated +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "El filament mixt de destinació utilitza aquest filament físic com a component. En fusionar-los s'eliminarà aquest filament físic i el filament mixt pot quedar no vàlid. Voleu continuar?" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "En completar l'operació, el projecte %s es tancarà i es crearà un nou projecte." @@ -8196,8 +8276,8 @@ msgid "Customized Preset" msgstr "Perfil personalitzat" # AI Translated -msgid "Component name(s) inside step file not in UTF8 format!" -msgstr "Els noms dels components dins del fitxer STEP no tenen format UTF8!" +msgid "Component name(s) inside step file not in UTF-8 format!" +msgstr "Els noms dels components dins del fitxer STEP no tenen format UTF-8!" # AI Translated msgid "Because of unsupported text encoding, garbage characters may appear!" @@ -8219,10 +8299,10 @@ msgstr "El volum de l'objecte és zero" #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "L'objecte del fitxer %s és massa petit, i potser en metres o polzades.\n" -" Vols escalar a mil·límetres?" +"Vols escalar a mil·límetres?" msgid "Object too small" msgstr "Objecte massa petit" @@ -8239,6 +8319,14 @@ msgstr "" msgid "Multi-part object detected" msgstr "Objecte de múltiples peces detectat" +# AI Translated +msgid "Matching textures to filaments" +msgstr "Associant les textures als filaments" + +# AI Translated +msgid "Texture Import Warning" +msgstr "Avís d'importació de textures" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Carregar aquests fitxers com un sol objecte amb diverses peces?\n" @@ -8361,21 +8449,21 @@ msgstr "No s'ha seleccionat el directori per a la substitució" msgid "Replaced with 3D files from directory:\n" msgstr "Substituït amb fitxers 3D del directori:\n" -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ Omès %1%: mateix fitxer.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ Omès %s: mateix fitxer.\n" -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ Omès %1%: el fitxer no existeix.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ Omès %s: el fitxer no existeix.\n" -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ Omès %1%: la substitució ha fallat.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ Omès %s: la substitució ha fallat.\n" -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔ Substituït %1%.\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔ Substituït %s.\n" msgid "Replaced volumes" msgstr "Volums substituïts" @@ -8452,6 +8540,22 @@ msgstr "" msgid "Sync now" msgstr "Sincronitza ara" +# AI Translated +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "No s'ha pogut importar la textura. El model sembla contenir dades de textura, però el procés d'importació no s'ha pogut completar. El model s'importarà només com a geometria." + +# AI Translated +msgid "Applying texture colors..." +msgstr "Aplicant els colors de textura..." + +# AI Translated +msgid "Updating 3D view..." +msgstr "Actualitzant la vista 3D..." + +# AI Translated +msgid "Texture colors applied." +msgstr "Colors de textura aplicats." + msgid "You can keep the modified presets for the new project or discard them" msgstr "Podeu mantenir els perfils modificats al projecte nou o descartar-los" @@ -9116,6 +9220,18 @@ msgstr "Amb aquesta opció habilitada, podeu enviar una tasca a diversos disposi msgid "Pop up to select filament grouping mode" msgstr "Finestra emergent per seleccionar el mode d'agrupació de filaments" +# AI Translated +msgid "Visible plugin pages" +msgstr "Pàgines de connectors visibles" + +# AI Translated +msgid "pages" +msgstr "pàgines" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Nombre de pàgines de connectors que es mostren com a pestanyes fixes abans que la resta de pàgines es replegui en un desplegable a l'última pestanya." + msgid "Behaviour" msgstr "Comportament" @@ -9506,6 +9622,18 @@ msgstr "Mostrar els perfils no compatibles" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Mostra els perfils incompatibles o no compatibles a les llistes desplegables d'impressora i de filament. Aquests perfils no es poden seleccionar." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Experimental) Utilitza agents d'impressora en lloc d'amfitrions d'impressió" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Envia els treballs d'impressió de les impressores que no són Bambu a través dels agents de connector d'impressora en lloc del flux clàssic de pujada a l'amfitrió d'impressió.\n" +"Quan està desactivat, OrcaSlicer utilitza el comportament antic de l'amfitrió d'impressió." + # AI Translated msgid "Experimental Features" msgstr "Funcions experimentals" @@ -9720,6 +9848,10 @@ msgstr "Gerro en Espiral" msgid "First layer filament sequence" msgstr "Seqüència d'impressió de la primera capa" +# AI Translated +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "La llista de filaments conté filaments mixtos. La seqüència de filaments personalitzada no tindrà efecte." + msgid "By Layer" msgstr "Per Capa" @@ -9776,9 +9908,25 @@ msgstr "Perfil d'usuari" msgid "Preset Inside Project" msgstr "Perfil intern del Projecte" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Copia en aquest perfil tots els valors heretats del perfil pare i elimina la relació d'herència. Els perfils compatibles només amb el perfil pare poden deixar de ser compatibles." + msgid "Detach from parent" msgstr "Desvincula del pare" +# AI Translated +msgid "Unique preset" +msgstr "Perfil únic" + +# AI Translated +msgid "Parent preset" +msgstr "Perfil pare" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Aquest perfil no hereta de cap altre perfil." + msgid "Name is unavailable." msgstr "El nom no està disponible." @@ -10521,22 +10669,6 @@ msgstr "Estàs segur que vols activar aquesta opció?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Els patrons de farciment estan dissenyats normalment per gestionar la rotació automàticament per garantir una impressió correcta i aconseguir els efectes desitjats (p. ex., Gyroid, Cúbic). Rotar el patró de farciment dispers actual pot portar a un suport insuficient. Procediu amb precaució i comproveu minuciosament qualsevol problema d'impressió potencial. Esteu segur que voleu activar aquesta opció?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"L'alçada de la capa és massa petita.\n" -"Es posarà a min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "L'alçada de la capa supera el límit a Configuració de la Impressora -> Extrusora -> Límits d'alçada de la capa, això pot causar problemes de qualitat d'impressió." - -msgid "Adjust to the set range automatically?\n" -msgstr "Voleu ajustar el rang automàticament?\n" - -msgid "Adjust" -msgstr "Ajustar" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Característica experimental: Retreure i tallar el filament a major distància durant els canvis de filaments per minimitzar el flux. Tot i que pot reduir notablement el flux, també pot elevar el risc d'esclops de broquets o altres complicacions d'impressió." @@ -10735,6 +10867,9 @@ msgstr "Trobades paraules clau reservades" msgid "Setting Overrides" msgstr "Anul·lacions de configuració" +msgid "Retraction when switching material" +msgstr "Retracció en canviar de material" + msgid "Basic information" msgstr "Informació bàsica" @@ -10867,6 +11002,12 @@ msgstr "Perfils de processos compatibles" msgid "Printable space" msgstr "Espai imprimible" +msgid "Printer Agent" +msgstr "Agent de la impressora" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Seleccioneu la implementació de l'agent de xarxa per a la comunicació amb la impressora. Els agents disponibles es registren a l'inici." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10997,9 +11138,6 @@ msgstr "Límits d'alçada de capa" msgid "Z-Hop" msgstr "Z-Hop" -msgid "Retraction when switching material" -msgstr "Retracció en canviar de material" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -11123,12 +11261,12 @@ msgid "No modifications need to be copied." msgstr "No cal copiar cap modificació." # AI Translated -msgid "Copy paramters" +msgid "Copy parameters" msgstr "Copiar els paràmetres" # AI Translated #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "Modificar els paràmetres de %s" # AI Translated @@ -11649,29 +11787,6 @@ msgstr "Volums de purga per al canvi de filament" msgid "Please choose the filament colour" msgstr "Trieu el color del filament" -# AI Translated -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "La visualització en directe nativa de Wayland requereix el sink de vídeo GTK de GStreamer. Instal·leu el connector gtksink per a GStreamer i reinicieu l'OrcaSlicer." - -# AI Translated -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "No s'ha pogut inicialitzar el sink de vídeo natiu de GStreamer per a Wayland. Comproveu la instal·lació del connector GTK de GStreamer." - -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "El Windows Media Player és necessari per a aquesta tasca. Voleu habilitar el \"Windows Media Player\" per al vostre sistema operatiu?" - -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "BambuSource no s'ha registrat correctament per a la reproducció multimèdia! Premeu Sí per tornar-lo a registrar. Seràs promocionat dues vegades" - -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "Falta el component BambuSource registrat per a la reproducció multimèdia! Reinstal·leu OrcaSlicer o cerqueu ajuda a la comunitat." - -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "Utilitzar un BambuSource des d'una instal·lació diferent, la reproducció de vídeo pot no funcionar correctament! Premeu Sí per solucionar-ho." - -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "Al vostre sistema li falten còdecs H.264 per al GStreamer, necessaris per reproduir vídeo. (Proveu d'instal·lar els paquets gstreamer1.0-plugins-bad o gstreamer1.0-libav i, a continuació, reinicieu Orca Slicer?)" - # AI Translated msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "L'agent del núvol no està disponible. Reinicieu l'OrcaSlicer i torneu-ho a provar." @@ -12380,6 +12495,10 @@ msgstr " està massa a prop de la zona d'exclusió, i es provocaran col·lisions msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " és massa a prop de l'àrea de detecció d'acumulació i es causaran col·lisions.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " està parcialment fora de l'àrea imprimible, i no es pot imprimir.\n" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Les temperatures de broquet seleccionades són incompatibles. La temperatura de broquet de cada filament ha d'estar dins del rang de temperatura de broquet recomanat dels altres filaments. Altrament, es pot produir una obturació del broquet o danys a la impressora." @@ -12395,6 +12514,10 @@ msgstr "Si tot i així voleu imprimir, podeu activar l'opció a Preferències / msgid "No extrusions under current settings." msgstr "No hi ha extrusions a la configuració actual." +# AI Translated +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "S'està utilitzant un filament mixt amb degradat, però 'Subcapa de color mixt' està desactivat. El degradat no s'imprimirà." + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "El mode suau de timelapse no està permès quan la seqüència \"Per objecte\" està habilitada." @@ -12431,6 +12554,10 @@ msgstr "Potser voleu reduir la mida del model o canviar la configuració d'impre msgid "Variable layer height is not supported with Organic supports." msgstr "Alçada de Capa Variable no és compatible amb suports Orgànics." +# AI Translated +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "El filament de la torre de purga no pot ser un filament mixt." + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "És possible que els diferents diàmetres de broquet i els diferents diàmetres de filament no funcionin bé quan la torre principal està activada. És molt experimental, així que si us plau, procediu amb precaució." @@ -12714,9 +12841,6 @@ msgstr "Utilitzar 3MF en lloc de G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Activeu-ho si la impressora accepta un fitxer 3MF com a treball d'impressió. Quan està activat, Orca Slicer envia el fitxer laminat com a .gcode.3mf, en lloc d'un fitxer .gcode simple." -msgid "Printer Agent" -msgstr "Agent de la impressora" - msgid "Select the network agent implementation for printer communication." msgstr "Seleccioneu la implementació de l'agent de xarxa per a la comunicació amb la impressora." @@ -12801,8 +12925,8 @@ msgstr "mm o %" msgid "Other layers" msgstr "Altres capes" -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." -msgstr "Temperatura del llit per a les capes excepte la primera. Un valor de 0 significa que el filament no admet la impressió sobre el Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgstr "Temperatura del llit de les capes excepte la inicial. Un valor de 0 significa que el filament no admet la impressió sobre el Cool Plate SuperTack." msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." msgstr "Temperatura del llit de les capes excepte la inicial. El valor 0 significa que el filament no admet imprimir a una Base Freda." @@ -13402,9 +13526,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Velocitat dels ponts interns. Si el valor s'expressa com un percentatge, es calcularà en funció de la velocitat del pont (bridge_speed). El valor per defecte és del 150%." -msgid "Brim width" -msgstr "Ample de la Vora d'Adherència" - msgid "This is the distance from the model to the outermost brim line." msgstr "Distància del model a la línia de la Vora d'Adherència més exterior" @@ -13488,6 +13609,14 @@ msgstr "" "La geometria es simplificarà abans de detectar angles pronunciats. Aquest paràmetre indica la longitud mínima de la desviació per a la simplificació.\n" "0 per desactivar" +# AI Translated +msgid "Brim ears outer only" +msgstr "Orelles de la Vora d'Adherència només a l'exterior" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Genera orelles de ratolí només al contorn exterior del model, excloent-ne els forats i les seccions tancades." + msgid "upward compatible machine" msgstr "màquina compatible ascendent" @@ -14191,6 +14320,8 @@ msgstr "Temps de capa" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "S'habilitarà el ventilador de refrigeració de peces per a capes el temps estimat de les quals sigui inferior a aquest valor. La velocitat del ventilador s'interpola entre les velocitats mínima i màxima del ventilador segons el temps d'impressió per capes" +# AI Translated +msgctxt "second" msgid "s" msgstr "s" @@ -14500,6 +14631,62 @@ msgstr "Material de suport" msgid "Support material is commonly used to print supports and support interfaces." msgstr "El material de suport s'utilitza habitualment per imprimir interfície de suport i suport" +# AI Translated +msgid "Is mixed filament" +msgstr "És filament mixt" + +# AI Translated +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "Indica si aquesta ranura de filament és un filament mixt compost per diversos filaments físics" + +# AI Translated +msgid "Mixed filament components" +msgstr "Components del filament mixt" + +# AI Translated +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "Índexs (començant per 1) dels filaments components, separats per comes; p. ex. \"1,3\"" + +# AI Translated +msgid "Mixed filament sublayer ratios" +msgstr "Proporcions de subcapa del filament mixt" + +# AI Translated +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "Valors de proporció separats per comes que sumin 1.0; p. ex. \"0.7,0.3\"" + +# AI Translated +msgid "Mixed filament gradient" +msgstr "Degradat del filament mixt" + +# AI Translated +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "Activa el mode de degradat en direcció Z per a les subcapes del filament mixt. En activar-lo, les proporcions de les subcapes varien linealment entre capes." + +# AI Translated +msgid "Mixed filament gradient range" +msgstr "Rang del degradat del filament mixt" + +# AI Translated +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "Proporcions inicial i final del primer component en el mode de degradat. Parell separat per comes; p. ex. \"0.10,0.90\" significa del 10% al 90%." + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "Corba del degradat del filament mixt" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "Corba personalitzada opcional, a l'estil de Photoshop, que assigna el progrés en Z a la proporció del primer component. Es codifica com a punts de control separats per barres verticals, amb el format \"x,y\" (heretat) o \"x,y,m_in,m_out\" quan cal anul·lar la tangent (un valor buit o \"nan\" fa servir el valor PCHIP predeterminat). x està dins de [0,1]; y es limita al rang de proporcions configurat; p. ex. \"0,0.15|0.5,0.50|1,0.85\". Si es deixa buit, s'utilitza el gradient_range lineal." + +# AI Translated +msgid "Mixed filament per-part gradient" +msgstr "Degradat per peça del filament mixt" + +# AI Translated +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "Quan el mode de degradat està activat, aplica el degradat a cada peça d'un muntatge de manera independent en lloc de tractar tot el muntatge com un únic rang Z." + msgid "Filament printable" msgstr "Filament imprimible" @@ -14679,6 +14866,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Giroide" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Factor de suavitzat del farciment poc dens" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Controla com s'arrodoneixen les cantonades del farciment poc dens. 0% manté el traçat original amb cantonades vives, mentre que 100% produeix les corbes més amples possibles entre línies de farciment adjacents." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Acceleració del farciment superficial superior. L'ús d'un valor inferior pot millorar la qualitat de la superfície superior" @@ -15232,6 +15427,14 @@ msgstr "Amb quin tipus de Codi-G és compatible la impressora." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Omet el bloc de configuració del G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "No escriu el CONFIG_BLOCK (els parells clau/valor de la configuració del laminador) al fitxer G-code. Això pot ajudar amb impressores el microprogramari de les quals falla en analitzar aquestes línies de comentari (p. ex. Anycubic go-klipper). Nota: el fitxer G-code ja no contindrà la configuració del laminador, de manera que en tornar-lo a importar a OrcaSlicer no es restaurarà la configuració." + msgid "Pellet Modded Printer" msgstr "Impressora modificada de pellets" @@ -15772,6 +15975,7 @@ msgid "The allowed maximum output force of Y axis" msgstr "Força de sortida màxima permesa de l'eix Y" # AI Translated +msgctxt "Newton" msgid "N" msgstr "N" @@ -15784,6 +15988,7 @@ msgid "The machine bed mass load of Y axis" msgstr "Càrrega de massa del llit de la màquina a l'eix Y" # AI Translated +msgctxt "gram" msgid "g" msgstr "g" @@ -16109,7 +16314,7 @@ msgid "Reduce infill retraction" msgstr "Reduir la retracció de farciment" # AI Translated -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." msgstr "No retrau quan el desplaçament està totalment a la zona de farciment. Això vol dir que l'oozing( goteig ) queda amagat. Això pot reduir els temps de retracció per a models complexos i estalviar temps d'impressió, però fer que el laminat i la generació de Codi-G siguin més lents. Tingueu en compte que el salt en Z tampoc es realitza a les zones on s'omet la retracció." msgid "This option will drop the temperature of the inactive extruders to prevent oozing." @@ -16285,7 +16490,7 @@ msgstr "Quantitat de retracció després de netejar" # AI Translated #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" "Longitud de la retracció ràpida després de netejar, relativa a la longitud de retracció.\n" @@ -16321,10 +16526,18 @@ msgstr "Retracció llarga al canviar d'extrusor" msgid "Retraction distance when extruder change" msgstr "Distància de retracció al canviar d'extrusor" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Longitud de retracció (Canvi d'eina)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Quan s'activa la retracció abans d'un canvi d'eina, el filament es retira la quantitat especificada (la longitud es mesura sobre el filament en brut, abans d'entrar a l'extrusor)." + msgid "Z-hop height" msgstr "Alçada Z-hop" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "Cade vegada que es fa una retracció, s'aixeca una mica el broquet per crear espai lliure entre el broquet i la impressió. Evita que el broquet colpegi la impressió en els desplaçaments. L'ús de la línia espiral per aixecar z pot evitar l'aparició de fils" msgid "Z-hop lower boundary" @@ -16419,6 +16632,10 @@ msgstr "Longitud addicional en reiniciar" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Quan la retracció es compensa després d'un desplaçament, l'extrusor introduirà una quantitat addicional de filament. Aquest ajustament rarament es necessita." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Longitud addicional en reiniciar (Canvi d'eina)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Quan la retracció es compensa després d'un canvi d'eina, l'extrusor introduirà una quantitat addicional de filament." @@ -16835,6 +17052,14 @@ msgstr "Canvi d'eina a la Torre de Purga" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Força el capçal a desplaçar-se a la Torre de Purga abans d'emetre l'ordre de canvi d'eina (Tx). Només és rellevant per a impressores multiextrusor (multicapçal) que utilitzen una Torre de Purga de tipus 2. Per defecte, Orca omet aquest desplaçament en màquines multicapçal perquè el firmware gestiona el canvi de capçal, cosa que pot fer que l'ordre Tx s'emeti sobre la peça impresa. Activeu aquesta opció si voleu que el canvi d'eina s'emeti sempre sobre la Torre de Purga." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Espera la temperatura a la Torre de Purga" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Recull la nova eina sense esperar que arribi a la temperatura d'impressió, es desplaça a la Torre de Purga i hi espera la temperatura, just abans de purgar. El degoteig de l'escalfament cau sobre la torre en lloc del model, i el desplaçament se solapa amb l'escalfament. Només és rellevant per a impressores multiextrusor (multicapçal) que utilitzen una Torre de Purga de tipus 2. El microprogramari o la macro de canvi d'eina no han d'esperar la temperatura pel seu compte. Quan està desactivat, l'espera de temperatura s'emet just després de l'ordre de canvi d'eina." + msgid "No sparse layers (beta)" msgstr "Sense capes poc denses( beta )" @@ -17374,6 +17599,14 @@ msgstr "" "\n" "L'establiment d'un valor en la quantitat de retractació abans de l'esborrat es realitzarà qualsevol retracció en excés abans de la neteja, sinó es realitzarà després." +# AI Translated +msgid "Mixed color sublayer" +msgstr "Subcapa de color mixt" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "Activa la divisió en subcapes de color mixt. En activar-la, les capes que contenen filaments de color mixt es divideixen en subcapes per aconseguir efectes de barreja de colors." + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "La Torre de Purga es pot utilitzar per netejar els residus al broquet i estabilitzar la pressió de la cambra dins del broquet, per tal d'evitar defectes d'aparença en imprimir objectes." @@ -18465,6 +18698,10 @@ msgstr "La generació de malla del fitxer del model ha fallat o la forma no és msgid "The supplied file couldn't be read because it's empty." msgstr "El fitxer subministrat no s'ha pogut llegir perquè està buit" +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "El format del fitxer és incompatible i no es pot analitzar." + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Format de fitxer desconegut. El fitxer d'entrada ha de tenir extensió .stl, .obj, .amf( .xml )." @@ -19963,19 +20200,19 @@ msgstr "Mostrar només els noms de les impressores amb canvis als perfils d'impr msgid "Only display the filament names with changes to filament presets." msgstr "Mostrar només els noms dels filaments amb canvis en els perfils de filament." -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "Només es mostraran els noms de les impressores amb perfils d'impressora d'usuari i cada perfil que trieu s'exportarà com a fitxer zip." msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" "Només es mostraran els noms dels filaments amb perfils de filament d'usuari, \n" "i tots els perfils de filament d'usuari de cada nom de filament que seleccioneu s'exportaran com a zip." msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" "Només es mostraran els noms de les impressores amb perfils de processament modificats, \n" "I tots els valors perfils del procés d'usuari de cada nom d'impressora que seleccioneu s'exportaran com a ZIP." @@ -20121,9 +20358,6 @@ msgstr "Impressora Física" msgid "Print Host upload" msgstr "Pujada al amfitrió( host ) d'impressió" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Seleccioneu la implementació de l'agent de xarxa per a la comunicació amb la impressora. Els agents disponibles es registren a l'inici." - # AI Translated msgid "Select a Flashforge printer" msgstr "Seleccioneu una impressora Flashforge" @@ -20223,7 +20457,7 @@ msgid "We need information for diagnosing source of the issue. Check wiki page f msgstr "Necessitem informació per diagnosticar l'origen del problema. Consulteu la pàgina wiki per a una guia detallada." # AI Translated -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "El botó Empaquetar recull el fitxer del projecte i els registres de la sessió actual en un fitxer zip." # AI Translated @@ -20279,7 +20513,7 @@ msgid "Stored logs" msgstr "Registres emmagatzemats" # AI Translated -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "Empaqueta tots els registres emmagatzemats en un fitxer zip." # AI Translated @@ -20367,7 +20601,7 @@ msgid "Authorizing..." msgstr "Autoritzant..." # AI Translated -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "Error. No es pot obtenir el testimoni d'api per a l'autorització" # AI Translated @@ -20823,8 +21057,8 @@ msgid "Enable smart filament assign: Assign one filament to multiple nozzles to msgstr "Activar l'assignació intel·ligent de filament: assigna un filament a diversos broquets per maximitzar l'estalvi" # AI Translated -msgid "Fila Saving" -msgstr "Estalvi de filament" +msgid "File Saving" +msgstr "Desament de fitxers" msgid "Don't remind me again" msgstr "No m'ho recordis més" @@ -21066,9 +21300,6 @@ msgstr "Alguna cosa inesperada ha passat en intentar iniciar sessió, torneu-ho msgid "User canceled." msgstr "Usuari cancel·lat." -msgid "Head diameter" -msgstr "Diàmetre del cap" - msgid "Max angle" msgstr "Angle màxim" @@ -21236,6 +21467,9 @@ msgstr "Reinicia ara" msgid "NO RAMMING AT ALL" msgstr "SENSE EMPENTA" +msgid "s" +msgstr "s" + msgid "Volumetric speed" msgstr "Velocitat volumètrica" @@ -21887,6 +22121,57 @@ msgstr "" "Evitar la deformació( warping )\n" "Sabíeu que quan imprimiu materials propensos a deformar-se, com ara l'ABS, augmentar adequadament la temperatura del llit pot reduir la probabilitat de deformació?" +# AI Translated +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "La visualització en directe nativa de Wayland requereix el sink de vídeo GTK de GStreamer. Instal·leu el connector gtksink per a GStreamer i reinicieu l'OrcaSlicer." + +# AI Translated +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "No s'ha pogut inicialitzar el sink de vídeo natiu de GStreamer per a Wayland. Comproveu la instal·lació del connector GTK de GStreamer." + +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "El Windows Media Player és necessari per a aquesta tasca. Voleu habilitar el \"Windows Media Player\" per al vostre sistema operatiu?" + +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "BambuSource no s'ha registrat correctament per a la reproducció multimèdia! Premeu Sí per tornar-lo a registrar. Seràs promocionat dues vegades" + +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "Falta el component BambuSource registrat per a la reproducció multimèdia! Reinstal·leu OrcaSlicer o cerqueu ajuda a la comunitat." + +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "Utilitzar un BambuSource des d'una instal·lació diferent, la reproducció de vídeo pot no funcionar correctament! Premeu Sí per solucionar-ho." + +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "Al vostre sistema li falten còdecs H.264 per al GStreamer, necessaris per reproduir vídeo. (Proveu d'instal·lar els paquets gstreamer1.0-plugins-bad o gstreamer1.0-libav i, a continuació, reinicieu Orca Slicer?)" + +# AI Translated +#~ msgid "N" +#~ msgstr "N" + +# AI Translated +#~ msgid "g" +#~ msgstr "g" + +# AI Translated +#~ msgid "Fila Saving" +#~ msgstr "Estalvi de filament" + +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "L'alçada de la capa és massa petita.\n" +#~ "Es posarà a min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "L'alçada de la capa supera el límit a Configuració de la Impressora -> Extrusora -> Límits d'alçada de la capa, això pot causar problemes de qualitat d'impressió." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Voleu ajustar el rang automàticament?\n" + +#~ msgid "Head diameter" +#~ msgstr "Diàmetre del cap" + #~ msgid "Print order within a single layer." #~ msgstr "Ordre d'impressió dins d'una sola capa" diff --git a/localization/i18n/cs/OrcaSlicer_cs.po b/localization/i18n/cs/OrcaSlicer_cs.po index 63b69765d7..d2ef18cd2a 100644 --- a/localization/i18n/cs/OrcaSlicer_cs.po +++ b/localization/i18n/cs/OrcaSlicer_cs.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "PO-Revision-Date: \n" "Last-Translator: Jakub Hencl\n" "Language-Team: \n" @@ -2977,6 +2977,10 @@ msgstr "Upravit" msgid "Merge with" msgstr "Sloučit s" +# AI Translated +msgid "Decompose Color" +msgstr "Rozložit barvu" + msgid "Delete this filament" msgstr "Smazat tento filament" @@ -3279,6 +3283,10 @@ msgstr "Sestava" msgid "Merge parts to an object" msgstr "Sloučit části do objektu" +# AI Translated +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "Použití proměnné výšky vrstvy společně s podvrstvou míchané barvy může zhoršit kvalitu míchání barev." + # AI Translated msgid "Add layers" msgstr "Přidat vrstvy" @@ -4786,6 +4794,23 @@ msgstr "Aktuální teplota komory je vyšší než bezpečná teplota materiálu msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Minimální teplota komory (%d℃) je vyšší než cílová teplota komory (%d℃). Minimální hodnota je práh, při kterém tisk začíná, zatímco se komora dále ohřívá k cílové teplotě, takže by ji neměla překročit. Bude omezena na cílovou hodnotu." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "Výška vrstvy je příliš malá. Bude nastavena na minimum (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "Výška vrstvy je mimo limity nastavené v Nastavení tiskárny -> Extruder -> Omezení výšky vrstvy, což může způsobit problémy s kvalitou tisku." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Upravit ji automaticky na limit (%g mm)?" + +msgid "Adjust" +msgstr "Upravit" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4834,7 +4859,7 @@ msgstr "" "\n" "Hodnota bude nastavena na 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Střídavá přídavná stěna nefunguje správně, pokud je zajištění tloušťky svislé stěny nastaveno na Vše." msgid "" @@ -4880,7 +4905,7 @@ msgstr "" "NE – Ponechat nezávislou výšku podpůrné vrstvy" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height musí být menší než layer_height.\n" @@ -4888,7 +4913,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "Hloubka zamčení musí být menší než hloubka krycí vrstvy.\n" @@ -4906,6 +4931,13 @@ msgstr "" "Ano – povolit Arachne Wall Generator\n" "Ne – zakázat Arachne Wall Generator a nastavit režim [Displacement] pro Fuzzy Skin" +# AI Translated +msgid "Brim ear radius" +msgstr "Poloměr ouška límce" + +msgid "Brim width" +msgstr "Šířka límce" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Spirálový režim funguje pouze tehdy, když je počet smyček stěny 1, podpěry jsou vypnuté, detekce usazenin sondováním je vypnutá, počet horních plných vrstev je 0, hustota řídké výplně je 0 a typ časosběru je tradiční." @@ -5160,6 +5192,14 @@ msgstr "Nepodařilo se vygenerovat kalibrační G-code." msgid "Calibration error" msgstr "Chyba kalibrace" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Tato tiskárna nemá nakonfigurovaný hardware, který tento ovládací prvek vyžaduje." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Tento ovládací prvek není na této tiskárně podporován." + # AI Translated msgid "Network unavailable" msgstr "Síť není dostupná" @@ -6029,7 +6069,7 @@ msgstr "Objem:" msgid "Size:" msgstr "Velikost:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Byly nalezeny konflikty drah G-kódu ve vrstvě %d, Z = %.2lf mm. Oddělte prosím konfliktní objekty více od sebe (%s <-> %s)." @@ -6210,6 +6250,10 @@ msgstr "Více zařízení" msgid "Project" msgstr "Projekt" +# AI Translated +msgid "Device (Web)" +msgstr "Zařízení (Web)" + msgid "Yes" msgstr "Ano" @@ -6344,10 +6388,10 @@ msgstr "Importovat 3MF/STL/STEP/SVG/OBJ/AMF" msgid "Load a model" msgstr "Načíst model" -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "Importovat ZIP archiv" -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "Načíst modely obsažené v zip archivu" msgid "Import Configs" @@ -7885,6 +7929,10 @@ msgstr "Přizpůsobit aktuální desku" msgid "The %s nozzle can not print %s." msgstr "Tryska %s nemůže tisknout %s." +# AI Translated +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "Tisk filamentu s míchanou barvou na tiskárně s jedním extruderem vyžaduje časté výměny filamentu a čištění, což může výrazně zvýšit množství odpadu a riziko ucpání trysky nebo odpadního skluzu." + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "Míchání %1% s %2% při tisku není doporučeno.\n" @@ -8014,12 +8062,44 @@ msgstr "Synchronizovat seznam filamentů z AMS" msgid "Set filaments to use" msgstr "Nastavit používané filamenty" +# AI Translated +msgid "Add Mixed Filament" +msgstr "Přidat míchaný filament" + +# AI Translated +msgid "Mixed Filament" +msgstr "Míchaný filament" + +# AI Translated +msgid "Remove last mixed filament" +msgstr "Odebrat poslední míchaný filament" + +# AI Translated +msgid "Add mixed filament" +msgstr "Přidat míchaný filament" + +# AI Translated +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "Míchaný filament má neplatné nebo neodpovídající komponenty. Upravte prosím dotčené položky znovu." + msgid "Search plate, object and part." msgstr "Hledat desku, objekt a díl." msgid "Pellets" msgstr "Pelety" +# AI Translated +msgid "Mixed filament has broken component references" +msgstr "Míchaný filament má poškozené odkazy na komponenty" + +# AI Translated +msgid "Edit / Delete / Merge" +msgstr "Upravit / Smazat / Sloučit" + +# AI Translated +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "Cílový míchaný filament používá tento fyzický filament jako komponentu. Sloučením se tento fyzický filament odstraní a míchaný filament může přestat být platný. Pokračovat?" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "Po dokončení operace bude projekt %s uzavřen a bude vytvořen nový projekt." @@ -8157,7 +8237,7 @@ msgstr "Potvrďte prosím, že je G-code v těchto předvolbách bezpečný, aby msgid "Customized Preset" msgstr "Přizpůsobená předvolba" -msgid "Component name(s) inside step file not in UTF8 format!" +msgid "Component name(s) inside step file not in UTF-8 format!" msgstr "Názvy komponent v souboru STEP nejsou ve formátu UTF-8!" # AI Translated @@ -8180,7 +8260,7 @@ msgstr "Objem objektu je nula." #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "Objekt ze souboru %s je příliš malý a může být v metrech nebo palcích.\n" "Chcete převést měřítko na milimetry?" @@ -8201,6 +8281,14 @@ msgstr "" msgid "Multi-part object detected" msgstr "Detekován vícedílný objekt" +# AI Translated +msgid "Matching textures to filaments" +msgstr "Přiřazování textur k filamentům" + +# AI Translated +msgid "Texture Import Warning" +msgstr "Upozornění při importu textury" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Načíst tyto soubory jako jeden objekt s více částmi?\n" @@ -8320,21 +8408,21 @@ msgstr "Nebyla vybrána složka pro nahrazení" msgid "Replaced with 3D files from directory:\n" msgstr "Nahrazeno 3D soubory ze složky:\n" -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ Přeskočeno %1%: stejný soubor.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ Přeskočeno %s: stejný soubor.\n" -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ Přeskočeno %1%: soubor neexistuje.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ Přeskočeno %s: soubor neexistuje.\n" -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ Přeskočeno %1%: nahrazení se nezdařilo.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ Přeskočeno %s: nahrazení se nezdařilo.\n" -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔ Nahrazeno %1%.\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔ Nahrazeno %s.\n" msgid "Replaced volumes" msgstr "Nahrazené objemy" @@ -8411,6 +8499,22 @@ msgstr "" msgid "Sync now" msgstr "Synchronizovat nyní" +# AI Translated +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "Import textury se nezdařil. Zdá se, že model obsahuje data textury, ale proces importu nebylo možné dokončit. Model bude importován pouze jako geometrie." + +# AI Translated +msgid "Applying texture colors..." +msgstr "Aplikují se barvy textury..." + +# AI Translated +msgid "Updating 3D view..." +msgstr "Aktualizuje se 3D náhled..." + +# AI Translated +msgid "Texture colors applied." +msgstr "Barvy textury byly použity." + msgid "You can keep the modified presets for the new project or discard them" msgstr "Upravené předvolby můžete ponechat v novém projektu nebo je zahodit" @@ -9070,6 +9174,18 @@ msgstr "Pokud je tato volba povolena, můžete odeslat úlohu na více zařízen msgid "Pop up to select filament grouping mode" msgstr "Zobrazit dialog pro výběr režimu seskupení filamentů" +# AI Translated +msgid "Visible plugin pages" +msgstr "Viditelné stránky pluginů" + +# AI Translated +msgid "pages" +msgstr "stránek" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Počet stránek pluginů zobrazených jako pevné karty, než se zbývající stránky sbalí do rozbalovací nabídky na poslední kartě." + msgid "Behaviour" msgstr "Chování" @@ -9457,6 +9573,18 @@ msgstr "Zobrazit nepodporované předvolby" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Zobrazovat nekompatibilní/nepodporované předvolby v rozevíracích seznamech tiskáren a filamentů. Tyto předvolby nelze vybrat." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Experimentální) Používat agenty tiskárny místo tiskových hostů" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Směruje tiskové úlohy pro tiskárny jiné než Bambu přes agenty pluginů tiskárny místo klasického nahrávání na tiskový host.\n" +"Pokud je vypnuto, OrcaSlicer používá původní chování tiskového hosta." + # AI Translated msgid "Experimental Features" msgstr "Experimentální funkce" @@ -9668,6 +9796,10 @@ msgstr "Spirálová váza" msgid "First layer filament sequence" msgstr "Pořadí filamentů v první vrstvě" +# AI Translated +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "Seznam filamentů obsahuje míchané filamenty. Vlastní pořadí filamentů se neuplatní." + msgid "By Layer" msgstr "Podle vrstvy" @@ -9724,10 +9856,26 @@ msgstr "Uživatelská předvolba" msgid "Preset Inside Project" msgstr "Předvolba v projektu" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Zkopíruje do této předvolby všechny hodnoty zděděné z nadřazené předvolby a odstraní vztah dědičnosti. Předvolby kompatibilní pouze s nadřazenou předvolbou mohou přestat být podporovány." + # AI Translated msgid "Detach from parent" msgstr "Oddělit od nadřazeného" +# AI Translated +msgid "Unique preset" +msgstr "Samostatná předvolba" + +# AI Translated +msgid "Parent preset" +msgstr "Nadřazená předvolba" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Tato předvolba nedědí z jiné předvolby." + msgid "Name is unavailable." msgstr "Název není k dispozici." @@ -10469,22 +10617,6 @@ msgstr "Opravdu chcete tuto možnost povolit?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Vzory výplně jsou obvykle navrženy tak, aby automaticky pracovaly s rotací a zajistily správný tisk i zamýšlený efekt (např. Gyroid, Cubic). Otočení aktuální řídké výplně může vést k nedostatečné opoře. Postupujte opatrně a pečlivě zkontrolujte možné problémy při tisku. Opravdu chcete tuto možnost povolit?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Výška vrstvy je příliš malá.\n" -"Bude nastavena na min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Výška vrstvy přesahuje limit v Nastavení tiskárny -> Extruder -> Omezení výšky vrstvy, což může způsobit problémy s kvalitou tisku." - -msgid "Adjust to the set range automatically?\n" -msgstr "Automaticky upravit do nastaveného rozsahu?\n" - -msgid "Adjust" -msgstr "Upravit" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Experimentální funkce: Stažení a odstřižení filamentu na větší vzdálenost během výměny filamentu pro minimalizaci purge. Ačkoliv to může výrazně snížit purge, může to také zvýšit riziko ucpání trysky nebo jiných komplikací při tisku." @@ -10684,6 +10816,9 @@ msgstr "Byla nalezena rezervovaná klíčová slova" msgid "Setting Overrides" msgstr "Přepisování nastavení" +msgid "Retraction when switching material" +msgstr "Retrakce při změně materiálu" + msgid "Basic information" msgstr "Základní informace" @@ -10816,6 +10951,13 @@ msgstr "Kompatibilní procesní profily" msgid "Printable space" msgstr "Tisknutelný prostor" +# AI Translated +msgid "Printer Agent" +msgstr "Agent tiskárny" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Vyberte implementaci síťového agenta pro komunikaci s tiskárnou. Dostupní agenti jsou registrováni při spuštění." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10943,9 +11085,6 @@ msgstr "Omezení výšky vrstvy" msgid "Z-Hop" msgstr "Z-Hop" -msgid "Retraction when switching material" -msgstr "Retrakce při změně materiálu" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -11066,12 +11205,12 @@ msgid "No modifications need to be copied." msgstr "Není třeba kopírovat žádné úpravy." # AI Translated -msgid "Copy paramters" +msgid "Copy parameters" msgstr "Kopírovat parametry" # AI Translated #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "Upravit parametry %s" # AI Translated @@ -11630,34 +11769,6 @@ msgstr "Objemy čištění při výměně filamentu" msgid "Please choose the filament colour" msgstr "Vyberte prosím barvu filamentu" -# AI Translated -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "Nativní živý náhled ve Waylandu vyžaduje video sink GStreamer GTK. Nainstalujte prosím plugin gtksink pro GStreamer a poté restartujte OrcaSlicer." - -# AI Translated -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "Nepodařilo se inicializovat nativní video sink GStreamer pro Wayland. Zkontrolujte prosím instalaci pluginu GStreamer GTK." - -# AI Translated -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "Pro tuto úlohu je vyžadován Windows Media Player! Chcete ve svém operačním systému povolit „Windows Media Player“?" - -# AI Translated -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "BambuSource nebyl správně zaregistrován pro přehrávání médií! Stisknutím Ano jej znovu zaregistrujete. Budete vyzváni dvakrát" - -# AI Translated -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "Chybí komponenta BambuSource registrovaná pro přehrávání médií! Přeinstalujte prosím OrcaSlicer nebo požádejte o pomoc komunitu." - -# AI Translated -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "Používá se BambuSource z jiné instalace, přehrávání videa nemusí fungovat správně! Stisknutím Ano to opravíte." - -# AI Translated -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "Ve vašem systému chybí kodeky H.264 pro GStreamer, které jsou nutné k přehrávání videa. (Zkuste nainstalovat balíčky gstreamer1.0-plugins-bad nebo gstreamer1.0-libav a poté restartovat Orca Slicer?)" - # AI Translated msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "Cloudový agent není dostupný. Restartujte prosím OrcaSlicer a zkuste to znovu." @@ -12363,6 +12474,10 @@ msgstr " je příliš blízko oblasti vyloučení a může způsobit kolize.\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " je příliš blízko oblasti detekce shlukování a dojde ke kolizi.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " je částečně mimo tisknutelnou oblast a nelze jej vytisknout.\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Vybrané teploty trysky nejsou kompatibilní. Teplota trysky každého filamentu musí spadat do doporučeného rozsahu teplot ostatních filamentů. Jinak může dojít k ucpání trysky nebo poškození tiskárny." @@ -12375,6 +12490,10 @@ msgstr "Pokud chcete přesto tisknout, můžete povolit možnost v Nastavení / msgid "No extrusions under current settings." msgstr "Při aktuálním nastavení nejsou žádné extruze." +# AI Translated +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "Je použit míchaný filament s gradientem, ale 'Podvrstva míchané barvy' je vypnutá. Gradient nebude vytištěn." + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Plynulý režim časosběru není podporován, pokud je povoleno pořadí tisku „podle objektu“." @@ -12411,6 +12530,10 @@ msgstr "Možná budete chtít zmenšit velikost modelu nebo změnit aktuální n msgid "Variable layer height is not supported with Organic supports." msgstr "Proměnná výška vrstvy není podporována s organickými podporami." +# AI Translated +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "Filament věže na očištění trysky nemůže být míchaný filament." + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Různé průměry trysek a filamentu nemusí správně fungovat, pokud je povolena základní věž. Jedná se o velmi experimentální funkci, proto pokračujte opatrně." @@ -12696,10 +12819,6 @@ msgstr "Použít 3MF místo G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Zapněte, pokud tiskárna přijímá jako tiskovou úlohu soubor 3MF. Je-li zapnuto, odešle Orca Slicer slicovaný soubor jako .gcode.3mf místo prostého souboru .gcode." -# AI Translated -msgid "Printer Agent" -msgstr "Agent tiskárny" - # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Vyberte implementaci síťového agenta pro komunikaci s tiskárnou." @@ -12786,7 +12905,7 @@ msgstr "mm nebo %" msgid "Other layers" msgstr "Ostatní vrstvy" -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." msgstr "Teplota desky pro vrstvy kromě počáteční. Hodnota 0 znamená, že filament nepodporuje tisk na chladicí podložce SuperTack." msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." @@ -13387,9 +13506,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Rychlost vnitřních mostů. Pokud je hodnota zadána v procentech, vypočítá se podle bridge_speed. Výchozí hodnota je 150 %." -msgid "Brim width" -msgstr "Šířka límce" - msgid "This is the distance from the model to the outermost brim line." msgstr "Vzdálenost od modelu k nejvzdálenější brim linii." @@ -13470,6 +13586,14 @@ msgstr "" "Geometrie bude decimována před detekcí ostrých úhlů. Tento parametr určuje minimální délku odchylky pro decimaci.\n" "0 pro deaktivaci." +# AI Translated +msgid "Brim ears outer only" +msgstr "Ouška límce pouze na vnějším obrysu" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Vytvoří myší ouška pouze na vnějším obrysu modelu, bez otvorů a uzavřených částí." + msgid "upward compatible machine" msgstr "stroj zpětně kompatibilní" @@ -14160,6 +14284,8 @@ msgstr "Čas vrstvy" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "Ventilátor chlazení části bude spuštěn u vrstev, jejichž odhadovaný čas je kratší než tato hodnota. Rychlost ventilátoru je interpolována mezi minimální a maximální podle času tisku vrstvy." +# AI Translated +msgctxt "second" msgid "s" msgstr "s" @@ -14468,6 +14594,62 @@ msgstr "Podpůrný materiál" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Podpůrný materiál se běžně používá pro tisk podpěr a rozhraní podpěr." +# AI Translated +msgid "Is mixed filament" +msgstr "Je míchaný filament" + +# AI Translated +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "Určuje, zda je tato pozice filamentu míchaným filamentem složeným z několika fyzických filamentů" + +# AI Translated +msgid "Mixed filament components" +msgstr "Komponenty míchaného filamentu" + +# AI Translated +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "Čárkami oddělené indexy komponentních filamentů počítané od 1, např. \"1,3\"" + +# AI Translated +msgid "Mixed filament sublayer ratios" +msgstr "Poměry podvrstev míchaného filamentu" + +# AI Translated +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "Čárkami oddělené hodnoty poměrů, jejichž součet je 1.0, např. \"0.7,0.3\"" + +# AI Translated +msgid "Mixed filament gradient" +msgstr "Gradient míchaného filamentu" + +# AI Translated +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "Zapne režim gradientu ve směru Z pro podvrstvy míchaného filamentu. Je-li zapnutý, poměry podvrstev se mění lineárně napříč vrstvami." + +# AI Translated +msgid "Mixed filament gradient range" +msgstr "Rozsah gradientu míchaného filamentu" + +# AI Translated +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "Počáteční a koncový poměr první komponenty v režimu gradientu. Dvojice oddělená čárkou, např. \"0.10,0.90\" znamená 10% až 90%." + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "Křivka gradientu míchaného filamentu" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "Volitelná vlastní křivka ve stylu Photoshopu, která mapuje postup v ose Z na poměr první komponenty. Zapisuje se jako řídicí body oddělené svislými čarami, buď ve tvaru \"x,y\" (starší formát), nebo \"x,y,m_in,m_out\", pokud je potřeba přepsat tečnu (prázdná hodnota nebo \"nan\" použije výchozí PCHIP). x je v intervalu [0,1]; y je omezeno na nastavený rozsah poměrů, např. \"0,0.15|0.5,0.50|1,0.85\". Pokud je pole prázdné, použije se lineární gradient_range." + +# AI Translated +msgid "Mixed filament per-part gradient" +msgstr "Gradient míchaného filamentu podle dílu" + +# AI Translated +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "Je-li zapnutý režim gradientu, aplikuje gradient na každý díl sestavy samostatně místo toho, aby se celá sestava brala jako jeden rozsah Z." + msgid "Filament printable" msgstr "Tisknutelný filament" @@ -14646,6 +14828,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroid" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Faktor vyhlazení řídké výplně" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Určuje, jak silně se zaoblují rohy řídké výplně. 0% zachová původní ostrou dráhu, zatímco 100% vytvoří největší možné křivky mezi sousedními liniemi výplně." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Akcelerace výplně horní plochy. Použití nižší hodnoty může zlepšit kvalitu horní plochy." @@ -15198,6 +15388,14 @@ msgstr "Jaký typ G-code je s tiskárnou kompatibilní." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Vynechat konfigurační blok G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Nezapisuje CONFIG_BLOCK (dvojice klíč/hodnota s konfigurací sliceru) do souboru G-code. Může to pomoci u tiskáren, jejichž firmware při zpracování těchto řádků s komentáři havaruje (např. Anycubic go-klipper). Poznámka: soubor G-code již nebude obsahovat nastavení sliceru, takže jeho opětovný import do OrcaSlicer konfiguraci neobnoví." + msgid "Pellet Modded Printer" msgstr "Tiskárna na pelety" @@ -15728,6 +15926,7 @@ msgid "The allowed maximum output force of Y axis" msgstr "Povolená maximální výstupní síla osy Y" # AI Translated +msgctxt "Newton" msgid "N" msgstr "N" @@ -15740,6 +15939,7 @@ msgid "The machine bed mass load of Y axis" msgstr "Zatížení osy Y hmotností podložky stroje" # AI Translated +msgctxt "gram" msgid "g" msgstr "g" @@ -16055,8 +16255,8 @@ msgstr "Počáteční a koncové body, které vedou z oblasti řezače do odpadk msgid "Reduce infill retraction" msgstr "Snížit retrakci při výplni" -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." -msgstr "Neprovádět retrakci, pokud se přejezd nachází zcela uvnitř oblasti výplně. Případné vytékání materiálu tak nebude viditelné. To může snížit počet retrakčních pohybů u složitých modelů a zkrátit dobu tisku, ale zároveň zpomalit slicování a generování G-code. V oblastech, kde je retrakce přeskočena, se zároveň neprovádí ani z-hop." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." +msgstr "Neprovádět retrakci, pokud se přejezd nachází zcela uvnitř oblasti výplně. Případné vytékání materiálu tak nebude viditelné. To může snížit počet retrakčních pohybů u složitých modelů a zkrátit dobu tisku, ale zároveň zpomalit slicování a generování G-code. V oblastech, kde je retrakce přeskočena, se zároveň neprovádí ani Z-hop." msgid "This option will drop the temperature of the inactive extruders to prevent oozing." msgstr "Tato možnost sníží teplotu neaktivních extruderů, aby se zabránilo vytékání." @@ -16229,7 +16429,7 @@ msgstr "Délka retrakce po očištění" # AI Translated #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" "Délka rychlé retrakce po očištění, vztažená k délce retrakce.\n" @@ -16265,10 +16465,18 @@ msgstr "Dlouhá retrakce při změně extruderu" msgid "Retraction distance when extruder change" msgstr "Délka retrakce při změně extruderu" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Délka retrakce (Změna nástroje)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Když je retrakce spuštěna před změnou nástroje, filament se zatáhne o zadanou hodnotu (délka se měří na nezpracovaném filamentu, než vstoupí do extruderu)." + msgid "Z-hop height" msgstr "Výška Z-hopu" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "Po každém stažení filamentu se tryska mírně zvedne, aby vznikla mezera mezi tryskou a tiskem. Zabrání kolizi trysky s tiskem při přesunu. Použití spirálových linií pro zvedání v ose Z může zabránit vytahování vláken." msgid "Z-hop lower boundary" @@ -16362,6 +16570,10 @@ msgstr "Dodatečná délka při restartu" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Při kompenzaci retrakce po pohybu přesunu extruder posune toto přídavné množství filamentu. Toto nastavení je potřeba jen zřídka." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Dodatečná délka při restartu (Změna nástroje)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Při kompenzaci retrakce po výměně nástroje extruder posune toto přídavné množství filamentu." @@ -16780,6 +16992,14 @@ msgstr "Výměna nástroje na věži na očištění trysky" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Vynutí přejezd tiskové hlavy k věži na očištění trysky před vydáním příkazu k výměně nástroje (Tx). Týká se pouze tiskáren s více extrudery (více tiskovými hlavami), které používají věž na očištění trysky typu 2. Ve výchozím nastavení Orca na strojích s více tiskovými hlavami tento přejezd vynechává, protože výměnu hlavy řeší firmware, což může vést k vydání příkazu Tx nad tištěným dílem. Zapněte tuto volbu, chcete-li, aby byla výměna nástroje vždy vydána nad věží na očištění trysky." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Čekat na teplotu na věži na očištění trysky" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Vyzvedne nový nástroj, aniž by čekal na dosažení tiskové teploty, přejede na věž na očištění trysky a počká na teplotu tam, těsně před čištěním. Materiál vytékající při ohřevu skončí na věži místo na modelu a přejezd se překrývá s ohřevem. Relevantní pouze pro tiskárny s více extrudery (více tiskovými hlavami) používající věž na očištění trysky typu 2. Firmware ani makro pro změnu nástroje nesmí na teplotu čekat samo. Pokud je vypnuto, čekání na teplotu se vloží hned po příkazu ke změně nástroje." + msgid "No sparse layers (beta)" msgstr "Žádné řídké vrstvy (beta)" @@ -17319,6 +17539,14 @@ msgstr "" "\n" "Nastavení hodnoty v parametru množství retrakce před očištěním níže provede případnou dodatečnou retrakci před očištěním, jinak bude provedena po něm." +# AI Translated +msgid "Mixed color sublayer" +msgstr "Podvrstva míchané barvy" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "Zapne dělení na podvrstvy míchané barvy. Je-li zapnuto, vrstvy obsahující filamenty s míchanou barvou se rozdělí na podvrstvy, aby vznikl efekt míchání barev." + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "Čistící věž lze použít k odstranění zbytků materiálu na trysce a ke stabilizaci tlaku v komoře trysky, aby se předešlo vizuálním vadám při tisku objektů." @@ -18392,6 +18620,10 @@ msgstr "Síťování modelového souboru selhalo nebo nebyl nalezen platný tvar msgid "The supplied file couldn't be read because it's empty." msgstr "Zadaný soubor nelze načíst, protože je prázdný." +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "Formát souboru není kompatibilní a nelze jej načíst." + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Neznámý formát souboru. Vstupní soubor musí mít příponu .stl, .obj nebo .amf(.xml)." @@ -19884,19 +20116,19 @@ msgstr "Zobrazit pouze názvy tiskáren se změnami v předvolbách tiskárny, f msgid "Only display the filament names with changes to filament presets." msgstr "Zobrazit pouze názvy filamentů se změnami ve filamentových profilech." -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "Zobrazí se pouze názvy tiskáren s uživatelskými předvolbami tiskárny, každá vybraná předvolba bude exportována jako zip archiv." msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" "Zobrazí se pouze názvy filamentů s uživatelskými filamentovými profily. \n" "Všechny uživatelské filamentové profily ve vybraných filamentech budou exportovány jako zip archiv." msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" "Zobrazí se pouze názvy tiskáren se změněnými procesními předvolbami. \n" "Všechny uživatelské procesní předvolby ve vybraných tiskárnách budou exportovány jako zip archiv." @@ -20043,9 +20275,6 @@ msgstr "Fyzická tiskárna" msgid "Print Host upload" msgstr "Nahrání na tiskový server" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Vyberte implementaci síťového agenta pro komunikaci s tiskárnou. Dostupní agenti jsou registrováni při spuštění." - # AI Translated msgid "Select a Flashforge printer" msgstr "Vyberte tiskárnu Flashforge" @@ -20145,7 +20374,7 @@ msgid "We need information for diagnosing source of the issue. Check wiki page f msgstr "Pro diagnostiku zdroje problému potřebujeme informace. Podrobný návod najdete na stránce wiki." # AI Translated -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "Tlačítko Zabalit shromáždí soubor projektu a protokoly aktuální relace do souboru zip." # AI Translated @@ -20201,7 +20430,7 @@ msgid "Stored logs" msgstr "Uložené protokoly" # AI Translated -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "Zabalí všechny uložené protokoly do souboru zip." # AI Translated @@ -20289,8 +20518,8 @@ msgid "Authorizing..." msgstr "Probíhá autorizace..." # AI Translated -msgid "Error. Can't get api token for authorization" -msgstr "Chyba. Nelze získat api token pro autorizaci" +msgid "Error. Can't get API token for authorization" +msgstr "Chyba. Nelze získat API token pro autorizaci" # AI Translated msgid "Could not parse server response." @@ -20746,8 +20975,8 @@ msgid "Enable smart filament assign: Assign one filament to multiple nozzles to msgstr "Povolit chytré přiřazení filamentu: přiřadit jeden filament více tryskám pro maximální úsporu" # AI Translated -msgid "Fila Saving" -msgstr "Úspora filamentu" +msgid "File Saving" +msgstr "Ukládání souboru" # AI Translated msgid "Don't remind me again" @@ -21002,9 +21231,6 @@ msgstr "Při pokusu o přihlášení došlo k neočekávané chybě, zkuste to p msgid "User canceled." msgstr "Zrušeno uživatelem." -msgid "Head diameter" -msgstr "Průměr hlavy" - msgid "Max angle" msgstr "Maximální úhel" @@ -21206,6 +21432,9 @@ msgstr "Restartovat nyní" msgid "NO RAMMING AT ALL" msgstr "ŽÁDNÝ RAMMING" +msgid "s" +msgstr "s" + # AI Translated msgid "Volumetric speed" msgstr "Objemová rychlost" @@ -21873,6 +22102,62 @@ msgstr "" "Zamezte kroucení\n" "Víte, že při tisku materiálů náchylných ke kroucení, jako je ABS, může vhodné zvýšení teploty vyhřívané desky snížit pravděpodobnost kroucení?" +# AI Translated +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "Nativní živý náhled ve Waylandu vyžaduje video sink GStreamer GTK. Nainstalujte prosím plugin gtksink pro GStreamer a poté restartujte OrcaSlicer." + +# AI Translated +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "Nepodařilo se inicializovat nativní video sink GStreamer pro Wayland. Zkontrolujte prosím instalaci pluginu GStreamer GTK." + +# AI Translated +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "Pro tuto úlohu je vyžadován Windows Media Player! Chcete ve svém operačním systému povolit „Windows Media Player“?" + +# AI Translated +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "BambuSource nebyl správně zaregistrován pro přehrávání médií! Stisknutím Ano jej znovu zaregistrujete. Budete vyzváni dvakrát" + +# AI Translated +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "Chybí komponenta BambuSource registrovaná pro přehrávání médií! Přeinstalujte prosím OrcaSlicer nebo požádejte o pomoc komunitu." + +# AI Translated +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "Používá se BambuSource z jiné instalace, přehrávání videa nemusí fungovat správně! Stisknutím Ano to opravíte." + +# AI Translated +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "Ve vašem systému chybí kodeky H.264 pro GStreamer, které jsou nutné k přehrávání videa. (Zkuste nainstalovat balíčky gstreamer1.0-plugins-bad nebo gstreamer1.0-libav a poté restartovat Orca Slicer?)" + +# AI Translated +#~ msgid "N" +#~ msgstr "N" + +# AI Translated +#~ msgid "g" +#~ msgstr "g" + +# AI Translated +#~ msgid "Fila Saving" +#~ msgstr "Úspora filamentu" + +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Výška vrstvy je příliš malá.\n" +#~ "Bude nastavena na min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Výška vrstvy přesahuje limit v Nastavení tiskárny -> Extruder -> Omezení výšky vrstvy, což může způsobit problémy s kvalitou tisku." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Automaticky upravit do nastaveného rozsahu?\n" + +#~ msgid "Head diameter" +#~ msgstr "Průměr hlavy" + #~ msgid "Print order within a single layer." #~ msgstr "Pořadí tisku v rámci jedné vrstvy." diff --git a/localization/i18n/de/OrcaSlicer_de.po b/localization/i18n/de/OrcaSlicer_de.po index b9389f5399..4ff61fb6ea 100644 --- a/localization/i18n/de/OrcaSlicer_de.po +++ b/localization/i18n/de/OrcaSlicer_de.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "PO-Revision-Date: \n" "Last-Translator: Heiko Liebscher <hliebschergmail.com>\n" "Language-Team: \n" @@ -2917,6 +2917,10 @@ msgstr "Bearbeiten" msgid "Merge with" msgstr "Zusammenführen mit" +# AI Translated +msgid "Decompose Color" +msgstr "Farbe zerlegen" + msgid "Delete this filament" msgstr "Diesen Filament löschen" @@ -3216,6 +3220,10 @@ msgstr "Zusammenbau" msgid "Merge parts to an object" msgstr "Teile zu einem Objekt zusammenführen" +# AI Translated +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "Die Verwendung einer variablen Schichthöhe zusammen mit der Mischfarben-Unterschicht kann zu einer schlechten Farbmischqualität führen." + # AI Translated msgid "Add layers" msgstr "Schichten hinzufügen" @@ -4692,6 +4700,23 @@ msgstr "Die aktuelle Kammer-Temperatur ist höher als die sichere Temperatur des msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Die minimale Druckraumtemperatur (%d℃) ist höher als die Ziel-Druckraumtemperatur (%d℃). Der Minimalwert ist der Schwellenwert, bei dem der Druck beginnt, während der Druckraum weiter auf die Zieltemperatur heizt; er sollte diese daher nicht überschreiten. Er wird auf die Zieltemperatur begrenzt." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "Die Schichthöhe ist zu klein. Sie wird auf den Mindestwert (%g mm) gesetzt." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "Die Schichthöhe liegt außerhalb der in Druckereinstellungen -> Extruder -> Schichthöhenlimits festgelegten Grenzen. Dies kann zu Problemen mit der Druckqualität führen." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Automatisch an den Grenzwert (%g mm) anpassen?" + +msgid "Adjust" +msgstr "Anpassen" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4740,7 +4765,7 @@ msgstr "" "\n" "Der Wert wird auf 0 zurückgesetzt." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Der alternative zusätzliche Wandmodus funktioniert nicht gut, wenn die vertikale Wanddicke auf Alle eingestellt ist." msgid "" @@ -4786,7 +4811,7 @@ msgstr "" "NEIN - unabhängige Stütz-Schichthöhen beibehalten" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height muss kleiner als layer_height sein.\n" @@ -4794,7 +4819,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "Die Verriegelungstiefe sollte kleiner als die Hauttiefe sein.\n" @@ -4812,6 +4837,13 @@ msgstr "" "Ja - Arachne Wall Generator aktivieren\n" "Nein - Arachne Wall Generator deaktivieren und den Modus [Verschiebung] des Fuzzy Skin setzen" +# AI Translated +msgid "Brim ear radius" +msgstr "Radius der Brim-Ohren" + +msgid "Brim width" +msgstr "Randbreite" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Der Spiralmodus funktioniert nur, wenn die Wandschleifen 1 sind, die Stütze deaktiviert ist, die Klumpenerkennung durch Abtasten deaktiviert ist, die oberen Schichtlagen 0 sind, die Dichte der spärlichen Füllung 0 ist und der Zeitraffertyp traditionell ist." @@ -5066,6 +5098,14 @@ msgstr "Fehler beim Generieren des Kalibrierungs-G-Codes" msgid "Calibration error" msgstr "Kalibrierungsfehler" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Dieser Drucker ist nicht mit der Hardware ausgestattet, die dieses Bedienelement benötigt." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Dieses Bedienelement wird von diesem Drucker nicht unterstützt." + # AI Translated msgid "Network unavailable" msgstr "Netzwerk nicht verfügbar" @@ -5923,7 +5963,7 @@ msgstr "Volumen:" msgid "Size:" msgstr "Größe:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Konflikte von G-Code-Pfaden wurden bei Layer %d, Z = %.2lf mm gefunden.Bitte trennen Sie die konfliktbehafteten Objekte weiter voneinander (%s <-> %s)." @@ -6103,6 +6143,10 @@ msgstr "Multi-Gerät" msgid "Project" msgstr "Projekt" +# AI Translated +msgid "Device (Web)" +msgstr "Gerät (Web)" + msgid "Yes" msgstr "Ja" @@ -6236,10 +6280,10 @@ msgstr "Importiere 3MF/STL/STEP/SVG/OBJ/AMF" msgid "Load a model" msgstr "Lade ein Modell" -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "Zip-Archiv importieren" -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "Modelle aus einem Zip-Archiv laden" msgid "Import Configs" @@ -7755,6 +7799,10 @@ msgstr "Aktuelle Platte anpassen" msgid "The %s nozzle can not print %s." msgstr "Die %s Düse kann %s nicht drucken." +# AI Translated +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "Der Druck von Mischfarben-Filament auf einem Drucker mit nur einem Extruder erfordert häufige Filamentwechsel und Spülvorgänge, was den Abfall und das Risiko einer Verstopfung von Düse bzw. Abfallschacht deutlich erhöhen kann." + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "Mischen von %1% mit %2% im Druck wird nicht empfohlen.\n" @@ -7880,6 +7928,26 @@ msgstr "Filamentliste von AMS synchronisieren" msgid "Set filaments to use" msgstr "Zu verwendende Filamente einstellen" +# AI Translated +msgid "Add Mixed Filament" +msgstr "Mischfilament hinzufügen" + +# AI Translated +msgid "Mixed Filament" +msgstr "Mischfilament" + +# AI Translated +msgid "Remove last mixed filament" +msgstr "Letztes Mischfilament entfernen" + +# AI Translated +msgid "Add mixed filament" +msgstr "Mischfilament hinzufügen" + +# AI Translated +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "Das Mischfilament enthält ungültige oder nicht zusammenpassende Komponenten. Bitte bearbeiten Sie die betroffenen Einträge erneut." + msgid "Search plate, object and part." msgstr "Suche Platte, Objekt und Teil." @@ -7887,6 +7955,18 @@ msgstr "Suche Platte, Objekt und Teil." msgid "Pellets" msgstr "Pellets" +# AI Translated +msgid "Mixed filament has broken component references" +msgstr "Das Mischfilament enthält ungültige Komponentenverweise" + +# AI Translated +msgid "Edit / Delete / Merge" +msgstr "Bearbeiten / Löschen / Zusammenführen" + +# AI Translated +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "Das Ziel-Mischfilament verwendet dieses physische Filament als Komponente. Beim Zusammenführen wird dieses physische Filament entfernt und das Mischfilament kann dadurch ungültig werden. Fortfahren?" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "Nach Abschluss Ihrer Operation wird das %s-Projekt geschlossen und ein neues Projekt erstellt." @@ -8027,8 +8107,8 @@ msgstr "Bitte bestätigen Sie, dass die G-Codes innerhalb dieser Profile sicher msgid "Customized Preset" msgstr "Benutzerdefinierte Profile" -msgid "Component name(s) inside step file not in UTF8 format!" -msgstr "Der Name der Komponenten in der Step-Datei ist nicht im UTF8-Format!" +msgid "Component name(s) inside step file not in UTF-8 format!" +msgstr "Der Name der Komponenten in der Step-Datei ist nicht im UTF-8-Format!" msgid "Because of unsupported text encoding, garbage characters may appear!" msgstr "Aufgrund der nicht unterstützten Textkodierung können unbrauchbare Zeichen erscheinen!" @@ -8049,10 +8129,10 @@ msgstr "Das Volumen des Objekts ist Null" #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "Das Objekt aus der Datei %s ist zu klein und vielleicht in Metern oder Zoll angeben.\n" -" Möchten Sie auf Millimeter skalieren?" +"Möchten Sie auf Millimeter skalieren?" msgid "Object too small" msgstr "Objekt zu klein" @@ -8070,6 +8150,14 @@ msgstr "" msgid "Multi-part object detected" msgstr "Mehrteiliges Objekt erkannt" +# AI Translated +msgid "Matching textures to filaments" +msgstr "Texturen werden den Filamenten zugeordnet" + +# AI Translated +msgid "Texture Import Warning" +msgstr "Warnung beim Texturimport" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Diese Dateien als ein einziges Objekt mit mehreren Teilen laden?\n" @@ -8191,21 +8279,21 @@ msgstr "Verzeichnis um daraus zu ersetzen wurde nicht ausgewählt" msgid "Replaced with 3D files from directory:\n" msgstr "Ersetzt durch 3D-Dateien aus Verzeichnis:\n" -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ Übersprungen %1%: gleiche Datei.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ Übersprungen %s: gleiche Datei.\n" -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ Übersprungen %1%: Datei existiert nicht.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ Übersprungen %s: Datei existiert nicht.\n" -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ Übersprungen %1%: Ersetzen fehlgeschlagen.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ Übersprungen %s: Ersetzen fehlgeschlagen.\n" -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔ Ersetzt %1%.\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔ Ersetzt %s.\n" msgid "Replaced volumes" msgstr "Ersetzte Volumen" @@ -8283,6 +8371,22 @@ msgstr "" msgid "Sync now" msgstr "Jetzt synchronisieren" +# AI Translated +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "Der Texturimport ist fehlgeschlagen. Das Modell scheint Texturdaten zu enthalten, der Importvorgang konnte jedoch nicht abgeschlossen werden. Das Modell wird nur als Geometrie importiert." + +# AI Translated +msgid "Applying texture colors..." +msgstr "Texturfarben werden angewendet..." + +# AI Translated +msgid "Updating 3D view..." +msgstr "3D-Ansicht wird aktualisiert..." + +# AI Translated +msgid "Texture colors applied." +msgstr "Texturfarben angewendet." + # AI Translated msgid "You can keep the modified presets for the new project or discard them" msgstr "Sie können die geänderten Profile für das neue Projekt beibehalten oder sie verwerfen" @@ -8941,6 +9045,18 @@ msgstr "Wenn diese Option aktiviert ist, können Sie eine Aufgabe gleichzeitig a msgid "Pop up to select filament grouping mode" msgstr "Popup zum Auswählen des Filament-Gruppierungsmodus" +# AI Translated +msgid "Visible plugin pages" +msgstr "Sichtbare Plugin-Seiten" + +# AI Translated +msgid "pages" +msgstr "Seiten" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Anzahl der Plugin-Seiten, die als feste Tabs angezeigt werden, bevor die übrigen Seiten im letzten Tab zu einem Dropdown zusammengefasst werden." + msgid "Behaviour" msgstr "Verhalten" @@ -9296,6 +9412,18 @@ msgstr "Nicht unterstützte Profile anzeigen" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Zeigt inkompatible/nicht unterstützte Profile in den Dropdown-Listen für Drucker und Filament an. Diese Profile können nicht ausgewählt werden." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Experimentell) Drucker-Agenten anstelle von Druck-Hosts verwenden" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Leitet Druckaufträge für Nicht-Bambu-Drucker über Drucker-Plugin-Agenten statt über den klassischen Druck-Host-Upload.\n" +"Wenn deaktiviert, verwendet OrcaSlicer das bisherige Druck-Host-Verhalten." + msgid "Experimental Features" msgstr "Experimentelle Funktionen" @@ -9503,6 +9631,10 @@ msgstr "Vasenmodus" msgid "First layer filament sequence" msgstr "Erste Filament-Schichtsequenz" +# AI Translated +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "Die Filamentliste enthält Mischfilamente. Die benutzerdefinierte Filamentreihenfolge wird nicht wirksam." + msgid "By Layer" msgstr "Nach Schicht" @@ -9558,9 +9690,25 @@ msgstr "Benutzerprofil" msgid "Preset Inside Project" msgstr "Projektbasiertes Profil" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Kopiert alle vom übergeordneten Profil geerbten Werte in dieses Profil und entfernt die Vererbungsbeziehung. Profile, die nur mit dem übergeordneten Profil kompatibel sind, können dadurch nicht mehr unterstützt werden." + msgid "Detach from parent" msgstr "Vom übergeordneten Element trennen" +# AI Translated +msgid "Unique preset" +msgstr "Eigenständiges Profil" + +# AI Translated +msgid "Parent preset" +msgstr "Übergeordnetes Profil" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Dieses Profil erbt nicht von einem anderen Profil." + msgid "Name is unavailable." msgstr "Der Name ist nicht verfügbar." @@ -10296,22 +10444,6 @@ msgstr "Sind Sie sicher, dass Sie diese Option aktivieren möchten?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Infill-Muster sind in der Regel so konzipiert, dass sie eine automatische Drehung ermöglichen, um einen ordnungsgemäßen Druck zu gewährleisten und die beabsichtigten Effekte zu erzielen (z. B. Gyroid, Cubic). Das Drehen des aktuellen spärlichen Infill-Musters kann zu unzureichender Unterstützung führen. Bitte gehen Sie vorsichtig vor und überprüfen Sie gründlich auf mögliche Druckprobleme. Sind Sie sicher, dass Sie diese Option aktivieren möchten?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Die Schichthöhe ist zu klein.\n" -"Sie wird auf min_layer_height gesetzt\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Die Schichthöhe überschreitet das Limit in Druckereinstellungen -> Extruder -> Schichthöhenlimits. Dies kann zu Problemen mit der Druckqualität führen." - -msgid "Adjust to the set range automatically?\n" -msgstr "Automatisch an den eingestellten Bereich anpassen?\n" - -msgid "Adjust" -msgstr "Anpassen" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Experimentelle Funktion: Filament beim Filamentwechsel weiter zurückziehen und abschneiden, um den Flush zu minimieren. Obwohl dies den Flush deutlich reduzieren kann, kann es auch das Risiko von Düsenverstopfungen oder anderen Druckkomplikationen erhöhen." @@ -10505,6 +10637,9 @@ msgstr "Reservierte Schlüsselwörter gefunden" msgid "Setting Overrides" msgstr "Überschreiben der Einstellungen" +msgid "Retraction when switching material" +msgstr "Rückzug bei Materialwechsel" + msgid "Basic information" msgstr "Grundlegende Informationen" @@ -10634,6 +10769,12 @@ msgstr "Kompatible Prozessprofile" msgid "Printable space" msgstr "Druckbarer Raum" +msgid "Printer Agent" +msgstr "Drucker-Agent" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Wählen Sie die Implementierung des Netzwerkagenten für die Druckerkommunikation. Verfügbare Agenten werden beim Start registriert." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10759,9 +10900,6 @@ msgstr "Höhenbegrenzungen für Schichten" msgid "Z-Hop" msgstr "Z-Hop" -msgid "Retraction when switching material" -msgstr "Rückzug bei Materialwechsel" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -10874,11 +11012,11 @@ msgstr "%s: %s" msgid "No modifications need to be copied." msgstr "Es müssen keine Änderungen kopiert werden." -msgid "Copy paramters" +msgid "Copy parameters" msgstr "Parameter kopieren" #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "Parameter von %s ändern" #, c-format, boost-format @@ -11394,27 +11532,6 @@ msgstr "Reinigungsvolumen für Filamentwechsel" msgid "Please choose the filament colour" msgstr "Bitte wählen Sie die Filamentfarbe" -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "Native Wayland Liveview erfordert das GStreamer GTK Video Sink. Bitte installieren Sie das gtksink-Plugin für GStreamer und starten Sie OrcaSlicer neu." - -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "Fehler beim Initialisieren des nativen Wayland GStreamer Video Sinks. Bitte überprüfen Ihre GStreamer GTK-Plugin-Installation." - -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "Windows Media Player wird für diese Aufgabe benötigt! Möchten Sie 'Windows Media Player' für Ihr Betriebssystem aktivieren?" - -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "BambuSource wurde nicht korrekt für das Abspielen von Medien registriert! Drücken Sie Ja, um es erneut zu registrieren. Sie werden zweimal aufgefordert" - -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "Fehlende BambuSource-Komponente, die für das Abspielen von Medien registriert ist! Bitte installieren Sie OrcaSlicer neu oder suchen Sie Hilfe in der Community." - -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "Verwendung eines BambuSource aus einer anderen Installation, das Abspielen von Videos funktioniert möglicherweise nicht korrekt! Drücken Sie Ja, um es zu beheben." - -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "Ihr System fehlt H.264-Codecs für GStreamer, die zum Abspielen von Videos erforderlich sind. (Versuchen Sie, die Pakete gstreamer1.0-plugins-bad oder gstreamer1.0-libav zu installieren und starten Sie Orca Slicer neu?)" - msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "Cloud-Agent ist nicht verfügbar. Bitte starten Sie OrcaSlicer neu und versuchen Sie es erneut." @@ -12103,6 +12220,10 @@ msgstr " ist zu nahe am Sperrbereich und es werden Kollisionen verursacht.\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " ist zu nahe am Klumpenerkennungsbereich und es werden Kollisionen verursacht.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " liegt teilweise außerhalb des druckbaren Bereichs und kann nicht gedruckt werden.\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Die ausgewählten Düsentemperaturen sind nicht kompatibel. Die Düsentemperatur jedes Filaments muss innerhalb des empfohlenen Düsentemperaturbereichs der anderen Filamente liegen. Andernfalls kann es zu Düsenverstopfungen oder Druckerschäden kommen." @@ -12115,6 +12236,10 @@ msgstr "Wenn Sie trotzdem drucken möchten, können Sie die Option in Einstellun msgid "No extrusions under current settings." msgstr "Keine Extrusion unter den aktuellen Einstellungen." +# AI Translated +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "Es wird ein Mischfilament mit Verlauf verwendet, aber „Mischfarben-Unterschicht“ ist deaktiviert. Der Verlauf wird nicht gedruckt." + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Der gewählte Zeitraffermodus wird nicht unterstützt, wenn die Sequenz \"nach Objekt\" aktiviert ist." @@ -12151,6 +12276,10 @@ msgstr "Sie möchten möglicherweise die Größe Ihres Modells reduzieren oder d msgid "Variable layer height is not supported with Organic supports." msgstr "Variable Schichthöhe wird nicht mit organischen Stützstrukturen unterstützt." +# AI Translated +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "Das Filament des Reinigungsturms darf kein Mischfilament sein." + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Unterschiedliche Düsendurchmesser und unterschiedliche Filamentdurchmesser funktionieren möglicherweise nicht gut, wenn der Reinigungsturm aktiviert ist. Es ist sehr experimentell, also gehen Sie bitte vorsichtig vor." @@ -12418,9 +12547,6 @@ msgstr "Benutze 3MF statt G-Code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Aktivieren Sie diese Option, wenn der Drucker eine 3MF-Datei als Druckauftrag akzeptiert. Wenn aktiviert, sendet Orca Slicer die geslicete Datei als .gcode.3mf, anstatt als einfache .gcode-Datei." -msgid "Printer Agent" -msgstr "Drucker-Agent" - msgid "Select the network agent implementation for printer communication." msgstr "Wählen Sie die Netzwerk-Agent-Implementierung für die Druckerkommunikation aus." @@ -12503,7 +12629,7 @@ msgstr "mm o. %" msgid "Other layers" msgstr "Andere Schichten" -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." msgstr "Dies ist die Betttemperatur für Schichten mit Ausnahme der Ersten. Ein Wert von 0 bedeutet, dass das Filament auf der kalten Druckplatte SuperTack nicht unterstützt wird." msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." @@ -13091,9 +13217,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Geschwindigkeit der internen Brücken. Wenn der Wert als Prozentsatz angegeben wird, wird er auf der Grundlage der Brückengeschwindigkeit berechnet. Der Standardwert beträgt 150 %." -msgid "Brim width" -msgstr "Randbreite" - msgid "This is the distance from the model to the outermost brim line." msgstr "Abstand vom Modell zur äußersten Randlinie" @@ -13174,6 +13297,14 @@ msgstr "" "Die Geometrie wird vor der Erkennung scharfer Winkel reduziert. Dieser Parameter ist ein Indikator für die minimale Länge der Abweichung für die Reduzierung.\n" "0 zum Deaktivieren." +# AI Translated +msgid "Brim ears outer only" +msgstr "Brim-Ohren nur außen" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Erzeugt Mausohren nur an der Außenkontur des Modells, ohne Löcher und geschlossene Bereiche." + msgid "upward compatible machine" msgstr "Aufwärtskompatible Maschine" @@ -13862,6 +13993,8 @@ msgstr "Schichtdauer" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "Der Bauteillüfter wird für Schichten aktiviert, deren geschätzte Zeit kürzer als dieser Wert ist. Die Lüftergeschwindigkeit wird zwischen der minimalen und maximalen Geschwindigkeit entsprechend der Druckzeit der Schicht interpoliert." +# AI Translated +msgctxt "second" msgid "s" msgstr "s" @@ -14166,6 +14299,62 @@ msgstr "Stützmaterial" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Stützmaterial wird üblicherweise zum Drucken von Stützen und Stütz-Schnittstellen verwendet." +# AI Translated +msgid "Is mixed filament" +msgstr "Ist Mischfilament" + +# AI Translated +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "Gibt an, ob dieser Filamentplatz ein Mischfilament aus mehreren physischen Filamenten ist" + +# AI Translated +msgid "Mixed filament components" +msgstr "Komponenten des Mischfilaments" + +# AI Translated +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "Durch Kommas getrennte Indizes der Komponentenfilamente, beginnend bei 1, z. B. \"1,3\"" + +# AI Translated +msgid "Mixed filament sublayer ratios" +msgstr "Unterschicht-Anteile des Mischfilaments" + +# AI Translated +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "Durch Kommas getrennte Anteilswerte, die in der Summe 1.0 ergeben, z. B. \"0.7,0.3\"" + +# AI Translated +msgid "Mixed filament gradient" +msgstr "Verlauf des Mischfilaments" + +# AI Translated +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "Aktiviert den Verlaufsmodus in Z-Richtung für die Unterschichten des Mischfilaments. Wenn aktiviert, ändern sich die Anteile der Unterschichten linear über die Schichten hinweg." + +# AI Translated +msgid "Mixed filament gradient range" +msgstr "Verlaufsbereich des Mischfilaments" + +# AI Translated +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "Start- und Endanteil der ersten Komponente im Verlaufsmodus. Durch Komma getrenntes Paar, z. B. \"0.10,0.90\" bedeutet 10% bis 90%." + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "Verlaufskurve des Mischfilaments" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "Optionale benutzerdefinierte Kurve im Photoshop-Stil, die den Z-Fortschritt auf den Anteil der ersten Komponente abbildet. Kodiert als durch senkrechte Striche getrennte Kontrollpunkte, entweder \"x,y\" (veraltet) oder \"x,y,m_in,m_out\", wenn die Tangente überschrieben werden soll (ein leerer Wert oder \"nan\" verwendet den PCHIP-Standard). x liegt in [0,1]; y wird auf den konfigurierten Anteilsbereich begrenzt, z. B. \"0,0.15|0.5,0.50|1,0.85\". Bleibt das Feld leer, wird stattdessen der lineare gradient_range verwendet." + +# AI Translated +msgid "Mixed filament per-part gradient" +msgstr "Verlauf pro Teil beim Mischfilament" + +# AI Translated +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "Wenn der Verlaufsmodus aktiviert ist, wird der Verlauf auf jedes Teil eines Zusammenbaus einzeln angewendet, anstatt den gesamten Zusammenbau als einen einzigen Z-Bereich zu behandeln." + msgid "Filament printable" msgstr "Filament druckbar" @@ -14341,6 +14530,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroid" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Glättungsfaktor der Füllung" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Legt fest, wie stark die Ecken der Füllung abgerundet werden. 0% behält den ursprünglichen scharfkantigen Pfad bei, während 100% die größtmöglichen Kurven zwischen benachbarten Fülllinien erzeugt." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Dies ist die Beschleunigung der Füllung von der obersten Schicht. Die Verwendung eines niedrigeren Werts kann die Qualität der Oberfläche verbessern." @@ -14874,6 +15071,14 @@ msgstr "Mit welcher Art von G-Code ist der Drucker kompatibel." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "G-code-Konfigurationsblock auslassen" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Schreibt den CONFIG_BLOCK (die Schlüssel-Wert-Paare der Slicer-Konfiguration) nicht in die G-code-Datei. Das kann bei Druckern helfen, deren Firmware beim Verarbeiten dieser Kommentarzeilen abstürzt (z. B. Anycubic go-klipper). Hinweis: Die G-code-Datei enthält dann keine Slicer-Einstellungen mehr, sodass beim erneuten Importieren in OrcaSlicer die Konfiguration nicht wiederhergestellt wird." + msgid "Pellet Modded Printer" msgstr "Pellet-Modifizierter Drucker" @@ -15390,6 +15595,7 @@ msgid "The allowed maximum output force of Y axis" msgstr "Die maximal zulässige Ausgangskraft der Y-Achse" # AI Translated +msgctxt "Newton" msgid "N" msgstr "N" @@ -15400,6 +15606,7 @@ msgid "The machine bed mass load of Y axis" msgstr "Die Maschinenbett-Massenlast der Y-Achse" # AI Translated +msgctxt "gram" msgid "g" msgstr "g" @@ -15714,7 +15921,7 @@ msgstr "Die Start- und Endpunkte, vom Schnittbereich bis zum Auswurfschacht." msgid "Reduce infill retraction" msgstr "Rückzug bei der Füllung verringern" -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." msgstr "Kein Rückzug, wenn sich die Bewegung des Druckkopfes vollständig in einem Füllbereich befindet. Das bedeutet, dass das herauslaufen des Filaments nicht zu sehen ist. Dies kann die Zeit für das zurückziehen des Filaments bei komplexeren Modellen verkürzen und Druckzeit sparen, verlangsamt aber das Slicen und die G-Code Generierung." msgid "This option will drop the temperature of the inactive extruders to prevent oozing." @@ -15884,7 +16091,7 @@ msgstr "Rückzugsmenge nach dem Wischen" # AI Translated #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" "Die Länge des schnellen Rückzugs nach dem Wischen, relativ zur Rückzugslänge.\n" @@ -15920,10 +16127,18 @@ msgstr "Langer Rückzug beim Extruderwechsel" msgid "Retraction distance when extruder change" msgstr "Rückzugslänge beim Extruderwechsel" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Rückzugslänge (Werkzeugwechsel)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Wenn vor einem Werkzeugwechsel ein Rückzug ausgelöst wird, wird das Filament um den angegebenen Betrag zurückgezogen (die Länge wird am rohen Filament gemessen, bevor es in den Extruder gelangt)." + msgid "Z-hop height" msgstr "Z-Hub-Höhe" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "Bei jedem Rückzug wird die Düse ein wenig angehoben, um einen Abstand zwischen Düse und Druck zu schaffen. Dadurch wird verhindert, dass die Düse bei der Verfahrbewegung gegen den Druck stößt. Die Verwendung einer Spirallinie zum Anheben von z kann Fadenbildung verhindern." msgid "Z-hop lower boundary" @@ -16014,6 +16229,10 @@ msgstr "Zusätzliche Länge beim Neustart" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Wenn die Rückzugskompensation nach dem Reisemove durchgeführt wird, wird der Extruder diese zusätzliche Menge an Filament schieben. Diese Einstellung wird nur selten benötigt." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Zusätzliche Länge beim Neustart (Werkzeugwechsel)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Wenn die Rückzugskompensation nach dem Wechsel des Werkzeugs durchgeführt wird, wird der Extruder diese zusätzliche Menge an Filament schieben." @@ -16431,6 +16650,14 @@ msgstr "Werkzeugwechsel auf dem Reinigungsturm" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Erzwinge, dass der Werkzeugkopf zum Reinigungsturm fährt, bevor der Werkzeugwechselbefehl (Tx) ausgegeben wird. Nur relevant für Mehrfach-Extruder (Mehrfach-Werkzeugkopf) Drucker, die einen Typ-2-Reinigungsturm verwenden. Standardmäßig überspringt Orca die Fahrt auf Mehrfach-Werkzeugkopf-Maschinen, da die Firmware den Kopfwechsel übernimmt, was dazu führen kann, dass der Tx-Befehl über dem gedruckten Teil ausgegeben wird. Aktivieren Sie diese Option, wenn Sie möchten, dass der Werkzeugwechsel immer über dem Reinigungsturm ausgegeben wird." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Auf Temperatur am Reinigungsturm warten" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Nimmt das neue Werkzeug auf, ohne auf das Erreichen der Drucktemperatur zu warten, fährt zum Reinigungsturm und wartet dort unmittelbar vor dem Spülen auf die Temperatur. Das beim Aufheizen austretende Material landet auf dem Turm statt auf dem Modell, und die Fahrt überlappt sich mit dem Aufheizen. Nur relevant für Multi-Extruder-Drucker (mehrere Werkzeugköpfe) mit einem Reinigungsturm vom Typ 2. Die Firmware bzw. das Werkzeugwechsel-Makro darf nicht selbst auf die Temperatur warten. Wenn deaktiviert, wird das Warten auf die Temperatur direkt nach dem Werkzeugwechselbefehl ausgegeben." + msgid "No sparse layers (beta)" msgstr "Keine dünnen Schichten (Beta)" @@ -16962,6 +17189,14 @@ msgstr "" "\n" "Wenn ein Wert in der Einstellung \"Rückzugsmenge vor dem Wischen\" unten angegeben ist, wird ein überschüssiger Rückzug vor dem Wischen ausgeführt, ansonsten wird er danach ausgeführt." +# AI Translated +msgid "Mixed color sublayer" +msgstr "Mischfarben-Unterschicht" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "Aktiviert die Aufteilung in Mischfarben-Unterschichten. Wenn aktiviert, werden Schichten mit Mischfarben-Filamenten in Unterschichten aufgeteilt, um Farbmischeffekte zu erzielen." + # AI Translated msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "Der Reinigungsturm kann verwendet werden, um Rückstände auf der Düse zu entfernen und den Kammerdruck im Inneren der Düse zu stabilisieren, um Erscheinungsdefekte beim Drucken von Objekten zu vermeiden." @@ -18032,6 +18267,10 @@ msgstr "Das Erstellen eines Netzes aus der Modelldatei ist fehlgeschlagen oder e msgid "The supplied file couldn't be read because it's empty." msgstr "Die angegebene Datei konnte nicht gelesen werden, weil sie leer ist." +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "Das Dateiformat ist nicht kompatibel und kann nicht gelesen werden." + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Unbekanntes Dateiformat: Die Eingabedatei muss die Endung .stl, .obj oder .amf(.xml) haben." @@ -19492,19 +19731,19 @@ msgstr "Nur Druckernamen mit Änderungen an Drucker-, Filament- und Prozessprofi msgid "Only display the filament names with changes to filament presets." msgstr "Nur die Filamentnamen mit Änderungen an den Filamentprofilen werden angezeigt." -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "Nur Druckernamen mit Benutzerdruckerprofilen werden angezeigt, und jedes Profil, das Sie auswählen, wird als ZIP exportiert." msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" "Nur die Filamentnamen mit Benutzerfilamentvorlagen werden angezeigt, \n" "und alle Benutzerfilamentvorlagen in jedem Filamentnamen, den Sie auswählen, " msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" "Nur Druckernamen mit geänderten Prozessvorlagen werden angezeigt, \n" "und alle Benutzerprozessvorlagen in jedem Druckernamen, den Sie auswählen, werden als ZIP exportiert." @@ -19650,9 +19889,6 @@ msgstr "Drucker" msgid "Print Host upload" msgstr "Hochladen zum Druck-Host" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Wählen Sie die Implementierung des Netzwerkagenten für die Druckerkommunikation. Verfügbare Agenten werden beim Start registriert." - msgid "Select a Flashforge printer" msgstr "Wählen Sie einen Flashforge-Drucker aus" @@ -19736,7 +19972,7 @@ msgstr "Systeminformationen in die Zwischenablage kopieren" msgid "We need information for diagnosing source of the issue. Check wiki page for detailed guide." msgstr "Wir benötigen Informationen zur Diagnose der Ursache des Problems. Überprüfen Sie die Wiki-Seite für eine detaillierte Anleitung." -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "Die Schaltfläche „Packen“ sammelt die Projektdatei und Protokolle der aktuellen Sitzung in einer ZIP-Datei." msgid "Any additional visual examples like images or screen recordings might be helpful while reporting the issue." @@ -19778,7 +20014,7 @@ msgstr "Protokollstufe" msgid "Stored logs" msgstr "Gespeicherte Protokolle" -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "Packt alle gespeicherten Protokolle in eine ZIP-Datei." msgid "Profiles" @@ -19845,7 +20081,7 @@ msgstr "Druckertyp nicht gefunden, bitte manuell auswählen." msgid "Authorizing..." msgstr "Autorisierung..." -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "Fehler. Kann kein API-Token für die Autorisierung erhalten" msgid "Could not parse server response." @@ -20293,8 +20529,8 @@ msgid "Enable smart filament assign: Assign one filament to multiple nozzles to msgstr "Intelligente Filamentzuweisung aktivieren: Ein Filament mehreren Düsen zuweisen, um Einsparungen zu maximieren" # AI Translated -msgid "Fila Saving" -msgstr "Filamentersparnis" +msgid "File Saving" +msgstr "Datei speichern" msgid "Don't remind me again" msgstr "Nicht mehr erinnern" @@ -20500,9 +20736,6 @@ msgstr "Es ist etwas Unerwartetes passiert, als Sie versucht haben, sich anzumel msgid "User canceled." msgstr "Benutzer abgebrochen." -msgid "Head diameter" -msgstr "Kopfdurchmesser" - msgid "Max angle" msgstr "Maximaler Winkel" @@ -20662,6 +20895,9 @@ msgstr "Jetzt neu starten" msgid "NO RAMMING AT ALL" msgstr "KEIN RAMMING ÜBERALL" +msgid "s" +msgstr "s" + msgid "Volumetric speed" msgstr "Volumetrische Geschwindigkeit" @@ -21286,6 +21522,55 @@ msgstr "" "Verwerfungen vermeiden\n" "Wussten Sie, dass beim Drucken von Materialien, die zu Verwerfungen neigen, wie z.B. ABS, durch eine entsprechende Erhöhung der Heizbetttemperatur die Wahrscheinlichkeit von Verwerfungen verringert werden kann?" +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "Native Wayland Liveview erfordert das GStreamer GTK Video Sink. Bitte installieren Sie das gtksink-Plugin für GStreamer und starten Sie OrcaSlicer neu." + +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "Fehler beim Initialisieren des nativen Wayland GStreamer Video Sinks. Bitte überprüfen Ihre GStreamer GTK-Plugin-Installation." + +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "Windows Media Player wird für diese Aufgabe benötigt! Möchten Sie 'Windows Media Player' für Ihr Betriebssystem aktivieren?" + +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "BambuSource wurde nicht korrekt für das Abspielen von Medien registriert! Drücken Sie Ja, um es erneut zu registrieren. Sie werden zweimal aufgefordert" + +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "Fehlende BambuSource-Komponente, die für das Abspielen von Medien registriert ist! Bitte installieren Sie OrcaSlicer neu oder suchen Sie Hilfe in der Community." + +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "Verwendung eines BambuSource aus einer anderen Installation, das Abspielen von Videos funktioniert möglicherweise nicht korrekt! Drücken Sie Ja, um es zu beheben." + +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "Ihr System fehlt H.264-Codecs für GStreamer, die zum Abspielen von Videos erforderlich sind. (Versuchen Sie, die Pakete gstreamer1.0-plugins-bad oder gstreamer1.0-libav zu installieren und starten Sie Orca Slicer neu?)" + +# AI Translated +#~ msgid "N" +#~ msgstr "N" + +# AI Translated +#~ msgid "g" +#~ msgstr "g" + +# AI Translated +#~ msgid "Fila Saving" +#~ msgstr "Filamentersparnis" + +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Die Schichthöhe ist zu klein.\n" +#~ "Sie wird auf min_layer_height gesetzt\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Die Schichthöhe überschreitet das Limit in Druckereinstellungen -> Extruder -> Schichthöhenlimits. Dies kann zu Problemen mit der Druckqualität führen." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Automatisch an den eingestellten Bereich anpassen?\n" + +#~ msgid "Head diameter" +#~ msgstr "Kopfdurchmesser" + #~ msgid "Print order within a single layer." #~ msgstr "Druckreihenfolge innerhalb einer einzelnen Schicht" diff --git a/localization/i18n/en/OrcaSlicer_en.po b/localization/i18n/en/OrcaSlicer_en.po index eb25f226ee..67d4d196d4 100644 --- a/localization/i18n/en/OrcaSlicer_en.po +++ b/localization/i18n/en/OrcaSlicer_en.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "PO-Revision-Date: 2026-06-17 15:44-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: \n" @@ -2758,6 +2758,9 @@ msgstr "" msgid "Merge with" msgstr "" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "" @@ -3035,6 +3038,9 @@ msgstr "" msgid "Merge parts to an object" msgstr "" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + msgid "Add layers" msgstr "" @@ -4448,6 +4454,20 @@ msgstr "" msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "" +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4479,7 +4499,7 @@ msgid "" "The value will be reset to 0." msgstr "" -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "" msgid "" @@ -4510,13 +4530,13 @@ msgid "" msgstr "" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" @@ -4529,6 +4549,12 @@ msgid "" "No - Disable Arachne Wall Generator and set [Displacement] mode of the Fuzzy Skin" msgstr "" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "" @@ -4780,6 +4806,12 @@ msgstr "" msgid "Calibration error" msgstr "" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + msgid "Network unavailable" msgstr "" @@ -5611,7 +5643,7 @@ msgstr "" msgid "Size:" msgstr "" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "" @@ -5786,6 +5818,9 @@ msgstr "" msgid "Project" msgstr "" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "" @@ -5918,10 +5953,10 @@ msgstr "" msgid "Load a model" msgstr "" -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "" -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "" msgid "Import Configs" @@ -7370,6 +7405,9 @@ msgstr "" msgid "The %s nozzle can not print %s." msgstr "" +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "" @@ -7490,12 +7528,36 @@ msgstr "" msgid "Set filaments to use" msgstr "" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "" msgid "Pellets" msgstr "" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "" @@ -7630,7 +7692,7 @@ msgstr "" msgid "Customized Preset" msgstr "" -msgid "Component name(s) inside step file not in UTF8 format!" +msgid "Component name(s) inside step file not in UTF-8 format!" msgstr "" msgid "Because of unsupported text encoding, garbage characters may appear!" @@ -7652,7 +7714,7 @@ msgstr "" #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" msgid "Object too small" @@ -7667,6 +7729,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "" @@ -7776,20 +7844,20 @@ msgstr "" msgid "Replaced with 3D files from directory:\n" msgstr "" -#, boost-format -msgid "✖ Skipped %1%: same file.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" msgstr "" -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" msgstr "" -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" msgstr "" -#, boost-format -msgid "✔ Replaced %1%.\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" msgstr "" msgid "Replaced volumes" @@ -7861,6 +7929,18 @@ msgstr "" msgid "Sync now" msgstr "" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "" @@ -8468,6 +8548,15 @@ msgstr "" msgid "Pop up to select filament grouping mode" msgstr "" +msgid "Visible plugin pages" +msgstr "" + +msgid "pages" +msgstr "" + +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "" + msgid "Behaviour" msgstr "" @@ -8793,6 +8882,14 @@ msgstr "" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "" +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + msgid "Experimental Features" msgstr "" @@ -8993,6 +9090,9 @@ msgstr "" msgid "First layer filament sequence" msgstr "" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "" @@ -9048,9 +9148,21 @@ msgstr "" msgid "Preset Inside Project" msgstr "" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + msgid "Detach from parent" msgstr "" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "" @@ -9728,20 +9840,6 @@ msgstr "" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "" - -msgid "Adjust to the set range automatically?\n" -msgstr "" - -msgid "Adjust" -msgstr "" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "" @@ -9927,6 +10025,9 @@ msgstr "" msgid "Setting Overrides" msgstr "" +msgid "Retraction when switching material" +msgstr "" + msgid "Basic information" msgstr "" @@ -10053,6 +10154,12 @@ msgstr "" msgid "Printable space" msgstr "" +msgid "Printer Agent" +msgstr "" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "" + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10175,9 +10282,6 @@ msgstr "" msgid "Z-Hop" msgstr "" -msgid "Retraction when switching material" -msgstr "" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -10275,11 +10379,11 @@ msgstr "" msgid "No modifications need to be copied." msgstr "" -msgid "Copy paramters" +msgid "Copy parameters" msgstr "" #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "" #, c-format, boost-format @@ -10753,27 +10857,6 @@ msgstr "" msgid "Please choose the filament colour" msgstr "" -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "" - -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "" - -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "" - -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "" - -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "" - -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "" - -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "" - msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "" @@ -11441,6 +11524,9 @@ msgstr "" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr "" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "" @@ -11453,6 +11539,9 @@ msgstr "" msgid "No extrusions under current settings." msgstr "" +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "" @@ -11489,6 +11578,9 @@ msgstr "" msgid "Variable layer height is not supported with Organic supports." msgstr "" +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "" @@ -11736,9 +11828,6 @@ msgstr "" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "" -msgid "Printer Agent" -msgstr "" - msgid "Select the network agent implementation for printer communication." msgstr "" @@ -11820,7 +11909,7 @@ msgstr "" msgid "Other layers" msgstr "" -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." msgstr "" msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." @@ -12275,9 +12364,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "" -msgid "Brim width" -msgstr "" - msgid "This is the distance from the model to the outermost brim line." msgstr "" @@ -12343,6 +12429,12 @@ msgid "" "0 to deactivate." msgstr "" +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "" @@ -12901,6 +12993,7 @@ msgstr "" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "" +msgctxt "second" msgid "s" msgstr "" @@ -13194,6 +13287,48 @@ msgstr "" msgid "Support material is commonly used to print supports and support interfaces." msgstr "" +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "" @@ -13355,6 +13490,12 @@ msgstr "" msgid "Gyroid" msgstr "" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "" @@ -13835,6 +13976,12 @@ msgstr "" msgid "Klipper" msgstr "" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "" @@ -14329,6 +14476,7 @@ msgstr "" msgid "The allowed maximum output force of Y axis" msgstr "" +msgctxt "Newton" msgid "N" msgstr "" @@ -14338,6 +14486,7 @@ msgstr "" msgid "The machine bed mass load of Y axis" msgstr "" +msgctxt "gram" msgid "g" msgstr "" @@ -14606,7 +14755,7 @@ msgstr "" msgid "Reduce infill retraction" msgstr "" -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." msgstr "" msgid "This option will drop the temperature of the inactive extruders to prevent oozing." @@ -14762,7 +14911,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" @@ -14796,10 +14945,16 @@ msgstr "" msgid "Retraction distance when extruder change" msgstr "" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "" msgid "Z-hop lower boundary" @@ -14889,6 +15044,9 @@ msgstr "" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "" +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "" @@ -15274,6 +15432,12 @@ msgstr "" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "" +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "" @@ -15750,6 +15914,12 @@ msgid "" "Setting a value in the retract amount before wipe setting below will perform any excess retraction before the wipe, else it will be performed after." msgstr "" +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "" @@ -16777,6 +16947,9 @@ msgstr "" msgid "The supplied file couldn't be read because it's empty." msgstr "" +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "" @@ -18107,17 +18280,17 @@ msgstr "" msgid "Only display the filament names with changes to filament presets." msgstr "" -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "" msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" msgid "Please select at least one printer or filament." @@ -18249,9 +18422,6 @@ msgstr "" msgid "Print Host upload" msgstr "" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "" - msgid "Select a Flashforge printer" msgstr "" @@ -18335,7 +18505,7 @@ msgstr "" msgid "We need information for diagnosing source of the issue. Check wiki page for detailed guide." msgstr "" -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "" msgid "Any additional visual examples like images or screen recordings might be helpful while reporting the issue." @@ -18377,7 +18547,7 @@ msgstr "" msgid "Stored logs" msgstr "" -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "" msgid "Profiles" @@ -18442,7 +18612,7 @@ msgstr "" msgid "Authorizing..." msgstr "" -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "" msgid "Could not parse server response." @@ -18880,7 +19050,7 @@ msgstr "" msgid "Enable smart filament assign: Assign one filament to multiple nozzles to maximize savings" msgstr "" -msgid "Fila Saving" +msgid "File Saving" msgstr "" msgid "Don't remind me again" @@ -19083,9 +19253,6 @@ msgstr "" msgid "User canceled." msgstr "" -msgid "Head diameter" -msgstr "" - msgid "Max angle" msgstr "" @@ -19243,6 +19410,9 @@ msgstr "" msgid "NO RAMMING AT ALL" msgstr "" +msgid "s" +msgstr "" + msgid "Volumetric speed" msgstr "" diff --git a/localization/i18n/es/OrcaSlicer_es.po b/localization/i18n/es/OrcaSlicer_es.po index c1709fe45f..bc8e1e9f79 100644 --- a/localization/i18n/es/OrcaSlicer_es.po +++ b/localization/i18n/es/OrcaSlicer_es.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "PO-Revision-Date: \n" "Last-Translator: Ian A. Bassi <>\n" "Language-Team: \n" @@ -2832,6 +2832,9 @@ msgstr "Editar" msgid "Merge with" msgstr "Fusionar con" +msgid "Decompose Color" +msgstr "Descomponer color" + msgid "Delete this filament" msgstr "Eliminar este filamento" @@ -3113,6 +3116,9 @@ msgstr "Ensamblaje" msgid "Merge parts to an object" msgstr "Fusionar piezas en un objeto" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "Usar altura de capa variable junto con la subcapa de color mezclado puede reducir la calidad de la mezcla de colores." + msgid "Add layers" msgstr "Añadir capas" @@ -4564,6 +4570,23 @@ msgstr "La temperatura actual de la recámara es superior a la temperatura de se msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "La temperatura mínima de la recámara (%d℃) es superior a la temperatura objetivo de la recámara (%d℃). El valor mínimo es el umbral en el que comienza la impresión mientras la recámara continúa calentándose hacia el objetivo, por lo que no debería superarlo. Se ajustará al valor objetivo." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "La altura de capa es demasiado pequeña. Se establecerá en el mínimo (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "La altura de capa está fuera de los límites establecidos en Ajustes de la Impresora -> Extrusor -> Limite de Altura de Capa, esto puede causar problemas de calidad de impresión." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "¿Ajustarla automáticamente al límite (%g mm)?" + +msgid "Adjust" +msgstr "Ajustar" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4612,7 +4635,7 @@ msgstr "" "\n" "El valor se restablecerá a 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Perímetro adicional alternado no funciona bien cuando \"Garantizar el grosor vertical de las cubiertas\" se establece en Todos." msgid "" @@ -4658,7 +4681,7 @@ msgstr "" "NO - Mantener la altura de capa de soportes independiente" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height debe ser menor que layer_height.\n" @@ -4666,7 +4689,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "La profundidad de bloqueo debe ser menor que la profundidad de piel.\n" @@ -4684,6 +4707,13 @@ msgstr "" "Sí: habilitar el generador de muros Arachne\n" "No: deshabilitar el generador de paredes Arachne y establecer el modo [Desplazamiento] de la piel rugosa" +# AI Translated +msgid "Brim ear radius" +msgstr "Radio de las orejas de borde" + +msgid "Brim width" +msgstr "Ancho del borde de adherencia" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "El modo espiral solo funciona cuando los bucles de perímetro son 1, el soporte está desactivado, la detección de agrupamientos mediante sondeo está desactivada, las capas superiores de la carcasa son 0, la densidad de relleno es 0 y el tipo de lapso de tiempo es tradicional." @@ -4938,6 +4968,14 @@ msgstr "Fallo al generar el G-Code de calibración" msgid "Calibration error" msgstr "Error de calibración" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Esta impresora no está configurada con el hardware que necesita este control." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Este control no es compatible con esta impresora." + msgid "Network unavailable" msgstr "Red no disponible" @@ -5779,7 +5817,7 @@ msgstr "Volumen:" msgid "Size:" msgstr "Tamaño:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Se han encontrado conflictos de rutas G-Code en la capa %d, Z = %.2lfmm. Por favor, separe más los objetos en conflicto (%s <-> %s)." @@ -5960,6 +5998,10 @@ msgstr "Multi-dispositivo" msgid "Project" msgstr "Proyecto" +# AI Translated +msgid "Device (Web)" +msgstr "Dispositivo (Web)" + msgid "Yes" msgstr "Sí" @@ -6093,10 +6135,10 @@ msgstr "Importar 3MF/STL/STEP/SVG/OBJ/AMF" msgid "Load a model" msgstr "Cargar un modelo" -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "Importar archivo Zip" -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "Cargar modelos contenidos en un archivo zip" msgid "Import Configs" @@ -7577,6 +7619,9 @@ msgstr "Personalizar cama actual" msgid "The %s nozzle can not print %s." msgstr "La boquilla %s no puede imprimir %s." +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "Imprimir filamento de color mezclado en una impresora de un solo extrusor requiere cambios de filamento y purgas frecuentes, lo que puede aumentar considerablemente el desperdicio y el riesgo de obstrucción de la boquilla o del conducto de residuos." + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "No se recomienda mezclar %1% con %2% en la impresión.\n" @@ -7699,12 +7744,36 @@ msgstr "Sicronizar filamentos de la lista AMS" msgid "Set filaments to use" msgstr "Elegir filamentos para usar" +msgid "Add Mixed Filament" +msgstr "Añadir filamento mixto" + +msgid "Mixed Filament" +msgstr "Filamento mixto" + +msgid "Remove last mixed filament" +msgstr "Eliminar el último filamento mixto" + +msgid "Add mixed filament" +msgstr "Añadir filamento mixto" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "El filamento mixto tiene componentes no válidos o incoherentes. Vuelva a editar las entradas afectadas." + msgid "Search plate, object and part." msgstr "Buscar cama, objeto y parte." msgid "Pellets" msgstr "Pellets" +msgid "Mixed filament has broken component references" +msgstr "El filamento mixto tiene referencias de componentes rotas" + +msgid "Edit / Delete / Merge" +msgstr "Editar / Borrar / Fusionar" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "El filamento mixto de destino usa este filamento físico como componente. Al fusionarlos se eliminará este filamento físico y el filamento mixto puede quedar no válido. ¿Continuar?" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "Al completar la operación, el proyecto %s se cerrará y se creará un nuevo proyecto." @@ -7841,8 +7910,8 @@ msgstr "¡Por favor, confirme que el G-Code dentro de los perfiles son seguros p msgid "Customized Preset" msgstr "Perfil Personalizado" -msgid "Component name(s) inside step file not in UTF8 format!" -msgstr "¡El nombre de los componentes dentro del archivo de pasos no tiene formato UTF8!" +msgid "Component name(s) inside step file not in UTF-8 format!" +msgstr "¡El nombre de los componentes dentro del archivo de pasos no tiene formato UTF-8!" msgid "Because of unsupported text encoding, garbage characters may appear!" msgstr "¡El nombre puede mostrar caracteres no válidos!" @@ -7863,10 +7932,10 @@ msgstr "El volumen del objeto es cero" #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "El objeto del archivo %s es demasiado pequeño, tal vez en metros o pulgadas.\n" -" ¿Quiere escalar a milímetros?" +"¿Quiere escalar a milímetros?" msgid "Object too small" msgstr "Objeto demasiado pequeño" @@ -7883,6 +7952,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "Objeto multipieza detectado" +msgid "Matching textures to filaments" +msgstr "Asociando texturas a los filamentos" + +msgid "Texture Import Warning" +msgstr "Aviso de importación de texturas" + msgid "Load these files as a single object with multiple parts?\n" msgstr "¿Cargar estos archivos como un objeto único con múltiples piezas?\n" @@ -7997,21 +8072,21 @@ msgstr "No se seleccionó el directorio para el reemplazo" msgid "Replaced with 3D files from directory:\n" msgstr "Reemplazado con archivos 3D desde el directorio:\n" -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ Omitido %1%: mismo archivo.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ Omitido %s: mismo archivo.\n" -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ Omitido %1%: el archivo no existe.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ Omitido %s: el archivo no existe.\n" -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ Omitido %1%: fallo al reemplazar.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ Omitido %s: fallo al reemplazar.\n" -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔ Reemplazado %1%.\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔ Reemplazado %s.\n" msgid "Replaced volumes" msgstr "Volúmenes reemplazados" @@ -8087,6 +8162,18 @@ msgstr "" msgid "Sync now" msgstr "Sincronizar ahora" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "No se ha podido importar la textura. El modelo parece contener datos de textura, pero no se ha podido completar el proceso de importación. El modelo se importará solo como geometría." + +msgid "Applying texture colors..." +msgstr "Aplicando colores de textura..." + +msgid "Updating 3D view..." +msgstr "Actualizando la vista 3D..." + +msgid "Texture colors applied." +msgstr "Colores de textura aplicados." + msgid "You can keep the modified presets for the new project or discard them" msgstr "Puedes mantener los perfiles modificados en el nuevo proyecto o descartarlos" @@ -8725,6 +8812,18 @@ msgstr "Con esta opción activada, puede enviar una tarea a varios dispositivos msgid "Pop up to select filament grouping mode" msgstr "Ventana emergente para seleccionar el modo de agrupación de filamentos" +# AI Translated +msgid "Visible plugin pages" +msgstr "Páginas de plugins visibles" + +# AI Translated +msgid "pages" +msgstr "páginas" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Número de páginas de plugins que se muestran como pestañas fijas antes de que el resto de páginas se agrupe en un desplegable en la última pestaña." + msgid "Behaviour" msgstr "Comportamiento" @@ -9074,6 +9173,18 @@ msgstr "Mostrar ajustes preestablecidos no compatibles" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Mostrar los ajustes preestablecidos incompatibles o no compatibles en los menús desplegables de impresoras y filamentos. Estos ajustes preestablecidos no se pueden seleccionar." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Experimental) Usar agentes de impresora en lugar de hosts de impresión" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Envía los trabajos de impresión de impresoras que no son Bambu a través de los agentes de plugin de impresora en lugar del flujo clásico de subida al host de impresión.\n" +"Cuando está desactivado, OrcaSlicer utiliza el comportamiento heredado del host de impresión." + msgid "Experimental Features" msgstr "Funciones experimentales" @@ -9278,6 +9389,9 @@ msgstr "Vaso en espiral" msgid "First layer filament sequence" msgstr "Secuencia de primera capa de filamento" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "La lista de filamentos contiene filamentos mixtos. La secuencia de filamentos personalizada no tendrá efecto." + msgid "By Layer" msgstr "Por Capa" @@ -9333,9 +9447,25 @@ msgstr "Perfil de usuario" msgid "Preset Inside Project" msgstr "Perfil interno del proyecto" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Copia en este perfil todos los valores heredados del perfil padre y elimina la relación de herencia. Los perfiles compatibles solo con el perfil padre pueden dejar de ser compatibles." + msgid "Detach from parent" msgstr "Separar del elemento padre" +# AI Translated +msgid "Unique preset" +msgstr "Perfil único" + +# AI Translated +msgid "Parent preset" +msgstr "Perfil padre" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Este perfil no hereda de otro perfil." + msgid "Name is unavailable." msgstr "El nombre no está disponible." @@ -10031,22 +10161,6 @@ msgstr "¿Está seguro de que desea activar esta opción?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Los patrones de relleno suelen diseñarse para gestionar la rotación automáticamente y asegurar una impresión adecuada y lograr sus efectos previstos (p. ej., Giroide, Cúbico). Rotar el patrón de relleno actual puede provocar soporte insuficiente. Proceda con precaución y compruebe detenidamente posibles problemas de impresión. ¿Está seguro de que desea activar esta opción?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"La altura de la capa es demasiado pequeña.\n" -"Se establecerá en min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "La altura de la capa excede el límite en Ajustes de la Impresora -> Extrusor -> Limite de Altura de Capa, esto puede causar problemas de calidad de impresión." - -msgid "Adjust to the set range automatically?\n" -msgstr "¿Desea ajustar el rango automáticamente?\n" - -msgid "Adjust" -msgstr "Ajustar" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Función experimental: retraer y cortar el filamento a una mayor distancia durante los cambios de filamento para minimizar el purgado. Aunque puede reducir notablemente el purgado, también puede aumentar el riesgo de atascos de boquilla u otras complicaciones de impresión.Característica experimental: Retraer y cortar el filamento a mayor distancia durante los cambios de filamento para minimizar el descarte. Aunque puede reducir notablemente el descarte, también puede elevar el riesgo de atascos de boquillas u otros problemas en la impresión." @@ -10238,6 +10352,9 @@ msgstr "Palabras clave utilizadas y encontradas" msgid "Setting Overrides" msgstr "Sobreescribir Ajustes de impresora" +msgid "Retraction when switching material" +msgstr "Retracción al cambiar de material" + msgid "Basic information" msgstr "Información básica" @@ -10364,6 +10481,12 @@ msgstr "Perfiles de proceso compatibles" msgid "Printable space" msgstr "Espacio imprimible" +msgid "Printer Agent" +msgstr "Agente de impresora" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Seleccione la implementación del agente de red para la comunicación con la impresora. Los agentes disponibles se registran al iniciar el sistema." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10489,9 +10612,6 @@ msgstr "Límites de altura de la capa" msgid "Z-Hop" msgstr "Salto en Z" -msgid "Retraction when switching material" -msgstr "Retracción al cambiar de material" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -10604,11 +10724,11 @@ msgstr "%s: %s" msgid "No modifications need to be copied." msgstr "No hay modificaciones que copiar." -msgid "Copy paramters" +msgid "Copy parameters" msgstr "Copiar parámetros" #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "Modificar parámetros de %s" #, c-format, boost-format @@ -11115,27 +11235,6 @@ msgstr "Volúmenes de purgado para el cambio de filamentos" msgid "Please choose the filament colour" msgstr "Por favor, elija el color del filamento" -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "La función de visualización en directo nativa de Wayland requiere el receptor de vídeo GTK de GStreamer. Instale el plugin gtksink para GStreamer y, a continuación, reinicie OrcaSlicer." - -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "No se pudo inicializar el receptor de vídeo nativo de Wayland GStreamer. Compruebe la instalación del plugin GTK de GStreamer." - -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "Para esta tarea se necesita el Reproductor de Windows Media. ¿Desea activar el \"Reproductor de Windows Media\" en su sistema operativo?" - -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "BambuSource no se ha registrado correctamente para la reproducción multimedia. Pulse Sí para volver a registrarlo. Será promocionado dos veces" - -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "¡Falta el componente BambuSource para la reproducción de medios! Reinstale OrcaSlicer o busque ayuda en la comunidad." - -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "Si utiliza una BambuSource de una instalación diferente, es posible que la reproducción de vídeo no funcione correctamente. Pulsa Sí para solucionarlo." - -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "A tu sistema le faltan los codecs H.264 para GStreamer, necesarios para reproducir vídeo. (Prueba a instalar los paquetes gstreamer1.0-plugins-bad o gstreamer1.0-libav y, a continuación, reinicia Orca Slicer...)." - msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "El proveedor de servicios en la nube no está disponible. Reinicia OrcaSlicer e inténtalo de nuevo." @@ -11809,6 +11908,10 @@ msgstr " está demasiado cerca de una zona de exclusión, lo que provocará coli msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " está demasiado cerca del área de detección de aglomeraciones, y se producirán colisiones.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " está parcialmente fuera del área imprimible, y no se puede imprimir.\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Las temperaturas de boquilla seleccionadas son incompatibles. La temperatura de boquilla de cada filamento debe estar dentro del rango de temperaturas recomendado para los demás filamentos. De lo contrario, podrían producirse atascos en la boquilla o daños en la impresora." @@ -11821,6 +11924,9 @@ msgstr "Si aún así quieres imprimir, puedes activar la opción en Preferencias msgid "No extrusions under current settings." msgstr "No hay extrusiones con los ajustes actuales." +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "Se está usando un filamento mixto con degradado, pero 'Subcapa de color mezclado' está desactivado. El degradado no se imprimirá." + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Modo de timelapse suave no está soportado cuando la secuencia \"por objeto\" está activada." @@ -11857,6 +11963,9 @@ msgstr "Es posible que desee reducir el tamaño de su modelo o cambiar la config msgid "Variable layer height is not supported with Organic supports." msgstr "La altura de capa adaptativa no es compatible con los soportes orgánicos." +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "El filamento de la torre de purga no puede ser un filamento mixto." + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Diámetros de boquillas y diámetros de filamento diferentes pueden no funcionar correctamente cuando la torre de purga está activada. Esta función es experimental, así que proceda con cautela." @@ -12116,9 +12225,6 @@ msgstr "Utiliza 3MF en lugar de G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Activa esta opción si la impresora admite un archivo 3MF como trabajo de impresión. Cuando está activada, Orca Slicer envía el archivo cortado como un archivo .gcode.3mf, en lugar de como un archivo .gcode convencional." -msgid "Printer Agent" -msgstr "Agente de impresora" - msgid "Select the network agent implementation for printer communication." msgstr "Seleccione la implementación del agente de red para la comunicación con la impresora." @@ -12200,8 +12306,8 @@ msgstr "mm o %" msgid "Other layers" msgstr "Otras capas" -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." -msgstr "Temperatura de la cama para las capas, excepto la inicial. Un valor de 0 significa que el filamento no es compatible con la Cama Fría SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgstr "Esta es la temperatura de la cama para las capas excepto la inicial. Un valor de 0 significa que el filamento no es compatible con la Cama Fría SuperTack." msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." msgstr "Esta es la temperatura de la cama para las capas excepto la inicial. Un valor de 0 significa que el filamento no admite la impresión en la Cama Fría." @@ -12794,9 +12900,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Velocidad de los puntes internos. Si se expresa como un porcentaje, será Calculado en base a la velocidad de puente. El valor por defecto es 150%." -msgid "Brim width" -msgstr "Ancho del borde de adherencia" - msgid "This is the distance from the model to the outermost brim line." msgstr "Distancia del modelo a la línea más externa del borde de adherencia." @@ -12876,6 +12979,14 @@ msgstr "" "La geometría se verá diezmada antes de detectar angulos agudos. Este parámetro indica la longitud mínima de desviación para el diezmado\n" "0 para desactivar." +# AI Translated +msgid "Brim ears outer only" +msgstr "Orejas de borde solo en el exterior" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Genera orejas de ratón únicamente en el contorno exterior del modelo, excluyendo agujeros y secciones cerradas." + msgid "upward compatible machine" msgstr "máquina compatible ascendente" @@ -13539,6 +13650,7 @@ msgstr "Tiempo de capa" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "El ventilador de refrigeración de la pieza se activará para las capas cuyo tiempo estimado sea inferior a este valor. La velocidad del ventilador se interpola entre las velocidades mínima y máxima del ventilador en función del tiempo de impresión de cada capa." +msgctxt "second" msgid "s" msgstr "s" @@ -13844,6 +13956,50 @@ msgstr "Material de soporte" msgid "Support material is commonly used to print supports and support interfaces." msgstr "El material de soporte se utiliza habitualmente para imprimir soportes y la interfaz de los soportes." +msgid "Is mixed filament" +msgstr "Es filamento mixto" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "Indica si esta ranura de filamento es un filamento mixto compuesto por varios filamentos físicos" + +msgid "Mixed filament components" +msgstr "Componentes del filamento mixto" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "Índices (empezando en 1) de los filamentos componentes, separados por comas; p. ej. \"1,3\"" + +msgid "Mixed filament sublayer ratios" +msgstr "Proporciones de subcapa del filamento mixto" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "Valores de proporción separados por comas cuya suma sea 1.0; p. ej. \"0.7,0.3\"" + +msgid "Mixed filament gradient" +msgstr "Degradado del filamento mixto" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "Activa el modo de degradado en dirección Z para las subcapas del filamento mixto. Al activarlo, las proporciones de las subcapas varían linealmente entre capas." + +msgid "Mixed filament gradient range" +msgstr "Rango del degradado del filamento mixto" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "Proporciones inicial y final del primer componente en el modo de degradado. Par separado por comas; p. ej. \"0.10,0.90\" significa del 10% al 90%." + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "Curva del degradado del filamento mixto" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "Curva personalizada opcional, al estilo de Photoshop, que asigna el progreso en Z a la proporción del primer componente. Se codifica como puntos de control separados por barras verticales, con el formato \"x,y\" (heredado) o \"x,y,m_in,m_out\" cuando se necesita anular la tangente (un valor vacío o \"nan\" usa el valor PCHIP predeterminado). x está en [0,1]; y se limita al rango de proporciones configurado; p. ej. \"0,0.15|0.5,0.50|1,0.85\". Si se deja vacío, se usa el gradient_range lineal." + +msgid "Mixed filament per-part gradient" +msgstr "Degradado por pieza del filamento mixto" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "Cuando el modo de degradado está activado, aplica el degradado a cada pieza de un ensamblaje de forma independiente en lugar de tratar todo el ensamblaje como un único rango Z." + msgid "Filament printable" msgstr "Filamento imprimible" @@ -14011,6 +14167,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Giroide" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Factor de suavizado del relleno poco denso" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Controla cuánto se redondean las esquinas del relleno poco denso. 0% mantiene el trazado original con esquinas vivas, mientras que 100% produce las curvas más amplias posibles entre líneas de relleno adyacentes." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Aceleración del relleno de la superficie superior. El uso de un valor más bajo puede mejorar la calidad de la superficie superior." @@ -14544,6 +14708,14 @@ msgstr "Con qué tipo de G-Code es compatible la impresora." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Omitir el bloque de configuración del G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "No escribe el CONFIG_BLOCK (los pares clave/valor de la configuración del laminador) en el archivo G-code. Esto puede ayudar con impresoras cuyo firmware falla al analizar esas líneas de comentario (p. ej. Anycubic go-klipper). Nota: el archivo G-code ya no contendrá los ajustes del laminador, por lo que al importarlo de nuevo en OrcaSlicer no se restaurará la configuración." + msgid "Pellet Modded Printer" msgstr "Impresora Modificada para Pellets" @@ -15062,6 +15234,7 @@ msgstr "Fuerza máxima del eje Y" msgid "The allowed maximum output force of Y axis" msgstr "La fuerza máxima permitida del eje Y" +msgctxt "Newton" msgid "N" msgstr "N" @@ -15071,6 +15244,7 @@ msgstr "Masa de la cama del eje Y" msgid "The machine bed mass load of Y axis" msgstr "La carga de la masa de la cama de la máquina del eje Y" +msgctxt "gram" msgid "g" msgstr "g" @@ -15382,7 +15556,7 @@ msgstr "Los puntos de inicio y fin, desde la zona de corte al cubo de basura." msgid "Reduce infill retraction" msgstr "Reducir la retracción del relleno" -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." msgstr "Desactiva la retracción cuando el desplazamiento se realiza en su totalidad dentro de un área de relleno, donde los artefactos causados por un rezumado no son visibles. Puede reducir el número de retracciones y por ende el tiempo total de retracción al imprimir modelos complejos, reduciendo el tiempo total de impresión. Sin embargo, puede que las operaciones de laminado y de generación del archivo G-Code sean más lentas." msgid "This option will drop the temperature of the inactive extruders to prevent oozing." @@ -15547,7 +15721,7 @@ msgstr "Cantidad de retracción después de la limpieza" #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" "La longitud de la retracción rápida después de la limpieza, relativa a la longitud de retracción.\n" @@ -15583,10 +15757,18 @@ msgstr "Retracción larga al cambiar de extrusor" msgid "Retraction distance when extruder change" msgstr "Distancia de retracción al cambiar de extrusor" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Longitud de retracción (Cambio de herramienta)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Cuando se activa la retracción antes de un cambio de herramienta, el filamento se retrae la cantidad especificada (la longitud se mide sobre el filamento en bruto, antes de entrar en el extrusor)." + msgid "Z-hop height" msgstr "Altura de Salto en Z" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "Cada vez que se realiza una retracción, la boquilla se levanta un poco para crear un pequeño margen entre la boquilla y la impresión. Esto evita que la boquilla golpee la pieza cuando se desplaza. El uso de la línea espiral para levantar z puede evitar la aparición de hilos." msgid "Z-hop lower boundary" @@ -15676,6 +15858,10 @@ msgstr "Longitud extra de reinicio" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Cuando la retracción se compensa después de un desplazamiento, el extrusor expulsará esta cantidad adicional de filamento. Esta función no suele ser necesaria." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Longitud extra de reinicio (Cambio de herramienta)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Cuando se compensa la retracción después de cambiar de cabezal, el extrusor expulsará esta cantidad adicional de filamento." @@ -16082,6 +16268,14 @@ msgstr "Cambio de herramienta en la torre de purga" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Obliga al cabezal a desplazarse hasta la torre de purga antes de emitir el comando de cambio de herramienta (Tx). Solo es relevante para impresoras con múltiples extrusores (múltiples cabezales) que utilicen una torre de limpieza de tipo 2. Por defecto, Orca omite el desplazamiento en máquinas con múltiples cabezales porque el firmware se encarga del cambio de cabezal, lo que puede provocar que el comando Tx se emita por encima de la pieza impresa. Habilita esta opción si deseas que el cambio de herramienta se emita siempre por encima de la torre de purga." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Esperar la temperatura en la torre de purga" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Recoge la nueva herramienta sin esperar a que alcance la temperatura de impresión, se desplaza a la torre de purga y espera allí la temperatura, justo antes de purgar. El rezumado del calentamiento cae sobre la torre en lugar de sobre el modelo, y el desplazamiento se solapa con el calentamiento. Solo es relevante para impresoras multiextrusor (multicabezal) que usan una torre de purga de tipo 2. El firmware o la macro de cambio de herramienta no deben esperar la temperatura por su cuenta. Cuando está desactivado, la espera de temperatura se emite justo después del comando de cambio de herramienta." + msgid "No sparse layers (beta)" msgstr "Sin capas de baja densidad (beta)" @@ -16607,6 +16801,14 @@ msgstr "" "\n" "Fijando un valor en la cantidad de retracción antes del purgado se realizará cualquier exceso de retracción antes del purgado, de lo contrario se realizará después." +# AI Translated +msgid "Mixed color sublayer" +msgstr "Subcapa de color mezclado" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "Activa la división en subcapas de color mezclado. Al activarlo, las capas que contienen filamentos de color mezclado se dividen en subcapas para lograr efectos de mezcla de colores." + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "La torre de purga puede utilizarse para limpiar los residuos de la boquilla y estabilizar la presión de la recámara en el interior de la boquilla, con el fin de evitar defectos visuales al imprimir objetos." @@ -17315,7 +17517,7 @@ msgid "Current Z-hop" msgstr "Z-Hop actual" msgid "Contains Z-hop present at the beginning of the custom G-code block." -msgstr "Contiene el z-hop presente al principio del bloque de G-Code personalizado." +msgstr "Contiene el Z-hop presente al principio del bloque de G-Code personalizado." msgid "Position of the extruder at the beginning of the custom G-code block. If the custom G-code travels somewhere else, it should write to this variable so OrcaSlicer knows where it travels from when it gets control back." msgstr "Posición del extrusor al comienzo del bloque de G-Code personalizado. Si el G-Code personalizado viaja a otro lugar, debe escribir en esta variable para que OrcaSlicer sepa desde dónde viaja cuando recupere el control." @@ -17663,6 +17865,10 @@ msgstr "La generación de la malla del archivo del modelo falló o no hay una fo msgid "The supplied file couldn't be read because it's empty." msgstr "No se ha podido leer el archivo proporcionado porque está vacío." +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "El formato del archivo es incompatible y no se puede analizar." + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Formato de archivo desconocido: el archivo de entrada debe tener extensión .STL, .obj o .amf (.xml)." @@ -19125,17 +19331,17 @@ msgstr "Mostrar sólo los nombres de impresora con cambios en los perfiles de im msgid "Only display the filament names with changes to filament presets." msgstr "Mostrar sólo los nombres de impresora con cambios en los perfiles de filamento." -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "Sólo se mostrarán los nombres de impresoras con perfiles de impresora de usuario, y cada perfil que elija se exportará como un archivo zip." msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "Sólo se mostrarán los nombres de filamento con perfiles de filamento de usuario, y todos los perfiles de filamento de usuario de cada nombre de filamento que seleccione se exportarán como un archivo zip." msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" "Sólo se mostrarán los nombres de impresoras con perfiles de procesos modificados, \n" "y todos los perfiles de procesos de usuario de cada nombre de impresora que seleccione se exportarán como un archivo zip." @@ -19281,9 +19487,6 @@ msgstr "Impresora física" msgid "Print Host upload" msgstr "Mandar al servidor de impresión" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Seleccione la implementación del agente de red para la comunicación con la impresora. Los agentes disponibles se registran al iniciar el sistema." - msgid "Select a Flashforge printer" msgstr "Selecciona una impresora Flashforge" @@ -19367,7 +19570,7 @@ msgstr "Copiar información del sistema al portapapeles" msgid "We need information for diagnosing source of the issue. Check wiki page for detailed guide." msgstr "Necesitamos información para diagnosticar el origen del problema. Consulta la página wiki para una guía detallada." -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "El botón Empaquetar recopila el archivo del proyecto y los registros de la sesión actual en un archivo zip." msgid "Any additional visual examples like images or screen recordings might be helpful while reporting the issue." @@ -19409,7 +19612,7 @@ msgstr "Nivel de registro" msgid "Stored logs" msgstr "Registros almacenados" -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "Empaqueta todos los registros almacenados en un archivo zip." msgid "Profiles" @@ -19476,7 +19679,7 @@ msgstr "No se ha encontrado el tipo de impresora; selecciónelo manualmente." msgid "Authorizing..." msgstr "Autorizando..." -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "Error. No se puede obtener el token de la API para la autorización" msgid "Could not parse server response." @@ -19922,8 +20125,9 @@ msgstr "Eliminado" msgid "Enable smart filament assign: Assign one filament to multiple nozzles to maximize savings" msgstr "Activar la asignación inteligente de filamento: asigna un filamento a varias boquillas para maximizar el ahorro" -msgid "Fila Saving" -msgstr "Ahorro de filamento" +# AI Translated +msgid "File Saving" +msgstr "Guardado de archivo" msgid "Don't remind me again" msgstr "No me recuerdes de nuevo" @@ -20125,9 +20329,6 @@ msgstr "Ha ocurrido algo inesperado al intentar iniciar sesión, inténtelo de n msgid "User canceled." msgstr "Cancelado por el usuario." -msgid "Head diameter" -msgstr "Diámetro de la cabeza" - msgid "Max angle" msgstr "Ángulo máximo" @@ -20285,6 +20486,9 @@ msgstr "Reiniciar ahora" msgid "NO RAMMING AT ALL" msgstr "NO CHOCAR EN ABSOLUTO" +msgid "s" +msgstr "s" + msgid "Volumetric speed" msgstr "Velocidad volumétrica" @@ -20861,6 +21065,52 @@ msgstr "" "Evita la deformación\n" "¿Sabías que al imprimir materiales propensos a la deformación como el ABS, aumentar adecuadamente la temperatura de la cama térmica puede reducir la probabilidad de deformaciones?" +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "La función de visualización en directo nativa de Wayland requiere el receptor de vídeo GTK de GStreamer. Instale el plugin gtksink para GStreamer y, a continuación, reinicie OrcaSlicer." + +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "No se pudo inicializar el receptor de vídeo nativo de Wayland GStreamer. Compruebe la instalación del plugin GTK de GStreamer." + +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "Para esta tarea se necesita el Reproductor de Windows Media. ¿Desea activar el \"Reproductor de Windows Media\" en su sistema operativo?" + +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "BambuSource no se ha registrado correctamente para la reproducción multimedia. Pulse Sí para volver a registrarlo. Será promocionado dos veces" + +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "¡Falta el componente BambuSource para la reproducción de medios! Reinstale OrcaSlicer o busque ayuda en la comunidad." + +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "Si utiliza una BambuSource de una instalación diferente, es posible que la reproducción de vídeo no funcione correctamente. Pulsa Sí para solucionarlo." + +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "A tu sistema le faltan los codecs H.264 para GStreamer, necesarios para reproducir vídeo. (Prueba a instalar los paquetes gstreamer1.0-plugins-bad o gstreamer1.0-libav y, a continuación, reinicia Orca Slicer...)." + +#~ msgid "N" +#~ msgstr "N" + +#~ msgid "g" +#~ msgstr "g" + +#~ msgid "Fila Saving" +#~ msgstr "Ahorro de filamento" + +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "La altura de la capa es demasiado pequeña.\n" +#~ "Se establecerá en min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "La altura de la capa excede el límite en Ajustes de la Impresora -> Extrusor -> Limite de Altura de Capa, esto puede causar problemas de calidad de impresión." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "¿Desea ajustar el rango automáticamente?\n" + +#~ msgid "Head diameter" +#~ msgstr "Diámetro de la cabeza" + #~ msgid "Print order within a single layer." #~ msgstr "Orden de impresión dentro de cada capa." @@ -20924,7 +21174,7 @@ msgstr "" #~ msgid "Select Filament && Hotends" #~ msgstr "Seleccionar Filamento && Hotends" -#~ msgid "External spools is not supported since Filament Track Switch has been installed. If you want to use external spool, please uninstall it." +#~ msgid "External spools are not supported since Filament Track Switch has been installed. If you want to use external spool, please uninstall it." #~ msgstr "Los carretes externos no son compatibles porque se ha instalado el Filament Track Switch. Si desea utilizar un carrete externo, desinstálelo." #, c-format, boost-format diff --git a/localization/i18n/eu/OrcaSlicer_eu.po b/localization/i18n/eu/OrcaSlicer_eu.po index db9235b8c1..9ee2e6451d 100644 --- a/localization/i18n/eu/OrcaSlicer_eu.po +++ b/localization/i18n/eu/OrcaSlicer_eu.po @@ -3,12 +3,11 @@ # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # -#, fuzzy msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "PO-Revision-Date: 2026-07-20 13:33+0200\n" "Last-Translator: Manu Goiogana <mgoiogana@gmail.com>\n" "Language-Team: \n" @@ -2869,6 +2868,10 @@ msgstr "Editatu" msgid "Merge with" msgstr "Batu honekin" +# AI Translated +msgid "Decompose Color" +msgstr "Deskonposatu kolorea" + msgid "Delete this filament" msgstr "Ezabatu filamentu hau" @@ -3150,6 +3153,10 @@ msgstr "Multzoa" msgid "Merge parts to an object" msgstr "Batu piezak objektu batean" +# AI Translated +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "Geruza-altuera aldakorra kolore nahasiaren azpigeruzarekin batera erabiltzeak kolore-nahasketaren kalitatea okertu dezake." + msgid "Add layers" msgstr "Geruzak gehitu" @@ -4606,6 +4613,23 @@ msgstr "Uneko ganberako tenperatura materialaren tenperatura segurua baino handi msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Ganberako gutxieneko tenperatura (%d ℃) helburuko ganbera-tenperatura (%d ℃) baino altuagoa da. Gutxieneko balioa inprimaketa hasten den atalasea da, ganberak helbururantz berotzen jarraitzen duen bitartean; beraz, ez luke helburua gainditu behar. Helburuko baliora mugatuko da." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "Geruza-altuera txikiegia da. Gutxienekora ezarriko da (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "Geruza-altuera Inprimagailuaren ezarpenak -> Estrusorea -> Geruza-altueraren mugak atalean ezarritako mugetatik kanpo dago; horrek inprimatze-kalitateko arazoak sor ditzake." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Automatikoki mugara (%g mm) doitu nahi duzu?" + +msgid "Adjust" +msgstr "Doitu" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4653,7 +4677,7 @@ msgstr "" "\n" "Balioa 0ra berrezarriko da." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Horma gehigarri txandakatuak ez du ondo funtzionatzen \"Bermatu oskolaren lodiera bertikala\" Guztiak gisa ezarrita dagoenean." msgid "" @@ -4699,7 +4723,7 @@ msgstr "" "NO - 'Euskarrien Geruza-Altuera Independentea' mantendu" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height txigiagoa izan behar da layer_height baino.\n" @@ -4707,7 +4731,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "Blokeo-sakonera gainazalaren sakonera baino txikiagoa izan behar da.\n" @@ -4725,6 +4749,13 @@ msgstr "" "Bai - Gaitu Arachne horma-sorgailua\n" "Ez - Desgaitu Arachne horma-sorgailua eta ezarri gainazal zimurraren [Desplazamendua] modua" +# AI Translated +msgid "Brim ear radius" +msgstr "Ertz-belarriaren erradioa" + +msgid "Brim width" +msgstr "Itsaspen ertzaren zabalera" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Espiral moduak baldintza hauetan bakarrik funtzionatzen du: horma-begiztak 1 izatea, euskarriak desgaituta egotea, haztatze bidezko material-metaketa detektatzea desgaituta egotea, goiko estalki-geruzak 0 izatea, dentsitate baxuko betegarriaren dentsitatea 0 izatea eta timelapse mota tradizionala izatea." @@ -4979,6 +5010,14 @@ msgstr "Hutsegitea gertatu da kalibrazioko G-Code-a sortzean" msgid "Calibration error" msgstr "Kalibrazio akatsa" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Inprimagailu honek ez dauka kontrol honek behar duen hardwarea konfiguratuta." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Kontrol hau ez da bateragarria inprimagailu honekin." + # AI Translated msgid "Network unavailable" msgstr "Sarea ez dago erabilgarri" @@ -5828,7 +5867,7 @@ msgstr "Bolumena:" msgid "Size:" msgstr "Tamaina:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "G-code ibilbideen gatazkak aurkitu dira %d geruzan, Z = %.2lf mm. Urrundu gehiago gatazkan dauden objektuak (%s <-> %s)." @@ -6005,6 +6044,10 @@ msgstr "Gailu anitz" msgid "Project" msgstr "Proiektua" +# AI Translated +msgid "Device (Web)" +msgstr "Gailua (Web)" + msgid "Yes" msgstr "Bai" @@ -6138,10 +6181,10 @@ msgstr "Inportatu 3MF/STL/STEP/SVG/OBJ/AMF" msgid "Load a model" msgstr "Modeloa kargatu" -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "Inportatu ZIP fitxategia" -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "Kargatu ZIP fitxategi baten barnean dauden modeloak" msgid "Import Configs" @@ -7644,6 +7687,10 @@ msgstr "Pertsonalizatu uneko plaka" msgid "The %s nozzle can not print %s." msgstr "%s pitak ezin du %s inprimatu." +# AI Translated +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "Kolore nahasiko filamentua estrusore bakarreko inprimagailu batean inprimatzeak filamentu-aldaketa eta purgatze ugari eskatzen ditu, eta horrek nabarmen handitu ditzake hondakinak eta pita edo hondakin-hodia buxatzeko arriskua." + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "Ez da gomendatzen inprimatzean %1% eta %2% nahastea.\n" @@ -7766,12 +7813,44 @@ msgstr "Sinkronizatu filamentuen zerrenda AMSarekin" msgid "Set filaments to use" msgstr "Ezarri erabili beharreko filamentuak" +# AI Translated +msgid "Add Mixed Filament" +msgstr "Gehitu filamentu nahasia" + +# AI Translated +msgid "Mixed Filament" +msgstr "Filamentu nahasia" + +# AI Translated +msgid "Remove last mixed filament" +msgstr "Kendu azken filamentu nahasia" + +# AI Translated +msgid "Add mixed filament" +msgstr "Gehitu filamentu nahasia" + +# AI Translated +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "Filamentu nahasiak baliogabeko edo bat ez datozen osagaiak ditu. Editatu berriro kaltetutako sarrerak." + msgid "Search plate, object and part." msgstr "Bilatu plaka, objektua eta pieza." msgid "Pellets" msgstr "Pelletak" +# AI Translated +msgid "Mixed filament has broken component references" +msgstr "Filamentu nahasiak hautsitako osagai-erreferentziak ditu" + +# AI Translated +msgid "Edit / Delete / Merge" +msgstr "Editatu / Ezabatu / Batu" + +# AI Translated +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "Helburuko filamentu nahasiak filamentu fisiko hau erabiltzen du osagai gisa. Batzeak filamentu fisiko hau ezabatuko du eta filamentu nahasia baliogabetu dezake. Jarraitu?" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "Eragiketa amaitzean, %s proiektua itxi eta proiektu berri bat sortuko da." @@ -7909,7 +7988,7 @@ msgstr "Berretsi aurrezarpen hauetako G-codea segurua dela, makinari kalterik ez msgid "Customized Preset" msgstr "Aurrezarpen pertsonalizatua" -msgid "Component name(s) inside step file not in UTF8 format!" +msgid "Component name(s) inside step file not in UTF-8 format!" msgstr "STEP fitxategiko osagai-izena(k) ez dago/daude UTF-8 formatuan!" msgid "Because of unsupported text encoding, garbage characters may appear!" @@ -7931,10 +8010,10 @@ msgstr "Objektuaren bolumena zero da" #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "%s fitxategiko objektua txikiegia da, eta baliteke metrotan edo hazbetetan egotea.\n" -" Milimetroetara eskalatu nahi duzu?" +"Milimetroetara eskalatu nahi duzu?" msgid "Object too small" msgstr "Objektua txikiegia da" @@ -7950,6 +8029,14 @@ msgstr "" msgid "Multi-part object detected" msgstr "Pieza anitzeko objektua detektatu da" +# AI Translated +msgid "Matching textures to filaments" +msgstr "Testurak filamentuekin parekatzen" + +# AI Translated +msgid "Texture Import Warning" +msgstr "Testura inportatzeko abisua" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Kargatu fitxategi hauek pieza anitzeko objektu bakar gisa?\n" @@ -8064,21 +8151,21 @@ msgstr "Ez da ordezkatzeko direktoriorik hautatu" msgid "Replaced with 3D files from directory:\n" msgstr "Direktorio honetako 3D fitxategiekin ordeztuta:\n" -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ %1% saltatu da: fitxategi bera.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ %s saltatu da: fitxategi bera.\n" -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ %1% saltatu da: fitxategia ez da existitzen.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ %s saltatu da: fitxategia ez da existitzen.\n" -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ %1% saltatu da: ezin izan da ordeztu.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ %s saltatu da: ezin izan da ordeztu.\n" -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔ %1% ordezkatu da.\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔ %s ordezkatu da.\n" msgid "Replaced volumes" msgstr "Ordeztutako bolumenak" @@ -8154,6 +8241,22 @@ msgstr "" msgid "Sync now" msgstr "Sinkronizatu orain" +# AI Translated +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "Testura inportatzeak huts egin du. Badirudi modeloak testura-datuak dituela, baina ezin izan da inportatze-prozesua osatu. Modeloa geometria gisa soilik inportatuko da." + +# AI Translated +msgid "Applying texture colors..." +msgstr "Testura-koloreak aplikatzen..." + +# AI Translated +msgid "Updating 3D view..." +msgstr "3D ikuspegia eguneratzen..." + +# AI Translated +msgid "Texture colors applied." +msgstr "Testura-koloreak aplikatuta." + msgid "You can keep the modified presets for the new project or discard them" msgstr "Aldatutako aurrezarpenak proiektu berrirako gorde edo baztertu ditzakezu" @@ -8790,6 +8893,18 @@ msgstr "Aukera hau gaituta, zeregin bat hainbat gailutara bidali eta hainbat gai msgid "Pop up to select filament grouping mode" msgstr "Erakutsi filamentuak taldekatzeko modua hautatzeko leihoa" +# AI Translated +msgid "Visible plugin pages" +msgstr "Ikusgai dauden plugin-orriak" + +# AI Translated +msgid "pages" +msgstr "orri" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Fitxa finko gisa erakusten diren plugin-orrien kopurua; gainerako orriak azken fitxako goitibeherako zerrendan bilduko dira." + msgid "Behaviour" msgstr "Jokabidea" @@ -9142,6 +9257,18 @@ msgstr "Erakutsi onartzen ez diren aurrezarpenak" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Erakutsi bateraezinak edo onartu gabeak diren aurrezarpenak inprimagailuaren eta filamentuaren goitibeherako zerrendetan. Aurrezarpen hauek ezin dira hautatu." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Esperimentala) Erabili inprimagailu-agenteak inprimatze-hostenen ordez" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Bideratu Bambu ez diren inprimagailuen inprimatze-lanak inprimagailuaren plugin-agenteen bidez, inprimatze-hostera igotzeko fluxu klasikoaren ordez.\n" +"Desgaituta dagoenean, OrcaSlicer-ek inprimatze-hostaren aurreko portaera erabiltzen du." + msgid "Experimental Features" msgstr "Ezaugarri esperimentalak" @@ -9347,6 +9474,10 @@ msgstr "Espiral formako loreontzia" msgid "First layer filament sequence" msgstr "Lehen geruzako filamentuen sekuentzia" +# AI Translated +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "Filamentu-zerrendak filamentu nahasiak ditu. Filamentu-sekuentzia pertsonalizatuak ez du eraginik izango." + msgid "By Layer" msgstr "Geruzaren arabera" @@ -9402,9 +9533,25 @@ msgstr "Erabiltzailearen aurrezarpena" msgid "Preset Inside Project" msgstr "Proiektu barruko aurrezarpena" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Aurrezarpen honetara gurasoaren balio heredatu guztiak kopiatzen ditu eta gurasoarekiko lotura kentzen du. Gurasoarekin soilik bateragarriak diren aurrezarpenak bateraezin gera daitezke." + msgid "Detach from parent" msgstr "Bereizi gurasotik" +# AI Translated +msgid "Unique preset" +msgstr "Aurrezarpen bakarra" + +# AI Translated +msgid "Parent preset" +msgstr "Guraso-aurrezarpena" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Aurrezarpen honek ez du beste aurrezarpen batetik heredatzen." + msgid "Name is unavailable." msgstr "Izena ez dago erabilgarri." @@ -10124,22 +10271,6 @@ msgstr "Ziur aukera hau gaitu nahi duzula?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Betegarri-patroiak normalean biraketa automatikoki kudeatzeko diseinatuta daude, behar bezala inprimatzeko eta nahi den efektua lortzeko (adibidez, Giroidea edo Kubikoa). Uneko dentsitate baxuko betegarri-patroia biratzeak euskarri eskasa eragin dezake. Kontuz jarraitu eta egiaztatu arretaz inprimatze-arazorik sor daitekeen. Ziur zaude aukera hau gaitu nahi duzula?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Geruza-altuera txikiegia da.\n" -"min_layer_height baliora ezarriko da\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Geruza-altuerak Inprimagailuaren ezarpenak -> Estrusorea -> Geruza-altueraren mugak ataleko muga gainditzen du; horrek inprimatze-kalitateko arazoak sor ditzake." - -msgid "Adjust to the set range automatically?\n" -msgstr "Doitu automatikoki ezarritako barrutira?\n" - -msgid "Adjust" -msgstr "Doitu" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Funtzio esperimentala: filamentu aldaketetan distantzia handiagoan atzera egitea eta moztea, purgatzea minimizatzeko. Purgatzea nabarmen murriztu dezakeen arren, pitaren buxadurak edo bestelako inprimatze-arazoak izateko arriskua ere handitu dezake." @@ -10333,6 +10464,9 @@ msgstr "Erreserbatutako gako-hitzak aurkitu dira" msgid "Setting Overrides" msgstr "Ezarpenen gainidazketak" +msgid "Retraction when switching material" +msgstr "Atzera-egitea materiala aldatzean" + msgid "Basic information" msgstr "Oinarrizko informazioa" @@ -10459,6 +10593,12 @@ msgstr "Prozesu-profil bateragarriak" msgid "Printable space" msgstr "Inprimatzeko espazioa" +msgid "Printer Agent" +msgstr "Inprimagailu-agentea" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Hautatu sare-agentearen inplementazioa inprimagailuarekin komunikatzeko. Erabilgarri dauden agenteak abioan erregistratzen dira." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10584,9 +10724,6 @@ msgstr "Geruza-altueraren mugak" msgid "Z-Hop" msgstr "Z jauzia" -msgid "Retraction when switching material" -msgstr "Atzera-egitea materiala aldatzean" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -10699,11 +10836,11 @@ msgstr "%s: %s" msgid "No modifications need to be copied." msgstr "Ez dago kopiatu beharreko aldaketarik." -msgid "Copy paramters" +msgid "Copy parameters" msgstr "Kopiatu parametroak" #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "Aldatu %s-(r)en parametroak" #, c-format, boost-format @@ -11208,27 +11345,6 @@ msgstr "Filamentua aldatzeko purgatze-bolumenak" msgid "Please choose the filament colour" msgstr "Hautatu filamentuen kolorea" -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "Waylanden jatorrizko zuzeneko ikuspegiak GStreamer GTK bideo-hustubidea behar du. Instalatu GStreamerrerako gtksink plugina eta berrabiarazi OrcaSlicer." - -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "Ezin izan da Waylanden jatorrizko GStreamer bideo-hustubidea hasieratu. Egiaztatu GStreamer GTK pluginaren instalazioa." - -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "Windows Media Player behar da zeregin honetarako! 'Windows Media Player' gaitu nahi duzu zure sistema eragilean?" - -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "BambuSource ez da behar bezala erregistratu multimedia erreproduzitzeko! Sakatu Bai berriro erregistratzeko. Bi aldiz galdetuko zaizu" - -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "Multimedia erreproduzitzeko erregistratutako BambuSource osagaia falta da! Berrinstalatu OrcaSlicer edo eskatu laguntza komunitateari." - -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "Beste instalazio bateko BambuSource erabiltzen ari zara; baliteke bideo-erreprodukzioak behar bezala ez funtzionatzea! Sakatu Bai konpontzeko." - -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "Zure sisteman GStreamer-erako H.264 kodekak falta dira, eta beharrezkoak dira bideoa erreproduzitzeko. (Saiatu gstreamer1.0-plugins-bad edo gstreamer1.0-libav paketeak instalatzen, eta berrabiarazi OrcaSlicer?)" - msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "Hodeiko agentea ez dago erabilgarri. Berrabiarazi OrcaSlicer eta saiatu berriro." @@ -11912,6 +12028,10 @@ msgstr " bazterketa-eremu batetik gertuegi dago, eta talkak eragingo ditu.\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " material-metaketa detektatzeko eremutik gertuegi dago, eta talkak eragingo ditu.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " inprimagarri den eremutik kanpo dago partzialki, eta ezin da inprimatu.\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Hautatutako pita-tenperaturak ez dira bateragarriak. Filamentu bakoitzaren pita-tenperaturak gainerako filamentuen gomendatutako pita-tenperatura tartean egon behar du. Bestela, pita buxatu edo inprimagailua kaltetu daiteke." @@ -11924,6 +12044,10 @@ msgstr "Hala ere inprimatu nahi baduzu, aukera hau gaitu dezakezu: Hobespenak / msgid "No extrusions under current settings." msgstr "Uneko ezarpenekin ez dago estrusiorik." +# AI Translated +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "Gradientedun filamentu nahasi bat erabiltzen ari da, baina 'Kolore nahasiaren azpigeruza' desgaituta dago. Gradientea ez da inprimatuko." + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Timelapsearen modu leuna ez da onartzen \"objektuka\" sekuentzia gaituta dagoenean." @@ -11960,6 +12084,10 @@ msgstr "Modeloaren tamaina txikitu edo uneko inprimatze-ezarpenak aldatu eta ber msgid "Variable layer height is not supported with Organic supports." msgstr "Geruza-altuera aldakorra ez da onartzen euskarri organikoekin." +# AI Translated +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "Purgatze-dorrearen filamentua ezin da filamentu nahasia izan." + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Pitaren diametro eta filamentu-diametro desberdinek agian ez dute ondo funtzionatuko purgatze-dorrea gaituta dagoenean. Oso esperimentala da; jarraitu kontuz." @@ -12013,11 +12141,11 @@ msgstr "Purgatze-dorreak euskarriak objektuaren geruza-altuera bera izatea eskat # AI Translated msgid "For Organic supports, two walls are supported only with the Hollow/Default base pattern." -msgstr "Euskarri organikoetan, bi horma Hollow/Default oinarri-patroiarekin soilik onartzen dira." +msgstr "Euskarri organikoetan, bi horma Hutsa/Lehenetsia oinarri-patroiarekin soilik onartzen dira." # AI Translated msgid "The Lightning base pattern is not supported by this support type; Rectilinear will be used instead." -msgstr "Lightning oinarri-patroia ez du euskarri mota honek onartzen; Rectilinear erabiliko da horren ordez." +msgstr "Tximista oinarri-patroia ez du euskarri mota honek onartzen; Lerrozuzena erabiliko da horren ordez." msgid "Organic support tree tip diameter must not be smaller than support material extrusion width." msgstr "Euskarri organikoaren zuhaitz-muturraren diametroak ezin du izan euskarri-materialaren estrusio-zabalera baino txikiagoa." @@ -12030,7 +12158,7 @@ msgstr "Euskarri organikoaren adar-diametroak ezin du izan euskarri-zuhaitzaren # AI Translated msgid "The Hollow base pattern is not supported by this support type; Rectilinear will be used instead." -msgstr "Hollow oinarri-patroia ez du euskarri mota honek onartzen; Rectilinear erabiliko da horren ordez." +msgstr "Hutsa oinarri-patroia ez du euskarri mota honek onartzen; Lerrozuzena erabiliko da horren ordez." msgid "Support enforcers are used but support is not enabled. Please enable support." msgstr "Euskarri-behartzaileak erabiltzen dira, baina euskarria ez dago gaituta. Gaitu euskarriak." @@ -12228,9 +12356,6 @@ msgstr "Erabili 3MF G-codearen ordez" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Gaitu aukera hau inprimagailuak 3MF fitxategi bat inprimatze-lan gisa onartzen badu. Gaituta dagoenean, OrcaSlicerrek xerratutako fitxategia .gcode.3mf gisa bidaltzen du, .gcode fitxategi arrunt baten ordez." -msgid "Printer Agent" -msgstr "Inprimagailu-agentea" - msgid "Select the network agent implementation for printer communication." msgstr "Hautatu inprimagailuarekin komunikatzeko sare-agentearen inplementazioa." @@ -12312,8 +12437,8 @@ msgstr "mm edo %" msgid "Other layers" msgstr "Beste geruzak" -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." -msgstr "Hasierakoa ez den geruzetarako ohearen tenperatura. 0 balioak filamentuak SuperTack Plaka hotzaren gainean inprimatzea ez duela onartzen esan nahi du." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgstr "Hau da lehenengoa ez den geruzetarako ohearen tenperatura. 0 balioak filamentuak SuperTack Plaka hotzaren gainean inprimatzea ez duela onartzen esan nahi du." msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." msgstr "Hau da lehenengoa ez den geruzetarako ohearen tenperatura. 0 balioak filamentuak Plaka hotzaren gainean inprimatzea ez duela onartzen esan nahi du." @@ -12905,9 +13030,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Barru-zubien abiadura. Balioa ehuneko gisa adierazten bada, Zubien abiadura-ren arabera kalkulatuko da. Lehenetsitako balioa % 150ekoa da." -msgid "Brim width" -msgstr "Itsaspen ertzaren zabalera" - msgid "This is the distance from the model to the outermost brim line." msgstr "Hau da modelotik itsaspen ertzaren kanporen lerrora dagoen distantzia." @@ -12987,6 +13109,14 @@ msgstr "" "Geometria sinplifikatu egingo da angelu zorrotzak detektatu aurretik. Parametro honek sinplifikaziorako desbideratzearen gutxieneko luzera adierazten du.\n" "0, desaktibatzeko." +# AI Translated +msgid "Brim ears outer only" +msgstr "Ertz-belarriak kanpoaldean soilik" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Sortu saguaren belarriak modeloaren kanpoko ingeradan soilik, zuloak eta itxitako atalak baztertuta." + msgid "upward compatible machine" msgstr "gorantz bateragarria den makina" @@ -13252,7 +13382,7 @@ msgstr "Moderatua" # AI Translated msgid "Top surface pattern" -msgstr "Goiko gainazalaren patroia" +msgstr "Goiko gainazaleko patroia" # AI Translated msgid "This is the line pattern for top surface infill." @@ -13265,13 +13395,13 @@ msgid "Monotonic line" msgstr "Lerro monotonikoa" msgid "Rectilinear" -msgstr "Rectilinear" +msgstr "Lerrozuzena" msgid "Aligned Rectilinear" msgstr "Lerrozuzen lerrokatua" msgid "Concentric" -msgstr "Concentric" +msgstr "Kontzentrikoa" msgid "Hilbert Curve" msgstr "Hilbert kurba" @@ -13337,7 +13467,7 @@ msgstr "Kanporantz" # AI Translated msgid "Bottom surface pattern" -msgstr "Beheko gainazalaren patroia" +msgstr "Beheko gainazaleko patroia" # AI Translated msgid "This is the line pattern of bottom surface infill, not including bridge infill." @@ -13362,7 +13492,7 @@ msgid "" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Gaineko gainazalak betetzen diren noranzkoa zentroan oinarritutako patroia erabiltzen denean (Konzentrikoa, Arkimedeen Akordeak, Oktograma Espirala).\n" +"Goiko gainazalak betetzen diren noranzkoa zentroan oinarritutako patroia erabiltzen denean (Kontzentrikoa, Arkimedesen kordak, Oktagrama-kiribila).\n" "Kanporanzkoa erdialdean hasten da, beraz, gehiegizko materiala gutxien ikusten den ertzera bultzatzen da. Barruranzkoa ertzean hasten da eta erdian kurba estuekin amaitzen da.\n" "Lehenetsiak bide laburreneko ordena erabiltzen du, zeina norabide batean zein bestean ibil daitekeen." @@ -13375,7 +13505,7 @@ msgid "" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Beheko gainazalak betetzen diren noranzkoa zentroan oinarritutako patroia erabiltzen denean (Konzentrikoa, Arkimedeen Akordeak, Oktograma Espirala).\n" +"Beheko gainazalak betetzen diren noranzkoa zentroan oinarritutako patroia erabiltzen denean (Kontzentrikoa, Arkimedesen kordak, Oktagrama-kiribila).\n" "Barruranzkoa hasten da gainazal bakoitza kanpoko kurba zabalagoekin, eta horrek lehen geruzaren atxikimendua hobetzen du erdiko kurba estuak itsatsi ez daitezkeen inprimatze-plaketan. Kanporanzkoa erdialdean hasten da, gehiegizko materiala ertzera bultzatuz.\n" "Lehenetsiak bide laburreneko ordena erabiltzen du, zeina norabide batean zein bestean ibil daitekeen." @@ -13658,6 +13788,8 @@ msgstr "Geruza-denbora" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "Piezaren hozte-haizagailua gaituko da estimatutako denbora balio hau baino laburragoa duten geruzetan. Haizagailuaren abiadura gutxienekoaren eta gehienekoaren artean interpolatuko da geruzaren inprimatze-denboraren arabera." +# AI Translated +msgctxt "second" msgid "s" msgstr "s" @@ -13963,6 +14095,62 @@ msgstr "Euskarri-materiala" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Euskarri-materiala euskarriak eta euskarri-interfazeak inprimatzeko erabiltzen da normalean." +# AI Translated +msgid "Is mixed filament" +msgstr "Filamentu nahasia da" + +# AI Translated +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "Filamentu-zirrikitu hau hainbat filamentu fisikoz osatutako filamentu nahasia den adierazten du" + +# AI Translated +msgid "Mixed filament components" +msgstr "Filamentu nahasiaren osagaiak" + +# AI Translated +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "Osagai-filamentuen indizeak, komaz bereizita eta 1etik hasita; adib. \"1,3\"" + +# AI Translated +msgid "Mixed filament sublayer ratios" +msgstr "Filamentu nahasiaren azpigeruza-ratioak" + +# AI Translated +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "Komaz bereizitako ratio-balioak, guztira 1.0 ematen dutenak; adib. \"0.7,0.3\"" + +# AI Translated +msgid "Mixed filament gradient" +msgstr "Filamentu nahasiaren gradientea" + +# AI Translated +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "Z norabideko gradiente modua gaitzen du filamentu nahasiaren azpigeruzetarako. Gaituta dagoenean, azpigeruzen ratioak linealki aldatzen dira geruzaz geruza." + +# AI Translated +msgid "Mixed filament gradient range" +msgstr "Filamentu nahasiaren gradiente-barrutia" + +# AI Translated +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "Lehen osagaiaren hasierako eta amaierako ratioak gradiente moduan. Komaz bereizitako parea; adib. \"0.10,0.90\" %10etik %90era esan nahi du." + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "Filamentu nahasiaren gradiente-kurba" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "Aukerako Photoshop estiloko kurba pertsonalizatua, Z progresioa lehen osagaiaren ratioarekin lotzen duena. Barra bertikalez bereizitako kontrol-puntu gisa kodetzen da, \"x,y\" (zaharra) edo \"x,y,m_in,m_out\" formatuan, tangentea gainidatzi behar denean (balio hutsak edo \"nan\" balioak PCHIP lehenetsia erabiltzen du). x [0,1] tartean dago; y konfiguratutako ratio-barrutira mugatzen da; adib. \"0,0.15|0.5,0.50|1,0.85\". Hutsik uzten bada, gradient_range lineala erabiltzen da haren ordez." + +# AI Translated +msgid "Mixed filament per-part gradient" +msgstr "Filamentu nahasiaren gradientea pieza bakoitzeko" + +# AI Translated +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "Gradiente modua gaituta dagoenean, gradientea multzoko pieza bakoitzari modu independentean aplikatzen zaio, multzo osoa Z barruti bakar gisa hartu beharrean." + msgid "Filament printable" msgstr "Filamentua inprimagarria" @@ -14137,6 +14325,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Giroidea" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Dentsitate baxuko betegarriaren leuntze-faktorea" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Dentsitate baxuko betegarriaren izkinak zenbateraino biribiltzen diren kontrolatzen du. 0% balioak jatorrizko ibilbide zorrotza mantentzen du, eta 100% balioak ondoz ondoko betegarri-lerroen arteko kurbarik zabalenak sortzen ditu." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Hau da goiko gainazaleko betegarriaren azelerazioa. Balio txikiago batek goiko gainazalaren kalitatea hobetu dezake." @@ -14676,6 +14872,14 @@ msgstr "Inprimagailua zer G-code motarekin den bateragarria." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Saltatu G-code-aren konfigurazio-blokea" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Ez idatzi CONFIG_BLOCK (xerragailuaren konfigurazioko gako/balio bikoteak) G-code fitxategian. Lagungarria izan daiteke firmwareak iruzkin-lerro horiek prozesatzean huts egiten duen inprimagailuetan (adib. Anycubic go-klipper). Oharra: G-code fitxategiak ez ditu jada xerragailuaren ezarpenak edukiko; beraz, OrcaSlicer-era berriro inportatzeak ez du konfigurazioa berreskuratuko." + msgid "Pellet Modded Printer" msgstr "Pelletekin moldatutako inprimagailua" @@ -15198,6 +15402,8 @@ msgstr "Y ardatzaren gehieneko indarra" msgid "The allowed maximum output force of Y axis" msgstr "Y ardatzaren gehieneko irteera-indar baimendua" +# AI Translated +msgctxt "Newton" msgid "N" msgstr "N" @@ -15207,6 +15413,8 @@ msgstr "Y ardatzeko ohearen masa" msgid "The machine bed mass load of Y axis" msgstr "Y ardatzeko makinaren oheak jasaten duen masa-karga" +# AI Translated +msgctxt "gram" msgid "g" msgstr "g" @@ -15518,8 +15726,8 @@ msgstr "Mozteko eremutik hondakin-ontzira doazen hasierako eta amaierako puntuak msgid "Reduce infill retraction" msgstr "Murriztu betegarriaren atzera-egitea" -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." -msgstr "Ez egin atzera-egiterik mugimendua guztiz betegarriaren eremuan dagoenean. Horrek esan nahi du jarioa ez dela ikusiko. Honek modelo konplexuetan atzera-egite kopurua murriztu eta inprimatze-denbora aurreztu dezake, baina xerraketa eta G-codearen sorrera motelduko ditu. Kontuan izan z-hop mugimendua ez dela egiten atzera-egitea saihesten den eremuetan." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." +msgstr "Ez egin atzera-egiterik mugimendua guztiz betegarriaren eremuan dagoenean. Horrek esan nahi du jarioa ez dela ikusiko. Honek modelo konplexuetan atzera-egite kopurua murriztu eta inprimatze-denbora aurreztu dezake, baina xerraketa eta G-codearen sorrera motelduko ditu. Kontuan izan Z-hop mugimendua ez dela egiten atzera-egitea saihesten den eremuetan." msgid "This option will drop the temperature of the inactive extruders to prevent oozing." msgstr "Aukera honek aktibo ez dauden estrusoreen tenperatura jaitsiko du, jarioa saihesteko." @@ -15683,7 +15891,7 @@ msgstr "Garbitu ondorengo atzera-egite kantitatea" #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" "Garbiketa-mugimenduaren ondorengo atzera-egite azkarraren luzera, atzera-egitearen luzerari dagokionez.\n" @@ -15719,10 +15927,18 @@ msgstr "Atzera-egite luzea estrusorea aldatzean" msgid "Retraction distance when extruder change" msgstr "Atzera-egite distantzia estrusorea aldatzean" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Atzera-egitearen luzera (Erreminta aldaketa)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Erreminta aldatu aurretik atzera-egitea abiarazten denean, filamentua zehaztutako kopurua atzeratzen da (luzera filamentu gordinean neurtzen da, estrusorean sartu aurretik)." + msgid "Z-hop height" msgstr "Z jauziaren altuera" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "Atzera-egite bakoitzean pita apur bat altxatzen da pitaren eta inprimaketaren artean tartea sortzeko. Horrela, mugimenduetan pitak pieza jotzea saihesten da. Z altxatzeko espiral-lerroak erabiltzeak hariak sortzea murriztu dezake." msgid "Z-hop lower boundary" @@ -15812,6 +16028,10 @@ msgstr "Berrabiaraztean luzera gehigarria" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Mugimenduaren ondoren atzera-egitea konpentsatzen denean, estrusoreak filamentu kantitate gehigarri hau bultzatuko du. Ezarpen hau gutxitan behar da." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Berrabiaraztean luzera gehigarria (Erreminta aldaketa)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Tresna aldatu ondoren atzera-egitea konpentsatzen denean, estrusoreak filamentu kantitate gehigarri hau bultzatuko du." @@ -16220,6 +16440,14 @@ msgstr "Tresna-aldaketa purgatze-dorrean" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Behartu inprimatze-burua purgatze-dorrera joatera tresna aldatzeko agindua (Tx) eman aurretik. 2. motako purgatze-dorrea erabiltzen duten estrusore anitzeko (inprimatze-buru anitzeko) inprimagailuetarako bakarrik da garrantzitsua. Lehenespenez, Orcak ez du joan-etorria egiten inprimatze-buru anitzeko makinetan, firmwareak buruaren aldaketa kudeatzen duelako; horren ondorioz, Tx agindua inprimatutako piezaren gainean eman daiteke. Gaitu aukera hau tresna-aldaketa beti purgatze-dorrearen gainean egin dadin." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Itxaron tenperatura purgatze-dorrean" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Hartu erreminta berria inprimatze-tenperaturara iritsi arte itxaron gabe, joan purgatze-dorrera eta itxaron han tenperatura, purgatu aurretik. Berotzeak eragindako jarioa dorrean erortzen da modeloan beharrean, eta desplazamendua berotzearekin gainjartzen da. Estrusore anitzeko (inprimatze-buru anitzeko) inprimagailuetan soilik da baliagarria, 2. motako purgatze-dorrea erabiltzen dutenean. Firmwareak edo erreminta aldaketaren makroak ez du tenperaturaren zain egon behar. Desgaituta dagoenean, tenperaturaren zain egoteko agindua erreminta aldaketaren komandoaren ondoren bidaltzen da." + msgid "No sparse layers (beta)" msgstr "Geruza bakandurik ez (beta)" @@ -16431,9 +16659,9 @@ msgid "" msgstr "" "Euskarriaren lerro-patroia.\n" "\n" -"Zuhaitz-euskarrien aukera lehenetsia Hutsa da, hau da, ez dago oinarri-patroirik. Beste euskarri motetarako, aukera lehenetsia patroi zuzenekoa da.\n" +"Zuhaitz-euskarrien aukera lehenetsia Hutsa da, hau da, ez dago oinarri-patroirik. Beste euskarri motetarako, aukera lehenetsia patroi lerrozuzena da.\n" "\n" -"OHARRA: Euskarri organikoetan, bi hormak Hutsa/Lehenetsia oinarri-patroiarekin bakarrik onartzen dira. Tximistetan oinarritutako patroia Zuhaitz mehea/Indartsua/Hibridoa euskarriek bakarrik onartzen dute. Beste euskarri motetarako, Zuzenekoa erabiliko da Tximistenaren ordez." +"OHARRA: Euskarri organikoetan, bi hormak Hutsa/Lehenetsia oinarri-patroiarekin bakarrik onartzen dira. Tximista oinarri-patroia Zuhaitz mehea/Indartsua/Hibridoa euskarriek bakarrik onartzen dute. Beste euskarri motetarako, Lerrozuzena erabiliko da Tximistaren ordez." msgid "Rectilinear grid" msgstr "Sare lerrozuzena" @@ -16713,7 +16941,7 @@ msgid "" " - Each Model: centers the pattern on each connected body. Parts that touch or overlap share one center; parts detached from the rest each get their own.\n" " - Each Assembly: uses a single shared center for the whole object or assembly." msgstr "" -"Goiko eta beheko gainazaleko patroi zentratuen (Arkimedesen kordak, Oktagrama-espirala) zentroa non kokatzen den aukeratzen du.\n" +"Goiko eta beheko gainazaleko patroi zentratuen (Arkimedesen kordak, Oktagrama-kiribila) zentroa non kokatzen den aukeratzen du.\n" " - Gainazal bakoitza: patroia gainazal-eskualde bakoitzean zentratzen du, uharte bakoitza bere kabuz simetrikoa izan dadin.\n" " - Modelo bakoitza: patroia konektatutako gorputz bakoitzean zentratzen du. Elkar ukitzen edo gainjartzen diren piezek zentro bera partekatzen dute; gainerakoetatik bereizitako piezek beren zentroa dute.\n" " - Muntaketa bakoitza: zentro partekatu bakarra erabiltzen du objektu edo muntaketa osorako." @@ -16752,6 +16980,14 @@ msgstr "" "\n" "Beheko garbitu aurreko atzera-egite kantitatearen ezarpenean balio batezarriz gero, gehiegizko atzera-egitea garbiketa baino lehen egingo da; bestela, ondoren egingo da." +# AI Translated +msgid "Mixed color sublayer" +msgstr "Kolore nahasiaren azpigeruza" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "Kolore nahasiaren azpigeruzatan zatitzea gaitzen du. Gaituta dagoenean, kolore nahasiko filamentuak dituzten geruzak azpigeruzatan zatitzen dira kolore-nahasketa efektuak lortzeko." + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "Purgatze-dorrea pitaren hondarrak garbitzeko eta pitaren barruko ganbera-presioa egonkortzeko erabil daiteke, objektuetan itxura-akatsak saihesteko." @@ -17144,7 +17380,7 @@ msgid "Detect narrow internal solid infills" msgstr "Detektatu barruko betegarri solido estua" msgid "This option will auto-detect narrow internal solid infill areas. If enabled, the concentric pattern will be used for the area to speed up printing. Otherwise, the rectilinear pattern will be used by default." -msgstr "Aukera honek barruko betegarri solido estuko eremuak automatikoki detektatuko ditu. Gaituta badago, eremu horretan patroi zentrokidea erabiliko da inprimaketa azkartzeko. Bestela, patroi lerrozuzena erabiliko da lehenespenez." +msgstr "Aukera honek barruko betegarri solido estuko eremuak automatikoki detektatuko ditu. Gaituta badago, eremu horretan patroi kontzentrikoa erabiliko da inprimaketa azkartzeko. Bestela, patroi lerrozuzena erabiliko da lehenespenez." msgid "invalid value " msgstr "balio baliogabea " @@ -17810,6 +18046,10 @@ msgstr "Modelo-fitxategiaren sareztatzeak huts egin du edo ez dago baliozko form msgid "The supplied file couldn't be read because it's empty." msgstr "Emandako fitxategia ezin izan da irakurri hutsik dagoelako." +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "Fitxategi-formatua bateraezina da eta ezin da analizatu." + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Fitxategi-formatu ezezaguna: sarrerako fitxategiak .stl, .obj edo .amf(.xml) luzapena izan behar du." @@ -18703,7 +18943,7 @@ msgstr "YOLO (perfekzionista)" # AI Translated msgid "Top Surface Pattern" -msgstr "Goiko gainazalaren patroia" +msgstr "Goiko gainazaleko patroia" msgid "Choose a slot for the selected color" msgstr "Aukeratu zirrikitu bat hautatutako kolorearentzat" @@ -19271,19 +19511,19 @@ msgstr "Inprimagailuaren, filamentuen edo prozesuen aurrezarpenetan aldaketak di msgid "Only display the filament names with changes to filament presets." msgstr "Bistaratu soilik filamentu-aurrezarpenetan aldaketak dituzten filamentu-izenak." -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "Erabiltzailearen inprimagailu-aurrezarpenak dituzten inprimagailu-izenak soilik bistaratuko dira, eta hautatzen duzun aurrezarpen bakoitza zip gisa esportatuko da." msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" "Erabiltzailearen filamentu-aurrezarpenak dituzten filamentu-izenak soilik bistaratuko dira, \n" "eta hautatzen duzun filamentu-izen bakoitzeko erabiltzailearen filamentu-aurrezarpen guztiak zip gisa esportatuko dira." msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" "Prozesu-aurrezarpen aldatuak dituzten inprimagailu-izenak soilik bistaratuko dira, \n" "eta hautatzen duzun inprimagailu-izen bakoitzeko erabiltzailearen prozesu-aurrezarpen guztiak zip gisa esportatuko dira." @@ -19429,9 +19669,6 @@ msgstr "Inprimagailu fisikoa" msgid "Print Host upload" msgstr "Inprimatze-ostalariaren karga" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Hautatu sare-agentearen inplementazioa inprimagailuarekin komunikatzeko. Erabilgarri dauden agenteak abioan erregistratzen dira." - msgid "Select a Flashforge printer" msgstr "Hautatu Flashforge inprimagailu bat" @@ -19515,7 +19752,7 @@ msgstr "Kopiatu sistemaren informazioa arbelera" msgid "We need information for diagnosing source of the issue. Check wiki page for detailed guide." msgstr "Arazoaren jatorria diagnostikatzeko informazioa behar dugu. Begiratu wiki-orria gida xehatua ikusteko." -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "Paketatu botoiak proiektu-fitxategia eta uneko saioko erregistroak ZIP fitxategi batean biltzen ditu." msgid "Any additional visual examples like images or screen recordings might be helpful while reporting the issue." @@ -19557,7 +19794,7 @@ msgstr "Erregistro-maila" msgid "Stored logs" msgstr "Gordetako erregistroak" -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "Gordetako erregistro guztiak ZIP fitxategi batean paketatzen ditu." msgid "Profiles" @@ -19624,7 +19861,7 @@ msgstr "Ez da inprimagailu mota aurkitu; hautatu eskuz." msgid "Authorizing..." msgstr "Baimena ematen..." -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "Errorea. Ezin da baimenerako API tokena lortu" msgid "Could not parse server response." @@ -20075,8 +20312,9 @@ msgstr "Ezabatuta" msgid "Enable smart filament assign: Assign one filament to multiple nozzles to maximize savings" msgstr "Gaitu filamentuen esleipen adimenduna: esleitu filamentua hainbat pitatara aurrezpena maximizatzeko" -msgid "Fila Saving" -msgstr "Filamentu-aurrezpena" +# AI Translated +msgid "File Saving" +msgstr "Fitxategia gordetzea" msgid "Don't remind me again" msgstr "Ez gogorarazi berriro" @@ -20278,9 +20516,6 @@ msgstr "Ustekabeko zerbait gertatu da saioa hasten saiatzean; saiatu berriro." msgid "User canceled." msgstr "Erabiltzaileak bertan behera utzi du." -msgid "Head diameter" -msgstr "Buruaren diametroa" - msgid "Max angle" msgstr "Gehieneko angelua" @@ -20440,6 +20675,9 @@ msgstr "Berrabiarazi orain" msgid "NO RAMMING AT ALL" msgstr "MUTURRAREN MOLDAKETARIK EZ INONDIK INORA" +msgid "s" +msgstr "s" + msgid "Volumetric speed" msgstr "Abiadura bolumetrikoa" @@ -21016,6 +21254,52 @@ msgstr "" "Saihestu okertzea\n" "Ba al zenekien ABS bezalako okertzeko joera duten materialak inprimatzean ohe beroaren tenperatura egoki igotzeak okertzeko probabilitatea murriztu dezakeela?" +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "Waylanden jatorrizko zuzeneko ikuspegiak GStreamer GTK bideo-hustubidea behar du. Instalatu GStreamerrerako gtksink plugina eta berrabiarazi OrcaSlicer." + +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "Ezin izan da Waylanden jatorrizko GStreamer bideo-hustubidea hasieratu. Egiaztatu GStreamer GTK pluginaren instalazioa." + +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "Windows Media Player behar da zeregin honetarako! 'Windows Media Player' gaitu nahi duzu zure sistema eragilean?" + +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "BambuSource ez da behar bezala erregistratu multimedia erreproduzitzeko! Sakatu Bai berriro erregistratzeko. Bi aldiz galdetuko zaizu" + +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "Multimedia erreproduzitzeko erregistratutako BambuSource osagaia falta da! Berrinstalatu OrcaSlicer edo eskatu laguntza komunitateari." + +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "Beste instalazio bateko BambuSource erabiltzen ari zara; baliteke bideo-erreprodukzioak behar bezala ez funtzionatzea! Sakatu Bai konpontzeko." + +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "Zure sisteman GStreamer-erako H.264 kodekak falta dira, eta beharrezkoak dira bideoa erreproduzitzeko. (Saiatu gstreamer1.0-plugins-bad edo gstreamer1.0-libav paketeak instalatzen, eta berrabiarazi OrcaSlicer?)" + +#~ msgid "N" +#~ msgstr "N" + +#~ msgid "g" +#~ msgstr "g" + +#~ msgid "Fila Saving" +#~ msgstr "Filamentu-aurrezpena" + +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Geruza-altuera txikiegia da.\n" +#~ "min_layer_height baliora ezarriko da\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Geruza-altuerak Inprimagailuaren ezarpenak -> Estrusorea -> Geruza-altueraren mugak ataleko muga gainditzen du; horrek inprimatze-kalitateko arazoak sor ditzake." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Doitu automatikoki ezarritako barrutira?\n" + +#~ msgid "Head diameter" +#~ msgstr "Buruaren diametroa" + #~ msgid "Print order within a single layer." #~ msgstr "Geruza bakarreko inprimatze-ordena." @@ -22141,20 +22425,12 @@ msgstr "" #~ msgid "The 3mf has the following customized filament or printer presets:" #~ msgstr "3mf-ak pertsonalizatutako filamentuen edo inprimagailuaren aurrezarpen hauek dauzka:" -#~ msgid "Name of components inside step file is not UTF8 format!" -#~ msgstr "Step fitxategiko osagaien izena ez dago UTF8 formatuan!" +#~ msgid "Name of components inside step file is not UTF-8 format!" +#~ msgstr "Step fitxategiko osagaien izena ez dago UTF-8 formatuan!" #~ msgid "The name may show garbage characters!" #~ msgstr "Izenak karaktere ulergaitzak erakuts ditzake!" -#, c-format, boost-format -#~ msgid "" -#~ "The object from file %s is too small, and maybe in meters or inches.\n" -#~ " Do you want to scale to millimeters?" -#~ msgstr "" -#~ "%s fitxategiko objektua txikiegia da, eta baliteke metroetan edo hazbetetan egotea.\n" -#~ " Milimetrotara eskalatu nahi duzu?" - #~ msgid "" #~ "This file contains several objects positioned at multiple heights.\n" #~ "Instead of considering them as multiple objects, should \n" @@ -23005,24 +23281,6 @@ msgstr "" #~ msgid "Maximum detour distance for avoiding crossing wall. Don't detour if the detour distance is larger than this value. Detour length could be specified either as an absolute value or as percentage (for example 50%) of a direct travel path. Zero to disable." #~ msgstr "Hormak gurutzatzea saihesteko gehieneko desbideratze-distantzia. Ez desbideratu distantzia balio hau baino handiagoa bada. Desbideratze-luzera balio absolutu gisa edo lekualdatze-ibilbide zuzenaren ehuneko gisa zehaztu daiteke (adibidez, 50%). Zero, desgaitzeko." -#~ msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate." -#~ msgstr "Ohearen tenperatura hasierakoa ez diren geruzetarako. 0 balioak esan nahi du filamentuak ez duela plaka hotzean inprimatzea onartzen." - -#~ msgid "°C" -#~ msgstr "°C" - -#~ msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Textured Cool Plate." -#~ msgstr "Ohearen tenperatura hasierakoa ez diren geruzetarako. 0 balioak esan nahi du filamentuak ez duela testuradun plaka hotzean inprimatzea onartzen." - -#~ msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Engineering Plate." -#~ msgstr "Ohearen tenperatura hasierakoa ez diren geruzetarako. 0 balioak esan nahi du filamentuak ez duela ingeniaritza-plakan inprimatzea onartzen." - -#~ msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the High Temp Plate." -#~ msgstr "Ohearen tenperatura hasierakoa ez diren geruzetarako. 0 balioak esan nahi du filamentuak ez duela tenperatura altuko plakan inprimatzea onartzen." - -#~ msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Textured PEI Plate." -#~ msgstr "Ohearen tenperatura hasierakoa ez diren geruzetarako. 0 balioak esan nahi du filamentuak ez duela testuradun PEI plakan inprimatzea onartzen." - #~ msgid "Initial layer" #~ msgstr "Hasierako geruza" diff --git a/localization/i18n/fr/OrcaSlicer_fr.po b/localization/i18n/fr/OrcaSlicer_fr.po index 2d0fb14b5c..e56aee63a7 100644 --- a/localization/i18n/fr/OrcaSlicer_fr.po +++ b/localization/i18n/fr/OrcaSlicer_fr.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Guislain Cyril, Thomas Lété\n" @@ -15,7 +15,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n==0 || n==1) ? 0 : 1;\n" -"X-Generator: Poedit 3.6\n" +"X-Generator: Poedit 3.9\n" # AI Translated msgid "Main Extruder" @@ -2896,6 +2896,10 @@ msgstr "Éditer" msgid "Merge with" msgstr "Fusionner avec" +# AI Translated +msgid "Decompose Color" +msgstr "Décomposer la couleur" + msgid "Delete this filament" msgstr "Supprimer ce filament" @@ -3178,6 +3182,10 @@ msgstr "Assemblé" msgid "Merge parts to an object" msgstr "Fusionner les pièces en un objet" +# AI Translated +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "L’utilisation d’une hauteur de couche variable avec la sous-couche de couleur mixte peut dégrader la qualité du mélange des couleurs." + msgid "Add layers" msgstr "Ajouter des couches" @@ -4643,6 +4651,23 @@ msgstr "La température actuelle du caisson est supérieure à la température d msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "La température minimale du caisson (%d℃) est supérieure à la température cible du caisson (%d℃). La valeur minimale est le seuil à partir duquel l’impression démarre tandis que le caisson continue de chauffer vers la cible ; elle ne doit donc pas la dépasser. Elle sera limitée à la cible." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "La hauteur de couche est trop faible. Elle sera définie au minimum (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "La hauteur de couche est en dehors des limites définies dans Paramètres de l’imprimante -> Extrudeur -> Limites de la hauteur de la couche, ce qui peut entraîner des problèmes de qualité d’impression." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "L’ajuster automatiquement à la limite (%g mm) ?" + +msgid "Adjust" +msgstr "Ajuster" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4690,7 +4715,7 @@ msgstr "" "\n" "La valeur sera remise à 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "La paroi supplémentaire alternée ne fonctionne pas bien lorsque le paramètre Assurer l’épaisseur de la coque verticale est réglée sur Tous." msgid "" @@ -4736,7 +4761,7 @@ msgstr "" "NON - Conserver la hauteur de la couche de support indépendante" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height doit être inférieur à la hauteur de couche.\n" @@ -4744,7 +4769,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "La profondeur de verrouillage doit être inférieure à la profondeur de la peau.\n" @@ -4762,6 +4787,13 @@ msgstr "" "Oui - Activer le générateur de parois Arachne\n" "Non - Désactiver le générateur de parois Arachne et définir le mode [Déplacement] de la surface irrégulière" +# AI Translated +msgid "Brim ear radius" +msgstr "Rayon de la bordure à oreilles" + +msgid "Brim width" +msgstr "Largeur de la bordure" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Le mode spirale ne fonctionne que lorsque le nombre de parois est 1, le support est désactivé, la détection d'agglomération par sondage est désactivée, les couches supérieures sont à 0, la densité de remplissage clairsemé est à 0 et le type de timelapse est traditionnel." @@ -4835,7 +4867,7 @@ msgid "Calibrating the micro lidar" msgstr "Calibrage du micro-Lidar" msgid "Calibrating flow ratio" -msgstr "Calibration du ratio de débit" +msgstr "Calibration du rapport de débit" msgid "Pause (nozzle temperature malfunction)" msgstr "Pause (dysfonctionnement de la température de la buse)" @@ -5016,6 +5048,14 @@ msgstr "Échec de la génération du G-code de calibration" msgid "Calibration error" msgstr "Erreur de la calibration" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Cette imprimante ne dispose pas du matériel requis par ce contrôle." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Ce contrôle n’est pas pris en charge sur cette imprimante." + # AI Translated msgid "Network unavailable" msgstr "Réseau indisponible" @@ -5871,7 +5911,7 @@ msgstr "Volume :" msgid "Size:" msgstr "Taille :" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Des conflits de chemins G-code ont été trouvés au niveau de la couche %d, z = %.2lfmm. Veuillez séparer davantage les objets en conflit (%s <-> %s)." @@ -6052,6 +6092,10 @@ msgstr "Multi-appareils" msgid "Project" msgstr "Projet" +# AI Translated +msgid "Device (Web)" +msgstr "Appareil (Web)" + msgid "Yes" msgstr "Oui" @@ -6185,10 +6229,10 @@ msgstr "Importer des fichiers 3MF/STL/STEP/SVG/OBJ/AMF" msgid "Load a model" msgstr "Charger un modèle" -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "Importer une archive Zip" -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "Charger les modèles contenus dans une archive zip" msgid "Import Configs" @@ -7434,11 +7478,11 @@ msgstr "Erreur lors du chargement des shaders" msgctxt "Layers" msgid "Top" -msgstr "Du haut" +msgstr "Supérieur" msgctxt "Layers" msgid "Bottom" -msgstr "Du bas" +msgstr "Inférieur" # AI Translated msgid "Plugin Selection" @@ -7697,6 +7741,10 @@ msgstr "Personnaliser le plateau actuel" msgid "The %s nozzle can not print %s." msgstr "La buse %s ne peut pas imprimer %s." +# AI Translated +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "L’impression d’un filament de couleur mixte sur une imprimante à extrudeur unique nécessite des changements de filament et des purges fréquents, ce qui peut augmenter considérablement les déchets et le risque d’obstruction de la buse ou de la goulotte à déchets." + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "Le mélange de %1% avec %2% lors de l'impression n'est pas recommandé.\n" @@ -7822,12 +7870,44 @@ msgstr "Synchroniser la liste des filaments depuis l'AMS" msgid "Set filaments to use" msgstr "Définir les filaments à utiliser" +# AI Translated +msgid "Add Mixed Filament" +msgstr "Ajouter un filament mixte" + +# AI Translated +msgid "Mixed Filament" +msgstr "Filament mixte" + +# AI Translated +msgid "Remove last mixed filament" +msgstr "Supprimer le dernier filament mixte" + +# AI Translated +msgid "Add mixed filament" +msgstr "Ajouter un filament mixte" + +# AI Translated +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "Le filament mixte comporte des composants invalides ou incohérents. Veuillez modifier à nouveau les entrées concernées." + msgid "Search plate, object and part." msgstr "Recherche de plaque, d'objet et de pièce." msgid "Pellets" msgstr "Granulés" +# AI Translated +msgid "Mixed filament has broken component references" +msgstr "Le filament mixte comporte des références de composants invalides" + +# AI Translated +msgid "Edit / Delete / Merge" +msgstr "Éditer / Supprimer / Fusionner" + +# AI Translated +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "Le filament mixte cible utilise ce filament physique comme composant. La fusion supprimera ce filament physique et risque de rendre le filament mixte invalide. Continuer ?" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "Une fois votre opération terminée, le projet %s sera fermé et un nouveau projet sera créé." @@ -7964,7 +8044,7 @@ msgstr "Veuillez vous assurer que les G-codes de ces préréglages sont sûrs af msgid "Customized Preset" msgstr "Préréglage personnalisé" -msgid "Component name(s) inside step file not in UTF8 format!" +msgid "Component name(s) inside step file not in UTF-8 format!" msgstr "Le nom des composants dans le fichier STEP n'est pas au format UTF-8 !" msgid "Because of unsupported text encoding, garbage characters may appear!" @@ -7986,10 +8066,10 @@ msgstr "Le volume de l'objet est nul" #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "L’objet du fichier %s est trop petit, et est peut-être en mètres ou en pouces.\n" -" Voulez-vous le mettre à l’échelle en millimètres ?" +"Voulez-vous le mettre à l’échelle en millimètres ?" msgid "Object too small" msgstr "Objet trop petit" @@ -8006,6 +8086,14 @@ msgstr "" msgid "Multi-part object detected" msgstr "Objet en plusieurs pièces détecté" +# AI Translated +msgid "Matching textures to filaments" +msgstr "Association des textures aux filaments" + +# AI Translated +msgid "Texture Import Warning" +msgstr "Avertissement d’importation de texture" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Charger ces fichiers en tant qu'objet unique avec plusieurs parties ?\n" @@ -8120,21 +8208,21 @@ msgstr "Le répertoire pour le remplacement n'a pas été sélectionné" msgid "Replaced with 3D files from directory:\n" msgstr "Remplacé par des fichiers 3D depuis le répertoire :\n" -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ Ignoré %1% : même fichier.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ Ignoré %s : même fichier.\n" -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ Ignoré %1% : le fichier n'existe pas.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ Ignoré %s : le fichier n'existe pas.\n" -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ Ignoré %1% : échec du remplacement.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ Ignoré %s : échec du remplacement.\n" -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔ Remplacé %1%.\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔ Remplacé %s.\n" msgid "Replaced volumes" msgstr "Volumes remplacés" @@ -8210,6 +8298,22 @@ msgstr "" msgid "Sync now" msgstr "Synchroniser maintenant" +# AI Translated +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "Échec de l’importation de la texture. Le modèle semble contenir des données de texture, mais le processus d’importation n’a pas pu être mené à son terme. Le modèle sera importé en tant que géométrie uniquement." + +# AI Translated +msgid "Applying texture colors..." +msgstr "Application des couleurs de texture..." + +# AI Translated +msgid "Updating 3D view..." +msgstr "Mise à jour de la vue 3D..." + +# AI Translated +msgid "Texture colors applied." +msgstr "Couleurs de texture appliquées." + msgid "You can keep the modified presets for the new project or discard them" msgstr "Vous pouvez conserver les préréglages modifiés dans le nouveau projet ou les supprimer" @@ -8857,6 +8961,18 @@ msgstr "Si cette option est activée, vous pouvez envoyer une tâche à plusieur msgid "Pop up to select filament grouping mode" msgstr "Fenêtre contextuelle pour sélectionner le mode de regroupement des filaments" +# AI Translated +msgid "Visible plugin pages" +msgstr "Pages de plugins visibles" + +# AI Translated +msgid "pages" +msgstr "pages" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Nombre de pages de plugins affichées sous forme d’onglets fixes avant que les pages restantes ne soient regroupées dans un menu déroulant sur le dernier onglet." + msgid "Behaviour" msgstr "Comportement" @@ -9211,6 +9327,18 @@ msgstr "Afficher les préréglages non pris en charge" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Affiche les préréglages incompatibles ou non pris en charge dans les listes déroulantes d’imprimantes et de filaments. Ces préréglages ne peuvent pas être sélectionnés." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Expérimental) Utiliser les agents d’imprimante au lieu des hôtes d’impression" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Achemine les tâches d’impression des imprimantes non Bambu via les agents de plugin d’imprimante au lieu du flux classique d’envoi vers l’hôte d’impression.\n" +"Lorsque cette option est désactivée, OrcaSlicer utilise l’ancien comportement de l’hôte d’impression." + msgid "Experimental Features" msgstr "Fonctionnalités expérimentales" @@ -9283,22 +9411,22 @@ msgid "DEV host: api-dev.bambu-lab.com/v1" msgstr "Hôte DEV : api-dev.bambu-lab.com/v1" msgid "QA host: api-qa.bambu-lab.com/v1" -msgstr "Hôte AQ : api-qa.bambu-lab.com/v1" +msgstr "Hôte QA : api-qa.bambu-lab.com/v1" msgid "PRE host: api-pre.bambu-lab.com/v1" -msgstr "Hébergeur PRE : api-pre.bambu-lab.com/v1" +msgstr "Hôte PRE : api-pre.bambu-lab.com/v1" msgid "Product host" msgstr "Hôte du produit" msgid "Debug save button" -msgstr "bouton d'enregistrement de débogage" +msgstr "Bouton d'enregistrement de debugage" msgid "Save debug settings" -msgstr "enregistrer les paramètres de débogage" +msgstr "Enregistrer les paramètres de debugage" msgid "Debug settings have been saved successfully!" -msgstr "Les paramètres DEBUG ont été enregistrés avec succès !" +msgstr "Les paramètres de debug ont été enregistrés avec succès !" msgid "Cloud environment switched; please login again!" msgstr "L'environnement Cloud a changé, veuillez vous reconnecter !" @@ -9417,6 +9545,10 @@ msgstr "Vase spirale" msgid "First layer filament sequence" msgstr "Séquence d’impression de la première couche" +# AI Translated +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "La liste des filaments contient des filaments mixtes. La séquence de filaments personnalisée ne sera pas appliquée." + msgid "By Layer" msgstr "Par Couche" @@ -9472,9 +9604,25 @@ msgstr "Préréglage utilisateur" msgid "Preset Inside Project" msgstr "Préréglage intégré au projet" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Copie dans ce préréglage toutes les valeurs héritées du préréglage parent et supprime le lien d’héritage. Les préréglages compatibles uniquement avec le parent peuvent devenir incompatibles." + msgid "Detach from parent" msgstr "Détacher du parent" +# AI Translated +msgid "Unique preset" +msgstr "Préréglage unique" + +# AI Translated +msgid "Parent preset" +msgstr "Préréglage parent" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Ce préréglage n’hérite d’aucun autre préréglage." + msgid "Name is unavailable." msgstr "Le nom n'est pas disponible." @@ -10211,27 +10359,11 @@ msgstr "Voulez-vous vraiment activer cette option ?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Les motifs de remplissage sont généralement conçus pour gérer la rotation automatiquement afin d'assurer une impression correcte et d'atteindre les effets souhaités (ex. : Gyroïde, Cubique). La rotation du motif de remplissage clairsemé actuel peut entraîner un support insuffisant. Veuillez procéder avec précaution et vérifier soigneusement tout problème d'impression potentiel. Voulez-vous vraiment activer cette option ?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"La hauteur de couche est trop faible.\n" -"Elle sera définie à min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "La hauteur de la couche dépasse la limite fixée dans Paramètres de l’imprimante -> Extrudeur -> Limites de la hauteur de la couche, ce qui peut entraîner des problèmes de qualité d’impression." - -msgid "Adjust to the set range automatically?\n" -msgstr "S’ajuster automatiquement à la plage définie ?\n" - -msgid "Adjust" -msgstr "Ajuster" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." -msgstr "Fonction expérimentale : Rétracter et couper le filament à une plus grande distance lors des changements de filament afin de minimiser le rinçage. Bien que cela puisse réduire considérablement le rinçage, cela peut également augmenter le risque de bouchage des buses ou d’autres complications d’impression." +msgstr "Fonction expérimentale : Rétracter et couper le filament à une plus grande distance lors des changements de filament afin de minimiser la purge. Bien que cela puisse réduire considérablement la purge, cela peut également augmenter le risque de bouchage des buses ou d’autres complications d’impression." msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications. Please use with the latest printer firmware." -msgstr "Fonction expérimentale : Rétracter et couper le filament à une plus grande distance lors des changements de filament afin de minimiser l’affleurement. Bien que cela puisse réduire sensiblement l’affleurement, cela peut également augmenter le risque d’obstruction des buses ou d’autres complications d’impression. Veuillez utiliser le dernier micrologiciel de l’imprimante." +msgstr "Fonction expérimentale : Rétracter et couper le filament à une plus grande distance lors des changements de filament afin de minimiser la purge. Bien que cela puisse réduire sensiblement la purge, cela peut également augmenter le risque d’obstruction des buses ou d’autres complications d’impression. Veuillez utiliser le dernier micrologiciel de l’imprimante." msgid "" "When recording timelapse without toolhead, it is recommended to add a \"Timelapse Wipe Tower\" \n" @@ -10422,6 +10554,9 @@ msgstr "Mots clés réservés trouvés" msgid "Setting Overrides" msgstr "Forçage des réglages" +msgid "Retraction when switching material" +msgstr "Rétraction lors du changement de matériau" + msgid "Basic information" msgstr "Informations de base" @@ -10548,6 +10683,12 @@ msgstr "Profils de traitement compatibles" msgid "Printable space" msgstr "Espace imprimable" +msgid "Printer Agent" +msgstr "Agent d'imprimante" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Sélectionner l'implémentation de l'agent réseau pour la communication avec l'imprimante. Les agents disponibles sont enregistrés au démarrage." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10638,7 +10779,7 @@ msgid "Acceleration limitation" msgstr "Limitation d'accélération" msgid "Jerk limitation" -msgstr "Limitation des secousses" +msgstr "Limitation du jerk" msgid "Single extruder multi-material setup" msgstr "Configuration multi-matériaux pour extrudeur unique" @@ -10673,9 +10814,6 @@ msgstr "Limites de hauteur de couche" msgid "Z-Hop" msgstr "Saut en Z" -msgid "Retraction when switching material" -msgstr "Rétraction lors du changement de matériau" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -10788,11 +10926,11 @@ msgstr "%s : %s" msgid "No modifications need to be copied." msgstr "Aucune modification à copier." -msgid "Copy paramters" +msgid "Copy parameters" msgstr "Copier les paramètres" #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "Modifier les paramètres de %s" #, c-format, boost-format @@ -11303,27 +11441,6 @@ msgstr "Volumes de purge pour le changement de filament" msgid "Please choose the filament colour" msgstr "Veuillez choisir la couleur du filament" -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "L’aperçu en direct natif sous Wayland nécessite le récepteur vidéo GStreamer GTK. Veuillez installer le plugin gtksink pour GStreamer, puis redémarrer OrcaSlicer." - -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "Échec de l’initialisation du récepteur vidéo GStreamer natif sous Wayland. Veuillez vérifier l’installation de votre plugin GStreamer GTK." - -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "Windows Media Player est nécessaire pour cette tâche ! Voulez-vous activer ‘Windows Media Player’ pour votre système d’exploitation ?" - -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "BambuSource n’a pas été correctement enregistré pour la lecture de médias ! Appuyez sur Oui pour le réenregistrer. Vous recevrez deux fois la demande de permission" - -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "Composant BambuSource manquant pour la lecture multimédia ! Veuillez réinstaller OrcaSlicer ou demander de l'aide à la communauté." - -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "Si vous utilisez une BambuSource provenant d’une autre installation, la lecture de la vidéo peut ne pas fonctionner correctement ! Appuyez sur Oui pour résoudre le problème." - -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "Il manque à votre système les codecs H.264 pour GStreamer, qui sont nécessaires pour lire la vidéo. (Essayez d’installer les paquets gstreamer1.0-plugins-bad ou gstreamer1.0-libav, puis redémarrez Orca Slicer)." - msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "L’agent cloud n’est pas disponible. Veuillez redémarrer OrcaSlicer et réessayer." @@ -12010,6 +12127,10 @@ msgstr " est trop proche d'une zone d'exclusion. Cela va entraîner des collisio msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " est trop proche de la zone de détection d'agglomération, et des collisions seront causées.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " est partiellement en dehors de la zone imprimable et ne peut pas être imprimé.\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Les températures de buse sélectionnées sont incompatibles. La température de buse de chaque filament doit se situer dans la plage de température de buse recommandée des autres filaments. Sinon, un bouchage de la buse ou des dommages à l’imprimante peuvent survenir." @@ -12022,6 +12143,10 @@ msgstr "Si vous souhaitez tout de même imprimer, vous pouvez activer l’option msgid "No extrusions under current settings." msgstr "Aucune extrusion dans les paramètres actuels." +# AI Translated +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "Un filament mixte avec dégradé est utilisé, mais « Sous-couche de couleur mixte » est désactivé. Le dégradé ne sera pas imprimé." + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Le mode fluide du timelapse n'est pas pris en charge lorsque le mode d'impression « par objet » est activé." @@ -12058,6 +12183,10 @@ msgstr "Vous devez réduire la taille de votre modèle ou modifier les paramètr msgid "Variable layer height is not supported with Organic supports." msgstr "La hauteur de couche variable n’est pas prise en charge avec les supports organiques." +# AI Translated +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "Le filament de la tour d’essuyage ne peut pas être un filament mixte." + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Différents diamètres de buses et de filaments peuvent ne pas fonctionner correctement lorsque la tour d’amorçage est activée. Il s’agit d’un projet très expérimental, il convient donc de procéder avec prudence." @@ -12156,7 +12285,7 @@ msgid "Plate %d: %s does not support filament %s" msgstr "Plaque %d : %s ne prend pas en charge le filament %s" msgid "Setting the jerk speed too low could lead to artifacts on curved surfaces" -msgstr "Un réglage trop bas de la vitesse de saccade peut entraîner des artefacts sur les surfaces courbes" +msgstr "Un réglage trop bas de la vitesse de jerk peut entraîner des artefacts sur les surfaces courbes" msgid "" "The jerk setting exceeds the printer's maximum jerk (machine_max_jerk_x/machine_max_jerk_y).\n" @@ -12164,7 +12293,7 @@ msgid "" "You can adjust the maximum jerk setting in your printer's configuration to get higher speeds." msgstr "" "Le réglage du jerk dépasse le jerk maximum de l’imprimante (machine_max_jerk_x/machine_max_jerk_y).\n" -"Orca plafonne automatiquement la vitesse de l’impulsion pour s’assurer qu’elle ne dépasse pas les capacités de l’imprimante.\n" +"Orca plafonne automatiquement la vitesse du jerk pour s’assurer qu’elle ne dépasse pas les capacités de l’imprimante.\n" "Vous pouvez ajuster le réglage du jerk maximum dans la configuration de votre imprimante pour obtenir des vitesses plus élevées." msgid "" @@ -12323,9 +12452,6 @@ msgstr "Utiliser le 3MF au lieu du G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Activez ceci si l’imprimante accepte un fichier 3MF comme tâche d’impression. Lorsque cette option est activée, Orca Slicer envoie le fichier découpé au format .gcode.3mf au lieu d’un simple fichier .gcode." -msgid "Printer Agent" -msgstr "Agent d'imprimante" - msgid "Select the network agent implementation for printer communication." msgstr "Sélectionner l'implémentation de l'agent réseau pour la communication avec l'imprimante." @@ -12408,7 +12534,7 @@ msgstr "mm ou %" msgid "Other layers" msgstr "Autres couches" -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." msgstr "Température du plateau pour les couches sauf la première. Une valeur de 0 signifie que le filament ne prend pas en charge l'impression sur la Cool Plate SuperTack." msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." @@ -12689,7 +12815,7 @@ msgstr "" "Si réglée à 0, la largeur de ligne correspond à celle du remplissage plein interne." msgid "Internal bridge flow ratio" -msgstr "Ratio de débit du pont interne" +msgstr "Rapport de débit du pont interne" msgid "" "This value governs the thickness of the internal bridge layer. This is the first layer over sparse infill so increasing it may increase strength and upper layer quality.\n" @@ -12729,13 +12855,13 @@ msgstr "" "Le débit réel du remplissage solide inférieur utilisé est calculé en multipliant cette valeur par le rapport de débit du filament et, s’il est défini, par le rapport de débit de l’objet." msgid "Set other flow ratios" -msgstr "Définir d'autres ratios de débit" +msgstr "Définir d'autres rapports de débit" msgid "Change flow ratios for other extrusion path types." -msgstr "Modifier les ratios de débit pour d'autres types de chemin d'extrusion." +msgstr "Modifier les rapports de débit pour d'autres types de chemin d'extrusion." msgid "First layer flow ratio" -msgstr "Ratio de débit de la première couche" +msgstr "Rapport de débit de la première couche" msgid "" "This factor affects the amount of material on the first layer for the extrusion path roles listed in this section.\n" @@ -12744,10 +12870,10 @@ msgid "" msgstr "" "Ce facteur affecte la quantité de matériau sur la première couche pour les rôles de chemin d'extrusion listés dans cette section.\n" "\n" -"Pour la première couche, le ratio de débit réel pour chaque rôle de chemin (n'affecte pas les bordures et les jupes) sera multiplié par cette valeur." +"Pour la première couche, le rapport de débit réel pour chaque rôle de chemin (n'affecte pas les bordures et les jupes) sera multiplié par cette valeur." msgid "Outer wall flow ratio" -msgstr "Ratio de débit de la paroi extérieure" +msgstr "Rapport de débit de la paroi extérieure" msgid "" "This factor affects the amount of material for outer walls.\n" @@ -12756,10 +12882,10 @@ msgid "" msgstr "" "Ce facteur affecte la quantité de matériau pour les parois extérieures.\n" "\n" -"Le débit réel de la paroi extérieure est calculé en multipliant cette valeur par le ratio de débit du filament, et si défini, le ratio de débit de l'objet." +"Le débit réel de la paroi extérieure est calculé en multipliant cette valeur par le rapport de débit du filament, et si défini, le rapport de débit de l'objet." msgid "Inner wall flow ratio" -msgstr "Ratio de débit de la paroi intérieure" +msgstr "Rapport de débit de la paroi intérieure" msgid "" "This factor affects the amount of material for inner walls.\n" @@ -12768,10 +12894,10 @@ msgid "" msgstr "" "Ce facteur affecte la quantité de matériau pour les parois intérieures.\n" "\n" -"Le débit réel de la paroi intérieure est calculé en multipliant cette valeur par le ratio de débit du filament, et si défini, le ratio de débit de l'objet." +"Le débit réel de la paroi intérieure est calculé en multipliant cette valeur par le rapport de débit du filament, et si défini, le rapport de débit de l'objet." msgid "Overhang flow ratio" -msgstr "Ratio de débit de surplomb" +msgstr "Rapport de débit de surplomb" msgid "" "This factor affects the amount of material for overhangs.\n" @@ -12780,10 +12906,10 @@ msgid "" msgstr "" "Ce facteur affecte la quantité de matériau pour les surplombs.\n" "\n" -"Le débit réel de surplomb est calculé en multipliant cette valeur par le ratio de débit du filament, et si défini, le ratio de débit de l'objet." +"Le débit réel de surplomb est calculé en multipliant cette valeur par le rapport de débit du filament, et si défini, le rapport de débit de l'objet." msgid "Sparse infill flow ratio" -msgstr "Ratio de débit du remplissage clairsemé" +msgstr "Rapport de débit du remplissage clairsemé" msgid "" "This factor affects the amount of material for sparse infill.\n" @@ -12792,10 +12918,10 @@ msgid "" msgstr "" "Ce facteur affecte la quantité de matériau pour le remplissage clairsemé.\n" "\n" -"Le débit réel du remplissage clairsemé est calculé en multipliant cette valeur par le ratio de débit du filament, et si défini, le ratio de débit de l'objet." +"Le débit réel du remplissage clairsemé est calculé en multipliant cette valeur par le rapport de débit du filament, et si défini, le rapport de débit de l'objet." msgid "Internal solid infill flow ratio" -msgstr "Ratio de débit du remplissage solide interne" +msgstr "Rapport de débit du remplissage solide interne" msgid "" "This factor affects the amount of material for internal solid infill.\n" @@ -12804,10 +12930,10 @@ msgid "" msgstr "" "Ce facteur affecte la quantité de matériau pour le remplissage solide interne.\n" "\n" -"Le débit réel du remplissage solide interne est calculé en multipliant cette valeur par le ratio de débit du filament, et si défini, le ratio de débit de l'objet." +"Le débit réel du remplissage solide interne est calculé en multipliant cette valeur par le rapport de débit du filament, et si défini, le rapport de débit de l'objet." msgid "Gap fill flow ratio" -msgstr "Ratio de débit du remplissage des espaces" +msgstr "Rapport de débit du remplissage des espaces" msgid "" "This factor affects the amount of material for filling the gaps.\n" @@ -12816,10 +12942,10 @@ msgid "" msgstr "" "Ce facteur affecte la quantité de matériau pour le remplissage des espaces.\n" "\n" -"Le débit réel du remplissage des espaces est calculé en multipliant cette valeur par le ratio de débit du filament, et si défini, le ratio de débit de l'objet." +"Le débit réel du remplissage des espaces est calculé en multipliant cette valeur par le rapport de débit du filament, et si défini, le rapport de débit de l'objet." msgid "Support flow ratio" -msgstr "Ratio de débit des supports" +msgstr "Rapport de débit des supports" msgid "" "This factor affects the amount of material for support.\n" @@ -12828,10 +12954,10 @@ msgid "" msgstr "" "Ce facteur affecte la quantité de matériau pour les supports.\n" "\n" -"Le débit réel des supports est calculé en multipliant cette valeur par le ratio de débit du filament, et si défini, le ratio de débit de l'objet." +"Le débit réel des supports est calculé en multipliant cette valeur par le rapport de débit du filament, et si défini, le rapport de débit de l'objet." msgid "Support interface flow ratio" -msgstr "Ratio de débit de l'interface de support" +msgstr "Rapport de débit de l'interface de support" msgid "" "This factor affects the amount of material for the support interface.\n" @@ -12840,7 +12966,7 @@ msgid "" msgstr "" "Ce facteur affecte la quantité de matériau pour l'interface de support.\n" "\n" -"Le débit réel de l'interface de support est calculé en multipliant cette valeur par le ratio de débit du filament, et si défini, le ratio de débit de l'objet." +"Le débit réel de l'interface de support est calculé en multipliant cette valeur par le rapport de débit du filament, et si défini, le rapport de débit de l'objet." msgid "Precise wall" msgstr "Parois précises" @@ -13000,9 +13126,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Vitesse des ponts internes. Si la valeur est exprimée en pourcentage, elle sera calculée sur la base de la vitesse du pont. La valeur par défaut est 150%." -msgid "Brim width" -msgstr "Largeur de la bordure" - msgid "This is the distance from the model to the outermost brim line." msgstr "Distance du modèle à la ligne de bord la plus externe" @@ -13043,10 +13166,10 @@ msgid "" "\n" "If your current setup already works well, enabling it may be unnecessary and can cause the brim to fuse with upper layers." msgstr "" -"Lorsqu'il est activé, le bordure est aligné avec la géométrie du périmètre de la première couche après l'application de la compensation du pied d'éléphant.\n" -"Cette option est destinée aux cas où la compensation du pied d'éléphant modifie considérablement l’empreinte de la première couche.\n" +"Lorsqu'il est activé, la bordure est alignée avec la géométrie du périmètre de la première couche après l'application de la compensation de la patte d'éléphant.\n" +"Cette option est destinée aux cas où la compensation de la patte d'éléphant modifie considérablement l’empreinte de la première couche.\n" "\n" -"Si votre configuration actuelle fonctionne déjà bien, son activation peut être inutile et peut provoquer la fusion du bordure avec les couches supérieures." +"Si votre configuration actuelle fonctionne déjà bien, son activation peut être inutile et peut provoquer la fusion de la bordure avec les couches supérieures." msgid "Combine brims" msgstr "Combiner les bordures" @@ -13082,6 +13205,14 @@ msgstr "" "La géométrie sera décimée avant de détecter les angles vifs. Ce paramètre indique la longueur minimale de l’écart pour la décimation.\n" "0 pour désactiver" +# AI Translated +msgid "Brim ears outer only" +msgstr "Bordure à oreilles sur le contour extérieur uniquement" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Génère des oreilles de souris uniquement sur le contour extérieur du modèle, en excluant les trous et les sections fermées." + msgid "upward compatible machine" msgstr "machine à compatibilité ascendante" @@ -13643,7 +13774,7 @@ msgid "" msgstr "" "Le matériau peut présenter un changement volumétrique après le passage de l’état fondu à l’état cristallin. Ce paramètre modifie proportionnellement tous les débits d’extrusion de ce filament dans le G-code. La valeur recommandée est comprise entre 0,95 et 1,05. Vous pouvez peut-être ajuster cette valeur pour obtenir une belle surface plate lorsqu’il y a un léger débordement ou un sous-débordement.\n" "\n" -"Le ratio de débit de l’objet final est cette valeur multipliée par le ratio de débit du filament." +"Le rapport de débit de l’objet final est cette valeur multipliée par le rapport de débit du filament." msgid "Enable pressure advance" msgstr "Activer la Pressure Advance" @@ -13759,6 +13890,8 @@ msgstr "Temps de couche" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "Le ventilateur de refroidissement des pièces sera activé pour les couches dont le temps estimé est inférieur à cette valeur. La vitesse du ventilateur est interpolée entre les vitesses minimale et maximale du ventilateur en fonction du temps d'impression de la couche" +# AI Translated +msgctxt "second" msgid "s" msgstr "s" @@ -14065,6 +14198,62 @@ msgstr "Supports" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Le matériau de support est généralement utilisé pour imprimer le support et les interfaces de support" +# AI Translated +msgid "Is mixed filament" +msgstr "Est un filament mixte" + +# AI Translated +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "Indique si cet emplacement de filament est un filament mixte composé de plusieurs filaments physiques" + +# AI Translated +msgid "Mixed filament components" +msgstr "Composants du filament mixte" + +# AI Translated +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "Indices (à partir de 1) des filaments composants, séparés par des virgules ; par ex. \"1,3\"" + +# AI Translated +msgid "Mixed filament sublayer ratios" +msgstr "Proportions des sous-couches du filament mixte" + +# AI Translated +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "Valeurs de proportion séparées par des virgules dont la somme vaut 1.0 ; par ex. \"0.7,0.3\"" + +# AI Translated +msgid "Mixed filament gradient" +msgstr "Dégradé du filament mixte" + +# AI Translated +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "Active le mode dégradé dans la direction Z pour les sous-couches du filament mixte. Lorsqu’il est activé, les proportions des sous-couches varient linéairement d’une couche à l’autre." + +# AI Translated +msgid "Mixed filament gradient range" +msgstr "Plage du dégradé du filament mixte" + +# AI Translated +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "Proportions de début et de fin du premier composant en mode dégradé. Paire séparée par une virgule ; par ex. \"0.10,0.90\" signifie de 10% à 90%." + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "Courbe du dégradé du filament mixte" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "Courbe personnalisée facultative, de style Photoshop, associant la progression en Z à la proportion du premier composant. Encodée sous forme de points de contrôle séparés par des barres verticales, au format \"x,y\" (ancien) ou \"x,y,m_in,m_out\" lorsqu’une tangente doit être forcée (une valeur vide ou \"nan\" utilise la valeur PCHIP par défaut). x est compris dans [0,1] ; y est limité à la plage de proportions configurée ; par ex. \"0,0.15|0.5,0.50|1,0.85\". Si le champ est vide, le gradient_range linéaire est utilisé à la place." + +# AI Translated +msgid "Mixed filament per-part gradient" +msgstr "Dégradé par pièce du filament mixte" + +# AI Translated +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "Lorsque le mode dégradé est activé, applique le dégradé à chaque pièce d’un assemblage indépendamment au lieu de traiter l’assemblage entier comme une seule plage Z." + msgid "Filament printable" msgstr "Filament imprimable" @@ -14236,6 +14425,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroïde" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Facteur de lissage du remplissage" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Contrôle le degré d’arrondi des angles du remplissage. 0% conserve le tracé anguleux d’origine, tandis que 100% produit les courbes les plus amples possibles entre les lignes de remplissage adjacentes." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Il s'agit de l'accélération de la surface supérieure du remplissage. Utiliser une valeur plus petite pourrait améliorer la qualité de la surface supérieure" @@ -14774,6 +14971,14 @@ msgstr "Avec quel type de G-code l'imprimante est-elle compatible." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Omettre le bloc de configuration du G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "N’écrit pas le CONFIG_BLOCK (les paires clé/valeur de la configuration du logiciel de découpe) dans le fichier G-code. Cela peut aider avec les imprimantes dont le firmware plante lors de l’analyse de ces lignes de commentaire (par ex. Anycubic go-klipper). Remarque : le fichier G-code ne contiendra plus les réglages du logiciel de découpe, sa réimportation dans OrcaSlicer ne restaurera donc pas la configuration." + msgid "Pellet Modded Printer" msgstr "Imprimante à pellets" @@ -15294,6 +15499,7 @@ msgid "The allowed maximum output force of Y axis" msgstr "Force de sortie maximale autorisée sur l'axe Y" # AI Translated +msgctxt "Newton" msgid "N" msgstr "N" @@ -15304,6 +15510,7 @@ msgid "The machine bed mass load of Y axis" msgstr "Charge massique du plateau de la machine sur l'axe Y" # AI Translated +msgctxt "gram" msgid "g" msgstr "g" @@ -15615,7 +15822,7 @@ msgstr "Les points de départ et d'arrivée qui se situent entre la zone de coup msgid "Reduce infill retraction" msgstr "Réduire la rétraction du remplissage" -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." msgstr "Ne pas effectuer de rétraction lors de déplacement en zone de remplissage car même si l’extrudeur suinte, les coulures ne seraient pas visibles. Cela peut réduire les rétractions pour les modèles complexes et économiser du temps d’impression, mais ralentit la découpe et la génération du G-code" msgid "This option will drop the temperature of the inactive extruders to prevent oozing." @@ -15785,7 +15992,7 @@ msgstr "Quantité de rétraction après essuyage" # AI Translated #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" "La longueur de la rétraction rapide après l'essuyage, relative à la longueur de rétraction.\n" @@ -15821,10 +16028,18 @@ msgstr "Rétraction longue lors du changement d'extrudeur" msgid "Retraction distance when extruder change" msgstr "Distance de rétraction lors du changement d'extrudeur" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Longueur de rétraction (Changement d’outil)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Lorsque la rétraction est déclenchée avant un changement d’outil, le filament est rétracté de la quantité spécifiée (la longueur est mesurée sur le filament brut, avant son entrée dans l’extrudeur)." + msgid "Z-hop height" msgstr "Hauteur du saut en Z" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "Chaque fois que la rétraction est effectuée, la buse est légèrement soulevée pour créer un espace entre la buse et l'impression. Il empêche la buse de toucher l'impression lors du déplacement. L'utilisation d'une ligne en spirale pour soulever z peut empêcher l'enfilage" msgid "Z-hop lower boundary" @@ -15914,6 +16129,10 @@ msgstr "Longueur supplémentaire" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Lorsque la rétraction est compensée après le mouvement de déplacement, l’extrudeuse poussera cette quantité supplémentaire de filament. Ce paramètre est rarement nécessaire." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Longueur supplémentaire à la reprise (Changement d’outil)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Lorsque la rétraction est compensée après le changement d’outil, l’extrudeur poussera cette quantité supplémentaire de filament." @@ -16012,11 +16231,11 @@ msgstr "" "Si l’angle maximal à l’intérieur de la boucle périmétrique dépasse cette valeur (indiquant l’absence d’angles vifs), une couture en biseau sera utilisée. La valeur par défaut est de 155°." msgid "Conditional overhang threshold" -msgstr "Seuil de dépassement conditionnel" +msgstr "Seuil de surplomb conditionnel" #, no-c-format, no-boost-format msgid "This option determines the overhang threshold for the application of scarf joint seams. If the unsupported portion of the perimeter is less than this threshold, scarf joint seams will be applied. The default threshold is set at 40% of the external wall's width. Due to performance considerations, the degree of overhang is estimated." -msgstr "Cette option détermine le seuil de surplomb pour l’application des coutures en écharpe. Si la partie non soutenue du périmètre est inférieure à ce seuil, des coutures en biseau seront appliquées. Le seuil par défaut est fixé à 40 % de la largeur de la paroi extérieure. Pour des raisons de performance, le degré de surplomb est estimé." +msgstr "Cette option détermine le seuil de surplomb pour l’application des coutures en biseau. Si la partie non soutenue du périmètre est inférieure à ce seuil, des coutures en biseau seront appliquées. Le seuil par défaut est fixé à 40 % de la largeur de la paroi extérieure. Pour des raisons de performance, le degré de surplomb est estimé." msgid "Scarf joint speed" msgstr "Vitesse de la couture en biseau" @@ -16025,7 +16244,7 @@ msgid "This option sets the printing speed for scarf joints. It is recommended t msgstr "Cette option définit la vitesse d’impression des coutures en biseau. Il est recommandé d’imprimer les coutures en biseau à une vitesse lente (moins de 100 mm/s). Il est également conseillé d’activer l’option « Lissage de la vitesse d’extrusion » si la vitesse définie varie de manière significative par rapport à la vitesse des parois extérieures ou intérieures. Si la vitesse spécifiée ici est supérieure à la vitesse des parois extérieures ou intérieures, l’imprimante prendra par défaut la plus lente des deux vitesses. Lorsqu’elle est spécifiée sous forme de pourcentage (par exemple, 80 %), la vitesse est calculée sur la base de la vitesse de la paroi extérieure ou intérieure. La valeur par défaut est fixée à 100 %." msgid "Scarf joint flow ratio" -msgstr "Ratio de débit de la couture en biseau" +msgstr "Rapport de débit de la couture en biseau" msgid "This factor affects the amount of material for scarf joints." msgstr "Ce facteur influe sur la quantité de matériau pour les coutures en biseau." @@ -16234,7 +16453,7 @@ msgstr "Taux de débit de la finition en spirale" #, no-c-format, no-boost-format msgid "Sets the finishing flow ratio while ending the spiral. Normally the spiral transition scales the flow ratio from 100% to 0% during the last loop which can in some cases lead to under extrusion at the end of the spiral." -msgstr "Définit le ratio de débit de finition lors de la fin de la spirale. Normalement, la transition de la spirale fait passer le taux de débit de 100% à 0% au cours de la dernière boucle, ce qui peut dans certains cas entraîner une sous-extrusion à la fin de la spirale." +msgstr "Définit le rapport de débit de finition lors de la fin de la spirale. Normalement, la transition de la spirale fait passer le taux de débit de 100% à 0% au cours de la dernière boucle, ce qui peut dans certains cas entraîner une sous-extrusion à la fin de la spirale." msgid "If smooth or traditional mode is selected, a timelapse video will be generated for each print. After each layer is printed, a snapshot is taken with the chamber camera. All of these snapshots are composed into a timelapse video when printing completes. If smooth mode is selected, the toolhead will move to the excess chute after each layer is printed and then take a snapshot. Since the melt filament may leak from the nozzle during the process of taking a snapshot, a prime tower is required for smooth mode to wipe the nozzle." msgstr "Si le mode fluide ou traditionnel est sélectionné, une vidéo en timelapse sera générée pour chaque impression. À chaque couche imprimée, un instantané est pris avec la caméra intégrée. Tous ces instantanés seront assemblés dans une vidéo timelapse une fois l'impression terminée. Si le mode lisse est sélectionné, l'extrudeur se déplace vers la goulotte d'évacuation à chaque couche imprimée, puis prend un cliché. Étant donné que le filament fondu peut s'échapper de la buse pendant la prise de vue, une tour d’amorçage est requise en mode lisse pour essuyer la buse." @@ -16326,6 +16545,14 @@ msgstr "Changement d’outil sur la tour d’essuyage" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Force la tête d’outil à se déplacer vers la tour d’essuyage avant d’émettre la commande de changement d’outil (Tx). Pertinent uniquement pour les imprimantes multi-extrudeurs (à têtes d’outil multiples) utilisant une tour d’essuyage de type 2. Par défaut, Orca omet ce déplacement sur les machines à têtes d’outil multiples car le firmware gère le changement de tête, ce qui peut entraîner l’émission de la commande Tx au-dessus de la pièce imprimée. Activez cette option si vous préférez que le changement d’outil soit toujours émis au-dessus de la tour d’essuyage." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Attendre la température sur la tour d’essuyage" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Prend le nouvel outil sans attendre qu’il atteigne la température d’impression, se déplace vers la tour d’essuyage et y attend la température, juste avant la purge. Le suintement dû à la chauffe se dépose sur la tour plutôt que sur le modèle, et le déplacement se superpose à la chauffe. Uniquement pertinent pour les imprimantes multi-extrudeurs (multi-têtes) utilisant une tour d’essuyage de type 2. Le firmware ou la macro de changement d’outil ne doivent pas attendre la température eux-mêmes. Lorsque cette option est désactivée, l’attente de température est émise juste après la commande de changement d’outil." + msgid "No sparse layers (beta)" msgstr "Pas de couches éparses (beta)" @@ -16857,6 +17084,14 @@ msgstr "" "\n" "Le réglage d’une valeur dans le paramètre de quantité de rétraction avant essuyage ci-dessous permet d’effectuer toute rétraction excédentaire avant l’essuyage, sinon elle sera effectuée après l’essuyage." +# AI Translated +msgid "Mixed color sublayer" +msgstr "Sous-couche de couleur mixte" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "Active la division en sous-couches de couleur mixte. Lorsqu’elle est activée, les couches contenant des filaments de couleur mixte sont divisées en sous-couches afin d’obtenir des effets de mélange de couleurs." + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "La tour de purge peut être utilisée pour nettoyer les résidus sur la buse et stabiliser la pression du caisson à l'intérieur de la buse afin d'éviter les défauts d'apparence lors de l'impression d'objets." @@ -17923,6 +18158,10 @@ msgstr "Le maillage d'un fichier modèle a échoué ou la forme n'est pas valide msgid "The supplied file couldn't be read because it's empty." msgstr "Le fichier fourni n'a pas pu être lu car il est vide" +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "Le format du fichier est incompatible et ne peut pas être analysé." + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Format de fichier inconnu : le fichier d'entrée doit porter l'extension .stl, .obj ou .amf (.xml)." @@ -18217,7 +18456,7 @@ msgid "Record Factor" msgstr "Enregistrer le facteur" msgid "We found the best flow ratio for you" -msgstr "Nous avons trouvé le meilleur ratio de débit pour vous" +msgstr "Nous avons trouvé le meilleur rapport de débit pour vous" msgid "Flow Ratio" msgstr "Rapport de débit" @@ -19384,19 +19623,19 @@ msgstr "N’afficher que les noms d’imprimantes avec les modifications apport msgid "Only display the filament names with changes to filament presets." msgstr "N’affichez que les noms des filaments lorsque vous modifiez les préréglages des filaments." -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "Seuls les noms d’imprimantes avec des préréglages d’imprimante utilisateur seront affichés, et chaque préréglage que vous choisissez sera exporté sous forme de fichier zip." msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" "Seuls les noms de filaments contenant des préréglages de filaments utilisateur seront affichés, \n" "et tous les préréglages de filament d’utilisateur dans chaque nom de filament que vous sélectionnez seront exportés sous forme de fichier zip." msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" "Seuls les noms d’imprimantes dont les préréglages de traitement ont été modifiés seront affichés, \n" "et tous les préréglages de processus de l’utilisateur dans chaque nom d’imprimante que vous sélectionnez seront exportés sous forme de fichier zip." @@ -19542,9 +19781,6 @@ msgstr "Imprimante Physique" msgid "Print Host upload" msgstr "Envoi vers l’imprimante hôte" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Sélectionner l'implémentation de l'agent réseau pour la communication avec l'imprimante. Les agents disponibles sont enregistrés au démarrage." - msgid "Select a Flashforge printer" msgstr "Sélectionner une imprimante Flashforge" @@ -19628,7 +19864,7 @@ msgstr "Copier les informations système dans le presse-papiers" msgid "We need information for diagnosing source of the issue. Check wiki page for detailed guide." msgstr "Nous avons besoin d’informations pour diagnostiquer l’origine du problème. Consultez la page wiki pour un guide détaillé." -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "Le bouton Compresser rassemble le fichier de projet et les journaux de la session actuelle dans un fichier ZIP." msgid "Any additional visual examples like images or screen recordings might be helpful while reporting the issue." @@ -19670,7 +19906,7 @@ msgstr "Niveau de journalisation" msgid "Stored logs" msgstr "Journaux stockés" -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "Compresse tous les journaux stockés dans un fichier ZIP." msgid "Profiles" @@ -19737,7 +19973,7 @@ msgstr "Type d’imprimante introuvable, veuillez le sélectionner manuellement. msgid "Authorizing..." msgstr "Autorisation en cours…" -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "Erreur : impossible d’obtenir le jeton d’API pour l’autorisation" msgid "Could not parse server response." @@ -20185,8 +20421,8 @@ msgid "Enable smart filament assign: Assign one filament to multiple nozzles to msgstr "Activer l'attribution intelligente de filament : attribuer un filament à plusieurs buses pour maximiser les économies" # AI Translated -msgid "Fila Saving" -msgstr "Économie de filament" +msgid "File Saving" +msgstr "Enregistrement du fichier" msgid "Don't remind me again" msgstr "Ne plus me rappeler" @@ -20392,9 +20628,6 @@ msgstr "Un événement inattendu s’est produit lors de la connexion, veuillez msgid "User canceled." msgstr "L’utilisateur a annulé." -msgid "Head diameter" -msgstr "Diamètre de la tête" - msgid "Max angle" msgstr "Angle maximal" @@ -20554,6 +20787,9 @@ msgstr "Redémarrer maintenant" msgid "NO RAMMING AT ALL" msgstr "AUCUN BOURRAGE" +msgid "s" +msgstr "s" + msgid "Volumetric speed" msgstr "Vitesse volumétrique" @@ -21176,6 +21412,55 @@ msgstr "" "Éviter la déformation\n" "Saviez-vous que lors de l’impression de matériaux susceptibles de se déformer, tels que l’ABS, une augmentation appropriée de la température du plateau chauffant peut réduire la probabilité de déformation?" +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "L’aperçu en direct natif sous Wayland nécessite le récepteur vidéo GStreamer GTK. Veuillez installer le plugin gtksink pour GStreamer, puis redémarrer OrcaSlicer." + +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "Échec de l’initialisation du récepteur vidéo GStreamer natif sous Wayland. Veuillez vérifier l’installation de votre plugin GStreamer GTK." + +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "Windows Media Player est nécessaire pour cette tâche ! Voulez-vous activer ‘Windows Media Player’ pour votre système d’exploitation ?" + +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "BambuSource n’a pas été correctement enregistré pour la lecture de médias ! Appuyez sur Oui pour le réenregistrer. Vous recevrez deux fois la demande de permission" + +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "Composant BambuSource manquant pour la lecture multimédia ! Veuillez réinstaller OrcaSlicer ou demander de l'aide à la communauté." + +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "Si vous utilisez une BambuSource provenant d’une autre installation, la lecture de la vidéo peut ne pas fonctionner correctement ! Appuyez sur Oui pour résoudre le problème." + +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "Il manque à votre système les codecs H.264 pour GStreamer, qui sont nécessaires pour lire la vidéo. (Essayez d’installer les paquets gstreamer1.0-plugins-bad ou gstreamer1.0-libav, puis redémarrez Orca Slicer)." + +# AI Translated +#~ msgid "N" +#~ msgstr "N" + +# AI Translated +#~ msgid "g" +#~ msgstr "g" + +# AI Translated +#~ msgid "Fila Saving" +#~ msgstr "Économie de filament" + +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "La hauteur de couche est trop faible.\n" +#~ "Elle sera définie à min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "La hauteur de la couche dépasse la limite fixée dans Paramètres de l’imprimante -> Extrudeur -> Limites de la hauteur de la couche, ce qui peut entraîner des problèmes de qualité d’impression." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "S’ajuster automatiquement à la plage définie ?\n" + +#~ msgid "Head diameter" +#~ msgstr "Diamètre de la tête" + #~ msgid "Print order within a single layer." #~ msgstr "Ordre d’impression au sein d’une même couche" @@ -22043,8 +22328,8 @@ msgstr "" #~ msgid "The 3mf has the following customized filament or printer presets:" #~ msgstr "Le 3mf dispose de filaments personnalisés ou de préréglages d'imprimante :" -#~ msgid "Name of components inside step file is not UTF8 format!" -#~ msgstr "Le nom des composants à l'intérieur du fichier .step n'est pas au format UTF8 !" +#~ msgid "Name of components inside step file is not UTF-8 format!" +#~ msgstr "Le nom des composants à l'intérieur du fichier .step n'est pas au format UTF-8 !" #~ msgid "Should printer/filament/process settings be loaded when opening a .3mf?" #~ msgstr "Les paramètres de l’imprimante/du filament/du processus doivent-ils être chargés lors de l’ouverture d’un fichier .3mf ?" diff --git a/localization/i18n/hu/OrcaSlicer_hu.po b/localization/i18n/hu/OrcaSlicer_hu.po index bd4a9915c8..a15d471073 100644 --- a/localization/i18n/hu/OrcaSlicer_hu.po +++ b/localization/i18n/hu/OrcaSlicer_hu.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "Language: hu\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -2948,6 +2948,10 @@ msgstr "Szerkesztés" msgid "Merge with" msgstr "Egyesítés ezzel" +# AI Translated +msgid "Decompose Color" +msgstr "Szín szétbontása" + msgid "Delete this filament" msgstr "Filament törlése" @@ -3247,6 +3251,10 @@ msgstr "Összeállítás" msgid "Merge parts to an object" msgstr "Tárgyak egyesítése egy objektummá" +# AI Translated +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "A változó rétegmagasság és a kevert színű alréteg együttes használata ronthatja a színkeverés minőségét." + # AI Translated msgid "Add layers" msgstr "Rétegek hozzáadása" @@ -4739,6 +4747,23 @@ msgstr "A kamra aktuális hőmérséklete magasabb az anyag biztonságos hőmér msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "A minimális kamrahőmérséklet (%d℃) magasabb a cél kamrahőmérsékletnél (%d℃). A minimális érték az a küszöb, amelynél a nyomtatás elindul, miközben a kamra tovább melegszik a célérték felé, ezért nem haladhatja meg azt. Az érték a célértékre lesz korlátozva." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "A rétegmagasság túl kicsi. A minimumra lesz állítva (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "A rétegmagasság a Nyomtatóbeállítások -> Extruder -> Rétegmagasság limitek menüpontban megadott határértékeken kívül esik, ez minőségbeli problémákat okozhat a nyomtatás során." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Szeretnéd automatikusan a határértékre (%g mm) igazítani?" + +msgid "Adjust" +msgstr "Módosítás" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4786,7 +4811,7 @@ msgstr "" "\n" "Az érték 0-ra áll vissza." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "A váltakozó extra fal nem működik jól, ha a függőleges héjvastagság biztosítása \"Mind\" értékre van állítva." msgid "" @@ -4832,7 +4857,7 @@ msgstr "" "NEM - Független támasz rétegmagasság megtartása" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "A seam_slope_start_height értékének kisebbnek kell lennie, mint a layer_height.\n" @@ -4840,7 +4865,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "A rögzítési mélységnek kisebbnek kell lennie, mint a felületi réteg mélysége.\n" @@ -4858,6 +4883,13 @@ msgstr "" "Igen - Engedélyezd az Arachne falgenerátort\n" "Nem - Tiltsd le az Arachne falgenerátort, majd állítsd a barázdált felületet [Eltolás] módra" +# AI Translated +msgid "Brim ear radius" +msgstr "Peremfül sugara" + +msgid "Brim width" +msgstr "Perem szélessége" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "A spirál mód csak akkor működik, ha a falhurkok száma 1, a támasz és a szondázásos csomósodásészlelés ki van kapcsolva, a felső héjrétegek száma 0, a kitöltés sűrűsége 0, a Timelapse típusa pedig hagyományos." @@ -5112,6 +5144,14 @@ msgstr "Nem sikerült létrehozni a kalibrációs G-kódot" msgid "Calibration error" msgstr "Kalibrációs hiba" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Ez a nyomtató nincs felszerelve a vezérlőelemhez szükséges hardverrel." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Ez a vezérlőelem nem támogatott ezen a nyomtatón." + # AI Translated msgid "Network unavailable" msgstr "A hálózat nem érhető el" @@ -5971,7 +6011,7 @@ msgstr "Térfogat:" msgid "Size:" msgstr "Méret:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "G-kód útvonalütközés található a(z) %d. rétegen, Z = %.2lfmm. Helyezd távolabb egymástól az ütköző objektumokat (%s <-> %s)." @@ -6153,6 +6193,10 @@ msgstr "Több eszköz" msgid "Project" msgstr "Projekt" +# AI Translated +msgid "Device (Web)" +msgstr "Nyomtató (Web)" + msgid "Yes" msgstr "Igen" @@ -6287,10 +6331,10 @@ msgstr "Importálás 3MF/STL/STEP/SVG/OBJ/AMF" msgid "Load a model" msgstr "Modell betöltése" -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "Zip archívum importálása" -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "Zip archívumban található modellek betöltése" msgid "Import Configs" @@ -7811,6 +7855,10 @@ msgstr "Aktuális tálca testreszabása" msgid "The %s nozzle can not print %s." msgstr "A(z) %s fúvóka nem tudja nyomtatni ezt: %s." +# AI Translated +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "A kevert színű filament nyomtatása egyextruderes nyomtatón gyakori filamentcserét és öblítést igényel, ami jelentősen növelheti a hulladék mennyiségét, valamint a fúvóka vagy a hulladékcsúszda eltömődésének kockázatát." + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "A(z) %1% és %2% keverése nyomtatás közben nem ajánlott.\n" @@ -7936,12 +7984,44 @@ msgstr "Filamentlista szinkronizálása az AMS-ből" msgid "Set filaments to use" msgstr "Használni kívánt filament beállítása" +# AI Translated +msgid "Add Mixed Filament" +msgstr "Kevert filament hozzáadása" + +# AI Translated +msgid "Mixed Filament" +msgstr "Kevert filament" + +# AI Translated +msgid "Remove last mixed filament" +msgstr "Utolsó kevert filament eltávolítása" + +# AI Translated +msgid "Add mixed filament" +msgstr "Kevert filament hozzáadása" + +# AI Translated +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "A kevert filament érvénytelen vagy nem egyező összetevőket tartalmaz. Szerkessze újra az érintett bejegyzéseket." + msgid "Search plate, object and part." msgstr "Tálca, objektum és tárgy keresése." msgid "Pellets" msgstr "Pelletek" +# AI Translated +msgid "Mixed filament has broken component references" +msgstr "A kevert filament hibás összetevő-hivatkozásokat tartalmaz" + +# AI Translated +msgid "Edit / Delete / Merge" +msgstr "Szerkesztés / Törlés / Egyesítés" + +# AI Translated +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "A cél kevert filament ezt a fizikai filamentet használja összetevőként. Az egyesítés eltávolítja ezt a fizikai filamentet, és a kevert filament érvénytelenné válhat. Folytatja?" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "A művelet befejezésekor a(z) %s projekt bezárul, majd új projekt jön létre." @@ -8079,7 +8159,7 @@ msgstr "Kérlek, győződj meg arról, hogy a beállításokban található G-k msgid "Customized Preset" msgstr "Egyedi beállítás" -msgid "Component name(s) inside step file not in UTF8 format!" +msgid "Component name(s) inside step file not in UTF-8 format!" msgstr "A STEP fájlon belüli komponens neve nem UTF-8 formátumban van!" # AI Translated @@ -8103,10 +8183,10 @@ msgstr "Az objektum térfogata nulla" #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "A(z) %s fájlból származó objektum túl kicsi, lehet, hogy méterben vagy hüvelykben lett létrehozva.\n" -" Szeretnéd milliméterre méretezni?" +"Szeretnéd milliméterre méretezni?" msgid "Object too small" msgstr "Objektum túl kicsi" @@ -8124,6 +8204,14 @@ msgstr "" msgid "Multi-part object detected" msgstr "Több részből álló objektum észlelve" +# AI Translated +msgid "Matching textures to filaments" +msgstr "Textúrák hozzárendelése a filamentekhez" + +# AI Translated +msgid "Texture Import Warning" +msgstr "Textúraimportálási figyelmeztetés" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Betöltöd ezeket a fájlokat több részből álló egyetlen objektumként?\n" @@ -8244,21 +8332,21 @@ msgstr "A cseréhez nem lett mappa kiválasztva" msgid "Replaced with 3D files from directory:\n" msgstr "Cserélve a mappából származó 3D fájlokra:\n" -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ Kihagyva %1%: azonos fájl.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ %s kihagyva: azonos fájl.\n" -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ Kihagyva %1%: a fájl nem létezik.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ %s kihagyva: a fájl nem létezik.\n" -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ Kihagyva %1%: a csere sikertelen.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ %s kihagyva: a csere sikertelen.\n" -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔ Lecserélve: %1%.\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔%s lecserélve.\n" msgid "Replaced volumes" msgstr "Lecserélt térfogatok" @@ -8335,6 +8423,22 @@ msgstr "" msgid "Sync now" msgstr "Szinkronizálás most" +# AI Translated +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "A textúra importálása sikertelen. Úgy tűnik, hogy a modell textúraadatokat tartalmaz, de az importálási folyamatot nem sikerült befejezni. A modell csak geometriaként lesz importálva." + +# AI Translated +msgid "Applying texture colors..." +msgstr "Textúraszínek alkalmazása..." + +# AI Translated +msgid "Updating 3D view..." +msgstr "3D nézet frissítése..." + +# AI Translated +msgid "Texture colors applied." +msgstr "A textúraszínek alkalmazva." + msgid "You can keep the modified presets for the new project or discard them" msgstr "Megtarthatod az új projekt módosított beállításait, vagy elvetheted őket" @@ -8993,6 +9097,18 @@ msgstr "Ezzel az opcióval egyszerre több eszközre küldhetsz feladatot és t msgid "Pop up to select filament grouping mode" msgstr "Felugró ablak a filamentcsoportosítási mód kiválasztásához" +# AI Translated +msgid "Visible plugin pages" +msgstr "Látható bővítményoldalak" + +# AI Translated +msgid "pages" +msgstr "oldal" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "A rögzített fülként megjelenő bővítményoldalak száma; a fennmaradó oldalak az utolsó fülön lenyíló listába kerülnek." + msgid "Behaviour" msgstr "Viselkedés" @@ -9362,6 +9478,18 @@ msgstr "Nem támogatott beállítások megjelenítése" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Megjeleníti a nem kompatibilis vagy nem támogatott beállításokat a nyomtató- és filamentlegördülő listákban. Ezek a beállítások nem választhatók ki." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Kísérleti) Nyomtatóügynökök használata nyomtatókiszolgálók helyett" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"A nem Bambu nyomtatók nyomtatási feladatait a nyomtató bővítményügynökein keresztül továbbítja a klasszikus nyomtatókiszolgálóra való feltöltés helyett.\n" +"Ha ki van kapcsolva, az OrcaSlicer a régi nyomtatókiszolgáló-viselkedést használja." + # AI Translated msgid "Experimental Features" msgstr "Kísérleti funkciók" @@ -9573,6 +9701,10 @@ msgstr "Spirál (váza)" msgid "First layer filament sequence" msgstr "Kezdőréteg filament sorrendje" +# AI Translated +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "A filamentlista kevert filamenteket tartalmaz. Az egyéni filamentsorrend nem lép érvénybe." + # AI Translated msgid "By Layer" msgstr "Rétegenként" @@ -9632,9 +9764,25 @@ msgstr "Felhasználói beállítás" msgid "Preset Inside Project" msgstr "Projekt a beállításon belül" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Az összes örökölt értéket átmásolja a szülő előbeállításból ebbe az előbeállításba, és megszünteti az öröklési kapcsolatot. A csak a szülővel kompatibilis előbeállítások támogatása megszűnhet." + msgid "Detach from parent" msgstr "Leválasztás a szülőről" +# AI Translated +msgid "Unique preset" +msgstr "Önálló előbeállítás" + +# AI Translated +msgid "Parent preset" +msgstr "Szülő előbeállítás" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Ez az előbeállítás nem örököl másik előbeállításból." + msgid "Name is unavailable." msgstr "A név nem elérhető." @@ -10376,22 +10524,6 @@ msgstr "Biztos, hogy engedélyezed ezt az opciót?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "A kitöltési minták általában maguk kezelik a forgatást a megfelelő nyomtatás és a kívánt hatás elérése érdekében (pl. Gyroid, Cubic). A jelenlegi kitöltési minta elforgatása elégtelen alátámasztáshoz vezethet. Kérlek, járj el körültekintően, és alaposan ellenőrizd a lehetséges nyomtatási problémákat. Biztos, hogy engedélyezed ezt a beállítást?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"A rétegmagasság túl kicsi.\n" -"A rendszer a min_layer_height értékre állítja.\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "A rétegmagasság meghaladja a Nyomtatóbeállítások -> Extruder -> Rétegmagasság limitek menüpontban megadott értéket, ez minőségbeli problémákat okozhat a nyomtatás során." - -msgid "Adjust to the set range automatically?\n" -msgstr "Szeretnéd az értéket automatikusan a beállított tartományhoz igazítani?\n" - -msgid "Adjust" -msgstr "Módosítás" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Kísérleti funkció: Filamentcsere közben nagyobb távolságon történő visszahúzás és elvágás az öblítés minimalizálása érdekében. Bár ez jelentősen csökkentheti az öblítés mértékét, növelheti a fúvóka eltömődésének vagy más nyomtatási problémák kockázatát." @@ -10587,6 +10719,9 @@ msgstr "Foglalt kulcsszavakat találtunk" msgid "Setting Overrides" msgstr "Beállítások felülbírálása" +msgid "Retraction when switching material" +msgstr "Visszahúzás anyagváltáskor" + msgid "Basic information" msgstr "Alapinformációk" @@ -10720,6 +10855,12 @@ msgstr "Kompatibilis folyamatprofilok" msgid "Printable space" msgstr "Nyomtatási terület" +msgid "Printer Agent" +msgstr "Nyomtatóügynök" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Válaszd ki a nyomtatóval való kommunikációhoz használt hálózati ügynököt. Az elérhető ügynököket indításkor regisztrálja a rendszer." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10845,9 +10986,6 @@ msgstr "Rétegmagasság limitek" msgid "Z-Hop" msgstr "Z-emelés" -msgid "Retraction when switching material" -msgstr "Visszahúzás anyagváltáskor" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -10964,12 +11102,12 @@ msgid "No modifications need to be copied." msgstr "Nincs másolandó módosítás." # AI Translated -msgid "Copy paramters" +msgid "Copy parameters" msgstr "Paraméterek másolása" # AI Translated #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "A(z) %s paramétereinek módosítása" # AI Translated @@ -11486,27 +11624,6 @@ msgstr "Filamentcsere öblítési mennyisége" msgid "Please choose the filament colour" msgstr "Kérlek, válaszd ki a filament színét" -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "A natív Wayland élőképhez a GStreamer GTK videonyelő szükséges. Telepítsd a gtksink beépülő modult a GStreamerhez, majd indítsd újra az OrcaSlicert." - -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "Nem sikerült inicializálni a natív Wayland GStreamer videonyelőt. Ellenőrizd a GStreamer GTK bővítmény telepítését." - -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "Ehhez a művelethez Windows Media Player szükséges. Szeretnéd engedélyezni a \"Windows Media Player\"-t az operációs rendszerben?" - -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "A BambuSource nincs megfelelően regisztrálva médialejátszáshoz. Kattints az Igen gombra az újbóli regisztráláshoz. Kétszer kapsz majd megerősítési kérést." - -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "A médialejátszáshoz szükséges BambuSource-összetevő hiányzik. Kérlek, telepítsd újra az OrcaSlicert, vagy kérj segítséget a közösségtől." - -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "Ha egy másik telepítésből származó BambuSource van használatban, előfordulhat, hogy a videólejátszás nem működik megfelelően. Kattints az Igen gombra a javításhoz." - -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "A rendszerből hiányoznak a GStreamer H.264 kodekjei, amelyek szükségesek a videolejátszáshoz. (Próbáld telepíteni a gstreamer1.0-plugins-bad vagy a gstreamer1.0-libav csomagokat, majd indítsd újra az Orca Slicert.)" - msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "A felhőszolgáltatás nem érhető el. Indítsd újra az OrcaSlicert, majd próbáld újra." @@ -12200,6 +12317,10 @@ msgstr " túl közel van a tiltott területhez, a nyomtatás során előfordulha msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " túl közel van a csomósodásészlelési területhez, és ez ütközést fog okozni.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " részben a nyomtatható területen kívül esik, ezért nem nyomtatható ki.\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "A kiválasztott fúvóka hőmérsékletek nem kompatibilisek. Mindegyik filament fúvóka hőmérsékletének a többi filament ajánlott fúvóka hőmérsékleti tartományába kell esnie. Ellenkező esetben a fúvóka eltömődhet vagy a nyomtató megsérülhet." @@ -12212,6 +12333,10 @@ msgstr "Ha továbbra is szeretnél nyomtatni, engedélyezheted az opciót itt: B msgid "No extrusions under current settings." msgstr "A jelenlegi beállításokkal nincsenek extrudálások." +# AI Translated +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "Színátmenetes kevert filament van használatban, de a 'Kevert színű alréteg' ki van kapcsolva. A színátmenet nem lesz kinyomtatva." + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "A sima Timelapse nem használható, ha a nyomtatási sorrend \"Tárgyanként\"." @@ -12248,6 +12373,10 @@ msgstr "Próbáld meg csökkenteni a modell méretét vagy módosítani a jelenl msgid "Variable layer height is not supported with Organic supports." msgstr "A változó rétegmagasság nem működik az organikus támaszokkal." +# AI Translated +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "A törlőtorony filamentje nem lehet kevert filament." + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Eltérő fúvókaátmérők és eltérő filamentátmérők mellett a törlőtorony nem biztos, hogy megfelelően működik. Ez nagyon kísérleti funkció, ezért körültekintően használd." @@ -12530,9 +12659,6 @@ msgstr "3MF használata G-kód helyett" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Kapcsold be, ha a nyomtató 3MF fájlt fogad el nyomtatási feladatként. Bekapcsolva az Orca Slicer a szeletelt fájlt .gcode.3mf formátumban küldi el egyszerű .gcode fájl helyett." -msgid "Printer Agent" -msgstr "Nyomtatóügynök" - msgid "Select the network agent implementation for printer communication." msgstr "Válaszd ki a nyomtató kommunikációjához használt hálózati ügynök implementációját." @@ -12618,7 +12744,7 @@ msgstr "mm vagy %" msgid "Other layers" msgstr "Többi réteg" -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." msgstr "A kezdőréteg utáni asztalhőmérséklet. A 0 azt jelenti, hogy a filament nem nyomtatható a SuperTack hűvös tálcára." msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." @@ -13220,9 +13346,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "A belső hidak sebessége. Ha az érték százalékban van megadva, a bridge_speed alapján lesz kiszámítva. Az alapértelmezett érték 150%." -msgid "Brim width" -msgstr "Perem szélessége" - msgid "This is the distance from the model to the outermost brim line." msgstr "A modell és a legkülső peremvonal közötti távolság" @@ -13302,6 +13425,14 @@ msgstr "" "Az éles szögek észlelése előtt a geometria egyszerűsítve lesz. Ez a paraméter a leegyszerűsítésnél figyelembe vett eltérés minimális hosszát adja meg.\n" "0 értékkel kikapcsolható." +# AI Translated +msgid "Brim ears outer only" +msgstr "Peremfülek csak kívül" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Egérfüleket csak a modell külső kontúrján hoz létre, a furatokat és a zárt szakaszokat kihagyva." + msgid "upward compatible machine" msgstr "felfelé kompatibilis gép" @@ -13991,6 +14122,8 @@ msgstr "Rétegidő" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "A tárgyhűtő ventilátor azon rétegek esetében lesz engedélyezve, amelyek becsült ideje rövidebb ennél az értéknél. A ventilátor fordulatszáma a rétegnyomtatási időnek megfelelően skálázódik a minimális és maximális ventilátor fordulatszám között" +# AI Translated +msgctxt "second" msgid "s" msgstr "mp" @@ -14298,6 +14431,62 @@ msgstr "Támaszanyag" msgid "Support material is commonly used to print supports and support interfaces." msgstr "A támaszanyagot általában a támaszok és azok érintkező felületeinek nyomtatására használják." +# AI Translated +msgid "Is mixed filament" +msgstr "Kevert filament-e" + +# AI Translated +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "Megadja, hogy ez a filamenthely több fizikai filamentből álló kevert filament-e" + +# AI Translated +msgid "Mixed filament components" +msgstr "Kevert filament összetevői" + +# AI Translated +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "Az összetevő filamentek vesszővel elválasztott, 1-től induló indexei, pl. \"1,3\"" + +# AI Translated +msgid "Mixed filament sublayer ratios" +msgstr "Kevert filament alréteg-arányai" + +# AI Translated +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "Vesszővel elválasztott arányértékek, amelyek összege 1.0, pl. \"0.7,0.3\"" + +# AI Translated +msgid "Mixed filament gradient" +msgstr "Kevert filament színátmenete" + +# AI Translated +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "Bekapcsolja a Z irányú színátmenet módot a kevert filament alrétegeihez. Bekapcsolva az alrétegek arányai lineárisan változnak a rétegek mentén." + +# AI Translated +msgid "Mixed filament gradient range" +msgstr "Kevert filament színátmenetének tartománya" + +# AI Translated +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "Az első összetevő kezdő és záró aránya színátmenet módban. Vesszővel elválasztott számpár, pl. a \"0.10,0.90\" 10%-tól 90%-ig tartót jelent." + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "Kevert filament színátmenet-görbéje" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "Opcionális, Photoshop stílusú egyéni görbe, amely a Z irányú haladást az első összetevő arányához rendeli. Függőleges vonallal elválasztott vezérlőpontokként van kódolva, \"x,y\" (régi) vagy \"x,y,m_in,m_out\" formában, ha az érintő felülbírálására van szükség (üres érték vagy \"nan\" esetén a PCHIP alapértelmezés érvényes). x a [0,1] tartományban van; y a beállított aránytartományra van korlátozva, pl. \"0,0.15|0.5,0.50|1,0.85\". Ha üresen marad, helyette a lineáris gradient_range kerül felhasználásra." + +# AI Translated +msgid "Mixed filament per-part gradient" +msgstr "Kevert filament színátmenete tárgyanként" + +# AI Translated +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "Ha a színátmenet mód be van kapcsolva, a színátmenet az összeállítás minden tárgyára külön-külön érvényesül, ahelyett hogy az egész összeállítást egyetlen Z tartományként kezelné." + msgid "Filament printable" msgstr "Filament nyomtatható" @@ -14475,6 +14664,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroid" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Kitöltés simítási tényezője" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Azt szabályozza, hogy a kitöltés sarkai mennyire legyenek lekerekítve. A 0% megtartja az eredeti éles útvonalat, a 100% pedig a lehető legnagyobb íveket hozza létre a szomszédos kitöltővonalak között." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "A felső felületi kitöltés gyorsulása. Alacsonyabb érték használata javíthatja a felső felület minőségét" @@ -15017,6 +15214,14 @@ msgstr "Milyen G-kóddal kompatibilis a nyomtató." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "G-code konfigurációs blokk kihagyása" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Nem írja a CONFIG_BLOCK blokkot (a szeletelő beállításainak kulcs/érték párjait) a G-code fájlba. Ez segíthet azoknál a nyomtatóknál, amelyek firmware-e összeomlik ezeknek a megjegyzéssoroknak a feldolgozásakor (pl. Anycubic go-klipper). Megjegyzés: a G-code fájl így már nem tartalmazza a szeletelő beállításait, ezért az OrcaSlicerbe való visszaimportálás nem állítja vissza a konfigurációt." + msgid "Pellet Modded Printer" msgstr "Granulátumos módosított nyomtató" @@ -15545,6 +15750,7 @@ msgid "The allowed maximum output force of Y axis" msgstr "Az Y tengely megengedett maximális kimeneti ereje" # AI Translated +msgctxt "Newton" msgid "N" msgstr "N" @@ -15555,6 +15761,7 @@ msgid "The machine bed mass load of Y axis" msgstr "Az Y tengely gépasztaltömeg-terhelése" # AI Translated +msgctxt "gram" msgid "g" msgstr "g" @@ -15869,7 +16076,7 @@ msgid "Reduce infill retraction" msgstr "Csökkentett visszahúzás kitöltésnél" # AI Translated -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." msgstr "Nem történik visszahúzás, amikor a fej csak kitöltés felett halad el, mert az itt történő szivárgás egyébként sem látható. Ez az opció lerövidítheti a nyomtatási időt a visszahúzások csökkentésével komplex modelleknél, de egyúttal lassabbá teszi a szeletelést és a G-kód generálást. Vedd figyelembe, hogy a Z-emelés sem történik meg azokon a területeken, ahol a visszahúzás ki van hagyva." msgid "This option will drop the temperature of the inactive extruders to prevent oozing." @@ -16043,7 +16250,7 @@ msgstr "Visszahúzás mértéke törlés után" # AI Translated #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" "A törlés utáni gyors visszahúzás hossza a visszahúzási hosszhoz viszonyítva.\n" @@ -16079,10 +16286,18 @@ msgstr "Hosszú visszahúzás extruderváltáskor" msgid "Retraction distance when extruder change" msgstr "Visszahúzási távolság extruderváltáskor" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Visszahúzás hossza (Eszközváltás)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Amikor a visszahúzás eszközváltás előtt aktiválódik, a filament a megadott értékkel húzódik vissza (a hossz a nyers filamenten mérve, mielőtt az az extruderbe kerülne)." + msgid "Z-hop height" msgstr "Z-emelés magassága" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "Visszahúzáskor a fúvóka egy kicsit megemelkedik, hogy a fúvóka és a nyomtatott tárgy között rés keletkezzen. Ez megakadályozza, hogy a fúvóka nagyobb mozgás közben a tárgynak ütközzön. A Z tengely emelésekor használt körkörös mozgás megelőzheti a szálazást." msgid "Z-hop lower boundary" @@ -16172,6 +16387,10 @@ msgstr "Extra hossz újraindításkor" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Amikor a visszahúzás kompenzálásra kerül utazási mozgás után, az extruder ezt a további szálmennyiséget nyomja előre. Erre a beállításra ritkán van szükség." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Extra hossz újraindításkor (Eszközváltás)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Amikor a visszahúzás kompenzálásra kerül szerszámváltás után, az extruder ezt a további szálmennyiséget nyomja előre." @@ -16588,6 +16807,14 @@ msgstr "Szerszámcsere a törlőtoronyban" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "A szerszámcsere parancs (Tx) kiadása előtt a törlőtoronyhoz mozgatja a szerszámfejet. Csak a 2-es típusú törlőtornyot használó többextruderes (több szerszámfejes) nyomtatóknál van jelentősége. Az Orca alapértelmezés szerint kihagyja ezt a mozgást a több szerszámfejes gépeknél, mert a fejcserét a firmware kezeli. Emiatt azonban előfordulhat, hogy a Tx parancsot a nyomtatott tárgy felett adja ki. Kapcsold be ezt a beállítást, ha azt szeretnéd, hogy a szerszámcsere mindig a törlőtorony felett történjen." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Várakozás a hőmérsékletre a törlőtornyon" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Felveszi az új szerszámot anélkül, hogy megvárná a nyomtatási hőmérséklet elérését, a törlőtoronyhoz áll, és ott várja meg a hőmérsékletet, közvetlenül az öblítés előtt. A felfűtés közben kiszivárgó anyag a toronyra kerül a modell helyett, a mozgás pedig átfedésben van a fűtéssel. Csak több extruderes (több szerszámfejes) nyomtatóknál releváns, amelyek 2-es típusú törlőtornyot használnak. A firmware vagy a szerszámváltó makró nem várhat magától a hőmérsékletre. Ha ki van kapcsolva, a hőmérsékletre várakozás közvetlenül a szerszámváltó parancs után kerül kiadásra." + msgid "No sparse layers (beta)" msgstr "Nincsenek ritka rétegek (béta)" @@ -17125,6 +17352,14 @@ msgstr "" "\n" "Ha az alábbi \"visszahúzási mennyiség törlés előtt\" beállításban értéket adsz meg, akkor az esetleges többletvisszahúzás a törlés előtt történik meg, különben utána." +# AI Translated +msgid "Mixed color sublayer" +msgstr "Kevert színű alréteg" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "Bekapcsolja a kevert színű alrétegekre bontást. Bekapcsolva a kevert színű filamentet tartalmazó rétegek alrétegekre lesznek bontva a színkeverési hatás eléréséhez." + # AI Translated msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "A törlőtorony a fúvókán lévő maradék eltávolítására és a fúvóka belsejében lévő nyomás stabilizálására szolgál, hogy elkerülhetők legyenek a megjelenésbeli hibák az objektumok nyomtatásakor." @@ -18198,6 +18433,10 @@ msgstr "A modellfájl hálósítása sikertelen volt, vagy nincs érvényes alak msgid "The supplied file couldn't be read because it's empty." msgstr "A megadott fájl nem olvasható be, mert üres" +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "A fájlformátum nem kompatibilis, ezért nem értelmezhető." + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Ismeretlen fájlformátum. A bemeneti fájlnak .stl, .obj vagy .amf(.xml) kiterjesztésűnek kell lennie." @@ -19688,19 +19927,19 @@ msgstr "Csak azok a nyomtatók jelennek meg, amelyeknél változtak a nyomtató- msgid "Only display the filament names with changes to filament presets." msgstr "Csak azok a filamentnevek jelennek meg, ahol változtak a filamentbeállítások." -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "Csak azok a nyomtatónevek jelennek meg, amelyekhez tartoznak felhasználói beállítások. A kiválasztott beállítások ZIP fájlként kerülnek exportálásra." msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" "Csak azok a filamentnevek jelennek meg, amelyekhez tartoznak felhasználói filamentbeállítások,\n" "és a kiválasztott filamentnevekhez tartozó összes felhasználói filamentbeállítás ZIP-fájlként lesz exportálva." msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" "Csak azok a nyomtatónevek jelennek meg, amelyekhez módosított folyamatbeállítások tartoznak,\n" "és a kiválasztott nyomtatónevekhez tartozó összes felhasználói folyamatbeállítás ZIP-fájlként lesz exportálva." @@ -19847,9 +20086,6 @@ msgstr "Fizikai nyomtató" msgid "Print Host upload" msgstr "Feltöltés a nyomtatóra" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Válaszd ki a nyomtatóval való kommunikációhoz használt hálózati ügynököt. Az elérhető ügynököket indításkor regisztrálja a rendszer." - # AI Translated msgid "Select a Flashforge printer" msgstr "Válassz egy Flashforge nyomtatót" @@ -19949,7 +20185,7 @@ msgid "We need information for diagnosing source of the issue. Check wiki page f msgstr "Információra van szükségünk a probléma forrásának feltárásához. A részletes útmutatót a wiki oldalon találod." # AI Translated -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "A Csomagolás gomb egy zip fájlba gyűjti a projektfájlt és az aktuális munkamenet naplóit." # AI Translated @@ -20005,7 +20241,7 @@ msgid "Stored logs" msgstr "Tárolt naplók" # AI Translated -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "Az összes tárolt naplót egy zip fájlba csomagolja." # AI Translated @@ -20093,7 +20329,7 @@ msgid "Authorizing..." msgstr "Engedélyezés..." # AI Translated -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "Hiba. Nem sikerült API-tokent szerezni az engedélyezéshez" # AI Translated @@ -20549,8 +20785,8 @@ msgid "Enable smart filament assign: Assign one filament to multiple nozzles to msgstr "Intelligens filament-hozzárendelés bekapcsolása: egy filament több fúvókához rendelése a megtakarítás maximalizálásához" # AI Translated -msgid "Fila Saving" -msgstr "Filamentmegtakarítás" +msgid "File Saving" +msgstr "Fájl mentése" msgid "Don't remind me again" msgstr "Ne emlékeztessen újra" @@ -20791,9 +21027,6 @@ msgstr "Bejelentkezés közben váratlan hiba történt, próbáld újra." msgid "User canceled." msgstr "Felhasználó által megszakítva." -msgid "Head diameter" -msgstr "Fej átmérő" - msgid "Max angle" msgstr "Maximális szög" @@ -20956,6 +21189,9 @@ msgstr "Újraindítás most" msgid "NO RAMMING AT ALL" msgstr "EGYÁLTALÁN NINCS TÖMÖRÍTÉS" +msgid "s" +msgstr "mp" + msgid "Volumetric speed" msgstr "Volumetrikus sebesség" @@ -21607,6 +21843,55 @@ msgstr "" "Kunkorodás elkerülése\n" "Tudtad, hogy a kunkorodásra hajlamos anyagok (például ABS) nyomtatásakor az asztal hőmérsékletének növelése csökkentheti a kunkorodás valószínűségét?" +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "A natív Wayland élőképhez a GStreamer GTK videonyelő szükséges. Telepítsd a gtksink beépülő modult a GStreamerhez, majd indítsd újra az OrcaSlicert." + +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "Nem sikerült inicializálni a natív Wayland GStreamer videonyelőt. Ellenőrizd a GStreamer GTK bővítmény telepítését." + +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "Ehhez a művelethez Windows Media Player szükséges. Szeretnéd engedélyezni a \"Windows Media Player\"-t az operációs rendszerben?" + +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "A BambuSource nincs megfelelően regisztrálva médialejátszáshoz. Kattints az Igen gombra az újbóli regisztráláshoz. Kétszer kapsz majd megerősítési kérést." + +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "A médialejátszáshoz szükséges BambuSource-összetevő hiányzik. Kérlek, telepítsd újra az OrcaSlicert, vagy kérj segítséget a közösségtől." + +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "Ha egy másik telepítésből származó BambuSource van használatban, előfordulhat, hogy a videólejátszás nem működik megfelelően. Kattints az Igen gombra a javításhoz." + +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "A rendszerből hiányoznak a GStreamer H.264 kodekjei, amelyek szükségesek a videolejátszáshoz. (Próbáld telepíteni a gstreamer1.0-plugins-bad vagy a gstreamer1.0-libav csomagokat, majd indítsd újra az Orca Slicert.)" + +# AI Translated +#~ msgid "N" +#~ msgstr "N" + +# AI Translated +#~ msgid "g" +#~ msgstr "g" + +# AI Translated +#~ msgid "Fila Saving" +#~ msgstr "Filamentmegtakarítás" + +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "A rétegmagasság túl kicsi.\n" +#~ "A rendszer a min_layer_height értékre állítja.\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "A rétegmagasság meghaladja a Nyomtatóbeállítások -> Extruder -> Rétegmagasság limitek menüpontban megadott értéket, ez minőségbeli problémákat okozhat a nyomtatás során." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Szeretnéd az értéket automatikusan a beállított tartományhoz igazítani?\n" + +#~ msgid "Head diameter" +#~ msgstr "Fej átmérő" + #~ msgid "Print order within a single layer." #~ msgstr "Nyomtatási sorrend egyetlen rétegen belül." diff --git a/localization/i18n/it/OrcaSlicer_it.po b/localization/i18n/it/OrcaSlicer_it.po index 7be197c578..9af642f536 100644 --- a/localization/i18n/it/OrcaSlicer_it.po +++ b/localization/i18n/it/OrcaSlicer_it.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -2953,6 +2953,10 @@ msgstr "Modifica" msgid "Merge with" msgstr "Unisci con" +# AI Translated +msgid "Decompose Color" +msgstr "Scomponi colore" + msgid "Delete this filament" msgstr "Elimina questo filamento" @@ -3252,6 +3256,10 @@ msgstr "Assemblaggio" msgid "Merge parts to an object" msgstr "Unisci parti in un oggetto" +# AI Translated +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "L'uso dell'altezza strato variabile insieme al sottostrato di colore misto può ridurre la qualità della miscelazione dei colori." + # AI Translated msgid "Add layers" msgstr "Aggiungi strati" @@ -4741,6 +4749,23 @@ msgstr "L'attuale temperatura della camera è superiore alla temperatura di sicu msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "La temperatura minima della camera (%d℃) è superiore alla temperatura target della camera (%d℃). Il valore minimo è la soglia alla quale inizia la stampa mentre la camera continua a riscaldarsi verso il target, quindi non dovrebbe superarlo. Verrà limitato al valore target." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "L'altezza dello strato è troppo piccola. Sarà impostata al valore minimo (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "L'altezza dello strato è fuori dai limiti impostati in Impostazioni stampante -> Estrusore -> Limiti Altezza Strato, ciò potrebbe causare problemi di qualità di stampa." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Regolarla automaticamente al limite (%g mm)?" + +msgid "Adjust" +msgstr "Regola" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4788,7 +4813,7 @@ msgstr "" "\n" "Il valore verrà reimpostato a 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Parete aggiuntiva alternativa non funziona bene quando \"Garantisci spessore verticale del guscio\" è impostato su Tutto." msgid "" @@ -4834,7 +4859,7 @@ msgstr "" "NO - Mantieni Altezza strato di supporto indipendente" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height deve essere inferiore a layer_height.\n" @@ -4842,7 +4867,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "La profondità di intersezione deve essere inferiore alla profondità della pelle.\n" @@ -4860,6 +4885,13 @@ msgstr "" "Sì - Abilita generatore di pareti Arachne\n" "No - Disabilita generatore di pareti Arachne e imposta la modalità [Spostamento] della Superficie ruvida" +# AI Translated +msgid "Brim ear radius" +msgstr "Raggio della tesa ad orecchio" + +msgid "Brim width" +msgstr "Larghezza tesa" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "La modalità spirale funziona solo quando i perimetri sono 1, il supporto è disabilitato, il rilevamento degli ammassi tramite sondaggio è disabilitato, gli strati superiori della shell sono 0, la densità del riempimento sparso è 0 e il tipo di timelapse è tradizionale." @@ -5114,6 +5146,14 @@ msgstr "Impossibile generare G-code di calibrazione" msgid "Calibration error" msgstr "Errore di calibrazione" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Questa stampante non dispone dell'hardware richiesto da questo controllo." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Questo controllo non è supportato su questa stampante." + # AI Translated msgid "Network unavailable" msgstr "Rete non disponibile" @@ -5973,7 +6013,7 @@ msgstr "Volume:" msgid "Size:" msgstr "Dimensione:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Sono stati trovati conflitti di percorsi nel G-code sullo strato %d, Z = %.2lfmm. Si prega di separare gli oggetti in conflitto (%s <-> %s)." @@ -6154,6 +6194,10 @@ msgstr "Multi-dispositivo" msgid "Project" msgstr "Progetto" +# AI Translated +msgid "Device (Web)" +msgstr "Dispositivo (Web)" + msgid "Yes" msgstr "Sì" @@ -6289,10 +6333,10 @@ msgstr "Importa 3MF/STL/STEP/SVG/OBJ/AMF" msgid "Load a model" msgstr "Carica modello" -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "Importa archivio Zip" -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "Carica i modelli contenuti in un archivio zip" msgid "Import Configs" @@ -7815,6 +7859,10 @@ msgstr "Personalizza il piatto corrente" msgid "The %s nozzle can not print %s." msgstr "L'ugello %s non può stampare %s." +# AI Translated +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "La stampa di filamento a colore misto su una stampante a estrusore singolo richiede frequenti cambi di filamento e spurghi, il che può aumentare notevolmente gli scarti e il rischio di intasamento dell'ugello o dello scivolo di scarto." + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "Non è consigliato miscelare %1% con %2% nella stampa.\n" @@ -7940,12 +7988,44 @@ msgstr "Sincronizza l'elenco filamenti dall'AMS" msgid "Set filaments to use" msgstr "Imposta filamenti da usare" +# AI Translated +msgid "Add Mixed Filament" +msgstr "Aggiungi filamento misto" + +# AI Translated +msgid "Mixed Filament" +msgstr "Filamento misto" + +# AI Translated +msgid "Remove last mixed filament" +msgstr "Rimuovi l'ultimo filamento misto" + +# AI Translated +msgid "Add mixed filament" +msgstr "Aggiungi filamento misto" + +# AI Translated +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "Il filamento misto ha componenti non validi o incoerenti. Modificare nuovamente le voci interessate." + msgid "Search plate, object and part." msgstr "Cerca piatto, oggetto e parte." msgid "Pellets" msgstr "Granuli" +# AI Translated +msgid "Mixed filament has broken component references" +msgstr "Il filamento misto ha riferimenti a componenti non validi" + +# AI Translated +msgid "Edit / Delete / Merge" +msgstr "Modifica / Elimina / Unisci" + +# AI Translated +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "Il filamento misto di destinazione utilizza questo filamento fisico come componente. L'unione rimuoverà questo filamento fisico e potrebbe invalidare il filamento misto. Continuare?" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "Al completamento dell'operazione, il progetto %s verrà chiuso e ne verrà creato uno nuovo." @@ -8083,8 +8163,8 @@ msgstr "Si prega di confermare che i G-code all'interno di questi profili sono s msgid "Customized Preset" msgstr "Profilo personalizzato" -msgid "Component name(s) inside step file not in UTF8 format!" -msgstr "Il nome dei componenti all'interno del file STEP non è in formato UTF8!" +msgid "Component name(s) inside step file not in UTF-8 format!" +msgstr "Il nome dei componenti all'interno del file STEP non è in formato UTF-8!" msgid "Because of unsupported text encoding, garbage characters may appear!" msgstr "A causa di una codifica del testo non supportata, potrebbero apparire caratteri inutili!" @@ -8105,10 +8185,10 @@ msgstr "Il volume dell'oggetto è zero" #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "L'oggetto del file %s è troppo piccolo e può essere in metri o pollici.\n" -" Si desidera scalare in millimetri?" +"Si desidera scalare in millimetri?" msgid "Object too small" msgstr "Oggetto troppo piccolo" @@ -8125,6 +8205,14 @@ msgstr "" msgid "Multi-part object detected" msgstr "Rilevato oggetto in più parti" +# AI Translated +msgid "Matching textures to filaments" +msgstr "Associazione delle trame ai filamenti" + +# AI Translated +msgid "Texture Import Warning" +msgstr "Avviso di importazione trama" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Caricare questi file come un singolo oggetto con più parti?\n" @@ -8244,21 +8332,21 @@ msgstr "La directory per la sostituzione non è stata selezionata" msgid "Replaced with 3D files from directory:\n" msgstr "Sostituito con file 3D dalla directory:\n" -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ Saltato %1%: stesso file.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ Saltato %s: stesso file.\n" -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ Saltato %1%: il file non esiste.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ Saltato %s: il file non esiste.\n" -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ Saltato %1%: sostituzione fallita.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ Saltato %s: sostituzione fallita.\n" -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔ Sostituito %1%.\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔ Sostituito %s.\n" msgid "Replaced volumes" msgstr "Volumi sostituiti" @@ -8335,6 +8423,22 @@ msgstr "" msgid "Sync now" msgstr "Sincronizza ora" +# AI Translated +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "Importazione della trama non riuscita. Il modello sembra contenere dati di trama, ma non è stato possibile completare il processo di importazione. Il modello verrà importato solo come geometria." + +# AI Translated +msgid "Applying texture colors..." +msgstr "Applicazione dei colori della trama..." + +# AI Translated +msgid "Updating 3D view..." +msgstr "Aggiornamento della vista 3D..." + +# AI Translated +msgid "Texture colors applied." +msgstr "Colori della trama applicati." + msgid "You can keep the modified presets for the new project or discard them" msgstr "È possibile conservare i profili modificati per il nuovo progetto o scartarli" @@ -8995,6 +9099,18 @@ msgstr "Abilitando questa opzione, puoi inviare un'attività a più dispositivi msgid "Pop up to select filament grouping mode" msgstr "Popup per selezionare la modalità di raggruppamento filamenti" +# AI Translated +msgid "Visible plugin pages" +msgstr "Pagine dei plugin visibili" + +# AI Translated +msgid "pages" +msgstr "pagine" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Numero di pagine dei plugin mostrate come schede fisse prima che le pagine rimanenti vengano raccolte in un menu a discesa nell'ultima scheda." + msgid "Behaviour" msgstr "Comportamento" @@ -9381,6 +9497,18 @@ msgstr "Mostra i profili non supportati" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Mostra i profili incompatibili/non supportati negli elenchi a discesa di stampante e filamento. Questi profili non possono essere selezionati." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Sperimentale) Usa gli agenti stampante invece degli host di stampa" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Instrada i lavori di stampa delle stampanti non Bambu attraverso gli agenti plugin della stampante invece del classico flusso di caricamento sull'host di stampa.\n" +"Quando è disattivato, OrcaSlicer usa il comportamento legacy dell'host di stampa." + # AI Translated msgid "Experimental Features" msgstr "Funzionalità sperimentali" @@ -9594,6 +9722,10 @@ msgstr "Vaso a spirale" msgid "First layer filament sequence" msgstr "Sequenza filamenti primo strato" +# AI Translated +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "L'elenco dei filamenti contiene filamenti misti. La sequenza di filamenti personalizzata non avrà effetto." + msgid "By Layer" msgstr "Per strato" @@ -9650,9 +9782,25 @@ msgstr "Profilo utente" msgid "Preset Inside Project" msgstr "Profilo interno al progetto" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Copia in questo profilo tutti i valori ereditati dal profilo padre e rimuove la relazione di ereditarietà. I profili compatibili solo con il profilo padre potrebbero non essere più supportati." + msgid "Detach from parent" msgstr "Scollega dal genitore" +# AI Translated +msgid "Unique preset" +msgstr "Profilo unico" + +# AI Translated +msgid "Parent preset" +msgstr "Profilo padre" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Questo profilo non eredita da un altro profilo." + msgid "Name is unavailable." msgstr "Nome non disponibile." @@ -10392,22 +10540,6 @@ msgstr "Sei sicuro di voler abilitare questa opzione?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "I pattern di riempimento sono generalmente progettati per gestire automaticamente la rotazione per garantire una stampa corretta e ottenere gli effetti desiderati (ad es. Gyroid, Cubico). La rotazione del pattern di riempimento sparso corrente potrebbe portare a un supporto insufficiente. Procedere con cautela e verificare accuratamente eventuali problemi di stampa. Sei sicuro di voler abilitare questa opzione?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"L'altezza dello strato è troppo piccola.\n" -"Sarà impostato su min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "L'altezza dello strato supera il limite in Impostazioni stampante -> Estrusore -> Limiti Altezza Strato. Ciò potrebbe causare problemi di qualità di stampa." - -msgid "Adjust to the set range automatically?\n" -msgstr "Regolare automaticamente l'intervallo impostato?\n" - -msgid "Adjust" -msgstr "Regola" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Funzionalità sperimentale: ritrazione e taglio del filamento a una distanza maggiore durante i cambi di filamento per ridurre al minimo lo spurgo. Sebbene possa ridurre notevolmente lo spurgo, può anche aumentare il rischio di intasamento degli ugelli o di altre complicazioni di stampa." @@ -10603,6 +10735,9 @@ msgstr "Parole chiave riservate trovate" msgid "Setting Overrides" msgstr "Sovrascrivi impostazioni" +msgid "Retraction when switching material" +msgstr "Retrazione quando si cambia materiale" + msgid "Basic information" msgstr "Informazioni di base" @@ -10734,6 +10869,12 @@ msgstr "Profili di processo compatibili" msgid "Printable space" msgstr "Spazio di stampa" +msgid "Printer Agent" +msgstr "Agente stampante" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Selezionare l'implementazione dell'agente di rete per la comunicazione con la stampante. Gli agenti disponibili vengono registrati all'avvio." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10859,9 +11000,6 @@ msgstr "Limiti altezza strati" msgid "Z-Hop" msgstr "Sollevamento Z" -msgid "Retraction when switching material" -msgstr "Retrazione quando si cambia materiale" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -10978,12 +11116,12 @@ msgid "No modifications need to be copied." msgstr "Non è necessario copiare alcuna modifica." # AI Translated -msgid "Copy paramters" +msgid "Copy parameters" msgstr "Copia parametri" # AI Translated #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "Modifica i parametri di %s" # AI Translated @@ -11505,27 +11643,6 @@ msgstr "Volumi di spurgo per cambio filamento" msgid "Please choose the filament colour" msgstr "Scegliere il colore del filamento" -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "La funzione di visualizzazione in tempo reale nativa di Wayland richiede il ricevitore video GTK di GStreamer. Installare il modulo gtksink per GStreamer e riavviare OrcaSlicer." - -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "Impossibile inizializzare il ricevitore video nativo di Wayland GStreamer. Verificare l'installazione del modulo GTK di GStreamer." - -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "Per questa operazione è necessario Windows Media Player! Desideri abilitare 'Windows Media Player' sul il tuo sistema operativo?" - -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "BambuSource non è stato registrato correttamente per la riproduzione multimediale! Fare clic su Sì per effettuare nuovamente la registrazione. Sarai promosso due volte" - -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "Componente BambuSource mancante per la riproduzione multimediale! Reinstallare OrcaSlicer o cercare aiuto nella comunità." - -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "È in uso una versione di BambuSource da un'installazione diversa. La riproduzione video potrebbe non funzionare correttamente! Fare clic su Sì per risolvere il problema." - -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "Nel tuo sistema mancano i codec H.264 per GStreamer, necessari per riprodurre i video. (Provare a installare i pacchetti gstreamer1.0-plugins-bad o gstreamer1.0-libav e riavviare OrcaSlicer?)" - msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "Il fornitore di servizi cloud non è disponibile. Riavviare OrcaSlicer e riprovare." @@ -12221,6 +12338,10 @@ msgstr " è troppo vicino all'area di esclusione e si verificheranno collisioni. msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " è troppo vicino all'area di rilevamento ammassi e verranno causate collisioni.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " è parzialmente fuori dall'area stampabile e non può essere stampato.\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Le temperature degli ugelli selezionate sono incompatibili. La temperatura dell'ugello per ciascun filamento deve rientrare nell'intervallo di temperatura consigliato per gli altri filamenti. In caso contrario, potrebbero verificarsi ostruzioni degli ugelli o danni alla stampante." @@ -12233,6 +12354,10 @@ msgstr "Se desideri comunque stampare, puoi abilitare l'opzione in Preferenze / msgid "No extrusions under current settings." msgstr "Nessuna estrusione con le impostazioni attuali." +# AI Translated +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "È in uso un filamento misto con gradiente, ma 'Sottostrato di colore misto' è disattivato. Il gradiente non verrà stampato." + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "La modalità fluida del timelapse non è supportata quando è abilitata la sequenza \"Per oggetto\"." @@ -12269,6 +12394,10 @@ msgstr "È possibile ridurre le dimensioni del modello o modificare le impostazi msgid "Variable layer height is not supported with Organic supports." msgstr "Altezza strato adattiva non è compatibile con i Supporti organici." +# AI Translated +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "Il filamento della torre di spurgo non può essere un filamento misto." + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Ugelli e filamenti di diverso diametro potrebbero non funzionare correttamente quando è abilitata la torre di spurgo. Questa funzione è sperimentale, quindi procedere con cautela." @@ -12550,9 +12679,6 @@ msgstr "Usa 3MF invece di G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Abilita questa opzione se la stampante accetta un file 3MF come processo di stampa. Quando è abilitata, Orca Slicer invia il file elaborato come .gcode.3mf, invece di un semplice file .gcode." -msgid "Printer Agent" -msgstr "Agente stampante" - msgid "Select the network agent implementation for printer communication." msgstr "Selezionare l'implementazione dell'agente di rete per la comunicazione con la stampante." @@ -12638,8 +12764,8 @@ msgstr "mm o %" msgid "Other layers" msgstr "Altri strati" -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." -msgstr "Temperatura del piatto per gli strati diversi dal primo. Un valore di 0 significa che il filamento non supporta la stampa su Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgstr "Indica la temperatura del piatto per tutti gli strati eccetto il primo. Un valore di 0 significa che il filamento non supporta la stampa su Cool Plate SuperTack." msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." msgstr "Indica la temperatura del piatto per tutti gli strati eccetto il primo. Un valore pari a 0 indica che il filamento non supporta la stampa su Piatto a bassa temperatura." @@ -13239,9 +13365,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Velocità dei ponti interni. Se il valore è espresso in percentuale, verrà calcolato in base a bridge_speed. Il valore predefinito è 150%." -msgid "Brim width" -msgstr "Larghezza tesa" - msgid "This is the distance from the model to the outermost brim line." msgstr "Questa è la distanza tra il modello e la linea più esterna della tesa." @@ -13321,6 +13444,14 @@ msgstr "" "La geometria verrà decimata prima di rilevare gli spigoli vivi. Questo parametro indica la lunghezza minima dello scostamento per la decimazione.\n" "0 per disattivare." +# AI Translated +msgid "Brim ears outer only" +msgstr "Tesa ad orecchio solo sul contorno esterno" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Genera gli orecchi di topo solo sul contorno esterno del modello, escludendo fori e sezioni chiuse." + msgid "upward compatible machine" msgstr "macchina compatibile con versioni successive" @@ -14010,6 +14141,8 @@ msgstr "Durata strato" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "La ventola di raffreddamento verrà attivata per gli strati in cui il tempo stimato è inferiore a questo valore. La velocità della ventola varierà tra la velocità minima e massima in base alla durata stimata di stampa dello strato." +# AI Translated +msgctxt "second" msgid "s" msgstr "s" @@ -14316,6 +14449,62 @@ msgstr "Materiale di supporto" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Il materiale di supporto viene comunemente utilizzato per stampare supporti e interfacce di supporto." +# AI Translated +msgid "Is mixed filament" +msgstr "È filamento misto" + +# AI Translated +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "Indica se questo slot di filamento è un filamento misto composto da più filamenti fisici" + +# AI Translated +msgid "Mixed filament components" +msgstr "Componenti del filamento misto" + +# AI Translated +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "Indici (a partire da 1) dei filamenti componenti, separati da virgole; es. \"1,3\"" + +# AI Translated +msgid "Mixed filament sublayer ratios" +msgstr "Proporzioni dei sottostrati del filamento misto" + +# AI Translated +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "Valori di proporzione separati da virgole la cui somma sia 1.0; es. \"0.7,0.3\"" + +# AI Translated +msgid "Mixed filament gradient" +msgstr "Gradiente del filamento misto" + +# AI Translated +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "Attiva la modalità gradiente in direzione Z per i sottostrati del filamento misto. Se attivata, le proporzioni dei sottostrati variano linearmente tra gli strati." + +# AI Translated +msgid "Mixed filament gradient range" +msgstr "Intervallo del gradiente del filamento misto" + +# AI Translated +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "Proporzioni iniziale e finale del primo componente in modalità gradiente. Coppia separata da virgola; es. \"0.10,0.90\" significa dal 10% al 90%." + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "Curva del gradiente del filamento misto" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "Curva personalizzata opzionale, in stile Photoshop, che mappa l'avanzamento in Z sulla proporzione del primo componente. Codificata come punti di controllo separati da barre verticali, nel formato \"x,y\" (legacy) oppure \"x,y,m_in,m_out\" quando è necessario forzare la tangente (un valore vuoto o \"nan\" usa il valore PCHIP predefinito). x è compreso in [0,1]; y viene limitato all'intervallo di proporzioni configurato; es. \"0,0.15|0.5,0.50|1,0.85\". Se vuoto, viene usato il gradient_range lineare." + +# AI Translated +msgid "Mixed filament per-part gradient" +msgstr "Gradiente per parte del filamento misto" + +# AI Translated +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "Quando la modalità gradiente è attiva, applica il gradiente a ciascuna parte di un assemblaggio in modo indipendente anziché trattare l'intero assemblaggio come un unico intervallo Z." + msgid "Filament printable" msgstr "Filamento stampabile" @@ -14495,6 +14684,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Giroide" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Fattore di arrotondamento del riempimento sparso" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Controlla quanto vengono arrotondati gli angoli del riempimento sparso. 0% mantiene il percorso originale con angoli vivi, mentre 100% produce le curve più ampie possibili tra linee di riempimento adiacenti." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Accelerazione del riempimento della superficie superiore. L'utilizzo di un valore inferiore può migliorare la qualità della superficie superiore." @@ -15039,6 +15236,14 @@ msgstr "Con quale tipo di G-code la stampante è compatibile." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Ometti il blocco di configurazione del G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Non scrive il CONFIG_BLOCK (le coppie chiave/valore della configurazione dello slicer) nel file G-code. Può essere utile con stampanti il cui firmware va in crash durante l'analisi di queste righe di commento (ad es. Anycubic go-klipper). Nota: il file G-code non conterrà più le impostazioni dello slicer, quindi reimportandolo in OrcaSlicer la configurazione non verrà ripristinata." + msgid "Pellet Modded Printer" msgstr "Stampante modificata per granuli" @@ -15567,6 +15772,7 @@ msgid "The allowed maximum output force of Y axis" msgstr "La forza di uscita massima consentita dell'asse Y" # AI Translated +msgctxt "Newton" msgid "N" msgstr "N" @@ -15577,6 +15783,7 @@ msgid "The machine bed mass load of Y axis" msgstr "Il carico di massa del piano della macchina sull'asse Y" # AI Translated +msgctxt "gram" msgid "g" msgstr "g" @@ -15888,7 +16095,7 @@ msgid "Reduce infill retraction" msgstr "Evita retrazione nel riempimento" # AI Translated -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." msgstr "Non ritrarre quando gli spostamenti si trovano interamente in un'area di riempimento. Ciò significa che il trasudo del materiale non è visibile. Questo può ridurre i tempi di retrazione per i modelli complessi e far risparmiare tempo di stampa, ma rende più lente l'elaborazione e la generazione del G-code. Nota che anche il sollevamento Z non viene eseguito nelle aree in cui la retrazione viene saltata." msgid "This option will drop the temperature of the inactive extruders to prevent oozing." @@ -16062,7 +16269,7 @@ msgstr "Quantità di retrazione dopo la pulizia" # AI Translated #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" "La lunghezza della retrazione rapida dopo la pulizia, relativa alla lunghezza di retrazione.\n" @@ -16098,10 +16305,18 @@ msgstr "Retrazione lunga al cambio estrusore" msgid "Retraction distance when extruder change" msgstr "Distanza di retrazione al cambio estrusore" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Lunghezza di retrazione (Cambio testina)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Quando la retrazione viene attivata prima di un cambio testina, il filamento viene ritirato della quantità specificata (la lunghezza è misurata sul filamento grezzo, prima che entri nell'estrusore)." + msgid "Z-hop height" msgstr "Altezza sollevamento Z" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "Ogni volta che si verifica una retrazione, l'ugello viene sollevato leggermente per creare spazio tra ugello e stampa. Ciò impedisce all'ugello di colpire la stampa negli spostamenti. L'uso di linee a spirale per il sollevamento sull'asse Z può evitare gli sfilacciamenti sulla stampa." msgid "Z-hop lower boundary" @@ -16195,6 +16410,10 @@ msgstr "Lunghezza aggiuntiva in ripresa" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Quando la retrazione è compensata dopo uno spostamento, l'estrusore espelle questa quantità aggiuntiva di filamento. Questa impostazione è raramente necessaria." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Lunghezza aggiuntiva in ripresa (Cambio testina)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Quando la retrazione è compensata dopo un cambio di testina, l'estrusore espelle questa quantità aggiuntiva di filamento." @@ -16612,6 +16831,14 @@ msgstr "Cambio utensile sulla torre di spurgo" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Forza la testa di stampa a spostarsi sulla torre di spurgo prima di emettere il comando di cambio utensile (Tx). Rilevante solo per le stampanti multi-estrusore (multi-testa) che utilizzano una torre di spurgo di Tipo 2. Per impostazione predefinita Orca salta lo spostamento sulle macchine multi-testa perché il firmware gestisce il cambio della testa, il che può far sì che il comando Tx venga emesso sopra la parte stampata. Abilita questa opzione se desideri che il cambio utensile venga sempre emesso sopra la torre di spurgo." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Attendi la temperatura sulla torre di spurgo" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Preleva la nuova testina senza attendere che raggiunga la temperatura di stampa, si sposta sulla torre di spurgo e attende lì la temperatura, subito prima dello spurgo. Il trasudo dovuto al riscaldamento finisce sulla torre invece che sul modello, e lo spostamento si sovrappone al riscaldamento. Rilevante solo per stampanti multi-estrusore (multi-testina) che usano una torre di spurgo di tipo 2. Il firmware o la macro di cambio testina non devono attendere la temperatura autonomamente. Quando è disattivato, l'attesa della temperatura viene emessa subito dopo il comando di cambio testina." + msgid "No sparse layers (beta)" msgstr "Nessuno strato sparso (beta)" @@ -17150,6 +17377,14 @@ msgstr "" "\n" "Impostando un valore di quantità di retrazione prima dell'impostazione di spurgo di seguito, verra eseguita qualsiasi retrazione in eccesso prima dello spurgo. Altrimenti verrà eseguita dopo." +# AI Translated +msgid "Mixed color sublayer" +msgstr "Sottostrato di colore misto" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "Attiva la suddivisione in sottostrati di colore misto. Se attivata, gli strati che contengono filamenti a colore misto vengono suddivisi in sottostrati per ottenere effetti di miscelazione dei colori." + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "La torre di spurgo può essere utilizzata per pulire i residui presenti sull'ugello e stabilizzare la pressione della camera all'interno dell'ugello, al fine di evitare difetti estetici durante la stampa." @@ -18221,6 +18456,10 @@ msgstr "La generazione della mesh del file del modello è fallita o la forma non msgid "The supplied file couldn't be read because it's empty." msgstr "Impossibile leggere il file fornito perché è vuoto" +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "Il formato del file non è compatibile e non può essere interpretato." + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Formato file sconosciuto: il file di input deve avere un'estensione .stl, .obj o .amf(.xml)." @@ -19705,21 +19944,21 @@ msgstr "Vengono visualizzate solo le stampanti con modifiche ai profili di stamp msgid "Only display the filament names with changes to filament presets." msgstr "Nomi dei filamenti con modifiche ai profili di filamento." -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "" "Nomi delle stampanti con profili creati dell'utente.\n" "Tutti i profili selezionati saranno esportati come file zip." msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" "Nomi dei filamenti con profili creati dell'utente.\n" "Tutti i profili selezionati saranno esportati come file zip." msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" "Nomi delle stampanti con profili di processo modificati.\n" "Tutti i profili selezionati saranno esportati come file zip." @@ -19865,9 +20104,6 @@ msgstr "Stampante fisica" msgid "Print Host upload" msgstr "Caricamento host di stampa" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Selezionare l'implementazione dell'agente di rete per la comunicazione con la stampante. Gli agenti disponibili vengono registrati all'avvio." - # AI Translated msgid "Select a Flashforge printer" msgstr "Seleziona una stampante Flashforge" @@ -19967,7 +20203,7 @@ msgid "We need information for diagnosing source of the issue. Check wiki page f msgstr "Abbiamo bisogno di informazioni per diagnosticare l'origine del problema. Consulta la pagina wiki per una guida dettagliata." # AI Translated -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "Il pulsante Comprimi raccoglie il file di progetto e i log della sessione corrente in un file zip." # AI Translated @@ -20023,7 +20259,7 @@ msgid "Stored logs" msgstr "Log memorizzati" # AI Translated -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "Comprime tutti i log memorizzati in un file zip." # AI Translated @@ -20111,7 +20347,7 @@ msgid "Authorizing..." msgstr "Autorizzazione in corso..." # AI Translated -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "Errore. Impossibile ottenere il token API per l'autorizzazione" # AI Translated @@ -20567,8 +20803,8 @@ msgid "Enable smart filament assign: Assign one filament to multiple nozzles to msgstr "Abilita l'assegnazione intelligente del filamento: assegna un filamento a più ugelli per massimizzare il risparmio" # AI Translated -msgid "Fila Saving" -msgstr "Risparmio filamento" +msgid "File Saving" +msgstr "Salvataggio file" msgid "Don't remind me again" msgstr "Non ricordarmelo più" @@ -20810,9 +21046,6 @@ msgstr "Si è verificato un problema imprevisto durante il tentativo di accesso. msgid "User canceled." msgstr "Utente rimosso." -msgid "Head diameter" -msgstr "Diametro testa" - msgid "Max angle" msgstr "Angolo massimo" @@ -20980,6 +21213,9 @@ msgstr "Riavvia ora" msgid "NO RAMMING AT ALL" msgstr "NESSUNA SPINTA" +msgid "s" +msgstr "s" + msgid "Volumetric speed" msgstr "Velocità volumetrica" @@ -21631,6 +21867,55 @@ msgstr "" "Evita le deformazioni\n" "Sapevi che quando si stampano materiali soggetti a deformazioni come l'ABS, aumentare in modo appropriato la temperatura del piano riscaldato può ridurre la probabilità di deformazione?" +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "La funzione di visualizzazione in tempo reale nativa di Wayland richiede il ricevitore video GTK di GStreamer. Installare il modulo gtksink per GStreamer e riavviare OrcaSlicer." + +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "Impossibile inizializzare il ricevitore video nativo di Wayland GStreamer. Verificare l'installazione del modulo GTK di GStreamer." + +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "Per questa operazione è necessario Windows Media Player! Desideri abilitare 'Windows Media Player' sul il tuo sistema operativo?" + +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "BambuSource non è stato registrato correttamente per la riproduzione multimediale! Fare clic su Sì per effettuare nuovamente la registrazione. Sarai promosso due volte" + +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "Componente BambuSource mancante per la riproduzione multimediale! Reinstallare OrcaSlicer o cercare aiuto nella comunità." + +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "È in uso una versione di BambuSource da un'installazione diversa. La riproduzione video potrebbe non funzionare correttamente! Fare clic su Sì per risolvere il problema." + +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "Nel tuo sistema mancano i codec H.264 per GStreamer, necessari per riprodurre i video. (Provare a installare i pacchetti gstreamer1.0-plugins-bad o gstreamer1.0-libav e riavviare OrcaSlicer?)" + +# AI Translated +#~ msgid "N" +#~ msgstr "N" + +# AI Translated +#~ msgid "g" +#~ msgstr "g" + +# AI Translated +#~ msgid "Fila Saving" +#~ msgstr "Risparmio filamento" + +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "L'altezza dello strato è troppo piccola.\n" +#~ "Sarà impostato su min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "L'altezza dello strato supera il limite in Impostazioni stampante -> Estrusore -> Limiti Altezza Strato. Ciò potrebbe causare problemi di qualità di stampa." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Regolare automaticamente l'intervallo impostato?\n" + +#~ msgid "Head diameter" +#~ msgstr "Diametro testa" + #~ msgid "Print order within a single layer." #~ msgstr "Ordine di stampa all'interno di un singolo strato." diff --git a/localization/i18n/ja/OrcaSlicer_ja.po b/localization/i18n/ja/OrcaSlicer_ja.po index 1c78f50b54..8ee8813f1d 100644 --- a/localization/i18n/ja/OrcaSlicer_ja.po +++ b/localization/i18n/ja/OrcaSlicer_ja.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -2969,6 +2969,10 @@ msgstr "編集" msgid "Merge with" msgstr "結合" +# AI Translated +msgid "Decompose Color" +msgstr "色を分解" + msgid "Delete this filament" msgstr "このフィラメントを削除" @@ -3266,6 +3270,10 @@ msgstr "アセンブリ" msgid "Merge parts to an object" msgstr "パーツをオブジェクトに結合" +# AI Translated +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "可変積層ピッチと混色サブレイヤーを併用すると、混色の品質が低下する場合があります。" + # AI Translated msgid "Add layers" msgstr "積層を追加" @@ -4750,6 +4758,23 @@ msgstr "現在のチャンバー温度が材料の安全温度を超えていま msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "最低庫内温度 (%d℃) が目標庫内温度 (%d℃) を上回っています。最低値は、チャンバーが目標に向けて加熱を続けながら印刷を開始するしきい値であるため、目標値を超えてはいけません。値は目標値に制限されます。" +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "積層ピッチが小さすぎます。最小値 (%g mm) に設定されます。" + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "積層ピッチが、プリンター設定 -> 押出機 -> 積層ピッチの制限 で設定された範囲を外れています。印刷品質の問題が発生する可能性があります。" + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "自動的に制限値 (%g mm) に調整しますか?" + +msgid "Adjust" +msgstr "調整" + # AI Translated msgid "" "Layer height too small\n" @@ -4801,7 +4826,7 @@ msgstr "" "\n" "値は0にリセットされます。" -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "垂直シェル厚さを「すべて」に設定すると、交互追加壁が適切に機能しません。" msgid "" @@ -4847,7 +4872,7 @@ msgstr "" "いいえ - 「独立サポート積層ピッチ」を有効にする" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_heightはlayer_heightより小さくする必要があります。\n" @@ -4855,7 +4880,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "ロック深さはスキン深さより小さくする必要があります。\n" @@ -4873,6 +4898,13 @@ msgstr "" "はい - Arachneウォールジェネレーターを有効にする\n" "いいえ - Arachneウォールジェネレーターを無効にし、ファジースキンを[変位]モードに設定する" +# AI Translated +msgid "Brim ear radius" +msgstr "ブリムイヤー半径" + +msgid "Brim width" +msgstr "ブリム幅" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "スパイラルモードは壁ループが1、サポートが無効、プロービングによるクランピング検出が無効、上部シェルレイヤーが0、スパースインフィル密度が0、タイムラプスタイプがトラディショナルの場合のみ機能します。" @@ -5127,6 +5159,14 @@ msgstr "キャリブレーションG-codeの生成に失敗しました" msgid "Calibration error" msgstr "キャリブレーションエラー" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "このプリンターには、このコントロールに必要なハードウェアが設定されていません。" + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "このコントロールはこのプリンターではサポートされていません。" + # AI Translated msgid "Network unavailable" msgstr "ネットワークが利用できません" @@ -5988,7 +6028,7 @@ msgstr "ボリューム" msgid "Size:" msgstr "サイズ:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "レイヤー%d、Z = %.2lfmmでG-codeパスの衝突が検出されました。衝突するオブジェクトをもっと離してください(%s <-> %s)。" @@ -6164,6 +6204,10 @@ msgstr "マルチデバイス" msgid "Project" msgstr "プロジェクト" +# AI Translated +msgid "Device (Web)" +msgstr "デバイス (Web)" + msgid "Yes" msgstr "はい" @@ -6298,10 +6342,10 @@ msgstr "3MF/STL/STEP/SVG/OBJ/AMFをインポート" msgid "Load a model" msgstr "モデルを読み込む" -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "ZIPアーカイブをインポート" -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "ZIPアーカイブ内のモデルをロード" msgid "Import Configs" @@ -7823,6 +7867,10 @@ msgstr "現在のプレートをカスタマイズ" msgid "The %s nozzle can not print %s." msgstr "%sノズルは%sを印刷できません。" +# AI Translated +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "単一押出機のプリンターで混色フィラメントを印刷すると、フィラメント交換とフラッシュが頻繁に発生し、廃材やノズル/廃棄シュートの詰まりのリスクが大幅に増加する可能性があります。" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "%1%と%2%を混合して印刷することは推奨されません。\n" @@ -7948,12 +7996,44 @@ msgstr "AMSと素材を同期" msgid "Set filaments to use" msgstr "フィラメントを選択" +# AI Translated +msgid "Add Mixed Filament" +msgstr "混合フィラメントを追加" + +# AI Translated +msgid "Mixed Filament" +msgstr "混合フィラメント" + +# AI Translated +msgid "Remove last mixed filament" +msgstr "最後の混合フィラメントを削除" + +# AI Translated +msgid "Add mixed filament" +msgstr "混合フィラメントを追加" + +# AI Translated +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "混合フィラメントのコンポーネントが無効か一致していません。該当する項目を編集し直してください。" + msgid "Search plate, object and part." msgstr "プレート、オブジェクト、パーツを検索。" msgid "Pellets" msgstr "ペレット" +# AI Translated +msgid "Mixed filament has broken component references" +msgstr "混合フィラメントのコンポーネント参照が壊れています" + +# AI Translated +msgid "Edit / Delete / Merge" +msgstr "編集 / 削除 / 結合" + +# AI Translated +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "対象の混合フィラメントはこの物理フィラメントをコンポーネントとして使用しています。結合するとこの物理フィラメントは削除され、混合フィラメントが無効になる可能性があります。続行しますか?" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "操作完了後、%sプロジェクトが閉じられ、新しいプロジェクトが作成されます。" @@ -8097,8 +8177,8 @@ msgstr "これらのプリセット内のG-codeがマシンに損傷を与えな msgid "Customized Preset" msgstr "カスタマイズされたプリセット" -msgid "Component name(s) inside step file not in UTF8 format!" -msgstr "ファイルのエンコーディング方式はUTF8形式ではありません" +msgid "Component name(s) inside step file not in UTF-8 format!" +msgstr "ファイルのエンコーディング方式はUTF-8形式ではありません" msgid "Because of unsupported text encoding, garbage characters may appear!" msgstr "文字化けがあるようです、ご確認ください" @@ -8119,7 +8199,7 @@ msgstr "オブジェクトの体積が0です" #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "ファイル %s 中のオブジェクトが小さいすぎます、ファイルの単位をご確認ください。\n" "mm単位に変換しますか?" @@ -8140,6 +8220,14 @@ msgstr "" msgid "Multi-part object detected" msgstr "マルチパーツ検出" +# AI Translated +msgid "Matching textures to filaments" +msgstr "テクスチャをフィラメントに対応付けています" + +# AI Translated +msgid "Texture Import Warning" +msgstr "テクスチャインポートの警告" + msgid "Load these files as a single object with multiple parts?\n" msgstr "これらのファイルを一つのオブジェクトとしてロードしますか?\n" @@ -8262,21 +8350,21 @@ msgstr "置換用のディレクトリが選択されていません" msgid "Replaced with 3D files from directory:\n" msgstr "ディレクトリの3Dファイルで置換しました:\n" -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ スキップ %1%: 同一ファイル。\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ スキップ %s: 同一ファイル。\n" -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ スキップ %1%: ファイルが存在しません。\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ スキップ %s: ファイルが存在しません。\n" -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ スキップ %1%: 置換に失敗しました。\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ スキップ %s: 置換に失敗しました。\n" -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔ 置換しました %1%。\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔ 置換しました %s。\n" msgid "Replaced volumes" msgstr "置換されたボリューム" @@ -8353,6 +8441,22 @@ msgstr "" msgid "Sync now" msgstr "今すぐ同期" +# AI Translated +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "テクスチャのインポートに失敗しました。モデルにテクスチャデータが含まれているようですが、インポート処理を完了できませんでした。モデルはジオメトリのみとしてインポートされます。" + +# AI Translated +msgid "Applying texture colors..." +msgstr "テクスチャの色を適用しています..." + +# AI Translated +msgid "Updating 3D view..." +msgstr "3Dビューを更新しています..." + +# AI Translated +msgid "Texture colors applied." +msgstr "テクスチャの色を適用しました。" + msgid "You can keep the modified presets for the new project or discard them" msgstr "変更したプリセットをプロジェクト内に保存するか、破棄もできます" @@ -9015,6 +9119,18 @@ msgstr "このオプションを有効にすると、複数のデバイスに同 msgid "Pop up to select filament grouping mode" msgstr "フィラメントグルーピングモード選択のポップアップ" +# AI Translated +msgid "Visible plugin pages" +msgstr "表示するプラグインページ数" + +# AI Translated +msgid "pages" +msgstr "ページ" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "固定タブとして表示するプラグインページの数です。残りのページは最後のタブのドロップダウンにまとめられます。" + msgid "Behaviour" msgstr "動作" @@ -9404,6 +9520,18 @@ msgstr "非対応のプリセットを表示" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "プリンターとフィラメントのドロップダウンリストに、互換性のない/非対応のプリセットを表示します。これらのプリセットは選択できません。" +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(実験的) プリントホストの代わりにプリンターエージェントを使用" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Bambu 以外のプリンターの印刷ジョブを、従来のプリントホストへのアップロードではなく、プリンターのプラグインエージェント経由で送信します。\n" +"無効の場合、OrcaSlicer は従来のプリントホストの動作を使用します。" + # AI Translated msgid "Experimental Features" msgstr "実験的機能" @@ -9616,6 +9744,10 @@ msgstr "スパイラル" msgid "First layer filament sequence" msgstr "初期レイヤーフィラメント順序" +# AI Translated +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "フィラメントリストに混合フィラメントが含まれています。カスタムフィラメント順序は適用されません。" + msgid "By Layer" msgstr "レイヤー別" @@ -9672,9 +9804,25 @@ msgstr "ユーザープリセット" msgid "Preset Inside Project" msgstr "プロジェクト プリセット" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "親プリセットから継承したすべての値をこのプリセットにコピーし、親との継承関係を解除します。親プリセットとのみ互換性のあるプリセットは、サポートされなくなる場合があります。" + msgid "Detach from parent" msgstr "親から分離" +# AI Translated +msgid "Unique preset" +msgstr "独立したプリセット" + +# AI Translated +msgid "Parent preset" +msgstr "親プリセット" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "このプリセットは他のプリセットを継承していません。" + msgid "Name is unavailable." msgstr "名称は使用できません" @@ -10416,22 +10564,6 @@ msgstr "このオプションを有効にしてもよろしいですか?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "インフィルパターンは通常、適切な印刷と意図した効果を確保するために回転を自動的に処理するように設計されています(例: ジャイロイド、キュービック)。現在のスパースインフィルパターンを回転させると、サポートが不十分になる可能性があります。慎重に進め、潜在的な印刷問題を十分に確認してください。このオプションを有効にしてもよろしいですか?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"レイヤー高さが小さすぎます。\n" -"min_layer_heightに設定されます\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "レイヤー高さがプリンター設定 -> エクストルーダー -> レイヤー高さ制限の上限を超えています。印刷品質の問題が発生する可能性があります。" - -msgid "Adjust to the set range automatically?\n" -msgstr "設定範囲に自動調整しますか?\n" - -msgid "Adjust" -msgstr "調整" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "実験的機能: フィラメント交換時により長い距離でフィラメントをリトラクト・カットしてフラッシュを最小化します。フラッシュを大幅に削減できますが、ノズル詰まりやその他の印刷問題のリスクが高まる可能性もあります。" @@ -10557,7 +10689,7 @@ msgid "Junction Deviation" msgstr "接合偏差" msgid "Jerk(XY)" -msgstr "ジャーク(XY)" +msgstr "Jerk(XY)" msgid "Raft" msgstr "ラフト" @@ -10621,6 +10753,9 @@ msgstr "保留キーワードが見つかりました" msgid "Setting Overrides" msgstr "上書き設定" +msgid "Retraction when switching material" +msgstr "素材変更時のリトラクション" + msgid "Basic information" msgstr "基本情報" @@ -10751,6 +10886,12 @@ msgstr "互換性のあるプロセスプロファイル" msgid "Printable space" msgstr "造形可能領域" +msgid "Printer Agent" +msgstr "プリンターエージェント" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "プリンター通信用のネットワークエージェント実装を選択します。使用可能なエージェントは起動時に登録されます。" + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10845,7 +10986,7 @@ msgid "Acceleration limitation" msgstr "加速制限" msgid "Jerk limitation" -msgstr "振動特性" +msgstr "Jerk制限" msgid "Single extruder multi-material setup" msgstr "シングルエクストルーダー マルチマテリアル設定" @@ -10877,9 +11018,6 @@ msgstr "積層ピッチの制限" msgid "Z-Hop" msgstr "Z-ホップ" -msgid "Retraction when switching material" -msgstr "素材変更時のリトラクション" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -11002,12 +11140,12 @@ msgid "No modifications need to be copied." msgstr "コピーが必要な変更はありません。" # AI Translated -msgid "Copy paramters" +msgid "Copy parameters" msgstr "パラメータをコピー" # AI Translated #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "%sのパラメータを変更" # AI Translated @@ -11525,33 +11663,6 @@ msgstr "フィラメントを入替える為のフラッシュ量" msgid "Please choose the filament colour" msgstr "フィラメントの色を選択してください" -# AI Translated -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "ネイティブWaylandのライブビューにはGStreamer GTKビデオシンクが必要です。GStreamer用のgtksinkプラグインをインストールし、OrcaSlicerを再起動してください。" - -# AI Translated -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "ネイティブWaylandのGStreamerビデオシンクの初期化に失敗しました。GStreamer GTKプラグインのインストール状況をご確認ください。" - -# AI Translated -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "このタスクにはWindows Media Playerが必要です!お使いのOSで「Windows Media Player」を有効にしますか?" - -# AI Translated -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "メディア再生用のBambuSourceが正しく登録されていません!「はい」を押して再登録してください。確認が2回表示されます" - -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "メディア再生用のBambuSourceコンポーネントが見つかりません!OrcaSlicerを再インストールするかコミュニティに助けを求めてください。" - -# AI Translated -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "別のインストール環境のBambuSourceを使用しているため、動画が正しく再生されない可能性があります!「はい」を押して修正してください。" - -# AI Translated -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "お使いのシステムには、動画再生に必要なGStreamer用のH.264コーデックがありません。(gstreamer1.0-plugins-badまたはgstreamer1.0-libavパッケージをインストールし、Orca Slicerを再起動してみてください)" - # AI Translated msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "クラウドエージェントが利用できません。OrcaSlicerを再起動して再試行してください。" @@ -12258,6 +12369,10 @@ msgstr " は除外エリアに近すぎるため、衝突が発生します。\n msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " がクランピング検出エリアに近すぎ、衝突が発生します。\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " は造形可能領域から一部はみ出しているため、印刷できません。\n" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "選択したノズル温度に互換性がありません。各フィラメントのノズル温度は、他のフィラメントの推奨ノズル温度範囲内に収まる必要があります。そうでない場合、ノズル詰まりやプリンターの損傷が発生する可能性があります。" @@ -12273,6 +12388,10 @@ msgstr "それでも印刷する場合は、環境設定 / 制御 / スライス msgid "No extrusions under current settings." msgstr "現在の設定では造形しません" +# AI Translated +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "グラデーション付きの混合フィラメントが使用されていますが、「混色サブレイヤー」が無効です。グラデーションは印刷されません。" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "オブジェクト順で造形するでは、この機能を使用できません。" @@ -12309,6 +12428,10 @@ msgstr "モデルのサイズを小さくするか、現在のプリント設定 msgid "Variable layer height is not supported with Organic supports." msgstr "可変レイヤー高さはオーガニックサポートではサポートされていません。" +# AI Translated +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "ワイプタワーのフィラメントに混合フィラメントは使用できません。" + # AI Translated msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "ノズル径やフィラメント径が異なる場合、プライムタワーを有効にすると正しく動作しないことがあります。非常に実験的な機能ですので、慎重にお進みください。" @@ -12415,7 +12538,7 @@ msgid "Plate %d: %s does not support filament %s" msgstr "プレート %d: %s がフィラメント %s を使用できません" msgid "Setting the jerk speed too low could lead to artifacts on curved surfaces" -msgstr "ジャーク速度を低く設定しすぎると曲面にアーティファクトが発生する可能性があります" +msgstr "Jerk速度を低く設定しすぎると曲面にアーティファクトが発生する可能性があります" # AI Translated msgid "" @@ -12599,9 +12722,6 @@ msgstr "G-codeの代わりに3MFを使用" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "プリンターが印刷ジョブとして3MFファイルを受け付ける場合に有効にします。有効にすると、Orca Slicerはスライス済みファイルを通常の.gcodeファイルではなく.gcode.3mfとして送信します。" -msgid "Printer Agent" -msgstr "プリンターエージェント" - msgid "Select the network agent implementation for printer communication." msgstr "プリンター通信用のネットワークエージェント実装を選択します。" @@ -12689,14 +12809,12 @@ msgstr "mm 或は %" msgid "Other layers" msgstr "他の層" -# AI Translated -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." msgstr "1層目を除く各層のベッド温度です。0の場合、そのフィラメントはCool Plate SuperTackでの印刷に対応していません。" msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." msgstr "ベッドの温度です(1層目以外)。値が0の場合、フィラメントが常温プレートで使用できないという意味です。" -# AI Translated msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Textured Cool Plate." msgstr "1層目を除く各層のベッド温度です。0の場合、そのフィラメントはTextured Cool Plateでの印刷に対応していません。" @@ -12707,7 +12825,7 @@ msgid "This is the bed temperature for layers except for the first one. A value msgstr "ベッドの温度です(1層目以外)。値が0の場合、フィラメントが高温プレートで使用できないという意味です。" msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Textured PEI Plate." -msgstr "1層目以外のベッド温度。値が0の場合、フィラメントがPEIプレートをサポートしない意味をします。" +msgstr "1層目を除く各層のベッド温度です。0の場合、そのフィラメントはTextured PEI Plateでの印刷に対応していません。" msgid "First layer" msgstr "1層目" @@ -13320,9 +13438,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "内部ブリッジの速度です。値を%で指定した場合、bridge_speedを基準に計算されます。デフォルト値は150%です。" -msgid "Brim width" -msgstr "ブリム幅" - msgid "This is the distance from the model to the outermost brim line." msgstr "一番外側のブリム線がモデルと距離です。" @@ -13411,6 +13526,14 @@ msgstr "" "鋭角を検出する前にジオメトリが間引かれます。このパラメータは、間引きにおける偏差の最小長さを指定します。\n" "0で無効になります。" +# AI Translated +msgid "Brim ears outer only" +msgstr "ブリムイヤーを外側の輪郭のみ" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "穴や閉じた部分を除き、モデルの外側の輪郭にのみマウスイヤーを生成します。" + msgid "upward compatible machine" msgstr "互換性のあるデバイス" @@ -14128,6 +14251,8 @@ msgstr "積層時間" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "パーツ冷却ファンは、積層造形時間がこの値より短い時に作動します。" +# AI Translated +msgctxt "second" msgid "s" msgstr "s" @@ -14446,6 +14571,62 @@ msgstr "サポート材料" msgid "Support material is commonly used to print supports and support interfaces." msgstr "サポート素材は、サポート又はサポート接触面の造形によく使われます。" +# AI Translated +msgid "Is mixed filament" +msgstr "混合フィラメントかどうか" + +# AI Translated +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "このフィラメントスロットが複数の物理フィラメントで構成される混合フィラメントかどうか" + +# AI Translated +msgid "Mixed filament components" +msgstr "混合フィラメントのコンポーネント" + +# AI Translated +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "構成フィラメントの番号(1 始まり)をカンマ区切りで指定します。例: \"1,3\"" + +# AI Translated +msgid "Mixed filament sublayer ratios" +msgstr "混合フィラメントのサブレイヤー比率" + +# AI Translated +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "合計が 1.0 になる比率をカンマ区切りで指定します。例: \"0.7,0.3\"" + +# AI Translated +msgid "Mixed filament gradient" +msgstr "混合フィラメントのグラデーション" + +# AI Translated +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "混合フィラメントのサブレイヤーに対して Z 方向のグラデーションモードを有効にします。有効にすると、サブレイヤーの比率が積層ごとに直線的に変化します。" + +# AI Translated +msgid "Mixed filament gradient range" +msgstr "混合フィラメントのグラデーション範囲" + +# AI Translated +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "グラデーションモードにおける最初のコンポーネントの開始比率と終了比率。カンマ区切りの 2 つの値で指定します。例: \"0.10,0.90\" は 10% から 90% を意味します。" + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "混合フィラメントのグラデーションカーブ" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "Z 方向の進行度を最初のコンポーネントの比率に対応付ける、Photoshop 風のカスタムカーブ(省略可)。制御点を縦棒区切りでエンコードし、\"x,y\"(旧形式)または接線を上書きする場合は \"x,y,m_in,m_out\" の形式で指定します(空欄または \"nan\" は PCHIP の既定値を使用します)。x は [0,1] の範囲で、y は設定された比率範囲に制限されます。例: \"0,0.15|0.5,0.50|1,0.85\"。空欄の場合は線形の gradient_range が使用されます。" + +# AI Translated +msgid "Mixed filament per-part gradient" +msgstr "混合フィラメントのパーツ別グラデーション" + +# AI Translated +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "グラデーションモードが有効な場合、アセンブリ全体を 1 つの Z 範囲として扱うのではなく、アセンブリ内の各パーツに個別にグラデーションを適用します。" + msgid "Filament printable" msgstr "フィラメント印刷可能" @@ -14634,6 +14815,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "ジャイロイド" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "スパース インフィルの平滑化係数" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "スパース インフィルの角をどの程度丸めるかを設定します。0% では元の鋭い経路のまま、100% では隣接するインフィル線の間で可能な限り大きな曲線になります。" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "トップ面のインフィル加速度です。遅くすると表面の仕上がりが向上させることができます" @@ -14670,7 +14859,7 @@ msgid "Klipper's max_accel_to_decel will be adjusted to this %% of acceleration. msgstr "Klipperのmax_accel_to_decelが、加速度のこの%%に調整されます。" msgid "Default jerk." -msgstr "デフォルトジャーク。" +msgstr "デフォルトのJerkです。" # AI Translated msgid "Marlin Firmware Junction Deviation (replaces the traditional XY Jerk setting)." @@ -15233,6 +15422,14 @@ msgstr "プリンターが対応するG-code" msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "G-code の設定ブロックを省略" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "CONFIG_BLOCK (スライサー設定のキーと値のペア) を G-code ファイルに書き込みません。これらのコメント行の解析でファームウェアがクラッシュするプリンター (例: Anycubic go-klipper) で役立ちます。注意: G-code ファイルにスライサー設定が含まれなくなるため、OrcaSlicer に読み込み直しても設定は復元されません。" + # AI Translated msgid "Pellet Modded Printer" msgstr "ペレット改造プリンター" @@ -15753,28 +15950,28 @@ msgid "Maximum acceleration of the E axis" msgstr "E軸最大加速度" msgid "Maximum jerk X" -msgstr "最大振動 X" +msgstr "最大Jerk X" msgid "Maximum jerk Y" -msgstr "最大振動 Y" +msgstr "最大Jerk Y" msgid "Maximum jerk Z" -msgstr "最大振動 Z" +msgstr "最大Jerk Z" msgid "Maximum jerk E" -msgstr "最大振動 E" +msgstr "最大Jerk E" msgid "Maximum jerk of the X axis" -msgstr "最大振動 X" +msgstr "X軸最大Jerk" msgid "Maximum jerk of the Y axis" -msgstr "最大振動 Y" +msgstr "Y軸最大Jerk" msgid "Maximum jerk of the Z axis" -msgstr "最大振動 Z" +msgstr "Z軸最大Jerk" msgid "Maximum jerk of the E axis" -msgstr "最大振動 E" +msgstr "E軸最大Jerk" msgid "Maximum Junction Deviation" msgstr "最大接合偏差" @@ -15806,6 +16003,7 @@ msgid "The allowed maximum output force of Y axis" msgstr "Y軸の許容最大出力" # AI Translated +msgctxt "Newton" msgid "N" msgstr "N" @@ -15816,6 +16014,7 @@ msgid "The machine bed mass load of Y axis" msgstr "Y軸のマシンベッド質量" # AI Translated +msgctxt "gram" msgid "g" msgstr "g" @@ -16150,7 +16349,7 @@ msgstr "カッター領域から廃料排出口までの終始点" msgid "Reduce infill retraction" msgstr "インフィルのリトラクション低減" -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." msgstr "インフィル領域内の移動はリトラクションしません。造形時間を節約できます。" msgid "This option will drop the temperature of the inactive extruders to prevent oozing." @@ -16335,7 +16534,7 @@ msgstr "ワイプ後のリトラクション量" # AI Translated #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" "ワイプ後の高速リトラクションの長さで、リトラクション長に対する割合です。\n" @@ -16374,11 +16573,19 @@ msgstr "押出機切り替え時のロングリトラクション" msgid "Retraction distance when extruder change" msgstr "押出機切替時のリトラクション距離" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "リトラクション量 (ツール交換)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "ツール交換の前にリトラクションが行われるとき、指定した量だけフィラメントが引き戻されます (長さは押出機に入る前の未加工のフィラメントで測定されます)。" + # AI Translated msgid "Z-hop height" msgstr "Zホップの高さ" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "リトラクション時に、ノズルを少し上げてから移動します。この動作でモデルとの衝突を回避できます。" # AI Translated @@ -16488,6 +16695,10 @@ msgstr "再開時の追加長さ" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "移動後に引込みが補償されると、エクストルーダーはこの追加量のフィラメントを押し出します。 この設定はほとんど必要ありません。" +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "再開時の追加長さ (ツール交換)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "ツールの交換後に吸込み分が補正されると、エクストルーダーはこの追加量のフィラメントを押し出します。" @@ -16963,6 +17174,14 @@ msgstr "ワイプタワー上でツール交換" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "ツール交換コマンド (Tx) を発行する前に、ツールヘッドを強制的にワイプタワーへ移動させます。タイプ2のワイプタワーを使用するマルチ押出機 (マルチツールヘッド) プリンターにのみ関係します。デフォルトでは、マルチツールヘッド機ではファームウェアがヘッドの交換を処理するためOrcaは移動をスキップしますが、その結果Txコマンドが造形物の上で発行される場合があります。ツール交換を常にワイプタワーの上で発行したい場合は、このオプションを有効にしてください。" +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "ワイプタワーで温度待機" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "印刷温度に達するのを待たずに新しいツールを取り付け、ワイプタワーへ移動し、パージ直前にそこで温度を待ちます。加熱中の垂れ出しはモデルではなくタワーに落ち、移動時間が加熱と重なります。タイプ 2 のワイプタワーを使用するマルチ押出機 (マルチツールヘッド) プリンターでのみ有効です。ファームウェアやツール交換マクロ側で温度待機を行わないようにしてください。無効の場合、温度待機はツール交換コマンドの直後に出力されます。" + # AI Translated msgid "No sparse layers (beta)" msgstr "スパース層なし (ベータ)" @@ -17525,6 +17744,14 @@ msgstr "" "\n" "下の「ワイプ前のリトラクション量」設定に値を設定すると、超過分のリトラクションはワイプの前に行われます。それ以外の場合はワイプの後に行われます。" +# AI Translated +msgid "Mixed color sublayer" +msgstr "混色サブレイヤー" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "混色サブレイヤーへの分割を有効にします。有効にすると、混色フィラメントを含む積層がサブレイヤーに分割され、色を混ぜる効果が得られます。" + # AI Translated msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "ワイプタワーは、ノズルに残った樹脂を除去し、ノズル内のチャンバー圧力を安定させることで、オブジェクト印刷時の外観不良を防ぐために使用できます。" @@ -18331,10 +18558,10 @@ msgid "Allow 3MF with newer version to be sliced." msgstr "新しいバージョンの3MFのスライスを許可します。" msgid "Current Z-hop" -msgstr "現在のz-hop" +msgstr "現在のZ-hop" msgid "Contains Z-hop present at the beginning of the custom G-code block." -msgstr "カスタムGコードブロックの先頭に存在するz-hopを含む。" +msgstr "カスタムGコードブロックの先頭に存在するZ-hopを含む。" msgid "Position of the extruder at the beginning of the custom G-code block. If the custom G-code travels somewhere else, it should write to this variable so OrcaSlicer knows where it travels from when it gets control back." msgstr "カスタム G コード ブロックの先頭のエクストルーダーのモーターの位置。 カスタム G コードで動かしたとき、PrusaSlicer が制御を取り戻したときにどこから移動したかを認識できるように、この変数に書き込む必要があります。" @@ -18696,6 +18923,10 @@ msgstr "モデルファイルのメッシュ処理に失敗したか、有効な msgid "The supplied file couldn't be read because it's empty." msgstr "提供されたファイルは空であるため読み込めませんでした。" +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "ファイル形式に互換性がないため、解析できません。" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "ファイル形式が不明です。入力ファイルの拡張子は .stl、.obj、.amf(.xml)である必要があります。" @@ -20232,19 +20463,19 @@ msgstr "プリンタ、フィラメント、加工プリセットに変更があ msgid "Only display the filament names with changes to filament presets." msgstr "フィラメントプリセットを変更したフィラメント名のみを表示します。" -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "ユーザープリンタプリセットを持つプリンタ名のみが表示され、選択した各プリセットがZIPとしてエクスポートされます。" msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" "ユーザーフィラメントプリセットを含むフィラメント名のみが表示されます、 \n" "選択した各フィラメント名に含まれるすべてのユーザーフィラメントプリセットがZIP形式でエクスポートされます。" msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "変更されたプロセスプリセットがあるプリンタ名のみが表示され、選択した各プリンタ名にあるすべてのユーザープロセスプリセットがZIP形式でエクスポートされます。" msgid "Please select at least one printer or filament." @@ -20389,9 +20620,6 @@ msgstr "実物プリンター" msgid "Print Host upload" msgstr "プリントホストのアップロード" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "プリンター通信用のネットワークエージェント実装を選択します。使用可能なエージェントは起動時に登録されます。" - # AI Translated msgid "Select a Flashforge printer" msgstr "Flashforgeプリンターを選択" @@ -20491,7 +20719,7 @@ msgid "We need information for diagnosing source of the issue. Check wiki page f msgstr "問題の原因を特定するために情報が必要です。詳しい手順はWikiページをご覧ください。" # AI Translated -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "「パック」ボタンは、現在のセッションのプロジェクトファイルとログをzipファイルにまとめます。" # AI Translated @@ -20547,7 +20775,7 @@ msgid "Stored logs" msgstr "保存されたログ" # AI Translated -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "保存されたすべてのログをzipファイルにまとめます。" # AI Translated @@ -20635,7 +20863,7 @@ msgid "Authorizing..." msgstr "認証中..." # AI Translated -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "エラー。認証用のAPIトークンを取得できません" # AI Translated @@ -21121,8 +21349,8 @@ msgid "Enable smart filament assign: Assign one filament to multiple nozzles to msgstr "スマートフィラメント割り当てを有効にする: 1つのフィラメントを複数のノズルに割り当てて節約を最大化します" # AI Translated -msgid "Fila Saving" -msgstr "フィラ節約" +msgid "File Saving" +msgstr "ファイル保存" msgid "Don't remind me again" msgstr "再度通知しない" @@ -21363,9 +21591,6 @@ msgstr "ログイン中に予期しない問題が発生しました。再試行 msgid "User canceled." msgstr "ユーザーがキャンセルしました。" -msgid "Head diameter" -msgstr "直径" - msgid "Max angle" msgstr "最大角度" @@ -21532,6 +21757,9 @@ msgstr "今すぐ再起動" msgid "NO RAMMING AT ALL" msgstr "ラミングなし" +msgid "s" +msgstr "s" + msgid "Volumetric speed" msgstr "体積速度" @@ -22194,6 +22422,61 @@ msgstr "" "反りを避ける\n" "ABSのような反りやすい素材を印刷する場合、ヒートベッドの温度を適切に上げることで、反りが発生する確率を下げることができることをご存知ですか?" +# AI Translated +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "ネイティブWaylandのライブビューにはGStreamer GTKビデオシンクが必要です。GStreamer用のgtksinkプラグインをインストールし、OrcaSlicerを再起動してください。" + +# AI Translated +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "ネイティブWaylandのGStreamerビデオシンクの初期化に失敗しました。GStreamer GTKプラグインのインストール状況をご確認ください。" + +# AI Translated +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "このタスクにはWindows Media Playerが必要です!お使いのOSで「Windows Media Player」を有効にしますか?" + +# AI Translated +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "メディア再生用のBambuSourceが正しく登録されていません!「はい」を押して再登録してください。確認が2回表示されます" + +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "メディア再生用のBambuSourceコンポーネントが見つかりません!OrcaSlicerを再インストールするかコミュニティに助けを求めてください。" + +# AI Translated +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "別のインストール環境のBambuSourceを使用しているため、動画が正しく再生されない可能性があります!「はい」を押して修正してください。" + +# AI Translated +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "お使いのシステムには、動画再生に必要なGStreamer用のH.264コーデックがありません。(gstreamer1.0-plugins-badまたはgstreamer1.0-libavパッケージをインストールし、Orca Slicerを再起動してみてください)" + +# AI Translated +#~ msgid "N" +#~ msgstr "N" + +# AI Translated +#~ msgid "g" +#~ msgstr "g" + +# AI Translated +#~ msgid "Fila Saving" +#~ msgstr "フィラ節約" + +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "レイヤー高さが小さすぎます。\n" +#~ "min_layer_heightに設定されます\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "レイヤー高さがプリンター設定 -> エクストルーダー -> レイヤー高さ制限の上限を超えています。印刷品質の問題が発生する可能性があります。" + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "設定範囲に自動調整しますか?\n" + +#~ msgid "Head diameter" +#~ msgstr "直径" + #~ msgid "Print order within a single layer." #~ msgstr "単一レイヤー内の印刷順序。" diff --git a/localization/i18n/ko/OrcaSlicer_ko.po b/localization/i18n/ko/OrcaSlicer_ko.po index debb3fb818..c415c90195 100644 --- a/localization/i18n/ko/OrcaSlicer_ko.po +++ b/localization/i18n/ko/OrcaSlicer_ko.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "PO-Revision-Date: 2025-06-02 17:12+0900\n" "Last-Translator: crwusiz <crwusiz@gmail.com>\n" "Language-Team: \n" @@ -2970,6 +2970,10 @@ msgstr "편집" msgid "Merge with" msgstr "병합" +# AI Translated +msgid "Decompose Color" +msgstr "색상 분해" + msgid "Delete this filament" msgstr "이 필라멘트 삭제" @@ -3266,6 +3270,10 @@ msgstr "조립" msgid "Merge parts to an object" msgstr "부품을 하나의 객체로 병합" +# AI Translated +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "가변 레이어 높이를 혼색 서브레이어와 함께 사용하면 색상 혼합 품질이 떨어질 수 있습니다." + # AI Translated msgid "Add layers" msgstr "레이어 추가" @@ -4763,6 +4771,23 @@ msgstr "현재 챔버 온도가 재료의 안전 온도보다 높으므로 재 msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "최소 챔버 온도(%d℃)가 목표 챔버 온도(%d℃)보다 높습니다. 최소값은 챔버가 목표 온도까지 계속 가열되는 동안 출력을 시작하는 기준값이므로 목표값을 초과해서는 안 됩니다. 이 값은 목표값으로 제한됩니다." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "레이어 높이가 너무 작습니다. 최솟값(%g mm)으로 설정됩니다." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "레이어 높이가 프린터 설정 -> 압출기 -> 레이어 높이 한도에서 설정한 범위를 벗어났습니다. 출력 품질 문제가 발생할 수 있습니다." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "한도(%g mm)에 맞게 자동으로 조정할까요?" + +msgid "Adjust" +msgstr "조정" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4812,7 +4837,7 @@ msgstr "" "\n" "값이 0으로 재설정됩니다." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "세로 쉘 두께가 모두로 설정된 경우 대체 여분의 벽이 제대로 작동하지 않습니다." msgid "" @@ -4858,7 +4883,7 @@ msgstr "" "아니요 - 독립적 서포트 레이어 높이 유지" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "심_경사_시작_높이는 레이어_높이보다 작아야 합니다.\n" @@ -4866,7 +4891,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "잠금 깊이는 스킨 깊이보다 작아야 합니다.\n" @@ -4884,6 +4909,13 @@ msgstr "" "예 - 아라크네 벽 생성기 활성화\n" "아니오 - 아라크네 벽 생성기 비활성화 및 퍼지 스킨 [변위] 모드 설정" +# AI Translated +msgid "Brim ear radius" +msgstr "브림 귀 반경" + +msgid "Brim width" +msgstr "브림 너비" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "나선형 모드는 벽 루프가 1이고, 서포트가 비활성화되고, 프로빙에 의한 클럼핑 감지가 비활성화되고, 상단 셸 레이어가 0이고, 희소 인필 밀도가 0이고 타임랩스 유형이 전통적인 경우에만 작동합니다." @@ -5138,6 +5170,14 @@ msgstr "교정 Gcode를 생성하지 못했습니다" msgid "Calibration error" msgstr "교정 오류" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "이 프린터에는 이 컨트롤에 필요한 하드웨어가 구성되어 있지 않습니다." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "이 컨트롤은 이 프린터에서 지원되지 않습니다." + # AI Translated msgid "Network unavailable" msgstr "네트워크를 사용할 수 없음" @@ -6001,7 +6041,7 @@ msgstr "용량:" msgid "Size:" msgstr "크기:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "레이어 %d, Z = %.2lf mm에서 Gcode 경로 충돌이 발견되었습니다. 충돌하는 객체를 더 멀리 분리하세요 (%s <-> %s)." @@ -6178,6 +6218,10 @@ msgstr "멀티 디바이스" msgid "Project" msgstr "프로젝트" +# AI Translated +msgid "Device (Web)" +msgstr "장치 (웹)" + msgid "Yes" msgstr "예" @@ -6312,10 +6356,10 @@ msgstr "3MF/STL/STEP/SVG/OBJ/AMF 가져오기" msgid "Load a model" msgstr "모델 불러오기" -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "ZIP 압축파일 가져오기" -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "ZIP 압축파일에 포함된 모델 로드" msgid "Import Configs" @@ -7835,6 +7879,10 @@ msgstr "사용자 정의 플레이트" msgid "The %s nozzle can not print %s." msgstr "%s 노즐은 %s 을 인쇄할 수 없습니다." +# AI Translated +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "단일 압출기 프린터에서 혼색 필라멘트를 출력하면 필라멘트 교체와 플러시가 자주 발생하여 낭비되는 재료와 노즐 / 폐기물 슈트 막힘 위험이 크게 늘어날 수 있습니다." + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "%1%와 %2% 혼합 출력을 권장하지 않습니다.\n" @@ -7961,12 +8009,44 @@ msgstr "AMS에서 필라멘트 목록 동기화" msgid "Set filaments to use" msgstr "사용할 필라멘트 설정" +# AI Translated +msgid "Add Mixed Filament" +msgstr "혼합 필라멘트 추가" + +# AI Translated +msgid "Mixed Filament" +msgstr "혼합 필라멘트" + +# AI Translated +msgid "Remove last mixed filament" +msgstr "마지막 혼합 필라멘트 제거" + +# AI Translated +msgid "Add mixed filament" +msgstr "혼합 필라멘트 추가" + +# AI Translated +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "혼합 필라멘트의 구성 요소가 잘못되었거나 일치하지 않습니다. 해당 항목을 다시 편집하세요." + msgid "Search plate, object and part." msgstr "플레이트, 객체 및 부품을 검색합니다." msgid "Pellets" msgstr "펠릿" +# AI Translated +msgid "Mixed filament has broken component references" +msgstr "혼합 필라멘트의 구성 요소 참조가 손상되었습니다" + +# AI Translated +msgid "Edit / Delete / Merge" +msgstr "편집 / 삭제 / 병합" + +# AI Translated +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "대상 혼합 필라멘트가 이 물리 필라멘트를 구성 요소로 사용합니다. 병합하면 이 물리 필라멘트가 제거되어 혼합 필라멘트가 무효가 될 수 있습니다. 계속할까요?" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "작업을 완료하면 %s 프로젝트가 닫히고 새 프로젝트가 만들어집니다." @@ -8116,8 +8196,8 @@ msgid "Customized Preset" msgstr "사용자 정의 프리셋" # AI Translated -msgid "Component name(s) inside step file not in UTF8 format!" -msgstr "STEP 파일 내부의 구성 요소 이름이 UTF8 형식이 아닙니다!" +msgid "Component name(s) inside step file not in UTF-8 format!" +msgstr "STEP 파일 내부의 구성 요소 이름이 UTF-8 형식이 아닙니다!" # AI Translated msgid "Because of unsupported text encoding, garbage characters may appear!" @@ -8139,7 +8219,7 @@ msgstr "물체의 부피는 0입니다" #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "%s 파일의 객체가 너무 작습니다. 단위가 미터나 인치일 수 있습니다.\n" " mm 단위로 확장하시겠습니까?" @@ -8159,6 +8239,14 @@ msgstr "" msgid "Multi-part object detected" msgstr "여러 부품으로 구성된 객체 감지됨" +# AI Translated +msgid "Matching textures to filaments" +msgstr "텍스처를 필라멘트에 매칭하는 중" + +# AI Translated +msgid "Texture Import Warning" +msgstr "텍스처 가져오기 경고" + msgid "Load these files as a single object with multiple parts?\n" msgstr "이 파일을 여러 부품이 있는 단일 객체로 로드하시겠습니까?\n" @@ -8288,24 +8376,24 @@ msgid "Replaced with 3D files from directory:\n" msgstr "다음 디렉터리의 3D 파일로 교체했습니다:\n" # AI Translated -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ 건너뜀 %1%: 동일한 파일입니다.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ 건너뜀 %s: 동일한 파일입니다.\n" # AI Translated -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ 건너뜀 %1%: 파일이 존재하지 않습니다.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ 건너뜀 %s: 파일이 존재하지 않습니다.\n" # AI Translated -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ 건너뜀 %1%: 교체하지 못했습니다.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ 건너뜀 %s: 교체하지 못했습니다.\n" # AI Translated -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔ %1%을(를) 교체했습니다.\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔ %s을(를) 교체했습니다.\n" # AI Translated msgid "Replaced volumes" @@ -8387,6 +8475,22 @@ msgstr "" msgid "Sync now" msgstr "지금 동기화" +# AI Translated +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "텍스처 가져오기에 실패했습니다. 모델에 텍스처 데이터가 포함된 것으로 보이지만 텍스처 가져오기 과정을 완료하지 못했습니다. 모델은 형상만 가져옵니다." + +# AI Translated +msgid "Applying texture colors..." +msgstr "텍스처 색상 적용 중..." + +# AI Translated +msgid "Updating 3D view..." +msgstr "3D 뷰 업데이트 중..." + +# AI Translated +msgid "Texture colors applied." +msgstr "텍스처 색상이 적용되었습니다." + msgid "You can keep the modified presets for the new project or discard them" msgstr "수정된 사전 설정을 새 프로젝트에 유지하거나 삭제할 수 있습니다" @@ -9077,6 +9181,18 @@ msgstr "활성화하면 여러 장치에 동시에 작업을 보내고 여러 msgid "Pop up to select filament grouping mode" msgstr "필라멘트 그룹화 모드를 선택하기 위한 팝업" +# AI Translated +msgid "Visible plugin pages" +msgstr "표시할 플러그인 페이지" + +# AI Translated +msgid "pages" +msgstr "페이지" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "고정 탭으로 표시되는 플러그인 페이지 수입니다. 나머지 페이지는 마지막 탭의 드롭다운으로 묶입니다." + # AI Translated msgid "Behaviour" msgstr "동작" @@ -9491,6 +9607,18 @@ msgstr "지원되지 않는 사전 설정 표시" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "프린터 및 필라멘트 드롭다운 목록에 호환되지 않거나 지원되지 않는 사전 설정을 표시합니다. 이러한 사전 설정은 선택할 수 없습니다." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(실험적) 출력 호스트 대신 프린터 에이전트 사용" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Bambu 이외의 프린터 출력 작업을 기존 출력 호스트 업로드 방식 대신 프린터 플러그인 에이전트를 통해 전달합니다.\n" +"비활성화하면 OrcaSlicer는 기존 출력 호스트 동작을 사용합니다." + # AI Translated msgid "Experimental Features" msgstr "실험적 기능" @@ -9706,6 +9834,10 @@ msgstr "나선형 꽃병 모드" msgid "First layer filament sequence" msgstr "첫 번째 레이어 필라멘트 순서" +# AI Translated +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "필라멘트 목록에 혼합 필라멘트가 있습니다. 사용자 지정 필라멘트 순서는 적용되지 않습니다." + msgid "By Layer" msgstr "레이어별" @@ -9762,10 +9894,26 @@ msgstr "사용자 사전 설정" msgid "Preset Inside Project" msgstr "프로젝트 내부 사전 설정" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "상위 사전 설정에서 상속한 모든 값을 이 사전 설정으로 복사하고 상속 관계를 제거합니다. 상위 사전 설정에서만 호환되는 사전 설정은 지원되지 않을 수 있습니다." + # AI Translated msgid "Detach from parent" msgstr "상위 항목에서 분리" +# AI Translated +msgid "Unique preset" +msgstr "독립 사전 설정" + +# AI Translated +msgid "Parent preset" +msgstr "상위 사전 설정" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "이 사전 설정은 다른 사전 설정을 상속하지 않습니다." + msgid "Name is unavailable." msgstr "이름을 사용할 수 없습니다." @@ -10519,22 +10667,6 @@ msgstr "이 옵션을 사용하시겠습니까?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "채우기 패턴은 일반적으로 올바른 출력과 의도한 효과를 위해 회전을 자동으로 처리하도록 설계되어 있습니다(예: 자이로이드, 큐빅). 현재 드문 채우기 패턴을 회전시키면 지지력이 부족해질 수 있습니다. 신중하게 진행하고 출력 문제가 발생하지 않는지 충분히 확인하십시오. 이 옵션을 활성화하시겠습니까?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"레이어 높이가 너무 작습니다.\n" -"min_layer_height로 설정됩니다.\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "레이어 높이가 프린터 설정 -> 압출기 -> 레이어의 제한을 초과합니다.높이 제한으로 인해 출력 품질 문제가 발생할 수 있습니다." - -msgid "Adjust to the set range automatically?\n" -msgstr "설정 범위에 자동으로 맞춰지나요?\n" - -msgid "Adjust" -msgstr "조정" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "실험적 기능: 플러시를 최소화하기 위해 필라멘트 교체 중에 더 먼 거리에서 필라멘트를 집어넣고 절단합니다. 플러시를 눈에 띄게 줄일 수 있지만 노즐 막힘이나 기타 출력 문제의 위험이 높아질 수도 있습니다." @@ -10728,6 +10860,9 @@ msgstr "예약어를 찾았습니다" msgid "Setting Overrides" msgstr "설정 덮어쓰기" +msgid "Retraction when switching material" +msgstr "재료 전환 시 후퇴" + msgid "Basic information" msgstr "기본 정보" @@ -10861,6 +10996,14 @@ msgstr "호환 프로세스 사전설정" msgid "Printable space" msgstr "출력 가능 공간" +# AI Translated +msgid "Printer Agent" +msgstr "프린터 에이전트" + +# AI Translated +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "프린터 통신에 사용할 네트워크 에이전트 구현을 선택합니다. 사용 가능한 에이전트는 시작 시 등록됩니다." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10993,9 +11136,6 @@ msgstr "레이어 높이 한도" msgid "Z-Hop" msgstr "Z올리기" -msgid "Retraction when switching material" -msgstr "재료 전환 시 후퇴" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -11120,12 +11260,12 @@ msgid "No modifications need to be copied." msgstr "복사할 변경 사항이 없습니다." # AI Translated -msgid "Copy paramters" +msgid "Copy parameters" msgstr "매개변수 복사" # AI Translated #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "%s의 매개변수 수정" # AI Translated @@ -11655,30 +11795,6 @@ msgstr "필라멘트 교체를 위한 버리기 볼륨" msgid "Please choose the filament colour" msgstr "필라멘트 색상을 선택하세요" -# AI Translated -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "네이티브 Wayland 실시간 보기에는 GStreamer GTK 비디오 싱크가 필요합니다. GStreamer용 gtksink 플러그인을 설치한 후 OrcaSlicer를 다시 시작하십시오." - -# AI Translated -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "네이티브 Wayland GStreamer 비디오 싱크를 초기화하지 못했습니다. GStreamer GTK 플러그인 설치 상태를 확인하십시오." - -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "이 작업에는 Windows Media Player가 필요합니다! 운영 체제에서 Windows Media Player를 활성화하시겠습니까?" - -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "뱀부소스가 미디어 재생에 올바르게 등록되지 않았습니다! 다시 등록하려면 예를 누르세요. 두 번 승격됩니다" - -# AI Translated -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "미디어 재생용으로 등록된 BambuSource 구성 요소가 없습니다! OrcaSlicer를 다시 설치하거나 커뮤니티에 도움을 요청하십시오." - -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "다른 설치 버전의 뱀부소스를 사용하면 동영상 재생이 제대로 작동하지 않을 수 있습니다! 예를 눌러 문제를 해결하세요." - -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "시스템에 동영상 재생을 위해 필요한 GStreamer용 H.264 코덱이 존재하지 않습니다. (gstreamer1.0-plugins-bad 또는 gstreamer1.0-libav 패키지를 설치한 다음 Orca Slicer를 다시 실행하십시오.)" - # AI Translated msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "클라우드 에이전트를 사용할 수 없습니다. OrcaSlicer를 다시 시작한 후 다시 시도하십시오." @@ -12388,6 +12504,10 @@ msgstr " 이(가) 제외 영역에 너무 가깝습니다. 출력 시 충돌이 msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " 뭉침 감지 영역에 너무 가까워 충돌이 발생할 수 있습니다.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " 이(가) 출력 가능 영역을 일부 벗어나 출력할 수 없습니다.\n" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "선택한 노즐 온도가 서로 호환되지 않습니다. 각 필라멘트의 노즐 온도는 다른 필라멘트의 권장 노즐 온도 범위 안에 있어야 합니다. 그렇지 않으면 노즐 막힘이나 프린터 손상이 발생할 수 있습니다." @@ -12403,6 +12523,10 @@ msgstr "그래도 출력하려면 기본 설정 / 제어 / 슬라이싱 / 혼합 msgid "No extrusions under current settings." msgstr "현재 설정에 압출기가 없습니다." +# AI Translated +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "그라데이션 혼합 필라멘트가 사용 중이지만 '혼색 서브레이어'가 비활성화되어 있습니다. 그라데이션은 출력되지 않습니다." + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "타임랩스의 유연 모드는 \"객체별\" 출력순서가 활성화된 경우 지원되지 않습니다." @@ -12441,6 +12565,10 @@ msgstr "모델 크기를 줄이거나 현재 출력 설정을 변경하고 다 msgid "Variable layer height is not supported with Organic supports." msgstr "유기체 서포트에서는 가변 레이어 높이가 지원되지 않습니다." +# AI Translated +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "프라임 타워 필라멘트는 혼합 필라멘트일 수 없습니다." + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "프라임 타워를 활성화하면 노즐 직경과 필라멘트 직경이 다르면 제대로 작동하지 않을 수 있습니다. 매우 실험적인 기능이므로 주의해서 사용하시기 바랍니다." @@ -12732,10 +12860,6 @@ msgstr "G-code 대신 3MF 사용" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "프린터가 출력 작업으로 3MF 파일을 허용하는 경우 이 옵션을 활성화하십시오. 활성화하면 Orca Slicer가 슬라이스된 파일을 일반 .gcode 파일 대신 .gcode.3mf로 전송합니다." -# AI Translated -msgid "Printer Agent" -msgstr "프린터 에이전트" - # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "프린터 통신에 사용할 네트워크 에이전트 구현을 선택합니다." @@ -12821,8 +12945,7 @@ msgstr "mm 또는 %" msgid "Other layers" msgstr "다른 레이어" -# AI Translated -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." msgstr "초기 레이어를 제외한 레이어의 베드 온도입니다. 값이 0이면 해당 필라멘트가 쿨 플레이트 슈퍼택에서의 출력을 지원하지 않음을 의미합니다." msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." @@ -13446,9 +13569,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "내부 브릿지의 속도. 값을 백분율로 표현하면 bridge_speed를 기준으로 계산됩니다. 기본값은 150%입니다." -msgid "Brim width" -msgstr "브림 너비" - msgid "This is the distance from the model to the outermost brim line." msgstr "모델과 가장 바깥쪽 브림 선까지의 거리" @@ -13533,6 +13653,14 @@ msgstr "" "날카로운 각도를 감지하기 전에 형상이 무시됩니다. 이 매개변수는 무시하는 형상의 최소 길이를 나타냅니다.\n" "0으로 비활성화합니다" +# AI Translated +msgid "Brim ears outer only" +msgstr "브림 귀를 바깥쪽에만" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "구멍과 닫힌 영역을 제외하고 모델의 바깥쪽 윤곽에만 생쥐 귀를 생성합니다." + msgid "upward compatible machine" msgstr "상향 호환 장치" @@ -14234,6 +14362,8 @@ msgstr "레이어 시간" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "예상 시간이 이 값보다 짧은 레이어에 대해 출력물 냉각 팬이 활성화됩니다. 팬 속도는 레이어 출력 시간에 따라 최소 및 최대 팬 속도 사이에서 보간됩니다" +# AI Translated +msgctxt "second" msgid "s" msgstr "s" @@ -14547,6 +14677,62 @@ msgstr "서포트 재료" msgid "Support material is commonly used to print supports and support interfaces." msgstr "서포트 재료는 일반적으로 서포트 및 서포트 접점을 출력하는 데 사용됩니다" +# AI Translated +msgid "Is mixed filament" +msgstr "혼합 필라멘트 여부" + +# AI Translated +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "이 필라멘트 슬롯이 여러 물리 필라멘트로 구성된 혼합 필라멘트인지 여부" + +# AI Translated +msgid "Mixed filament components" +msgstr "혼합 필라멘트 구성 요소" + +# AI Translated +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "구성 필라멘트의 1부터 시작하는 인덱스를 쉼표로 구분하여 입력합니다. 예: \"1,3\"" + +# AI Translated +msgid "Mixed filament sublayer ratios" +msgstr "혼합 필라멘트 서브레이어 비율" + +# AI Translated +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "합이 1.0이 되는 비율 값을 쉼표로 구분하여 입력합니다. 예: \"0.7,0.3\"" + +# AI Translated +msgid "Mixed filament gradient" +msgstr "혼합 필라멘트 그라데이션" + +# AI Translated +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "혼합 필라멘트 서브레이어에 대해 Z 방향 그라데이션 모드를 활성화합니다. 활성화하면 서브레이어 비율이 레이어에 따라 선형으로 변합니다." + +# AI Translated +msgid "Mixed filament gradient range" +msgstr "혼합 필라멘트 그라데이션 범위" + +# AI Translated +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "그라데이션 모드에서 첫 번째 구성 요소의 시작 및 종료 비율입니다. 쉼표로 구분된 한 쌍이며, 예를 들어 \"0.10,0.90\"은 10%에서 90%를 의미합니다." + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "혼합 필라멘트 그라데이션 곡선" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "Z 진행도를 첫 번째 구성 요소의 비율에 매핑하는 선택적 Photoshop 스타일 사용자 지정 곡선입니다. 제어점을 세로줄로 구분하여 \"x,y\"(레거시) 또는 접선을 재정의해야 할 때는 \"x,y,m_in,m_out\" 형식으로 인코딩합니다(빈 값 또는 \"nan\"은 PCHIP 기본값을 사용). x는 [0,1] 범위이며, y는 설정된 비율 범위로 제한됩니다. 예: \"0,0.15|0.5,0.50|1,0.85\". 비워 두면 대신 선형 gradient_range가 사용됩니다." + +# AI Translated +msgid "Mixed filament per-part gradient" +msgstr "혼합 필라멘트 부품별 그라데이션" + +# AI Translated +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "그라데이션 모드가 활성화되면 조립 전체를 하나의 Z 범위로 처리하지 않고 조립의 각 부품에 그라데이션을 개별적으로 적용합니다." + msgid "Filament printable" msgstr "필라멘트 출력 가능" @@ -14729,6 +14915,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "자이로이드" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "드문 채우기 부드러움 계수" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "드문 채우기의 모서리를 얼마나 둥글게 할지 조절합니다. 0%는 원래의 날카로운 경로를 유지하고, 100%는 인접한 채우기 선 사이에 가능한 가장 큰 곡선을 만듭니다." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "상단 표면 가속도. 낮은 값을 사용하면 상단 표면 품질이 향상될 수 있습니다" @@ -15291,6 +15485,14 @@ msgstr "프린터와 호환되는 Gcode 종류" msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "G-code 설정 블록 생략" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "CONFIG_BLOCK(슬라이서 설정의 키/값 쌍)을 G-code 파일에 기록하지 않습니다. 이 주석 줄을 해석할 때 펌웨어가 중단되는 프린터(예: Anycubic go-klipper)에 도움이 될 수 있습니다. 참고: G-code 파일에 슬라이서 설정이 더 이상 포함되지 않으므로, 이 파일을 OrcaSlicer로 다시 가져와도 설정이 복원되지 않습니다." + msgid "Pellet Modded Printer" msgstr "펠릿 프린터" @@ -15849,6 +16051,7 @@ msgid "The allowed maximum output force of Y axis" msgstr "Y축의 허용 최대 출력 힘" # AI Translated +msgctxt "Newton" msgid "N" msgstr "N" @@ -15859,6 +16062,7 @@ msgid "The machine bed mass load of Y axis" msgstr "Y축 장비 베드 질량 하중" # AI Translated +msgctxt "gram" msgid "g" msgstr "g" @@ -16187,7 +16391,7 @@ msgid "Reduce infill retraction" msgstr "채우기 후퇴 감소" # AI Translated -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." msgstr "이동 경로가 완전히 채우기 영역 안에 있으면 후퇴하지 않습니다. 흘러내림이 보이지 않기 때문입니다. 복잡한 모델에서 후퇴 횟수를 줄여 출력 시간을 절약할 수 있지만 슬라이싱과 G-code 생성 속도는 느려집니다. 후퇴를 건너뛴 영역에서는 Z 올리기도 수행되지 않습니다." msgid "This option will drop the temperature of the inactive extruders to prevent oozing." @@ -16364,7 +16568,7 @@ msgstr "노즐 청소 후 후퇴량" # AI Translated #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" "노즐 청소 후 빠른 후퇴의 길이로, 후퇴 길이를 기준으로 합니다.\n" @@ -16400,10 +16604,18 @@ msgstr "압출기 교체 시 긴 수축" msgid "Retraction distance when extruder change" msgstr "압출기 교체 시 수축 거리" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "후퇴 길이 (툴 체인지)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "툴 체인지 전에 후퇴가 실행되면 지정한 양만큼 필라멘트가 뒤로 당겨집니다 (길이는 압출기에 들어가기 전의 원래 필라멘트를 기준으로 측정됩니다)." + msgid "Z-hop height" msgstr "Z올리기 높이" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "후퇴가 완료될 때마다 노즐이 약간 올라가서 노즐과 출력물 사이에 간격이 생깁니다. 이동 시 노즐이 출력물에 닿는 것을 방지합니다. 나선형 선을 사용하여 z를 들어 올리면 스트링 현상을 방지할 수 있습니다" msgid "Z-hop lower boundary" @@ -16498,6 +16710,10 @@ msgstr "재 시작 시 추가 길이" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "이동 후 후퇴가 보상되면 압출기는 이 추가 양의 필라멘트를 밀어냅니다. 이 설정은 거의 필요하지 않습니다." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "재 시작 시 추가 길이 (툴 체인지)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "툴 체인지 후 후퇴가 보상되면 압출기는 이 추가 양의 필라멘트를 밀어냅니다." @@ -16922,6 +17138,14 @@ msgstr "프라임 타워에서 툴 체인지" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "툴 체인지 명령(Tx)을 실행하기 전에 툴헤드가 반드시 프라임 타워로 이동하도록 합니다. 유형 2 프라임 타워를 사용하는 다중 압출기(멀티 툴헤드) 프린터에만 해당됩니다. 기본적으로 Orca는 멀티 툴헤드 장비에서 펌웨어가 헤드 교체를 처리하므로 이동을 생략하는데, 이 때문에 Tx 명령이 출력물 위에서 실행될 수 있습니다. 툴 체인지가 항상 프라임 타워 위에서 실행되도록 하려면 이 옵션을 활성화하십시오." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "프라임 타워에서 온도 대기" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "출력 온도에 도달할 때까지 기다리지 않고 새 툴을 집은 뒤 프라임 타워로 이동하여, 퍼지 직전에 그곳에서 온도를 기다립니다. 가열 중 흘러나온 재료는 모델이 아닌 타워에 떨어지고, 이동 시간이 가열 시간과 겹칩니다. 타입 2 프라임 타워를 사용하는 다중 압출기(다중 툴헤드) 프린터에만 해당합니다. 펌웨어나 툴 체인지 매크로가 직접 온도를 기다려서는 안 됩니다. 비활성화하면 툴 체인지 명령 직후에 온도 대기가 실행됩니다." + msgid "No sparse layers (beta)" msgstr "희소 레이어 없음(베타)" @@ -17470,6 +17694,14 @@ msgstr "" "\n" "아래의 와이프 전 후퇴량 설정에서 값을 설정하면 와이프 전에 초과 후퇴가 수행되고, 그렇지 않으면 와이프 후에 수행됩니다." +# AI Translated +msgid "Mixed color sublayer" +msgstr "혼색 서브레이어" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "혼색 서브레이어 분할을 활성화합니다. 활성화하면 혼색 필라멘트가 포함된 레이어가 서브레이어로 분할되어 색상 혼합 효과를 얻습니다." + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "프라임 타워는 객체를 출력할 때 외관 결함을 방지하기 위해 노즐의 잔류물을 청소하고 노즐 내부의 압력을 안정화하는 데 사용할 수 있습니다." @@ -18573,6 +18805,10 @@ msgstr "모델 파일의 메싱이 실패했거나 유효한 형태가 없습니 msgid "The supplied file couldn't be read because it's empty." msgstr "제공된 파일이 비어 있어 읽을 수 없습니다" +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "파일 형식이 호환되지 않아 해석할 수 없습니다." + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "알 수 없는 파일 형식: 입력 파일의 확장자는 .stl, .obj 또는 .amf(.xml)여야 합니다." @@ -20104,19 +20340,19 @@ msgstr "프린터, 필라멘트 및 프로세스 사전 설정이 변경된 경 msgid "Only display the filament names with changes to filament presets." msgstr "필라멘트 사전 설정이 변경된 경우에만 필라멘트 이름을 표시합니다." -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "사용자 프린터 사전 설정이 있는 프린터 이름만 표시되며, 선택한 각 사전 설정은 zip으로 내보내집니다." msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" "사용자 필라멘트 사전 설정이 있는 필라멘트 이름만 표시됩니다.\n" "선택한 각 필라멘트 이름의 모든 사용자 필라멘트 사전 설정은 zip으로 내보내집니다." msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" "프로세스 사전 설정이 변경된 프린터 이름만 표시됩니다.\n" "선택한 각 프린터 이름의 모든 사용자 프로세스 사전 설정은 zip으로 내보내집니다." @@ -20261,10 +20497,6 @@ msgstr "물리 프린터" msgid "Print Host upload" msgstr "출력 호스트 업로드" -# AI Translated -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "프린터 통신에 사용할 네트워크 에이전트 구현을 선택합니다. 사용 가능한 에이전트는 시작 시 등록됩니다." - # AI Translated msgid "Select a Flashforge printer" msgstr "Flashforge 프린터 선택" @@ -20364,7 +20596,7 @@ msgid "We need information for diagnosing source of the issue. Check wiki page f msgstr "문제의 원인을 진단하려면 정보가 필요합니다. 자세한 안내는 위키 페이지를 확인하십시오." # AI Translated -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "패키지 버튼은 현재 세션의 프로젝트 파일과 로그를 zip 파일로 모읍니다." # AI Translated @@ -20420,7 +20652,7 @@ msgid "Stored logs" msgstr "저장된 로그" # AI Translated -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "저장된 모든 로그를 zip 파일로 묶습니다." # AI Translated @@ -20508,7 +20740,7 @@ msgid "Authorizing..." msgstr "인증 중..." # AI Translated -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "오류. 인증용 API 토큰을 가져올 수 없습니다" # AI Translated @@ -20970,8 +21202,8 @@ msgid "Enable smart filament assign: Assign one filament to multiple nozzles to msgstr "스마트 필라멘트 할당 활성화: 하나의 필라멘트를 여러 노즐에 할당하여 절약을 극대화합니다" # AI Translated -msgid "Fila Saving" -msgstr "필라멘트 절약" +msgid "File Saving" +msgstr "파일 저장" msgid "Don't remind me again" msgstr "다시 알리지 마세요" @@ -21217,9 +21449,6 @@ msgstr "로그인을 시도하는 동안 예기치 않은 문제가 발생했습 msgid "User canceled." msgstr "사용자가 취소했습니다." -msgid "Head diameter" -msgstr "헤드 직경" - msgid "Max angle" msgstr "최대 각도" @@ -21404,6 +21633,9 @@ msgstr "지금 다시 시작" msgid "NO RAMMING AT ALL" msgstr "채워넣기 전혀 없음" +msgid "s" +msgstr "s" + # AI Translated msgid "Volumetric speed" msgstr "압출 속도" @@ -22057,6 +22289,58 @@ msgstr "" "뒤틀림 방지\n" "ABS와 같이 뒤틀림이 발생하기 쉬운 소재를 출력할 때, 히트베드 온도를 적절하게 높이면 뒤틀림 가능성을 줄일 수 있다는 사실을 알고 계셨나요?" +# AI Translated +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "네이티브 Wayland 실시간 보기에는 GStreamer GTK 비디오 싱크가 필요합니다. GStreamer용 gtksink 플러그인을 설치한 후 OrcaSlicer를 다시 시작하십시오." + +# AI Translated +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "네이티브 Wayland GStreamer 비디오 싱크를 초기화하지 못했습니다. GStreamer GTK 플러그인 설치 상태를 확인하십시오." + +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "이 작업에는 Windows Media Player가 필요합니다! 운영 체제에서 Windows Media Player를 활성화하시겠습니까?" + +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "뱀부소스가 미디어 재생에 올바르게 등록되지 않았습니다! 다시 등록하려면 예를 누르세요. 두 번 승격됩니다" + +# AI Translated +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "미디어 재생용으로 등록된 BambuSource 구성 요소가 없습니다! OrcaSlicer를 다시 설치하거나 커뮤니티에 도움을 요청하십시오." + +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "다른 설치 버전의 뱀부소스를 사용하면 동영상 재생이 제대로 작동하지 않을 수 있습니다! 예를 눌러 문제를 해결하세요." + +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "시스템에 동영상 재생을 위해 필요한 GStreamer용 H.264 코덱이 존재하지 않습니다. (gstreamer1.0-plugins-bad 또는 gstreamer1.0-libav 패키지를 설치한 다음 Orca Slicer를 다시 실행하십시오.)" + +# AI Translated +#~ msgid "N" +#~ msgstr "N" + +# AI Translated +#~ msgid "g" +#~ msgstr "g" + +# AI Translated +#~ msgid "Fila Saving" +#~ msgstr "필라멘트 절약" + +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "레이어 높이가 너무 작습니다.\n" +#~ "min_layer_height로 설정됩니다.\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "레이어 높이가 프린터 설정 -> 압출기 -> 레이어의 제한을 초과합니다.높이 제한으로 인해 출력 품질 문제가 발생할 수 있습니다." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "설정 범위에 자동으로 맞춰지나요?\n" + +#~ msgid "Head diameter" +#~ msgstr "헤드 직경" + #~ msgid "Print order within a single layer." #~ msgstr "단일 레이어 내의 출력 순서" diff --git a/localization/i18n/list.txt b/localization/i18n/list.txt index 1614bc453b..5174679abf 100644 --- a/localization/i18n/list.txt +++ b/localization/i18n/list.txt @@ -193,7 +193,6 @@ src/slic3r/GUI/ObjColorDialog.cpp src/slic3r/GUI/SyncAmsInfoDialog.cpp src/slic3r/GUI/WipeTowerDialog.cpp src/slic3r/GUI/wxExtensions.cpp -src/slic3r/GUI/wxMediaCtrl2.cpp src/slic3r/GUI/WebUserLoginDialog.cpp src/slic3r/GUI/WebGuideDialog.cpp src/slic3r/GUI/KBShortcutsDialog.hpp diff --git a/localization/i18n/lt/OrcaSlicer_lt.po b/localization/i18n/lt/OrcaSlicer_lt.po index 23d2aae6f1..b3458ec0e4 100644 --- a/localization/i18n/lt/OrcaSlicer_lt.po +++ b/localization/i18n/lt/OrcaSlicer_lt.po @@ -3,12 +3,11 @@ # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # -#, fuzzy msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "PO-Revision-Date: 2026-07-02 14:13+0300\n" "Last-Translator: Gintaras Kučinskas <sharanchius@gmail.com>\n" "Language-Team: \n" @@ -2937,6 +2936,10 @@ msgstr "Redaguoti" msgid "Merge with" msgstr "Sujungti su" +# AI Translated +msgid "Decompose Color" +msgstr "Išskaidyti spalvą" + msgid "Delete this filament" msgstr "Ištrinti šią giją" @@ -3238,6 +3241,10 @@ msgstr "Surinkimas" msgid "Merge parts to an object" msgstr "Sujungti dalis į objektą" +# AI Translated +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "Kintamo sluoksnio aukščio naudojimas kartu su mišrios spalvos posluoksniu gali pabloginti spalvų maišymo kokybę." + # AI Translated msgid "Add layers" msgstr "Pridėti sluoksnių" @@ -4728,6 +4735,23 @@ msgstr "Dabartinė kameros temperatūra yra aukštesnė už saugią medžiagos t msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Minimali kameros temperatūra (%d℃) yra aukštesnė nei tikslinė kameros temperatūra (%d℃). Minimali vertė yra slenkstis, kurį pasiekus pradedamas spausdinimas, kol kamera vis dar kaitinama iki tikslinės temperatūros, todėl ji neturėtų viršyti tikslinės. Vertė bus apribota iki tikslinės temperatūros." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "Sluoksnio aukštis per mažas. Jis bus nustatytas į mažiausią reikšmę (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "Sluoksnio aukštis yra už ribų, nurodytų Spausdintuvo nustatymai -> Ekstruderis -> Sluoksnio aukščio ribos, tai gali sukelti spausdinimo kokybės problemų." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Automatiškai sureguliuoti iki ribos (%g mm)?" + +msgid "Adjust" +msgstr "Sureguliuoti" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4775,7 +4799,7 @@ msgstr "" "\n" "Reikšmė bus atstatyta į 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Alternatyvi papildoma sienelė veikia prastai, kai vertikalaus apvalkalo storio užtikrinimas nustatytas į „Visi“." msgid "" @@ -4821,7 +4845,7 @@ msgstr "" "NE – palikti nepriklausomą atramų sluoksnio aukštį" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "„seam_slope_start_height“ turi būti mažesnis už „layer_height“\n" @@ -4829,7 +4853,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "Fiksavimo gylis turėtų būti mažesnis už išorinio sluoksnio gylį.\n" @@ -4847,6 +4871,13 @@ msgstr "" "Taip – įjungti „Arachne“ sienelių generatorių\n" "Ne – išjungti „Arachne“ sienelių generatorių ir nustatyti „Šiurkštaus paviršius“ režimą [Slinktis]" +# AI Translated +msgid "Brim ear radius" +msgstr "Apvado „ausies“ spindulys" + +msgid "Brim width" +msgstr "Pado apvado plotis" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Spiralinis režimas veikia tik tada, kai sienelės kilpų skaičius yra 1, atramos išjungtos, sulipimo aptikimas zonduojant išjungtas, viršutinių apvalkalo sluoksnių yra 0, reto užpildo tankis yra 0 %, o laiko intervalų vaizdo įrašo tipas – tradicinis." @@ -5101,6 +5132,14 @@ msgstr "Nepavyko sugeneruoti kalibravimo G-kodo" msgid "Calibration error" msgstr "Kalibravimo klaida" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Šiame spausdintuve nėra sukonfigūruotos įrangos, kurios reikia šiam valdikliui." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Šis valdiklis šiame spausdintuve nepalaikomas." + # AI Translated msgid "Network unavailable" msgstr "Tinklas neprieinamas" @@ -5961,7 +6000,7 @@ msgstr "Tūris:" msgid "Size:" msgstr "Dydis:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Rasta G-kodo trajektorijų konfliktų %d sluoksnyje, Z = %.2lfmm. Prašome labiau atskirti konfliktuojančius objektus (%s <-> %s)." @@ -6142,6 +6181,10 @@ msgstr "Kelių įrenginių valdymas (Multi-device)" msgid "Project" msgstr "Projektas" +# AI Translated +msgid "Device (Web)" +msgstr "Įrenginys (Web)" + msgid "Yes" msgstr "Taip" @@ -6275,10 +6318,10 @@ msgstr "Importuoti 3MF/STL/STEP/SVG/OBJ/AMF" msgid "Load a model" msgstr "Įkelti modelį" -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "Importuoti Zip archyvą" -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "Įkelti modelius iš ZIP archyvo" msgid "Import Configs" @@ -7799,6 +7842,10 @@ msgstr "Individualizuoti esamą plokštę" msgid "The %s nozzle can not print %s." msgstr "%s purkštukas negali spausdinti %s." +# AI Translated +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "Spausdinant mišrios spalvos giją vieno ekstruderio spausdintuvu reikia dažnai keisti giją ir pravalyti, todėl gali gerokai padidėti atliekų kiekis ir purkštuko arba atliekų latako užsikimšimo rizika." + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "" @@ -7932,12 +7979,44 @@ msgstr "Sinchronizuoti gijų sąrašą iš AMS" msgid "Set filaments to use" msgstr "Nustatyti gijas naudojimui" +# AI Translated +msgid "Add Mixed Filament" +msgstr "Pridėti mišrią giją" + +# AI Translated +msgid "Mixed Filament" +msgstr "Mišri gija" + +# AI Translated +msgid "Remove last mixed filament" +msgstr "Pašalinti paskutinę mišrią giją" + +# AI Translated +msgid "Add mixed filament" +msgstr "Pridėti mišrią giją" + +# AI Translated +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "Mišri gija turi netinkamų arba neatitinkančių komponentų. Iš naujo suredaguokite susijusius įrašus." + msgid "Search plate, object and part." msgstr "Plokštės, objekto ir dalies paieška." msgid "Pellets" msgstr "Granulės" +# AI Translated +msgid "Mixed filament has broken component references" +msgstr "Mišri gija turi sugadintų komponentų nuorodų" + +# AI Translated +msgid "Edit / Delete / Merge" +msgstr "Redaguoti / Ištrinti / Sujungti" + +# AI Translated +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "Paskirties mišri gija naudoja šią fizinę giją kaip komponentą. Sujungus ši fizinė gija bus pašalinta ir mišri gija gali tapti netinkama. Tęsti?" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "Baigus šią operaciją, projektas „%s“ bus uždarytas ir bus sukurtas naujas projektas." @@ -8074,7 +8153,7 @@ msgstr "Patvirtinkite, kad šiuose profiliuose esantis G-kodas yra saugus, kad i msgid "Customized Preset" msgstr "Pritaikytas profilis" -msgid "Component name(s) inside step file not in UTF8 format!" +msgid "Component name(s) inside step file not in UTF-8 format!" msgstr "Komponentų pavadinimai STEP faile nėra UTF-8 formato!" msgid "Because of unsupported text encoding, garbage characters may appear!" @@ -8096,7 +8175,7 @@ msgstr "Objekto tūris nulinis" #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "Objektas iš failo „%s“ yra per mažas ir galimai nurodytas metrais arba coliais.\n" "Ar norite mastelį pakeisti į milimetrus?" @@ -8116,6 +8195,14 @@ msgstr "" msgid "Multi-part object detected" msgstr "Aptiktas kelių dalių objektas" +# AI Translated +msgid "Matching textures to filaments" +msgstr "Tekstūros priskiriamos gijoms" + +# AI Translated +msgid "Texture Import Warning" +msgstr "Tekstūros importavimo įspėjimas" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Ar įkelti šiuos failus kaip vieną objektą su keliomis detalėmis?\n" @@ -8239,21 +8326,21 @@ msgstr "" "Pakeista 3D failais iš katalogo:\n" "\n" -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ Praleistas %1%: tas pats failas.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ Praleistas %s: tas pats failas.\n" -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ Praleistas %1%: failas neegzistuoja.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ Praleistas %s: failas neegzistuoja.\n" -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ Praleistas %1%: nepavyko pakeisti.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ Praleistas %s: nepavyko pakeisti.\n" -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔ Pakeistas %1%.\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔ Pakeistas %s.\n" msgid "Replaced volumes" msgstr "Pakeisti tūriai" @@ -8330,6 +8417,22 @@ msgstr "" msgid "Sync now" msgstr "Sinchronizuoti dabar" +# AI Translated +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "Tekstūros importuoti nepavyko. Panašu, kad modelyje yra tekstūros duomenų, tačiau importavimo proceso nepavyko užbaigti. Modelis bus importuotas tik kaip geometrija." + +# AI Translated +msgid "Applying texture colors..." +msgstr "Taikomos tekstūros spalvos..." + +# AI Translated +msgid "Updating 3D view..." +msgstr "Atnaujinamas 3D vaizdas..." + +# AI Translated +msgid "Texture colors applied." +msgstr "Tekstūros spalvos pritaikytos." + msgid "You can keep the modified presets for the new project or discard them" msgstr "Pakeistus profilius galite išsaugoti naujam projektui arba juos atmesti" @@ -8977,6 +9080,18 @@ msgstr "Kai įjungta ši funkcija, jūs galite siųsti užduotį keliems įrengi msgid "Pop up to select filament grouping mode" msgstr "Iššokantis langas gijų grupavimo režimui pasirinkti" +# AI Translated +msgid "Visible plugin pages" +msgstr "Matomi papildinių puslapiai" + +# AI Translated +msgid "pages" +msgstr "puslapiai" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Papildinių puslapių, rodomų kaip fiksuotos kortelės, skaičius; likę puslapiai sutraukiami į išskleidžiamąjį sąrašą paskutinėje kortelėje." + msgid "Behaviour" msgstr "Elgsena" @@ -9329,6 +9444,18 @@ msgstr "Rodyti nepalaikomus profilius" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Rodyti nesuderinamus / nepalaikomus profilius spausdintuvų ir gijų išskleidžiamuosiuose sąrašuose. Šių profilių pasirinkti negalima." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Eksperimentinė) Naudoti spausdintuvo agentus vietoj spausdinimo serverių" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Nukreipia ne Bambu spausdintuvų spausdinimo užduotis per spausdintuvo papildinių agentus, o ne per klasikinį įkėlimo į spausdinimo serverį srautą.\n" +"Kai išjungta, OrcaSlicer naudoja senąjį spausdinimo serverio veikimą." + msgid "Experimental Features" msgstr "Eksperimentinis" @@ -9535,6 +9662,10 @@ msgstr "Spiralinė vaza" msgid "First layer filament sequence" msgstr "Pirmojo sluoksnio gijos eiga" +# AI Translated +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "Gijų sąraše yra mišrių gijų. Pasirinktinė gijų seka nebus taikoma." + msgid "By Layer" msgstr "Pagal sluoksnį" @@ -9590,9 +9721,25 @@ msgstr "Naudotojo profilis" msgid "Preset Inside Project" msgstr "Profilis projekto viduje" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Nukopijuoja į šį profilį visas iš pirminio profilio paveldėtas reikšmes ir pašalina paveldėjimo ryšį. Profiliai, suderinami tik su pirminiu profiliu, gali tapti nepalaikomi." + msgid "Detach from parent" msgstr "Atskirti nuo tėvinio profilio" +# AI Translated +msgid "Unique preset" +msgstr "Savarankiškas profilis" + +# AI Translated +msgid "Parent preset" +msgstr "Pirminis profilis" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Šis profilis nepaveldi iš kito profilio." + msgid "Name is unavailable." msgstr "Nėra pavadinimo." @@ -10330,24 +10477,6 @@ msgstr "Ar tikrai norite įjungti šią parinktį?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Užpildymo modeliai paprastai yra suprojektuoti taip, kad automatiškai tvarkytų sukimąsi, siekiant užtikrinti tinkamą spausdinimą ir pasiekti numatytus efektus (pvz., Gyroid, Cubic). Sukant esamą retą užpildymo modelį, gali atsirasti nepakankamas atraminis paviršius. Prašome elgtis atsargiai ir atidžiai patikrinti, ar nėra galimų spausdinimo problemų. Ar tikrai norite įjungti šią parinktį?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Per mažas sluoksnio aukštis.\n" -"Jis bus nustatytas į min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Sluoksnio aukštis viršija ribą, nurodytą Spausdintuvo nustatymai -> Ekstruderis -> Sluoksnio aukščio ribos, tai gali sukelti spausdinimo kokybės problemų." - -msgid "Adjust to the set range automatically?\n" -msgstr "" -"Sureguliuoti pagal nustatytą diapazoną automatiškai?\n" -"\n" - -msgid "Adjust" -msgstr "Sureguliuoti" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Eksperimentinė funkcija: gijos įtraukimas ir nukirpimas didesniu atstumu keičiant giją, siekiant sumažinti išvalymą (flush). Nors tai gali pastebimai sumažinti išvalymą, taip pat gali padidėti purkštuko užsikimšimo ar kitų spausdinimo komplikacijų rizika." @@ -10547,6 +10676,9 @@ msgstr "Rasti rezervuoti raktažodžiai" msgid "Setting Overrides" msgstr "Nustatymų perrašymas" +msgid "Retraction when switching material" +msgstr "Įtraukimas keičiant medžiagą" + msgid "Basic information" msgstr "Pagrindinė informacija" @@ -10673,6 +10805,12 @@ msgstr "Suderinami apdorojimo profiliai" msgid "Printable space" msgstr "Erdvė spausdinimui" +msgid "Printer Agent" +msgstr "Spausdintuvo agentas" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Pasirinkite tinklo agento modulį ryšiui su spausdintuvu palaikyti. Prieinami agentai užregistruojami paleidimo metu." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10798,9 +10936,6 @@ msgstr "Sluoksnio aukščio ribos" msgid "Z-Hop" msgstr "Z šuolis" -msgid "Retraction when switching material" -msgstr "Įtraukimas keičiant medžiagą" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -10917,11 +11052,11 @@ msgstr "%s: %s" msgid "No modifications need to be copied." msgstr "Nereikia kopijuoti jokių modifikacijų" -msgid "Copy paramters" +msgid "Copy parameters" msgstr "Kopijuoti parametrus" #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "Modifikuoti %s parametrus" #, c-format, boost-format @@ -11434,27 +11569,6 @@ msgstr "Išmetimo tūris keičiant gijas" msgid "Please choose the filament colour" msgstr "Pasirinkite gijos spalvą" -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "Tiesioginei „Native Wayland“ peržiūrai reikalingas „GStreamer GTK“ vaizdo sinchronizatorius (video sink). Įdiekite „GStreamer“ skirtą „gtksink“ papildinį, tada iš naujo paleiskite „OrcaSlicer“." - -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "Nepavyko inicijuoti „native Wayland GStreamer“ vaizdo sinchronizatoriaus (video sink). Patikrinkite „GStreamer GTK“ papildinio įdiegimą." - -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "Šiai užduočiai atlikti reikalingas \"Windows Media Player\"! Ar norite įjungti \"Windows Media Player\" savo operacinėje sistemoje?" - -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "„BambuSource“ neteisingai užregistruotas medijos atkūrimui! Paspauskite „Taip“, kad jį perregistruotumėte. Jums reikės patvirtinti du kartus." - -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "Trūksta BambuSource komponento, užregistruoto medijos atkūrimui! Iš naujo įdiekite OrcaSlicer arba kreipkitės pagalbos į bendruomenę." - -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "Naudojant \"BambuSource\" iš kito diegimo šaltinio, vaizdo įrašų atkūrimas gali būti neteisingas! Paspauskite Taip, kad tai ištaisytumėte." - -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "Jūsų sistemoje nėra \"GStreamer\" H.264 kodekų, reikalingų vaizdo įrašams atkurti. (Pabandykite įdiegti gstreamer1.0-plugins-bad arba gstreamer1.0-libav paketus, tada iš naujo paleiskite \"Orca Slicer\")" - msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "Debesies agentas nepasiekiamas. Iš naujo paleiskite „OrcaSlicer“ ir bandykite vėl." @@ -12146,6 +12260,10 @@ msgstr "" " yra per arti sulipimo aptikimo zonos, todėl įvyks susidūrimai.\n" "\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " yra iš dalies už spausdinimo srities ribų ir negali būti atspausdintas.\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Pasirinktos purkštuko temperatūros yra nesuderinamos. Kiekvienos gijos purkštuko temperatūra turi patekti į kitų gijų rekomenduojamos temperatūros diapazoną. Priešingu atveju gali užsikimšti purkštukas arba sugesti spausdintuvas." @@ -12158,6 +12276,10 @@ msgstr "Jei vis tiek norite spausdinti, galite įjungti šią parinktį skiltyje msgid "No extrusions under current settings." msgstr "Pagal dabartinius nustatymus nėra išspaudimų." +# AI Translated +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "Naudojama mišri gija su gradientu, tačiau 'Mišrios spalvos posluoksnis' yra išjungtas. Gradientas nebus atspausdintas." + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Sklandus pakadrinio filmavimo (timelapse) režimas nepalaikomas, kai įjungta spausdinimo seka „pagal objektą“." @@ -12194,6 +12316,10 @@ msgstr "Galbūt norėsite sumažinti modelio dydį arba pakeisti esamus spausdin msgid "Variable layer height is not supported with Organic supports." msgstr "Kintamas sluoksnio aukštis nepalaikomas su \" Organinėmis atramomis\"." +# AI Translated +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "Valymo bokšto gija negali būti mišri gija." + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Skirtingo skersmens purkštukai ir skirtingo skersmens gijos gali neveikti gerai, kai įjungtas valymo bokštas. Tai labai eksperimentinė priemonė, todėl elkitės atsargiai." @@ -12459,9 +12585,6 @@ msgstr "Vietoj G-kodo naudoti 3MF" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Įjunkite, jei spausdintuvas spausdinimo užduotims priima 3MF failus. Kai įjungta, „Orca Slicer“ sugeneruotą failą siunčia kaip „.gcode.3mf“, o ne kaip paprastą „.gcode“ failą." -msgid "Printer Agent" -msgstr "Spausdintuvo agentas" - msgid "Select the network agent implementation for printer communication." msgstr "Pasirinkite tinklo agento modulį ryšiui su spausdintuvu palaikyti." @@ -12544,7 +12667,7 @@ msgstr "mm arba %" msgid "Other layers" msgstr "Kiti sluoksniai" -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." msgstr "Pagrindo temperatūra visiems sluoksniams, išskyrus pirmąjį. Reikšmė 0 reiškia, kad ši gija nepritaikyta spausdinti ant „Cool Plate SuperTack“ pagrindo." msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." @@ -13134,9 +13257,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Vidinių tiltelių spausdinimo greitis. Jei reikšmė nurodoma procentais, ji apskaičiuojama pagal „bridge_speed“ (tiltelių greitį). Numatytoji reikšmė – 150 %." -msgid "Brim width" -msgstr "Pado apvado plotis" - msgid "This is the distance from the model to the outermost brim line." msgstr "Atstumas nuo modelio iki išorinės krašto linijos" @@ -13217,6 +13337,14 @@ msgstr "" "Prieš aptinkant aštrius kampus, geometrija yra supaprastinama (decimuojama). Šis parametras nurodo minimalų nuokrypio ilgį supaprastinimui atlikti.\n" "Įrašykite 0, kad išjungtumėte." +# AI Translated +msgid "Brim ears outer only" +msgstr "Apvado „ausys“ tik išorėje" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Kuria peliukų ausis tik ant išorinio modelio kontūro, praleidžiant skyles ir uždaras sritis." + msgid "upward compatible machine" msgstr "atgaliniu būdu suderinamas įrenginys" @@ -13892,6 +14020,8 @@ msgstr "Sluoksnio laikas" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "Detalės aušinimo ventiliatorius bus suaktyvintas tiems sluoksniams, kurių preliminarus spausdinimo laikas yra trumpesnis už šią reikšmę. Ventiliatoriaus sūkiai bus interpoliuojami tarp minimalaus ir maksimalaus greičio, priklausomai nuo sluoksnio spausdinimo trukmės." +# AI Translated +msgctxt "second" msgid "s" msgstr "s" @@ -14199,6 +14329,62 @@ msgstr "Atramų gija" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Atramų gija paprastai naudojama atramoms ir atramų skiriamiesiems sluoksniams spausdinti." +# AI Translated +msgid "Is mixed filament" +msgstr "Yra mišri gija" + +# AI Translated +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "Nurodo, ar ši gijos vieta yra mišri gija, sudaryta iš kelių fizinių gijų" + +# AI Translated +msgid "Mixed filament components" +msgstr "Mišrios gijos komponentai" + +# AI Translated +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "Kableliais atskirti komponentinių gijų indeksai, skaičiuojami nuo 1, pvz. \"1,3\"" + +# AI Translated +msgid "Mixed filament sublayer ratios" +msgstr "Mišrios gijos posluoksnių santykiai" + +# AI Translated +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "Kableliais atskirtos santykio reikšmės, kurių suma lygi 1.0, pvz. \"0.7,0.3\"" + +# AI Translated +msgid "Mixed filament gradient" +msgstr "Mišrios gijos gradientas" + +# AI Translated +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "Įjungia gradiento režimą Z kryptimi mišrios gijos posluoksniams. Įjungus posluoksnių santykiai tarp sluoksnių kinta tiesiškai." + +# AI Translated +msgid "Mixed filament gradient range" +msgstr "Mišrios gijos gradiento intervalas" + +# AI Translated +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "Pirmojo komponento pradinis ir galutinis santykis gradiento režimu. Kableliu atskirta pora, pvz. \"0.10,0.90\" reiškia nuo 10% iki 90%." + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "Mišrios gijos gradiento kreivė" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "Neprivaloma pasirinktinė Photoshop stiliaus kreivė, susiejanti Z eigą su pirmojo komponento santykiu. Užrašoma kaip vertikaliais brūkšniais atskirti valdymo taškai, formatu \"x,y\" (senasis) arba \"x,y,m_in,m_out\", kai reikia nustatyti liestinę (tuščia reikšmė arba \"nan\" reiškia numatytąjį PCHIP). x priklauso [0,1]; y apribojamas iki nustatyto santykių intervalo, pvz. \"0,0.15|0.5,0.50|1,0.85\". Jei palikta tuščia, naudojamas tiesinis gradient_range." + +# AI Translated +msgid "Mixed filament per-part gradient" +msgstr "Mišrios gijos gradientas kiekvienai daliai" + +# AI Translated +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "Kai gradiento režimas įjungtas, gradientas taikomas kiekvienai surinkimo daliai atskirai, o ne visam surinkimui kaip vienam Z intervalui." + msgid "Filament printable" msgstr "Gija tinkama spausdinti" @@ -14370,6 +14556,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Giroidas" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Reto užpildo glotninimo koeficientas" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Nustato, kaip stipriai suapvalinami reto užpildo kampai. 0% palieka pradinę aštrią trajektoriją, o 100% sukuria didžiausias įmanomas kreives tarp gretimų užpildo linijų." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Viršutinio paviršiaus užpildo pagreitis. Naudojant mažesnę vertę gali pagerėti viršutinio paviršiaus kokybė." @@ -14914,6 +15108,14 @@ msgstr "Su kokiu G kodu suderinamas spausdintuvas." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Praleisti G-code konfigūracijos bloką" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Neįrašo CONFIG_BLOCK (pjaustyklės konfigūracijos raktų ir reikšmių porų) į G-code failą. Tai gali padėti su spausdintuvais, kurių programinė įranga stringa apdorodama šias komentarų eilutes (pvz., Anycubic go-klipper). Pastaba: G-code faile nebeliks pjaustyklės nustatymų, todėl importavus jį atgal į OrcaSlicer konfigūracija nebus atkurta." + msgid "Pellet Modded Printer" msgstr "Modifikuotas granulinis spausdintuvas" @@ -15433,6 +15635,7 @@ msgid "The allowed maximum output force of Y axis" msgstr "Leidžiama didžiausia Y ašies išvesties jėga" # AI Translated +msgctxt "Newton" msgid "N" msgstr "N" @@ -15445,6 +15648,7 @@ msgid "The machine bed mass load of Y axis" msgstr "Įrenginio pagrindo masės apkrova Y ašiai" # AI Translated +msgctxt "gram" msgid "g" msgstr "g" @@ -15753,7 +15957,7 @@ msgstr "Pradžios ir pabaigos taškai, esantys tarp gijos kirpimo zonos ir atlie msgid "Reduce infill retraction" msgstr "Sumažinti užpildo įtraukimą (Retraction)" -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." msgstr "Neatlikti gijos įtraukimo, kai tuščioji eiga vyksta tik užpildo zonoje. Tokiu atveju gijos varvėjimas (oozing) išorėje bus nematomas. Tai leidžia sumažinti įtraukimų skaičių sudėtingiems modeliams ir sutaupyti spausdinimo laiko, tačiau sulėtina sluoksniavimą bei G-kodo (G-code) generavimą. Pastaba: zonose, kuriose įtraukimas praleidžiamas, „Z-hop“ judesys taip pat neatliekamas." msgid "This option will drop the temperature of the inactive extruders to prevent oozing." @@ -15919,7 +16123,7 @@ msgstr "Atitraukimo kiekis po nubraukimo" # AI Translated #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" "Greito atitraukimo ilgis po nubraukimo, atsižvelgiant į atitraukimo ilgį.\n" @@ -15955,10 +16159,18 @@ msgstr "Ilgas įtraukimas keičiant ekstruderį" msgid "Retraction distance when extruder change" msgstr "Įtraukimo atstumas keičiant ekstruderį" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Atitraukimo ilgis (Įrankio keitimas)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Kai atitraukimas suaktyvinamas prieš įrankio keitimą, gija atitraukiama nurodytu atstumu (ilgis matuojamas ant neapdorotos gijos, prieš jai patenkant į ekstruderį)." + msgid "Z-hop height" msgstr "„Z-hop“ (pakėlimo) aukštis" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "Kaskart atliekant atitraukimą, purkštukas šiek tiek pakeliamas, kad tarp jo ir spaudinio atsirastų tarpas. Tai apsaugo nuo purkštuko užkliudymo už spaudinio judėjimo metu. „Spiralinių“ linijų naudojimas Z ašies kėlimui gali padėti išvengti „tįstančių siūlų“ (angl. \"stringing\")." msgid "Z-hop lower boundary" @@ -16049,6 +16261,10 @@ msgstr "Papildomas ilgis po sugrąžinimo" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Kai po judėjimo kompensuojamas gijos įtraukimas, ekstruderis papildomai išstums šį gijos kiekį. Šis nustatymas reikalingas retai." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Papildomas ilgis po sugrąžinimo (Įrankio keitimas)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Kai po įrankio pakeitimo kompensuojamas gijos įtraukimas, ekstruderis papildomai išstums šį gijos kiekį." @@ -16461,6 +16677,14 @@ msgstr "Įrankio keitimas virš valymo bokšto" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Priverstinai nukreipti spausdinimo galvutę prie valymo bokšto prieš vykdant įrankio keitimo komandą (Tx). Aktualu tik spausdintuvams su keliais ekstruderiais (keliomis galvutėmis), naudojantiems 2 tipo valymo bokštą. Pagal numatytuosius nustatymus „OrcaSlicer“ praleidžia šį judesį kelių galvučių įrenginiuose, nes galvučių sukeitimą valdo aparatinė programinė įranga, todėl Tx komanda gali būti įvykdyta virš spausdinamos detalės. Įjunkite šią parinktį, jei norite, kad įrankio keitimas visada vyktų virš valymo bokšto." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Laukti temperatūros ant valymo bokšto" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Paima naują įrankį nelaukdamas, kol jis pasieks spausdinimo temperatūrą, nuvažiuoja prie valymo bokšto ir ten laukia temperatūros, prieš pat pravalymą. Kaitinant ištekėjusi medžiaga patenka ant bokšto, o ne ant modelio, o pervažiavimas persidengia su kaitinimu. Aktualu tik daugiaekstruderiams (kelių spausdinimo galvučių) spausdintuvams, naudojantiems 2 tipo valymo bokštą. Programinė įranga ar įrankio keitimo makrokomanda neturi pati laukti temperatūros. Kai išjungta, laukimo temperatūros komanda pateikiama iškart po įrankio keitimo komandos." + msgid "No sparse layers (beta)" msgstr "Nėra retų sluoksnių (beta)" @@ -16993,6 +17217,14 @@ msgstr "" "\n" "Jei žemiau esančiame nustatyme „Įtraukimo kiekis prieš nuvalymą“ nurodysite reikšmę, perteklinis gijos įtraukimas bus atliktas prieš nuvalymą, kitu atveju – po jo." +# AI Translated +msgid "Mixed color sublayer" +msgstr "Mišrios spalvos posluoksnis" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "Įjungia skaidymą į mišrios spalvos posluoksnius. Įjungus sluoksniai, kuriuose yra mišrios spalvos gijų, skaidomi į posluoksnius, kad būtų pasiektas spalvų maišymo efektas." + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "Valymo bokštelis (angl. \"wiping tower\") gali būti naudojamas likučiams nuo purkštuko nuvalyti ir purkštuko viduje esančiam slėgiui stabilizuoti, siekiant išvengti išvaizdos defektų spausdinant objektus." @@ -18076,6 +18308,10 @@ msgstr "Nepavyko suformuoti modelio failo poligonažo arba jame nėra tinkamos g msgid "The supplied file couldn't be read because it's empty." msgstr "Pateikto failo nepavyko nuskaityti, nes jis yra tuščias." +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "Failo formatas nesuderinamas ir jo nepavyksta perskaityti." + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Nežinomas failo formatas: įvesties failas privalo turėti .stl, .obj arba .amf(.xml) plėtinį." @@ -19543,19 +19779,19 @@ msgstr "Rodomi tik tie spausdintuvai, kurių spausdintuvo, kaitinamojo siūlo ir msgid "Only display the filament names with changes to filament presets." msgstr "Rodyti tik tuos gijų pavadinimus, kurių gijos profiliai buvo pakeisti." -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "Bus rodomi tik tie spausdintuvų pavadinimai, kurie turi naudotojo sukurtų profilių, o kiekvienas pasirinktas profilis bus eksportuotas kaip ZIP failas." msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" "Bus rodomi tik tie gijų pavadinimai, kurie turi naudotojo sukurtų profilių, \n" "o visi pasirinkto pavadinimo naudotojo gijos profiliai bus eksportuoti kaip ZIP failas." msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" "Bus rodomi tik tie spausdintuvų pavadinimai, kurie turi pakeistų proceso profilių, \n" "o visi pasirinkto spausdintuvo naudotojo proceso profiliai bus eksportuoti kaip ZIP failas." @@ -19702,9 +19938,6 @@ msgstr "Fizinis spausdintuvas" msgid "Print Host upload" msgstr "Įkėlimas spausdinimui tinkle" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Pasirinkite tinklo agento modulį ryšiui su spausdintuvu palaikyti. Prieinami agentai užregistruojami paleidimo metu." - msgid "Select a Flashforge printer" msgstr "Pasirinkite „Flashforge“ spausdintuvą" @@ -19788,7 +20021,7 @@ msgstr "Kopijuoti sistemos informaciją į iškarpinę" msgid "We need information for diagnosing source of the issue. Check wiki page for detailed guide." msgstr "Mums reikia informacijos problemos šaltiniui diagnozuoti. Išsamų vadovą rasite „Wiki“ puslapyje." -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "Mygtukas „Supakuoti“ (Pack) surenka projekto failą ir dabartinės sesijos žurnalus į ZIP failą." msgid "Any additional visual examples like images or screen recordings might be helpful while reporting the issue." @@ -19830,7 +20063,7 @@ msgstr "Žurnalo lygis" msgid "Stored logs" msgstr "Saugomi žurnalai" -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "Supakuoja visus saugomus žurnalus į ZIP failą." msgid "Profiles" @@ -19897,7 +20130,7 @@ msgstr "Spausdintuvo tipas nerastas, pasirinkite rankiniu būdu." msgid "Authorizing..." msgstr "Autorizuojama..." -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "Klaida. Nepavyko gauti API žetono (token) autorizacijai" msgid "Could not parse server response." @@ -20345,8 +20578,8 @@ msgid "Enable smart filament assign: Assign one filament to multiple nozzles to msgstr "Įjungti išmanųjį gijos priskyrimą: priskirkite vieną giją keliems purkštukams, kad sutaupytumėte kuo daugiau" # AI Translated -msgid "Fila Saving" -msgstr "Gijos taupymas" +msgid "File Saving" +msgstr "Failo įrašymas" msgid "Don't remind me again" msgstr "Daugiau neberodyti šio priminimo" @@ -20552,9 +20785,6 @@ msgstr "Bandant prisijungti įvyko kažkas netikėto. Bandykite dar kartą." msgid "User canceled." msgstr "Vartotojas atšaukė." -msgid "Head diameter" -msgstr "Galvutės skersmuo" - msgid "Max angle" msgstr "Maksimalus kampas" @@ -20714,6 +20944,9 @@ msgstr "Paleisti iš naujo dabar" msgid "NO RAMMING AT ALL" msgstr "VISIŠKAI NENAUDOTI SUTANKINIMO" +msgid "s" +msgstr "s" + msgid "Volumetric speed" msgstr "Tūrinis greitis" @@ -21336,6 +21569,57 @@ msgstr "" "Venkite deformacijų (warping)\n" "Ar žinojote, kad spausdinant medžiagas, kurios yra linkusios trauktis ir riestis (pvz., ABS), tinkamas kaitinamojo pagrindo temperatūros padidinimas gali sumažinti deformacijų (warping) tikimybę?" +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "Tiesioginei „Native Wayland“ peržiūrai reikalingas „GStreamer GTK“ vaizdo sinchronizatorius (video sink). Įdiekite „GStreamer“ skirtą „gtksink“ papildinį, tada iš naujo paleiskite „OrcaSlicer“." + +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "Nepavyko inicijuoti „native Wayland GStreamer“ vaizdo sinchronizatoriaus (video sink). Patikrinkite „GStreamer GTK“ papildinio įdiegimą." + +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "Šiai užduočiai atlikti reikalingas \"Windows Media Player\"! Ar norite įjungti \"Windows Media Player\" savo operacinėje sistemoje?" + +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "„BambuSource“ neteisingai užregistruotas medijos atkūrimui! Paspauskite „Taip“, kad jį perregistruotumėte. Jums reikės patvirtinti du kartus." + +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "Trūksta BambuSource komponento, užregistruoto medijos atkūrimui! Iš naujo įdiekite OrcaSlicer arba kreipkitės pagalbos į bendruomenę." + +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "Naudojant \"BambuSource\" iš kito diegimo šaltinio, vaizdo įrašų atkūrimas gali būti neteisingas! Paspauskite Taip, kad tai ištaisytumėte." + +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "Jūsų sistemoje nėra \"GStreamer\" H.264 kodekų, reikalingų vaizdo įrašams atkurti. (Pabandykite įdiegti gstreamer1.0-plugins-bad arba gstreamer1.0-libav paketus, tada iš naujo paleiskite \"Orca Slicer\")" + +# AI Translated +#~ msgid "N" +#~ msgstr "N" + +# AI Translated +#~ msgid "g" +#~ msgstr "g" + +# AI Translated +#~ msgid "Fila Saving" +#~ msgstr "Gijos taupymas" + +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Per mažas sluoksnio aukštis.\n" +#~ "Jis bus nustatytas į min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Sluoksnio aukštis viršija ribą, nurodytą Spausdintuvo nustatymai -> Ekstruderis -> Sluoksnio aukščio ribos, tai gali sukelti spausdinimo kokybės problemų." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "" +#~ "Sureguliuoti pagal nustatytą diapazoną automatiškai?\n" +#~ "\n" + +#~ msgid "Head diameter" +#~ msgstr "Galvutės skersmuo" + #~ msgid "Print order within a single layer." #~ msgstr "Elementų spausdinimo eiliškumas vieno sluoksnio ribose." @@ -22088,20 +22372,12 @@ msgstr "" #~ msgid "Please correct them in the param tabs" #~ msgstr "Prašome juos ištaisyti parametrų skirtukuose" -#~ msgid "Name of components inside STEP file is not UTF8 format!" +#~ msgid "Name of components inside STEP file is not UTF-8 format!" #~ msgstr "Komponentų pavadinimai STEP faile nėra UTF-8 formato!" #~ msgid "The name may show garbage characters!" #~ msgstr "Pavadinimas gali rodyti neskaitomus simbolius!" -#, c-format, boost-format -#~ msgid "" -#~ "The object from file %s is too small, and maybe in meters or inches.\n" -#~ " Do you want to scale to millimeters?" -#~ msgstr "" -#~ "Objektas iš failo „%s“ yra per mažas ir galimai nurodytas metrais arba coliais.\n" -#~ "Ar norite mastelį pakeisti į milimetrus?" - #~ msgid "" #~ "This file contains several objects positioned at multiple heights.\n" #~ "Instead of considering them as multiple objects, should \n" @@ -22491,21 +22767,6 @@ msgstr "" #~ msgid "Maximum detour distance for avoiding crossing wall. Don't detour if the detour distance is larger than this value. Detour length could be specified either as an absolute value or as percentage (for example 50%) of a direct travel path. Zero to disable." #~ msgstr "Maksimalus apylankos atstumas sienelių kirtimui išvengti. Apylanka nedaroma, jei jos atstumas viršija šią reikšmę. Apylankos ilgis gali būti nurodomas absoliučia verte arba procentais (pavyzdžiui, 50 %) nuo tiesioginio judėjimo trajektorijos. Įrašius 0 – funkcija išjungiama." -#~ msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate." -#~ msgstr "Pagrindo temperatūra visiems sluoksniams, išskyrus pirmąjį. Reikšmė 0 reiškia, kad ši gija nepritaikyta spausdinti ant šalto pagrindo (Cool Plate)." - -#~ msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Textured Cool Plate." -#~ msgstr "Pagrindo temperatūra visiems sluoksniams, išskyrus pirmąjį. Reikšmė 0 reiškia, kad ši gija nepritaikyta spausdinti ant tekstūruoto šalto pagrindo (Textured Cool Plate)." - -#~ msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Engineering Plate." -#~ msgstr "Pagrindo temperatūra visiems sluoksniams, išskyrus pirmąjį. Reikšmė 0 reiškia, kad ši gija nepritaikyta spausdinti ant inžinerinio pagrindo (Engineering Plate)." - -#~ msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the High Temp Plate." -#~ msgstr "Pagrindo temperatūra visiems sluoksniams, išskyrus pirmąjį. Reikšmė 0 reiškia, kad ši gija nepritaikyta spausdinti ant aukštos temperatūros pagrindo (High Temp Plate)." - -#~ msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Textured PEI Plate." -#~ msgstr "Pagrindo temperatūra visiems sluoksniams, išskyrus pirmąjį. Reikšmė 0 reiškia, kad ši gija nepritaikyta spausdinti ant tekstūruoto PEI pagrindo (Textured PEI Plate)." - #~ msgid "Bed temperature of the first layer. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." #~ msgstr "Pirmojo sluoksnio pagrindo temperatūra. Reikšmė 0 reiškia, kad ši gija nepritaikyta spausdinti ant „Cool Plate SuperTack“ pagrindo." diff --git a/localization/i18n/nl/OrcaSlicer_nl.po b/localization/i18n/nl/OrcaSlicer_nl.po index a7ef340804..4fd3601c0e 100644 --- a/localization/i18n/nl/OrcaSlicer_nl.po +++ b/localization/i18n/nl/OrcaSlicer_nl.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -3197,6 +3197,10 @@ msgstr "Bewerken" msgid "Merge with" msgstr "Samenvoegen met" +# AI Translated +msgid "Decompose Color" +msgstr "Kleur ontleden" + # AI Translated msgid "Delete this filament" msgstr "Dit filament verwijderen" @@ -3519,6 +3523,10 @@ msgstr "Montage" msgid "Merge parts to an object" msgstr "Onderdelen samenvoegen tot een object" +# AI Translated +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "Het gebruik van variabele laaghoogte in combinatie met de sublaag met gemengde kleur kan de kwaliteit van de kleurmenging verslechteren." + # AI Translated msgid "Add layers" msgstr "Lagen toevoegen" @@ -5150,6 +5158,23 @@ msgstr "De huidige kamertemperatuur is hoger dan de veilige temperatuur van het msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "De minimale kamertemperatuur (%d℃) is hoger dan de doelkamertemperatuur (%d℃). De minimale waarde is de drempel waarbij het printen start terwijl de kamer verder opwarmt naar de doelwaarde; deze mag die dus niet overschrijden. De waarde wordt begrensd tot de doelwaarde." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "De laaghoogte is te klein. Deze wordt ingesteld op het minimum (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "De laaghoogte valt buiten de limieten die zijn ingesteld in Printerinstellingen -> Extruder -> Laaghoogtelimieten, dit kan problemen met de afdrukkwaliteit veroorzaken." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Automatisch aanpassen naar de limiet (%g mm)?" + +msgid "Adjust" +msgstr "Aanpassen" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -5200,7 +5225,7 @@ msgstr "" "De waarde wordt teruggezet naar 0." # AI Translated -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Afwisselende extra wand werkt niet goed wanneer 'verticale wanddikte waarborgen' op Alles is ingesteld." # AI Translated @@ -5248,7 +5273,7 @@ msgstr "" # AI Translated msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height moet kleiner zijn dan layer_height.\n" @@ -5257,7 +5282,7 @@ msgstr "" # AI Translated #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "De vergrendeldiepte moet kleiner zijn dan de huiddiepte.\n" @@ -5277,6 +5302,13 @@ msgstr "" "Ja - Arachne-wandgenerator inschakelen\n" "Nee - Arachne-wandgenerator uitschakelen en de modus [Displacement] van Vage buitenkant instellen" +# AI Translated +msgid "Brim ear radius" +msgstr "Straal van randoren" + +msgid "Brim width" +msgstr "Rand breedte" + # AI Translated msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "De spiraalmodus werkt alleen wanneer Wanden 1 is, ondersteuning is uitgeschakeld, klontdetectie via aftasten is uitgeschakeld, het aantal bovenste buitenlagen 0 is, de dichtheid van de dunne vulling (infill) 0 is en het timelapse-type traditioneel is." @@ -5582,6 +5614,14 @@ msgstr "Cali G-code niet gegenereerd" msgid "Calibration error" msgstr "Kalibratiefout" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Deze printer beschikt niet over de hardware die dit besturingselement nodig heeft." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Dit besturingselement wordt niet ondersteund op deze printer." + # AI Translated msgid "Network unavailable" msgstr "Netwerk niet beschikbaar" @@ -6513,7 +6553,7 @@ msgid "Size:" msgstr "Maat:" # AI Translated -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Er zijn conflicten tussen G-code-paden gevonden op laag %d, Z = %.2lfmm. Plaats de conflicterende objecten verder uit elkaar (%s <-> %s)." @@ -6714,6 +6754,10 @@ msgstr "Meerdere apparaten" msgid "Project" msgstr "Project" +# AI Translated +msgid "Device (Web)" +msgstr "Apparaat (Web)" + msgid "Yes" msgstr "Ja" @@ -6851,11 +6895,11 @@ msgid "Load a model" msgstr "Laad een model" # AI Translated -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "ZIP-archief importeren" # AI Translated -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "Laad modellen uit een ZIP-archief" msgid "Import Configs" @@ -8516,6 +8560,10 @@ msgstr "Huidig printbed aanpassen" msgid "The %s nozzle can not print %s." msgstr "Het %s-mondstuk kan %s niet printen." +# AI Translated +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "Het printen van filament met gemengde kleur op een printer met één extruder vereist frequente filamentwissels en spoelbeurten, wat het afval en het risico op verstopping van het mondstuk of de afvalgoot aanzienlijk kan vergroten." + # AI Translated #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" @@ -8659,6 +8707,26 @@ msgstr "Synchroniseer filamentlijst vanuit AMS" msgid "Set filaments to use" msgstr "Stel filamenten in om te gebruiken" +# AI Translated +msgid "Add Mixed Filament" +msgstr "Gemengd filament toevoegen" + +# AI Translated +msgid "Mixed Filament" +msgstr "Gemengd filament" + +# AI Translated +msgid "Remove last mixed filament" +msgstr "Laatste gemengde filament verwijderen" + +# AI Translated +msgid "Add mixed filament" +msgstr "Gemengd filament toevoegen" + +# AI Translated +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "Het gemengde filament bevat ongeldige of niet-overeenkomende componenten. Bewerk de betrokken items opnieuw." + msgid "Search plate, object and part." msgstr "Zoek plaat, object en onderdeel." @@ -8666,6 +8734,18 @@ msgstr "Zoek plaat, object en onderdeel." msgid "Pellets" msgstr "Pellets" +# AI Translated +msgid "Mixed filament has broken component references" +msgstr "Het gemengde filament bevat ongeldige componentverwijzingen" + +# AI Translated +msgid "Edit / Delete / Merge" +msgstr "Bewerken / Verwijderen / Samenvoegen" + +# AI Translated +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "Het gemengde doelfilament gebruikt dit fysieke filament als component. Bij het samenvoegen wordt dit fysieke filament verwijderd en kan het gemengde filament ongeldig worden. Doorgaan?" + # AI Translated #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." @@ -8824,8 +8904,8 @@ msgstr "Controleer of de G-codes in deze presets veilig zijn om schade aan de ma msgid "Customized Preset" msgstr "Aangepaste voorinstelling" -msgid "Component name(s) inside step file not in UTF8 format!" -msgstr "Naam van componenten in step-bestand is niet UTF8-formaat!" +msgid "Component name(s) inside step file not in UTF-8 format!" +msgstr "Naam van componenten in step-bestand is niet UTF-8-formaat!" msgid "Because of unsupported text encoding, garbage characters may appear!" msgstr "Vanwege niet-ondersteunde tekstcodering kunnen er onjuiste tekens verschijnen!" @@ -8847,10 +8927,10 @@ msgstr "Het volume van het object is 0" #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "Het object uit bestand %s is erg klein, en misschien in meters of inches.\n" -" Wil je schalen naar millimeters?" +"Wil je schalen naar millimeters?" msgid "Object too small" msgstr "He tobject is te klein" @@ -8867,6 +8947,14 @@ msgstr "" msgid "Multi-part object detected" msgstr "Object met meerdere onderdelen gedetecteerd" +# AI Translated +msgid "Matching textures to filaments" +msgstr "Texturen aan filamenten koppelen" + +# AI Translated +msgid "Texture Import Warning" +msgstr "Waarschuwing bij importeren van textuur" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Wilt u deze bestanden laden als een enkel object bestaande uit meerdere onderdelen?\n" @@ -8999,24 +9087,24 @@ msgid "Replaced with 3D files from directory:\n" msgstr "Vervangen door 3D-bestanden uit de map:\n" # AI Translated -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ Overgeslagen %1%: hetzelfde bestand.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ Overgeslagen %s: hetzelfde bestand.\n" # AI Translated -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ Overgeslagen %1%: bestand bestaat niet.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ Overgeslagen %s: bestand bestaat niet.\n" # AI Translated -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ Overgeslagen %1%: vervangen is mislukt.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ Overgeslagen %s: vervangen is mislukt.\n" # AI Translated -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔ Vervangen %1%.\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔ Vervangen %s.\n" # AI Translated msgid "Replaced volumes" @@ -9098,6 +9186,22 @@ msgstr "" msgid "Sync now" msgstr "Nu synchroniseren" +# AI Translated +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "Het importeren van de textuur is mislukt. Het model lijkt textuurgegevens te bevatten, maar het importproces kon niet worden voltooid. Het model wordt alleen als geometrie geïmporteerd." + +# AI Translated +msgid "Applying texture colors..." +msgstr "Textuurkleuren toepassen..." + +# AI Translated +msgid "Updating 3D view..." +msgstr "3D-weergave bijwerken..." + +# AI Translated +msgid "Texture colors applied." +msgstr "Textuurkleuren toegepast." + msgid "You can keep the modified presets for the new project or discard them" msgstr "Je kunt de aangepaste voorinstellingen bewaren voor het nieuwe project of ze laten vervallen" @@ -9827,6 +9931,18 @@ msgstr "Met deze optie ingeschakeld kunt u een taak tegelijkertijd naar meerdere msgid "Pop up to select filament grouping mode" msgstr "Pop-up om de filamentgroeperingsmodus te kiezen" +# AI Translated +msgid "Visible plugin pages" +msgstr "Zichtbare plug-inpagina's" + +# AI Translated +msgid "pages" +msgstr "pagina's" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Aantal plug-inpagina's dat als vaste tabbladen wordt getoond voordat de overige pagina's worden samengevouwen in een vervolgkeuzelijst op het laatste tabblad." + msgid "Behaviour" msgstr "Gedrag" @@ -10243,6 +10359,18 @@ msgstr "Niet-ondersteunde voorinstellingen tonen" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Toon incompatibele/niet-ondersteunde voorinstellingen in de keuzelijsten voor printer en filament. Deze voorinstellingen kunnen niet worden geselecteerd." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Experimenteel) Printeragents gebruiken in plaats van printhosts" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Stuurt printtaken voor niet-Bambu-printers via printer-plug-inagents in plaats van via de klassieke uploadstroom naar de printhost.\n" +"Wanneer dit is uitgeschakeld, gebruikt OrcaSlicer het oude printhostgedrag." + # AI Translated msgid "Experimental Features" msgstr "Experimentele functies" @@ -10467,6 +10595,10 @@ msgstr "Spiraalvaas" msgid "First layer filament sequence" msgstr "Eerste laag filamentvolgorde" +# AI Translated +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "De filamentlijst bevat gemengde filamenten. De aangepaste filamentvolgorde wordt niet toegepast." + msgid "By Layer" msgstr "Op laag" @@ -10523,10 +10655,26 @@ msgstr "Gebruikersvoorinstelling" msgid "Preset Inside Project" msgstr "Voorinstelling binnen project" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Kopieert alle overgeërfde waarden van de bovenliggende voorinstelling naar deze voorinstelling en verwijdert de overervingsrelatie. Voorinstellingen die alleen met de bovenliggende voorinstelling compatibel zijn, kunnen daardoor niet meer worden ondersteund." + # AI Translated msgid "Detach from parent" msgstr "Losmaken van bovenliggend element" +# AI Translated +msgid "Unique preset" +msgstr "Unieke voorinstelling" + +# AI Translated +msgid "Parent preset" +msgstr "Bovenliggende voorinstelling" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Deze voorinstelling erft niet van een andere voorinstelling." + msgid "Name is unavailable." msgstr "Naam is niet beschikbaar." @@ -11336,22 +11484,6 @@ msgstr "Weet u zeker dat u deze optie wilt inschakelen?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Vulpatronen zijn doorgaans ontworpen om rotatie automatisch af te handelen, zodat ze goed printen en hun beoogde effect bereiken (bijv. Gyroide, Kubisch). Het roteren van het huidige patroon voor de dunne vulling (infill) kan tot onvoldoende ondersteuning leiden. Ga voorzichtig te werk en controleer grondig op mogelijke printproblemen. Weet u zeker dat u deze optie wilt inschakelen?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Laaghoogte is te klein.\n" -"Het zal worden ingesteld op min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "De laaghoogte overschrijdt de limiet in Printerinstellingen -> Extruder -> Laaghoogtelimieten, dit kan problemen met de afdrukkwaliteit veroorzaken." - -msgid "Adjust to the set range automatically?\n" -msgstr "Automatisch aanpassen aan het ingestelde bereik?\n" - -msgid "Adjust" -msgstr "Aanpassen" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Experimentele functie: Het filament op grotere afstand terugtrekken en afsnijden tijdens filamentwisselingen om flush te minimaliseren. Hoewel het het doorspoelen aanzienlijk kan verminderen, kan het ook het risico op een verstopt mondstuk of andere printcomplicaties vergroten." @@ -11551,6 +11683,9 @@ msgstr "Gereserveerde zoekworden gevonden" msgid "Setting Overrides" msgstr "Overschrijvingen instellen" +msgid "Retraction when switching material" +msgstr "Terugtrekken (retraction) bij het wisselen van filament" + msgid "Basic information" msgstr "Basisinformatie" @@ -11689,6 +11824,14 @@ msgstr "Geschikte proces profielen" msgid "Printable space" msgstr "Ruimte waarbinnen geprint kan worden" +# AI Translated +msgid "Printer Agent" +msgstr "Printeragent" + +# AI Translated +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Selecteer de implementatie van de netwerkagent voor de communicatie met de printer. Beschikbare agenten worden bij het opstarten geregistreerd." + # AI Translated #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format @@ -11829,9 +11972,6 @@ msgstr "Limieten voor laaghoogte" msgid "Z-Hop" msgstr "Z-hop" -msgid "Retraction when switching material" -msgstr "Terugtrekken (retraction) bij het wisselen van filament" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -11959,12 +12099,12 @@ msgid "No modifications need to be copied." msgstr "Er hoeven geen wijzigingen te worden gekopieerd." # AI Translated -msgid "Copy paramters" +msgid "Copy parameters" msgstr "Parameters kopiëren" # AI Translated #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "Parameters van %s wijzigen" # AI Translated @@ -12550,33 +12690,6 @@ msgstr "Volumes reinigen voor filament wijziging" msgid "Please choose the filament colour" msgstr "Kies de filamentkleur" -# AI Translated -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "Voor de native Wayland-liveview is de GStreamer GTK-videosink nodig. Installeer de gtksink-plug-in voor GStreamer en start OrcaSlicer opnieuw." - -# AI Translated -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "Initialiseren van de native Wayland GStreamer-videosink is mislukt. Controleer de installatie van uw GStreamer GTK-plug-in." - -# AI Translated -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "Voor deze taak is Windows Media Player vereist! Wilt u 'Windows Media Player' inschakelen voor uw besturingssysteem?" - -# AI Translated -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "BambuSource is niet correct geregistreerd voor het afspelen van media! Klik op Ja om het opnieuw te registreren. U krijgt twee keer een melding" - -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "Ontbrekend BambuSource-component geregistreerd voor media afspelen! Installeer OrcaSlicer opnieuw of zoek hulp in de community." - -# AI Translated -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "Er wordt een BambuSource van een andere installatie gebruikt; het afspelen van video werkt mogelijk niet correct! Klik op Ja om dit te herstellen." - -# AI Translated -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "Op uw systeem ontbreken H.264-codecs voor GStreamer, die nodig zijn om video af te spelen. (Probeer de pakketten gstreamer1.0-plugins-bad of gstreamer1.0-libav te installeren en Orca Slicer opnieuw te starten.)" - # AI Translated msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "De cloudagent is niet beschikbaar. Start OrcaSlicer opnieuw en probeer het nogmaals." @@ -13323,6 +13436,10 @@ msgstr " bevindt zich te dicht bij het uitsluitingsgebied en er zullen botsingen msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " ligt te dicht bij het gebied voor klontdetectie, waardoor er botsingen zullen ontstaan.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " ligt gedeeltelijk buiten het printbare gebied en kan niet worden geprint.\n" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "De geselecteerde mondstuktemperaturen zijn niet compatibel. De mondstuktemperatuur van elk filament moet binnen het aanbevolen mondstuktemperatuurbereik van de andere filamenten vallen. Anders kan het mondstuk verstopt raken of kan de printer beschadigd raken." @@ -13338,6 +13455,10 @@ msgstr "Als u toch wilt printen, kunt u de optie inschakelen via Voorkeuren / Be msgid "No extrusions under current settings." msgstr "Geen extrusion onder de huidige instellingen" +# AI Translated +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "Er wordt een gemengd filament met verloop gebruikt, maar 'Sublaag met gemengde kleur' is uitgeschakeld. Het verloop wordt niet geprint." + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Vloeiende modus van timelapse wordt niet ondersteund wanneer \"per object\" sequentie is ingeschakeld." @@ -13380,6 +13501,10 @@ msgstr "Verklein eventueel uw model of wijzig de huidige printinstellingen en pr msgid "Variable layer height is not supported with Organic supports." msgstr "Variabele laaghoogte wordt niet ondersteund met organische steunen." +# AI Translated +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "Het filament van het afveegblok mag geen gemengd filament zijn." + # AI Translated msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Verschillende mondstukdiameters en verschillende filamentdiameters werken mogelijk niet goed wanneer de prime toren is ingeschakeld. Dit is zeer experimenteel; ga daarom voorzichtig te werk." @@ -13686,10 +13811,6 @@ msgstr "3MF gebruiken in plaats van G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Schakel dit in als de printer een 3MF-bestand als printopdracht accepteert. Indien ingeschakeld verzendt Orca Slicer het geslicede bestand als een .gcode.3mf in plaats van als een gewoon .gcode-bestand." -# AI Translated -msgid "Printer Agent" -msgstr "Printeragent" - # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Selecteer de implementatie van de netwerkagent voor de communicatie met de printer." @@ -13775,14 +13896,12 @@ msgstr "mm of %" msgid "Other layers" msgstr "Andere lagen" -# AI Translated -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." msgstr "Bedtemperatuur voor alle lagen behalve de eerste. Een waarde van 0 betekent dat het filament printen op de Cool Plate SuperTack niet ondersteunt." msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." msgstr "Dit is de bedtemperatuur voor alle lagen behalve de eerste. Een waarde van 0 betekent dat het filament het afdrukken op de Cool Plate niet ondersteunt." -# AI Translated msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Textured Cool Plate." msgstr "Dit is de bedtemperatuur voor alle lagen behalve de eerste. Een waarde van 0 betekent dat het filament printen op de Textured Cool Plate niet ondersteunt." @@ -14443,9 +14562,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Snelheid van interne bruggen. Als de waarde als percentage wordt uitgedrukt, wordt deze berekend op basis van bridge_speed. De standaardwaarde is 150%." -msgid "Brim width" -msgstr "Rand breedte" - msgid "This is the distance from the model to the outermost brim line." msgstr "Dit is de afstand van het model tot de buitenste randlijn." @@ -14537,6 +14653,14 @@ msgstr "" "De geometrie wordt vereenvoudigd voordat scherpe hoeken worden gedetecteerd. Deze parameter geeft de minimale lengte van de afwijking voor die vereenvoudiging aan.\n" "0 om uit te schakelen." +# AI Translated +msgid "Brim ears outer only" +msgstr "Randoren alleen aan de buitenzijde" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Genereert alleen muisoren op de buitencontour van het model, met uitsluiting van gaten en gesloten secties." + msgid "upward compatible machine" msgstr "opwaarts compatibele machine" @@ -15292,6 +15416,8 @@ msgstr "Laag tijd" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "De printkop ventilator wordt ingeschakeld voor lagen waarvan de geschatte printtijd korter is dan deze waarde. Ventilatorsnelheid wordt geïnterpoleerd tussen de minimale en maximale ventilatorsnelheden volgens de printtijd van de laag" +# AI Translated +msgctxt "second" msgid "s" msgstr "s" @@ -15651,6 +15777,62 @@ msgstr "Support materiaal" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Support materiaal wordt vaak gebruikt om support en support interfaces af te drukken." +# AI Translated +msgid "Is mixed filament" +msgstr "Is gemengd filament" + +# AI Translated +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "Geeft aan of dit filamentslot een gemengd filament is dat uit meerdere fysieke filamenten bestaat" + +# AI Translated +msgid "Mixed filament components" +msgstr "Componenten van gemengd filament" + +# AI Translated +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "Door komma's gescheiden indexen van de componentfilamenten, beginnend bij 1, bijv. \"1,3\"" + +# AI Translated +msgid "Mixed filament sublayer ratios" +msgstr "Sublaagverhoudingen van gemengd filament" + +# AI Translated +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "Door komma's gescheiden verhoudingswaarden die samen 1.0 zijn, bijv. \"0.7,0.3\"" + +# AI Translated +msgid "Mixed filament gradient" +msgstr "Verloop van gemengd filament" + +# AI Translated +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "Schakelt de verloopmodus in Z-richting in voor de sublagen van gemengd filament. Wanneer ingeschakeld variëren de sublaagverhoudingen lineair over de lagen." + +# AI Translated +msgid "Mixed filament gradient range" +msgstr "Verloopbereik van gemengd filament" + +# AI Translated +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "Begin- en eindverhouding van de eerste component in de verloopmodus. Door een komma gescheiden paar, bijv. \"0.10,0.90\" betekent 10% tot 90%." + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "Verloopcurve van gemengd filament" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "Optionele aangepaste curve in Photoshop-stijl die de Z-voortgang koppelt aan de verhouding van de eerste component. Gecodeerd als door verticale strepen gescheiden controlepunten, in de vorm \"x,y\" (verouderd) of \"x,y,m_in,m_out\" wanneer de raaklijn moet worden overschreven (een lege waarde of \"nan\" gebruikt de PCHIP-standaard). x ligt in [0,1]; y wordt begrensd tot het ingestelde verhoudingsbereik, bijv. \"0,0.15|0.5,0.50|1,0.85\". Wanneer het veld leeg is, wordt het lineaire gradient_range gebruikt." + +# AI Translated +msgid "Mixed filament per-part gradient" +msgstr "Verloop per onderdeel van gemengd filament" + +# AI Translated +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "Wanneer de verloopmodus is ingeschakeld, wordt het verloop op elk onderdeel van een montage afzonderlijk toegepast in plaats van de hele montage als één Z-bereik te behandelen." + # AI Translated msgid "Filament printable" msgstr "Filament printbaar" @@ -15846,6 +16028,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroide" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Afvlakkingsfactor voor dunne vulling" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Bepaalt hoe sterk de hoeken van de dunne vulling worden afgerond. 0% behoudt het oorspronkelijke scherpe pad, terwijl 100% de grootst mogelijke bochten tussen aangrenzende vullijnen oplevert." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Versnelling van de topoppervlakte-invulling. Gebruik van een lagere waarde kan de kwaliteit van de bovenlaag verbeteren." @@ -16456,6 +16646,14 @@ msgstr "Het type G-code waarmee de printer compatibel is." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "G-code-configuratieblok overslaan" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Schrijft het CONFIG_BLOCK (de sleutel/waarde-paren van de slicerconfiguratie) niet naar het G-code-bestand. Dit kan helpen bij printers waarvan de firmware vastloopt bij het verwerken van deze commentaarregels (bijv. Anycubic go-klipper). Let op: het G-code-bestand bevat dan geen slicerinstellingen meer, dus door het weer in OrcaSlicer te importeren wordt de configuratie niet hersteld." + # AI Translated msgid "Pellet Modded Printer" msgstr "Printer omgebouwd voor pellets" @@ -17073,6 +17271,7 @@ msgid "The allowed maximum output force of Y axis" msgstr "De maximaal toegestane uitgangskracht van de Y-as" # AI Translated +msgctxt "Newton" msgid "N" msgstr "N" @@ -17083,6 +17282,7 @@ msgid "The machine bed mass load of Y axis" msgstr "De massabelasting van het machinebed op de Y-as" # AI Translated +msgctxt "gram" msgid "g" msgstr "g" @@ -17421,8 +17621,8 @@ msgid "Reduce infill retraction" msgstr "Reduceer terugtrekken (retraction) bij vulling (infill)" # AI Translated -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." -msgstr "Trek niet terug als de beweging zich volledig in een opvulgebied bevindt. Dat betekent dat het sijpelen niet zichtbaar is. Dit kan de retraction times voor complexe modellen verkorten en printtijd besparen, maar het segmenteren en het genereren van G-codes langzamer maken. Let op: z-hop wordt ook niet uitgevoerd in gebieden waar het terugtrekken wordt overgeslagen." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." +msgstr "Trek niet terug als de beweging zich volledig in een opvulgebied bevindt. Dat betekent dat het sijpelen niet zichtbaar is. Dit kan de retraction times voor complexe modellen verkorten en printtijd besparen, maar het segmenteren en het genereren van G-codes langzamer maken. Let op: Z-hop wordt ook niet uitgevoerd in gebieden waar het terugtrekken wordt overgeslagen." # AI Translated msgid "This option will drop the temperature of the inactive extruders to prevent oozing." @@ -17611,7 +17811,7 @@ msgstr "Terugtrekhoeveelheid na vegen" # AI Translated #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" "De lengte van de snelle terugtrekking na het vegen, relatief ten opzichte van de terugtreklengte.\n" @@ -17653,11 +17853,19 @@ msgstr "Lange terugtrekking bij extruderwissel" msgid "Retraction distance when extruder change" msgstr "Terugtrekafstand bij extruderwissel" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Terugtreklengte (Gereedschapswissel)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Wanneer het terugtrekken vóór een gereedschapswissel wordt geactiveerd, wordt het filament met de opgegeven hoeveelheid teruggetrokken (de lengte wordt gemeten op het onbewerkte filament, voordat het de extruder ingaat)." + # AI Translated msgid "Z-hop height" msgstr "Z-hop-hoogte" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "Wanneer er een terugtrekking (retraction) is, wordt het mondstuk een beetje opgetild om ruimte te creëren tussen het mondstuk en de print. Dit voorkomt dat het mondstuk de print raakt bij verplaatsen. Het gebruik van spiraallijnen om Z op te tillen kan stringing voorkomen." msgid "Z-hop lower boundary" @@ -17763,6 +17971,10 @@ msgstr "Extra lengte bij herstart" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Als retracten wordt gecompenseerd na een beweging, wordt deze extra hoeveelheid filament geëxtrudeerd. Deze instelling is zelden van toepassing." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Extra lengte bij herstart (Gereedschapswissel)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Als retracten wordt gecompenseerd na een toolwisseling, wordt deze extra hoeveelheid filament geëxtrudeerd." @@ -18255,6 +18467,14 @@ msgstr "Toolwissel op het afveegblok" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Dwing de printkop naar het afveegblok te bewegen voordat de opdracht voor de toolwissel (Tx) wordt gegeven. Alleen relevant voor printers met meerdere extruders (meerdere printkoppen) die een afveegblok van type 2 gebruiken. Standaard slaat Orca deze verplaatsing op machines met meerdere printkoppen over, omdat de firmware de kopwissel afhandelt, waardoor de Tx-opdracht boven het geprinte onderdeel kan worden gegeven. Schakel deze optie in als u wilt dat de toolwissel altijd boven het afveegblok wordt uitgevoerd." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Wachten op temperatuur bij het afveegblok" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Pakt het nieuwe gereedschap op zonder te wachten tot het de printtemperatuur bereikt, verplaatst zich naar het afveegblok en wacht daar op de temperatuur, vlak voor het spoelen. Het materiaal dat tijdens het opwarmen uitloopt komt op het blok terecht in plaats van op het model, en de verplaatsing overlapt met het opwarmen. Alleen relevant voor printers met meerdere extruders (meerdere printkoppen) die een afveegblok van type 2 gebruiken. De firmware of de gereedschapswisselmacro mag niet zelf op de temperatuur wachten. Wanneer dit is uitgeschakeld, wordt het wachten op de temperatuur direct na het gereedschapswisselcommando uitgevoerd." + # AI Translated msgid "No sparse layers (beta)" msgstr "Geen dunne lagen (bèta)" @@ -18838,6 +19058,14 @@ msgstr "" "\n" "Als u hieronder een waarde instelt bij de terugtrekhoeveelheid vóór het vegen, wordt de overtollige terugtrekking vóór het vegen uitgevoerd; anders gebeurt dat erna." +# AI Translated +msgid "Mixed color sublayer" +msgstr "Sublaag met gemengde kleur" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "Schakelt het opsplitsen in sublagen met gemengde kleur in. Wanneer ingeschakeld worden lagen die filamenten met gemengde kleur bevatten opgesplitst in sublagen om kleurmengeffecten te bereiken." + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "De veegtoren kan worden gebruikt om resten op het mondstuk te verwijderen en de druk in het mondstuk te stabiliseren om uiterlijke gebreken bij het printen van objecten te voorkomen." @@ -19655,7 +19883,7 @@ msgid "Allow 3MF with newer version to be sliced." msgstr "Sta toe dat een 3MF met een nieuwere versie wordt geslicet." msgid "Current Z-hop" -msgstr "Huidige z-hop" +msgstr "Huidige Z-hop" # AI Translated msgid "Contains Z-hop present at the beginning of the custom G-code block." @@ -20104,6 +20332,10 @@ msgstr "Het meshen van een modelbestand is mislukt of er is geen geldige vorm." msgid "The supplied file couldn't be read because it's empty." msgstr "Het opgegeven bestand kon niet worden gelezen omdat het leeg is." +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "De bestandsindeling is niet compatibel en kan niet worden gelezen." + # AI Translated msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Onbekende bestandsindeling: het invoerbestand moet de extensie .stl, .obj of .amf(.xml) hebben." @@ -21692,19 +21924,19 @@ msgstr "Alleen printers met wijzigingen in printer-, filament- en proces presets msgid "Only display the filament names with changes to filament presets." msgstr "Geef alleen de filamentnamen weer met wijzigingen in filament presets." -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "Alleen printernamen met gebruikersprinter presets worden weergegeven en elke preset die je kiest, wordt als zip geëxporteerd." msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" "Alleen de filamentnamen met gebruikers presets worden weergegeven, \n" "en alle gebruikers presets in elke filamentnaam die u selecteert, worden geëxporteerd als zip-bestand." msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" "Alleen printernamen met gewijzigde proces presets worden weergegeven, \n" "en alle gebruikersproces presets in elke printernaam die u selecteert, worden als zip geëxporteerd." @@ -21860,10 +22092,6 @@ msgstr "Fysieke printer" msgid "Print Host upload" msgstr "Host-upload afdrukken" -# AI Translated -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Selecteer de implementatie van de netwerkagent voor de communicatie met de printer. Beschikbare agenten worden bij het opstarten geregistreerd." - # AI Translated msgid "Select a Flashforge printer" msgstr "Selecteer een Flashforge-printer" @@ -21963,7 +22191,7 @@ msgid "We need information for diagnosing source of the issue. Check wiki page f msgstr "We hebben informatie nodig om de oorzaak van het probleem te achterhalen. Raadpleeg de wikipagina voor een uitgebreide handleiding." # AI Translated -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "De knop Inpakken verzamelt het projectbestand en de logboeken van de huidige sessie in een zipbestand." # AI Translated @@ -22019,7 +22247,7 @@ msgid "Stored logs" msgstr "Opgeslagen logboeken" # AI Translated -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "Pakt alle opgeslagen logboeken in een zipbestand." # AI Translated @@ -22107,7 +22335,7 @@ msgid "Authorizing..." msgstr "Autoriseren..." # AI Translated -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "Fout. Kan geen API-token voor autorisatie ophalen" # AI Translated @@ -22656,8 +22884,8 @@ msgid "Enable smart filament assign: Assign one filament to multiple nozzles to msgstr "Slimme filamenttoewijzing inschakelen: wijs één filament aan meerdere mondstukken toe om de besparing te maximaliseren" # AI Translated -msgid "Fila Saving" -msgstr "Filamentbesparing" +msgid "File Saving" +msgstr "Bestand opslaan" # AI Translated msgid "Don't remind me again" @@ -22918,9 +23146,6 @@ msgstr "Er is iets onverwachts gebeurd bij het inloggen. Probeer het opnieuw." msgid "User canceled." msgstr "Gebruiker geannuleerd." -msgid "Head diameter" -msgstr "Kopdiameter" - # AI Translated msgid "Max angle" msgstr "Maximale hoek" @@ -23115,6 +23340,9 @@ msgstr "Nu opnieuw starten" msgid "NO RAMMING AT ALL" msgstr "HELEMAAL GEEN RAMMING" +msgid "s" +msgstr "s" + # AI Translated msgid "Volumetric speed" msgstr "Volumetrische snelheid" @@ -23781,6 +24009,61 @@ msgstr "" "Kromtrekken voorkomen\n" "Wist je dat bij het printen van materialen die gevoelig zijn voor kromtrekken, zoals ABS, een juiste verhoging van de temperatuur van het warmtebed de kans op kromtrekken kan verkleinen?" +# AI Translated +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "Voor de native Wayland-liveview is de GStreamer GTK-videosink nodig. Installeer de gtksink-plug-in voor GStreamer en start OrcaSlicer opnieuw." + +# AI Translated +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "Initialiseren van de native Wayland GStreamer-videosink is mislukt. Controleer de installatie van uw GStreamer GTK-plug-in." + +# AI Translated +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "Voor deze taak is Windows Media Player vereist! Wilt u 'Windows Media Player' inschakelen voor uw besturingssysteem?" + +# AI Translated +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "BambuSource is niet correct geregistreerd voor het afspelen van media! Klik op Ja om het opnieuw te registreren. U krijgt twee keer een melding" + +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "Ontbrekend BambuSource-component geregistreerd voor media afspelen! Installeer OrcaSlicer opnieuw of zoek hulp in de community." + +# AI Translated +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "Er wordt een BambuSource van een andere installatie gebruikt; het afspelen van video werkt mogelijk niet correct! Klik op Ja om dit te herstellen." + +# AI Translated +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "Op uw systeem ontbreken H.264-codecs voor GStreamer, die nodig zijn om video af te spelen. (Probeer de pakketten gstreamer1.0-plugins-bad of gstreamer1.0-libav te installeren en Orca Slicer opnieuw te starten.)" + +# AI Translated +#~ msgid "N" +#~ msgstr "N" + +# AI Translated +#~ msgid "g" +#~ msgstr "g" + +# AI Translated +#~ msgid "Fila Saving" +#~ msgstr "Filamentbesparing" + +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Laaghoogte is te klein.\n" +#~ "Het zal worden ingesteld op min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "De laaghoogte overschrijdt de limiet in Printerinstellingen -> Extruder -> Laaghoogtelimieten, dit kan problemen met de afdrukkwaliteit veroorzaken." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Automatisch aanpassen aan het ingestelde bereik?\n" + +#~ msgid "Head diameter" +#~ msgstr "Kopdiameter" + # AI Translated #~ msgid "Print order within a single layer." #~ msgstr "Printvolgorde binnen één laag." diff --git a/localization/i18n/pl/OrcaSlicer_pl.po b/localization/i18n/pl/OrcaSlicer_pl.po index 8599e750e4..0f6fa10e3b 100644 --- a/localization/i18n/pl/OrcaSlicer_pl.po +++ b/localization/i18n/pl/OrcaSlicer_pl.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: OrcaSlicer 2.3.0-rc\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "PO-Revision-Date: \n" "Last-Translator: Krzysztof Morga <<tlumaczeniebs@gmail.com>>\n" "Language-Team: \n" @@ -3012,6 +3012,10 @@ msgstr "Edytuj" msgid "Merge with" msgstr "Scal z" +# AI Translated +msgid "Decompose Color" +msgstr "Rozłóż kolor" + msgid "Delete this filament" msgstr "Usuń ten filament" @@ -3319,6 +3323,10 @@ msgstr "Złożenie" msgid "Merge parts to an object" msgstr "Scal części w obiekt" +# AI Translated +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "Użycie zmiennej wysokości warstwy razem z podwarstwą mieszanego koloru może pogorszyć jakość mieszania kolorów." + # AI Translated msgid "Add layers" msgstr "Dodaj warstwy" @@ -4843,6 +4851,23 @@ msgstr "Obecna temperatura komory jest wyższa niż bezpieczna temperatura dla f msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Minimalna temperatura komory (%d℃) jest wyższa niż docelowa temperatura komory (%d℃). Wartość minimalna to próg, przy którym rozpoczyna się druk, podczas gdy komora nadal nagrzewa się do wartości docelowej, więc nie powinna jej przekraczać. Zostanie ograniczona do wartości docelowej." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "Wysokość warstwy jest zbyt mała. Zostanie ustawiona na minimum (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "Wysokość warstwy wykracza poza limity ustawione w Ustawieniach Drukarki -> Ekstruder -> Limity wysokości warstwy, co może powodować problemy z jakością druku." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Dostosować ją automatycznie do limitu (%g mm)?" + +msgid "Adjust" +msgstr "Dostosuj" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4890,7 +4915,7 @@ msgstr "" "\n" "Wartość zostanie zresetowana do 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Alternatywna dodatkowa ściana działa tylko wtedy, gdy jest wyłączona opcja „zapewnij stałą grubość pionowej powłoki”." msgid "" @@ -4936,7 +4961,7 @@ msgstr "" "NIE - Zachowaj Niezależną wysokość warstwy podpory" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height musi być mniejsza niż wysokość warstwy.\n" @@ -4945,7 +4970,7 @@ msgstr "" # AI Translated #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "Głębokość blokady powinna być mniejsza niż głębokość skóry.\n" @@ -4965,6 +4990,13 @@ msgstr "" "Tak — włącz generator ścian Arachne\n" "Nie — wyłącz generator ścian Arachne i ustaw tryb [Przesunięcie] skóry fuzzy" +# AI Translated +msgid "Brim ear radius" +msgstr "Promień ucha brim" + +msgid "Brim width" +msgstr "Szerokość brimu" + # AI Translated msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Tryb spiralny działa tylko wtedy, gdy liczba pętli ściany wynosi 1, podpory są wyłączone, wykrywanie zlepiania przez sondowanie jest wyłączone, liczba warstw górnej powłoki wynosi 0, gęstość wypełnienia wynosi 0, a typ timelapse jest tradycyjny." @@ -5226,6 +5258,14 @@ msgstr "Nie udało się wygenerować kodu kalibracji" msgid "Calibration error" msgstr "Błąd kalibracji" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Ta drukarka nie ma skonfigurowanego sprzętu wymaganego przez ten element sterujący." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Ten element sterujący nie jest obsługiwany przez tę drukarkę." + # AI Translated msgid "Network unavailable" msgstr "Sieć niedostępna" @@ -6109,7 +6149,7 @@ msgstr "Objętość:" msgid "Size:" msgstr "Rozmiar:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Wykryto konflikty ścieżek G-code na warstwie %d, Z = %.2lfmm. Proszę oddalić od siebie obiekty będące w konflikcie (%s <-> %s)." @@ -6295,6 +6335,10 @@ msgstr "Wiele urządzeń" msgid "Project" msgstr "Projekt" +# AI Translated +msgid "Device (Web)" +msgstr "Urządzenie (Web)" + msgid "Yes" msgstr "Tak" @@ -6431,10 +6475,10 @@ msgstr "Importuj 3MF/STL/STEP/SVG/OBJ/AMF" msgid "Load a model" msgstr "Wczytaj model" -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "Importuj archiwum ZIP" -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "Wczytaj modele zawarte w archiwum ZIP" msgid "Import Configs" @@ -7994,6 +8038,10 @@ msgstr "Dostosuj bieżący stół" msgid "The %s nozzle can not print %s." msgstr "Dysza %s nie może drukować %s." +# AI Translated +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "Drukowanie filamentu o mieszanym kolorze na drukarce z jednym ekstruderem wymaga częstych zmian filamentu i płukania, co może znacznie zwiększyć ilość odpadów oraz ryzyko zatkania dyszy lub rynny na odpady." + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "Mieszanie %1% z %2% podczas druku nie jest zalecane.\n" @@ -8120,12 +8168,44 @@ msgstr "Synchronizuj listę filamentów z AMS" msgid "Set filaments to use" msgstr "Wybierz filamenty do użycia" +# AI Translated +msgid "Add Mixed Filament" +msgstr "Dodaj filament mieszany" + +# AI Translated +msgid "Mixed Filament" +msgstr "Filament mieszany" + +# AI Translated +msgid "Remove last mixed filament" +msgstr "Usuń ostatni filament mieszany" + +# AI Translated +msgid "Add mixed filament" +msgstr "Dodaj filament mieszany" + +# AI Translated +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "Filament mieszany ma nieprawidłowe lub niezgodne składniki. Proszę ponownie edytować odpowiednie pozycje." + msgid "Search plate, object and part." msgstr "Szukaj stołu, obiektu i części." msgid "Pellets" msgstr "Granulat" +# AI Translated +msgid "Mixed filament has broken component references" +msgstr "Filament mieszany ma uszkodzone odwołania do składników" + +# AI Translated +msgid "Edit / Delete / Merge" +msgstr "Edytuj / Usuń / Scal" + +# AI Translated +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "Docelowy filament mieszany używa tego fizycznego filamentu jako składnika. Scalenie usunie ten fizyczny filament i może unieważnić filament mieszany. Kontynuować?" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "Po zakończeniu operacji projekt %s zostanie zamknięty i zostanie utworzony nowy projekt." @@ -8273,8 +8353,8 @@ msgstr "Proszę potwierdź, że G-code w tych profilach jest bezpieczny, aby zap msgid "Customized Preset" msgstr "Dostosowany profil" -msgid "Component name(s) inside step file not in UTF8 format!" -msgstr "Nazwa komponentów w pliku step nie jest w formacie UTF8!" +msgid "Component name(s) inside step file not in UTF-8 format!" +msgstr "Nazwa komponentów w pliku step nie jest w formacie UTF-8!" msgid "Because of unsupported text encoding, garbage characters may appear!" msgstr "Nazwa może zawierać nieczytelne znaki!" @@ -8295,10 +8375,10 @@ msgstr "Objętość tego obiektu wynosi zero" #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "Obiekt z pliku %s jest zbyt mały i możliwe, że jest określony w metrach lub calach.\n" -" Czy przeskalować na milimetry?" +"Czy przeskalować na milimetry?" msgid "Object too small" msgstr "Zbyt mały obiekt" @@ -8315,6 +8395,14 @@ msgstr "" msgid "Multi-part object detected" msgstr "Wykryto obiekt składający się z wielu części" +# AI Translated +msgid "Matching textures to filaments" +msgstr "Dopasowywanie tekstur do filamentów" + +# AI Translated +msgid "Texture Import Warning" +msgstr "Ostrzeżenie importu tekstury" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Czy wczytać te pliki jako pojedynczy obiekt składający się z wielu części?\n" @@ -8444,24 +8532,24 @@ msgid "Replaced with 3D files from directory:\n" msgstr "Zastąpiono plikami 3D z katalogu:\n" # AI Translated -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ Pominięto %1%: ten sam plik.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ Pominięto %s: ten sam plik.\n" # AI Translated -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ Pominięto %1%: plik nie istnieje.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ Pominięto %s: plik nie istnieje.\n" # AI Translated -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ Pominięto %1%: nie udało się zastąpić.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ Pominięto %s: nie udało się zastąpić.\n" # AI Translated -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔ Zastąpiono %1%.\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔ Zastąpiono %s.\n" # AI Translated msgid "Replaced volumes" @@ -8543,6 +8631,22 @@ msgstr "" msgid "Sync now" msgstr "Synchronizuj teraz" +# AI Translated +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "Import tekstury nie powiódł się. Model zdaje się zawierać dane tekstury, ale nie udało się ukończyć procesu importu. Model zostanie zaimportowany wyłącznie jako geometria." + +# AI Translated +msgid "Applying texture colors..." +msgstr "Stosowanie kolorów tekstury..." + +# AI Translated +msgid "Updating 3D view..." +msgstr "Aktualizowanie widoku 3D..." + +# AI Translated +msgid "Texture colors applied." +msgstr "Zastosowano kolory tekstury." + msgid "You can keep the modified presets for the new project or discard them" msgstr "Można zachować zmodyfikowane profile w nowym projekcie lub je odrzucić." @@ -9232,6 +9336,18 @@ msgstr "Umożliwia wysyłanie zadania do wielu urządzeń jednocześnie i zarzą msgid "Pop up to select filament grouping mode" msgstr "Okno dialogowe do wyboru trybu grupowania filamentów" +# AI Translated +msgid "Visible plugin pages" +msgstr "Widoczne strony wtyczek" + +# AI Translated +msgid "pages" +msgstr "stron" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Liczba stron wtyczek wyświetlanych jako stałe karty, zanim pozostałe strony zostaną zwinięte do listy rozwijanej na ostatniej karcie." + # AI Translated msgid "Behaviour" msgstr "Zachowanie" @@ -9647,6 +9763,18 @@ msgstr "Pokaż nieobsługiwane profile" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Pokazuj niekompatybilne/nieobsługiwane profile na listach rozwijanych drukarek i filamentów. Tych profili nie można wybrać." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Eksperymentalne) Używaj agentów drukarki zamiast serwerów druku" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Kieruje zadania druku dla drukarek innych niż Bambu przez agentów wtyczek drukarki zamiast klasycznego przesyłania do serwera druku.\n" +"Gdy opcja jest wyłączona, OrcaSlicer korzysta z dotychczasowego działania serwera druku." + # AI Translated msgid "Experimental Features" msgstr "Funkcje eksperymentalne" @@ -9862,6 +9990,10 @@ msgstr "Tryb wazy" msgid "First layer filament sequence" msgstr "Sekwencja koloru pierwszej warstwy" +# AI Translated +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "Lista filamentów zawiera filamenty mieszane. Niestandardowa kolejność filamentów nie zostanie zastosowana." + msgid "By Layer" msgstr "Wg warstwy" @@ -9918,10 +10050,26 @@ msgstr "Profil użytkownika" msgid "Preset Inside Project" msgstr "Profil wewnątrz projektu" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Kopiuje do tego profilu wszystkie wartości odziedziczone z profilu nadrzędnego i usuwa relację dziedziczenia. Profile zgodne wyłącznie z profilem nadrzędnym mogą przestać być obsługiwane." + # AI Translated msgid "Detach from parent" msgstr "Odłącz od elementu nadrzędnego" +# AI Translated +msgid "Unique preset" +msgstr "Profil niezależny" + +# AI Translated +msgid "Parent preset" +msgstr "Profil nadrzędny" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Ten profil nie dziedziczy z innego profilu." + msgid "Name is unavailable." msgstr "Nazwa jest niedostępna." @@ -10684,22 +10832,6 @@ msgstr "Czy na pewno włączyć tę opcję?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Wzory wypełnienia są zwykle projektowane tak, aby samodzielnie obsługiwać obrót, co zapewnia prawidłowy druk i zamierzony efekt (np. Gyroidalny, Sześcienny). Obracanie bieżącego wzoru wypełnienia może prowadzić do niewystarczającego podparcia. Zachowaj ostrożność i dokładnie sprawdź, czy nie występują problemy z drukiem. Czy na pewno chcesz włączyć tę opcję?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Wysokość warstwy jest zbyt mała.\n" -"Ustawione zostanie na min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Wysokość warstwy przekracza limit w Ustawieniach Drukarki -> Extruder -> Limity wysokości warstwy, co może powodować problemy z jakością druku." - -msgid "Adjust to the set range automatically?\n" -msgstr "Dostosować automatycznie do ustawionego zakresu?\n" - -msgid "Adjust" -msgstr "Dostosuj" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Funkcja eksperymentalna: Polega na wycofywaniu filamentu na większą odległość w celu zminimalizowania płukania, a następne jego odcięcie. Choć może to znacząco zmniejszyć ilość zużytego filamentu, może również zwiększyć ryzyko zatknięcia dyszy lub innych problemów z drukowaniem." @@ -10899,6 +11031,9 @@ msgstr "Znaleziono zarezerwowane słowa kluczowe" msgid "Setting Overrides" msgstr "Nadpisywane Ustawień" +msgid "Retraction when switching material" +msgstr "Retrakcja podczas zmiany filamentu" + msgid "Basic information" msgstr "Podstawowe informacje" @@ -11033,6 +11168,14 @@ msgstr "Kompatybilne profile procesów" msgid "Printable space" msgstr "Przestrzeń do druku" +# AI Translated +msgid "Printer Agent" +msgstr "Agent drukarki" + +# AI Translated +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Wybierz implementację agenta sieciowego do komunikacji z drukarką. Dostępni agenci są rejestrowani przy uruchamianiu." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -11165,9 +11308,6 @@ msgstr "Ograniczenia wysokości warstwy" msgid "Z-Hop" msgstr "Z-Hop" -msgid "Retraction when switching material" -msgstr "Retrakcja podczas zmiany filamentu" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -11295,12 +11435,12 @@ msgid "No modifications need to be copied." msgstr "Nie ma modyfikacji do skopiowania." # AI Translated -msgid "Copy paramters" +msgid "Copy parameters" msgstr "Kopiuj parametry" # AI Translated #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "Zmodyfikuj parametry %s" # AI Translated @@ -11828,30 +11968,6 @@ msgstr "Objętości płukania przy zmianie filamentu" msgid "Please choose the filament colour" msgstr "Proszę wybrać kolor filamentu" -# AI Translated -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "Natywny podgląd na żywo w Wayland wymaga ujścia wideo GStreamer GTK. Zainstaluj wtyczkę gtksink dla GStreamer, a następnie uruchom ponownie OrcaSlicer." - -# AI Translated -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "Nie udało się zainicjować natywnego ujścia wideo GStreamer dla Wayland. Sprawdź instalację wtyczki GStreamer GTK." - -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "Do wykonania tego zadania wymagany jest Windows Media Player! Czy włączyć „Windows Media Player” dla systemu operacyjnego?" - -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "BambuSource nie został poprawnie zarejestrowany do odtwarzania mediów! Naciśnij Tak, aby ponownie go zarejestrować. Będziesz poproszony dwa razy." - -# AI Translated -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "Brak zarejestrowanego komponentu BambuSource do odtwarzania multimediów! Zainstaluj ponownie OrcaSlicer lub poszukaj pomocy w społeczności." - -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "Jeśli używasz BambuSource z innej instalacji programu, odtwarzanie wideo może nie działać poprawnie! Naciśnij Tak, aby to naprawić." - -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "Twój system nie posiada kodeków H.264 dla GStreamer, które są wymagane do odtwarzania wideo. (Spróbuj zainstalować pakiety gstreamer1.0-plugins-bad lub gstreamer1.0-libav, a następnie zrestartuj Orca Slicer?)" - # AI Translated msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "Agent chmury jest niedostępny. Uruchom ponownie OrcaSlicer i spróbuj jeszcze raz." @@ -12559,6 +12675,10 @@ msgstr " jest zbyt blisko obszaru wykluczenia, mogą wystąpić kolizje.\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " jest zbyt blisko obszaru wykrywania zalepienia dyszy, co doprowadzi do kolizji.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " znajduje się częściowo poza obszarem druku i nie może zostać wydrukowany.\n" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Wybrane temperatury dyszy są niezgodne. Temperatura dyszy każdego filamentu musi mieścić się w zalecanym zakresie temperatur dyszy pozostałych filamentów. W przeciwnym razie może dojść do zatkania dyszy lub uszkodzenia drukarki." @@ -12574,6 +12694,10 @@ msgstr "Jeśli mimo to chcesz drukować, możesz włączyć opcję w Preferencje msgid "No extrusions under current settings." msgstr "Brak ekstruzji przy obecnych ustawieniach." +# AI Translated +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "Używany jest filament mieszany z gradientem, ale opcja 'Podwarstwa mieszanego koloru' jest wyłączona. Gradient nie zostanie wydrukowany." + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Tryb „Wygładzony” timelapse nie jest obsługiwany, gdy włączona jest sekwencja druku „według obiektu”." @@ -12612,6 +12736,10 @@ msgstr "Może być konieczne zmniejszenie rozmiaru modelu lub zmiana bieżących msgid "Variable layer height is not supported with Organic supports." msgstr "Zmienna wysokość warstwy nie jest dostępna w przypadku podpór organicznych." +# AI Translated +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "Filament wieży czyszczącej nie może być filamentem mieszanym." + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Różne średnice dysz i filamentu mogą nie działać poprawnie, gdy włączona jest wieża czyszcząca. Jest to mocno eksperymentalna funkcja, więc zaleca się ostrożność." @@ -12901,10 +13029,6 @@ msgstr "Użyj 3MF zamiast G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Włącz tę opcję, jeśli drukarka przyjmuje plik 3MF jako zadanie druku. Po włączeniu Orca Slicer wysyła plik po cięciu jako .gcode.3mf zamiast zwykłego pliku .gcode." -# AI Translated -msgid "Printer Agent" -msgstr "Agent drukarki" - # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Wybierz implementację agenta sieciowego do komunikacji z drukarką." @@ -12992,8 +13116,7 @@ msgstr "mm lub %" msgid "Other layers" msgstr "Pozostałe warstwy" -# AI Translated -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." msgstr "Temperatura stołu dla warstw poza pierwszą. Wartość 0 oznacza, że filament nie obsługuje druku na Cool Plate SuperTack." msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." @@ -13617,9 +13740,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Prędkość wewnętrznych mostów. Jeśli wartość jest wyrażona w procentach, będzie obliczana na podstawie prędkości mostu. Wartość domyślna wynosi 150%." -msgid "Brim width" -msgstr "Szerokość brimu" - msgid "This is the distance from the model to the outermost brim line." msgstr "Odległość od modelu do najbardziej zewnętrznej linii brimu" @@ -13703,6 +13823,14 @@ msgstr "" "Kształt zostanie zredukowany przed wykryciem ostrych kątów. Ten parametr wskazuje minimalną długość odchylenia dla redukcji.\n" "0, aby dezaktywować" +# AI Translated +msgid "Brim ears outer only" +msgstr "Uszy brim tylko na zewnątrz" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Generuje uszy myszy tylko na zewnętrznym obrysie modelu, z pominięciem otworów i zamkniętych sekcji." + msgid "upward compatible machine" msgstr "drukarka kompatybilna i wzwyż" @@ -14404,6 +14532,8 @@ msgstr "Czas warstwy" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "Wentylator chłodzący części zostanie włączony dla warstw, których szacowany czas jest krótszy niż ta wartość. Prędkość wentylatora jest interpolowana między minimalną a maksymalną prędkością wentylatora zgodnie z czasem druku warstwy" +# AI Translated +msgctxt "second" msgid "s" msgstr "s" @@ -14714,6 +14844,62 @@ msgstr "Materiał podporowy" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Materiał podporowy jest powszechnie używany do drukowania podpór i warstw łączących podpory z modelem" +# AI Translated +msgid "Is mixed filament" +msgstr "Jest filamentem mieszanym" + +# AI Translated +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "Określa, czy to gniazdo filamentu jest filamentem mieszanym złożonym z kilku fizycznych filamentów" + +# AI Translated +msgid "Mixed filament components" +msgstr "Składniki filamentu mieszanego" + +# AI Translated +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "Rozdzielone przecinkami indeksy filamentów składowych liczone od 1, np. \"1,3\"" + +# AI Translated +msgid "Mixed filament sublayer ratios" +msgstr "Proporcje podwarstw filamentu mieszanego" + +# AI Translated +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "Rozdzielone przecinkami wartości proporcji sumujące się do 1.0, np. \"0.7,0.3\"" + +# AI Translated +msgid "Mixed filament gradient" +msgstr "Gradient filamentu mieszanego" + +# AI Translated +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "Włącza tryb gradientu w kierunku Z dla podwarstw filamentu mieszanego. Po włączeniu proporcje podwarstw zmieniają się liniowo między warstwami." + +# AI Translated +msgid "Mixed filament gradient range" +msgstr "Zakres gradientu filamentu mieszanego" + +# AI Translated +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "Początkowa i końcowa proporcja pierwszego składnika w trybie gradientu. Para rozdzielona przecinkiem, np. \"0.10,0.90\" oznacza od 10% do 90%." + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "Krzywa gradientu filamentu mieszanego" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "Opcjonalna własna krzywa w stylu Photoshopa odwzorowująca postęp w osi Z na proporcję pierwszego składnika. Zapisywana jako punkty kontrolne rozdzielone pionowymi kreskami, w postaci \"x,y\" (starszy format) lub \"x,y,m_in,m_out\", gdy potrzebne jest nadpisanie stycznej (pusta wartość lub \"nan\" oznacza domyślną wartość PCHIP). x należy do [0,1]; y jest ograniczane do skonfigurowanego zakresu proporcji, np. \"0,0.15|0.5,0.50|1,0.85\". Gdy pole jest puste, używany jest liniowy gradient_range." + +# AI Translated +msgid "Mixed filament per-part gradient" +msgstr "Gradient filamentu mieszanego dla każdej części" + +# AI Translated +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "Gdy tryb gradientu jest włączony, gradient jest stosowany do każdej części złożenia niezależnie, zamiast traktować całe złożenie jako jeden zakres Z." + msgid "Filament printable" msgstr "Filament do druku" @@ -14896,6 +15082,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroidalny" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Współczynnik wygładzania wypełnienia" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Określa, jak mocno zaokrąglane są narożniki wypełnienia. 0% zachowuje oryginalną ostrą ścieżkę, a 100% tworzy największe możliwe łuki pomiędzy sąsiednimi liniami wypełnienia." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Przyspieszenie dla wypełnienia górnej powierzchni. Użycie niższej wartości może poprawić jakość górnej powierzchni" @@ -15459,6 +15653,14 @@ msgstr "Z jakim rodzajem G-code drukarka jest kompatybilna." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Pomiń blok konfiguracyjny G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Nie zapisuje bloku CONFIG_BLOCK (par klucz/wartość z konfiguracją slicera) do pliku G-code. Może to pomóc w przypadku drukarek, których firmware ulega awarii podczas przetwarzania tych linii komentarza (np. Anycubic go-klipper). Uwaga: plik G-code nie będzie już zawierał ustawień slicera, więc ponowne zaimportowanie go do OrcaSlicer nie przywróci konfiguracji." + msgid "Pellet Modded Printer" msgstr "Drukarka do druku granulatem" @@ -16018,6 +16220,7 @@ msgid "The allowed maximum output force of Y axis" msgstr "Dopuszczalna maksymalna siła wyjściowa na osi Y" # AI Translated +msgctxt "Newton" msgid "N" msgstr "N" @@ -16028,6 +16231,7 @@ msgid "The machine bed mass load of Y axis" msgstr "Obciążenie masowe stołu maszyny na osi Y" # AI Translated +msgctxt "gram" msgid "g" msgstr "g" @@ -16359,7 +16563,7 @@ msgstr "Punkty początkowe i końcowe, od obszaru cięcia do kanału wyrzutowego msgid "Reduce infill retraction" msgstr "Zmniejszanie retrakcji wypełnienia" -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." msgstr "Nie wykonuj retrakcji, gdy ruch odbywa się całkowicie w obszarze wypełnienia. Oznacza to, że wyciek nie będzie widoczny. Może to zmniejszyć liczbę retrakcji dla skomplikowanego modelu i zaoszczędzić czas druku, ale spowolnić krojenie i generowanie G-code" msgid "This option will drop the temperature of the inactive extruders to prevent oozing." @@ -16535,7 +16739,7 @@ msgstr "Wartość retrakcji po czyszczeniu" # AI Translated #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" "Długość szybkiej retrakcji po czyszczeniu, względem długości retrakcji.\n" @@ -16571,10 +16775,18 @@ msgstr "Długa retrakcja podczas zmian ekstruderów" msgid "Retraction distance when extruder change" msgstr "Długość retrakcji podczas zmian ekstruderów" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Długość retrakcji (Zmiana narzędzia)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Gdy retrakcja jest wyzwalana przed zmianą narzędzia, filament zostaje wycofany o określoną wartość (długość mierzona jest na surowym filamencie, przed wejściem do ekstrudera)." + msgid "Z-hop height" msgstr "Wysokość Z-hop" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "Zawsze gdy wykonana jest retrakcja, dysza jest nieco podnoszona, aby stworzyć odstęp między dyszą a wydrukiem. Zapobiega to uderzeniu dyszy w wydruk podczas przemieszczania. Użycie linii spiralnej do podniesienia Z może zapobiec powstawaniu strun" msgid "Z-hop lower boundary" @@ -16669,6 +16881,10 @@ msgstr "Dodatkowa ilość dla powrotu" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Gdy retrakcja jest kompensowana po przemieszczeniu, ekstruder przepycha tę dodatkową ilość filamentu. To opcja jest rzadko potrzebna." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Dodatkowa ilość dla powrotu (Zmiana narzędzia)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Jeśli retrakcja jest korygowana po zmianie narzędzia, extruder przepchnie taką dodatkową ilość filamentu." @@ -17099,6 +17315,14 @@ msgstr "Zmiana narzędzia na wieży czyszczącej" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Wymusza przemieszczenie głowicy do wieży czyszczącej przed wydaniem polecenia zmiany narzędzia (Tx). Dotyczy tylko drukarek wieloekstruderowych (wielogłowicowych) korzystających z wieży czyszczącej typu 2. Domyślnie Orca pomija to przemieszczenie na maszynach wielogłowicowych, ponieważ zamianą głowic zajmuje się oprogramowanie sprzętowe, przez co polecenie Tx może zostać wydane nad drukowaną częścią. Włącz tę opcję, jeśli chcesz, aby zmiana narzędzia zawsze następowała nad wieżą czyszczącą." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Czekaj na temperaturę na wieży czyszczącej" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Pobiera nowe narzędzie bez czekania, aż osiągnie temperaturę druku, przejeżdża do wieży czyszczącej i tam czeka na temperaturę, tuż przed płukaniem. Materiał wyciekający podczas nagrzewania trafia na wieżę zamiast na model, a przejazd nakłada się na nagrzewanie. Dotyczy wyłącznie drukarek z wieloma ekstruderami (wieloma głowicami) używających wieży czyszczącej typu 2. Firmware ani makro zmiany narzędzia nie mogą samodzielnie czekać na temperaturę. Gdy opcja jest wyłączona, oczekiwanie na temperaturę jest wysyłane bezpośrednio po poleceniu zmiany narzędzia." + msgid "No sparse layers (beta)" msgstr "Warstwy bez czyszczenia (beta)" @@ -17648,6 +17872,14 @@ msgstr "" "\n" "Ustawienie wartości w ilości cofania przed ustawieniem czyszczenia poniżej spowoduje wykonanie nadmiernego cofania przed czyszczeniem, w przeciwnym razie zostanie wykonane po nim." +# AI Translated +msgid "Mixed color sublayer" +msgstr "Podwarstwa mieszanego koloru" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "Włącza podział na podwarstwy mieszanego koloru. Po włączeniu warstwy zawierające filamenty o mieszanym kolorze są dzielone na podwarstwy w celu uzyskania efektu mieszania kolorów." + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "Wieża czyszcząca może być używana do czyszczenia resztek na dyszy i stabilizacji ciśnienia w komorze wewnątrz dyszy, aby uniknąć defektów wyglądu podczas drukowania obiektów." @@ -18395,7 +18627,7 @@ msgid "Current Z-hop" msgstr "Bieżący Z-hop" msgid "Contains Z-hop present at the beginning of the custom G-code block." -msgstr "Zawiera z-hop obecny na początku bloku niestandardowego G-code" +msgstr "Zawiera Z-hop obecny na początku bloku niestandardowego G-code" msgid "Position of the extruder at the beginning of the custom G-code block. If the custom G-code travels somewhere else, it should write to this variable so OrcaSlicer knows where it travels from when it gets control back." msgstr "Pozycja ekstrudera na początku bloku niestandardowego G-kodu. Jeśli niestandardowy G-code przemieszcza się gdzieś indziej, powinien zostać zapisany do tej zmiennej, aby OrcaSlicer wiedział, skąd się przemieszcza, gdy odzyska kontrolę." @@ -18749,6 +18981,10 @@ msgstr "Siatkowanie pliku modelu nie powiodło się lub nie ma prawidłowego ksz msgid "The supplied file couldn't be read because it's empty." msgstr "Dostarczony plik nie mógł być odczytany, ponieważ jest pusty" +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "Format pliku jest niezgodny i nie można go odczytać." + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Nieznany format pliku. Plik wejściowy musi mieć rozszerzenie .stl, .obj, .amf(.xml)." @@ -20285,19 +20521,19 @@ msgstr "Wyświetlane są jedynie drukarki ze zmienionymi profilami drukarki, fil msgid "Only display the filament names with changes to filament presets." msgstr "Wyświetlone są jedynie nazwy filamentów, które zostały zmodyfikowane w ustawieniach." -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "Wyświetlane są tylko nazwy drukarek z ustawieniami użytkownika, wybrany zestaw zostanie wyeksportowany jako plik zip." msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" "Na liście widoczne są tylko nazwy filamentów ze zmodyfikowanymi ustawieniami, \n" "a dla każdej wybranej nazwy filamentu zostaną wyeksportowane wszystkie ustawienia jako archiwum zip." msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" "Na liście widoczne są tylko nazwy drukarek ze zmodyfikowanymi ustawieniami procesu, \n" "a dla każdej wybranej nazwy drukarki zostaną wyeksportowane wszystkie \n" @@ -20445,10 +20681,6 @@ msgstr "Fizyczna drukarka" msgid "Print Host upload" msgstr "Przesyłanie do hosta drukowania" -# AI Translated -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Wybierz implementację agenta sieciowego do komunikacji z drukarką. Dostępni agenci są rejestrowani przy uruchamianiu." - # AI Translated msgid "Select a Flashforge printer" msgstr "Wybierz drukarkę Flashforge" @@ -20548,7 +20780,7 @@ msgid "We need information for diagnosing source of the issue. Check wiki page f msgstr "Potrzebujemy informacji do zdiagnozowania źródła problemu. Szczegółowy przewodnik znajdziesz na stronie wiki." # AI Translated -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "Przycisk Spakuj zbiera plik projektu i dzienniki bieżącej sesji do pliku zip." # AI Translated @@ -20604,7 +20836,7 @@ msgid "Stored logs" msgstr "Zapisane dzienniki" # AI Translated -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "Pakuje wszystkie zapisane dzienniki do pliku zip." # AI Translated @@ -20692,7 +20924,7 @@ msgid "Authorizing..." msgstr "Autoryzowanie..." # AI Translated -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "Błąd. Nie można uzyskać tokenu API do autoryzacji" # AI Translated @@ -21154,8 +21386,8 @@ msgid "Enable smart filament assign: Assign one filament to multiple nozzles to msgstr "Włącz inteligentne przypisywanie filamentu: przypisz jeden filament do wielu dysz, aby zmaksymalizować oszczędności" # AI Translated -msgid "Fila Saving" -msgstr "Oszczędność fil." +msgid "File Saving" +msgstr "Zapisywanie pliku" msgid "Don't remind me again" msgstr "Nie przypominaj mi ponownie" @@ -21401,9 +21633,6 @@ msgstr "Wystąpił problem podczas próby logowania, proszę spróbować ponowni msgid "User canceled." msgstr "Anulowane przez użytkownika." -msgid "Head diameter" -msgstr "Średnica łącznika" - msgid "Max angle" msgstr "Maksymalny kąt" @@ -21589,6 +21818,9 @@ msgstr "Uruchom ponownie teraz" msgid "NO RAMMING AT ALL" msgstr "BRAK WYCISKANIA" +msgid "s" +msgstr "s" + msgid "Volumetric speed" msgstr "Prędkości Przepływu" @@ -22234,6 +22466,58 @@ msgstr "" "Unikaj odkształceń\n" "Czy wiesz, że podczas drukowania filamentami podatnymi na odkształcenia, takimi jak ABS, odpowiednie zwiększenie temperatury podgrzewanej płyty może zmniejszyć prawdopodobieństwo odkształceń?" +# AI Translated +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "Natywny podgląd na żywo w Wayland wymaga ujścia wideo GStreamer GTK. Zainstaluj wtyczkę gtksink dla GStreamer, a następnie uruchom ponownie OrcaSlicer." + +# AI Translated +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "Nie udało się zainicjować natywnego ujścia wideo GStreamer dla Wayland. Sprawdź instalację wtyczki GStreamer GTK." + +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "Do wykonania tego zadania wymagany jest Windows Media Player! Czy włączyć „Windows Media Player” dla systemu operacyjnego?" + +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "BambuSource nie został poprawnie zarejestrowany do odtwarzania mediów! Naciśnij Tak, aby ponownie go zarejestrować. Będziesz poproszony dwa razy." + +# AI Translated +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "Brak zarejestrowanego komponentu BambuSource do odtwarzania multimediów! Zainstaluj ponownie OrcaSlicer lub poszukaj pomocy w społeczności." + +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "Jeśli używasz BambuSource z innej instalacji programu, odtwarzanie wideo może nie działać poprawnie! Naciśnij Tak, aby to naprawić." + +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "Twój system nie posiada kodeków H.264 dla GStreamer, które są wymagane do odtwarzania wideo. (Spróbuj zainstalować pakiety gstreamer1.0-plugins-bad lub gstreamer1.0-libav, a następnie zrestartuj Orca Slicer?)" + +# AI Translated +#~ msgid "N" +#~ msgstr "N" + +# AI Translated +#~ msgid "g" +#~ msgstr "g" + +# AI Translated +#~ msgid "Fila Saving" +#~ msgstr "Oszczędność fil." + +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Wysokość warstwy jest zbyt mała.\n" +#~ "Ustawione zostanie na min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Wysokość warstwy przekracza limit w Ustawieniach Drukarki -> Extruder -> Limity wysokości warstwy, co może powodować problemy z jakością druku." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Dostosować automatycznie do ustawionego zakresu?\n" + +#~ msgid "Head diameter" +#~ msgstr "Średnica łącznika" + #~ msgid "Print order within a single layer." #~ msgstr "Kolejność druku obiektów w obrębie jednej warstwy. Domyślnie lub według listy obiektów" diff --git a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po index 58eabd99ef..0d9698d746 100644 --- a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po +++ b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "PO-Revision-Date: 2026-07-26 11:14-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: Portuguese, Brazilian\n" @@ -217,7 +217,6 @@ msgstr "Os filamentos %s são duros e quebradiços, podendo se romper no AMS. E msgid "%s has a risk of nozzle clogging when using 0.4, 0.6, 0.8mm high-flow nozzles. Use with caution." msgstr "%s apresenta risco de entupimento do bico ao utilizar bicos de alto fluxo de 0,4, 0,6 ou 0,8 mm. Use com cautela." -# AI Translated #, c-format, boost-format msgid "%s may fail to load or unload due to the Filament Track Switch. If you wish to continue." msgstr "%s pode falhar ao carregar ou descarregar devido ao Filament Track Switch. Se você deseja continuar." @@ -347,7 +346,6 @@ msgstr "Leitura " msgid "Please wait" msgstr "Por favor, aguarde" -# AI Translated msgid "Reading" msgstr "Lendo" @@ -700,7 +698,6 @@ msgstr "Redefinir posição" msgid "Reset rotation" msgstr "Redefinir rotação" -# AI Translated msgid "World" msgstr "Mundo" @@ -988,7 +985,6 @@ msgstr "Plano de corte com cavidade é inválido" msgid "Connector" msgstr "Conector" -# AI Translated #, boost-format msgid "" "Objects(%1%) have duplicated connectors. Some connectors may be missing in slicing result.\n" @@ -2032,7 +2028,6 @@ msgstr "" "\n" "Se você não usava o Bambu Cloud para sincronizar perfis, esta mudança não afeta você e você pode ignorar esta mensagem com segurança." -# AI Translated msgid "Profile syncing change" msgstr "Alteração de sincronização de perfil" @@ -2841,6 +2836,10 @@ msgstr "Editar" msgid "Merge with" msgstr "Mesclar com" +# AI Translated +msgid "Decompose Color" +msgstr "Decompor Cor" + msgid "Delete this filament" msgstr "Apagar este filamento" @@ -3123,6 +3122,10 @@ msgstr "Montagem" msgid "Merge parts to an object" msgstr "Mesclar peças com um objeto" +# AI Translated +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "Usar altura de camada variável junto com a subcamada de cor mista pode resultar em uma mistura de cores de baixa qualidade." + msgid "Add layers" msgstr "Adicionar camadas" @@ -3429,7 +3432,7 @@ msgid "AMS has not been initialized. Please initialize it before use." msgstr "O AMS não foi inicializado. Por favor, inicialize-o antes de usar." msgid "Changing fan speed during printing may affect print quality, please choose carefully." -msgstr "Mudar a velocidade do ventilador durante a impressão pode afetar a qualidade da impressão. Escolha com cuidado." +msgstr "Mudar a velocidade da ventoinha durante a impressão pode afetar a qualidade da impressão. Escolha com cuidado." msgid "Change Anyway" msgstr "Mudar Mesmo Assim" @@ -3441,7 +3444,7 @@ msgid "Filter" msgstr "Filtrar" msgid "Enabling filtration redirects the right fan to filter gas, which may reduce cooling performance." -msgstr "Ativar a filtragem redireciona o ventilador direito para filtrar o gás, o que pode reduzir o desempenho de resfriamento." +msgstr "Ativar a filtragem redireciona a ventoinha direito para filtrar o gás, o que pode reduzir o desempenho de resfriamento." msgid "Enabling filtration during printing may reduce cooling and affect print quality. Please choose carefully." msgstr "Habilitar a filtragem durante a impressão pode reduzir o resfriamento e afetar a qualidade da impressão. Escolha com cuidado." @@ -3474,7 +3477,7 @@ msgid "Top" msgstr "Topo" msgid "The fan controls the temperature during printing to improve print quality. The system automatically adjusts the fan's switch and speed according to different printing materials." -msgstr "O ventilador controla a temperatura durante a impressão para melhorar a qualidade da impressão. O sistema ajusta automaticamente a ativação e a velocidade do ventilador de acordo com os diferentes materiais de impressão." +msgstr "A ventoinha controla a temperatura durante a impressão para melhorar a qualidade da impressão. O sistema ajusta automaticamente a ativação e a velocidade da ventoinha de acordo com os diferentes materiais de impressão." msgid "Cooling mode is suitable for printing PLA/PETG/TPU materials and filters the chamber air." msgstr "O modo de resfriamento é adequado para impressão com materiais PLA/PETG/TPU e filtra o ar da câmara." @@ -4582,6 +4585,23 @@ msgstr "A temperatura da câmara atual está mais alta do que a temperatura segu msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "A temperatura mínima da câmara (%d℃) é superior à temperatura alvo da câmara (%d℃). O valor mínimo é o limite no qual a impressão começa enquanto a câmara continua aquecendo em direção ao alvo; portanto, não deve execedê-lo. O valor será limitado ao alvo." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "A altura da camada é muito pequena. Ela será definida para o mínimo (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "A altura da camada está fora dos limites definidos em Configurações da Impressora -> Extrusora -> Limites de altura da camada, isso pode causar problemas de qualidade de impressão." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Ajustar automaticamente para o limite (%g mm)?" + +msgid "Adjust" +msgstr "Ajustar" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4629,7 +4649,7 @@ msgstr "" "\n" "O valor será redefinido para 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "A parede extra alternada não funciona bem quando a espessura vertical da casca está definida para Todos." msgid "" @@ -4675,7 +4695,7 @@ msgstr "" "NÃO — Manter a Altura da Camada de Suporte Independente" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height precisa ser menor que layer_height.\n" @@ -4683,7 +4703,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "A profundidade do travamento deve ser menor que a profundidade da textura.\n" @@ -4701,6 +4721,13 @@ msgstr "" "Sim - Habilitar Gerador de Parede Arachne\n" "Não - Desabilitar Gerador de Parede Arachne e setar o modo [Deslocamento] da Textura Difusa" +# AI Translated +msgid "Brim ear radius" +msgstr "Raio da orelha da borda" + +msgid "Brim width" +msgstr "Largura da borda" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "O modo espiral só funciona quando as voltas da parede são 1, o suporte está desativado, a detecção de aglomeração por sondagem está desativada, as camadas da casca de topo são 0, a densidade de preenchimento esparso é 0 e o tipo de timelapse é tradicional." @@ -4798,7 +4825,7 @@ msgid "Pause (AMS offline)" msgstr "Pausa (AMS offline)" msgid "Pause (low speed of the heatbreak fan)" -msgstr "Pausa (baixa velocidade do ventilador do heatbreak)" +msgstr "Pausa (baixa velocidade da ventoinha do heatbreak)" msgid "Pause (chamber temperature control problem)" msgstr "Pausa (problema no controle de temperatura da câmara)" @@ -4922,7 +4949,7 @@ msgstr "Para garantir sua segurança, certas tarefas de processamento (como o la #, c-format, boost-format msgid "The chamber temperature is too high, which may cause the filament to soften. Please wait until the chamber temperature drops below %d℃. You may open the front door or enable fans to cool down." -msgstr "A temperatura da câmara está muito alta, o que pode causar o amolecimento do filamento. Aguarde até que a temperatura da câmara caia abaixo de %d℃. Você pode abrir a porta frontal ou ligar os ventiladores para resfriar." +msgstr "A temperatura da câmara está muito alta, o que pode causar o amolecimento do filamento. Aguarde até que a temperatura da câmara caia abaixo de %d℃. Você pode abrir a porta frontal ou ligar as ventoinhas para resfriar." #, c-format, boost-format msgid "AMS temperature is too high, which may cause the filament to soften. Please wait until the AMS temperature drops below %d℃." @@ -4955,6 +4982,14 @@ msgstr "Falha ao gerar o G-code de calibração" msgid "Calibration error" msgstr "Erro de calibração" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Esta impressora não está configurada com o hardware que este controle requer." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Este controle não é suportado nesta impressora." + msgid "Network unavailable" msgstr "Rede indisponível" @@ -5208,7 +5243,7 @@ msgid "Jerk" msgstr "Jerk" msgid "Fan Speed" -msgstr "Velocidade do Ventilador" +msgstr "Velocidade da Ventoinha" msgid "Flow" msgstr "Fluxo" @@ -5314,7 +5349,7 @@ msgid "Flow: " msgstr "Fluxo: " msgid "Fan: " -msgstr "Ventilador: " +msgstr "Ventoinha: " msgid "Temperature: " msgstr "Temperatura: " @@ -5350,7 +5385,7 @@ msgid "Flow rate" msgstr "Taxa de fluxo" msgid "Fan speed" -msgstr "Velocidade do ventilador" +msgstr "Velocidade da ventoinha" msgid "Time" msgstr "Tempo" @@ -5464,7 +5499,7 @@ msgid "Jerk (mm/s)" msgstr "Jerk (mm/s)" msgid "Fan speed (%)" -msgstr "Velocidade do ventilador (%)" +msgstr "Velocidade da ventoinha (%)" msgid "Temperature (℃)" msgstr "Temperatura (℃)" @@ -5798,7 +5833,7 @@ msgstr "Volume:" msgid "Size:" msgstr "Tamanho:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Foram encontrados conflitos de caminhos de G-code na camada %d, Z = %.2lfmm. Por favor, separe mais os objetos em conflito (%s <-> %s)." @@ -5979,6 +6014,10 @@ msgstr "Multi-dispositivo" msgid "Project" msgstr "Projeto" +# AI Translated +msgid "Device (Web)" +msgstr "Dispositivo (Web)" + msgid "Yes" msgstr "Sim" @@ -6112,11 +6151,11 @@ msgstr "Importar 3MF/STL/STEP/SVG/OBJ/AMF" msgid "Load a model" msgstr "Carregar um modelo" -msgid "Import Zip Archive" -msgstr "Importar Arquivo Zip" +msgid "Import ZIP Archive" +msgstr "Importar Arquivo ZIP" -msgid "Load models contained within a zip archive" -msgstr "Carregar modelos contidos em um arquivo zip" +msgid "Load models contained within a ZIP archive" +msgstr "Carregar modelos contidos em um arquivo ZIP" msgid "Import Configs" msgstr "Importar Configurações" @@ -7368,12 +7407,11 @@ msgstr "Inferior" msgid "Plugin Selection" msgstr "Seleção de plugins" -# AI Translated msgid "" "No plugins capabilities available for this type.\n" "Enable or install some to use." msgstr "" -"Nenhum recurso de plugins disponível para este tipo.\n" +"Nenhuma capacidade de plugin disponível para este tipo.\n" "Ative ou instale algum para usar." msgid "There is stringing-prone filament in the current print job. Enabling nozzle clumping detection now may degrade print quality. Are you sure you want to enable it?" @@ -7601,6 +7639,10 @@ msgstr "Personalizar a placa atual" msgid "The %s nozzle can not print %s." msgstr "O bico %s não pode imprimir %s." +# AI Translated +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "Imprimir filamento de cor mista em uma impressora de extrusora única exige trocas de filamento e purgas frequentes, o que pode aumentar significativamente o desperdício e o risco de entupimento do bico ou da calha de resíduos." + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "Misturar %1% com %2% na impressão não é recomendado.\n" @@ -7725,12 +7767,44 @@ msgstr "Sincronizar lista de filamentos do AMS" msgid "Set filaments to use" msgstr "Definir filamentos para usar" +# AI Translated +msgid "Add Mixed Filament" +msgstr "Adicionar Filamento Misto" + +# AI Translated +msgid "Mixed Filament" +msgstr "Filamento Misto" + +# AI Translated +msgid "Remove last mixed filament" +msgstr "Remover o último filamento misto" + +# AI Translated +msgid "Add mixed filament" +msgstr "Adicionar filamento misto" + +# AI Translated +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "O filamento misto tem componentes inválidos ou incompatíveis. Edite novamente as entradas afetadas." + msgid "Search plate, object and part." msgstr "Pesquisar placa, objeto e peça." msgid "Pellets" msgstr "Pellets" +# AI Translated +msgid "Mixed filament has broken component references" +msgstr "O filamento misto tem referências de componentes quebradas" + +# AI Translated +msgid "Edit / Delete / Merge" +msgstr "Editar / Excluir / Mesclar" + +# AI Translated +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "O filamento misto de destino usa este filamento físico como componente. Mesclar removerá este filamento físico e poderá invalidar o filamento misto. Continuar?" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "Após a conclusão da sua operação, o projeto %s será encerrado e um novo projeto será criado." @@ -7867,7 +7941,7 @@ msgstr "Por favor, confirme se o G-code dentro dessas predefinições é seguro msgid "Customized Preset" msgstr "Predefinição Personalizada" -msgid "Component name(s) inside step file not in UTF8 format!" +msgid "Component name(s) inside step file not in UTF-8 format!" msgstr "Os nomes dos componentes dentro do arquivo STEP não estão no formato UTF-8!" msgid "Because of unsupported text encoding, garbage characters may appear!" @@ -7889,7 +7963,7 @@ msgstr "O volume do objeto é zero" #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "O objeto do arquivo %s é muito pequeno, e pode estar em metros ou polegadas.\n" "Deseja redimensioná-lo para milímetros?" @@ -7909,6 +7983,14 @@ msgstr "" msgid "Multi-part object detected" msgstr "Objeto multi-peça detectado" +# AI Translated +msgid "Matching textures to filaments" +msgstr "Associando texturas aos filamentos" + +# AI Translated +msgid "Texture Import Warning" +msgstr "Aviso de Importação de Textura" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Carregar esses arquivos como um único objeto com múltiplas peças?\n" @@ -8026,21 +8108,21 @@ msgstr "Diretório para substituição não foi selecionado" msgid "Replaced with 3D files from directory:\n" msgstr "Substituído por arquivos 3D do diretório:\n" -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ %1% Ignorados: mesmo arquivo.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ %s Ignorados: mesmo arquivo.\n" -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ %1% Ignorados: arquivo não existe.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ %s Ignorados: arquivo não existe.\n" -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ %1% Ignorados: falha ao substituir.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ %s Ignorados: falha ao substituir.\n" -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔ %1% Substituídos.\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔ %s Substituídos.\n" msgid "Replaced volumes" msgstr "Volumes substiruídos" @@ -8116,6 +8198,22 @@ msgstr "" msgid "Sync now" msgstr "Sincronizar agora" +# AI Translated +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "Falha ao importar a textura. O modelo parece conter dados de textura, mas o processo de importação não pôde ser concluído. O modelo será importado apenas como geometria." + +# AI Translated +msgid "Applying texture colors..." +msgstr "Aplicando cores de textura..." + +# AI Translated +msgid "Updating 3D view..." +msgstr "Atualizando a visualização 3D..." + +# AI Translated +msgid "Texture colors applied." +msgstr "Cores de textura aplicadas." + msgid "You can keep the modified presets for the new project or discard them" msgstr "Você pode manter as predefinições modificadas no novo projeto ou descartá-las" @@ -8765,6 +8863,18 @@ msgstr "Com esta opção habilitada, você pode enviar uma tarefa para vários d msgid "Pop up to select filament grouping mode" msgstr "Abrir seleção do modo de agrupamento de filamento" +# AI Translated +msgid "Visible plugin pages" +msgstr "Páginas de plugin visíveis" + +# AI Translated +msgid "pages" +msgstr "páginas" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Número de páginas de plugin exibidas como abas fixas antes que as páginas restantes sejam agrupadas em um menu suspenso na última aba." + msgid "Behaviour" msgstr "Comportamento" @@ -9119,6 +9229,18 @@ msgstr "Mostrar predefinições não suportadas" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Exibir predefinições incompatíveis e não suportadas nas listas de impressora e filamento. Essas predefinições não podem ser selecionadas." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Experimental) Usar agentes de impressora em vez de hosts de impressão" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Encaminha os trabalhos de impressão de impressoras que não são Bambu pelos agentes de plugin de impressora em vez do fluxo clássico de envio ao host de impressão.\n" +"Quando desativado, o OrcaSlicer usa o comportamento antigo do host de impressão." + msgid "Experimental Features" msgstr "Recursos Experimentais" @@ -9325,6 +9447,10 @@ msgstr "Vaso espiral" msgid "First layer filament sequence" msgstr "Sequência de filamento da primeira camada" +# AI Translated +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "A lista de filamentos contém filamentos mistos. A sequência de filamentos personalizada não terá efeito." + msgid "By Layer" msgstr "Por Camada" @@ -9380,9 +9506,25 @@ msgstr "Predefinição do Usuário" msgid "Preset Inside Project" msgstr "Predefinição Dentro do Projeto" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Copia para esta predefinição todos os valores herdados da predefinição pai e remove a relação de herança. Predefinições compatíveis apenas com a predefinição pai podem deixar de ser suportadas." + msgid "Detach from parent" msgstr "Separar do pai" +# AI Translated +msgid "Unique preset" +msgstr "Predefinição única" + +# AI Translated +msgid "Parent preset" +msgstr "Predefinição pai" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Esta predefinição não herda de outra predefinição." + msgid "Name is unavailable." msgstr "O nome não está disponível." @@ -9758,7 +9900,7 @@ msgid "Unable to automatically match to suitable filament. Please click to manua msgstr "Não foi possível encontrar automaticamente um filamento adequado. Clique para selecionar manualmente." msgid "Install toolhead enhanced cooling fan to prevent filament softening." -msgstr "Instale um ventilador de resfriamento aprimorado no cabeçote de impressão para evitar o amolecimento do filamento." +msgstr "Instale uma ventoinha de resfriamento aprimorado no cabeçote de impressão para evitar o amolecimento do filamento." msgid "Smooth Cool Plate" msgstr "Placa Fria Lisa" @@ -10102,24 +10244,6 @@ msgstr "Tem certeza de que deseja habilitar esta opção?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Padrões de preenchimento são projetados para lidar com a rotação automaticamente para garantir a impressão adequada e atingir os efeitos pretendidos (Ex. Giroide, Cúbico). Girar o padrão de preenchimento esparso atual pode causar suporte insuficiente. Prossiga com cautela e verifique cuidadosamente se há possíveis problemas de impressão. Tem certeza de que deseja habilitar esta opção?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"A altura da camada é muito pequena.\n" -"Ela será definida como altura mínima da camada\n" -"A altura da camada é muito pequena.\n" -"Ela será definida como altura mínima da camada\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "A altura da camada excede o limite em Configurações da Impressora -> Extrusora -> Limites de altura da camada, isso pode causar problemas de qualidade de impressão." - -msgid "Adjust to the set range automatically?\n" -msgstr "Ajustar automaticamente à faixa definida?\n" - -msgid "Adjust" -msgstr "Ajustar" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Funcionalidade experimental: Retrair e cortar o filamento a uma distância maior durante mudanças de filamento para minimizar a purga. Embora possa reduzir notavelmente a purga, ele também pode elevar o risco de bolhas no bico ou outras complicações de impressão." @@ -10314,6 +10438,9 @@ msgstr "Palavras-chave reservadas encontradas" msgid "Setting Overrides" msgstr "Sobrescrever configurações" +msgid "Retraction when switching material" +msgstr "Retração ao trocar material" + msgid "Basic information" msgstr "Informações básicas" @@ -10382,25 +10509,25 @@ msgid "Cooling for specific layer" msgstr "Resfriamento para camada específica" msgid "Part cooling fan" -msgstr "Ventilador de resfriamento de peças" +msgstr "Ventoinha de resfriamento de peças" msgid "Min fan speed threshold" -msgstr "Limiar de velocidade mínima do ventilador" +msgstr "Limiar de velocidade mínima da ventoinha" msgid "The part cooling fan will run at the minimum fan speed when the estimated layer time is longer than the threshold value. When the layer time is shorter than the threshold, the fan speed will be interpolated between the minimum and maximum fan speed according to layer printing time." -msgstr "O ventilador de resfriamento de peças irá girar na velocidade mínima quando o tempo estimado da camada for mais longo do que o valor de limiar. Quando o tempo da camada for mais curto que o limiar, a velocidade do ventilador é interpolada entre a velocidade mínima e máxima de acordo com o tempo de impressão da camada." +msgstr "A ventoinha de resfriamento de peças irá girar na velocidade mínima quando o tempo estimado da camada for mais longo do que o valor de limiar. Quando o tempo da camada for mais curto que o limiar, a velocidade da ventoinha é interpolada entre a velocidade mínima e máxima de acordo com o tempo de impressão da camada." msgid "Max fan speed threshold" -msgstr "Limiar de velocidade máxima do ventilador" +msgstr "Limiar de velocidade máxima da ventoinha" msgid "The part cooling fan will run at maximum speed when the estimated layer time is shorter than the threshold value." -msgstr "O ventilador de resfriamento de peças irá girar na velocidade máxima quando o tempo estimado da camada for mais curto que o limiar." +msgstr "A ventoinha de resfriamento de peças irá girar na velocidade máxima quando o tempo estimado da camada for mais curto que o limiar." msgid "Auxiliary part cooling fan" -msgstr "Ventilador auxiliar de resfriamento de peças" +msgstr "Ventoinha auxiliar de resfriamento de peças" msgid "Exhaust fan" -msgstr "Ventilador de exaustão" +msgstr "Ventoinha de exaustão" msgid "During print" msgstr "Durante a impressão" @@ -10441,6 +10568,12 @@ msgstr "Perfis de processo compatíveis" msgid "Printable space" msgstr "Espaço de impressão" +msgid "Printer Agent" +msgstr "Agente de Impressora" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Selecione a implementação do agente de rede para comunicação com a impressora. Os agentes disponíveis são registrados na inicialização." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10450,10 +10583,10 @@ msgid "G-code flavor is switched" msgstr "Tipo de G-code está trocado" msgid "Cooling Fan" -msgstr "Ventilador de resfriamento" +msgstr "Ventoinha de resfriamento" msgid "Fan speed-up time" -msgstr "Tempo de aceleração do ventilador" +msgstr "Tempo de aceleração da ventoinha" msgid "Extruder Clearance" msgstr "Folga da extrusora" @@ -10566,9 +10699,6 @@ msgstr "Limites de altura da camada" msgid "Z-Hop" msgstr "Z-Hop" -msgid "Retraction when switching material" -msgstr "Retração ao trocar material" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -10682,11 +10812,11 @@ msgstr "%s: %s" msgid "No modifications need to be copied." msgstr "Nenhuma modificação precisa ser copiada." -msgid "Copy paramters" +msgid "Copy parameters" msgstr "Copiar parâmetros" #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "Modificar parâmetros de %s" #, c-format, boost-format @@ -11197,27 +11327,6 @@ msgstr "Volumes de purga para troca de filamento" msgid "Please choose the filament colour" msgstr "Escolha a cor do filamento" -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "A visualização ao vivo nativa do Wayland requer o receptor de vídeo GTK do GStreamer. Instale o plugin gtksink para GStreamer e reinicie o OrcaSlicer." - -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "Falha ao inicializar o receptor de vídeo nativo do Wayland GStreamer. Verifique a instalação do plugin GStreamer GTK." - -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "O Windows Media Player é necessário para esta tarefa! Você quer habilitar o 'Windows Media Player' para seu sistema operacional?" - -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "BambuSource não foi registrado corretamente para reprodução de mídia! Pressione Sim para registrá-lo novamente. Você será promovido duas vezes" - -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "Componente BambuSource registrado para reprodução de mídia não encontrado! Por favor, reinstale o OrcaSlicer ou procure ajuda da comunidade." - -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "Usando um BambuSource de uma instalação diferente, a reprodução de vídeo pode não funcionar corretamente! Pressione Sim para consertar." - -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "Seu sistema não possui codecs H.264 para o GStreamer, que são necessários para reproduzir vídeos. (Tente instalar os pacotes gstreamer1.0-plugins-bad ou gstreamer1.0-libav e depois reinicie o OrcaSlicer?)" - msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "O agente na nuvem não está disponível. Reinicie o OrcaSlicer e tente novamente." @@ -11770,7 +11879,6 @@ msgstr "Erro de agrupamento: " msgid " can not be placed in the " msgstr " não pode ser colocado na " -# AI Translated msgid "Group error in manual mode. Please check nozzle count or regroup." msgstr "Erro de agrupamento no modo manual. Por favor, verifique o número de bicos ou reagrupe." @@ -11900,6 +12008,10 @@ msgstr " está muito perto de uma área de exclusão, e colisões vão ocorrer.\ msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " está muito perto da área de detecção de aglomeração, e ocorrerão colisões.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " está parcialmente fora da área imprimível, e não pode ser impresso.\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "As temperaturas dos bicos selecionadas são incompatíveis. A temperatura do bico de cada filamento deve estar dentro da faixa de temperatura recomendada para os demais filamentos. Caso contrário, pode ocorrer entupimento do bico ou danos à impressora." @@ -11912,6 +12024,10 @@ msgstr "Se ainda assim desejar imprimir, você pode ativar a opção em Preferê msgid "No extrusions under current settings." msgstr "Nenhuma extrusão com as configurações atuais." +# AI Translated +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "Um filamento misto com gradiente está em uso, mas 'Subcamada de cor mista' está desativado. O gradiente não será impresso." + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "O modo suave do timelapse não é suportado quando a sequência \"por objeto\" está ativada." @@ -11948,6 +12064,10 @@ msgstr "Você pode querer reduzir o tamanho do seu modelo ou alterar as configur msgid "Variable layer height is not supported with Organic supports." msgstr "A altura de camada variável não é suportada com suportes Orgânicos." +# AI Translated +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "O filamento da torre de purga não pode ser um filamento misto." + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Diferentes diâmetros de bico e diferentes diâmetros de filamento podem não funcionar bem quando a torre de purga estiver habilitada. É muito experimental, então prossiga com cautela." @@ -12096,7 +12216,6 @@ msgstr "A contração de filamento não será usada porque a contração dos fil msgid "Generating skirt & brim" msgstr "Gerando saia e borda" -# AI Translated msgid "" "Per-object skirts cannot fit between the objects in By object print sequence.\n" "\n" @@ -12214,9 +12333,6 @@ msgstr "Usar 3MF em vez de G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Ative esta opção se a impressora aceitar um arquivo 3MF como trabalho de impressão. Quando ativada, o OrcaSlicer envia o arquivo fatiado como .gcode.3mf, em vez de um arquivo .gcode comum." -msgid "Printer Agent" -msgstr "Agente de Impressora" - msgid "Select the network agent implementation for printer communication." msgstr "Selecione a implementação do agente de rede para comunicação com a impressora." @@ -12277,9 +12393,8 @@ msgstr "API Key" msgid "HTTP digest" msgstr "Digest HTTP" -# AI Translated msgid "Configuration for the plugin capabilities this preset uses, overriding the global Capabilities configuration. Stored as a raw JSON array and edited through the dialog behind the button, never typed in directly." -msgstr "Configuração dos recursos de plugin que esta predefinição usa, substituindo a configuração global de Recursos. Armazenada como um array JSON bruto e editada por meio da caixa de diálogo atrás do botão, nunca digitada diretamente." +msgstr "Configuração das capacidades de plugin que esta predefinição usa, substituindo a configuração global de Capacidades. Armazenada como um array JSON bruto e editada por meio da caixa de diálogo atrás do botão, nunca digitada diretamente." msgid "Avoid crossing walls" msgstr "Evitar atravessar paredes" @@ -12299,7 +12414,7 @@ msgstr "mm ou %" msgid "Other layers" msgstr "Outras camadas" -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." msgstr "Essa é a temperatura da mesa para camadas exceto a inicial. O valor 0 significa que o filamento não suporta a impressão na Placa Fria SuperTack." msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." @@ -12420,26 +12535,26 @@ msgid "Force cooling for overhangs and bridges" msgstr "Resfriamento forçado para saliências e pontes" msgid "Enable this option to allow adjustment of the part cooling fan speed for specifically for overhangs, internal and external bridges. Setting the fan speed specifically for these features can improve overall print quality and reduce warping." -msgstr "Habilite esta opção para permitir o ajuste da velocidade do ventilador de resfriamento de peças especificamente para saliências, pontes internas e externas. Definir a velocidade do ventilador especificamente para esses recursos pode melhorar a qualidade geral da impressão e reduzir a deformação." +msgstr "Habilite esta opção para permitir o ajuste da velocidade da ventoinha de resfriamento de peças especificamente para saliências, pontes internas e externas. Definir a velocidade da ventoinha especificamente para esses recursos pode melhorar a qualidade geral da impressão e reduzir a deformação." msgid "Overhangs and external bridges fan speed" -msgstr "Velocidade do ventilador para saliências e pontes externas" +msgstr "Velocidade da ventoinha para saliências e pontes externas" msgid "" "Use this part cooling fan speed when printing bridges or overhang walls with an overhang threshold that exceeds the value set in the 'Overhangs cooling threshold' parameter above. Increasing the cooling specifically for overhangs and bridges can improve the overall print quality of these features.\n" "\n" "Please note, this fan speed is clamped on the lower end by the minimum fan speed threshold set above. It is also adjusted upwards up to the maximum fan speed threshold when the minimum layer time threshold is not met." msgstr "" -"Use esta parte da velocidade do ventilador de resfriamento ao imprimir pontes ou paredes salientes com um limite de saliência que exceda o valor definido no parâmetro 'Limiar de resfriamento de saliências' acima. Aumentar o resfriamento especificamente para saliências e pontes pode melhorar a qualidade geral de impressão desses recursos.\n" +"Use esta parte da velocidade da ventoinha de resfriamento ao imprimir pontes ou paredes salientes com um limite de saliência que exceda o valor definido no parâmetro 'Limiar de resfriamento de saliências' acima. Aumentar o resfriamento especificamente para saliências e pontes pode melhorar a qualidade geral de impressão desses recursos.\n" "\n" -"Observe que esta velocidade do ventilador é fixada na extremidade inferior pelo limiar mínimo de velocidade do ventilador definido acima. Ela também é ajustada para cima até o limiar máximo de velocidade do ventilador quando o limiar mínimo de tempo da camada não é atingido." +"Observe que esta velocidade da ventoinha é fixada na extremidade inferior pelo limiar mínimo de velocidade da ventoinha definido acima. Ela também é ajustada para cima até o limiar máximo de velocidade da ventoinha quando o limiar mínimo de tempo da camada não é atingido." msgid "Overhang cooling activation threshold" msgstr "Limiar de ativação de resfriamento de saliência" #, no-c-format, no-boost-format msgid "When the overhang exceeds this specified threshold, force the cooling fan to run at the 'Overhang Fan Speed' set below. This threshold is expressed as a percentage, indicating the portion of each line's width that is unsupported by the layer beneath it. Setting this value to 0% forces the cooling fan to run for all outer walls, regardless of the overhang degree." -msgstr "Quando a saliência excede esse limiar especificado, força o ventilador de resfriamento a funcionar na 'Velocidade da ventoinha de saliência' definida abaixo. Esse limiar é expresso como uma porcentagem, indicando a parte da largura de cada linha que não é suportada pela camada abaixo dela. Definir esse valor como 0% força o ventilador de resfriamento a funcionar para todas as paredes externas, independentemente do grau de saliência." +msgstr "Quando a saliência excede esse limiar especificado, força a ventoinha de resfriamento a funcionar na 'Velocidade da ventoinha de saliência' definida abaixo. Esse limiar é expresso como uma porcentagem, indicando a parte da largura de cada linha que não é suportada pela camada abaixo dela. Definir esse valor como 0% força a ventoinha de resfriamento a funcionar para todas as paredes externas, independentemente do grau de saliência." msgid "External bridge infill direction" msgstr "Direção de preenchimento de ponte externa" @@ -12897,9 +13012,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Velocidade de pontes internas. Se o valor for expresso como uma porcentagem, ele será calculado com base na bridge_speed. O valor padrão é 150%." -msgid "Brim width" -msgstr "Largura da borda" - msgid "This is the distance from the model to the outermost brim line." msgstr "Essa é a distância do modelo até a linha da borda mais externa." @@ -12979,6 +13091,14 @@ msgstr "" "A geometria será decimada antes de detectar ângulos agudos. Este parâmetro indica o comprimento mínimo da divergência para a decimação.\n" "0 para desativar." +# AI Translated +msgid "Brim ears outer only" +msgstr "Orelhas da borda apenas externas" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Gera orelhas de rato apenas no contorno externo do modelo, excluindo furos e seções fechadas." + msgid "upward compatible machine" msgstr "uáquina compatível ascendente" @@ -13026,11 +13146,9 @@ msgstr "" msgid "As object list" msgstr "Como lista de objetos" -# AI Translated msgid "Best of all (shortest path)" msgstr "Melhor de todas (caminho mais curto)" -# AI Translated msgid "Snake" msgstr "Serpentina" @@ -13038,7 +13156,7 @@ msgid "Slow printing down for better layer cooling" msgstr "Diminuir a velocidade de impressão para melhor resfriamento de camada" msgid "Enable this option to slow printing speed down to ensure that the final layer time is not shorter than the layer time threshold in \"Max fan speed threshold\", so that the layer can be cooled for a longer time. This can improve the quality for small details." -msgstr "Ative esta opção para diminuir a velocidade de impressão para que o tempo da camada final não seja menor do que o limiar de tempo da camada em \"Limiar de velocidade máxima do ventilador\", para que a camada possa ser resfriada um tempo mais longo. Isso pode melhorar a qualidade para detalhes pequenos." +msgstr "Ative esta opção para diminuir a velocidade de impressão para que o tempo da camada final não seja menor do que o limiar de tempo da camada em \"Limiar de velocidade máxima da ventoinha\", para que a camada possa ser resfriada um tempo mais longo. Isso pode melhorar a qualidade para detalhes pequenos." msgid "Normal printing" msgstr "Impressão normal" @@ -13093,16 +13211,16 @@ msgid "Enable this to override the fan speed set in custom G-code after print co msgstr "Habilite para substituir a velocidade da ventoinha definida no G-code personalizado após a conclusão da impressão." msgid "Speed of exhaust fan during printing. This speed will override the speed in filament custom G-code." -msgstr "Velocidade do ventilador de exaustão durante a impressão. Esta velocidade substituirá a velocidade no G-code personalizado do filamento." +msgstr "Velocidade da ventoinha de exaustão durante a impressão. Esta velocidade substituirá a velocidade no G-code personalizado do filamento." msgid "Speed of exhaust fan after printing completes." -msgstr "Velocidade do ventilador de exaustão após a conclusão da impressão." +msgstr "Velocidade da ventoinha de exaustão após a conclusão da impressão." msgid "No cooling for the first" msgstr "Sem resfriamento para as primeiras" msgid "Turn off all cooling fans for the first few layers. This can be used to improve build plate adhesion." -msgstr "Desligar todos os ventiladores de resfriamento para as primeiras camadas. Isso pode ser usado para obter uma melhor adesão à placa de impressão." +msgstr "Desligar todos as ventoinhas de resfriamento para as primeiras camadas. Isso pode ser usado para obter uma melhor adesão à placa de impressão." msgid "Don't support bridges" msgstr "Não suportar pontes" @@ -13278,11 +13396,9 @@ msgstr "Densidade da superfície superior" msgid "Density of top surface layer. A value of 100% creates a fully solid, smooth top layer. Reducing this value results in a textured top surface, according to the chosen top surface pattern. A value of 0% will result in only the walls on the top layer being created. Intended for aesthetic or functional purposes, not to fix issues such as over-extrusion." msgstr "Densidade da camada superior. Um valor de 100% cria uma camada superior totalmente sólida e lisa. Reduzir esse valor resulta em uma superfície superior texturizada, de acordo com o padrão de superfície superior escolhido. Um valor de 0% resultará na criação apenas das paredes da camada superior. Destinado a fins estéticos ou funcionais, não para corrigir problemas como extrusão excessiva." -# AI Translated msgid "Top surface expansion" msgstr "Expansão da superfície superior" -# AI Translated msgid "" "Expands the top surfaces by this distance to connect distinct top surfaces and fill gaps.\n" "Useful for cases where the top surface is interrupted by a raised feature, such as text on a plane. Expanding it removes the holes beneath these features and creates a continuous path with a better finish for printing on top. The expansion is applied to the original top surface, before any other processing such as bridging or overhang detection." @@ -13290,11 +13406,9 @@ msgstr "" "Expande as superfícies superiores por esta distância para conectar superfícies superiores distintas e preencher lacunas.\n" "Útil para casos em que a superfície superior é interrompida por um recurso elevado, como um texto sobre um plano. Expandi-la remove os buracos sob esses recursos e cria um caminho contínuo com melhor acabamento para imprimir por cima. A expansão é aplicada à superfície superior original, antes de qualquer outro processamento, como detecção de ponte ou de saliência." -# AI Translated msgid "Top expansion wall margin" msgstr "Margem de parede da expansão superior" -# AI Translated msgid "" "Using “Top surface expansion” may cause a surface that did not previously touch the model's outer walls to now do so.\n" "This can cause contraction marks (such as the hull line) on the outer walls.\n" @@ -13304,11 +13418,9 @@ msgstr "" "Isso pode causar marcas de contração (como a linha do casco) nas paredes externas.\n" "Ao adicionar uma pequena margem, essa contração não ocorrerá diretamente nas paredes, evitando assim uma marca visível." -# AI Translated msgid "Top expansion direction" msgstr "Direção da expansão superior" -# AI Translated msgid "" "Direction in which the top surface expansion grows.\n" " - Inward grows into the holes and gaps left by features rising from the middle of a top surface.\n" @@ -13335,11 +13447,9 @@ msgstr "Padrão de superfície inferior" msgid "This is the line pattern of bottom surface infill, not including bridge infill." msgstr "Este é o padrão de linha do preenchimento da superfície inferior, não incluindo o preenchimento de ponte." -# AI Translated msgid "Bottom surface density" msgstr "Densidade da superfície inferior" -# AI Translated msgid "" "Density of the bottom surface layer. Intended for aesthetic or functional purposes, not to fix issues such as over-extrusion.\n" "WARNING: Lowering this value may negatively affect bed adhesion." @@ -13347,31 +13457,27 @@ msgstr "" "Densidade da camada da superfície inferior. Destinada a fins estéticos ou funcionais, não a corrigir problemas como sobre-extrusão.\n" "AVISO: reduzir este valor pode afetar negativamente a aderência à mesa." -# AI Translated msgid "Top surface fill order" msgstr "Ordem de preenchimento da superfície superior" -# AI Translated msgid "" "Direction in which top surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed towards the edge where it is least visible. Inward starts at the edge and ends with the tight curves at the center.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Direção em que as superfícies superiores são preenchidas ao usar um padrão baseado no centro (Concêntrico, Cordas de Arquimedes, Espiral Octograma).\n" +"Direção em que as superfícies superiores são preenchidas ao usar um padrão baseado no centro (Concêntrico, Cordas de Arquimedes, Espiral de Octograma).\n" "Para fora começa no centro da superfície, de modo que qualquer excesso de material seja empurrado em direção à borda, onde é menos visível. Para dentro começa na borda e termina com as curvas fechadas no centro.\n" "O padrão usa a ordenação de caminho mais curto, que pode seguir em qualquer direção." -# AI Translated msgid "Bottom surface fill order" msgstr "Ordem de preenchimento da superfície inferior" -# AI Translated msgid "" "Direction in which bottom surfaces are filled when using a center-based pattern (Concentric, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer adhesion on build plates where the tight curves at the center may not stick. Outward starts at the center, pushing any excess material towards the edge.\n" "Default uses shortest-path ordering, which may run in either direction." msgstr "" -"Direção em que as superfícies inferiores são preenchidas ao usar um padrão baseado no centro (Concêntrico, Cordas de Arquimedes, Espiral Octograma).\n" +"Direção em que as superfícies inferiores são preenchidas ao usar um padrão baseado no centro (Concêntrico, Cordas de Arquimedes, Espiral de Octograma).\n" "Para dentro começa cada superfície com as curvas externas mais largas, o que melhora a aderência da primeira camada em mesas onde as curvas fechadas no centro podem não aderir. Para fora começa no centro, empurrando qualquer excesso de material em direção à borda.\n" "O padrão usa a ordenação de caminho mais curto, que pode seguir em qualquer direção." @@ -13399,19 +13505,15 @@ msgstr "Limiar de pequenos perímetros" msgid "This sets the threshold for small perimeter length. Default threshold is 0mm." msgstr "Isso define o limiar para o comprimento do perímetro pequeno. O limiar padrão é 0 mm." -# AI Translated msgid "Small support perimeters" msgstr "Pequenos perímetros de suporte" -# AI Translated msgid "Same as \"Small perimeters\", but for supports. This separate setting will affect the speed of support for areas <= `small_support_perimeter_threshold`. If expressed as a percentage (for example: 80%), it will be calculated on the support or support interface speed setting above. Set to zero for auto." msgstr "Igual a \"Pequenos perímetros\", mas para suportes. Esta configuração separada afetará a velocidade do suporte para áreas <= `small_support_perimeter_threshold`. Se expressa como uma porcentagem (por exemplo: 80%), será calculada com base na configuração de velocidade de suporte ou de interface de suporte acima. Defina como zero para automático." -# AI Translated msgid "Small support perimeters threshold" -msgstr "Limite de pequenos perímetros de suporte" +msgstr "Limiar de pequenos perímetros de suporte" -# AI Translated msgid "This sets the threshold for small support perimeter length. The default threshold is 0mm." msgstr "Isto define o limite para o comprimento de pequenos perímetros de suporte. O limite padrão é 0mm." @@ -13603,7 +13705,6 @@ msgstr "" msgid "Enable adaptive pressure advance within features (beta)" msgstr "Habilitar pressure advance adaptativo nos recursos (beta)" -# AI Translated msgid "" "Enable adaptive PA whenever there are flow changes in a feature, such as line width changes in a corner or overhangs.\n" "\n" @@ -13635,10 +13736,10 @@ msgid "Default line width if other line widths are set to 0. If expressed as a % msgstr "Largura de linha padrão se outras larguras de linha estiverem definidas como 0. Se expresso como %, será calculado sobre o diâmetro do bico." msgid "Keep fan always on" -msgstr "Manter o ventilador sempre ligado" +msgstr "Manter a ventoinha sempre ligado" msgid "Enabling this setting means that part cooling fan will never stop entirely and will instead run at least at minimum speed to reduce the frequency of starting and stopping." -msgstr "Habilitar esta configuração significa que o ventilador de resfriamento da peça nunca será desligado completamente e funcionará pelo menos na velocidade mínima para reduzir a frequência de inícios e paradas." +msgstr "Habilitar esta configuração significa que a ventoinha de resfriamento da peça nunca será desligado completamente e funcionará pelo menos na velocidade mínima para reduzir a frequência de inícios e paradas." msgid "Don't slow down outer walls" msgstr "Não desacelerar as paredes externas" @@ -13658,8 +13759,10 @@ msgid "Layer time" msgstr "Tempo da camada" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." -msgstr "O ventilador de resfriamento de peças será ativado para camadas cujo tempo estimado seja mais curto que esse valor. A velocidade do ventilador é interpolada entre as velocidades mínima e máxima do ventilador de acordo com o tempo de impressão da camada." +msgstr "A ventoinha de resfriamento de peças será ativado para camadas cujo tempo estimado seja mais curto que esse valor. A velocidade da ventoinha é interpolada entre as velocidades mínima e máxima da ventoinha de acordo com o tempo de impressão da camada." +# AI Translated +msgctxt "second" msgid "s" msgstr "s" @@ -13706,7 +13809,6 @@ msgstr "Temperatura de purga" msgid "Temperature when flushing filament. 0 indicates the upper bound of the recommended nozzle temperature range." msgstr "Temperatura ao purgar filamento. 0 indica o limite superior da faixa de temperatura recomendada para o bico." -# AI Translated msgid "Flush temperature used in fast purge mode." msgstr "Temperatura de purga usada no modo de purga rápida." @@ -13966,17 +14068,71 @@ msgstr "Material de suporte" msgid "Support material is commonly used to print supports and support interfaces." msgstr "O material de suporte é comumente usado para imprimir suportes e interfaces de suporte." +# AI Translated +msgid "Is mixed filament" +msgstr "É filamento misto" + +# AI Translated +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "Define se este slot de filamento é um filamento misto composto por vários filamentos físicos" + +# AI Translated +msgid "Mixed filament components" +msgstr "Componentes do filamento misto" + +# AI Translated +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "Índices (começando em 1) dos filamentos componentes, separados por vírgulas; ex.: \"1,3\"" + +# AI Translated +msgid "Mixed filament sublayer ratios" +msgstr "Proporções de subcamada do filamento misto" + +# AI Translated +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "Valores de proporção separados por vírgulas cuja soma seja 1.0; ex.: \"0.7,0.3\"" + +# AI Translated +msgid "Mixed filament gradient" +msgstr "Gradiente do filamento misto" + +# AI Translated +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "Ativa o modo de gradiente na direção Z para as subcamadas do filamento misto. Quando ativado, as proporções das subcamadas variam linearmente ao longo das camadas." + +# AI Translated +msgid "Mixed filament gradient range" +msgstr "Faixa do gradiente do filamento misto" + +# AI Translated +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "Proporções inicial e final do primeiro componente no modo de gradiente. Par separado por vírgula; ex.: \"0.10,0.90\" significa de 10% a 90%." + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "Curva do gradiente do filamento misto" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "Curva personalizada opcional, no estilo do Photoshop, que mapeia o progresso em Z para a proporção do primeiro componente. Codificada como pontos de controle separados por barras verticais, no formato \"x,y\" (legado) ou \"x,y,m_in,m_out\" quando é necessário substituir a tangente (um valor vazio ou \"nan\" usa o padrão PCHIP). x está em [0,1]; y é limitado à faixa de proporção configurada; ex.: \"0,0.15|0.5,0.50|1,0.85\". Quando vazio, o gradient_range linear é usado." + +# AI Translated +msgid "Mixed filament per-part gradient" +msgstr "Gradiente por peça do filamento misto" + +# AI Translated +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "Quando o modo de gradiente está ativado, aplica o gradiente a cada peça de uma montagem de forma independente, em vez de tratar toda a montagem como uma única faixa Z." + msgid "Filament printable" msgstr "Filamento imprimível" msgid "The filament is printable in extruder." msgstr "O filamento é imprimível na extrusora." -# AI Translated msgid "Filament-extruder compatibility" msgstr "Compatibilidade filamento-extrusora" -# AI Translated msgid "A single 32-bit int encoding the compatibility level of a filament across all extruders (up to 10). Every 3 bits represent one extruder (bits [3*i, 3*i+2] for extruder i). 0: printable, 1: error, 2: critical warning, 3: warning, 4-7: reserved." msgstr "Um único inteiro de 32 bits que codifica o nível de compatibilidade de um filamento em todas as extrusoras (até 10). Cada 3 bits representam uma extrusora (bits [3*i, 3*i+2] para a extrusora i). 0: imprimível, 1: erro, 2: aviso crítico, 3: aviso, 4-7: reservado." @@ -14016,11 +14172,9 @@ msgstr "Direção do preenchimento sólido" msgid "Angle for solid infill pattern, which controls the start or main direction of line." msgstr "Ângulo para padrão de preenchimento sólido, que controla a direção inicial ou principal da linha." -# AI Translated msgid "Top layer direction" msgstr "Direção da camada superior" -# AI Translated msgid "" "Fixed angle for the top solid infill and ironing lines.\n" "Set to -1 to follow the default solid infill direction." @@ -14028,11 +14182,9 @@ msgstr "" "Ângulo fixo para o preenchimento sólido superior e as linhas de alisamento.\n" "Defina como -1 para seguir a direção padrão do preenchimento sólido." -# AI Translated msgid "Bottom layer direction" msgstr "Direção da camada inferior" -# AI Translated msgid "" "Fixed angle for the bottom solid infill lines.\n" "Set to -1 to follow the default solid infill direction." @@ -14047,11 +14199,9 @@ msgstr "Densidade do preenchimento esparso" msgid "Density of internal sparse infill, 100% turns all sparse infill into solid infill and internal solid infill pattern will be used." msgstr "Densidade do preenchimento esparso interno, 100% transforma todo o preenchimento esparso em preenchimento sólido e será usado o padrão de preenchimento sólido interno." -# AI Translated msgid "Align directions to model" msgstr "Alinhar direções ao modelo" -# AI Translated msgid "" "Aligns infill, bridge, ironing, and top/bottom surface directions to follow the model's orientation on the build plate.\n" "When enabled, these directions rotate together with the model so the printed features keep their intended orientation relative to the part, preserving optimal strength and surface characteristics regardless of how the model is placed." @@ -14071,11 +14221,9 @@ msgstr "Multilinhas de Preenchimento" msgid "Using multiple lines for the infill pattern, if supported by infill pattern." msgstr "Usar múltiplas linhas para o padrão de preenchimento, se suportado pelo padrão de preenchimento." -# AI Translated msgid "Z-buckling bias optimization (experimental)" msgstr "Otimização de tendência à flambagem em Z (experimental)" -# AI Translated #, no-c-format, no-boost-format msgid "Tightens the gyroid wave along the Z (vertical) axis at low infill density to shorten the effective vertical column length and improve Z-axis compression buckling resistance. Filament use is preserved. No effect at ~30% sparse infill density and above. Only applies when Sparse infill pattern is set to Gyroid." msgstr "Aperta a onda giroide ao longo do eixo Z (vertical) em baixa densidade de preenchimento para encurtar o comprimento efetivo da coluna vertical e melhorar a resistência à flambagem por compressão no eixo Z. O uso de filamento é preservado. Sem efeito em densidade de preenchimento esparso de ~30% ou mais. Aplica-se apenas quando o padrão de Preenchimento esparso está definido como Giroide." @@ -14143,6 +14291,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Giroide" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Fator de suavização do preenchimento esparso" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Controla o quanto os cantos do preenchimento esparso são arredondados. 0% mantém o trajeto original com cantos vivos, enquanto 100% produz as maiores curvas possíveis entre linhas de preenchimento adjacentes." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Esta é a aceleração do preenchimento da superfície superior. Usar um valor menor pode melhorar a qualidade da superfície superior." @@ -14198,13 +14354,12 @@ msgstr "Jerk para primeira camada." msgid "Jerk for travel." msgstr "Jerk para deslocamento." -# AI Translated msgid "" "Travel jerk of first layer.\n" "The percentage value is relative to Travel Jerk." msgstr "" "Jerk de deslocamento da primeira camada.\n" -"O valor percentual é relativo ao Jerk de deslocamento." +"O valor percentual é relativo ao Jerk de Deslocamento." msgid "Line width of the first layer. If expressed as a %, it will be computed over the nozzle diameter." msgstr "Largura da linha da primeira camada. Se expresso como uma %, será calculado sobre o diâmetro do bico." @@ -14243,10 +14398,10 @@ msgid "Nozzle temperature for printing the first layer with this filament" msgstr "Temperatura do bico para imprimir a primeira camada com este filamento" msgid "Full fan speed at layer" -msgstr "Velocidade total do ventilador na camada" +msgstr "Velocidade total da ventoinha na camada" msgid "Fan speed will be ramped up linearly from zero at layer \"close_fan_the_first_x_layers\" to maximum at layer \"full_fan_speed_layer\". \"full_fan_speed_layer\" will be ignored if lower than \"close_fan_the_first_x_layers\", in which case the fan will be running at maximum allowed speed at layer \"close_fan_the_first_x_layers\" + 1." -msgstr "A velocidade do ventilador aumentará linearmente de zero na camada \"close_fan_the_first_x_layers\" para o máximo na camada \"full_fan_speed_layer\". \"full_fan_speed_layer\" será ignorado se for menor que \"close_fan_the_first_x_layers\", caso em que o ventilador funcionará na velocidade máxima permitida na camada \"close_fan_the_first_x_layers\" + 1." +msgstr "A velocidade da ventoinha aumentará linearmente de zero na camada \"close_fan_the_first_x_layers\" para o máximo na camada \"full_fan_speed_layer\". \"full_fan_speed_layer\" será ignorado se for menor que \"close_fan_the_first_x_layers\", caso em que a ventoinha funcionará na velocidade máxima permitida na camada \"close_fan_the_first_x_layers\" + 1." msgid "layer" msgstr "camada" @@ -14254,7 +14409,6 @@ msgstr "camada" msgid "First layer fan speed" msgstr "Velocidade da ventoinha na primeira camada" -# AI Translated msgid "" "Sets an exact fan speed for the first layer, overriding all other cooling settings. Useful for protecting 3D-printed toolhead parts (e.g. Voron-style ABS/ASA ducts) from a hot bed. A small amount of airflow cools the ducts down, without using full cooling that may in certain conditions hurt first-layer adhesion.\n" "From the second layer onwards, normal cooling resumes.\n" @@ -14262,44 +14416,44 @@ msgid "" "Only available when \"No cooling for the first\" is 0.\n" "Set to -1 to disable it." msgstr "" -"Define uma velocidade exata do ventilador para a primeira camada, substituindo todas as outras configurações de resfriamento. Útil para proteger peças impressas em 3D da cabeça da ferramenta (por exemplo, dutos ABS/ASA no estilo Voron) de uma mesa quente. Uma pequena quantidade de fluxo de ar resfria os dutos, sem usar o resfriamento total que pode, em certas condições, prejudicar a aderência da primeira camada.\n" +"Define uma velocidade exata da ventoinha para a primeira camada, substituindo todas as outras configurações de resfriamento. Útil para proteger peças impressas em 3D da cabeça da ferramenta (por exemplo, dutos ABS/ASA no estilo Voron) de uma mesa aquecida. Uma pequena quantidade de fluxo de ar resfria os dutos, sem usar o resfriamento total que pode, em certas condições, prejudicar a aderência da primeira camada.\n" "A partir da segunda camada, o resfriamento normal é retomado.\n" -"Se \"Velocidade total do ventilador na camada\" também estiver definida, o ventilador aumenta suavemente deste valor na primeira camada até o seu alvo na camada escolhida.\n" +"Se \"Velocidade total da ventoinha na camada\" também estiver definida, a ventoinha aumenta suavemente deste valor na primeira camada até o seu alvo na camada escolhida.\n" "Disponível apenas quando \"Sem resfriamento nas primeiras\" é 0.\n" "Defina como -1 para desativá-la." msgid "Support interface fan speed" -msgstr "Velocidade do ventilador para interface de suporte" +msgstr "Velocidade da ventoinha para interface de suporte" msgid "" "This part cooling fan speed is applied when printing support interfaces. Setting this parameter to a higher than regular speed reduces the layer binding strength between supports and the supported part, making them easier to separate.\n" "Set to -1 to disable it.\n" "This setting is overridden by disable_fan_first_layers." msgstr "" -"Esta velocidade do ventilador de resfriamento de peças é aplicada ao imprimir interfaces de suporte. Definir este parâmetro para uma velocidade maior que a normal reduz a força de adesão de camada entre os suportes e a peça suportada, tornando-os mais fáceis de separar.\n" +"Esta velocidade da ventoinha de resfriamento de peças é aplicada ao imprimir interfaces de suporte. Definir este parâmetro para uma velocidade maior que a normal reduz a força de adesão de camada entre os suportes e a peça suportada, tornando-os mais fáceis de separar.\n" "Defina como -1 para desabilitá-lo.\n" "Esta configuração é substituída por disable_fan_first_layers." msgid "Internal bridges fan speed" -msgstr "Velocidade do ventilador para pontes internas" +msgstr "Velocidade da ventoinha para pontes internas" msgid "" "The part cooling fan speed used for all internal bridges. Set to -1 to use the overhang fan speed settings instead.\n" "\n" "Reducing the internal bridges fan speed, compared to your regular fan speed, can help reduce part warping due to excessive cooling applied over a large surface for a prolonged period of time." msgstr "" -"A velocidade do ventilador de resfriamento de peças usada para todas as pontes internas. Defina como -1 para usar as configurações de velocidade do ventilador de sobreposição.\n" +"A velocidade da ventoinha de resfriamento de peças usada para todas as pontes internas. Defina como -1 para usar as configurações de velocidade da ventoinha de sobreposição.\n" "\n" -"Reduzir a velocidade do ventilador das pontes internas, em comparação com a velocidade normal do ventilador, pode ajudar a reduzir a deformação das peças devido ao resfriamento excessivo aplicado sobre uma grande superfície por um período prolongado de tempo." +"Reduzir a velocidade da ventoinha das pontes internas, em comparação com a velocidade normal da ventoinha, pode ajudar a reduzir a deformação das peças devido ao resfriamento excessivo aplicado sobre uma grande superfície por um período prolongado de tempo." msgid "Ironing fan speed" -msgstr "Velocidade do ventilador para alisamento" +msgstr "Velocidade da ventoinha para alisamento" msgid "" "This part cooling fan speed is applied when ironing. Setting this parameter to a lower than regular speed reduces possible nozzle clogging due to the low volumetric flow rate, making the interface smoother.\n" "Set to -1 to disable it." msgstr "" -"Esta velocidade do ventilador de resfriamento de peças é aplicada durante o alisamento. Definir este parâmetro para uma velocidade menor que a normal reduz a possibilidade de entupimento do bico devido ao baixa taxa de fluxo volumétrico, tornando a interface mais suave.\n" +"Esta velocidade da ventoinha de resfriamento de peças é aplicada durante o alisamento. Definir este parâmetro para uma velocidade menor que a normal reduz a possibilidade de entupimento do bico devido ao baixa taxa de fluxo volumétrico, tornando a interface mais suave.\n" "Defina como -1 para desabilitá-lo." msgid "Ironing flow" @@ -14584,7 +14738,7 @@ msgid "Best auto arranging position in range [0,1] w.r.t. bed shape." msgstr "Melhor posição de arranjo automático na faixa [0,1] em relação ao formato da mesa." msgid "Enable this option if machine has auxiliary part cooling fan. G-code command: M106 P2 S(0-255)." -msgstr "Habilitar esta opção se a máquina tiver ventilador auxiliar de resfriamento de peças. Comando G-code: M106 P2 S(0-255)." +msgstr "Habilitar esta opção se a máquina tiver ventoinha auxiliar de resfriamento de peças. Comando G-code: M106 P2 S(0-255)." msgid "Fan direction" msgstr "Direção da ventoinha" @@ -14592,7 +14746,6 @@ msgstr "Direção da ventoinha" msgid "Cooling fan direction of the printer" msgstr "Direção da ventoinha de resfriamento da impressora" -# AI Translated msgid "Both" msgstr "Ambos" @@ -14602,9 +14755,9 @@ msgid "" "It won't move fan commands into the start G-code if the 'only custom start G-code' is activated.\n" "Use 0 to deactivate." msgstr "" -"Ativar o ventilador este número de segundos antes do seu tempo de início alvo (você pode usar frações de segundos). Ele assume aceleração infinita para esta estimativa de tempo e levará em conta apenas os movimentos G1 e G0 (o ajuste de arco não é suportado).\n" -"Não moverá G-code de comandos do ventilador personalizados (eles funcionam como uma espécie de 'barreira').\n" -"Não moverá comandos do ventilador para o início do G-code se 'apenas G-code de início personalizado' estiver ativo.\n" +"Ativar a ventoinha este número de segundos antes do seu tempo de início alvo (você pode usar frações de segundos). Ele assume aceleração infinita para esta estimativa de tempo e levará em conta apenas os movimentos G1 e G0 (o ajuste de arco não é suportado).\n" +"Não moverá G-code de comandos da ventoinha personalizados (eles funcionam como uma espécie de 'barreira').\n" +"Não moverá comandos da ventoinha para o início do G-code se 'apenas G-code de início personalizado' estiver ativo.\n" "Use 0 para desativar." msgid "Only overhangs" @@ -14614,15 +14767,15 @@ msgid "Will only take into account the delay for the cooling of overhangs." msgstr "Levará em conta apenas o atraso para o resfriamento das saliências." msgid "Fan kick-start time" -msgstr "Tempo de inicialização do ventilador" +msgstr "Tempo de inicialização da ventoinha" msgid "" "Emit a max fan speed command for this amount of seconds before reducing to target speed to kick-start the cooling fan.\n" "This is useful for fans where a low PWM/power may be insufficient to get the fan started spinning from a stop, or to get the fan up to speed faster.\n" "Set to 0 to deactivate." msgstr "" -"Emita um comando de velocidade máxima do ventilador por esta quantidade de segundos antes de reduzir para a velocidade alvo para iniciar o ventilador de resfriamento.\n" -"Isto é útil para ventiladores onde um baixo PWM/potência pode ser insuficiente para fazer o ventilador começar a girar a partir de uma parada, ou para fazer o ventilador alcançar a velocidade mais rapidamente.\n" +"Emita um comando de velocidade máxima da ventoinha por esta quantidade de segundos antes de reduzir para a velocidade alvo para iniciar a ventoinha de resfriamento.\n" +"Isto é útil para ventoinhas onde um baixo PWM/potência pode ser insuficiente para fazer a ventoinha começar a girar a partir de uma parada, ou para fazer a ventoinha alcançar a velocidade mais rapidamente.\n" "Defina como 0 para desativar." msgid "Minimum non-zero part cooling fan speed" @@ -14681,6 +14834,14 @@ msgstr "Com que tipo de G-code a impressora é compatível." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Omitir o bloco de configuração do G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Não grava o CONFIG_BLOCK (os pares chave/valor da configuração do fatiador) no arquivo G-code. Isso pode ajudar com impressoras cujo firmware trava ao interpretar essas linhas de comentário (por exemplo, Anycubic go-klipper). Observação: o arquivo G-code não conterá mais as configurações do fatiador, então importá-lo de volta no OrcaSlicer não restaurará a configuração." + msgid "Pellet Modded Printer" msgstr "Impressora Modificada para Pellets" @@ -14830,19 +14991,15 @@ msgstr "Ângulo de saliência do preenchimento" msgid "The angle of the infill angled lines. 60° will result in a pure honeycomb." msgstr "O ângulo das linhas de preenchimento. 60° resultará em um favo de mel puro." -# AI Translated msgid "Lightning overhang angle" -msgstr "Ângulo de saliência Relâmpago" +msgstr "Ângulo de saliência de Relâmpago" -# AI Translated msgid "Maximum overhang angle for Lightning infill support propagation." msgstr "Ângulo máximo de saliência para a propagação de suporte do preenchimento Relâmpago." -# AI Translated msgid "Prune angle" msgstr "Ângulo de poda" -# AI Translated msgid "" "Controls how aggressively short or unsupported Lightning branches are pruned.\n" "This angle is converted internally to a per-layer distance." @@ -14850,11 +15007,9 @@ msgstr "" "Controla a agressividade com que os ramos Relâmpago curtos ou sem suporte são podados.\n" "Este ângulo é convertido internamente em uma distância por camada." -# AI Translated msgid "Straightening angle" msgstr "Ângulo de retificação" -# AI Translated msgid "Maximum straightening angle used to simplify Lightning branches." msgstr "Ângulo máximo de retificação usado para simplificar os ramos Relâmpago." @@ -15206,6 +15361,7 @@ msgid "The allowed maximum output force of Y axis" msgstr "A força máxima de saída permitida do eixo Y" # AI Translated +msgctxt "Newton" msgid "N" msgstr "N" @@ -15215,8 +15371,10 @@ msgstr "Massa da mesa do eixo Y" msgid "The machine bed mass load of Y axis" msgstr "A carga de massa da mesa do equipamento no eixo Y" +# AI Translated +msgctxt "gram" msgid "g" -msgstr "G" +msgstr "g" msgid "The allowed max printed mass" msgstr "Massa máxima de impressão permitida" @@ -15369,7 +15527,7 @@ msgstr "" "Para desativar o modelador de entrada, use o tipo Desativar." msgid "The part cooling fan speed may be increased when auto cooling is enabled. This is the maximum speed for the part cooling fan." -msgstr "A velocidade do ventilador de resfriamento de peças pode ser aumentada quando o resfriamento automático está habilitado. Este é o limite máximo de velocidade do ventilador de resfriamento de peças." +msgstr "A velocidade da ventoinha de resfriamento de peças pode ser aumentada quando o resfriamento automático está habilitado. Este é o limite máximo de velocidade da ventoinha de resfriamento de peças." msgid "The highest printable layer height for the extruder. Used to limit the maximum layer height when enable adaptive layer height." msgstr "A maior altura de camada imprimível para a extrusora. Usada para limitar a altura máxima da camada quando a altura da camada adaptativa está ativada." @@ -15432,31 +15590,31 @@ msgid "Applies extrusion rate smoothing only on external perimeters and overhang msgstr "Aplica suavização de taxa de extrusão somente em perímetros externos e saliências. Isso pode ajudar a reduzir artefatos devido a transições de velocidade bruscas em saliências visíveis externamente sem impactar a velocidade de impressão de recursos que não serão visíveis ao usuário." msgid "Minimum speed for part cooling fan." -msgstr "Velocidade mínima para o ventilador de resfriamento de peças." +msgstr "Velocidade mínima para a ventoinha de resfriamento de peças." msgid "" "Speed of auxiliary part cooling fan. Auxiliary fan will run at this speed during printing except the first several layers which is defined by no cooling layers.\n" "Please enable auxiliary_fan in printer settings to use this feature. G-code command: M106 P2 S(0-255)" msgstr "" -"Velocidade do ventilador auxiliar de resfriamento de peças. O ventilador auxiliar funcionará nesta velocidade durante a impressão, exceto nas primeiras camadas, que são definidas por camadas sem resfriamento.\n" +"Velocidade da ventoinha auxiliar de resfriamento de peças. A ventoinha auxiliar funcionará nesta velocidade durante a impressão, exceto nas primeiras camadas, que são definidas por camadas sem resfriamento.\n" "\n" -"Por favor, habilite o ventilador auxiliar nas configurações da impressora para usar esta função. Comando G-code: M106 P2 S(0-255)" +"Por favor, habilite a ventoinha auxiliar nas configurações da impressora para usar esta função. Comando G-code: M106 P2 S(0-255)" msgid "For the first" msgstr "Para as primeiras" msgid "Set special auxiliary cooling fan for the first certain layers." -msgstr "Definir um ventilador auxiliar de resfriamento específico para as primeiras camadas." +msgstr "Definir uma ventoinha auxiliar de resfriamento específico para as primeiras camadas." msgid "" "Auxiliary fan speed will be ramped up linearly from layer \"For the first\" to maximum at layer \"Full fan speed at layer\".\n" "\"Full fan speed at layer\" will be ignored if lower than \"For the first\", in which case the fan will run at maximum allowed speed at layer \"For the first\" + 1." msgstr "" -"A velocidade do ventilador auxiliar aumentará linearmente da camada \"Para as primeiras\" até o máximo na camada \"Velocidade total do ventilador na camada\".\n" -"A \"Velocidade total do ventilador na camada\" será ignorada se for menor que \"Para as primeiras\", caso em que o ventilador funcionará na velocidade máxima permitida na camada \"Para as primeiras\" + 1." +"A velocidade da ventoinha auxiliar aumentará linearmente da camada \"Para as primeiras\" até o máximo na camada \"Velocidade total da ventoinha na camada\".\n" +"A \"Velocidade total da ventoinha na camada\" será ignorada se for menor que \"Para as primeiras\", caso em que a ventoinha funcionará na velocidade máxima permitida na camada \"Para as primeiras\" + 1." msgid "Special auxiliary cooling fan speed, effective only for the first x layers." -msgstr "Velocidade especial do ventilador de resfriamento auxiliar, efetiva apenas para as primeiras x camadas." +msgstr "Velocidade especial da ventoinha de resfriamento auxiliar, efetiva apenas para as primeiras x camadas." msgid "The lowest printable layer height for the extruder. Used to limit the minimum layer height when enable adaptive layer height." msgstr "A menor altura de camada imprimível para a extrusora. Usada para limitar a altura mínima da camada ao habilitar a altura de camada adaptativa." @@ -15527,7 +15685,7 @@ msgstr "Os pontos de início e fim que vão da área do cortador até a calha de msgid "Reduce infill retraction" msgstr "Reduzir retração durante o preenchimento" -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." msgstr "Não retrair quando o movimento está completamente na área de preenchimento. Isso significa que o vazamento não pode ser visto. Isso pode reduzir o número de retratações para modelos complexos e economizar tempo de impressão, mas torna a geração de fatiamento e G-code mais lenta. Note que o Z-Hop também não é realizado em áreas onde a retração é omitida." msgid "This option will drop the temperature of the inactive extruders to prevent oozing." @@ -15621,11 +15779,9 @@ msgstr "Este G-code é inserido quando a função de extrusão é trocada. Ele msgid "Plugins Used" msgstr "Plugins Utilizados" -# AI Translated msgid "Plugin capabilities referenced by this preset, stored as name;uuid;capability." -msgstr "Recursos de plugin referenciados por esta predefinição, armazenados como name;uuid;capability." +msgstr "Capacidades de plugin referenciados por esta predefinição, armazenados como name;uuid;capability." -# AI Translated msgid "Python plugin(s) invoked at each slicing pipeline step to read and modify intermediate slicing data, including a final G-code post-processing step. Research/experimental." msgstr "Plugin(s) Python invocado(s) em cada etapa do pipeline de fatiamento para ler e modificar dados intermediários de fatiamento, incluindo uma etapa final de pós-processamento do G-code. Pesquisa/experimental." @@ -15694,7 +15850,7 @@ msgstr "Quantidade de retração depois da limpeza" #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" "Este é o comprimento da retração rápida antes de uma limpeza, em relação ao comprimento da retração.\n" @@ -15730,10 +15886,16 @@ msgstr "Retração longa na troca de extrusora" msgid "Retraction distance when extruder change" msgstr "Distância de retração na troca de extrusora" +msgid "Retraction Length (Toolchange)" +msgstr "Comprimento da Retração (Troca de ferramenta)" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Quando a retração é acionada antes da troca de ferramenta, o filamento é puxado de volta na quantidade especificada (o comprimento é medido no filamento bruto, antes de entrar na extrusora)." + msgid "Z-hop height" msgstr "Altura de Z-hop" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "Sempre que há uma retração, o bico é levantado um pouco para criar folga entre o bico e a impressão. Isso evita que o bico atinja a impressão ao se mover. Usar linhas em espiral para levantar Z pode evitar stringing." msgid "Z-hop lower boundary" @@ -15823,6 +15985,9 @@ msgstr "Comprimento extra na retração" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Quando a retração é compensada após o movimento de deslocamento, a extrusora empurrará essa quantidade adicional de filamento. Esta configuração é raramente necessária." +msgid "Extra length on restart (Toolchange)" +msgstr "Comprimento extra no reinício (Troca de ferramenta)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Quando a retração é compensada após a troca de ferramenta, a extrusora empurrará essa quantidade adicional de filamento." @@ -16231,6 +16396,12 @@ msgstr "Troca de ferramenta na torre de purga" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Força o cabeçote de impressão a se deslocar até a torre de purga antes de emitir o comando de troca de ferramenta (Tx). Relevante apenas para impressoras com múltiplas extrusoras (múltiplos cabeçotes de impressão) que utilizam uma torre de purga Tipo 2. Por padrão, o Orca ignora o deslocamento em máquinas com múltiplos cabeçotes de impressão, pois o firmware gerencia a troca do cabeçote, o que pode resultar na emissão do comando Tx acima da peça impressa. Habilite esta opção se desejar que a troca de ferramenta seja sempre emitida acima da torre de purga." +msgid "Wait for temperature on wipe tower" +msgstr "Aguardar a temperatura na torre de purga" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Pega a nova ferramenta sem esperar que ela atinja a temperatura de impressão, desloca-se até a torre de purga e aguarda a temperatura ali, logo antes de purgar. O vazamento causado pelo aquecimento cai na torre em vez do modelo, e o deslocamento acontece junto com o aquecimento. Relevante apenas para impressoras multiextrusora (multicabeça) que usam uma torre de purga do Tipo 2. O firmware ou a macro de troca de ferramenta não devem aguardar a temperatura por conta própria. Quando desativado, a espera de temperatura é emitida logo após o comando de troca de ferramenta." + msgid "No sparse layers (beta)" msgstr "Sem camadas esparsas (beta)" @@ -16243,11 +16414,9 @@ msgstr "Preparar todas as extrusoras de impressão" msgid "If enabled, all printing extruders will be primed at the front edge of the print bed at the start of the print." msgstr "Se ativado, todos as extrusoras de impressão serão preparados na borda frontal da mesa de impressão no início da impressão." -# AI Translated msgid "Toolchange ordering" msgstr "Ordenação de troca de ferramenta" -# AI Translated msgid "" "Determines the order of tool changes on each layer.\n" "- Default: Starts with the last used extruder to minimize tool changes.\n" @@ -16257,7 +16426,6 @@ msgstr "" "- Padrão: começa com a última extrusora usada para minimizar as trocas de ferramenta.\n" "- Cíclico: usa uma sequência fixa de ferramentas em cada camada. Isso sacrifica a velocidade em prol de uma melhor qualidade de superfície, pois as trocas de ferramenta extras dão mais tempo para as camadas resfriarem." -# AI Translated msgid "Cyclic" msgstr "Cíclico" @@ -16638,7 +16806,6 @@ msgstr "" "\n" "Se habilitado, este parâmetro também define uma variável G-code chamada chamber_temperature, que pode ser usada para passar a temperatura desejada da câmara para sua macro de início de impressão ou uma macro de absorção de calor como esta: PRINT_START (outras variáveis) CHAMBER_TEMP=[chamber_temperature]. Isso pode ser útil se sua impressora não suportar comandos M141/M191 ou se você desejar lidar com a absorção de calor na macro de início de impressão se nenhum aquecedor de câmara ativo estiver instalado." -# AI Translated msgid "" "This is the chamber temperature at which printing should start, while the chamber continues heating toward the \"Target\" chamber temperature. For example, set the Target to 60 and the Minimal to 50 to begin printing once the chamber reaches 50℃, without waiting for the full 60℃.\n" "\n" @@ -16646,11 +16813,11 @@ msgid "" "\n" "Unlike the \"Target\" chamber temperature, this option does not emit any M141/M191 commands; it only exposes the value to your custom G-code. It should not exceed the \"Target\" chamber temperature." msgstr "" -"Esta é a temperatura da câmara na qual a impressão deve começar, enquanto a câmara continua aquecendo em direção à temperatura da câmara \"Alvo\". Por exemplo, defina o Alvo como 60 e o Mínimo como 50 para começar a imprimir assim que a câmara atingir 50℃, sem esperar pelos 60℃ completos.\n" +"Esta é a temperatura da câmara na qual a impressão deve começar, enquanto a câmara continua aquecendo em direção à temperatura \"Alvo\" da câmara. Por exemplo, defina o Alvo como 60 e o Mínimo como 50 para começar a imprimir assim que a câmara atingir 50℃, sem esperar pelos 60℃ completos.\n" "\n" "Isso define uma variável de G-code chamada chamber_minimal_temperature, que pode ser passada para a sua macro de início de impressão ou uma macro de aquecimento prolongado, assim: PRINT_START (outras variáveis) CHAMBER_MIN_TEMP=[chamber_minimal_temperature].\n" "\n" -"Ao contrário da temperatura da câmara \"Alvo\", esta opção não emite nenhum comando M141/M191; ela apenas expõe o valor ao seu G-code personalizado. Não deve exceder a temperatura da câmara \"Alvo\"." +"Ao contrário da temperatura \"Alvo\" da câmara, esta opção não emite nenhum comando M141/M191; ela apenas expõe o valor ao seu G-code personalizado. Não deve exceder a temperatura \"Alvo\" da câmara." msgid "Chamber minimal temperature" msgstr "Temperatura mínima da câmara" @@ -16694,20 +16861,18 @@ msgstr "Espessura da casca do topo" msgid "The number of top solid layers is increased when slicing if the thickness calculated by top shell layers is thinner than this value. This can avoid having too thin a shell when layer height is small. 0 means that this setting is disabled and thickness of top shell is determined simply by the number of top shell layers." msgstr "O número de camadas sólidas superiores é aumentado ao fatiar se a espessura calculada pelas camadas da casca do topo for menor do que este valor. Isso pode evitar que a casca seja muito fina quando a altura da camada é pequena. 0 significa que esta configuração está desativada e a espessura da casca do topo é determinada apenas pelo número de camadas da casca do topo." -# AI Translated msgid "Separated infills" msgstr "Preenchimentos separados" -# AI Translated msgid "" "Centers the internal infill of each part on itself, as if it were sliced on its own, instead of on the whole assembly. Parts that touch or overlap are treated as one body and share a center; separate parts (or distinct 3D objects) each get their own.\n" "Useful when an assembly groups several objects that should each keep a consistent, self-centered infill.\n" "Affects line and grid patterns and rotation-template infills.\n" "Patterns locked to global coordinates (Gyroid, Honeycomb, TPMS, ...) are unaffected." msgstr "" -"Centraliza o preenchimento interno de cada peça em si mesma, como se fosse fatiada isoladamente, em vez de no conjunto inteiro. Peças que se tocam ou se sobrepõem são tratadas como um único corpo e compartilham um centro; peças separadas (ou objetos 3D distintos) recebem cada uma o seu próprio.\n" +"Centraliza o preenchimento interno de cada peça em si mesmo, como se fosse fatiada isoladamente, em vez de no conjunto inteiro. Peças que se tocam ou se sobrepõem são tratadas como um único corpo e compartilham um centro; peças separadas (ou objetos 3D distintos) recebem cada uma o seu próprio.\n" "Útil quando um conjunto agrupa vários objetos que devem manter, cada um, um preenchimento consistente e autocentrado.\n" -"Afeta os padrões de linha e grade e os preenchimentos com modelo de rotação.\n" +"Afeta os padrões de linha e grade e os preenchimentos com gabarito de rotação.\n" "Os padrões fixados em coordenadas globais (Giroide, Favo de mel, TPMS, ...) não são afetados." msgid "Center surface pattern on" @@ -16758,6 +16923,14 @@ msgstr "" "\n" "Definir um valor na configuração de quantidade de retração antes da limpeza abaixo executará qualquer retração em excesso antes da limpeza, caso contrário, será realizada após." +# AI Translated +msgid "Mixed color sublayer" +msgstr "Subcamada de cor mista" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "Ativa a divisão em subcamadas de cor mista. Quando ativado, as camadas que contêm filamentos de cor mista são divididas em subcamadas para obter efeitos de mistura de cores." + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "A torre de purga pode ser usada para limpar o resíduo no bico e estabilizar a pressão na câmara dentro do bico, a fim de evitar defeitos de aparência ao imprimir objetos." @@ -16776,11 +16949,9 @@ msgstr "Multiplicador de purga" msgid "The actual flushing volumes is equal to the flush multiplier value multiplied by the flushing volumes in the table." msgstr "Os volumes de purga reais são iguais ao multiplicador de purga multiplicado pelos volumes de purga na tabela." -# AI Translated msgid "Flush multiplier (Fast mode)" msgstr "Multiplicador de purga (Modo rápido)" -# AI Translated msgid "The flush multiplier used in fast purge mode." msgstr "O multiplicador de purga usado no modo de purga rápida." @@ -16790,13 +16961,11 @@ msgstr "Volume de preparo" msgid "This is the volume of material to prime the extruder with on the tower." msgstr "Este é o volume de material para preparar a extrusora na torre." -# AI Translated msgid "Prime volume mode" msgstr "Modo de volume de preparação" -# AI Translated msgid "Selects how the wipe-tower prime and flush volumes are computed on multi-extruder printers." -msgstr "Seleciona como os volumes de preparação e de purga da torre de purga são calculados em impressoras com várias extrusoras." +msgstr "Seleciona como os volumes de preparação e de purga da torre de purga são computados em impressoras com múltiplas extrusoras." msgid "Saving" msgstr "Salvando" @@ -17116,7 +17285,7 @@ msgid "The maximum volumetric speed for ramming before extruder change, where -1 msgstr "A velocidade volumétrica máxima para compactação antes da troca de extrusor, onde -1 significa usar a velocidade volumétrica máxima." msgid "To prevent oozing, the nozzle temperature will be cooled during ramming. Note: only a cooldown command and fan activation are triggered, reaching the target temperature is not guaranteed. 0 means disabled." -msgstr "Para evitar o gotejamento, a temperatura do bico será reduzida durante a compactação. Nota: somente um comando de resfriamento e ativação do ventilador são acionados, não sendo garantido o alcance da temperatura alvo. 0 significa desativado." +msgstr "Para evitar o gotejamento, a temperatura do bico será reduzida durante a compactação. Nota: somente um comando de resfriamento e ativação da ventoinha são acionados, não sendo garantido o alcance da temperatura alvo. 0 significa desativado." msgid "The maximum volumetric speed for ramming before a hotend change, where -1 means using the maximum volumetric speed." msgstr "A velocidade volumétrica máxima para compactação antes de uma troca de hotend, em que -1 significa usar a velocidade volumétrica máxima." @@ -17130,15 +17299,12 @@ msgstr "Quando este valor de retração for modificado, ele será usado como a q msgid "Support fast purge mode" msgstr "Suporte ao modo de purga rápida" -# AI Translated msgid "Whether this printer supports fast purge mode with optimized temperature and multiplier." msgstr "Se esta impressora suporta o modo de purga rápida com temperatura e multiplicador otimizados." -# AI Translated msgid "Filament change" msgstr "Troca de filamento" -# AI Translated msgid "The volume of material required to prime the extruder on the tower, excluding a hotend change." msgstr "O volume de material necessário para preparar a extrusora na torre, excluindo uma troca de hotend." @@ -17821,6 +17987,10 @@ msgstr "A geração da malha do arquivo do modelo falhou ou não há forma váli msgid "The supplied file couldn't be read because it's empty." msgstr "O arquivo fornecido não pôde ser lido porque está vazio." +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "O formato do arquivo é incompatível e não pode ser lido." + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Formato de arquivo desconhecido: o arquivo de entrada deve ter extensão .stl, .obj, .amf(.xml)." @@ -18323,11 +18493,9 @@ msgstr "" "Há vários endereços IP resolvendo para o nome do host %1%.\n" "Por favor, selecione um que deve ser usado." -# AI Translated msgid "Auto-scale for nozzle" msgstr "Escala automática para o bico" -# AI Translated msgid "" "This model is designed around a 0.4 mm nozzle with a 0.2 mm layer height. \n" "When the scaling option is enabled (recommended), it dynamically resizes to match your current nozzle diameter and an appropriate layer height, making the test both accurate and easy to read.\n" @@ -18473,11 +18641,9 @@ msgstr "Velocidade Inicial: " msgid "End speed: " msgstr "Velocidade Final: " -# AI Translated msgid "Auto-adjust to max volumetric speed" msgstr "Ajuste automático à velocidade volumétrica máxima" -# AI Translated msgid "If the end speed would exceed the filament's maximum volumetric speed, automatically lower the layer height (keeping standard values and staying within the machine's limits) to reach it. If even the minimum layer height is not enough, lower the end speed instead." msgstr "Se a velocidade final ultrapassar a velocidade volumétrica máxima do filamento, reduz automaticamente a altura de camada (mantendo valores padrão e respeitando os limites da máquina) para alcançá-la. Se nem mesmo a altura de camada mínima for suficiente, reduz a velocidade final." @@ -18492,7 +18658,6 @@ msgstr "" "passo >= 0\n" "fim > início + passo" -# AI Translated #, c-format, boost-format msgid "" "The end speed (%.0f mm/s) exceeds the filament's maximum volumetric speed (%.1f mm³/s), which limits the outer wall to about %.0f mm/s at this line width and layer height.\n" @@ -18500,12 +18665,11 @@ msgid "" "\n" "%s" msgstr "" -"A velocidade final (%.0f mm/s) ultrapassa a velocidade volumétrica máxima do filamento (%.1f mm³/s), o que limita a parede externa a cerca de %.0f mm/s com esta largura de linha e altura de camada.\n" +"A velocidade final (%.0f mm/s) excede a velocidade volumétrica máxima do filamento (%.1f mm³/s), o que limita a parede externa a cerca de %.0f mm/s com esta largura de linha e altura de camada.\n" " Velocidades acima disso serão limitadas, portanto os blocos superiores da torre não serão impressos na velocidade solicitada.\n" "\n" "%s" -# AI Translated #, c-format, boost-format msgid "" "The end speed (%.0f mm/s) exceeds the filament's maximum volumetric speed (%.1f mm³/s) at the default layer height (%.2f mm).\n" @@ -18516,7 +18680,6 @@ msgstr "" "\n" "A altura de camada foi reduzida para %.2f mm (um valor usado pelos perfis desta impressora) para que a torre possa atingir a velocidade solicitada." -# AI Translated #, c-format, boost-format msgid "" "Even at the smallest layer height used by this printer's profiles (%.2f mm) the end speed (%.0f mm/s) exceeds the filament's maximum volumetric speed (%.1f mm³/s).\n" @@ -18531,15 +18694,12 @@ msgstr "" "\n" "Continuar?" -# AI Translated msgid "Continue anyway?" msgstr "Continuar mesmo assim?" -# AI Translated msgid "Enable \"Auto-adjust\" to fix this automatically, or continue anyway?" msgstr "Ativar \"Ajuste automático\" para corrigir isso automaticamente ou continuar mesmo assim?" -# AI Translated msgid "Enable \"Auto-scale for nozzle\" and \"Auto-adjust\" to fix this automatically, or continue anyway?" msgstr "Ativar \"Escala automática para o bico\" e \"Ajuste automático\" para corrigir isso automaticamente ou continuar mesmo assim?" @@ -19282,19 +19442,19 @@ msgstr "Apenas impressoras com alterações nas predefinições de impressora, f msgid "Only display the filament names with changes to filament presets." msgstr "Exibir apenas os nomes dos filamentos com alterações nas predefinições de filamento." -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "Apenas os nomes das impressoras com predefinições de impressora do usuário serão exibidos, e cada predefinição escolhida será exportada como um arquivo zip." msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" "Apenas os nomes dos filamentos com predefinições de filamento do usuário serão exibidos, \n" "e todas as predefinições de filamento do usuário em cada nome de filamento selecionadas serão exportadas como um arquivo zip." msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" "Apenas os nomes das impressoras com predefinições de processo alterados serão exibidos, \n" "e todas os predefinições de processo do usuário em cada nome de impressora selecionadas serão exportados como um arquivo zip." @@ -19440,9 +19600,6 @@ msgstr "Impressora Física" msgid "Print Host upload" msgstr "Upload do Host de Impressão" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Selecione a implementação do agente de rede para comunicação com a impressora. Os agentes disponíveis são registrados na inicialização." - msgid "Select a Flashforge printer" msgstr "Selecione uma impressora Flashforge" @@ -19526,7 +19683,7 @@ msgstr "Copiar informações do sistema para a área de transferência" msgid "We need information for diagnosing source of the issue. Check wiki page for detailed guide." msgstr "Precisamos de informações para diagnosticar a origem do problema. Consulte a página da wiki para obter um guia detalhado." -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "O botão \"Empacotar\" reúne o arquivo do projeto e os logs da sessão atual em um arquivo ZIP." msgid "Any additional visual examples like images or screen recordings might be helpful while reporting the issue." @@ -19568,7 +19725,7 @@ msgstr "Nível de log" msgid "Stored logs" msgstr "Logs armazenados" -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "Compacta todos os logs armazenados em um arquivo ZIP." msgid "Profiles" @@ -19635,7 +19792,7 @@ msgstr "Tipo de impressora não encontrado, selecione manualmente." msgid "Authorizing..." msgstr "Autorizando…" -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "Erro. ​​Não foi possível obter o token de API para autorização" msgid "Could not parse server response." @@ -19644,9 +19801,8 @@ msgstr "Não foi possível decifrar a resposta do servidor." msgid "Error saving session to file" msgstr "Erro salvando sessão para arquivo" -# AI Translated msgid "Error session check" -msgstr "Verificação de sessão de erro" +msgstr "Erro na verificação de sessão" msgid "Error during file upload" msgstr "Erro durante a subida do arquivo" @@ -20079,12 +20235,12 @@ msgstr "Impressão Falhou" msgid "Removed" msgstr "Removido" -# AI Translated msgid "Enable smart filament assign: Assign one filament to multiple nozzles to maximize savings" -msgstr "Ativar atribuição inteligente de filamento: atribui um filamento a vários bicos para maximizar a economia" +msgstr "Ativar atribuição inteligente de filamento: Atribui um filamento a vários bicos para maximizar a economia" -msgid "Fila Saving" -msgstr "Econo Filamento" +# AI Translated +msgid "File Saving" +msgstr "Salvamento de Arquivo" msgid "Don't remind me again" msgstr "Não me avise novamente" @@ -20119,7 +20275,6 @@ msgstr "Tutorial em vídeo" msgid "(Sync with printer)" msgstr "(Sinc. com impressora)" -# AI Translated #, c-format, boost-format msgid "Error: %s extruder has no available %s nozzle, current group result is invalid." msgstr "Erro: a extrusora %s não tem nenhum bico %s disponível, o resultado do grupo atual é inválido." @@ -20130,17 +20285,14 @@ msgstr "Vamos fatiar de acordo com este método de agrupamento:" msgid "Tip: You can drag the filaments to reassign them to different nozzles." msgstr "Dica: Você pode arrastar os filamentos para reatribuí-los a diferentes bicos." -# AI Translated msgid "Please adjust your grouping or click " msgstr "Por favor, ajuste seu agrupamento ou clique em " -# AI Translated msgid " to set nozzle count" msgstr " para definir o número de bicos" -# AI Translated msgid "Set the physical nozzle count..." -msgstr "Definir o número físico de bicos..." +msgstr "Definir o número de bicos físicos…" msgid "The filament grouping method for current plate is determined by the dropdown option at the slicing plate button." msgstr "O método de agrupamento de filamentos para a placa atual é determinado pela opção no botão de fatiamento da placa." @@ -20290,9 +20442,6 @@ msgstr "Algo inesperado aconteceu ao tentar conectar, por favor tente novamente. msgid "User canceled." msgstr "Cancelado pelo usuário." -msgid "Head diameter" -msgstr "Diâmetro da cabeça" - msgid "Max angle" msgstr "Ângulo máx" @@ -20450,6 +20599,9 @@ msgstr "Reiniciar Agora" msgid "NO RAMMING AT ALL" msgstr "SEM NENHUM MOLDEAMENTO" +msgid "s" +msgstr "s" + msgid "Volumetric speed" msgstr "Velocidade volumétrica" @@ -20480,99 +20632,75 @@ msgstr "Numero de facetas triangulares" msgid "Calculating, please wait..." msgstr "Calculando, por favor aguarde…" -# AI Translated msgid "Save these settings as default" msgstr "Salvar estas configurações como padrão" -# AI Translated msgid "If enabled, the values above are stored as the defaults used for future STEP imports (and shown in Preferences)." msgstr "Se ativado, os valores acima são armazenados como os padrões usados para futuras importações STEP (e mostrados nas Preferências)." -# AI Translated msgid "PresetBundle" msgstr "PresetBundle" -# AI Translated msgid "Bundle folder does not exist." msgstr "A pasta do pacote não existe." -# AI Translated msgid "Failed to open folder." msgstr "Falha ao abrir a pasta." -# AI Translated msgid "Delete selected bundle from folder and all presets loaded from it?" msgstr "Excluir o pacote selecionado da pasta e todas as predefinições carregadas a partir dele?" -# AI Translated msgid "Delete Bundle" -msgstr "Excluir pacote" +msgstr "Excluir Pacote" -# AI Translated msgid "Failed to remove bundle." msgstr "Falha ao remover o pacote." -# AI Translated msgid "Remove Bundle" msgstr "Remover pacote" -# AI Translated msgid "Unsubscribe bundle?" msgstr "Cancelar a inscrição do pacote?" -# AI Translated msgid "UnsubscribeBundle" msgstr "UnsubscribeBundle" -# AI Translated msgid "Failed to unsubscribe bundle." msgstr "Falha ao cancelar a inscrição do pacote." -# AI Translated msgid "Unsubscribe Bundle" -msgstr "Cancelar inscrição do pacote" +msgstr "Cancelar inscrição do Pacote" -# AI Translated msgid "ExportPresetBundle" msgstr "ExportPresetBundle" -# AI Translated msgid "Save preset bundle" msgstr "Salvar pacote de predefinições" -# AI Translated msgid "Performing desktop integration failed - boost::filesystem::canonical did not return appimage path." msgstr "Falha ao realizar a integração com a área de trabalho - boost::filesystem::canonical não retornou o caminho do appimage." -# AI Translated msgid "Performing desktop integration failed - Could not find executable." msgstr "Falha ao realizar a integração com a área de trabalho - não foi possível encontrar o executável." -# AI Translated msgid "Performing desktop integration failed because the application directory was not found." msgstr "Falha ao realizar a integração com a área de trabalho porque o diretório do aplicativo não foi encontrado." -# AI Translated msgid "Performing desktop integration failed - could not create Gcodeviewer desktop file. OrcaSlicer desktop file was probably created successfully." msgstr "Falha ao realizar a integração com a área de trabalho - não foi possível criar o arquivo desktop do Gcodeviewer. O arquivo desktop do OrcaSlicer provavelmente foi criado com sucesso." -# AI Translated msgid "Performing downloader desktop integration failed - boost::filesystem::canonical did not return appimage path." msgstr "Falha ao realizar a integração do downloader com a área de trabalho - boost::filesystem::canonical não retornou o caminho do appimage." -# AI Translated msgid "Performing downloader desktop integration failed - Could not find executable." msgstr "Falha ao realizar a integração do downloader com a área de trabalho - não foi possível encontrar o executável." -# AI Translated msgid "Performing downloader desktop integration failed because the application directory was not found." msgstr "Falha ao realizar a integração do downloader com a área de trabalho porque o diretório do aplicativo não foi encontrado." -# AI Translated msgid "Desktop Integration" -msgstr "Integração com a área de trabalho" +msgstr "Integração com a Área de Trabalho" -# AI Translated msgid "" "Desktop Integration sets this binary to be searchable by the system.\n" "\n" @@ -20596,40 +20724,33 @@ msgstr "Arquivar pré-visualização" msgid "Open File" msgstr "Abrir Arquivo" -# AI Translated msgid "AMS Dryness Control" -msgstr "Controle de secura do AMS" +msgstr "Controle de Secura do AMS" -# AI Translated msgid "Filament Drying Settings" -msgstr "Configurações de secagem de filamento" +msgstr "Configurações de Secagem de Filamento" msgid "Stopping" msgstr "Parando" -# AI Translated msgid "Unable to dry temporarily due to ..." -msgstr "Não é possível secar temporariamente devido a ..." +msgstr "Não é possível secar temporariamente devido a…" msgid "Drying Error" msgstr "Erro de Secagem" -# AI Translated msgid "Please check the Assistant for troubleshooting" msgstr "Por favor, verifique o Assistente para solução de problemas" -# AI Translated msgid "Please remove and store the filament (as shown)." msgstr "Por favor, remova e guarde o filamento (como mostrado)." -# AI Translated msgid "The AMS can rotate the filament which is properly stored, providing better drying results." msgstr "O AMS pode girar o filamento que está corretamente armazenado, proporcionando melhores resultados de secagem." msgid "Rotate spool when drying" msgstr "Girar o carretel durante a secagem" -# AI Translated msgctxt "amsdrying" msgid "Back" msgstr "Voltar" @@ -20649,11 +20770,9 @@ msgstr " temperatura mínima de secagem é " msgid "This filament may not be completely dried." msgstr "Este filamento pode não estar completamente seco." -# AI Translated msgid "This AMS is currently printing. To ensure print quality, the drying temperature cannot exceed the recommended drying temperature." msgstr "Este AMS está imprimindo no momento. Para garantir a qualidade da impressão, a temperatura de secagem não pode exceder a temperatura de secagem recomendada." -# AI Translated msgid "The temperature shall not exceed the filament's heat distortion temperature" msgstr "A temperatura não deve exceder a temperatura de distorção térmica do filamento" @@ -20666,22 +20785,18 @@ msgstr "O valor máximo de tempo não pode ser superior a 24." msgid "Insufficient power" msgstr "Potência insuficiente" -# AI Translated msgid " Too many AMS drying simultaneously. Please plug in the power or stop other drying processes before starting." msgstr " Muitos AMS secando simultaneamente. Por favor, conecte à energia ou pare outros processos de secagem antes de iniciar." msgid "AMS is busy" msgstr "O AMS está ocupado" -# AI Translated msgid " AMS is calibrating | reading RFID | loading/unloading material, please wait." msgstr " O AMS está calibrando | lendo RFID | carregando/descarregando material, por favor aguarde." -# AI Translated msgid "Filament in AMS outlet" msgstr "Filamento na saída do AMS" -# AI Translated msgid " The high drying temperature may cause AMS blockage, please unload first." msgstr " A alta temperatura de secagem pode causar bloqueio do AMS, por favor descarregue primeiro." @@ -20694,18 +20809,15 @@ msgstr "Não suportado no modo 2D" msgid "Task in progress" msgstr "Tarefa em progresso" -# AI Translated msgid " The AMS might be in use during Task." msgstr " O AMS pode estar em uso durante a Tarefa." msgid " Firmware update in progress, please wait..." -msgstr " Atualização de firmware em progresso, por favor aguarde..." +msgstr " Atualização de firmware em progresso, por favor aguarde…" -# AI Translated msgid " Please plug in the power and then use the drying function." -msgstr " Por favor, conecte à energia e depois use a função de secagem." +msgstr " Por favor, conecte à energia e então use a função de secagem." -# AI Translated msgid " The high drying temperature may cause AMS blockage. Please unload the filament manually before proceeding." msgstr " A alta temperatura de secagem pode causar bloqueio do AMS. Por favor, descarregue o filamento manualmente antes de prosseguir." @@ -20713,7 +20825,7 @@ msgid "System is busy" msgstr "O sistema está ocupado" msgid " Initiating other drying processes, please wait a few seconds..." -msgstr " Iniciando outros processos de secagem, por favor aguarde alguns segundos..." +msgstr " Iniciando outros processos de secagem, por favor aguarde alguns segundos…" msgid "For better drying results, remove the filament and allow it to rotate." msgstr "Para obter melhores resultados de secagem, remova o filamento e permita que ele gire." @@ -20803,8 +20915,8 @@ msgid "" "Auxiliary fan\n" "Did you know that OrcaSlicer supports Auxiliary part cooling fan?" msgstr "" -"Ventilador auxiliar\n" -"Você sabia que o OrcaSlicer suporta ventilador auxiliar de resfriamento de peças?" +"Ventoinha auxiliar\n" +"Você sabia que o OrcaSlicer suporta ventoinha auxiliar de resfriamento de peças?" #: resources/data/hints.ini: [hint:Air filtration] msgid "" @@ -21066,6 +21178,54 @@ msgstr "" "Evitar empenamento\n" "Você sabia que ao imprimir materiais propensos ao empenamento como ABS, aumentar adequadamente a temperatura da mesa aquecida pode reduzir a probabilidade de empenamento?" +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "A visualização ao vivo nativa do Wayland requer o receptor de vídeo GTK do GStreamer. Instale o plugin gtksink para GStreamer e reinicie o OrcaSlicer." + +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "Falha ao inicializar o receptor de vídeo nativo do Wayland GStreamer. Verifique a instalação do plugin GStreamer GTK." + +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "O Windows Media Player é necessário para esta tarefa! Você quer habilitar o 'Windows Media Player' para seu sistema operacional?" + +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "BambuSource não foi registrado corretamente para reprodução de mídia! Pressione Sim para registrá-lo novamente. Você será promovido duas vezes" + +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "Componente BambuSource registrado para reprodução de mídia não encontrado! Por favor, reinstale o OrcaSlicer ou procure ajuda da comunidade." + +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "Usando um BambuSource de uma instalação diferente, a reprodução de vídeo pode não funcionar corretamente! Pressione Sim para consertar." + +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "Seu sistema não possui codecs H.264 para o GStreamer, que são necessários para reproduzir vídeos. (Tente instalar os pacotes gstreamer1.0-plugins-bad ou gstreamer1.0-libav e depois reinicie o OrcaSlicer?)" + +#~ msgid "N" +#~ msgstr "N" + +#~ msgid "g" +#~ msgstr "g" + +#~ msgid "Fila Saving" +#~ msgstr "Econo Filamento" + +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "A altura da camada é muito pequena.\n" +#~ "Ela será definida como altura mínima da camada\n" +#~ "A altura da camada é muito pequena.\n" +#~ "Ela será definida como altura mínima da camada\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "A altura da camada excede o limite em Configurações da Impressora -> Extrusora -> Limites de altura da camada, isso pode causar problemas de qualidade de impressão." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Ajustar automaticamente à faixa definida?\n" + +#~ msgid "Head diameter" +#~ msgstr "Diâmetro da cabeça" + #~ msgid "Print order within a single layer." #~ msgstr "Ordem de impressão dentro de uma única camada." @@ -21078,17 +21238,6 @@ msgstr "" #~ msgid "Rear" #~ msgstr "Traseira" -# AI Translated -#, boost-format -#~ msgid "" -#~ "Objects(%1%) have duplicated connectors. Some connectors may be missing in slicing result.\n" -#~ "Please report to PrusaSlicer team in which scenario this issue happened.\n" -#~ "Thank you." -#~ msgstr "" -#~ "Os objetos(%1%) têm conectores duplicados. Alguns conectores podem estar faltando no resultado do fatiamento.\n" -#~ "Por favor, informe à equipe do PrusaSlicer em qual cenário esse problema ocorreu.\n" -#~ "Obrigado." - #~ msgid "Skip for Now" #~ msgstr "Pular por Enquanto" @@ -22009,7 +22158,7 @@ msgstr "" #~ msgstr "Pausado devido à perda do AMS" #~ msgid "Paused due to low speed of the heat break fan" -#~ msgstr "Pausado devido a baixa velocidade do ventilador do bloco de aquecimento" +#~ msgstr "Pausado devido a baixa velocidade da ventoinha do bloco de aquecimento" #~ msgid "Paused due to chamber temperature control error" #~ msgstr "Pausado devido a erro no controle de temperatura da câmara" @@ -22538,20 +22687,20 @@ msgstr "" #~ msgstr "Forçar resfriamento para saliências e pontes" #~ msgid "Enable this option to optimize part cooling fan speed for overhang and bridge to get better cooling" -#~ msgstr "Ative esta opção para otimizar a velocidade do ventilador de resfriamento de peças para saliência e ponte para obter melhor resfriamento" +#~ msgstr "Ative esta opção para otimizar a velocidade da ventoinha de resfriamento de peças para saliência e ponte para obter melhor resfriamento" #~ msgid "Fan speed for overhang" -#~ msgstr "Velocidade do ventilador para saliência" +#~ msgstr "Velocidade da ventoinha para saliência" #~ msgid "Force part cooling fan to be this speed when printing bridge or overhang wall which has large overhang degree. Forcing cooling for overhang and bridge can get better quality for these part" -#~ msgstr "Forçar o ventilador de resfriamento de peças a ser nesta velocidade ao imprimir ponte ou parede saliente que tenha um grande grau de saliência. Forçar o resfriamento para saliência e ponte pode obter melhor qualidade para estas partes" +#~ msgstr "Forçar a ventoinha de resfriamento de peças a ser nesta velocidade ao imprimir ponte ou parede saliente que tenha um grande grau de saliência. Forçar o resfriamento para saliência e ponte pode obter melhor qualidade para estas partes" #~ msgid "Cooling overhang threshold" #~ msgstr "Limiar de resfriamento de saliência" #, c-format #~ msgid "Force cooling fan to be specific speed when overhang degree of printed part exceeds this value. Expressed as percentage which indicates how much width of the line without support from lower layer. 0% means forcing cooling for all outer wall no matter how much overhang degree" -#~ msgstr "Forçar o ventilador de resfriamento a ser uma velocidade específica quando o grau de saliência das peças impressa excede este valor. Expresso como porcentagem, que indica quanto da largura da linha sem suporte da camada inferior. Zero significa forçar o resfriamento para toda a parede externa, não importa quanto seja o grau de saliência" +#~ msgstr "Forçar a ventoinha de resfriamento a ser uma velocidade específica quando o grau de saliência das peças impressa excede este valor. Expresso como porcentagem, que indica quanto da largura da linha sem suporte da camada inferior. Zero significa forçar o resfriamento para toda a parede externa, não importa quanto seja o grau de saliência" #~ msgid "Density of external bridges. 100% means solid bridge. Default is 100%." #~ msgstr "Densidade de pontes externas. 100% significa ponte sólida. O padrão é 100%." diff --git a/localization/i18n/ru/OrcaSlicer_ru.po b/localization/i18n/ru/OrcaSlicer_ru.po index a0a48560ff..112fbc2834 100644 --- a/localization/i18n/ru/OrcaSlicer_ru.po +++ b/localization/i18n/ru/OrcaSlicer_ru.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: OrcaSlicer V2.5.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "PO-Revision-Date: 2026-02-25 13:38+0300\n" "Last-Translator: Felix14_v2\n" "Language-Team: Felix14_v2 (ДС/ТГ: @felix14_v2, почта: aleks111001@list.ru), Andylg <andylg@yandex.ru>\n" @@ -27,15 +27,12 @@ msgstr "Основной экструдер" msgid "main extruder" msgstr "основной экструдер" -# AI Translated msgid "Auxiliary Extruder" msgstr "Вспомогательный экструдер" -# AI Translated msgid "Auxiliary extruder" msgstr "Вспомогательный экструдер" -# AI Translated msgid "auxiliary extruder" msgstr "вспомогательный экструдер" @@ -57,27 +54,21 @@ msgstr "Правый экструдер" msgid "right extruder" msgstr "правый экструдер" -# AI Translated msgid "Main Nozzle" msgstr "Основное сопло" -# AI Translated msgid "Main nozzle" msgstr "Основное сопло" -# AI Translated msgid "main nozzle" msgstr "основное сопло" -# AI Translated msgid "Auxiliary Nozzle" msgstr "Вспомогательное сопло" -# AI Translated msgid "Auxiliary nozzle" msgstr "Вспомогательное сопло" -# AI Translated msgid "auxiliary nozzle" msgstr "вспомогательное сопло" @@ -99,67 +90,51 @@ msgstr "Правый экструдер" msgid "right nozzle" msgstr "правого сопла" -# AI Translated msgid "Main Hotend" msgstr "Основной хотэнд" -# AI Translated msgid "Main hotend" msgstr "Основной хотэнд" -# AI Translated msgid "main hotend" msgstr "основной хотэнд" -# AI Translated msgid "Auxiliary Hotend" msgstr "Вспомогательный хотэнд" -# AI Translated msgid "Auxiliary hotend" msgstr "Вспомогательный хотэнд" -# AI Translated msgid "auxiliary hotend" msgstr "вспомогательный хотэнд" -# AI Translated msgid "Left Hotend" msgstr "Левый хотэнд" -# AI Translated msgid "Left hotend" msgstr "Левый хотэнд" -# AI Translated msgid "left hotend" msgstr "левый хотэнд" -# AI Translated msgid "Right Hotend" msgstr "Правый хотэнд" -# AI Translated msgid "Right hotend" msgstr "Правый хотэнд" -# AI Translated msgid "right hotend" msgstr "правый хотэнд" -# AI Translated msgid "main" msgstr "основной" -# AI Translated msgid "auxiliary" msgstr "вспомогательный" -# AI Translated msgid "Main" msgstr "Основной" -# AI Translated msgid "Auxiliary" msgstr "Вспомогательный" @@ -279,7 +254,7 @@ msgstr "Высокий расход" msgid "Standard" msgstr "Обычный" -# AI Translated +# На бамбувики просто "хотэнд ТПУ". Очевидно, машинный перевод. Оставляю оригинальное. msgid "TPU High Flow" msgstr "TPU High Flow" @@ -295,7 +270,6 @@ msgstr "Нержавеющая сталь" msgid "Tungsten Carbide" msgstr "Карбид вольфрама" -# AI Translated msgid "The toolhead and hotend rack may move. Please keep your hands away from the chamber." msgstr "Печатающая голова и стойка хотэндов могут смещаться. Держите руки подальше от камеры." @@ -350,15 +324,12 @@ msgstr "Версия:" msgid "Latest version" msgstr "Последняя версия" -# AI Translated msgid "Row A" msgstr "Ряд A" -# AI Translated msgid "Row B" -msgstr "Ряд B" +msgstr "Ряд Б" -# AI Translated msgid "Toolhead" msgstr "Печатающая голова" @@ -368,9 +339,8 @@ msgstr "Пусто" msgid "Error" msgstr "Ошибка" -# AI Translated msgid "Induction Hotend Rack" -msgstr "Индукционная стойка хотэндов" +msgstr "Стойка индукционных хотэндов" msgid "Hotends Info" msgstr "Информация о хотэндах" @@ -384,30 +354,25 @@ msgstr "Чтение " msgid "Please wait" msgstr "Подождите" -# AI Translated msgid "Reading" msgstr "Чтение" -# AI Translated msgid "Running..." msgstr "Выполнение..." -# AI Translated +# Ряд msgid "Raised" -msgstr "Поднято" +msgstr "Поднят" -# AI Translated msgid "The hotend is in an abnormal state and currently unavailable. Please go to 'Device -> Upgrade' to upgrade firmware." -msgstr "Хотэнд находится в нештатном состоянии и сейчас недоступен. Перейдите в «Устройство -> Обновление», чтобы обновить прошивку." +msgstr "Хотэнд находится в нештатном состоянии и сейчас недоступен. Перейдите в «Принтер» → «Обновление», чтобы обновить прошивку." -# AI Translated msgid "Abnormal Hotend" msgstr "Нештатное состояние хотэнда" msgid "Cancel" msgstr "Отмена" -# AI Translated msgid "Jump to the upgrade page" msgstr "Перейти на страницу обновления" @@ -417,9 +382,8 @@ msgstr "Обновить" msgid "Refreshing" msgstr "Обновление" -# AI Translated msgid "Hotend status abnormal, unavailable at present. Please upgrade the firmware and try again." -msgstr "Состояние хотэнда нештатное, сейчас недоступно. Обновите прошивку и повторите попытку." +msgstr "Хотэнд находится в нештатном состоянии и сейчас недоступен. Обновите прошивку и попробуйте ещё раз." msgid "SN" msgstr "Серийный номер" @@ -431,19 +395,16 @@ msgstr "Версия" msgid "Used Time: %s" msgstr "Время использования: %s" -# AI Translated +# Полагаю, речь об этом https://wiki.bambulab.com/ru/software/bambu-studio/filament-track-switch-dynamic-mapping msgid "Dynamic nozzles are allocated on the current plate. Picking hotend is not supported." msgstr "На текущем столе назначены динамические сопла. Выбор хотэнда не поддерживается." -# AI Translated msgid "Hotend Rack" msgstr "Стойка хотэндов" -# AI Translated msgid "ToolHead" msgstr "Печатающая голова" -# AI Translated msgid "Nozzle information needs to be read" msgstr "Необходимо считать информацию о сопле" @@ -948,7 +909,6 @@ msgstr "Снять выбор" msgid "Select all connectors" msgstr "Выбрать все соединения" -# AI Translated msgctxt "Cut tool" msgid "Cut" msgstr "Разрезать" @@ -1885,14 +1845,14 @@ msgid "" "Select 2 faces on objects and \n" " make objects assemble together." msgstr "" -"Выберите две грани на моделях и\n" -"соберите объекты вместе." +"Выберите две грани на моделях и соберите\n" +"объекты вместе." msgid "" "Select 2 points or circles on objects and \n" " specify distance between them." msgstr "" -"Выберите 2 точки или окружности на моделях \n" +"Выберите 2 точки или окружности на моделях\n" "и укажите расстояние между ними." msgid "Face" @@ -1954,7 +1914,6 @@ msgstr "Запуск режима измерения" msgid "Leaving Measure gizmo" msgstr "Выход из режима измерения" -# AI Translated msgctxt "Assembly tool" msgid "Assemble" msgstr "Собрать" @@ -1968,8 +1927,9 @@ msgstr "Выберите минимум две модели." msgid "(Moving)" msgstr "(подвижная)" +# Пробел в конце, чтобы не срезалась часть msgid "Point and point assembly" -msgstr "Сборка по точкам" +msgstr "Сборка по точкам " # Зачем здесь "внимание"? Это просто руководство к действию msgid "Warning: please select two different meshes." @@ -1992,8 +1952,9 @@ msgstr "" "для получения возможности поднимать их\n" "над столом." +# Пробел в конце, чтобы не срезалась часть msgid "Face and face assembly" -msgstr "Сборка по граням" +msgstr "Сборка по граням " msgid "Entering Assembly gizmo" msgstr "Запуск режима сборки" @@ -2133,7 +2094,7 @@ msgstr "" "\n" "Для автоматического переноса существующих профилей войдите в Orca Cloud. Посетите нашу Вики, чтобы узнать подробнее о ручном переносе, хранении и синхронизации профилей.\n" "\n" -"Можно спокойно игнорировать это сообщение, если вы ранее не использовали Bambu Cloud для синхронизации." +"Можно спокойно игнорировать это сообщение, если вы ранее не использовали Bambu Cloud для синхронизации. " msgid "Profile syncing change" msgstr "Изменения в синхронизации профилей" @@ -2214,7 +2175,6 @@ msgstr "Загрузка плагинов" msgid "Plugin %s is no longer available." msgstr "Плагин «%s» больше недоступен." -# AI Translated #, c-format, boost-format msgid "Plugin %s access is unauthorized." msgstr "Доступ к плагину %s не авторизован." @@ -2951,6 +2911,10 @@ msgstr "Правка" msgid "Merge with" msgstr "Объединить с" +# AI Translated +msgid "Decompose Color" +msgstr "Разложить цвет" + msgid "Delete this filament" msgstr "Удалить материал" @@ -3262,6 +3226,10 @@ msgstr "Сборка" msgid "Merge parts to an object" msgstr "Сборка моделей" +# AI Translated +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "Использование переменной высоты слоя вместе с подслоем смешанного цвета может ухудшить качество смешивания цветов." + # Запись в истории действий msgid "Add layers" msgstr "Добавление слоёв" @@ -3661,10 +3629,9 @@ msgid "Left(Aux)" msgstr "Левый (вспом.)" # FAN_HEAT_BREAK_0_IDX - охлаждение термобарьера в первом хотэнде -# AI Translated msgctxt "Hotend Heat Breaker Fan" msgid "Hotend" -msgstr "Хотэнд" +msgstr "1 термобарьер" msgid "Parts" msgstr "Основной" @@ -3713,29 +3680,23 @@ msgstr "Подтверждение экструзии" msgid "Check filament location" msgstr "Проверка расположения прутка" -# AI Translated msgid "Switch" msgstr "Переключить" -# AI Translated msgid "hotend" msgstr "хотэнд" -# AI Translated msgid "Wait for AMS cooling" msgstr "Дождитесь охлаждения AMS" -# AI Translated msgid "Switch current filament at Filament Track Switch" msgstr "Переключить текущий материал на Filament Track Switch" -# AI Translated msgid "Pull back current filament at Filament Track Switch" msgstr "Втянуть текущий материал на Filament Track Switch" -# AI Translated msgid "Switch track at Filament Track Switch" -msgstr "Переключить дорожку на Filament Track Switch" +msgstr "Переключить подачу на Filament Track Switch" msgid "The maximum temperature cannot exceed " msgstr "Температура не должна превышать " @@ -3743,45 +3704,37 @@ msgstr "Температура не должна превышать " msgid "The minmum temperature should not be less than " msgstr "Температура не должна быть ниже " -# AI Translated msgid "Type to filter..." -msgstr "Введите текст для фильтрации..." +msgstr "Поиск..." # в Сохранение толщины вертикальной оболочки. # было Везде, но из-за условия совместимости изменено.... как тогда быть? msgid "All" msgstr "Все" -# AI Translated msgid "No selected items..." -msgstr "Нет выбранных элементов..." +msgstr "Ничего не выбрано..." -# AI Translated msgid "All items selected..." -msgstr "Выбраны все элементы..." +msgstr "Выбраны все..." -# AI Translated msgid "No matching items..." -msgstr "Нет подходящих элементов..." +msgstr "Ничего не найдено..." msgid "Deselect All" msgstr "Снять выбор со всего" -# AI Translated msgid "Select visible" msgstr "Выбрать видимые" -# AI Translated msgid "Deselect visible" msgstr "Снять выбор с видимых" -# AI Translated msgid "Filter selected" -msgstr "Отфильтровать выбранные" +msgstr "Фильтровать выбранные" -# AI Translated msgid "Filter nonSelected" -msgstr "Отфильтровать невыбранные" +msgstr "Фильтровать невыбранные" msgid "Simple settings" msgstr "Простые настройки" @@ -3799,18 +3752,15 @@ msgstr "Режим разработчика" msgid "Launch troubleshoot center" msgstr "Запустить экран отладки" -# AI Translated msgid "Set nozzle count" msgstr "Задать количество сопел" -# AI Translated msgid "Please set nozzle count" -msgstr "Задайте количество сопел" +msgstr "Укажите количество сопел" msgid "Error: Can not set both nozzle count to zero." msgstr "Ошибка: оба значения не могут быть нулевыми." -# AI Translated #, c-format, boost-format msgid "Error: Nozzle count can not exceed %d." msgstr "Ошибка: количество сопел не может превышать %d." @@ -3821,45 +3771,36 @@ msgstr "Подтвердить" msgid "Extruder" msgstr "Экструдер" -# AI Translated msgid "Nozzle Selection" msgstr "Выбор сопла" -# AI Translated msgid "Available Nozzles" msgstr "Доступные сопла" msgid "Nozzle Info" msgstr "Информация о сопле" -# AI Translated msgid "Sync Nozzle status" msgstr "Синхронизировать состояние сопла" -# AI Translated msgid "Caution: Mixing nozzle diameters in one print is not supported. If the selected size is only on one extruder, single-extruder printing will be enforced." -msgstr "Внимание: смешивание диаметров сопел в одной печати не поддерживается. Если выбранный размер есть только на одном экструдере, будет принудительно применена печать одним экструдером." +msgstr "Внимание: смешивание диаметров сопел в одной печати не поддерживается. Печать будет производиться одним соплом, если выбранный диаметр есть только на одном экструдере." -# AI Translated #, c-format, boost-format msgid "Refresh %d/%d..." -msgstr "Обновление %d/%d..." +msgstr "Обновить %d/%d..." -# AI Translated msgid "Unknown nozzle detected. Refresh to update info (unrefreshed nozzles will be excluded during slicing). Verify nozzle diameter & flow rate against displayed values." -msgstr "Обнаружено неизвестное сопло. Обновите, чтобы получить сведения (необновлённые сопла будут исключены при нарезке). Сверьте диаметр сопла и расход с отображаемыми значениями." +msgstr "Обнаружено неизвестное сопло. Обновите для получения сведений (неизвестные сопла будут исключены при нарезке). Сверьте диаметр сопла и расход с отображаемыми значениями." -# AI Translated msgid "Unknown nozzle detected. Refresh to update (unrefreshed nozzles will be skipped in slicing)." -msgstr "Обнаружено неизвестное сопло. Обновите для получения сведений (необновлённые сопла будут пропущены при нарезке)." +msgstr "Обнаружено неизвестное сопло. Обновите для получения сведений (неизвестные сопла будут пропущены при нарезке)." -# AI Translated msgid "Please confirm whether the required nozzle diameter and flow rate match the currently displayed values." msgstr "Убедитесь, что требуемый диаметр сопла и расход соответствуют отображаемым значениям." -# AI Translated msgid "Your printer has different nozzles installed. Please select a nozzle for this print." -msgstr "На вашем принтере установлены разные сопла. Выберите сопло для этой печати." +msgstr "В принтере установлены разные сопла. Выберите сопло для этой печати." msgid "Ignore" msgstr "Игнорировать" @@ -3888,15 +3829,14 @@ msgstr "Расстановка..." msgid "Arranging" msgstr "Расстановка" -# AI Translated msgid "Arranging " -msgstr "Расстановка " +msgstr "Расстановка: " msgid "Arranging canceled." msgstr "Расстановка отменена." msgid "Arranging complete, but some items were not able to be arranged. Reduce spacing and try again." -msgstr "Расстановка завершена, но не всё удалось уместить на столе. Уменьшите отступ расстановки и повторите попытку." +msgstr "Расстановка завершена, но не всё удалось уместить на столе. Уменьшите отступ и повторите попытку." msgid "Arranging done." msgstr "Расстановка выполнена." @@ -4346,8 +4286,8 @@ msgid "" "Lower half area: The filament from original project will be used when unmapped.\n" "And you can click it to modify" msgstr "" -"Верхняя половина: Исходный\n" -"Нижняя половина: При отсутствии назначения будет использоваться филамент из исходного проекта.\n" +"Сверху: оригинальный материал\n" +"Снизу: материал исходного проекта (если не назначен).\n" "Нажмите для изменения" msgid "" @@ -4357,7 +4297,7 @@ msgid "" msgstr "" "Сверху: оригинальный материал\n" "Снизу: материал из AMS\n" -"Нажмите, чтобы изменить" +"Нажмите для изменения" msgid "" "Upper half area: Original\n" @@ -4371,45 +4311,41 @@ msgid "AMS Slots" msgstr "Слоты AMS" msgid "Please select from the following filaments" -msgstr "Пожалуйста, выберите из следующих филаментов" +msgstr "Выберите из следующих материалов" -# AI Translated #, c-format, boost-format msgid "Select filament that installed to the %s" msgstr "Выберите материал, установленный в %s" msgid "Left AMS" -msgstr "Левый AMS" +msgstr "Левая AMS" +# Оверюзиниг: внешние мосты/внешняя катушка msgid "External" msgstr "Внешние" msgid "Reset current filament mapping" -msgstr "Сбросить текущее назначение филамента" +msgstr "Сбросить переназначенные материалы" msgid "Right AMS" -msgstr "Правый AMS" +msgstr "Правая AMS" #, c-format, boost-format msgid "Printing with the current nozzle may produce an extra %0.2f g of waste." msgstr "Печать текущим соплом может привести к дополнительным затратам %0.2f г материала." -# AI Translated #, c-format, boost-format msgid "Tips: the filament type(%s) does not match with the filament type(%s) in the slicing file. If you want to use this slot, you can install %s instead of %s and change slot information on the 'Device' page." -msgstr "Совет: тип материала (%s) не совпадает с типом материала (%s) в файле нарезки. Если вы хотите использовать этот слот, установите %s вместо %s и измените информацию о слоте на странице «Устройство»." +msgstr "Совет: тип материала (%s) не совпадает с типом материала (%s) в файле нарезки. Если вы хотите использовать этот слот, установите %s вместо %s и измените информацию о слоте на странице «Принтер»." -# AI Translated #, c-format, boost-format msgid "Cannot select: the filament type(%s) does not match with the filament type(%s) in the slicing file. If you want to use this slot, you can install %s instead of %s and change slot information on the 'Device' page." -msgstr "Невозможно выбрать: тип материала (%s) не совпадает с типом материала (%s) в файле нарезки. Если вы хотите использовать этот слот, установите %s вместо %s и измените информацию о слоте на странице «Устройство»." +msgstr "Невозможно выбрать: тип материала (%s) не совпадает с типом материала (%s) в файле нарезки. Если вы хотите использовать этот слот, установите %s вместо %s и измените информацию о слоте на странице «Принтер»." -# AI Translated #, c-format, boost-format msgid "Cannot select: the slot is empty or undefined. If you want to use this slot, you can install %s and change slot information on the 'Device' page." -msgstr "Невозможно выбрать: слот пуст или не определён. Если вы хотите использовать этот слот, установите %s и измените информацию о слоте на странице «Устройство»." +msgstr "Невозможно выбрать: слот пуст или не определён. Если вы хотите использовать этот слот, установите %s и измените информацию о слоте на странице «Принтер»." -# AI Translated msgid "Cannot select: No filament loaded in current slot." msgstr "Невозможно выбрать: в текущий слот не загружен материал." @@ -4451,12 +4387,10 @@ msgstr "Использовать для печати материал с вне msgid "Print with filament in AMS" msgstr "Печать материалом из AMS" -# AI Translated msgctxt "Nozzle position" msgid "Left" msgstr "Левое" -# AI Translated msgctxt "Nozzle position" msgid "Right" msgstr "Правое" @@ -4526,7 +4460,7 @@ msgid "Update remaining capacity" msgstr "Обновлять оставшуюся ёмкость катушки" msgid "AMS will attempt to estimate the remaining capacity of the Bambu Lab filaments." -msgstr "AMS попытается оценить оставшееся количество филаментов Bambu Lab." +msgstr "AMS будет пытаться оценивать оставшееся количество материалов Bambu Lab." msgid "AMS filament backup" msgstr "Резервирование материала AMS" @@ -4789,6 +4723,23 @@ msgstr "Текущая температура внутри термокамер msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Стартовая температура внутри термокамеры (%d℃) превышает целевую (%d℃). Подразумевается, что печать начинается заранее, поэтому стартовая температура не должна превышать её. Значение будет уменьшено." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "Высота слоя слишком мала. Будет установлено минимальное значение (%g мм)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "Высота слоя выходит за пределы, заданные в настройках принтера → Экструдер → Ограничение высоты слоя. Это может вызвать проблемы с качеством печати." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Автоматически подстроить под предел (%g мм)?" + +msgid "Adjust" +msgstr "Подстроиться" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4808,9 +4759,9 @@ msgid "" "\n" "The first layer height will be reset to 0.2." msgstr "" -"Нулевая высота начального слоя недопустима.\n" +"Нулевая высота первого слоя недопустима.\n" "\n" -"Высота первого слоя будет сброшена до 0.2." +"Значение будет сброшено до 0,2." # Оба первых предложения легко объединяются в одно. msgid "" @@ -4837,7 +4788,7 @@ msgstr "" "\n" "Значение будет сброшено до 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Чередующаяся дополнительная стенка не работает, если для «Сохранение толщины вертикальной оболочки» установлено значение «Все»." msgid "" @@ -4890,7 +4841,7 @@ msgstr "" "Нет – отключить черновую башню" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "Начальная высота не должна превышать высоту слоя.\n" @@ -4898,7 +4849,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "Перекрытие должно быть меньше внутренней оболочки.\n" @@ -4913,6 +4864,13 @@ msgid "" "No - Disable Arachne Wall Generator and set [Displacement] mode of the Fuzzy Skin" msgstr "Использовать нечёткую оболочку с движком Arachne?" +# AI Translated +msgid "Brim ear radius" +msgstr "Радиус ушек каймы" + +msgid "Brim width" +msgstr "Ширина каймы" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "" "Для печати в режиме вазы необходимы следующие настройки:\n" @@ -5050,9 +5008,9 @@ msgstr "Измерение точности движений" msgid "Enhancing motion precision" msgstr "Улучшение точности движений" -# ??? Измерение точности позиционирования +# Относится к motion accuracy points – точкам фактического положения головы на сетке с эталонными координатами msgid "Measure motion accuracy" -msgstr "Измерение точности перемещения" +msgstr "Измерение точности позиционирования" msgid "Nozzle offset calibration" msgstr "Калибровка смещения сопла" @@ -5061,30 +5019,30 @@ msgid "High temperature auto bed leveling" msgstr "Измерение кривизны разогретого стола" msgid "Auto Check: Quick Release Lever" -msgstr "Автопроверка: быстросъёмный рычаг" +msgstr "Проверка: быстросъёмный модуль" msgid "Auto Check: Door and Upper Cover" -msgstr "Автопроверка: дверца и верхняя крышка" +msgstr "Проверка: дверца и верхняя крышка" msgid "Laser Calibration" msgstr "Калибровка лазера" msgid "Auto Check: Platform" -msgstr "Автопроверка: платформа" +msgstr "Проверка: стол" -# ??? Подтверждение положения камеры msgid "Confirming BirdsEye Camera location" -msgstr "Подтверждение расположения камеры BirdsEye" +msgstr "Подтверждение положения камеры" # ??? Калибровка ракурса камеры msgid "Calibrating BirdsEye Camera" msgstr "Калибровка камеры BirdsEye" +# Нет здесь никакого выравнивания, у бамбуков даже крутилок нет. Это просто снятие карты высот. msgid "Auto bed leveling -phase 1" -msgstr "Автоматическое выравнивание стола — фаза 1" +msgstr "Измерение кривизны стола — фаза 1" msgid "Auto bed leveling -phase 2" -msgstr "Автоматическое выравнивание стола — фаза 2" +msgstr "Измерение кривизны стола — фаза 2" msgid "Heating chamber" msgstr "Нагрев камеры" @@ -5096,19 +5054,22 @@ msgid "Printing calibration lines" msgstr "Печать калибровочных линий" msgid "Auto Check: Material" -msgstr "Автопроверка: материал" +msgstr "Проверка: материал" +# https://wiki.bambulab.com/ru/h2/troubleshooting/hmscode/0C00_0300_0002_0014#:~:text=калибровку%20камеры%20реального%20времени,-: msgid "Live View Camera Calibration" -msgstr "Калибровка камеры Live View" +msgstr "Калибровка камеры реального времени" msgid "Waiting for heatbed to reach target temperature" msgstr "Ожидание нагрева стола" +# у H2D система определяет, где лежит обрезок листа. Но может также относиться и к 3D-гравировке, где определяется расстояние до поверхности. msgid "Auto Check: Material Position" -msgstr "Автопроверка: положение материала" +msgstr "Проверка: положение заготовки" +# https://wiki.bambulab.com/ru/h2/troubleshooting/hmscode/0500_0400_0002_0037 msgid "Cutting Module Offset Calibration" -msgstr "Калибровка смещения режущего модуля" +msgstr "Калибровка смещения модуля резки" msgid "Measuring Surface" msgstr "Измерение поверхности" @@ -5141,7 +5102,7 @@ msgid "Timelapse is not supported while the storage is readonly." msgstr "Запись таймлапсов невозможна на защищённый от записи накопитель." msgid "To ensure your safety, certain processing tasks (such as laser) can only be resumed on printer." -msgstr "В целях безопасности некоторые задачи обработки (например, лазерная) могут быть возобновлены только на принтере." +msgstr "В целях безопасности некоторые виды обработки (например, лазерная) можно возобновить только вручную на принтере." #, c-format, boost-format msgid "The chamber temperature is too high, which may cause the filament to soften. Please wait until the chamber temperature drops below %d℃. You may open the front door or enable fans to cool down." @@ -5179,6 +5140,13 @@ msgid "Calibration error" msgstr "Ошибка калибровки" # AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "На этом принтере не настроено оборудование, необходимое для этого элемента управления." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Этот элемент управления не поддерживается на этом принтере." + msgid "Network unavailable" msgstr "Сеть недоступна" @@ -5194,9 +5162,9 @@ msgstr "Продолжить (проблема решена)" msgid "Stop Printing" msgstr "Остановить печать" -# ??? Перейти к помощнику, Помощник по проверке +# "Ассистент" плохо ложится в "обратитесь к ассистенту", когда речь о руководстве. msgid "Check Assistant" -msgstr "Ассистент проверки" +msgstr "Помощник проверки" msgid "Filament Extruded, Continue" msgstr "Пруток выдавлен, продолжить" @@ -5217,9 +5185,9 @@ msgstr "Просмотр трансляции" msgid "No Reminder Next Time" msgstr "Больше не спрашивать" -# AI Translated +# Перепроверить msgid "Recheck" -msgstr "Проверить снова" +msgstr "Перепроверить" msgid "Ignore. Don't Remind Next Time" msgstr "Игнорировать и больше не спрашивать" @@ -5231,7 +5199,7 @@ msgid "Problem Solved and Resume" msgstr "Проблема решена, продолжить" msgid "Got it, Turn off the Fire Alarm." -msgstr "Понятно, выключить пожарную сигнализацию." +msgstr "Ясно, выключить пожарную сигнализацию." msgid "Retry (problem solved)" msgstr "Повторить (проблема решена)" @@ -5242,15 +5210,12 @@ msgstr "Остановить сушку" msgid "Proceed" msgstr "Продолжить" -# AI Translated msgid "Abort" msgstr "Прервать" -# AI Translated msgid "Disable Purification for This Print" msgstr "Отключить очистку воздуха для этой печати" -# AI Translated msgid "Don't Remind Me" msgstr "Не напоминать" @@ -5263,7 +5228,6 @@ msgstr "Продолжить" msgid "Unknown error." msgstr "Неизвестная ошибка." -# AI Translated msgid "Loading ..." msgstr "Загрузка ..." @@ -5344,8 +5308,9 @@ msgstr "слой(-я)" msgid "Range" msgstr "Диапазон" +# Подставляется в "По умолчанию" в подсказке к параметрам. Не уверен, нужно ли здесь "строка", может просто "пусто"? msgid "Empty string" -msgstr "Пустая строка" +msgstr "пустая строка" msgid "Value is out of range." msgstr "Введённое значение вне диапазона." @@ -5731,7 +5696,7 @@ msgid "Fan speed (%)" msgstr "Скорость вентилятора (%)" msgid "Temperature (℃)" -msgstr "Температура (°C)" +msgstr "Температура (℃)" msgid "Volumetric flow rate (mm³/s)" msgstr "Объёмный расход (мм³/с)" @@ -5815,9 +5780,8 @@ msgid "Adaptive" msgstr "Адаптировать" msgid "Quality / Speed" -msgstr "Качество/Скорость" +msgstr "Качество/скорость" -# AI Translated msgctxt "Mesh action" msgid "Smooth" msgstr "Сгладить" @@ -5919,7 +5883,6 @@ msgstr "Избегать зону калибровки экструзии" msgid "Align to Y axis" msgstr "Выравнивать по оси Y" -# AI Translated msgctxt "Camera View" msgid "Front" msgstr "Спереди" @@ -5928,13 +5891,11 @@ msgctxt "Camera View" msgid "Back" msgstr "Сзади" -# AI Translated #. TRN To be shown in the main menu View->Top msgctxt "Camera View" msgid "Top" msgstr "Сверху" -# AI Translated #. TRN To be shown in the main menu View->Bottom msgctxt "Camera View" msgid "Bottom" @@ -6007,7 +5968,7 @@ msgstr "Назад" # 2026). Похоже, это внутреннее название кнопки с панелью управления визуалом # пространства моделей. msgid "Canvas Toolbar" -msgstr "Панель инструментов холста" +msgstr "Панель инструментов рабочей области" # Тут баг с переносом строк, каждое слово переносится. Чем короче – тем лучше. msgid "Fit camera to scene or selected object." @@ -6070,7 +6031,7 @@ msgstr "Объём:" msgid "Size:" msgstr "Размер:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "В G-коде на %d слое (z = %.2lf мм) обнаружен конфликт путей. Пожалуйста, разместите конфликтующие модели дальше друг от друга (%s <-> %s)." @@ -6138,7 +6099,7 @@ msgid "PLA and PETG filaments detected in the mixture. Adjust parameters accordi msgstr "Обнаружено совместное использование PLA и PETG. Для повышения качества рекомендуется настроить печать в соответствии с" msgid "The prime tower extends beyond the plate boundary." -msgstr "Башня очистки выходит за пределы области печати." +msgstr "Черновая башня выходит за пределы области печати." # После строки подставляется кнопка настройки msgid "Partial flushing volume set to 0. Multi-color printing may cause color mixing in models. Please readjust flushing settings." @@ -6231,15 +6192,16 @@ msgid "" "You can find it in \"Settings > Network > Access code\"\n" "on the printer, as shown in the figure:" msgstr "" -"Вы можете найти его на принтере в разделе \n" -"Настройки > Сеть > Код подключения, как показано на рисунке:" +"Его можно найти на принтере в разделе\n" +"«Настройки» > «Сеть» > «Код подключения», как показано на рисунке:" msgid "" "You can find it in \"Setting > Setting > LAN only > Access Code\"\n" "on the printer, as shown in the figure:" msgstr "" -"Вы можете найти его в «Настройки > Настройки > Только LAN > Код доступа»\n" -"на принтере, как показано на рисунке:" +"Его можно найти на принтере в разделе\n" +"«Настройки» > «Настройки» > «Только LAN» > «Код доступа»,\n" +"как показано на рисунке:" msgid "Invalid input" msgstr "Неверный ввод" @@ -6276,6 +6238,10 @@ msgstr "Принтеры" msgid "Project" msgstr "Проект" +# AI Translated +msgid "Device (Web)" +msgstr "Принтер (веб)" + msgid "Yes" msgstr "Да" @@ -6283,7 +6249,7 @@ msgid "No" msgstr "Нет" msgid "will be closed before creating a new model. Do you want to continue?" -msgstr "будет закрыт перед созданием новой модели. Продолжить?" +msgstr "будет закрыт перед созданием нового проекта. Продолжить?" # Возможно, имеет смысл убрать тут "стол". Подавляющее большинство проектов – # это размещение моделей и их печать на одном столе, поэтому намёк на работу с @@ -6372,7 +6338,6 @@ msgstr "Вид снизу" msgid "Front View" msgstr "Вид спереди" -# AI Translated msgctxt "Camera View" msgid "Rear" msgstr "Сзади" @@ -6419,10 +6384,10 @@ msgstr "Импорт 3MF/STL/STEP/SVG/OBJ/AMF" msgid "Load a model" msgstr "Загрузка модели" -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "Импорт ZIP-архива" -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "Загрузка моделей, содержащихся в ZIP-архиве" msgid "Import Configs" @@ -6589,7 +6554,6 @@ msgstr "Отображение контура вокруг выбранных м msgid "Preferences" msgstr "Настройки" -# AI Translated msgctxt "Menu" msgid "Edit" msgstr "Правка" @@ -7009,7 +6973,7 @@ msgstr "" "Если накопитель не определяется, попробуйте отформатировать его." msgid "The firmware version of the printer is too low. Please update the firmware and try again." -msgstr "Версия прошивки принтера слишком старая. Пожалуйста, обновите прошивку и попробуйте снова." +msgstr "Слишком старая версия прошивки принтера. Пожалуйста, обновите прошивку и попробуйте снова." msgid "The file already exists, do you want to replace it?" msgstr "Файл уже существует, заменить?" @@ -7217,7 +7181,6 @@ msgstr "Настройки печати" msgid "Safety Options" msgstr "Настройки защиты" -# AI Translated msgid "Hotends" msgstr "Хотэнды" @@ -7254,11 +7217,9 @@ msgstr "Во время паузы смена материала поддерж msgid "Current extruder is busy changing filament." msgstr "В экструдере производится смена материала." -# AI Translated msgid "\"Load\" or \"Unload\" is not supported for external spool while using Filament Track Switch." msgstr "«Загрузка» и «Выгрузка» не поддерживаются для внешней катушки при использовании Filament Track Switch." -# AI Translated msgid "The Filament Track Switch has not been setup. Please setup on printer." msgstr "Filament Track Switch не настроен. Выполните настройку на принтере." @@ -7268,10 +7229,9 @@ msgstr "Слот уже занят." msgid "The selected slot is empty." msgstr "Выбранный слот пуст." -# Так и не нашёл, что это за 2D-режим такой. Выводится в пояснении к -# отключённой функции. +# Так и не нашёл, что это за 2D-режим такой. Выводится в пояснении к отключённой кнопке калибровки. Вероятно, блокировка калибровок печати у H2D при работе с лазерной (и не только) резкой msgid "Printer 2D mode does not support 3D calibration" -msgstr "2D-режим принтера не поддерживает 3D-калибровку" +msgstr "3D-калиброка недоступна в 2D-режиме" msgid "Downloading..." msgstr "Загрузка..." @@ -7298,7 +7258,7 @@ msgid "Chamber temperature cannot be changed in cooling mode while printing." msgstr "Температуру камеры нельзя изменить при печати в режиме «Охлаждение»." msgid "If the chamber temperature exceeds 40℃, the system will automatically switch to heating mode. Please confirm whether to switch." -msgstr "Если температура камеры превысит 40℃, система автоматически переключится в режим нагрева. Подтвердите переключение." +msgstr "Если температура камеры превышает 40℃, система автоматически переключится в режим нагрева. Подтвердите переключение." msgid "Please select an AMS slot before calibration" msgstr "Пожалуйста, выберите слот AMS перед калибровкой" @@ -7440,6 +7400,7 @@ msgctxt "Firmware" msgid "Update" msgstr "Обновление" +# https://wiki.bambulab.com/ru/hms/home msgid "Assistant(HMS)" msgstr "Помощник (HMS)" @@ -7653,7 +7614,7 @@ msgid "Serious warning:" msgstr "Серьёзное предупреждение:" msgid " (Repair)" -msgstr " (Восстановить)" +msgstr "(Восстановить)" msgid " Click here to install it." msgstr " Нажмите здесь, чтобы установить." @@ -7701,33 +7662,27 @@ msgctxt "Layers" msgid "Bottom" msgstr "Снизу" -# AI Translated msgid "Plugin Selection" -msgstr "Выбор плагина" +msgstr "Выбор плагинов" -# AI Translated msgid "" "No plugins capabilities available for this type.\n" "Enable or install some to use." msgstr "" -"Для этого типа нет доступных возможностей плагинов.\n" +"Плагины с требуемым функционалом отсутствуют для этого типа.\n" "Включите или установите их для использования." -# AI Translated msgid "There is stringing-prone filament in the current print job. Enabling nozzle clumping detection now may degrade print quality. Are you sure you want to enable it?" -msgstr "В текущем задании печати есть материал, склонный к образованию волос. Включение обнаружения налипания на сопло сейчас может ухудшить качество печати. Вы уверены, что хотите включить его?" +msgstr "В текущем проекте есть материал, склонный к образованию паутины. Работа проверки налипаний на сопле сейчас может ухудшить качество печати. Вы действительно хотите включить её?" -# AI Translated msgid "Enable Nozzle Clumping Detection" msgstr "Включить обнаружение налипания на сопло" -# AI Translated msgid "When enabled, the printer will automatically capture photos of printed parts and upload them to the cloud. Would you like to enable this option?" -msgstr "Когда включено, принтер будет автоматически делать фотографии печатаемых деталей и загружать их в облако. Хотите включить эту опцию?" +msgstr "Принтер будет автоматически делать фотографии печатаемых деталей и загружать их в облако. Включить эту настройку?" -# AI Translated msgid "Confirm Enable Print Status Snapshot" -msgstr "Подтвердите включение снимков состояния печати" +msgstr "Подтверждение активации снимков состояния печати" msgid "Enable detection of build plate position" msgstr "Определение положения покрытия" @@ -7741,28 +7696,23 @@ msgstr "Обнаружение покрытия стола" msgid "Identifies the type and position of the build plate on the heatbed. Pausing printing if a mismatch is detected." msgstr "Определение типа и положения покрытия стола. В случае обнаружения смещения печать приостанавливается." -# AI Translated msgid "Purifies the chamber air as the print finishes, based on the selected mode." msgstr "Очищает воздух в камере по завершении печати в соответствии с выбранным режимом." -# AI Translated msgid "Purifies the chamber air through internal circulation as each print finishes." msgstr "Очищает воздух в камере за счёт внутренней циркуляции по завершении каждой печати." -# AI Translated msgid "Automatically match the corresponding switch strategy for leak-prone filaments (disable blob detection) and regular filaments (enable blob detection)." -msgstr "Автоматически подбирает соответствующую стратегию переключения для склонных к вытеканию материалов (отключает обнаружение налипаний) и обычных материалов (включает обнаружение налипаний)." +msgstr "Автоматически подбирает соответствующую стратегию для склонных к подтёкам материалов (отключает обнаружение налипаний) и обычных материалов (включает обнаружение налипаний)." -# AI Translated msgid "Detect whether the nozzle is wrapped by filament or other foreign matter." -msgstr "Определяет, обмотано ли сопло материалом или иными посторонними частицами." +msgstr "Определяет налипший на сопло пластик или иные объекты." -# AI Translated msgid "After disabling, nozzle wrapping cannot be detected, which may lead to print failure or nozzle damage." -msgstr "После отключения обмотку сопла нельзя будет обнаружить, что может привести к сбою печати или повреждению сопла." +msgstr "После отключения налипший пластик на сопле нельзя будет обнаружить, что может привести к сбою печати или повреждению сопла." msgid "AI Detections" -msgstr "ИИ-обнаружение" +msgstr "ИИ-мониторинг" msgid "Printer will send assistant message or pause printing if any of the following problem is detected." msgstr "Принтер отправит сообщение помощника или приостановит печать, если обнаружит одну из следующих проблем." @@ -7774,27 +7724,27 @@ msgstr "Контроль печати с помощью ИИ" msgid "Pausing Sensitivity:" msgstr "Чувствительность:" +# Печать воздухом/в воздухе – другой частный случай при перехлёсте прутка или заторе msgid "Spaghetti Detection" -msgstr "Обнаружение «спагетти»" +msgstr "Обнаружение слетевших моделей" msgid "Detect spaghetti failures (scattered lose filament)." -msgstr "Обнаружение дефектов типа «спагетти» (разбросанная нить)." +msgstr "Обнаружение «спагетти» при печати в воздухе." msgid "Purge Chute Pile-Up Detection" -msgstr "Обнаружение скопления в лотке очистки" +msgstr "Обнаружение заполнения лотка прочистки" msgid "Monitor if the waste is piled up in the purge chute." -msgstr "Контроль скопления отходов в лотке очистки." +msgstr "Контроль скопления отходов в лотке прочистки." -# ???протечки, засорения msgid "Nozzle Clumping Detection" msgstr "Обнаружение пластика на сопле" msgid "Check if the nozzle is clumping by filaments or other foreign objects." -msgstr "Определение налипшего на сопле пластика или иных объектов." +msgstr "Определение налипшего на сопло пластика или иных объектов." msgid "Detects air printing caused by nozzle clogging or filament grinding." -msgstr "Определение холостой печати из-за затора или перетирания прутка." +msgstr "Определение холостой печати из-за затора или перехлёста/перетирания прутка." msgid "First Layer Inspection" msgstr "Проверка первого слоя" @@ -7805,9 +7755,8 @@ msgstr "Автовосстановление после смещения сло msgid "Store Sent Files on External Storage" msgstr "Сохранять файлы печати на внешнем накопителе" -# AI Translated msgid "Save the printing files sent from the slicer and other apps on External Storage" -msgstr "Сохранять файлы печати, отправленные из слайсера и других приложений, на внешнем накопителе" +msgstr "Сохранять на внешнем накопителе файлы печати, отправленные из слайсера и других приложений" msgid "Allow Prompt Sound" msgstr "Разрешить звуковые уведомления" @@ -7818,41 +7767,32 @@ msgstr "Обнаружение запутывания прутка" msgid "Check if the nozzle is clumping by filament or other foreign objects." msgstr "Обнаружение скапливания на сопле материала в результате засорения/протечки сопла или других причин." -# AI Translated msgid "Purify Air at Print End" msgstr "Очищать воздух по завершении печати" -# AI Translated msgid "Internal Circulation" msgstr "Внутренняя циркуляция" -# AI Translated msgid "Alignment Detection" msgstr "Обнаружение смещения" -# AI Translated msgid "Pauses printing when build plate misalignment is detected." msgstr "Приостанавливает печать при обнаружении смещения стола." -# AI Translated msgid "Foreign Object Detection" msgstr "Обнаружение посторонних предметов" -# AI Translated msgid "Checks for any objects on the build plate at the start of a print to avoid collisions." msgstr "Проверяет наличие любых предметов на столе в начале печати во избежание столкновений." -# AI Translated msgid "Printed Part Displacement Detection" msgstr "Обнаружение смещения печатаемой детали" -# AI Translated msgid "Monitors the printed part during printing and alerts immediately if it shifts or collapses." -msgstr "Отслеживает печатаемую деталь во время печати и немедленно предупреждает, если она сместилась или обрушилась." +msgstr "Отслеживает положение детали во время печати и немедленно предупреждает, если она сместилась или обрушилась." -# AI Translated msgid "Checks if the nozzle is clumping by filament or other foreign objects." -msgstr "Проверяет, не залипло ли сопло материалом или иными посторонними предметами." +msgstr "Проверяет присутствие пластика или инородных объектов на сопле." msgid "On" msgstr "Вкл" @@ -7866,13 +7806,11 @@ msgstr "Уведомление" msgid "Pause printing" msgstr "Пауза печати" -# AI Translated msgid "Print Status Snapshot" msgstr "Снимок состояния печати" -# AI Translated msgid "Automatically capture and upload print photos, showing defects during printing and the final result for remote viewing." -msgstr "Автоматически делает и загружает фотографии печати, показывая дефекты во время печати и итоговый результат для удалённого просмотра." +msgstr "Автоматически снимает и загружает в облако фотографии печати, показывая дефекты во время печати и итоговый результат для удалённого просмотра." msgctxt "Nozzle Type" msgid "Type" @@ -7887,7 +7825,7 @@ msgid "Flow" msgstr "Расход" msgid "Please change the nozzle settings on the printer." -msgstr "Пожалуйста, измените настройки сопла на принтере." +msgstr "Измените настройки сопла на принтере." msgid "Brass" msgstr "Латунь" @@ -7895,7 +7833,6 @@ msgstr "Латунь" msgid "High flow" msgstr "Высокий расход" -# AI Translated msgid "TPU High flow" msgstr "TPU High flow" @@ -7918,9 +7855,8 @@ msgstr "Общие" msgid "Objects" msgstr "Модели" -# AI Translated msgid "Cycle settings visibility" -msgstr "Переключать видимость настроек" +msgstr "Переключить видимость настроек" msgid "Compare presets" msgstr "Сравнить профили" @@ -7963,6 +7899,10 @@ msgstr "Настроить стол" msgid "The %s nozzle can not print %s." msgstr "Внимание: «%s» экструдер не может печатать %s." +# AI Translated +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "Печать материала смешанного цвета на принтере с одним экструдером требует частой смены материала и прочистки, что может значительно увеличить количество отходов и риск засорения сопла или жёлоба для отходов." + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "Совмещение %1% с %2% при печати не рекомендуется.\n" @@ -8033,11 +7973,9 @@ msgstr "Переключение диаметра" msgid "Configuration incompatible" msgstr "Несовместимый профиль" -# AI Translated msgid "Filament switcher detected. All AMS filaments are now available for both extruders. The slicer will auto-assign for optimal printing." -msgstr "Обнаружен переключатель материалов. Все материалы AMS теперь доступны для обоих экструдеров. Слайсер автоматически распределит их для оптимальной печати." +msgstr "Обнаружен переключатель материалов. Все материалы из AMS теперь доступны для обоих экструдеров. Слайсер автоматически распределит их для оптимальной печати." -# AI Translated msgid "A filament switcher is detected but not calibrated and thus currently unavailable. Please calibrate it on the printer and synchronize before use." msgstr "Обнаружен переключатель материалов, но он не откалиброван и потому сейчас недоступен. Откалибруйте его на принтере и синхронизируйте перед использованием." @@ -8052,12 +7990,13 @@ msgid "" "The currently selected machine preset is inconsistent with the connected printer type.\n" "Are you sure to continue syncing?" msgstr "" -"Текущий выбранный профиль принтера не соответствует типу подключённого принтера.\n" +"Выбранный профиль принтера не соответствует типу подключённого принтера.\n" "Продолжить синхронизацию?" msgid "There are unset nozzle types. Please set the nozzle types of all extruders before synchronizing." msgstr "Типы сопел не заданы. Перед синхронизацией необходимо указать типы всех установленных сопел." +# О сопле/соплах? msgid "Sync extruder infomation" msgstr "Синхронизировать информацию об экструдере" @@ -8071,12 +8010,11 @@ msgid "Click to edit preset" msgstr "Изменить профиль" msgid "Nozzle" -msgstr "Экструдер" +msgstr "Сопло" msgid "Project Filaments" msgstr "Материалы проекта" -# AI Translated msgid "Purge mode" msgstr "Режим прочистки" @@ -8095,15 +8033,48 @@ msgstr "Синхронизировать материалы" msgid "Set filaments to use" msgstr "Выбрать материал" +# AI Translated +msgid "Add Mixed Filament" +msgstr "Добавить смешанный материал" + +# AI Translated +msgid "Mixed Filament" +msgstr "Смешанный материал" + +# AI Translated +msgid "Remove last mixed filament" +msgstr "Удалить последний смешанный материал" + +# AI Translated +msgid "Add mixed filament" +msgstr "Добавить смешанный материал" + +# AI Translated +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "У смешанного материала недопустимые или несовпадающие компоненты. Пожалуйста, отредактируйте затронутые записи заново." + msgid "Search plate, object and part." msgstr "Поиск стола, модели или части..." msgid "Pellets" msgstr "Гранулы" +# AI Translated +msgid "Mixed filament has broken component references" +msgstr "У смешанного материала нарушены ссылки на компоненты" + +# AI Translated +msgid "Edit / Delete / Merge" +msgstr "Изменить / Удалить / Объединить" + +# AI Translated +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "Целевой смешанный материал использует этот физический материал в качестве компонента. Объединение удалит этот физический материал и может сделать смешанный материал недействительным. Продолжить?" + +# Порядок слов сильно зависит от контекста; не могу воспроизвести в интерфейсе. По идее, выводится при bool Sidebar::is_new_project_in_gcode3mf(), но что-либо менять в нарезанном .gcode.3mf вообще нельзя #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." -msgstr "После завершения операции проект %s будет закрыт и создан новый проект." +msgstr "После завершения операции и закрытия текущего проекта %s будет создан новый проект." msgid "There are no compatible filaments, and sync is not performed." msgstr "Синхронизация не выполнена ввиду отсутствия совместимых материалов." @@ -8120,7 +8091,7 @@ msgid "Only filament color information has been synchronized from printer." msgstr "Синхронизирована только информация о цвете материала." msgid "Filament type and color information have been synchronized, but slot information is not included." -msgstr "Информация о типе и цвете филамента синхронизирована, но информация о слотах не включена." +msgstr "Синхронизирована информация о типе и цвете материала, но не о слотах." #, boost-format msgid "Do you want to save changes to \"%1%\"?" @@ -8241,8 +8212,8 @@ msgstr "Во избежание повреждения принтера убед msgid "Customized Preset" msgstr "Пользовательский профиль" -msgid "Component name(s) inside step file not in UTF8 format!" -msgstr "Имена компонентов внутри файла STEP не в формате UTF8." +msgid "Component name(s) inside step file not in UTF-8 format!" +msgstr "Имена компонентов внутри файла STEP не в формате UTF-8." msgid "Because of unsupported text encoding, garbage characters may appear!" msgstr "Из-за неподдерживаемой кодировки в названии могут присутствовать ошибочные символы." @@ -8263,7 +8234,7 @@ msgstr "Объём модели равен нулю" #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "Модель из файла %s слишком мала. Возможно, её размеры \n" "в метрах или дюймах. Внутренней единицей программы \n" @@ -8284,6 +8255,14 @@ msgstr "" msgid "Multi-part object detected" msgstr "Обнаружена модель, состоящая из нескольких частей" +# AI Translated +msgid "Matching textures to filaments" +msgstr "Сопоставление текстур с материалами" + +# AI Translated +msgid "Texture Import Warning" +msgstr "Предупреждение при импорте текстуры" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Загрузить эти файлы как единую модель, состоящую из нескольких частей?\n" @@ -8355,22 +8334,18 @@ msgstr "" "Это действие приведёт к удалению информации о разрезе.\n" "Целостность модели после этого не гарантируется." -# AI Translated msgid "Delete Object" msgstr "Удалить модель" -# AI Translated msgid "Delete All Objects" msgstr "Удалить все модели" -# AI Translated msgid "Reset Project" msgstr "Сбросить проект" msgid "The selected object couldn't be split." msgstr "Невозможно разделить выбранную модель." -# AI Translated msgid "Split to Objects" msgstr "Разделить на модели" @@ -8399,9 +8374,8 @@ msgstr "Выбор нового файла" msgid "File for the replacement wasn't selected" msgstr "Файл для замены не выбран" -# AI Translated msgid "Replace with 3D file" -msgstr "Заменить 3D-файлом" +msgstr "Замена файла модели" # В заголовке окна выбора папки и ошибки "папка не найдена" msgid "Select folder to replace from" @@ -8413,21 +8387,21 @@ msgstr "Расположение для замены не указано" msgid "Replaced with 3D files from directory:\n" msgstr "Заменено файлами из расположения:\n" -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ Пропущен %1%: идентичный файл.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ Пропущен %s: идентичный файл.\n" -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ Пропущен %1%: файл не существует.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ Пропущен %s: файл не существует.\n" -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ Пропущен %1%: не удалось заменить.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ Пропущен %s: не удалось заменить.\n" -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔ Заменён %1%.\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔ Заменён %s.\n" msgid "Replaced volumes" msgstr "Модели заменены" @@ -8450,7 +8424,6 @@ msgstr "Не удалось перезагрузить:" msgid "Error during reload" msgstr "Ошибка во время перезагрузки" -# AI Translated msgid "Reload all" msgstr "Перезагрузить всё" @@ -8497,13 +8470,29 @@ msgid "" "After syncing, software can optimize printing time and filament usage when slicing.\n" "Would you like to sync now?" msgstr "" -"Информация о типе сопла и количестве AMS не синхронизирована с подключённого принтера.\n" -"После синхронизации программа сможет оптимизировать время печати и расход филамента при нарезке.\n" -"Хотите синхронизировать сейчас?" +"Синхронизация информации о типе сопла и количестве AMS подключённого принтера не выполнена.\n" +"После синхронизации программа сможет оптимизировать время печати и расход материала при нарезке.\n" +"Выполнить синхронизацию?" msgid "Sync now" msgstr "Синхронизировать" +# AI Translated +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "Не удалось импортировать текстуру. Похоже, модель содержит данные текстуры, но завершить импорт не удалось. Модель будет импортирована только как геометрия." + +# AI Translated +msgid "Applying texture colors..." +msgstr "Применение цветов текстуры..." + +# AI Translated +msgid "Updating 3D view..." +msgstr "Обновление 3D-вида..." + +# AI Translated +msgid "Texture colors applied." +msgstr "Цвета текстуры применены." + msgid "You can keep the modified presets for the new project or discard them" msgstr "Вы можете перенести сделанные изменения в новый проект или отказаться от их сохранения" @@ -8549,10 +8538,10 @@ msgid "INFO:" msgstr "Информация:" msgid "No accelerations provided for calibration. Use default acceleration value " -msgstr "Не заданы ускорения для калибровки. Использовать значение ускорения по умолчанию " +msgstr "Не заданы ускорения для калибровки. Используется значение по умолчанию: " msgid "No speeds provided for calibration. Use default optimal speed " -msgstr "Не заданы скорости для калибровки. Использовать оптимальную скорость по умолчанию " +msgstr "Не заданы скорости для калибровки. Используется значение по умолчанию: " msgid "Import SLA archive" msgstr "Импорт SLA архива" @@ -8664,17 +8653,15 @@ msgstr "Причина: «%1%» не имеет пересечений с дру msgid "Unable to perform boolean operation on model meshes. Only positive parts will be exported." msgstr "Невозможно выполнить булеву операцию над сетками модели. Будут экспортированы только положительные части." -# AI Translated msgid "Flashforge host is not available." msgstr "Хост Flashforge недоступен." # Авторизация принтера Flashforge не удалась. -# AI Translated msgid "Unable to log in to the Flashforge printer." -msgstr "Не удалось войти в принтер Flashforge." +msgstr "Не удалось авторизоваться в панели управления Flashforge." msgid "Is the printer ready? Is the print sheet in place, empty and clean?" -msgstr "Готов ли Принтер? Печатная пластина на месте, пустая и чистая?" +msgstr "Готов ли принтер? Проверьте установку и состояние покрытия стола." msgid "Upload and Print" msgstr "Загрузить и напечатать" @@ -8729,47 +8716,36 @@ msgstr "«Принтер»" msgid "Synchronize AMS Filament Information" msgstr "Синхронизировать материалы в AMS" -# AI Translated msgid "OrcaCloud plugins required by the current preset are not installed:" -msgstr "Плагины OrcaCloud, необходимые для текущего профиля, не установлены:" +msgstr "Для текущего профиля требуется установка плагинов OrcaCloud:" -# AI Translated msgid "Install Plugins" msgstr "Установить плагины" -# AI Translated msgid "Local plugins required by the current preset are missing:" -msgstr "Отсутствуют локальные плагины, необходимые для текущего профиля:" +msgstr "Для текущего профиля требуются локальные плагины:" -# AI Translated msgid "Find on OrcaCloud" msgstr "Найти в OrcaCloud" -# AI Translated msgid "Plugins required by the current preset are not activated:" -msgstr "Плагины, необходимые для текущего профиля, не активированы:" +msgstr "Для текущего профиля требуются неактивные плагины:" -# AI Translated msgid "Activate Now" -msgstr "Активировать сейчас" +msgstr "Активировать" -# AI Translated msgid "The installed plugin does not provide the required capability — it may be outdated:" -msgstr "Установленный плагин не предоставляет требуемую возможность — возможно, он устарел:" +msgstr "В установленном плагине отсутствует нужный функционал (устаревшая версия?):" -# AI Translated msgid "Preparing to install plugins..." msgstr "Подготовка к установке плагинов..." -# AI Translated msgid "Installing plugins" msgstr "Установка плагинов" -# AI Translated msgid "Cancelling — finishing the current plugin..." msgstr "Отмена — завершение текущего плагина..." -# AI Translated #, boost-format msgid "Installing %1%..." msgstr "Установка %1%..." @@ -8813,9 +8789,8 @@ msgstr "Объём: %1% мм³\n" msgid "Triangles: %1%\n" msgstr "Треугольников: %1%\n" -# AI Translated msgid "Use \"Fix Model\" to repair the mesh." -msgstr "Используйте «Исправить модель» для восстановления сетки." +msgstr "Используйте «Восстановить» для исправления сетки. " #, c-format, boost-format msgid "Plate %d: %s is not suggested for use printing filament %s (%s). If you still want to do this print job, please set this filament's bed temperature to a number that is not zero." @@ -9167,7 +9142,19 @@ msgstr "Если включено, вы сможете управлять нес # Запрашивать выбор режима группировки? msgid "Pop up to select filament grouping mode" -msgstr "Всплывающее окно для выбора режима группировки филаментов" +msgstr "Всплывающее окно для выбора режима группировки материалов" + +# AI Translated +msgid "Visible plugin pages" +msgstr "Видимые страницы плагинов" + +# AI Translated +msgid "pages" +msgstr "стр." + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Количество страниц плагинов, отображаемых как закреплённые вкладки, прежде чем остальные страницы будут свёрнуты в выпадающий список на последней вкладке." msgid "Behaviour" msgstr "Автоматизация" @@ -9290,15 +9277,14 @@ msgstr "Графика" msgid "Smooth normals" msgstr "Сглаживание бликов" -# AI Translated msgid "" "Applies smooth normals to the model.\n" "\n" "Requires manual scene reload to take effect (right-click on 3D view → \"Reload All\")." msgstr "" -"Применяет сглаженные нормали к модели.\n" +"Применять сглаживание нормалей моделей.\n" "\n" -"Для вступления в силу требуется ручная перезагрузка сцены (правый клик в 3D-виде → «Перезагрузить всё»)." +"Для применения изменений требуется ручная перезагрузка сцены (контекстное меню стола → «Перезагрузить всё»)." msgid "Phong shading" msgstr "Затенение по Фонгу" @@ -9315,9 +9301,9 @@ msgstr "Применять фоновое затенение в простран msgid "Shadows" msgstr "Тени" -# AI Translated +# Опять переусложнили техническим нюансом. Изначально тени рисовались только на столе, потом это исправили и решили отметить здесь. msgid "Renders cast shadows on the plate, other objects, and each object onto itself in realistic view." -msgstr "Отображает отбрасываемые тени на столе, других моделях и на самой модели в режиме продвинутой графики." +msgstr "Отрисовывать тени в режиме продвинутой графики." msgid "Anti-aliasing" msgstr "Сглаживание" @@ -9379,32 +9365,28 @@ msgstr "Отображать частоту кадров" msgid "Displays current viewport FPS in the top-right corner." msgstr "Выводить частоту кадров рабочего пространства в правом верхнем углу." -# AI Translated msgid "G-code Preview" -msgstr "Предпросмотр G-code" +msgstr "Просмотр нарезки" -# AI Translated msgid "Dim lower layers" -msgstr "Затемнять нижние слои" +msgstr "Затемнять предыдущие слои" -# AI Translated msgid "When scrubbing the layer slider in the sliced preview, render the layers below the current one darkened so that only the layer being viewed is shown at full brightness." -msgstr "При перемещении ползунка слоёв в предпросмотре нарезки слои ниже текущего отображаются затемнёнными, так что на полной яркости показан только просматриваемый слой." +msgstr "Затемнять слои, находящиеся ниже текущего. Просматриваемый слой отображается на полной яркости." -# AI Translated msgid "Dimmed layer brightness" msgstr "Яркость затемнённых слоёв" msgid "%" msgstr "%" -# AI Translated msgid "" "How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n" "99% is barely darkened, 0% renders them black. Capped at 99% because 100% would be the same as disabling the option." msgstr "" -"Насколько ярко отображаются затемнённые слои, когда включена опция «Затемнять нижние слои».\n" -"99% — затемнение почти незаметно, 0% — слои становятся чёрными. Максимум ограничен 99%, так как 100% равносильно отключению опции." +"Процент яркости предыдущих слоёв при включении их затемнения.\n" +"99% — почти полная яркость.\n" +"0% — полное затенение." msgid "Login region" msgstr "Регион входа" @@ -9534,6 +9516,18 @@ msgstr "" "\n" "Примечание: профили остаются недоступными для выбора." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Экспериментально) Использовать агентов принтера вместо хостов печати" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Отправлять задания печати для принтеров, отличных от Bambu, через агентов плагинов принтера вместо классической загрузки на хост печати.\n" +"Если отключено, OrcaSlicer использует прежнее поведение хоста печати." + msgid "Experimental Features" msgstr "Экспериментальные настройки" @@ -9639,7 +9633,6 @@ msgstr "Несовместимые профили" msgid "My Printer" msgstr "Мой принтер" -# AI Translated msgid "AMS filaments" msgstr "Материалы AMS" @@ -9650,7 +9643,7 @@ msgid "AMS filament" msgstr "Материал AMS" msgid "Right filaments" -msgstr "Филаменты правого экструдера" +msgstr "Материалы правого экструдера" msgid "Click to select filament color" msgstr "Изменить цвет" @@ -9661,7 +9654,6 @@ msgstr "Добавить/удалить профиль" msgid "Edit preset" msgstr "Изменить профиль" -# AI Translated msgid "Change extruder color" msgstr "Изменить цвет экструдера" @@ -9705,10 +9697,9 @@ msgstr "Несовместимы" msgid "The selected preset is null!" msgstr "Выбранный профиль пуст!" -# AI Translated msgctxt "Layer range" msgid "End" -msgstr "до конца" +msgstr "конца" msgid "Customize" msgstr "Настроить" @@ -9748,6 +9739,10 @@ msgstr "Режим вазы" msgid "First layer filament sequence" msgstr "Очерёдность материалов на первом слое" +# AI Translated +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "Список материалов содержит смешанные материалы. Пользовательская последовательность материалов не будет применена." + msgid "By Layer" msgstr "Послойно" @@ -9803,9 +9798,25 @@ msgstr "Пользовательский профиль" msgid "Preset Inside Project" msgstr "Профиль внутри проекта" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Копирует в этот профиль все значения, унаследованные от родительского профиля, и удаляет связь наследования. Профили, совместимые только с родительским, могут стать неподдерживаемыми." + msgid "Detach from parent" msgstr "Сделать независимым" +# AI Translated +msgid "Unique preset" +msgstr "Независимый профиль" + +# AI Translated +msgid "Parent preset" +msgstr "Родительский профиль" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Этот профиль не наследуется от другого профиля." + msgid "Name is unavailable." msgstr "Имя недоступно." @@ -9818,10 +9829,14 @@ msgstr "Профиль «%1%» уже существует." #, boost-format msgid "Preset \"%1%\" already exists and is incompatible with the current printer." -msgstr "Профиль «%1%» уже существует и несовместим с текущим принтером." +msgstr "" +"Профиль «%1%» уже существует и\n" +"несовместим с текущим принтером." msgid "Please note that saving will overwrite the current preset." -msgstr "Обратите внимание, что при сохранении произойдёт перезапись текущего профиля." +msgstr "" +"Обратите внимание: при сохранении произойдёт\n" +"перезапись текущего профиля." msgid "The name cannot be the same as a preset alias name." msgstr "Имя не должно совпадать с именем предустановленного профиля." @@ -9883,8 +9898,9 @@ msgstr "Отправка задания на печать" msgid "Not satisfied with the grouping of filaments? Regroup and slice ->" msgstr "Не нравится текущая группировка материалов? Нажмите сюда, чтобы изменить и нарезать заново." +# Подсказка msgid "Manually change external spool during printing for multi-color printing" -msgstr "Ручная смена внешней катушки во время печати для многоцветной печати" +msgstr "Смена внешней катушки вручную во время многоцветной печати" msgid "Multi-color with external" msgstr "Многоцветная печать с внешней катушкой" @@ -9892,23 +9908,23 @@ msgstr "Многоцветная печать с внешней катушкой msgid "Your filament grouping method in the sliced file is not optimal." msgstr "Группировка материалов в файле печати не оптимальна." -# AI Translated msgid "To ensure print quality, the drying temperature will be lowered during printing." -msgstr "Для обеспечения качества печати температура сушки будет снижена во время печати." +msgstr "Температура сушки будет снижена на время печати во избежание проблем с качеством." -# AI Translated msgid "Select timelapse storage location" msgstr "Выберите место хранения таймлапсов" +# Ничего тут не выравнивается, банбуки чисто карту снимают msgid "Auto Bed Leveling" -msgstr "Автоматическое выравнивание стола" +msgstr "Измерение кривизны стола" +# "Пропуск" – это пояснение логики работы режима или призыв к действию? msgid "" "This checks the flatness of heatbed. Leveling makes extruded height uniform.\n" "*Automatic mode: Run a leveling check(about 10 seconds). Skip if surface is fine." msgstr "" -"Проверка ровности нагревательного стола. Выравнивание обеспечивает равномерную высоту экструзии.\n" -"*Автоматический режим: выполнить проверку (около 10 секунд). Пропустить, если поверхность в порядке." +"Снятие карты высот стола. Позволяет обеспечить равномерность высоты слоя.\n" +"*Автоматический режим: выполнить проверку (около 10 секунд). Пропуск, если поверхность в порядке." msgid "Flow Dynamics Calibration" msgstr "Калибровка динамики потока" @@ -9917,24 +9933,22 @@ msgid "" "This process determines the dynamic flow values to improve overall print quality.\n" "*Automatic mode: Skip if the filament was calibrated recently." msgstr "" -"Этот процесс определяет значения динамического потока для улучшения общего качества печати.\n" -"*Автоматический режим: пропустить, если филамент был откалиброван недавно." +"Анализ инертности системы подачи материала для улучшения общего качества печати.\n" +"*Автоматический режим: пропуск, если материал был недавно откалиброван." msgid "Nozzle Offset Calibration" -msgstr "Калибровка смещения сопла" +msgstr "Калибровка смещения сопел" msgid "" "Calibrate nozzle offsets to enhance print quality.\n" "*Automatic mode: Check for calibration before printing. Skip if unnecessary." msgstr "" -"Калибровка смещений сопел для улучшения качества печати.\n" -"*Автоматический режим: проверять калибровку перед печатью. Пропустить, если не требуется." +"Калибровка смещения сопел для улучшения качества печати.\n" +"*Автоматический режим: выполнять калибровку перед печатью по необходимости." -# AI Translated msgid "Shared PA Profile" msgstr "Общий профиль PA" -# AI Translated msgid "Nozzles and filaments of the same type share the same PA profile." msgstr "Сопла и материалы одного типа используют общий профиль PA." @@ -9950,50 +9964,40 @@ msgstr "Описание ошибки" msgid "Extra info" msgstr "Доп. информация" -# AI Translated msgid "The Filament Track Switch installed on the printer does not match the slicing file. Please re-slice to avoid print quality issues." -msgstr "Filament Track Switch, установленный на принтере, не соответствует файлу нарезки. Выполните повторную нарезку во избежание проблем с качеством печати." +msgstr "Filament Track Switch в принтере не соответствует файлу нарезки. Выполните повторную нарезку во избежание проблем с качеством печати." -# AI Translated msgid "This print requires a Filament Track Switch. Please install it first." -msgstr "Для этой печати требуется Filament Track Switch. Сначала установите его." +msgstr "Для печати этого файла требуется Filament Track Switch. Сначала установите его." -# AI Translated msgid "The Filament Track Switch has not been setup. Please setup it first." msgstr "Filament Track Switch не настроен. Сначала выполните его настройку." -# AI Translated #, c-format, boost-format msgid "Failed to send nozzle auto-mapping request to printer { code: %d }. Please try to refresh the printer information. If it still does not recover, you can try to rebind the printer and check the network connection." -msgstr "Не удалось отправить запрос на авто-сопоставление сопел принтеру { code: %d }. Попробуйте обновить информацию о принтере. Если это не помогает, попробуйте перепривязать принтер и проверить сетевое соединение." +msgstr "Не удалось отправить принтеру запрос на сопоставление сопел { код: %d }. Попробуйте обновить информацию о принтере. Если это не помогает, попробуйте проверить соединение и перепривязать принтер." -# AI Translated msgid "The printer is calculating nozzle mapping." -msgstr "Принтер вычисляет сопоставление сопел." +msgstr "Принтер выполняет сопоставление сопел." -# AI Translated msgid "Please wait a moment..." msgstr "Пожалуйста, подождите..." -# AI Translated #, c-format, boost-format msgid "Failed to receive nozzle auto-mapping table from printer { msg: %s }. Please refresh the printer information." -msgstr "Не удалось получить таблицу авто-сопоставления сопел от принтера { msg: %s }. Обновите информацию о принтере." +msgstr "Не удалось получить от принтера таблицу сопоставления сопел { ответ: %s }. Обновите информацию о принтере." -# AI Translated #, c-format, boost-format msgid "The printer failed to build the nozzle auto-mapping table { code: %d }. Please refresh nozzle information." -msgstr "Принтеру не удалось построить таблицу авто-сопоставления сопел { code: %d }. Обновите информацию о соплах." +msgstr "Принтеру не удалось составить таблицу сопоставления сопел { код: %d }. Обновите информацию о соплах." -# AI Translated #, c-format, boost-format msgid "The current nozzle mapping may produce an extra %0.2f g of waste." -msgstr "Текущее сопоставление сопел может привести к дополнительным %0.2f г отходов." +msgstr "Текущее сопоставление сопел может привести к дополнительным затратам %0.2fг материала." -# AI Translated #, c-format, boost-format msgid "Recommended filament arrangement saves %s->" -msgstr "Рекомендуемое расположение материалов экономит %s->" +msgstr "Рекомендуемое расположение материалов экономит %s→" #, c-format, boost-format msgid "Filament %s does not match the filament in AMS slot %s. Please update the printer firmware to support AMS slot assignment." @@ -10012,18 +10016,17 @@ msgstr "При включении режима вазы принтеры с ки msgid "The current printer does not support timelapse in Traditional Mode when printing By-Object." msgstr "Принтер не поддерживает таймлапс в режиме по умолчанию при печати моделей по очереди." -# AI Translated msgid "I have checked the installed nozzle and want to print anyway." -msgstr "Я проверил установленное сопло и всё равно хочу печатать." +msgstr "Установленное сопло проверено, продолжить в любом случае." msgid "Errors" msgstr "Ошибок" msgid "More than one filament types have been mapped to the same external spool, which may cause printing issues. The printer won't pause during printing." -msgstr "Несколько типов филамента назначены на одну внешнюю катушку, что может вызвать проблемы при печати. Принтер не будет приостанавливаться во время печати." +msgstr "На одну внешнюю катушку назначено несколько типов материала, что может вызвать проблемы при печати. Принтер не будет приостанавливаться во время печати." msgid "The filament type setting of external spool is different from the filament in the slicing file." -msgstr "Тип филамента на внешней катушке отличается от филамента в файле нарезки." +msgstr "Тип материала на внешней катушке отличается от материала в файле нарезки." msgid "The printer type selected when generating G-code is not consistent with the currently selected printer. It is recommended that you use the same printer type for slicing." msgstr "Выбранный профиль принтера в настройках слайсера не совпадает с фактическим принтером. Для нарезки рекомендуется использовать тот же профиль принтера." @@ -10041,48 +10044,40 @@ msgid "Please click the confirm button if you still want to proceed with printin msgstr "Нажмите кнопку подтверждения, если всё ещё хотите продолжить печать." msgid "This checks the flatness of heatbed. Leveling makes extruded height uniform." -msgstr "Проверка ровности нагревательного стола. Выравнивание обеспечивает равномерную высоту экструзии." +msgstr "Проверка кривизны стола. Обеспечивает равномерную высоту слоя." msgid "This process determines the dynamic flow values to improve overall print quality." -msgstr "Этот процесс определяет значения динамического потока для улучшения общего качества печати." +msgstr "Определение коэффициента динамического потока для улучшения общего качества печати." msgid "Internal" msgstr "Внутренние" -# AI Translated +# Подставляется storage_name #, c-format, boost-format msgid "%s space less than 20MB. Timelapse may not save properly. You can turn it off or" -msgstr "%s: свободного места меньше 20 МБ. Таймлапс может сохраниться некорректно. Вы можете отключить его или" +msgstr "%s: свободного места меньше 20 МБ. Таймлапс может сохраниться некорректно. Рекомендуется отключить его или " -# AI Translated msgid "Clean up files" -msgstr "Очистить файлы" +msgstr "очистить файлы" -# AI Translated msgid "Low internal storage. This timelapse will overwrite the oldest video files." msgstr "Мало внутренней памяти. Этот таймлапс перезапишет самые старые видеофайлы." -# AI Translated msgid "Low external storage. This timelapse will overwrite the oldest video files." msgstr "Мало внешней памяти. Этот таймлапс перезапишет самые старые видеофайлы." -# AI Translated msgid "Insufficient external storage for time-lapse photography. Connect to computer to delete files, or use a larger memory card." msgstr "Недостаточно внешней памяти для съёмки таймлапса. Подключитесь к компьютеру для удаления файлов или используйте карту памяти большего объёма." -# AI Translated msgid "Storage Space Not Enough" msgstr "Недостаточно места в хранилище" -# AI Translated msgid "Confirm & Print" -msgstr "Подтвердить и печатать" +msgstr "Подтвердить и продолжить" -# AI Translated msgid "Cancel Timelapse & Print" -msgstr "Отменить таймлапс и печатать" +msgstr "Отключить таймлапс и продолжить" -# AI Translated msgid "Clean Up" msgstr "Очистить" @@ -10100,47 +10095,38 @@ msgstr "Будет затрачено на %d г материала и на %d msgid "nozzle" msgstr "сопло" -# AI Translated #, c-format, boost-format msgid "Refreshing information of hotends(%d/%d)." msgstr "Обновление информации о хотэндах (%d/%d)." -# AI Translated msgid "There are not enough available hotends currently." -msgstr "Сейчас недостаточно доступных хотэндов." +msgstr "Доступных хотэндов сейчас недостаточно." -# AI Translated msgid "Please complete the hotend rack setup and try again." msgstr "Завершите настройку стойки хотэндов и повторите попытку." -# AI Translated msgid "Please refresh the nozzle information and try again." msgstr "Обновите информацию о соплах и повторите попытку." -# AI Translated msgid "Please re-slice to avoid filament waste." -msgstr "Выполните повторную нарезку во избежание расхода материала впустую." +msgstr "Выполните повторную нарезку во избежание излишнего расхода материала." -# AI Translated msgid "The reported hotend information may be unreliable." msgstr "Переданная информация о хотэнде может быть недостоверной." -# AI Translated #, c-format, boost-format msgid "The printer has no nozzle matching the slicing file (%s)." msgstr "На принтере нет сопла, соответствующего файлу нарезки (%s)." -# AI Translated msgid "Please install a matching nozzle in the hotend rack, or set the corresponding printer preset while slicing." msgstr "Установите подходящее сопло в стойку хотэндов или задайте соответствующий профиль принтера при нарезке." -# AI Translated msgid "The toolhead and hotend rack are full. Please remove at least one hotend before printing." -msgstr "Печатающая голова и стойка хотэндов заполнены. Перед печатью снимите хотя бы один хотэнд." +msgstr "Все места в печатающей голове и стойке хотэндов заняты. Перед печатью снимите хотя бы один хотэнд." #, c-format, boost-format msgid "The nozzle flow setting of %s(%s) doesn't match with the slicing file(%s). Please make sure the nozzle installed matches with settings in printer, then set the corresponding printer preset while slicing." -msgstr "Настройка потока сопла %s(%s) не совпадает с файлом нарезки (%s). Убедитесь, что настройки принтера соответствуют установленному соплу, затем выберите соответствующий профиль принтера при нарезке." +msgstr "Настройка расхода сопла %s(%s) не совпадает с файлом нарезки (%s). Убедитесь, что настройки принтера соответствуют установленному соплу, затем выберите соответствующий профиль принтера при нарезке." msgid "Tips: If you changed your nozzle of your printer lately, please go to 'Device -> Printer parts' to change your nozzle setting." msgstr "Совет: после замены сопла в принтере необходимо обновить его настройки («Принтер» → «Части принтера»)." @@ -10158,28 +10144,23 @@ msgstr "обоих экструдерах" #, c-format, boost-format msgid "The hardness of current material (%s) exceeds the hardness of %s(%s). Please verify the nozzle or material settings and try again." -msgstr "Требования к твёрдости у выбранного материала (%s) превышают возможности %s(%s). Проверьте настройки профиля принтера или материала и попробуйте ещё раз." +msgstr "Требования к твёрдости у выбранного материала (%s) превышают возможности %s (%s). Проверьте настройки профиля принтера или материала и попробуйте ещё раз." -# AI Translated msgid "Your current firmware version cannot start this print job. Please update to the latest version and try again." -msgstr "Текущая версия прошивки не может запустить это задание печати. Обновите до последней версии и повторите попытку." +msgstr "Текущая версия прошивки не может обработать этот файл. Обновитесь до последней версии и повторите попытку." -# AI Translated #, c-format, boost-format msgid "The hardness of current material (%s) exceeds the hardness of %s(%s). It may cause nozzle wear, leading to material leakage and unstable flow. Please exercise caution when using it." -msgstr "Твёрдость текущего материала (%s) превышает твёрдость %s(%s). Это может вызвать износ сопла, приводящий к утечке материала и нестабильному потоку. Соблюдайте осторожность при использовании." +msgstr "Требования к твёрдости у выбранного материала (%s) превышают возможности %s (%s). Это может привести к износу сопла и утечкам материала/нестабильному потоку в будущем. Используйте материал с осторожностью." -# AI Translated msgid "Some filaments may switch between extruders during printing. Manual K-value calibration cannot be applied throughout the entire print, which may affect print quality. Enabling Flow Dynamics Calibration is recommended." -msgstr "Некоторые материалы могут переключаться между экструдерами во время печати. Ручную калибровку K-value нельзя применить на протяжении всей печати, что может повлиять на её качество. Рекомендуется включить калибровку динамики потока (Flow Dynamics Calibration)." +msgstr "Некоторые материалы могут переключаться между экструдерами во время печати. Ручная калибровка K-фактора не применима для всей печати, что может повлиять на её качество. Рекомендуется включить калибровку динамики потока (Flow Dynamics Calibration)." -# AI Translated msgid "There is stringing-prone filament in this file. For best print quality, we recommend switching nozzle clumping detection to Auto mode." -msgstr "В этом файле есть материал, склонный к образованию волос. Для наилучшего качества печати рекомендуем переключить обнаружение налипания на сопло в режим «Авто»." +msgstr "В текущем проекте есть материал, склонный к образованию паутины. Во избежание проблем с качеством рекомендуется включить автоматический режим обнаружения налипаний на сопло." -# AI Translated msgid "If 'Dynamic Flow Calibration' is set to Auto/On, the system will use the manual calibration value or the default value and skip the flow calibration process. You can perform a manual flow calibration for TPU filament on the 'Calibration' page." -msgstr "Если «Динамическая калибровка потока» установлена в «Авто/Вкл», система будет использовать значение ручной калибровки или значение по умолчанию и пропустит процесс калибровки потока. Вы можете выполнить ручную калибровку потока для материала TPU на странице «Калибровка»." +msgstr "Если «Калибровка динамики потока» установлена в «Авто/Вкл», система будет использовать значение ручной калибровки или значение по умолчанию и пропустит процесс калибровки. Ручную калибровку TPU можно выполнить на странице «Калибровка»." #, c-format, boost-format msgid "[ %s ] requires printing in a high-temperature environment. Please close the door." @@ -10250,16 +10231,16 @@ msgid "The printer is executing instructions. Please restart printing after it e msgstr "Принтер выполняет команды. Перезапустите печать после их завершения." msgid "AMS is setting up. Please try again later." -msgstr "AMS настраивается. Пожалуйста, попробуйте позже." +msgstr "AMS настраивается. Попробуйте позднее." msgid "Not all filaments used in slicing are mapped to the printer. Please check the mapping of filaments." -msgstr "Не все филаменты, использованные при нарезке, назначены на принтер. Проверьте назначение филаментов." +msgstr "Не все материалы, использованные при нарезке, сопоставлены с принтером. Проверьте назначение материалов." msgid "Please do not mix-use the Ext with AMS." -msgstr "Пожалуйста, не используйте одновременно внешнюю катушку и AMS." +msgstr "Не стоит использовать AMS и внешние катушки совместно." msgid "Invalid nozzle information, please refresh or manually set nozzle information." -msgstr "Недопустимая информация о сопле. Обновите или вручную задайте информацию о сопле." +msgstr "Недопустимая информация о сопле. Обновите или задайте её вручную." msgid "Storage needs to be inserted before printing via LAN." msgstr "Перед печатью по локальной сети необходимо вставить хранилище данных." @@ -10286,24 +10267,21 @@ msgid "TPU 90A/TPU 85A is too soft and does not support automatic Flow Dynamics msgstr "TPU 85A/90A слишком мягкий для автоматической калибровки." msgid "Set dynamic flow calibration to 'OFF' to enable custom dynamic flow value." -msgstr "Установите калибровку динамического потока в «ВЫКЛ.», чтобы задать пользовательское значение динамического потока." +msgstr "Отключите калибровку динамики потока, чтобы задать пользовательское значение." msgid "This printer does not support printing all plates." msgstr "Принтер не поддерживает печать нескольких столов." -# AI Translated #, c-format, boost-format msgid "The current firmware supports a maximum of %s materials. You can either reduce the number of materials to %s or fewer on the Preparation Page, or try updating the firmware. If you are still restricted after the update, please wait for subsequent firmware support." -msgstr "Текущая прошивка поддерживает не более %s материалов. Вы можете либо уменьшить количество материалов до %s или менее на странице подготовки, либо попробовать обновить прошивку. Если после обновления ограничение сохраняется, дождитесь поддержки в последующих версиях прошивки." +msgstr "Текущая прошивка поддерживает не более %s материалов. Попробуйте обновить прошивку или уменьшить количество материалов до %s (или менее) на странице подготовки. Если после обновления ограничение сохраняется, ожидайте добавления поддержки в последующих версиях." msgid "The type of external filament is unknown or does not match with the filament type in the slicing file. Please make sure you have installed the correct filament in the external spool." msgstr "Тип материала на внешней катушке неизвестен или не соответствует материалу в файле печати. Убедитесь, что установлена внешняя катушка с требуемым материалом." -# AI Translated msgid "TPU 90A/TPU 85A are too soft. It is recommended to perform manual flow calibration on the 'Calibration' page. If 'Dynamic Flow Calibration' is set to auto/on, the system will use the previous calibration value and skip the flow calibration process." -msgstr "TPU 90A/TPU 85A слишком мягкие. Рекомендуется выполнить ручную калибровку потока на странице «Калибровка». Если «Динамическая калибровка потока» установлена в «авто/вкл», система будет использовать предыдущее значение калибровки и пропустит процесс калибровки потока." +msgstr "TPU 90A/TPU 85A слишком мягкий. Рекомендуется выполнить ручную калибровку потока на странице «Калибровка». Если «Калибровка динамики потока» установлена в «Авто/Вкл», система будет использовать предыдущие результаты и пропустит процесс калибровки." -# AI Translated msgid "The filament in the AMS may be insufficient for this print. Please refill or replace it." msgstr "Материала в AMS может быть недостаточно для этой печати. Пополните или замените его." @@ -10475,7 +10453,6 @@ msgstr "Удалить этот профиль" msgid "Search in preset" msgstr "Поиск в профиле" -# AI Translated msgid "Synchronization of different extruder drives or nozzle volume types is not supported." msgstr "Синхронизация разных приводов экструдера или типов объёма сопла не поддерживается." @@ -10485,9 +10462,8 @@ msgstr "Перенести изменения в настройки другог msgid "Click to reset all settings to the last saved preset." msgstr "Сбросить все изменения" -# AI Translated msgid "Prime tower is required for nozzle changing. There may be flaws on the model without prime tower. Are you sure you want to disable prime tower?" -msgstr "Для смены сопла требуется черновая башня. Без черновой башни на модели могут появиться дефекты. Вы уверены, что хотите отключить черновую башню?" +msgstr "Для смены сопла требуется черновая башня. Без черновой башни на модели могут появиться дефекты. Вы действительно хотите отключить черновую башню?" msgid "A prime tower is required for smooth timelapse mode. There may be flaws on the model without a prime tower. Are you sure you want to disable the prime tower?" msgstr "Для сглаженного таймлапса требуется черновая башня, без неё на модели могут возникнуть дефекты. Вы действительно хотите отключить черновую башню?" @@ -10503,7 +10479,7 @@ msgid "A prime tower is required for clumping detection. There may be flaws on t msgstr "Для обнаружения налипаний на сопле требуется черновая башня, без неё на модели могут образоваться дефекты. Включить обнаружение налипаний?" msgid "Enabling both precise Z height and the prime tower may cause slicing errors. Do you still want to enable precise Z height?" -msgstr "Включение «Точной высоты по Z» вместе с черновой башней может привести к ошибкам нарезки. Продолжить?" +msgstr "Включение «Точной высоты по Z» совместно с черновой башней может привести к ошибкам нарезки. Продолжить?" msgid "A prime tower is required for smooth timelapse mode. There may be flaws on the model without prime tower. Do you want to enable the prime tower?" msgstr "Для сглаженного таймлапса требуется черновая башня, без неё на модели могут возникнуть дефекты. Включить черновую башню?" @@ -10563,22 +10539,6 @@ msgstr "Вы действительно хотите задействовать msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Многие шаблоны заполнения разработаны на основе автоматического поворота по определённым правилам для поддержания правильной печати и желаемого эффекта (например, «Гироид» или «Куб»). Изменение правила поворота текущего шаблона может привести к его провисанию. Будьте осторожны и внимательно проверяйте результат на наличие потенциальных проблем с печатью." -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Высота слоя слишком мала.\n" -"Будет установлено значение min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Высота слоя не может превышать ограничения, установленные в настройках принтера → Экструдер → Ограничение высоты слоя. Это может вызвать проблемы с качеством печати." - -msgid "Adjust to the set range automatically?\n" -msgstr "Автоматически подстроиться под заданный в настройках диапазон?\n" - -msgid "Adjust" -msgstr "Подстроиться" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "[Экспериментальная функция] Втягивание и обрезка прутка на большем расстоянии во время его замены для минимизации очистки. Хотя это значительно сокращает величину очистки, это может повысить риск возникновения затора или вызвать другие проблемы при печати." @@ -10715,6 +10675,7 @@ msgstr "Материал поддержки" msgid "Support ironing" msgstr "Разглаживание поддержки" +# Технически точной локализацией было бы "фрактальные поддержки", т.к. сама структура представляет собой фрактал, а не дерево. Но у всех уже на слуху, так что менять нет смысла. Хотя звучало бы прям круто :3 msgid "Tree supports" msgstr "Древовидная поддержка" @@ -10741,13 +10702,12 @@ msgstr "G-код при смене типа линии" msgid "Post-processing Scripts" msgstr "Скрипты постобработки" -# AI Translated msgid "Slicing Pipeline Plugin" msgstr "Плагин конвейера нарезки" -# AI Translated +# В данном контексте можно, наверное, "конфигурация", т.к. тут именно управление набором плагинов, а не просто их настройками msgid "Plugin Configuration" -msgstr "Настройка плагина" +msgstr "Настройка плагинов" msgid "Notes" msgstr "Заметки" @@ -10778,6 +10738,9 @@ msgstr "Найдены зарезервированные ключевые сл msgid "Setting Overrides" msgstr "Замещение настроек" +msgid "Retraction when switching material" +msgstr "Откат при смене материала" + msgid "Basic information" msgstr "Основные" @@ -10904,7 +10867,7 @@ msgid "Multi Filament" msgstr "Печать несколькими материалами" msgid "Tool change parameters with single extruder MM printers" -msgstr "Смена материала при комбинированной печати одним экструдером" +msgstr "Смена материала при печати одним экструдером" msgid "Set" msgstr "Выбор" @@ -10925,6 +10888,12 @@ msgstr "Совместимые настройки" msgid "Printable space" msgstr "Область печати" +msgid "Printer Agent" +msgstr "Сетевой агент" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Реализация сетевого агента для обмена информацией с принтером. Доступные реализации определяются при запуске." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -11053,9 +11022,6 @@ msgstr "Ограничение высоты слоя" msgid "Z-Hop" msgstr "Подъём головы при откате" -msgid "Retraction when switching material" -msgstr "Откат при смене материала" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -11069,10 +11035,8 @@ msgstr "" msgid "Firmware Retraction" msgstr "Откат из прошивки" -# Изменения в настройках ... будут сброшены при переключении на принтер с -# другим типом или количеством сопел. msgid "Switching to a printer with different extruder types or numbers will discard or reset changes to extruder or multi-nozzle-related parameters." -msgstr "Переключение на принтер с другим типом или количеством экструдеров приведёт к сбросу или удалению изменений параметров, связанных с экструдерами и многосопельной конфигурацией." +msgstr "Изменения в настройках экструдера и параметрах сопел не будут перенесены в профиль принтера с другим типом или количеством сопел." msgid "Use Modified Value" msgstr "Использовать изменённое значение" @@ -11087,7 +11051,7 @@ msgstr "" "• профили материалов: %d\n" "• профили настроек: %d\n" "\n" -"Эти профили будут удалены при удалении принтера." +"Эти профили будут удалены вместе с принтером." # ??? Профили, наследуемые от других профилей, не могут быть удалены. msgid "Presets inherited by other presets cannot be deleted!" @@ -11115,8 +11079,8 @@ msgid "" "If the preset corresponds to a filament currently in use on your printer, please reset the filament information for that slot." msgstr "" "Вы действительно хотите удалить выбранный профиль? \n" -"Если материал из этого профиля сейчас используется в вашем принтере,\n" -"необходимо сбросить информацию о материале для этого слота." +"Если вы используете его в принтере, сбросьте информацию\n" +"о нём у соответствующего слота." #, boost-format msgid "Are you sure you want to %1% the selected preset?" @@ -11173,7 +11137,6 @@ msgstr "Продолжить" msgid "Don't warn again for this preset" msgstr "Больше не спрашивать для этого профиля" -# AI Translated #, c-format, boost-format msgid "%s: %s" msgstr "%s: %s" @@ -11181,11 +11144,11 @@ msgstr "%s: %s" msgid "No modifications need to be copied." msgstr "Перенос изменений не требуется." -msgid "Copy paramters" +msgid "Copy parameters" msgstr "Копировать настройки" #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "Изменить настройки %s" #, c-format, boost-format @@ -11326,16 +11289,15 @@ msgstr "" msgid "Extruder count" msgstr "Количество экструдеров" +# Какая-то древняя строка. Ранее – "Характеристики принтера", сейчас используется ещё и в конфигурации плагинов в профиле материала (вкладка "Расширенные") msgid "Capabilities" -msgstr "Характеристики принтера" +msgstr "Возможности" -# AI Translated msgid "Left: " -msgstr "Слева: " +msgstr "Левый: " -# AI Translated msgid "Right: " -msgstr "Справа: " +msgstr "Правый: " msgid "Show all presets (including incompatible)" msgstr "Показать все профили (включая несовместимые)" @@ -11488,7 +11450,7 @@ msgid "Color match" msgstr "Подбор цвета" msgid "Approximate color matching." -msgstr "Приблизительный подбор по цвету ваших прутков." +msgstr "Приблизительный подбор по цвету ваших материалов." msgid "Append" msgstr "Добавить" @@ -11510,7 +11472,7 @@ msgid "" msgstr "выбор цветов можно изменить вручную." msgid "—> " -msgstr "—> " +msgstr "→ " msgid "" "Synchronizing AMS filaments will discard your modified but unsaved filament presets.\n" @@ -11534,7 +11496,7 @@ msgid "Plate" msgstr "Стол" msgid "The connected printer does not match the currently selected printer. Please change the selected printer." -msgstr "Подключённый принтер не соответствует текущему выбранному принтеру. Пожалуйста, измените выбранный принтер." +msgstr "Подключённый принтер не соответствует текущему выбранному. Пожалуйста, измените выбранный принтер." msgid "Mapping" msgstr "Назначение" @@ -11543,7 +11505,7 @@ msgid "Overwriting" msgstr "Переназначение" msgid "Reset all filament mapping" -msgstr "Сбросить все назначения филаментов" +msgstr "Сбросить назначения всех материалов" msgid "(Recommended filament)" msgstr "(рекомендуется)" @@ -11555,8 +11517,8 @@ msgid "" "Check heatbed flatness. Leveling makes extruded height uniform.\n" "*Automatic mode: Level first (about 10 seconds). Skip if surface is fine." msgstr "" -"Проверка ровности стола. Выравнивание обеспечивает равномерную высоту экструзии.\n" -"*Автоматический режим: сначала выполнить выравнивание (около 10 секунд). Пропустить, если поверхность в порядке." +"Снятие карты высот стола. Обеспечивает равномерную высоту слоя.\n" +"*Автоматический режим: выполнить проверку (около 10 секунд). Пропуск, если поверхность в порядке." msgid "" "Calibrate nozzle offsets to enhance print quality.\n" @@ -11572,43 +11534,43 @@ msgid "Tip" msgstr "Совет" msgid "Only synchronize filament type and color, not including AMS slot information." -msgstr "Синхронизировать только тип и цвет филамента, без информации о слотах AMS." +msgstr "Синхронизировать только тип и цвет материала, без информации о слотах AMS." msgid "Replace the project filaments list sequentially based on printer filaments. And unused printer filaments will be automatically added to the end of the list." -msgstr "Заменить список филаментов проекта последовательно на основе филаментов принтера. Неиспользуемые филаменты принтера будут автоматически добавлены в конец списка." +msgstr "Заменить список материалов проекта последовательно на основе материалов принтера. Свободные материалы будут автоматически добавлены в конец списка." msgid "Add unused AMS filaments to filaments list." msgstr "Добавить незадействованные материалы из AMS в список" msgid "Automatically merge the same colors in the model after mapping." -msgstr "Автоматически объединять одинаковые цвета в модели после назначения." +msgstr "Автоматически объединять одинаковые цвета в модели после их назначения." msgid "After being synced, this action cannot be undone." msgstr "После синхронизации это действие нельзя отменить." msgid "After being synced, the project's filament presets and colors will be replaced with the mapped filament types and colors. This action cannot be undone." -msgstr "После синхронизации профили филаментов и цвета проекта будут заменены назначенными типами и цветами филаментов. Это действие нельзя отменить." +msgstr "После синхронизации профили и цвета материалов проекта будут заменены назначенными им типами и цветами материалов. Это действие нельзя отменить." msgid "Are you sure to synchronize the filaments?" -msgstr "Вы уверены, что хотите синхронизировать филаменты?" +msgstr "Вы уверены, что хотите синхронизировать материалы?" msgid "Synchronize now" msgstr "Синхронизировать" msgid "Synchronize Filament Information" -msgstr "Синхронизация информации о филаменте" +msgstr "Синхронизация информации о материале" msgid "Add unused filaments to filaments list." -msgstr "Добавить неиспользуемые филаменты в список." +msgstr "Добавить неиспользуемые материалы в список." msgid "Only synchronize filament type and color, not including slot information." -msgstr "Синхронизировать только тип и цвет филамента, без информации о слотах." +msgstr "Синхронизировать только тип и цвет материала, без информации о слотах." msgid "Ext spool" msgstr "Внешняя катушка" msgid "Please check whether the nozzle type of the device is the same as the preset nozzle type." -msgstr "Проверьте, совпадает ли тип сопла устройства с типом сопла в профиле." +msgstr "Проверьте, совпадает ли тип сопла в принтере с типом сопла в профиле." msgid "Storage is not available or is in read-only mode." msgstr "Хранилище недоступно или защищено от записи." @@ -11621,7 +11583,7 @@ msgid "Timelapse is not supported because Print sequence is set to \"By object\" msgstr "Таймлапс не поддерживается, поскольку для последовательности печати установлено значение «Печать по очереди»." msgid "You selected external and AMS filament at the same time in an extruder, you will need manually change external filament." -msgstr "Вы выбрали одновременно внешний филамент и филамент из AMS в одном экструдере. Вам нужно будет вручную менять внешний филамент." +msgstr "Для одного экструдера одновременно выбраны внешняя катушка и катушка из AMS. Потребуется ручная смена прутка." msgid "Successfully synchronized nozzle information." msgstr "Информация о сопле успешно синхронизирована." @@ -11629,9 +11591,8 @@ msgstr "Информация о сопле успешно синхронизир msgid "Successfully synchronized nozzle and AMS number information." msgstr "Информация о сопле и количестве AMS успешно синхронизирована." -# AI Translated msgid "Do you want to continue to sync filaments?" -msgstr "Хотите продолжить синхронизацию материалов?" +msgstr "Продолжить синхронизацию материалов?" msgid "Successfully synchronized filament color from printer." msgstr "Цвет материала успешно синхронизирован с принтером." @@ -11657,7 +11618,7 @@ msgstr "" #, boost-format msgid "For constant flow rate, hold %1% while dragging." -msgstr "Для постоянного объёмного расхода удерживайте нажатой клавишу %1% при перетаскивании." +msgstr "Для выравнивания значений расхода удерживайте клавишу %1% при перетаскивании." msgid "ms" msgstr "мс" @@ -11697,28 +11658,6 @@ msgstr "Объёмы прочистки при смене материала" msgid "Please choose the filament colour" msgstr "Изменение цвета" -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "Для нативного отображения трансляции в Wayland требуется gtksink (плагин для GStreamer). Установите необходимый пакет плагинов и перезапустите OrcaSlicer." - -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "Не удалось запустить нативную трансляцию GSteramer через Wayland. Проверьте наличие установленного пакета плагинов GStreamer." - -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "Для этой задачи требуется Windows Media Player! Хотите включить его в своей ОС?" - -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "Компонент BambuSource неправильно зарегистрирован для воспроизведения медиафайлов! Нажмите «Да», чтобы повторно зарегистрировать его" - -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "Отсутствует компонент BambuSource для воспроизведения медиа! Пожалуйста, переустановите OrcaSlicer или обратитесь за помощью к сообществу." - -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "При использовании компонентов BambuSource из другого инсталлятора, воспроизведение видео может работать некорректно! Нажмите «Да», чтобы исправить это." - -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "В вашей системе отсутствуют кодеки H.264 для GStreamer, которые необходимы для воспроизведения видео (попробуйте установить пакеты gstreamer1.0-plugins-bad или gstreamer1.0-libav, а затем перезапустить Orca Slicer)." - -# AI Translated msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "Облачный агент недоступен. Перезапустите OrcaSlicer и повторите попытку." @@ -11734,9 +11673,8 @@ msgstr "Войти" msgid "Login failed. Please try again." msgstr "Ошибка входа. Попробуйте ещё раз." -# AI Translated msgid "parse json failed" -msgstr "не удалось разобрать JSON" +msgstr "не удалось обработать JSON" msgid "[Action Required] " msgstr "[Требуется действие] " @@ -11928,12 +11866,10 @@ msgctxt "Keyboard Shortcut" msgid "Space" msgstr "Пробел" -# AI Translated msgid "Open actions speed dial" -msgstr "Открыть панель быстрых действий" +msgstr "Открыть строку быстрых действий" # Plater – это название библиотеки. Используется в меню горячих клавиш в качестве заголовка сочетаний клавиш, которые работают внутри пространства Plater. Как минимум на Windows не отображается. -# AI Translated msgid "Plater" msgstr "Рабочая область" @@ -12012,17 +11948,15 @@ msgstr "Информация об изменениях в версии %s:" msgid "Network plug-in update" msgstr "Обновление сетевого плагина" -# AI Translated msgid "Click OK to update the Network plug-in now. If a file is in use, the update will be applied the next time Orca Slicer launches." -msgstr "Нажмите OK, чтобы обновить сетевой плагин сейчас. Если файл используется, обновление будет применено при следующем запуске Orca Slicer." +msgstr "Нажмите OK для обновления сетевого плагина. Если файл занят, обновление будет завершено при следующем запуске Orca Slicer." -# AI Translated msgid "A new Network plug-in is available. Do you want to install it?" -msgstr "Доступен новый сетевой плагин. Хотите установить его?" +msgstr "Доступна новая версия сетевого плагина. Выполнить установку?" #, c-format, boost-format msgid "A new Network plug-in (%s) is available. Do you want to install it?" -msgstr "Доступен новый сетевой плагин (%s). Хотите установить?" +msgstr "Доступна новая версия сетевого плагина: %s. Выполнить установку?" msgid "New version of Orca Slicer" msgstr "Доступна новая версия Orca Slicer" @@ -12077,9 +12011,8 @@ msgstr "Имя принтера" msgid "Where to find your printer's IP and Access Code?" msgstr "Где найти IP-адрес и код доступа к вашему принтеру?" -# AI Translated msgid "How to trouble shooting" -msgstr "Как устранить неполадки" +msgstr "Помощь в устранении неполадок" msgid "Connect" msgstr "Подключить" @@ -12140,13 +12073,13 @@ msgid "Laser 40W" msgstr "40 Вт лазер" msgid "Cutting Module" -msgstr "Модуль обрезки" +msgstr "Модуль резки" # система пожаротушения? msgid "Auto Fire Extinguishing System" msgstr "Автоматическая система пожаротушения" -# AI Translated +# По сути, локализовывается как "Переключатель AMS". Но сами бамбуки используют кривой машинный перевод в русскоязычной документации, и там переводится от случая к случаю. Поэтому имеет полный смысл оставить брендированное название. msgid "Filament Track Switch" msgstr "Filament Track Switch" @@ -12168,7 +12101,6 @@ msgstr "Сбой обновления" msgid "Update successful" msgstr "Обновление успешно завершено" -# AI Translated msgid "Hotends on Rack" msgstr "Хотэнды на стойке" @@ -12266,9 +12198,8 @@ msgstr "" "Input Shaping поддерживается только в Marlin 2.1.2 и новее.\n" "Обновите прошивку и установите тип G-кода на «Marlin 2»." -# AI Translated msgid "Input shaping is only supported by Klipper, RepRapFirmware and Marlin 2." -msgstr "Input shaping поддерживается только Klipper, RepRapFirmware и Marlin 2." +msgstr "Input shaping поддерживается только в Klipper, RepRapFirmware и Marlin 2." msgid "Grouping error: " msgstr "Ошибка группировки: " @@ -12277,9 +12208,8 @@ msgstr "Ошибка группировки: " msgid " can not be placed in the " msgstr " нельзя заправить в " -# AI Translated msgid "Group error in manual mode. Please check nozzle count or regroup." -msgstr "Ошибка группировки в ручном режиме. Проверьте количество сопел или перегруппируйте." +msgstr "Ошибка группировки в ручном режиме. Проверьте количество сопел или измените группировку." msgid "Internal Bridge" msgstr "Внутренний мост" @@ -12407,6 +12337,10 @@ msgstr " находится слишком близко к области иск msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " находится слишком близко к зоне обнаружения налипаний, столкновения неизбежны.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " частично находится за пределами области печати и не может быть напечатан.\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Обнаружен недопустимый перепад температур. Каждый из используемых материалов должен иметь в профиле температуру печати в пределах допустимого диапазона других материалов. В противном случае сопло может забиться и повредить принтер." @@ -12420,6 +12354,10 @@ msgstr "Если вас это не пугает, эту проверку мож msgid "No extrusions under current settings." msgstr "При текущих настройках экструзия отсутствует." +# AI Translated +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "Используется смешанный материал с градиентом, но параметр «Подслой смешанного цвета» отключён. Градиент не будет напечатан." + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Плавный режим таймлапса не поддерживается, когда включена последовательность печати моделей по очереди." @@ -12427,10 +12365,10 @@ msgid "Clumping detection is not supported when \"by object\" sequence is enable msgstr "Обнаружение налипаний не поддерживается при печати моделей по очереди." msgid "Enabling both precise Z height and the prime tower may cause slicing errors." -msgstr "Одновременное включение точной высоты Z и башни очистки может вызвать ошибки нарезки." +msgstr "Совместное использование точной высоты Z и черновой башни может вызвать ошибки нарезки." msgid "A prime tower is required for clumping detection; otherwise, there may be flaws on the model." -msgstr "Для обнаружения налипаний требуется башня очистки; в противном случае на модели могут быть дефекты." +msgstr "Для обнаружения налипаний требуется черновая башня; в противном случае на модели могут возникнуть дефекты." msgid "Please select \"By object\" print sequence to print multiple objects in spiral vase mode." msgstr "Выберите последовательность печати «По очереди» для поддержки печати несколько моделей в режиме вазы." @@ -12458,6 +12396,10 @@ msgstr "Попробуйте уменьшить размер модели или msgid "Variable layer height is not supported with Organic supports." msgstr "Функция переменной высоты слоя несовместима с органическим стилем древовидных поддержек." +# AI Translated +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "Материал черновой башни не может быть смешанным материалом." + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Совместное использование черновой башни с разными диаметрами сопел и прутков может привести к некорректной нарезке при включённой черновой башне. Этот метод работы экспериментальный, поэтому будьте осторожны при использовании." @@ -12529,10 +12471,10 @@ msgid "Organic support branch diameter must not be smaller than support tree tip msgstr "Диаметр ветвей органической поддержки не может быть меньше диаметра их кончиков." msgid "The Hollow base pattern is not supported by this support type; Rectilinear will be used instead." -msgstr "Шаблон полого основания не поддерживается этим типом поддержки; вместо него будет использоваться прямолинейный." +msgstr "Шаблон «Полость» не поддерживается этим типом поддержек и будет заменён на «Зигзаг»." msgid "Support enforcers are used but support is not enabled. Please enable support." -msgstr "Используется принудительная поддержка, но её генерация не включена. Пожалуйста, включите генерацию поддержки в настройках слайсера." +msgstr "Используется принудительная поддержка, но её генерация не включена. Включите генерацию поддержек в настройках слайсера." msgid "Layer height cannot exceed nozzle diameter." msgstr "Высота слоя не может быть больше диаметра сопла." @@ -12540,24 +12482,21 @@ msgstr "Высота слоя не может быть больше диамет msgid "Bridge line width must not exceed nozzle diameter" msgstr "Ширина линии моста не может превышать диаметр сопла" -# AI Translated msgid "\"G92 E0\" was found in before_layer_change_gcode, but the G or E are not uppercase. Please change them to the exact uppercase \"G92 E0\"." -msgstr "«G92 E0» обнаружено в before_layer_change_gcode, но G или E не в верхнем регистре. Измените их на точное «G92 E0» в верхнем регистре." +msgstr "В G-коде перед сменой слоя обнаружена команда «G92 E0» с неправильным регистром. Измените регистр «G92 E0» на заглавные буквы." -# AI Translated msgid "\"G92 E0\" was found in layer_change_gcode, but the G or E are not uppercase. Please change them to the exact uppercase \"G92 E0\"." -msgstr "«G92 E0» обнаружено в layer_change_gcode, но G или E не в верхнем регистре. Измените их на точное «G92 E0» в верхнем регистре." +msgstr "В G-коде после смены слоя обнаружена команда «G92 E0» с неправильным регистром. Измените регистр «G92 E0» на заглавные буквы." msgid "Relative extruder addressing requires resetting the extruder position at each layer to prevent loss of floating point accuracy. Add \"G92 E0\" to layer_gcode." msgstr "При относительной адресации экструдера его положение необходимо корректировать на каждом слое, чтобы предотвратить потерю точности с плавающей запятой. Добавьте \"G92 E0\" в G-код выполняемый при смене слоя (layer_gcode)." -# AI Translated +# на самом деле, вполне совместима и часто используется в самих прошивках при сбросе координат msgid "\"G92 E0\" was found in before_layer_change_gcode, which is incompatible with absolute extruder addressing." -msgstr "«G92 E0» обнаружено в before_layer_change_gcode, что несовместимо с абсолютной адресацией экструдера." +msgstr "В G-коде перед сменой слоя обнаружена команда «G92 E0», которая несовместима с абсолютными координатами экструдера." -# AI Translated msgid "\"G92 E0\" was found in layer_change_gcode, which is incompatible with absolute extruder addressing." -msgstr "«G92 E0» обнаружено в layer_change_gcode, что несовместимо с абсолютной адресацией экструдера." +msgstr "В G-коде после смены слоя обнаружена команда «G92 E0», которая несовместима с абсолютными координатами экструдера." #, c-format, boost-format msgid "Plate %d: %s does not support filament %s" @@ -12614,15 +12553,14 @@ msgstr "Компенсация усадки материала не будет msgid "Generating skirt & brim" msgstr "Генерация юбки и каймы" -# AI Translated msgid "" "Per-object skirts cannot fit between the objects in By object print sequence.\n" "\n" "Move the objects farther apart, reduce brim/skirt size, switch Skirt type to Combined, or switch Print sequence to By layer." msgstr "" -"Пообъектные юбки не помещаются между моделями при последовательности печати «По объектам».\n" +"Независимые юбки не помещаются между моделями при печати моделей по очереди.\n" "\n" -"Раздвиньте модели дальше друг от друга, уменьшите размер каймы/юбки, переключите тип юбки на «Комбинированная» или измените последовательность печати на «По слоям»." +"Попробуйте увеличить отступ между моделями, уменьшить размер юбки/каймы, использовать совместный тип юбки или печатать модели послойно." msgid "Exporting G-code" msgstr "Экспорт в G-код" @@ -12642,29 +12580,23 @@ msgstr "Область печати" msgid "Extruder printable area" msgstr "Область печати экструдера" -# AI Translated msgid "Support parallel printheads" -msgstr "Поддержка параллельных печатающих головок" +msgstr "Поддержка параллельных печатающих голов" -# AI Translated msgid "Enable printer settings for machines that can use multiple printheads in parallel." -msgstr "Включает настройки принтера для машин, способных использовать несколько печатающих головок параллельно." +msgstr "Отобразить настройки для принтеров, способных параллельно использовать несколько зависимых печатающих голов." -# AI Translated msgid "Parallel printheads count" -msgstr "Количество параллельных печатающих головок" +msgstr "Количество зависимых голов" -# AI Translated msgid "Set the number of parallel printheads for machines like OrangeStorm Giga printer." -msgstr "Задаёт количество параллельных печатающих головок для машин вроде принтера OrangeStorm Giga." +msgstr "Задаёт количество зависимых печатающих голов для принтеров вроде OrangeStorm Giga." -# AI Translated msgid "Parallel printheads bed exclude areas" -msgstr "Исключаемые зоны стола для параллельных печатающих головок" +msgstr "Области исключения для зависимых голов" -# AI Translated msgid "Ordered list of bed exclude areas by parallel printhead count. Item 1 applies to one printhead, item 2 to two printheads, and so on. Leave an item empty for no excluded area." -msgstr "Упорядоченный список исключаемых зон стола по количеству параллельных печатающих голов. Поле 1 применяется к одной голове, поле 2 — к двум, и так далее. Оставьте поле пустым, если исключаемые зоны отсутствуют." +msgstr "Список исключаемых областей стола, упорядоченный по количеству зависимых голов. 1 – область для одной головы, 2 — область для двух, и т.д. Оставьте пустым, если исключаемые области отсутствуют." msgid "Excluded bed area" msgstr "Область исключения" @@ -12685,23 +12617,21 @@ msgid "This shrinks the first layer on the build plate to compensate for elephan msgstr "Сужает контур первого слоя на заданное значение для компенсации дефекта слоновьей ноги." msgid "Elephant foot compensation layers" -msgstr "Компенсирующих слоёв «слоновьей ноги»" +msgstr "Слои компенсации" msgid "The number of layers on which the elephant foot compensation will be active. The first layer will be shrunk by the elephant foot compensation value, then the next layers will be linearly shrunk less, up to the layer indicated by this value." -msgstr "Количество слоёв, на которые будет распространяться компенсация слоновьей ноги. Первый слой будет уменьшен на величину компенсации слоновьей ноги с последующим линейным уменьшением до слоя, указанного здесь." +msgstr "Количество слоёв для компенсации избытка материала. Сужение контура первого слоя управляется настройкой выше, последующие слои линейно расширяются до нормального размера." msgid "Elephant foot layers density" -msgstr "Плотность слоёв компенсации" +msgstr "Плотность слоя компенсации" -# AI Translated msgid "" "Density of internal solid infill for Elephant foot layers compensation.\n" "The initial value for the second layer is set.\n" "Subsequent layers become linearly denser by the height specified in elefant_foot_compensation_layers." msgstr "" -"Плотность внутреннего сплошного заполнения для компенсации слоёв «слоновьей ноги».\n" -"Задаётся начальное значение для второго слоя.\n" -"Последующие слои линейно уплотняются на высоту, заданную в elefant_foot_compensation_layers." +"Начальное значение плотности сплошного заполнения для компенсации дефекта «слоновьей ноги».\n" +"Позволяет компенсировать избыток материала за счёт снижения плотности первых слоёв с постепенным её восстановлением к слою, указанному выше." msgid "This is the height for each layer. Smaller layer heights give greater accuracy but longer printing time." msgstr "Высота каждого слоя. Чем меньше, тем выше качество поверхности и затраты времени (и наоборот)." @@ -12735,14 +12665,11 @@ msgid "Allow controlling BambuLab's printer through 3rd party print hosts." msgstr "Позволяет управлять принтером BambuLab через сторонние хосты печати." msgid "Use 3MF instead of G-code" -msgstr "Сжимать G-код перед отправкой" +msgstr "Сжатие G-кода перед отправкой" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Рекомендуется для принтеров, поддерживающих печать из архивов 3MF. Файлы печати будут отправляться с расширением \".gcode.3mf\"." -msgid "Printer Agent" -msgstr "Сетевой агент" - msgid "Select the network agent implementation for printer communication." msgstr "Реализация сетевого агента для обмена информацией с принтером." @@ -12803,9 +12730,8 @@ msgstr "API-ключ" msgid "HTTP digest" msgstr "HTTP digest-авторизация" -# AI Translated msgid "Configuration for the plugin capabilities this preset uses, overriding the global Capabilities configuration. Stored as a raw JSON array and edited through the dialog behind the button, never typed in directly." -msgstr "Конфигурация возможностей плагинов, используемых этим профилем, переопределяющая глобальную конфигурацию возможностей. Хранится как необработанный массив JSON и редактируется через диалоговое окно за кнопкой, а не вводится напрямую." +msgstr "Настройки функционала плагинов, используемого этим профилем. Переопределяет глобальные настройки функционала. Хранится как необработанный массив JSON и редактируется через диалоговое окно по кнопке." # Логическая ошибка; пересекаются не периметры, а траектория холостого # перемещения со стенкой модели @@ -12827,7 +12753,7 @@ msgstr "мм или %" msgid "Other layers" msgstr "Основная" -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." msgstr "Температура стола для всех остальных слоёв. 0 – материал не поддерживает это покрытие." msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." @@ -13255,7 +13181,7 @@ msgstr "" "Фактический поток для поддержек рассчитывается путём умножения этого значения на поток материала и общий поток модели (если он задан)." msgid "Support interface flow ratio" -msgstr "Интерфейс поддержек" +msgstr "Связующие слои" msgid "" "This factor affects the amount of material for the support interface.\n" @@ -13302,6 +13228,7 @@ msgstr "Дополнительные периметры на нависания msgid "Create additional perimeter paths over steep overhangs and areas where bridges cannot be anchored." msgstr "" "Создание дополнительных периметров над крутыми нависаниями и участками, где невозможно закрепить мосты.\n" +"\n" "Внимание: включение этой настройки может повлиять на корректность генерации мостов!" # ??? Реверс на чётных слоях нависаний @@ -13384,7 +13311,7 @@ msgstr "Включение динамического управления ск msgid "Slow down for curled perimeters" msgstr "Замедляться на изогнутых периметрах" -# AI Translated +# Обновил, отформатировал и провёл семантическую оптимизацию #, no-c-format, no-boost-format msgid "" "Enable this option to slow down printing in areas where perimeters may have curled upwards.\n" @@ -13402,20 +13329,20 @@ msgid "" "Note: When this option is enabled, overhang perimeters are treated like overhangs, meaning the overhang speed is applied even if the overhanging perimeter is part of a bridge.\n" "For example, when the perimeters are 100% overhanging, with no wall supporting them from underneath, the 100% overhang speed will be applied." msgstr "" -"Включите эту опцию, чтобы замедлять печать в областях, где периметры могли загнуться вверх.\n" -"Например, дополнительное замедление будет применяться при печати нависаний на острых углах, таких как нос корпуса Benchy, уменьшая загибание, которое накапливается на нескольких слоях.\n" +"Позволяет автоматически замедлять печать в областях, где выступающие периметры могут деформироваться из-за усадки.\n" +"Например, форштевень Benchy, который сильнее подвержен накоплению усадочной деформации.\n" "\n" -"Обычно рекомендуется держать эту опцию включённой, если только охлаждение вашего принтера не достаточно мощное или скорость печати не достаточно низкая, чтобы загибание периметров не происходило. \n" -"При печати с высокой скоростью внешнего периметра этот параметр может вносить артефакты на стенках при замедлении из-за потенциально большого разброса скоростей печати, из-за которого экструдер не успевает за требуемым изменением потока.\n" -"Коренная причина этих артефактов, скорее всего, — слегка неточная настройка PA, особенно в сочетании с большим временем сглаживания PA.\n" +"Рекомендуется использовать в случаях, если скоростной режим, конструкция принтера или его система охлаждения не способны обеспечить качественного сопротивления усадке материала.\n" "\n" -"Рекомендации при включении этой опции:\n" -"1. Уменьшите время сглаживания Pressure Advance до 0,015 - 0,02, чтобы экструдер быстро реагировал на изменения скорости.\n" -"2. Увеличьте минимальные скорости печати, чтобы ограничить величину замедления и уменьшить разброс между быстрыми и медленными участками.\n" -"3. Если артефакты всё ещё появляются, включите сглаживание скорости экструзии (ERS) для дальнейшего сглаживания переходов потока.\n" +"Примечание: отключает скоростной режим мостов для нависающих периметров (передаёт его в управление настройкам нависаний ниже).\n" "\n" -"Примечание: когда эта опция включена, нависающие периметры рассматриваются как нависания, то есть скорость нависания применяется, даже если нависающий периметр является частью моста.\n" -"Например, когда периметры нависают на 100%, без стенки, поддерживающей их снизу, будет применена скорость нависания 100%." +"Внимание: требуется крайне точная настройка коррекции давления. В противном случае при быстрой печати стенок в местах перепада скоростей будут возникать заметные артефакты поверхности.\n" +"\n" +"Рекомендации по настройке:\n" +"1. Уменьшите время сглаживания Pressure Advance до 0.015-0.02, чтобы\n" +"    повысить реактивность экструдера на изменения скорости.\n" +"2. Повысьте ограничения скоростей нависаний.\n" +"3. Если это не помогло, воспользуйтесь сглаживанием расхода." msgid "mm/s or %" msgstr "мм/с или %" @@ -13433,9 +13360,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Скорость печати внутреннего моста. Можно указать процент от скорости внешнего моста (bridge_speed). По умолчанию – 150%." -msgid "Brim width" -msgstr "Ширина каймы" - msgid "This is the distance from the model to the outermost brim line." msgstr "Расстояние от модели до внешней линии каймы." @@ -13517,6 +13441,14 @@ msgstr "" "Геометрия модели будет упрощена перед обнаружением острых углов. Этот параметр задаёт минимальную длину отклонения для её упрощения.\n" "Установите 0 для отключения." +# AI Translated +msgid "Brim ears outer only" +msgstr "Ушки каймы только снаружи" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Создавать мышиные ушки только на внешнем контуре модели, исключая отверстия и замкнутые участки." + msgid "upward compatible machine" msgstr "условия для совместимых принтеров" @@ -13546,11 +13478,12 @@ msgstr "Послойно" msgid "By object" msgstr "По очереди" -# ???Внутрислойный порядок печати msgid "Intra-layer order" msgstr "Очерёдность моделей" -# AI Translated +# До 2.5.0 было легко и просто, теперь взяли и кучу подкапотных алгоритмов на пользователя вывалили... Упростил, насколько это возможно без потери смысла, чтобы всем угодить. Контекст: в 2.5.0 настройку переработали, чтобы все алгоритмы создавали цикличный маршрут, при котором конечная точка была бы максимально близко к началу (предположительно, следующего слоя). Цикличный маршрут позволяет избежать перемещений над уже напечатанным на слое, что убирает необходимость в Z-hop для простых моделей. PR: https://github.com/OrcaSlicer/OrcaSlicer/pull/13578 +# +# Примечания: Default использует алгоритм Greedy, 2-opt – алгоритм устранения самопересечений. Кратчайший путь проверяет всего 2 стратегии, остальные уже удалены как неэффективные и медленные. msgid "" "Order in which object instances are visited within a single layer, which controls how much travel is spent moving between them.\n" "\n" @@ -13561,23 +13494,26 @@ msgid "" "\n" "With multiple filaments or tools in the same layer, minimizing tool changes takes priority: objects are grouped by filament first and this setting only orders the instances within each filament group, so the overall sequence may not look like the shortest path across the plate." msgstr "" -"Порядок обхода экземпляров моделей в пределах одного слоя; определяет, сколько перемещений тратится на переходы между ними.\n" +"Последовательность печати моделей в пределах одного слоя. Влияет на суммарное время перемещений от модели к модели.\n" "\n" -"По умолчанию: построение цепочки методом ближайшего соседа с последующим улучшением алгоритмом 2-opt и устранением пересечений. Хороший универсальный вариант.\n" -"По списку: экземпляры печатаются в том же порядке, что и в списке моделей, без какой-либо оптимизации пути. Используйте, когда нужен предсказуемый, задаваемый вручную порядок.\n" -"Лучший из всех (кратчайший путь): оцениваются все стратегии и применяется та, что даёт кратчайший путь. Порядок экземпляров моделей определяется один раз для всей печати, а порядок отдельных островков — для каждого слоя, поэтому на разных слоях могут использоваться разные стратегии. Нарезка идёт немного медленнее.\n" -"Змейкой: змеевидный обход ряд за рядом с улучшением алгоритмом 2-opt. Хорошо подходит для регулярных сеток из множества мелких деталей.\n" +"• По умолчанию: поиск ближайших моделей с оптимизацией пути\n" +"   и устранением пересечений. Быстрый универсальный вариант.\n" +"• По списку: сохранять тот же порядок, что и в списке моделей, без\n" +"   какой-либо оптимизации. Полезно для ручной настройки очерёдности.\n" +"• Кратчайший путь: выбор кратчайшего маршрута из всех стратегий.\n" +"   Маршрут может меняться от слоя к слою из-за изменения числа\n" +"   отдельных контуров. Требует больше времени.\n" +"• Змейкой: зигзагообразный маршрут с оптимизацией. Хорошо\n" +"   подходит для массивов из множества мелких деталей.\n" "\n" -"Если в одном слое используется несколько материалов или инструментов, приоритет отдаётся минимизации смен инструмента: модели сначала группируются по материалу, и эта настройка упорядочивает только экземпляры внутри каждой группы, поэтому общая последовательность может не выглядеть как кратчайший путь по столу." +"Примечание: при использовании нескольких материалов приоритет отдаётся минимизации числа их смен. Очерёдность определяется для каждого материала отдельно, и визуально маршрут может казаться неоптимальным." msgid "As object list" msgstr "По списку" -# AI Translated msgid "Best of all (shortest path)" -msgstr "Лучший из всех (кратчайший путь)" +msgstr "Кратчайший путь" -# AI Translated msgid "Snake" msgstr "Змейкой" @@ -13632,17 +13568,17 @@ msgstr "" "Включение вытяжного вентилятора для лучшего охлаждения внутренней области принтера.\n" "Команда G-кода: M106 P3 S(0-255)" -# AI Translated +# Сломано и не отображается, портировано для X2D msgid "Enable this to override the fan speed set in custom G-code during print." -msgstr "Включите, чтобы переопределить скорость вентилятора, заданную в пользовательском G-code во время печати." +msgstr "Заменять процент скорости вентилятора во время печати, указанную через пользовательский G-код." -# AI Translated +# Сломано и не отображается, портировано для X2D msgid "On completion" msgstr "По завершении" -# AI Translated +# Сломано и не отображается, портировано для X2D msgid "Enable this to override the fan speed set in custom G-code after print completion." -msgstr "Включите, чтобы переопределить скорость вентилятора, заданную в пользовательском G-code после завершения печати." +msgstr "Заменить процент скорости вентилятора после завершения печати, указанную через пользовательский G-код." msgid "Speed of exhaust fan during printing. This speed will override the speed in filament custom G-code." msgstr "Скорость вытяжного вентилятора во время печати. Эта скорость переопределяет скорость в пользовательском G-коде материала." @@ -14134,7 +14070,7 @@ msgid "Extruder Color" msgstr "Цвет экструдера" msgid "Only used as a visual help on UI." -msgstr "Используется только в качестве визуальной помощи в пользовательском интерфейсе." +msgstr "Используется для визуализации в интерфейсе слайсера." msgid "Extruder offset" msgstr "Смещение координат экструдера" @@ -14211,11 +14147,11 @@ msgstr "" " • наиболее высокий расход при печати (обычно, заполнения).\n" "Выбор тестируемых ускорений:\n" " • наиболее низкое ускорение из настроек печати\n" -" • наиболее высокое ускорение (не должно превышать\n" -"    рекомендуемый предел калибровщика Input Shaper в Klipper)\n" +" • наиболее высокое ускорение (не должно превышать предел\n" +"    рекомендуемого шейпера в Klipper)\n" "\n" -"2. Выпишите коэффициенты PA по примеру выше для каждой пары расхода/ускорения. Удельный расход можно посмотреть в «Просмотре нарезки», выбрав режим отображения «Объёмный расход». Значение отображается над горизонтальной шкалой печати слоя. Особенности:\n" -"• Как правило, значение PA должно снижаться с повышением расхода.\n" +"2. Выпишите коэффициенты PA по примеру выше для каждой пары расхода/ускорения. Значения расхода можно посмотреть в «Просмотре нарезки», выбрав режим отображения «Объёмный расход». Значение отображается над горизонтальной шкалой печати слоя. Особенности:\n" +"• Как правило, коэффициент должен снижаться с повышением расхода.\n" "   Если это не так, проверьте экструдер и корректность тестов.\n" "• Диапазон PA растёт со снижением скоростей и ускорений.\n" "• Если разницы между тестами не наблюдается, выбирайте PA из\n" @@ -14226,7 +14162,6 @@ msgstr "" msgid "Enable adaptive pressure advance within features (beta)" msgstr "Адаптироваться к изменениям линии" -# AI Translated msgid "" "Enable adaptive PA whenever there are flow changes in a feature, such as line width changes in a corner or overhangs.\n" "\n" @@ -14234,11 +14169,11 @@ msgid "" "\n" "This is an experimental option, as if the PA profile is not set accurately, it will cause uniformity issues." msgstr "" -"Включает адаптивный PA всякий раз, когда в элементе есть изменения потока, например изменение ширины линии на углу или нависаниях.\n" +"Экспериментальный режим смены коэффициента РА прямо посреди печати линии для адаптации к изменениям расхода на ней (например, на нависаниях или периметрах переменной ширины).\n" "\n" -"Несовместимо с принтерами Prusa, так как они делают паузу для обработки изменений PA, что вызывает задержки и дефекты.\n" +"Предупреждение: неточный подбор значений РА может привести к дефектам при работе этой настройки.\n" "\n" -"Это экспериментальная опция: если профиль PA задан неточно, это вызовет проблемы с однородностью." +"Внимание: несовместимо с принтерами Prusa, поскольку им требуется остановка для изменения коэффициента РА." msgid "Static pressure advance for bridges" msgstr "Фиксированный PA на мостах" @@ -14283,6 +14218,8 @@ msgstr "Время слоя" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "Вентилятор охлаждения моделей будет включён для слоёв, расчётное время которых меньше этого значения. Скорость вентилятора интерполируется между минимальной и максимальной скоростями зависимости от времени печати слоя." +# AI Translated +msgctxt "second" msgid "s" msgstr "с" @@ -14309,18 +14246,18 @@ msgid "Minimum HRC of nozzle required to print the filament. A value of 0 means msgstr "Минимальная твёрдость материала сопла (HRC), необходимая для печати материалом. 0 – отключение проверки твёрдости сопел." msgid "Filament map to extruder" -msgstr "Назначение филамента на экструдер" +msgstr "Назначение материала на экструдер" msgid "Filament map to extruder." -msgstr "Назначение филамента на экструдер." +msgstr "Назначение материала на экструдер." msgid "Auto For Flush" msgstr "Авто для прочистки" +# Не смог найти в интерфейсе msgid "Auto For Match" msgstr "Авто для сопоставления" -# AI Translated msgid "Nozzle Manual" msgstr "Ручная настройка сопел" @@ -14330,9 +14267,8 @@ msgstr "Температура прочистки" msgid "Temperature when flushing filament. 0 indicates the upper bound of the recommended nozzle temperature range." msgstr "Температура при прочистке. 0 – использовать максимально допустимую температуру." -# AI Translated msgid "Flush temperature used in fast purge mode." -msgstr "Температура прочистки, используемая в режиме быстрой прочистки." +msgstr "Температура в режиме быстрой прочистки." msgid "Flush volumetric speed" msgstr "Расход при прочистке" @@ -14402,9 +14338,9 @@ msgid "" "\n" "Note: Experimental and incomplete feature imported from BBS. Functional for some profiles that already have the variable saved." msgstr "" -"При включении поток экструзии ограничивается меньшим из расчётного значения (вычисленного по ширине линии и высоте слоя) и заданного пользователем максимального потока. При отключении применяется только заданный пользователем максимальный поток.\n" +"Регулирует значение максимального расхода при печати тонких и узких линий для достижения оптимальной линейной скорости. По сути, ограничивает расход материала в случаях, когда завышенная скорость движения сопла не позволяет материалу надёжно спекаться с предыдущим слоем.\n" "\n" -"Примечание: экспериментальная и неполная функция, перенесённая из BBS. Работает для некоторых профилей, в которых уже сохранена эта переменная." +"Внимание: требует настройки модели расхода и пока работает только у преднастроенных производителем профилей материалов." msgid "Max volumetric speed multinomial coefficients" msgstr "Коэффициенты расчёта максимального расхода" @@ -14496,28 +14432,33 @@ msgstr "Мин. объём прочистки на черновой башне" msgid "After a tool change, the exact position of the newly loaded filament inside the nozzle may not be known, and the filament pressure is likely not yet stable. Before purging the print head into an infill or a sacrificial object, Orca Slicer will always prime this amount of material into the wipe tower to produce successive infill or sacrificial object extrusions reliably." msgstr "После смены инструмента, точное положение вновь загруженного прутка внутри него может быть неизвестно, и давление прутка, вероятно, ещё не стабильно. Перед тем, как очистить печатающую головку в заполнение или в «жертвенную» модель Orca Slicer всегда будет выдавливать это количество материала на черновую башню, чтобы обеспечить надёжную печать заполнения или «жертвенной» модели." -# AI Translated +# Не реализовано msgid "Wipe tower cooling" -msgstr "Охлаждение черновой башни" +msgstr "Охлаждение на черновой башне" -# AI Translated msgid "Temperature drop before entering filament tower" -msgstr "Снижение температуры перед входом в башню материала" +msgstr "Снижение температуры перед входом в башню" msgid "Interface layer pre-extrusion distance" -msgstr "Расстояние предэкструзии слоя интерфейса" +msgstr "Дистанция избыточной подачи при смене" msgid "Pre-extrusion distance for prime tower interface layer (where different materials meet)." -msgstr "Расстояние предэкструзии слоя интерфейса башни очистки (где соприкасаются разные материалы)." +msgstr "" +"Протяжённость первичного движения прочистки после смены материала. Позволяет быстро набрать давление в сопле и сбросить перегретый материал.\n" +"\n" +"Примечание: фактическая длина может быть ограничена шириной башни." msgid "Interface layer pre-extrusion length" -msgstr "Длина предэкструзии слоя интерфейса" +msgstr "Длина прутка для избыточной подачи" msgid "Pre-extrusion length for prime tower interface layer (where different materials meet)." -msgstr "Длина предэкструзии слоя интерфейса башни очистки (где соприкасаются разные материалы)." +msgstr "" +"Длина прутка, которую необходимо продавить на этапе избыточной подачи.\n" +"\n" +"0 – отключить этот этап." msgid "Tower ironing area" -msgstr "Разглаживание черновой башни" +msgstr "Разглаживание кончиков" # У H2D не требует, на Вики при этом написано, что требует... В баганном порте # "улучшений" башни вообще не работает ни при каких обстоятельствах. @@ -14530,15 +14471,18 @@ msgstr "" msgid "mm²" msgstr "мм²" +# Сломано и ни на что не влияет, даже на H2D и даже в Bambu Studio. msgid "Interface layer purge length" msgstr "Прочистка на слое интерфейса" +# Из ченжлогов Bambu: Studio will do nozzle wiping before flushing rather than after flushing, and print the prime tower directly, reducing extrusion volume fluctuations and improving layer height consistency. Added a new parameter for interface-layer flushing length. msgid "Purge length for prime tower interface layer (where different materials meet)." -msgstr "Длина прочистки на слое интерфейса башни (где соприкасаются разные материалы)." +msgstr "В настоящее время ничего не делает. Задумывалось как настройка расстояния внешней очистки сопла перед основной процедурой его прочистки." msgid "Interface layer print temperature" msgstr "Температура при прочистке" +# Не используется, настройка сломана. Перевёл на случай, если починят в будущем. msgid "Print temperature for prime tower interface layer (where different materials meet). If set to -1, use max recommended nozzle temperature." msgstr "" "Целевая температура при печати слоёв прочистки в черновой башне (место контакта разных материалов).\n" @@ -14605,6 +14549,62 @@ msgstr "Материал поддержки" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Обычно используется для печати поддержки и связующего слоя (интерфейса)." +# AI Translated +msgid "Is mixed filament" +msgstr "Является смешанным материалом" + +# AI Translated +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "Определяет, является ли этот слот материала смешанным материалом, состоящим из нескольких физических материалов" + +# AI Translated +msgid "Mixed filament components" +msgstr "Компоненты смешанного материала" + +# AI Translated +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "Разделённые запятыми индексы материалов-компонентов, начиная с 1, например \"1,3\"" + +# AI Translated +msgid "Mixed filament sublayer ratios" +msgstr "Доли подслоёв смешанного материала" + +# AI Translated +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "Разделённые запятыми значения долей, дающие в сумме 1.0, например \"0.7,0.3\"" + +# AI Translated +msgid "Mixed filament gradient" +msgstr "Градиент смешанного материала" + +# AI Translated +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "Включает режим градиента по оси Z для подслоёв смешанного материала. При включении доли подслоёв линейно меняются от слоя к слою." + +# AI Translated +msgid "Mixed filament gradient range" +msgstr "Диапазон градиента смешанного материала" + +# AI Translated +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "Начальная и конечная доли первого компонента в режиме градиента. Пара значений через запятую, например \"0.10,0.90\" означает от 10% до 90%." + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "Кривая градиента смешанного материала" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "Необязательная пользовательская кривая в стиле Photoshop, сопоставляющая прогресс по оси Z с долей первого компонента. Задаётся как контрольные точки, разделённые вертикальной чертой, в виде \"x,y\" (устаревший формат) или \"x,y,m_in,m_out\", если требуется переопределить касательную (пустое значение или \"nan\" означает использование значения PCHIP по умолчанию). x лежит в [0,1]; y ограничивается заданным диапазоном долей, например \"0,0.15|0.5,0.50|1,0.85\". Если поле пустое, вместо этого используется линейный gradient_range." + +# AI Translated +msgid "Mixed filament per-part gradient" +msgstr "Градиент смешанного материала по частям" + +# AI Translated +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "Если включён режим градиента, градиент применяется к каждой части сборки отдельно, а не ко всей сборке как к единому диапазону Z." + # ??? Настройка в режиме разработчика. Должна отображаться где-то в настройках # профиля, но поиском не ищется. msgid "Filament printable" @@ -14613,11 +14613,9 @@ msgstr "Пригодный для печати" msgid "The filament is printable in extruder." msgstr "Материалом можно печатать через экструдер." -# AI Translated msgid "Filament-extruder compatibility" msgstr "Совместимость материала и экструдера" -# AI Translated msgid "A single 32-bit int encoding the compatibility level of a filament across all extruders (up to 10). Every 3 bits represent one extruder (bits [3*i, 3*i+2] for extruder i). 0: printable, 1: error, 2: critical warning, 3: warning, 4-7: reserved." msgstr "Одно 32-битное целое число, кодирующее уровень совместимости материала со всеми экструдерами (до 10). Каждые 3 бита представляют один экструдер (биты [3*i, 3*i+2] для экструдера i). 0: пригоден для печати, 1: ошибка, 2: критическое предупреждение, 3: предупреждение, 4-7: зарезервировано." @@ -14825,6 +14823,14 @@ msgstr "ТПМП Фишера-Коха S" msgid "Gyroid" msgstr "Гироид" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Коэффициент сглаживания заполнения" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Определяет, насколько сильно скругляются углы заполнения. 0% сохраняет исходную траекторию с острыми углами, а 100% создаёт максимально возможные скругления между соседними линиями заполнения." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Ускорение на верхней поверхности. Использование меньшего значения может улучшить качество верхней поверхности." @@ -14863,36 +14869,35 @@ msgstr "" "Внимание: параметр является устаревшим и в новых версиях Klipper был переименован в \"minimum_cruise_ratio\"." msgid "Default jerk." -msgstr "Рывок по умолчанию." +msgstr "Рывки по умолчанию." msgid "Marlin Firmware Junction Deviation (replaces the traditional XY Jerk setting)." msgstr "Junction Deviation для прошивки Marlin (заменяет традиционную настройку рывка XY)." msgid "Jerk of outer walls." -msgstr "Рывок для внешних периметров." +msgstr "Рывки на внешних периметрах." msgid "Jerk of inner walls." -msgstr "Рывок для внутренних периметров." +msgstr "Рывки на внутренних периметрах." msgid "Jerk for top surface." -msgstr "Рывок для верхней поверхности." +msgstr "Рывки на верхней поверхности." msgid "Jerk for infill." -msgstr "Рывок для заполнения." +msgstr "Рывки на заполнении." msgid "Jerk for the first layer." -msgstr "Рывок для первого слоя." +msgstr "Рывки на первом слое." msgid "Jerk for travel." -msgstr "Рывок при перемещении." +msgstr "Рывки при перемещениях." -# AI Translated msgid "" "Travel jerk of first layer.\n" "The percentage value is relative to Travel Jerk." msgstr "" -"Рывок перемещения первого слоя.\n" -"Процентное значение задаётся относительно рывка перемещения." +"Рывки при перемещениях на первом слое.\n" +"Можно указать процент от рывков при обычных перемещениях." msgid "Line width of the first layer. If expressed as a %, it will be computed over the nozzle diameter." msgstr "Ширина линий первого слоя. Можно указать процент от диаметра сопла." @@ -15015,7 +15020,7 @@ msgid "Ironing line spacing" msgstr "Интервал линий" msgid "Filament-specific override for ironing line spacing. This allows you to customize the spacing between ironing lines for each filament type." -msgstr "Индивидуальная настройка расстояния между линиями разглаживания для каждого типа филамента." +msgstr "Замещение интервала между линиями разглаживания. Позволяет настроить интервал отдельно для каждого материала." # "Разглаживание" явно задано в заголовке раздела msgid "Ironing inset" @@ -15115,7 +15120,7 @@ msgid "" msgstr "" "Тип шума, используемый для генерации нечёткой оболочки.\n" "\n" -"• Случайный: равномерно резкий шум.\n" +"• Классический: равномерно резкий шум.\n" "• Шум Перлина: согласованный однородный шум.\n" "• Волновой: более резкий вариант шума Перлина.\n" "• Ребристый: резкий сглаженный шум с мраморной текстурой.\n" @@ -15124,9 +15129,9 @@ msgstr "" "\n" "Примечание: для разных алгоритмов оптимальны разные масштабы." -# Поменял на "случайный" из-за нехватки места. +# Поменял на "случайный" из-за нехватки места. UPD: Вернул из-за конфликта с генератором периметров msgid "Classic" -msgstr "Случайный" +msgstr "Классический" msgid "Perlin" msgstr "Шум Перлина" @@ -15312,31 +15317,28 @@ msgstr "Наилучшее расположение модели при авто msgid "Enable this option if machine has auxiliary part cooling fan. G-code command: M106 P2 S(0-255)." msgstr "" -"Если в принтере имеется вспомогательный вентилятор для охлаждения моделей (обычно это боковой вентилятор), можете включить эту опцию.\n" +"Включить управление вспомогательным вентилятором из настроек материала.\n" "Команда G-кода: M106 P2 S(0-255)." -# AI Translated msgid "Fan direction" msgstr "Направление вентилятора" -# AI Translated msgid "Cooling fan direction of the printer" -msgstr "Направление вентилятора охлаждения принтера" +msgstr "Направление потока охлаждения вспомогательного вентилятора" -# AI Translated msgid "Both" -msgstr "Оба" +msgstr "Обе стороны" +# "only custom start G-code" в Орке не существует. Вероятно, забыли стереть при портировании из SuperSlicer. msgid "" "Start the fan this number of seconds earlier than its target start time (you can use fractional seconds). It assumes infinite acceleration for this time estimation, and will only take into account G1 and G0 moves (arc fitting is unsupported).\n" "It won't move fan commands from custom G-code (they act as a sort of 'barrier').\n" "It won't move fan commands into the start G-code if the 'only custom start G-code' is activated.\n" "Use 0 to deactivate." msgstr "" -"Запуск вентилятора на указанное количество секунд раньше целевого времени запуска (поддерживаются доли секунды). При этом предполагается бесконечное ускорение для оценки этого времени, и учёт только перемещений G1 и G0 (Аппроксимация дугами).\n" -"Это не приведёт к сдвигу команд вентилятора из пользовательских G-кодов (они действуют как своего рода барьер).\n" -"Это не приведёт к сдвигу команд вентилятора в стартовом G-коде, если активировано «только пользовательский стартовый G-код».\n" -"Установите 0 для отключения." +"Сместить управление вентилятором на заданное время для его запуска заранее. Можно указать дробное количество секунд.\n" +"\n" +"Примечание: расчёт смещения не учитывает ускорения, команды движения по дуге (аппроксимацию дугами) и пользовательский G-код." msgid "Only overhangs" msgstr "Только на нависаниях" @@ -15352,7 +15354,8 @@ msgid "" "This is useful for fans where a low PWM/power may be insufficient to get the fan started spinning from a stop, or to get the fan up to speed faster.\n" "Set to 0 to deactivate." msgstr "" -"Время принудительного запуска (kick-start) вентилятора на максимальной скорости, после чего скорость снижается до целевой. Это необходимо для вентиляторов у которых низкое значение уровня ШИМ/мощности может быть недостаточен для запуска вентилятора после остановки или для более быстрого увеличения скорости его вращения.\n" +"Время принудительного запуска на максимальных оборотах перед выходом на заданный процент скорости. Полезно для повышения отзывчивости вентиляторов с инертным ротором (а также в случаях, когда для его страгивания требуется дополнительное усилие).\n" +"\n" "Установите 0 для отключения." msgid "Minimum non-zero part cooling fan speed" @@ -15396,16 +15399,15 @@ msgid "" "Enable this if printer support air filtration\n" "G-code command: M106 P3 S(0-255)" msgstr "" -"Если в принтере имеется вытяжной вентилятор и вам требуется дополнительное охлаждение внутренней области принтера, включите эту опцию.\n" +"Включить управление вытяжным вентилятором из настроек материала.\n" "Команда G-кода: M106 P3 S(0-255)" -# AI Translated +# Из ченжлогов Bambu: Added a filtering option with cooling mode for the adaptive air circulation system. This option can be enabled via the slicer or the printer UI, and it's mainly used to filter the air when exhausting the air for low-temperature filaments msgid "Use cooling filter" -msgstr "Использовать фильтр охлаждения" +msgstr "Охлаждать через фильтр" -# AI Translated msgid "Enable this if printer support cooling filter" -msgstr "Включите, если принтер поддерживает фильтр охлаждения" +msgstr "Включить управление вытяжным вентилятором." msgid "G-code flavor" msgstr "Тип G-кода" @@ -15416,6 +15418,14 @@ msgstr "Выбор типа G-кода для совместимости с пр msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Пропустить блок конфигурации в G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Не записывать CONFIG_BLOCK (пары ключ/значение с настройками слайсера) в файл G-code. Это может помочь с принтерами, прошивка которых аварийно завершается при разборе этих строк комментариев (например, Anycubic go-klipper). Примечание: файл G-code больше не будет содержать настройки слайсера, поэтому при обратном импорте в OrcaSlicer конфигурация не восстановится." + msgid "Pellet Modded Printer" msgstr "Гранульная модификация принтера" @@ -15586,33 +15596,28 @@ msgstr "Угол нависания заполнения" msgid "The angle of the infill angled lines. 60° will result in a pure honeycomb." msgstr "Угол нависания линий заполнения. При 60° получаются правильные соты." -# AI Translated msgid "Lightning overhang angle" -msgstr "Угол нависания для «Молнии»" +msgstr "Наклон поверхности для поддержки молнией" -# AI Translated msgid "Maximum overhang angle for Lightning infill support propagation." -msgstr "Максимальный угол нависания для распространения поддержки заполнения «Молния»." +msgstr "Максимальный угол наклона внутренних поверхностей для их поддержки отдельными ветвями молнии." -# AI Translated msgid "Prune angle" -msgstr "Угол обрезки" +msgstr "Наклон опор" -# AI Translated +# Оооочень технично. Самая главная проблема в понимании – шаблон генерируется "сверху вниз" (а не наоборот, как можно бы подумать), из-за чего и возникает вся эта техничность в содержании подсказок. На Вики чуть понятнее, но всё равно чрезвычайно технично. Убрал описание тонкостей фильтрации алгоритма и заменил на то, что по сути делает настройка. msgid "" "Controls how aggressively short or unsupported Lightning branches are pruned.\n" "This angle is converted internally to a per-layer distance." -msgstr "" -"Определяет, насколько агрессивно обрезаются короткие или неподдерживаемые ветви «Молнии».\n" -"Внутренне этот угол преобразуется в расстояние на слой." +msgstr "Допустимый наклон опор молнии. Чем выше, тем быстрее и экономичнее распространяются её ветви." -# AI Translated +# "Выпрямление" здесь, вопреки первой мысли – это как раз-таки наоборот искажение шаблона по ходу печати для сокращения количества ветвей. Короче, опять путаница из-за того, что генерация ветвей происходит сверху вниз. При печати снизу вверх шаблон именно что искажается. msgid "Straightening angle" -msgstr "Угол выпрямления" +msgstr "Наклон локальных искажений" -# AI Translated +# При минимальном значении сразу понятно, что делает. Может стоит это указать, чтобы людям было проще осваивать настройку. msgid "Maximum straightening angle used to simplify Lightning branches." -msgstr "Максимальный угол выпрямления, используемый для упрощения ветвей «Молнии»." +msgstr "Дополнительные искажения позволяют эффективнее объединять опоры для ветвей. Чем больше наклон, тем сильнее может искажаться контур молнии." msgid "Sparse infill anchor length" msgstr "Длина привязок шаблона заполнения" @@ -15983,37 +15988,34 @@ msgstr "Минимальная скорость холостых перемещ msgid "Minimum travel speed (M205 T)" msgstr "Минимальная скорость перемещения без печати (M205 T)" -# AI Translated msgid "Maximum force of the Y axis" msgstr "Максимальное усилие оси Y" -# AI Translated +# По сути, перемножение массы на ускорение msgid "The allowed maximum output force of Y axis" -msgstr "Допустимое максимальное выходное усилие оси Y" +msgstr "Максимально допустимое усилие по оси Y." # AI Translated +msgctxt "Newton" msgid "N" msgstr "Н" -# AI Translated msgid "Bed mass of the Y axis" -msgstr "Масса стола по оси Y" +msgstr "Масса стола (оси Y)" -# AI Translated msgid "The machine bed mass load of Y axis" -msgstr "Массовая нагрузка стола машины по оси Y" +msgstr "Пассивная нагрузка механики оси Y массой стола." # AI Translated +msgctxt "gram" msgid "g" msgstr "г" -# AI Translated msgid "The allowed max printed mass" -msgstr "Допустимая максимальная масса печати" +msgstr "Максимально допустимая масса печати" -# AI Translated msgid "The allowed max printed mass on a plate" -msgstr "Допустимая максимальная масса печати на столе" +msgstr "Максимально допустимая масса деталей на столе." msgid "Maximum acceleration for extruding" msgstr "Максимальное ускорение при печати" @@ -16171,8 +16173,9 @@ msgid "The highest printable layer height for the extruder. Used to limit the ma msgstr "Максимальная высота слоя для печати этим экструдером. Используется в качестве ограничения при использовании адаптивной высоты слоя." msgid "Extrusion rate smoothing" -msgstr "Сглаживание подачи" +msgstr "Сглаживание расхода" +# Провёл семантическую оптимизацию, очень уж длинно расписано. Плюс убрал про PrusaSlicer, т.к. в локализациях одинаково. + пояснения про скорость/ширину, т.к. Орка теперь умеет напрямую раскрашивать расход без необходимости считать его ручками. msgid "" "This parameter smooths out sudden extrusion rate changes that happen when the printer transitions from printing a high flow (high speed/larger width) extrusion to a lower flow (lower speed/smaller width) extrusion and vice versa.\n" "\n" @@ -16188,16 +16191,15 @@ msgid "" "\n" "Note: this parameter disables arc fitting." msgstr "" -"Сглаживает резкие изменения скорости подачи материала, которые происходят при переходе от печати с большим расходом (высокая скорость/большая ширина линии) к печати с меньшим расходом (меньшая скорость/меньшая ширина) и наоборот.\n" +"Сглаживает резкие изменения скорости подачи материала, которые происходят при переходе от печати с бóльшим расходом к печати с меньшим (и наоборот).\n" +"\n" +"По сути, ограничение производной от расхода: ограничивает скорость, с которой расход материала может меняться за единицу времени. Чем выше лимит, тем быстрее может меняться расход.\n" "\n" -"Параметр задаёт максимальную скорость, с которой расход материала может измениться за единицу времени. Чем выше лимит, тем быстрее может меняться расход материала. \n" "Установите 0 для отключения.\n" "\n" -"Для скоростных принтеров с прямой системой подачи и производительным экструдером (например, Bambu lab или Voron) сглаживание подачи обычно не требуется. Однако в некоторых случаях, когда скорость печати сильно различается, это может принести дополнительную пользу. Например, когда происходят резкие замедления из-за нависаний. В этих случаях рекомендуется использовать высокое значение, составляющее около 300-350 мм³/с², при оптимально настроенном Pressure Advance (коррекции давления) это поможет достичь более плавного перехода.\n" +"Для скоростных принтеров с производительным экструдером сглаживание обычно не требуется, но может быть полезным в местах с высоким перепадом скоростей. Например, для сглаживания замедлений при печати нависаний. При точно настроенной коррекции давления (коэффициент PA и время сглаживания) значения около 300-350 мм³/с² помогут дополнительно сгладить такие места.\n" "\n" -"У более медленных принтеров с внешней системой подачи или прошивкой без коррекции давления значение должно быть значительно ниже. 10-15 мм³/с² является хорошей отправной точкой для экструдеров с прямой подачей и 5-10 мм³/с² для внешней.\n" -"\n" -"В Prusa Slicer эта функция известна как «Сглаживание расхода» (Pressure equalizer).\n" +"Для более медленных принтеров с внешней системой подачи или прошивкой без коррекции давления значение должно быть гораздо ниже: 10-15 мм³/с² при прямой подаче и 5-10 мм³/с² при внешней.\n" "\n" "Примечание: при ненулевом значении отключает аппроксимацию дугами." @@ -16205,7 +16207,7 @@ msgid "mm³/s²" msgstr "мм³/с²" msgid "Smoothing segment length" -msgstr "Длина сглаживающего сегмента" +msgstr "Протяжённость сглаживания" msgid "" "A lower value results in smoother extrusion rate transitions. However, this results in a significantly larger G-code file and more instructions for the printer to process.\n" @@ -16220,14 +16222,13 @@ msgstr "" "\n" "Допустимые значения: 0,5–5" -# ??? msgid "Apply only on external features" -msgstr "Применять только к видимым элементам" +msgstr "Применять только к видимым элементам" msgid "Applies extrusion rate smoothing only on external perimeters and overhangs. This can help reduce artefacts due to sharp speed transitions on externally visible overhangs without impacting the print speed of features that will not be visible to the user." msgstr "" -"Сглаживание скорости экструзии будет применяться только к внешним периметрам и нависаниям.\n" -"Это помогает уменьшить количество артефактов, вызванные резкими перепадами скорости на видимых внешних участках, без влияния на скорость печати внутренних элементов, которые не видны пользователю." +"Применять сглаживание расхода только к внешним периметрам и нависаниям.\n" +"Помогает компенсировать видимые артефакты перепада скоростей без влияния на скорость печати внутренних элементов." msgid "Minimum speed for part cooling fan." msgstr "Минимальная скорость вентилятора обдува модели." @@ -16236,29 +16237,28 @@ msgid "" "Speed of auxiliary part cooling fan. Auxiliary fan will run at this speed during printing except the first several layers which is defined by no cooling layers.\n" "Please enable auxiliary_fan in printer settings to use this feature. G-code command: M106 P2 S(0-255)" msgstr "" -"Скорость вращения вспомогательного вентилятора для охлаждения моделей. Обычно это боковой вентилятор. Он всегда будет работать с этой скоростью, за исключением первых нескольких слоёв, которые обычно настроены на работу без охлаждения.\n" -"Пожалуйста, включите вспомогательный вентилятор для охлаждения моделей (auxiliary_fan) в настройках принтера, чтобы использовать эту функцию.\n" +"Процент скорости вспомогательного вентилятора для охлаждения моделей. Используется на протяжении всей печати (кроме слоёв, указанных в разделе «Охлаждение начала печати»).\n" +"Для работы этой настройки необходимо включить поддержку вспомогательного вентилятора на стороне принтера в его профиле.\n" "Команда G-кода: M106 P2 S(0-255)." -# AI Translated msgid "For the first" msgstr "Для первых" -# AI Translated msgid "Set special auxiliary cooling fan for the first certain layers." -msgstr "Задаёт особую скорость вспомогательного вентилятора обдува для первых нескольких слоёв." +msgstr "Особая скорость вспомогательного вентилятора для первых нескольких слоёв." -# AI Translated msgid "" "Auxiliary fan speed will be ramped up linearly from layer \"For the first\" to maximum at layer \"Full fan speed at layer\".\n" "\"Full fan speed at layer\" will be ignored if lower than \"For the first\", in which case the fan will run at maximum allowed speed at layer \"For the first\" + 1." msgstr "" -"Скорость вспомогательного вентилятора будет линейно увеличиваться от слоя «Для первых» до максимума на слое «Полная скорость вентилятора на слое».\n" -"«Полная скорость вентилятора на слое» будет проигнорирована, если она меньше «Для первых»; в этом случае вентилятор будет работать на максимально допустимой скорости на слое «Для первых» + 1." +"Начиная с указанного слоя, интенсивность охлаждения будет равномерно меняться для перехода к требуемым условиям обдува детали.\n" +"\n" +"Если активна настройка «Не обдувать первые N слоёв» – с учётом этих слоёв.\n" +"\n" +"Примечание: если слоёв с заблокированным обдувом больше, чем указано здесь, то восстановление происходит моментально на первом доступном слое." -# AI Translated msgid "Special auxiliary cooling fan speed, effective only for the first x layers." -msgstr "Особая скорость вспомогательного вентилятора обдува, действует только для первых x слоёв." +msgstr "Особая скорость вспомогательного вентилятора для первых N слоёв." msgid "The lowest printable layer height for the extruder. Used to limit the minimum layer height when enable adaptive layer height." msgstr "Минимальная высота слоя для печати этим экструдером. Используется в качестве ограничения при использовании адаптивной высоты слоя." @@ -16344,7 +16344,7 @@ msgstr "Начальная и конечная точки траектории msgid "Reduce infill retraction" msgstr "Откат только при пересечении периметров" -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." msgstr "" "Отключает откат (и подъём по Z), когда перемещения совершаются полностью над заполнением (где любые подтёки, вероятно, будут скрыты). Это поможет снизить количество откатов при печати сложных моделей и немного сэкономить время, но увеличит время нарезки и генерации G-кода.\n" "\n" @@ -16380,9 +16380,9 @@ msgstr "Глухие отверстия в основании модели ра msgid "Detect overhang walls" msgstr "Обнаруживать нависающие периметры" -#, fuzzy, c-format, boost-format +#, c-format, boost-format msgid "This detects the overhang percentage relative to line width and uses a different speed to print. For 100%% overhang, bridging speed is used." -msgstr "Определяет процент нависания относительно ширины линии и использует разную скорость печати. Для 100%-го нависания используется скорость печати мостов." +msgstr "Использовать разную скорость печати в зависимости от выноса линии относительно её опоры. Для нависаний без опоры используется скорость печати мостов." # В секции "Материал для линий" msgid "Outer walls" @@ -16440,17 +16440,15 @@ msgstr "G-код при смене типа линии (настройки пе msgid "This G-code is inserted when the extrusion role is changed. It runs after the machine and filament extrusion role G-code." msgstr "Команды в G-коде, которые выполняются между печатью разных элементов структуры (например, при переходе от периметра к заполнению). Выполняются после команд смены типа линии из настроек принтера и материала." -# AI Translated msgid "Plugins Used" msgstr "Используемые плагины" -# AI Translated +# Функционал плагинов для этого прояиля, указывается... msgid "Plugin capabilities referenced by this preset, stored as name;uuid;capability." -msgstr "Возможности плагинов, на которые ссылается этот профиль, хранятся как name;uuid;capability." +msgstr "Функционал плагинов, на который ссылается этот профиль, указывается как name;uuid;capability." -# AI Translated msgid "Python plugin(s) invoked at each slicing pipeline step to read and modify intermediate slicing data, including a final G-code post-processing step. Research/experimental." -msgstr "Плагины Python, вызываемые на каждом шаге конвейера нарезки для чтения и изменения промежуточных данных нарезки, включая финальный шаг постобработки G-code. Исследовательская/экспериментальная функция." +msgstr "Python-плагины, вызываемые на каждом этапе нарезки для чтения и изменения промежуточных её данных (включая финальную постобработку G-кода). Тестовая/экспериментальная функция." msgid "Printer type" msgstr "Тип принтера" @@ -16519,18 +16517,16 @@ msgstr "" "\n" "Примечание: значение не может быть меньше 25% или больше 100% и будет скорректировано автоматически при нарезке." -# AI Translated msgid "Retract amount after wipe" -msgstr "Величина отката после обтирания" +msgstr "Вторичный откат" -# AI Translated #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" -"Длина быстрого отката после обтирания относительно длины отката.\n" -"Значение будет ограничено 100% минус величина отката до обтирания." +"Быстрый откат после очистки, выраженный в процентах от общей длины отката. В некоторых случаях позволяет значительно снизить количество «паутины».\n" +"Примечание: суммарное значение не должно превышать 100% и будет скорректировано автоматически." msgid "Retract on layer change" msgstr "Откат при смене слоя" @@ -16563,10 +16559,18 @@ msgstr "Длинный откат перед сменой экструдера" msgid "Retraction distance when extruder change" msgstr "Длина отката перед сменой экструдера" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Длина отката (смена инструмента)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "При срабатывании отката перед сменой инструмента материал втягивается на указанную величину (длина измеряется по прутку материала до его входа в экструдер)." + msgid "Z-hop height" msgstr "Высота подъёма" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "При каждом откате печатная голова немного приподнимается, создавая зазор между соплом и моделью. Это предотвращает столкновение сопла с моделью при перемещении. Использование спирального подъёма может помочь сократить образование \"паутины\"." msgid "Z-hop lower boundary" @@ -16657,18 +16661,16 @@ msgstr "Прямой (Direct)" msgid "Bowden" msgstr "Внешний (Bowden)" -# AI Translated +# https://wiki.bambulab.com/ru/h2c/manual/bambu-studio-h2c-operation#:~:text=Гибридный msgid "Hybrid" msgstr "Гибридный" -# Разобраться позже: wiki.bambulab.com/en/software/bambu-studio/filament-track-switch-dynamic-mapping -# AI Translated +# https://wiki.bambulab.com/ru/software/bambu-studio/filament-track-switch-dynamic-mapping msgid "Enable filament dynamic map" -msgstr "Включить динамическое сопоставление материалов" +msgstr "Динамическое сопоставление материалов" -# AI Translated msgid "Enable dynamic filament mapping during print." -msgstr "Включает динамическое сопоставление материалов во время печати." +msgstr "Включить динамическое сопоставление материалов во время печати." msgid "Has filament switcher" msgstr "Автосмена материала" @@ -16682,6 +16684,10 @@ msgstr "Доп. подача после отката" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Дополнительная длина подачи при возврате прутка после отката. Требуется крайне редко (например, для компенсации багов прошивки принтера)." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Доп. подача после отката (смена инструмента)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Дополнительная длина подачи после смены насадки." @@ -16695,15 +16701,15 @@ msgid "Deretraction speed" msgstr "Скорость возврата" msgid "Speed for reloading filament into the nozzle. Zero means same speed of retraction." -msgstr "Скорость возврата материала в экструдер после отката. При значении 0 используется скорость отката." +msgstr "" +"Скорость возврата материала в сопло после отката.\n" +"0 – использовать скорость отката." -# AI Translated msgid "Deretraction speed (extruder change)" -msgstr "Скорость подачи (смена экструдера)" +msgstr "Скорость возврата (смена экструдера)" -# AI Translated msgid "Speed for reloading filament into the nozzle when switching extruder." -msgstr "Скорость возврата материала в сопло при смене экструдера." +msgstr "Скорость возврата материала в сопло после смены экструдера." msgid "Use firmware retraction" msgstr "Откат на уровне прошивки" @@ -16964,13 +16970,13 @@ msgstr "Тип юбки" # Про расширенное описание см. в комментарии к перевод подсказки настройки "Skirt minimum extrusion length" msgid "Combined - single skirt for all objects, Per object - individual object skirt." msgstr "" -"Выбор типа печатаемой юбки – одна общая для всех моделей или отдельные юбки для каждой модели.\n" +"Выбор типа печатаемой юбки – одна общая для всех моделей или независимые юбки для каждой модели.\n" "\n" "Внимание: при создании индивидуальных юбок проверка пересечения не совершается, из-за чего при близком расположении моделей они могут накладываться друг на друга. В таких случаях рекомендуется понизить количество контуров." # Отдельный (антоним к "совместный") msgid "Per object" -msgstr "Для каждой модели" +msgstr "Независимый" msgid "Skirt loops" msgstr "Контуров юбки" @@ -17076,21 +17082,20 @@ msgstr "" "Можно указать своё конечное значение потока, чтобы избежать подобных проблем." msgid "If smooth or traditional mode is selected, a timelapse video will be generated for each print. After each layer is printed, a snapshot is taken with the chamber camera. All of these snapshots are composed into a timelapse video when printing completes. If smooth mode is selected, the toolhead will move to the excess chute after each layer is printed and then take a snapshot. Since the melt filament may leak from the nozzle during the process of taking a snapshot, a prime tower is required for smooth mode to wipe the nozzle." -msgstr "На протяжении всей печати встроенная камера делает снимки, которые затем объединяются в ускоренное видео. Избыточные резкие движения в кадре можно сгладить при помощи соответствующего режима; после печати каждого слоя для создания снимка экструдер будет отводиться к лотку для удаления излишков. В этом режиме необходима черновая башня для устранения возможных подтёков во время создания снимка." +msgstr "На протяжении всей печати в конце слоя встроенная камера делает снимки, которые затем объединяются в ускоренное видео. Избыточные резкие движения в кадре можно сгладить при помощи «Плавного» режима, в котором для создания снимка экструдер будет отводиться к лотку для сброса материала. В этом режиме необходима черновая башня для устранения возможных подтёков во время создания снимка." msgid "Traditional" -msgstr "По умолчанию" +msgstr "Обычный" +# Для кнопки "Сгладить" добавили контекст, теперь их можно разделить. msgid "Smooth" -msgstr "Сгладить" +msgstr "Плавный" -# AI Translated msgid "Farthest point timelapse" -msgstr "Таймлапс из дальней точки" +msgstr "Съёмка из дальней точки" -# AI Translated msgid "When enabled, the timelapse snapshot is taken at the farthest point from camera instead of traveling to the wipe tower or excess chute. Only effective in traditional timelapse mode on non-I3 printers." -msgstr "Когда включено, снимок таймлапса делается из наиболее удалённой от камеры точки вместо перемещения к черновой башне или лотку сброса излишков. Действует только в традиционном режиме таймлапса на принтерах, отличных от I3." +msgstr "Делать снимки из наиболее удалённой от камеры точки вместо перемещения к черновой башне или лотку для сброса материала. Работает только в обычном режиме таймлапса на принтерах с кинематикой, отличной от I3." msgid "Temperature variation" msgstr "Разница температур" @@ -17140,10 +17145,10 @@ msgid "Enable this option to omit the custom Change filament G-code only at the msgstr "Полезно для смены материала вручную при совместной печати через общий экструдер, где для этого используются команды M600/PAUSE. Отключает выполнение «G-кода смены материала» в самом начале печати (обычно это не требуется, так как пруток уже заправлен). Команда смены инструмента (например, T0) будет пропускаться на протяжении всей печати." msgid "Wipe tower type" -msgstr "Тип башни очистки" +msgstr "Тип черновой башни" msgid "Choose the wipe tower implementation for multi-material prints. Type 1 is recommended for Bambu and Qidi printers with a filament cutter. Type 2 offers better compatibility with multi-tool and MMU printers and provide overall better compatibility." -msgstr "Выберите реализацию башни очистки для многоматериальной печати. Тип 1 рекомендуется для принтеров Bambu и Qidi с обрезчиком филамента. Тип 2 обеспечивает лучшую совместимость с многоинструментальными и MMU-принтерами и в целом более универсален." +msgstr "Выбор реализации черновой башни при печати несколькими материалами. Тип 1 рекомендуется для принтеров Bambu и Qidi с обрезкой прутка. Тип 2 – универсальный вариант с поддержкой широкого спектра принтеров (в т.ч. с системами смены материала и инструментов)." msgid "Type 1" msgstr "Тип 1" @@ -17169,6 +17174,14 @@ msgstr "" "\n" "Внимание: применимо только к многоэкструдерным принтерам с черновой башней 2 типа." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Ожидание температуры на черновой башне" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Забирает новый инструмент, не дожидаясь достижения температуры печати, перемещается к черновой башне и ждёт нагрева там, непосредственно перед прочисткой. Подтёки при нагреве попадают на башню, а не на модель, а перемещение совмещается с нагревом. Актуально только для принтеров с несколькими экструдерами (несколькими печатающими головами), использующих черновую башню типа 2. Прошивка или макрос смены инструмента не должны сами ждать нагрева. Если отключено, команда ожидания температуры выдаётся сразу после команды смены инструмента." + msgid "No sparse layers (beta)" msgstr "Без разреженных слоёв (beta)" @@ -17768,6 +17781,14 @@ msgstr "" "\n" "Внимание: процент первичного отката будет увеличен, если длины очистки окажется недостаточно при текущей скорости отката (или из-за иных ограничений)." +# AI Translated +msgid "Mixed color sublayer" +msgstr "Подслой смешанного цвета" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "Включает разбиение на подслои смешанного цвета. При включении слои, содержащие материалы смешанного цвета, разбиваются на подслои для получения эффекта смешивания цветов." + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "Черновая башня – специальная структура, которая используется для прочистки сопла от остатков материала и стабилизации давления внутри сопла при смене экструдера, чтобы избежать дефектов на поверхности печатаемой модели." @@ -17780,24 +17801,22 @@ msgstr "" "\n" "Примечание: влияет только на принтеры Bambu, в остальных случаях башня автоматически сужается по необходимости." -# ??? настройка отключена в коде +# настройка отключена в коде msgid "Purging volumes" msgstr "Объём прочистки" -# ??? настройка отключена в коде +# настройка отключена в коде msgid "Flush multiplier" msgstr "Множитель прочистки" msgid "The actual flushing volumes is equal to the flush multiplier value multiplied by the flushing volumes in the table." msgstr "Реальные объёмы прочистки равны произведению множителя и значений, указанных в таблице." -# AI Translated msgid "Flush multiplier (Fast mode)" msgstr "Множитель прочистки (быстрый режим)" -# AI Translated msgid "The flush multiplier used in fast purge mode." -msgstr "Множитель прочистки, используемый в режиме быстрой прочистки." +msgstr "Множитель для режима быстрой прочистки." msgid "Prime volume" msgstr "Объём сброса материала на черновой башне" @@ -17805,24 +17824,20 @@ msgstr "Объём сброса материала на черновой баш msgid "This is the volume of material to prime the extruder with on the tower." msgstr "Объём материала, который необходимо выдавить для подготовки экструдера на черновой башне." -# AI Translated msgid "Prime volume mode" -msgstr "Режим объёма подготовки" +msgstr "Режим прочистки" -# AI Translated msgid "Selects how the wipe-tower prime and flush volumes are computed on multi-extruder printers." -msgstr "Определяет, как вычисляются объёмы подготовки и прочистки черновой башни на многоэкструдерных принтерах." +msgstr "Режим расчёта прочистки на черновой башне для многоэкструдерных принтеров." -# AI Translated msgid "Saving" msgstr "Экономия" -# AI Translated msgid "Fast" msgstr "Быстрый" msgid "This is the width of prime towers." -msgstr "Размер черновой башни по оси X. Размер по оси Y будет автоматически вычислен исходя из необходимого объёма очистки и ширины башни. Таким образом, увеличивая ширину башни вы уменьшаете её длину и наоборот." +msgstr "Размер черновой башни по оси X. Размер по оси Y будет автоматически вычислен исходя из необходимого объёма прочистки и ширины башни. Таким образом, увеличивая ширину башни вы уменьшаете её длину и наоборот." msgid "Wipe tower rotation angle" msgstr "Угол поворота черновой башни" @@ -17854,7 +17869,7 @@ msgid "" "For the wipe tower external perimeters the internal perimeter speed is used regardless of this setting." msgstr "" "Максимальная скорость печати при очистке в черновую башню и печати её разреженных слоёв.\n" -"Во время очистки программа сопоставляет скорость разреженного заполнения и скорость, рассчитанную по 'максимальному объёмному расходу', и использует наименьшую.\n" +"Во время прочистки программа сопоставляет скорость разреженного заполнения и скорость, рассчитанную по 'максимальному объёмному расходу', и использует наименьшую.\n" "При печати разреженных слоёв программа сопоставляет скорость внутренних периметров и скорость, рассчитанную по 'максимальному объёмному расходу', и также использует наименьшую.\n" "\n" "Увеличение этой скорости может повлиять на устойчивость башни, а также увеличить силу, с которой сопло сталкивается с любыми наплывами, которые могли образоваться на черновой башне.\n" @@ -17888,7 +17903,7 @@ msgid "Extra rib length" msgstr "Вынос основания ребра" msgid "Positive values can increase the size of the rib wall, while negative values can reduce the size. However, the size of the rib wall can not be smaller than that determined by the cleaning volume." -msgstr "Положительное значение может увеличить длину ребра, а отрицательное - уменьшить. Однако она не может быть меньше размера, определяемого объёмом очистки." +msgstr "Положительное значение может увеличить длину ребра, а отрицательное - уменьшить. Однако она не может быть меньше размера, определяемого объёмом прочистки." msgid "Rib width" msgstr "Ширина ребра" @@ -18150,79 +18165,70 @@ msgstr "" "\n" "Примечание: периметр может расширяться до ширины элемента." -# AI Translated msgid "Hotend change time" msgstr "Время смены хотэнда" -# AI Translated msgid "Time to change hotend." -msgstr "Время смены хотэнда." +msgstr "Время, затрачиваемое на смену хотэнда." -# AI Translated msgid "Hotend change" msgstr "Смена хотэнда" -# AI Translated msgid "When changing the hotend, it is recommended to extrude a certain length of filament from the original nozzle. This helps minimize nozzle oozing." -msgstr "При смене хотэнда рекомендуется выдавить определённую длину материала из исходного сопла. Это помогает минимизировать вытекание из сопла." +msgstr "При смене хотэнда рекомендуется выдавить немного материала из прежнего сопла. Помогает минимизировать последующие подтёки." -# AI Translated msgid "Extruder change" msgstr "Смена экструдера" msgid "To prevent oozing, the nozzle will perform a reverse travel movement for a certain period after the ramming is complete. The setting define the travel time." -msgstr "Чтобы материал не подтекал, материал после рэмминга немного отъезжает назад. Этот параметр задаёт время движения в обратном направлении." +msgstr "Время движения сопла в обратном направлении во избежание подтёков из него. Выполняется по завершении рэмминга." msgid "To prevent oozing, the nozzle temperature will be cooled during ramming. Therefore, the ramming time must be greater than the cooldown time. 0 means disabled." -msgstr "Для предотвращения подтекания материала, температура сопла будет снижена во время рэмминга. Поэтому время рэмминга должно быть больше времени охлаждения. 0 значит отключено." +msgstr "Во избежание подтёков температура сопла будет снижена на время рэмминга. Поэтому время рэмминга должно быть больше времени охлаждения. 0 значит отключено." -# AI Translated msgid "The maximum volumetric speed for ramming before extruder change, where -1 means using the maximum volumetric speed." -msgstr "Максимальный объёмный расход при рэмминге перед сменой экструдера, где -1 означает использование максимального объёмного расхода." +msgstr "" +"Максимальный объёмный расход для рэмминга перед сменой экструдера.\n" +"-1 – использовать максимальный расход." -# AI Translated msgid "To prevent oozing, the nozzle temperature will be cooled during ramming. Note: only a cooldown command and fan activation are triggered, reaching the target temperature is not guaranteed. 0 means disabled." -msgstr "Во избежание вытекания температура сопла будет снижена во время рэмминга. Примечание: срабатывают только команда охлаждения и включение вентилятора; достижение целевой температуры не гарантируется. 0 означает отключено." +msgstr "" +"Во избежание подтёков температура сопла будет снижена на время рэмминга.\n" +"0 – не менять температуру.\n" +"\n" +"Примечание: срабатывают только команда охлаждения и включение вентилятора; достижение целевой температуры не гарантируется." -# AI Translated msgid "The maximum volumetric speed for ramming before a hotend change, where -1 means using the maximum volumetric speed." -msgstr "Максимальный объёмный расход при рэмминге перед сменой хотэнда, где -1 означает использование максимального объёмного расхода." +msgstr "" +"Максимальный объёмный расход для рэмминга перед сменой хотэнда.\n" +"-1 – использовать максимальный расход." -# AI Translated msgid "length when change hotend" -msgstr "длина при смене хотэнда" +msgstr "Откат при смене хотэнда" -# AI Translated msgid "When this retraction value is modified, it will be used as the amount of filament retracted inside the hotend before changing hotends." -msgstr "При изменении этого значения отката оно будет использоваться как величина отката материала внутри хотэнда перед сменой хотэндов." +msgstr "Величина отката материала внутри хотэнда перед его сменой." -# AI Translated msgid "Support fast purge mode" -msgstr "Поддержка режима быстрой прочистки" +msgstr "Режим быстрой прочистки" -# AI Translated msgid "Whether this printer supports fast purge mode with optimized temperature and multiplier." -msgstr "Поддерживает ли этот принтер режим быстрой прочистки с оптимизированной температурой и множителем." +msgstr "Поддерживает ли принтер режим быстрой прочистки с оптимизированной температурой и множителем." -# AI Translated msgid "Filament change" msgstr "Смена материала" -# AI Translated msgid "The volume of material required to prime the extruder on the tower, excluding a hotend change." -msgstr "Объём материала, необходимый для подготовки экструдера на башне, исключая смену хотэнда." +msgstr "Объём материала, необходимый для прочистки экструдера на башне, не считая смены хотэнда." -# AI Translated msgid "The volume of material required to prime the extruder for a hotend change on the tower." -msgstr "Объём материала, необходимый для подготовки экструдера при смене хотэнда на башне." +msgstr "Объём материала, необходимый для прочистки экструдера на башне при смене хотэнда." -# AI Translated msgid "Preheat temperature delta" -msgstr "Дельта температуры предварительного нагрева" +msgstr "Дельта преднагрева" -# AI Translated msgid "Temperature delta applied during pre-heating before tool change." -msgstr "Дельта температуры, применяемая при предварительном нагреве перед сменой инструмента." +msgstr "Разница температуры для предварительного нагрева перед сменой инструмента." msgid "Detect narrow internal solid infills" msgstr "Оптимизация заполнения узких мест" @@ -18345,11 +18351,11 @@ msgstr "Экспорт настроек в файл." # командная строка? нужен ли пеевод? msgid "Send progress to pipe" -msgstr "Отправлять прогресс в канал" +msgstr "Send progress to pipe" # ??? это относится к командной строке msgid "Send progress to pipe." -msgstr "Отправлять прогресс в канал." +msgstr "Send progress to pipe." msgid "Arrange Options" msgstr "Параметры расстановки" @@ -18499,15 +18505,14 @@ msgstr "" msgid "Log file" msgstr "Файл журнала" -# AI Translated msgid "Redirects debug logging to file.\n" -msgstr "Перенаправляет отладочный журнал в файл.\n" +msgstr "Направляет отладочные записи в файл.\n" msgid "Enable timelapse for print" msgstr "Вкл. таймлапс для печати" msgid "If enabled, this slicing will be considered using timelapse." -msgstr "Если включено, текущая нарезка будет выполнена с учётом функции таймлапса." +msgstr "Выполнять нарезку с учётом записи таймлапса." msgid "Load custom G-code" msgstr "Загрузить пользовательский G-код" @@ -18515,7 +18520,7 @@ msgstr "Загрузить пользовательский G-код" msgid "Load custom G-code from json." msgstr "Загрузить G-код из json." -# ??? назначить идентификаторы +# назначить идентификаторы msgid "Load filament IDs" msgstr "Загрузить идентификаторы материалов" @@ -18525,7 +18530,6 @@ msgstr "Загрузить идентификаторы материалов д msgid "Allow multiple colors on one plate" msgstr "Игнорировать разницу в цвете" -# ???? msgid "If enabled, Arrange will allow multiple colors on one plate." msgstr "Если включено, модели разных цветов не будут разделяться на разные столы." @@ -18574,9 +18578,8 @@ msgstr "Список значений метаданных, добавляемы msgid "Allow 3MF with newer version to be sliced" msgstr "Разрешить нарезку 3MF более новой версии" -# ??? msgid "Allow 3MF with newer version to be sliced." -msgstr "Разрешить нарезку новых версий 3MF-файлов." +msgstr "Разрешить нарезку проектов из более новых версий." msgid "Current Z-hop" msgstr "Подъём оси Z" @@ -18921,13 +18924,13 @@ msgid "" "An object's XY size compensation will not be used because it is also color-painted.\n" "XY Size compensation cannot be combined with color-painting." msgstr "" -"Коррекция горизонтальных размеров модели не будет действовать, поскольку для этой модели была выполнена операция окрашивания.\n" -"Коррекция горизонтальных размеров модели не может использоваться в сочетании с функцией раскрашивания." +"Функция «Расширение контура/пустот слоя» игнорируется.\n" +"Коррекцию невозможно выполнить для нескольких материалов в окрашенной модели." msgid "" "An object has enabled XY Size compensation which will not be used because it is also fuzzy skin painted.\n" "XY Size compensation cannot be combined with fuzzy skin painting." -msgstr "Функция «Расширение контура слоя» игнорируется. Коррекцию контура слоя невозможно выполнить, если его часть принадлежит нечёткой оболочке." +msgstr "Функция «Расширение контура/пустот слоя» игнорируется. Коррекцию невозможно выполнить, если часть слоя принадлежит нечёткой оболочке." msgid "Object name" msgstr "Имя модели" @@ -18944,6 +18947,10 @@ msgstr "Не удалось обнаружить форму или создат msgid "The supplied file couldn't be read because it's empty." msgstr "Невозможно прочитать файл, так как он пуст." +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "Формат файла несовместим и не может быть прочитан." + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Неизвестный формат файла: входной файл должен иметь расширение *.stl, *.obj или *.amf(.xml)." @@ -19078,7 +19085,7 @@ msgstr "" #, c-format, boost-format msgid "Only one of the results with the same name: %s will be saved. Are you sure you want to override the other results?" -msgstr "Будет сохранён только один из одноимённых результатов (%s). Вы действительно хотите перезаписать остальные?" +msgstr "Будет сохранён только один из одноимённых результатов (%s). Перезаписать остальные?" #, c-format, boost-format msgid "There is already a previous calibration result with the same name: %s. Only one result with a name is saved. Are you sure you want to overwrite the previous result?" @@ -19089,7 +19096,7 @@ msgid "" "Within the same extruder, the name(%s) must be unique when the filament type, nozzle diameter, and nozzle flow are the same.\n" "Are you sure you want to override the historical result?" msgstr "" -"Для одного экструдера имя (%s) должно быть уникальным, если тип материала, диаметр сопла и поток одинаковы.\n" +"В рамках одного экструдера имя (%s) должно быть уникальным, если тип материала, диаметр сопла и поток одинаковы.\n" "Перезаписать прошлый результат?" #, c-format, boost-format @@ -19221,13 +19228,11 @@ msgstr "Введите имя для сохранения на принтере. msgid "The name cannot exceed 40 characters." msgstr "Максимальная длина имени 40 символов." -# AI Translated msgid "Nozzle ID" msgstr "ID сопла" -# AI Translated msgid "Standard Flow" -msgstr "Стандартный расход" +msgstr "Обычный расход" msgid "Please find the best line on your plate" msgstr "Пожалуйста, найдите лучшую линию на столе" @@ -19394,7 +19399,7 @@ msgstr "История успешных результатов калибров msgid "Refreshing the previous Flow Dynamics Calibration records" msgstr "Обновление записей прошлых калибровок динамики потока" -# AI Translated +# Подставляется toolhead_display_name #, c-format, boost-format msgid "Note: The hotend number on the %s is tied to the holder. When the hotend is moved to a new holder, its number will update automatically." msgstr "Примечание: номер хотэнда на %s привязан к держателю. При перемещении хотэнда в новый держатель его номер обновится автоматически." @@ -19411,7 +19416,7 @@ msgstr "Редактировать калибровку динамики пот #, c-format, boost-format msgid "Within the same extruder, the name '%s' must be unique when the filament type, nozzle diameter, and nozzle flow are identical. Please choose a different name." -msgstr "Для одного экструдера имя «%s» должно быть уникальным при одинаковых типе филамента, диаметре и потоке сопла. Пожалуйста, выберите другое имя." +msgstr "В рамках одного экструдера имя (%s) должно быть уникальным, если тип материала, диаметр сопла и поток одинаковы. Укажите другое имя." msgid "New Flow Dynamic Calibration" msgstr "Новая калибровка динамики потока" @@ -19457,19 +19462,19 @@ msgstr "" "По имени хоста %1% обнаружено несколько IP-адресов.\n" "Выберите адрес для использования." -# AI Translated +# В калибровке температуры и расхода msgid "Auto-scale for nozzle" -msgstr "Автомасштабирование под сопло" +msgstr "Адаптация к соплу" -# AI Translated +# Речь о температурной башне msgid "" "This model is designed around a 0.4 mm nozzle with a 0.2 mm layer height. \n" "When the scaling option is enabled (recommended), it dynamically resizes to match your current nozzle diameter and an appropriate layer height, making the test both accurate and easy to read.\n" "Turn scaling off only if you wish to print the reference model exactly as-is." msgstr "" -"Эта модель рассчитана на сопло 0,4 мм и высоту слоя 0,2 мм. \n" -"Если включено масштабирование (рекомендуется), размер модели динамически подстраивается под диаметр вашего текущего сопла и подходящую высоту слоя, благодаря чему тест получается и точным, и легко читаемым.\n" -"Отключайте масштабирование, только если хотите напечатать эталонную модель ровно в исходном виде." +"Тестовая модель рассчитана на сопло 0,4 мм и высоту слоя 0,2 мм.\n" +"При включении адаптации (рекомендуется) её размер динамически подстраивается под диаметр текущего сопла и подходящую высоту слоя, благодаря чему тест получается более точным и читаемым.\n" +"Отключать имееет смысл для печати эталонной модели ровно в исходном виде." # В заголовке окна куча места msgid "PA Calibration" @@ -19607,13 +19612,14 @@ msgstr "Начальная скорость: " msgid "End speed: " msgstr "Конечная скорость: " -# AI Translated msgid "Auto-adjust to max volumetric speed" -msgstr "Автоподстройка под предел объёмного расхода" +msgstr "Адаптация к расходу" -# AI Translated msgid "If the end speed would exceed the filament's maximum volumetric speed, automatically lower the layer height (keeping standard values and staying within the machine's limits) to reach it. If even the minimum layer height is not enough, lower the end speed instead." -msgstr "Если конечная скорость превысит предел объёмного расхода материала, автоматически уменьшать высоту слоя (сохраняя стандартные значения и оставаясь в пределах ограничений принтера), чтобы её достичь. Если даже минимальной высоты слоя недостаточно, вместо этого снижается конечная скорость." +msgstr "" +"Автоматически уменьшать высоту слоя для обеспечения требуемой скорости. Позволяет не упираться в предел объёмного расхода материала.\n" +"\n" +"Примечание: скорость всё равно может снижаться в случае упора в минимальную высоту слоя из заданных ограничений принтера." msgid "" "Please input valid values:\n" @@ -19626,7 +19632,6 @@ msgstr "" "Шаг ≥ 0\n" "Конечное > начальное + шаг" -# AI Translated #, c-format, boost-format msgid "" "The end speed (%.0f mm/s) exceeds the filament's maximum volumetric speed (%.1f mm³/s), which limits the outer wall to about %.0f mm/s at this line width and layer height.\n" @@ -19634,23 +19639,20 @@ msgid "" "\n" "%s" msgstr "" -"Конечная скорость (%.0f мм/с) превышает предел объёмного расхода материала (%.1f мм³/с), который при такой ширине линии и высоте слоя ограничивает скорость внешних периметров примерно до %.0f мм/с.\n" -" Более высокие скорости будут ограничены, поэтому верхние блоки башни не напечатаются с заданной скоростью.\n" +"Конечная скорость (%.0f мм/с) приводит к превышению предела объёмного расхода материала (%.1f мм³/с). При такой ширине линии периметров и высоте слоя расход ограничивает линейную скорость примерно до %.0f мм/с, и верхние сегменты башни не смогут достичь заданной скорости.\n" "\n" "%s" -# AI Translated #, c-format, boost-format msgid "" "The end speed (%.0f mm/s) exceeds the filament's maximum volumetric speed (%.1f mm³/s) at the default layer height (%.2f mm).\n" "\n" "The layer height has been reduced to %.2f mm (a value used by this printer's profiles) so the tower can reach the requested speed." msgstr "" -"Конечная скорость (%.0f мм/с) превышает предел объёмного расхода материала (%.1f мм³/с) при высоте слоя по умолчанию (%.2f мм).\n" +"Конечная скорость (%.0f мм/с) приводит к превышению предела объёмного расхода материала (%.1f мм³/с) при высоте слоя по умолчанию (%.2f мм).\n" "\n" -"Высота слоя уменьшена до %.2f мм (значение, используемое профилями этого принтера), чтобы башня могла достичь заданной скорости." +"Высота слоя уменьшена до %.2f мм (минимально значение из профиля принтера), чтобы башня могла достичь заданной скорости." -# AI Translated #, c-format, boost-format msgid "" "Even at the smallest layer height used by this printer's profiles (%.2f mm) the end speed (%.0f mm/s) exceeds the filament's maximum volumetric speed (%.1f mm³/s).\n" @@ -19659,23 +19661,24 @@ msgid "" "\n" "Continue?" msgstr "" -"Даже при наименьшей высоте слоя, используемой профилями этого принтера (%.2f мм), конечная скорость (%.0f мм/с) превышает предел объёмного расхода материала (%.1f мм³/с).\n" +"Конечная скорость (%.0f мм/с) приводит к превышению предела объёмного расхода материала (%.1f мм³/с) даже при минимально допустимой высоте слоя (в соответствии с профилем принтера, %.2f мм).\n" "\n" -"Высота слоя будет установлена в %.2f мм, а конечная скорость снижена до %.0f мм/с.\n" +"Высота слоя будет установлена на %.2f мм, а конечная скорость снижена до %.0f мм/с.\n" "\n" "Продолжить?" -# AI Translated msgid "Continue anyway?" msgstr "Всё равно продолжить?" -# AI Translated msgid "Enable \"Auto-adjust\" to fix this automatically, or continue anyway?" -msgstr "Включить «Автоподстройку» для автоматического исправления или всё равно продолжить?" +msgstr "" +"Включить адаптацию к расходу для автоматического исправления?\n" +"Нет – игнорировать предупреждение." -# AI Translated msgid "Enable \"Auto-scale for nozzle\" and \"Auto-adjust\" to fix this automatically, or continue anyway?" -msgstr "Включить «Автомасштабирование под сопло» и «Автоподстройку» для автоматического исправления или всё равно продолжить?" +msgstr "" +"Включить адаптацию к соплу и расходу для автоматического исправления?\n" +"Нет – игнорировать предупреждение." msgid "Start retraction length: " msgstr "Начальная длина отката: " @@ -20173,10 +20176,10 @@ msgid "Create Nozzle for Existing Printer" msgstr "Сопло для принтера" msgid "Create from Template" -msgstr "Создать из шаблона" +msgstr "Создать из общих шаблонов" msgid "Create Based on Current Printer" -msgstr "Создать на основе профиля выбранного принтера" +msgstr "Создать из профилей производителя" msgid "Import Preset" msgstr "Импорт профиля" @@ -20305,8 +20308,8 @@ msgid "" "The system preset does not allow creation. \n" "Please re-enter the printer model or nozzle diameter." msgstr "" -"Системный профиль не допускает создания.\n" -"Пожалуйста, повторно введите модель принтера или диаметр сопла." +"Имя совпадает с системным профилем.\n" +"Назовите модель принтера или диаметр сопла иначе." msgid "" "\n" @@ -20436,21 +20439,21 @@ msgstr "Будут отображаться только изменённые п msgid "Only display the filament names with changes to filament presets." msgstr "Будут отображаться только изменённые профили материалов." -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "" "Будут отображаться только изменённые профили принтеров.\n" "Все они будут экспортированы в единый zip-файл." msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" "Будут отображаться только изменённые профили материалов.\n" "Все они будут экспортированы в единый zip-файл." msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" "Будут отображаться только профили принтеров с изменёнными настройками печати.\n" "Все изменённые профили настроек будут экспортированы в единый zip-файл." @@ -20500,11 +20503,12 @@ msgid "" "All the filament presets belong to this filament would be deleted.\n" "If you are using this filament on your printer, please reset the filament information for that slot." msgstr "" -"Все профили прутка, относящиеся к этому материалу, будут удалены.\n" -"Если вы используете этот пруток в принтере, пожалуйста, сбросьте информацию о прутке для этого слота." +"Все профили, относящиеся к этому материалу, будут удалены.\n" +"Если вы используете его в принтере, сбросьте информацию о\n" +"нём у соответствующего слота." msgid "Delete filament" -msgstr "Удаление прутка" +msgstr "Удаление материала" msgid "Add Preset" msgstr "Добавить профиль" @@ -20513,13 +20517,13 @@ msgid "Add preset for new printer" msgstr "Добавление профиля для нового принтера" msgid "Copy preset from filament" -msgstr "Копировать профиль из прутка" +msgstr "Копировать профиль из материала" msgid "The filament choice not find filament preset, please reselect it" -msgstr "Не удалось найти профиль прутка. Выберите его повторно" +msgstr "Не удалось найти профиль материала. Выберите его повторно" msgid "[Delete Required]" -msgstr "[Необходимо удалить]" +msgstr "[Требуется удаление]" msgid "Edit Preset" msgstr "Изменить профиль" @@ -20586,8 +20590,8 @@ msgid "" "The currently selected nozzle type of %s extruder does not match the actual printer nozzle type.\n" "Please click the Sync button above and restart the calibration." msgstr "" -"Выбранный тип сопла экструдера %s не соответствует фактическому типу сопла принтера.\n" -"Нажмите кнопку «Синхронизация» выше и перезапустите калибровку." +"Выбранный тип сопла экструдера (%s) не соответствует фактическому типу сопла принтера.\n" +"Нажмите кнопку синхронизации выше и перезапустите калибровку." msgid "Unable to calibrate: maybe because the set calibration value range is too large, or the step is too small" msgstr "Невозможно выполнить калибровку: возможно, установленный диапазон значений калибровки слишком велик или шаг слишком мал" @@ -20598,9 +20602,6 @@ msgstr "Физический принтер" msgid "Print Host upload" msgstr "Загрузка на хост печати" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Реализация сетевого агента для обмена информацией с принтером. Доступные реализации определяются при запуске." - msgid "Select a Flashforge printer" msgstr "Выберите принтер Flashforge" @@ -20692,7 +20693,7 @@ msgstr "Копировать в буфер обмена" msgid "We need information for diagnosing source of the issue. Check wiki page for detailed guide." msgstr "Эта информация требуется для диагностики проблем. Ознакомьтесь с руководством для получения подробностей." -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "Нажмите «Экспорт», чтобы сохранить текущий проект и журнал сессии в ZIP-архив." msgid "Any additional visual examples like images or screen recordings might be helpful while reporting the issue." @@ -20734,7 +20735,7 @@ msgstr "Уровень ведения журнала" msgid "Stored logs" msgstr "Сохранённые журналы" -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "Нажмите «Экспорт», чтобы упаковать все сохранённые журналы в единый ZIP-архив" msgid "Profiles" @@ -20801,7 +20802,7 @@ msgstr "Требуется указать принтер вручную" msgid "Authorizing..." msgstr "Авторизация..." -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "Ошибка получения токена авторизации" msgid "Could not parse server response." @@ -21249,13 +21250,12 @@ msgstr "Ошибка печати" msgid "Removed" msgstr "Удалено" -# AI Translated msgid "Enable smart filament assign: Assign one filament to multiple nozzles to maximize savings" -msgstr "Включить умное назначение материалов: назначайте один материал нескольким соплам для максимальной экономии" +msgstr "Включить умное назначение материалов: назначить один материал нескольким соплам для максимальной экономии" # AI Translated -msgid "Fila Saving" -msgstr "Экономия материала" +msgid "File Saving" +msgstr "Сохранение файла" msgid "Don't remind me again" msgstr "Больше не показывать" @@ -21292,10 +21292,10 @@ msgstr "Видеоурок" msgid "(Sync with printer)" msgstr " (состояние принтера)" -# AI Translated +# Отвратительная подстановка левое/правое и тип расхода сопла #, c-format, boost-format msgid "Error: %s extruder has no available %s nozzle, current group result is invalid." -msgstr "Ошибка: у экструдера %s нет доступного сопла %s, текущий результат группировки недействителен." +msgstr "Ошибка: %s сопло не поддерживает требуемый расход (%s), результат текущей группировки недействителен." msgid "We will slice according to this grouping method:" msgstr "Осуществлять нарезку в соответствии с этой группировкой:" @@ -21303,15 +21303,12 @@ msgstr "Осуществлять нарезку в соответствии с msgid "Tip: You can drag the filaments to reassign them to different nozzles." msgstr "Совет: для назначения материала перетащите его в нужное поле." -# AI Translated msgid "Please adjust your grouping or click " -msgstr "Отрегулируйте группировку или нажмите " +msgstr "Измените группировку или нажмите " -# AI Translated msgid " to set nozzle count" msgstr " для задания количества сопел" -# AI Translated msgid "Set the physical nozzle count..." msgstr "Задать физическое количество сопел..." @@ -21464,9 +21461,6 @@ msgstr "При попытке войти произошла какая-то ош msgid "User canceled." msgstr "Отменено пользователем." -msgid "Head diameter" -msgstr "Диаметр уха" - msgid "Max angle" msgstr "Макс. угол" @@ -21548,7 +21542,7 @@ msgid "Skipping objects." msgstr "Исключение объектов." msgid "Select Filament" -msgstr "Выбрать филамент" +msgstr "Выбрать материал" msgid "Null Color" msgstr "Цвет не задан" @@ -21581,7 +21575,7 @@ msgstr "Ошибка: %s" msgid "Show details" msgstr "Подробнее" -# AI Translated +# В информации о сетевом плагине msgid "Hide details" msgstr "Скрыть подробности" @@ -21610,9 +21604,8 @@ msgstr "Установить обновление" msgid "(Latest)" msgstr "(новейшая)" -# AI Translated msgid "(installed)" -msgstr "(установлено)" +msgstr "(установлена)" msgid "The Bambu Network Plug-in has been installed successfully." msgstr "Сетевой плагин Bambu успешно установлен." @@ -21627,6 +21620,9 @@ msgstr "Перезапустить сейчас" msgid "NO RAMMING AT ALL" msgstr "Рэмминг отключён" +msgid "s" +msgstr "с" + # Тянется только в окно настроек рэмминга msgid "Volumetric speed" msgstr "Объёмный расход" @@ -21664,7 +21660,6 @@ msgstr "Сохранить как настройки по умолчанию" msgid "If enabled, the values above are stored as the defaults used for future STEP imports (and shown in Preferences)." msgstr "Активируйте для сохранения настроек импорта после его завершения." -# AI Translated msgid "PresetBundle" msgstr "PresetBundle" @@ -21691,7 +21686,6 @@ msgstr "Удаление пакета" msgid "Unsubscribe bundle?" msgstr "Отписаться от пакета?" -# AI Translated msgid "UnsubscribeBundle" msgstr "UnsubscribeBundle" @@ -21702,11 +21696,9 @@ msgstr "Не удалось отписаться от пакета." msgid "Unsubscribe Bundle" msgstr "Ошибка" -# AI Translated msgid "ExportPresetBundle" msgstr "ExportPresetBundle" -# AI Translated msgid "Save preset bundle" msgstr "Сохранить пакет профилей" @@ -21760,190 +21752,146 @@ msgstr "Просмотр архива" msgid "Open File" msgstr "Открыть файл" -# AI Translated msgid "AMS Dryness Control" msgstr "Управление сушкой AMS" -# AI Translated msgid "Filament Drying Settings" msgstr "Настройки сушки материала" -# AI Translated msgid "Stopping" msgstr "Остановка" -# AI Translated msgid "Unable to dry temporarily due to ..." msgstr "Временно невозможно выполнить сушку из-за ..." -# AI Translated msgid "Drying Error" msgstr "Ошибка сушки" -# AI Translated msgid "Please check the Assistant for troubleshooting" msgstr "Обратитесь к Помощнику для устранения неполадок" -# AI Translated msgid "Please remove and store the filament (as shown)." msgstr "Извлеките и уберите материал на хранение (как показано)." -# AI Translated msgid "The AMS can rotate the filament which is properly stored, providing better drying results." -msgstr "AMS может вращать правильно уложенный на хранение материал, обеспечивая лучший результат сушки." +msgstr "AMS может вращать правильно уложенный на хранение материал, тем самым улучшая эффективность сушки." -# AI Translated msgid "Rotate spool when drying" msgstr "Вращать катушку при сушке" -# AI Translated msgctxt "amsdrying" msgid "Back" msgstr "Назад" -# AI Translated msgid "Drying-Heating" msgstr "Сушка — нагрев" -# AI Translated msgid "Drying-Dehumidifying" -msgstr "Сушка — осушение" +msgstr "Сушка — вывод влаги" -# AI Translated msgid " maximum drying temperature is " msgstr " максимальная температура сушки — " -# AI Translated msgid " minimum drying temperature is " msgstr " минимальная температура сушки — " -# AI Translated msgid "This filament may not be completely dried." -msgstr "Этот материал может быть высушен не полностью." +msgstr "Сушка этого материала может не быть эффективной." -# AI Translated msgid "This AMS is currently printing. To ensure print quality, the drying temperature cannot exceed the recommended drying temperature." -msgstr "Этот AMS сейчас печатает. Для обеспечения качества печати температура сушки не может превышать рекомендованную температуру сушки." +msgstr "AMS сейчас печатает. Во избежание проблем с печатью температура сушки не должна превышать рекомендованную." -# AI Translated msgid "The temperature shall not exceed the filament's heat distortion temperature" -msgstr "Температура не должна превышать температуру тепловой деформации материала" +msgstr "Температура сушки не должна превышать температуру размягчения материала." -# AI Translated msgid "Minimum time value cannot be less than 1." -msgstr "Минимальное значение времени не может быть меньше 1." +msgstr "Минимальное время не может быть меньше 1." -# AI Translated msgid "Maximum time value cannot be greater than 24." -msgstr "Максимальное значение времени не может быть больше 24." +msgstr "Максимальное время не может быть больше 24." -# AI Translated msgid "Insufficient power" -msgstr "Недостаточно питания" +msgstr "Нехватка мощности" -# AI Translated msgid " Too many AMS drying simultaneously. Please plug in the power or stop other drying processes before starting." -msgstr " Слишком много AMS сушат одновременно. Подключите питание или остановите другие процессы сушки перед началом." +msgstr " Слишком много AMS выполняют сушку. Подключите питание или остановите другие сеансы сушки перед началом." -# AI Translated msgid "AMS is busy" -msgstr "AMS занят" +msgstr "AMS занята" -# AI Translated msgid " AMS is calibrating | reading RFID | loading/unloading material, please wait." -msgstr " AMS калибруется | считывает RFID | загружает/выгружает материал, подождите." +msgstr " AMS калибруется | считывает RFID | меняет материал, подождите." -# AI Translated +# на Вики бамбу есть упоминание: https://wiki.bambulab.com/ru/h2/troubleshooting/hmscode/0700_2000_0002_0025#:~:text=выходного%20отверстия%20AMS msgid "Filament in AMS outlet" msgstr "Материал в выходном отверстии AMS" -# AI Translated msgid " The high drying temperature may cause AMS blockage, please unload first." -msgstr " Высокая температура сушки может вызвать засорение AMS, сначала выгрузите материал." +msgstr " Из-за высокой температуры сушки пруток может застрять в AMS, сначала выгрузите материал." -# AI Translated msgid "Initiating AMS drying" msgstr "Запуск сушки AMS" -# AI Translated msgid "Not supported in 2D mode" -msgstr "Не поддерживается в режиме 2D" +msgstr "Не поддерживается в 2D-режиме" -# AI Translated msgid "Task in progress" msgstr "Выполняется задача" -# AI Translated +# CannotDryReason: DryingInProgress msgid " The AMS might be in use during Task." -msgstr " AMS может использоваться во время задачи." +msgstr " AMS может иметь задачи в процессе." -# AI Translated msgid " Firmware update in progress, please wait..." msgstr " Выполняется обновление прошивки, подождите..." -# AI Translated msgid " Please plug in the power and then use the drying function." msgstr " Подключите питание, а затем используйте функцию сушки." -# AI Translated msgid " The high drying temperature may cause AMS blockage. Please unload the filament manually before proceeding." -msgstr " Высокая температура сушки может вызвать засорение AMS. Перед продолжением выгрузите материал вручную." +msgstr " Из-за высокой температуры сушки пруток может застрять в AMS. Перед продолжением выгрузите материал вручную." -# AI Translated msgid "System is busy" msgstr "Система занята" -# AI Translated msgid " Initiating other drying processes, please wait a few seconds..." msgstr " Запускаются другие процессы сушки, подождите несколько секунд..." -# AI Translated msgid "For better drying results, remove the filament and allow it to rotate." -msgstr "Для лучшего результата сушки извлеките материал и дайте ему вращаться." +msgstr "Для лучшего результата сушки извлеките материал и включите вращение катушки." -# AI Translated msgid "The AMS will automatically rotate the stored filament slots to enhance the drying performance." -msgstr "AMS будет автоматически вращать слоты с уложенным на хранение материалом для повышения эффективности сушки." +msgstr "AMS будет автоматически вращать слоты с хранящимися катушками для повышения эффективности сушки." -# AI Translated msgid "Alternatively, you can dry the filament without removing it." -msgstr "Кроме того, вы можете сушить материал, не извлекая его." +msgstr "Кроме того, сушить материал можно без его извлечения." -# AI Translated msgid "Unknown filaments will be treated as PLA." msgstr "Неизвестные материалы будут рассматриваться как PLA." -# AI Translated msgid "Please store the filament marked with an exclamation mark." msgstr "Уберите на хранение материал, отмеченный восклицательным знаком." -# AI Translated msgid "Filament left in the feeder during drying may soften because the drying temperature exceeds the softening point of materials like PLA and TPU." -msgstr "Материал, оставленный в подающем механизме во время сушки, может размягчиться, поскольку температура сушки превышает точку размягчения таких материалов, как PLA и TPU." +msgstr "Заправленный материал может размягчиться во время сушки, поскольку её сушки превышает точку размягчения материалов вроде PLA и TPU." -# AI Translated msgid "Starting: Checking adapter connection" msgstr "Запуск: проверка подключения адаптера" -# AI Translated msgid "Starting: Checking filament status" msgstr "Запуск: проверка состояния материала" -# AI Translated msgid "Starting: Checking drying presets" msgstr "Запуск: проверка профилей сушки" -# AI Translated msgid "Starting: Checking filament location" msgstr "Запуск: проверка расположения материала" -# AI Translated msgid "Starting: Checking air intake" msgstr "Запуск: проверка забора воздуха" -# AI Translated msgid "Starting: Checking air vent" -msgstr "Запуск: проверка вентиляционного отверстия" +msgstr "Запуск: проверка вентиляции" msgid "The filament may not be compatible with the current machine settings. Generic filament presets will be used." msgstr "Материал может быть несовместим с текущими настройками принтера. Будет использоваться базовый профиль материала." @@ -21953,14 +21901,14 @@ msgstr "Материал может быть несовместим с теку # подобных). Она задаётся вручную в файле профиля (производителем) и # отсутствует у большинства системных профилей. msgid "The filament model is unknown. Still using the previous filament preset." -msgstr "Модель филамента неизвестна. Используется предыдущий профиль филамента." +msgstr "Модель материала неизвестна. Используется предыдущий профиль материала." # Не знаю, что за "модель" материала, пропускаю. Возможно, имеется ввиду # модель коррекции объёмного расхода для функции адаптивного расхода у TPU (и # подобных). Она задаётся вручную в файле профиля (производителем) и # отсутствует у большинства системных профилей. msgid "The filament model is unknown. Generic filament presets will be used." -msgstr "Модель филамента неизвестна. Будут использованы стандартные профили филамента." +msgstr "Модель материала неизвестна. Будут использованы стандартные профили материала." msgid "The filament may not be compatible with the current machine settings. A random filament preset will be used." msgstr "Материал может быть несовместим с текущими настройками принтера. Будет использоваться случайный профиль материала." @@ -21970,7 +21918,7 @@ msgstr "Материал может быть несовместим с теку # подобных). Она задаётся вручную в файле профиля (производителем) и # отсутствует у большинства системных профилей. msgid "The filament model is unknown. A random filament preset will be used." -msgstr "Модель филамента неизвестна. Будет использован случайный профиль филамента." +msgstr "Модель материала неизвестна. Будет использован случайный профиль материала." #: resources/data/hints.ini: [hint:Precise wall] msgid "" @@ -22270,6 +22218,52 @@ msgstr "" "Предотвращение коробления материала\n" "Знаете ли вы, что при печати материалами, склонными к короблению, таких как ABS, повышение температуры подогреваемого стола может снизить эту вероятность?" +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "Для нативного отображения трансляции в Wayland требуется gtksink (плагин для GStreamer). Установите необходимый пакет плагинов и перезапустите OrcaSlicer." + +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "Не удалось запустить нативную трансляцию GSteramer через Wayland. Проверьте наличие установленного пакета плагинов GStreamer." + +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "Для этой задачи требуется Windows Media Player! Хотите включить его в своей ОС?" + +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "Компонент BambuSource неправильно зарегистрирован для воспроизведения медиафайлов! Нажмите «Да», чтобы повторно зарегистрировать его" + +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "Отсутствует компонент BambuSource для воспроизведения медиа. Переустановите OrcaSlicer или обратитесь за помощью к сообществу." + +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "При использовании компонентов BambuSource из другого инсталлятора, воспроизведение видео может работать некорректно! Нажмите «Да», чтобы исправить это." + +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "В вашей системе отсутствуют кодеки H.264 для GStreamer, которые необходимы для воспроизведения видео (попробуйте установить пакеты gstreamer1.0-plugins-bad или gstreamer1.0-libav, а затем перезапустить Orca Slicer)." + +#~ msgid "N" +#~ msgstr "Н" + +#~ msgid "g" +#~ msgstr "г" + +#~ msgid "Fila Saving" +#~ msgstr "Экономия материала" + +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Высота слоя слишком мала.\n" +#~ "Будет установлено значение min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Высота слоя не может превышать ограничения, установленные в настройках принтера → Экструдер → Ограничение высоты слоя. Это может вызвать проблемы с качеством печати." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Автоматически подстроиться под заданный в настройках диапазон?\n" + +#~ msgid "Head diameter" +#~ msgstr "Диаметр уха" + #~ msgid "Print order within a single layer." #~ msgstr "Последовательность печати моделей в пределах одного слоя." @@ -22372,9 +22366,6 @@ msgstr "" #~ "\n" #~ "Внимание: несовместимо с принтерами Prusa, поскольку им требуется остановка для изменения коэффициента PA." -#~ msgid "Continue to sync filaments" -#~ msgstr "Продолжить синхронизацию филаментов" - #~ msgctxt "Sync_Nozzle_AMS" #~ msgid "Cancel" #~ msgstr "Отмена" diff --git a/localization/i18n/sv/OrcaSlicer_sv.po b/localization/i18n/sv/OrcaSlicer_sv.po index 6d28e4e66f..7ec20d136d 100644 --- a/localization/i18n/sv/OrcaSlicer_sv.po +++ b/localization/i18n/sv/OrcaSlicer_sv.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -3289,6 +3289,10 @@ msgstr "Redigera" msgid "Merge with" msgstr "Slå ihop med" +# AI Translated +msgid "Decompose Color" +msgstr "Dela upp färg" + # AI Translated msgid "Delete this filament" msgstr "Radera detta filament" @@ -3610,6 +3614,10 @@ msgstr "Montering" msgid "Merge parts to an object" msgstr "Slå ihop delar till ett objekt" +# AI Translated +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "Att använda variabel lagerhöjd tillsammans med underlager med blandad färg kan ge sämre färgblandningskvalitet." + # AI Translated msgid "Add layers" msgstr "Lägg till lager" @@ -5213,6 +5221,23 @@ msgstr "Kammarens aktuella temperatur är högre än materialets säkra temperat msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Kammarens minimitemperatur (%d℃) är högre än kammarens måltemperatur (%d℃). Minimivärdet är tröskeln där utskriften startar medan kammaren fortsätter värmas mot målet, så det bör inte överstiga målet. Värdet begränsas till måltemperaturen." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "Lagerhöjden är för liten. Den kommer att sättas till minimivärdet (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "Lagerhöjden ligger utanför gränserna som anges i Skrivarinställningar -> Extruder -> Lagerhöjds gränser, detta kan orsaka problem med utskriftskvaliteten." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Justera automatiskt till gränsvärdet (%g mm)?" + +msgid "Adjust" +msgstr "Justera" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -5262,7 +5287,7 @@ msgstr "" "Värdet kommer att återställas till 0." # AI Translated -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Alternerande extra vägg fungerar inte bra när säkerställ vertikal skaltjocklek är inställt på Alla." # AI Translated @@ -5310,7 +5335,7 @@ msgstr "" # AI Translated msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height måste vara mindre än layer_height.\n" @@ -5319,7 +5344,7 @@ msgstr "" # AI Translated #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "Låsdjupet bör vara mindre än ytskiktets djup.\n" @@ -5339,6 +5364,13 @@ msgstr "" "Ja – Aktivera Arachne-väggeneratorn\n" "Nej – Inaktivera Arachne-väggeneratorn och ställ in läget [Förskjutning] för ojämn yta" +# AI Translated +msgid "Brim ear radius" +msgstr "Radie för brim-öra" + +msgid "Brim width" +msgstr "Brim bredd" + # AI Translated msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Spiralläget fungerar bara när antal väggar är 1, support är avstängt, detektering av klumpbildning med sondering är avstängd, antal översta skallager är 0, sparsam ifyllnadsdensitet är 0 och timelapse-typen är traditionell." @@ -5645,6 +5677,14 @@ msgstr "Misslyckades med att generera cali G kod" msgid "Calibration error" msgstr "Fel vid kalibrering" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Den här skrivaren är inte konfigurerad med den maskinvara som den här kontrollen kräver." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Den här kontrollen stöds inte på den här skrivaren." + # AI Translated msgid "Network unavailable" msgstr "Nätverket är inte tillgängligt" @@ -6596,7 +6636,7 @@ msgid "Size:" msgstr "Storlek:" # AI Translated -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Konflikter mellan G-code-banor hittades på lager %d, Z = %.2lfmm. Placera de objekt som krockar längre ifrån varandra (%s <-> %s)." @@ -6798,6 +6838,10 @@ msgstr "Flera enheter" msgid "Project" msgstr "Projekt" +# AI Translated +msgid "Device (Web)" +msgstr "Enhet (Webb)" + msgid "Yes" msgstr "Ja" @@ -6935,11 +6979,11 @@ msgid "Load a model" msgstr "Ladda modell" # AI Translated -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "Importera ZIP-arkiv" # AI Translated -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "Läs in modeller som finns i ett ZIP-arkiv" msgid "Import Configs" @@ -8608,6 +8652,10 @@ msgstr "Anpassa aktuell platta" msgid "The %s nozzle can not print %s." msgstr "Nozzeln %s kan inte skriva ut %s." +# AI Translated +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "Att skriva ut filament med blandad färg på en skrivare med en enda extruder kräver täta filamentbyten och rensningar, vilket kan öka spillet och risken för stopp i nozzeln eller avfallsrännan avsevärt." + # AI Translated #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" @@ -8751,6 +8799,26 @@ msgstr "Synkronisera filament listan från AMS" msgid "Set filaments to use" msgstr "Ställ in filament som ska användas" +# AI Translated +msgid "Add Mixed Filament" +msgstr "Lägg till blandat filament" + +# AI Translated +msgid "Mixed Filament" +msgstr "Blandat filament" + +# AI Translated +msgid "Remove last mixed filament" +msgstr "Ta bort det senaste blandade filamentet" + +# AI Translated +msgid "Add mixed filament" +msgstr "Lägg till blandat filament" + +# AI Translated +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "Det blandade filamentet har ogiltiga eller inkompatibla komponenter. Redigera de berörda posterna igen." + msgid "Search plate, object and part." msgstr "Sök platta, objekt och del." @@ -8758,6 +8826,18 @@ msgstr "Sök platta, objekt och del." msgid "Pellets" msgstr "Pellets" +# AI Translated +msgid "Mixed filament has broken component references" +msgstr "Det blandade filamentet har trasiga komponentreferenser" + +# AI Translated +msgid "Edit / Delete / Merge" +msgstr "Redigera / Radera / Slå ihop" + +# AI Translated +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "Det blandade målfilamentet använder detta fysiska filament som komponent. Sammanslagningen tar bort det fysiska filamentet och kan göra det blandade filamentet ogiltigt. Fortsätt?" + # AI Translated #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." @@ -8916,8 +8996,8 @@ msgstr "Bekräfta att G-koderna i dessa inställningar är säkra för att förh msgid "Customized Preset" msgstr "Anpassad inställning" -msgid "Component name(s) inside step file not in UTF8 format!" -msgstr "Komponent namnet i STEP filen är inte UTF8 format!" +msgid "Component name(s) inside step file not in UTF-8 format!" +msgstr "Komponent namnet i STEP filen är inte UTF-8 format!" msgid "Because of unsupported text encoding, garbage characters may appear!" msgstr "På grund av textkodning som inte stöds så kan skräptecken visas!" @@ -8938,7 +9018,7 @@ msgstr "Objektet är utan volym" #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "Objektets fil %s är för litet, det är kanske i meter eller inches.\n" "Skala om till millimeter?" @@ -8958,6 +9038,14 @@ msgstr "" msgid "Multi-part object detected" msgstr "Objekt i flera delar har upptäckts" +# AI Translated +msgid "Matching textures to filaments" +msgstr "Matchar texturer mot filament" + +# AI Translated +msgid "Texture Import Warning" +msgstr "Varning vid texturimport" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Ladda dessa filer som ett enkelt objekt med multipla delar?\n" @@ -9088,24 +9176,24 @@ msgid "Replaced with 3D files from directory:\n" msgstr "Ersatt med 3D-filer från mappen:\n" # AI Translated -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ Hoppade över %1%: samma fil.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ Hoppade över %s: samma fil.\n" # AI Translated -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ Hoppade över %1%: filen finns inte.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ Hoppade över %s: filen finns inte.\n" # AI Translated -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ Hoppade över %1%: det gick inte att ersätta.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ Hoppade över %s: det gick inte att ersätta.\n" # AI Translated -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔ Ersatte %1%.\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔ Ersatte %s.\n" # AI Translated msgid "Replaced volumes" @@ -9187,6 +9275,22 @@ msgstr "" msgid "Sync now" msgstr "Synkronisera nu" +# AI Translated +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "Texturimporten misslyckades. Modellen verkar innehålla texturdata, men importprocessen kunde inte slutföras. Modellen importeras endast som geometri." + +# AI Translated +msgid "Applying texture colors..." +msgstr "Tillämpar texturfärger..." + +# AI Translated +msgid "Updating 3D view..." +msgstr "Uppdaterar 3D-vyn..." + +# AI Translated +msgid "Texture colors applied." +msgstr "Texturfärger tillämpade." + msgid "You can keep the modified presets for the new project or discard them" msgstr "Fortsätt med redigerings inställningarna till nytt projekt eller avfärda dem" @@ -9933,6 +10037,18 @@ msgstr "Med det här alternativet aktiverat kan du skicka en uppgift till flera msgid "Pop up to select filament grouping mode" msgstr "Visa dialogruta för val av filamentgrupperingsläge" +# AI Translated +msgid "Visible plugin pages" +msgstr "Synliga insticksmodulsidor" + +# AI Translated +msgid "pages" +msgstr "sidor" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Antal insticksmodulsidor som visas som fasta flikar innan de återstående sidorna fälls ihop i en rullgardinsmeny på den sista fliken." + # AI Translated msgid "Behaviour" msgstr "Beteende" @@ -10357,6 +10473,18 @@ msgstr "Visa förinställningar som inte stöds" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Visa inkompatibla förinställningar och förinställningar som inte stöds i rullgardinslistorna för skrivare och filament. Dessa förinställningar kan inte väljas." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Experimentellt) Använd skrivaragenter i stället för utskriftsvärdar" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Skickar utskriftsjobb för icke-Bambu-skrivare via skrivarens insticksmodulagenter i stället för det klassiska uppladdningsflödet till utskriftsvärden.\n" +"När detta är avaktiverat använder OrcaSlicer det äldre beteendet för utskriftsvärdar." + # AI Translated msgid "Experimental Features" msgstr "Experimentella funktioner" @@ -10581,6 +10709,10 @@ msgstr "Spiral vas" msgid "First layer filament sequence" msgstr "Första lagrets filament sekvens" +# AI Translated +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "Filamentlistan innehåller blandade filament. Den anpassade filamentordningen får ingen effekt." + msgid "By Layer" msgstr "Per lager" @@ -10637,10 +10769,26 @@ msgstr "Användar förinställning" msgid "Preset Inside Project" msgstr "Projekt förinställning" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Kopierar alla ärvda värden från den överordnade förinställningen till den här förinställningen och tar bort arvsrelationen. Förinställningar som endast är kompatibla med den överordnade förinställningen kan sluta stödjas." + # AI Translated msgid "Detach from parent" msgstr "Koppla loss från överordnad" +# AI Translated +msgid "Unique preset" +msgstr "Unik förinställning" + +# AI Translated +msgid "Parent preset" +msgstr "Överordnad förinställning" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Den här förinställningen ärver inte från någon annan förinställning." + msgid "Name is unavailable." msgstr "Namnet ej tillgängligt." @@ -11459,23 +11607,6 @@ msgstr "Är du säker på att du vill aktivera det här alternativet?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Ifyllnadsmönster är oftast konstruerade för att hantera rotation automatiskt så att de skrivs ut korrekt och ger avsedd effekt (t.ex. Gyroid, Kubisk). Att rotera det aktuella sparsamma ifyllnadsmönstret kan ge otillräckligt stöd. Var försiktig och kontrollera noga om det uppstår utskriftsproblem. Är du säker på att du vill aktivera det här alternativet?" -# AI Translated -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Lagerhöjden är för liten.\n" -"Den ställs in på min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Lagerhöjden överskrider gränsen i Skrivarinställningar -> Extruder -> Lagerhöjds gränser, detta kan orsaka problem med utskriftskvaliteten." - -msgid "Adjust to the set range automatically?\n" -msgstr "Justera automatiskt till det inställda området?\n" - -msgid "Adjust" -msgstr "Justera" - # AI Translated msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Experimentell funktion: Filamentet dras tillbaka och kapas på ett längre avstånd vid filamentbyten för att minimera rensningen. Det kan minska rensningen avsevärt, men kan också öka risken för igensatt nozzel eller andra utskriftsproblem." @@ -11707,6 +11838,9 @@ msgstr "Hittade reserverade nyckelord" msgid "Setting Overrides" msgstr "Åsidosätter inställningar" +msgid "Retraction when switching material" +msgstr "Reduktion vid material byte" + msgid "Basic information" msgstr "Allmän information" @@ -11848,6 +11982,14 @@ msgstr "Kompatibla process profiler" msgid "Printable space" msgstr "Utskriftsbar yta" +# AI Translated +msgid "Printer Agent" +msgstr "Skrivaragent" + +# AI Translated +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Välj vilken nätverksagentimplementation som ska användas för kommunikation med skrivaren. Tillgängliga agenter registreras vid start." + # AI Translated #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format @@ -11992,9 +12134,6 @@ msgstr "Lagerhöjds begränsning" msgid "Z-Hop" msgstr "Z-Hop" -msgid "Retraction when switching material" -msgstr "Reduktion vid material byte" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -12123,12 +12262,12 @@ msgid "No modifications need to be copied." msgstr "Inga ändringar behöver kopieras." # AI Translated -msgid "Copy paramters" +msgid "Copy parameters" msgstr "Kopiera parametrar" # AI Translated #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "Ändra parametrar för %s" # AI Translated @@ -12716,33 +12855,6 @@ msgstr "Rensnings volym för filament byte" msgid "Please choose the filament colour" msgstr "Välj filamentfärg" -# AI Translated -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "Liveview i Wayland kräver GStreamers GTK-videosink. Installera insticksmodulen gtksink för GStreamer och starta sedan om OrcaSlicer." - -# AI Translated -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "Det gick inte att initiera Waylands GStreamer-videosink. Kontrollera installationen av din GStreamer GTK-insticksmodul." - -# AI Translated -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "Windows Media Player krävs för den här uppgiften! Vill du aktivera 'Windows Media Player' i ditt operativsystem?" - -# AI Translated -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "BambuSource har inte registrerats korrekt för mediauppspelning! Tryck på Ja för att registrera om den. Du får två frågor" - -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "Saknad BambuSource-komponent registrerad för mediauppspelning! Installera om OrcaSlicer eller sök hjälp i gemenskapen." - -# AI Translated -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "Du använder en BambuSource från en annan installation, videouppspelningen kanske inte fungerar korrekt! Tryck på Ja för att åtgärda det." - -# AI Translated -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "Ditt system saknar H.264-kodekar för GStreamer, som krävs för att spela upp video. (Prova att installera paketen gstreamer1.0-plugins-bad eller gstreamer1.0-libav och starta sedan om Orca Slicer.)" - # AI Translated msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "Molnagenten är inte tillgänglig. Starta om OrcaSlicer och försök igen." @@ -13486,6 +13598,10 @@ msgstr " är för nära uteslutningsområdet, och kollisioner kommer att orsakas msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " ligger för nära området för klumpdetektering, vilket kommer att orsaka kollisioner.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " är delvis utanför det utskrivbara området och kan inte skrivas ut.\n" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "De valda nozzeltemperaturerna är inkompatibla. Varje filaments nozzeltemperatur måste ligga inom de andra filamentens rekommenderade nozzeltemperaturintervall. Annars kan nozzeln sättas igen eller skrivaren skadas." @@ -13501,6 +13617,10 @@ msgstr "Om du ändå vill skriva ut kan du aktivera alternativet i Inställninga msgid "No extrusions under current settings." msgstr "Nuvarande inställning har ingen extrudering." +# AI Translated +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "Ett blandat filament med gradient används, men 'Underlager med blandad färg' är inaktiverat. Gradienten kommer inte att skrivas ut." + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Smooth läge för timelapse stöds inte när ”per objekt” -sekvens är aktiverad." @@ -13543,6 +13663,10 @@ msgstr "Du kan behöva minska modellens storlek eller ändra de aktuella utskrif msgid "Variable layer height is not supported with Organic supports." msgstr "Variabel lagerhöjd stöds inte med organiska support." +# AI Translated +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "Filamentet för prime tornet kan inte vara ett blandat filament." + # AI Translated msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Olika nozzeldiametrar och olika filamentdiametrar kanske inte fungerar bra när prime tornet är aktiverat. Det är mycket experimentellt, så var försiktig." @@ -13856,10 +13980,6 @@ msgstr "Använd 3MF i stället för G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Aktivera detta om skrivaren tar emot en 3MF-fil som utskriftsjobb. När det är aktiverat skickar Orca Slicer den beredda filen som en .gcode.3mf i stället för en vanlig .gcode-fil." -# AI Translated -msgid "Printer Agent" -msgstr "Skrivaragent" - # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Välj vilken nätverksagentimplementation som ska användas för kommunikation med skrivaren." @@ -13945,14 +14065,12 @@ msgstr "mm eller %" msgid "Other layers" msgstr "Andra lager" -# AI Translated -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." msgstr "Byggplattans temperatur för alla lager utom det första. Värdet 0 innebär att filamentet inte stöder utskrift på Cool Plate SuperTack." msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." msgstr "Detta är byggplattans temperatur för lager förutom det första. Värdet 0 betyder att filamentet inte stöder utskrift på Cool Plate." -# AI Translated msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Textured Cool Plate." msgstr "Detta är byggplattans temperatur för lager förutom det första. Värdet 0 betyder att filamentet inte stöder utskrift på Textured Cool Plate." @@ -14616,9 +14734,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Hastighet för inre bridges. Om värdet anges i procent beräknas det utifrån bridge_speed. Standardvärdet är 150 %." -msgid "Brim width" -msgstr "Brim bredd" - msgid "This is the distance from the model to the outermost brim line." msgstr "Avståndet från modellen till yttersta brim linjen" @@ -14707,6 +14822,14 @@ msgstr "" "Geometrin decimeras innan skarpa vinklar detekteras. Den här parametern anger avvikelsens minsta längd för decimeringen.\n" "0 för att avaktivera." +# AI Translated +msgid "Brim ears outer only" +msgstr "Brim-öron endast utvändigt" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Genererar musöron endast på modellens yttre kontur, exklusive hål och slutna sektioner." + msgid "upward compatible machine" msgstr "uppåt kompatibel maskin" @@ -15462,6 +15585,8 @@ msgstr "Lager tid" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "Del kylfläkten kommer att aktiveras för lager vars beräknade tid är kortare än detta värde. Fläkthastigheten interpoleras mellan den lägsta och högsta fläkthastigheten enligt utskriftstiden för lager" +# AI Translated +msgctxt "second" msgid "s" msgstr "s" @@ -15840,6 +15965,62 @@ msgstr "Supportmaterial" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Support material används ofta för att skriva ut support och stödja gränssnittet" +# AI Translated +msgid "Is mixed filament" +msgstr "Är blandat filament" + +# AI Translated +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "Anger om denna filamentplats är ett blandat filament som består av flera fysiska filament" + +# AI Translated +msgid "Mixed filament components" +msgstr "Komponenter i blandat filament" + +# AI Translated +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "Kommaseparerade index för komponentfilamenten, med start på 1, t.ex. \"1,3\"" + +# AI Translated +msgid "Mixed filament sublayer ratios" +msgstr "Underlagerandelar för blandat filament" + +# AI Translated +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "Kommaseparerade andelsvärden vars summa är 1.0, t.ex. \"0.7,0.3\"" + +# AI Translated +msgid "Mixed filament gradient" +msgstr "Gradient för blandat filament" + +# AI Translated +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "Aktiverar gradientläge i Z-riktningen för det blandade filamentets underlager. När det är aktiverat varierar underlagrens andelar linjärt över lagren." + +# AI Translated +msgid "Mixed filament gradient range" +msgstr "Gradientintervall för blandat filament" + +# AI Translated +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "Start- och slutandel för den första komponenten i gradientläge. Kommaseparerat par, t.ex. \"0.10,0.90\" betyder 10% till 90%." + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "Gradientkurva för blandat filament" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "Valfri anpassad kurva i Photoshop-stil som mappar Z-förloppet till den första komponentens andel. Kodas som kontrollpunkter separerade med lodstreck, antingen \"x,y\" (äldre format) eller \"x,y,m_in,m_out\" när tangenten behöver åsidosättas (ett tomt värde eller \"nan\" använder PCHIP-standarden). x ligger i [0,1]; y begränsas till det konfigurerade andelsintervallet, t.ex. \"0,0.15|0.5,0.50|1,0.85\". Om fältet är tomt används det linjära gradient_range i stället." + +# AI Translated +msgid "Mixed filament per-part gradient" +msgstr "Gradient per del för blandat filament" + +# AI Translated +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "När gradientläget är aktiverat tillämpas gradienten på varje del i en montering separat i stället för att behandla hela monteringen som ett enda Z-intervall." + # AI Translated msgid "Filament printable" msgstr "Filament utskrivbart" @@ -16039,6 +16220,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroid" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Utjämningsfaktor för sparsam ifyllnad" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Styr hur kraftigt hörnen i den sparsamma ifyllnaden rundas av. 0% behåller den ursprungliga skarpa banan, medan 100% ger största möjliga kurvor mellan intilliggande ifyllnadslinjer." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Acceleration av fyllning av toppytan. Att använda ett lägre värde kan förbättra ytkvaliteten" @@ -16651,6 +16840,14 @@ msgstr "Vilken typ av G-kod är skrivaren kompatibel med" msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Hoppa över G-code-konfigurationsblocket" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Skriver inte CONFIG_BLOCK (nyckel/värde-paren för slicerkonfigurationen) till G-code-filen. Detta kan hjälpa med skrivare vars firmware kraschar när dessa kommentarrader tolkas (t.ex. Anycubic go-klipper). Obs: G-code-filen kommer inte längre att innehålla slicerinställningarna, så att importera den tillbaka till OrcaSlicer återställer inte konfigurationen." + # AI Translated msgid "Pellet Modded Printer" msgstr "Skrivare ombyggd för pellets" @@ -17271,6 +17468,7 @@ msgid "The allowed maximum output force of Y axis" msgstr "Den maximalt tillåtna utgående kraften för Y-axeln" # AI Translated +msgctxt "Newton" msgid "N" msgstr "N" @@ -17281,6 +17479,7 @@ msgid "The machine bed mass load of Y axis" msgstr "Maskinbäddens massbelastning på Y-axeln" # AI Translated +msgctxt "gram" msgid "g" msgstr "g" @@ -17632,8 +17831,8 @@ msgid "Reduce infill retraction" msgstr "Minska ifyllnads retraktionen" # AI Translated -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." -msgstr "Dra inte tillbaka när förflyttningen är helt i ett utfyllnadsområde. Det betyder att läckage av filament inte kan ses. Detta kan minska tiderna för indragning för komplexa modeller och spara utskriftstid, men gör beredning och generering av G-kod långsammare. Observera att z-hop inte heller utförs i områden där retraktionen hoppas över." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." +msgstr "Dra inte tillbaka när förflyttningen är helt i ett utfyllnadsområde. Det betyder att läckage av filament inte kan ses. Detta kan minska tiderna för indragning för komplexa modeller och spara utskriftstid, men gör beredning och generering av G-kod långsammare. Observera att Z-hop inte heller utförs i områden där retraktionen hoppas över." # AI Translated msgid "This option will drop the temperature of the inactive extruders to prevent oozing." @@ -17825,7 +18024,7 @@ msgstr "Reduktionsmängd efter avtorkning" # AI Translated #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" "Längden på den snabba reduktionen efter avtorkning, i förhållande till reduktionslängden.\n" @@ -17868,11 +18067,19 @@ msgstr "Lång reduktion vid extruderbyte" msgid "Retraction distance when extruder change" msgstr "Reduktionssträcka vid extruderbyte" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Reduktionslängd (Verktygsbyte)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "När reduktionen utlöses före ett verktygsbyte dras filamentet tillbaka med den angivna mängden (längden mäts på det obearbetade filamentet, innan det når extrudern)." + # AI Translated msgid "Z-hop height" msgstr "Z-hop-höjd" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "När det är en retraktion lyfts nozzel en aning för att skapa ett spel mellan nozzel och utskriften. Detta förhindrar att nozzel träffar utskriften när den förflyttas. Att använda spirallinjer för att lyfta z kan förhindra strängning" msgid "Z-hop lower boundary" @@ -17983,6 +18190,10 @@ msgstr "Extra längd vid omstart" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "När reduktionen kompenseras efter flyttrörelsen trycker extrudern fram den här extra mängden filament. Den här inställningen behövs sällan." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Extra längd vid omstart (Verktygsbyte)" + # AI Translated msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "När reduktionen kompenseras efter verktygsbyte trycker extrudern fram den här extra mängden filament." @@ -18477,6 +18688,14 @@ msgstr "Verktygsbyte vid prime tornet" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Tvinga verktygshuvudet att flytta till prime tornet innan verktygsbyteskommandot (Tx) skickas. Endast relevant för skrivare med flera extrudrar (flera verktygshuvuden) som använder ett prime torn av typ 2. Som standard hoppar Orca över flytten på maskiner med flera verktygshuvuden, eftersom den fasta programvaran hanterar huvudbytet, vilket kan leda till att Tx-kommandot skickas ovanför den utskrivna delen. Aktivera det här alternativet om du vill att verktygsbytet alltid ska ske ovanför prime tornet i stället." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Vänta på temperatur vid prime tornet" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Hämtar det nya verktyget utan att vänta på att det ska nå utskriftstemperatur, förflyttar sig till prime tornet och väntar på temperaturen där, precis före rensningen. Materialet som droppar under uppvärmningen hamnar på tornet i stället för på modellen, och förflyttningen sker samtidigt som uppvärmningen. Endast relevant för skrivare med flera extrudrar (flera verktygshuvuden) som använder ett prime torn av typ 2. Firmware eller verktygsbytesmakrot får inte vänta på temperaturen själv. När detta är avaktiverat utfärdas temperaturväntan direkt efter verktygsbyteskommandot." + # AI Translated msgid "No sparse layers (beta)" msgstr "Inga glesa lager (beta)" @@ -19060,6 +19279,14 @@ msgstr "" "\n" "Om du anger ett värde i inställningen reduktionsmängd före avtorkning nedan utförs eventuell överskjutande reduktion före avtorkningen, annars utförs den efteråt." +# AI Translated +msgid "Mixed color sublayer" +msgstr "Underlager med blandad färg" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "Aktiverar uppdelning i underlager med blandad färg. När det är aktiverat delas lager som innehåller filament med blandad färg upp i underlager för att skapa färgblandningseffekter." + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "Avstryknings tornet kan användas för att avlägsna rester på munstycket och stabilisera kammartrycket inuti munstycket för att undvika utseendefel vid utskrift av objekt." @@ -20333,6 +20560,10 @@ msgstr "Det gick inte att skapa mesh från modellfilen, eller så saknas en gilt msgid "The supplied file couldn't be read because it's empty." msgstr "Den medföljande filen kunde inte läsas eftersom den är tom." +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "Filformatet är inkompatibelt och kan inte tolkas." + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Okänt filformat: indata filen måste ha tillägget .stl, .obj eller .amf(.xml)." @@ -21933,19 +22164,19 @@ msgstr "Endast printer med ändringar av inställningar för printer, filament o msgid "Only display the filament names with changes to filament presets." msgstr "Visa endast filament namnen vid ändringar av filament inställningar." -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "Endast printer namn med inställningar för printer visas, och varje inställning du väljer exporteras som zip-fil." msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" "Endast filament namn med inställningar för användar filament visas, \n" "och alla inställningar för användar filament i varje filament namn du väljer exporteras som en zip-fil." msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" "Endast printer namn med ändrade process inställningar visas, \n" "och alla användarprocess inställningar i varje printer namn som du väljer exporteras som zip-fil." @@ -22101,10 +22332,6 @@ msgstr "Fysisk printer" msgid "Print Host upload" msgstr "Uppladdning utskriftsvärd" -# AI Translated -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Välj vilken nätverksagentimplementation som ska användas för kommunikation med skrivaren. Tillgängliga agenter registreras vid start." - # AI Translated msgid "Select a Flashforge printer" msgstr "Välj en Flashforge-skrivare" @@ -22208,7 +22435,7 @@ msgid "We need information for diagnosing source of the issue. Check wiki page f msgstr "Vi behöver information för att diagnostisera orsaken till problemet. Se wiki-sidan för en detaljerad guide." # AI Translated -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "Paketera-knappen samlar projektfilen och loggarna från den aktuella sessionen i en zip-fil." # AI Translated @@ -22264,7 +22491,7 @@ msgid "Stored logs" msgstr "Sparade loggar" # AI Translated -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "Paketerar alla sparade loggar i en zip-fil." # AI Translated @@ -22352,7 +22579,7 @@ msgid "Authorizing..." msgstr "Auktoriserar..." # AI Translated -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "Fel. Det går inte att hämta api-token för auktorisering" # AI Translated @@ -22913,8 +23140,8 @@ msgid "Enable smart filament assign: Assign one filament to multiple nozzles to msgstr "Aktivera smart filamenttilldelning: Tilldela ett filament till flera nozzlar för att maximera besparingen" # AI Translated -msgid "Fila Saving" -msgstr "Filamentbesparing" +msgid "File Saving" +msgstr "Filsparande" # AI Translated msgid "Don't remind me again" @@ -23181,10 +23408,6 @@ msgstr "Något oväntat hände vid inloggningen, försök igen." msgid "User canceled." msgstr "Användaren avbröt." -# AI Translated -msgid "Head diameter" -msgstr "Huvuddiameter" - # AI Translated msgid "Max angle" msgstr "Maxvinkel" @@ -23391,6 +23614,9 @@ msgstr "Starta om nu" msgid "NO RAMMING AT ALL" msgstr "INGEN RAMMING ALLS" +msgid "s" +msgstr "s" + # AI Translated msgid "Volumetric speed" msgstr "Volymetrisk hastighet" @@ -24071,6 +24297,63 @@ msgstr "" "Undvik vridning\n" "Visste du att när du skriver ut material som är benägna att vrida, såsom ABS, kan en lämplig ökning av värmebäddens temperatur minska sannolikheten för vridning?" +# AI Translated +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "Liveview i Wayland kräver GStreamers GTK-videosink. Installera insticksmodulen gtksink för GStreamer och starta sedan om OrcaSlicer." + +# AI Translated +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "Det gick inte att initiera Waylands GStreamer-videosink. Kontrollera installationen av din GStreamer GTK-insticksmodul." + +# AI Translated +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "Windows Media Player krävs för den här uppgiften! Vill du aktivera 'Windows Media Player' i ditt operativsystem?" + +# AI Translated +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "BambuSource har inte registrerats korrekt för mediauppspelning! Tryck på Ja för att registrera om den. Du får två frågor" + +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "Saknad BambuSource-komponent registrerad för mediauppspelning! Installera om OrcaSlicer eller sök hjälp i gemenskapen." + +# AI Translated +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "Du använder en BambuSource från en annan installation, videouppspelningen kanske inte fungerar korrekt! Tryck på Ja för att åtgärda det." + +# AI Translated +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "Ditt system saknar H.264-kodekar för GStreamer, som krävs för att spela upp video. (Prova att installera paketen gstreamer1.0-plugins-bad eller gstreamer1.0-libav och starta sedan om Orca Slicer.)" + +# AI Translated +#~ msgid "N" +#~ msgstr "N" + +# AI Translated +#~ msgid "g" +#~ msgstr "g" + +# AI Translated +#~ msgid "Fila Saving" +#~ msgstr "Filamentbesparing" + +# AI Translated +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Lagerhöjden är för liten.\n" +#~ "Den ställs in på min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Lagerhöjden överskrider gränsen i Skrivarinställningar -> Extruder -> Lagerhöjds gränser, detta kan orsaka problem med utskriftskvaliteten." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Justera automatiskt till det inställda området?\n" + +# AI Translated +#~ msgid "Head diameter" +#~ msgstr "Huvuddiameter" + # AI Translated #~ msgid "Print order within a single layer." #~ msgstr "Utskriftsordning inom ett enskilt lager." diff --git a/localization/i18n/th/OrcaSlicer_th.po b/localization/i18n/th/OrcaSlicer_th.po index 84b41505a9..16dcb17ab1 100644 --- a/localization/i18n/th/OrcaSlicer_th.po +++ b/localization/i18n/th/OrcaSlicer_th.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "PO-Revision-Date: 2026-06-19 13:40+0700\n" "Last-Translator: Icezaza\n" "Language-Team: Thai\n" @@ -843,7 +843,7 @@ msgid "Hexagon" msgstr "หกเหลี่ยม" msgid "Keep orientation" -msgstr "รักษาปฐมนิเทศ" +msgstr "คงการวางแนว" msgid "Place on cut" msgstr "วางบนการตัด" @@ -891,7 +891,7 @@ msgid "Cut position" msgstr "ตำแหน่งตัด" msgid "Build Volume" -msgstr "ปริมาตรพื้นที่พิมพ์" +msgstr "ปริมาตรการพิมพ์ (Build Volume)" msgid "Multiple" msgstr "หลายรายการ" @@ -1200,7 +1200,7 @@ msgid "Horizontal text" msgstr "ข้อความแนวนอน" msgid "Shift+" -msgstr "กะ+" +msgstr "Shift+" msgid "Mouse move up or down" msgstr "เมาส์เลื่อนขึ้นหรือลง" @@ -2592,7 +2592,7 @@ msgid "Ironing" msgstr "รีดผิว" msgid "Fuzzy skin" -msgstr "ผิวฟัซซี" +msgstr "ผิวฟัซซี (Fuzzy Skin)" msgid "Extruders" msgstr "ชุดดันเส้น" @@ -2817,10 +2817,10 @@ msgid "current" msgstr "ปัจจุบัน" msgid "Scale to build volume" -msgstr "ปรับขนาดเพื่อสร้างปริมาณ" +msgstr "ปรับขนาดให้พอดีกับปริมาตรการพิมพ์" msgid "Scale an object to fit the build volume" -msgstr "ปรับขนาดวัตถุให้พอดีกับปริมาณงานสร้าง" +msgstr "ปรับขนาดวัตถุให้พอดีกับปริมาตรการพิมพ์" msgid "Flush Options" msgstr "ตัวเลือกการไล่เส้น" @@ -2898,10 +2898,10 @@ msgid "Change SVG source file, projection, size, ..." msgstr "เปลี่ยนไฟล์ต้นฉบับ SVG, การฉายภาพ, ขนาด, ..." msgid "Invalidate cut info" -msgstr "ข้อมูลการตัดไม่ถูกต้อง" +msgstr "ยกเลิกข้อมูลการตัด" msgid "Add Primitive" -msgstr "เพิ่มดั้งเดิม" +msgstr "เพิ่มรูปทรงพื้นฐาน" msgid "Add Handy models" msgstr "เพิ่มรุ่นแฮนดี้" @@ -2942,6 +2942,10 @@ msgstr "แก้ไข" msgid "Merge with" msgstr "รวมกับ" +# AI Translated +msgid "Decompose Color" +msgstr "แยกสี" + msgid "Delete this filament" msgstr "ลบเส้นพลาสติกนี้" @@ -3027,7 +3031,7 @@ msgid "Center" msgstr "กึ่งกลาง" msgid "Drop" -msgstr "หยด" +msgstr "วางลงฐานพิมพ์" msgid "Edit Process Settings" msgstr "แก้ไขการตั้งค่ากระบวนการ" @@ -3182,7 +3186,7 @@ msgid "Switch to per-object setting mode to edit process settings of selected ob msgstr "สลับไปที่โหมดการตั้งค่าต่ออ็อบเจ็กต์เพื่อแก้ไขการตั้งค่ากระบวนการของอ็อบเจ็กต์ที่เลือก" msgid "Remove paint-on fuzzy skin" -msgstr "ลบสีบนผิวที่คลุมเครือ" +msgstr "ลบการระบายสีผิวฟัซซี" # AI Translated msgid "Delete Settings" @@ -3237,12 +3241,16 @@ msgstr "การประกอบ" msgid "Merge parts to an object" msgstr "รวมชิ้นส่วนเป็นวัตถุเดียว" +# AI Translated +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "การใช้ความสูงเลเยอร์แบบแปรผันร่วมกับซับเลเยอร์สีผสมอาจทำให้คุณภาพการผสมสีไม่ดี" + # AI Translated msgid "Add layers" msgstr "เพิ่มเลเยอร์" msgid "Cut Connectors information" -msgstr "ตัดข้อมูลตัวเชื่อมต่อ" +msgstr "ข้อมูลตัวเชื่อมสำหรับการตัด" msgid "Object manipulation" msgstr "การจัดการวัตถุ" @@ -3279,7 +3287,7 @@ msgid "Layer" msgstr "เลเยอร์" msgid "Selection conflicts" -msgstr "ข้อขัดแย้งในการคัดเลือก" +msgstr "การเลือกขัดแย้งกัน" msgid "If the first selected item is an object, the second should also be an object." msgstr "หากรายการแรกที่เลือกเป็นวัตถุ รายการที่สองก็ควรเป็นวัตถุด้วย" @@ -3390,7 +3398,7 @@ msgid "Plate" msgstr "ฐานพิมพ์" msgid "Brim" -msgstr "ขอบยึดชิ้นงาน" +msgstr "ขอบยึดชิ้นงาน (Brim)" msgid "Object/Part Settings" msgstr "การตั้งค่าวัตถุ/ชิ้นส่วน" @@ -4720,6 +4728,23 @@ msgstr "อุณหภูมิห้องพิมพ์ปัจจุบั msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "อุณหภูมิห้องพิมพ์ต่ำสุด (%d℃) สูงกว่าอุณหภูมิห้องพิมพ์เป้าหมาย (%d℃) ค่าต่ำสุดคือเกณฑ์ที่การพิมพ์จะเริ่มต้นในขณะที่ห้องพิมพ์ยังคงร้อนขึ้นไปสู่เป้าหมาย จึงไม่ควรเกินค่าเป้าหมาย ระบบจะจำกัดค่าให้เท่ากับเป้าหมาย" +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "ความสูงเลเยอร์น้อยเกินไป จะถูกตั้งค่าเป็นค่าต่ำสุด (%g mm)" + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "ความสูงเลเยอร์อยู่นอกขีดจำกัดที่ตั้งไว้ใน การตั้งค่าเครื่องพิมพ์ -> ชุดดันเส้น -> การจำกัดความสูงของเลเยอร์ ซึ่งอาจทำให้เกิดปัญหาคุณภาพการพิมพ์" + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "ปรับเป็นค่าขีดจำกัด (%g mm) โดยอัตโนมัติหรือไม่?" + +msgid "Adjust" +msgstr "ปรับ" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4768,7 +4793,7 @@ msgstr "" "\n" "ค่าจะถูกรีเซ็ตเป็น 0" -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "ผนังเสริมสำรองทำงานได้ไม่ดีเมื่อตั้งค่าความหนาของเปลือกแนวตั้งเป็นทั้งหมด" msgid "" @@ -4786,7 +4811,7 @@ msgid "" "YES - Keep Prime Tower\n" "NO - Keep Adaptive Layer Height and Independent Support Layer Height" msgstr "" -"ไพรม์ทาวเวอร์ไม่ทำงานเมื่อเปิดใช้งาน Adaptive Layer Height หรือ Independent ส่วนรองรับ Layer Height\n" +"Prime Tower ไม่ทำงานเมื่อเปิดใช้งาน Adaptive Layer Height หรือ Independent ส่วนรองรับ Layer Height\n" "คุณต้องการเก็บอันไหน?\n" "ใช่ - เก็บ Prime Tower ไว้\n" "ไม่ - คงความสูงของเลเยอร์แบบปรับได้และความสูงของเลเยอร์รองรับที่เป็นอิสระ" @@ -4797,7 +4822,7 @@ msgid "" "YES - Keep Prime Tower\n" "NO - Keep Adaptive Layer Height" msgstr "" -"ไพรม์ทาวเวอร์ไม่ทำงานเมื่อเปิด Adaptive Layer Height\n" +"Prime Tower ไม่ทำงานเมื่อเปิด Adaptive Layer Height\n" "คุณต้องการเก็บอันไหน?\n" "ใช่ - เก็บ Prime Tower ไว้\n" "ไม่ - คงความสูงของเลเยอร์แบบปรับได้" @@ -4808,13 +4833,13 @@ msgid "" "YES - Keep Prime Tower\n" "NO - Keep Independent Support Layer Height" msgstr "" -"ไพร์มทาวเวอร์ไม่ทำงานเมื่อเปิดความสูงของเลเยอร์รองรับอิสระ\n" +"Prime Tower ไม่ทำงานเมื่อเปิดความสูงของเลเยอร์รองรับอิสระ\n" "คุณต้องการเก็บอันไหน?\n" "ใช่ - เก็บ Prime Tower ไว้\n" "ไม่ - รักษาความสูงของชั้นรองรับที่เป็นอิสระ" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height ต้องเล็กกว่า layer_height\n" @@ -4822,7 +4847,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "ความลึกของล็อคควรน้อยกว่าความลึกของผิวหนัง\n" @@ -4840,6 +4865,13 @@ msgstr "" "ใช่ - เปิดใช้งาน Arachne Wall Generator\n" "ไม่ - ปิดการใช้งาน Arachne Wall Generator และตั้งค่าโหมด [Displacement] ของ Fuzzy Skin" +# AI Translated +msgid "Brim ear radius" +msgstr "รัศมีของหูขอบยึดชิ้นงาน" + +msgid "Brim width" +msgstr "ความกว้าง ขอบยึดชิ้นงาน" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "โหมดเกลียวจะทำงานเฉพาะเมื่อลูปติดผนังเป็น 1, ปิดใช้งานส่วนรองรับ, การตรวจจับการจับตัวเป็นก้อนโดยการตรวจวัดถูกปิดใช้งาน, ชั้นเปลือกด้านบนเป็น 0, ความหนาแน่นของไส้ในแบบกระจายเป็น 0 และประเภทไทม์แลปส์เป็นแบบดั้งเดิม" @@ -5094,6 +5126,14 @@ msgstr "ไม่สามารถสร้าง cali G-code" msgid "Calibration error" msgstr "ข้อผิดพลาดในการสอบเทียบ" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "เครื่องพิมพ์นี้ไม่ได้ตั้งค่าฮาร์ดแวร์ที่ตัวควบคุมนี้ต้องการ" + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "ตัวควบคุมนี้ไม่รองรับบนเครื่องพิมพ์นี้" + # AI Translated msgid "Network unavailable" msgstr "เครือข่ายไม่พร้อมใช้งาน" @@ -5381,7 +5421,7 @@ msgid "Pressure Advance" msgstr "แรงดันล่วงหน้า (Pressure Advance)" msgid "Noop" -msgstr "นะ" +msgstr "ไม่มีการดำเนินการ" msgid "Retract" msgstr "ดึงกลับ" @@ -5405,7 +5445,7 @@ msgid "Travel" msgstr "เดินหัวเปล่า" msgid "Wipe" -msgstr "เช็ดหัวฉีด" +msgstr "เช็ดหัวฉีด (Wipe)" msgid "Extrude" msgstr "ฉีดเส้น" @@ -5441,7 +5481,7 @@ msgid "Support interface" msgstr "ผิวสัมผัสส่วนรองรับ" msgid "Prime tower" -msgstr "ทาวเวอร์ไล่เส้น" +msgstr "Prime Tower" msgid "Bottom surface" msgstr "ผิวด้านล่าง" @@ -5486,7 +5526,7 @@ msgid "Jerk: " msgstr "เจิร์ก: " msgid "PA: " -msgstr "พ่อ:" +msgstr "PA: " msgid "mm/s" msgstr "มม./วินาที" @@ -5552,7 +5592,7 @@ msgid "Tips:" msgstr "เคล็ดลับ:" msgid "Current grouping of slice result is not optimal." -msgstr "การจัดกลุ่มผลลัพธ์การแบ่งส่วนในปัจจุบันไม่เหมาะสมที่สุด" +msgstr "การจัดกลุ่มผลการสไลซ์ปัจจุบันยังไม่เหมาะสม" #, boost-format msgid "Increase %1%g filament and %2% changes compared to optimal grouping." @@ -5585,7 +5625,7 @@ msgid "Regroup filament" msgstr "จัดกลุ่มเส้นพลาสติกใหม่" msgid "up to" -msgstr "ขึ้นไป" +msgstr "สูงสุด" msgid "above" msgstr "ข้างบน" @@ -5642,7 +5682,7 @@ msgid "Filament change times" msgstr "จำนวนครั้งที่เปลี่ยนเส้น" msgid "Tool changes" -msgstr "การเปลี่ยนแปลงเครื่องมือ" +msgstr "การเปลี่ยนเครื่องมือ" msgid "Color change" msgstr "เปลี่ยนสี" @@ -5673,7 +5713,7 @@ msgid "Model printing time" msgstr "ระยะเวลาในการพิมพ์โมเดล" msgid "Show stealth mode" -msgstr "แสดงโหมดซ่อนตัว" +msgstr "แสดงโหมดเงียบ" msgid "Show normal mode" msgstr "แสดงโหมดปกติ" @@ -5690,10 +5730,10 @@ msgid "" "Please solve the problem by moving it totally on or off the plate, and confirming that the height is within the build volume." msgstr "" "วัตถุวางอยู่เหนือขอบเขตของแผ่นหรือสูงเกินขีดจำกัดความสูง\n" -"โปรดแก้ไขปัญหาด้วยการเลื่อนเข้าหรือออกจากเพลตโดยสิ้นเชิง และยืนยันว่าความสูงอยู่ภายในปริมาตรงานประกอบ" +"โปรดแก้ไขปัญหาด้วยการเลื่อนเข้าหรือออกจากเพลตโดยสิ้นเชิง และยืนยันว่าความสูงอยู่ภายในปริมาตรการพิมพ์" msgid "Variable layer height" -msgstr "ความสูงของชั้นตัวแปร" +msgstr "ความสูงเลเยอร์แบบแปรผัน" msgid "Adaptive" msgstr "ปรับตัวได้" @@ -5743,7 +5783,7 @@ msgid "Following objects are laid over the boundary of plate or exceeds the heig msgstr "วัตถุต่อไปนี้วางอยู่เหนือขอบเขตของแผ่นหรือสูงเกินขีดจำกัดความสูง:\n" msgid "Please solve the problem by moving it totally on or off the plate, and confirming that the height is within the build volume.\n" -msgstr "โปรดแก้ไขปัญหาด้วยการเลื่อนเข้าหรือออกจากเพลตโดยสิ้นเชิง และยืนยันว่าความสูงอยู่ภายในปริมาตรงานประกอบ\n" +msgstr "โปรดแก้ไขปัญหาด้วยการเลื่อนเข้าหรือออกจากเพลตโดยสิ้นเชิง และยืนยันว่าความสูงอยู่ภายในปริมาตรการพิมพ์\n" #, c-format, boost-format msgid "The position or size of some models exceeds the %s's printable range." @@ -5783,7 +5823,7 @@ msgid "Optimize support interface area" msgstr "ปรับพื้นที่อินเทอร์เฟซส่วนรองรับให้เหมาะสม" msgid "Orient" -msgstr "ตะวันออก" +msgstr "จัดวางแนว" msgid "Arrange options" msgstr "ตัวเลือกจัดเรียง" @@ -5929,7 +5969,7 @@ msgid "Paint Toolbar" msgstr "แถบเครื่องมือสี" msgid "Explosion Ratio" -msgstr "อัตราส่วนการระเบิด" +msgstr "ระดับการแยกชิ้นส่วน" msgid "Section View" msgstr "มุมมองส่วน" @@ -5952,7 +5992,7 @@ msgstr "ปริมาณ:" msgid "Size:" msgstr "ขนาด:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "พบความขัดแย้งของเส้นทางรหัส G ที่เลเยอร์ %d, Z = %.2lfmm โปรดแยกวัตถุที่ขัดแย้งกันให้ไกลออกไป (%s <-> %s)" @@ -6002,7 +6042,7 @@ msgid "PLA and PETG filaments detected in the mixture. Adjust parameters accordi msgstr "ตรวจพบเส้นพลาสติก PLA และ PETG ในส่วนผสม ปรับพารามิเตอร์ตาม Wiki เพื่อรับรองคุณภาพการพิมพ์" msgid "The prime tower extends beyond the plate boundary." -msgstr "หอคอยหลักขยายออกไปเกินขอบเขตแผ่นเปลือกโลก" +msgstr "Prime Tower ยื่นออกนอกขอบเขตของเพลตพิมพ์" msgid "Partial flushing volume set to 0. Multi-color printing may cause color mixing in models. Please readjust flushing settings." msgstr "ตั้งค่าปริมาณการไล่เส้นบางส่วนเป็น 0 การพิมพ์หลายสีอาจทำให้เกิดการผสมสีในรุ่นต่างๆ โปรดปรับการตั้งค่าการไล่เส้นใหม่" @@ -6133,6 +6173,10 @@ msgstr "หลายอุปกรณ์" msgid "Project" msgstr "โปรเจกต์" +# AI Translated +msgid "Device (Web)" +msgstr "อุปกรณ์ (เว็บ)" + msgid "Yes" msgstr "ใช่" @@ -6266,10 +6310,10 @@ msgstr "นำเข้า 3MF/STL/STEP/SVG/OBJ/AMF" msgid "Load a model" msgstr "โหลดโมเดล" -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "นำเข้าไฟล์ Zip" -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "โหลดโมเดลที่มีอยู่ในไฟล์ zip" msgid "Import Configs" @@ -7771,6 +7815,10 @@ msgstr "ปรับแต่งแผ่นปัจจุบัน" msgid "The %s nozzle can not print %s." msgstr "หัวฉีด %s ไม่สามารถพิมพ์ %s ได้" +# AI Translated +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "การพิมพ์เส้นพลาสติกสีผสมบนเครื่องพิมพ์ที่มีชุดดันเส้นเดียวต้องเปลี่ยนเส้นพลาสติกและไล่เส้นบ่อยครั้ง ซึ่งอาจเพิ่มเศษวัสดุและความเสี่ยงที่หัวฉีดหรือช่องทิ้งเศษวัสดุจะอุดตันอย่างมาก" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "ไม่แนะนำให้ผสม %1% กับ %2% ในการพิมพ์\n" @@ -7896,12 +7944,44 @@ msgstr "ประสานรายการเส้นพลาสติกจ msgid "Set filaments to use" msgstr "ตั้งค่าเส้นพลาสติกที่จะใช้" +# AI Translated +msgid "Add Mixed Filament" +msgstr "เพิ่มเส้นพลาสติกผสม" + +# AI Translated +msgid "Mixed Filament" +msgstr "เส้นพลาสติกผสม" + +# AI Translated +msgid "Remove last mixed filament" +msgstr "ลบเส้นพลาสติกผสมล่าสุด" + +# AI Translated +msgid "Add mixed filament" +msgstr "เพิ่มเส้นพลาสติกผสม" + +# AI Translated +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "เส้นพลาสติกผสมมีส่วนประกอบที่ไม่ถูกต้องหรือไม่ตรงกัน กรุณาแก้ไขรายการที่ได้รับผลกระทบอีกครั้ง" + msgid "Search plate, object and part." msgstr "ค้นหาจาน วัตถุ และชิ้นส่วน" msgid "Pellets" msgstr "เม็ด" +# AI Translated +msgid "Mixed filament has broken component references" +msgstr "เส้นพลาสติกผสมมีการอ้างอิงส่วนประกอบที่เสียหาย" + +# AI Translated +msgid "Edit / Delete / Merge" +msgstr "แก้ไข / ลบ / รวม" + +# AI Translated +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "เส้นพลาสติกผสมปลายทางใช้เส้นพลาสติกจริงนี้เป็นส่วนประกอบ การรวมจะลบเส้นพลาสติกจริงนี้และอาจทำให้เส้นพลาสติกผสมใช้ไม่ได้ ดำเนินการต่อหรือไม่?" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "หลังจากเสร็จสิ้นการดำเนินการของคุณ โครงการ %s จะถูกปิดและสร้างโครงการใหม่" @@ -7953,7 +8033,7 @@ msgid "Enabling traditional timelapse photography may cause surface imperfection msgstr "การเปิดใช้งานการถ่ายภาพไทม์แลปส์แบบดั้งเดิมอาจทำให้เกิดความไม่สมบูรณ์ของพื้นผิวได้ ขอแนะนำให้เปลี่ยนเป็นโหมดราบรื่น" msgid "Smooth mode for timelapse is enabled, but the prime tower is off, which may cause print defects. Please enable the prime tower, re-slice and print again." -msgstr "เปิดใช้งานโหมด Smooth สำหรับไทม์แลปส์แล้ว แต่ไพรม์ทาวเวอร์ปิดอยู่ ซึ่งอาจทำให้เกิดข้อบกพร่องในการพิมพ์ โปรดเปิดใช้งานไพร์มทาวเวอร์ สไลซ์ใหม่และพิมพ์อีกครั้ง" +msgstr "เปิดใช้งานโหมด Smooth สำหรับไทม์แลปส์แล้ว แต่ Prime Tower ปิดอยู่ ซึ่งอาจทำให้เกิดข้อบกพร่องในการพิมพ์ โปรดเปิดใช้งาน Prime Tower สไลซ์ใหม่และพิมพ์อีกครั้ง" msgid "Expand sidebar" msgstr "ขยายแถบด้านข้าง" @@ -8038,8 +8118,8 @@ msgstr "โปรดยืนยันว่ารหัส G ภายในค msgid "Customized Preset" msgstr "ค่าที่ตั้งไว้ล่วงหน้าที่กำหนดเอง" -msgid "Component name(s) inside step file not in UTF8 format!" -msgstr "ชื่อของส่วนประกอบภายในไฟล์ STEP ไม่ใช่รูปแบบ UTF8!" +msgid "Component name(s) inside step file not in UTF-8 format!" +msgstr "ชื่อของส่วนประกอบภายในไฟล์ STEP ไม่ใช่รูปแบบ UTF-8!" msgid "Because of unsupported text encoding, garbage characters may appear!" msgstr "ชื่ออาจแสดงตัวอักษรขยะ!" @@ -8060,7 +8140,7 @@ msgstr "ปริมาตรของวัตถุเป็นศูนย์ #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "วัตถุจากไฟล์ %s มีขนาดเล็กเกินไป และอาจมีหน่วยเป็นเมตรหรือนิ้ว\n" " คุณต้องการขยายขนาดเป็นมิลลิเมตรหรือไม่?" @@ -8080,6 +8160,14 @@ msgstr "" msgid "Multi-part object detected" msgstr "ตรวจพบวัตถุที่มีหลายส่วน" +# AI Translated +msgid "Matching textures to filaments" +msgstr "กำลังจับคู่พื้นผิวกับเส้นพลาสติก" + +# AI Translated +msgid "Texture Import Warning" +msgstr "คำเตือนการนำเข้าพื้นผิว" + msgid "Load these files as a single object with multiple parts?\n" msgstr "โหลดไฟล์เหล่านี้เป็นออบเจ็กต์เดียวที่มีหลายส่วนใช่ไหม\n" @@ -8199,21 +8287,21 @@ msgstr "ไม่ได้เลือกไดเรกทอรีสำหร msgid "Replaced with 3D files from directory:\n" msgstr "แทนที่ด้วยไฟล์ 3D จากไดเรกทอรี:\n" -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ ข้าม %1%: ไฟล์เดียวกัน\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ ข้าม %s: ไฟล์เดียวกัน\n" -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ ข้าม %1%: ไม่มีไฟล์อยู่\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ ข้าม %s: ไม่มีไฟล์อยู่\n" -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ ข้าม %1%: ไม่สามารถแทนที่ได้\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ ข้าม %s: ไม่สามารถแทนที่ได้\n" -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔แทนที่ %1%\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔แทนที่ %s\n" msgid "Replaced volumes" msgstr "ปริมาณที่ถูกแทนที่" @@ -8290,6 +8378,22 @@ msgstr "" msgid "Sync now" msgstr "ซิงค์เลย" +# AI Translated +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "การนำเข้าพื้นผิวล้มเหลว โมเดลดูเหมือนจะมีข้อมูลพื้นผิว แต่ไม่สามารถดำเนินการนำเข้าพื้นผิวจนเสร็จสิ้นได้ โมเดลจะถูกนำเข้าเป็นเรขาคณิตเท่านั้น" + +# AI Translated +msgid "Applying texture colors..." +msgstr "กำลังใช้สีพื้นผิว..." + +# AI Translated +msgid "Updating 3D view..." +msgstr "กำลังอัปเดตมุมมอง 3D..." + +# AI Translated +msgid "Texture colors applied." +msgstr "ใช้สีพื้นผิวแล้ว" + msgid "You can keep the modified presets for the new project or discard them" msgstr "คุณสามารถเก็บค่าที่ตั้งไว้ล่วงหน้าที่แก้ไขแล้วไว้ในโปรเจ็กต์ใหม่หรือทิ้งก็ได้" @@ -8945,6 +9049,18 @@ msgstr "เมื่อเปิดใช้งานตัวเลือกน msgid "Pop up to select filament grouping mode" msgstr "ปรากฏขึ้นเพื่อเลือกโหมดการจัดกลุ่มเส้นพลาสติก" +# AI Translated +msgid "Visible plugin pages" +msgstr "หน้าปลั๊กอินที่แสดง" + +# AI Translated +msgid "pages" +msgstr "หน้า" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "จำนวนหน้าปลั๊กอินที่แสดงเป็นแท็บถาวร ก่อนที่หน้าที่เหลือจะถูกยุบรวมเป็นเมนูแบบเลื่อนลงในแท็บสุดท้าย" + msgid "Behaviour" msgstr "พฤติกรรม" @@ -9299,6 +9415,18 @@ msgstr "แสดงค่าที่ตั้งไว้ล่วงหน้ msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "แสดงค่าที่ตั้งไว้ล่วงหน้าที่ไม่เข้ากันหรือไม่รองรับในรายการเลือกเครื่องพิมพ์และเส้นพลาสติก ไม่สามารถเลือกค่าที่ตั้งไว้ล่วงหน้าเหล่านี้ได้" +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(ทดลอง) ใช้เอเจนต์เครื่องพิมพ์แทนโฮสต์การพิมพ์" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"ส่งงานพิมพ์ของเครื่องพิมพ์ที่ไม่ใช่ Bambu ผ่านเอเจนต์ปลั๊กอินของเครื่องพิมพ์ แทนการอัพโหลดไปยังโฮสต์การพิมพ์แบบเดิม\n" +"เมื่อปิดใช้ OrcaSlicer จะใช้พฤติกรรมโฮสต์การพิมพ์แบบเดิม" + # AI Translated msgid "Experimental Features" msgstr "ฟีเจอร์ทดลอง" @@ -9311,7 +9439,7 @@ msgid "" "Attempt to keep painted features (color/seam/support/fuzzy etc.) after changing the object mesh (such as cut/reload from disk/simplify/fix etc.)\n" "Highly experimental! Slow and may create artifact." msgstr "" -"พยายามคงคุณสมบัติการทาสีไว้ (สี/รอยตะเข็บ/ส่วนรองรับ/คลุมเครือ ฯลฯ) หลังจากเปลี่ยนตาข่ายวัตถุ (เช่น ตัด/โหลดซ้ำจากดิสก์/ลดความซับซ้อน/แก้ไข ฯลฯ)\n" +"พยายามคงคุณสมบัติการทาสีไว้ (สี/รอยตะเข็บ/ส่วนรองรับ/ผิวฟัซซี ฯลฯ) หลังจากเปลี่ยนตาข่ายวัตถุ (เช่น ตัด/โหลดซ้ำจากดิสก์/ลดความซับซ้อน/แก้ไข ฯลฯ)\n" "น่าทดลองมาก! ช้าและอาจสร้างสิ่งประดิษฐ์" msgid "Allow Abnormal Storage" @@ -9508,6 +9636,10 @@ msgstr "แจกันเกลียว" msgid "First layer filament sequence" msgstr "ลำดับเส้นพลาสติกชั้นแรก" +# AI Translated +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "รายการเส้นพลาสติกมีเส้นพลาสติกผสมอยู่ ลำดับเส้นพลาสติกที่กำหนดเองจะไม่มีผล" + msgid "By Layer" msgstr "โดยเลเยอร์" @@ -9563,9 +9695,25 @@ msgstr "พรีเซ็ตผู้ใช้" msgid "Preset Inside Project" msgstr "พรีเซ็ตภายในโปรเจ็กต์" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "คัดลอกค่าที่สืบทอดมาจากพรีเซ็ตแม่ทั้งหมดมาไว้ในพรีเซ็ตนี้ และตัดความสัมพันธ์กับพรีเซ็ตแม่ พรีเซ็ตที่เข้ากันได้กับพรีเซ็ตแม่เท่านั้นอาจไม่ได้รับการรองรับอีกต่อไป" + msgid "Detach from parent" msgstr "แยกออกจากพรีเซ็ตแม่" +# AI Translated +msgid "Unique preset" +msgstr "พรีเซ็ตอิสระ" + +# AI Translated +msgid "Parent preset" +msgstr "พรีเซ็ตแม่" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "พรีเซ็ตนี้ไม่ได้สืบทอดมาจากพรีเซ็ตอื่น" + msgid "Name is unavailable." msgstr "ชื่อไม่พร้อมใช้งาน" @@ -10241,25 +10389,25 @@ msgstr "คลิกเพื่อรีเซ็ตการตั้งค่ # AI Translated msgid "Prime tower is required for nozzle changing. There may be flaws on the model without prime tower. Are you sure you want to disable prime tower?" -msgstr "จำเป็นต้องใช้ทาวเวอร์ไล่เส้นสำหรับการเปลี่ยนหัวฉีด อาจเกิดข้อบกพร่องบนโมเดลหากไม่มีทาวเวอร์ไล่เส้น คุณแน่ใจหรือไม่ว่าต้องการปิดทาวเวอร์ไล่เส้น?" +msgstr "จำเป็นต้องใช้ Prime Tower สำหรับการเปลี่ยนหัวฉีด อาจเกิดข้อบกพร่องบนโมเดลหากไม่มี Prime Tower คุณแน่ใจหรือไม่ว่าต้องการปิด Prime Tower?" msgid "A prime tower is required for smooth timelapse mode. There may be flaws on the model without a prime tower. Are you sure you want to disable the prime tower?" -msgstr "โหมดไทม์แลปส์แบบราบรื่นต้องใช้ไพรม์ทาวเวอร์ หากไม่มีไพรม์ทาวเวอร์อาจเกิดตำหนิบนโมเดลได้ คุณแน่ใจหรือไม่ว่าต้องการปิดไพรม์ทาวเวอร์?" +msgstr "จำเป็นต้องใช้ Prime Tower สำหรับโหมดไทม์แลปส์แบบราบรื่น หากไม่มี Prime Tower อาจเกิดตำหนิบนโมเดลได้ คุณแน่ใจหรือไม่ว่าต้องการปิด Prime Tower?" msgid "A prime tower is required for clumping detection. There may be flaws on the model without prime tower. Are you sure you want to disable prime tower?" -msgstr "ต้องใช้ไพรม์ทาวเวอร์ในการตรวจจับการจับกันเป็นก้อน อาจมีตำหนิตรงรุ่นที่ไม่มีไพร์มทาวเวอร์ คุณแน่ใจหรือไม่ว่าต้องการปิดการใช้งานไพร์มทาวเวอร์?" +msgstr "จำเป็นต้องใช้ Prime Tower ในการตรวจจับการจับกันเป็นก้อน อาจมีตำหนิบนโมเดลที่ไม่มี Prime Tower คุณแน่ใจหรือไม่ว่าต้องการปิด Prime Tower?" msgid "Enabling both precise Z height and the prime tower may cause slicing errors. Do you still want to enable?" -msgstr "การเปิดใช้งานทั้งความสูง Z ที่แม่นยำและหอคอยหลักอาจทำให้เกิดข้อผิดพลาดในการแบ่งส่วน คุณยังต้องการเปิดใช้งานหรือไม่?" +msgstr "การเปิดใช้งานทั้งความสูง Z ที่แม่นยำและ Prime Tower อาจทำให้เกิดข้อผิดพลาดในการแบ่งส่วน คุณยังต้องการเปิดใช้งานหรือไม่?" msgid "A prime tower is required for clumping detection. There may be flaws on the model without prime tower. Do you still want to enable clumping detection?" -msgstr "ต้องใช้ไพรม์ทาวเวอร์ในการตรวจจับการจับกันเป็นก้อน อาจมีตำหนิตรงรุ่นที่ไม่มีไพร์มทาวเวอร์ คุณยังต้องการเปิดใช้งานการตรวจจับการจับกันเป็นก้อนหรือไม่" +msgstr "จำเป็นต้องใช้ Prime Tower ในการตรวจจับการจับกันเป็นก้อน อาจมีตำหนิบนโมเดลที่ไม่มี Prime Tower คุณยังต้องการเปิดใช้งานการตรวจจับการจับกันเป็นก้อนหรือไม่" msgid "Enabling both precise Z height and the prime tower may cause slicing errors. Do you still want to enable precise Z height?" -msgstr "การเปิดใช้งานทั้งความสูง Z ที่แม่นยำและหอคอยหลักอาจทำให้เกิดข้อผิดพลาดในการแบ่งส่วน คุณยังต้องการเปิดใช้งานความสูง Z ที่แม่นยำหรือไม่" +msgstr "การเปิดใช้งานทั้งความสูง Z ที่แม่นยำและ Prime Tower อาจทำให้เกิดข้อผิดพลาดในการแบ่งส่วน คุณยังต้องการเปิดใช้งานความสูง Z ที่แม่นยำหรือไม่" msgid "A prime tower is required for smooth timelapse mode. There may be flaws on the model without prime tower. Do you want to enable the prime tower?" -msgstr "โหมดไทม์แลปส์แบบราบรื่นต้องใช้ไพรม์ทาวเวอร์ หากไม่มีไพรม์ทาวเวอร์อาจเกิดตำหนิบนโมเดลได้ ต้องการเปิดใช้ไพรม์ทาวเวอร์หรือไม่?" +msgstr "จำเป็นต้องใช้ Prime Tower สำหรับโหมดไทม์แลปส์แบบราบรื่น หากไม่มี Prime Tower อาจเกิดตำหนิบนโมเดลได้ ต้องการเปิดใช้ Prime Tower หรือไม่?" msgid "Still print by object?" msgstr "ยังคงพิมพ์ตามวัตถุใช่ไหม" @@ -10305,22 +10453,6 @@ msgstr "คุณแน่ใจหรือไม่ว่าต้องกา msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "โดยทั่วไปรูปแบบไส้ในได้รับการออกแบบให้รองรับการหมุนโดยอัตโนมัติเพื่อให้แน่ใจว่าการพิมพ์ถูกต้องและบรรลุผลตามที่ต้องการ (เช่น Gyroid, ลูกบาศก์) การหมุนรูปแบบ ไส้ใน แบบกระจัดกระจายในปัจจุบันอาจทำให้ส่วนรองรับไม่เพียงพอ โปรดดำเนินการด้วยความระมัดระวังและตรวจสอบปัญหาการพิมพ์ที่อาจเกิดขึ้นอย่างละเอียด คุณแน่ใจหรือไม่ว่าต้องการเปิดใช้งานตัวเลือกนี้" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"ความสูงของเลเยอร์น้อยเกินไป\n" -"มันจะตั้งค่าเป็น min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "ความสูงของเลเยอร์เกินขีดจำกัดในการตั้งค่าเครื่องพิมพ์ -> ชุดดันเส้น -> ขีดจำกัดความสูงของเลเยอร์ ซึ่งอาจทำให้เกิดปัญหาคุณภาพการพิมพ์" - -msgid "Adjust to the set range automatically?\n" -msgstr "ปรับเป็นช่วงที่ตั้งไว้อัตโนมัติ?\n" - -msgid "Adjust" -msgstr "ปรับ" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "คุณลักษณะการทดลอง: การดึงกลับและตัดเส้นพลาสติกออกในระยะห่างที่มากขึ้นระหว่างการเปลี่ยนเส้นพลาสติกเพื่อลดการไล่เส้น แม้ว่าจะสามารถลดการไล่เส้นได้อย่างเห็นได้ชัด แต่ก็อาจเพิ่มความเสี่ยงของการอุดตันของหัวฉีดหรือภาวะแทรกซ้อนในการพิมพ์อื่นๆ อีกด้วย" @@ -10411,7 +10543,7 @@ msgid "Z contouring" msgstr "รูปร่าง Z" msgid "Wall generator" -msgstr "เครื่องกำเนิดไฟฟ้าติดผนัง" +msgstr "ตัวสร้างผนัง" msgid "Walls and surfaces" msgstr "ผนังและพื้นผิว" @@ -10477,7 +10609,7 @@ msgid "G-code output" msgstr "เอาต์พุตรหัส G" msgid "Change extrusion role G-code" -msgstr "เปลี่ยนบทบาทการอัดขึ้นรูป G-code" +msgstr "เปลี่ยนประเภทการพิมพ์ G-code" msgid "Post-processing Scripts" msgstr "สคริปต์หลังการประมวลผล" @@ -10513,6 +10645,9 @@ msgstr "พบคีย์เวิร์ดที่สงวนไว้" msgid "Setting Overrides" msgstr "การตั้งค่าการแทนที่" +msgid "Retraction when switching material" +msgstr "การร่นกลับเมื่อเปลี่ยนวัสดุ" + msgid "Basic information" msgstr "ข้อมูลพื้นฐาน" @@ -10616,7 +10751,7 @@ msgid "Filament end G-code" msgstr "G-code สิ้นสุดของเส้นพลาสติก" msgid "Wipe tower parameters" -msgstr "พารามิเตอร์ทาวเวอร์เช็ดหัวฉีด" +msgstr "พารามิเตอร์ Wipe Tower" msgid "Multi Filament" msgstr "เส้นพลาสติกแบบหลากหลาย" @@ -10642,6 +10777,12 @@ msgstr "โปรไฟล์กระบวนการที่เข้าก msgid "Printable space" msgstr "พื้นที่ที่สามารถพิมพ์ได้" +msgid "Printer Agent" +msgstr "ตัวแทนเครื่องพิมพ์" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "เลือกการใช้งานตัวแทนเครือข่ายสำหรับการสื่อสารของเครื่องพิมพ์ ตัวแทนที่มีอยู่จะได้รับการลงทะเบียนเมื่อเริ่มต้น" + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10753,7 +10894,7 @@ msgid "Nozzle diameter" msgstr "เส้นผ่านศูนย์กลางหัวฉีด" msgid "Wipe tower" -msgstr "ทาวเวอร์เช็ดหัวฉีด" +msgstr "Wipe Tower" msgid "Single extruder multi-material parameters" msgstr "พารามิเตอร์วัสดุหลายชุดดันเส้นเดี่ยว" @@ -10767,9 +10908,6 @@ msgstr "การจำกัดความสูงของเลเยอร msgid "Z-Hop" msgstr "ยกแกน Z" -msgid "Retraction when switching material" -msgstr "การร่นกลับเมื่อเปลี่ยนวัสดุ" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -10780,7 +10918,7 @@ msgstr "" "ต้องการตั้งค่าเป็น 100% เพื่อเปิดใช้งาน Firmware Retraction หรือไม่?" msgid "Firmware Retraction" -msgstr "การเพิกถอนเฟิร์มแวร์" +msgstr "การดึงกลับด้วยเฟิร์มแวร์ (Firmware Retraction)" msgid "Switching to a printer with different extruder types or numbers will discard or reset changes to extruder or multi-nozzle-related parameters." msgstr "การเปลี่ยนไปใช้เครื่องพิมพ์ที่มีประเภทหรือหมายเลขชุดดันเส้นที่แตกต่างกันจะยกเลิกหรือรีเซ็ตการเปลี่ยนแปลงในชุดดันเส้นหรือพารามิเตอร์ที่เกี่ยวข้องกับหัวฉีดหลายตัว" @@ -10882,12 +11020,12 @@ msgid "No modifications need to be copied." msgstr "ไม่มีการแก้ไขที่ต้องคัดลอก" # AI Translated -msgid "Copy paramters" +msgid "Copy parameters" msgstr "คัดลอกพารามิเตอร์" # AI Translated #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "แก้ไขพารามิเตอร์ของ %s" # AI Translated @@ -11403,27 +11541,6 @@ msgstr "ปริมาณการไล่เส้นชิ่งสำหร msgid "Please choose the filament colour" msgstr "กรุณาเลือกสีเส้นพลาสติก" -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "Native Wayland liveview ต้องใช้ GStreamer GTK video sink โปรดติดตั้งปลั๊กอิน gtksink สำหรับ GStreamer จากนั้นรีสตาร์ท OrcaSlicer" - -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "ไม่สามารถเริ่มต้น sink วิดีโอ Wayland GStreamer ดั้งเดิมได้ โปรดตรวจสอบการติดตั้งปลั๊กอิน GStreamer GTK ของคุณ" - -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "งานนี้ต้องใช้ Windows Media Player! คุณต้องการเปิดใช้งาน 'Windows Media Player' สำหรับระบบปฏิบัติการของคุณหรือไม่?" - -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "BambuSource ยังไม่ได้รับการลงทะเบียนอย่างถูกต้องสำหรับการเล่นสื่อ! กดใช่เพื่อลงทะเบียนใหม่ คุณจะได้รับการเลื่อนตำแหน่งสองครั้ง" - -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "ไม่มีส่วนประกอบ BambuSource ที่ลงทะเบียนสำหรับการเล่นสื่อ! โปรดติดตั้ง OrcaSlicer ใหม่หรือขอความช่วยเหลือจากชุมชน" - -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "การใช้ BambuSource จากการติดตั้งอื่น การเล่นวิดีโออาจทำงานไม่ถูกต้อง! กดใช่เพื่อแก้ไข" - -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "ระบบของคุณไม่มีตัวแปลงสัญญาณ H.264 สำหรับ GStreamer ซึ่งจำเป็นในการเล่นวิดีโอ (ลองติดตั้งแพ็คเกจ gstreamer1.0-plugins-bad หรือ gstreamer1.0-libav จากนั้นรีสตาร์ท Orca Slicer หรือไม่)" - msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "ตัวแทนระบบคลาวด์ไม่พร้อมใช้งาน โปรดรีสตาร์ท OrcaSlicer แล้วลองอีกครั้ง" @@ -11596,7 +11713,7 @@ msgid "Gizmo mesh boolean" msgstr "Gizmo mesh บูลีน" msgid "Gizmo FDM paint-on fuzzy skin" -msgstr "Gizmo FDM เพ้นท์บนผิวที่คลุมเครือ" +msgstr "Gizmo FDM เพ้นท์ผิวฟัซซี" msgid "Gizmo SLA support points" msgstr "จุดส่วนรองรับ Gizmo SLA" @@ -11614,7 +11731,7 @@ msgid "Gizmo assemble" msgstr "กิสโมประกอบ" msgid "Gizmo brim ears" -msgstr "กิสโม่ขอบหู" +msgstr "Gizmo หูขอบยึดชิ้นงาน (Brim Ears)" msgid "Zoom in" msgstr "ซูมเข้า" @@ -11936,10 +12053,10 @@ msgid "Parts of the object at these heights may be too thin or the object may ha msgstr "บางส่วนของวัตถุที่ความสูงเหล่านี้อาจบางเกินไป หรือวัตถุอาจมี mesh ผิดปกติ" msgid "Process change extrusion role G-code" -msgstr "กระบวนการเปลี่ยนบทบาทการอัดขึ้นรูป G-code" +msgstr "กระบวนการเปลี่ยนประเภทการพิมพ์ G-code" msgid "Filament change extrusion role G-code" -msgstr "เส้นพลาสติกเปลี่ยนบทบาทการอัดขึ้นรูป G-code" +msgstr "เส้นพลาสติกเปลี่ยนประเภทการพิมพ์ G-code" msgid "No object can be printed. It may be too small." msgstr "ไม่สามารถพิมพ์วัตถุได้ อาจจะเล็กเกินไป" @@ -12100,7 +12217,7 @@ msgid " is too close to clumping detection area, there may be collisions when pr msgstr "อยู่ใกล้พื้นที่การตรวจจับการจับตัวกันมากเกินไป อาจเกิดการชนกันเมื่อพิมพ์" msgid "Prime Tower" -msgstr "ทาวเวอร์ไล่เส้น" +msgstr "Prime Tower" msgid " is too close to others, and collisions may be caused.\n" msgstr "อยู่ใกล้ผู้อื่นมากเกินไปและอาจเกิดการชนได้\n" @@ -12111,6 +12228,10 @@ msgstr "อยู่ใกล้เขตหวงห้ามมากเกิ msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr "อยู่ใกล้พื้นที่การตรวจจับการจับตัวกันมากเกินไป และจะเกิดการชนกัน\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "อยู่นอกพื้นที่การพิมพ์บางส่วน จึงไม่สามารถพิมพ์ได้\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "อุณหภูมิหัวฉีดที่เลือกเข้ากันไม่ได้ อุณหภูมิหัวฉีดของเส้นพลาสติกแต่ละเส้นต้องอยู่ในช่วงอุณหภูมิหัวฉีดที่แนะนำของเส้นพลาสติกอื่นๆ มิฉะนั้นอาจเกิดการอุดตันของหัวฉีดหรือเครื่องพิมพ์เสียหายได้" @@ -12123,6 +12244,10 @@ msgstr "หากคุณยังต้องการพิมพ์ คุ msgid "No extrusions under current settings." msgstr "ไม่มีการอัดขึ้นรูปภายใต้การตั้งค่าปัจจุบัน" +# AI Translated +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "มีการใช้เส้นพลาสติกผสมแบบเกรเดียนต์ แต่ 'ซับเลเยอร์สีผสม' ถูกปิดใช้งาน เกรเดียนต์จะไม่ถูกพิมพ์" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "ไม่รองรับโหมดไทม์แลปส์แบบราบรื่นเมื่อเปิดใช้งานลำดับ \"ตามวัตถุ\"" @@ -12130,10 +12255,10 @@ msgid "Clumping detection is not supported when \"by object\" sequence is enable msgstr "ไม่รองรับการตรวจจับการจับกันเป็นก้อนเมื่อเปิดใช้งานลำดับ \"ตามวัตถุ\"" msgid "Enabling both precise Z height and the prime tower may cause slicing errors." -msgstr "การเปิดใช้งานทั้งความสูง Z ที่แม่นยำและหอคอยหลักอาจทำให้เกิดข้อผิดพลาดในการแบ่งส่วน" +msgstr "การเปิดใช้งานทั้งความสูง Z ที่แม่นยำและ Prime Tower อาจทำให้เกิดข้อผิดพลาดในการแบ่งส่วน" msgid "A prime tower is required for clumping detection; otherwise, there may be flaws on the model." -msgstr "จำเป็นต้องใช้หอคอยหลักในการตรวจจับการจับกันเป็นก้อน มิฉะนั้นอาจมีข้อบกพร่องในแบบจำลอง" +msgstr "จำเป็นต้องใช้ Prime Tower ในการตรวจจับการจับกันเป็นก้อน มิฉะนั้นอาจมีข้อบกพร่องในแบบจำลอง" msgid "Please select \"By object\" print sequence to print multiple objects in spiral vase mode." msgstr "โปรดเลือกลำดับการพิมพ์ \"ตามวัตถุ\" เพื่อพิมพ์วัตถุหลายชิ้นในโหมดแจกันเกลียว" @@ -12143,15 +12268,15 @@ msgstr "โหมดแจกันเกลียวจะไม่ทำงา #, boost-format msgid "While the object %1% itself fits the build volume, it exceeds the maximum build volume height because of material shrinkage compensation." -msgstr "แม้ว่าวัตถุ %1% จะพอดีกับปริมาตรการสร้าง แต่วัตถุนั้นเกินความสูงของปริมาตรการสร้างสูงสุดเนื่องจากการชดเชยการหดตัวของวัสดุ" +msgstr "แม้ว่าวัตถุ %1% จะพอดีกับปริมาตรการพิมพ์ แต่วัตถุนั้นเกินความสูงของปริมาตรการพิมพ์สูงสุดเนื่องจากการชดเชยการหดตัวของวัสดุ" #, boost-format msgid "The object %1% exceeds the maximum build volume height." -msgstr "วัตถุ %1% เกินความสูงของปริมาตรบิลด์สูงสุด" +msgstr "วัตถุ %1% เกินความสูงของปริมาตรการพิมพ์สูงสุด" #, boost-format msgid "While the object %1% itself fits the build volume, its last layer exceeds the maximum build volume height." -msgstr "แม้ว่าออบเจ็กต์ %1% จะพอดีกับปริมาณการสร้าง แต่เลเยอร์สุดท้ายก็เกินความสูงของปริมาตรการสร้างสูงสุด" +msgstr "แม้ว่าวัตถุ %1% จะพอดีกับปริมาตรการพิมพ์ แต่วัตถุนั้นเกินความสูงของปริมาตรการพิมพ์สูงสุด" msgid "You might want to reduce the size of your model or change current print settings and retry." msgstr "คุณอาจต้องการลดขนาดแบบจำลองของคุณหรือเปลี่ยนการตั้งค่าการพิมพ์ปัจจุบันแล้วลองอีกครั้ง" @@ -12159,41 +12284,45 @@ msgstr "คุณอาจต้องการลดขนาดแบบจำ msgid "Variable layer height is not supported with Organic supports." msgstr "ไม่รองรับความสูงของเลเยอร์ที่แปรผันได้ด้วยการรองรับแบบออร์แกนิก" +# AI Translated +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "เส้นพลาสติกของ Wipe Tower ไม่สามารถเป็นเส้นพลาสติกผสมได้" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." -msgstr "เส้นผ่านศูนย์กลางของหัวฉีดที่แตกต่างกันและเส้นผ่านศูนย์กลางของเส้นพลาสติกที่แตกต่างกันอาจทำงานได้ไม่ดีนักเมื่อเปิดใช้งานไพรม์ทาวเวอร์ ยังเป็นการทดลองอยู่มาก ดังนั้นโปรดดำเนินการด้วยความระมัดระวัง" +msgstr "เส้นผ่านศูนย์กลางของหัวฉีดที่แตกต่างกันและเส้นผ่านศูนย์กลางของเส้นพลาสติกที่แตกต่างกันอาจทำงานได้ไม่ดีนักเมื่อเปิดใช้งาน Prime Tower ยังเป็นการทดลองอยู่มาก ดังนั้นโปรดดำเนินการด้วยความระมัดระวัง" msgid "The Wipe Tower is currently only supported with the relative extruder addressing (use_relative_e_distances=1)." msgstr "ขณะนี้ Wipe Tower รองรับการกำหนดที่อยู่ของชุดดันเส้นแบบสัมพันธ์เท่านั้น (use_relative_e_distances=1)" msgid "Ooze prevention is only supported with the wipe tower when 'single_extruder_multi_material' is off." -msgstr "รองรับการป้องกันน้ำซึมด้วยหอเช็ดเมื่อปิด 'single_extruder_multi_material' เท่านั้น" +msgstr "รองรับการป้องกันน้ำซึมด้วย Wipe Tower เมื่อปิด 'single_extruder_multi_material' เท่านั้น" msgid "The prime tower is currently only supported for the Marlin, RepRap/Sprinter, RepRapFirmware and Repetier G-code flavors." -msgstr "ขณะนี้ไพรม์ทาวเวอร์รองรับเฉพาะรสชาติ Marlin, RepRap/Sprinter, RepRapFirmware และ Repetier G-code เท่านั้น" +msgstr "ขณะนี้ Prime Tower รองรับเฉพาะรสชาติ Marlin, RepRap/Sprinter, RepRapFirmware และ Repetier G-code เท่านั้น" msgid "A prime tower is not supported in “By object” print." -msgstr "ไม่รองรับไพรม์ทาวเวอร์ในการพิมพ์ \"ตามวัตถุ\"" +msgstr "ไม่รองรับ Prime Tower ในการพิมพ์ \"ตามวัตถุ\"" msgid "A prime tower is not supported when adaptive layer height is on. It requires that all objects have the same layer height." -msgstr "ไม่รองรับไพรม์ทาวเวอร์เมื่อเปิดความสูงของเลเยอร์แบบปรับได้ กำหนดให้วัตถุทั้งหมดมีความสูงของชั้นเท่ากัน" +msgstr "ไม่รองรับ Prime Tower เมื่อเปิดความสูงของเลเยอร์แบบปรับได้ กำหนดให้วัตถุทั้งหมดมีความสูงของชั้นเท่ากัน" msgid "A prime tower requires any “support gap” to be a multiple of layer height." -msgstr "ไพรม์ทาวเวอร์ต้องการให้ “support gap” เป็นจำนวนเท่าของความสูงชั้น" +msgstr "Prime Tower ต้องการให้ “support gap” เป็นจำนวนเท่าของความสูงชั้น" msgid "A prime tower requires that all objects have the same layer height." -msgstr "ไพรม์ทาวเวอร์ต้องการให้วัตถุทั้งหมดมีความสูงชั้นเท่ากัน" +msgstr "Prime Tower ต้องการให้วัตถุทั้งหมดมีความสูงชั้นเท่ากัน" msgid "A prime tower requires that all objects are printed over the same number of raft layers." -msgstr "ไพรม์ทาวเวอร์ต้องการให้วัตถุทั้งหมดพิมพ์บนจำนวนชั้น raft เท่ากัน" +msgstr "Prime Tower ต้องการให้วัตถุทั้งหมดพิมพ์บนจำนวนชั้น raft เท่ากัน" msgid "The prime tower is only supported for multiple objects if they are printed with the same support_top_z_distance." -msgstr "ไพรม์ทาวเวอร์รองรับวัตถุหลายชิ้นเท่านั้นหากพิมพ์ด้วย support_top_z_distance เท่ากัน" +msgstr "Prime Tower รองรับวัตถุหลายชิ้นเท่านั้นหากพิมพ์ด้วย support_top_z_distance เท่ากัน" msgid "A prime tower requires that all objects are sliced with the same layer height." -msgstr "ไพรม์ทาวเวอร์ต้องการให้วัตถุทั้งหมดถูกสไลซ์ด้วยความสูงชั้นเท่ากัน" +msgstr "Prime Tower ต้องการให้วัตถุทั้งหมดถูกสไลซ์ด้วยความสูงชั้นเท่ากัน" msgid "The prime tower is only supported if all objects have the same variable layer height." -msgstr "ไพรม์ทาวเวอร์ได้รับส่วนรองรับก็ต่อเมื่อวัตถุทั้งหมดมีความสูงของเลเยอร์ที่แปรผันเท่ากัน" +msgstr "Prime Tower ได้รับส่วนรองรับก็ต่อเมื่อวัตถุทั้งหมดมีความสูงของเลเยอร์ที่แปรผันเท่ากัน" msgid "One or more object were assigned an extruder that the printer does not have." msgstr "วัตถุอย่างน้อยหนึ่งชิ้นถูกกำหนดให้เป็นชุดดันเส้นที่เครื่องพิมพ์ไม่มี" @@ -12208,7 +12337,7 @@ msgid "Printing with multiple extruders of differing nozzle diameters. If suppor msgstr "การพิมพ์ด้วยชุดดันเส้นหลายเครื่องที่มีเส้นผ่านศูนย์กลางหัวฉีดต่างกัน หากจะพิมพ์ส่วนรองรับด้วยฟิลาเมนต์ปัจจุบัน (support_filament == 0 หรือ support_interface_filament == 0) หัวฉีดทั้งหมดจะต้องมีเส้นผ่านศูนย์กลางเท่ากัน" msgid "A prime tower requires that support has the same layer height as the object." -msgstr "ไพรม์ทาวเวอร์ต้องการให้ส่วนรองรับมีความสูงชั้นเท่ากับวัตถุ" +msgstr "Prime Tower ต้องการให้ส่วนรองรับมีความสูงชั้นเท่ากับวัตถุ" msgid "For Organic supports, two walls are supported only with the Hollow/Default base pattern." msgstr "สำหรับการรองรับแบบออร์แกนิก ผนังทั้งสองได้รับการรองรับด้วยรูปแบบฐานกลวง/ค่าเริ่มต้นเท่านั้น" @@ -12426,9 +12555,6 @@ msgstr "ใช้ 3MF แทน G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "เปิดใช้งานหากเครื่องพิมพ์รับไฟล์ 3MF เป็นงานพิมพ์ เมื่อเปิดใช้งาน OrcaSlicer จะส่งไฟล์ที่สไลซ์แล้วเป็น .gcode.3mf แทนไฟล์ .gcode ธรรมดา" -msgid "Printer Agent" -msgstr "ตัวแทนเครื่องพิมพ์" - msgid "Select the network agent implementation for printer communication." msgstr "เลือกการใช้งานตัวแทนเครือข่ายสำหรับการสื่อสารของเครื่องพิมพ์" @@ -12511,7 +12637,7 @@ msgstr "มม. หรือ %" msgid "Other layers" msgstr "ชั้นอื่นๆ" -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." msgstr "อุณหภูมิฐานพิมพ์สำหรับชั้นต่างๆ ยกเว้นอุณหภูมิเริ่มต้น ค่า 0 หมายความว่าเส้นพลาสติกไม่รองรับการพิมพ์บน Cool Plate SuperTack" msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." @@ -12845,9 +12971,9 @@ msgid "" "\n" "For the first layer, the actual flow ratio for each path role (does not affect brims and skirts) will be multiplied by this value." msgstr "" -"ปัจจัยนี้ส่งผลต่อปริมาณวัสดุในชั้นแรกสำหรับบทบาทเส้นทางการอัดขึ้นรูปที่แสดงอยู่ในส่วนนี้\n" +"ปัจจัยนี้ส่งผลต่อปริมาณวัสดุในชั้นแรกสำหรับประเภทการพิมพ์ที่แสดงอยู่ในส่วนนี้\n" "\n" -"สำหรับชั้นแรก อัตราการไหลตามจริงสำหรับแต่ละบทบาทของเส้นทาง (ไม่ส่งผลต่อขอบยึดชิ้นงานและเส้นล้อมชิ้นงาน) จะถูกคูณด้วยค่านี้" +"สำหรับชั้นแรก อัตราการไหลตามจริงสำหรับแต่ละประเภทการพิมพ์ (ไม่ส่งผลต่อขอบยึดชิ้นงานและเส้นล้อมชิ้นงาน) จะถูกคูณด้วยค่านี้" msgid "Outer wall flow ratio" msgstr "อัตราส่วนการไหลของผนังด้านนอก" @@ -13103,9 +13229,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "ความเร็วของสะพานภายใน หากค่าแสดงเป็นเปอร์เซ็นต์ ค่าดังกล่าวจะถูกคำนวณตาม bridge_speed ค่าเริ่มต้นคือ 150%" -msgid "Brim width" -msgstr "ความกว้าง ขอบยึดชิ้นงาน" - msgid "This is the distance from the model to the outermost brim line." msgstr "ระยะห่างจากแบบจำลองถึงเส้นขอบยึดชิ้นงานด้านนอกสุด" @@ -13138,7 +13261,7 @@ msgstr "" "หมายเหตุ: ค่าผลลัพธ์จะไม่ได้รับผลกระทบจากอัตราส่วนการไหลของชั้นแรก" msgid "Brim follows compensated outline" -msgstr "ขอบยึดชิ้นงาน ปฏิบัติตามโครงร่างที่ได้รับการชดเชย" +msgstr "Brim ตามแนวที่ชดเชยแล้ว" msgid "" "When enabled, the brim is aligned with the first-layer perimeter geometry after Elephant Foot Compensation is applied.\n" @@ -13161,10 +13284,10 @@ msgid "Brim ears" msgstr "หู ขอบยึดชิ้นงาน" msgid "Only draw brim over the sharp edges of the model." -msgstr "วาดขอบยึดชิ้นงานไว้เหนือขอบคมของนางแบบเท่านั้น" +msgstr "วาดขอบยึดชิ้นงานไว้เหนือขอบคมของชิ้นงานเท่านั้น" msgid "Brim ear max angle" -msgstr "มุมสูงสุดของหูขอบยึดชิ้นงานนก" +msgstr "มุมสูงสุดของหูขอบยึดชิ้นงาน (Brim Ears)" msgid "" "Maximum angle to let a brim ear appear.\n" @@ -13185,6 +13308,14 @@ msgstr "" "รูปทรงจะถูกทำลายก่อนที่จะตรวจจับมุมแหลม พารามิเตอร์นี้ระบุความยาวขั้นต่ำของการเบี่ยงเบนสำหรับการทำลาย\n" "0 เพื่อปิดการใช้งาน" +# AI Translated +msgid "Brim ears outer only" +msgstr "หูขอบยึดชิ้นงานเฉพาะด้านนอก" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "สร้างหูหนูเฉพาะบนคอนทัวร์ด้านนอกของโมเดล โดยไม่รวมรูและส่วนที่ปิดล้อม" + msgid "upward compatible machine" msgstr "เครื่องที่รองรับขึ้นไป" @@ -13753,7 +13884,7 @@ msgstr "" "อัตราการไหลของวัตถุขั้นสุดท้ายคือค่านี้คูณด้วยอัตราการไหลของเส้นพลาสติก" msgid "Enable pressure advance" -msgstr "เปิดใช้งานPressure Advance" +msgstr "เปิดใช้ Pressure Advance" msgid "Enable pressure advance, auto calibration result will be overwritten once enabled." msgstr "เปิดใช้งานการเลื่อนแรงดัน ผลลัพธ์การสอบเทียบอัตโนมัติจะถูกเขียนทับเมื่อเปิดใช้งาน" @@ -13762,7 +13893,7 @@ msgid "Pressure advance (Klipper) AKA Linear advance factor (Marlin)." msgstr "แรงดันล่วงหน้า (Pressure Advance) (Klipper) AKA Linear Advance Factor (Marlin)" msgid "Enable adaptive pressure advance (beta)" -msgstr "เปิดใช้งานการปรับPressure Advance (เบต้า)" +msgstr "เปิดใช้ Adaptive Pressure Advance (เบต้า)" #, no-c-format, no-boost-format msgid "" @@ -13780,7 +13911,7 @@ msgstr "" "เมื่อเปิดใช้งาน ค่าล่วงหน้าของแรงดันด้านบนจะถูกแทนที่ อย่างไรก็ตาม แนะนำให้ใช้ค่าเริ่มต้นที่สมเหตุสมผลด้านบนเพื่อเป็นทางเลือกและเมื่อมีการเปลี่ยนเครื่องมือ\n" msgid "Adaptive pressure advance measurements (beta)" -msgstr "การวัดล่วงหน้าด้วยแรงดันแบบปรับได้ (เบต้า)" +msgstr "ข้อมูลวัด Adaptive Pressure Advance (เบต้า)" #, no-c-format, no-boost-format msgid "" @@ -13869,6 +14000,8 @@ msgstr "เวลาเลเยอร์" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "พัดลมระบายความร้อนบางส่วนจะถูกเปิดใช้งานสำหรับเลเยอร์ที่เวลาโดยประมาณสั้นกว่าค่านี้ ความเร็วพัดลมจะถูกประมาณค่าระหว่างความเร็วพัดลมขั้นต่ำและสูงสุดตามเวลาการพิมพ์เลเยอร์" +# AI Translated +msgctxt "second" msgid "s" msgstr "วินาที" @@ -14021,7 +14154,7 @@ msgid "Loading speed" msgstr "ความเร็วกำลังโหลด" msgid "Speed used for loading the filament on the wipe tower." -msgstr "ความเร็วที่ใช้ในการโหลดเส้นพลาสติกบนไวด์ทาวเวอร์" +msgstr "ความเร็วที่ใช้ในการโหลดเส้นพลาสติกบน Wipe Tower" msgid "Loading speed at the start" msgstr "ความเร็วในการโหลดเมื่อเริ่มต้น" @@ -14033,7 +14166,7 @@ msgid "Unloading speed" msgstr "ความเร็วในการขนถ่าย" msgid "Speed used for unloading the filament on the wipe tower (does not affect initial part of unloading just after ramming)." -msgstr "ความเร็วที่ใช้ในการขนถ่ายเส้นพลาสติกบนไวด์ทาวเวอร์ (ไม่ส่งผลต่อส่วนเริ่มแรกของการขนถ่ายหลังจากการชน)" +msgstr "ความเร็วที่ใช้ในการขนถ่ายเส้นพลาสติกบน Wipe Tower (ไม่ส่งผลต่อส่วนเริ่มแรกของการขนถ่ายหลังจากการชน)" msgid "Unloading speed at the start" msgstr "ขนถ่ายความเร็วที่จุดเริ่มต้น" @@ -14075,10 +14208,10 @@ msgid "Minimal purge on wipe tower" msgstr "การล้างข้อมูลบน Wipe Tower น้อยที่สุด" msgid "After a tool change, the exact position of the newly loaded filament inside the nozzle may not be known, and the filament pressure is likely not yet stable. Before purging the print head into an infill or a sacrificial object, Orca Slicer will always prime this amount of material into the wipe tower to produce successive infill or sacrificial object extrusions reliably." -msgstr "หลังจากเปลี่ยนเครื่องมือ อาจไม่ทราบตำแหน่งที่แน่นอนของเส้นพลาสติกที่เพิ่งโหลดใหม่ภายในหัวฉีด และความดันเส้นพลาสติกก็มีแนวโน้มว่ายังไม่เสถียร ก่อนที่จะล้างหัวพิมพ์ลงในวัสดุไส้ในหรือวัตถุบูชายัญ Orca Slicer จะเตรียมวัสดุจำนวนนี้ลงในหอเช็ดเสมอเพื่อสร้างการอัดขึ้นรูปวัตถุแบบไส้ในหรือบูชายัญต่อเนื่องกันอย่างน่าเชื่อถือ" +msgstr "หลังจากเปลี่ยนเครื่องมือ อาจไม่ทราบตำแหน่งที่แน่นอนของเส้นพลาสติกที่เพิ่งโหลดใหม่ภายในหัวฉีด และความดันเส้นพลาสติกก็มีแนวโน้มว่ายังไม่เสถียร ก่อนที่จะล้างหัวพิมพ์ลงในวัสดุไส้ในหรือวัตถุบูชายัญ Orca Slicer จะเตรียมวัสดุจำนวนนี้ลงใน Wipe Tower เสมอเพื่อสร้างการอัดขึ้นรูปวัตถุแบบไส้ในหรือบูชายัญต่อเนื่องกันอย่างน่าเชื่อถือ" msgid "Wipe tower cooling" -msgstr "เช็ดทาวเวอร์คูลลิ่ง" +msgstr "Wipe Tower คูลลิ่ง" msgid "Temperature drop before entering filament tower" msgstr "อุณหภูมิลดลงก่อนเข้าหอใย" @@ -14087,19 +14220,19 @@ msgid "Interface layer pre-extrusion distance" msgstr "ระยะการอัดรีดชั้นอินเตอร์เฟซ" msgid "Pre-extrusion distance for prime tower interface layer (where different materials meet)." -msgstr "ระยะก่อนการอัดขึ้นรูปสำหรับชั้นอินเทอร์เฟซของไพรม์ทาวเวอร์ (ที่วัสดุที่แตกต่างกันมาบรรจบกัน)" +msgstr "ระยะก่อนการอัดขึ้นรูปสำหรับชั้นอินเทอร์เฟซของ Prime Tower (ที่วัสดุที่แตกต่างกันมาบรรจบกัน)" msgid "Interface layer pre-extrusion length" msgstr "ความยาวชั้นอินเตอร์เฟซก่อนการอัดขึ้นรูป" msgid "Pre-extrusion length for prime tower interface layer (where different materials meet)." -msgstr "ความยาวก่อนการอัดขึ้นรูปสำหรับชั้นอินเทอร์เฟซของไพรม์ทาวเวอร์ (ที่วัสดุที่แตกต่างกันมาบรรจบกัน)" +msgstr "ความยาวก่อนการอัดขึ้นรูปสำหรับชั้นอินเทอร์เฟซของ Prime Tower (ที่วัสดุที่แตกต่างกันมาบรรจบกัน)" msgid "Tower ironing area" msgstr "พื้นที่รีดผิวแบบทาวเวอร์" msgid "Ironing area for prime tower interface layer (where different materials meet)." -msgstr "พื้นที่รีดผิวสำหรับชั้นอินเทอร์เฟซของไพร์มทาวเวอร์ (บริเวณที่วัสดุต่างกันมาบรรจบกัน)" +msgstr "พื้นที่รีดผิวสำหรับชั้นอินเทอร์เฟซของ Prime Tower (บริเวณที่วัสดุต่างกันมาบรรจบกัน)" msgid "mm²" msgstr "มม.²" @@ -14108,13 +14241,13 @@ msgid "Interface layer purge length" msgstr "ความยาวการล้างเลเยอร์อินเทอร์เฟซ" msgid "Purge length for prime tower interface layer (where different materials meet)." -msgstr "ความยาวในการไล่ล้างสำหรับชั้นอินเทอร์เฟซของไพรม์ทาวเวอร์ (เมื่อวัสดุต่างกันมาบรรจบกัน)" +msgstr "ความยาวในการไล่ล้างสำหรับชั้นอินเทอร์เฟซของ Prime Tower (เมื่อวัสดุต่างกันมาบรรจบกัน)" msgid "Interface layer print temperature" msgstr "อุณหภูมิการพิมพ์เลเยอร์อินเทอร์เฟซ" msgid "Print temperature for prime tower interface layer (where different materials meet). If set to -1, use max recommended nozzle temperature." -msgstr "อุณหภูมิการพิมพ์สำหรับชั้นอินเทอร์เฟซของไพรม์ทาวเวอร์ (เมื่อวัสดุต่างกันมาบรรจบกัน) หากตั้งค่าเป็น -1 ให้ใช้อุณหภูมิหัวฉีดสูงสุดที่แนะนำ" +msgstr "อุณหภูมิการพิมพ์สำหรับชั้นอินเทอร์เฟซของ Prime Tower (เมื่อวัสดุต่างกันมาบรรจบกัน) หากตั้งค่าเป็น -1 ให้ใช้อุณหภูมิหัวฉีดสูงสุดที่แนะนำ" msgid "Speed of the last cooling move" msgstr "ความเร็วของการทำความเย็นครั้งล่าสุด" @@ -14132,7 +14265,7 @@ msgid "Enable ramming for multi-tool setups" msgstr "เปิดใช้งานการกระแทกสำหรับการตั้งค่าหลายเครื่องมือ" msgid "Perform ramming when using multi-tool printer (i.e. when the 'Single Extruder Multimaterial' in Printer Settings is unchecked). When checked, a small amount of filament is rapidly extruded on the wipe tower just before the tool change. This option is only used when the wipe tower is enabled." -msgstr "ทำการกระแทกเมื่อใช้เครื่องพิมพ์แบบหลายเครื่องมือ (เช่น เมื่อไม่ได้เลือก 'Single ชุดดันเส้น Multimaterial' ในการตั้งค่าเครื่องพิมพ์) เมื่อตรวจสอบแล้ว เส้นพลาสติกจำนวนเล็กน้อยจะถูกอัดรีดอย่างรวดเร็วบนไวด์ทาวเวอร์ก่อนที่จะเปลี่ยนเครื่องมือ ตัวเลือกนี้ใช้เฉพาะเมื่อเปิดใช้งาน Wipe Tower เท่านั้น" +msgstr "ทำการกระแทกเมื่อใช้เครื่องพิมพ์แบบหลายเครื่องมือ (เช่น เมื่อไม่ได้เลือก 'Single ชุดดันเส้น Multimaterial' ในการตั้งค่าเครื่องพิมพ์) เมื่อตรวจสอบแล้ว เส้นพลาสติกจำนวนเล็กน้อยจะถูกอัดรีดอย่างรวดเร็วบน Wipe Tower ก่อนที่จะเปลี่ยนเครื่องมือ ตัวเลือกนี้ใช้เฉพาะเมื่อเปิดใช้งาน Wipe Tower เท่านั้น" msgid "Multi-tool ramming volume" msgstr "ปริมาณการกระแทกหลายเครื่องมือ" @@ -14176,6 +14309,62 @@ msgstr "วัสดุส่วนรองรับ" msgid "Support material is commonly used to print supports and support interfaces." msgstr "วัสดุรองรับมักใช้ในการพิมพ์ส่วนรองรับและอินเทอร์เฟซรองรับ" +# AI Translated +msgid "Is mixed filament" +msgstr "เป็นเส้นพลาสติกผสมหรือไม่" + +# AI Translated +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "กำหนดว่าช่องเส้นพลาสติกนี้เป็นเส้นพลาสติกผสมที่ประกอบด้วยเส้นพลาสติกจริงหลายเส้นหรือไม่" + +# AI Translated +msgid "Mixed filament components" +msgstr "ส่วนประกอบของเส้นพลาสติกผสม" + +# AI Translated +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "ลำดับของเส้นพลาสติกส่วนประกอบโดยเริ่มจาก 1 คั่นด้วยเครื่องหมายจุลภาค เช่น \"1,3\"" + +# AI Translated +msgid "Mixed filament sublayer ratios" +msgstr "สัดส่วนซับเลเยอร์ของเส้นพลาสติกผสม" + +# AI Translated +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "ค่าสัดส่วนที่คั่นด้วยเครื่องหมายจุลภาคและรวมกันได้ 1.0 เช่น \"0.7,0.3\"" + +# AI Translated +msgid "Mixed filament gradient" +msgstr "เกรเดียนต์ของเส้นพลาสติกผสม" + +# AI Translated +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "เปิดใช้โหมดเกรเดียนต์ในแนวแกน Z สำหรับซับเลเยอร์ของเส้นพลาสติกผสม เมื่อเปิดใช้ สัดส่วนของซับเลเยอร์จะเปลี่ยนแปลงเชิงเส้นตามเลเยอร์" + +# AI Translated +msgid "Mixed filament gradient range" +msgstr "ช่วงเกรเดียนต์ของเส้นพลาสติกผสม" + +# AI Translated +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "สัดส่วนเริ่มต้นและสิ้นสุดของส่วนประกอบแรกในโหมดเกรเดียนต์ เป็นคู่ค่าที่คั่นด้วยเครื่องหมายจุลภาค เช่น \"0.10,0.90\" หมายถึง 10% ถึง 90%" + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "เส้นโค้งเกรเดียนต์ของเส้นพลาสติกผสม" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "เส้นโค้งกำหนดเองสไตล์ Photoshop (ไม่บังคับ) ที่แมปความคืบหน้าในแกน Z กับสัดส่วนของส่วนประกอบแรก เข้ารหัสเป็นจุดควบคุมที่คั่นด้วยเครื่องหมายขีดตั้ง ในรูปแบบ \"x,y\" (รูปแบบเดิม) หรือ \"x,y,m_in,m_out\" เมื่อจำเป็นต้องกำหนดค่าแทนเจนต์เอง (ค่าว่างหรือ \"nan\" หมายถึงใช้ค่าเริ่มต้นของ PCHIP) x อยู่ในช่วง [0,1] ส่วน y จะถูกจำกัดให้อยู่ในช่วงสัดส่วนที่ตั้งค่าไว้ เช่น \"0,0.15|0.5,0.50|1,0.85\" หากเว้นว่างไว้ จะใช้ gradient_range แบบเชิงเส้นแทน" + +# AI Translated +msgid "Mixed filament per-part gradient" +msgstr "เกรเดียนต์แยกตามชิ้นส่วนของเส้นพลาสติกผสม" + +# AI Translated +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "เมื่อเปิดใช้โหมดเกรเดียนต์ จะใช้เกรเดียนต์กับแต่ละชิ้นส่วนของการประกอบแยกกัน แทนที่จะถือว่าการประกอบทั้งหมดเป็นช่วง Z เดียว" + msgid "Filament printable" msgstr "พิมพ์เส้นพลาสติกได้" @@ -14351,6 +14540,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "ไจรอยด์" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "ค่าความเรียบของไส้ในแบบโปร่ง" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "ควบคุมระดับความมนของมุมไส้ในแบบโปร่ง ค่า 0% จะคงเส้นทางเดิมที่เป็นมุมแหลม ส่วน 100% จะสร้างส่วนโค้งที่ใหญ่ที่สุดเท่าที่เป็นไปได้ระหว่างเส้นไส้ในที่อยู่ติดกัน" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "ความเร่งของไส้ในพื้นผิวด้านบน การใช้ค่าที่ต่ำกว่าอาจปรับปรุงคุณภาพพื้นผิวด้านบนได้" @@ -14517,13 +14714,13 @@ msgid "Filament-specific override for ironing flow. This allows you to customize msgstr "การแทนที่เส้นพลาสติกเฉพาะสำหรับกระแสการรีดผิว ซึ่งช่วยให้คุณปรับแต่งกระแสการรีดผิวสำหรับเส้นพลาสติกแต่ละประเภทได้ ค่าที่สูงเกินไปส่งผลให้เกิดการอัดขึ้นรูปมากเกินไปบนพื้นผิว" msgid "Ironing line spacing" -msgstr "ระยะห่างระหว่างสายรีดผิว" +msgstr "ระยะห่างระหว่างเส้นรีดผิว" msgid "Filament-specific override for ironing line spacing. This allows you to customize the spacing between ironing lines for each filament type." msgstr "การแทนที่เส้นพลาสติกเฉพาะสำหรับระยะห่างระหว่างรีดผิว ซึ่งช่วยให้คุณปรับแต่งระยะห่างระหว่างเส้นรีดผิวสำหรับเส้นพลาสติกแต่ละประเภทได้" msgid "Ironing inset" -msgstr "อุปกรณ์รีดผิว" +msgstr "ระยะเว้นขอบการรีดผิว" msgid "Filament-specific override for ironing inset. This allows you to customize the distance to keep from the edges when ironing for each filament type." msgstr "การแทนที่เส้นพลาสติกเฉพาะสำหรับส่วนเสริมการรีดผิว ซึ่งช่วยให้คุณปรับแต่งระยะห่างจากขอบเมื่อรีดผิวสำหรับเส้นพลาสติกแต่ละประเภทได้" @@ -14565,10 +14762,10 @@ msgid "The average distance between the random points introduced on each line se msgstr "ระยะห่างเฉลี่ยระหว่างจุดสุ่มที่แนะนำในแต่ละส่วนของเส้น" msgid "Apply fuzzy skin to first layer" -msgstr "ทาผิวที่คลุมเครือเป็นชั้นแรก" +msgstr "ใช้ Fuzzy Skin กับชั้นแรก" msgid "Whether to apply fuzzy skin on the first layer." -msgstr "ไม่ว่าจะทาผิวฟุ้งๆในชั้นแรกหรือไม่" +msgstr "กำหนดว่าจะใช้ Fuzzy Skin กับชั้นแรกหรือไม่" msgid "Fuzzy skin generator mode" msgstr "โหมดสร้างผิวฟัซซี" @@ -14599,7 +14796,7 @@ msgid "Combined" msgstr "รวม" msgid "Fuzzy skin noise type" -msgstr "ประเภทเสียงผิวเลือน" +msgstr "ประเภท Noise ของ Fuzzy Skin" msgid "" "Noise type to use for fuzzy skin generation:\n" @@ -14610,12 +14807,12 @@ msgid "" "Voronoi: Divides the surface into voronoi cells, and displaces each one by a random amount. Creates a patchwork texture.\n" "Ripple: Uniform ripple pattern that ripples left and right of the original path. Repeating pattern, woven appearance." msgstr "" -"ประเภทเสียงรบกวนที่ใช้สำหรับการสร้างผิวที่คลุมเครือ:\n" +"ประเภท Noise ที่ใช้สำหรับการสร้างผิวฟัซซี:\n" "คลาสสิก: เสียงสุ่มเครื่องแบบคลาสสิก\n" "Perlin: เสียง Perlin ซึ่งให้เนื้อสัมผัสที่สม่ำเสมอยิ่งขึ้น\n" "Billow: คล้ายกับเสียงเพอร์ลิน แต่เป็นกลุ่มมากกว่า\n" "Ridged Multifractal: สัญญาณรบกวนที่คมชัดพร้อมคุณสมบัติหยัก สร้างพื้นผิวเหมือนหินอ่อน\n" -"โวโรนอย: แบ่งพื้นผิวออกเป็นเซลล์โวโรนอย และแทนที่แต่ละเซลล์ด้วยจำนวนสุ่ม สร้างพื้นผิวแบบเย็บปะติดปะต่อกัน\n" +"Voronoi: แบ่งพื้นผิวออกเป็นเซลล์ Voronoi และแทนที่แต่ละเซลล์ด้วยจำนวนสุ่ม สร้างพื้นผิวแบบเย็บปะติดปะต่อกัน\n" "ระลอกคลื่น: รูปแบบระลอกคลื่นสม่ำเสมอที่กระเพื่อมไปทางซ้ายและขวาของเส้นทางเดิม ลายซ้ำ ลักษณะการทอ." msgid "Classic" @@ -14631,7 +14828,7 @@ msgid "Ridged Multifractal" msgstr "Multifractal แบบสัน" msgid "Voronoi" -msgstr "โวโรน้อย" +msgstr "Voronoi" msgid "Ripple" msgstr "ระลอกคลื่น" @@ -14643,13 +14840,13 @@ msgid "The base size of the coherent noise features, in mm. Higher values will r msgstr "ขนาดฐานของคุณสมบัติเสียงที่สอดคล้องกัน หน่วยเป็น มม. ค่าที่สูงกว่าจะส่งผลให้มีคุณลักษณะที่ใหญ่ขึ้น" msgid "Fuzzy Skin Noise Octaves" -msgstr "อ็อกเทฟเสียงผิวฟัซซี" +msgstr "จำนวน Octave ของ Noise ใน Fuzzy Skin" msgid "The number of octaves of coherent noise to use. Higher values increase the detail of the noise, but also increase computation time." msgstr "จำนวนอ็อกเทฟของสัญญาณรบกวนที่สอดคล้องกันที่จะใช้ ค่าที่สูงกว่าจะเพิ่มรายละเอียดของสัญญาณรบกวน แต่ยังเพิ่มเวลาในการคำนวณด้วย" msgid "Fuzzy skin noise persistence" -msgstr "ความคงอยู่ของเสียงผิวเลือน" +msgstr "ค่า Persistence ของ Noise ใน Fuzzy Skin" msgid "The decay rate for higher octaves of the coherent noise. Lower values will result in smoother noise." msgstr "อัตราการสลายตัวของอ็อกเทฟที่สูงขึ้นของสัญญาณรบกวนที่สอดคล้องกัน ค่าที่ต่ำกว่าจะส่งผลให้มีสัญญาณรบกวนที่นุ่มนวลขึ้น" @@ -14893,6 +15090,14 @@ msgstr "เครื่องพิมพ์ G-code ชนิดใดที่ msgid "Klipper" msgstr "คลิปเปอร์" +# AI Translated +msgid "Skip G-code config block" +msgstr "ข้ามบล็อกการตั้งค่าใน G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "ไม่เขียน CONFIG_BLOCK (คู่คีย์/ค่าของการตั้งค่าโปรแกรมสไลซ์) ลงในไฟล์ G-code ซึ่งอาจช่วยได้กับเครื่องพิมพ์ที่เฟิร์มแวร์ขัดข้องเมื่ออ่านบรรทัดคอมเมนต์เหล่านี้ (เช่น Anycubic go-klipper) หมายเหตุ: ไฟล์ G-code จะไม่มีการตั้งค่าโปรแกรมสไลซ์อีกต่อไป ดังนั้นการนำเข้ากลับมาใน OrcaSlicer จะไม่คืนค่าการตั้งค่า" + msgid "Pellet Modded Printer" msgstr "เครื่องพิมพ์ Modded เม็ด" @@ -15414,8 +15619,9 @@ msgid "The allowed maximum output force of Y axis" msgstr "แรงขับออกสูงสุดที่อนุญาตของแกน Y" # AI Translated +msgctxt "Newton" msgid "N" -msgstr "N" +msgstr "นิวตัน" # AI Translated msgid "Bed mass of the Y axis" @@ -15426,8 +15632,9 @@ msgid "The machine bed mass load of Y axis" msgstr "ภาระมวลฐานของเครื่องบนแกน Y" # AI Translated +msgctxt "gram" msgid "g" -msgstr "g" +msgstr "กรัม" # AI Translated msgid "The allowed max printed mass" @@ -15737,10 +15944,10 @@ msgid "The start and end points which are from the cutter area to the excess chu msgstr "จุดเริ่มต้นและจุดสิ้นสุดตั้งแต่บริเวณเครื่องตัดถึงถังขยะ" msgid "Reduce infill retraction" -msgstr "ลดการหดตัวของ ไส้ใน" +msgstr "ลดการดึงกลับในไส้ใน" -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." -msgstr "อย่าถอยกลับเมื่อการเดินทางอยู่ภายในพื้นที่ที่ไส้ในเข้าไปทั้งหมด นั่นหมายความว่าไม่สามารถมองเห็นการรั่วไหลได้ วิธีนี้จะช่วยลดเวลาในการดึงกลับสำหรับโมเดลที่ซับซ้อนและประหยัดเวลาในการพิมพ์ แต่จะทำให้การแบ่งส่วนและการสร้าง G-code ช้าลง โปรดทราบว่า z-hop จะไม่ดำเนินการในพื้นที่ที่มีการข้ามการถอนกลับ" +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." +msgstr "อย่าถอยกลับเมื่อการเดินทางอยู่ภายในพื้นที่ที่ไส้ในเข้าไปทั้งหมด นั่นหมายความว่าไม่สามารถมองเห็นการรั่วไหลได้ วิธีนี้จะช่วยลดเวลาในการดึงกลับสำหรับโมเดลที่ซับซ้อนและประหยัดเวลาในการพิมพ์ แต่จะทำให้การแบ่งส่วนและการสร้าง G-code ช้าลง โปรดทราบว่า Z-hop จะไม่ดำเนินการในพื้นที่ที่มีการข้ามการถอนกลับ" msgid "This option will drop the temperature of the inactive extruders to prevent oozing." msgstr "ตัวเลือกนี้จะทำให้อุณหภูมิของชุดดันเส้นที่ไม่ใช้งานลดลงเพื่อป้องกันการไหลซึม" @@ -15825,10 +16032,10 @@ msgid "If you want to process the output G-code through custom scripts, just lis msgstr "หากคุณต้องการประมวลผลเอาต์พุต G-code ผ่านสคริปต์ที่กำหนดเอง เพียงระบุเส้นทางสัมบูรณ์ของสคริปต์ไว้ที่นี่ แยกสคริปต์หลายรายการด้วยเครื่องหมายอัฒภาค สคริปต์จะถูกส่งผ่านเส้นทางสัมบูรณ์ไปยังไฟล์ G-code เป็นอาร์กิวเมนต์แรก และสคริปต์เหล่านี้สามารถเข้าถึงการตั้งค่าการกำหนดค่า Orca Slicer ได้โดยการอ่านตัวแปรสภาพแวดล้อม" msgid "Change extrusion role G-code (process)" -msgstr "เปลี่ยนบทบาทการอัดขึ้นรูป G-code (กระบวนการ)" +msgstr "เปลี่ยนประเภทการพิมพ์ G-code (กระบวนการ)" msgid "This G-code is inserted when the extrusion role is changed. It runs after the machine and filament extrusion role G-code." -msgstr "G-code นี้จะถูกแทรกเมื่อบทบาทการอัดขึ้นรูปมีการเปลี่ยนแปลง มันทำงานหลังจากบทบาทการอัดขึ้นรูปของเครื่องจักรและการอัดขึ้นรูปเส้นพลาสติก G-code" +msgstr "G-code นี้จะถูกแทรกเมื่อประเภทการพิมพ์มีการเปลี่ยนแปลง มันทำงานหลังจากประเภทการพิมพ์ของเครื่องจักรและเส้นพลาสติก G-code" # AI Translated msgid "Plugins Used" @@ -15897,7 +16104,7 @@ msgid "Only trigger retraction when the travel distance is longer than this thre msgstr "ทริกเกอร์การถอนกลับเมื่อระยะการเดินทางยาวกว่าเกณฑ์นี้เท่านั้น" msgid "Retract amount before wipe" -msgstr "ถอนจำนวนก่อนเช็ด" +msgstr "สัดส่วนการดึงกลับก่อน Wipe" msgid "This is the length of fast retraction before a wipe, relative to retraction length." msgstr "ความยาวของการดึงกลับอย่างรวดเร็วก่อนเช็ด สัมพันธ์กับความยาวการดึงกลับ" @@ -15909,14 +16116,14 @@ msgstr "ปริมาณการดึงกลับหลังเช็ด # AI Translated #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" "ความยาวของการดึงกลับอย่างเร็วหลังเช็ดหัว สัมพันธ์กับความยาวการดึงกลับ\n" "ค่าจะถูกจำกัดด้วย 100% ลบด้วยปริมาณการดึงกลับก่อนค่าเช็ดหัว" msgid "Retract on layer change" -msgstr "ถอนออกเมื่อเปลี่ยนเลเยอร์" +msgstr "ดึงเส้นกลับเมื่อเปลี่ยนเลเยอร์" msgid "This forces a retraction on layer changes." msgstr "บังคับให้ถอนกลับเมื่อเปลี่ยนเลเยอร์" @@ -15925,7 +16132,7 @@ msgid "Retraction Length" msgstr "ระยะดึงกลับ" msgid "Some amount of material in extruder is pulled back to avoid ooze during long travel. Set zero to disable retraction." -msgstr "วัสดุบางส่วนในชุดดันเส้นถูกดึงกลับเพื่อหลีกเลี่ยงไม่ให้ซึ่มในระหว่างการเดินทางระยะไกล ตั้งค่าเป็นศูนย์เพื่อปิดใช้งานการเพิกถอน" +msgstr "วัสดุบางส่วนในชุดดันเส้นถูกดึงกลับเพื่อหลีกเลี่ยงไม่ให้ซึ่มในระหว่างการเดินทางระยะไกล ตั้งเป็น 0 เพื่อปิดการดึงกลับ" msgid "Long retraction when cut (beta)" msgstr "การถอยกลับยาวเมื่อตัด (เบต้า)" @@ -15945,10 +16152,18 @@ msgstr "การถอยกลับนานเมื่อเปลี่ย msgid "Retraction distance when extruder change" msgstr "ระยะการดึงกลับเมื่อชุดดันเส้นเปลี่ยน" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "ความยาวการดึงกลับ (การเปลี่ยนเครื่องมือ)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "เมื่อการดึงกลับทำงานก่อนการเปลี่ยนเครื่องมือ เส้นพลาสติกจะถูกดึงกลับตามระยะที่กำหนด (วัดความยาวบนเส้นพลาสติกดิบ ก่อนเข้าสู่ชุดดันเส้น)" + msgid "Z-hop height" msgstr "ความสูงยกแกน Z" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "เมื่อใดก็ตามที่การดึงกลับเสร็จสิ้น หัวฉีดจะถูกยกขึ้นเล็กน้อยเพื่อสร้างระยะห่างระหว่างหัวฉีดและงานพิมพ์ ป้องกันไม่ให้หัวฉีดชนกับงานพิมพ์ขณะเคลื่อนตัว การใช้เส้นเกลียวเพื่อยก Z สามารถป้องกันการร้อยสายได้" msgid "Z-hop lower boundary" @@ -16039,6 +16254,10 @@ msgstr "ความยาวพิเศษเมื่อรีสตาร์ msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "เมื่อชดเชยการดึงกลับหลังการเคลื่อนที่เดินทาง ชุดดันเส้นจะดันเส้นพลาสติกเพิ่มเติมในปริมาณนี้ การตั้งค่านี้ไม่ค่อยจำเป็น" +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "ความยาวพิเศษเมื่อรีสตาร์ท (การเปลี่ยนเครื่องมือ)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "เมื่อชดเชยการดึงกลับหลังเปลี่ยนเครื่องมือ ชุดดันเส้นจะดันเส้นพลาสติกเพิ่มเติมในปริมาณนี้" @@ -16049,7 +16268,7 @@ msgid "Speed for retracting filament from the nozzle." msgstr "ความเร็วในการดึงเส้นพลาสติกออกจากหัวฉีด" msgid "Deretraction speed" -msgstr "ความเร็วในการถอนกลับ" +msgstr "ความเร็วคืนเส้นหลังดึงกลับ" msgid "Speed for reloading filament into the nozzle. Zero means same speed of retraction." msgstr "ความเร็วในการบรรจุเส้นพลาสติกลงในหัวฉีด ศูนย์หมายถึงความเร็วการถอยกลับเท่ากัน" @@ -16063,7 +16282,7 @@ msgid "Speed for reloading filament into the nozzle when switching extruder." msgstr "ความเร็วในการโหลดเส้นพลาสติกกลับเข้าหัวฉีดเมื่อเปลี่ยนชุดดันเส้น" msgid "Use firmware retraction" -msgstr "ใช้การเพิกถอนเฟิร์มแวร์" +msgstr "ใช้การดึงกลับด้วยเฟิร์มแวร์" msgid "This experimental setting uses G10 and G11 commands to have the firmware handle the retraction. This is only supported in recent Marlin." msgstr "การตั้งค่าทดลองนี้ใช้คำสั่ง G10 และ G11 เพื่อให้เฟิร์มแวร์จัดการกับการเพิกถอน สิ่งนี้รองรับใน Marlin ล่าสุดเท่านั้น" @@ -16115,16 +16334,16 @@ msgstr "" "ปริมาณนี้สามารถระบุได้ในหน่วยมิลลิเมตรหรือเป็นเปอร์เซ็นต์ของเส้นผ่านศูนย์กลางของชุดดันเส้นในปัจจุบัน ค่าเริ่มต้นสำหรับพารามิเตอร์นี้คือ 10%" msgid "Scarf joint seam (beta)" -msgstr "รอยต่อเฉียง (เบต้า)" +msgstr "รอยต่อ Scarf (เบต้า)" msgid "Use scarf joint to minimize seam visibility and increase seam strength." -msgstr "ใช้ข้อต่อเฉียงเพื่อลดการมองเห็นรอยตะเข็บและเพิ่มความแข็งแรงของรอยตะเข็บ" +msgstr "ใช้รอยต่อ Scarf เพื่อลดการมองเห็นรอยตะเข็บและเพิ่มความแข็งแรง" msgid "Conditional scarf joint" -msgstr "ข้อต่อเฉียงแบบมีเงื่อนไข" +msgstr "รอยต่อ Scarf แบบมีเงื่อนไข" msgid "Apply scarf joints only to smooth perimeters where traditional seams do not conceal the seams at sharp corners effectively." -msgstr "ใช้ข้อต่อเฉียงกับขอบเรียบเท่านั้น โดยที่รอยตะเข็บแบบเดิมไม่สามารถปกปิดรอยตะเข็บที่มุมแหลมคมได้อย่างมีประสิทธิภาพ" +msgstr "ใช้รอยต่อ Scarf กับขอบเรียบเท่านั้น โดยที่รอยตะเข็บแบบเดิมไม่สามารถปกปิดรอยตะเข็บที่มุมแหลมคมได้อย่างมีประสิทธิภาพ" msgid "Conditional angle threshold" msgstr "เกณฑ์มุมแบบมีเงื่อนไข" @@ -16133,67 +16352,67 @@ msgid "" "This option sets the threshold angle for applying a conditional scarf joint seam.\n" "If the maximum angle within the perimeter loop exceeds this value (indicating the absence of sharp corners), a scarf joint seam will be used. The default value is 155°." msgstr "" -"ตัวเลือกนี้จะกำหนดมุมเกณฑ์สำหรับการใช้รอยตะเข็บข้อต่อเฉียงแบบมีเงื่อนไข\n" -"หากมุมสูงสุดภายในวงรอบปริมณฑลเกินค่านี้ (แสดงว่าไม่มีมุมแหลมคม) จะใช้รอยตะเข็บข้อต่อเฉียง ค่าเริ่มต้นคือ 155°" +"ตัวเลือกนี้จะกำหนดมุมเกณฑ์สำหรับการใช้รอยต่อ Scarf แบบมีเงื่อนไข\n" +"หากมุมสูงสุดภายในวงรอบปริมณฑลเกินค่านี้ (แสดงว่าไม่มีมุมแหลมคม) จะใช้รอยต่อ Scarf ค่าเริ่มต้นคือ 155°" msgid "Conditional overhang threshold" msgstr "เกณฑ์ระยะยื่นแบบมีเงื่อนไข" #, no-c-format, no-boost-format msgid "This option determines the overhang threshold for the application of scarf joint seams. If the unsupported portion of the perimeter is less than this threshold, scarf joint seams will be applied. The default threshold is set at 40% of the external wall's width. Due to performance considerations, the degree of overhang is estimated." -msgstr "ตัวเลือกนี้จะกำหนดเกณฑ์ส่วนยื่นสำหรับการใช้รอยตะเข็บข้อต่อเฉียง หากส่วนที่ไม่ได้รับส่วนรองรับของเส้นรอบวงน้อยกว่าเกณฑ์นี้ จะมีการเย็บรอยตะเข็บเฉียง เกณฑ์เริ่มต้นตั้งไว้ที่ 40% ของความกว้างของผนังภายนอก เมื่อพิจารณาถึงประสิทธิภาพแล้ว ระดับของระยะยื่นจึงถูกประมาณไว้" +msgstr "ตัวเลือกนี้จะกำหนดเกณฑ์ส่วนยื่นสำหรับการใช้รอยต่อ Scarf หากส่วนที่ไม่ได้รับส่วนรองรับของเส้นรอบวงน้อยกว่าเกณฑ์นี้ จะใช้รอยต่อ Scarf เกณฑ์เริ่มต้นตั้งไว้ที่ 40% ของความกว้างของผนังภายนอก เมื่อพิจารณาถึงประสิทธิภาพแล้ว ระดับของระยะยื่นจึงถูกประมาณไว้" msgid "Scarf joint speed" -msgstr "ความเร็วของข้อต่อเฉียง" +msgstr "ความเร็วรอยต่อ Scarf" msgid "This option sets the printing speed for scarf joints. It is recommended to print scarf joints at a slow speed (less than 100 mm/s). It's also advisable to enable 'Extrusion rate smoothing' if the set speed varies significantly from the speed of the outer or inner walls. If the speed specified here is higher than the speed of the outer or inner walls, the printer will default to the slower of the two speeds. When specified as a percentage (e.g., 80%), the speed is calculated based on the respective outer or inner wall speed. The default value is set to 100%." -msgstr "ตัวเลือกนี้จะตั้งค่าความเร็วในการพิมพ์สำหรับข้อต่อเฉียง ขอแนะนำให้พิมพ์ข้อต่อเฉียงด้วยความเร็วต่ำ (น้อยกว่า 100 มม./วินาที) ขอแนะนำให้เปิดใช้งาน 'การปรับอัตราการอัดรีดให้เรียบ' หากความเร็วที่ตั้งไว้แตกต่างอย่างมากจากความเร็วของผนังด้านนอกหรือด้านใน หากความเร็วที่ระบุที่นี่สูงกว่าความเร็วของผนังด้านนอกหรือด้านใน เครื่องพิมพ์จะตั้งค่าเริ่มต้นไว้ที่ความเร็วที่ช้ากว่าทั้งสอง เมื่อระบุเป็นเปอร์เซ็นต์ (เช่น 80%) ความเร็วจะคำนวณตามความเร็วผนังด้านนอกหรือด้านในตามลำดับ ค่าเริ่มต้นตั้งไว้ที่ 100%" +msgstr "ตัวเลือกนี้จะตั้งค่าความเร็วในการพิมพ์สำหรับรอยต่อ Scarf ขอแนะนำให้พิมพ์รอยต่อ Scarfด้วยความเร็วต่ำ (น้อยกว่า 100 มม./วินาที) ขอแนะนำให้เปิดใช้งาน 'การปรับอัตราการอัดรีดให้เรียบ' หากความเร็วที่ตั้งไว้แตกต่างอย่างมากจากความเร็วของผนังด้านนอกหรือด้านใน หากความเร็วที่ระบุที่นี่สูงกว่าความเร็วของผนังด้านนอกหรือด้านใน เครื่องพิมพ์จะตั้งค่าเริ่มต้นไว้ที่ความเร็วที่ช้ากว่าทั้งสอง เมื่อระบุเป็นเปอร์เซ็นต์ (เช่น 80%) ความเร็วจะคำนวณตามความเร็วผนังด้านนอกหรือด้านในตามลำดับ ค่าเริ่มต้นตั้งไว้ที่ 100%" msgid "Scarf joint flow ratio" -msgstr "อัตราการไหลของข้อต่อเฉียง" +msgstr "อัตราส่วนการไหลของรอยต่อ Scarf" msgid "This factor affects the amount of material for scarf joints." -msgstr "ปัจจัยนี้ส่งผลต่อปริมาณวัสดุสำหรับข้อต่อเฉียง" +msgstr "ปัจจัยนี้ส่งผลต่อปริมาณวัสดุสำหรับรอยต่อ Scarf" msgid "Scarf start height" -msgstr "ความสูงเริ่มต้นของเฉียง" +msgstr "ความสูงเริ่มต้นของรอยต่อ Scarf" msgid "" "Start height of the scarf.\n" "This amount can be specified in millimeters or as a percentage of the current layer height. The default value for this parameter is 0." msgstr "" -"เริ่มต้นความสูงของเฉียง\n" +"ความสูงเริ่มต้นของรอยต่อ Scarf\n" "จำนวนนี้สามารถระบุได้ในหน่วยมิลลิเมตรหรือเป็นเปอร์เซ็นต์ของความสูงของเลเยอร์ปัจจุบัน ค่าเริ่มต้นสำหรับพารามิเตอร์นี้คือ 0" msgid "Scarf around entire wall" -msgstr "เฉียงพันรอบผนังทั้งหมด" +msgstr "ใช้รอยต่อ Scarf ตลอดทั้งผนัง" msgid "The scarf extends to the entire length of the wall." -msgstr "เฉียงยาวตลอดความยาวของผนัง" +msgstr "รอยต่อ Scarf ครอบคลุมตลอดความยาวผนัง" msgid "Scarf length" -msgstr "ความยาวเฉียง" +msgstr "ความยาวรอยต่อ Scarf" msgid "Length of the scarf. Setting this parameter to zero effectively disables the scarf." -msgstr "ความยาวของเฉียง. การตั้งค่าพารามิเตอร์นี้เป็นศูนย์จะปิดใช้เฉียงอย่างมีประสิทธิภาพ" +msgstr "ความยาวของรอยต่อ Scarf ตั้งเป็น 0 เพื่อปิดการใช้ Scarf" msgid "Scarf steps" -msgstr "ขั้นตอนเฉียง" +msgstr "จำนวนขั้นของรอยต่อ Scarf" msgid "Minimum number of segments of each scarf." -msgstr "จำนวนขั้นต่ำของส่วนเฉียงแต่ละอัน" +msgstr "จำนวนเซกเมนต์ขั้นต่ำของรอยต่อ Scarf" msgid "Scarf joint for inner walls" -msgstr "ข้อต่อเฉียงสำหรับผนังด้านใน" +msgstr "รอยต่อ Scarf สำหรับผนังด้านใน" msgid "Use scarf joint for inner walls as well." -msgstr "ใช้ข้อต่อเฉียงสำหรับผนังด้านในด้วย" +msgstr "ใช้รอยต่อ Scarf กับผนังด้านในด้วย" msgid "Role base wipe speed" -msgstr "ความเร็วในการล้างฐานบทบาท" +msgstr "ความเร็ว Wipe ตามประเภทการพิมพ์" msgid "The wipe speed is determined by the speed of the current extrusion role. e.g. if a wipe action is executed immediately following an outer wall extrusion, the speed of the outer wall extrusion will be utilized for the wipe action." -msgstr "ความเร็วในการเช็ดถูกกำหนดโดยความเร็วของบทบาทการอัดขึ้นรูปในปัจจุบัน เช่น หากการดำเนินการเช็ดถูกดำเนินการทันทีหลังจากการอัดขึ้นรูปผนังด้านนอก ความเร็วของการอัดขึ้นรูปผนังด้านนอกจะถูกใช้สำหรับการดำเนินการเช็ด" +msgstr "ความเร็ว Wipe จะอิงจากความเร็วของประเภทการพิมพ์ปัจจุบัน เช่น หากการ Wipe เกิดขึ้นทันทีหลังจากพิมพ์ผนังด้านนอก ความเร็วของผนังด้านนอกจะถูกใช้สำหรับการ Wipe" msgid "Wipe on loops" msgstr "เช็ดบนลูป" @@ -16241,10 +16460,10 @@ msgid "Single loop after first layer" msgstr "วนรอบเดียวหลังจากชั้นแรก" msgid "Limits the skirt/draft shield loops to one wall after the first layer. This is useful, on occasion, to conserve filament but may cause the draft shield/skirt to warp / crack." -msgstr "จำกัดห่วงสเกิร์ต/โล่ครอบไว้ที่ผนังด้านหนึ่งหลังจากชั้นแรก สิ่งนี้มีประโยชน์ในบางครั้งเพื่ออนุรักษ์เส้นพลาสติก แต่อาจทำให้โครง/เส้นล้อมชิ้นงานบิดเบี้ยว/แตกร้าวได้" +msgstr "จำกัดห่วงสเกิร์ต/แนวป้องกันลม (Draft Shield) ไว้ที่ผนังเดียวหลังจากชั้นแรก สิ่งนี้มีประโยชน์ในบางครั้งเพื่อประหยัดเส้นพลาสติก แต่อาจทำให้แนวป้องกันลม/เส้นล้อมชิ้นงานบิดเบี้ยว/แตกร้าวได้" msgid "Draft shield" -msgstr "โล่ร่าง" +msgstr "แนวป้องกันลม (Draft Shield)" msgid "" "A draft shield is useful to protect an ABS or ASA print from warping and detaching from print bed due to wind draft. It is usually needed only with open frame printers, i.e. without an enclosure.\n" @@ -16252,10 +16471,10 @@ msgid "" "Enabled = skirt is as tall as the highest printed object. Otherwise 'Skirt height' is used.\n" "Note: With the draft shield active, the skirt will be printed at skirt distance from the object. Therefore, if brims are active it may intersect with them. To avoid this, increase the skirt distance value.\n" msgstr "" -"แผงครอบมีประโยชน์ในการปกป้องงานพิมพ์ ABS หรือ ASA จากการบิดงอและการหลุดออกจากฐานพิมพ์เนื่องจากกระแสลม โดยทั่วไปจำเป็นต้องใช้กับเครื่องพิมพ์แบบเปิดเฟรมเท่านั้น กล่าวคือ ไม่มีกล่องหุ้ม\n" +"แนวป้องกันลม (Draft Shield) มีประโยชน์ในการปกป้องงานพิมพ์ ABS หรือ ASA จากการบิดงอและการหลุดออกจากฐานพิมพ์เนื่องจากกระแสลม โดยทั่วไปจำเป็นต้องใช้กับเครื่องพิมพ์แบบเปิดเฟรมเท่านั้น กล่าวคือ ไม่มีกล่องหุ้ม\n" "\n" "Enabled = เส้นล้อมชิ้นงานสูงเท่ากับวัตถุที่พิมพ์สูงสุด มิฉะนั้น จะใช้ 'ความสูงของเส้นล้อมชิ้นงาน'\n" -"หมายเหตุ: เมื่อใช้งานดราฟชีลด์ เส้นล้อมชิ้นงานจะถูกพิมพ์ที่ระยะห่างจากเส้นล้อมชิ้นงานจากวัตถุ ดังนั้นหากขอบยึดชิ้นงานยังทำงานอยู่ ขอบยึดชิ้นงานอาจตัดกัน เพื่อหลีกเลี่ยงปัญหานี้ ให้เพิ่มค่าระยะห่างของเส้นล้อมชิ้นงาน\n" +"หมายเหตุ: เมื่อใช้งานแนวป้องกันลม (Draft Shield) เส้นล้อมชิ้นงานจะถูกพิมพ์ที่ระยะห่างจากวัตถุ ดังนั้นหากขอบยึดชิ้นงานยังทำงานอยู่ ขอบยึดชิ้นงานอาจตัดกัน เพื่อหลีกเลี่ยงปัญหานี้ ให้เพิ่มค่าระยะห่างของเส้นล้อมชิ้นงาน\n" msgid "Enabled" msgstr "เปิดใช้" @@ -16362,7 +16581,7 @@ msgid "Sets the finishing flow ratio while ending the spiral. Normally the spira msgstr "ตั้งค่าอัตราส่วนการไหลขั้นสุดท้ายขณะสิ้นสุดเกลียว โดยปกติการเปลี่ยนผ่านของเกลียวจะปรับขนาดอัตราส่วนการไหลจาก 100% เป็น 0% ในระหว่างลูปสุดท้าย ซึ่งในบางกรณีอาจนำไปสู่การรีดขึ้นรูปที่ปลายเกลียว" msgid "If smooth or traditional mode is selected, a timelapse video will be generated for each print. After each layer is printed, a snapshot is taken with the chamber camera. All of these snapshots are composed into a timelapse video when printing completes. If smooth mode is selected, the toolhead will move to the excess chute after each layer is printed and then take a snapshot. Since the melt filament may leak from the nozzle during the process of taking a snapshot, a prime tower is required for smooth mode to wipe the nozzle." -msgstr "หากเลือกโหมดเรียบหรือโหมดดั้งเดิม วิดีโอไทม์แลปส์จะถูกสร้างขึ้นสำหรับการพิมพ์แต่ละครั้ง หลังจากพิมพ์แต่ละชั้นแล้ว กล้องจะถ่ายภาพสแนปช็อตด้วยกล้องแชมเบอร์ สแน็ปช็อตทั้งหมดนี้จะถูกประกอบเป็นวิดีโอไทม์แลปส์เมื่อการพิมพ์เสร็จสิ้น หากเลือกโหมดเรียบ หัวเครื่องมือจะย้ายไปยังรางส่วนเกินหลังจากพิมพ์แต่ละเลเยอร์แล้วจึงถ่ายภาพสแน็ปช็อต เนื่องจากเส้นพลาสติกที่หลอมละลายอาจรั่วไหลออกจากหัวฉีดในระหว่างขั้นตอนการถ่ายภาพ จึงจำเป็นต้องมีไพรม์ทาวเวอร์เพื่อให้โหมดราบรื่นในการเช็ดหัวฉีด" +msgstr "หากเลือกโหมดเรียบหรือโหมดดั้งเดิม วิดีโอไทม์แลปส์จะถูกสร้างขึ้นสำหรับการพิมพ์แต่ละครั้ง หลังจากพิมพ์แต่ละชั้นแล้ว กล้องจะถ่ายภาพสแนปช็อตด้วยกล้องแชมเบอร์ สแน็ปช็อตทั้งหมดนี้จะถูกประกอบเป็นวิดีโอไทม์แลปส์เมื่อการพิมพ์เสร็จสิ้น หากเลือกโหมดเรียบ หัวเครื่องมือจะย้ายไปยังรางส่วนเกินหลังจากพิมพ์แต่ละเลเยอร์แล้วจึงถ่ายภาพสแน็ปช็อต เนื่องจากเส้นพลาสติกที่หลอมละลายอาจรั่วไหลออกจากหัวฉีดในระหว่างขั้นตอนการถ่ายภาพ จึงจำเป็นต้องมี Prime Tower เพื่อให้โหมดราบรื่นในการเช็ดหัวฉีด" msgid "Traditional" msgstr "แบบดั้งเดิม" @@ -16376,7 +16595,7 @@ msgstr "ไทม์แลปส์จุดไกลสุด" # AI Translated msgid "When enabled, the timelapse snapshot is taken at the farthest point from camera instead of traveling to the wipe tower or excess chute. Only effective in traditional timelapse mode on non-I3 printers." -msgstr "เมื่อเปิดใช้งาน ภาพไทม์แลปส์จะถูกถ่ายที่จุดไกลสุดจากกล้องแทนที่จะเดินหัวไปยังทาวเวอร์เช็ดหัวฉีดหรือช่องทิ้งส่วนเกิน มีผลเฉพาะในโหมดไทม์แลปส์แบบดั้งเดิมบนเครื่องพิมพ์ที่ไม่ใช่ I3" +msgstr "เมื่อเปิดใช้งาน ภาพไทม์แลปส์จะถูกถ่ายที่จุดไกลสุดจากกล้องแทนที่จะเดินหัวไปยัง Wipe Tower หรือช่องทิ้งส่วนเกิน มีผลเฉพาะในโหมดไทม์แลปส์แบบดั้งเดิมบนเครื่องพิมพ์ที่ไม่ใช่ I3" msgid "Temperature variation" msgstr "การเปลี่ยนแปลงของอุณหภูมิ" @@ -16425,10 +16644,10 @@ msgid "Enable this option to omit the custom Change filament G-code only at the msgstr "เปิดใช้งานตัวเลือกนี้เพื่อละเว้น G-code เปลี่ยนฟิลาเมนต์แบบกำหนดเองเฉพาะตอนเริ่มต้นการพิมพ์เท่านั้น คำสั่งเปลี่ยนเครื่องมือ (เช่น T0) จะถูกข้ามไปตลอดการพิมพ์ทั้งหมด สิ่งนี้มีประโยชน์สำหรับการพิมพ์หลายวัสดุด้วยตนเอง โดยที่เราใช้ M600/PAUSE เพื่อกระตุ้นการดำเนินการเปลี่ยนเส้นพลาสติกด้วยตนเอง" msgid "Wipe tower type" -msgstr "ชนิดทาวเวอร์เช็ด" +msgstr "ชนิด Wipe Tower" msgid "Choose the wipe tower implementation for multi-material prints. Type 1 is recommended for Bambu and Qidi printers with a filament cutter. Type 2 offers better compatibility with multi-tool and MMU printers and provide overall better compatibility." -msgstr "เลือกการใช้งานไวด์ทาวเวอร์สำหรับการพิมพ์แบบหลายวัสดุ แนะนำให้ใช้ประเภท 1 สำหรับเครื่องพิมพ์ Bambu และ Qidi ที่มีเครื่องตัดเส้นพลาสติก Type 2 ให้ความเข้ากันได้ที่ดีกว่ากับเครื่องพิมพ์หลายเครื่องมือและ MMU และให้ความเข้ากันได้โดยรวมดีขึ้น" +msgstr "เลือกการใช้งาน Wipe Tower สำหรับการพิมพ์แบบหลายวัสดุ แนะนำให้ใช้ประเภท 1 สำหรับเครื่องพิมพ์ Bambu และ Qidi ที่มีเครื่องตัดเส้นพลาสติก Type 2 ให้ความเข้ากันได้ที่ดีกว่ากับเครื่องพิมพ์หลายเครื่องมือและ MMU และให้ความเข้ากันได้โดยรวมดีขึ้น" msgid "Type 1" msgstr "ประเภทที่ 1" @@ -16437,25 +16656,33 @@ msgid "Type 2" msgstr "ประเภทที่ 2" msgid "Purge in prime tower" -msgstr "ระยะเว้นในไพร์มทาวเวอร์" +msgstr "ระยะเว้นใน Prime Tower" msgid "Purge remaining filament into prime tower." -msgstr "ล้างเส้นพลาสติกที่เหลือลงในไพร์มทาวเวอร์" +msgstr "ล้างเส้นพลาสติกที่เหลือลงใน Prime Tower" msgid "Enable filament ramming" msgstr "เปิดใช้งานการอัดกระแทกเส้นเส้นพลาสติก" msgid "Tool change on wipe tower" -msgstr "การเปลี่ยนเครื่องมือบนไวด์ทาวเวอร์" +msgstr "การเปลี่ยนเครื่องมือบน Wipe Tower" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." -msgstr "บังคับให้หัวเครื่องมือเคลื่อนที่ไปที่หอเช็ดก่อนที่จะออกคำสั่งเปลี่ยนเครื่องมือ (Tx) เกี่ยวข้องเฉพาะกับเครื่องพิมพ์ที่มีชุดดันเส้นหลายเครื่อง (หลายหัวเครื่องมือ) ที่ใช้แผ่นเช็ดแบบ Type 2 ตามค่าเริ่มต้น Orca จะข้ามการเดินทางบนเครื่องที่มีหัวเครื่องมือหลายหัวเนื่องจากเฟิร์มแวร์จัดการการสลับหัว ซึ่งอาจส่งผลให้มีการออกคำสั่ง Tx เหนือส่วนที่พิมพ์ เปิดใช้งานตัวเลือกนี้หากคุณต้องการให้ทำการเปลี่ยนแปลงเครื่องมือเหนือหอเช็ดแทนเสมอ" +msgstr "บังคับให้หัวเครื่องมือเคลื่อนที่ไปที่ Wipe Tower ก่อนที่จะออกคำสั่งเปลี่ยนเครื่องมือ (Tx) เกี่ยวข้องเฉพาะกับเครื่องพิมพ์ที่มีชุดดันเส้นหลายเครื่อง (หลายหัวเครื่องมือ) ที่ใช้แผ่นเช็ดแบบ Type 2 ตามค่าเริ่มต้น Orca จะข้ามการเดินทางบนเครื่องที่มีหัวเครื่องมือหลายหัวเนื่องจากเฟิร์มแวร์จัดการการสลับหัว ซึ่งอาจส่งผลให้มีการออกคำสั่ง Tx เหนือส่วนที่พิมพ์ เปิดใช้งานตัวเลือกนี้หากคุณต้องการให้ทำการเปลี่ยนแปลงเครื่องมือเหนือ Wipe Tower แทนเสมอ" + +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "รอให้ถึงอุณหภูมิที่ Wipe Tower" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "รับเครื่องมือใหม่โดยไม่รอให้ถึงอุณหภูมิการพิมพ์ แล้วเคลื่อนที่ไปยัง Wipe Tower และรออุณหภูมิที่นั่นก่อนไล่เส้นทันที เส้นพลาสติกที่ซึมออกมาระหว่างการอุ่นจะตกลงบน Wipe Tower แทนที่จะตกบนโมเดล และการเคลื่อนที่จะเกิดขึ้นพร้อมกับการอุ่น ใช้ได้เฉพาะกับเครื่องพิมพ์แบบหลายชุดดันเส้น (หลายหัวพิมพ์) ที่ใช้ Wipe Tower ชนิดที่ 2 เฟิร์มแวร์หรือแมโครการเปลี่ยนเครื่องมือต้องไม่รออุณหภูมิเอง เมื่อปิดใช้ คำสั่งรออุณหภูมิจะถูกส่งทันทีหลังคำสั่งเปลี่ยนเครื่องมือ" msgid "No sparse layers (beta)" msgstr "ไม่มีชั้นกระจัดกระจาย (เบต้า)" msgid "If enabled, the wipe tower will not be printed on layers with no tool changes. On layers with a tool change, extruder will travel downward to print the wipe tower. User is responsible for ensuring there is no collision with the print." -msgstr "หากเปิดใช้งาน หอเช็ดจะไม่ถูกพิมพ์บนเลเยอร์โดยไม่มีการเปลี่ยนแปลงเครื่องมือ บนเลเยอร์ที่มีการเปลี่ยนเครื่องมือ ชุดดันเส้นจะเคลื่อนลงด้านล่างเพื่อพิมพ์ไวด์ทาวเวอร์ ผู้ใช้มีหน้าที่รับผิดชอบในการตรวจสอบให้แน่ใจว่าไม่มีการชนกันกับงานพิมพ์" +msgstr "หากเปิดใช้งาน Wipe Tower จะไม่ถูกพิมพ์บนเลเยอร์โดยไม่มีการเปลี่ยนแปลงเครื่องมือ บนเลเยอร์ที่มีการเปลี่ยนเครื่องมือ ชุดดันเส้นจะเคลื่อนลงด้านล่างเพื่อพิมพ์ Wipe Tower ผู้ใช้มีหน้าที่รับผิดชอบในการตรวจสอบให้แน่ใจว่าไม่มีการชนกันกับงานพิมพ์" msgid "Prime all printing extruders" msgstr "ใช้ชุดดันเส้นการพิมพ์ทั้งหมด" @@ -16720,7 +16947,7 @@ msgid "Independent support layer height" msgstr "ความสูงของชั้นรองรับอิสระ" msgid "Support layer uses layer height independent with object layer. This is to support customizing Z-gap and save print time. This option will be invalid when the prime tower is enabled." -msgstr "เลเยอร์ส่วนรองรับใช้ความสูงของเลเยอร์ที่เป็นอิสระจากเลเยอร์วัตถุ เพื่อรองรับการปรับแต่ง Z-gap และประหยัดเวลาในการพิมพ์ ตัวเลือกนี้จะไม่ถูกต้องเมื่อเปิดใช้งานไพรม์ทาวเวอร์" +msgstr "เลเยอร์ส่วนรองรับใช้ความสูงของเลเยอร์ที่เป็นอิสระจากเลเยอร์วัตถุ เพื่อรองรับการปรับแต่ง Z-gap และประหยัดเวลาในการพิมพ์ ตัวเลือกนี้จะไม่ถูกต้องเมื่อเปิดใช้งาน Prime Tower" msgid "Threshold angle" msgstr "มุมเกณฑ์" @@ -16885,13 +17112,13 @@ msgid "This G-code is inserted when filament is changed, including T commands to msgstr "รหัส G นี้จะถูกแทรกเมื่อมีการเปลี่ยนเส้นพลาสติก รวมถึงคำสั่ง T เพื่อกระตุ้นการเปลี่ยนเครื่องมือ" msgid "This G-code is inserted when the extrusion role is changed." -msgstr "G-code นี้จะถูกแทรกเมื่อบทบาทการอัดขึ้นรูปมีการเปลี่ยนแปลง" +msgstr "G-code นี้จะถูกแทรกเมื่อประเภทการพิมพ์มีการเปลี่ยนแปลง" msgid "Change extrusion role G-code (filament)" -msgstr "เปลี่ยนบทบาทการอัดขึ้นรูป G-code (เส้นพลาสติก)" +msgstr "เปลี่ยนประเภทการพิมพ์ G-code (เส้นพลาสติก)" msgid "This G-code is inserted when the extrusion role is changed for the active filament." -msgstr "รหัส G นี้จะถูกแทรกเมื่อมีการเปลี่ยนบทบาทการอัดขึ้นรูปสำหรับเส้นพลาสติกที่ใช้งานอยู่" +msgstr "รหัส G นี้จะถูกแทรกเมื่อมีการเปลี่ยนประเภทการพิมพ์สำหรับเส้นพลาสติกที่ใช้งานอยู่" msgid "Line width for top surfaces. If expressed as a %, it will be computed over the nozzle diameter." msgstr "ความกว้างของเส้นสำหรับพื้นผิวด้านบน หากแสดงเป็น % จะคำนวณตามเส้นผ่านศูนย์กลางของหัวฉีด" @@ -16980,14 +17207,22 @@ msgstr "" "\n" "การตั้งค่าในจำนวนการถอนก่อนการล้างการตั้งค่าด้านล่างจะทำการถอนส่วนที่เกินก่อนการล้าง มิฉะนั้นจะดำเนินการหลังจากนั้น" +# AI Translated +msgid "Mixed color sublayer" +msgstr "ซับเลเยอร์สีผสม" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "เปิดใช้การแบ่งซับเลเยอร์สีผสม เมื่อเปิดใช้ เลเยอร์ที่มีเส้นพลาสติกสีผสมจะถูกแบ่งเป็นซับเลเยอร์เพื่อให้เกิดเอฟเฟกต์การผสมสี" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." -msgstr "หอเช็ดสามารถใช้เพื่อทำความสะอาดสิ่งตกค้างบนหัวฉีด และทำให้แรงดันในห้องภายในหัวฉีดคงที่ เพื่อหลีกเลี่ยงข้อบกพร่องในลักษณะที่ปรากฏเมื่อพิมพ์วัตถุ" +msgstr "Wipe Tower สามารถใช้เพื่อทำความสะอาดสิ่งตกค้างบนหัวฉีด และทำให้แรงดันในห้องภายในหัวฉีดคงที่ เพื่อหลีกเลี่ยงข้อบกพร่องในลักษณะที่ปรากฏเมื่อพิมพ์วัตถุ" msgid "Internal ribs" -msgstr "ซี่โครงภายใน" +msgstr "ครีบเสริมภายใน" msgid "Enable internal ribs to increase the stability of the prime tower." -msgstr "เปิดใช้งานซี่โครงภายในเพื่อเพิ่มความมั่นคงของหอคอยหลัก" +msgstr "เปิดใช้ครีบเสริมภายในเพื่อเพิ่มความมั่นคงของ Prime Tower" msgid "Purging volumes" msgstr "ปริมาตรการไล่เส้น" @@ -17007,10 +17242,10 @@ msgid "The flush multiplier used in fast purge mode." msgstr "ตัวคูณการไล่เส้นที่ใช้ในโหมดไล่เส้นเร็ว" msgid "Prime volume" -msgstr "ปริมาณเฉพาะ" +msgstr "ปริมาตร Prime" msgid "This is the volume of material to prime the extruder with on the tower." -msgstr "ปริมาตรวัสดุสำหรับเตรียมหัวฉีดบนไพรม์ทาวเวอร์" +msgstr "ปริมาตรวัสดุสำหรับเตรียมหัวฉีดบน Prime Tower" # AI Translated msgid "Prime volume mode" @@ -17018,7 +17253,7 @@ msgstr "โหมดปริมาณการไพรม์" # AI Translated msgid "Selects how the wipe-tower prime and flush volumes are computed on multi-extruder printers." -msgstr "เลือกวิธีการคำนวณปริมาณการไพรม์และการไล่เส้นของทาวเวอร์เช็ดหัวฉีดบนเครื่องพิมพ์แบบหลายชุดดันเส้น" +msgstr "เลือกวิธีการคำนวณปริมาณการไพรม์และการไล่เส้นของ Wipe Tower บนเครื่องพิมพ์แบบหลายชุดดันเส้น" # AI Translated msgid "Saving" @@ -17029,25 +17264,25 @@ msgid "Fast" msgstr "เร็ว" msgid "This is the width of prime towers." -msgstr "ความกว้างของไพรม์ทาวเวอร์" +msgstr "ความกว้างของ Prime Tower" msgid "Wipe tower rotation angle" -msgstr "เช็ดมุมการหมุนของทาวเวอร์" +msgstr "มุมหมุนของ Wipe Tower" msgid "Wipe tower rotation angle with respect to X axis." -msgstr "เช็ดมุมการหมุนของทาวเวอร์ตามแกน X" +msgstr "มุมหมุนของ Wipe Tower เทียบกับแกน X" msgid "Brim width of prime tower, negative number means auto calculated width based on the height of prime tower." -msgstr "ความกว้างขอบของหอคอยหลัก ตัวเลขติดลบหมายถึงความกว้างที่คำนวณโดยอัตโนมัติตามความสูงของหอคอยหลัก" +msgstr "ความกว้างขอบของ Prime Tower ตัวเลขติดลบหมายถึงความกว้างที่คำนวณโดยอัตโนมัติตามความสูงของ Prime Tower" msgid "Stabilization cone apex angle" msgstr "มุมเอเพ็กซ์ของกรวยป้องกันการสั่นไหว" msgid "Angle at the apex of the cone that is used to stabilize the wipe tower. Larger angle means wider base." -msgstr "มุมที่ปลายกรวยที่ใช้เพื่อรักษาเสถียรภาพของหอเช็ด มุมที่ใหญ่ขึ้นหมายถึงฐานที่กว้างขึ้น" +msgstr "มุมที่ปลายกรวยที่ใช้เพื่อรักษาเสถียรภาพของ Wipe Tower มุมที่ใหญ่ขึ้นหมายถึงฐานที่กว้างขึ้น" msgid "Maximum wipe tower print speed" -msgstr "ความเร็วการพิมพ์ไวต์ทาวเวอร์สูงสุด" +msgstr "ความเร็วการพิมพ์ Wipe Tower สูงสุด" msgid "" "The maximum print speed when purging in the wipe tower and printing the wipe tower sparse layers. When purging, if the sparse infill speed or calculated speed from the filament max volumetric speed is lower, the lowest will be used instead.\n" @@ -17064,11 +17299,11 @@ msgstr "" "\n" "เมื่อพิมพ์ชั้นเบาบาง หากความเร็วเส้นรอบวงภายในหรือความเร็วที่คำนวณจากความเร็วปริมาตรสูงสุดของเส้นพลาสติกต่ำกว่า ความเร็วต่ำสุดจะถูกนำมาใช้แทน\n" "\n" -"การเพิ่มความเร็วนี้อาจส่งผลต่อเสถียรภาพของทาวเวอร์ รวมทั้งเพิ่มแรงที่หัวฉีดชนกับหยดใดๆ ที่อาจก่อตัวบนทาวเวอร์เช็ด\n" +"การเพิ่มความเร็วนี้อาจส่งผลต่อเสถียรภาพของทาวเวอร์ รวมทั้งเพิ่มแรงที่หัวฉีดชนกับหยดใดๆ ที่อาจก่อตัวบน Wipe Tower\n" "\n" "ก่อนที่จะเพิ่มพารามิเตอร์นี้เกินกว่าค่าเริ่มต้นที่ 90 มม./วินาที ตรวจสอบให้แน่ใจว่าเครื่องพิมพ์ของคุณสามารถเชื่อมต่อที่ความเร็วที่เพิ่มขึ้นได้อย่างน่าเชื่อถือ และจะมีการควบคุมอย่างดีเมื่อเปลี่ยนเครื่องมือ\n" "\n" -"สำหรับปริมณฑลภายนอกของไวต์ทาวเวอร์ ความเร็วของปริมณฑลภายในจะถูกใช้โดยไม่คำนึงถึงการตั้งค่านี้" +"สำหรับปริมณฑลภายนอกของ Wipe Tower ความเร็วของปริมณฑลภายในจะถูกใช้โดยไม่คำนึงถึงการตั้งค่านี้" msgid "Wall type" msgstr "ชนิดติดผนัง" @@ -17079,10 +17314,10 @@ msgid "" "2. Cone: A cone with a fillet at the bottom to help stabilize the wipe tower.\n" "3. Rib: Adds four ribs to the tower wall for enhanced stability." msgstr "" -"เช็ดทาวเวอร์ชนิดผนังด้านนอก\n" +"Wipe Tower ชนิดผนังด้านนอก\n" "1. สี่เหลี่ยมผืนผ้า: ประเภทผนังเริ่มต้น ซึ่งเป็นสี่เหลี่ยมผืนผ้าที่มีความกว้างและความสูงคงที่\n" -"2. กรวย: กรวยที่มีเนื้ออยู่ด้านล่างเพื่อช่วยรักษาเสถียรภาพของหอเช็ด\n" -"3. ซี่โครง: เพิ่มสี่ซี่โครงเข้ากับผนังหอคอยเพื่อเพิ่มความมั่นคง" +"2. กรวย: กรวยที่มีเนื้ออยู่ด้านล่างเพื่อช่วยรักษาเสถียรภาพของ Wipe Tower\n" +"3. ซี่โครง: เพิ่มสี่ซี่โครงเข้ากับผนัง Wipe Tower เพื่อเพิ่มความมั่นคง" msgid "Rectangle" msgstr "สี่เหลี่ยมผืนผ้า" @@ -17100,40 +17335,40 @@ msgid "Rib width" msgstr "ความกว้างของซี่โครง" msgid "Rib width is always less than half the prime tower side length." -msgstr "ความกว้างของซี่โครงจะน้อยกว่าครึ่งหนึ่งของความยาวด้านของไพรม์ทาวเวอร์เสมอ" +msgstr "ความกว้างของซี่โครงจะน้อยกว่าครึ่งหนึ่งของความยาวด้านของ Prime Tower เสมอ" msgid "Fillet wall" msgstr "ผนังเนื้อ" msgid "The wall of prime tower will fillet." -msgstr "ผนังของไพร์มทาวเวอร์จะแล่เป็นเนื้อเดียวกัน" +msgstr "ผนังของ Prime Tower จะแล่เป็นเนื้อเดียวกัน" msgid "The extruder to use when printing perimeter of the wipe tower. Set to 0 to use the one that is available (non-soluble would be preferred)." -msgstr "ชุดดันเส้นที่จะใช้ในการพิมพ์ปริมณฑลของหอเช็ด ตั้งค่าเป็น 0 เพื่อใช้อันที่มีอยู่ (แนะนำให้ใช้แบบไม่ละลายน้ำ)" +msgstr "ชุดดันเส้นที่จะใช้ในการพิมพ์ปริมณฑลของ Wipe Tower ตั้งค่าเป็น 0 เพื่อใช้อันที่มีอยู่ (แนะนำให้ใช้แบบไม่ละลายน้ำ)" msgid "Purging volumes - load/unload volumes" msgstr "การล้างไดรฟ์ข้อมูล - โหลด/ยกเลิกการโหลดไดรฟ์ข้อมูล" msgid "This vector saves required volumes to change from/to each tool used on the wipe tower. These values are used to simplify creation of the full purging volumes below." -msgstr "เวกเตอร์นี้จะบันทึกปริมาณที่ต้องการเพื่อเปลี่ยนจาก/ไปยังแต่ละเครื่องมือที่ใช้บนไวด์ทาวเวอร์ ค่าเหล่านี้ใช้เพื่อทำให้การสร้างวอลุ่มการล้างข้อมูลทั้งหมดด้านล่างง่ายขึ้น" +msgstr "เวกเตอร์นี้จะบันทึกปริมาณที่ต้องการเพื่อเปลี่ยนจาก/ไปยังแต่ละเครื่องมือที่ใช้บน Wipe Tower ค่าเหล่านี้ใช้เพื่อทำให้การสร้างวอลุ่มการล้างข้อมูลทั้งหมดด้านล่างง่ายขึ้น" msgid "Skip points" msgstr "ข้ามจุด" msgid "The wall of prime tower will skip the start points of wipe path." -msgstr "ผนังของไพร์มทาวเวอร์จะข้ามจุดเริ่มต้นของเส้นทางการเช็ด" +msgstr "ผนังของ Prime Tower จะข้ามจุดเริ่มต้นของเส้นทางการเช็ด" msgid "Enable tower interface features" msgstr "เปิดใช้งานคุณสมบัติอินเทอร์เฟซแบบทาวเวอร์" msgid "Enable optimized prime tower interface behavior when different materials meet." -msgstr "เปิดใช้งานพฤติกรรมอินเทอร์เฟซของไพรม์ทาวเวอร์ที่ได้รับการปรับให้เหมาะสมเมื่อวัสดุที่แตกต่างกันมาบรรจบกัน" +msgstr "เปิดใช้งานพฤติกรรมอินเทอร์เฟซของ Prime Tower ที่ได้รับการปรับให้เหมาะสมเมื่อวัสดุที่แตกต่างกันมาบรรจบกัน" msgid "Cool down from interface boost during prime tower" -msgstr "เย็นลงจากการเพิ่มอินเทอร์เฟซระหว่างหอคอยหลัก" +msgstr "การระบายความร้อนที่เลเยอร์อินเทอร์เฟซของ Prime Tower" msgid "When interface-layer temperature boost is active, set the nozzle back to print temperature at the start of the prime tower so it cools down during the tower." -msgstr "เมื่อเปิดใช้งานการเพิ่มอุณหภูมิของชั้นอินเทอร์เฟซ ให้ตั้งค่าหัวฉีดกลับไปเป็นอุณหภูมิการพิมพ์ที่จุดเริ่มต้นของไพรม์ทาวเวอร์ เพื่อให้เย็นลงระหว่างทาวเวอร์" +msgstr "เมื่อเปิดใช้งานการเพิ่มอุณหภูมิของชั้นอินเทอร์เฟซ ให้ตั้งค่าหัวฉีดกลับไปเป็นอุณหภูมิการพิมพ์ที่จุดเริ่มต้นของ Prime Tower เพื่อให้เย็นลงระหว่าง Prime Tower" msgid "Infill gap" msgstr "การเติมช่องว่าง" @@ -17142,13 +17377,13 @@ msgid "Infill gap." msgstr "การเติมช่องว่าง." msgid "Purging after filament change will be done inside objects' infills. This may lower the amount of waste and decrease the print time. If the walls are printed with transparent filament, the mixed color infill will be visible. It will not take effect unless the prime tower is enabled." -msgstr "การล้างหลังจากเปลี่ยนเส้นพลาสติกจะดำเนินการภายในส่วนไส้ในของวัตถุ สิ่งนี้อาจลดปริมาณขยะและลดเวลาในการพิมพ์ หากผนังพิมพ์ด้วยเส้นพลาสติกโปร่งใส จะเห็นไส้ในสีผสมไว้ด้านนอก มันจะไม่มีผลเว้นแต่จะเปิดใช้งานไพรม์ทาวเวอร์" +msgstr "การล้างหลังจากเปลี่ยนเส้นพลาสติกจะดำเนินการภายในส่วนไส้ในของวัตถุ สิ่งนี้อาจลดปริมาณขยะและลดเวลาในการพิมพ์ หากผนังพิมพ์ด้วยเส้นพลาสติกโปร่งใส จะเห็นไส้ในสีผสมไว้ด้านนอก มันจะไม่มีผลเว้นแต่จะเปิดใช้งาน Prime Tower" msgid "Purging after filament change will be done inside objects' support. This may lower the amount of waste and decrease the print time. It will not take effect unless a prime tower is enabled." -msgstr "การล้างข้อมูลหลังจากเปลี่ยนเส้นพลาสติกจะดำเนินการภายในส่วนรองรับของวัตถุ สิ่งนี้อาจลดปริมาณขยะและลดเวลาในการพิมพ์ มันจะไม่มีผลเว้นแต่จะเปิดใช้งานไพรม์ทาวเวอร์" +msgstr "การล้างข้อมูลหลังจากเปลี่ยนเส้นพลาสติกจะดำเนินการภายในส่วนรองรับของวัตถุ สิ่งนี้อาจลดปริมาณขยะและลดเวลาในการพิมพ์ มันจะไม่มีผลเว้นแต่จะเปิดใช้งาน Prime Tower" msgid "This object will be used to purge the nozzle after a filament change to save filament and decrease the print time. Colors of the objects will be mixed as a result. It will not take effect unless the prime tower is enabled." -msgstr "วัตถุนี้จะใช้ในการล้างหัวฉีดหลังจากเปลี่ยนเส้นพลาสติกเพื่อประหยัดเส้นพลาสติกและลดเวลาในการพิมพ์ สีของวัตถุจะผสมกัน มันจะไม่มีผลเว้นแต่จะเปิดใช้งานไพรม์ทาวเวอร์" +msgstr "วัตถุนี้จะใช้ในการล้างหัวฉีดหลังจากเปลี่ยนเส้นพลาสติกเพื่อประหยัดเส้นพลาสติกและลดเวลาในการพิมพ์ สีของวัตถุจะผสมกัน มันจะไม่มีผลเว้นแต่จะเปิดใช้งาน Prime Tower" msgid "Maximal bridging distance" msgstr "ระยะเชื่อมต่อสูงสุด" @@ -17157,16 +17392,16 @@ msgid "Maximal distance between supports on sparse infill sections." msgstr "ระยะห่างสูงสุดระหว่างส่วนรองรับในส่วน ไส้ใน แบบกระจัดกระจาย" msgid "Wipe tower purge lines spacing" -msgstr "เช็ดระยะห่างบรรทัดล้างทาวเวอร์" +msgstr "ระยะห่างเส้นไล่พลาสติกของ Wipe Tower" msgid "Spacing of purge lines on the wipe tower." -msgstr "ระยะห่างของเส้นไล่ล้างบนหอเช็ด" +msgstr "ระยะห่างของเส้นไล่ล้างบน Wipe Tower" msgid "Extra flow for purging" msgstr "กระแสพิเศษสำหรับการล้าง" msgid "Extra flow used for the purging lines on the wipe tower. This makes the purging lines thicker or narrower than they normally would be. The spacing is adjusted automatically." -msgstr "การไหลพิเศษที่ใช้สำหรับท่อไล่ล้างบนหอเช็ด ซึ่งจะทำให้เส้นการล้างหนาหรือแคบกว่าปกติ ระยะห่างจะถูกปรับโดยอัตโนมัติ" +msgstr "การไหลพิเศษที่ใช้สำหรับท่อไล่ล้างบน Wipe Tower ซึ่งจะทำให้เส้นการล้างหนาหรือแคบกว่าปกติ ระยะห่างจะถูกปรับโดยอัตโนมัติ" msgid "Idle temperature" msgstr "อุณหภูมิว่าง" @@ -17748,10 +17983,10 @@ msgid "Specific for sequential printing. Zero-based index of currently printed o msgstr "เฉพาะสำหรับการพิมพ์ตามลำดับ ดัชนีแบบศูนย์ของวัตถุที่พิมพ์ในปัจจุบัน" msgid "Has wipe tower" -msgstr "มีหอเช็ด" +msgstr "มี Wipe Tower" msgid "Whether or not wipe tower is being generated in the print." -msgstr "มีการสร้างเช็ดทาวเวอร์ในการพิมพ์หรือไม่" +msgstr "มีการสร้าง Wipe Tower ในการพิมพ์หรือไม่" msgid "Initial extruder" msgstr "ชุดดันเส้นเริ่มต้น" @@ -17838,16 +18073,16 @@ msgid "Total cost of all material used in the print. Calculated from filament_co msgstr "ต้นทุนรวมของวัสดุทั้งหมดที่ใช้ในการพิมพ์ คำนวณจากค่า fil_cost ในการตั้งค่า เส้นพลาสติก" msgid "Total wipe tower cost" -msgstr "ต้นทุนเช็ดทาวเวอร์ทั้งหมด" +msgstr "ต้นทุน Wipe Tower ทั้งหมด" msgid "Total cost of the material wasted on the wipe tower. Calculated from filament_cost value in Filament Settings." -msgstr "ต้นทุนรวมของวัสดุที่เสียไปบนไวด์ทาวเวอร์ คำนวณจากค่า fil_cost ในการตั้งค่า เส้นพลาสติก" +msgstr "ต้นทุนรวมของวัสดุที่เสียไปบน Wipe Tower คำนวณจากค่า fil_cost ในการตั้งค่า เส้นพลาสติก" msgid "Wipe tower volume" msgstr "เช็ดปริมาตรทาวเวอร์" msgid "Total filament volume extruded on the wipe tower." -msgstr "ปริมาตรเส้นพลาสติกทั้งหมดที่อัดบนไวด์ทาวเวอร์" +msgstr "ปริมาตรเส้นพลาสติกทั้งหมดที่อัดบน Wipe Tower" msgid "Used filament" msgstr "เส้นพลาสติกที่ใช้แล้ว" @@ -18045,8 +18280,8 @@ msgid "" "An object has enabled XY Size compensation which will not be used because it is also fuzzy skin painted.\n" "XY Size compensation cannot be combined with fuzzy skin painting." msgstr "" -"วัตถุได้เปิดใช้งานการชดเชยขนาด XY ซึ่งจะไม่ถูกใช้เนื่องจากเป็นสีที่ไม่ชัดเจนเช่นกัน\n" -"การชดเชยขนาด XY ไม่สามารถใช้ร่วมกับการลงสีผิวแบบคลุมเครือได้" +"วัตถุได้เปิดใช้งานการชดเชยขนาด XY ซึ่งจะไม่ถูกใช้เนื่องจากถูกระบายสีผิวฟัซซีไว้เช่นกัน\n" +"การชดเชยขนาด XY ไม่สามารถใช้ร่วมกับการระบายสีผิวฟัซซีได้" msgid "Object name" msgstr "ชื่อออบเจ็กต์" @@ -18063,6 +18298,10 @@ msgstr "การประสานไฟล์โมเดลล้มเหล msgid "The supplied file couldn't be read because it's empty." msgstr "ไม่สามารถอ่านไฟล์ที่ให้มาได้เนื่องจากไฟล์ว่างเปล่า" +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "รูปแบบไฟล์ไม่รองรับ จึงไม่สามารถอ่านข้อมูลได้" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "รูปแบบไฟล์ที่ไม่รู้จัก ไฟล์อินพุตต้องมีนามสกุล .stl, .obj, .amf(.xml)" @@ -19524,19 +19763,19 @@ msgstr "แสดงเฉพาะชื่อเครื่องพิมพ msgid "Only display the filament names with changes to filament presets." msgstr "แสดงเฉพาะชื่อเส้นพลาสติกที่มีการเปลี่ยนแปลงการตั้งค่าล่วงหน้าของเส้นพลาสติก" -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "เฉพาะชื่อเครื่องพิมพ์ที่มีการตั้งค่าเครื่องพิมพ์ล่วงหน้าของผู้ใช้เท่านั้นที่จะแสดง และการตั้งค่าล่วงหน้าแต่ละรายการที่คุณเลือกจะถูกส่งออกเป็นไฟล์ ZIP" msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" "เฉพาะชื่อเส้นพลาสติกที่มีการตั้งค่าล่วงหน้าของเส้นพลาสติกผู้ใช้เท่านั้นที่จะถูกแสดง \n" "และค่าฟิลาเมนต์ของผู้ใช้ทั้งหมดที่กำหนดไว้ล่วงหน้าในแต่ละชื่อฟิลาเมนต์ที่คุณเลือกจะถูกส่งออกเป็นไฟล์ ZIP" msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" "เฉพาะชื่อเครื่องพิมพ์ที่มีการตั้งค่าล่วงหน้าของกระบวนการที่เปลี่ยนแปลงเท่านั้นที่จะปรากฏขึ้น \n" "และกระบวนการของผู้ใช้ที่ตั้งไว้ล่วงหน้าในชื่อเครื่องพิมพ์แต่ละเครื่องที่คุณเลือกจะถูกส่งออกเป็นไฟล์ ZIP" @@ -19681,9 +19920,6 @@ msgstr "เครื่องพิมพ์ทางกายภาพ" msgid "Print Host upload" msgstr "อัพโหลดโฮสต์การพิมพ์" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "เลือกการใช้งานตัวแทนเครือข่ายสำหรับการสื่อสารของเครื่องพิมพ์ ตัวแทนที่มีอยู่จะได้รับการลงทะเบียนเมื่อเริ่มต้น" - msgid "Select a Flashforge printer" msgstr "เลือกเครื่องพิมพ์ Flashforge" @@ -19780,7 +20016,7 @@ msgid "We need information for diagnosing source of the issue. Check wiki page f msgstr "เราต้องการข้อมูลเพื่อวินิจฉัยแหล่งที่มาของปัญหา ตรวจสอบหน้า wiki สำหรับคำแนะนำโดยละเอียด" # AI Translated -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "ปุ่ม Pack จะรวบรวมไฟล์โปรเจกต์และบันทึกของเซสชันปัจจุบันลงในไฟล์ zip" # AI Translated @@ -19836,7 +20072,7 @@ msgid "Stored logs" msgstr "บันทึกที่จัดเก็บ" # AI Translated -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "รวมบันทึกที่จัดเก็บทั้งหมดลงในไฟล์ zip" # AI Translated @@ -19916,7 +20152,7 @@ msgstr "ไม่พบประเภทเครื่องพิมพ์ msgid "Authorizing..." msgstr "กำลังอนุญาต..." -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "ข้อผิดพลาด ไม่สามารถรับโทเคน API สำหรับการอนุญาตได้" msgid "Could not parse server response." @@ -20368,8 +20604,8 @@ msgid "Enable smart filament assign: Assign one filament to multiple nozzles to msgstr "เปิดใช้การกำหนดเส้นพลาสติกอัจฉริยะ: กำหนดเส้นพลาสติกหนึ่งชนิดให้กับหัวฉีดหลายตัวเพื่อประหยัดสูงสุด" # AI Translated -msgid "Fila Saving" -msgstr "การประหยัดเส้นพลาสติก" +msgid "File Saving" +msgstr "การบันทึกไฟล์" msgid "Don't remind me again" msgstr "ไม่ต้องเตือนฉันอีก" @@ -20575,9 +20811,6 @@ msgstr "เกิดสิ่งที่ไม่คาดคิดขณะพ msgid "User canceled." msgstr "ผู้ใช้ยกเลิก" -msgid "Head diameter" -msgstr "เส้นผ่านศูนย์กลางหัว" - msgid "Max angle" msgstr "มุมสูงสุด" @@ -20591,7 +20824,7 @@ msgid "Auto-generate" msgstr "สร้างอัตโนมัติ" msgid "Generate brim ears using Max angle and Detection radius" -msgstr "สร้างหูขอบยึดชิ้นงานนกโดยใช้มุมสูงสุดและรัศมีการตรวจจับ" +msgstr "สร้างหูขอบยึดชิ้นงาน (Brim Ears) โดยใช้มุมสูงสุดและรัศมีการตรวจจับ" msgid "Add or Select" msgstr "เพิ่มหรือเลือก" @@ -20606,7 +20839,7 @@ msgid "invalid brim ears" msgstr "หูขอบยึดชิ้นงานไม่ถูกต้อง" msgid "Brim Ears" -msgstr "หูขอบยึดชิ้นงาน" +msgstr "หูขอบยึดชิ้นงาน (Brim Ears)" msgid "Please select single object." msgstr "กรุณาเลือกวัตถุเดียว" @@ -20737,6 +20970,9 @@ msgstr "เริ่มใหม่ตอนนี้" msgid "NO RAMMING AT ALL" msgstr "ไม่มีการกระแทกเลย" +msgid "s" +msgstr "วินาที" + msgid "Volumetric speed" msgstr "ความเร็วปริมาตร" @@ -21361,6 +21597,55 @@ msgstr "" "หลีกเลี่ยงการบิดเบี้ยว\n" "คุณรู้หรือไม่ว่าเมื่อพิมพ์วัสดุที่มีแนวโน้มที่จะเกิดการบิดเบี้ยว เช่น ABS การเพิ่มอุณหภูมิฐานพิมพ์อย่างเหมาะสมสามารถลดความน่าจะเป็นของการบิดเบี้ยวได้" +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "Native Wayland liveview ต้องใช้ GStreamer GTK video sink โปรดติดตั้งปลั๊กอิน gtksink สำหรับ GStreamer จากนั้นรีสตาร์ท OrcaSlicer" + +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "ไม่สามารถเริ่มต้น sink วิดีโอ Wayland GStreamer ดั้งเดิมได้ โปรดตรวจสอบการติดตั้งปลั๊กอิน GStreamer GTK ของคุณ" + +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "งานนี้ต้องใช้ Windows Media Player! คุณต้องการเปิดใช้งาน 'Windows Media Player' สำหรับระบบปฏิบัติการของคุณหรือไม่?" + +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "BambuSource ยังไม่ได้รับการลงทะเบียนอย่างถูกต้องสำหรับการเล่นสื่อ! กดใช่เพื่อลงทะเบียนใหม่ คุณจะได้รับการเลื่อนตำแหน่งสองครั้ง" + +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "ไม่มีส่วนประกอบ BambuSource ที่ลงทะเบียนสำหรับการเล่นสื่อ! โปรดติดตั้ง OrcaSlicer ใหม่หรือขอความช่วยเหลือจากชุมชน" + +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "การใช้ BambuSource จากการติดตั้งอื่น การเล่นวิดีโออาจทำงานไม่ถูกต้อง! กดใช่เพื่อแก้ไข" + +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "ระบบของคุณไม่มีตัวแปลงสัญญาณ H.264 สำหรับ GStreamer ซึ่งจำเป็นในการเล่นวิดีโอ (ลองติดตั้งแพ็คเกจ gstreamer1.0-plugins-bad หรือ gstreamer1.0-libav จากนั้นรีสตาร์ท Orca Slicer หรือไม่)" + +# AI Translated +#~ msgid "N" +#~ msgstr "N" + +# AI Translated +#~ msgid "g" +#~ msgstr "g" + +# AI Translated +#~ msgid "Fila Saving" +#~ msgstr "การประหยัดเส้นพลาสติก" + +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "ความสูงของเลเยอร์น้อยเกินไป\n" +#~ "มันจะตั้งค่าเป็น min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "ความสูงของเลเยอร์เกินขีดจำกัดในการตั้งค่าเครื่องพิมพ์ -> ชุดดันเส้น -> ขีดจำกัดความสูงของเลเยอร์ ซึ่งอาจทำให้เกิดปัญหาคุณภาพการพิมพ์" + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "ปรับเป็นช่วงที่ตั้งไว้อัตโนมัติ?\n" + +#~ msgid "Head diameter" +#~ msgstr "เส้นผ่านศูนย์กลางหัว" + #~ msgid "Print order within a single layer." #~ msgstr "สั่งพิมพ์ภายในชั้นเดียว" @@ -21572,7 +21857,7 @@ msgstr "" #~ msgstr "เนื้อหาที่ตั้งไว้ล่วงหน้ามีขนาดใหญ่เกินกว่าจะซิงค์กับระบบคลาวด์ (เกิน 1MB) โปรดลดขนาดที่กำหนดไว้ล่วงหน้าโดยการลบการกำหนดค่าที่กำหนดเองออกหรือใช้เฉพาะในเครื่องเท่านั้น" #~ msgid "Enable adaptive pressure advance for overhangs (beta)" -#~ msgstr "เปิดใช้งานการปรับPressure Advanceสำหรับระยะยื่น (เบต้า)" +#~ msgstr "เปิดใช้ Adaptive Pressure Advance สำหรับส่วนยื่น (เบต้า)" #~ msgid "" #~ "Enable adaptive PA for overhangs as well as when flow changes within the same feature. This is an experimental option, as if the PA profile is not set accurately, it will cause uniformity issues on the external surfaces before and after overhangs.\n" @@ -21582,7 +21867,7 @@ msgstr "" #~ "ไม่รองรับเครื่องพิมพ์ Prusa เพราะจะหยุดชั่วคราวเพื่อประมวลผลการเปลี่ยน PA ทำให้เกิดความล่าช้าและข้อบกพร่อง" #~ msgid "Pressure advance for bridges" -#~ msgstr "แรงดันล่วงหน้า (Pressure Advance)สำหรับสะพาน" +#~ msgstr "Pressure Advance สำหรับสะพาน" #~ msgid "" #~ "Pressure advance value for bridges. Set to 0 to disable.\n" diff --git a/localization/i18n/tr/OrcaSlicer_tr.po b/localization/i18n/tr/OrcaSlicer_tr.po index fd1ad673af..0e51b7ca9c 100644 --- a/localization/i18n/tr/OrcaSlicer_tr.po +++ b/localization/i18n/tr/OrcaSlicer_tr.po @@ -3,8 +3,8 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" -"PO-Revision-Date: 2026-08-01 20:32+0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" +"PO-Revision-Date: 2026-08-21 23:18+0300\n" "Last-Translator: GlauTech\n" "Language-Team: \n" "Language: tr\n" @@ -14,27 +14,21 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n==1) ? 0 : 1;\n" "X-Generator: Poedit 3.9\n" -# AI Translated msgid "Main Extruder" msgstr "Ana Ekstruder" -# AI Translated msgid "Main extruder" msgstr "Ana ekstruder" -# AI Translated msgid "main extruder" msgstr "ana ekstruder" -# AI Translated msgid "Auxiliary Extruder" msgstr "Yardımcı Ekstruder" -# AI Translated msgid "Auxiliary extruder" msgstr "Yardımcı ekstruder" -# AI Translated msgid "auxiliary extruder" msgstr "yardımcı ekstruder" @@ -56,27 +50,21 @@ msgstr "Sağ ekstruder" msgid "right extruder" msgstr "sağ ekstruder" -# AI Translated msgid "Main Nozzle" msgstr "Ana Nozul" -# AI Translated msgid "Main nozzle" msgstr "Ana nozul" -# AI Translated msgid "main nozzle" msgstr "ana nozul" -# AI Translated msgid "Auxiliary Nozzle" msgstr "Yardımcı Nozul" -# AI Translated msgid "Auxiliary nozzle" msgstr "Yardımcı nozul" -# AI Translated msgid "auxiliary nozzle" msgstr "yardımcı nozul" @@ -106,59 +94,45 @@ msgstr "Ana Hotend" msgid "Main hotend" msgstr "Ana hotend" -# AI Translated msgid "main hotend" msgstr "ana hotend" -# AI Translated msgid "Auxiliary Hotend" msgstr "Yardımcı Hotend" -# AI Translated msgid "Auxiliary hotend" msgstr "Yardımcı hotend" -# AI Translated msgid "auxiliary hotend" msgstr "yardımcı hotend" -# AI Translated msgid "Left Hotend" msgstr "Sol Hotend" -# AI Translated msgid "Left hotend" msgstr "Sol hotend" -# AI Translated msgid "left hotend" msgstr "sol hotend" -# AI Translated msgid "Right Hotend" msgstr "Sağ Hotend" -# AI Translated msgid "Right hotend" msgstr "Sağ hotend" -# AI Translated msgid "right hotend" msgstr "sağ hotend" -# AI Translated msgid "main" msgstr "ana" -# AI Translated msgid "auxiliary" msgstr "yardımcı" -# AI Translated msgid "Main" msgstr "Ana" -# AI Translated msgid "Auxiliary" msgstr "Yardımcı" @@ -738,9 +712,8 @@ msgstr "Sabit adım sürükleme" msgid "Context Menu" msgstr "Bağlam Menüsü" -# AI Translated msgid "Toggle Auto-Drop" -msgstr "Otomatik Bırakmayı Aç/Kapat" +msgstr "Otomatik düşürmeyi aç / kapat" msgid "Single sided scaling" msgstr "Tek taraflı ölçekleme" @@ -791,9 +764,8 @@ msgstr "Nesne" msgid "Part" msgstr "Parça" -# AI Translated msgid "Relative" -msgstr "Göreli" +msgstr "Göreceli" # AI Translated msgid "Coordinate system used for transform actions." @@ -1239,7 +1211,7 @@ msgid "Text move" msgstr "Metin taşıma" msgid "Set Mirror" -msgstr "Aynayı Ayarla" +msgstr "Aynalamayı ayarla" msgid "Embossed text" msgstr "Kabartmalı metin" @@ -1786,10 +1758,10 @@ msgid "Lock/unlock rotation angle when dragging above the surface." msgstr "Yüzeyin üzerinde sürüklerken dönüş açısını kilitleyin/kilidini açın." msgid "Mirror vertically" -msgstr "Dikey olarak yansıt" +msgstr "Dikey aynala" msgid "Mirror horizontally" -msgstr "Yatay olarak yansıt" +msgstr "Yatay aynala" #. TRN: This is the name of the action that shows in undo/redo stack (changing part type from SVG to something else). msgid "Change SVG Type" @@ -1797,7 +1769,7 @@ msgstr "SVG Türünü Değiştir" #. TRN - Input label. Be short as possible msgid "Mirror" -msgstr "Ayna" +msgstr "Aynala" msgid "Choose SVG file for emboss:" msgstr "Kabartma için SVG dosyasını seçin:" @@ -2074,10 +2046,10 @@ msgid "3MF files" msgstr "3MF dosyaları" msgid "G-code 3MF files" -msgstr "Gcode 3MF dosyaları" +msgstr "G-code 3MF dosyaları" msgid "G-code files" -msgstr "G kodu dosyaları" +msgstr "G-code dosyaları" msgid "Supported files" msgstr "Desteklenen dosyalar" @@ -2306,7 +2278,7 @@ msgid "new or open project file is not allowed during the slicing process!" msgstr "dilimleme işlemi sırasında yeni veya açık proje dosyasına izin verilmez!" msgid "Open Project" -msgstr "Projeyi Aç" +msgstr "Projeyi aç" msgid "The version of Orca Slicer is too low and needs to be updated to the latest version before it can be used normally." msgstr "Orca Slicer'ın sürümü çok düşük ve normal şekilde kullanılabilmesi için en son sürüme güncellenmesi gerekiyor." @@ -2571,7 +2543,7 @@ msgid "Ongoing uploads" msgstr "Devam eden yüklemeler" msgid "Select a G-code file:" -msgstr "G kodu dosyası seçin:" +msgstr "G-code dosyası seçin:" msgid "Could not start URL download. Destination folder is not set. Please choose destination folder in Configuration Wizard." msgstr "URL indirme işlemi başlatılamadı. Hedef klasör ayarlanmamış. Lütfen Yapılandırma Sihirbazı’nda hedef klasörü seçin." @@ -2665,7 +2637,7 @@ msgid "Add Negative Part" msgstr "Negatif parça ekle" msgid "Add Modifier" -msgstr "Değiştirici Ekle" +msgstr "Değiştirici ekle" msgid "Add Support Blocker" msgstr "Destek engelleyici ekle" @@ -2734,16 +2706,15 @@ msgstr "Simit" msgid "Orca Cube" msgstr "Orca Küpü" -# AI Translated msgid "OrcaSliced Combo" -msgstr "OrcaSliced Combo" +msgstr "Orca Dilimleme Paketi" # AI Translated msgid "Orca Badge" msgstr "Orca Rozeti" msgid "Orca Tolerance Test" -msgstr "Orca tolerans testi" +msgstr "Orca Tolerans Testi" msgid "3DBenchy" msgstr "3DBenchy" @@ -2807,17 +2778,16 @@ msgid "Set as Individual Objects" msgstr "Bireysel nesneler olarak ayarla" msgid "Fill bed with copies" -msgstr "Yatağı kopyalarla doldurun" +msgstr "Yatağı kopyalarla doldur" msgid "Fill the remaining area of bed with copies of the selected object" -msgstr "Yatağın kalan alanını seçilen nesnenin kopyalarıyla doldurun" +msgstr "Yatağın kalan alanını seçili nesnenin kopyalarıyla doldur" msgid "Printable" msgstr "Yazdırılabilir" -# AI Translated msgid "Auto Drop" -msgstr "Otomatik Bırakma" +msgstr "Otomatik düşür" # AI Translated msgid "Automatically drops the selected object to the build plate." @@ -2925,19 +2895,19 @@ msgid "Along X Axis" msgstr "X ekseni boyunca" msgid "Mirror along the X Axis" -msgstr "X ekseni boyunca aynalama" +msgstr "X ekseni boyunca aynala" msgid "Along Y Axis" msgstr "Y ekseni boyunca" msgid "Mirror along the Y Axis" -msgstr "Y ekseni boyunca aynalama" +msgstr "Y ekseni boyunca aynala" msgid "Along Z Axis" msgstr "Z ekseni boyunca" msgid "Mirror along the Z Axis" -msgstr "Z ekseni boyunca aynalama" +msgstr "Z ekseni boyunca aynala" msgid "Mirror object" msgstr "Nesneyi aynala" @@ -2967,7 +2937,7 @@ msgid "Add Models" msgstr "Model ekle" msgid "Show Labels" -msgstr "Etiketleri Göster" +msgstr "Etiketleri göster" msgid "To Objects" msgstr "Nesnelere" @@ -2999,6 +2969,10 @@ msgstr "Düzenle" msgid "Merge with" msgstr "Şununla birleştir" +# AI Translated +msgid "Decompose Color" +msgstr "Rengi ayrıştır" + msgid "Delete this filament" msgstr "Bu filamenti sil" @@ -3009,7 +2983,7 @@ msgid "Select all objects on the current plate" msgstr "Mevcut plakadaki tüm nesneleri seç" msgid "Select All Plates" -msgstr "Tüm Plakaları Seç" +msgstr "Tüm plakaları seç" msgid "Select all objects on all plates" msgstr "Tüm plakalardaki tüm nesneleri seç" @@ -3045,28 +3019,28 @@ msgid "Remove the selected plate" msgstr "Seçilen plakayı kaldır" msgid "Add instance" -msgstr "Örnek ekle" +msgstr "Eş kopya ekle" msgid "Add one more instance of the selected object" -msgstr "Seçilen nesnenin bir örneğini daha ekle" +msgstr "Seçili nesneye bir eş kopya ekle" msgid "Remove instance" -msgstr "Örneği kaldır" +msgstr "Eş kopyayı kaldır" msgid "Remove one instance of the selected object" -msgstr "Seçilen nesnenin bir örneğini kaldır" +msgstr "Seçili nesnenin bir eş kopyasını kaldır" msgid "Set number of instances" -msgstr "Örnek sayısını ayarlayın" +msgstr "Eş kopya sayısını ayarla" msgid "Change the number of instances of the selected object" -msgstr "Seçilen nesnenin örnek sayısını değiştirme" +msgstr "Seçili nesnenin eş kopya sayısını değiştir" msgid "Fill bed with instances" -msgstr "Yatağı örneklerle doldurun" +msgstr "Yatağı eş kopyalarla doldur" msgid "Fill the remaining area of bed with instances of the selected object" -msgstr "Yatağın kalan alanını seçilen nesnenin örnekleriyle doldurun" +msgstr "Yatağın kalan alanını seçili nesnenin eş kopyalarıyla doldur" msgid "Clone" msgstr "Klon oluştur" @@ -3075,7 +3049,7 @@ msgid "Simplify Model" msgstr "Modeli basitleştir" msgid "Subdivision mesh" -msgstr "Alt bölüm ağı" +msgstr "Poligon artırma" msgid "(Lost color)" msgstr "(Renk kaybı)" @@ -3090,10 +3064,10 @@ msgid "Edit Process Settings" msgstr "İşlem ayarlarını düzenle" msgid "Copy Process Settings" -msgstr "İşlem Ayarlarını Kopyala" +msgstr "İşlem ayarlarını kopyala" msgid "Paste Process Settings" -msgstr "İşlem Ayarlarını Yapıştır" +msgstr "İşlem ayarlarını yapıştır" msgid "Edit print parameters for a single object" msgstr "Tek bir nesne için yazdırma parametrelerini düzenleme" @@ -3299,6 +3273,10 @@ msgstr "Birleştir" msgid "Merge parts to an object" msgstr "Parçaları bir nesnede birleştir" +# AI Translated +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "Değişken katman yüksekliğinin karışık renk alt katmanı ile birlikte kullanılması renk karışımının kalitesini düşürebilir." + # AI Translated msgid "Add layers" msgstr "Katman ekle" @@ -3325,7 +3303,7 @@ msgid "Part manipulation" msgstr "Parça manipülasyonu" msgid "Instance manipulation" -msgstr "Örnek manipülasyonu" +msgstr "Eş kopya manipülasyonu" msgid "Height ranges" msgstr "Yükseklik aralıkları" @@ -3361,7 +3339,7 @@ msgstr "Parça tipini seçin" # AI Translated msgid "Instances to Separated Objects" -msgstr "Örnekleri Ayrı Nesnelere Dönüştür" +msgstr "Eş Kopyaları Ayrı Nesnelere Dönüştür" msgid "Enter new name" msgstr "Yeni adı girin" @@ -3478,7 +3456,7 @@ msgid "More" msgstr "Daha" msgid "Open Preferences" -msgstr "Tercihleri Aç" +msgstr "Tercihleri aç" msgid "Open next tip" msgstr "Sonraki ipucunu aç" @@ -3505,13 +3483,13 @@ msgid "Custom Template:" msgstr "Özel Şablon:" msgid "Custom G-code:" -msgstr "Özel G kodu:" +msgstr "Özel G-code:" msgid "Custom G-code" -msgstr "Özel G kodu" +msgstr "Özel G-code" msgid "Enter Custom G-code used on current layer:" -msgstr "Geçerli katmanda kullanılan Özel G kodunu girin:" +msgstr "Geçerli katmanda kullanılan Özel G-code'u girin:" msgid "Jump to layer" msgstr "Katmana Atla" @@ -3526,16 +3504,16 @@ msgid "Insert a pause command at the beginning of this layer." msgstr "Bu katmanın başına bir duraklatma komutu ekleyin." msgid "Add Custom G-code" -msgstr "Özel G Kodu Ekle" +msgstr "Özel G-code Ekle" msgid "Insert custom G-code at the beginning of this layer." -msgstr "Bu katmanın başına özel G kodunu ekleyin." +msgstr "Bu katmanın başına özel G-code'u ekleyin." msgid "Add Custom Template" msgstr "Özel Şablon Ekle" msgid "Insert template custom G-code at the beginning of this layer." -msgstr "Bu katmanın başlangıcına şablon özel G kodunu ekleyin." +msgstr "Bu katmanın başlangıcına şablon özel G-code'u ekleyin." # AI Translated msgid "Filament " @@ -3551,10 +3529,10 @@ msgid "Delete Custom Template" msgstr "Özel Şablonu Sil" msgid "Edit Custom G-code" -msgstr "Özel G Kodunu Düzenle" +msgstr "Özel G-code'u Düzenle" msgid "Delete Custom G-code" -msgstr "Özel G Kodunu Sil" +msgstr "Özel G-code'u Sil" msgid "Delete Filament Change" msgstr "Filament Değişikliğini Sil" @@ -4069,10 +4047,10 @@ msgid "Encountered an unknown error with the Storage status. Please try again." msgstr "Depolama durumuyla ilgili bilinmeyen bir hatayla karşılaşıldı. Lütfen tekrar deneyin." msgid "Sending G-code file over LAN" -msgstr "LAN üzerinden gcode dosyası gönderiliyor" +msgstr "LAN üzerinden G-code dosyası gönderiliyor" msgid "Sending G-code file to SD card" -msgstr "Gcode dosyası sdcard'a gönderiliyor" +msgstr "G-code dosyası sdcard'a gönderiliyor" #, c-format, boost-format msgid "Successfully sent. Close current page in %s s" @@ -4082,7 +4060,7 @@ msgid "Storage needs to be inserted before sending to printer." msgstr "Yazıcıya göndermeden önce depolama biriminin eklenmesi gerekir." msgid "Sending G-code file over LAN, but the Storage in the printer is abnormal and print-issues may be caused by this." -msgstr "G kodu dosyası LAN üzerinden gönderiliyor ancak yazıcıdaki Depolama anormal ve yazdırma sorunları bundan kaynaklanabilir." +msgstr "G-code dosyası LAN üzerinden gönderiliyor ancak yazıcıdaki Depolama anormal ve yazdırma sorunları bundan kaynaklanabilir." msgid "The Storage in the printer is abnormal. Please replace it with a normal Storage before sending to printer." msgstr "Yazıcıdaki Depolama anormal. Lütfen yazıcıya göndermeden önce normal bir Depolama ile değiştirin." @@ -4622,7 +4600,7 @@ msgid "Please save your project and restart the application." msgstr "Lütfen projeyi kaydedin ve programı yeniden başlatın." msgid "Processing G-Code from previous file…" -msgstr "Önceki dosyadan G-Kodu işleniyor…" +msgstr "Önceki dosyadan G-code işleniyor…" msgid "Slicing complete" msgstr "Dilimleme tamamlandı" @@ -4655,35 +4633,35 @@ msgid "Successfully executed post-processing script" msgstr "İşlem sonrası komut dosyası başarıyla çalıştırıldı" msgid "Unknown error occurred during exporting G-code." -msgstr "G kodu dışa aktarılırken bilinmeyen bir hata oluştu." +msgstr "G-code dışa aktarılırken bilinmeyen bir hata oluştu." #, boost-format msgid "" "Copying of the temporary G-code to the output G-code failed. Maybe the SD card is write locked?\n" "Error message: %1%" msgstr "" -"Geçici G kodunun çıkış G koduna kopyalanması başarısız oldu. Belki SD kart yazma kilitlidir.\n" +"Geçici G-code'un çıkış G-code'a kopyalanması başarısız oldu. Belki SD kart yazma kilitlidir.\n" "Hata mesajı: %1%" #, boost-format msgid "Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp." -msgstr "Geçici G kodunun çıkış G koduna kopyalanması başarısız oldu. Hedef cihazda sorun olabilir, lütfen tekrar dışa aktarmayı veya farklı bir cihaz kullanmayı deneyin. Bozuk çıktı G kodu %1%.tmp konumunda." +msgstr "Geçici G-code'un çıkış G-code'a kopyalanması başarısız oldu. Hedef cihazda sorun olabilir, lütfen tekrar dışa aktarmayı veya farklı bir cihaz kullanmayı deneyin. Bozuk çıktı G-code %1%.tmp konumunda." #, boost-format msgid "Renaming of the G-code after copying to the selected destination folder has failed. Current path is %1%.tmp. Please try exporting again." -msgstr "Seçilen hedef klasöre kopyalandıktan sonra G kodunun yeniden adlandırılması başarısız oldu. Geçerli yol: %1%.tmp. Lütfen dışa aktarmayı tekrar deneyin." +msgstr "Seçilen hedef klasöre kopyalandıktan sonra G-code'un yeniden adlandırılması başarısız oldu. Geçerli yol: %1%.tmp. Lütfen dışa aktarmayı tekrar deneyin." #, boost-format msgid "Copying of the temporary G-code has finished but the original code at %1% couldn't be opened during copy check. The output G-code is at %2%.tmp." -msgstr "Geçici G kodunun kopyalanması tamamlandı ancak %1% konumundaki orijinal kod kopyalama kontrolü sırasında açılamadı. Çıkış G kodu %2%.tmp konumundadır." +msgstr "Geçici G-code'un kopyalanması tamamlandı ancak %1% konumundaki orijinal kod kopyalama kontrolü sırasında açılamadı. Çıkış G-code %2%.tmp konumundadır." #, boost-format msgid "Copying of the temporary G-code has finished but the exported code couldn't be opened during copy check. The output G-code is at %1%.tmp." -msgstr "Geçici G kodunun kopyalanması tamamlandı ancak kopya kontrolü sırasında dışa aktarılan kod açılamadı. Çıkış G kodu %1%.tmp konumundadır." +msgstr "Geçici G-code'un kopyalanması tamamlandı ancak kopya kontrolü sırasında dışa aktarılan kod açılamadı. Çıkış G-code %1%.tmp konumundadır." #, boost-format msgid "G-code file exported to %1%" -msgstr "G kodu dosyası %1%’e aktarıldı" +msgstr "G-code dosyası %1%’e aktarıldı" msgid "Unknown error with G-code export" msgstr "G-code dışa aktarımında bilinmeyen hata" @@ -4694,12 +4672,12 @@ msgid "" "Error message: %1%.\n" "Source file %2%." msgstr "" -"Gcode dosyası kaydedilemedi.\n" +"G-code dosyası kaydedilemedi.\n" "Hata mesajı: %1%.\n" "Kaynak dosya %2%." msgid "Copying of the temporary G-code to the output G-code failed." -msgstr "Geçici G-kodu dosyasının çıktı G-kodu dosyasına kopyalanması başarısız oldu." +msgstr "Geçici G-code dosyasının çıktı G-code dosyasına kopyalanması başarısız oldu." #, boost-format msgid "Scheduling upload to `%1%`. See Window -> Print Host Upload Queue" @@ -4712,7 +4690,7 @@ msgid "Size in X and Y of the rectangular plate." msgstr "Dikdörtgen plakanın X ve Y boyutları." msgid "Distance of the 0,0 G-code coordinate from the front left corner of the rectangle." -msgstr "0,0 G kodu koordinatının dikdörtgenin sol ön köşesinden uzaklığı." +msgstr "0,0 G-code koordinatının dikdörtgenin sol ön köşesinden uzaklığı." msgid "Diameter of the print bed. It is assumed that origin (0,0) is located in the center." msgstr "Baskı yatağının çapı. Orjinin (0,0) merkezde olduğu varsayılmaktadır." @@ -4812,6 +4790,23 @@ msgstr "Mevcut hazne sıcaklığı malzemenin güvenli sıcaklığından yüksek msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Minimum oda sıcaklığı (%d℃), hedef oda sıcaklığından (%d℃) yüksek. Minimum değer, oda hedefe doğru ısınmaya devam ederken baskının başladığı eşiktir; bu nedenle hedefi aşmamalıdır. Değer hedefe sınırlandırılacak." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "Katman yüksekliği çok küçük. Minimum değere (%g mm) ayarlanacak." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "Katman yüksekliği, Yazıcı Ayarları -> Ekstruder -> Katman yüksekliği sınırları bölümünde ayarlanan sınırların dışında, bu durum baskı kalitesi sorunlarına neden olabilir." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Otomatik olarak sınır değerine (%g mm) ayarlansın mı?" + +msgid "Adjust" +msgstr "Ayarla" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4860,7 +4855,7 @@ msgstr "" "\n" "Değer 0'a sıfırlanacaktır." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Alternatif ekstra duvar, dikey kabuk kalınlığının Tümü olarak ayarlandığından emin olunduğunda iyi çalışmaz." msgid "" @@ -4906,7 +4901,7 @@ msgstr "" "HAYIR - Bağımsız Destek Katmanı Yüksekliğini Koruyun" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height, layer_height değerinden küçük olmalıdır.\n" @@ -4914,7 +4909,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "Kilit derinliği cilt derinliğinden daha küçük olmalıdır.\n" @@ -4932,6 +4927,13 @@ msgstr "" "Evet - Arachne Duvarı Oluşturucusunu Etkinleştir\n" "Hayır - Arachne Duvarı Oluşturucusunu Devre Dışı Bırak ve Pütürlü Yüzey [Yer Değiştirme] modunu ayarla" +# AI Translated +msgid "Brim ear radius" +msgstr "Kenar kulak yarıçapı" + +msgid "Brim width" +msgstr "Kenar genişliği" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Spiral mod yalnızca duvar döngüleri 1 olduğunda, destek devre dışı bırakıldığında, problama yoluyla topaklanma tespiti devre dışı bırakıldığında, üst kabuk katmanları 0 olduğunda, seyrek dolgu yoğunluğu 0 olduğunda ve hızlandırılmış tip geleneksel olduğunda çalışır." @@ -5038,7 +5040,7 @@ msgid "Cooling chamber" msgstr "Soğutma haznesi" msgid "Pause (G-code inserted by user)" -msgstr "Duraklat (Kullanıcı tarafından eklenen G kodu)" +msgstr "Duraklat (Kullanıcı tarafından eklenen G-code)" msgid "Motor noise showoff" msgstr "Motor gürültü gösterimi" @@ -5186,6 +5188,14 @@ msgstr "Cali G-code oluşturma başarısız oldu" msgid "Calibration error" msgstr "Kalibrasyon hatası" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Bu yazıcı, bu denetimin ihtiyaç duyduğu donanımla yapılandırılmamış." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Bu denetim bu yazıcıda desteklenmiyor." + # AI Translated msgid "Network unavailable" msgstr "Ağ kullanılamıyor" @@ -5278,16 +5288,16 @@ msgstr "varsayılan" #, boost-format msgid "Edit Custom G-code (%1%)" -msgstr "Özel G Kodunu Düzenle (%1%)" +msgstr "Özel G-code'u Düzenle (%1%)" msgid "Built-in placeholders (Double click item to add to G-code)" -msgstr "Yerleşik yer tutucular (G koduna eklemek için öğeye çift tıklayın)" +msgstr "Yerleşik yer tutucular (G-code'a eklemek için öğeye çift tıklayın)" msgid "Search G-code placeholders" -msgstr "Gcode yer tutucularını arayın" +msgstr "G-code yer tutucularını arayın" msgid "Add selected placeholder to G-code" -msgstr "Seçili yer tutucuyu G koduna ekle" +msgstr "Seçili yer tutucuyu G-code'a ekle" msgid "Select placeholder" msgstr "Yer tutucuyu seçin" @@ -5450,10 +5460,10 @@ msgid "Acceleration" msgstr "Hızlanma" msgid "Jerk" -msgstr "Jerk" +msgstr "Sarsıntı" msgid "Fan Speed" -msgstr "Fan hızı" +msgstr "Fan Hızı" msgid "Flow" msgstr "Akış" @@ -5468,7 +5478,7 @@ msgid "Layer Time" msgstr "Katman Süresi" msgid "Layer Time (log)" -msgstr "Katman Süresi (günlük)" +msgstr "Katman Süresi (log)" msgid "Pressure Advance" msgstr "Basınç İlerlemesi" @@ -5477,10 +5487,10 @@ msgid "Noop" msgstr "Hayır" msgid "Retract" -msgstr "Geri Çekme" +msgstr "Geri çekme" msgid "Unretract" -msgstr "İleri İtme" +msgstr "İleri itme" msgid "Seam" msgstr "Dikiş" @@ -5578,7 +5588,7 @@ msgid "Acceleration: " msgstr "İvme: " msgid "Jerk: " -msgstr "Jerk: " +msgstr "Sarsıntı: " msgid "PA: " msgstr "PA: " @@ -5608,7 +5618,7 @@ msgid "Actual speed profile" msgstr "Gerçek hız profili" msgid "Statistics of All Plates" -msgstr "Tüm Plakaların İstatistikleri" +msgstr "Tüm plakaların istatistikleri" msgid "Display" msgstr "Ekran" @@ -5708,7 +5718,7 @@ msgid "Acceleration (mm/s²)" msgstr "İvme (mm/s²)" msgid "Jerk (mm/s)" -msgstr "Jerk (mm/s)" +msgstr "Sarsıntı (mm/s)" msgid "Fan speed (%)" msgstr "Fan hızı (%)" @@ -5759,9 +5769,8 @@ msgstr "Normal mod" msgid "Total Filament" msgstr "Toplam filament" -# AI Translated msgid "Model Filament" -msgstr "Model Filamenti" +msgstr "Model filamenti" msgid "Prepare time" msgstr "Hazırlık süresi" @@ -6050,18 +6059,18 @@ msgstr "Hacim:" msgid "Size:" msgstr "Boyut:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." -msgstr "%d katmanında gcode yollarında çakışmalar bulundu, Z = %.2lfmm. Lütfen çakışan nesneleri daha uzağa ayırın (%s <-> %s)." +msgstr "%d katmanında G-code yollarında çakışmalar bulundu, Z = %.2lfmm. Lütfen çakışan nesneleri daha uzağa ayırın (%s <-> %s)." msgid "An object is laid over the plate boundaries." msgstr "Plakanın sınırına bir nesne serilir." msgid "A G-code path goes beyond the max print height." -msgstr "Bir G kodu yolu maksimum baskı yüksekliğinin ötesine geçer." +msgstr "Bir G-code yolu maksimum baskı yüksekliğinin ötesine geçer." msgid "A G-code path goes beyond plate boundaries." -msgstr "Bir G kodu yolu plakanın sınırlarının ötesine geçer." +msgstr "Bir G-code yolu plakanın sınırlarının ötesine geçer." msgid "Not support printing 2 or more TPU filaments." msgstr "2 veya daha fazla TPU filamentinin yazdırılmasını desteklemez." @@ -6072,19 +6081,19 @@ msgstr "Araç %d" #, c-format, boost-format msgid "Filament %s is placed in the %s, but the generated G-code path exceeds the printable range of the %s." -msgstr "%s filamenti %s içine yerleştirildi, ancak oluşturulan G kodu yolu %s'nin yazdırılabilir aralığını aşıyor." +msgstr "%s filamenti %s içine yerleştirildi, ancak oluşturulan G-code yolu %s'nin yazdırılabilir aralığını aşıyor." #, c-format, boost-format msgid "Filaments %s are placed in the %s, but the generated G-code path exceeds the printable range of the %s." -msgstr "%s filamentleri %s içine yerleştirildi, ancak oluşturulan G kodu yolu %s'nin yazdırılabilir aralığını aşıyor." +msgstr "%s filamentleri %s içine yerleştirildi, ancak oluşturulan G-code yolu %s'nin yazdırılabilir aralığını aşıyor." #, c-format, boost-format msgid "Filament %s is placed in the %s, but the generated G-code path exceeds the printable height of the %s." -msgstr "%s filamenti %s'e yerleştirildi, ancak oluşturulan G kodu yolu %s'nin yazdırılabilir yüksekliğini aşıyor." +msgstr "%s filamenti %s'e yerleştirildi, ancak oluşturulan G-code yolu %s'nin yazdırılabilir yüksekliğini aşıyor." #, c-format, boost-format msgid "Filaments %s are placed in the %s, but the generated G-code path exceeds the printable height of the %s." -msgstr "%s filamentleri %s'e yerleştirildi, ancak oluşturulan G kodu yolu %s'nin yazdırılabilir yüksekliğini aşıyor." +msgstr "%s filamentleri %s'e yerleştirildi, ancak oluşturulan G-code yolu %s'nin yazdırılabilir yüksekliğini aşıyor." msgid "Open wiki for more information." msgstr "Daha fazla bilgi için wiki'yi açın." @@ -6232,6 +6241,10 @@ msgstr "Çoklu cihaz" msgid "Project" msgstr "Proje" +# AI Translated +msgid "Device (Web)" +msgstr "Cihaz (Web)" + msgid "Yes" msgstr "Evet" @@ -6248,7 +6261,7 @@ msgid "Print plate" msgstr "Plakayı Yazdır" msgid "Export G-code file" -msgstr "G-kod dosyasını dışa aktar" +msgstr "G-code dosyasını dışa aktar" msgctxt "Verb" msgid "Print" @@ -6282,20 +6295,19 @@ msgid "Setup Wizard" msgstr "Kurulum sihirbazı" msgid "Show Configuration Folder" -msgstr "Yapılandırma Klasörünü Göster" +msgstr "Yapılandırma klasörünü göster" -# AI Translated msgid "Troubleshoot Center" -msgstr "Sorun Giderme Merkezi" +msgstr "Sorun giderme merkezi" msgid "Open Network Test" -msgstr "Ağ Testini Aç" +msgstr "Ağ testini aç" msgid "Show Tip of the Day" -msgstr "Günün İpucunu Göster" +msgstr "Günün ipucunu göster" msgid "Check for Updates" -msgstr "Güncellemeleri Kontrol Et" +msgstr "Güncellemeleri kontrol et" #, c-format, boost-format msgid "&About %s" @@ -6349,7 +6361,7 @@ msgid "Recent files" msgstr "Son dosyalar" msgid "Save Project" -msgstr "Projeyi Kaydet" +msgstr "Projeyi kaydet" msgid "Save current project to file" msgstr "Mevcut projeyi dosyaya kaydet" @@ -6366,10 +6378,10 @@ msgstr "3MF/STL/STEP/SVG/OBJ/AMF'yi içe aktar" msgid "Load a model" msgstr "Model yükle" -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "Zip Arşivini İçe Aktar" -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "Zip arşivindeki modelleri yükle" msgid "Import Configs" @@ -6406,22 +6418,22 @@ msgid "Export all plate sliced file" msgstr "Dilimlenmiş tüm plaka dosyalarını dışa aktar" msgid "Export G-code" -msgstr "G-kodunu dışa aktar" +msgstr "G-code'u dışa aktar" msgid "Export current plate as G-code" -msgstr "Geçerli plakayı G kodu olarak dışa aktar" +msgstr "Geçerli plakayı G-code olarak dışa aktar" msgid "Export toolpaths as OBJ" msgstr "Takımyollarını OBJ olarak dışa aktar" msgid "Export Preset Bundle" -msgstr "Ön Ayar Paketini Dışa Aktar" +msgstr "Ön ayar paketini dışa aktar" msgid "Export current configuration to files" msgstr "Geçerli yapılandırmayı dosyalara aktar" msgid "Export" -msgstr "Dışa Aktar" +msgstr "Dışa aktar" msgid "Quit" msgstr "Çıkış" @@ -6478,13 +6490,13 @@ msgid "Deselects all objects" msgstr "Tüm nesnelerin seçimini kaldırır" msgid "Use Perspective View" -msgstr "Perspektif Görünüm" +msgstr "Perspektif görünüm" msgid "Use Orthogonal View" -msgstr "Ortogonal Görünüm" +msgstr "Ortogonal görünüm" msgid "Auto Perspective" -msgstr "Otomatik Perspektif" +msgstr "Otomatik perspektif" msgid "Automatically switch between orthographic and perspective when changing from top/bottom/side views." msgstr "Üst/Alt/Yan görünümler arasında geçiş yaparken ortografik ve perspektif arasında otomatik olarak geçiş yapın." @@ -6493,40 +6505,40 @@ msgid "Show &G-code Window" msgstr "&G-code Penceresini Göster" msgid "Show G-code window in Preview scene." -msgstr "Previce sahnesinde G-kodu penceresini göster." +msgstr "Previce sahnesinde G-code penceresini göster." msgid "Show 3D Navigator" -msgstr "3D Gezgini Göster" +msgstr "3D gezgini göster" msgid "Show 3D navigator in Prepare and Preview scene." msgstr "Hazırlama ve Önizleme sahnesinde 3D gezgini göster." msgid "Show Gridlines" -msgstr "Kılavuz Çizgilerini Göster" +msgstr "Kılavuz çizgilerini göster" msgid "Show Gridlines on plate" msgstr "Kılavuz Çizgilerini plaka üzerinde göster" msgid "Reset Window Layout" -msgstr "Pencere Düzenini Sıfırla" +msgstr "Pencere düzenini sıfırla" msgid "Reset to default window layout" msgstr "Varsayılan pencere düzenine sıfırla" msgid "Show &Labels" -msgstr "Etiketleri Göster" +msgstr "Etiketleri göster" msgid "Show object labels in 3D scene." msgstr "3B sahnede nesne etiketlerini göster." msgid "Show &Overhang" -msgstr "Çıkıntıyı Göster" +msgstr "Çıkıntıyı göster" msgid "Show object overhang highlight in 3D scene." msgstr "3B sahnede nesne çıkıntısı vurgusunu göster." msgid "Show Selected Outline (beta)" -msgstr "Seçilen Taslağı Göster (Deneysel)" +msgstr "Seçilen taslağı göster (deneysel)" msgid "Show outline around selected object in 3D scene." msgstr "3D sahnede seçilen nesnenin etrafındaki ana hatları göster." @@ -6542,13 +6554,11 @@ msgstr "Düzen" msgid "View" msgstr "Görünüm" -# AI Translated msgid "Preset Bundle" -msgstr "Ön Ayar Paketi" +msgstr "Ön ayar paketi" -# AI Translated msgid "Sync Presets" -msgstr "Ön Ayarları Eşitle" +msgstr "Ön ayarları eşitle" # AI Translated msgid "Pull and apply the latest presets from OrcaCloud" @@ -6590,10 +6600,10 @@ msgid "Cornering calibration" msgstr "Viraj kalibrasyonu" msgid "Input Shaping Frequency" -msgstr "Input shaping Frekansı" +msgstr "Input shaping frekansı" msgid "Input Shaping Damping/zeta factor" -msgstr "Input shaping Sönümleme/zeta faktörü" +msgstr "Input shaping sönümleme/zeta faktörü" msgid "Input Shaping" msgstr "Input shaping" @@ -6601,15 +6611,14 @@ msgstr "Input shaping" msgid "VFA" msgstr "VFA" -# AI Translated msgid "Calibration Guide" -msgstr "Kalibrasyon Kılavuzu" +msgstr "Kalibrasyon kılavuzu" msgid "&Open G-code" -msgstr "&G kodunu aç" +msgstr "&G-code'u aç" msgid "Open a G-code file" -msgstr "G kodu dosyası aç" +msgstr "G-code dosyası aç" msgid "Re&load from Disk" msgstr "Diskten yeniden yükle" @@ -6900,7 +6909,7 @@ msgid "Failed to parse model information." msgstr "Model bilgileri ayrıştırılamadı." msgid "The .gcode.3mf file contains no G-code data. Please slice it with Orca Slicer and export a new .gcode.3mf file." -msgstr ".gcode.3mf dosyası hiçbir G kodu verisi içermiyor. Lütfen dosyayı Bambu Studio ile dilimleyin ve yeni bir .gcode.3mf dosyasını dışa aktarın." +msgstr ".gcode.3mf dosyası hiçbir G-code verisi içermiyor. Lütfen dosyayı Bambu Studio ile dilimleyin ve yeni bir .gcode.3mf dosyasını dışa aktarın." #, c-format, boost-format msgid "File '%s' was lost! Please download it again." @@ -7602,7 +7611,7 @@ msgid "Your model needs support! Please enable support material." msgstr "Modelinizin desteğe ihtiyacı var! Lütfen destek materyalini etkinleştirin." msgid "G-code path overlap" -msgstr "Gcode yolu çakışması" +msgstr "G-code yolu çakışması" msgid "Cut connectors" msgstr "Konektörleri kes" @@ -7896,6 +7905,10 @@ msgstr "Mevcut plakayı özelleştir" msgid "The %s nozzle can not print %s." msgstr "%s püskürtme ucu %s'yi yazdıramıyor." +# AI Translated +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "Karışık renkli filamenti tek ekstruderli bir yazıcıda basmak sık filament değişimi ve yıkama gerektirir; bu da atığı ve nozul ya da atık oluğu tıkanma riskini önemli ölçüde artırabilir." + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "Yazdırmada %1% ile %2%'nin karıştırılması önerilmez.\n" @@ -8021,12 +8034,44 @@ msgstr "Filament listesini AMS'den senkronize edin" msgid "Set filaments to use" msgstr "Kullanılacak filamentleri ayarla" +# AI Translated +msgid "Add Mixed Filament" +msgstr "Karışık Filament Ekle" + +# AI Translated +msgid "Mixed Filament" +msgstr "Karışık Filament" + +# AI Translated +msgid "Remove last mixed filament" +msgstr "Son karışık filamenti kaldır" + +# AI Translated +msgid "Add mixed filament" +msgstr "Karışık filament ekle" + +# AI Translated +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "Karışık filamentte geçersiz veya uyumsuz bileşenler var. Lütfen etkilenen girdileri yeniden düzenleyin." + msgid "Search plate, object and part." msgstr "Arama plakası, nesne ve parça." msgid "Pellets" msgstr "Peletler" +# AI Translated +msgid "Mixed filament has broken component references" +msgstr "Karışık filamentin bileşen referansları bozuk" + +# AI Translated +msgid "Edit / Delete / Merge" +msgstr "Düzenle / Sil / Birleştir" + +# AI Translated +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "Hedef karışık filament bu fiziksel filamenti bileşen olarak kullanıyor. Birleştirme bu fiziksel filamenti kaldıracak ve karışık filamenti geçersiz kılabilir. Devam edilsin mi?" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "İşleminizi tamamladıktan sonra %s projesi kapatılacak ve yeni bir proje oluşturulacak." @@ -8153,25 +8198,25 @@ msgid "Please correct them in the Param tabs" msgstr "Lütfen bunları parametre sekmelerinde düzeltin" msgid "The 3MF has the following modified G-code in filament or printer presets:" -msgstr "3mf dosyasında filament veya yazıcı ön ayarlarında şu değiştirilmiş G-kodları bulunmaktadır:" +msgstr "3mf dosyasında filament veya yazıcı ön ayarlarında şu değiştirilmiş G-code'ları bulunmaktadır:" msgid "Please confirm that all modified G-code is safe to prevent any damage to the machine!" -msgstr "Lütfen bu değiştirilmiş G-kodlarının makineye herhangi bir zarar vermemesi için güvenli olduğunu onaylayın!" +msgstr "Lütfen bu değiştirilmiş G-code'larının makineye herhangi bir zarar vermemesi için güvenli olduğunu onaylayın!" msgid "Modified G-code" -msgstr "G-kodları Değişti" +msgstr "G-code'ları Değişti" msgid "The 3MF has the following customized filament or printer presets:" msgstr "3mf dosyasında şu özel filament veya yazıcı ayarları bulunmaktadır:" msgid "Please confirm that the G-code within these presets is safe to prevent any damage to the machine!" -msgstr "Lütfen bu ön ayarlar içindeki G-kodlarının makineye herhangi bir zararı önlemek için güvenli olduğunu onaylayın!" +msgstr "Lütfen bu ön ayarlar içindeki G-code'larının makineye herhangi bir zararı önlemek için güvenli olduğunu onaylayın!" msgid "Customized Preset" msgstr "Özel Ayar" -msgid "Component name(s) inside step file not in UTF8 format!" -msgstr "Step dosyasındaki bileşenlerin adı UTF8 formatında değil!" +msgid "Component name(s) inside step file not in UTF-8 format!" +msgstr "Step dosyasındaki bileşenlerin adı UTF-8 formatında değil!" # AI Translated msgid "Because of unsupported text encoding, garbage characters may appear!" @@ -8193,10 +8238,10 @@ msgstr "Cismin hacmi sıfır" #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "%s dosyasındaki nesne çok küçük; metre veya inç cinsinden olabilir.\n" -" Milimetreye ölçeklendirmek istiyor musunuz?" +"Milimetreye ölçeklendirmek istiyor musunuz?" msgid "Object too small" msgstr "Nesne çok küçük" @@ -8213,15 +8258,22 @@ msgstr "" msgid "Multi-part object detected" msgstr "Çok parçalı nesne algılandı" +# AI Translated +msgid "Matching textures to filaments" +msgstr "Dokular filamentlerle eşleştiriliyor" + +# AI Translated +msgid "Texture Import Warning" +msgstr "Doku İçe Aktarma Uyarısı" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Bu dosyalar birden fazla parçadan oluşan tek bir nesne olarak mı yüklensin?\n" msgid "An object with multiple parts was detected" msgstr "Birden fazla parçaya sahip nesne algılandı" -# AI Translated msgid "Auto-Drop" -msgstr "Otomatik Bırakma" +msgstr "Otomatik düşür" #, c-format, boost-format msgid "Connected printer is %s. It must match the project preset for printing.\n" @@ -8296,9 +8348,8 @@ msgstr "Seçilen nesne bölünemedi." msgid "Split to Objects" msgstr "Nesnelere Ayır" -# AI Translated msgid "Disable Auto-Drop to preserve Z positioning?\n" -msgstr "Z konumunu korumak için Otomatik Bırakma devre dışı bırakılsın mı?\n" +msgstr "Z konumunu korumak için Otomatik düşürme devre dışı bırakılsın mı?\n" # AI Translated msgid "Object with floating parts was detected" @@ -8335,21 +8386,21 @@ msgstr "Değiştirme için dizin seçilmedi" msgid "Replaced with 3D files from directory:\n" msgstr "Dizindeki 3D dosyalarla değiştirildi:\n" -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ %1% atlandı: aynı dosya.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ %s atlandı: aynı dosya.\n" -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ %1% atlandı: dosya mevcut değil.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ %s atlandı: dosya mevcut değil.\n" -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ %1% atlandı: değiştirilemedi.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ %s atlandı: değiştirilemedi.\n" -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔ %1% değiştirildi.\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔ %s değiştirildi.\n" msgid "Replaced volumes" msgstr "Değiştirilen birimler" @@ -8412,7 +8463,7 @@ msgid "" "The loaded file contains G-code only, cannot enter the Prepare page." msgstr "" "Yalnızca önizleme modu:\n" -"Yüklenen dosya yalnızca Gcode içeriyor, hazırlama sayfasına girilemiyor." +"Yüklenen dosya yalnızca G-code içeriyor, hazırlama sayfasına girilemiyor." msgid "" "The nozzle type and AMS quantity information has not been synced from the connected printer.\n" @@ -8426,6 +8477,22 @@ msgstr "" msgid "Sync now" msgstr "Şimdi senkronize et" +# AI Translated +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "Doku içe aktarılamadı. Model doku verisi içeriyor gibi görünüyor, ancak içe aktarma işlemi tamamlanamadı. Model yalnızca geometri olarak içe aktarılacak." + +# AI Translated +msgid "Applying texture colors..." +msgstr "Doku renkleri uygulanıyor..." + +# AI Translated +msgid "Updating 3D view..." +msgstr "3D görünüm güncelleniyor..." + +# AI Translated +msgid "Texture colors applied." +msgstr "Doku renkleri uygulandı." + msgid "You can keep the modified presets for the new project or discard them" msgstr "Değiştirilen ön ayarları yeni projede tutabilir veya silebilirsiniz" @@ -8433,7 +8500,7 @@ msgid "Creating a new project" msgstr "Yeni bir proje oluşturma" msgid "Load project" -msgstr "Projeyi Aç" +msgstr "Projeyi aç" msgid "" "Failed to save the project.\n" @@ -8483,7 +8550,7 @@ msgid "The selected file" msgstr "Seçili dosya" msgid "Does not contain valid G-code." -msgstr "Geçerli bir G-kodu içermiyor." +msgstr "Geçerli bir G-code içermiyor." msgid "An Error has occurred while loading the G-code file." msgstr "G-code dosyası yüklenirken bir hata oluştu." @@ -8515,13 +8582,13 @@ msgid "Import geometry only" msgstr "Yalnızca geometriyi içe aktar" msgid "Only one G-code file can be opened at a time." -msgstr "Aynı anda yalnızca bir G kodu dosyası açılabilir." +msgstr "Aynı anda yalnızca bir G-code dosyası açılabilir." msgid "G-code loading" -msgstr "G-kod yükleniyor" +msgstr "G-code yükleniyor" msgid "G-code files and models cannot be loaded together!" -msgstr "G kodu dosyaları modellerle birlikte yüklenemez!" +msgstr "G-code dosyaları modellerle birlikte yüklenemez!" msgid "Unable to add models in preview mode" msgstr "Önizleme modundayken model ekleyemezsiniz" @@ -8539,7 +8606,7 @@ msgid "Copies of the selected object" msgstr "Seçilen nesnenin kopyaları" msgid "Save G-code file as:" -msgstr "G-kod dosyasını şu şekilde kaydedin:" +msgstr "G-code dosyasını şu şekilde kaydedin:" msgid "Save SLA file as:" msgstr "SLA dosyasını farklı bir isimle kaydet:" @@ -8610,7 +8677,7 @@ msgstr "" "Yazdırma sırasında çarpışmaları önlemek için otomatik düzenlemeyi kullanmanızı önerin." msgid "Send G-code" -msgstr "G-kodu gönder" +msgstr "G-code gönder" msgid "Send to printer" msgstr "Yazıcıya gönder" @@ -8899,13 +8966,13 @@ msgid "Enable dark Mode" msgstr "Karanlık modu etkinleştir" msgid "Allow only one OrcaSlicer instance" -msgstr "Yalnızca bir orca slicer örneğine izin ver" +msgstr "Yalnızca tek bir OrcaSlicer örneğine izin ver" msgid "On OSX there is always only one instance of app running by default. However it is allowed to run multiple instances of same app from the command line. In such case this settings will allow only one instance." -msgstr "OSX’te her zaman varsayılan olarak çalışan tek bir uygulama örneği vardır. Ancak aynı uygulamanın birden fazla örneğinin komut satırından çalıştırılmasına izin verilir. Böyle bir durumda bu ayarlar yalnızca bir örneğe izin verecektir." +msgstr "macOS'ta varsayılan olarak her zaman uygulamanın yalnızca tek bir örneği çalışır. Ancak komut satırından aynı uygulamanın birden fazla örneğinin çalıştırılmasına izin verilir. Böyle bir durumda bu ayar, yalnızca tek bir örneğe izin verecektir." msgid "If this is enabled, when starting OrcaSlicer and another instance of the same OrcaSlicer is already running, that instance will be reactivated instead." -msgstr "Bu etkinleştirilirse, OrcaSlicer başlatıldığında ve aynı OrcaSlicer’ın başka bir örneği zaten çalışıyorken, bunun yerine bu örnek yeniden etkinleştirilecektir." +msgstr "Bu seçenek etkinleştirildiğinde; OrcaSlicer başlatılırken aynı OrcaSlicer'ın başka bir örneği zaten çalışıyorsa, yeni bir pencere yerine o örnek yeniden etkinleştirilir." msgid "Show splash screen" msgstr "Açılış ekranını göster" @@ -8960,7 +9027,7 @@ msgid "Add STL/STEP files to recent files list" msgstr "STL/STEP dosyalarını son dosyalar listesine ekle" msgid "Don't warn when loading 3MF with modified G-code" -msgstr "Değiştirilmiş G-kodları içeren 3MF dosyalarını yüklerken uyarma" +msgstr "Değiştirilmiş G-code'ları içeren 3MF dosyalarını yüklerken uyarma" msgid "Show options when importing STEP file" msgstr "STEP dosyasını içe aktarırken seçenekleri göster" @@ -9087,6 +9154,18 @@ msgstr "Bu seçenek etkinleştirildiğinde, aynı anda birden fazla cihaza bir g msgid "Pop up to select filament grouping mode" msgstr "Filament gruplama modunu seçmek için açılır pencere" +# AI Translated +msgid "Visible plugin pages" +msgstr "Görünür eklenti sayfaları" + +# AI Translated +msgid "pages" +msgstr "sayfa" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Kalan sayfalar son sekmedeki açılır listeye toplanmadan önce sabit sekme olarak gösterilen eklenti sayfalarının sayısı." + msgid "Behaviour" msgstr "Davranış" @@ -9477,6 +9556,18 @@ msgstr "Desteklenmeyen ön ayarları göster" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Yazıcı ve filament açılır listelerinde uyumsuz/desteklenmeyen ön ayarları gösterir. Bu ön ayarlar seçilemez." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Deneysel) Baskı sunucuları yerine yazıcı aracılarını kullan" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Bambu olmayan yazıcıların baskı işlerini, klasik baskı sunucusuna yükleme akışı yerine yazıcı eklenti aracıları üzerinden yönlendirir.\n" +"Devre dışı bırakıldığında OrcaSlicer eski baskı sunucusu davranışını kullanır." + # AI Translated msgid "Experimental Features" msgstr "Deneysel Özellikler" @@ -9689,6 +9780,10 @@ msgstr "Spiral vazo" msgid "First layer filament sequence" msgstr "İlk katman filament dizisi" +# AI Translated +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "Filament listesi karışık filamentler içeriyor. Özel filament sırası etkili olmayacak." + msgid "By Layer" msgstr "Katmana göre" @@ -9744,9 +9839,25 @@ msgstr "Kullanıcı Ön Ayarı" msgid "Preset Inside Project" msgstr "Ön ayar içerisinde proje" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Üst ön ayardan devralınan tüm değerleri bu ön ayara kopyalar ve üst ön ayarla olan ilişkiyi kaldırır. Yalnızca üst ön ayarla uyumlu olan ön ayarlar desteklenmeyebilir." + msgid "Detach from parent" msgstr "Ebeveynden ayrıl" +# AI Translated +msgid "Unique preset" +msgstr "Bağımsız ön ayar" + +# AI Translated +msgid "Parent preset" +msgstr "Üst ön ayar" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Bu ön ayar başka bir ön ayardan devralmıyor." + msgid "Name is unavailable." msgstr "Ad kullanılamıyor." @@ -9968,7 +10079,7 @@ msgid "The filament type setting of external spool is different from the filamen msgstr "Harici makaranın filament türü ayarı, dilimleme dosyasındaki filamentden farklıdır." msgid "The printer type selected when generating G-code is not consistent with the currently selected printer. It is recommended that you use the same printer type for slicing." -msgstr "G Kodu oluşturulurken seçilen yazıcı türü mevcut seçili yazıcıyla tutarlı değil. Dilimleme için aynı yazıcı tipini kullanmanız tavsiye edilir." +msgstr "G-code oluşturulurken seçilen yazıcı türü mevcut seçili yazıcıyla tutarlı değil. Dilimleme için aynı yazıcı tipini kullanmanız tavsiye edilir." msgid "There are some unknown filaments in the AMS mappings. Please check whether they are the required filaments. If they are okay, click \"Confirm\" to start printing." msgstr "AMS eşlemelerinde bazı bilinmeyen filamentler var. Lütfen bunların gerekli filamentler olup olmadığını kontrol edin. Sorun yoksa, yazdırmayı başlatmak için \"Onayla\"ya basın." @@ -10496,22 +10607,6 @@ msgstr "Bu seçeneği etkinleştirmek istediğinizden emin misiniz?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Dolgu desenleri genellikle, doğru baskı alınmasını ve istenen etkilerin (ör. Gyroid, Kübik) elde edilmesini sağlamak için döndürme işlemini otomatik olarak yapacak şekilde tasarlanmıştır. Mevcut seyrek dolgu desenini döndürmek, yetersiz destekle sonuçlanabilir. Lütfen dikkatli ilerleyin ve olası baskı sorunlarını iyice kontrol edin. Bu seçeneği etkinleştirmek istediğinizden emin misiniz?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Katman yüksekliği çok küçük.\n" -"min_layer_height olarak ayarlanacak\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Katman yüksekliği, Yazıcı Ayarları -> Ekstruder -> Katman yüksekliği sınırları bölümündeki sınırı aşıyor bu durum baskı kalitesi sorunlarına neden olabilir." - -msgid "Adjust to the set range automatically?\n" -msgstr "Ayarlanan aralığa otomatik olarak ayarlansın mı?\n" - -msgid "Adjust" -msgstr "Ayarla" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Deneysel özellik: Filament değişiklikleri sırasında, floşu en aza indirmek için filamenti daha büyük bir mesafeden geri çekmek ve kesmek. Flush’u önemli ölçüde azaltabilmesine rağmen, aynı zamanda nozul tıkanmaları veya diğer yazdırma komplikasyonları riskini de artırabilir." @@ -10668,10 +10763,10 @@ msgid "Special mode" msgstr "Özel Mod" msgid "G-code output" -msgstr "G Kodu Çıktısı" +msgstr "G-code Çıktısı" msgid "Change extrusion role G-code" -msgstr "Ekstrüzyon Rolü G-kodu Değiştirme" +msgstr "Ekstrüzyon Rolü G-code Değiştirme" msgid "Post-processing Scripts" msgstr "İşlem Sonrası Komut Dosyaları" @@ -10699,16 +10794,19 @@ msgid_plural "" "Please remove them, or G-code visualization and print time estimation will be broken." msgstr[0] "" "Aşağıdaki %s satırı ayrılmış anahtar kelimeler içeriyor.\n" -"Lütfen onu kaldırın, aksi takdirde G kodu görselleştirmesini ve yazdırma süresi tahminini geçeceksiniz." +"Lütfen onu kaldırın, aksi takdirde G-code görselleştirmesini ve yazdırma süresi tahminini geçeceksiniz." msgstr[1] "" "Aşağıdaki satırlar %s ayrılmış anahtar sözcükler içeriyor.\n" -"Lütfen bunları kaldırın, aksi takdirde G kodu görselleştirmesini ve yazdırma süresi tahminini geçeceksiniz." +"Lütfen bunları kaldırın, aksi takdirde G-code görselleştirmesini ve yazdırma süresi tahminini geçeceksiniz." msgid "Reserved keywords found" msgstr "Ayrılmış anahtar kelimeler bulundu" msgid "Setting Overrides" -msgstr "Ayarların Üzerine Yazma" +msgstr "Ayarların Üzerine Yaz" + +msgid "Retraction when switching material" +msgstr "Malzemeyi Değiştirirken Geri Çekme" msgid "Basic information" msgstr "Temel Bilgiler" @@ -10811,10 +10909,10 @@ msgid "Complete print" msgstr "Baskı tamamlanınca" msgid "Filament start G-code" -msgstr "Filament Başlangıç G Kodu" +msgstr "Filament Başlangıç G-code" msgid "Filament end G-code" -msgstr "Filament Bitiş G Kodu" +msgstr "Filament Bitiş G-code" msgid "Wipe tower parameters" msgstr "Silme Kulesi Parametreleri" @@ -10843,13 +10941,19 @@ msgstr "Uyumlu süreç profilleri" msgid "Printable space" msgstr "Plaka Ayarı" +msgid "Printer Agent" +msgstr "Yazıcı Aracısı" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Yazıcı iletişimi için ağ aracısı uygulamasını seçin. Kullanılabilir aracılar başlangıçta kaydedilir." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" msgstr "%1% parametresi için geçersiz değer sağlandı: %2%" msgid "G-code flavor is switched" -msgstr "G-kod çeşidi değiştirildi" +msgstr "G-code çeşidi değiştirildi" msgid "Cooling Fan" msgstr "Soğutucu Fan" @@ -10867,40 +10971,40 @@ msgid "Accessory" msgstr "Aksesuar" msgid "Machine G-code" -msgstr "Yazıcı G-kod" +msgstr "Yazıcı G-code" msgid "File header G-code" -msgstr "Dosya başlığı G kodu" +msgstr "Dosya başlığı G-code" msgid "Machine start G-code" -msgstr "Yazıcı Başlangıç G-kod" +msgstr "Yazıcı Başlangıç G-code" msgid "Machine end G-code" -msgstr "Yazıcı Bitiş G-kod" +msgstr "Yazıcı Bitiş G-code" msgid "Printing by object G-code" -msgstr "Nesneye Göre Yazdırma G-kod" +msgstr "Nesneye Göre Yazdırma G-code" msgid "Before layer change G-code" -msgstr "Katman Değişimi Öncesi G-kod" +msgstr "Katman Değişimi Öncesi G-code" msgid "Layer change G-code" -msgstr "Katman Değişimi G-kod" +msgstr "Katman Değişimi G-code" msgid "Timelapse G-code" -msgstr "Timelapse G-kod" +msgstr "Timelapse G-code" msgid "Clumping Detection G-code" -msgstr "Topaklanma Tespiti G Kodu" +msgstr "Topaklanma Tespiti G-code" msgid "Change filament G-code" -msgstr "Filament Değişimi G-kod" +msgstr "Filament Değişimi G-code" msgid "Pause G-code" -msgstr "Duraklatma G-Kod" +msgstr "Duraklatma G-code" msgid "Template Custom G-code" -msgstr "Şablon Özel G-kod" +msgstr "Şablon Özel G-code" msgid "Motion ability" msgstr "Hareket" @@ -10973,9 +11077,6 @@ msgstr "Katman Yüksekliği Sınırları" msgid "Z-Hop" msgstr "Z Sıçraması" -msgid "Retraction when switching material" -msgstr "Malzemeyi Değiştirirken Geri Çekme" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -11099,12 +11200,12 @@ msgid "No modifications need to be copied." msgstr "Kopyalanması gereken değişiklik yok." # AI Translated -msgid "Copy paramters" +msgid "Copy parameters" msgstr "Parametreleri kopyala" # AI Translated #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "%s parametrelerini değiştir" # AI Translated @@ -11624,29 +11725,6 @@ msgstr "Filament değişimi için temizleme hacmi" msgid "Please choose the filament colour" msgstr "Lütfen filament rengini seçin" -# AI Translated -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "Yerel Wayland canlı görüntüsü, GStreamer GTK video alıcısını gerektirir. Lütfen GStreamer için gtksink eklentisini yükleyin ve ardından OrcaSlicer'ı yeniden başlatın." - -# AI Translated -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "Yerel Wayland GStreamer video alıcısı başlatılamadı. Lütfen GStreamer GTK eklentisi kurulumunuzu denetleyin." - -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "Bu görev için Windows Media Player gereklidir! İşletim sisteminiz için ‘Windows Media Player’ı etkinleştirmek istiyor musunuz?" - -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "BambuSource medya oynatımı için doğru şekilde kaydedilmemiş! Yeniden kaydetmek için Evet’e basın" - -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "Medya oynatma için kayıtlı BambuSource bileşeni eksik! Lütfen OrcaSlicer'ı yeniden yükleyin veya topluluk yardımı isteyin." - -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "Farklı bir kurulumdan bir BambuSource kullanıyorsunuz, video oynatma doğru çalışmayabilir! Düzeltmek için Evet’e basın." - -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "Sisteminizde video oynatmak için gerekli olan GStreamer H.264 codec bileşenleri eksik. (gstreamer1.0-plugins-bad veya gstreamer1.0-libav paketlerini kurmayı deneyin, ardından Orca Slicer’ı yeniden başlatın)" - # AI Translated msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "Bulut aracısı kullanılamıyor. Lütfen OrcaSlicer'ı yeniden başlatıp yeniden deneyin." @@ -11919,7 +11997,7 @@ msgid "On/Off one layer mode of the vertical slider" msgstr "Dikey kaydırıcının tek katman modunu açma/kapama" msgid "On/Off G-code window" -msgstr "G-kodu penceresini aç/kapat" +msgstr "G-code penceresini aç/kapat" msgid "Move slider 5x faster" msgstr "Kaydırıcıyı 5 kat daha hızlı hareket ettirin" @@ -12153,7 +12231,7 @@ msgid " updated to " msgstr " güncellendi " msgid "Open G-code file:" -msgstr "G kodu dosyasını açın:" +msgstr "G-code dosyasını açın:" msgid "One object has an empty first layer and can't be printed. Please Cut the bottom or enable supports." msgstr "Bir nesnenin ilk katmanı boş ve yazdırılamıyor. Lütfen alt kısmı kesin veya destekleri etkinleştirin." @@ -12187,15 +12265,15 @@ msgid "" "Failed to generate G-code for invalid custom G-code.\n" "\n" msgstr "" -"Geçersiz özel G kodu için gcode oluşturulamadı.\n" +"Geçersiz özel G-code için G-code oluşturulamadı.\n" "\n" msgid "Please check the custom G-code or use the default custom G-code." -msgstr "Lütfen özel G kodunu kontrol edin veya varsayılan özel G kodunu kullanın." +msgstr "Lütfen özel G-code'u kontrol edin veya varsayılan özel G-code'u kullanın." #, boost-format msgid "Generating G-code: layer %1%" -msgstr "G kodu oluşturuluyor: katman %1%" +msgstr "G-code oluşturuluyor: katman %1%" msgid "Flush volumes matrix do not match to the correct size!" msgstr "Yıkama hacimleri matrisi doğru boyutla eşleşmiyor!" @@ -12227,7 +12305,7 @@ msgid "Group error in manual mode. Please check nozzle count or regroup." msgstr "Elle modda gruplama hatası. Lütfen nozul sayısını denetleyin veya yeniden gruplayın." msgid "Internal Bridge" -msgstr "İç Köprü" +msgstr "İç köprü" msgid "undefined error" msgstr "bilinmeyen hata" @@ -12353,6 +12431,10 @@ msgstr " dışlama alanına çok yakın ve çarpışmalara neden olacak.\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " topaklanma algılama alanına çok yakın, çarpışmalar meydana gelecektir.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " yazdırılabilir alanın kısmen dışında ve yazdırılamaz.\n" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Seçilen nozul sıcaklıkları uyumsuz. Her filamentin nozul sıcaklığı, diğer filamentlerin önerilen nozul sıcaklığı aralığında olmalıdır. Aksi hâlde nozul tıkanması veya yazıcıda hasar oluşabilir." @@ -12368,6 +12450,10 @@ msgstr "Yine de yazdırmak istiyorsanız Tercihler / Denetim / Dilimleme / Karı msgid "No extrusions under current settings." msgstr "Mevcut ayarlarda ekstrüzyon yok." +# AI Translated +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "Gradyanlı bir karışık filament kullanılıyor ancak 'Karışık renk alt katmanı' devre dışı. Gradyan basılmayacak." + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "\"Nesneye göre\" dizisi etkinleştirildiğinde, hızlandırılmış çekimin yumuşak modu desteklenmez." @@ -12404,6 +12490,10 @@ msgstr "Modelinizin boyutunu küçültmek veya mevcut yazdırma ayarlarını de msgid "Variable layer height is not supported with Organic supports." msgstr "Değişken katman yüksekliği Organik desteklerle desteklenmez." +# AI Translated +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "Silme kulesi filamenti karışık filament olamaz." + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Farklı püskürtme ucu çapları ve farklı filament çapları, ana kule etkinleştirildiğinde iyi çalışmayabilir. Oldukça deneysel olduğundan lütfen dikkatli ilerleyin." @@ -12414,7 +12504,7 @@ msgid "Ooze prevention is only supported with the wipe tower when 'single_extrud msgstr "Sızıntı önleme yalnızca ‘tek ekstruder çoklu malzeme’ kapalıyken silme kulesiyle desteklenir." msgid "The prime tower is currently only supported for the Marlin, RepRap/Sprinter, RepRapFirmware and Repetier G-code flavors." -msgstr "Prime tower şu anda yalnızca Marlin, RepRap/Sprinter, RepRapFirmware ve Repetier G kodu türleri için desteklenmektedir." +msgstr "Prime tower şu anda yalnızca Marlin, RepRap/Sprinter, RepRapFirmware ve Repetier G-code türleri için desteklenmektedir." msgid "A prime tower is not supported in “By object” print." msgstr "Prime tower, \"Nesneye göre\" yazdırmada desteklenmez." @@ -12569,10 +12659,10 @@ msgstr "" "Nesneleri birbirinden uzaklaştırın, kenar/etek boyutunu küçültün, Etek tipini Birleşik olarak değiştirin veya Yazdırma sırasını Katmana göre olarak değiştirin." msgid "Exporting G-code" -msgstr "G kodu dışa aktarılıyor" +msgstr "G-code dışa aktarılıyor" msgid "Generating G-code" -msgstr "G kodu oluşturuluyor" +msgstr "G-code oluşturuluyor" # AI Translated msgid "Processing of the filename_format template failed." @@ -12688,9 +12778,6 @@ msgstr "G-code yerine 3MF kullan" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Yazıcı, baskı işi olarak 3MF dosyası kabul ediyorsa bunu etkinleştirin. Etkinleştirildiğinde Orca Slicer, dilimlenmiş dosyayı düz bir .gcode dosyası yerine .gcode.3mf olarak gönderir." -msgid "Printer Agent" -msgstr "Yazıcı Aracısı" - msgid "Select the network agent implementation for printer communication." msgstr "Yazıcı iletişimi için ağ aracısı uygulamasını seçin." @@ -12698,7 +12785,7 @@ msgid "Hostname, IP or URL" msgstr "Ana bilgisayar adı, IP veya URL" msgid "Orca Slicer can upload G-code files to a printer host. This field should contain the hostname, IP address or URL of the printer host instance. Print host behind HAProxy with basic auth enabled can be accessed by putting the user name and password into the URL in the following format: https://username:password@your-octopi-address/" -msgstr "Orca Slicer, G kodu dosyalarını bir yazıcı ana bilgisayarına yükleyebilir. Bu alan, yazıcı ana bilgisayar örneğinin ana bilgisayar adını, IP adresini veya URL'sini içermelidir. Temel kimlik doğrulamanın etkin olduğu HAProxy'nin arkasındaki yazdırma ana bilgisayarına, kullanıcı adı ve parolanın aşağıdaki biçimdeki URL'ye girilmesiyle erişilebilir: https://username:password@your-octopi-address/" +msgstr "OrcaSlicer, G-code dosyalarını bir yazıcı ana bilgisayarına yükleyebilir. Bu alan; yazıcı ana bilgisayarı örneğinin ana bilgisayar adını, IP adresini veya URL'sini içermelidir. Temel kimlik doğrulaması etkin ve HAProxy arkasında çalışan yazıcı ana bilgisayarlarına URL içine kullanıcı adı ve parola şu biçimde eklenerek erişilebilir: https://kullaniciadi:parola@octopi-adresiniz/" msgid "Device UI" msgstr "Cihaz kullanıcı arayüzü" @@ -12710,7 +12797,7 @@ msgid "API Key / Password" msgstr "API Anahtarı / Şifre" msgid "Orca Slicer can upload G-code files to a printer host. This field should contain the API Key or the password required for authentication." -msgstr "Orca Slicer, G kodu dosyalarını bir yazıcı ana bilgisayarına yükleyebilir. Bu alan, kimlik doğrulama için gereken API Anahtarını veya şifreyi içermelidir." +msgstr "Orca Slicer, G-code dosyalarını bir yazıcı ana bilgisayarına yükleyebilir. Bu alan, kimlik doğrulama için gereken API Anahtarını veya şifreyi içermelidir." # AI Translated msgid "Serial Number" @@ -12775,7 +12862,7 @@ msgstr "mm veya %" msgid "Other layers" msgstr "Diğer katmanlar" -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." msgstr "İlk katman hariç tüm katmanlar için yatak sıcaklığı. 0 değeri, filamentin Cool Plate SuperTack üzerine baskıyı desteklemediği anlamına gelir." msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." @@ -12839,7 +12926,7 @@ msgid "Other layers filament sequence" msgstr "Diğer katmanlar filament dizisi" msgid "This G-code is inserted at every layer change before the Z lift." -msgstr "Bu G kodu, z'yi kaldırmadan önce her katman değişikliğinde eklenir." +msgstr "Bu G-code, z'yi kaldırmadan önce her katman değişikliğinde eklenir." msgid "Bottom shell layers" msgstr "Alt katmanlar" @@ -12920,7 +13007,6 @@ msgstr "Çıkıntı bu belirtilen eşiği aştığında, soğutma fanını aşa msgid "External bridge infill direction" msgstr "Dış köprü dolgu yönü" -# AI Translated #, no-c-format, no-boost-format msgid "" "External Bridging angle override.\n" @@ -12937,14 +13023,13 @@ msgstr "" "Aksi hâlde verilen açı şuna göre kullanılır:\n" " - Mutlak koordinatlar\n" " - Mutlak koordinatlar + Model dönüşü: Yönleri modele hizala etkinse\n" -" - En uygun otomatik açı + bu değer: 'Göreli Köprü Açısı' etkinse\n" +" - En uygun otomatik açı + bu değer: ‘Göreceli Köprü Açısı' etkinse\n" "\n" "Sıfır mutlak açı için 180° kullanın." msgid "Internal bridge infill direction" msgstr "İç köprü dolgu yönü" -# AI Translated msgid "" "Internal Bridging angle override.\n" "If left to zero, the bridging angle will be calculated automatically for each specific bridge.\n" @@ -12960,13 +13045,12 @@ msgstr "" "Aksi hâlde verilen açı şuna göre kullanılır:\n" " - Mutlak koordinatlar\n" " - Mutlak koordinatlar + Model dönüşü: Yönleri modele hizala etkinse\n" -" - En uygun otomatik açı + bu değer: 'Göreli Köprü Açısı' etkinse\n" +" - En uygun otomatik açı + bu değer: 'Göreceli Köprü Açısı' etkinse\n" "\n" "Sıfır mutlak açı için 180° kullanın." -# AI Translated msgid "Relative bridge angle" -msgstr "Göreli köprü açısı" +msgstr "Göreceli köprü açısı" # AI Translated msgid "When enabled, the bridge angle values are added to the automatically calculated bridge direction instead of overriding it." @@ -13376,9 +13460,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "İç köprülerin hızı. Değer yüzde olarak ifade edilirse köprü hızına göre hesaplanacaktır. Varsayılan değer %150’dir." -msgid "Brim width" -msgstr "Kenar genişliği" - msgid "This is the distance from the model to the outermost brim line." msgstr "Modelden en dış kenar çizgisine kadar olan mesafe." @@ -13413,7 +13494,7 @@ msgstr "" "Not: Elde edilen değer ilk katman akış oranından etkilenmez." msgid "Brim follows compensated outline" -msgstr "Kenar telafi edilen taslağı takip ediyor" +msgstr "Kenar toleranslı dış sınırı takip etsin" # AI Translated msgid "" @@ -13463,6 +13544,14 @@ msgstr "" "Keskin açılar algılanmadan önce geometri azaltılacaktır. Bu parametre, azaltma için minimum sapma uzunluğunu belirtir.\n" "Devre dışı bırakmak için 0." +# AI Translated +msgid "Brim ears outer only" +msgstr "Kenar kulakları yalnızca dışta" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Fare kulaklarını yalnızca modelin dış konturunda oluşturur, delikleri ve kapalı bölümleri hariç tutar." + msgid "upward compatible machine" msgstr "yukarı doğru uyumlu makine" @@ -13487,7 +13576,6 @@ msgstr "Nesneye göre" msgid "Intra-layer order" msgstr "Katman içi sıra" -# AI Translated msgid "" "Order in which object instances are visited within a single layer, which controls how much travel is spent moving between them.\n" "\n" @@ -13498,14 +13586,17 @@ msgid "" "\n" "With multiple filaments or tools in the same layer, minimizing tool changes takes priority: objects are grouped by filament first and this setting only orders the instances within each filament group, so the overall sequence may not look like the shortest path across the plate." msgstr "" -"Tek bir katman içinde nesne örneklerinin hangi sırayla ziyaret edileceği; bu da aralarında ne kadar seyahat harcanacağını belirler.\n" +"Tek bir katman içinde nesne eş kopyalarının (instances) basılma sırasıdır; bunlar arasındaki seyahat mesafesini ve süresini kontrol eder.\n" "\n" -"Varsayılan: en yakın komşu zincirlemesi, 2-opt ve kesişim giderme ile iyileştirilir. İyi bir genel seçim.\n" -"Nesne listesi olarak: örnekler, herhangi bir yol optimizasyonu olmadan nesne listesindeki sırayla yazdırılır. Öngörülebilir, elle denetlenen bir sıraya ihtiyacınız olduğunda kullanın.\n" -"Hepsinin en iyisi (en kısa yol): her strateji değerlendirilir ve en kısa olanı kullanılır. Nesne örneklerinin sırası tüm baskı için bir kez belirlenir, tek tek adaların sırası ise her katman için ayrı belirlenir; bu nedenle farklı katmanlar farklı stratejiler kullanabilir. Dilimleme biraz daha yavaştır.\n" -"Yılankavi: satır satır ilerleyen yılankavi geçiş, 2-opt ile iyileştirilir. Çok sayıda küçük parçadan oluşan düzenli ızgaralar için çok uygundur.\n" +"Varsayılan (Default): 2-opt algoritması ve hat kesişimi giderme ile iyileştirilmiş en yakın komşu zincirleme yöntemi. Genel kullanım için dengeli ve ideal bir tercihtir.\n" "\n" -"Aynı katmanda birden fazla filament veya araç varsa, araç değişimlerini en aza indirmek önceliklidir: nesneler önce filamente göre gruplanır ve bu ayar yalnızca her filament grubu içindeki örnekleri sıralar; bu nedenle genel sıra, tabla genelindeki en kısa yol gibi görünmeyebilir." +"Nesne listesi olarak (As object list): Eş kopyalar (instances), herhangi bir rota optimizasyonu yapılmadan doğrudan nesne listesindeki sıralamayla basılır. Manuel ve öngörülebilir bir sıra istendiğinde kullanılır.\n" +"\n" +"Hepsinin en iyisi (en kısa yol): Mevcut tüm stratejiler hesaplanır ve en kısa mesafe sunan rota seçilir. Nesne eş kopyalarının sırası tüm baskı için tek seferde kararlaştırılırken, bağımsız adacıkların sıralaması katman bazında hesaplanır (farklı katmanlarda farklı stratejiler devreye girebilir). Dilimleme süresini biraz uzatabilir.\n" +"\n" +"Yılankavi (Snake): 2-opt ile optimize edilmiş satır satır kıvrımlı (serpantin) tarama rotası. Yatağa ızgara şeklinde dizilmiş çok sayıda küçük parçalı baskılar için son derece uygundur.\n" +"\n" +"Aynı katmanda birden fazla filament veya nozül/takım kullanıldığında, takım değişimlerini en aza indirmek önceliklidir: Nesneler önce filamente göre gruplanır; bu ayar ise sadece ilgili filament grubu içindeki eş kopyaları (instances) sıralar. Bu nedenle genel hareket sırası plakanın tamamına bakıldığında her zaman en kısa rota gibi görünmeyebilir." msgid "As object list" msgstr "Nesne listesi olarak" @@ -13568,7 +13659,7 @@ msgid "Activate air filtration" msgstr "Hava filtrelemesini etkinleştirin" msgid "Activate for better air filtration. G-code command: M106 P3 S(0-255)" -msgstr "Daha iyi hava filtrasyonu için etkinleştirin. G-kodu komutu: M106 P3 S(0-255)" +msgstr "Daha iyi hava filtrasyonu için etkinleştirin. G-code komutu: M106 P3 S(0-255)" # AI Translated msgid "Enable this to override the fan speed set in custom G-code during print." @@ -13583,7 +13674,7 @@ msgid "Enable this to override the fan speed set in custom G-code after print co msgstr "Baskı tamamlandıktan sonra özel G-code'da ayarlanan fan hızını geçersiz kılmak için bunu etkinleştirin." msgid "Speed of exhaust fan during printing. This speed will override the speed in filament custom G-code." -msgstr "Baskı sırasında egzoz fanının hızı. Bu hız, filament özel gcode'undaki hızın üzerine yazılacaktır." +msgstr "Baskı sırasında egzoz fanının hızı. Bu hız, filament özel G-code'undaki hızın üzerine yazılacaktır." msgid "Speed of exhaust fan after printing completes." msgstr "Baskı tamamlandıktan sonra egzoz fanının hızı." @@ -13697,19 +13788,19 @@ msgid "This is the maximum length of bridges that don't need support. Set it to msgstr "Desteğe ihtiyaç duymayan maksimum köprü uzunluğu. Tüm köprülerin desteklenmesini istiyorsanız bunu 0'a, hiçbir köprünün desteklenmesini istemiyorsanız çok büyük bir değere ayarlayın." msgid "End G-code" -msgstr "Bitiş G kodu" +msgstr "Bitiş G-code" msgid "Add end G-Code when finishing the entire print." -msgstr "Tüm yazdırmayı tamamladığında çalışacak olan G Kodu." +msgstr "Tüm yazdırmayı tamamladığında çalışacak olan G-code." msgid "Between Object G-code" -msgstr "Nesne Arası Gcode" +msgstr "Nesne Arası G-code" msgid "Insert G-code between objects. This parameter will only come into effect when you print your models object by object." -msgstr "Nesnelerin arasına Gcode ekleyin. Bu parametre yalnızca modellerinizi nesne nesne yazdırdığınızda etkili olacaktır." +msgstr "Nesnelerin arasına G-code ekleyin. Bu parametre yalnızca modellerinizi nesne nesne yazdırdığınızda etkili olacaktır." msgid "Add end G-code when finishing the printing of this filament." -msgstr "Bu filament ile baskı bittiğinde çalışacak G kod." +msgstr "Bu filament ile baskı bittiğinde çalışacak G-code." msgid "Ensure vertical shell thickness" msgstr "Dikey kabuk kalınlığını koru" @@ -13724,13 +13815,13 @@ msgid "" msgstr "" "Dikey kabuk kalınlığını garanti etmek için eğimli yüzeylerin yakınına katı dolgu ekleyin (üst + alt katı katmanlar)\n" "Yok: Hiçbir yere katı dolgu eklenmez. Dikkat: Modelinizin eğimli yüzeyleri varsa bu seçeneği dikkatli kullanın.\n" -"Yalnızca kritik: Duvarlar için katı dolgu eklemekten kaçının\n" +"Kritik: Duvarlar için katı dolgu eklemekten kaçının\n" "Orta: Yalnızca çok eğimli yüzeyler için katı dolgu ekleyin\n" "Hepsi: Tüm uygun eğimli yüzeyler için katı dolgu ekleyin\n" "Varsayılan değer Tümü'dür." msgid "Critical Only" -msgstr "Yalnızca kritik" +msgstr "Kritik" msgid "Moderate" msgstr "Orta" @@ -14031,14 +14122,14 @@ msgid "Extruder offset" msgstr "Ekstruder konumu" msgid "The material may have volumetric change after switching between molten and crystalline states. This setting changes all extrusion flow of this filament in G-code proportionally. The recommended value range is between 0.95 and 1.05. You may be able to tune this value to get a nice flat surface if there is slight overflow or underflow." -msgstr "Malzeme, erimiş hal ile kristal hal arasında geçiş yaptıktan sonra hacimsel değişime sahip olabilir. Bu ayar, bu filamentin gcode'daki tüm ekstrüzyon akışını orantılı olarak değiştirir. Önerilen değer aralığı 0,95 ile 1,05 arasındadır. Belki hafif taşma veya taşma olduğunda güzel düz bir yüzey elde etmek için bu değeri ayarlayabilirsiniz." +msgstr "Malzeme, erimiş hal ile kristal hal arasında geçiş yaptıktan sonra hacimsel değişime sahip olabilir. Bu ayar, bu filamentin G-code'daki tüm ekstrüzyon akışını orantılı olarak değiştirir. Önerilen değer aralığı 0,95 ile 1,05 arasındadır. Belki hafif taşma veya taşma olduğunda güzel düz bir yüzey elde etmek için bu değeri ayarlayabilirsiniz." msgid "" "The material may have volumetric change after switching between molten and crystalline states. This setting changes all extrusion flow of this filament in G-code proportionally. The recommended value range is between 0.95 and 1.05. You may be able to tune this value to get a nice flat surface if there is slight overflow or underflow.\n" "\n" "The final object flow ratio is this value multiplied by the filament flow ratio." msgstr "" -"Malzeme, erimiş hal ile kristal hal arasında geçiş yaptıktan sonra hacimsel değişime sahip olabilir. Bu ayar, bu filamentin gcode’daki tüm ekstrüzyon akışını orantılı olarak değiştirir. Önerilen değer aralığı 0,95 ile 1,05 arasındadır. Belki hafif taşma veya taşma olduğunda güzel düz bir yüzey elde etmek için bu değeri ayarlayabilirsiniz.\n" +"Malzeme, erimiş hal ile kristal hal arasında geçiş yaptıktan sonra hacimsel değişime sahip olabilir. Bu ayar, bu filamentin G-code’daki tüm ekstrüzyon akışını orantılı olarak değiştirir. Önerilen değer aralığı 0,95 ile 1,05 arasındadır. Belki hafif taşma veya taşma olduğunda güzel düz bir yüzey elde etmek için bu değeri ayarlayabilirsiniz.\n" "\n" "Nihai nesne akış oranı, bu değerin filament akış oranıyla çarpılmasıyla elde edilir." @@ -14159,6 +14250,8 @@ msgstr "Katman süresi" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "Tahmini süresi bu değerden kısa olan katlarda parça soğutma fanı devreye girecektir. Fan hızı, katman yazdırma süresine göre minimum ve maksimum fan hızları arasında enterpole edilir." +# AI Translated +msgctxt "second" msgid "s" msgstr "s" @@ -14247,10 +14340,10 @@ msgid "By First filament" msgstr "İlk filamente göre" msgid "By Highest Temp" -msgstr "En Yüksek Sıcaklığa Göre" +msgstr "En yüksek sıcaklığa göre" msgid "Filament diameter is used to calculate extrusion variables in G-code, so it is important that this is accurate and precise." -msgstr "Filament çapı, gcode'da ekstrüzyonu hesaplamak için kullanılır; bu nedenle önemlidir ve doğru olmalıdır." +msgstr "Filament çapı, G-code'da ekstrüzyonu hesaplamak için kullanılır; bu nedenle önemlidir ve doğru olmalıdır." msgid "Pellet flow coefficient" msgstr "Pelet akış katsayısı" @@ -14468,6 +14561,62 @@ msgstr "Destek malzemesi" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Destek malzemesi yaygın olarak destek ve destek arayüzünü yazdırmak için kullanılır." +# AI Translated +msgid "Is mixed filament" +msgstr "Karışık filament mi" + +# AI Translated +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "Bu filament yuvasının birden fazla fiziksel filamentten oluşan bir karışık filament olup olmadığını belirler" + +# AI Translated +msgid "Mixed filament components" +msgstr "Karışık filament bileşenleri" + +# AI Translated +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "Bileşen filamentlerin virgülle ayrılmış, 1'den başlayan sıra numaraları; ör. \"1,3\"" + +# AI Translated +msgid "Mixed filament sublayer ratios" +msgstr "Karışık filament alt katman oranları" + +# AI Translated +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "Toplamı 1.0 olan, virgülle ayrılmış oran değerleri; ör. \"0.7,0.3\"" + +# AI Translated +msgid "Mixed filament gradient" +msgstr "Karışık filament gradyanı" + +# AI Translated +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "Karışık filamentin alt katmanları için Z yönünde gradyan modunu etkinleştirir. Etkinleştirildiğinde alt katman oranları katmanlar boyunca doğrusal olarak değişir." + +# AI Translated +msgid "Mixed filament gradient range" +msgstr "Karışık filament gradyan aralığı" + +# AI Translated +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "Gradyan modunda ilk bileşenin başlangıç ve bitiş oranları. Virgülle ayrılmış çift; ör. \"0.10,0.90\" %10 ile %90 arasını ifade eder." + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "Karışık filament gradyan eğrisi" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "Z ilerlemesini ilk bileşenin oranına eşleyen, isteğe bağlı Photoshop tarzı özel eğri. Dikey çizgiyle ayrılmış kontrol noktaları olarak kodlanır; \"x,y\" (eski) veya teğetin geçersiz kılınması gerektiğinde \"x,y,m_in,m_out\" biçiminde (boş değer ya da \"nan\" varsayılan PCHIP değerini kullanır). x [0,1] aralığındadır; y yapılandırılmış oran aralığına kırpılır; ör. \"0,0.15|0.5,0.50|1,0.85\". Boş bırakıldığında bunun yerine doğrusal gradient_range kullanılır." + +# AI Translated +msgid "Mixed filament per-part gradient" +msgstr "Karışık filamentte parça başına gradyan" + +# AI Translated +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "Gradyan modu etkinleştirildiğinde, gradyan montajın tamamı tek bir Z aralığı olarak ele alınmak yerine montajdaki her parçaya bağımsız olarak uygulanır." + msgid "Filament printable" msgstr "Filament yazdırılabilir" @@ -14647,6 +14796,14 @@ msgstr "Tpms-fk" msgid "Gyroid" msgstr "Jiroid" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Dolgu yumuşatma faktörü" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Dolgu köşelerinin ne kadar yuvarlatılacağını belirler. 0% özgün keskin yolu korur, 100% ise komşu dolgu çizgileri arasında mümkün olan en büyük eğrileri üretir." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Üst yüzey dolgusunun hızlandırılması. Daha düşük bir değerin kullanılması üst yüzey kalitesini iyileştirebilir." @@ -14685,30 +14842,29 @@ msgid "Marlin Firmware Junction Deviation (replaces the traditional XY Jerk sett msgstr "Marlin Firmware Köşe Sapması (geleneksel XY Sarsıntı ayarının yerini alır)" msgid "Jerk of outer walls." -msgstr "Dış duvar JERK değeri." +msgstr "Dış duvar sarsıntı değeri." msgid "Jerk of inner walls." -msgstr "İç duvarlar JERK değeri." +msgstr "İç duvarlar sarsıntı değeri." msgid "Jerk for top surface." -msgstr "Üst yüzey için JERK değeri." +msgstr "Üst yüzey için Sarsıntı değeri." msgid "Jerk for infill." -msgstr "Dolgu için JERK değeri." +msgstr "Dolgu için Sarsıntı değeri." msgid "Jerk for the first layer." -msgstr "İlk katman için JERK değeri." +msgstr "İlk katman için Sarsıntı değeri." msgid "Jerk for travel." -msgstr "Seyahat için JERK değeri." +msgstr "Seyahat için Sarsıntı değeri." -# AI Translated msgid "" "Travel jerk of first layer.\n" "The percentage value is relative to Travel Jerk." msgstr "" -"İlk katmanın seyahat jerk'i.\n" -"Yüzde değeri Seyahat Jerk'ine göredir." +"İlk katmanın seyahat sarsıntısı (travel jerk).\n" +"Yüzde değeri, Seyahat Sarsıntısı (Travel Jerk) değerine bağlıdır." msgid "Line width of the first layer. If expressed as a %, it will be computed over the nozzle diameter." msgstr "İlk katmanın çizgi genişliği. % olarak ifade edilirse Nozul çapı üzerinden hesaplanacaktır." @@ -15027,7 +15183,7 @@ msgid "" "\n" "Note: For Klipper machines, this option is recommended to be disabled. Klipper does not benefit from arc commands as these are split again into line segments by the firmware. This results in a reduction in surface quality as line segments are converted to arcs by the slicer and then back to line segments by the firmware." msgstr "" -"G2 ve G3 hareketlerine sahip bir G kodu dosyası elde etmek için bunu etkinleştirin. Montaj toleransı çözünürlükle aynıdır.\n" +"G2 ve G3 hareketlerine sahip bir G-code dosyası elde etmek için bunu etkinleştirin. Montaj toleransı çözünürlükle aynıdır.\n" "\n" "Not: Klipper makineler için bu seçeneğin devre dışı bırakılması önerilir. Klipper, yazılım tarafından tekrar çizgi bölümlerine bölündüğü için yay komutlarından faydalanmaz. Bu, çizgi bölümlerinin dilimleyici tarafından yaylara dönüştürülmesi ve ardından donanım yazılımı tarafından tekrar çizgi bölümlerine dönüştürülmesi nedeniyle yüzey kalitesinde bir azalmaya neden olur." @@ -15035,7 +15191,7 @@ msgid "Add line number" msgstr "Satır numarası ekle" msgid "Enable this to add line number(Nx) at the beginning of each G-code line." -msgstr "Her G Kodu satırının başına satır numarası (Nx) eklemek için bunu etkinleştirin." +msgstr "Her G-code satırının başına satır numarası (Nx) eklemek için bunu etkinleştirin." msgid "Scan first layer" msgstr "İlk katmanı tara" @@ -15047,7 +15203,7 @@ msgid "Power Loss Recovery" msgstr "Güç Kaybının Geri Kazanımı" msgid "Choose how to control power loss recovery. When set to Printer configuration, the slicer will not emit power loss recovery G-code and will leave the printer's configuration unchanged. Applicable to Bambu Lab or Marlin 2 firmware based printers." -msgstr "Güç kaybı kurtarmanın nasıl kontrol edileceğini seçin. Yazıcı yapılandırması olarak ayarlandığında, dilimleyici güç kaybı kurtarma G kodunu yayınlamayacak ve yazıcının yapılandırmasını değiştirmeden bırakacaktır. Bambu Lab veya Marlin 2 ürün yazılımı tabanlı yazıcılar için geçerlidir." +msgstr "Güç kaybı kurtarmanın nasıl kontrol edileceğini seçin. Yazıcı yapılandırması olarak ayarlandığında, dilimleyici güç kaybı kurtarma G-code'u yayınlamayacak ve yazıcının yapılandırmasını değiştirmeden bırakacaktır. Bambu Lab veya Marlin 2 ürün yazılımı tabanlı yazıcılar için geçerlidir." msgid "Printer configuration" msgstr "Yazıcı yapılandırması" @@ -15123,7 +15279,7 @@ msgid "" msgstr "" "Fanı hedef başlangıç zamanından bu kadar saniye önce başlatın (kesirli saniyeleri kullanabilirsiniz). Bu süre tahmini için sonsuz ivme varsayar ve yalnızca G1 ve G0 hareketlerini hesaba katar (yay uydurma desteklenmez).\n" "Fan komutlarını özel kodlardan taşımaz (bir çeşit 'bariyer' görevi görürler).\n" -"'Yalnızca özel başlangıç gcode'u etkinleştirilmişse, fan komutları başlangıç gcode'una taşınmayacaktır.\n" +"'Yalnızca özel başlangıç G-code'u etkinleştirilmişse, fan komutları başlangıç G-code'una taşınmayacaktır.\n" "Devre dışı bırakmak için 0'ı kullanın." msgid "Only overhangs" @@ -15200,11 +15356,19 @@ msgid "G-code flavor" msgstr "G-code türü" msgid "What kind of G-code the printer is compatible with." -msgstr "Yazıcının ne tür bir gcode ile uyumlu olduğu." +msgstr "Yazıcının ne tür bir G-code ile uyumlu olduğu." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "G-code yapılandırma bloğunu atla" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "CONFIG_BLOCK bloğunu (dilimleyici yapılandırmasının anahtar/değer çiftlerini) G-code dosyasına yazmaz. Bu, bu yorum satırlarını ayrıştırırken donanım yazılımı çöken yazıcılarda yardımcı olabilir (ör. Anycubic go-klipper). Not: G-code dosyası artık dilimleyici ayarlarını içermeyeceğinden, dosyayı OrcaSlicer'a geri aktarmak yapılandırmayı geri yüklemez." + msgid "Pellet Modded Printer" msgstr "Pelet modlu yazıcı" @@ -15227,13 +15391,13 @@ msgid "Exclude objects" msgstr "Nesneleri hariç tut" msgid "Enable this option to add EXCLUDE OBJECT command in G-code." -msgstr "G koduna EXCLUDE OBJECT komutunu eklemek için bu seçeneği etkinleştirin." +msgstr "G-code'a EXCLUDE OBJECT komutunu eklemek için bu seçeneği etkinleştirin." msgid "Verbose G-code" msgstr "Ayrıntılı G-code" msgid "Enable this to get a commented G-code file, with each line explained by a descriptive text. If you print from SD card, the additional weight of the file could make your firmware slow down." -msgstr "Her satırın açıklayıcı bir metinle açıklandığı, yorumlu bir G kodu dosyası almak için bunu etkinleştirin. SD karttan yazdırırsanız dosyanın ilave ağırlığı ürün yazılımınızın yavaşlamasına neden olabilir." +msgstr "Her satırın açıklayıcı bir metinle açıklandığı, yorumlu bir G-code dosyası almak için bunu etkinleştirin. SD karttan yazdırırsanız dosyanın ilave ağırlığı ürün yazılımınızın yavaşlamasına neden olabilir." msgid "Infill combination" msgstr "Dolgu kombinasyonu" @@ -15598,10 +15762,10 @@ msgstr "" "Ayrıca dilimleme düzlemini de denetler." msgid "This G-code is inserted at every layer change after the Z lift." -msgstr "Bu gcode kısmı, z kaldırma işleminden sonra her katman değişikliğinde eklenir." +msgstr "Bu G-code kısmı, z kaldırma işleminden sonra her katman değişikliğinde eklenir." msgid "Clumping detection G-code" -msgstr "Topaklanma tespiti G kodu" +msgstr "Topaklanma tespiti G-code" # AI Translated msgid "Silent Mode" @@ -15611,7 +15775,7 @@ msgid "Whether the machine supports silent mode in which machine uses lower acce msgstr "Daha sessiz baskı için ivmelenmeyi düşüren sessiz mod desteği" msgid "Emit limits to G-code" -msgstr "G-kod sınırları" +msgstr "G-code sınırları" msgid "Machine limits" msgstr "Yazıcı sınırları" @@ -15620,14 +15784,14 @@ msgid "" "If enabled, the machine limits will be emitted to G-code file.\n" "This option will be ignored if the G-code flavor is set to Klipper." msgstr "" -"Etkinleştirilirse, makine sınırları G kodu dosyasına aktarılacaktır.\n" -"G kodu tadı Klipper olarak ayarlandığında bu seçenek göz ardı edilecektir." +"Etkinleştirilirse, makine sınırları G-code dosyasına aktarılacaktır.\n" +"G-code tadı Klipper olarak ayarlandığında bu seçenek göz ardı edilecektir." msgid "This G-code will be used as a code for the pause print. Users can insert pause G-code in the G-code viewer." -msgstr "Bu G kodu duraklatma yazdırması için bir kod olarak kullanılacaktır. Kullanıcı gcode görüntüleyiciye duraklatma G kodunu ekleyebilir." +msgstr "Bu G-code duraklatma yazdırması için bir kod olarak kullanılacaktır. Kullanıcı G-code görüntüleyiciye duraklatma G-code'u ekleyebilir." msgid "This G-code will be used as a custom code." -msgstr "Bu G kodu özel kod olarak kullanılacak." +msgstr "Bu G-code özel kod olarak kullanılacak." msgid "Small area flow compensation (beta)" msgstr "Küçük alan akış telafisi (beta)" @@ -15721,7 +15885,7 @@ msgid "" "If your Marlin 2 printer uses Classic Jerk set this value to 0.)" msgstr "" "Maksimum bağlantı sapması (M205 J, yalnızca Marlin Aygıt Yazılımı için JD > 0 ise geçerlidir)\n" -"Marlin 2 yazıcınız Classic Jerk kullanıyorsa bu değeri 0 olarak ayarlayın.)" +"Marlin 2 yazıcınız Classic sarsıntı kullanıyorsa bu değeri 0 olarak ayarlayın.)" msgid "Minimum speed for extruding" msgstr "Ekstrüzyon için minimum hız" @@ -15744,6 +15908,7 @@ msgid "The allowed maximum output force of Y axis" msgstr "Y ekseninin izin verilen maksimum çıkış kuvveti" # AI Translated +msgctxt "Newton" msgid "N" msgstr "N" @@ -15756,6 +15921,7 @@ msgid "The machine bed mass load of Y axis" msgstr "Y ekseninin makine yatağı kütle yükü" # AI Translated +msgctxt "gram" msgid "g" msgstr "g" @@ -15969,7 +16135,7 @@ msgid "" "\n" "Allowed values: 0.5-5" msgstr "" -"Daha düşük bir değer, daha düzgün ekstrüzyon hızı geçişleriyle sonuçlanır. Ancak bu, önemli ölçüde daha büyük bir gcode dosyasına ve yazıcının işlemesi için daha fazla talimata neden olur.\n" +"Daha düşük bir değer, daha düzgün ekstrüzyon hızı geçişleriyle sonuçlanır. Ancak bu, önemli ölçüde daha büyük bir G-code dosyasına ve yazıcının işlemesi için daha fazla talimata neden olur.\n" "\n" "Varsayılan 3 değeri çoğu durumda işe yarar. Yazıcınız tutukluk yapıyorsa, yapılan ayarlama sayısını azaltmak için bu değeri artırın\n" "\n" @@ -16026,13 +16192,13 @@ msgid "Configuration notes" msgstr "Yapılandırma notları" msgid "You can put here your personal notes. This text will be added to the G-code header comments." -msgstr "Buraya kişisel notlarınızı yazabilirsiniz. Bu not G-kodu başlık yorumlarına eklenecektir." +msgstr "Buraya kişisel notlarınızı yazabilirsiniz. Bu not G-code başlık yorumlarına eklenecektir." msgid "Host Type" msgstr "Bağlantı Türü" msgid "Orca Slicer can upload G-code files to a printer host. This field must contain the kind of the host." -msgstr "Orca Slicer, G kodu dosyalarını bir yazıcı ana bilgisayarına yükleyebilir. Bu alan ana bilgisayarın türünü içermelidir." +msgstr "Orca Slicer, G-code dosyalarını bir yazıcı ana bilgisayarına yükleyebilir. Bu alan ana bilgisayarın türünü içermelidir." msgid "Nozzle volume" msgstr "Nozul hacmi" @@ -16080,8 +16246,8 @@ msgid "Reduce infill retraction" msgstr "Dolguda geri çekmeyi azalt" # AI Translated -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." -msgstr "Hareket tamamen dolgu alanı içindeyken geri çekme yapılmaz. Bu, sızıntının görülemeyeceği anlamına gelir. Bu, karmaşık modellerde geri çekme sayısını azaltabilir ve yazdırma süresinden tasarruf sağlayabilir, ancak dilimlemeyi ve G kodu oluşturmayı yavaşlatır. Geri çekmenin atlandığı alanlarda z-hop'un da uygulanmadığını unutmayın." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." +msgstr "Hareket tamamen dolgu alanı içindeyken geri çekme yapılmaz. Bu, sızıntının görülemeyeceği anlamına gelir. Bu, karmaşık modellerde geri çekme sayısını azaltabilir ve yazdırma süresinden tasarruf sağlayabilir, ancak dilimlemeyi ve G-code oluşturmayı yavaşlatır. Geri çekmenin atlandığı alanlarda Z-hop'un da uygulanmadığını unutmayın." msgid "This option will drop the temperature of the inactive extruders to prevent oozing." msgstr "Bu seçenek sızıntıyı önlemek için aktif olmayan ekstrüderlerin sıcaklığını düşürecektir." @@ -16167,7 +16333,7 @@ msgstr "" "İlave çevrelerin sabitleneceği dolgu sınırlı olduğundan, bu seçenekle birlikte yıldırım dolgusunun kullanılması önerilmez." msgid "If you want to process the output G-code through custom scripts, just list their absolute paths here. Separate multiple scripts with a semicolon. Scripts will be passed the absolute path to the G-code file as the first argument, and they can access the Orca Slicer config settings by reading environment variables." -msgstr "Çıktı G-kodunu özel komut dosyaları aracılığıyla işlemek istiyorsanız, mutlak yollarını burada listeleyin. Birden fazla betiği noktalı virgülle ayırın. Betiklere ilk argüman olarak G-code dosyasının mutlak yolu aktarılır ve ortam değişkenlerini okuyarak Orca Slicer yapılandırma ayarlarına erişebilirler." +msgstr "Çıktı G-code'u özel komut dosyaları aracılığıyla işlemek istiyorsanız, mutlak yollarını burada listeleyin. Birden fazla betiği noktalı virgülle ayırın. Betiklere ilk argüman olarak G-code dosyasının mutlak yolu aktarılır ve ortam değişkenlerini okuyarak Orca Slicer yapılandırma ayarlarına erişebilirler." # AI Translated msgid "Change extrusion role G-code (process)" @@ -16235,7 +16401,7 @@ msgid "Object will be raised by this number of support layers. Use this function msgstr "Nesne bu sayıdaki destek katmanı tarafından yükseltilecektir. ABS yazdırırken sarmayı önlemek için bu işlevi kullanın." msgid "The G-code path is generated after simplifying the contour of models to avoid too many points and G-code lines. Smaller values mean higher resolution and more time required to slice." -msgstr "Gcode dosyasında çok fazla nokta ve gcode çizgisinin olmaması için modelin konturu basitleştirildikten sonra G-code yolu oluşturulur. Daha küçük değer, daha yüksek çözünürlük ve dilimleme için daha fazla zaman anlamına gelir." +msgstr "G-code dosyasında çok fazla nokta ve G-code çizgisinin olmaması için modelin konturu basitleştirildikten sonra G-code yolu oluşturulur. Daha küçük değer, daha yüksek çözünürlük ve dilimleme için daha fazla zaman anlamına gelir." msgid "Travel distance threshold" msgstr "Seyahat mesafesi" @@ -16256,7 +16422,7 @@ msgstr "Silme sonrası geri çekme miktarı" # AI Translated #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" "Silme sonrasındaki hızlı geri çekmenin uzunluğu; geri çekme uzunluğuna göredir.\n" @@ -16292,10 +16458,18 @@ msgstr "Ekstruder değiştiğinde uzun geri çekilme" msgid "Retraction distance when extruder change" msgstr "Ekstruder değiştiğinde geri çekilme mesafesi" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Geri çekme uzunluğu (Takım değişimi)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Takım değişiminden önce geri çekme tetiklendiğinde, filament belirtilen miktarda geri çekilir (uzunluk, ekstrudere girmeden önce ham filament üzerinde ölçülür)." + msgid "Z-hop height" msgstr "Z-Sıçrama yüksekliği" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "Geri çekme işlemi her yapıldığında, nozul ile baskı arasında açıklık oluşturmak için nozul biraz kaldırılır. Hareket halindeyken nozulun baskıya çarpmasını önler. Z'yi kaldırmak için spiral çizgi kullanmak çekmeyi önleyebilir." msgid "Z-hop lower boundary" @@ -16391,6 +16565,10 @@ msgstr "Yeniden başlatma sırasında ekstra uzunluk" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "İlerleme hareketinden sonra geri çekilme telafi edildiğinde, ekstruder bu ek filament miktarını itecektir. Bu ayara nadiren ihtiyaç duyulur." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Yeniden başlatma sırasında ekstra uzunluk (Takım değişimi)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Takım değiştirildikten sonra geri çekilme telafi edildiğinde, ekstruder bu ilave filament miktarını itecektir." @@ -16427,7 +16605,7 @@ msgid "Disable set remaining print time" msgstr "Kalan yazdırma süresini ayarlamayı devre dışı bırak" msgid "Disable generating of the M73: Set remaining print time in the final G-code." -msgstr "M73'ün oluşturulmasını devre dışı bırakın: Son gcode'da kalan yazdırma süresini ayarlayın." +msgstr "M73'ün oluşturulmasını devre dışı bırakın: Son G-code'da kalan yazdırma süresini ayarlayın." msgid "Seam position" msgstr "Dikiş konumu" @@ -16648,7 +16826,7 @@ msgstr "" "Nihai döngü sayısı, nesnelerin mesafesini düzenlerken veya doğrularken dikkate alınmaz. Böyle bir durumda döngü sayısını artırın." msgid "The printing speed in exported G-code will be slowed down when the estimated layer time is shorter than this value in order to get better cooling for these layers." -msgstr "Tahmini katman süresi bu değerden kısa olduğunda, bu katmanlar için daha iyi soğutma sağlamak amacıyla, dışa aktarılan gcode'daki yazdırma hızı yavaşlatılacaktır." +msgstr "Tahmini katman süresi bu değerden kısa olduğunda, bu katmanlar için daha iyi soğutma sağlamak amacıyla, dışa aktarılan G-code'daki yazdırma hızı yavaşlatılacaktır." msgid "Minimum sparse infill threshold" msgstr "Minimum seyrek dolgu" @@ -16756,16 +16934,16 @@ msgid "Insert multiple preheat commands (e.g. M104.1). Only useful for Prusa XL. msgstr "Birden fazla ön ısıtma komutu ekleyin (örn. M104.1). Yalnızca Prusa XL için kullanışlıdır. Diğer yazıcılar için lütfen 1’e ayarlayın." msgid "G-code written at the very top of the output file, before any other content. Useful for adding metadata that printer firmware reads from the first lines of the file (e.g. estimated print time, filament usage). Supports placeholders like {print_time_sec} and {used_filament_length}." -msgstr "G kodu, çıktı dosyasının en üstünde, diğer içeriklerden önce yazılır. Yazıcı ürün yazılımının dosyanın ilk satırlarından okuduğu meta verileri (ör. tahmini yazdırma süresi, filament kullanımı) eklemek için kullanışlıdır. {print_time_sec} ve {used_filament_length} gibi yer tutucuları destekler." +msgstr "G-code, çıktı dosyasının en üstünde, diğer içeriklerden önce yazılır. Yazıcı ürün yazılımının dosyanın ilk satırlarından okuduğu meta verileri (ör. tahmini yazdırma süresi, filament kullanımı) eklemek için kullanışlıdır. {print_time_sec} ve {used_filament_length} gibi yer tutucuları destekler." msgid "Start G-code" -msgstr "Başlangıç G Kodu" +msgstr "Başlangıç G-code" msgid "G-code added when starting a print." -msgstr "Baskı başladığında çalışacak G Kodu." +msgstr "Baskı başladığında çalışacak G-code." msgid "G-code added when the printer starts using this filament" -msgstr "Bu filament kullanılırken yazıcı başladığında eklenen G-kodu" +msgstr "Bu filament kullanılırken yazıcı başladığında eklenen G-code" msgid "Single Extruder Multi Material" msgstr "Tek ekstruder çoklu malzeme" @@ -16777,7 +16955,7 @@ msgid "Manual Filament Change" msgstr "Manuel filament değişimi" msgid "Enable this option to omit the custom Change filament G-code only at the beginning of the print. The tool change command (e.g., T0) will be skipped throughout the entire print. This is useful for manual multi-material printing, where we use M600/PAUSE to trigger the manual filament change action." -msgstr "Sadece baskının başında özel Filament Değiştirme G-kodu'nu atlamak için bu seçeneği etkinleştirin. Aracı değiştirme komutu (örneğin, T0), baskının tamamı boyunca atlanacaktır. Bu, manuel çoklu malzeme baskısı için kullanışlıdır, burada manuel filament değişim eylemini tetiklemek için M600/PAUSE kullanırız." +msgstr "Sadece baskının başında özel Filament Değiştirme G-code'u atlamak için bu seçeneği etkinleştirin. Aracı değiştirme komutu (örneğin, T0), baskının tamamı boyunca atlanacaktır. Bu, manuel çoklu malzeme baskısı için kullanışlıdır, burada manuel filament değişim eylemini tetiklemek için M600/PAUSE kullanırız." msgid "Wipe tower type" msgstr "Temizleme kulesi tipi" @@ -16808,6 +16986,14 @@ msgstr "Silme kulesinde takım değişimi" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Takım değişimi komutu (Tx) verilmeden önce baskı kafasını silme kulesine gitmeye zorlar. Yalnızca Tip 2 silme kulesi kullanan çok ekstruderli (çok baskı kafalı) yazıcılar için geçerlidir. Orca, çok baskı kafalı makinelerde bu seyahati varsayılan olarak atlar çünkü kafa değişimini ürün yazılımı yönetir; bu da Tx komutunun yazdırılan parçanın üzerinde verilmesine yol açabilir. Takım değişiminin her zaman silme kulesinin üzerinde verilmesini istiyorsanız bu seçeneği etkinleştirin." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Silme kulesinde sıcaklığı bekle" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Yeni takımı baskı sıcaklığına ulaşmasını beklemeden alır, silme kulesine gider ve sıcaklığı orada, yıkamadan hemen önce bekler. Isınma sırasında sızan malzeme modele değil kuleye düşer ve hareket ısınmayla çakışır. Yalnızca 2. tip silme kulesi kullanan çok ekstruderli (çok baskı kafalı) yazıcılar için geçerlidir. Donanım yazılımı veya takım değişimi makrosu sıcaklığı kendisi beklememelidir. Devre dışı bırakıldığında, sıcaklık bekleme komutu takım değişimi komutundan hemen sonra verilir." + msgid "No sparse layers (beta)" msgstr "Seyrek katman yok (beta)" @@ -16866,7 +17052,7 @@ msgid "Z offset" msgstr "Z ofseti" msgid "This value will be added (or subtracted) from all the Z coordinates in the output G-code. It is used to compensate for bad Z endstop position: for example, if your endstop zero actually leaves the nozzle 0.3mm far from the print bed, set this to -0.3 (or fix your endstop)." -msgstr "Bu değer, çıkış G-kodu içindeki tüm Z koordinatlarına eklenir (veya çıkarılır).Bu, kötü Z endstop konumunu telafi etmek için kullanılır: örneğin, endstop sıfır noktanız aslında nozulu baskı plakasından 0.3mm uzakta bırakıyorsa, bu değeri -0.3 olarak ayarlayın (veya endstop'unuzu düzeltin)." +msgstr "Bu değer, çıkış G-code içindeki tüm Z koordinatlarına eklenir (veya çıkarılır).Bu, kötü Z endstop konumunu telafi etmek için kullanılır: örneğin, endstop sıfır noktanız aslında nozulu baskı plakasından 0.3mm uzakta bırakıyorsa, bu değeri -0.3 olarak ayarlayın (veya endstop'unuzu düzeltin)." msgid "Enable support" msgstr "Desteği etkinleştir" @@ -16914,7 +17100,7 @@ msgid "This setting only generates supports that begin on the build plate." msgstr "Model yüzeyinde destek oluşturmayın, yalnızca baskı plakasında." msgid "Support critical regions only" -msgstr "Yalnızca kritik bölgeleri destekleyin" +msgstr "Kritik bölgeleri destekleyin" msgid "Only create support for critical regions including sharp tail, cantilever, etc." msgstr "Yalnızca keskin kuyruk, konsol vb. gibi kritik bölgeler için destek oluşturun." @@ -17217,7 +17403,7 @@ msgstr "" "\n" "PLA, PETG, TPU, PVA ve diğer düşük sıcaklıktaki malzemeler için, ısı kırılmasında malzemenin yumuşamasından kaynaklanan ekstrüderin tıkanmasını önlemek için oda sıcaklığının düşük olması gerektiğinden bu seçenek devre dışı bırakılmalıdır (0’a ayarlanmalıdır).\n" "\n" -"Etkinleştirilirse, bu parametre aynı zamanda istenen oda sıcaklığını yazdırma başlatma makronuza veya şuna benzer bir ısı emme makrosuna iletmek için kullanılabilecek Chamber_temperature adlı bir gcode değişkenini de ayarlar: PRINT_START (diğer değişkenler) CHAMBER_TEMP=[chamber_temperature]. Yazıcınız M141/M191 komutlarını desteklemiyorsa veya aktif oda ısıtıcısı takılı değilse yazdırma başlatma makrosunda ısı bekletme işlemini gerçekleştirmek istiyorsanız bu yararlı olabilir." +"Etkinleştirilirse, bu parametre aynı zamanda istenen oda sıcaklığını yazdırma başlatma makronuza veya şuna benzer bir ısı emme makrosuna iletmek için kullanılabilecek Chamber_temperature adlı bir G-code değişkenini de ayarlar: PRINT_START (diğer değişkenler) CHAMBER_TEMP=[chamber_temperature]. Yazıcınız M141/M191 komutlarını desteklemiyorsa veya aktif oda ısıtıcısı takılı değilse yazdırma başlatma makrosunda ısı bekletme işlemini gerçekleştirmek istiyorsanız bu yararlı olabilir." # AI Translated msgid "" @@ -17247,10 +17433,10 @@ msgid "This detects thin walls which can’t contain two lines and uses a single msgstr "İki çizgi genişliğini içeremeyen ince duvarı tespit edin. Ve yazdırmak için tek satır kullanın. Kapalı döngü olmadığından pek iyi basılmamış olabilir." msgid "This G-code is inserted when filament is changed, including T commands to trigger tool change." -msgstr "Bu gcode, takım değişimini tetiklemek için T komutu da dahil olmak üzere filament değiştirildiğinde eklenir." +msgstr "Bu G-code, takım değişimini tetiklemek için T komutu da dahil olmak üzere filament değiştirildiğinde eklenir." msgid "This G-code is inserted when the extrusion role is changed." -msgstr "Bu gcode, ekstrüzyon rolü değiştirildiğinde eklenir." +msgstr "Bu G-code, ekstrüzyon rolü değiştirildiğinde eklenir." # AI Translated msgid "Change extrusion role G-code (filament)" @@ -17348,6 +17534,14 @@ msgstr "" "\n" "Aşağıdaki silme ayarından önce geri çekme miktarına bir değer ayarlamak, silme işleminden önce aşırı geri çekme işlemini gerçekleştirecektir, aksi takdirde silme işleminden sonra gerçekleştirilecektir." +# AI Translated +msgid "Mixed color sublayer" +msgstr "Karışık renk alt katmanı" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "Karışık renk alt katmanlarına bölmeyi etkinleştirir. Etkinleştirildiğinde, karışık renkli filament içeren katmanlar renk karışımı efektleri elde etmek için alt katmanlara bölünür." + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "Temizleme kulesi, nesneleri yazdırırken görünüm kusurlarını önlemek amacıyla nozul üzerindeki kalıntıları temizlemek ve nozul içindeki oda basıncını dengelemek için kullanılabilir." @@ -17602,10 +17796,10 @@ msgid "Picture sizes to be stored into a .gcode and .sl1 / .sl1s files, in the f msgstr "Resim boyutları aşağıdaki formatta bir .gcode ve .sl1 / .sl1s dosyalarında saklanacaktır: \"XxY, XxY, ...\"" msgid "Format of G-code thumbnails" -msgstr "G kodu küçük resimlerinin formatı" +msgstr "G-code küçük resimlerinin formatı" msgid "Format of G-code thumbnails: PNG for best quality, JPG for smallest size, QOI for low memory firmware." -msgstr "G kodu küçük resimlerinin formatı: En iyi kalite için PNG, en küçük boyut için JPG, düşük bellekli donanım yazılımı için QOI." +msgstr "G-code küçük resimlerinin formatı: En iyi kalite için PNG, en küçük boyut için JPG, düşük bellekli donanım yazılımı için QOI." msgid "Use relative E distances" msgstr "Göreceli (relative) E mesafelerini kullan" @@ -17855,7 +18049,7 @@ msgid "No check" msgstr "Kontrol yok" msgid "Do not run any validity checks, such as G-code path conflicts check." -msgstr "Gcode yol çakışmaları kontrolü gibi herhangi bir geçerlilik kontrolü çalıştırmayın." +msgstr "G-code yol çakışmaları kontrolü gibi herhangi bir geçerlilik kontrolü çalıştırmayın." msgid "Normative check" msgstr "Normatif kontrol" @@ -18019,10 +18213,10 @@ msgid "If enabled, this slicing will be considered using timelapse." msgstr "Etkinleştirilirse, bu dilimleme hızlandırılmış çekim kullanılarak değerlendirilecektir." msgid "Load custom G-code" -msgstr "Özel gcode yükle" +msgstr "Özel G-code yükle" msgid "Load custom G-code from json." -msgstr "Json'dan özel gcode yükleyin." +msgstr "Json'dan özel G-code yükleyin." msgid "Load filament IDs" msgstr "Filament kimliklerini yükle" @@ -18049,10 +18243,10 @@ msgid "If enabled, Arrange will avoid extrusion calibrate region when placing ob msgstr "Etkinleştirilirse, nesne yerleştirildiğinde düzenleme ekstrüzyon kalibrasyon bölgesini önleyecektir." msgid "Skip modified G-code in 3MF" -msgstr "3mf’de değiştirilmiş gcode’ları atla" +msgstr "3mf’de değiştirilmiş G-code’ları atla" msgid "Skip the modified G-code in 3MF from printer or filament presets." -msgstr "Yazıcı veya filament Ön Ayarlarından 3mf’deki değiştirilmiş gcode’ları atlayın." +msgstr "Yazıcı veya filament Ön Ayarlarından 3mf’deki değiştirilmiş G-code’ları atlayın." msgid "MakerLab name" msgstr "MakerLab adı" @@ -18086,16 +18280,16 @@ msgid "Allow 3MF with newer version to be sliced." msgstr "Daha yeni sürüme sahip 3mf’nin dilimlenmesine izin ver." msgid "Current Z-hop" -msgstr "Mevcut z-hop" +msgstr "Mevcut Z-hop" msgid "Contains Z-hop present at the beginning of the custom G-code block." -msgstr "Özel G kodu bloğunun başında bulunan z-hop'u içerir." +msgstr "Özel G-code bloğunun başında bulunan Z-hop'u içerir." msgid "Position of the extruder at the beginning of the custom G-code block. If the custom G-code travels somewhere else, it should write to this variable so OrcaSlicer knows where it travels from when it gets control back." -msgstr "Ekstruderin özel G kodu bloğunun başlangıcındaki konumu. Özel G kodu başka bir yere seyahat ederse, Slicer'ın kontrolü geri aldığında nereden seyahat ettiğini bilmesi için bu değişkene yazması gerekir." +msgstr "Ekstruderin özel G-code bloğunun başlangıcındaki konumu. Özel G-code başka bir yere seyahat ederse, Slicer'ın kontrolü geri aldığında nereden seyahat ettiğini bilmesi için bu değişkene yazması gerekir." msgid "Retraction state at the beginning of the custom G-code block. If the custom G-code moves the extruder axis, it should write to this variable so OrcaSlicer de-retracts correctly when it gets control back." -msgstr "Özel G kodu bloğunun başlangıcındaki geri çekilme durumu. Özel G kodu ekstruder eksenini hareket ettirirse, Slicer'ın kontrolü geri aldığında doğru şekilde geri çekme yapması için bu değişkene yazması gerekir." +msgstr "Özel G-code bloğunun başlangıcındaki geri çekilme durumu. Özel G-code ekstruder eksenini hareket ettirirse, Slicer'ın kontrolü geri aldığında doğru şekilde geri çekme yapması için bu değişkene yazması gerekir." msgid "Extra de-retraction" msgstr "Ekstra deretraksiyon" @@ -18248,10 +18442,10 @@ msgid "Total number of objects in the print." msgstr "Baskıdaki toplam nesne sayısı." msgid "Number of instances" -msgstr "Örnek sayısı" +msgstr "Eş kopya sayısı" msgid "Total number of object instances in the print, summed over all objects." -msgstr "Tüm nesneler üzerinden toplanan, yazdırmadaki nesne örneklerinin toplam sayısı." +msgstr "Tüm nesneler genelinde toplanmış, baskıdaki toplam nesne eş kopyası (instance) sayısı." msgid "Scale per object" msgstr "Nesne başına ölçeklendirme" @@ -18436,6 +18630,10 @@ msgstr "Bir model dosyasının meshlenmesi başarısız oldu veya geçerli bir msgid "The supplied file couldn't be read because it's empty." msgstr "Seçilen dosya boş olduğundan okunamıyor." +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "Dosya biçimi uyumsuz olduğundan ayrıştırılamıyor." + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Bilinmeyen dosya formatı. Giriş dosyası .stl, .obj, .amf(.xml) uzantılı olmalıdır." @@ -19288,13 +19486,13 @@ msgid "" "To test Classic Jerk, set 'Maximum Junction Deviation' in Motion ability to 0." msgstr "" "Marlin 2 Kavşak Sapması tespit edildi:\n" -"Classic Jerk'i test etmek için Hareket yeteneğinde 'Maksimum Kavşak Sapması'nı 0'a ayarlayın." +"Classic sarsıntıyı test etmek için Hareket yeteneğinde 'Maksimum Kavşak Sapması'nı 0'a ayarlayın." msgid "" "Marlin 2 Classic Jerk detected:\n" "To test Junction Deviation, set 'Maximum Junction Deviation' in Motion ability to a value > 0." msgstr "" -"Marlin 2 Classic Jerk tespit edildi:\n" +"Marlin 2 Classic sarsıntı tespit edildi:\n" "Kavşak Sapmasını test etmek için Hareket yeteneğinde 'Maksimum Kavşak Sapması'nı > 0 değerine ayarlayın." msgid "" @@ -19356,7 +19554,7 @@ msgid "Only materials of the same type can be selected." msgstr "Yalnızca aynı tipteki malzemeler seçilebilir." msgid "Send G-code to printer host" -msgstr "G Kodunu yazıcı ana bilgisayarına gönder" +msgstr "G-code'u yazıcı ana bilgisayarına gönder" msgid "Upload to Printer Host with the following filename:" msgstr "Yazıcıya aşağıdaki dosya adıyla yükleyin:" @@ -19538,7 +19736,7 @@ msgid "Start Test Single-Thread" msgstr "Tek İş Parçacığı Testini Başlat" msgid "Export Log" -msgstr "Logu Dışa Aktar" +msgstr "Logu dışa aktar" msgid "OrcaSlicer Version:" msgstr "OrcaSlicer Sürümü:" @@ -19934,19 +20132,19 @@ msgstr "Yazıcı adlarını yalnızca yazıcı, filament ve işlem ön ayarları msgid "Only display the filament names with changes to filament presets." msgstr "Filament adlarını yalnızca filament ön ayarlarında yapılan değişikliklerle görüntüleyin." -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "Yalnızca kullanıcı yazıcı ön ayarlarına sahip yazıcı adları görüntülenecek ve seçtiğiniz her ön ayar zip olarak dışa aktarılacaktır." msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" "Yalnızca kullanıcı filamenti ön ayarlarına sahip filament adları \n" "görüntülenecek ve seçtiğiniz her filament adındaki tüm kullanıcı filamenti ön ayarları zip olarak dışa aktarılacaktır." msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" "Yalnızca işlem ön ayarları değiştirilen yazıcı adları görüntülenecek \n" "ve seçtiğiniz her yazıcı adındaki tüm kullanıcı işlem ön ayarları zip olarak dışa aktarılacaktır." @@ -20092,9 +20290,6 @@ msgstr "Fiziksel Yazıcı" msgid "Print Host upload" msgstr "Yazıcı Bağlantı Ayarları" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Yazıcı iletişimi için ağ aracısı uygulamasını seçin. Kullanılabilir aracılar başlangıçta kaydedilir." - # AI Translated msgid "Select a Flashforge printer" msgstr "Bir Flashforge yazıcısı seçin" @@ -20194,7 +20389,7 @@ msgid "We need information for diagnosing source of the issue. Check wiki page f msgstr "Sorunun kaynağını teşhis etmek için bilgiye ihtiyacımız var. Ayrıntılı kılavuz için wiki sayfasına bakın." # AI Translated -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "Paketle düğmesi, geçerli oturumun proje dosyasını ve günlüklerini bir zip dosyasında toplar." # AI Translated @@ -20250,7 +20445,7 @@ msgid "Stored logs" msgstr "Saklanan günlükler" # AI Translated -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "Saklanan tüm günlükleri bir zip dosyasında paketler." # AI Translated @@ -20265,9 +20460,8 @@ msgstr "İletişim kutusunu kapatıp projeyi incelemek için HAYIR'ı seçin." msgid "No project file on current session. Only logs will be included to package" msgstr "Geçerli oturumda proje dosyası yok. Pakete yalnızca günlükler eklenecek" -# AI Translated msgid "Please make sure any instances of OrcaSlicer are not running" -msgstr "Lütfen çalışan bir OrcaSlicer örneği olmadığından emin olun" +msgstr "Lütfen hiçbir OrcaSlicer örneğinin çalışmadığından emin olun" # AI Translated msgid "System folder cannot be deleted because some files are in use by another application. Please close any applications using these files and try again." @@ -20281,7 +20475,6 @@ msgstr "Sistem klasörü silinemedi..." msgid "Failed to determine executable path." msgstr "Yürütülebilir dosya yolu belirlenemedi." -# AI Translated msgid "Failed to launch a new instance." msgstr "Yeni bir örnek başlatılamadı." @@ -20338,7 +20531,7 @@ msgid "Authorizing..." msgstr "Yetkilendiriliyor..." # AI Translated -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "Hata. Yetkilendirme için api belirteci alınamıyor" # AI Translated @@ -20795,8 +20988,8 @@ msgid "Enable smart filament assign: Assign one filament to multiple nozzles to msgstr "Akıllı filament atamasını etkinleştir: Tasarrufu en üst düzeye çıkarmak için bir filamenti birden çok nozula atayın" # AI Translated -msgid "Fila Saving" -msgstr "Filament Tasarrufu" +msgid "File Saving" +msgstr "Dosya Kaydetme" msgid "Don't remind me again" msgstr "Bana bir daha hatırlatma" @@ -21037,9 +21230,6 @@ msgstr "Giriş yapmaya çalışırken beklenmeyen bir şey oldu, lütfen tekrar msgid "User canceled." msgstr "Kullanıcı iptal edildi." -msgid "Head diameter" -msgstr "Kafa çapı" - msgid "Max angle" msgstr "Maksimum açı" @@ -21207,6 +21397,9 @@ msgstr "Şimdi Yeniden Başlat" msgid "NO RAMMING AT ALL" msgstr "HİÇBİR ŞEKİLDE ÇARPMA YOK" +msgid "s" +msgstr "s" + msgid "Volumetric speed" msgstr "Hacimsel hız" @@ -21610,8 +21803,8 @@ msgid "" "G-code window\n" "You can turn on/off the G-code window by pressing the <b>C</b> key." msgstr "" -"G-kodu penceresi\n" -"<b>C</b> tuşuna basarak G*kodu penceresini açabilir/kapatabilirsiniz." +"G-code penceresi\n" +"<b>C</b> tuşuna basarak G-code penceresini açabilir/kapatabilirsiniz." #: resources/data/hints.ini: [hint:Switch workspaces] msgid "" @@ -21768,7 +21961,8 @@ msgstr "" "Baskılarınızı plakalara ayırın\n" "Çok sayıda parçası olan bir modeli baskıya hazır ayrı kalıplara bölebileceğinizi biliyor muydunuz? Bu, tüm parçaları takip etme sürecini basitleştirecektir." -#: resources/data/hints.ini: [hint:Speed up your print with Adaptive Layer Height] +#: resources/data/hints.ini: [hint:Speed up your print with Adaptive Layer +#: Height] msgid "" "Speed up your print with Adaptive Layer Height\n" "Did you know that you can print a model even faster by using the Adaptive Layer Height option? Check it out!" @@ -21841,7 +22035,8 @@ msgstr "" "Gücü artırın\n" "Modelin gücünü artırmak için daha fazla duvar halkası ve daha yüksek seyrek dolgu yoğunluğu kullanabileceğinizi biliyor muydunuz?" -#: resources/data/hints.ini: [hint:When do you need to print with the printer door opened] +#: resources/data/hints.ini: [hint:When do you need to print with the printer +#: door opened] msgid "" "When do you need to print with the printer door opened?\n" "Did you know that opening the printer door can reduce the probability of extruder/hotend clogging when printing lower temperature filament with a higher enclosure temperature? There is more info about this in the Wiki." @@ -21857,6 +22052,57 @@ msgstr "" "Eğilmeyi önleyin\n" "ABS gibi bükülmeye yatkın malzemelere baskı yaparken, ısıtma yatağı sıcaklığının uygun şekilde arttırılmasının bükülme olasılığını azaltabileceğini biliyor muydunuz?" +# AI Translated +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "Yerel Wayland canlı görüntüsü, GStreamer GTK video alıcısını gerektirir. Lütfen GStreamer için gtksink eklentisini yükleyin ve ardından OrcaSlicer'ı yeniden başlatın." + +# AI Translated +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "Yerel Wayland GStreamer video alıcısı başlatılamadı. Lütfen GStreamer GTK eklentisi kurulumunuzu denetleyin." + +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "Bu görev için Windows Media Player gereklidir! İşletim sisteminiz için ‘Windows Media Player’ı etkinleştirmek istiyor musunuz?" + +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "BambuSource medya oynatımı için doğru şekilde kaydedilmemiş! Yeniden kaydetmek için Evet’e basın" + +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "Medya oynatma için kayıtlı BambuSource bileşeni eksik! Lütfen OrcaSlicer'ı yeniden yükleyin veya topluluk yardımı isteyin." + +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "Farklı bir kurulumdan bir BambuSource kullanıyorsunuz, video oynatma doğru çalışmayabilir! Düzeltmek için Evet’e basın." + +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "Sisteminizde video oynatmak için gerekli olan GStreamer H.264 codec bileşenleri eksik. (gstreamer1.0-plugins-bad veya gstreamer1.0-libav paketlerini kurmayı deneyin, ardından Orca Slicer’ı yeniden başlatın)" + +# AI Translated +#~ msgid "N" +#~ msgstr "N" + +# AI Translated +#~ msgid "g" +#~ msgstr "g" + +# AI Translated +#~ msgid "Fila Saving" +#~ msgstr "Filament Tasarrufu" + +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Katman yüksekliği çok küçük.\n" +#~ "min_layer_height olarak ayarlanacak\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Katman yüksekliği, Yazıcı Ayarları -> Ekstruder -> Katman yüksekliği sınırları bölümündeki sınırı aşıyor bu durum baskı kalitesi sorunlarına neden olabilir." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Ayarlanan aralığa otomatik olarak ayarlansın mı?\n" + +#~ msgid "Head diameter" +#~ msgstr "Kafa çapı" + #~ msgid "Print order within a single layer." #~ msgstr "Tek bir katmanda yazdırma sırası." diff --git a/localization/i18n/uk/OrcaSlicer_uk.po b/localization/i18n/uk/OrcaSlicer_uk.po index 464cd4042a..3d23f7bbb1 100644 --- a/localization/i18n/uk/OrcaSlicer_uk.po +++ b/localization/i18n/uk/OrcaSlicer_uk.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: orcaslicerua\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "PO-Revision-Date: 2026-07-17 16:25+0300\n" "Last-Translator: Andrij Mizyk <andm1zyk@proton.me>\n" "Language-Team: Ukrainian\n" @@ -2906,6 +2906,10 @@ msgstr "Змінити" msgid "Merge with" msgstr "Обʼєднати з" +# AI Translated +msgid "Decompose Color" +msgstr "Розкласти колір" + msgid "Delete this filament" msgstr "Видалити цей філамент" @@ -3197,6 +3201,10 @@ msgstr "Збірка" msgid "Merge parts to an object" msgstr "Обʼєднати частини в обʼєкт" +# AI Translated +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "Використання змінної висоти шару разом із підшаром змішаного кольору може погіршити якість змішування кольорів." + msgid "Add layers" msgstr "Додати шари" @@ -4142,10 +4150,10 @@ msgid "PA Profile" msgstr "Профіль PA" msgid "Factor K" -msgstr "Коэф. K" +msgstr "Коеф. K" msgid "Factor N" -msgstr "Коэф. N" +msgstr "Коеф. N" msgid "Setting AMS slot information while printing is not supported" msgstr "Зміна інформації про слоти AMS під час друку не підтримується" @@ -4716,6 +4724,23 @@ msgstr "Поточна температура камери вища, ніж бе msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Мінімальна температура камери (%d℃) вища за цільову температуру камери (%d℃). Мінімальне значення — це поріг, за якого починається друк, поки камера продовжує нагріватися до цільової температури, тому воно не повинно її перевищувати. Значення буде обмежено цільовим." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "Висота шару занадто мала. Буде встановлено мінімальне значення (%g мм)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "Висота шару виходить за межі, задані в Налаштуваннях принтера -> Екструдер -> Ліміти висоти шару, це може призвести до проблем з якістю друку." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Автоматично налаштувати до межі (%g мм)?" + +msgid "Adjust" +msgstr "Налаштувати" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4764,7 +4789,7 @@ msgstr "" "\n" "Значення буде скинуто на 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Чергування додаткової стінки не працює добре, якщо для параметра \"Забезпечити товщину вертикальної оболонки\" встановлено значення \"Всі\"." msgid "" @@ -4810,7 +4835,7 @@ msgstr "" "НІ - Залишити незалежну висоту шарів підтримки" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height має бути менше висоти шару.\n" @@ -4819,7 +4844,7 @@ msgstr "" # AI Translated #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "Глибина зчеплення має бути меншою за глибину оболонки.\n" @@ -4839,6 +4864,13 @@ msgstr "" "Так - Увімкнути генератор стінок Arachne\n" "Ні - Вимкнути генератор стінок Arachne і встановити режим [Зміщення] для шорсткої поверхні" +# AI Translated +msgid "Brim ear radius" +msgstr "Радіус вушка кайми" + +msgid "Brim width" +msgstr "Ширина кайми" + # AI Translated msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Спіральний режим працює лише тоді, коли кількість стінок дорівнює 1, підтримки вимкнено, виявлення налипання зондуванням вимкнено, кількість верхніх шарів оболонки дорівнює 0, щільність часткового заповнення дорівнює 0, а тип таймлапсу — традиційний." @@ -5104,6 +5136,14 @@ msgstr "Не вдалося згенерувати калібрувальний msgid "Calibration error" msgstr "Помилка калібрування" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "На цьому принтері не налаштовано обладнання, потрібне для цього елемента керування." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Цей елемент керування не підтримується на цьому принтері." + # AI Translated msgid "Network unavailable" msgstr "Мережа недоступна" @@ -5978,7 +6018,7 @@ msgid "Size:" msgstr "Розмір:" # AI Translated -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Виявлено конфлікти шляхів G-коду на шарі %d, Z = %.2lf мм. Будь ласка, рознесіть конфліктуючі обʼєкти далі один від одного (%s <-> %s)." @@ -6170,6 +6210,10 @@ msgstr "Багато пристроїв" msgid "Project" msgstr "Проєкт" +# AI Translated +msgid "Device (Web)" +msgstr "Пристрій (Веб)" + msgid "Yes" msgstr "Так" @@ -6303,10 +6347,10 @@ msgstr "Імпорт 3MF/STL/STEP/SVG/OBJ/AMF" msgid "Load a model" msgstr "Завантажте модель" -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "Імпорт Zip-архіву" -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "Завантажити моделі, що містяться в zip-архіві" msgid "Import Configs" @@ -7855,6 +7899,10 @@ msgstr "Пристосувати поточну пластину" msgid "The %s nozzle can not print %s." msgstr "Сопло %s не може друкувати %s." +# AI Translated +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "Друк філаменту змішаного кольору на принтері з одним екструдером потребує частої зміни філаменту та промивок, що може суттєво збільшити кількість відходів і ризик засмічення сопла або жолоба для відходів." + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "Змішування %1% з %2% в друці не рекомендується.\n" @@ -7983,12 +8031,44 @@ msgstr "Синхронізувати список ниток з AMS" msgid "Set filaments to use" msgstr "Встановити філаменти для використання" +# AI Translated +msgid "Add Mixed Filament" +msgstr "Додати змішаний філамент" + +# AI Translated +msgid "Mixed Filament" +msgstr "Змішаний філамент" + +# AI Translated +msgid "Remove last mixed filament" +msgstr "Вилучити останній змішаний філамент" + +# AI Translated +msgid "Add mixed filament" +msgstr "Додати змішаний філамент" + +# AI Translated +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "У змішаного філаменту недійсні або невідповідні компоненти. Будь ласка, відредагуйте відповідні записи ще раз." + msgid "Search plate, object and part." msgstr "Пошук пластини, об’єкта і деталі." msgid "Pellets" msgstr "Гранули" +# AI Translated +msgid "Mixed filament has broken component references" +msgstr "У змішаного філаменту пошкоджені посилання на компоненти" + +# AI Translated +msgid "Edit / Delete / Merge" +msgstr "Редагувати / Видалити / Обʼєднати" + +# AI Translated +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "Цільовий змішаний філамент використовує цей фізичний філамент як компонент. Обʼєднання вилучить цей фізичний філамент і може зробити змішаний філамент недійсним. Продовжити?" + # AI Translated #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." @@ -8144,8 +8224,8 @@ msgid "Customized Preset" msgstr "Пристосований пресет" # AI Translated -msgid "Component name(s) inside step file not in UTF8 format!" -msgstr "Назви компонентів усередині файлу STEP не у форматі UTF8!" +msgid "Component name(s) inside step file not in UTF-8 format!" +msgstr "Назви компонентів усередині файлу STEP не у форматі UTF-8!" msgid "Because of unsupported text encoding, garbage characters may appear!" msgstr "Через непідтримуване кодування тексту можуть зʼявлятися непотрібні символи!" @@ -8166,7 +8246,7 @@ msgstr "Обʼєм обʼєкта дорівнює нулю" #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "Обʼєкт із файлу %s занадто малий, можливо, в метрах або дюймах.\n" "Ви хочете масштабувати до міліметрів?" @@ -8187,6 +8267,14 @@ msgstr "" msgid "Multi-part object detected" msgstr "Виявлено обʼєкт, що складається з кількох частин" +# AI Translated +msgid "Matching textures to filaments" +msgstr "Зіставлення текстур із філаментами" + +# AI Translated +msgid "Texture Import Warning" +msgstr "Попередження під час імпорту текстури" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Завантажити ці файли як єдиний обʼєкт з кількома частинами?\n" @@ -8306,21 +8394,21 @@ msgstr "Каталог для заміни не вибрано" msgid "Replaced with 3D files from directory:\n" msgstr "Замінено 3D-файлами з каталогу:\n" -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ Пропущено %1%: той самий файл.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ Пропущено %s: той самий файл.\n" -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ Пропущено %1%: файл не існує.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ Пропущено %s: файл не існує.\n" -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ Пропущено %1%: не вдалося замінити.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ Пропущено %s: не вдалося замінити.\n" -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔ Замінено %1%.\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔ Замінено %s.\n" msgid "Replaced volumes" msgstr "Замінені обʼєми" @@ -8397,6 +8485,22 @@ msgstr "" msgid "Sync now" msgstr "Синхронізувати зараз" +# AI Translated +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "Не вдалося імпортувати текстуру. Схоже, модель містить дані текстури, але завершити процес імпорту не вдалося. Модель буде імпортовано лише як геометрію." + +# AI Translated +msgid "Applying texture colors..." +msgstr "Застосування кольорів текстури..." + +# AI Translated +msgid "Updating 3D view..." +msgstr "Оновлення 3D-вигляду..." + +# AI Translated +msgid "Texture colors applied." +msgstr "Кольори текстури застосовано." + msgid "You can keep the modified presets for the new project or discard them" msgstr "Ви можете зберегти змінені пресети у новому проекті або відмовитися від них" @@ -9069,6 +9173,18 @@ msgstr "З цією опцією ввімкненою, ви можете від msgid "Pop up to select filament grouping mode" msgstr "Показувати вікно вибору режиму групування філаментів" +# AI Translated +msgid "Visible plugin pages" +msgstr "Видимі сторінки плагінів" + +# AI Translated +msgid "pages" +msgstr "стор." + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Кількість сторінок плагінів, що показуються як закріплені вкладки, перш ніж решта сторінок згорнеться у випадний список на останній вкладці." + msgid "Behaviour" msgstr "Поведінка" @@ -9446,6 +9562,18 @@ msgstr "Показати непідтримувані пресети" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Показати несумісні/непідтримувані пресети у випадаючому списку принтера і філаменту. Ці пресети не можна вибрати." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Експериментально) Використовувати агентів принтера замість хостів друку" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Спрямовує завдання друку для принтерів, відмінних від Bambu, через агентів плагінів принтера замість класичного завантаження на хост друку.\n" +"Коли вимкнено, OrcaSlicer використовує попередню поведінку хоста друку." + msgid "Experimental Features" msgstr "Експериментальні функції" @@ -9654,6 +9782,10 @@ msgstr "Спіральна ваза" msgid "First layer filament sequence" msgstr "Послідовність філаменту першого шару" +# AI Translated +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "Список філаментів містить змішані філаменти. Власна послідовність філаментів не діятиме." + msgid "By Layer" msgstr "По шарах" @@ -9710,10 +9842,26 @@ msgstr "Пресети користувача" msgid "Preset Inside Project" msgstr "Налаштування проекту всередині" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Копіює в цей пресет усі значення, успадковані від батьківського пресета, і видаляє звʼязок успадкування. Пресети, сумісні лише з батьківським, можуть стати непідтримуваними." + # AI Translated msgid "Detach from parent" msgstr "Відʼєднати від батьківського" +# AI Translated +msgid "Unique preset" +msgstr "Незалежний пресет" + +# AI Translated +msgid "Parent preset" +msgstr "Батьківський пресет" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Цей пресет не успадковується від іншого пресета." + msgid "Name is unavailable." msgstr "Назва недоступна." @@ -10492,22 +10640,6 @@ msgstr "Ви впевнені, що хочете ввімкнути цю опц msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Шаблони заповнення зазвичай розроблені так, щоб автоматично враховувати обертання, забезпечувати належний друк і досягати задуманого ефекту (наприклад, Гіроїд, Кубічний). Обертання поточного шаблону часткового заповнення може призвести до недостатньої підтримки. Дійте обережно та ретельно перевіряйте можливі проблеми друку. Ви впевнені, що хочете увімкнути цю опцію?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Висота шару занадто мала.\n" -"Буде встановлено значення min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Висота шару перевищує ліміт у Налаштуваннях принтера -> Екструдер -> Ліміти висоти шару, це може призвести до проблем з якістю друку." - -msgid "Adjust to the set range automatically?\n" -msgstr "Автоматично налаштувати на встановлений діапазон?\n" - -msgid "Adjust" -msgstr "Налаштувати" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Експериментальна функція: Втягування та відрізання філаменту на більшій відстані під час зміни філаменту для мінімізації промивання. Хоча це може помітно зменшити промивання, це також може підвищити ризик засмічення сопла або інших ускладнень друку." @@ -10711,6 +10843,9 @@ msgstr "Знайдено зарезервовані ключові слова" msgid "Setting Overrides" msgstr "Налаштування перевизначень" +msgid "Retraction when switching material" +msgstr "Втягування під час зміни матеріалу" + msgid "Basic information" msgstr "Базова інформація" @@ -10848,6 +10983,13 @@ msgstr "Сумісні профілі процесів" msgid "Printable space" msgstr "Місце для друку" +msgid "Printer Agent" +msgstr "Агент принтера" + +# AI Translated +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Виберіть реалізацію мережевого агента для звʼязку з принтером. Доступні агенти реєструються під час запуску." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10978,9 +11120,6 @@ msgstr "Обмеження висоти шару" msgid "Z-Hop" msgstr "Стрибок-Z" -msgid "Retraction when switching material" -msgstr "Втягування під час зміни матеріалу" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -11106,12 +11245,12 @@ msgstr "%s: %s" msgid "No modifications need to be copied." msgstr "Немає змін для копіювання." -msgid "Copy paramters" +msgid "Copy parameters" msgstr "Параметри копіювання" # AI Translated #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "Змінити параметри %s" # AI Translated @@ -11650,30 +11789,6 @@ msgstr "Обʼєми промивки для зміни філаменту" msgid "Please choose the filament colour" msgstr "Будь ласка, виберіть колір філаменту" -# AI Translated -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "Нативний перегляд у Wayland потребує відеоприймача GStreamer GTK. Встановіть плагін gtksink для GStreamer, а потім перезапустіть OrcaSlicer." - -# AI Translated -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "Не вдалося ініціалізувати нативний відеоприймач GStreamer для Wayland. Перевірте встановлення плагіна GStreamer GTK." - -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "Для виконання цього завдання потрібен Windows Media Player! Бажаєте увімкнути 'Windows Media Player' для вашої операційної системи?" - -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "BambuSource не було правильно зареєстровано для відтворення медіафайлів! Натисніть \"Так\", щоб зареєструвати його повторно. Вас буде сповіщено двічі" - -# AI Translated -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "Відсутній компонент BambuSource, зареєстрований для відтворення медіа! Перевстановіть OrcaSlicer або зверніться по допомогу до спільноти." - -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "Використовується BambuSource з іншої інсталяції, відтворення відео може працювати неправильно! Натисніть \"Так\", щоб виправити це." - -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "Вашій системі бракує кодеків H.264 для GStreamer, які необхідні для відтворення відео. (Спробуйте встановити пакети gstreamer1.0-plugins-bad або gstreamer1.0-libav, а потім перезапустіть Orca Slicer?)" - # AI Translated msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "Хмарний агент недоступний. Перезапустіть OrcaSlicer і спробуйте ще раз." @@ -12376,6 +12491,10 @@ msgstr " знаходиться надто близько до зони відч msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " розташовано занадто близько до зони виявлення налипання, і це спричинить зіткнення.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " частково знаходиться за межами області друку, і його неможливо надрукувати.\n" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Вибрані температури сопла несумісні. Температура сопла кожного філаменту має входити в рекомендований діапазон температур сопла інших філаментів. Інакше можливе засмічення сопла або пошкодження принтера." @@ -12391,6 +12510,10 @@ msgstr "Якщо ви все одно хочете друкувати, може msgid "No extrusions under current settings." msgstr "Немає екструзій під час поточних налаштувань." +# AI Translated +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "Використовується змішаний філамент із градієнтом, але параметр «Підшар змішаного кольору» вимкнено. Градієнт не буде надруковано." + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Плавний режим таймлапсу не підтримується, коли послідовність \"по обʼєкт\" увімкнено." @@ -12431,6 +12554,10 @@ msgstr "Можливо, ви захочете зменшити розмір мо msgid "Variable layer height is not supported with Organic supports." msgstr "Змінна висота шару не підтримується з Органічними підтримками." +# AI Translated +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "Філамент вежі протирання не може бути змішаним філаментом." + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Різні діаметри сопел та різні діаметри філаменту можуть працювати некоректно коли ввімкнена підготовча вежа. Це експериментальна функція, тому використовуйте її з обережністю." @@ -12722,9 +12849,6 @@ msgstr "Використовувати 3MF замість G-коду" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Увімкніть, якщо принтер приймає файл 3MF як завдання друку. Якщо увімкнено, Orca Slicer надсилає нарізаний файл як .gcode.3mf замість звичайного файлу .gcode." -msgid "Printer Agent" -msgstr "Агент принтера" - # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Виберіть реалізацію мережевого агента для звʼязку з принтером." @@ -12813,8 +12937,7 @@ msgstr "мм або %" msgid "Other layers" msgstr "Інші шари" -# AI Translated -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." msgstr "Температура столу для всіх шарів, крім першого. Значення 0 означає, що філамент не підтримує друк на Cool Plate SuperTack." msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." @@ -13438,9 +13561,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Швидкість внутрішніх мостів. Якщо значення вказано у відсотках, воно буде розраховане на основі bridge_speed. Значення за замовчуванням: 150%." -msgid "Brim width" -msgstr "Ширина кайми" - msgid "This is the distance from the model to the outermost brim line." msgstr "Відстань від моделі до останньої зовнішньої лінії кайми" @@ -13525,6 +13645,14 @@ msgstr "" "Геометрія буде оброблена перед детектуванням гострих кутів. Цей параметр вказує мінімальну довжину відхилення для обробки.\n" "0 для вимкнення" +# AI Translated +msgid "Brim ears outer only" +msgstr "Вушка кайми лише ззовні" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Створювати мишачі вушка лише на зовнішньому контурі моделі, за винятком отворів і замкнених ділянок." + msgid "upward compatible machine" msgstr "висхідна сумісна машина" @@ -14223,8 +14351,10 @@ msgstr "Час шару" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "Вентилятор охолодження деталей вмикається для шарів, розрахунковий час яких менший за це значення. Швидкість вентилятора інтерполюється між мінімальною та максимальною швидкістю вентилятора відповідно до часу друку шару" +# AI Translated +msgctxt "second" msgid "s" -msgstr "c" +msgstr "с" msgid "Default color" msgstr "Типовий колір" @@ -14554,6 +14684,62 @@ msgstr "Матеріал підтримки" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Допоміжний матеріал зазвичай використовується для друку підтримки" +# AI Translated +msgid "Is mixed filament" +msgstr "Є змішаним філаментом" + +# AI Translated +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "Визначає, чи є цей слот філаменту змішаним філаментом, складеним з кількох фізичних філаментів" + +# AI Translated +msgid "Mixed filament components" +msgstr "Компоненти змішаного філаменту" + +# AI Translated +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "Розділені комами індекси філаментів-компонентів, починаючи з 1, наприклад \"1,3\"" + +# AI Translated +msgid "Mixed filament sublayer ratios" +msgstr "Частки підшарів змішаного філаменту" + +# AI Translated +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "Розділені комами значення часток, сума яких дорівнює 1.0, наприклад \"0.7,0.3\"" + +# AI Translated +msgid "Mixed filament gradient" +msgstr "Градієнт змішаного філаменту" + +# AI Translated +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "Вмикає режим градієнта за віссю Z для підшарів змішаного філаменту. Коли увімкнено, частки підшарів змінюються лінійно від шару до шару." + +# AI Translated +msgid "Mixed filament gradient range" +msgstr "Діапазон градієнта змішаного філаменту" + +# AI Translated +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "Початкова та кінцева частки першого компонента в режимі градієнта. Пара значень через кому, наприклад \"0.10,0.90\" означає від 10% до 90%." + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "Крива градієнта змішаного філаменту" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "Необовʼязкова власна крива у стилі Photoshop, що зіставляє поступ за віссю Z із часткою першого компонента. Записується як контрольні точки, розділені вертикальними рисками, у вигляді \"x,y\" (застарілий формат) або \"x,y,m_in,m_out\", коли потрібно перевизначити дотичну (порожнє значення або \"nan\" означає типове значення PCHIP). x належить до [0,1]; y обмежується налаштованим діапазоном часток, наприклад \"0,0.15|0.5,0.50|1,0.85\". Якщо поле порожнє, замість цього використовується лінійний gradient_range." + +# AI Translated +msgid "Mixed filament per-part gradient" +msgstr "Градієнт змішаного філаменту для кожної частини" + +# AI Translated +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "Коли режим градієнта увімкнено, градієнт застосовується до кожної частини збірки окремо, а не до всієї збірки як до єдиного діапазону Z." + msgid "Filament printable" msgstr "Філамент придатний для друку" @@ -14734,6 +14920,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Гіроїд" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Коефіцієнт згладжування часткового заповнення" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Визначає, наскільки сильно заокруглюються кути часткового заповнення. 0% зберігає початкову траєкторію з гострими кутами, а 100% створює максимально можливі заокруглення між сусідніми лініями заповнення." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Прискорення заповнення верхньої поверхні. Використання меншого значенняможе покращити якість верхньої поверхні" @@ -15300,6 +15494,14 @@ msgstr "З яким gcode сумісний принтер" msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Пропустити блок конфігурації G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Не записувати CONFIG_BLOCK (пари ключ/значення з конфігурацією слайсера) у файл G-code. Це може допомогти з принтерами, прошивка яких аварійно завершується під час розбору цих рядків коментарів (напр. Anycubic go-klipper). Примітка: файл G-code більше не міститиме налаштувань слайсера, тож зворотний імпорт до OrcaSlicer не відновить конфігурацію." + msgid "Pellet Modded Printer" msgstr "Принтер модифікований гранулами" @@ -15884,8 +16086,9 @@ msgid "The allowed maximum output force of Y axis" msgstr "Максимальна дозволена вихідна сила осі Y" # AI Translated +msgctxt "Newton" msgid "N" -msgstr "N" +msgstr "Н" msgid "Bed mass of the Y axis" msgstr "Маса столу осі Y" @@ -15893,6 +16096,8 @@ msgstr "Маса столу осі Y" msgid "The machine bed mass load of Y axis" msgstr "Маса навантаження столу машини на вісь Y" +# AI Translated +msgctxt "gram" msgid "g" msgstr "г" @@ -16224,8 +16429,8 @@ msgid "Reduce infill retraction" msgstr "Зменшити втягування заповнення" # AI Translated -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." -msgstr "Не втягувати, коли переміщення повністю проходить у зоні заповнення. Це означає, що витікання не буде видно. Це може зменшити кількість втягувань для складних моделей і заощадити час друку, але сповільнює нарізку та генерацію G-коду. Зверніть увагу, що z-hop також не виконується в зонах, де втягування пропускається." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." +msgstr "Не втягувати, коли переміщення повністю проходить у зоні заповнення. Це означає, що витікання не буде видно. Це може зменшити кількість втягувань для складних моделей і заощадити час друку, але сповільнює нарізку та генерацію G-коду. Зверніть увагу, що Z-hop також не виконується в зонах, де втягування пропускається." msgid "This option will drop the temperature of the inactive extruders to prevent oozing." msgstr "Цей параметр знижує температуру неактивних екструдерів, щоб запобігти підтіканням." @@ -16400,7 +16605,7 @@ msgstr "Величина втягування після протирання" # AI Translated #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" "Довжина швидкого втягування після протирання, відносно довжини втягування.\n" @@ -16438,10 +16643,18 @@ msgstr "Довге втягування при зміні екструдера" msgid "Retraction distance when extruder change" msgstr "Відстань втягування при зміні екструдера" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Довжина втягування (Зміна інструменту)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Коли втягування спрацьовує перед зміною інструменту, філамент відтягується на вказану величину (довжина вимірюється на необробленому філаменті, до його входу в екструдер)." + msgid "Z-hop height" msgstr "Висота Z-підйому" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "Під час кожного втягування сопло трохи піднімається, щоб створити зазор між соплом та об’єктом друку. Це запобігає зіткненню сопла з об’єктом друку під час переміщення. Використання спіральної лінії підняття по осі Z може запобігти появі ниток" msgid "Z-hop lower boundary" @@ -16534,6 +16747,10 @@ msgstr "Додаткова довжина під час перезавантаж msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Коли втягування компенсується після переміщення, екструдер проштовхуєЦе додаткова кількість нитки. Ця установка рідко потрібна." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Додаткова довжина під час перезавантаження (Зміна інструменту)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Коли втягування компенсується після заміни інструменту, екструдерпроштовхує цю додаткову кількість нитки." @@ -16960,6 +17177,14 @@ msgstr "Зміна інструмента на вежі протирання" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Примусово переміщати головку до вежі протирання перед видачею команди зміни інструмента (Tx). Стосується лише багатоекструдерних (багатоінструментальних) принтерів з вежею протирання типу 2. Типово Orca пропускає це переміщення на багатоінструментальних машинах, оскільки заміну головки виконує прошивка, через що команда Tx може бути видана над надрукованою деталлю. Увімкніть цю опцію, якщо хочете, щоб зміна інструмента завжди відбувалася над вежею протирання." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Очікувати температуру на вежі протирання" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Бере новий інструмент, не чекаючи, доки він досягне температури друку, переміщується до вежі протирання й чекає на температуру там, безпосередньо перед промивкою. Матеріал, що витікає під час нагрівання, потрапляє на вежу, а не на модель, а переміщення збігається з нагріванням. Актуально лише для принтерів із кількома екструдерами (кількома головками), які використовують вежу протирання типу 2. Прошивка або макрос зміни інструменту не повинні самі чекати на температуру. Коли вимкнено, команда очікування температури видається одразу після команди зміни інструменту." + msgid "No sparse layers (beta)" msgstr "Без розріджених шарів (бета)" @@ -17513,6 +17738,14 @@ msgstr "" "\n" "Якщо встановити значення у параметрі \"Кількість втягування перед витиранням\" нижче, надлишкове втягування буде виконано перед витиранням, інакше воно буде виконано після нього." +# AI Translated +msgid "Mixed color sublayer" +msgstr "Підшар змішаного кольору" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "Вмикає розбиття на підшари змішаного кольору. Коли увімкнено, шари, що містять філаменти змішаного кольору, розбиваються на підшари для отримання ефекту змішування кольорів." + # AI Translated msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "Вежа протирання може використовуватися для очищення залишків на соплі та стабілізації тиску в камері всередині сопла, щоб уникнути дефектів зовнішнього вигляду під час друку обʼєктів." @@ -18619,6 +18852,10 @@ msgstr "Не вдалося побудувати сітку файлу моде msgid "The supplied file couldn't be read because it's empty." msgstr "Наданий файл не вдалося прочитати, оскільки він порожній" +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "Формат файлу несумісний, і його не вдається прочитати." + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Невідомий формат файлу: вхідний файл повинен мати розширення .stl, .obj або .amf (.xml)." @@ -20135,12 +20372,12 @@ msgstr "Показувати лише назви принтерів зі змі msgid "Only display the filament names with changes to filament presets." msgstr "Показувати лише назви філаментів зі змінами у пресетах філаменту." -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "Будуть відображатися лише назви принтерів з пресетами принтера користувача, і кожен обраний пресет буде експортовано у форматі ZIP." msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" "Будуть відображатися лише назви філаментів з налаштуваннями філаменту користувача, \n" "і всі налаштування філаменту користувача для кожної вибраної назви філаменту \n" @@ -20148,7 +20385,7 @@ msgstr "" msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" "Будуть відображатися лише назви принтерів зі зміненими налаштуваннями процесу, \n" "і всі налаштування процесу користувача для кожної вибраної назви принтера будуть \n" @@ -20304,10 +20541,6 @@ msgstr "Фізичний принтер" msgid "Print Host upload" msgstr "Завантаження хоста друку" -# AI Translated -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Виберіть реалізацію мережевого агента для звʼязку з принтером. Доступні агенти реєструються під час запуску." - msgid "Select a Flashforge printer" msgstr "Вибрати принтер Flashforge" @@ -20394,7 +20627,7 @@ msgid "We need information for diagnosing source of the issue. Check wiki page f msgstr "Нам потрібна інформація для встановлення джерела проблеми. Докладний посібник дивіться на сторінці wiki." # AI Translated -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "Кнопка «Пакувати» збирає файл проєкту та журнали поточного сеансу в один zip-файл." # AI Translated @@ -20438,7 +20671,7 @@ msgstr "Рівень журналу" msgid "Stored logs" msgstr "Збережені логи" -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "Запаковує всі збережені логи у файл zip." msgid "Profiles" @@ -20505,7 +20738,7 @@ msgstr "Тип принтера не знайдено, будь ласка ви msgid "Authorizing..." msgstr "Авторизування..." -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "Помилка. Не вдалося отримати api-токен для авторизування" msgid "Could not parse server response." @@ -20969,8 +21202,9 @@ msgstr "Вилучено" msgid "Enable smart filament assign: Assign one filament to multiple nozzles to maximize savings" msgstr "Дозволити розумне призначення філаменту: Призначте одну нитку до кількох сопел, щоб максимально заощадити" -msgid "Fila Saving" -msgstr "Збереження філаменту" +# AI Translated +msgid "File Saving" +msgstr "Збереження файлу" msgid "Don't remind me again" msgstr "Не нагадувати мені більше" @@ -21181,9 +21415,6 @@ msgstr "Під час спроби входу трапилося щось нес msgid "User canceled." msgstr "Користувача скасовано." -msgid "Head diameter" -msgstr "Діаметр голови" - msgid "Max angle" msgstr "Максимальний кут" @@ -21343,6 +21574,9 @@ msgstr "Перезапустити зараз" msgid "NO RAMMING AT ALL" msgstr "ВЗАГАЛІ БЕЗ УТРАМБУВАННЯ" +msgid "s" +msgstr "c" + msgid "Volumetric speed" msgstr "Обʼємна швидкість" @@ -21979,6 +22213,56 @@ msgstr "" "Уникнення деформації\n" "Чи знаєте ви, що при друку матеріалами, схильними до деформації, такими як ABS, відповідне підвищення температури столу може зменшити ймовірність деформації?" +# AI Translated +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "Нативний перегляд у Wayland потребує відеоприймача GStreamer GTK. Встановіть плагін gtksink для GStreamer, а потім перезапустіть OrcaSlicer." + +# AI Translated +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "Не вдалося ініціалізувати нативний відеоприймач GStreamer для Wayland. Перевірте встановлення плагіна GStreamer GTK." + +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "Для виконання цього завдання потрібен Windows Media Player! Бажаєте увімкнути 'Windows Media Player' для вашої операційної системи?" + +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "BambuSource не було правильно зареєстровано для відтворення медіафайлів! Натисніть \"Так\", щоб зареєструвати його повторно. Вас буде сповіщено двічі" + +# AI Translated +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "Відсутній компонент BambuSource, зареєстрований для відтворення медіа! Перевстановіть OrcaSlicer або зверніться по допомогу до спільноти." + +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "Використовується BambuSource з іншої інсталяції, відтворення відео може працювати неправильно! Натисніть \"Так\", щоб виправити це." + +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "Вашій системі бракує кодеків H.264 для GStreamer, які необхідні для відтворення відео. (Спробуйте встановити пакети gstreamer1.0-plugins-bad або gstreamer1.0-libav, а потім перезапустіть Orca Slicer?)" + +# AI Translated +#~ msgid "N" +#~ msgstr "N" + +#~ msgid "g" +#~ msgstr "г" + +#~ msgid "Fila Saving" +#~ msgstr "Збереження філаменту" + +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Висота шару занадто мала.\n" +#~ "Буде встановлено значення min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Висота шару перевищує ліміт у Налаштуваннях принтера -> Екструдер -> Ліміти висоти шару, це може призвести до проблем з якістю друку." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Автоматично налаштувати на встановлений діапазон?\n" + +#~ msgid "Head diameter" +#~ msgstr "Діаметр голови" + #~ msgid "Print order within a single layer." #~ msgstr "Друк замовлення в один шар" diff --git a/localization/i18n/vi/OrcaSlicer_vi.po b/localization/i18n/vi/OrcaSlicer_vi.po index 2133579d50..8ec3447568 100644 --- a/localization/i18n/vi/OrcaSlicer_vi.po +++ b/localization/i18n/vi/OrcaSlicer_vi.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "PO-Revision-Date: 2025-10-02 17:43+0700\n" "Last-Translator: \n" "Language-Team: hainguyen.ts13@gmail.com\n" @@ -3077,6 +3077,10 @@ msgstr "Chỉnh sửa" msgid "Merge with" msgstr "Gộp với" +# AI Translated +msgid "Decompose Color" +msgstr "Phân tách màu" + # AI Translated msgid "Delete this filament" msgstr "Xóa filament này" @@ -3389,6 +3393,10 @@ msgstr "Lắp ráp" msgid "Merge parts to an object" msgstr "Gộp các phần thành một vật thể" +# AI Translated +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "Sử dụng chiều cao lớp biến đổi cùng với lớp con màu pha trộn có thể làm giảm chất lượng pha màu." + # AI Translated msgid "Add layers" msgstr "Thêm lớp" @@ -4975,6 +4983,23 @@ msgstr "Nhiệt độ buồng hiện tại cao hơn nhiệt độ an toàn của msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Nhiệt độ buồng tối thiểu (%d℃) cao hơn nhiệt độ buồng mục tiêu (%d℃). Giá trị tối thiểu là ngưỡng để bắt đầu in trong khi buồng vẫn tiếp tục gia nhiệt tới mục tiêu, nên nó không được vượt quá giá trị mục tiêu. Nó sẽ được giới hạn về mức mục tiêu." +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "Chiều cao lớp quá nhỏ. Nó sẽ được đặt về giá trị tối thiểu (%g mm)." + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "Chiều cao lớp nằm ngoài giới hạn được đặt trong Cài đặt máy in -> Extruder -> Giới hạn chiều cao lớp, điều này có thể gây ra vấn đề chất lượng in." + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "Tự động điều chỉnh về giới hạn (%g mm)?" + +msgid "Adjust" +msgstr "Điều chỉnh" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -5023,7 +5048,7 @@ msgstr "" "\n" "Giá trị sẽ được đặt lại về 0." -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "Luân phiên wall phụ không hoạt động tốt khi đảm bảo độ dày shell dọc được đặt thành Tất cả." msgid "" @@ -5069,7 +5094,7 @@ msgstr "" "NO - Giữ chiều cao lớp support độc lập" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height cần nhỏ hơn layer_height.\n" @@ -5077,7 +5102,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "Độ sâu khóa phải nhỏ hơn độ sâu skin.\n" @@ -5095,6 +5120,13 @@ msgstr "" "Yes - Bật trình tạo wall Arachne\n" "No - Tắt trình tạo wall Arachne và đặt chế độ [Displacement] của Fuzzy Skin" +# AI Translated +msgid "Brim ear radius" +msgstr "Bán kính tai brim" + +msgid "Brim width" +msgstr "Độ rộng brim" + # AI Translated msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Chế độ xoắn ốc chỉ hoạt động khi vòng wall bằng 1, support bị tắt, phát hiện vón cục bằng dò bị tắt, số lớp vỏ trên bằng 0, mật độ infill thưa bằng 0 và loại timelapse là truyền thống." @@ -5399,6 +5431,14 @@ msgstr "Không thể tạo G-code hiệu chỉnh" msgid "Calibration error" msgstr "Lỗi hiệu chỉnh" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "Máy in này không được cấu hình phần cứng mà điều khiển này cần." + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "Điều khiển này không được hỗ trợ trên máy in này." + # AI Translated msgid "Network unavailable" msgstr "Mạng không khả dụng" @@ -6317,7 +6357,7 @@ msgstr "Thể tích:" msgid "Size:" msgstr "Kích thước:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Đã tìm thấy xung đột đường đi G-code tại lớp %d, Z = %.2lfmm. Vui lòng tách các vật thể xung đột ra xa hơn (%s <-> %s)." @@ -6516,6 +6556,10 @@ msgstr "Nhiều thiết bị" msgid "Project" msgstr "Dự án" +# AI Translated +msgid "Device (Web)" +msgstr "Thiết bị (Web)" + msgid "Yes" msgstr "Có" @@ -6651,10 +6695,10 @@ msgstr "Nhập 3MF/STL/STEP/SVG/OBJ/AMF" msgid "Load a model" msgstr "Tải model" -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "Nhập lưu trữ Zip" -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "Tải các model chứa trong lưu trữ zip" msgid "Import Configs" @@ -8252,6 +8296,10 @@ msgstr "Tùy chỉnh bản hiện tại" msgid "The %s nozzle can not print %s." msgstr "Đầu phun %s không thể in %s." +# AI Translated +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "Việc in filament màu pha trộn trên máy in một extruder đòi hỏi thay filament và xả thường xuyên, điều này có thể làm tăng đáng kể lượng phế liệu và nguy cơ tắc đầu phun / máng chứa phế liệu." + # AI Translated #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" @@ -8394,12 +8442,44 @@ msgstr "Đồng bộ danh sách filament từ AMS" msgid "Set filaments to use" msgstr "Đặt filament để sử dụng" +# AI Translated +msgid "Add Mixed Filament" +msgstr "Thêm filament pha trộn" + +# AI Translated +msgid "Mixed Filament" +msgstr "Filament pha trộn" + +# AI Translated +msgid "Remove last mixed filament" +msgstr "Xóa filament pha trộn cuối cùng" + +# AI Translated +msgid "Add mixed filament" +msgstr "Thêm filament pha trộn" + +# AI Translated +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "Filament pha trộn có thành phần không hợp lệ hoặc không khớp. Vui lòng chỉnh sửa lại các mục bị ảnh hưởng." + msgid "Search plate, object and part." msgstr "Tìm kiếm bản, đối tượng và phần." msgid "Pellets" msgstr "Viên" +# AI Translated +msgid "Mixed filament has broken component references" +msgstr "Filament pha trộn có tham chiếu thành phần bị lỗi" + +# AI Translated +msgid "Edit / Delete / Merge" +msgstr "Chỉnh sửa / Xóa / Gộp" + +# AI Translated +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "Filament pha trộn đích sử dụng filament vật lý này làm thành phần. Việc gộp sẽ xóa filament vật lý này và có thể làm filament pha trộn không còn hợp lệ. Tiếp tục?" + # AI Translated #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." @@ -8549,8 +8629,8 @@ msgstr "Vui lòng xác nhận G-code trong các preset này an toàn để ngăn msgid "Customized Preset" msgstr "Preset tùy chỉnh" -msgid "Component name(s) inside step file not in UTF8 format!" -msgstr "Tên của các thành phần bên trong file STEP không phải định dạng UTF8!" +msgid "Component name(s) inside step file not in UTF-8 format!" +msgstr "Tên của các thành phần bên trong file STEP không phải định dạng UTF-8!" # AI Translated msgid "Because of unsupported text encoding, garbage characters may appear!" @@ -8572,10 +8652,10 @@ msgstr "Thể tích của đối tượng bằng không" #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "Đối tượng từ file %s quá nhỏ, và có thể đang ở đơn vị mét hoặc inch.\n" -" Bạn có muốn chuyển đổi sang milimét?" +"Bạn có muốn chuyển đổi sang milimét?" msgid "Object too small" msgstr "Đối tượng quá nhỏ" @@ -8592,6 +8672,14 @@ msgstr "" msgid "Multi-part object detected" msgstr "Phát hiện đối tượng nhiều phần" +# AI Translated +msgid "Matching textures to filaments" +msgstr "Đang khớp kết cấu với filament" + +# AI Translated +msgid "Texture Import Warning" +msgstr "Cảnh báo nhập kết cấu" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Tải các file này như một đối tượng đơn với nhiều phần?\n" @@ -8721,24 +8809,24 @@ msgid "Replaced with 3D files from directory:\n" msgstr "Đã thay thế bằng file 3D từ thư mục:\n" # AI Translated -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ Đã bỏ qua %1%: cùng một file.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ Đã bỏ qua %s: cùng một file.\n" # AI Translated -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ Đã bỏ qua %1%: file không tồn tại.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ Đã bỏ qua %s: file không tồn tại.\n" # AI Translated -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ Đã bỏ qua %1%: thay thế thất bại.\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ Đã bỏ qua %s: thay thế thất bại.\n" # AI Translated -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔ Đã thay thế %1%.\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔ Đã thay thế %s.\n" # AI Translated msgid "Replaced volumes" @@ -8821,6 +8909,22 @@ msgstr "" msgid "Sync now" msgstr "Đồng bộ ngay" +# AI Translated +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "Nhập kết cấu thất bại. Mô hình có vẻ chứa dữ liệu kết cấu, nhưng không thể hoàn tất quá trình nhập kết cấu. Mô hình sẽ chỉ được nhập dưới dạng hình học." + +# AI Translated +msgid "Applying texture colors..." +msgstr "Đang áp dụng màu kết cấu..." + +# AI Translated +msgid "Updating 3D view..." +msgstr "Đang cập nhật khung nhìn 3D..." + +# AI Translated +msgid "Texture colors applied." +msgstr "Đã áp dụng màu kết cấu." + msgid "You can keep the modified presets for the new project or discard them" msgstr "Bạn có thể giữ các preset đã chỉnh sửa cho dự án mới hoặc loại bỏ chúng" @@ -9532,6 +9636,18 @@ msgstr "Với tùy chọn này được bật, bạn có thể gửi tác vụ msgid "Pop up to select filament grouping mode" msgstr "Hiện cửa sổ để chọn chế độ nhóm filament" +# AI Translated +msgid "Visible plugin pages" +msgstr "Số trang plugin hiển thị" + +# AI Translated +msgid "pages" +msgstr "trang" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "Số trang plugin được hiển thị dưới dạng tab cố định trước khi các trang còn lại được gom vào danh sách thả xuống ở tab cuối cùng." + # AI Translated msgid "Behaviour" msgstr "Hành vi" @@ -9947,6 +10063,18 @@ msgstr "Hiện cài đặt sẵn không được hỗ trợ" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Hiện các cài đặt sẵn không tương thích/không được hỗ trợ trong danh sách thả xuống máy in và filament. Không thể chọn các cài đặt sẵn này." +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(Thử nghiệm) Dùng tác nhân máy in thay cho máy chủ in" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"Định tuyến các tác vụ in của máy in không phải Bambu qua các tác nhân plugin máy in thay vì luồng tải lên máy chủ in cổ điển.\n" +"Khi tắt, OrcaSlicer sẽ dùng hành vi máy chủ in cũ." + # AI Translated msgid "Experimental Features" msgstr "Tính năng thử nghiệm" @@ -10167,6 +10295,10 @@ msgstr "Bình xoắn ốc" msgid "First layer filament sequence" msgstr "Trình tự filament lớp đầu tiên" +# AI Translated +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "Danh sách filament có chứa filament pha trộn. Trình tự filament tùy chỉnh sẽ không có hiệu lực." + msgid "By Layer" msgstr "Theo lớp" @@ -10223,10 +10355,26 @@ msgstr "Preset người dùng" msgid "Preset Inside Project" msgstr "Preset bên trong dự án" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "Sao chép tất cả các giá trị kế thừa từ preset cha vào preset này và gỡ bỏ quan hệ kế thừa. Các preset chỉ tương thích với preset cha có thể không còn được hỗ trợ." + # AI Translated msgid "Detach from parent" msgstr "Tách khỏi vật thể cha" +# AI Translated +msgid "Unique preset" +msgstr "Preset độc lập" + +# AI Translated +msgid "Parent preset" +msgstr "Preset cha" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "Preset này không kế thừa từ preset khác." + msgid "Name is unavailable." msgstr "Tên không khả dụng." @@ -11026,22 +11174,6 @@ msgstr "Bạn có chắc chắn muốn bật tùy chọn này?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Mẫu infill thường được thiết kế để xử lý xoay tự động nhằm đảm bảo in đúng cách và đạt được hiệu quả dự kiến (ví dụ: Gyroid, Cubic). Xoay mẫu infill thưa hiện tại có thể dẫn đến support không đủ . Vui lòng tiến hành thận trọng và kiểm tra kỹ bất kỳ vấn đề in tiềm ẩn nào. Bạn có chắc chắn muốn bật tùy chọn này?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Chiều cao lớp quá nhỏ.\n" -"Nó sẽ được đặt thành min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Chiều cao lớp vượt quá giới hạn trong Cài đặt máy in -> Extruder -> Giới hạn chiều cao lớp, điều này có thể gây ra vấn đề chất lượng in." - -msgid "Adjust to the set range automatically?\n" -msgstr "Điều chỉnh về phạm vi đặt tự động?\n" - -msgid "Adjust" -msgstr "Điều chỉnh" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Tính năng thử nghiệm: Rút và cắt filament ở khoảng cách lớn hơn trong quá trình thay filament để giảm thiểu xả. Mặc dù có thể giảm đáng kể lượng xả, nó cũng có thể làm tăng nguy cơ tắc đầu phun hoặc các vấn đề in khác." @@ -11235,6 +11367,9 @@ msgstr "Tìm thấy từ khóa dành riêng" msgid "Setting Overrides" msgstr "Ghi đè cài đặt" +msgid "Retraction when switching material" +msgstr "Rút khi chuyển vật liệu" + msgid "Basic information" msgstr "Thông tin cơ bản" @@ -11366,6 +11501,14 @@ msgstr "Hồ sơ quy trình tương thích" msgid "Printable space" msgstr "Không gian in" +# AI Translated +msgid "Printer Agent" +msgstr "Tác nhân máy in" + +# AI Translated +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Chọn cách triển khai tác nhân mạng cho việc giao tiếp với máy in. Các tác nhân khả dụng được đăng ký khi khởi động." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -11498,9 +11641,6 @@ msgstr "Giới hạn chiều cao lớp" msgid "Z-Hop" msgstr "Z-Hop" -msgid "Retraction when switching material" -msgstr "Rút khi chuyển vật liệu" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -11624,12 +11764,12 @@ msgid "No modifications need to be copied." msgstr "Không có thay đổi nào cần sao chép." # AI Translated -msgid "Copy paramters" +msgid "Copy parameters" msgstr "Sao chép tham số" # AI Translated #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "Sửa tham số của %s" # AI Translated @@ -12198,29 +12338,6 @@ msgstr "Khối lượng xả khi thay filament" msgid "Please choose the filament colour" msgstr "Vui lòng chọn màu filament" -# AI Translated -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "Xem trực tiếp trên Wayland thuần cần GStreamer GTK video sink. Vui lòng cài đặt plugin gtksink cho GStreamer, sau đó khởi động lại OrcaSlicer." - -# AI Translated -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "Khởi tạo GStreamer video sink trên Wayland thuần thất bại. Vui lòng kiểm tra việc cài đặt plugin GStreamer GTK của bạn." - -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "Windows Media Player là cần thiết cho tác vụ này! Bạn có muốn bật 'Windows Media Player' cho hệ điều hành của bạn?" - -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "BambuSource chưa được đăng ký chính xác để phát media! Nhấn Có để đăng ký lại. Bạn sẽ được nhắc hai lần" - -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "Thiếu thành phần BambuSource đã đăng ký để phát media! Vui lòng cài đặt lại OrcaSlicer hoặc tìm kiếm sự giúp đỡ từ cộng đồng." - -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "Đang sử dụng BambuSource từ cài đặt khác, phát video có thể không hoạt động đúng! Nhấn Có để sửa nó." - -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "Hệ thống của bạn thiếu codec H.264 cho GStreamer, cần thiết để phát video. (Hãy thử cài đặt gói gstreamer1.0-plugins-bad hoặc gstreamer1.0-libav, sau đó khởi động lại Orca Slicer?)" - # AI Translated msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "Tác nhân cloud không khả dụng. Vui lòng khởi động lại OrcaSlicer rồi thử lại." @@ -12950,6 +13067,10 @@ msgstr " quá gần vùng loại trừ, và sẽ gây va chạm.\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " ở quá gần vùng phát hiện vón cục, và sẽ gây ra va chạm.\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr " nằm một phần ngoài vùng in được, và không thể in.\n" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Nhiệt độ đầu phun đã chọn không tương thích. Nhiệt độ đầu phun của mỗi filament phải nằm trong dải nhiệt độ đầu phun được khuyến nghị của các filament còn lại. Nếu không, có thể xảy ra tắc đầu phun hoặc hư hỏng máy in." @@ -12965,6 +13086,10 @@ msgstr "Nếu bạn vẫn muốn in, bạn có thể bật tùy chọn trong Tù msgid "No extrusions under current settings." msgstr "Không có đùn dưới cài đặt hiện tại." +# AI Translated +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "Đang dùng filament pha trộn có gradient, nhưng 'Lớp con màu pha trộn' đã bị tắt. Gradient sẽ không được in." + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Chế độ mượt của timelapse không được hỗ trợ khi trình tự \"theo đối tượng\" được bật." @@ -13003,6 +13128,10 @@ msgstr "Bạn có thể muốn giảm kích thước model của mình hoặc th msgid "Variable layer height is not supported with Organic supports." msgstr "Chiều cao lớp thay đổi không được hỗ trợ với support hữu cơ." +# AI Translated +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "Filament của wipe tower không thể là filament pha trộn." + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Đường kính đầu phun khác nhau và đường kính filament khác nhau có thể không hoạt động tốt khi prime tower được bật. Nó rất thử nghiệm, vì vậy vui lòng tiến hành thận trọng." @@ -13291,10 +13420,6 @@ msgstr "Dùng 3MF thay cho G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Bật tùy chọn này nếu máy in nhận file 3MF làm tác vụ in. Khi bật, Orca Slicer sẽ gửi file đã slice dưới dạng .gcode.3mf thay vì file .gcode thuần." -# AI Translated -msgid "Printer Agent" -msgstr "Tác nhân máy in" - # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Chọn cách triển khai tác nhân mạng cho việc giao tiếp với máy in." @@ -13381,7 +13506,7 @@ msgstr "mm hoặc %" msgid "Other layers" msgstr "Lớp khác" -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." msgstr "Nhiệt độ bàn cho các lớp ngoại trừ lớp đầu tiên. Giá trị 0 có nghĩa là filament không hỗ trợ in trên bản Cool SuperTack." msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." @@ -14002,9 +14127,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Tốc độ của cầu bên trong. Nếu giá trị được biểu thị dưới dạng phần trăm, nó sẽ được tính dựa trên bridge_speed. Giá trị mặc định là 150%." -msgid "Brim width" -msgstr "Độ rộng brim" - msgid "This is the distance from the model to the outermost brim line." msgstr "Khoảng cách từ model đến đường brim ngoài cùng." @@ -14088,6 +14210,14 @@ msgstr "" "Hình học sẽ được giảm trước khi phát hiện góc sắc. Tham số này chỉ ra độ dài tối thiểu của độ lệch cho việc giảm.\n" "0 để vô hiệu hóa." +# AI Translated +msgid "Brim ears outer only" +msgstr "Tai brim chỉ ở mặt ngoài" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "Chỉ tạo tai chuột trên đường viền ngoài của mô hình, không tính các lỗ và phần khép kín." + msgid "upward compatible machine" msgstr "máy tương thích ngược" @@ -14784,6 +14914,8 @@ msgstr "Thời gian lớp" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "Quạt làm mát phần sẽ được bật cho các lớp có thời gian ước tính ngắn hơn giá trị này. Tốc độ quạt được nội suy giữa tốc độ quạt tối thiểu và tối đa theo thời gian in lớp." +# AI Translated +msgctxt "second" msgid "s" msgstr "s" @@ -15123,6 +15255,62 @@ msgstr "Vật liệu support" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Vật liệu support thường được sử dụng để in support và giao diện support." +# AI Translated +msgid "Is mixed filament" +msgstr "Là filament pha trộn" + +# AI Translated +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "Xác định xem khe filament này có phải là filament pha trộn gồm nhiều filament vật lý hay không" + +# AI Translated +msgid "Mixed filament components" +msgstr "Thành phần của filament pha trộn" + +# AI Translated +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "Chỉ số của các filament thành phần, bắt đầu từ 1, phân tách bằng dấu phẩy, ví dụ \"1,3\"" + +# AI Translated +msgid "Mixed filament sublayer ratios" +msgstr "Tỷ lệ lớp con của filament pha trộn" + +# AI Translated +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "Các giá trị tỷ lệ phân tách bằng dấu phẩy, có tổng bằng 1.0, ví dụ \"0.7,0.3\"" + +# AI Translated +msgid "Mixed filament gradient" +msgstr "Gradient của filament pha trộn" + +# AI Translated +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "Bật chế độ gradient theo phương Z cho các lớp con của filament pha trộn. Khi bật, tỷ lệ các lớp con thay đổi tuyến tính qua các lớp." + +# AI Translated +msgid "Mixed filament gradient range" +msgstr "Phạm vi gradient của filament pha trộn" + +# AI Translated +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "Tỷ lệ đầu và cuối của thành phần thứ nhất ở chế độ gradient. Cặp giá trị phân tách bằng dấu phẩy, ví dụ \"0.10,0.90\" nghĩa là từ 10% đến 90%." + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "Đường cong gradient của filament pha trộn" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "Đường cong tùy chỉnh kiểu Photoshop (tùy chọn) ánh xạ tiến trình theo Z sang tỷ lệ của thành phần thứ nhất. Được mã hóa dưới dạng các điểm điều khiển phân tách bằng dấu gạch đứng, theo dạng \"x,y\" (kiểu cũ) hoặc \"x,y,m_in,m_out\" khi cần ghi đè tiếp tuyến (giá trị trống hoặc \"nan\" nghĩa là dùng mặc định PCHIP). x nằm trong [0,1]; y bị giới hạn trong phạm vi tỷ lệ đã cấu hình, ví dụ \"0,0.15|0.5,0.50|1,0.85\". Khi để trống, gradient_range tuyến tính sẽ được dùng thay thế." + +# AI Translated +msgid "Mixed filament per-part gradient" +msgstr "Gradient theo từng phần của filament pha trộn" + +# AI Translated +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "Khi chế độ gradient được bật, áp dụng gradient cho từng phần của một cụm lắp ráp một cách độc lập thay vì coi toàn bộ cụm lắp ráp là một phạm vi Z duy nhất." + # AI Translated msgid "Filament printable" msgstr "Filament in được" @@ -15305,6 +15493,14 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroid" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "Hệ số làm mượt infill thưa" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "Điều chỉnh mức độ bo tròn các góc của infill thưa. 0% giữ nguyên đường đi sắc cạnh ban đầu, còn 100% tạo ra các đường cong lớn nhất có thể giữa các đường infill liền kề." + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Gia tốc của infill bề mặt trên. Sử dụng giá trị thấp hơn có thể cải thiện chất lượng bề mặt trên." @@ -15868,6 +16064,14 @@ msgstr "Loại G-code mà máy in tương thích." msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "Bỏ qua khối cấu hình G-code" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "Không ghi CONFIG_BLOCK (các cặp khóa/giá trị cấu hình của phần mềm slice) vào tệp G-code. Điều này có thể hữu ích với các máy in có firmware bị treo khi phân tích những dòng chú thích này (ví dụ Anycubic go-klipper). Lưu ý: tệp G-code sẽ không còn chứa các thiết lập slice, nên việc nhập lại tệp vào OrcaSlicer sẽ không khôi phục được cấu hình." + msgid "Pellet Modded Printer" msgstr "Máy in Pellet đã chỉnh sửa" @@ -16419,6 +16623,7 @@ msgid "The allowed maximum output force of Y axis" msgstr "Lực đầu ra tối đa cho phép của trục Y" # AI Translated +msgctxt "Newton" msgid "N" msgstr "N" @@ -16431,6 +16636,7 @@ msgid "The machine bed mass load of Y axis" msgstr "Tải khối lượng bàn máy của trục Y" # AI Translated +msgctxt "gram" msgid "g" msgstr "g" @@ -16757,8 +16963,8 @@ msgid "Reduce infill retraction" msgstr "Giảm rút infill" # AI Translated -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." -msgstr "Không rút khi di chuyển ở vùng infill hoàn toàn. Điều đó có nghĩa là chảy nhựa không thể nhìn thấy. Điều này có thể giảm số lần rút cho model phức tạp và tiết kiệm thời gian in, nhưng làm cho slice và tạo G-code chậm hơn. Lưu ý rằng z-hop cũng không được thực hiện ở những vùng bỏ qua việc rút." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." +msgstr "Không rút khi di chuyển ở vùng infill hoàn toàn. Điều đó có nghĩa là chảy nhựa không thể nhìn thấy. Điều này có thể giảm số lần rút cho model phức tạp và tiết kiệm thời gian in, nhưng làm cho slice và tạo G-code chậm hơn. Lưu ý rằng Z-hop cũng không được thực hiện ở những vùng bỏ qua việc rút." msgid "This option will drop the temperature of the inactive extruders to prevent oozing." msgstr "Tùy chọn này sẽ giảm nhiệt độ của các extruder không hoạt động để ngăn chảy nhựa." @@ -16933,7 +17139,7 @@ msgstr "Lượng rút sau khi lau" # AI Translated #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" "Độ dài rút nhanh sau khi lau, tương đối so với độ dài rút.\n" @@ -16971,10 +17177,18 @@ msgstr "Rút dài khi đổi extruder" msgid "Retraction distance when extruder change" msgstr "Khoảng cách rút khi đổi extruder" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "Độ dài rút (Đổi công cụ)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "Khi rút được kích hoạt trước khi đổi công cụ, filament sẽ bị kéo lùi lại theo lượng đã chỉ định (độ dài được đo trên filament thô, trước khi nó đi vào extruder)." + msgid "Z-hop height" msgstr "Chiều cao Z-hop" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "Bất cứ khi nào rút được thực hiện, đầu phun được nâng lên một chút để tạo khoảng trống giữa đầu phun và bản in. Nó ngăn đầu phun va vào bản in khi di chuyển. Sử dụng đường xoắn ốc để nâng Z có thể ngăn dây kéo." msgid "Z-hop lower boundary" @@ -17069,6 +17283,10 @@ msgstr "Độ dài bổ sung khi khởi động lại" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Khi rút được bù sau khi di chuyển, extruder sẽ đẩy lượng filament bổ sung này. Cài đặt này hiếm khi cần thiết." +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "Độ dài bổ sung khi khởi động lại (Đổi công cụ)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Khi rút được bù sau khi thay công cụ, extruder sẽ đẩy lượng filament bổ sung này." @@ -17489,6 +17707,14 @@ msgstr "Đổi công cụ trên wipe tower" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Buộc đầu công cụ di chuyển đến wipe tower trước khi phát lệnh đổi công cụ (Tx). Chỉ liên quan đến máy in nhiều extruder (nhiều đầu công cụ) dùng wipe tower Loại 2. Theo mặc định, Orca bỏ qua bước di chuyển này trên máy nhiều đầu công cụ vì firmware tự xử lý việc đổi đầu, điều này có thể khiến lệnh Tx được phát ra ngay phía trên phần đang in. Hãy bật tùy chọn này nếu bạn muốn việc đổi công cụ luôn diễn ra phía trên wipe tower." +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "Chờ nhiệt độ tại wipe tower" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "Lấy công cụ mới mà không chờ nó đạt nhiệt độ in, di chuyển đến wipe tower và chờ nhiệt độ tại đó, ngay trước khi xả. Nhựa chảy ra trong lúc gia nhiệt sẽ rơi lên wipe tower thay vì lên mô hình, và quãng di chuyển diễn ra đồng thời với quá trình gia nhiệt. Chỉ áp dụng cho máy in nhiều extruder (nhiều đầu công cụ) dùng wipe tower loại 2. Firmware hoặc macro đổi công cụ không được tự chờ nhiệt độ. Khi tắt, lệnh chờ nhiệt độ sẽ được phát ngay sau lệnh đổi công cụ." + msgid "No sparse layers (beta)" msgstr "Không có lớp thưa (beta)" @@ -18031,6 +18257,14 @@ msgstr "" "\n" "Đặt giá trị trong cài đặt lượng rút trước khi lau bên dưới sẽ thực hiện bất kỳ rút dư nào trước khi lau, nếu không nó sẽ được thực hiện sau." +# AI Translated +msgid "Mixed color sublayer" +msgstr "Lớp con màu pha trộn" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "Bật việc chia thành lớp con màu pha trộn. Khi bật, các lớp có chứa filament màu pha trộn sẽ được chia thành các lớp con để tạo hiệu ứng pha màu." + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "Wipe tower có thể được sử dụng để làm sạch cặn trên đầu phun và ổn định áp suất buồng bên trong đầu phun, để tránh khuyết điểm bề ngoài khi in đối tượng." @@ -19151,6 +19385,10 @@ msgstr "Tạo lưới cho file mô hình thất bại hoặc không có hình d msgid "The supplied file couldn't be read because it's empty." msgstr "File được cung cấp không thể đọc được vì nó trống" +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "Định dạng tệp không tương thích nên không thể phân tích." + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Định dạng file không xác định. File đầu vào phải có phần mở rộng .stl, .obj, .amf(.xml)." @@ -20684,19 +20922,19 @@ msgstr "Chỉ hiển thị tên máy in có thay đổi đối với cài đặt msgid "Only display the filament names with changes to filament presets." msgstr "Chỉ hiển thị tên filament có thay đổi đối với cài đặt sẵn filament." -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "Chỉ tên máy in có cài đặt sẵn máy in người dùng sẽ được hiển thị, và mỗi cài đặt sẵn bạn chọn sẽ được xuất dưới dạng zip." msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" "Chỉ tên filament có cài đặt sẵn filament người dùng sẽ được hiển thị, \n" "và tất cả cài đặt sẵn filament người dùng trong mỗi tên filament bạn chọn sẽ được xuất dưới dạng zip." msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" "Chỉ tên máy in có cài đặt sẵn quy trình đã thay đổi sẽ được hiển thị, \n" "và tất cả cài đặt sẵn quy trình người dùng trong mỗi tên máy in bạn chọn sẽ được xuất dưới dạng zip." @@ -20849,10 +21087,6 @@ msgstr "Máy in vật lý" msgid "Print Host upload" msgstr "Tải lên máy chủ in" -# AI Translated -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Chọn cách triển khai tác nhân mạng cho việc giao tiếp với máy in. Các tác nhân khả dụng được đăng ký khi khởi động." - # AI Translated msgid "Select a Flashforge printer" msgstr "Chọn một máy in Flashforge" @@ -20952,7 +21186,7 @@ msgid "We need information for diagnosing source of the issue. Check wiki page f msgstr "Chúng tôi cần thông tin để chẩn đoán nguồn gốc vấn đề. Hãy xem trang wiki để có hướng dẫn chi tiết." # AI Translated -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "Nút Đóng gói thu thập file dự án và nhật ký của phiên hiện tại vào một file zip." # AI Translated @@ -21008,7 +21242,7 @@ msgid "Stored logs" msgstr "Nhật ký đã lưu" # AI Translated -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "Đóng gói toàn bộ nhật ký đã lưu vào một file zip." # AI Translated @@ -21096,7 +21330,7 @@ msgid "Authorizing..." msgstr "Đang xác thực..." # AI Translated -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "Lỗi. Không lấy được token api để xác thực" # AI Translated @@ -21576,8 +21810,8 @@ msgid "Enable smart filament assign: Assign one filament to multiple nozzles to msgstr "Bật gán filament thông minh: Gán một filament cho nhiều đầu phun để tối đa hóa tiết kiệm" # AI Translated -msgid "Fila Saving" -msgstr "Tiết kiệm fila" +msgid "File Saving" +msgstr "Lưu tệp" # AI Translated msgid "Don't remind me again" @@ -21832,9 +22066,6 @@ msgstr "Đã xảy ra điều gì đó không mong đợi khi cố gắng đăng msgid "User canceled." msgstr "Người dùng đã hủy." -msgid "Head diameter" -msgstr "Đường kính đầu" - msgid "Max angle" msgstr "Góc tối đa" @@ -22036,6 +22267,9 @@ msgstr "Khởi động lại ngay" msgid "NO RAMMING AT ALL" msgstr "HOÀN TOÀN KHÔNG RAMMING" +msgid "s" +msgstr "s" + # AI Translated msgid "Volumetric speed" msgstr "Tốc độ thể tích" @@ -22702,6 +22936,57 @@ msgstr "" "Tránh cong vênh\n" "Bạn có biết rằng khi in vật liệu dễ cong vênh như ABS, tăng nhiệt độ bàn nóng một cách thích hợp có thể giảm xác suất cong vênh không?" +# AI Translated +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "Xem trực tiếp trên Wayland thuần cần GStreamer GTK video sink. Vui lòng cài đặt plugin gtksink cho GStreamer, sau đó khởi động lại OrcaSlicer." + +# AI Translated +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "Khởi tạo GStreamer video sink trên Wayland thuần thất bại. Vui lòng kiểm tra việc cài đặt plugin GStreamer GTK của bạn." + +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "Windows Media Player là cần thiết cho tác vụ này! Bạn có muốn bật 'Windows Media Player' cho hệ điều hành của bạn?" + +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "BambuSource chưa được đăng ký chính xác để phát media! Nhấn Có để đăng ký lại. Bạn sẽ được nhắc hai lần" + +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "Thiếu thành phần BambuSource đã đăng ký để phát media! Vui lòng cài đặt lại OrcaSlicer hoặc tìm kiếm sự giúp đỡ từ cộng đồng." + +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "Đang sử dụng BambuSource từ cài đặt khác, phát video có thể không hoạt động đúng! Nhấn Có để sửa nó." + +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "Hệ thống của bạn thiếu codec H.264 cho GStreamer, cần thiết để phát video. (Hãy thử cài đặt gói gstreamer1.0-plugins-bad hoặc gstreamer1.0-libav, sau đó khởi động lại Orca Slicer?)" + +# AI Translated +#~ msgid "N" +#~ msgstr "N" + +# AI Translated +#~ msgid "g" +#~ msgstr "g" + +# AI Translated +#~ msgid "Fila Saving" +#~ msgstr "Tiết kiệm fila" + +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Chiều cao lớp quá nhỏ.\n" +#~ "Nó sẽ được đặt thành min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Chiều cao lớp vượt quá giới hạn trong Cài đặt máy in -> Extruder -> Giới hạn chiều cao lớp, điều này có thể gây ra vấn đề chất lượng in." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Điều chỉnh về phạm vi đặt tự động?\n" + +#~ msgid "Head diameter" +#~ msgstr "Đường kính đầu" + #~ msgid "Print order within a single layer." #~ msgstr "Thứ tự in trong một lớp đơn." diff --git a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po index f7ca502c9e..81cd5a402b 100644 --- a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po +++ b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Slic3rPE\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "PO-Revision-Date: 2026-06-11 12:37-0300\n" "Last-Translator: Handle <mail@bysb.net>\n" "Language-Team: \n" @@ -2843,6 +2843,10 @@ msgstr "编辑" msgid "Merge with" msgstr "与其合并" +# AI Translated +msgid "Decompose Color" +msgstr "分解颜色" + msgid "Delete this filament" msgstr "移除此耗材" @@ -3124,6 +3128,10 @@ msgstr "组合体" msgid "Merge parts to an object" msgstr "合并零件到对象" +# AI Translated +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "同时使用可变层高与混色子层可能导致混色质量不佳。" + msgid "Add layers" msgstr "添加层" @@ -4574,6 +4582,23 @@ msgstr "当前腔体温度高于材料的安全温度,这可能导致材料软 msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "最低机箱温度(%d℃)高于目标机箱温度(%d℃)。最低值是开始打印的阈值,此时机箱会持续朝目标温度加热,因此它不应超过目标值。该值将被限制到目标值。" +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "层高太小,将设置为最小值(%g mm)。" + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "层高超出了打印机设置 -> 挤出机 -> 层高限制中设置的范围,这可能导致打印质量问题。" + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "是否自动调整到限制值(%g mm)?" + +msgid "Adjust" +msgstr "调整" + # AI Translated msgid "" "Layer height too small\n" @@ -4624,7 +4649,7 @@ msgstr "" "\n" "这个数值将被重置为0。" -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "“交替添加额外”与“确保垂直外壳厚度”的”全部“选项不兼容。" msgid "" @@ -4670,7 +4695,7 @@ msgstr "" "否 - 选择保留支撑独立层高" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height需要小于layer_height。\n" @@ -4678,7 +4703,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "锁定深度应小于表皮深度。\n" @@ -4696,6 +4721,13 @@ msgstr "" "是 - 启用Arachne墙生成器\n" "否 - 禁用Arachne墙生成器并将绒毛表面设置为[位移]模式" +# AI Translated +msgid "Brim ear radius" +msgstr "圆盘半径" + +msgid "Brim width" +msgstr "Brim宽度" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "螺旋模式仅在壁环为 1、支撑被禁用、探测结块检测被禁用、顶部壳层为 0、稀疏填充密度为 0 且延时类型为传统时才起作用。" @@ -4950,6 +4982,14 @@ msgstr "生成校准gcode失败" msgid "Calibration error" msgstr "校准错误" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "此打印机未配置该控件所需的硬件。" + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "此打印机不支持该控件。" + # AI Translated msgid "Network unavailable" msgstr "网络不可用" @@ -5807,7 +5847,7 @@ msgstr "体积:" msgid "Size:" msgstr "尺寸:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "发现G-code路径在层%d,高度为%.2lf mm处有冲突。请将有冲突的对象分离得更远(%s <-> %s)。" @@ -5988,6 +6028,10 @@ msgstr "多设备" msgid "Project" msgstr "项目" +# AI Translated +msgid "Device (Web)" +msgstr "设备(网页)" + msgid "Yes" msgstr "是" @@ -6121,10 +6165,10 @@ msgstr "导入 3MF/STL/STEP/SVG/OBJ/AMF" msgid "Load a model" msgstr "加载模型" -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "导入 ZIP 压缩文件" -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "从 ZIP 压缩文件中导入一个或多个模型" msgid "Import Configs" @@ -7602,6 +7646,10 @@ msgstr "自定义当前盘" msgid "The %s nozzle can not print %s." msgstr "%s 喷嘴无法打印 %s。" +# AI Translated +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "在单挤出机打印机上打印混色耗材丝需要频繁换料和冲刷,可能显著增加废料以及喷嘴/废料槽堵塞的风险。" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "不建议在打印时将 %1% 与 %2% 混合。\n" @@ -7727,12 +7775,44 @@ msgstr "从AMS同步材料列表" msgid "Set filaments to use" msgstr "配置可选择的材料" +# AI Translated +msgid "Add Mixed Filament" +msgstr "添加混合耗材丝" + +# AI Translated +msgid "Mixed Filament" +msgstr "混合耗材丝" + +# AI Translated +msgid "Remove last mixed filament" +msgstr "移除最后一个混合耗材丝" + +# AI Translated +msgid "Add mixed filament" +msgstr "添加混合耗材丝" + +# AI Translated +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "混合耗材丝的组分无效或不匹配。请重新编辑受影响的条目。" + msgid "Search plate, object and part." msgstr "搜索盘、模型和零件。" msgid "Pellets" msgstr "颗粒" +# AI Translated +msgid "Mixed filament has broken component references" +msgstr "混合耗材丝的组分引用已失效" + +# AI Translated +msgid "Edit / Delete / Merge" +msgstr "编辑 / 删除 / 合并" + +# AI Translated +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "目标混合耗材丝将此物理耗材丝用作组分。合并将移除此物理耗材丝,并可能使该混合耗材丝失效。是否继续?" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "完成操作后,%s 项目将关闭并创建一个新项目。" @@ -7869,8 +7949,8 @@ msgstr "请确认这些预设中的G-codes是否安全,以防止对机器造 msgid "Customized Preset" msgstr "自定义的预设" -msgid "Component name(s) inside step file not in UTF8 format!" -msgstr "STEP 文件中的部件名称不是 UTF8 格式!" +msgid "Component name(s) inside step file not in UTF-8 format!" +msgstr "STEP 文件中的部件名称不是 UTF-8 格式!" # AI Translated msgid "Because of unsupported text encoding, garbage characters may appear!" @@ -7892,7 +7972,7 @@ msgstr "对象的体积为零" #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "文件 %s 中对象的尺寸过小,似乎是以米(m)或者英寸(inch)为单位定义的。\n" "OrcaSlicer的内部单位为毫米(mm)。是否要转换成毫米(mm)?" @@ -7913,6 +7993,14 @@ msgstr "" msgid "Multi-part object detected" msgstr "检测到多部分对象" +# AI Translated +msgid "Matching textures to filaments" +msgstr "正在将纹理匹配到耗材丝" + +# AI Translated +msgid "Texture Import Warning" +msgstr "纹理导入警告" + msgid "Load these files as a single object with multiple parts?\n" msgstr "将这些文件加载为一个多零件对象?\n" @@ -8028,21 +8116,21 @@ msgstr "未选择替换目录" msgid "Replaced with 3D files from directory:\n" msgstr "替换为目录中的 3D 文件:\n" -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ 跳过 %1%:同一文件。\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ 跳过 %s:同一文件。\n" -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ 跳过%1%:文件不存在。\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ 跳过%s:文件不存在。\n" -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ 跳过%1%:替换失败。\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ 跳过%s:替换失败。\n" -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔ 替换了 %1%。\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔ 替换了 %s。\n" msgid "Replaced volumes" msgstr "替换的卷" @@ -8118,6 +8206,22 @@ msgstr "" msgid "Sync now" msgstr "立即同步" +# AI Translated +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "纹理导入失败。模型中似乎包含纹理数据,但纹理导入过程未能完成。将仅导入模型的几何数据。" + +# AI Translated +msgid "Applying texture colors..." +msgstr "正在应用纹理颜色..." + +# AI Translated +msgid "Updating 3D view..." +msgstr "正在更新 3D 视图..." + +# AI Translated +msgid "Texture colors applied." +msgstr "纹理颜色已应用。" + msgid "You can keep the modified presets for the new project or discard them" msgstr "您可以保留修改的预设到新项目中或者忽略这些修改" @@ -8767,6 +8871,18 @@ msgstr "启用此选项后,您可以同时向多个设备发送任务并管理 msgid "Pop up to select filament grouping mode" msgstr "弹出选择耗材丝分组模式" +# AI Translated +msgid "Visible plugin pages" +msgstr "可见插件页数" + +# AI Translated +msgid "pages" +msgstr "页" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "作为固定标签显示的插件页数量,其余页面将折叠到最后一个标签的下拉菜单中。" + msgid "Behaviour" msgstr "行为" @@ -9121,6 +9237,18 @@ msgstr "显示不受支持的预设" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "在打印机和耗材下拉列表中显示不兼容/不受支持的预设。这些预设无法被选择。" +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(实验性)使用打印机代理替代打印主机" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"非 Bambu 打印机的打印任务将通过打印机插件代理发送,而不是经典的打印主机上传流程。\n" +"禁用时,OrcaSlicer 使用旧的打印主机行为。" + # AI Translated msgid "Experimental Features" msgstr "实验性功能" @@ -9329,6 +9457,10 @@ msgstr "旋转花瓶" msgid "First layer filament sequence" msgstr "首层耗材打印顺序" +# AI Translated +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "耗材丝列表中包含混合耗材丝。自定义耗材丝顺序将不会生效。" + msgid "By Layer" msgstr "逐层" @@ -9385,9 +9517,25 @@ msgstr "用户预设" msgid "Preset Inside Project" msgstr "项目预设" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "将父预设继承的所有数值复制到当前预设,并解除继承关系。仅与父预设兼容的预设可能会变为不受支持。" + msgid "Detach from parent" msgstr "与父级分离" +# AI Translated +msgid "Unique preset" +msgstr "独立预设" + +# AI Translated +msgid "Parent preset" +msgstr "父预设" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "此预设未继承自其它预设。" + msgid "Name is unavailable." msgstr "名称不可用。" @@ -10093,24 +10241,6 @@ msgstr "您确定要启用此选项吗?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "填充图案通常设计为自动处理旋转,以确保正确打印并实现其预期效果(例如,Gyroid、Cubic)。旋转当前的稀疏填充图案可能会导致支撑不足。请谨慎操作并彻底检查是否存在任何潜在的打印问题。您确定要启用此选项吗?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"层高太小。\n" -"将设置为min_layer_height\n" -"层高太小。\n" -"将自动设置为min_layer_height的值\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "层高超出了打印机设置->挤出机->层高限制中的范围,这可能导致打印质量问题。" - -msgid "Adjust to the set range automatically?\n" -msgstr "是否自动调整到范围内?\n" - -msgid "Adjust" -msgstr "调整" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "实验性选项。在更换耗材丝时,将耗材丝回抽一段距离后再切断以最小化冲刷。虽然这可以显著减少冲刷,但也可能增加喷嘴堵塞或其他打印问题的风险。" @@ -10303,6 +10433,9 @@ msgstr "检测到保留的关键字" msgid "Setting Overrides" msgstr "参数覆盖" +msgid "Retraction when switching material" +msgstr "切换材料时的回抽量" + msgid "Basic information" msgstr "基础信息" @@ -10433,6 +10566,12 @@ msgstr "兼容的切片配置" msgid "Printable space" msgstr "可打印区域" +msgid "Printer Agent" +msgstr "打印机代理" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "为打印机通信选择网络代理。可用的代理将在启动时列出。" + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10558,9 +10697,6 @@ msgstr "层高限制" msgid "Z-Hop" msgstr "Z轴抬升" -msgid "Retraction when switching material" -msgstr "切换材料时的回抽量" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -10674,12 +10810,12 @@ msgid "No modifications need to be copied." msgstr "没有需要复制的修改。" # AI Translated -msgid "Copy paramters" +msgid "Copy parameters" msgstr "复制参数" # AI Translated #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "修改 %s 的参数" # AI Translated @@ -11194,27 +11330,6 @@ msgstr "耗材丝更换时的冲刷体积" msgid "Please choose the filament colour" msgstr "请选择耗材丝颜色" -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "原生 Wayland 实时画面需要 GStreamer GTK 视频接收器。请安装 GStreamer 的 gtksink 插件,然后重启 OrcaSlicer。" - -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "原生 Wayland GStreamer 视频接收器初始化失败。请检查您的 GStreamer GTK 插件安装情况。" - -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "此任务需要 Windows Media Player!您是否要为您的操作系统启用'Windows Media Player'?" - -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "BambuSource 未正确注册用于媒体播放!按是重新注册它。您将被提示两次" - -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "缺少用于媒体播放的已注册 BambuSource 组件!请重新安装 OrcaSlicer 或寻求社区帮助。" - -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "使用来自不同安装的 BambuSource,视频播放可能无法正常工作!按是修复它。" - -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "您的系统缺少 GStreamer 所需的 H.264 编解码器,这是播放视频所必需的。(尝试安装 gstreamer1.0-plugins-bad 或 gstreamer1.0-libav 软件包,然后重新启动 Orca Slicer?)" - msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "云代理不可用。请重启 OrcaSlicer 后重试。" @@ -11911,6 +12026,10 @@ msgstr "离不可打印区域太近,会发生碰撞。\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr "距离聚集检测区域太近,会引起碰撞。\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "有部分超出可打印区域,无法打印。\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "所选的喷嘴温度不兼容。每种耗材的喷嘴温度都必须落在其他耗材的推荐喷嘴温度范围内。否则可能会发生喷嘴堵塞或打印机损坏。" @@ -11923,6 +12042,10 @@ msgstr "如果您仍要打印,可以在 偏好设置 / 控制 / 切片 / 移 msgid "No extrusions under current settings." msgstr "根据当前设置,不会生成任何打印。" +# AI Translated +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "使用了渐变混合耗材丝,但“混色子层”已关闭。渐变将不会被打印。" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "平滑模式的延时摄影不支持在逐件打印模式下使用。" @@ -11959,6 +12082,10 @@ msgstr "或许您想要缩小模型的尺寸,或者更改当前打印设置, msgid "Variable layer height is not supported with Organic supports." msgstr "Organic支撑不支持可变层高。" +# AI Translated +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "擦拭塔耗材丝不能是混合耗材丝。" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "当启用擦拭塔时,不同的喷嘴直径和不同的耗材丝直径可能无法很好地工作。这是非常实验性的,所以请谨慎操作。" @@ -12224,9 +12351,6 @@ msgstr "使用 3MF 代替 G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "如果打印机接受 3MF 文件作为打印任务,请启用此选项。启用后,Orca Slicer 将以 .gcode.3mf 格式发送切片文件,而不是普通的 .gcode 文件。" -msgid "Printer Agent" -msgstr "打印机代理" - msgid "Select the network agent implementation for printer communication." msgstr "选择打印机通信的网络代理实施。" @@ -12309,7 +12433,7 @@ msgstr "mm 或 %" msgid "Other layers" msgstr "其它层" -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." msgstr "除首层外的其他层的热床温度。0值表示该耗材丝不支持在Cool Plate SuperTack上打印。" msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." @@ -12861,9 +12985,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "内部桥接的速度。如果该值以百分比表示,将基于桥接速度计算。默认值为150%。" -msgid "Brim width" -msgstr "Brim宽度" - msgid "This is the distance from the model to the outermost brim line." msgstr "从模型到最外圈brim走线的距离" @@ -12944,6 +13065,14 @@ msgstr "" "在检测尖锐角度之前,几何形状将被简化。此参数表示简化的最小偏差长度。\n" "设为0以停用" +# AI Translated +msgid "Brim ears outer only" +msgstr "仅外轮廓生成圆盘" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "仅在模型的外轮廓上生成圆盘,不包括孔洞和封闭区域。" + msgid "upward compatible machine" msgstr "向上兼容的机器" @@ -13625,6 +13754,8 @@ msgstr "层时间" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "当层预估打印时间小于该数值时,部件冷却风扇将会被开启。风扇转速将根据层打印时间在最大和最小风扇转速之间插值获得" +# AI Translated +msgctxt "second" msgid "s" msgstr "s" @@ -13943,6 +14074,62 @@ msgstr "支撑材料" msgid "Support material is commonly used to print supports and support interfaces." msgstr "支撑材料通常用于打印支撑体和支撑接触面" +# AI Translated +msgid "Is mixed filament" +msgstr "是否为混合耗材丝" + +# AI Translated +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "该耗材丝槽位是否为由多种物理耗材丝组成的混合耗材丝" + +# AI Translated +msgid "Mixed filament components" +msgstr "混合耗材丝组分" + +# AI Translated +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "以逗号分隔的组分耗材丝序号(从 1 开始),例如 \"1,3\"" + +# AI Translated +msgid "Mixed filament sublayer ratios" +msgstr "混合耗材丝子层比例" + +# AI Translated +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "以逗号分隔、总和为 1.0 的比例值,例如 \"0.7,0.3\"" + +# AI Translated +msgid "Mixed filament gradient" +msgstr "混合耗材丝渐变" + +# AI Translated +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "为混合耗材丝的子层启用 Z 方向渐变模式。启用后,子层比例会随层数线性变化。" + +# AI Translated +msgid "Mixed filament gradient range" +msgstr "混合耗材丝渐变范围" + +# AI Translated +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "渐变模式下第一个组分的起始和结束比例。以逗号分隔的一对数值,例如 \"0.10,0.90\" 表示从 10% 到 90%。" + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "混合耗材丝渐变曲线" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "可选的 Photoshop 风格自定义曲线,将 Z 方向进度映射到第一个组分的比例。以竖线分隔的控制点进行编码,格式为 \"x,y\"(旧格式)或在需要覆盖切线时使用 \"x,y,m_in,m_out\"(留空或填 \"nan\" 表示使用 PCHIP 默认值)。x 取值范围为 [0,1];y 会被限制在所配置的比例范围内,例如 \"0,0.15|0.5,0.50|1,0.85\"。留空时改用线性的 gradient_range。" + +# AI Translated +msgid "Mixed filament per-part gradient" +msgstr "混合耗材丝逐零件渐变" + +# AI Translated +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "启用渐变模式时,对组合体中的每个零件分别应用渐变,而不是将整个组合体视为一个 Z 范围。" + msgid "Filament printable" msgstr "可打印耗材" @@ -14119,6 +14306,14 @@ msgstr "TPMS-FK结构" msgid "Gyroid" msgstr "螺旋体" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "稀疏填充平滑系数" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "控制稀疏填充拐角的圆滑程度。0% 保持原有的尖锐路径,100% 则在相邻填充线之间生成尽可能大的圆弧。" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "顶面填充的加速度。使用较低值可能会改善顶面质量" @@ -14659,6 +14854,14 @@ msgstr "打印机兼容的G-code风格'" msgid "Klipper" msgstr "Klipper固件" +# AI Translated +msgid "Skip G-code config block" +msgstr "跳过 G-code 配置块" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "不将 CONFIG_BLOCK(切片软件配置的键值对)写入 G-code 文件。这对固件在解析这些注释行时会崩溃的打印机(例如 Anycubic go-klipper)有帮助。注意:G-code 文件将不再包含切片设置,因此重新导入到 OrcaSlicer 时无法恢复配置。" + msgid "Pellet Modded Printer" msgstr "颗粒改装打印机" @@ -15178,6 +15381,7 @@ msgid "The allowed maximum output force of Y axis" msgstr "Y 轴允许的最大输出力" # AI Translated +msgctxt "Newton" msgid "N" msgstr "N" @@ -15190,6 +15394,7 @@ msgid "The machine bed mass load of Y axis" msgstr "Y 轴的机器热床质量负载" # AI Translated +msgctxt "gram" msgid "g" msgstr "g" @@ -15497,7 +15702,7 @@ msgstr "从切割区域到垃圾桶的起始和结束点。" msgid "Reduce infill retraction" msgstr "减小填充回抽" -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." msgstr "当空驶完全在填充区域内时不触发回抽。这意味着即使漏料也是不可见的。对于复杂模型,该设置能够减少回抽次数以及打印时长,但是会造成G-code生成变慢" msgid "This option will drop the temperature of the inactive extruders to prevent oozing." @@ -15668,7 +15873,7 @@ msgstr "擦拭后的回抽量" # AI Translated #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" "擦拭后快速回抽的长度,相对于回抽长度。\n" @@ -15704,10 +15909,18 @@ msgstr "更换挤出机时长回缩" msgid "Retraction distance when extruder change" msgstr "更换挤出机时的回缩距离" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "回抽长度(换工具头)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "在换工具头之前触发回抽时,耗材丝会按指定的长度回抽(长度是在耗材丝进入挤出机之前,以原始耗材丝测量的)。" + msgid "Z-hop height" msgstr "Z抬升高度" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "回抽完成之后,喷嘴轻微抬升,和打印件之间产生一定间隙。这能够避免空驶时喷嘴和打印件剐蹭和碰撞。使用螺旋线抬升z能够减少拉丝。" msgid "Z-hop lower boundary" @@ -15797,6 +16010,10 @@ msgstr "额外回填长度" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "每当空驶后回抽被补偿时,挤出机将推入额外数量的耗材丝。很少需要此设置。" +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "额外回填长度(换工具头)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "当换色后回抽被补偿时,挤出机将推入额外数量的耗材丝。" @@ -16211,6 +16428,14 @@ msgstr "在擦拭塔上换头" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "在发出换头命令 (Tx) 之前,强制打印头先移动到擦拭塔。仅与使用第 2 类擦拭塔的多挤出机(多打印头)打印机相关。默认情况下,Orca 会在多打印头机器上跳过此移动,因为固件会处理换头,这可能导致 Tx 命令在打印件上方发出。如果您希望换头命令始终在擦拭塔上方发出,请启用此选项。" +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "在擦拭塔上等待温度" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "拾取新工具头后不等待其达到打印温度,直接移动到擦拭塔,并在冲刷前于擦拭塔上等待温度。升温过程中渗出的耗材丝会落在擦拭塔上而不是模型上,且移动时间与加热过程重叠。仅适用于使用 2 型擦拭塔的多挤出机(多工具头)打印机。固件或换工具头宏本身不得等待温度。禁用时,等待温度的指令将在换工具头命令之后立即发出。" + msgid "No sparse layers (beta)" msgstr "无稀疏层 (实验功能)" @@ -16741,6 +16966,14 @@ msgstr "" "\n" "在下方的擦拭前回抽量设置中输入一个数值,将在擦拭动作之前执行任何超出部分的回抽,否则超出部分的回抽将在擦拭之后执行。" +# AI Translated +msgid "Mixed color sublayer" +msgstr "混色子层" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "启用混色子层拆分。启用后,含有混色耗材丝的层将被拆分为子层,以实现混色效果。" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "擦拭塔可以用来清理喷嘴上的残留料和让喷嘴内部的腔压达到稳定状态,以避免打印物体时出现外观瑕疵。" @@ -17816,6 +18049,10 @@ msgstr "模型文件的网格划分失败,或缺少有效的形状。" msgid "The supplied file couldn't be read because it's empty." msgstr "无法读取提供的文件,因为该文件内容为空。" +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "文件格式不兼容,无法解析。" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "未知的文件格式。输入文件的扩展名必须为 .stl、.obj 或 .amf(.xml)。" @@ -19280,17 +19517,17 @@ msgstr "仅显示对打印机、耗材和工艺预设有改动的打印机名称 msgid "Only display the filament names with changes to filament presets." msgstr "仅显示对耗材预设有改动的耗材名称。" -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "只显示带有用户打印机预设的打印机名称,并且您选择的每个预设都将导出为一个 ZIP 文件。" msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "只显示带有用户耗材预设的耗材名称,您选择的每个耗材名称中的所有用户耗材预设都将导出为一个 ZIP 文件。" msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "只显示带有更改的工艺预设的打印机名称,您选择的每个打印机名称中的所有用户工艺预设都将导出为一个 ZIP 文件。" msgid "Please select at least one printer or filament." @@ -19433,9 +19670,6 @@ msgstr "物理打印机" msgid "Print Host upload" msgstr "打印主机上传" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "为打印机通信选择网络代理。可用的代理将在启动时列出。" - msgid "Select a Flashforge printer" msgstr "选择一台 Flashforge 打印机" @@ -19532,7 +19766,7 @@ msgid "We need information for diagnosing source of the issue. Check wiki page f msgstr "我们需要相关信息来诊断问题的根源。请查看 wiki 页面获取详细指南。" # AI Translated -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "“打包”按钮会将当前会话的项目文件和日志收集到一个 zip 文件中。" # AI Translated @@ -19588,7 +19822,7 @@ msgid "Stored logs" msgstr "已存储的日志" # AI Translated -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "将所有已存储的日志打包到一个 zip 文件中。" # AI Translated @@ -19668,7 +19902,7 @@ msgstr "未找到打印机类型,请手动选择。" msgid "Authorizing..." msgstr "正在授权..." -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "错误。无法获取用于授权的 API 令牌" msgid "Could not parse server response." @@ -20118,8 +20352,8 @@ msgid "Enable smart filament assign: Assign one filament to multiple nozzles to msgstr "启用智能耗材分配:将一种耗材分配给多个喷嘴以最大化节省" # AI Translated -msgid "Fila Saving" -msgstr "耗材节省" +msgid "File Saving" +msgstr "文件保存" msgid "Don't remind me again" msgstr "不再提醒" @@ -20325,9 +20559,6 @@ msgstr "在尝试登录时发生了异常,请重试。" msgid "User canceled." msgstr "用户已取消。" -msgid "Head diameter" -msgstr "Brim 直径" - msgid "Max angle" msgstr "最大角度" @@ -20487,6 +20718,9 @@ msgstr "立即重启" msgid "NO RAMMING AT ALL" msgstr "完全不挤压" +msgid "s" +msgstr "s" + msgid "Volumetric speed" msgstr "体积流量" @@ -21111,6 +21345,57 @@ msgstr "" "避免翘曲\n" "您知道吗?打印ABS这类易翘曲材料时,适当提高热床温度可以降低翘曲的概率。" +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "原生 Wayland 实时画面需要 GStreamer GTK 视频接收器。请安装 GStreamer 的 gtksink 插件,然后重启 OrcaSlicer。" + +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "原生 Wayland GStreamer 视频接收器初始化失败。请检查您的 GStreamer GTK 插件安装情况。" + +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "此任务需要 Windows Media Player!您是否要为您的操作系统启用'Windows Media Player'?" + +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "BambuSource 未正确注册用于媒体播放!按是重新注册它。您将被提示两次" + +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "缺少用于媒体播放的已注册 BambuSource 组件!请重新安装 OrcaSlicer 或寻求社区帮助。" + +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "使用来自不同安装的 BambuSource,视频播放可能无法正常工作!按是修复它。" + +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "您的系统缺少 GStreamer 所需的 H.264 编解码器,这是播放视频所必需的。(尝试安装 gstreamer1.0-plugins-bad 或 gstreamer1.0-libav 软件包,然后重新启动 Orca Slicer?)" + +# AI Translated +#~ msgid "N" +#~ msgstr "N" + +# AI Translated +#~ msgid "g" +#~ msgstr "g" + +# AI Translated +#~ msgid "Fila Saving" +#~ msgstr "耗材节省" + +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "层高太小。\n" +#~ "将设置为min_layer_height\n" +#~ "层高太小。\n" +#~ "将自动设置为min_layer_height的值\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "层高超出了打印机设置->挤出机->层高限制中的范围,这可能导致打印质量问题。" + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "是否自动调整到范围内?\n" + +#~ msgid "Head diameter" +#~ msgstr "Brim 直径" + #~ msgid "Print order within a single layer." #~ msgstr "同一层内的打印顺序" diff --git a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po index 912396eea8..ded90c8950 100644 --- a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po +++ b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-09-02 09:39-0300\n" "PO-Revision-Date: 2025-11-28 13:48-0600\n" "Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n" "Language-Team: \n" @@ -2913,6 +2913,10 @@ msgstr "編輯" msgid "Merge with" msgstr "合併到" +# AI Translated +msgid "Decompose Color" +msgstr "分解顏色" + msgid "Delete this filament" msgstr "刪除此線材" @@ -3209,6 +3213,10 @@ msgstr "組合體" msgid "Merge parts to an object" msgstr "合併零件為物件" +# AI Translated +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "同時使用可變層高與混色子層可能導致混色品質不佳。" + # AI Translated msgid "Add layers" msgstr "新增層" @@ -4691,6 +4699,23 @@ msgstr "目前列印裝置內部溫度高於線材的安全溫度,可能會導 msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "最低倉室溫度(%d℃)高於目標倉室溫度(%d℃)。最低值是列印開始的門檻,此時倉室會持續朝目標溫度加熱,因此不應超過目標值。系統會將其限制在目標值。" +# AI Translated +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "層高過小,將設定為最小值(%g mm)。" + +# AI Translated +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "層高超出了印表裝置設定 -> 擠出機 -> 層高限制中設定的範圍,這可能會導致列印品質問題。" + +# AI Translated +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "是否自動調整至限制值(%g mm)?" + +msgid "Adjust" +msgstr "調整" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4741,7 +4766,7 @@ msgstr "" "\n" "該值將會重設為 0。" -msgid "Alternate extra wall does't work well when ensure vertical shell thickness is set to All." +msgid "Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All." msgstr "當確保垂直外殼厚度設為『全部』時,交錯額外牆壁效果不佳。" msgid "" @@ -4798,7 +4823,7 @@ msgstr "" "否 - 選擇保留獨立支撐層高" msgid "" -"seam_slope_start_height need to be smaller than layer_height.\n" +"seam_slope_start_height needs to be smaller than layer_height.\n" "Reset to 0." msgstr "" "seam_slope_start_height 必須小於 layer_height。\n" @@ -4807,7 +4832,7 @@ msgstr "" #, no-c-format, no-boost-format msgid "" -"Lock depth should smaller than skin depth.\n" +"Lock depth should be smaller than skin depth.\n" "Reset to 50% of skin depth." msgstr "" "鎖定深度應小於表皮深度。\n" @@ -4825,6 +4850,13 @@ msgstr "" "是 - 啟用 Arachne Wall 產生器\n" "否 - 停用 Arachne Wall 產生器,並將 Fuzzy Skin 設定為 [位移] 模式" +# AI Translated +msgid "Brim ear radius" +msgstr "耳狀 Brim 半徑" + +msgid "Brim width" +msgstr "Brim 寬度" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "花瓶模式僅適用於牆體圈數為 1、停用支撐、停用偵測堵塞、頂部外殼層數為 0、稀疏填充密度為 0,且延時攝影類型為傳統模式時。" @@ -5079,6 +5111,14 @@ msgstr "產生校正代碼失敗" msgid "Calibration error" msgstr "校正錯誤" +# AI Translated +msgid "This printer is not configured with the hardware this control needs." +msgstr "此列印裝置未配置此控制項所需的硬體。" + +# AI Translated +msgid "This control is not supported on this printer." +msgstr "此列印裝置不支援此控制項。" + # AI Translated msgid "Network unavailable" msgstr "網路無法使用" @@ -5936,7 +5976,7 @@ msgstr "體積:" msgid "Size:" msgstr "尺寸:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "發現 G-code 路徑在 %d 層,Z = %.2lf mm 處的衝突。請將有衝突的物件分離得更遠(%s <-> %s)。" @@ -6118,6 +6158,10 @@ msgstr "多臺裝置" msgid "Project" msgstr "專案" +# AI Translated +msgid "Device (Web)" +msgstr "裝置(網頁)" + msgid "Yes" msgstr "是" @@ -6251,10 +6295,10 @@ msgstr "匯入 3MF/STL/STEP/SVG/OBJ/AMF" msgid "Load a model" msgstr "載入模型" -msgid "Import Zip Archive" +msgid "Import ZIP Archive" msgstr "匯入壓縮檔" -msgid "Load models contained within a zip archive" +msgid "Load models contained within a ZIP archive" msgstr "載入壓縮檔中的模型" msgid "Import Configs" @@ -7763,6 +7807,10 @@ msgstr "自訂列印板參數" msgid "The %s nozzle can not print %s." msgstr "%s 噴嘴無法列印 %s。" +# AI Translated +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "在單擠出機列印裝置上列印混色線材需要頻繁換料與清理,可能大幅增加廢料以及噴嘴/廢料槽堵塞的風險。" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "不建議在列印時混用 %1% 和 %2%。\n" @@ -7888,12 +7936,44 @@ msgstr "從 AMS 同步線材清單" msgid "Set filaments to use" msgstr "設定可選擇的線材" +# AI Translated +msgid "Add Mixed Filament" +msgstr "新增混合線材" + +# AI Translated +msgid "Mixed Filament" +msgstr "混合線材" + +# AI Translated +msgid "Remove last mixed filament" +msgstr "移除最後一個混合線材" + +# AI Translated +msgid "Add mixed filament" +msgstr "新增混合線材" + +# AI Translated +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "混合線材的成分無效或不相符。請重新編輯受影響的項目。" + msgid "Search plate, object and part." msgstr "搜尋列印板、物件和零件。" msgid "Pellets" msgstr "顆粒" +# AI Translated +msgid "Mixed filament has broken component references" +msgstr "混合線材的成分參照已失效" + +# AI Translated +msgid "Edit / Delete / Merge" +msgstr "編輯 / 刪除 / 合併" + +# AI Translated +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "目標混合線材使用此實體線材作為成分。合併將移除此實體線材,並可能使該混合線材失效。是否繼續?" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "完成操作後,%s 專案將關閉並建立新專案。" @@ -8030,7 +8110,7 @@ msgstr "請確認這些預設中的 G-code 是安全的,以防止對列印裝 msgid "Customized Preset" msgstr "自訂預設" -msgid "Component name(s) inside step file not in UTF8 format!" +msgid "Component name(s) inside step file not in UTF-8 format!" msgstr "STEP 檔案內部元件的名稱不是 UTF-8 格式!" # AI Translated @@ -8053,7 +8133,7 @@ msgstr "物件的體積為零" #, c-format, boost-format msgid "" "The object from file %s is too small, and may be in meters or inches.\n" -" Do you want to scale to millimeters?" +"Do you want to scale to millimeters?" msgstr "" "檔案 %s 中的物件太小,可能以公尺(M)或英吋(Inches)為單位。\n" "是否要轉換成毫米(mm)?" @@ -8074,6 +8154,14 @@ msgstr "" msgid "Multi-part object detected" msgstr "偵測到多部分物件" +# AI Translated +msgid "Matching textures to filaments" +msgstr "正在將紋理對應到線材" + +# AI Translated +msgid "Texture Import Warning" +msgstr "紋理匯入警告" + msgid "Load these files as a single object with multiple parts?\n" msgstr "將這些檔案載入為一個多零件物件?\n" @@ -8193,21 +8281,21 @@ msgstr "未選擇替換的目錄" msgid "Replaced with 3D files from directory:\n" msgstr "已從目錄替換為 3D 檔案:\n" -#, boost-format -msgid "✖ Skipped %1%: same file.\n" -msgstr "✖ 已跳過 %1%:相同檔案。\n" +#, c-format, boost-format +msgid "✖ Skipped %s: same file.\n" +msgstr "✖ 已跳過 %s:相同檔案。\n" -#, boost-format -msgid "✖ Skipped %1%: file does not exist.\n" -msgstr "✖ 已跳過 %1%:檔案不存在。\n" +#, c-format, boost-format +msgid "✖ Skipped %s: file does not exist.\n" +msgstr "✖ 已跳過 %s:檔案不存在。\n" -#, boost-format -msgid "✖ Skipped %1%: failed to replace.\n" -msgstr "✖ 已跳過 %1%:無法替換。\n" +#, c-format, boost-format +msgid "✖ Skipped %s: failed to replace.\n" +msgstr "✖ 已跳過 %s:無法替換。\n" -#, boost-format -msgid "✔ Replaced %1%.\n" -msgstr "✔ 已替換 %1%。\n" +#, c-format, boost-format +msgid "✔ Replaced %s.\n" +msgstr "✔ 已替換 %s。\n" msgid "Replaced volumes" msgstr "已替換體積" @@ -8284,6 +8372,22 @@ msgstr "" msgid "Sync now" msgstr "立即同步" +# AI Translated +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "紋理匯入失敗。模型中似乎包含紋理資料,但紋理匯入程序未能完成。將僅匯入模型的幾何資料。" + +# AI Translated +msgid "Applying texture colors..." +msgstr "正在套用紋理顏色..." + +# AI Translated +msgid "Updating 3D view..." +msgstr "正在更新 3D 檢視..." + +# AI Translated +msgid "Texture colors applied." +msgstr "紋理顏色已套用。" + msgid "You can keep the modified presets for the new project or discard them" msgstr "您可以將修改後的預設檔保留到新專案中或者忽略這些修改" @@ -8940,6 +9044,18 @@ msgstr "啟用時可以同時傳送到並管理多個機臺。" msgid "Pop up to select filament grouping mode" msgstr "彈出視窗選擇線材分組模式" +# AI Translated +msgid "Visible plugin pages" +msgstr "可見的外掛頁面數" + +# AI Translated +msgid "pages" +msgstr "頁" + +# AI Translated +msgid "Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab." +msgstr "以固定分頁顯示的外掛頁面數量,其餘頁面會收合至最後一個分頁的下拉選單中。" + msgid "Behaviour" msgstr "行為" @@ -9294,6 +9410,18 @@ msgstr "顯示不支援的預設" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "在列印裝置和線材下拉選單中顯示不相容/不支援的預設。這些預設無法選取。" +# AI Translated +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "(實驗性)使用列印裝置代理程式取代列印主機" + +# AI Translated +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" +"將非 Bambu 列印裝置的列印工作透過列印裝置外掛代理程式傳送,而非傳統的列印主機上傳流程。\n" +"停用時,OrcaSlicer 會使用舊有的列印主機行為。" + # AI Translated msgid "Experimental Features" msgstr "實驗性功能" @@ -9503,6 +9631,10 @@ msgstr "螺旋花瓶模式" msgid "First layer filament sequence" msgstr "首層線材列印順序" +# AI Translated +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "線材清單中包含混合線材。自訂線材順序將不會生效。" + msgid "By Layer" msgstr "逐層" @@ -9558,9 +9690,25 @@ msgstr "使用者預設" msgid "Preset Inside Project" msgstr "項目預設" +# AI Translated +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "將父配置繼承的所有數值複製到目前的配置,並解除繼承關係。僅與父配置相容的配置可能會變成不受支援。" + msgid "Detach from parent" msgstr "從父預設分離" +# AI Translated +msgid "Unique preset" +msgstr "獨立配置" + +# AI Translated +msgid "Parent preset" +msgstr "父配置" + +# AI Translated +msgid "This preset does not inherit from another preset." +msgstr "此配置未繼承自其他配置。" + msgid "Name is unavailable." msgstr "名稱不可用。" @@ -10299,22 +10447,6 @@ msgstr "您確認要啟用此選項嗎?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "填充模式通常設計為自動處理旋轉,以確保正確列印並實現其預期效果(例如:Gyroid、Cubic)。旋轉目前的稀疏填充模式可能會導致支撐不足。請謹慎操作,並仔細檢查任何潛在的列印問題。您確定要啟用此選項嗎?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"層高過薄\n" -"將改為 min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "層高超過了印表裝置設定 -> 擠出機 -> 層高限制,這可能會導致列印品質問題。" - -msgid "Adjust to the set range automatically?\n" -msgstr "是否自動調整至設定範圍?\n" - -msgid "Adjust" -msgstr "調整" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "實驗性功能:在換線過程中以更大的距離收回並切斷線材,以減少沖洗量。儘管這可以顯著減少沖洗,但也可能增加噴嘴堵塞或其他列印問題的風險。" @@ -10507,6 +10639,9 @@ msgstr "偵測到保留的關鍵字" msgid "Setting Overrides" msgstr "參數覆蓋" +msgid "Retraction when switching material" +msgstr "切換線材時的回抽量" + msgid "Basic information" msgstr "基本資訊" @@ -10637,6 +10772,12 @@ msgstr "相容的切片設定" msgid "Printable space" msgstr "可列印區域" +msgid "Printer Agent" +msgstr "列印裝置代理" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "選擇列印裝置通訊的網路代理實施。可用代理在啟動時註冊。" + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10762,9 +10903,6 @@ msgstr "層高限制" msgid "Z-Hop" msgstr "Z 軸抬升" -msgid "Retraction when switching material" -msgstr "切換線材時的回抽量" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -10878,12 +11016,12 @@ msgid "No modifications need to be copied." msgstr "沒有需要複製的修改。" # AI Translated -msgid "Copy paramters" +msgid "Copy parameters" msgstr "複製參數" # AI Translated #, c-format, boost-format -msgid "Modify paramters of %s" +msgid "Modify parameters of %s" msgstr "修改 %s 的參數" # AI Translated @@ -11400,27 +11538,6 @@ msgstr "線材更換時產生的廢料體積" msgid "Please choose the filament colour" msgstr "請選擇線材顏色" -msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." -msgstr "原生 Wayland 即時檢視需要 GStreamer GTK 視訊接收器。請為 GStreamer 安裝 gtksink 外掛程式,然後重新啟動 OrcaSlicer。" - -msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." -msgstr "無法初始化原生 Wayland GStreamer 視訊接收器。請檢查您的 GStreamer GTK 外掛程式安裝。" - -msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" -msgstr "執行此設定需要 Windows Media Player!您是否要啟用 Windows Media Player?" - -msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" -msgstr "「BambuSource 未正確註冊為媒體播放模組!請點選『是』進行重新註冊,過程中會有兩次提示" - -msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." -msgstr "缺少用於媒體播放的已註冊 BambuSource 元件!請重新安裝 OrcaSlicer 或尋求社群協助。" - -msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." -msgstr "BambuSource 來自其他安裝版本,可能導致影片播放異常!請點選『是』進行修復。" - -msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" -msgstr "您的系統缺少 GStreamer 的 H.264 編解碼器,這是播放影片所必需的。(請嘗試安裝 gstreamer1.0-plugins-bad 或 gstreamer1.0-libav 套件,然後重新啟動 Orca Slicer。)" - msgid "Cloud agent is not available. Please restart OrcaSlicer and try again." msgstr "雲端代理無法使用。請重新啟動 OrcaSlicer 後再試一次。" @@ -12113,6 +12230,10 @@ msgstr "離淨空區域太近,會發生碰撞。\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr "離堵塞偵測區域太近,會發生碰撞。\n" +# AI Translated +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "有部分超出可列印區域,無法列印。\n" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "所選的噴嘴溫度不相容。每種線材的噴嘴溫度都必須落在其他線材的建議噴嘴溫度範圍內。否則可能會發生噴嘴堵塞或列印裝置損壞。" @@ -12125,6 +12246,10 @@ msgstr "如果您仍想列印,可以在「偏好設定 / 控制 / 切片 / 移 msgid "No extrusions under current settings." msgstr "根據目前設定,不會進行任何列印。" +# AI Translated +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "使用了漸層混合線材,但「混色子層」已停用。漸層將不會列印。" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "逐件列印模式下不支援使用平滑模式的縮時錄影。" @@ -12161,6 +12286,10 @@ msgstr "您可能想要減小模型的尺寸或更改目前的列印設定並重 msgid "Variable layer height is not supported with Organic supports." msgstr "有機樹支撐不支持可變層高。" +# AI Translated +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "換料塔線材不能是混合線材。" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "當啟用換料時,不同的噴嘴直徑和線材直徑可能無法正常配合。此功能屬於實驗性階段,請小心使用。" @@ -12426,9 +12555,6 @@ msgstr "使用 3MF 取代 G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "若列印裝置接受 3MF 檔案作為列印作業,請啟用此選項。啟用後,Orca Slicer 會將切片後的檔案以 .gcode.3mf 形式傳送,而非單純的 .gcode 檔案。" -msgid "Printer Agent" -msgstr "列印裝置代理" - msgid "Select the network agent implementation for printer communication." msgstr "選擇用於列印裝置通訊的網路代理實作。" @@ -12511,7 +12637,7 @@ msgstr "mm 或 %" msgid "Other layers" msgstr "其它層" -msgid "Bed temperature for layers except the initial one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." +msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack." msgstr "除首層外的其他層的熱床溫度。0值表示該線材不支援在低溫增穩列印板上列印。" msgid "This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate." @@ -13074,9 +13200,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "內部橋接速度。如果該值以百分比表示,將基於 bridge_speed 進行計算。預設值為 150%。" -msgid "Brim width" -msgstr "Brim 寬度" - msgid "This is the distance from the model to the outermost brim line." msgstr "從模型到 Brim 最外圈的距離" @@ -13157,6 +13280,14 @@ msgstr "" "在偵測尖銳角度之前,幾何形狀將被簡化。此參數表示簡化的最小偏差長度。\n" "設為 0 以停用" +# AI Translated +msgid "Brim ears outer only" +msgstr "僅外輪廓產生耳狀 Brim" + +# AI Translated +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "僅在模型的外輪廓上產生耳狀 Brim,不包含孔洞與封閉區域。" + msgid "upward compatible machine" msgstr "向上相容的裝置" @@ -13836,6 +13967,8 @@ msgstr "每一層列印時間" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "當層預估列印時間小於該數值時,物件冷卻風扇將會被開啟。風扇轉速將根據層列印時間在最大和最小風扇轉速之間自動調整" +# AI Translated +msgctxt "second" msgid "s" msgstr "秒" @@ -14141,6 +14274,62 @@ msgstr "支撐材料" msgid "Support material is commonly used to print supports and support interfaces." msgstr "支撐材料通常用於列印支撐體和支撐接觸面" +# AI Translated +msgid "Is mixed filament" +msgstr "是否為混合線材" + +# AI Translated +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "此線材槽是否為由多種實體線材組成的混合線材" + +# AI Translated +msgid "Mixed filament components" +msgstr "混合線材成分" + +# AI Translated +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "以逗號分隔的成分線材序號(從 1 開始),例如 \"1,3\"" + +# AI Translated +msgid "Mixed filament sublayer ratios" +msgstr "混合線材子層比例" + +# AI Translated +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "以逗號分隔、總和為 1.0 的比例值,例如 \"0.7,0.3\"" + +# AI Translated +msgid "Mixed filament gradient" +msgstr "混合線材漸層" + +# AI Translated +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "為混合線材的子層啟用 Z 方向漸層模式。啟用後,子層比例會隨層數線性變化。" + +# AI Translated +msgid "Mixed filament gradient range" +msgstr "混合線材漸層範圍" + +# AI Translated +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "漸層模式下第一個成分的起始與結束比例。以逗號分隔的一對數值,例如 \"0.10,0.90\" 表示從 10% 到 90%。" + +# AI Translated +msgid "Mixed filament gradient curve" +msgstr "混合線材漸層曲線" + +# AI Translated +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "選用的 Photoshop 風格自訂曲線,將 Z 方向進度對應到第一個成分的比例。以直立線分隔的控制點進行編碼,格式為 \"x,y\"(舊格式)或在需要覆寫切線時使用 \"x,y,m_in,m_out\"(留空或填入 \"nan\" 表示使用 PCHIP 預設值)。x 的取值範圍為 [0,1];y 會被限制在所設定的比例範圍內,例如 \"0,0.15|0.5,0.50|1,0.85\"。留空時改用線性的 gradient_range。" + +# AI Translated +msgid "Mixed filament per-part gradient" +msgstr "混合線材逐零件漸層" + +# AI Translated +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "啟用漸層模式時,對組合體中的每個零件分別套用漸層,而不是將整個組合體視為單一 Z 範圍。" + msgid "Filament printable" msgstr "線材可列印" @@ -14316,6 +14505,14 @@ msgstr "TPMS-FK結構" msgid "Gyroid" msgstr "螺旋體" +# AI Translated +msgid "Sparse infill smooth factor" +msgstr "稀疏填充平滑係數" + +# AI Translated +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." +msgstr "控制稀疏填充轉角的圓滑程度。0% 保持原有的銳利路徑,100% 則在相鄰填充線之間產生盡可能大的圓弧。" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "頂面填充的加速度。使用較低值可能會改善頂面列印品質" @@ -14856,6 +15053,14 @@ msgstr "列印裝置相容的 G-code 樣式" msgid "Klipper" msgstr "Klipper" +# AI Translated +msgid "Skip G-code config block" +msgstr "略過 G-code 設定區塊" + +# AI Translated +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "不將 CONFIG_BLOCK(切片軟體設定的鍵值對)寫入 G-code 檔案。這對於韌體在解析這些註解行時會當機的列印裝置(例如 Anycubic go-klipper)有幫助。注意:G-code 檔案將不再包含切片設定,因此重新匯入 OrcaSlicer 時無法還原設定。" + msgid "Pellet Modded Printer" msgstr "顆粒改裝列印裝置" @@ -15377,6 +15582,7 @@ msgid "The allowed maximum output force of Y axis" msgstr "Y 軸允許的最大輸出力" # AI Translated +msgctxt "Newton" msgid "N" msgstr "N" @@ -15389,6 +15595,7 @@ msgid "The machine bed mass load of Y axis" msgstr "Y 軸的機器床台質量負載" # AI Translated +msgctxt "gram" msgid "g" msgstr "g" @@ -15702,7 +15909,7 @@ msgstr "從切割區域到垃圾桶的起始和結束點。" msgid "Reduce infill retraction" msgstr "減小填充回抽" -msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped." +msgid "Don't retract when the travel is entirely within an infill area. That means the oozing can't been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped." msgstr "當空駛完全在填充區域內時不觸發回抽。這意味著即使漏料也是不可見的。對於複雜模型,該設定能夠減少回抽次數以及列印時長,但是會造成 G-code 產生變慢" msgid "This option will drop the temperature of the inactive extruders to prevent oozing." @@ -15873,7 +16080,7 @@ msgstr "擦拭後的回抽量" # AI Translated #, no-c-format, no-boost-format msgid "" -"The length of fast retraction after wipe, relative to retraction length.\n" +"This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" "擦拭後快速回抽的長度,相對於回抽長度。\n" @@ -15909,10 +16116,18 @@ msgstr "更換擠出機時長回抽" msgid "Retraction distance when extruder change" msgstr "更換擠出機時的回抽距離" +# AI Translated +msgid "Retraction Length (Toolchange)" +msgstr "回抽長度(換工具)" + +# AI Translated +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "在換工具之前觸發回抽時,線材會依指定的長度回抽(長度是在線材進入擠出機之前,以原始線材測量)。" + msgid "Z-hop height" msgstr "Z 抬升高度" -msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing." +msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." msgstr "回抽完成之後,噴嘴輕微抬升,和列印物件之間產生一定間隙。這能夠避免空駛時噴嘴和列印物件剮蹭和碰撞。使用螺旋線抬升z能夠減少拉絲" msgid "Z-hop lower boundary" @@ -16002,6 +16217,10 @@ msgstr "額外回填長度" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "每當空駛後回抽被補償時,擠出機將推入額外長度的線材。很少需要此設定。" +# AI Translated +msgid "Extra length on restart (Toolchange)" +msgstr "額外回填長度(換工具)" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "當換色後回抽被補償時,擠出機將推入額外長度的線材。" @@ -16405,6 +16624,14 @@ msgstr "在換料塔上換刀" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "強制工具頭在發出換刀指令 (Tx) 之前先移動到換料塔。僅適用於使用 Type 2 換料塔的多擠出機(多工具頭)列印裝置。預設情況下,Orca 會在多工具頭機器上略過此空駛,因為韌體會處理工具頭交換,這可能導致 Tx 指令在已列印零件上方發出。若您希望換刀一律改在換料塔上方發出,請啟用此選項。" +# AI Translated +msgid "Wait for temperature on wipe tower" +msgstr "在換料塔上等待溫度" + +# AI Translated +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "取用新工具時不等待其達到列印溫度,先移動到換料塔,並在清理前於換料塔上等待溫度。升溫過程中滲出的線材會落在換料塔上而非模型上,且移動時間與加熱過程重疊。僅適用於使用第 2 型換料塔的多擠出機(多工具頭)列印裝置。韌體或換工具巨集本身不得等待溫度。停用時,等待溫度的指令會在換工具命令之後立即發出。" + msgid "No sparse layers (beta)" msgstr "取消稀疏層(Beta)" @@ -16933,6 +17160,14 @@ msgstr "" "\n" "在以下的『擦拭前的回抽量』設定中設定一個值,將在擦拭之前執行任何額外的回抽操作;否則,將在擦拭之後執行。" +# AI Translated +msgid "Mixed color sublayer" +msgstr "混色子層" + +# AI Translated +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "啟用混色子層分割。啟用後,含有混色線材的層將被分割為子層,以達成混色效果。" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "換料塔的功能是用於清除噴嘴上的殘留物,同時穩定噴嘴內的壓力,從而避免列印物件時出現外觀瑕疵。" @@ -18002,6 +18237,10 @@ msgstr "模型檔案網格化失敗或沒有有效形狀" msgid "The supplied file couldn't be read because it's empty." msgstr "無法讀取提供的檔案,因為該檔案為空" +# AI Translated +msgid "The file format is incompatible and cannot be parsed." +msgstr "檔案格式不相容,無法解析。" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "檔案格式未知。輸入的檔案必須是 .stl、.obj 或 .amf(.xml) 格式。" @@ -19465,19 +19704,19 @@ msgstr "僅顯示對列印裝置、線材和處理預設設定有變更的列印 msgid "Only display the filament names with changes to filament presets." msgstr "僅顯示對線材預設設定有變更的線材名稱。" -msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip." +msgid "Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive." msgstr "只有具有使用者列印裝置預設設定的列印裝置名稱會顯示,您選擇的每個預設設定將以 zip 檔案格式匯出。" msgid "" "Only the filament names with user filament presets will be displayed, \n" -"and all user filament presets in each filament name you select will be exported as a zip." +"and all user filament presets in each filament name you select will be exported as a ZIP archive." msgstr "" "只有具有使用者線材預設設定的線材名稱會顯示,\n" "您選擇的每個線材名稱中的所有使用者線材預設設定將以 zip 檔案格式匯出。" msgid "" "Only printer names with changed process presets will be displayed, \n" -"and all user process presets in each printer name you select will be exported as a zip." +"and all user process presets in each printer name you select will be exported as a ZIP archive." msgstr "" "只有具有變更過處理預設設定的列印裝置名稱會顯示,\n" "您選擇的每個列印裝置名稱中的所有使用者處理預設設定將以 zip 檔案格式匯出。" @@ -19622,9 +19861,6 @@ msgstr "實體列印裝置" msgid "Print Host upload" msgstr "列印主機上傳" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "選擇列印裝置通訊的網路代理實施。可用代理在啟動時註冊。" - msgid "Select a Flashforge printer" msgstr "選取 Flashforge 列印裝置" @@ -19721,7 +19957,7 @@ msgid "We need information for diagnosing source of the issue. Check wiki page f msgstr "我們需要相關資訊以診斷問題來源。請查看 wiki 頁面以取得詳細指南。" # AI Translated -msgid "Pack button collects project file and logs of current session onto a zip file." +msgid "Pack button collects project file and logs of current session onto a ZIP archive." msgstr "「打包」按鈕會將目前工作階段的專案檔案與記錄收集到一個 zip 檔案中。" # AI Translated @@ -19777,7 +20013,7 @@ msgid "Stored logs" msgstr "已儲存的記錄" # AI Translated -msgid "Packs all stored logs onto a zip file." +msgid "Packs all stored logs onto a ZIP archive." msgstr "將所有已儲存的記錄打包成一個 zip 檔案。" # AI Translated @@ -19857,7 +20093,7 @@ msgstr "找不到列印裝置類型,請手動選取。" msgid "Authorizing..." msgstr "授權中..." -msgid "Error. Can't get api token for authorization" +msgid "Error. Can't get API token for authorization" msgstr "錯誤。無法取得用於授權的 API token。" msgid "Could not parse server response." @@ -20309,8 +20545,8 @@ msgid "Enable smart filament assign: Assign one filament to multiple nozzles to msgstr "啟用智慧線材指派:將一種線材指派給多個噴嘴以最大化節省" # AI Translated -msgid "Fila Saving" -msgstr "線材節省" +msgid "File Saving" +msgstr "檔案儲存" msgid "Don't remind me again" msgstr "不要再提醒我" @@ -20516,9 +20752,6 @@ msgstr "嘗試登入時發生了意外錯誤,請再試一次。" msgid "User canceled." msgstr "使用者取消。" -msgid "Head diameter" -msgstr "頭直徑" - msgid "Max angle" msgstr "最大角度" @@ -20678,6 +20911,9 @@ msgstr "立即重新啟動" msgid "NO RAMMING AT ALL" msgstr "完全不擠壓" +msgid "s" +msgstr "秒" + msgid "Volumetric speed" msgstr "體積速度" @@ -21323,6 +21559,55 @@ msgstr "" "避免翹曲\n" "您知道嗎?當列印容易翹曲的材料(如 ABS)時,適當提高熱床溫度可以降低翹曲的機率。" +#~ msgid "Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer." +#~ msgstr "原生 Wayland 即時檢視需要 GStreamer GTK 視訊接收器。請為 GStreamer 安裝 gtksink 外掛程式,然後重新啟動 OrcaSlicer。" + +#~ msgid "Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation." +#~ msgstr "無法初始化原生 Wayland GStreamer 視訊接收器。請檢查您的 GStreamer GTK 外掛程式安裝。" + +#~ msgid "Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?" +#~ msgstr "執行此設定需要 Windows Media Player!您是否要啟用 Windows Media Player?" + +#~ msgid "BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice" +#~ msgstr "「BambuSource 未正確註冊為媒體播放模組!請點選『是』進行重新註冊,過程中會有兩次提示" + +#~ msgid "Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help." +#~ msgstr "缺少用於媒體播放的已註冊 BambuSource 元件!請重新安裝 OrcaSlicer 或尋求社群協助。" + +#~ msgid "Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it." +#~ msgstr "BambuSource 來自其他安裝版本,可能導致影片播放異常!請點選『是』進行修復。" + +#~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" +#~ msgstr "您的系統缺少 GStreamer 的 H.264 編解碼器,這是播放影片所必需的。(請嘗試安裝 gstreamer1.0-plugins-bad 或 gstreamer1.0-libav 套件,然後重新啟動 Orca Slicer。)" + +# AI Translated +#~ msgid "N" +#~ msgstr "N" + +# AI Translated +#~ msgid "g" +#~ msgstr "g" + +# AI Translated +#~ msgid "Fila Saving" +#~ msgstr "線材節省" + +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "層高過薄\n" +#~ "將改為 min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "層高超過了印表裝置設定 -> 擠出機 -> 層高限制,這可能會導致列印品質問題。" + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "是否自動調整至設定範圍?\n" + +#~ msgid "Head diameter" +#~ msgstr "頭直徑" + #~ msgid "Print order within a single layer." #~ msgstr "每一層的列印順序" diff --git a/resources/filament_mixing/standard_color_recipes.json b/resources/filament_mixing/standard_color_recipes.json new file mode 100644 index 0000000000..df03b49ac3 --- /dev/null +++ b/resources/filament_mixing/standard_color_recipes.json @@ -0,0 +1,14705 @@ +{ + "_comment": "Simulated values (source=filament_mixer) are generated by FilamentMixer, a degree-4 polynomial regression trained to approximate Mixbox behavior (Mean Delta-E ~2.07). This file does not use Mixbox source code, binaries, or data files. See src/libslic3r/FilamentMixerModel.hpp.", + "entries": [ + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 48.14, + 33.87, + -25.42 + ], + "measured_rgb": "#965E9E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 48.2, + 33.11, + -25.85 + ], + "measured_rgb": "#955F9E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 48.06, + 29.44, + -27.62 + ], + "measured_rgb": "#8D61A1", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 47.82, + 28.45, + -28.19 + ], + "measured_rgb": "#8A62A1", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 47.94, + 22.08, + -30.15 + ], + "measured_rgb": "#7D67A5", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 47.64, + 24.63, + -30.31 + ], + "measured_rgb": "#8164A4", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 48.28, + 17.77, + -31.76 + ], + "measured_rgb": "#746BA8", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 48.15, + 16.73, + -32.42 + ], + "measured_rgb": "#706BA9", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 48.57, + 17.11, + -32.94 + ], + "measured_rgb": "#716CAB", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 48.95, + 13.88, + -33.83 + ], + "measured_rgb": "#6A6FAE", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 49.1, + 13.76, + -34.39 + ], + "measured_rgb": "#6A70AF", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 49.58, + 11.74, + -35.13 + ], + "measured_rgb": "#6572B1", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 50.88, + 5.61, + -36.4 + ], + "measured_rgb": "#5679B7", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 70.0, + -35.0, + 56.27 + ], + "measured_rgb": "#8ABA3C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 69.5, + -36.46, + 55.41 + ], + "measured_rgb": "#85B93C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 67.44, + -38.62, + 50.18 + ], + "measured_rgb": "#78B443", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 66.34, + -39.72, + 47.6 + ], + "measured_rgb": "#71B246", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 65.4, + -40.42, + 42.8 + ], + "measured_rgb": "#6AB04E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 63.51, + -42.24, + 38.44 + ], + "measured_rgb": "#5DAB52", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 63.04, + -42.17, + 35.8 + ], + "measured_rgb": "#59AA56", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 62.21, + -43.03, + 32.64 + ], + "measured_rgb": "#51A85A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 60.68, + -43.94, + 27.49 + ], + "measured_rgb": "#44A560", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 60.36, + -43.76, + 23.75 + ], + "measured_rgb": "#3FA466", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 59.06, + -44.3, + 17.42 + ], + "measured_rgb": "#2CA16E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 58.47, + -44.1, + 14.26 + ], + "measured_rgb": "#219F72", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 58.09, + -43.7, + 10.53 + ], + "measured_rgb": "#139E78", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 73.74, + -14.74, + -21.41 + ], + "measured_rgb": "#78BFDC", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 71.76, + -15.4, + -24.59 + ], + "measured_rgb": "#69BADC", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 70.11, + -15.73, + -25.86 + ], + "measured_rgb": "#60B6DA", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 68.1, + -16.03, + -28.26 + ], + "measured_rgb": "#53B1D8", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 67.93, + -15.44, + -28.32 + ], + "measured_rgb": "#55B0D8", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 66.5, + -15.79, + -29.78 + ], + "measured_rgb": "#4AACD6", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 65.75, + -15.69, + -30.79 + ], + "measured_rgb": "#44AAD6", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 64.67, + -15.68, + -31.78 + ], + "measured_rgb": "#3DA8D5", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 62.88, + -16.04, + -34.06 + ], + "measured_rgb": "#27A3D4", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 62.86, + -15.71, + -34.68 + ], + "measured_rgb": "#26A3D5", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 62.09, + -15.68, + -35.76 + ], + "measured_rgb": "#19A1D5", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 60.73, + -15.52, + -36.7 + ], + "measured_rgb": "#009DD3", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 60.03, + -15.72, + -37.19 + ], + "measured_rgb": "#009CD1", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 65.7, + 19.56, + 48.15 + ], + "measured_rgb": "#D69148", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 63.26, + 25.13, + 42.51 + ], + "measured_rgb": "#D5864E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 60.24, + 30.06, + 32.69 + ], + "measured_rgb": "#D17B59", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 60.41, + 29.22, + 34.96 + ], + "measured_rgb": "#D17C55", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 57.97, + 35.04, + 26.17 + ], + "measured_rgb": "#CF7160", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 57.16, + 35.32, + 24.58 + ], + "measured_rgb": "#CC6F60", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 57.46, + 36.08, + 25.44 + ], + "measured_rgb": "#CE6F60", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 56.02, + 38.35, + 20.53 + ], + "measured_rgb": "#CC6965", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 55.1, + 39.69, + 16.16 + ], + "measured_rgb": "#C9666A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 54.88, + 41.31, + 15.93 + ], + "measured_rgb": "#CB646A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 53.45, + 45.1, + 7.0 + ], + "measured_rgb": "#C85D76", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 53.23, + 44.29, + 7.96 + ], + "measured_rgb": "#C75D73", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 53.17, + 45.44, + 5.5 + ], + "measured_rgb": "#C75C77", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 65.05, + 41.46, + -15.23 + ], + "measured_rgb": "#D981BA", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 64.76, + 41.42, + -14.99 + ], + "measured_rgb": "#D881B9", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 61.49, + 47.18, + -15.56 + ], + "measured_rgb": "#D773B1", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 62.17, + 44.07, + -15.56 + ], + "measured_rgb": "#D577B3", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 61.67, + 45.03, + -15.24 + ], + "measured_rgb": "#D575B1", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 60.38, + 47.58, + -15.12 + ], + "measured_rgb": "#D56FAD", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 57.67, + 51.19, + -15.52 + ], + "measured_rgb": "#D264A7", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 57.19, + 52.57, + -15.0 + ], + "measured_rgb": "#D361A5", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 57.75, + 52.16, + -14.62 + ], + "measured_rgb": "#D463A6", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 57.2, + 51.19, + -14.81 + ], + "measured_rgb": "#D163A4", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 55.57, + 53.66, + -14.82 + ], + "measured_rgb": "#D05BA0", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 55.51, + 53.08, + -14.46 + ], + "measured_rgb": "#CF5C9F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 54.62, + 54.16, + -14.51 + ], + "measured_rgb": "#CE589D", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 87.72, + -15.64, + 55.43 + ], + "measured_rgb": "#E1E26F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 87.51, + -15.48, + 58.85 + ], + "measured_rgb": "#E2E167", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 87.35, + -15.37, + 61.45 + ], + "measured_rgb": "#E3E161", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 87.0, + -14.73, + 63.73 + ], + "measured_rgb": "#E4DF5A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 86.76, + -14.25, + 65.47 + ], + "measured_rgb": "#E4DE56", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 86.6, + -13.9, + 67.58 + ], + "measured_rgb": "#E5DD50", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 86.23, + -14.03, + 72.5 + ], + "measured_rgb": "#E5DC42", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 86.91, + -12.84, + 74.11 + ], + "measured_rgb": "#EADE3F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 86.24, + -13.03, + 75.23 + ], + "measured_rgb": "#E8DC3A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 86.01, + -12.73, + 76.77 + ], + "measured_rgb": "#E8DB34", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 85.85, + -12.44, + 78.22 + ], + "measured_rgb": "#E8DA2E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 85.71, + -12.39, + 81.24 + ], + "measured_rgb": "#E9DA21", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 87.04, + -10.65, + 83.79 + ], + "measured_rgb": "#F0DC1A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 20, + 60 + ], + "measured_lab": [ + 57.59, + -7.31, + 27.59 + ], + "measured_rgb": "#8F8D5A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 25, + 55 + ], + "measured_lab": [ + 56.47, + -5.15, + 29.22 + ], + "measured_rgb": "#908954", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 30, + 50 + ], + "measured_lab": [ + 53.76, + 1.47, + 16.52 + ], + "measured_rgb": "#8E7F64", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 35, + 45 + ], + "measured_lab": [ + 53.89, + 1.26, + 22.63 + ], + "measured_rgb": "#917F5A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 40, + 40 + ], + "measured_lab": [ + 53.08, + 4.21, + 20.07 + ], + "measured_rgb": "#927B5D", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 45, + 35 + ], + "measured_lab": [ + 51.5, + 7.8, + 12.87 + ], + "measured_rgb": "#907565", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 50, + 30 + ], + "measured_lab": [ + 50.11, + 12.7, + 3.91 + ], + "measured_rgb": "#8F6F71", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 55, + 25 + ], + "measured_lab": [ + 49.99, + 13.42, + 6.09 + ], + "measured_rgb": "#916F6D", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 60, + 20 + ], + "measured_lab": [ + 49.64, + 14.45, + 6.31 + ], + "measured_rgb": "#926D6C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 20, + 55 + ], + "measured_lab": [ + 57.53, + -11.27, + 27.15 + ], + "measured_rgb": "#888F5A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 25, + 50 + ], + "measured_lab": [ + 54.99, + -7.03, + 22.56 + ], + "measured_rgb": "#86865C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 30, + 45 + ], + "measured_lab": [ + 54.25, + -4.47, + 21.25 + ], + "measured_rgb": "#88835D", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 35, + 40 + ], + "measured_lab": [ + 53.04, + -2.53, + 17.95 + ], + "measured_rgb": "#867F60", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 40, + 35 + ], + "measured_lab": [ + 51.71, + 2.72, + 11.95 + ], + "measured_rgb": "#887967", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 45, + 30 + ], + "measured_lab": [ + 50.85, + 5.89, + 10.93 + ], + "measured_rgb": "#8A7567", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 50, + 25 + ], + "measured_lab": [ + 49.74, + 8.68, + 4.66 + ], + "measured_rgb": "#88716F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 55, + 20 + ], + "measured_lab": [ + 49.3, + 9.76, + 2.9 + ], + "measured_rgb": "#886F71", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 30, + 20, + 50 + ], + "measured_lab": [ + 55.45, + -14.4, + 18.45 + ], + "measured_rgb": "#788B64", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 30, + 25, + 45 + ], + "measured_lab": [ + 55.07, + -10.24, + 24.11 + ], + "measured_rgb": "#82885A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 30, + 30, + 40 + ], + "measured_lab": [ + 53.44, + -6.68, + 18.83 + ], + "measured_rgb": "#81825F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 30, + 35, + 35 + ], + "measured_lab": [ + 52.37, + -3.05, + 13.89 + ], + "measured_rgb": "#817E65", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 30, + 40, + 30 + ], + "measured_lab": [ + 51.03, + 0.35, + 11.39 + ], + "measured_rgb": "#827966", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 30, + 45, + 25 + ], + "measured_lab": [ + 50.12, + 4.03, + 7.42 + ], + "measured_rgb": "#83756B", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 30, + 50, + 20 + ], + "measured_lab": [ + 49.36, + 7.35, + 3.42 + ], + "measured_rgb": "#847170", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 35, + 20, + 45 + ], + "measured_lab": [ + 55.52, + -16.34, + 20.5 + ], + "measured_rgb": "#758C61", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 35, + 25, + 40 + ], + "measured_lab": [ + 53.19, + -11.0, + 14.49 + ], + "measured_rgb": "#768466", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 35, + 30, + 35 + ], + "measured_lab": [ + 53.8, + -8.48, + 16.8 + ], + "measured_rgb": "#7D8463", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 35, + 35, + 30 + ], + "measured_lab": [ + 51.71, + -4.82, + 12.37 + ], + "measured_rgb": "#7C7D66", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 35, + 40, + 25 + ], + "measured_lab": [ + 49.92, + 7.98, + 2.05 + ], + "measured_rgb": "#867274", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 35, + 45, + 20 + ], + "measured_lab": [ + 48.86, + 10.46, + 0.0 + ], + "measured_rgb": "#866E74", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 40, + 20, + 40 + ], + "measured_lab": [ + 54.4, + -10.2, + 19.17 + ], + "measured_rgb": "#7D8661", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 40, + 25, + 35 + ], + "measured_lab": [ + 52.52, + -4.36, + 12.99 + ], + "measured_rgb": "#7F7F67", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 40, + 30, + 30 + ], + "measured_lab": [ + 51.21, + -1.28, + 6.49 + ], + "measured_rgb": "#7D7A6F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 40, + 35, + 25 + ], + "measured_lab": [ + 50.18, + 4.0, + 4.31 + ], + "measured_rgb": "#817570", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 40, + 40, + 20 + ], + "measured_lab": [ + 48.97, + 7.15, + 1.93 + ], + "measured_rgb": "#827071", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 45, + 20, + 35 + ], + "measured_lab": [ + 52.71, + -11.69, + 14.25 + ], + "measured_rgb": "#738365", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 45, + 25, + 30 + ], + "measured_lab": [ + 51.33, + -3.15, + 5.87 + ], + "measured_rgb": "#797C70", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 45, + 30, + 25 + ], + "measured_lab": [ + 49.87, + -1.14, + 1.5 + ], + "measured_rgb": "#767774", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 45, + 35, + 20 + ], + "measured_lab": [ + 50.92, + -2.35, + 7.7 + ], + "measured_rgb": "#7B7A6C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 50, + 20, + 30 + ], + "measured_lab": [ + 52.42, + -12.64, + 11.74 + ], + "measured_rgb": "#6E8369", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 50, + 25, + 25 + ], + "measured_lab": [ + 50.67, + -4.67, + -2.82 + ], + "measured_rgb": "#6D7B7D", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 50, + 30, + 20 + ], + "measured_lab": [ + 49.65, + -0.75, + -0.75 + ], + "measured_rgb": "#747677", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 55, + 20, + 25 + ], + "measured_lab": [ + 52.24, + -12.06, + 7.79 + ], + "measured_rgb": "#6C826F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 55, + 25, + 20 + ], + "measured_lab": [ + 50.78, + -8.82, + 4.42 + ], + "measured_rgb": "#6C7D71", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 60, + 20, + 20 + ], + "measured_lab": [ + 52.04, + -14.16, + -0.63 + ], + "measured_rgb": "#5F837D", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 20, + 60 + ], + "measured_lab": [ + 60.62, + 16.41, + -27.19 + ], + "measured_rgb": "#978BC2", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 25, + 55 + ], + "measured_lab": [ + 58.416, + 18.423, + -27.936 + ], + "measured_rgb": "#9484BD", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 30, + 50 + ], + "measured_lab": [ + 56.19, + 22.63, + -28.47 + ], + "measured_rgb": "#967BB8", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 35, + 45 + ], + "measured_lab": [ + 55.817, + 22.511, + -27.942 + ], + "measured_rgb": "#957AB6", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 40, + 40 + ], + "measured_lab": [ + 56.48, + 23.36, + -26.2 + ], + "measured_rgb": "#9A7BB5", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 45, + 35 + ], + "measured_lab": [ + 54.683, + 24.228, + -27.087 + ], + "measured_rgb": "#9676B2", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 50, + 30 + ], + "measured_lab": [ + 53.86, + 25.93, + -26.83 + ], + "measured_rgb": "#9773AF", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 55, + 25 + ], + "measured_lab": [ + 52.924, + 27.03, + -27.166 + ], + "measured_rgb": "#966FAD", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 60, + 20 + ], + "measured_lab": [ + 51.71, + 29.47, + -27.22 + ], + "measured_rgb": "#976AAA", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 20, + 55 + ], + "measured_lab": [ + 60.566, + 13.416, + -27.029 + ], + "measured_rgb": "#918CC2", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 25, + 50 + ], + "measured_lab": [ + 58.437, + 16.227, + -28.148 + ], + "measured_rgb": "#9085BE", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 30, + 45 + ], + "measured_lab": [ + 55.09, + 20.811, + -29.603 + ], + "measured_rgb": "#8E7AB7", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 35, + 40 + ], + "measured_lab": [ + 54.78, + 21.542, + -29.157 + ], + "measured_rgb": "#8F78B6", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 40, + 35 + ], + "measured_lab": [ + 54.433, + 22.913, + -28.35 + ], + "measured_rgb": "#9276B3", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 45, + 30 + ], + "measured_lab": [ + 53.707, + 23.395, + -28.23 + ], + "measured_rgb": "#9174B1", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 50, + 25 + ], + "measured_lab": [ + 53.303, + 23.898, + -28.057 + ], + "measured_rgb": "#9173B0", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 55, + 20 + ], + "measured_lab": [ + 52.606, + 25.673, + -27.911 + ], + "measured_rgb": "#9270AE", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 20, + 50 + ], + "measured_lab": [ + 62.64, + 7.61, + -25.75 + ], + "measured_rgb": "#8C95C5", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 25, + 45 + ], + "measured_lab": [ + 57.776, + 12.715, + -29.283 + ], + "measured_rgb": "#8586BE", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 30, + 40 + ], + "measured_lab": [ + 54.3, + 18.26, + -31.18 + ], + "measured_rgb": "#857AB8", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 35, + 35 + ], + "measured_lab": [ + 53.449, + 19.947, + -30.78 + ], + "measured_rgb": "#8776B5", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 40, + 30 + ], + "measured_lab": [ + 52.15, + 21.92, + -30.78 + ], + "measured_rgb": "#8772B1", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 45, + 25 + ], + "measured_lab": [ + 52.733, + 22.562, + -29.373 + ], + "measured_rgb": "#8B72B0", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 50, + 20 + ], + "measured_lab": [ + 52.34, + 22.37, + -29.11 + ], + "measured_rgb": "#8A72AF", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 20, + 45 + ], + "measured_lab": [ + 58.326, + 9.338, + -30.016 + ], + "measured_rgb": "#7E89C1", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 25, + 40 + ], + "measured_lab": [ + 56.387, + 12.275, + -30.918 + ], + "measured_rgb": "#7F83BD", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 30, + 35 + ], + "measured_lab": [ + 53.575, + 16.404, + -32.238 + ], + "measured_rgb": "#7E79B8", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 35, + 30 + ], + "measured_lab": [ + 52.46, + 18.41, + -32.043 + ], + "measured_rgb": "#7F75B4", + "source": "interpolated" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 40, + 25 + ], + "measured_lab": [ + 50.49, + 23.51, + -31.23 + ], + "measured_rgb": "#856CAE", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 45, + 20 + ], + "measured_lab": [ + 49.65, + 26.8, + -30.56 + ], + "measured_rgb": "#8A68AA", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 20, + 40 + ], + "measured_lab": [ + 55.49, + 12.01, + -31.53 + ], + "measured_rgb": "#7B81BB", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 25, + 35 + ], + "measured_lab": [ + 54.0, + 18.26, + -30.55 + ], + "measured_rgb": "#8579B6", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 30, + 30 + ], + "measured_lab": [ + 52.11, + 16.18, + -32.9 + ], + "measured_rgb": "#7976B5", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 35, + 25 + ], + "measured_lab": [ + 50.58, + 22.22, + -31.81 + ], + "measured_rgb": "#826EAF", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 40, + 20 + ], + "measured_lab": [ + 49.52, + 22.45, + -31.95 + ], + "measured_rgb": "#806BAC", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 20, + 35 + ], + "measured_lab": [ + 55.21, + 10.3, + -31.74 + ], + "measured_rgb": "#7681BB", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 25, + 30 + ], + "measured_lab": [ + 53.01, + 17.69, + -31.28 + ], + "measured_rgb": "#8077B4", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 30, + 25 + ], + "measured_lab": [ + 51.1, + 17.85, + -33.01 + ], + "measured_rgb": "#7972B2", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 35, + 20 + ], + "measured_lab": [ + 50.31, + 22.05, + -31.57 + ], + "measured_rgb": "#816DAE", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 20, + 30 + ], + "measured_lab": [ + 53.39, + 8.16, + -34.46 + ], + "measured_rgb": "#687EBB", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 25, + 25 + ], + "measured_lab": [ + 52.44, + 17.84, + -31.9 + ], + "measured_rgb": "#7E75B4", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 30, + 20 + ], + "measured_lab": [ + 50.6, + 16.89, + -33.72 + ], + "measured_rgb": "#7571B2", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 20, + 25 + ], + "measured_lab": [ + 53.48, + 9.63, + -33.29 + ], + "measured_rgb": "#6D7DB9", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 25, + 20 + ], + "measured_lab": [ + 51.33, + 13.07, + -34.41 + ], + "measured_rgb": "#6E76B5", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 20, + 20 + ], + "measured_lab": [ + 52.25, + 10.16, + -35.14 + ], + "measured_rgb": "#687AB9", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 20, + 60 + ], + "measured_lab": [ + 74.3, + -35.02, + 32.23 + ], + "measured_rgb": "#87C77A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 25, + 55 + ], + "measured_lab": [ + 73.72, + -36.25, + 35.28 + ], + "measured_rgb": "#85C572", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 30, + 50 + ], + "measured_lab": [ + 73.49, + -37.39, + 44.85 + ], + "measured_rgb": "#88C55E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 35, + 45 + ], + "measured_lab": [ + 73.18, + -37.11, + 44.81 + ], + "measured_rgb": "#88C45E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 40, + 40 + ], + "measured_lab": [ + 73.27, + -36.92, + 47.03 + ], + "measured_rgb": "#8AC459", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 45, + 35 + ], + "measured_lab": [ + 72.96, + -36.08, + 48.85 + ], + "measured_rgb": "#8CC355", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 50, + 30 + ], + "measured_lab": [ + 72.86, + -36.19, + 52.04 + ], + "measured_rgb": "#8DC24D", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 55, + 25 + ], + "measured_lab": [ + 72.69, + -36.31, + 54.47 + ], + "measured_rgb": "#8EC247", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 60, + 20 + ], + "measured_lab": [ + 73.02, + -34.99, + 57.46 + ], + "measured_rgb": "#93C241", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 20, + 55 + ], + "measured_lab": [ + 72.41, + -36.38, + 27.96 + ], + "measured_rgb": "#7BC27D", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 25, + 50 + ], + "measured_lab": [ + 71.93, + -38.31, + 37.24 + ], + "measured_rgb": "#7DC16A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 30, + 45 + ], + "measured_lab": [ + 72.08, + -39.6, + 45.03 + ], + "measured_rgb": "#7FC25A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 35, + 40 + ], + "measured_lab": [ + 71.28, + -39.24, + 43.36 + ], + "measured_rgb": "#7DC05C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 40, + 35 + ], + "measured_lab": [ + 71.07, + -38.65, + 44.35 + ], + "measured_rgb": "#7EBF59", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 45, + 30 + ], + "measured_lab": [ + 71.18, + -38.31, + 50.43 + ], + "measured_rgb": "#83BF4C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 50, + 25 + ], + "measured_lab": [ + 70.55, + -38.48, + 48.2 + ], + "measured_rgb": "#80BD50", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 55, + 20 + ], + "measured_lab": [ + 71.23, + -37.53, + 52.06 + ], + "measured_rgb": "#86BE49", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 20, + 50 + ], + "measured_lab": [ + 70.62, + -38.31, + 27.17 + ], + "measured_rgb": "#70BE7A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 25, + 45 + ], + "measured_lab": [ + 70.27, + -40.7, + 36.95 + ], + "measured_rgb": "#72BE66", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 30, + 40 + ], + "measured_lab": [ + 69.68, + -39.3, + 32.01 + ], + "measured_rgb": "#70BB6E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 35, + 35 + ], + "measured_lab": [ + 69.43, + -40.46, + 39.0 + ], + "measured_rgb": "#72BB60", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 40, + 30 + ], + "measured_lab": [ + 68.97, + -40.7, + 38.6 + ], + "measured_rgb": "#70BA5F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 45, + 25 + ], + "measured_lab": [ + 69.63, + -40.15, + 47.16 + ], + "measured_rgb": "#79BB4F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 50, + 20 + ], + "measured_lab": [ + 69.41, + -39.15, + 50.18 + ], + "measured_rgb": "#7CBA48", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 20, + 45 + ], + "measured_lab": [ + 68.36, + -39.74, + 23.12 + ], + "measured_rgb": "#62B87B", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 25, + 40 + ], + "measured_lab": [ + 68.0, + -41.34, + 28.71 + ], + "measured_rgb": "#62B870", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 30, + 35 + ], + "measured_lab": [ + 67.59, + -41.06, + 29.34 + ], + "measured_rgb": "#63B76E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 35, + 30 + ], + "measured_lab": [ + 67.99, + -40.51, + 34.43 + ], + "measured_rgb": "#6AB765", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 40, + 25 + ], + "measured_lab": [ + 68.47, + -41.01, + 39.93 + ], + "measured_rgb": "#6EB95B", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 45, + 20 + ], + "measured_lab": [ + 68.39, + -40.84, + 44.81 + ], + "measured_rgb": "#72B851", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 20, + 40 + ], + "measured_lab": [ + 68.26, + -42.84, + 32.38 + ], + "measured_rgb": "#62B96A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 25, + 35 + ], + "measured_lab": [ + 67.53, + -43.05, + 33.52 + ], + "measured_rgb": "#61B765", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 30, + 30 + ], + "measured_lab": [ + 67.97, + -43.08, + 40.92 + ], + "measured_rgb": "#68B858", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 35, + 25 + ], + "measured_lab": [ + 67.12, + -42.36, + 35.29 + ], + "measured_rgb": "#63B661", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 40, + 20 + ], + "measured_lab": [ + 67.2, + -42.55, + 42.58 + ], + "measured_rgb": "#69B653", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 20, + 35 + ], + "measured_lab": [ + 66.46, + -43.54, + 30.78 + ], + "measured_rgb": "#5AB468", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 25, + 30 + ], + "measured_lab": [ + 67.09, + -43.25, + 40.33 + ], + "measured_rgb": "#65B657", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 30, + 25 + ], + "measured_lab": [ + 66.18, + -43.88, + 36.56 + ], + "measured_rgb": "#5EB35C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 35, + 20 + ], + "measured_lab": [ + 66.19, + -43.23, + 41.16 + ], + "measured_rgb": "#63B353", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 20, + 30 + ], + "measured_lab": [ + 64.97, + -43.99, + 23.68 + ], + "measured_rgb": "#4BB172", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 25, + 25 + ], + "measured_lab": [ + 65.56, + -44.13, + 36.03 + ], + "measured_rgb": "#5BB25C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 30, + 20 + ], + "measured_lab": [ + 64.72, + -44.5, + 33.55 + ], + "measured_rgb": "#55B05E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 20, + 25 + ], + "measured_lab": [ + 64.39, + -44.62, + 30.57 + ], + "measured_rgb": "#50AF63", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 25, + 20 + ], + "measured_lab": [ + 64.28, + -44.35, + 33.12 + ], + "measured_rgb": "#54AF5E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Cyan", + "rgb": "#0086D6" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 20, + 20 + ], + "measured_lab": [ + 63.99, + -44.5, + 31.36 + ], + "measured_rgb": "#50AE61", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 20, + 60 + ], + "measured_lab": [ + 69.61, + 16.79, + 31.35 + ], + "measured_rgb": "#D99E72", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 25, + 55 + ], + "measured_lab": [ + 68.82, + 19.72, + 31.9 + ], + "measured_rgb": "#DB996F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 30, + 50 + ], + "measured_lab": [ + 68.33, + 17.1, + 39.2 + ], + "measured_rgb": "#D89A60", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 35, + 45 + ], + "measured_lab": [ + 68.46, + 16.11, + 42.41 + ], + "measured_rgb": "#D89B5A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 40, + 40 + ], + "measured_lab": [ + 68.13, + 20.07, + 35.11 + ], + "measured_rgb": "#DA9768", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 45, + 35 + ], + "measured_lab": [ + 67.98, + 17.82, + 41.09 + ], + "measured_rgb": "#D9985C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 50, + 30 + ], + "measured_lab": [ + 67.42, + 17.87, + 44.12 + ], + "measured_rgb": "#D89654", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 55, + 25 + ], + "measured_lab": [ + 67.92, + 16.3, + 49.75 + ], + "measured_rgb": "#D9994A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 60, + 20 + ], + "measured_lab": [ + 67.28, + 19.02, + 46.35 + ], + "measured_rgb": "#DA9550", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 20, + 55 + ], + "measured_lab": [ + 67.5, + 21.75, + 22.94 + ], + "measured_rgb": "#D7957C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 25, + 50 + ], + "measured_lab": [ + 67.1, + 22.38, + 25.65 + ], + "measured_rgb": "#D79376", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 30, + 45 + ], + "measured_lab": [ + 66.59, + 19.58, + 37.69 + ], + "measured_rgb": "#D6935F", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 35, + 40 + ], + "measured_lab": [ + 65.65, + 23.82, + 31.58 + ], + "measured_rgb": "#D78E68", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 40, + 35 + ], + "measured_lab": [ + 65.85, + 22.45, + 35.34 + ], + "measured_rgb": "#D78F62", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 45, + 30 + ], + "measured_lab": [ + 66.33, + 18.54, + 46.19 + ], + "measured_rgb": "#D6934E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 50, + 25 + ], + "measured_lab": [ + 66.54, + 17.79, + 48.57 + ], + "measured_rgb": "#D69449", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 55, + 20 + ], + "measured_lab": [ + 66.05, + 22.67, + 41.69 + ], + "measured_rgb": "#DA8F56", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 20, + 50 + ], + "measured_lab": [ + 65.98, + 25.27, + 25.85 + ], + "measured_rgb": "#D98E73", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 25, + 45 + ], + "measured_lab": [ + 64.68, + 23.73, + 29.99 + ], + "measured_rgb": "#D48C69", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 30, + 40 + ], + "measured_lab": [ + 65.13, + 22.9, + 33.41 + ], + "measured_rgb": "#D58D63", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 35, + 35 + ], + "measured_lab": [ + 65.09, + 25.37, + 33.28 + ], + "measured_rgb": "#D98B64", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 40, + 30 + ], + "measured_lab": [ + 63.98, + 23.77, + 34.82 + ], + "measured_rgb": "#D48A5E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 45, + 25 + ], + "measured_lab": [ + 63.93, + 23.89, + 38.63 + ], + "measured_rgb": "#D58957", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 50, + 20 + ], + "measured_lab": [ + 63.96, + 22.94, + 39.59 + ], + "measured_rgb": "#D48A55", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 20, + 45 + ], + "measured_lab": [ + 64.24, + 30.82, + 10.43 + ], + "measured_rgb": "#D5868B", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 25, + 40 + ], + "measured_lab": [ + 63.91, + 27.46, + 22.59 + ], + "measured_rgb": "#D48774", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 30, + 35 + ], + "measured_lab": [ + 62.99, + 27.33, + 27.98 + ], + "measured_rgb": "#D38568", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 35, + 30 + ], + "measured_lab": [ + 63.4, + 25.17, + 33.91 + ], + "measured_rgb": "#D4875E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 40, + 25 + ], + "measured_lab": [ + 61.18, + 29.42, + 27.72 + ], + "measured_rgb": "#D17E64", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 45, + 20 + ], + "measured_lab": [ + 59.89, + 31.27, + 25.11 + ], + "measured_rgb": "#CF7966", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 20, + 40 + ], + "measured_lab": [ + 59.5, + 33.72, + 17.49 + ], + "measured_rgb": "#CF7772", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 25, + 35 + ], + "measured_lab": [ + 59.49, + 33.87, + 17.34 + ], + "measured_rgb": "#CF7773", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 30, + 30 + ], + "measured_lab": [ + 59.88, + 32.77, + 22.0 + ], + "measured_rgb": "#D0786B", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 35, + 25 + ], + "measured_lab": [ + 58.58, + 33.89, + 21.05 + ], + "measured_rgb": "#CD746A", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 40, + 20 + ], + "measured_lab": [ + 59.5, + 31.18, + 26.51 + ], + "measured_rgb": "#CE7862", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 20, + 35 + ], + "measured_lab": [ + 58.96, + 35.14, + 15.3 + ], + "measured_rgb": "#CE7475", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 25, + 30 + ], + "measured_lab": [ + 58.15, + 35.31, + 18.11 + ], + "measured_rgb": "#CD726E", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 30, + 25 + ], + "measured_lab": [ + 58.1, + 36.43, + 15.98 + ], + "measured_rgb": "#CE7172", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 35, + 20 + ], + "measured_lab": [ + 57.38, + 34.94, + 21.31 + ], + "measured_rgb": "#CB7066", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 20, + 30 + ], + "measured_lab": [ + 57.08, + 37.78, + 13.15 + ], + "measured_rgb": "#CB6D74", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 25, + 25 + ], + "measured_lab": [ + 56.95, + 38.09, + 14.5 + ], + "measured_rgb": "#CC6D71", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 30, + 20 + ], + "measured_lab": [ + 57.03, + 37.09, + 17.89 + ], + "measured_rgb": "#CC6D6C", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 20, + 25 + ], + "measured_lab": [ + 56.59, + 40.06, + 11.45 + ], + "measured_rgb": "#CC6A76", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 25, + 20 + ], + "measured_lab": [ + 56.35, + 39.5, + 14.12 + ], + "measured_rgb": "#CC6A71", + "source": "measured" + }, + { + "mode": "CMYW", + "material": "PLA Basic", + "components": [ + { + "key": "Magenta", + "rgb": "#EC008C" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 20, + 20 + ], + "measured_lab": [ + 56.79, + 44.06, + 0.12 + ], + "measured_rgb": "#CE688A", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 60.61, + 34.14, + 50.34 + ], + "measured_rgb": "#DB7839", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 57.92, + 38.03, + 46.93 + ], + "measured_rgb": "#D86D39", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 54.45, + 45.01, + 43.39 + ], + "measured_rgb": "#D55D39", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 52.18, + 47.64, + 41.46 + ], + "measured_rgb": "#D15537", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 54.08, + 42.25, + 42.36 + ], + "measured_rgb": "#D05F3A", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 51.19, + 47.93, + 39.54 + ], + "measured_rgb": "#CE5239", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 49.49, + 50.76, + 38.06 + ], + "measured_rgb": "#CC4A38", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 50.15, + 48.98, + 38.57 + ], + "measured_rgb": "#CC4E38", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 49.79, + 47.23, + 37.84 + ], + "measured_rgb": "#C94F39", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 46.81, + 51.92, + 34.7 + ], + "measured_rgb": "#C54138", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 46.58, + 51.44, + 34.56 + ], + "measured_rgb": "#C34137", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 46.73, + 51.61, + 33.68 + ], + "measured_rgb": "#C44139", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 46.44, + 51.41, + 32.33 + ], + "measured_rgb": "#C3413B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 28.39, + 4.73, + -17.78 + ], + "measured_rgb": "#3A425E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 27.99, + 5.7, + -13.75 + ], + "measured_rgb": "#404057", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 28.01, + 5.88, + -12.79 + ], + "measured_rgb": "#414056", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 28.36, + 7.17, + -7.97 + ], + "measured_rgb": "#48404F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 28.46, + 8.34, + -6.09 + ], + "measured_rgb": "#4C3F4D", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 29.18, + 7.99, + -6.06 + ], + "measured_rgb": "#4D414E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 29.47, + 9.68, + -3.27 + ], + "measured_rgb": "#52404B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 29.14, + 11.96, + -0.91 + ], + "measured_rgb": "#563E46", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 29.79, + 10.84, + -2.53 + ], + "measured_rgb": "#55404A", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 29.84, + 13.65, + 0.54 + ], + "measured_rgb": "#5B3F46", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 30.46, + 16.09, + 2.93 + ], + "measured_rgb": "#613E44", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 31.4, + 20.19, + 7.06 + ], + "measured_rgb": "#6B3D40", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 32.0, + 21.78, + 8.02 + ], + "measured_rgb": "#6E3D40", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 61.58, + 37.37, + 13.91 + ], + "measured_rgb": "#D8797E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 60.1, + 40.18, + 16.87 + ], + "measured_rgb": "#D97375", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 59.12, + 40.19, + 17.09 + ], + "measured_rgb": "#D67072", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 56.42, + 42.46, + 18.74 + ], + "measured_rgb": "#D26769", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 54.17, + 44.16, + 20.38 + ], + "measured_rgb": "#CE5F61", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 53.53, + 44.14, + 20.39 + ], + "measured_rgb": "#CC5D5F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 55.16, + 41.55, + 17.38 + ], + "measured_rgb": "#CC6468", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 52.54, + 43.49, + 19.07 + ], + "measured_rgb": "#C85B5F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 52.43, + 43.2, + 19.01 + ], + "measured_rgb": "#C75B5F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 49.22, + 47.26, + 24.6 + ], + "measured_rgb": "#C44E4E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 48.94, + 47.01, + 23.99 + ], + "measured_rgb": "#C34E4E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 48.65, + 46.66, + 23.5 + ], + "measured_rgb": "#C14D4F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 46.24, + 48.73, + 26.28 + ], + "measured_rgb": "#BD4444", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 35.03, + -18.52, + -8.74 + ], + "measured_rgb": "#185B60", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 34.35, + -18.03, + -10.06 + ], + "measured_rgb": "#145960", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 36.58, + -19.91, + -5.37 + ], + "measured_rgb": "#205F5E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 40.4, + -23.93, + 5.19 + ], + "measured_rgb": "#306956", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 40.47, + -25.72, + 6.68 + ], + "measured_rgb": "#2D6A54", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 42.28, + -26.69, + 10.44 + ], + "measured_rgb": "#346F52", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 42.84, + -27.22, + 11.67 + ], + "measured_rgb": "#357051", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 45.25, + -27.6, + 15.74 + ], + "measured_rgb": "#3F7750", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 48.65, + -28.39, + 22.55 + ], + "measured_rgb": "#4C7F4C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 50.85, + -29.47, + 27.4 + ], + "measured_rgb": "#538549", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 54.25, + -29.09, + 32.92 + ], + "measured_rgb": "#608E46", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 49.95, + -30.96, + 23.7 + ], + "measured_rgb": "#4A844D", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 59.02, + -29.15, + 40.67 + ], + "measured_rgb": "#719A43", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 86.91, + -14.57, + 48.46 + ], + "measured_rgb": "#DDDF7B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 86.88, + -15.04, + 54.18 + ], + "measured_rgb": "#DFDF6F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 86.41, + -15.7, + 60.31 + ], + "measured_rgb": "#DFDE61", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 86.06, + -14.91, + 59.87 + ], + "measured_rgb": "#DFDD61", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 85.89, + -14.7, + 63.29 + ], + "measured_rgb": "#E0DC58", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 86.91, + -13.69, + 66.56 + ], + "measured_rgb": "#E6DE53", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 85.49, + -13.71, + 65.56 + ], + "measured_rgb": "#E1DA52", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 86.77, + -12.65, + 71.25 + ], + "measured_rgb": "#E9DD47", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 85.39, + -13.63, + 72.72 + ], + "measured_rgb": "#E3DA3F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 85.21, + -13.5, + 76.71 + ], + "measured_rgb": "#E4D931", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 86.67, + -11.8, + 76.43 + ], + "measured_rgb": "#EBDC37", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 85.49, + -11.97, + 77.96 + ], + "measured_rgb": "#E8D92E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 85.33, + -12.02, + 80.1 + ], + "measured_rgb": "#E8D925", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 80 + ], + "measured_lab": [ + 59.26, + -2.93, + -35.61 + ], + "measured_rgb": "#5593CD", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 75 + ], + "measured_lab": [ + 59.41, + -2.35, + -34.23 + ], + "measured_rgb": "#5B93CB", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 70 + ], + "measured_lab": [ + 53.09, + -0.39, + -40.48 + ], + "measured_rgb": "#3E83C4", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 65 + ], + "measured_lab": [ + 52.81, + 0.36, + -39.81 + ], + "measured_rgb": "#4281C2", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 60 + ], + "measured_lab": [ + 48.06, + 2.47, + -43.46 + ], + "measured_rgb": "#2D75BB", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 55 + ], + "measured_lab": [ + 46.13, + 3.72, + -44.68 + ], + "measured_rgb": "#2670B8", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 50 + ], + "measured_lab": [ + 46.04, + 3.98, + -43.55 + ], + "measured_rgb": "#2D6FB6", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 45 + ], + "measured_lab": [ + 44.42, + 5.04, + -44.41 + ], + "measured_rgb": "#286BB3", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 40 + ], + "measured_lab": [ + 44.98, + 5.1, + -42.4 + ], + "measured_rgb": "#336CB1", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 65, + 35 + ], + "measured_lab": [ + 42.2, + 6.27, + -44.38 + ], + "measured_rgb": "#2664AD", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 70, + 30 + ], + "measured_lab": [ + 39.63, + 8.04, + -46.0 + ], + "measured_rgb": "#1C5EA9", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 75, + 25 + ], + "measured_lab": [ + 38.59, + 8.36, + -46.18 + ], + "measured_rgb": "#175BA6", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 80, + 20 + ], + "measured_lab": [ + 37.5, + 9.22, + -46.13 + ], + "measured_rgb": "#1858A3", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 20, + 60 + ], + "measured_lab": [ + 32.23, + -3.53, + -0.01 + ], + "measured_rgb": "#464E4C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 25, + 55 + ], + "measured_lab": [ + 32.81, + -4.24, + 0.56 + ], + "measured_rgb": "#464F4C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 30, + 50 + ], + "measured_lab": [ + 34.15, + -4.49, + 3.87 + ], + "measured_rgb": "#4B524A", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 35, + 45 + ], + "measured_lab": [ + 37.35, + -6.99, + 11.16 + ], + "measured_rgb": "#545B46", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 40, + 40 + ], + "measured_lab": [ + 36.99, + -4.61, + 9.94 + ], + "measured_rgb": "#565947", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 45, + 35 + ], + "measured_lab": [ + 37.25, + -1.03, + 10.47 + ], + "measured_rgb": "#5D5847", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 50, + 30 + ], + "measured_lab": [ + 42.82, + -4.42, + 20.44 + ], + "measured_rgb": "#6A6643", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 55, + 25 + ], + "measured_lab": [ + 44.71, + -2.87, + 24.85 + ], + "measured_rgb": "#736A40", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 20, + 60, + 20 + ], + "measured_lab": [ + 46.34, + -1.41, + 27.3 + ], + "measured_rgb": "#7B6D40", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 25, + 20, + 55 + ], + "measured_lab": [ + 30.83, + -1.07, + -2.8 + ], + "measured_rgb": "#45494D", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 25, + 25, + 50 + ], + "measured_lab": [ + 32.93, + -0.38, + 3.48 + ], + "measured_rgb": "#4F4D48", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 25, + 30, + 45 + ], + "measured_lab": [ + 34.58, + -2.45, + 6.26 + ], + "measured_rgb": "#525247", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 25, + 35, + 40 + ], + "measured_lab": [ + 36.48, + -2.69, + 10.74 + ], + "measured_rgb": "#585745", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 25, + 40, + 35 + ], + "measured_lab": [ + 36.81, + 0.18, + 11.24 + ], + "measured_rgb": "#5E5645", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 25, + 45, + 30 + ], + "measured_lab": [ + 40.56, + -1.97, + 18.21 + ], + "measured_rgb": "#676042", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 25, + 50, + 25 + ], + "measured_lab": [ + 44.1, + -1.72, + 24.33 + ], + "measured_rgb": "#736840", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 25, + 55, + 20 + ], + "measured_lab": [ + 45.28, + 1.15, + 26.65 + ], + "measured_rgb": "#7C693E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 30, + 20, + 50 + ], + "measured_lab": [ + 31.65, + 0.49, + 0.75 + ], + "measured_rgb": "#4C4A49", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 30, + 25, + 45 + ], + "measured_lab": [ + 34.96, + -1.74, + 8.49 + ], + "measured_rgb": "#555345", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 30, + 30, + 40 + ], + "measured_lab": [ + 35.0, + -0.46, + 8.13 + ], + "measured_rgb": "#575245", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 30, + 35, + 35 + ], + "measured_lab": [ + 35.49, + 2.18, + 9.86 + ], + "measured_rgb": "#5D5244", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 30, + 40, + 30 + ], + "measured_lab": [ + 37.72, + 0.55, + 13.56 + ], + "measured_rgb": "#625843", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 30, + 45, + 25 + ], + "measured_lab": [ + 43.18, + 0.87, + 23.69 + ], + "measured_rgb": "#75643F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 30, + 50, + 20 + ], + "measured_lab": [ + 43.6, + 2.7, + 23.97 + ], + "measured_rgb": "#78643F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 35, + 20, + 45 + ], + "measured_lab": [ + 33.04, + 2.13, + 3.9 + ], + "measured_rgb": "#544C48", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 35, + 25, + 40 + ], + "measured_lab": [ + 33.94, + 2.25, + 7.42 + ], + "measured_rgb": "#584E44", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 35, + 30, + 35 + ], + "measured_lab": [ + 33.08, + 7.54, + 8.06 + ], + "measured_rgb": "#5E4941", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 35, + 35, + 30 + ], + "measured_lab": [ + 39.5, + -1.02, + 16.66 + ], + "measured_rgb": "#655D42", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 35, + 40, + 25 + ], + "measured_lab": [ + 40.42, + 3.78, + 19.13 + ], + "measured_rgb": "#705C40", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 35, + 45, + 20 + ], + "measured_lab": [ + 42.47, + 7.35, + 22.43 + ], + "measured_rgb": "#7C5F40", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 40, + 20, + 40 + ], + "measured_lab": [ + 32.03, + 5.65, + 4.57 + ], + "measured_rgb": "#574844", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 40, + 25, + 35 + ], + "measured_lab": [ + 32.62, + 7.5, + 6.94 + ], + "measured_rgb": "#5C4842", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 40, + 30, + 30 + ], + "measured_lab": [ + 33.68, + 8.82, + 9.29 + ], + "measured_rgb": "#624A41", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 40, + 35, + 25 + ], + "measured_lab": [ + 38.68, + 6.76, + 16.51 + ], + "measured_rgb": "#6F5641", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 40, + 40, + 20 + ], + "measured_lab": [ + 38.56, + 10.09, + 17.18 + ], + "measured_rgb": "#73543F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 45, + 20, + 35 + ], + "measured_lab": [ + 33.4, + 5.95, + 7.54 + ], + "measured_rgb": "#5C4B43", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 45, + 25, + 30 + ], + "measured_lab": [ + 33.27, + 11.92, + 8.78 + ], + "measured_rgb": "#654741", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 45, + 30, + 25 + ], + "measured_lab": [ + 39.06, + 7.04, + 17.32 + ], + "measured_rgb": "#705740", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 45, + 35, + 20 + ], + "measured_lab": [ + 38.92, + 11.63, + 18.79 + ], + "measured_rgb": "#77543E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 50, + 20, + 30 + ], + "measured_lab": [ + 32.56, + 13.56, + 9.0 + ], + "measured_rgb": "#66443F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 50, + 25, + 25 + ], + "measured_lab": [ + 33.92, + 11.71, + 10.22 + ], + "measured_rgb": "#674940", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 50, + 30, + 20 + ], + "measured_lab": [ + 35.68, + 13.35, + 13.16 + ], + "measured_rgb": "#6F4C40", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 55, + 20, + 25 + ], + "measured_lab": [ + 32.37, + 16.51, + 9.16 + ], + "measured_rgb": "#69423E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 55, + 25, + 20 + ], + "measured_lab": [ + 38.21, + 13.95, + 17.68 + ], + "measured_rgb": "#78513E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + } + ], + "ratios": [ + 60, + 20, + 20 + ], + "measured_lab": [ + 35.06, + 16.29, + 13.06 + ], + "measured_rgb": "#71483E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 20, + 60 + ], + "measured_lab": [ + 60.59, + 34.71, + 26.62 + ], + "measured_rgb": "#D67865", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 25, + 55 + ], + "measured_lab": [ + 59.65, + 36.54, + 32.53 + ], + "measured_rgb": "#D87458", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 30, + 50 + ], + "measured_lab": [ + 60.45, + 34.97, + 33.46 + ], + "measured_rgb": "#D87759", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 35, + 45 + ], + "measured_lab": [ + 60.48, + 35.03, + 35.45 + ], + "measured_rgb": "#D97755", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 40, + 40 + ], + "measured_lab": [ + 60.74, + 35.59, + 37.63 + ], + "measured_rgb": "#DB7752", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 45, + 35 + ], + "measured_lab": [ + 60.84, + 33.42, + 34.85 + ], + "measured_rgb": "#D87A57", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 50, + 30 + ], + "measured_lab": [ + 59.98, + 34.25, + 42.34 + ], + "measured_rgb": "#D87647", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 55, + 25 + ], + "measured_lab": [ + 59.5, + 34.06, + 45.9 + ], + "measured_rgb": "#D7753F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 60, + 20 + ], + "measured_lab": [ + 60.73, + 32.52, + 47.98 + ], + "measured_rgb": "#D97A3E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 20, + 55 + ], + "measured_lab": [ + 57.33, + 40.74, + 32.02 + ], + "measured_rgb": "#D66A54", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 25, + 50 + ], + "measured_lab": [ + 57.86, + 38.74, + 32.01 + ], + "measured_rgb": "#D56D55", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 30, + 45 + ], + "measured_lab": [ + 59.26, + 36.69, + 29.23 + ], + "measured_rgb": "#D6735D", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 35, + 40 + ], + "measured_lab": [ + 57.69, + 38.61, + 37.53 + ], + "measured_rgb": "#D66D4B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 40, + 35 + ], + "measured_lab": [ + 58.84, + 37.53, + 35.54 + ], + "measured_rgb": "#D77151", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 45, + 30 + ], + "measured_lab": [ + 58.26, + 36.31, + 36.61 + ], + "measured_rgb": "#D4704E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 50, + 25 + ], + "measured_lab": [ + 58.63, + 36.73, + 43.16 + ], + "measured_rgb": "#D77142", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 55, + 20 + ], + "measured_lab": [ + 58.5, + 37.1, + 43.64 + ], + "measured_rgb": "#D87041", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 20, + 50 + ], + "measured_lab": [ + 56.37, + 41.44, + 30.12 + ], + "measured_rgb": "#D46755", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 25, + 45 + ], + "measured_lab": [ + 55.61, + 41.45, + 34.19 + ], + "measured_rgb": "#D2654C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 30, + 40 + ], + "measured_lab": [ + 55.53, + 41.59, + 37.12 + ], + "measured_rgb": "#D36447", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 35, + 35 + ], + "measured_lab": [ + 56.91, + 39.02, + 32.21 + ], + "measured_rgb": "#D36A53", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 40, + 30 + ], + "measured_lab": [ + 57.16, + 38.42, + 33.48 + ], + "measured_rgb": "#D36C51", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 45, + 25 + ], + "measured_lab": [ + 56.26, + 39.43, + 40.55 + ], + "measured_rgb": "#D36842", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 50, + 20 + ], + "measured_lab": [ + 56.61, + 38.72, + 43.85 + ], + "measured_rgb": "#D4693C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 20, + 45 + ], + "measured_lab": [ + 54.0, + 44.02, + 32.55 + ], + "measured_rgb": "#D05E4B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 25, + 40 + ], + "measured_lab": [ + 53.03, + 44.01, + 34.64 + ], + "measured_rgb": "#CE5B45", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 30, + 35 + ], + "measured_lab": [ + 53.36, + 44.2, + 37.81 + ], + "measured_rgb": "#D05C41", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 35, + 30 + ], + "measured_lab": [ + 55.03, + 40.44, + 32.66 + ], + "measured_rgb": "#CF644D", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 40, + 25 + ], + "measured_lab": [ + 54.828, + 42.215, + 34.274 + ], + "measured_rgb": "#D1624A", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 45, + 20 + ], + "measured_lab": [ + 55.474, + 41.194, + 34.226 + ], + "measured_rgb": "#D2654C", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 20, + 40 + ], + "measured_lab": [ + 52.99, + 45.97, + 30.69 + ], + "measured_rgb": "#CF594C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 25, + 35 + ], + "measured_lab": [ + 54.026, + 44.398, + 31.429 + ], + "measured_rgb": "#D15E4D", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 30, + 30 + ], + "measured_lab": [ + 53.48, + 44.5, + 33.71 + ], + "measured_rgb": "#D05C48", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 35, + 25 + ], + "measured_lab": [ + 53.875, + 43.798, + 34.292 + ], + "measured_rgb": "#D05E48", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 40, + 20 + ], + "measured_lab": [ + 53.05, + 44.39, + 35.65 + ], + "measured_rgb": "#CF5B44", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 20, + 35 + ], + "measured_lab": [ + 52.053, + 47.001, + 31.923 + ], + "measured_rgb": "#CE5548", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 25, + 30 + ], + "measured_lab": [ + 51.938, + 46.902, + 33.508 + ], + "measured_rgb": "#CE5545", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 30, + 25 + ], + "measured_lab": [ + 51.974, + 46.493, + 35.433 + ], + "measured_rgb": "#CE5642", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 35, + 20 + ], + "measured_lab": [ + 52.243, + 45.967, + 35.487 + ], + "measured_rgb": "#CE5742", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 20, + 30 + ], + "measured_lab": [ + 51.23, + 48.13, + 31.57 + ], + "measured_rgb": "#CD5247", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 25, + 25 + ], + "measured_lab": [ + 51.072, + 48.014, + 34.379 + ], + "measured_rgb": "#CD5242", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 30, + 20 + ], + "measured_lab": [ + 50.05, + 49.01, + 38.06 + ], + "measured_rgb": "#CC4D39", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 20, + 25 + ], + "measured_lab": [ + 50.014, + 48.998, + 33.907 + ], + "measured_rgb": "#CB4E40", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 25, + 20 + ], + "measured_lab": [ + 50.106, + 48.917, + 34.856 + ], + "measured_rgb": "#CB4E3F", + "source": "interpolated" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 20, + 20 + ], + "measured_lab": [ + 48.46, + 50.2, + 35.77 + ], + "measured_rgb": "#C84839", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 20, + 60 + ], + "measured_lab": [ + 43.95, + 12.77, + -5.1 + ], + "measured_rgb": "#796171", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 25, + 55 + ], + "measured_lab": [ + 42.04, + 9.64, + -8.16 + ], + "measured_rgb": "#6D5E71", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 30, + 50 + ], + "measured_lab": [ + 41.99, + 8.57, + -9.11 + ], + "measured_rgb": "#6B5F72", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 35, + 45 + ], + "measured_lab": [ + 37.82, + 7.98, + -12.1 + ], + "measured_rgb": "#5D566D", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 40, + 40 + ], + "measured_lab": [ + 37.04, + 10.17, + -6.18 + ], + "measured_rgb": "#635261", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 45, + 35 + ], + "measured_lab": [ + 35.88, + 6.85, + -11.4 + ], + "measured_rgb": "#575267", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 50, + 30 + ], + "measured_lab": [ + 34.82, + 8.38, + -8.59 + ], + "measured_rgb": "#594E60", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 55, + 25 + ], + "measured_lab": [ + 33.2, + 9.06, + -8.11 + ], + "measured_rgb": "#574A5B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 60, + 20 + ], + "measured_lab": [ + 32.07, + 8.43, + -9.28 + ], + "measured_rgb": "#52485A", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 20, + 55 + ], + "measured_lab": [ + 43.13, + 13.68, + -2.83 + ], + "measured_rgb": "#7A5E6B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 25, + 50 + ], + "measured_lab": [ + 40.14, + 12.29, + -4.61 + ], + "measured_rgb": "#6F5866", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 30, + 45 + ], + "measured_lab": [ + 37.63, + 10.55, + -7.27 + ], + "measured_rgb": "#645364", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 35, + 40 + ], + "measured_lab": [ + 38.38, + 9.16, + -8.16 + ], + "measured_rgb": "#635668", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 40, + 35 + ], + "measured_lab": [ + 35.95, + 10.58, + -5.28 + ], + "measured_rgb": "#624F5D", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 45, + 30 + ], + "measured_lab": [ + 34.42, + 9.15, + -7.21 + ], + "measured_rgb": "#5A4C5C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 50, + 25 + ], + "measured_lab": [ + 34.05, + 8.82, + -7.67 + ], + "measured_rgb": "#594C5C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 55, + 20 + ], + "measured_lab": [ + 31.57, + 8.0, + -9.3 + ], + "measured_rgb": "#504759", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 20, + 50 + ], + "measured_lab": [ + 42.67, + 15.13, + -1.45 + ], + "measured_rgb": "#7C5C68", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 25, + 45 + ], + "measured_lab": [ + 38.88, + 13.65, + -2.95 + ], + "measured_rgb": "#6F5461", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 30, + 40 + ], + "measured_lab": [ + 36.38, + 12.23, + -5.02 + ], + "measured_rgb": "#664F5E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 35, + 35 + ], + "measured_lab": [ + 36.56, + 10.6, + -6.32 + ], + "measured_rgb": "#635160", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 40, + 30 + ], + "measured_lab": [ + 33.78, + 10.4, + -5.72 + ], + "measured_rgb": "#5C4A59", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 45, + 25 + ], + "measured_lab": [ + 33.68, + 9.66, + -5.71 + ], + "measured_rgb": "#5B4A58", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 50, + 20 + ], + "measured_lab": [ + 31.46, + 8.64, + -7.9 + ], + "measured_rgb": "#524656", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 20, + 45 + ], + "measured_lab": [ + 37.64, + 16.45, + -0.45 + ], + "measured_rgb": "#724F5A", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 25, + 40 + ], + "measured_lab": [ + 35.97, + 14.23, + -2.54 + ], + "measured_rgb": "#694D59", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 30, + 35 + ], + "measured_lab": [ + 33.12, + 14.21, + -3.07 + ], + "measured_rgb": "#624653", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 35, + 30 + ], + "measured_lab": [ + 34.92, + 11.2, + -4.87 + ], + "measured_rgb": "#614C5A", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 40, + 25 + ], + "measured_lab": [ + 32.77, + 11.87, + -3.19 + ], + "measured_rgb": "#5D4752", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 45, + 20 + ], + "measured_lab": [ + 30.53, + 13.08, + -1.32 + ], + "measured_rgb": "#5B414A", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 20, + 40 + ], + "measured_lab": [ + 39.18, + 16.47, + 0.67 + ], + "measured_rgb": "#76535C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 25, + 35 + ], + "measured_lab": [ + 33.52, + 16.44, + 0.11 + ], + "measured_rgb": "#68454F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 30, + 30 + ], + "measured_lab": [ + 33.8, + 14.01, + -1.98 + ], + "measured_rgb": "#644853", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 35, + 25 + ], + "measured_lab": [ + 32.67, + 16.81, + 2.7 + ], + "measured_rgb": "#674349", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 40, + 20 + ], + "measured_lab": [ + 31.2, + 13.76, + -0.59 + ], + "measured_rgb": "#5E424B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 20, + 35 + ], + "measured_lab": [ + 37.08, + 19.55, + 3.74 + ], + "measured_rgb": "#774B52", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 25, + 30 + ], + "measured_lab": [ + 32.33, + 16.27, + -0.27 + ], + "measured_rgb": "#64434D", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 30, + 25 + ], + "measured_lab": [ + 32.15, + 14.65, + -1.47 + ], + "measured_rgb": "#61434E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 35, + 20 + ], + "measured_lab": [ + 31.51, + 13.89, + -1.21 + ], + "measured_rgb": "#5E424C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 20, + 30 + ], + "measured_lab": [ + 36.64, + 20.21, + 4.79 + ], + "measured_rgb": "#774A4F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 25, + 25 + ], + "measured_lab": [ + 33.48, + 17.53, + 2.16 + ], + "measured_rgb": "#6A444C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 30, + 20 + ], + "measured_lab": [ + 32.33, + 15.16, + 0.23 + ], + "measured_rgb": "#63434C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 20, + 25 + ], + "measured_lab": [ + 34.44, + 20.68, + 5.02 + ], + "measured_rgb": "#72444A", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 25, + 20 + ], + "measured_lab": [ + 31.82, + 18.04, + 1.96 + ], + "measured_rgb": "#674048", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Red", + "rgb": "#C12E1F" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 20, + 20 + ], + "measured_lab": [ + 33.8, + 20.81, + 5.55 + ], + "measured_rgb": "#714248", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 20, + 60 + ], + "measured_lab": [ + 61.95, + -24.93, + 11.39 + ], + "measured_rgb": "#6CA181", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 25, + 55 + ], + "measured_lab": [ + 58.64, + -24.28, + 6.71 + ], + "measured_rgb": "#609881", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 30, + 50 + ], + "measured_lab": [ + 53.18, + -26.38, + 5.24 + ], + "measured_rgb": "#4A8B75", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 35, + 45 + ], + "measured_lab": [ + 48.85, + -26.53, + 1.34 + ], + "measured_rgb": "#388071", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 40, + 40 + ], + "measured_lab": [ + 47.76, + -25.67, + 0.94 + ], + "measured_rgb": "#377D6F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 45, + 35 + ], + "measured_lab": [ + 47.71, + -23.33, + -0.88 + ], + "measured_rgb": "#3B7C72", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 50, + 30 + ], + "measured_lab": [ + 45.3, + -23.42, + -3.16 + ], + "measured_rgb": "#307670", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 55, + 25 + ], + "measured_lab": [ + 42.32, + -22.68, + -5.37 + ], + "measured_rgb": "#256E6C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 20, + 60, + 20 + ], + "measured_lab": [ + 40.66, + -22.75, + -6.54 + ], + "measured_rgb": "#1C6A6A", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 20, + 55 + ], + "measured_lab": [ + 58.19, + -28.22, + 13.86 + ], + "measured_rgb": "#5C9973", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 25, + 50 + ], + "measured_lab": [ + 57.54, + -26.75, + 11.39 + ], + "measured_rgb": "#5C9675", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 30, + 45 + ], + "measured_lab": [ + 54.04, + -25.84, + 7.96 + ], + "measured_rgb": "#518D73", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 35, + 40 + ], + "measured_lab": [ + 49.15, + -27.67, + 5.27 + ], + "measured_rgb": "#3B816B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 40, + 35 + ], + "measured_lab": [ + 50.78, + -24.99, + 5.57 + ], + "measured_rgb": "#48846F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 45, + 30 + ], + "measured_lab": [ + 46.19, + -27.43, + 6.2 + ], + "measured_rgb": "#367962", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 50, + 25 + ], + "measured_lab": [ + 44.25, + -26.25, + 2.19 + ], + "measured_rgb": "#2D7464", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 25, + 55, + 20 + ], + "measured_lab": [ + 43.05, + -25.29, + -0.35 + ], + "measured_rgb": "#297166", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 20, + 50 + ], + "measured_lab": [ + 60.51, + -27.72, + 13.78 + ], + "measured_rgb": "#649F79", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 25, + 45 + ], + "measured_lab": [ + 56.39, + -27.38, + 13.6 + ], + "measured_rgb": "#5A936F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 30, + 40 + ], + "measured_lab": [ + 55.04, + -26.57, + 11.27 + ], + "measured_rgb": "#56906F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 35, + 35 + ], + "measured_lab": [ + 50.22, + -27.61, + 7.99 + ], + "measured_rgb": "#428469", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 40, + 30 + ], + "measured_lab": [ + 48.43, + -28.67, + 10.95 + ], + "measured_rgb": "#3F7F60", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 45, + 25 + ], + "measured_lab": [ + 46.49, + -27.7, + 8.65 + ], + "measured_rgb": "#397A5F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 30, + 50, + 20 + ], + "measured_lab": [ + 45.03, + -26.36, + 4.91 + ], + "measured_rgb": "#347662", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 20, + 45 + ], + "measured_lab": [ + 60.7, + -28.15, + 21.36 + ], + "measured_rgb": "#6A9F6C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 25, + 40 + ], + "measured_lab": [ + 58.01, + -27.55, + 16.4 + ], + "measured_rgb": "#60986E", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 30, + 35 + ], + "measured_lab": [ + 53.3, + -28.62, + 14.7 + ], + "measured_rgb": "#508C65", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 35, + 30 + ], + "measured_lab": [ + 51.14, + -28.08, + 12.6 + ], + "measured_rgb": "#498663", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 40, + 25 + ], + "measured_lab": [ + 48.27, + -28.82, + 13.08 + ], + "measured_rgb": "#407F5C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 35, + 45, + 20 + ], + "measured_lab": [ + 44.97, + -27.91, + 6.3 + ], + "measured_rgb": "#31765F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 20, + 40 + ], + "measured_lab": [ + 59.26, + -29.58, + 23.49 + ], + "measured_rgb": "#659C64", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 25, + 35 + ], + "measured_lab": [ + 55.03, + -29.9, + 20.19 + ], + "measured_rgb": "#569160", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 30, + 30 + ], + "measured_lab": [ + 52.18, + -28.85, + 16.83 + ], + "measured_rgb": "#4F895F", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 35, + 25 + ], + "measured_lab": [ + 51.03, + -30.5, + 17.18 + ], + "measured_rgb": "#48865B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 40, + 40, + 20 + ], + "measured_lab": [ + 50.23, + -27.11, + 13.19 + ], + "measured_rgb": "#4A8360", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 20, + 35 + ], + "measured_lab": [ + 59.6, + -28.48, + 25.96 + ], + "measured_rgb": "#6A9C61", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 25, + 30 + ], + "measured_lab": [ + 56.1, + -28.9, + 21.55 + ], + "measured_rgb": "#5D9360", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 30, + 25 + ], + "measured_lab": [ + 52.36, + -29.7, + 18.82 + ], + "measured_rgb": "#4F8A5C", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 45, + 35, + 20 + ], + "measured_lab": [ + 50.32, + -30.46, + 18.03 + ], + "measured_rgb": "#478558", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 20, + 30 + ], + "measured_lab": [ + 58.9, + -29.55, + 28.26 + ], + "measured_rgb": "#689A5B", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 25, + 25 + ], + "measured_lab": [ + 55.52, + -29.56, + 25.37 + ], + "measured_rgb": "#5D9258", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 50, + 30, + 20 + ], + "measured_lab": [ + 51.61, + -30.92, + 22.57 + ], + "measured_rgb": "#4D8853", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 20, + 25 + ], + "measured_lab": [ + 57.98, + -30.58, + 31.16 + ], + "measured_rgb": "#659853", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 55, + 25, + 20 + ], + "measured_lab": [ + 55.14, + -30.46, + 27.15 + ], + "measured_rgb": "#5B9153", + "source": "measured" + }, + { + "mode": "RYBW", + "material": "PLA Basic", + "components": [ + { + "key": "Yellow", + "rgb": "#F4EE2A" + }, + { + "key": "Blue", + "rgb": "#0A2989" + }, + { + "key": "White", + "rgb": "#FFFFFF" + } + ], + "ratios": [ + 60, + 20, + 20 + ], + "measured_lab": [ + 57.58, + -31.03, + 33.45 + ], + "measured_rgb": "#65974D", + "source": "measured" + } + ] +} diff --git a/resources/images/live_stream_default.png b/resources/images/live_stream_default.png index 326ceaf994..c1aa90bf17 100644 Binary files a/resources/images/live_stream_default.png and b/resources/images/live_stream_default.png differ diff --git a/resources/profiles/Creality.json b/resources/profiles/Creality.json index 8e91974b9c..077ee827e5 100644 --- a/resources/profiles/Creality.json +++ b/resources/profiles/Creality.json @@ -1,6 +1,6 @@ { "name": "Creality", - "version": "02.03.02.75", + "version": "02.03.02.76", "force_update": "0", "description": "Creality configurations", "machine_model_list": [ diff --git a/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1 (0.4 nozzle).json b/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1 (0.4 nozzle).json index 8bf73caf41..7300c45c8d 100644 --- a/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1 (0.4 nozzle).json +++ b/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1 (0.4 nozzle).json @@ -39,7 +39,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1 Max_CFS-C 0.4 nozzle.json b/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1 Max_CFS-C 0.4 nozzle.json index 5ff07e264b..adc86b4768 100644 --- a/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1 Max_CFS-C 0.4 nozzle.json +++ b/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1 Max_CFS-C 0.4 nozzle.json @@ -39,7 +39,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1C 0.4 nozzle.json b/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1C 0.4 nozzle.json index fbc953f420..976756a212 100644 --- a/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1C 0.4 nozzle.json +++ b/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1C 0.4 nozzle.json @@ -39,7 +39,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1C_CFS-C 0.4 nozzle.json b/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1C_CFS-C 0.4 nozzle.json index bacd88b9f3..6748914ae7 100644 --- a/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1C_CFS-C 0.4 nozzle.json +++ b/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1C_CFS-C 0.4 nozzle.json @@ -39,7 +39,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1Max (0.4 nozzle).json b/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1Max (0.4 nozzle).json index 1083550af8..f3f37caca7 100644 --- a/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1Max (0.4 nozzle).json +++ b/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1Max (0.4 nozzle).json @@ -39,7 +39,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1_CFS-C 0.4 nozzle.json b/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1_CFS-C 0.4 nozzle.json index 3e98181430..0144a35372 100644 --- a/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1_CFS-C 0.4 nozzle.json +++ b/resources/profiles/Creality/process/0.08mm SuperDetail @Creality K1_CFS-C 0.4 nozzle.json @@ -39,7 +39,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.16mm Optimal @Creality K1 (0.4 nozzle).json b/resources/profiles/Creality/process/0.16mm Optimal @Creality K1 (0.4 nozzle).json index dba47d76a5..d6caa5e775 100644 --- a/resources/profiles/Creality/process/0.16mm Optimal @Creality K1 (0.4 nozzle).json +++ b/resources/profiles/Creality/process/0.16mm Optimal @Creality K1 (0.4 nozzle).json @@ -39,7 +39,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.16mm Optimal @Creality K1 Max_CFS-C 0.4 nozzle.json b/resources/profiles/Creality/process/0.16mm Optimal @Creality K1 Max_CFS-C 0.4 nozzle.json index ef22dc37ab..a1896fa377 100644 --- a/resources/profiles/Creality/process/0.16mm Optimal @Creality K1 Max_CFS-C 0.4 nozzle.json +++ b/resources/profiles/Creality/process/0.16mm Optimal @Creality K1 Max_CFS-C 0.4 nozzle.json @@ -39,7 +39,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.16mm Optimal @Creality K1 SE 0.4 nozzle.json b/resources/profiles/Creality/process/0.16mm Optimal @Creality K1 SE 0.4 nozzle.json index 2ac16fb484..087d90a584 100644 --- a/resources/profiles/Creality/process/0.16mm Optimal @Creality K1 SE 0.4 nozzle.json +++ b/resources/profiles/Creality/process/0.16mm Optimal @Creality K1 SE 0.4 nozzle.json @@ -38,7 +38,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.16mm Optimal @Creality K1 SE_CFS-C 0.4 nozzle.json b/resources/profiles/Creality/process/0.16mm Optimal @Creality K1 SE_CFS-C 0.4 nozzle.json index b247bece36..62708a822a 100644 --- a/resources/profiles/Creality/process/0.16mm Optimal @Creality K1 SE_CFS-C 0.4 nozzle.json +++ b/resources/profiles/Creality/process/0.16mm Optimal @Creality K1 SE_CFS-C 0.4 nozzle.json @@ -38,7 +38,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.16mm Optimal @Creality K1C 0.4 nozzle.json b/resources/profiles/Creality/process/0.16mm Optimal @Creality K1C 0.4 nozzle.json index b1d6201ba7..5deb17002e 100644 --- a/resources/profiles/Creality/process/0.16mm Optimal @Creality K1C 0.4 nozzle.json +++ b/resources/profiles/Creality/process/0.16mm Optimal @Creality K1C 0.4 nozzle.json @@ -39,7 +39,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.16mm Optimal @Creality K1C_CFS-C 0.4 nozzle.json b/resources/profiles/Creality/process/0.16mm Optimal @Creality K1C_CFS-C 0.4 nozzle.json index 44dc07e070..2365a82d31 100644 --- a/resources/profiles/Creality/process/0.16mm Optimal @Creality K1C_CFS-C 0.4 nozzle.json +++ b/resources/profiles/Creality/process/0.16mm Optimal @Creality K1C_CFS-C 0.4 nozzle.json @@ -39,7 +39,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.16mm Optimal @Creality K1Max (0.4 nozzle).json b/resources/profiles/Creality/process/0.16mm Optimal @Creality K1Max (0.4 nozzle).json index 0b4ae9c777..02507c806f 100644 --- a/resources/profiles/Creality/process/0.16mm Optimal @Creality K1Max (0.4 nozzle).json +++ b/resources/profiles/Creality/process/0.16mm Optimal @Creality K1Max (0.4 nozzle).json @@ -39,7 +39,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.16mm Optimal @Creality K1_CFS-C 0.4 nozzle.json b/resources/profiles/Creality/process/0.16mm Optimal @Creality K1_CFS-C 0.4 nozzle.json index 6b201d6f4d..cda928b2e7 100644 --- a/resources/profiles/Creality/process/0.16mm Optimal @Creality K1_CFS-C 0.4 nozzle.json +++ b/resources/profiles/Creality/process/0.16mm Optimal @Creality K1_CFS-C 0.4 nozzle.json @@ -39,7 +39,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.20mm Standard @Creality K1 (0.4 nozzle).json b/resources/profiles/Creality/process/0.20mm Standard @Creality K1 (0.4 nozzle).json index 42e894eecd..eaaeb4529f 100644 --- a/resources/profiles/Creality/process/0.20mm Standard @Creality K1 (0.4 nozzle).json +++ b/resources/profiles/Creality/process/0.20mm Standard @Creality K1 (0.4 nozzle).json @@ -128,7 +128,7 @@ "detect_narrow_internal_solid_infill": "1", "dont_filter_internal_bridges": "disabled", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enforce_support_layers": "0", "ensure_vertical_shell_thickness": "ensure_all", diff --git a/resources/profiles/Creality/process/0.20mm Standard @Creality K1 Max_CFS-C 0.4 nozzle.json b/resources/profiles/Creality/process/0.20mm Standard @Creality K1 Max_CFS-C 0.4 nozzle.json index f765f850c8..1a4bd4a3f3 100644 --- a/resources/profiles/Creality/process/0.20mm Standard @Creality K1 Max_CFS-C 0.4 nozzle.json +++ b/resources/profiles/Creality/process/0.20mm Standard @Creality K1 Max_CFS-C 0.4 nozzle.json @@ -39,7 +39,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.20mm Standard @Creality K1 SE 0.4.json b/resources/profiles/Creality/process/0.20mm Standard @Creality K1 SE 0.4.json index 003a8287ae..19b01a9964 100644 --- a/resources/profiles/Creality/process/0.20mm Standard @Creality K1 SE 0.4.json +++ b/resources/profiles/Creality/process/0.20mm Standard @Creality K1 SE 0.4.json @@ -124,7 +124,7 @@ "detect_narrow_internal_solid_infill": "1", "dont_filter_internal_bridges": "disabled", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enforce_support_layers": "0", "ensure_vertical_shell_thickness": "ensure_all", diff --git a/resources/profiles/Creality/process/0.20mm Standard @Creality K1 SE_CFS-C 0.4 nozzle.json b/resources/profiles/Creality/process/0.20mm Standard @Creality K1 SE_CFS-C 0.4 nozzle.json index 85de09803b..b7365760f4 100644 --- a/resources/profiles/Creality/process/0.20mm Standard @Creality K1 SE_CFS-C 0.4 nozzle.json +++ b/resources/profiles/Creality/process/0.20mm Standard @Creality K1 SE_CFS-C 0.4 nozzle.json @@ -38,7 +38,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.20mm Standard @Creality K1C 0.4 nozzle.json b/resources/profiles/Creality/process/0.20mm Standard @Creality K1C 0.4 nozzle.json index c33dc038f5..aa8e7b440a 100644 --- a/resources/profiles/Creality/process/0.20mm Standard @Creality K1C 0.4 nozzle.json +++ b/resources/profiles/Creality/process/0.20mm Standard @Creality K1C 0.4 nozzle.json @@ -128,7 +128,7 @@ "detect_narrow_internal_solid_infill": "1", "dont_filter_internal_bridges": "disabled", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enforce_support_layers": "0", "ensure_vertical_shell_thickness": "ensure_all", diff --git a/resources/profiles/Creality/process/0.20mm Standard @Creality K1C_CFS-C 0.4 nozzle.json b/resources/profiles/Creality/process/0.20mm Standard @Creality K1C_CFS-C 0.4 nozzle.json index 7957e09303..882703434d 100644 --- a/resources/profiles/Creality/process/0.20mm Standard @Creality K1C_CFS-C 0.4 nozzle.json +++ b/resources/profiles/Creality/process/0.20mm Standard @Creality K1C_CFS-C 0.4 nozzle.json @@ -39,7 +39,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.20mm Standard @Creality K1Max (0.4 nozzle).json b/resources/profiles/Creality/process/0.20mm Standard @Creality K1Max (0.4 nozzle).json index cea8966d46..51ada298d8 100644 --- a/resources/profiles/Creality/process/0.20mm Standard @Creality K1Max (0.4 nozzle).json +++ b/resources/profiles/Creality/process/0.20mm Standard @Creality K1Max (0.4 nozzle).json @@ -128,7 +128,7 @@ "detect_narrow_internal_solid_infill": "1", "dont_filter_internal_bridges": "disabled", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enforce_support_layers": "0", "ensure_vertical_shell_thickness": "ensure_all", diff --git a/resources/profiles/Creality/process/0.20mm Standard @Creality K1_CFS-C 0.4 nozzle.json b/resources/profiles/Creality/process/0.20mm Standard @Creality K1_CFS-C 0.4 nozzle.json index a617d8e2b4..85dd6bbf3d 100644 --- a/resources/profiles/Creality/process/0.20mm Standard @Creality K1_CFS-C 0.4 nozzle.json +++ b/resources/profiles/Creality/process/0.20mm Standard @Creality K1_CFS-C 0.4 nozzle.json @@ -39,7 +39,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.24mm Draft @Creality K1 (0.4 nozzle).json b/resources/profiles/Creality/process/0.24mm Draft @Creality K1 (0.4 nozzle).json index 051a14e1d4..ce0abbf5ac 100644 --- a/resources/profiles/Creality/process/0.24mm Draft @Creality K1 (0.4 nozzle).json +++ b/resources/profiles/Creality/process/0.24mm Draft @Creality K1 (0.4 nozzle).json @@ -39,7 +39,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.24mm Draft @Creality K1 Max_CFS-C 0.4 nozzle.json b/resources/profiles/Creality/process/0.24mm Draft @Creality K1 Max_CFS-C 0.4 nozzle.json index 8170eb02d6..afd921a54b 100644 --- a/resources/profiles/Creality/process/0.24mm Draft @Creality K1 Max_CFS-C 0.4 nozzle.json +++ b/resources/profiles/Creality/process/0.24mm Draft @Creality K1 Max_CFS-C 0.4 nozzle.json @@ -39,7 +39,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.24mm Draft @Creality K1 SE 0.4 nozzle.json b/resources/profiles/Creality/process/0.24mm Draft @Creality K1 SE 0.4 nozzle.json index 349601c857..34a577c896 100644 --- a/resources/profiles/Creality/process/0.24mm Draft @Creality K1 SE 0.4 nozzle.json +++ b/resources/profiles/Creality/process/0.24mm Draft @Creality K1 SE 0.4 nozzle.json @@ -38,7 +38,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.24mm Draft @Creality K1 SE_CFS-C 0.4 nozzle.json b/resources/profiles/Creality/process/0.24mm Draft @Creality K1 SE_CFS-C 0.4 nozzle.json index 40ecd965b1..3522e46d5a 100644 --- a/resources/profiles/Creality/process/0.24mm Draft @Creality K1 SE_CFS-C 0.4 nozzle.json +++ b/resources/profiles/Creality/process/0.24mm Draft @Creality K1 SE_CFS-C 0.4 nozzle.json @@ -38,7 +38,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.24mm Draft @Creality K1C 0.4 nozzle.json b/resources/profiles/Creality/process/0.24mm Draft @Creality K1C 0.4 nozzle.json index ef58869e10..697593d8c4 100644 --- a/resources/profiles/Creality/process/0.24mm Draft @Creality K1C 0.4 nozzle.json +++ b/resources/profiles/Creality/process/0.24mm Draft @Creality K1C 0.4 nozzle.json @@ -39,7 +39,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.24mm Draft @Creality K1C_CFS-C 0.4 nozzle.json b/resources/profiles/Creality/process/0.24mm Draft @Creality K1C_CFS-C 0.4 nozzle.json index af9f2c5b86..b3bf816290 100644 --- a/resources/profiles/Creality/process/0.24mm Draft @Creality K1C_CFS-C 0.4 nozzle.json +++ b/resources/profiles/Creality/process/0.24mm Draft @Creality K1C_CFS-C 0.4 nozzle.json @@ -39,7 +39,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.24mm Draft @Creality K1Max (0.4 nozzle).json b/resources/profiles/Creality/process/0.24mm Draft @Creality K1Max (0.4 nozzle).json index cea238f1f3..9f886804e3 100644 --- a/resources/profiles/Creality/process/0.24mm Draft @Creality K1Max (0.4 nozzle).json +++ b/resources/profiles/Creality/process/0.24mm Draft @Creality K1Max (0.4 nozzle).json @@ -39,7 +39,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.24mm Draft @Creality K1_CFS-C 0.4 nozzle.json b/resources/profiles/Creality/process/0.24mm Draft @Creality K1_CFS-C 0.4 nozzle.json index 5f91995b41..16ebf21b9a 100644 --- a/resources/profiles/Creality/process/0.24mm Draft @Creality K1_CFS-C 0.4 nozzle.json +++ b/resources/profiles/Creality/process/0.24mm Draft @Creality K1_CFS-C 0.4 nozzle.json @@ -39,7 +39,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "1", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.30mm Standard @Creality K1 (0.6 nozzle).json b/resources/profiles/Creality/process/0.30mm Standard @Creality K1 (0.6 nozzle).json index 1259ac5186..a4bd5b2abc 100644 --- a/resources/profiles/Creality/process/0.30mm Standard @Creality K1 (0.6 nozzle).json +++ b/resources/profiles/Creality/process/0.30mm Standard @Creality K1 (0.6 nozzle).json @@ -126,7 +126,7 @@ "detect_narrow_internal_solid_infill": "1", "dont_filter_internal_bridges": "disabled", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enforce_support_layers": "0", "ensure_vertical_shell_thickness": "ensure_all", diff --git a/resources/profiles/Creality/process/0.30mm Standard @Creality K1 SE 0.6 nozzle.json b/resources/profiles/Creality/process/0.30mm Standard @Creality K1 SE 0.6 nozzle.json index 81e54598e3..d4073f4221 100644 --- a/resources/profiles/Creality/process/0.30mm Standard @Creality K1 SE 0.6 nozzle.json +++ b/resources/profiles/Creality/process/0.30mm Standard @Creality K1 SE 0.6 nozzle.json @@ -38,7 +38,7 @@ "draft_shield": "disabled", "elefant_foot_compensation": "0.15", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enable_prime_tower": "0", "enable_support": "0", diff --git a/resources/profiles/Creality/process/0.30mm Standard @Creality K1C 0.6 nozzle.json b/resources/profiles/Creality/process/0.30mm Standard @Creality K1C 0.6 nozzle.json index 71ecb52e77..3f86d9e613 100644 --- a/resources/profiles/Creality/process/0.30mm Standard @Creality K1C 0.6 nozzle.json +++ b/resources/profiles/Creality/process/0.30mm Standard @Creality K1C 0.6 nozzle.json @@ -126,7 +126,7 @@ "detect_narrow_internal_solid_infill": "1", "dont_filter_internal_bridges": "disabled", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enforce_support_layers": "0", "ensure_vertical_shell_thickness": "ensure_all", diff --git a/resources/profiles/Creality/process/0.30mm Standard @Creality K1Max (0.6 nozzle).json b/resources/profiles/Creality/process/0.30mm Standard @Creality K1Max (0.6 nozzle).json index db9e4b1539..54ac0870a4 100644 --- a/resources/profiles/Creality/process/0.30mm Standard @Creality K1Max (0.6 nozzle).json +++ b/resources/profiles/Creality/process/0.30mm Standard @Creality K1Max (0.6 nozzle).json @@ -126,7 +126,7 @@ "detect_narrow_internal_solid_infill": "1", "dont_filter_internal_bridges": "disabled", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enforce_support_layers": "0", "ensure_vertical_shell_thickness": "ensure_all", diff --git a/resources/profiles/Creality/process/0.40mm Standard @Creality K1 (0.8 nozzle).json b/resources/profiles/Creality/process/0.40mm Standard @Creality K1 (0.8 nozzle).json index 5f61b218bc..2de391b8ad 100644 --- a/resources/profiles/Creality/process/0.40mm Standard @Creality K1 (0.8 nozzle).json +++ b/resources/profiles/Creality/process/0.40mm Standard @Creality K1 (0.8 nozzle).json @@ -126,7 +126,7 @@ "detect_narrow_internal_solid_infill": "1", "dont_filter_internal_bridges": "disabled", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enforce_support_layers": "0", "ensure_vertical_shell_thickness": "ensure_all", diff --git a/resources/profiles/Creality/process/0.40mm Standard @Creality K1C 0.8 nozzle.json b/resources/profiles/Creality/process/0.40mm Standard @Creality K1C 0.8 nozzle.json index d5d5d62cda..5cd1a9cb6a 100644 --- a/resources/profiles/Creality/process/0.40mm Standard @Creality K1C 0.8 nozzle.json +++ b/resources/profiles/Creality/process/0.40mm Standard @Creality K1C 0.8 nozzle.json @@ -126,7 +126,7 @@ "detect_narrow_internal_solid_infill": "1", "dont_filter_internal_bridges": "disabled", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enforce_support_layers": "0", "ensure_vertical_shell_thickness": "ensure_all", diff --git a/resources/profiles/Creality/process/0.40mm Standard @Creality K1Max (0.8 nozzle).json b/resources/profiles/Creality/process/0.40mm Standard @Creality K1Max (0.8 nozzle).json index 793c6aa43e..c1c968cbb6 100644 --- a/resources/profiles/Creality/process/0.40mm Standard @Creality K1Max (0.8 nozzle).json +++ b/resources/profiles/Creality/process/0.40mm Standard @Creality K1Max (0.8 nozzle).json @@ -126,7 +126,7 @@ "detect_narrow_internal_solid_infill": "1", "dont_filter_internal_bridges": "disabled", "elefant_foot_compensation_layers": "1", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "enable_overhang_speed": "1", "enforce_support_layers": "0", "ensure_vertical_shell_thickness": "ensure_all", diff --git a/resources/profiles/Custom.json b/resources/profiles/Custom.json index a416f17db0..7eba013111 100644 --- a/resources/profiles/Custom.json +++ b/resources/profiles/Custom.json @@ -1,6 +1,6 @@ { "name": "Custom Printer", - "version": "02.04.00.01", + "version": "02.04.00.03", "force_update": "0", "description": "My configurations", "machine_model_list": [ diff --git a/resources/profiles/Custom/filament/Generic ABS @MyToolChanger.json b/resources/profiles/Custom/filament/Generic ABS @MyToolChanger.json index 1683513dbd..bf48e0488b 100644 --- a/resources/profiles/Custom/filament/Generic ABS @MyToolChanger.json +++ b/resources/profiles/Custom/filament/Generic ABS @MyToolChanger.json @@ -24,12 +24,6 @@ "filament_loading_speed_start": [ "50" ], - "filament_multitool_ramming": [ - "1" - ], - "filament_multitool_ramming_flow": [ - "40" - ], "filament_stamping_distance": [ "45" ], diff --git a/resources/profiles/Custom/filament/Generic ASA @MyToolChanger.json b/resources/profiles/Custom/filament/Generic ASA @MyToolChanger.json index 5cc46deb7e..ad26608bff 100644 --- a/resources/profiles/Custom/filament/Generic ASA @MyToolChanger.json +++ b/resources/profiles/Custom/filament/Generic ASA @MyToolChanger.json @@ -24,12 +24,6 @@ "filament_loading_speed_start": [ "50" ], - "filament_multitool_ramming": [ - "1" - ], - "filament_multitool_ramming_flow": [ - "40" - ], "filament_stamping_distance": [ "45" ], diff --git a/resources/profiles/Custom/filament/Generic PA @MyToolChanger.json b/resources/profiles/Custom/filament/Generic PA @MyToolChanger.json index 0c8918427d..d710282360 100644 --- a/resources/profiles/Custom/filament/Generic PA @MyToolChanger.json +++ b/resources/profiles/Custom/filament/Generic PA @MyToolChanger.json @@ -24,12 +24,6 @@ "filament_loading_speed_start": [ "50" ], - "filament_multitool_ramming": [ - "1" - ], - "filament_multitool_ramming_flow": [ - "40" - ], "filament_stamping_distance": [ "45" ], diff --git a/resources/profiles/Custom/filament/Generic PA-CF @MyToolChanger.json b/resources/profiles/Custom/filament/Generic PA-CF @MyToolChanger.json index a7d86885ef..481fd04152 100644 --- a/resources/profiles/Custom/filament/Generic PA-CF @MyToolChanger.json +++ b/resources/profiles/Custom/filament/Generic PA-CF @MyToolChanger.json @@ -24,12 +24,6 @@ "filament_loading_speed_start": [ "50" ], - "filament_multitool_ramming": [ - "1" - ], - "filament_multitool_ramming_flow": [ - "40" - ], "filament_stamping_distance": [ "45" ], diff --git a/resources/profiles/Custom/filament/Generic PC @MyToolChanger.json b/resources/profiles/Custom/filament/Generic PC @MyToolChanger.json index d80b197394..47f175c277 100644 --- a/resources/profiles/Custom/filament/Generic PC @MyToolChanger.json +++ b/resources/profiles/Custom/filament/Generic PC @MyToolChanger.json @@ -25,12 +25,6 @@ "filament_loading_speed_start": [ "50" ], - "filament_multitool_ramming": [ - "1" - ], - "filament_multitool_ramming_flow": [ - "40" - ], "filament_stamping_distance": [ "45" ], diff --git a/resources/profiles/Custom/filament/Generic PETG @MyToolChanger.json b/resources/profiles/Custom/filament/Generic PETG @MyToolChanger.json index 693551c428..95e47a32a8 100644 --- a/resources/profiles/Custom/filament/Generic PETG @MyToolChanger.json +++ b/resources/profiles/Custom/filament/Generic PETG @MyToolChanger.json @@ -25,12 +25,6 @@ "filament_loading_speed_start": [ "50" ], - "filament_multitool_ramming": [ - "1" - ], - "filament_multitool_ramming_flow": [ - "40" - ], "filament_stamping_distance": [ "45" ], diff --git a/resources/profiles/Custom/filament/Generic PLA @MyToolChanger.json b/resources/profiles/Custom/filament/Generic PLA @MyToolChanger.json index cf1db6e225..b8fe088325 100644 --- a/resources/profiles/Custom/filament/Generic PLA @MyToolChanger.json +++ b/resources/profiles/Custom/filament/Generic PLA @MyToolChanger.json @@ -25,12 +25,6 @@ "filament_loading_speed_start": [ "50" ], - "filament_multitool_ramming": [ - "1" - ], - "filament_multitool_ramming_flow": [ - "40" - ], "filament_stamping_distance": [ "45" ], diff --git a/resources/profiles/Custom/filament/Generic PLA-CF @MyToolChanger.json b/resources/profiles/Custom/filament/Generic PLA-CF @MyToolChanger.json index 6ae729d622..4a766aa3ef 100644 --- a/resources/profiles/Custom/filament/Generic PLA-CF @MyToolChanger.json +++ b/resources/profiles/Custom/filament/Generic PLA-CF @MyToolChanger.json @@ -25,12 +25,6 @@ "filament_loading_speed_start": [ "50" ], - "filament_multitool_ramming": [ - "1" - ], - "filament_multitool_ramming_flow": [ - "40" - ], "filament_stamping_distance": [ "45" ], diff --git a/resources/profiles/Custom/filament/Generic PVA @MyToolChanger.json b/resources/profiles/Custom/filament/Generic PVA @MyToolChanger.json index ccb6d16c73..6fe168abcc 100644 --- a/resources/profiles/Custom/filament/Generic PVA @MyToolChanger.json +++ b/resources/profiles/Custom/filament/Generic PVA @MyToolChanger.json @@ -25,12 +25,6 @@ "filament_loading_speed_start": [ "50" ], - "filament_multitool_ramming": [ - "1" - ], - "filament_multitool_ramming_flow": [ - "40" - ], "filament_stamping_distance": [ "45" ], diff --git a/resources/profiles/Custom/machine/fdm_klipper_common.json b/resources/profiles/Custom/machine/fdm_klipper_common.json index 36f3fe13c6..a0ee0f1a47 100644 --- a/resources/profiles/Custom/machine/fdm_klipper_common.json +++ b/resources/profiles/Custom/machine/fdm_klipper_common.json @@ -116,7 +116,7 @@ "deretraction_speed": [ "30" ], - "z_hop_types": "Normal Lift", + "z_hop_types": "Slope Lift", "silent_mode": "0", "single_extruder_multi_material": "1", "change_filament_gcode": "", diff --git a/resources/profiles/Custom/machine/fdm_repetier_common.json b/resources/profiles/Custom/machine/fdm_repetier_common.json index 1171559086..b36b716e4e 100644 --- a/resources/profiles/Custom/machine/fdm_repetier_common.json +++ b/resources/profiles/Custom/machine/fdm_repetier_common.json @@ -118,7 +118,7 @@ "deretraction_speed": [ "30" ], - "z_hop_types": "Normal Lift", + "z_hop_types": "Slope Lift", "silent_mode": "0", "single_extruder_multi_material": "1", "change_filament_gcode": "", diff --git a/resources/profiles/Custom/machine/fdm_rrf_common.json b/resources/profiles/Custom/machine/fdm_rrf_common.json index 68708490a1..2fc9df9a3d 100644 --- a/resources/profiles/Custom/machine/fdm_rrf_common.json +++ b/resources/profiles/Custom/machine/fdm_rrf_common.json @@ -116,7 +116,7 @@ "deretraction_speed": [ "30" ], - "z_hop_types": "Normal Lift", + "z_hop_types": "Slope Lift", "silent_mode": "0", "single_extruder_multi_material": "1", "change_filament_gcode": "", diff --git a/resources/profiles/Custom/machine/fdm_toolchanger_common.json b/resources/profiles/Custom/machine/fdm_toolchanger_common.json index ff702d0034..55f1b2cadb 100644 --- a/resources/profiles/Custom/machine/fdm_toolchanger_common.json +++ b/resources/profiles/Custom/machine/fdm_toolchanger_common.json @@ -6,6 +6,7 @@ "instantiation": "false", "gcode_flavor": "klipper", "single_extruder_multi_material": "0", + "wait_for_temp_on_wipe_tower": "1", "default_filament_profile": [ "Generic PLA @MyToolChanger" ], @@ -172,11 +173,11 @@ "0.4" ], "z_hop_types": [ - "Normal Lift", - "Normal Lift", - "Normal Lift", - "Normal Lift", - "Normal Lift" + "Slope Lift", + "Slope Lift", + "Slope Lift", + "Slope Lift", + "Slope Lift" ], "purge_in_prime_tower": "0", "machine_pause_gcode": "M601", diff --git a/resources/profiles/OrcaFilamentLibrary.json b/resources/profiles/OrcaFilamentLibrary.json index cd9abd8b0d..9701c6eb0d 100644 --- a/resources/profiles/OrcaFilamentLibrary.json +++ b/resources/profiles/OrcaFilamentLibrary.json @@ -1,6 +1,6 @@ { "name": "OrcaFilamentLibrary", - "version": "02.04.00.03", + "version": "02.04.00.04", "force_update": "0", "description": "Orca Filament Library", "filament_list": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PET-CF @base.json b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PET-CF @base.json index 5a4e910502..c455832b9c 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PET-CF @base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PET-CF @base.json @@ -36,6 +36,9 @@ "filament_max_volumetric_speed": [ "8" ], + "filament_multitool_ramming_flow": [ + "8" + ], "filament_type": [ "PET-CF" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PETG Basic @base.json b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PETG Basic @base.json index 9117643990..755f78cf08 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PETG Basic @base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PETG Basic @base.json @@ -39,6 +39,9 @@ "filament_max_volumetric_speed": [ "8" ], + "filament_multitool_ramming_flow": [ + "8" + ], "filament_vendor": [ "Bambu Lab" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PETG Translucent @base.json b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PETG Translucent @base.json index 69cfb1dab7..0026f408f2 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PETG Translucent @base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PETG Translucent @base.json @@ -39,6 +39,9 @@ "filament_max_volumetric_speed": [ "6" ], + "filament_multitool_ramming_flow": [ + "6" + ], "filament_vendor": [ "Bambu Lab" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PLA Aero @base.json b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PLA Aero @base.json index 889822b02f..1f11ddcc54 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PLA Aero @base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu PLA Aero @base.json @@ -21,6 +21,9 @@ "filament_max_volumetric_speed": [ "6" ], + "filament_multitool_ramming_flow": [ + "6" + ], "filament_type": [ "PLA-AERO" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu Support For PLA-PETG @base.json b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu Support For PLA-PETG @base.json index 49fdcb1e1b..1dea20d081 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu Support For PLA-PETG @base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu Support For PLA-PETG @base.json @@ -27,6 +27,9 @@ "filament_max_volumetric_speed": [ "6" ], + "filament_multitool_ramming_flow": [ + "6" + ], "filament_scarf_seam_type": [ "none" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu Support for ABS @base.json b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu Support for ABS @base.json index 3fe47727e1..bf714a0352 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu Support for ABS @base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Bambu/Bambu Support for ABS @base.json @@ -21,6 +21,9 @@ "filament_max_volumetric_speed": [ "6" ], + "filament_multitool_ramming_flow": [ + "6" + ], "filament_vendor": [ "Bambu Lab" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/COEX/COEX TPU 60A @base.json b/resources/profiles/OrcaFilamentLibrary/filament/COEX/COEX TPU 60A @base.json index 00834bb4a5..a4b0ebce09 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/COEX/COEX TPU 60A @base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/COEX/COEX TPU 60A @base.json @@ -36,6 +36,9 @@ "filament_max_volumetric_speed": [ "1" ], + "filament_multitool_ramming_flow": [ + "1" + ], "filament_retraction_minimum_travel": [ "3" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Elas/Elas ASA @base.json b/resources/profiles/OrcaFilamentLibrary/filament/Elas/Elas ASA @base.json index d6ca0d695b..b4da81db72 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Elas/Elas ASA @base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Elas/Elas ASA @base.json @@ -72,15 +72,6 @@ "fan_min_speed": [ "10" ], - "filament_cooling_final_speed": [ - "0" - ], - "filament_cooling_initial_speed": [ - "0" - ], - "filament_cooling_moves": [ - "0" - ], "filament_cost": [ "599" ], @@ -99,30 +90,12 @@ "filament_is_support": [ "0" ], - "filament_loading_speed": [ - "0" - ], - "filament_loading_speed_start": [ - "0" - ], "filament_long_retractions_when_cut": [ "nil" ], "filament_max_volumetric_speed": [ "15" ], - "filament_minimal_purge_on_wipe_tower": [ - "0" - ], - "filament_multitool_ramming": [ - "0" - ], - "filament_multitool_ramming_flow": [ - "10" - ], - "filament_multitool_ramming_volume": [ - "10" - ], "filament_notes": [ "" ], @@ -165,21 +138,6 @@ "filament_soluble": [ "0" ], - "filament_stamping_distance": [ - "0" - ], - "filament_stamping_loading_speed": [ - "0" - ], - "filament_toolchange_delay": [ - "0" - ], - "filament_unloading_speed": [ - "0" - ], - "filament_unloading_speed_start": [ - "0" - ], "filament_vendor": [ "Elas" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Elas/Elas PETG Basic @base.json b/resources/profiles/OrcaFilamentLibrary/filament/Elas/Elas PETG Basic @base.json index 478210bb7d..8b6db5d3ad 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Elas/Elas PETG Basic @base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Elas/Elas PETG Basic @base.json @@ -42,15 +42,6 @@ "fan_min_speed": [ "20" ], - "filament_cooling_final_speed": [ - "0" - ], - "filament_cooling_initial_speed": [ - "0" - ], - "filament_cooling_moves": [ - "0" - ], "filament_cost": [ "445" ], @@ -69,30 +60,12 @@ "filament_is_support": [ "0" ], - "filament_loading_speed": [ - "0" - ], - "filament_loading_speed_start": [ - "0" - ], "filament_long_retractions_when_cut": [ "1" ], "filament_max_volumetric_speed": [ "18" ], - "filament_minimal_purge_on_wipe_tower": [ - "0" - ], - "filament_multitool_ramming": [ - "0" - ], - "filament_multitool_ramming_flow": [ - "10" - ], - "filament_multitool_ramming_volume": [ - "10" - ], "filament_notes": [ "" ], @@ -135,21 +108,6 @@ "filament_soluble": [ "0" ], - "filament_stamping_distance": [ - "0" - ], - "filament_stamping_loading_speed": [ - "0" - ], - "filament_toolchange_delay": [ - "0" - ], - "filament_unloading_speed": [ - "0" - ], - "filament_unloading_speed_start": [ - "0" - ], "filament_wipe": [ "nil" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Elas/Elas PLA Basic @base.json b/resources/profiles/OrcaFilamentLibrary/filament/Elas/Elas PLA Basic @base.json index abfa611aa3..d3f34beb80 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Elas/Elas PLA Basic @base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Elas/Elas PLA Basic @base.json @@ -71,15 +71,6 @@ "fan_min_speed": [ "60" ], - "filament_cooling_final_speed": [ - "0" - ], - "filament_cooling_initial_speed": [ - "0" - ], - "filament_cooling_moves": [ - "0" - ], "filament_cost": [ "449.5" ], @@ -98,30 +89,12 @@ "filament_is_support": [ "0" ], - "filament_loading_speed": [ - "0" - ], - "filament_loading_speed_start": [ - "0" - ], "filament_long_retractions_when_cut": [ "nil" ], "filament_max_volumetric_speed": [ "25" ], - "filament_minimal_purge_on_wipe_tower": [ - "0" - ], - "filament_multitool_ramming": [ - "0" - ], - "filament_multitool_ramming_flow": [ - "10" - ], - "filament_multitool_ramming_volume": [ - "10" - ], "filament_notes": [ "" ], @@ -164,21 +137,6 @@ "filament_soluble": [ "0" ], - "filament_stamping_distance": [ - "0" - ], - "filament_stamping_loading_speed": [ - "0" - ], - "filament_toolchange_delay": [ - "0" - ], - "filament_unloading_speed": [ - "0" - ], - "filament_unloading_speed_start": [ - "0" - ], "filament_vendor": [ "Elas" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Elas/Elas PLA Pro @base.json b/resources/profiles/OrcaFilamentLibrary/filament/Elas/Elas PLA Pro @base.json index 38313d1d55..41fd91f011 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Elas/Elas PLA Pro @base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Elas/Elas PLA Pro @base.json @@ -71,15 +71,6 @@ "fan_min_speed": [ "100" ], - "filament_cooling_final_speed": [ - "0" - ], - "filament_cooling_initial_speed": [ - "0" - ], - "filament_cooling_moves": [ - "0" - ], "filament_cost": [ "500" ], @@ -98,30 +89,12 @@ "filament_is_support": [ "0" ], - "filament_loading_speed": [ - "0" - ], - "filament_loading_speed_start": [ - "0" - ], "filament_long_retractions_when_cut": [ "nil" ], "filament_max_volumetric_speed": [ "18" ], - "filament_minimal_purge_on_wipe_tower": [ - "0" - ], - "filament_multitool_ramming": [ - "0" - ], - "filament_multitool_ramming_flow": [ - "10" - ], - "filament_multitool_ramming_volume": [ - "10" - ], "filament_notes": [ "" ], @@ -164,21 +137,6 @@ "filament_soluble": [ "0" ], - "filament_stamping_distance": [ - "0" - ], - "filament_stamping_loading_speed": [ - "0" - ], - "filament_toolchange_delay": [ - "0" - ], - "filament_unloading_speed": [ - "0" - ], - "filament_unloading_speed_start": [ - "0" - ], "filament_vendor": [ "Elas" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Elegoo/Elegoo PAHT-CF @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Elegoo/Elegoo PAHT-CF @System.json index d7dd19c086..7d22d28387 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Elegoo/Elegoo PAHT-CF @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Elegoo/Elegoo PAHT-CF @System.json @@ -24,6 +24,9 @@ "filament_max_volumetric_speed": [ "6" ], + "filament_multitool_ramming_flow": [ + "6" + ], "nozzle_temperature": [ "290" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Elegoo/Elegoo PETG @base.json b/resources/profiles/OrcaFilamentLibrary/filament/Elegoo/Elegoo PETG @base.json index d6cd360eba..997f38ed4d 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Elegoo/Elegoo PETG @base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Elegoo/Elegoo PETG @base.json @@ -35,6 +35,9 @@ "filament_max_volumetric_speed": [ "8" ], + "filament_multitool_ramming_flow": [ + "8" + ], "filament_vendor": [ "Elegoo" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Elegoo/Elegoo PLA Wood @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Elegoo/Elegoo PLA Wood @System.json index abae441db5..3d6c95ed00 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Elegoo/Elegoo PLA Wood @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Elegoo/Elegoo PLA Wood @System.json @@ -12,6 +12,9 @@ "filament_max_volumetric_speed": [ "10" ], + "filament_multitool_ramming_flow": [ + "10" + ], "nozzle_temperature": [ "220" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Eolas Prints/Eolas Prints PLA Neon @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Eolas Prints/Eolas Prints PLA Neon @System.json index b40ce2310b..dff734c96d 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Eolas Prints/Eolas Prints PLA Neon @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Eolas Prints/Eolas Prints PLA Neon @System.json @@ -44,5 +44,8 @@ ], "filament_max_volumetric_speed": [ "10" + ], + "filament_multitool_ramming_flow": [ + "10" ] } diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Eolas Prints/Eolas Prints PLA Silk @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Eolas Prints/Eolas Prints PLA Silk @System.json index 843cf72fc5..1c3184da11 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Eolas Prints/Eolas Prints PLA Silk @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Eolas Prints/Eolas Prints PLA Silk @System.json @@ -44,5 +44,8 @@ ], "filament_max_volumetric_speed": [ "8" + ], + "filament_multitool_ramming_flow": [ + "8" ] } diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Generic PA-CF @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Generic PA-CF @System.json index f7b9df437a..cf01ddf65b 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Generic PA-CF @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Generic PA-CF @System.json @@ -10,5 +10,8 @@ "filament_type": [ "PA-CF" ], + "filament_multitool_ramming_flow": [ + "8" + ], "compatible_printers": [] } diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Generic PETG HF @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Generic PETG HF @System.json index e402cdf775..25cf20c304 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Generic PETG HF @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Generic PETG HF @System.json @@ -8,5 +8,8 @@ "filament_max_volumetric_speed": [ "20" ], + "filament_multitool_ramming_flow": [ + "20" + ], "compatible_printers": [] } diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Generic PETG-CF @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Generic PETG-CF @System.json index 5ee99ffed9..30884cf5a0 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Generic PETG-CF @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Generic PETG-CF @System.json @@ -17,6 +17,9 @@ "filament_max_volumetric_speed": [ "11.5" ], + "filament_multitool_ramming_flow": [ + "13" + ], "overhang_fan_speed": [ "100" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Generic PLA High Speed @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Generic PLA High Speed @System.json index 041ff59c44..1880ca2b21 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Generic PLA High Speed @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Generic PLA High Speed @System.json @@ -12,6 +12,9 @@ "filament_max_volumetric_speed": [ "18" ], + "filament_multitool_ramming_flow": [ + "25" + ], "slow_down_layer_time": [ "4" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Generic PLA Matte @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Generic PLA Matte @System.json index d3a6d4813c..39c71d0d62 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Generic PLA Matte @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Generic PLA Matte @System.json @@ -8,6 +8,9 @@ "filament_max_volumetric_speed": [ "11" ], + "filament_multitool_ramming_flow": [ + "11" + ], "filament_retraction_length": [ "0.8" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Generic PP-CF @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Generic PP-CF @System.json index 65420e9dae..a7c1bbff00 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Generic PP-CF @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Generic PP-CF @System.json @@ -15,6 +15,9 @@ "filament_max_volumetric_speed": [ "6" ], + "filament_multitool_ramming_flow": [ + "6" + ], "filament_type": [ "PP-CF" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Generic PP-GF @System.json b/resources/profiles/OrcaFilamentLibrary/filament/Generic PP-GF @System.json index 931ee4b7b3..cf9ea7ace2 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Generic PP-GF @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Generic PP-GF @System.json @@ -15,6 +15,9 @@ "filament_max_volumetric_speed": [ "6" ], + "filament_multitool_ramming_flow": [ + "6" + ], "filament_type": [ "PP-GF" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Overture/Overture Air PLA @base.json b/resources/profiles/OrcaFilamentLibrary/filament/Overture/Overture Air PLA @base.json index 39e351a24c..98d390e9b8 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Overture/Overture Air PLA @base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Overture/Overture Air PLA @base.json @@ -17,6 +17,9 @@ "filament_max_volumetric_speed": [ "8" ], + "filament_multitool_ramming_flow": [ + "8" + ], "filament_vendor": [ "Overture" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Valment/Valment PLA Silk @base.json b/resources/profiles/OrcaFilamentLibrary/filament/Valment/Valment PLA Silk @base.json index d873590f08..258f80fb15 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Valment/Valment PLA Silk @base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Valment/Valment PLA Silk @base.json @@ -23,6 +23,9 @@ "filament_max_volumetric_speed": [ "7.5" ], + "filament_multitool_ramming_flow": [ + "7.5" + ], "filament_vendor": [ "Valment" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_abs.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_abs.json index 6d9d015c11..dea3f03264 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_abs.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_abs.json @@ -43,6 +43,12 @@ "filament_max_volumetric_speed": [ "12" ], + "filament_multitool_ramming_flow": [ + "15" + ], + "filament_multitool_ramming_volume": [ + "10" + ], "filament_type": [ "ABS" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_asa.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_asa.json index 922a5eaa7a..09a29ce792 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_asa.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_asa.json @@ -43,6 +43,12 @@ "filament_max_volumetric_speed": [ "12" ], + "filament_multitool_ramming_flow": [ + "12" + ], + "filament_multitool_ramming_volume": [ + "10" + ], "filament_type": [ "ASA" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_bvoh.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_bvoh.json index d37476f458..cd15600a52 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_bvoh.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_bvoh.json @@ -41,6 +41,15 @@ "filament_max_volumetric_speed": [ "8" ], + "filament_multitool_ramming": [ + "0" + ], + "filament_multitool_ramming_flow": [ + "6" + ], + "filament_multitool_ramming_volume": [ + "0.1" + ], "filament_type": [ "BVOH" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_common.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_common.json index ce95ca7c52..3ff7e805ae 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_common.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_common.json @@ -66,6 +66,12 @@ "filament_minimal_purge_on_wipe_tower": [ "15" ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_volume": [ + "5" + ], "filament_retract_before_wipe": [ "nil" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_cope.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_cope.json index 80ffd173de..798f560e01 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_cope.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_cope.json @@ -41,6 +41,9 @@ "filament_max_volumetric_speed": [ "16" ], + "filament_multitool_ramming_flow": [ + "16" + ], "filament_scarf_seam_type": [ "none" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_eva.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_eva.json index bf1ff86af1..40498a05aa 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_eva.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_eva.json @@ -8,6 +8,9 @@ "filament_type": [ "EVA" ], + "filament_multitool_ramming_flow": [ + "12" + ], "supertack_plate_temp": [ "0" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_hips.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_hips.json index beaebbd0e7..f67709b4b2 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_hips.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_hips.json @@ -41,6 +41,9 @@ "filament_max_volumetric_speed": [ "8" ], + "filament_multitool_ramming_flow": [ + "8" + ], "filament_type": [ "HIPS" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pa.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pa.json index c2e4cf86df..185af32de7 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pa.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pa.json @@ -44,6 +44,9 @@ "filament_max_volumetric_speed": [ "8" ], + "filament_multitool_ramming_flow": [ + "12" + ], "filament_type": [ "PA" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_paht.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_paht.json index 058469ff71..2a1ca6d510 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_paht.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_paht.json @@ -7,6 +7,9 @@ "filament_max_volumetric_speed": [ "12" ], + "filament_multitool_ramming_flow": [ + "12" + ], "filament_type": [ "PAHT" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pc.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pc.json index 0f550063ad..d8fb7d00ae 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pc.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pc.json @@ -86,6 +86,9 @@ "filament_max_volumetric_speed": [ "12" ], + "filament_multitool_ramming_flow": [ + "16" + ], "filament_flow_ratio": [ "0.94" ] diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pctg.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pctg.json index 8ca7e39994..e656fb7715 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pctg.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pctg.json @@ -26,6 +26,9 @@ "filament_max_volumetric_speed": [ "12" ], + "filament_multitool_ramming_flow": [ + "10" + ], "filament_type": [ "PCTG" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pe.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pe.json index beb533d07f..a47543334b 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pe.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pe.json @@ -38,6 +38,9 @@ "filament_max_volumetric_speed": [ "12" ], + "filament_multitool_ramming_flow": [ + "12" + ], "filament_type": [ "PE" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pet.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pet.json index eabb12c708..8b3fef2f2b 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pet.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pet.json @@ -26,6 +26,9 @@ "filament_max_volumetric_speed": [ "10" ], + "filament_multitool_ramming_flow": [ + "15" + ], "filament_type": [ "PETG" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pha.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pha.json index f6ef3ffc0c..7719c6563d 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pha.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pha.json @@ -38,6 +38,9 @@ "filament_max_volumetric_speed": [ "6" ], + "filament_multitool_ramming_flow": [ + "6" + ], "filament_type": [ "PHA" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pla.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pla.json index ae82d44129..21b7c44365 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pla.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pla.json @@ -38,6 +38,9 @@ "filament_max_volumetric_speed": [ "12" ], + "filament_multitool_ramming_flow": [ + "20" + ], "filament_scarf_seam_type": [ "none" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pla_silk.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pla_silk.json index 3680a85602..a5fd601060 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pla_silk.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pla_silk.json @@ -9,6 +9,9 @@ "filament_flow_ratio": [ "0.98" ], + "filament_multitool_ramming_flow": [ + "10" + ], "slow_down_layer_time": [ "8" ] diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pp.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pp.json index ec9405ef9a..4df7753132 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pp.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pp.json @@ -38,6 +38,9 @@ "filament_max_volumetric_speed": [ "12" ], + "filament_multitool_ramming_flow": [ + "12" + ], "filament_type": [ "PP" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_ppa.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_ppa.json index 3eac71bc69..c250e588b0 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_ppa.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_ppa.json @@ -47,6 +47,9 @@ "filament_max_volumetric_speed": [ "8" ], + "filament_multitool_ramming_flow": [ + "8" + ], "filament_type": [ "PPA-CF" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pps.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pps.json index a6a95a804e..49e0bddefd 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pps.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pps.json @@ -43,6 +43,9 @@ "filament_max_volumetric_speed": [ "4" ], + "filament_multitool_ramming_flow": [ + "4" + ], "filament_type": [ "PPS" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pva.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pva.json index 0109726b0d..4bb30d7e11 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pva.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_pva.json @@ -40,6 +40,15 @@ "filament_max_volumetric_speed": [ "12" ], + "filament_multitool_ramming": [ + "0" + ], + "filament_multitool_ramming_flow": [ + "6" + ], + "filament_multitool_ramming_volume": [ + "0.1" + ], "filament_soluble": [ "1" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_sbs.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_sbs.json index 96626d808d..9bc4d5ce36 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_sbs.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_sbs.json @@ -11,6 +11,9 @@ "filament_max_volumetric_speed": [ "12" ], + "filament_multitool_ramming_flow": [ + "12" + ], "filament_type": [ "SBS" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_tpu.json b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_tpu.json index 30d9415ad3..d6fb38a6ba 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_tpu.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/base/fdm_filament_tpu.json @@ -43,6 +43,15 @@ "filament_max_volumetric_speed": [ "3.2" ], + "filament_multitool_ramming": [ + "0" + ], + "filament_multitool_ramming_flow": [ + "3.2" + ], + "filament_multitool_ramming_volume": [ + "0.1" + ], "filament_retraction_length": [ "0.4" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/eSUN/eSUN PLA-Marble @base.json b/resources/profiles/OrcaFilamentLibrary/filament/eSUN/eSUN PLA-Marble @base.json index 40c314585c..cbd4f84ed4 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/eSUN/eSUN PLA-Marble @base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/eSUN/eSUN PLA-Marble @base.json @@ -17,6 +17,9 @@ "filament_max_volumetric_speed": [ "8" ], + "filament_multitool_ramming_flow": [ + "8" + ], "filament_vendor": [ "eSUN" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/eSUN/eSUN PLA-Matte @base.json b/resources/profiles/OrcaFilamentLibrary/filament/eSUN/eSUN PLA-Matte @base.json index 85c6049935..8d22168408 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/eSUN/eSUN PLA-Matte @base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/eSUN/eSUN PLA-Matte @base.json @@ -17,6 +17,9 @@ "filament_max_volumetric_speed": [ "8" ], + "filament_multitool_ramming_flow": [ + "8" + ], "filament_vendor": [ "eSUN" ], diff --git a/resources/profiles/OrcaFilamentLibrary/filament/eSUN/eSUN ePLA-LW @System.json b/resources/profiles/OrcaFilamentLibrary/filament/eSUN/eSUN ePLA-LW @System.json index 138a388419..732eb3fd5b 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/eSUN/eSUN ePLA-LW @System.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/eSUN/eSUN ePLA-LW @System.json @@ -21,6 +21,9 @@ "filament_max_volumetric_speed": [ "6" ], + "filament_multitool_ramming_flow": [ + "6" + ], "filament_vendor": [ "eSUN" ], diff --git a/resources/profiles/Qidi.json b/resources/profiles/Qidi.json index 9e7a290f96..ecceff5a8a 100644 --- a/resources/profiles/Qidi.json +++ b/resources/profiles/Qidi.json @@ -1,6 +1,6 @@ { "name": "Qidi", - "version": "02.04.00.08", + "version": "02.04.00.11", "force_update": "0", "description": "Qidi configurations", "machine_model_list": [ @@ -47,6 +47,10 @@ { "name": "Qidi X-Smart 3", "sub_path": "machine/Qidi X-Smart 3.json" + }, + { + "name": "Qidi X-Plus 5", + "sub_path": "machine/Qidi X-Plus 5.json" } ], "process_list": [ @@ -885,6 +889,70 @@ { "name": "0.56mm Standard @Qidi XSmart3 0.8 nozzle", "sub_path": "process/0.56mm Standard @Qidi XSmart3 0.8 nozzle.json" + }, + { + "name": "0.08mm High Quality @X-Plus 5", + "sub_path": "process/0.08mm High Quality @X-Plus 5.json" + }, + { + "name": "0.12mm High Quality @X-Plus 5", + "sub_path": "process/0.12mm High Quality @X-Plus 5.json" + }, + { + "name": "0.16mm High Quality @X-Plus 5", + "sub_path": "process/0.16mm High Quality @X-Plus 5.json" + }, + { + "name": "0.16mm Standard @X-Plus 5", + "sub_path": "process/0.16mm Standard @X-Plus 5.json" + }, + { + "name": "0.20mm High Quality @X-Plus 5", + "sub_path": "process/0.20mm High Quality @X-Plus 5.json" + }, + { + "name": "0.20mm Standard @X-Plus 5", + "sub_path": "process/0.20mm Standard @X-Plus 5.json" + }, + { + "name": "0.24mm Standard @X-Plus 5", + "sub_path": "process/0.24mm Standard @X-Plus 5.json" + }, + { + "name": "0.08mm High Quality @X-Plus 5 0.2 nozzle", + "sub_path": "process/0.08mm High Quality @X-Plus 5 0.2 nozzle.json" + }, + { + "name": "0.10mm Standard @X-Plus 5 0.2 nozzle", + "sub_path": "process/0.10mm Standard @X-Plus 5 0.2 nozzle.json" + }, + { + "name": "0.12mm Balanced Quality @X-Plus 5 0.2 nozzle", + "sub_path": "process/0.12mm Balanced Quality @X-Plus 5 0.2 nozzle.json" + }, + { + "name": "0.18mm Balanced Quality @X-Plus 5 0.6 nozzle", + "sub_path": "process/0.18mm Balanced Quality @X-Plus 5 0.6 nozzle.json" + }, + { + "name": "0.24mm Balanced Quality @X-Plus 5 0.6 nozzle", + "sub_path": "process/0.24mm Balanced Quality @X-Plus 5 0.6 nozzle.json" + }, + { + "name": "0.30mm Standard @X-Plus 5 0.6 nozzle", + "sub_path": "process/0.30mm Standard @X-Plus 5 0.6 nozzle.json" + }, + { + "name": "0.24mm Balanced Quality @X-Plus 5 0.8 nozzle", + "sub_path": "process/0.24mm Balanced Quality @X-Plus 5 0.8 nozzle.json" + }, + { + "name": "0.32mm Balanced Quality @X-Plus 5 0.8 nozzle", + "sub_path": "process/0.32mm Balanced Quality @X-Plus 5 0.8 nozzle.json" + }, + { + "name": "0.40mm Standard @X-Plus 5 0.8 nozzle", + "sub_path": "process/0.40mm Standard @X-Plus 5 0.8 nozzle.json" } ], "filament_list": [ @@ -6035,6 +6103,998 @@ { "name": "Qidi Generic PLA High Speed @Qidi X-Plus 4 0.8 nozzle", "sub_path": "filament/Qidi Generic PLA High Speed @Qidi X-Plus 4 0.8 nozzle.json" + }, + { + "name": "fdm_filament_x5_common", + "sub_path": "filament/X5/fdm_filament_x5_common.json" + }, + { + "name": "Generic ABS@X-Plus 5-Series", + "sub_path": "filament/X5/Generic ABS @X-Plus 5.json" + }, + { + "name": "QIDI ASA@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI ASA @X-Plus 5.json" + }, + { + "name": "Generic PETG@X-Plus 5-Series", + "sub_path": "filament/X5/Generic PETG @X-Plus 5.json" + }, + { + "name": "Generic PLA Silk@X-Plus 5-Series", + "sub_path": "filament/X5/Generic PLA Silk @X-Plus 5.json" + }, + { + "name": "Generic PLA@X-Plus 5-Series", + "sub_path": "filament/X5/Generic PLA @X-Plus 5.json" + }, + { + "name": "Generic PLA+@X-Plus 5-Series", + "sub_path": "filament/X5/Generic PLA+ @X-Plus 5.json" + }, + { + "name": "PolyLite PLA@X-Plus 5-Series", + "sub_path": "filament/X5/PolyLite PLA @X-Plus 5.json" + }, + { + "name": "Polymaker PLA-HT@X-Plus 5-Series", + "sub_path": "filament/X5/Polymaker PLA-HT @X-Plus 5.json" + }, + { + "name": "QIDI PLA-CF@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PLA-CF @X-Plus 5.json" + }, + { + "name": "QIDI PLA-CF @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PLA-CF @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PLA-CF @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PLA-CF @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PLA-CF @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI PLA-CF @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI ABS Rapido@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI ABS Rapido @X-Plus 5.json" + }, + { + "name": "QIDI ABS Rapido @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/QIDI ABS Rapido @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "QIDI ABS Rapido @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI ABS Rapido @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI ABS Rapido @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI ABS Rapido @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI ABS Rapido @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI ABS Rapido @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI ABS Rapido Metal@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI ABS Rapido Metal @X-Plus 5.json" + }, + { + "name": "QIDI ABS Rapido Metal @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/QIDI ABS Rapido Metal @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "QIDI ABS Rapido Metal @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI ABS Rapido Metal @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI ABS Rapido Metal @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI ABS Rapido Metal @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI ABS Rapido Metal @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI ABS Rapido Metal @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI ABS Odorless@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI ABS Odorless @X-Plus 5.json" + }, + { + "name": "QIDI ABS Odorless @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/QIDI ABS Odorless @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "QIDI ABS Odorless @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI ABS Odorless @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI ABS Odorless @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI ABS Odorless @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI ABS Odorless @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI ABS Odorless @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI PLA Rapido@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PLA Rapido @X-Plus 5.json" + }, + { + "name": "QIDI PLA Rapido @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/QIDI PLA Rapido @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "QIDI PLA Rapido @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PLA Rapido @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PLA Rapido @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PLA Rapido @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PLA Rapido @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI PLA Rapido @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI PLA Rapido Matte@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PLA Rapido Matte @X-Plus 5.json" + }, + { + "name": "QIDI PLA Rapido Matte @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/QIDI PLA Rapido Matte @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "QIDI PLA Rapido Matte @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PLA Rapido Matte @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PLA Rapido Matte @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PLA Rapido Matte @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PLA Rapido Matte @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI PLA Rapido Matte @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI PLA Silk@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PLA Silk @X-Plus 5.json" + }, + { + "name": "QIDI PLA Silk @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PLA Silk @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PLA Silk @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PLA Silk @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PLA Rapido Metal@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PLA Rapido Metal @X-Plus 5.json" + }, + { + "name": "QIDI PLA Rapido Metal @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/QIDI PLA Rapido Metal @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "QIDI PLA Rapido Metal @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PLA Rapido Metal @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PLA Rapido Metal @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PLA Rapido Metal @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PLA Rapido Metal @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI PLA Rapido Metal @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI PETG Tough@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PETG Tough @X-Plus 5.json" + }, + { + "name": "QIDI PETG Tough @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/QIDI PETG Tough @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "QIDI PETG Tough @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PETG Tough @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PETG Tough @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PETG Tough @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PETG Tough @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI PETG Tough @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI PET-CF@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PET-CF @X-Plus 5.json" + }, + { + "name": "QIDI PET-CF @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PET-CF @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PET-CF @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PET-CF @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PET-CF @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI PET-CF @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI PA12-CF@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PA12-CF @X-Plus 5.json" + }, + { + "name": "QIDI PA12-CF @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PA12-CF @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PA12-CF @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PA12-CF @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PA12-CF @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI PA12-CF @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI PA6-CF@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PA6-CF @X-Plus 5.json" + }, + { + "name": "QIDI PA6-CF @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PA6-CF @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PA6-CF @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PA6-CF @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PA6-CF @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI PA6-CF @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI PAHT-CF@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PAHT-CF @X-Plus 5.json" + }, + { + "name": "QIDI PAHT-CF @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PAHT-CF @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PAHT-CF @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PAHT-CF @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PAHT-CF @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI PAHT-CF @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI PPS-CF@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PPS-CF @X-Plus 5.json" + }, + { + "name": "QIDI PPS-CF @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PPS-CF @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PPS-CF @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PPS-CF @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PPS-CF @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI PPS-CF @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI ABS-GF@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI ABS-GF @X-Plus 5.json" + }, + { + "name": "QIDI ABS-GF @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI ABS-GF @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI ABS-GF @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI ABS-GF @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI ABS-GF @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI ABS-GF @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI UltraPA@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI UltraPA @X-Plus 5.json" + }, + { + "name": "QIDI UltraPA @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI UltraPA @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI UltraPA @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI UltraPA @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI UltraPA @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI UltraPA @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "Generic TPU 95A@X-Plus 5-Series", + "sub_path": "filament/X5/Generic TPU 95A @X-Plus 5.json" + }, + { + "name": "QIDI PC/ABS-FR@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PC-ABS-FR @X-Plus 5.json" + }, + { + "name": "QIDI PC/ABS-FR @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PC-ABS-FR @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PC/ABS-FR @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PC-ABS-FR @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PC/ABS-FR @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI PC-ABS-FR @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI ASA-Aero@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI ASA-Aero @X-Plus 5.json" + }, + { + "name": "QIDI ASA-Aero @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI ASA-Aero @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "Generic ABS @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/Generic ABS @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "Generic ABS @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/Generic ABS @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "Generic ABS @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/Generic ABS @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "Generic ABS @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/Generic ABS @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI ASA @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/QIDI ASA @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "QIDI ASA @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI ASA @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI ASA @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI ASA @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI ASA @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI ASA @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "Generic PETG @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/Generic PETG @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "Generic PETG @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/Generic PETG @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "Generic PETG @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/Generic PETG @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "Generic PETG @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/Generic PETG @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "Generic PLA @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/Generic PLA @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "Generic PLA @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/Generic PLA @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "Generic PLA @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/Generic PLA @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "Generic PLA @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/Generic PLA @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "Generic PLA+ @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/Generic PLA+ @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "Generic PLA+ @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/Generic PLA+ @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "Generic PLA+ @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/Generic PLA+ @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "Generic PLA+ @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/Generic PLA+ @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "Generic PLA Silk @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/Generic PLA Silk @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "Generic PLA Silk @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/Generic PLA Silk @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "Generic TPU 95A @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/Generic TPU 95A @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "Generic TPU 95A @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/Generic TPU 95A @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "Generic TPU 95A @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/Generic TPU 95A @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI TPU 95A-HF@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI TPU 95A-HF @X-Plus 5.json" + }, + { + "name": "QIDI TPU 95A-HF @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI TPU 95A-HF @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI TPU 95A-HF @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI TPU 95A-HF @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI TPU 95A-HF @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI TPU 95A-HF @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "PolyLite PLA @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/PolyLite PLA @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "PolyLite PLA @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/PolyLite PLA @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "PolyLite PLA @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/PolyLite PLA @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "PolyLite PLA @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/PolyLite PLA @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "Polymaker PLA-HT @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/Polymaker PLA-HT @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "Polymaker PLA-HT @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/Polymaker PLA-HT @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "Polymaker PLA-HT @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/Polymaker PLA-HT @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "Polymaker PLA-HT @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/Polymaker PLA-HT @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "PolyLite ABS@X-Plus 5-Series", + "sub_path": "filament/X5/PolyLite ABS @X-Plus 5.json" + }, + { + "name": "PolyLite ABS @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/PolyLite ABS @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "PolyLite ABS @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/PolyLite ABS @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "PolyLite ABS @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/PolyLite ABS @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "PolyLite ABS @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/PolyLite ABS @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "Overture PLA@X-Plus 5-Series", + "sub_path": "filament/X5/Overture PLA @X-Plus 5.json" + }, + { + "name": "Overture PLA @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/Overture PLA @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "Overture PLA @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/Overture PLA @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "Overture PLA @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/Overture PLA @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "Overture PLA @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/Overture PLA @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "Overture ABS@X-Plus 5-Series", + "sub_path": "filament/X5/Overture ABS @X-Plus 5.json" + }, + { + "name": "Overture ABS @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/Overture ABS @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "Overture ABS @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/Overture ABS @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "Overture ABS @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/Overture ABS @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "Overture ABS @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/Overture ABS @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "Bambu PLA@X-Plus 5-Series", + "sub_path": "filament/X5/Bambu PLA @X-Plus 5.json" + }, + { + "name": "Bambu PLA @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/Bambu PLA @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "Bambu PLA @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/Bambu PLA @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "Bambu PLA @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/Bambu PLA @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "Bambu PLA @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/Bambu PLA @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "Bambu ABS@X-Plus 5-Series", + "sub_path": "filament/X5/Bambu ABS @X-Plus 5.json" + }, + { + "name": "Bambu ABS @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/Bambu ABS @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "Bambu ABS @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/Bambu ABS @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "Bambu ABS @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/Bambu ABS @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "Bambu ABS @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/Bambu ABS @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "Bambu PETG@X-Plus 5-Series", + "sub_path": "filament/X5/Bambu PETG @X-Plus 5.json" + }, + { + "name": "Bambu PETG @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/Bambu PETG @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "Bambu PETG @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/Bambu PETG @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "Bambu PETG @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/Bambu PETG @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "Bambu PETG @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/Bambu PETG @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "HATCHBOX PLA@X-Plus 5-Series", + "sub_path": "filament/X5/HATCHBOX PLA @X-Plus 5.json" + }, + { + "name": "HATCHBOX PLA @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/HATCHBOX PLA @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "HATCHBOX PLA @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/HATCHBOX PLA @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "HATCHBOX PLA @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/HATCHBOX PLA @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "HATCHBOX PLA @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/HATCHBOX PLA @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "HATCHBOX ABS@X-Plus 5-Series", + "sub_path": "filament/X5/HATCHBOX ABS @X-Plus 5.json" + }, + { + "name": "HATCHBOX ABS @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/HATCHBOX ABS @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "HATCHBOX ABS @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/HATCHBOX ABS @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "HATCHBOX ABS @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/HATCHBOX ABS @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "HATCHBOX ABS @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/HATCHBOX ABS @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "HATCHBOX PETG@X-Plus 5-Series", + "sub_path": "filament/X5/HATCHBOX PETG @X-Plus 5.json" + }, + { + "name": "HATCHBOX PETG @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/HATCHBOX PETG @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "HATCHBOX PETG @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/HATCHBOX PETG @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "HATCHBOX PETG @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/HATCHBOX PETG @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "HATCHBOX PETG @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/HATCHBOX PETG @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI PAHT-GF@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PAHT-GF @X-Plus 5.json" + }, + { + "name": "QIDI PAHT-GF @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PAHT-GF @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PAHT-GF @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PAHT-GF @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PAHT-GF @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI PAHT-GF @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI PET-GF@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PET-GF @X-Plus 5.json" + }, + { + "name": "QIDI PET-GF @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PET-GF @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PET-GF @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PET-GF @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PET-GF @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI PET-GF @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI UltraPA-CF25@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI UltraPA-CF25 @X-Plus 5.json" + }, + { + "name": "QIDI UltraPA-CF25 @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI UltraPA-CF25 @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI UltraPA-CF25 @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI UltraPA-CF25 @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI UltraPA-CF25 @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI UltraPA-CF25 @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI WOOD Rapido@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI WOOD Rapido @X-Plus 5.json" + }, + { + "name": "QIDI WOOD Rapido @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI WOOD Rapido @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI WOOD Rapido @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI WOOD Rapido @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI WOOD Rapido @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI WOOD Rapido @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "Generic PC@X-Plus 5-Series", + "sub_path": "filament/X5/Generic PC @X-Plus 5.json" + }, + { + "name": "Generic PC @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/Generic PC @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "Generic PC @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/Generic PC @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "Generic PC @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/Generic PC @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "Generic PC @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/Generic PC @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI TPU-Aero@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI TPU-Aero @X-Plus 5.json" + }, + { + "name": "QIDI TPU-Aero @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI TPU-Aero @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI TPU-Aero @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI TPU-Aero @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI Support For PET/PA@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI Support For PET-PA @X-Plus 5.json" + }, + { + "name": "QIDI Support For PET/PA @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI Support For PET-PA @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI Support For PET/PA @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI Support For PET-PA @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI Support For PET/PA @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI Support For PET-PA @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI Support For PAHT@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI Support For PAHT @X-Plus 5.json" + }, + { + "name": "QIDI Support For PAHT @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI Support For PAHT @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI Support For PAHT @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI Support For PAHT @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI Support For PAHT @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI Support For PAHT @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI PLA Basic@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PLA Basic @X-Plus 5.json" + }, + { + "name": "QIDI PLA Basic @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/QIDI PLA Basic @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "QIDI PLA Basic @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PLA Basic @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PLA Basic @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PLA Basic @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PLA Basic @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI PLA Basic @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI PLA Matte Basic@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PLA Matte Basic @X-Plus 5.json" + }, + { + "name": "QIDI PLA Matte Basic @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/QIDI PLA Matte Basic @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "QIDI PLA Matte Basic @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PLA Matte Basic @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PLA Matte Basic @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PLA Matte Basic @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PLA Matte Basic @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI PLA Matte Basic @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI PETG Rapido@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PETG Rapido @X-Plus 5.json" + }, + { + "name": "QIDI PETG Rapido @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/QIDI PETG Rapido @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "QIDI PETG Rapido @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PETG Rapido @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PETG Rapido @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PETG Rapido @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PETG Rapido @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI PETG Rapido @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI PETG Basic@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PETG Basic @X-Plus 5.json" + }, + { + "name": "QIDI PETG Basic @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/QIDI PETG Basic @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "QIDI PETG Basic @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PETG Basic @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PETG Basic @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PETG Basic @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PETG Basic @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI PETG Basic @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI PETG Translucent@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PETG Translucent @X-Plus 5.json" + }, + { + "name": "QIDI PETG Translucent @Qidi X-Plus 5 0.2 nozzle", + "sub_path": "filament/X5/QIDI PETG Translucent @Qidi X-Plus 5 0.2 nozzle.json" + }, + { + "name": "QIDI PETG Translucent @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PETG Translucent @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PETG Translucent @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PETG Translucent @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PETG Translucent @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI PETG Translucent @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI PETG-CF@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PETG-CF @X-Plus 5.json" + }, + { + "name": "QIDI PETG-CF @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PETG-CF @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PETG-CF @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PETG-CF @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PETG-CF @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI PETG-CF @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI PETG-GF@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PETG-GF @X-Plus 5.json" + }, + { + "name": "QIDI PETG-GF @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PETG-GF @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PETG-GF @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PETG-GF @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PETG-GF @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI PETG-GF @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI PPS-GF@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PPS-GF @X-Plus 5.json" + }, + { + "name": "QIDI PPS-GF @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PPS-GF @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PPS-GF @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PPS-GF @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI PPS-GF @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI PPS-GF @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI PEBA 95A@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI PEBA 95A @X-Plus 5.json" + }, + { + "name": "QIDI PEBA 95A @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI PEBA 95A @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI PEBA 95A @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI PEBA 95A @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI ASA-CF@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI ASA-CF @X-Plus 5.json" + }, + { + "name": "QIDI ASA-CF @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI ASA-CF @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI ASA-CF @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI ASA-CF @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI ASA-CF @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI ASA-CF @Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "QIDI TPU-GF@X-Plus 5-Series", + "sub_path": "filament/X5/QIDI TPU-GF @X-Plus 5.json" + }, + { + "name": "QIDI TPU-GF @Qidi X-Plus 5 0.4 nozzle", + "sub_path": "filament/X5/QIDI TPU-GF @Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "QIDI TPU-GF @Qidi X-Plus 5 0.6 nozzle", + "sub_path": "filament/X5/QIDI TPU-GF @Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "QIDI TPU-GF @Qidi X-Plus 5 0.8 nozzle", + "sub_path": "filament/X5/QIDI TPU-GF @Qidi X-Plus 5 0.8 nozzle.json" } ], "machine_list": [ @@ -6197,6 +7257,22 @@ { "name": "Qidi Q2C 0.8 nozzle", "sub_path": "machine/Qidi Q2C 0.8 nozzle.json" + }, + { + "name": "Qidi X-Plus 5 0.4 nozzle", + "sub_path": "machine/Qidi X-Plus 5 0.4 nozzle.json" + }, + { + "name": "Qidi X-Plus 5 0.6 nozzle", + "sub_path": "machine/Qidi X-Plus 5 0.6 nozzle.json" + }, + { + "name": "Qidi X-Plus 5 0.8 nozzle", + "sub_path": "machine/Qidi X-Plus 5 0.8 nozzle.json" + }, + { + "name": "Qidi X-Plus 5 0.2 nozzle", + "sub_path": "machine/Qidi X-Plus 5 0.2 nozzle.json" } ] } diff --git a/resources/profiles/Qidi/Qidi X-Plus 5_cover.png b/resources/profiles/Qidi/Qidi X-Plus 5_cover.png new file mode 100644 index 0000000000..06f5c5858d Binary files /dev/null and b/resources/profiles/Qidi/Qidi X-Plus 5_cover.png differ diff --git a/resources/profiles/Qidi/filament/X4/QIDI PA12-CF @X-Max 4.json b/resources/profiles/Qidi/filament/X4/QIDI PA12-CF @X-Max 4.json index e217915579..e268f6081c 100644 --- a/resources/profiles/Qidi/filament/X4/QIDI PA12-CF @X-Max 4.json +++ b/resources/profiles/Qidi/filament/X4/QIDI PA12-CF @X-Max 4.json @@ -20,6 +20,9 @@ "close_fan_the_first_x_layers": [ "3" ], + "during_print_exhaust_fan_speed": [ + "0" + ], "fan_cooling_layer_time": [ "10" ], diff --git a/resources/profiles/Qidi/filament/X4/QIDI PAHT-CF @X-Max 4.json b/resources/profiles/Qidi/filament/X4/QIDI PAHT-CF @X-Max 4.json index 83cd3e27cb..2bf1ca53fc 100644 --- a/resources/profiles/Qidi/filament/X4/QIDI PAHT-CF @X-Max 4.json +++ b/resources/profiles/Qidi/filament/X4/QIDI PAHT-CF @X-Max 4.json @@ -20,6 +20,9 @@ "close_fan_the_first_x_layers": [ "3" ], + "during_print_exhaust_fan_speed": [ + "0" + ], "fan_cooling_layer_time": [ "10" ], diff --git a/resources/profiles/Qidi/filament/X4/QIDI PAHT-GF @X-Max 4.json b/resources/profiles/Qidi/filament/X4/QIDI PAHT-GF @X-Max 4.json index 8a35c7330b..37afc97c29 100644 --- a/resources/profiles/Qidi/filament/X4/QIDI PAHT-GF @X-Max 4.json +++ b/resources/profiles/Qidi/filament/X4/QIDI PAHT-GF @X-Max 4.json @@ -20,6 +20,9 @@ "close_fan_the_first_x_layers": [ "3" ], + "during_print_exhaust_fan_speed": [ + "0" + ], "fan_cooling_layer_time": [ "10" ], diff --git a/resources/profiles/Qidi/filament/X5/Bambu ABS @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/Bambu ABS @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..32352a70c8 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Bambu ABS @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,20 @@ +{ + "type": "filament", + "setting_id": "v4Of6ePWz1oGVLqU", + "name": "Bambu ABS @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Bambu ABS@X-Plus 5-Series", + "chamber_temperatures": [ + "0" + ], + "filament_max_volumetric_speed": [ + "2" + ], + "pressure_advance": [ + "0.03" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Bambu ABS @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/Bambu ABS @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..8b6bc2ebb6 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Bambu ABS @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "K3KqxJ3z6lSKoCsQ", + "name": "Bambu ABS @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Bambu ABS@X-Plus 5-Series", + "pressure_advance": [ + "0.03" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Bambu ABS @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/Bambu ABS @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..3fa0dfca1f --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Bambu ABS @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "D3OOS6ShJDdn4dlU", + "name": "Bambu ABS @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Bambu ABS@X-Plus 5-Series", + "nozzle_temperature": [ + "250" + ], + "pressure_advance": [ + "0.014" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Bambu ABS @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/Bambu ABS @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..833774ecff --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Bambu ABS @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,20 @@ +{ + "type": "filament", + "setting_id": "8zWM4mqGjwjy2yJb", + "name": "Bambu ABS @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Bambu ABS@X-Plus 5-Series", + "nozzle_temperature": [ + "250" + ], + "pressure_advance": [ + "0.011" + ], + "slow_down_min_speed": [ + "10" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Bambu ABS @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/Bambu ABS @X-Plus 5.json new file mode 100644 index 0000000000..1464576f61 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Bambu ABS @X-Plus 5.json @@ -0,0 +1,105 @@ +{ + "type": "filament", + "filament_id": "GFB99", + "name": "Bambu ABS@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "45" + ], + "chamber_temperatures": [ + "55" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "80" + ], + "cool_plate_temp": [ + "80" + ], + "eng_plate_temp_initial_layer": [ + "90" + ], + "eng_plate_temp": [ + "90" + ], + "during_print_exhaust_fan_speed": [ + "0" + ], + "fan_max_speed": [ + "80" + ], + "fan_min_speed": [ + "20" + ], + "filament_adhesiveness_category": [ + "200" + ], + "filament_density": [ + "1.05" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_type": [ + "ABS" + ], + "filament_vendor": [ + "Bambu Lab" + ], + "hot_plate_temp_initial_layer": [ + "90" + ], + "hot_plate_temp": [ + "90" + ], + "nozzle_temperature_initial_layer": [ + "250" + ], + "nozzle_temperature_range_high": [ + "280" + ], + "nozzle_temperature_range_low": [ + "240" + ], + "nozzle_temperature": [ + "260" + ], + "overhang_fan_speed": [ + "80" + ], + "overhang_fan_threshold": [ + "25%" + ], + "pressure_advance": [ + "0.035" + ], + "slow_down_layer_time": [ + "4" + ], + "supertack_plate_temp_initial_layer": [ + "80" + ], + "supertack_plate_temp": [ + "80" + ], + "temperature_vitrification": [ + "100" + ], + "textured_plate_temp_initial_layer": [ + "90" + ], + "textured_plate_temp": [ + "90" + ], + "textured_cool_plate_temp_initial_layer": [ + "80" + ], + "textured_cool_plate_temp": [ + "80" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/Bambu PETG @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/Bambu PETG @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..67725a5ee4 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Bambu PETG @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "ed9ZVS66ll61akjK", + "name": "Bambu PETG @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Bambu PETG@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "1" + ], + "pressure_advance": [ + "0.056" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Bambu PETG @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/Bambu PETG @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..953359bb36 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Bambu PETG @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "mPLqTb7smgIDOZTg", + "name": "Bambu PETG @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Bambu PETG@X-Plus 5-Series", + "pressure_advance": [ + "0.056" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Bambu PETG @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/Bambu PETG @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..77014127cd --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Bambu PETG @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "A3K4JfwyqUnciD2e", + "name": "Bambu PETG @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Bambu PETG@X-Plus 5-Series", + "pressure_advance": [ + "0.04" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Bambu PETG @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/Bambu PETG @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..3e95da1618 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Bambu PETG @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "4qjG4MYXmLPJ5Qjm", + "name": "Bambu PETG @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Bambu PETG@X-Plus 5-Series", + "pressure_advance": [ + "0.04" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Bambu PETG @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/Bambu PETG @X-Plus 5.json new file mode 100644 index 0000000000..0a4d2ce22c --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Bambu PETG @X-Plus 5.json @@ -0,0 +1,102 @@ +{ + "type": "filament", + "filament_id": "GFG99", + "name": "Bambu PETG@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "45" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "cool_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "eng_plate_temp": [ + "80" + ], + "fan_cooling_layer_time": [ + "30" + ], + "fan_max_speed": [ + "40" + ], + "fan_min_speed": [ + "10" + ], + "filament_adhesiveness_category": [ + "300" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_max_volumetric_speed": [ + "13" + ], + "filament_type": [ + "PETG" + ], + "filament_vendor": [ + "Bambu Lab" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "hot_plate_temp": [ + "80" + ], + "nozzle_temperature_initial_layer": [ + "240" + ], + "nozzle_temperature_range_high": [ + "270" + ], + "nozzle_temperature_range_low": [ + "220" + ], + "nozzle_temperature": [ + "250" + ], + "overhang_fan_speed": [ + "90" + ], + "overhang_fan_threshold": [ + "10%" + ], + "pressure_advance": [ + "0.056" + ], + "slow_down_layer_time": [ + "8" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], + "supertack_plate_temp": [ + "60" + ], + "temperature_vitrification": [ + "70" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "textured_cool_plate_temp_initial_layer": [ + "60" + ], + "textured_cool_plate_temp": [ + "60" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/Bambu PLA @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/Bambu PLA @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..8cebbd7581 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Bambu PLA @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "cI5eJwzwoecS97SU", + "name": "Bambu PLA @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Bambu PLA@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "2" + ], + "pressure_advance": [ + "0.034" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Bambu PLA @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/Bambu PLA @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..3e7d24cf85 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Bambu PLA @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "UP5ST3BYA3nWaT5i", + "name": "Bambu PLA @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Bambu PLA@X-Plus 5-Series", + "pressure_advance": [ + "0.034" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Bambu PLA @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/Bambu PLA @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..e3e192d1e8 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Bambu PLA @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "jSFJn0WmV76aBwPt", + "name": "Bambu PLA @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Bambu PLA@X-Plus 5-Series", + "pressure_advance": [ + "0.016" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Bambu PLA @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/Bambu PLA @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..2390355042 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Bambu PLA @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "MYCn1SS32p63RzX0", + "name": "Bambu PLA @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Bambu PLA@X-Plus 5-Series", + "pressure_advance": [ + "0.008" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Bambu PLA @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/Bambu PLA @X-Plus 5.json new file mode 100644 index 0000000000..7660c8174d --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Bambu PLA @X-Plus 5.json @@ -0,0 +1,54 @@ +{ + "type": "filament", + "filament_id": "GFL99", + "name": "Bambu PLA@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "filament_adhesiveness_category": [ + "100" + ], + "filament_type": [ + "PLA" + ], + "filament_vendor": [ + "Bambu Lab" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "nozzle_temperature_initial_layer": [ + "210" + ], + "nozzle_temperature": [ + "210" + ], + "overhang_fan_threshold": [ + "50%" + ], + "supertack_plate_temp_initial_layer": [ + "45" + ], + "supertack_plate_temp": [ + "45" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp_initial_layer": [ + "45" + ], + "textured_cool_plate_temp": [ + "45" + ], + "compatible_printers": [], + "close_additional_fan_first_x_layers": [ + "1" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic ABS @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic ABS @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..c12bcc6687 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic ABS @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "xxRxeoAbiYJcH1P4", + "name": "Generic ABS @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic ABS@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "2" + ], + "pressure_advance": [ + "0.03" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic ABS @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic ABS @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..7807bc8420 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic ABS @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "MM6yfNPfpCtbsm2M", + "name": "Generic ABS @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic ABS@X-Plus 5-Series", + "pressure_advance": [ + "0.03" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic ABS @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic ABS @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..6de2b02613 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic ABS @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "BIiPt2AExUkASm2F", + "name": "Generic ABS @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic ABS@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "24.5" + ], + "pressure_advance": [ + "0.014" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic ABS @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic ABS @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..6e7bda8441 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic ABS @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "VzKqGnik79qlJ1bA", + "name": "Generic ABS @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic ABS@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "24.5" + ], + "pressure_advance": [ + "0.011" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic ABS @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/Generic ABS @X-Plus 5.json new file mode 100644 index 0000000000..4bb0cbe387 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic ABS @X-Plus 5.json @@ -0,0 +1,108 @@ +{ + "type": "filament", + "filament_id": "QD_4_0_11", + "name": "Generic ABS@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "45" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "80" + ], + "cool_plate_temp": [ + "80" + ], + "eng_plate_temp_initial_layer": [ + "90" + ], + "eng_plate_temp": [ + "90" + ], + "during_print_exhaust_fan_speed": [ + "0" + ], + "fan_cooling_layer_time": [ + "30" + ], + "fan_max_speed": [ + "80" + ], + "fan_min_speed": [ + "10" + ], + "filament_adhesiveness_category": [ + "200" + ], + "filament_density": [ + "1.04" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_max_volumetric_speed": [ + "17" + ], + "filament_type": [ + "ABS" + ], + "filament_vendor": [ + "Generic" + ], + "hot_plate_temp_initial_layer": [ + "90" + ], + "hot_plate_temp": [ + "90" + ], + "nozzle_temperature_initial_layer": [ + "250" + ], + "nozzle_temperature_range_high": [ + "280" + ], + "nozzle_temperature_range_low": [ + "240" + ], + "nozzle_temperature": [ + "250" + ], + "overhang_fan_speed": [ + "80" + ], + "overhang_fan_threshold": [ + "25%" + ], + "pressure_advance": [ + "0.021" + ], + "slow_down_layer_time": [ + "4" + ], + "supertack_plate_temp_initial_layer": [ + "80" + ], + "supertack_plate_temp": [ + "80" + ], + "temperature_vitrification": [ + "100" + ], + "textured_plate_temp_initial_layer": [ + "90" + ], + "textured_plate_temp": [ + "90" + ], + "textured_cool_plate_temp_initial_layer": [ + "80" + ], + "textured_cool_plate_temp": [ + "80" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PC @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic PC @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..da5c50af1a --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PC @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,23 @@ +{ + "type": "filament", + "setting_id": "ERbh7DQQfK7A623W", + "name": "Generic PC @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic PC@X-Plus 5-Series", + "filament_flow_ratio": [ + "0.94" + ], + "filament_max_volumetric_speed": [ + "1" + ], + "pressure_advance": [ + "0.04" + ], + "chamber_temperatures": [ + "0" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PC @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic PC @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..487d8dd31d --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PC @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,20 @@ +{ + "type": "filament", + "setting_id": "MK8OyxUEziAeEbXk", + "name": "Generic PC @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic PC@X-Plus 5-Series", + "filament_flow_ratio": [ + "0.95" + ], + "filament_max_volumetric_speed": [ + "10" + ], + "pressure_advance": [ + "0.04" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PC @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic PC @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..90829d6463 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PC @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,20 @@ +{ + "type": "filament", + "setting_id": "EQIfBmyVNumOKVWU", + "name": "Generic PC @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic PC@X-Plus 5-Series", + "filament_flow_ratio": [ + "0.95" + ], + "filament_max_volumetric_speed": [ + "10" + ], + "pressure_advance": [ + "0.03" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PC @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic PC @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..514715f8a2 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PC @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,20 @@ +{ + "type": "filament", + "setting_id": "FsTuQT5hxmfjHyVZ", + "name": "Generic PC @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic PC@X-Plus 5-Series", + "filament_flow_ratio": [ + "0.95" + ], + "filament_max_volumetric_speed": [ + "10" + ], + "pressure_advance": [ + "0.008" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PC @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/Generic PC @X-Plus 5.json new file mode 100644 index 0000000000..360f0ee803 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PC @X-Plus 5.json @@ -0,0 +1,108 @@ +{ + "type": "filament", + "filament_id": "QD_4_0_23", + "name": "Generic PC@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "65" + ], + "chamber_temperatures": [ + "60" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "0" + ], + "cool_plate_temp": [ + "0" + ], + "eng_plate_temp_initial_layer": [ + "110" + ], + "eng_plate_temp": [ + "110" + ], + "during_print_exhaust_fan_speed": [ + "0" + ], + "fan_cooling_layer_time": [ + "30" + ], + "fan_max_speed": [ + "60" + ], + "fan_min_speed": [ + "10" + ], + "filament_adhesiveness_category": [ + "500" + ], + "filament_density": [ + "1.04" + ], + "filament_max_volumetric_speed": [ + "10" + ], + "filament_type": [ + "PC" + ], + "filament_vendor": [ + "Generic" + ], + "hot_plate_temp_initial_layer": [ + "110" + ], + "hot_plate_temp": [ + "110" + ], + "nozzle_temperature_initial_layer": [ + "270" + ], + "nozzle_temperature_range_high": [ + "290" + ], + "nozzle_temperature_range_low": [ + "260" + ], + "nozzle_temperature": [ + "280" + ], + "overhang_fan_speed": [ + "60" + ], + "overhang_fan_threshold": [ + "25%" + ], + "pressure_advance": [ + "0.021" + ], + "slow_down_layer_time": [ + "2" + ], + "supertack_plate_temp_initial_layer": [ + "0" + ], + "supertack_plate_temp": [ + "0" + ], + "temperature_vitrification": [ + "100" + ], + "textured_plate_temp_initial_layer": [ + "110" + ], + "textured_plate_temp": [ + "110" + ], + "textured_cool_plate_temp_initial_layer": [ + "0" + ], + "textured_cool_plate_temp": [ + "0" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PETG @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic PETG @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..2b4e220a54 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PETG @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "KaxkabCgu6EWZBzj", + "name": "Generic PETG @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic PETG@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "1" + ], + "pressure_advance": [ + "0.056" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PETG @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic PETG @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..4da60c3c48 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PETG @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "ckqzES7LsTgYKKhb", + "name": "Generic PETG @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic PETG@X-Plus 5-Series", + "pressure_advance": [ + "0.056" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PETG @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic PETG @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..6f1b9667ab --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PETG @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "1ID4LaQVZN4WjJ4v", + "name": "Generic PETG @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic PETG@X-Plus 5-Series", + "pressure_advance": [ + "0.04" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PETG @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic PETG @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..764e313efe --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PETG @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "yzkFoRA3R01K3lCa", + "name": "Generic PETG @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic PETG@X-Plus 5-Series", + "pressure_advance": [ + "0.04" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PETG @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/Generic PETG @X-Plus 5.json new file mode 100644 index 0000000000..ab1b23bb60 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PETG @X-Plus 5.json @@ -0,0 +1,105 @@ +{ + "type": "filament", + "filament_id": "QD_4_0_41", + "name": "Generic PETG@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "45" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "cool_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "eng_plate_temp": [ + "80" + ], + "fan_cooling_layer_time": [ + "30" + ], + "fan_max_speed": [ + "90" + ], + "fan_min_speed": [ + "40" + ], + "filament_adhesiveness_category": [ + "300" + ], + "filament_density": [ + "1.27" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_max_volumetric_speed": [ + "12" + ], + "filament_type": [ + "PETG" + ], + "filament_vendor": [ + "Generic" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "hot_plate_temp": [ + "80" + ], + "nozzle_temperature_initial_layer": [ + "245" + ], + "nozzle_temperature_range_high": [ + "270" + ], + "nozzle_temperature_range_low": [ + "220" + ], + "nozzle_temperature": [ + "250" + ], + "overhang_fan_speed": [ + "90" + ], + "overhang_fan_threshold": [ + "10%" + ], + "pressure_advance": [ + "0.056" + ], + "slow_down_layer_time": [ + "12" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], + "supertack_plate_temp": [ + "60" + ], + "temperature_vitrification": [ + "70" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "textured_cool_plate_temp_initial_layer": [ + "60" + ], + "textured_cool_plate_temp": [ + "60" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PLA @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic PLA @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..218b407d21 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PLA @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "m5zUk3OpAayCbBwF", + "name": "Generic PLA @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic PLA@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "2" + ], + "pressure_advance": [ + "0.034" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PLA @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic PLA @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..c8dd22aba5 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PLA @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "5Eorr9nKVhLbnYAZ", + "name": "Generic PLA @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic PLA@X-Plus 5-Series", + "pressure_advance": [ + "0.034" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PLA @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic PLA @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..ee352a5667 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PLA @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "nqOawjrkU2lp3P0K", + "name": "Generic PLA @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic PLA@X-Plus 5-Series", + "pressure_advance": [ + "0.016" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PLA @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic PLA @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..14a7fa59c1 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PLA @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "nA20S71nTMCso0Bn", + "name": "Generic PLA @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic PLA@X-Plus 5-Series", + "pressure_advance": [ + "0.008" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PLA @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/Generic PLA @X-Plus 5.json new file mode 100644 index 0000000000..e3939b7796 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PLA @X-Plus 5.json @@ -0,0 +1,66 @@ +{ + "type": "filament", + "filament_id": "QD_4_0_1", + "name": "Generic PLA@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "additional_cooling_fan_speed": [ + "100" + ], + "cool_plate_temp_initial_layer": [ + "45" + ], + "cool_plate_temp": [ + "45" + ], + "filament_adhesiveness_category": [ + "100" + ], + "filament_density": [ + "1.2" + ], + "filament_max_volumetric_speed": [ + "14" + ], + "filament_type": [ + "PLA" + ], + "filament_vendor": [ + "Generic" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "nozzle_temperature": [ + "220" + ], + "overhang_fan_threshold": [ + "50%" + ], + "supertack_plate_temp_initial_layer": [ + "45" + ], + "supertack_plate_temp": [ + "45" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp_initial_layer": [ + "45" + ], + "textured_cool_plate_temp": [ + "45" + ], + "compatible_printers": [], + "close_additional_fan_first_x_layers": [ + "1" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PLA Silk @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic PLA Silk @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..f701cf5b50 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PLA Silk @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "42feLKkUTBQTPNDm", + "name": "Generic PLA Silk @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic PLA Silk@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PLA Silk @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic PLA Silk @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..b898df762a --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PLA Silk @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "uyCmwBkoRNNRbG5R", + "name": "Generic PLA Silk @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic PLA Silk@X-Plus 5-Series", + "pressure_advance": [ + "0.014" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PLA Silk @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/Generic PLA Silk @X-Plus 5.json new file mode 100644 index 0000000000..1c0d325a37 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PLA Silk @X-Plus 5.json @@ -0,0 +1,90 @@ +{ + "type": "filament", + "filament_id": "QD_4_0_4", + "name": "Generic PLA Silk@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "additional_cooling_fan_speed": [ + "100" + ], + "filament_adhesiveness_category": [ + "100" + ], + "filament_density": [ + "1.2" + ], + "filament_max_volumetric_speed": [ + "7.5" + ], + "filament_retraction_length": [ + "0.5" + ], + "filament_type": [ + "PLA" + ], + "filament_vendor": [ + "Generic" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "nozzle_temperature": [ + "220" + ], + "overhang_fan_threshold": [ + "50%" + ], + "pressure_advance": [ + "0.032" + ], + "supertack_plate_temp_initial_layer": [ + "45" + ], + "supertack_plate_temp": [ + "45" + ], + "cool_plate_temp_initial_layer": [ + "45" + ], + "cool_plate_temp": [ + "45" + ], + "eng_plate_temp_initial_layer": [ + "55" + ], + "eng_plate_temp": [ + "55" + ], + "hot_plate_temp_initial_layer": [ + "55" + ], + "hot_plate_temp": [ + "55" + ], + "textured_plate_temp_initial_layer": [ + "55" + ], + "textured_plate_temp": [ + "55" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp_initial_layer": [ + "45" + ], + "textured_cool_plate_temp": [ + "45" + ], + "compatible_printers": [], + "close_additional_fan_first_x_layers": [ + "1" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PLA+ @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic PLA+ @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..a7a736a95a --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PLA+ @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "aD9SNuHAOxVFczNM", + "name": "Generic PLA+ @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic PLA+@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "2" + ], + "pressure_advance": [ + "0.034" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PLA+ @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic PLA+ @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..e10196f45c --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PLA+ @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "QDAKF2VaO4MkiYZ7", + "name": "Generic PLA+ @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic PLA+@X-Plus 5-Series", + "pressure_advance": [ + "0.034" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PLA+ @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic PLA+ @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..62c8ce9de1 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PLA+ @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "kNONALYQIZ8hkU2f", + "name": "Generic PLA+ @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic PLA+@X-Plus 5-Series", + "pressure_advance": [ + "0.016" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PLA+ @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic PLA+ @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..78599cb326 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PLA+ @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "ehm8mY40hPq6Bjby", + "name": "Generic PLA+ @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic PLA+@X-Plus 5-Series", + "pressure_advance": [ + "0.008" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic PLA+ @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/Generic PLA+ @X-Plus 5.json new file mode 100644 index 0000000000..e1d40b179f --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic PLA+ @X-Plus 5.json @@ -0,0 +1,60 @@ +{ + "type": "filament", + "filament_id": "GFL99", + "name": "Generic PLA+@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "additional_cooling_fan_speed": [ + "100" + ], + "filament_adhesiveness_category": [ + "100" + ], + "filament_density": [ + "1.2" + ], + "filament_max_volumetric_speed": [ + "12" + ], + "filament_type": [ + "PLA" + ], + "filament_vendor": [ + "Generic" + ], + "nozzle_temperature_initial_layer": [ + "230" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "nozzle_temperature": [ + "230" + ], + "overhang_fan_threshold": [ + "50%" + ], + "supertack_plate_temp_initial_layer": [ + "45" + ], + "supertack_plate_temp": [ + "45" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp_initial_layer": [ + "45" + ], + "textured_cool_plate_temp": [ + "45" + ], + "compatible_printers": [], + "close_additional_fan_first_x_layers": [ + "1" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic TPU 95A @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic TPU 95A @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..525652742c --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic TPU 95A @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "blCxzXDeplxz7FrS", + "name": "Generic TPU 95A @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic TPU 95A@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic TPU 95A @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic TPU 95A @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..f17a6e91e5 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic TPU 95A @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "mXwJpelMKlY7SQR8", + "name": "Generic TPU 95A @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic TPU 95A@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic TPU 95A @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/Generic TPU 95A @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..072983e488 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic TPU 95A @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "o9NLhClfpsJFEE3P", + "name": "Generic TPU 95A @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Generic TPU 95A@X-Plus 5-Series", + "nozzle_temperature": [ + "220" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Generic TPU 95A @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/Generic TPU 95A @X-Plus 5.json new file mode 100644 index 0000000000..d5f56d408b --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Generic TPU 95A @X-Plus 5.json @@ -0,0 +1,81 @@ +{ + "type": "filament", + "filament_id": "QD_4_0_50", + "name": "Generic TPU 95A@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "cool_plate_temp_initial_layer": [ + "30" + ], + "cool_plate_temp": [ + "30" + ], + "eng_plate_temp_initial_layer": [ + "35" + ], + "eng_plate_temp": [ + "35" + ], + "filament_adhesiveness_category": [ + "600" + ], + "filament_density": [ + "1.21" + ], + "filament_flow_ratio": [ + "1" + ], + "filament_max_volumetric_speed": [ + "4" + ], + "filament_type": [ + "TPU" + ], + "filament_vendor": [ + "Generic" + ], + "hot_plate_temp_initial_layer": [ + "35" + ], + "hot_plate_temp": [ + "35" + ], + "nozzle_temperature_initial_layer": [ + "230" + ], + "nozzle_temperature_range_high": [ + "250" + ], + "nozzle_temperature_range_low": [ + "200" + ], + "nozzle_temperature": [ + "230" + ], + "pressure_advance": [ + "0.1" + ], + "supertack_plate_temp_initial_layer": [ + "30" + ], + "supertack_plate_temp": [ + "30" + ], + "temperature_vitrification": [ + "30" + ], + "textured_plate_temp_initial_layer": [ + "35" + ], + "textured_plate_temp": [ + "35" + ], + "textured_cool_plate_temp_initial_layer": [ + "30" + ], + "textured_cool_plate_temp": [ + "30" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/HATCHBOX ABS @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/HATCHBOX ABS @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..2346539b73 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/HATCHBOX ABS @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,20 @@ +{ + "type": "filament", + "setting_id": "uarx8NrJerlKsoA8", + "name": "HATCHBOX ABS @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "HATCHBOX ABS@X-Plus 5-Series", + "chamber_temperatures": [ + "0" + ], + "filament_max_volumetric_speed": [ + "2" + ], + "pressure_advance": [ + "0.03" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/HATCHBOX ABS @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/HATCHBOX ABS @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..4566ece152 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/HATCHBOX ABS @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "T52l6W0u1uoiXrAx", + "name": "HATCHBOX ABS @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "HATCHBOX ABS@X-Plus 5-Series", + "pressure_advance": [ + "0.03" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/HATCHBOX ABS @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/HATCHBOX ABS @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..bb16d32b06 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/HATCHBOX ABS @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "krTIEBmeK9q2R60M", + "name": "HATCHBOX ABS @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "HATCHBOX ABS@X-Plus 5-Series", + "nozzle_temperature": [ + "250" + ], + "pressure_advance": [ + "0.014" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/HATCHBOX ABS @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/HATCHBOX ABS @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..19b4ce995f --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/HATCHBOX ABS @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,20 @@ +{ + "type": "filament", + "setting_id": "B99z7PiwClkWtyqv", + "name": "HATCHBOX ABS @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "HATCHBOX ABS@X-Plus 5-Series", + "nozzle_temperature": [ + "250" + ], + "pressure_advance": [ + "0.011" + ], + "slow_down_min_speed": [ + "10" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/HATCHBOX ABS @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/HATCHBOX ABS @X-Plus 5.json new file mode 100644 index 0000000000..3f5987240c --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/HATCHBOX ABS @X-Plus 5.json @@ -0,0 +1,105 @@ +{ + "type": "filament", + "filament_id": "GFB99", + "name": "HATCHBOX ABS@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "45" + ], + "chamber_temperatures": [ + "55" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "80" + ], + "cool_plate_temp": [ + "80" + ], + "eng_plate_temp_initial_layer": [ + "90" + ], + "eng_plate_temp": [ + "90" + ], + "during_print_exhaust_fan_speed": [ + "0" + ], + "fan_max_speed": [ + "80" + ], + "fan_min_speed": [ + "20" + ], + "filament_adhesiveness_category": [ + "200" + ], + "filament_density": [ + "1.05" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_type": [ + "ABS" + ], + "filament_vendor": [ + "HATCHBOX" + ], + "hot_plate_temp_initial_layer": [ + "90" + ], + "hot_plate_temp": [ + "90" + ], + "nozzle_temperature_initial_layer": [ + "250" + ], + "nozzle_temperature_range_high": [ + "280" + ], + "nozzle_temperature_range_low": [ + "240" + ], + "nozzle_temperature": [ + "260" + ], + "overhang_fan_speed": [ + "80" + ], + "overhang_fan_threshold": [ + "25%" + ], + "pressure_advance": [ + "0.035" + ], + "slow_down_layer_time": [ + "4" + ], + "supertack_plate_temp_initial_layer": [ + "80" + ], + "supertack_plate_temp": [ + "80" + ], + "temperature_vitrification": [ + "100" + ], + "textured_plate_temp_initial_layer": [ + "90" + ], + "textured_plate_temp": [ + "90" + ], + "textured_cool_plate_temp_initial_layer": [ + "80" + ], + "textured_cool_plate_temp": [ + "80" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/HATCHBOX PETG @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/HATCHBOX PETG @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..dc87553374 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/HATCHBOX PETG @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "2y03Jbrd8epf06MW", + "name": "HATCHBOX PETG @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "HATCHBOX PETG@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "1" + ], + "pressure_advance": [ + "0.056" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/HATCHBOX PETG @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/HATCHBOX PETG @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..21441a8d3e --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/HATCHBOX PETG @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "KHbEUzPhVICXYhTs", + "name": "HATCHBOX PETG @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "HATCHBOX PETG@X-Plus 5-Series", + "pressure_advance": [ + "0.056" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/HATCHBOX PETG @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/HATCHBOX PETG @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..85219cea79 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/HATCHBOX PETG @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "3HPxSQqEM8XGZqgU", + "name": "HATCHBOX PETG @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "HATCHBOX PETG@X-Plus 5-Series", + "pressure_advance": [ + "0.04" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/HATCHBOX PETG @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/HATCHBOX PETG @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..c6b4b699cd --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/HATCHBOX PETG @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "eZnTfUmPGkeoEP87", + "name": "HATCHBOX PETG @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "HATCHBOX PETG@X-Plus 5-Series", + "pressure_advance": [ + "0.04" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/HATCHBOX PETG @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/HATCHBOX PETG @X-Plus 5.json new file mode 100644 index 0000000000..5f794e561c --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/HATCHBOX PETG @X-Plus 5.json @@ -0,0 +1,102 @@ +{ + "type": "filament", + "filament_id": "GFG99", + "name": "HATCHBOX PETG@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "45" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "cool_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "eng_plate_temp": [ + "80" + ], + "fan_cooling_layer_time": [ + "30" + ], + "fan_max_speed": [ + "40" + ], + "fan_min_speed": [ + "10" + ], + "filament_adhesiveness_category": [ + "300" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_max_volumetric_speed": [ + "13" + ], + "filament_type": [ + "PETG" + ], + "filament_vendor": [ + "HATCHBOX" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "hot_plate_temp": [ + "80" + ], + "nozzle_temperature_initial_layer": [ + "240" + ], + "nozzle_temperature_range_high": [ + "270" + ], + "nozzle_temperature_range_low": [ + "220" + ], + "nozzle_temperature": [ + "250" + ], + "overhang_fan_speed": [ + "90" + ], + "overhang_fan_threshold": [ + "10%" + ], + "pressure_advance": [ + "0.056" + ], + "slow_down_layer_time": [ + "8" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], + "supertack_plate_temp": [ + "60" + ], + "temperature_vitrification": [ + "70" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "textured_cool_plate_temp_initial_layer": [ + "60" + ], + "textured_cool_plate_temp": [ + "60" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/HATCHBOX PLA @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/HATCHBOX PLA @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..a1838c2041 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/HATCHBOX PLA @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "Sn0LAKII01kQONZS", + "name": "HATCHBOX PLA @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "HATCHBOX PLA@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "2" + ], + "pressure_advance": [ + "0.034" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/HATCHBOX PLA @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/HATCHBOX PLA @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..82e54e389e --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/HATCHBOX PLA @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "HWmejqWfehK4LG6C", + "name": "HATCHBOX PLA @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "HATCHBOX PLA@X-Plus 5-Series", + "pressure_advance": [ + "0.034" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/HATCHBOX PLA @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/HATCHBOX PLA @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..d6ac4c9a7e --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/HATCHBOX PLA @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "zjIVoA34wN8AT6cp", + "name": "HATCHBOX PLA @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "HATCHBOX PLA@X-Plus 5-Series", + "pressure_advance": [ + "0.016" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/HATCHBOX PLA @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/HATCHBOX PLA @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..5b89e541e3 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/HATCHBOX PLA @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "sTjweBRTvE0P6YtE", + "name": "HATCHBOX PLA @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "HATCHBOX PLA@X-Plus 5-Series", + "pressure_advance": [ + "0.008" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/HATCHBOX PLA @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/HATCHBOX PLA @X-Plus 5.json new file mode 100644 index 0000000000..094e540793 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/HATCHBOX PLA @X-Plus 5.json @@ -0,0 +1,54 @@ +{ + "type": "filament", + "filament_id": "GFL99", + "name": "HATCHBOX PLA@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "filament_adhesiveness_category": [ + "100" + ], + "filament_type": [ + "PLA" + ], + "filament_vendor": [ + "HATCHBOX" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "nozzle_temperature_initial_layer": [ + "210" + ], + "nozzle_temperature": [ + "210" + ], + "overhang_fan_threshold": [ + "50%" + ], + "supertack_plate_temp_initial_layer": [ + "45" + ], + "supertack_plate_temp": [ + "45" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp_initial_layer": [ + "45" + ], + "textured_cool_plate_temp": [ + "45" + ], + "compatible_printers": [], + "close_additional_fan_first_x_layers": [ + "1" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Overture ABS @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/Overture ABS @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..7194b54b7f --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Overture ABS @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,20 @@ +{ + "type": "filament", + "setting_id": "tLj2E2dWAdLKpC4K", + "name": "Overture ABS @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Overture ABS@X-Plus 5-Series", + "chamber_temperatures": [ + "0" + ], + "filament_max_volumetric_speed": [ + "2" + ], + "pressure_advance": [ + "0.054" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Overture ABS @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/Overture ABS @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..5e50aa5dbc --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Overture ABS @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "1IkBG0ywAkk3VhOd", + "name": "Overture ABS @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Overture ABS@X-Plus 5-Series", + "pressure_advance": [ + "0.033" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Overture ABS @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/Overture ABS @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..9bacef9d3a --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Overture ABS @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "4SXA27uTG5kcbnAq", + "name": "Overture ABS @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Overture ABS@X-Plus 5-Series", + "pressure_advance": [ + "0.02" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Overture ABS @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/Overture ABS @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..a53148ed26 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Overture ABS @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "AzC8XjbkzsxEPDMG", + "name": "Overture ABS @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Overture ABS@X-Plus 5-Series", + "pressure_advance": [ + "0.01" + ], + "slow_down_min_speed": [ + "10" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Overture ABS @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/Overture ABS @X-Plus 5.json new file mode 100644 index 0000000000..ea12b78ead --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Overture ABS @X-Plus 5.json @@ -0,0 +1,108 @@ +{ + "type": "filament", + "filament_id": "GFB99", + "name": "Overture ABS@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "45" + ], + "chamber_temperatures": [ + "60" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "80" + ], + "cool_plate_temp": [ + "80" + ], + "eng_plate_temp_initial_layer": [ + "90" + ], + "eng_plate_temp": [ + "90" + ], + "during_print_exhaust_fan_speed": [ + "0" + ], + "fan_max_speed": [ + "80" + ], + "fan_min_speed": [ + "20" + ], + "filament_adhesiveness_category": [ + "200" + ], + "filament_density": [ + "1.12" + ], + "filament_flow_ratio": [ + "0.96" + ], + "filament_max_volumetric_speed": [ + "17" + ], + "filament_type": [ + "ABS" + ], + "filament_vendor": [ + "Overture" + ], + "hot_plate_temp_initial_layer": [ + "90" + ], + "hot_plate_temp": [ + "90" + ], + "nozzle_temperature_initial_layer": [ + "250" + ], + "nozzle_temperature_range_high": [ + "280" + ], + "nozzle_temperature_range_low": [ + "240" + ], + "nozzle_temperature": [ + "255" + ], + "overhang_fan_speed": [ + "80" + ], + "overhang_fan_threshold": [ + "25%" + ], + "pressure_advance": [ + "0.033" + ], + "slow_down_layer_time": [ + "6" + ], + "supertack_plate_temp_initial_layer": [ + "80" + ], + "supertack_plate_temp": [ + "80" + ], + "temperature_vitrification": [ + "100" + ], + "textured_plate_temp_initial_layer": [ + "90" + ], + "textured_plate_temp": [ + "90" + ], + "textured_cool_plate_temp_initial_layer": [ + "80" + ], + "textured_cool_plate_temp": [ + "80" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/Overture PLA @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/Overture PLA @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..f209b21a6e --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Overture PLA @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "2TAd2Dlk6KPvvmMg", + "name": "Overture PLA @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Overture PLA@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "2" + ], + "pressure_advance": [ + "0.062" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Overture PLA @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/Overture PLA @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..a625953f60 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Overture PLA @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "1rKASIobH0DqQUeH", + "name": "Overture PLA @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Overture PLA@X-Plus 5-Series", + "pressure_advance": [ + "0.037" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Overture PLA @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/Overture PLA @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..5a3dfdee8f --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Overture PLA @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "UWQ7bLf74AxEPDdc", + "name": "Overture PLA @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Overture PLA@X-Plus 5-Series", + "pressure_advance": [ + "0.019" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Overture PLA @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/Overture PLA @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..86d8605933 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Overture PLA @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "bIg1NW0EwTlmRNAF", + "name": "Overture PLA @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Overture PLA@X-Plus 5-Series", + "pressure_advance": [ + "0.012" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Overture PLA @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/Overture PLA @X-Plus 5.json new file mode 100644 index 0000000000..571eb2bbeb --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Overture PLA @X-Plus 5.json @@ -0,0 +1,66 @@ +{ + "type": "filament", + "filament_id": "GFL99", + "name": "Overture PLA@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "filament_adhesiveness_category": [ + "100" + ], + "filament_density": [ + "1.2" + ], + "filament_flow_ratio": [ + "1" + ], + "filament_max_volumetric_speed": [ + "11" + ], + "filament_type": [ + "PLA" + ], + "filament_vendor": [ + "Overture" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "nozzle_temperature_initial_layer": [ + "210" + ], + "nozzle_temperature": [ + "210" + ], + "overhang_fan_threshold": [ + "50%" + ], + "slow_down_layer_time": [ + "10" + ], + "supertack_plate_temp_initial_layer": [ + "45" + ], + "supertack_plate_temp": [ + "45" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp_initial_layer": [ + "45" + ], + "textured_cool_plate_temp": [ + "45" + ], + "compatible_printers": [], + "close_additional_fan_first_x_layers": [ + "1" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/PolyLite ABS @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/PolyLite ABS @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..0618d3c36f --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/PolyLite ABS @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,20 @@ +{ + "type": "filament", + "setting_id": "AJYooe7fIzg43McT", + "name": "PolyLite ABS @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "PolyLite ABS@X-Plus 5-Series", + "chamber_temperatures": [ + "0" + ], + "filament_max_volumetric_speed": [ + "2" + ], + "pressure_advance": [ + "0.054" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/PolyLite ABS @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/PolyLite ABS @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..36e012b0d6 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/PolyLite ABS @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "S0uCyMruXw5waEYf", + "name": "PolyLite ABS @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "PolyLite ABS@X-Plus 5-Series", + "pressure_advance": [ + "0.033" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/PolyLite ABS @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/PolyLite ABS @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..1bf7cdf030 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/PolyLite ABS @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "oYlfF3MMF5DEeJpK", + "name": "PolyLite ABS @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "PolyLite ABS@X-Plus 5-Series", + "pressure_advance": [ + "0.02" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/PolyLite ABS @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/PolyLite ABS @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..9d1f541dfc --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/PolyLite ABS @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "hHXz74Ps9x4T7kZl", + "name": "PolyLite ABS @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "PolyLite ABS@X-Plus 5-Series", + "pressure_advance": [ + "0.01" + ], + "slow_down_min_speed": [ + "10" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/PolyLite ABS @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/PolyLite ABS @X-Plus 5.json new file mode 100644 index 0000000000..48b6caa32b --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/PolyLite ABS @X-Plus 5.json @@ -0,0 +1,108 @@ +{ + "type": "filament", + "filament_id": "GFB99", + "name": "PolyLite ABS@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "45" + ], + "chamber_temperatures": [ + "60" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "80" + ], + "cool_plate_temp": [ + "80" + ], + "eng_plate_temp_initial_layer": [ + "90" + ], + "eng_plate_temp": [ + "90" + ], + "during_print_exhaust_fan_speed": [ + "0" + ], + "fan_max_speed": [ + "80" + ], + "fan_min_speed": [ + "20" + ], + "filament_adhesiveness_category": [ + "200" + ], + "filament_density": [ + "1.12" + ], + "filament_flow_ratio": [ + "0.96" + ], + "filament_max_volumetric_speed": [ + "17" + ], + "filament_type": [ + "ABS" + ], + "filament_vendor": [ + "Polymaker" + ], + "hot_plate_temp_initial_layer": [ + "90" + ], + "hot_plate_temp": [ + "90" + ], + "nozzle_temperature_initial_layer": [ + "250" + ], + "nozzle_temperature_range_high": [ + "280" + ], + "nozzle_temperature_range_low": [ + "240" + ], + "nozzle_temperature": [ + "255" + ], + "overhang_fan_speed": [ + "80" + ], + "overhang_fan_threshold": [ + "25%" + ], + "pressure_advance": [ + "0.033" + ], + "slow_down_layer_time": [ + "6" + ], + "supertack_plate_temp_initial_layer": [ + "80" + ], + "supertack_plate_temp": [ + "80" + ], + "temperature_vitrification": [ + "100" + ], + "textured_plate_temp_initial_layer": [ + "90" + ], + "textured_plate_temp": [ + "90" + ], + "textured_cool_plate_temp_initial_layer": [ + "80" + ], + "textured_cool_plate_temp": [ + "80" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/PolyLite PLA @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/PolyLite PLA @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..f823b38e67 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/PolyLite PLA @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "kKfybaeH6Llb3Vdc", + "name": "PolyLite PLA @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "PolyLite PLA@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "2" + ], + "pressure_advance": [ + "0.062" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/PolyLite PLA @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/PolyLite PLA @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..b7f3ef07f6 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/PolyLite PLA @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "hzEsCxIKwuV2ubrz", + "name": "PolyLite PLA @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "PolyLite PLA@X-Plus 5-Series", + "pressure_advance": [ + "0.037" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/PolyLite PLA @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/PolyLite PLA @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..8b98d3842a --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/PolyLite PLA @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "PODs7XKIgMiJZI5x", + "name": "PolyLite PLA @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "PolyLite PLA@X-Plus 5-Series", + "pressure_advance": [ + "0.019" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/PolyLite PLA @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/PolyLite PLA @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..f4b6b8c2ee --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/PolyLite PLA @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "LQ1rP7NSTjw2moHk", + "name": "PolyLite PLA @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "PolyLite PLA@X-Plus 5-Series", + "pressure_advance": [ + "0.012" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/PolyLite PLA @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/PolyLite PLA @X-Plus 5.json new file mode 100644 index 0000000000..5b37f7ae8a --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/PolyLite PLA @X-Plus 5.json @@ -0,0 +1,66 @@ +{ + "type": "filament", + "filament_id": "GFL99", + "name": "PolyLite PLA@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "filament_adhesiveness_category": [ + "100" + ], + "filament_density": [ + "1.2" + ], + "filament_flow_ratio": [ + "1" + ], + "filament_max_volumetric_speed": [ + "11" + ], + "filament_type": [ + "PLA" + ], + "filament_vendor": [ + "Polymaker" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "nozzle_temperature_initial_layer": [ + "210" + ], + "nozzle_temperature": [ + "210" + ], + "overhang_fan_threshold": [ + "50%" + ], + "slow_down_layer_time": [ + "10" + ], + "supertack_plate_temp_initial_layer": [ + "45" + ], + "supertack_plate_temp": [ + "45" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp_initial_layer": [ + "45" + ], + "textured_cool_plate_temp": [ + "45" + ], + "compatible_printers": [], + "close_additional_fan_first_x_layers": [ + "1" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Polymaker PLA-HT @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/Polymaker PLA-HT @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..78389ceeb5 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Polymaker PLA-HT @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "p9cDXBgbjwjOX5me", + "name": "Polymaker PLA-HT @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Polymaker PLA-HT@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "2" + ], + "pressure_advance": [ + "0.062" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Polymaker PLA-HT @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/Polymaker PLA-HT @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..9f77b41119 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Polymaker PLA-HT @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "TpI2jqmcVygyTO8N", + "name": "Polymaker PLA-HT @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Polymaker PLA-HT@X-Plus 5-Series", + "pressure_advance": [ + "0.037" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Polymaker PLA-HT @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/Polymaker PLA-HT @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..3c46385369 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Polymaker PLA-HT @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "69AgpjCCTxF5ffW1", + "name": "Polymaker PLA-HT @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Polymaker PLA-HT@X-Plus 5-Series", + "pressure_advance": [ + "0.019" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Polymaker PLA-HT @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/Polymaker PLA-HT @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..788db83504 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Polymaker PLA-HT @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "tcoQNICpWYGdE24e", + "name": "Polymaker PLA-HT @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "Polymaker PLA-HT@X-Plus 5-Series", + "pressure_advance": [ + "0.012" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/Polymaker PLA-HT @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/Polymaker PLA-HT @X-Plus 5.json new file mode 100644 index 0000000000..f3c54e81f1 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/Polymaker PLA-HT @X-Plus 5.json @@ -0,0 +1,66 @@ +{ + "type": "filament", + "filament_id": "GFL99", + "name": "Polymaker PLA-HT@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "filament_adhesiveness_category": [ + "100" + ], + "filament_density": [ + "1.2" + ], + "filament_flow_ratio": [ + "1" + ], + "filament_max_volumetric_speed": [ + "11" + ], + "filament_type": [ + "PLA" + ], + "filament_vendor": [ + "Polymaker" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "nozzle_temperature": [ + "215" + ], + "overhang_fan_threshold": [ + "50%" + ], + "slow_down_layer_time": [ + "10" + ], + "supertack_plate_temp_initial_layer": [ + "45" + ], + "supertack_plate_temp": [ + "45" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp_initial_layer": [ + "45" + ], + "textured_cool_plate_temp": [ + "45" + ], + "compatible_printers": [], + "close_additional_fan_first_x_layers": [ + "1" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ABS Odorless @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ABS Odorless @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..254422bb19 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ABS Odorless @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,20 @@ +{ + "type": "filament", + "setting_id": "8SvrXXv743rCyyeN", + "name": "QIDI ABS Odorless @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ABS Odorless@X-Plus 5-Series", + "chamber_temperatures": [ + "0" + ], + "pressure_advance": [ + "0.03" + ], + "filament_max_volumetric_speed": [ + "2" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ABS Odorless @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ABS Odorless @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..1bd37ac08a --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ABS Odorless @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "YviPsfkGYv6Q3r3T", + "name": "QIDI ABS Odorless @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ABS Odorless@X-Plus 5-Series", + "pressure_advance": [ + "0.03" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ABS Odorless @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ABS Odorless @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..cbb361fb9c --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ABS Odorless @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,20 @@ +{ + "type": "filament", + "setting_id": "YLOcbaCppSmQ1twv", + "name": "QIDI ABS Odorless @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ABS Odorless@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "24.5" + ], + "nozzle_temperature": [ + "250" + ], + "pressure_advance": [ + "0.014" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ABS Odorless @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ABS Odorless @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..912dc04ed7 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ABS Odorless @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,23 @@ +{ + "type": "filament", + "setting_id": "Klsu6iALsWgbg3vK", + "name": "QIDI ABS Odorless @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ABS Odorless@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "24.5" + ], + "nozzle_temperature": [ + "250" + ], + "pressure_advance": [ + "0.011" + ], + "slow_down_min_speed": [ + "10" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ABS Odorless @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI ABS Odorless @X-Plus 5.json new file mode 100644 index 0000000000..a2f14984a0 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ABS Odorless @X-Plus 5.json @@ -0,0 +1,108 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_14", + "name": "QIDI ABS Odorless@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "45" + ], + "chamber_temperatures": [ + "55" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "80" + ], + "cool_plate_temp": [ + "80" + ], + "eng_plate_temp_initial_layer": [ + "90" + ], + "eng_plate_temp": [ + "90" + ], + "during_print_exhaust_fan_speed": [ + "0" + ], + "fan_max_speed": [ + "80" + ], + "fan_min_speed": [ + "10" + ], + "filament_adhesiveness_category": [ + "200" + ], + "filament_density": [ + "1.02" + ], + "filament_flow_ratio": [ + "0.92" + ], + "filament_max_volumetric_speed": [ + "22" + ], + "filament_type": [ + "ABS" + ], + "hot_plate_temp_initial_layer": [ + "90" + ], + "hot_plate_temp": [ + "90" + ], + "impact_strength_z": [ + "7.4" + ], + "nozzle_temperature_initial_layer": [ + "250" + ], + "nozzle_temperature_range_high": [ + "280" + ], + "nozzle_temperature_range_low": [ + "240" + ], + "nozzle_temperature": [ + "260" + ], + "overhang_fan_speed": [ + "80" + ], + "overhang_fan_threshold": [ + "25%" + ], + "pressure_advance": [ + "0.035" + ], + "slow_down_layer_time": [ + "4" + ], + "temperature_vitrification": [ + "100" + ], + "supertack_plate_temp_initial_layer": [ + "80" + ], + "supertack_plate_temp": [ + "80" + ], + "textured_plate_temp_initial_layer": [ + "90" + ], + "textured_plate_temp": [ + "90" + ], + "textured_cool_plate_temp_initial_layer": [ + "80" + ], + "textured_cool_plate_temp": [ + "80" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..99a9e16c08 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,20 @@ +{ + "type": "filament", + "setting_id": "bTwNiMSHwex0gxrd", + "name": "QIDI ABS Rapido @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ABS Rapido@X-Plus 5-Series", + "chamber_temperatures": [ + "0" + ], + "filament_max_volumetric_speed": [ + "2" + ], + "pressure_advance": [ + "0.03" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..30086f5dce --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "mcPlHcr33zPv3aIf", + "name": "QIDI ABS Rapido @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ABS Rapido@X-Plus 5-Series", + "pressure_advance": [ + "0.03" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..47278a61b1 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "0np334pRpl3qa8Uq", + "name": "QIDI ABS Rapido @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ABS Rapido@X-Plus 5-Series", + "nozzle_temperature": [ + "250" + ], + "pressure_advance": [ + "0.014" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..f79fcfdfac --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,20 @@ +{ + "type": "filament", + "setting_id": "Vp2bJxFhsPvW6K0P", + "name": "QIDI ABS Rapido @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ABS Rapido@X-Plus 5-Series", + "nozzle_temperature": [ + "250" + ], + "pressure_advance": [ + "0.011" + ], + "slow_down_min_speed": [ + "10" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido @X-Plus 5.json new file mode 100644 index 0000000000..0ecb9c656c --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido @X-Plus 5.json @@ -0,0 +1,105 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_11", + "name": "QIDI ABS Rapido@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "45" + ], + "chamber_temperatures": [ + "55" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "80" + ], + "cool_plate_temp": [ + "80" + ], + "eng_plate_temp_initial_layer": [ + "90" + ], + "eng_plate_temp": [ + "90" + ], + "during_print_exhaust_fan_speed": [ + "0" + ], + "fan_max_speed": [ + "80" + ], + "fan_min_speed": [ + "20" + ], + "filament_adhesiveness_category": [ + "200" + ], + "filament_density": [ + "1.05" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_type": [ + "ABS" + ], + "hot_plate_temp_initial_layer": [ + "90" + ], + "hot_plate_temp": [ + "90" + ], + "impact_strength_z": [ + "7.4" + ], + "nozzle_temperature_initial_layer": [ + "250" + ], + "nozzle_temperature_range_high": [ + "280" + ], + "nozzle_temperature_range_low": [ + "240" + ], + "nozzle_temperature": [ + "260" + ], + "overhang_fan_speed": [ + "80" + ], + "overhang_fan_threshold": [ + "25%" + ], + "pressure_advance": [ + "0.035" + ], + "slow_down_layer_time": [ + "4" + ], + "supertack_plate_temp_initial_layer": [ + "80" + ], + "supertack_plate_temp": [ + "80" + ], + "temperature_vitrification": [ + "100" + ], + "textured_plate_temp_initial_layer": [ + "90" + ], + "textured_plate_temp": [ + "90" + ], + "textured_cool_plate_temp_initial_layer": [ + "80" + ], + "textured_cool_plate_temp": [ + "80" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido Metal @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido Metal @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..34632b393f --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido Metal @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,20 @@ +{ + "type": "filament", + "setting_id": "U69dFGlkEk9twDF6", + "name": "QIDI ABS Rapido Metal @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ABS Rapido Metal@X-Plus 5-Series", + "chamber_temperatures": [ + "0" + ], + "filament_max_volumetric_speed": [ + "2" + ], + "pressure_advance": [ + "0.03" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido Metal @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido Metal @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..59492201a5 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido Metal @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "XaJbATawtVrUsXpz", + "name": "QIDI ABS Rapido Metal @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ABS Rapido Metal@X-Plus 5-Series", + "pressure_advance": [ + "0.03" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido Metal @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido Metal @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..93b4cb3aba --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido Metal @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "Xb0A7YqmqP2peSM3", + "name": "QIDI ABS Rapido Metal @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ABS Rapido Metal@X-Plus 5-Series", + "nozzle_temperature": [ + "250" + ], + "pressure_advance": [ + "0.016" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido Metal @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido Metal @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..c4edce73be --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido Metal @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,20 @@ +{ + "type": "filament", + "setting_id": "4AA9wGDNebbTHf4X", + "name": "QIDI ABS Rapido Metal @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ABS Rapido Metal@X-Plus 5-Series", + "nozzle_temperature": [ + "250" + ], + "pressure_advance": [ + "0.008" + ], + "slow_down_min_speed": [ + "10" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido Metal @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido Metal @X-Plus 5.json new file mode 100644 index 0000000000..f3603eda99 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ABS Rapido Metal @X-Plus 5.json @@ -0,0 +1,105 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_13", + "name": "QIDI ABS Rapido Metal@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "45" + ], + "chamber_temperatures": [ + "55" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "80" + ], + "cool_plate_temp": [ + "80" + ], + "eng_plate_temp_initial_layer": [ + "90" + ], + "eng_plate_temp": [ + "90" + ], + "during_print_exhaust_fan_speed": [ + "0" + ], + "fan_max_speed": [ + "80" + ], + "fan_min_speed": [ + "20" + ], + "filament_adhesiveness_category": [ + "200" + ], + "filament_density": [ + "1.06" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_type": [ + "ABS" + ], + "hot_plate_temp_initial_layer": [ + "90" + ], + "hot_plate_temp": [ + "90" + ], + "impact_strength_z": [ + "7.4" + ], + "nozzle_temperature_initial_layer": [ + "250" + ], + "nozzle_temperature_range_high": [ + "280" + ], + "nozzle_temperature_range_low": [ + "240" + ], + "nozzle_temperature": [ + "260" + ], + "overhang_fan_speed": [ + "80" + ], + "overhang_fan_threshold": [ + "25%" + ], + "pressure_advance": [ + "0.035" + ], + "slow_down_layer_time": [ + "4" + ], + "supertack_plate_temp_initial_layer": [ + "80" + ], + "supertack_plate_temp": [ + "80" + ], + "temperature_vitrification": [ + "100" + ], + "textured_plate_temp_initial_layer": [ + "90" + ], + "textured_plate_temp": [ + "90" + ], + "textured_cool_plate_temp_initial_layer": [ + "80" + ], + "textured_cool_plate_temp": [ + "80" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ABS-GF @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ABS-GF @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..d81d623d33 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ABS-GF @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "V9h7YWcaTejmFCh0", + "name": "QIDI ABS-GF @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ABS-GF@X-Plus 5-Series", + "pressure_advance": [ + "0.03" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ABS-GF @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ABS-GF @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..88ee5f2146 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ABS-GF @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "hO8H1GUCzMkwFtQH", + "name": "QIDI ABS-GF @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ABS-GF@X-Plus 5-Series", + "pressure_advance": [ + "0.01" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ABS-GF @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ABS-GF @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..997e67a0f0 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ABS-GF @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "LskHdeEngKlRCSmg", + "name": "QIDI ABS-GF @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ABS-GF@X-Plus 5-Series", + "pressure_advance": [ + "0.01" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ABS-GF @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI ABS-GF @X-Plus 5.json new file mode 100644 index 0000000000..2ab2fbfc84 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ABS-GF @X-Plus 5.json @@ -0,0 +1,114 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_12", + "name": "QIDI ABS-GF@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "45" + ], + "box_temperature_range_low": [ + "0" + ], + "box_temperature": [ + "45" + ], + "chamber_temperatures": [ + "0" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "80" + ], + "cool_plate_temp": [ + "80" + ], + "eng_plate_temp_initial_layer": [ + "90" + ], + "eng_plate_temp": [ + "90" + ], + "during_print_exhaust_fan_speed": [ + "0" + ], + "fan_max_speed": [ + "20" + ], + "fan_min_speed": [ + "20" + ], + "filament_adhesiveness_category": [ + "200" + ], + "filament_density": [ + "1.15" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_max_volumetric_speed": [ + "12" + ], + "filament_type": [ + "ABS-GF" + ], + "hot_plate_temp_initial_layer": [ + "90" + ], + "hot_plate_temp": [ + "90" + ], + "impact_strength_z": [ + "5.3" + ], + "nozzle_temperature_initial_layer": [ + "260" + ], + "nozzle_temperature_range_high": [ + "280" + ], + "nozzle_temperature_range_low": [ + "240" + ], + "nozzle_temperature": [ + "270" + ], + "overhang_fan_speed": [ + "80" + ], + "overhang_fan_threshold": [ + "25%" + ], + "pressure_advance": [ + "0.035" + ], + "slow_down_layer_time": [ + "5" + ], + "supertack_plate_temp_initial_layer": [ + "80" + ], + "supertack_plate_temp": [ + "80" + ], + "temperature_vitrification": [ + "100" + ], + "textured_plate_temp_initial_layer": [ + "90" + ], + "textured_plate_temp": [ + "90" + ], + "textured_cool_plate_temp_initial_layer": [ + "80" + ], + "textured_cool_plate_temp": [ + "80" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ASA @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ASA @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..d417265f5a --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ASA @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,20 @@ +{ + "type": "filament", + "setting_id": "FgeIGgT8xZCm4UO8", + "name": "QIDI ASA @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ASA@X-Plus 5-Series", + "chamber_temperatures": [ + "0" + ], + "filament_max_volumetric_speed": [ + "2" + ], + "pressure_advance": [ + "0.03" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ASA @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ASA @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..ed86d714a1 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ASA @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "VRicNQizOCKkgLBJ", + "name": "QIDI ASA @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ASA@X-Plus 5-Series", + "pressure_advance": [ + "0.03" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ASA @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ASA @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..a7870accce --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ASA @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "KDjln7WdhhbSztX3", + "name": "QIDI ASA @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ASA@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "13" + ], + "pressure_advance": [ + "0.014" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ASA @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ASA @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..30496917d1 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ASA @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,20 @@ +{ + "type": "filament", + "setting_id": "X9TOBuOKBbJIoF7y", + "name": "QIDI ASA @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ASA@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "13" + ], + "pressure_advance": [ + "0.011" + ], + "slow_down_min_speed": [ + "10" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ASA @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI ASA @X-Plus 5.json new file mode 100644 index 0000000000..97127fe57b --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ASA @X-Plus 5.json @@ -0,0 +1,111 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_18", + "name": "QIDI ASA@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "45" + ], + "chamber_temperatures": [ + "55" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "80" + ], + "cool_plate_temp": [ + "80" + ], + "eng_plate_temp_initial_layer": [ + "90" + ], + "eng_plate_temp": [ + "90" + ], + "during_print_exhaust_fan_speed": [ + "0" + ], + "fan_cooling_layer_time": [ + "40" + ], + "fan_max_speed": [ + "50" + ], + "fan_min_speed": [ + "10" + ], + "filament_adhesiveness_category": [ + "200" + ], + "filament_density": [ + "1.07" + ], + "filament_flow_ratio": [ + "0.92" + ], + "filament_max_volumetric_speed": [ + "16" + ], + "filament_type": [ + "ASA" + ], + "hot_plate_temp_initial_layer": [ + "90" + ], + "hot_plate_temp": [ + "90" + ], + "impact_strength_z": [ + "4.9" + ], + "nozzle_temperature_initial_layer": [ + "250" + ], + "nozzle_temperature_range_high": [ + "280" + ], + "nozzle_temperature_range_low": [ + "240" + ], + "nozzle_temperature": [ + "255" + ], + "overhang_fan_speed": [ + "80" + ], + "overhang_fan_threshold": [ + "25%" + ], + "pressure_advance": [ + "0.035" + ], + "slow_down_layer_time": [ + "4" + ], + "supertack_plate_temp_initial_layer": [ + "80" + ], + "supertack_plate_temp": [ + "80" + ], + "temperature_vitrification": [ + "100" + ], + "textured_plate_temp_initial_layer": [ + "90" + ], + "textured_plate_temp": [ + "90" + ], + "textured_cool_plate_temp_initial_layer": [ + "80" + ], + "textured_cool_plate_temp": [ + "80" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ASA-Aero @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ASA-Aero @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..08a4ed22a9 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ASA-Aero @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "g5F7NP83yx0Iub4c", + "name": "QIDI ASA-Aero @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ASA-Aero@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ASA-Aero @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI ASA-Aero @X-Plus 5.json new file mode 100644 index 0000000000..14423fd58c --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ASA-Aero @X-Plus 5.json @@ -0,0 +1,126 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_19", + "name": "QIDI ASA-Aero@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "45" + ], + "chamber_temperatures": [ + "60" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "80" + ], + "cool_plate_temp": [ + "80" + ], + "eng_plate_temp_initial_layer": [ + "90" + ], + "eng_plate_temp": [ + "90" + ], + "during_print_exhaust_fan_speed": [ + "0" + ], + "fan_cooling_layer_time": [ + "40" + ], + "fan_max_speed": [ + "50" + ], + "fan_min_speed": [ + "10" + ], + "filament_adhesiveness_category": [ + "200" + ], + "filament_density": [ + "1.03" + ], + "filament_flow_ratio": [ + "0.7" + ], + "filament_max_volumetric_speed": [ + "12" + ], + "filament_retract_when_changing_layer": [ + "0" + ], + "filament_retraction_length": [ + "0.01" + ], + "filament_retraction_minimum_travel": [ + "0" + ], + "filament_type": [ + "ASA-AERO" + ], + "hot_plate_temp_initial_layer": [ + "90" + ], + "hot_plate_temp": [ + "90" + ], + "filament_wipe": [ + "0" + ], + "filament_z_hop": [ + "0" + ], + "impact_strength_z": [ + "3.4" + ], + "nozzle_temperature_initial_layer": [ + "250" + ], + "nozzle_temperature_range_high": [ + "280" + ], + "nozzle_temperature_range_low": [ + "240" + ], + "nozzle_temperature": [ + "260" + ], + "overhang_fan_speed": [ + "80" + ], + "overhang_fan_threshold": [ + "25%" + ], + "pressure_advance": [ + "0.021" + ], + "slow_down_layer_time": [ + "4" + ], + "supertack_plate_temp_initial_layer": [ + "80" + ], + "supertack_plate_temp": [ + "80" + ], + "temperature_vitrification": [ + "100" + ], + "textured_plate_temp_initial_layer": [ + "90" + ], + "textured_plate_temp": [ + "90" + ], + "textured_cool_plate_temp_initial_layer": [ + "80" + ], + "textured_cool_plate_temp": [ + "80" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ASA-CF @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ASA-CF @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..be97fdfd58 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ASA-CF @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "Aqh00EJ8ZaiPCU6F", + "name": "QIDI ASA-CF @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ASA-CF@X-Plus 5-Series", + "pressure_advance": [ + "0.03" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ASA-CF @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ASA-CF @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..cd8bbc98bd --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ASA-CF @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "fgwkL5igRvM0leJf", + "name": "QIDI ASA-CF @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ASA-CF@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "13" + ], + "pressure_advance": [ + "0.014" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ASA-CF @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI ASA-CF @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..3095a16138 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ASA-CF @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,20 @@ +{ + "type": "filament", + "setting_id": "bDAcTzwdiXT4fFnT", + "name": "QIDI ASA-CF @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI ASA-CF@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "13" + ], + "pressure_advance": [ + "0.011" + ], + "slow_down_min_speed": [ + "10" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI ASA-CF @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI ASA-CF @X-Plus 5.json new file mode 100644 index 0000000000..9a8958c860 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI ASA-CF @X-Plus 5.json @@ -0,0 +1,111 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_20", + "name": "QIDI ASA-CF@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "45" + ], + "chamber_temperatures": [ + "55" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "80" + ], + "cool_plate_temp": [ + "80" + ], + "eng_plate_temp_initial_layer": [ + "90" + ], + "eng_plate_temp": [ + "90" + ], + "during_print_exhaust_fan_speed": [ + "0" + ], + "fan_cooling_layer_time": [ + "35" + ], + "fan_max_speed": [ + "25" + ], + "fan_min_speed": [ + "10" + ], + "filament_adhesiveness_category": [ + "200" + ], + "filament_density": [ + "1.07" + ], + "filament_flow_ratio": [ + "0.9" + ], + "filament_max_volumetric_speed": [ + "18" + ], + "filament_type": [ + "ASA-CF" + ], + "hot_plate_temp_initial_layer": [ + "90" + ], + "hot_plate_temp": [ + "90" + ], + "impact_strength_z": [ + "4.9" + ], + "nozzle_temperature_initial_layer": [ + "275" + ], + "nozzle_temperature_range_high": [ + "280" + ], + "nozzle_temperature_range_low": [ + "260" + ], + "nozzle_temperature": [ + "275" + ], + "overhang_fan_speed": [ + "80" + ], + "overhang_fan_threshold": [ + "25%" + ], + "pressure_advance": [ + "0.03" + ], + "slow_down_layer_time": [ + "12" + ], + "supertack_plate_temp_initial_layer": [ + "80" + ], + "supertack_plate_temp": [ + "80" + ], + "temperature_vitrification": [ + "100" + ], + "textured_plate_temp_initial_layer": [ + "90" + ], + "textured_plate_temp": [ + "90" + ], + "textured_cool_plate_temp_initial_layer": [ + "80" + ], + "textured_cool_plate_temp": [ + "80" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PA12-CF @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PA12-CF @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..165c5cfb62 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PA12-CF @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "vDXAb7Kkj7nFc8go", + "name": "QIDI PA12-CF @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PA12-CF@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PA12-CF @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PA12-CF @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..ad26af1f86 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PA12-CF @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "KjZ3jgo2Zoklkk0F", + "name": "QIDI PA12-CF @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PA12-CF@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PA12-CF @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PA12-CF @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..5324f0c183 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PA12-CF @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "Xcl3UPe7tmXw9hXa", + "name": "QIDI PA12-CF @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PA12-CF@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PA12-CF @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PA12-CF @X-Plus 5.json new file mode 100644 index 0000000000..3464bed9a3 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PA12-CF @X-Plus 5.json @@ -0,0 +1,111 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_27", + "name": "QIDI PA12-CF@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "65" + ], + "box_temperature_range_low": [ + "0" + ], + "box_temperature": [ + "55" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "cool_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "eng_plate_temp": [ + "80" + ], + "fan_cooling_layer_time": [ + "10" + ], + "fan_max_speed": [ + "40" + ], + "fan_min_speed": [ + "20" + ], + "filament_adhesiveness_category": [ + "400" + ], + "filament_density": [ + "1.09" + ], + "filament_flow_ratio": [ + "0.96" + ], + "filament_max_volumetric_speed": [ + "8" + ], + "filament_type": [ + "PA12-CF" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "hot_plate_temp": [ + "80" + ], + "impact_strength_z": [ + "5.7" + ], + "nozzle_temperature_initial_layer": [ + "280" + ], + "nozzle_temperature_range_high": [ + "300" + ], + "nozzle_temperature_range_low": [ + "280" + ], + "nozzle_temperature": [ + "280" + ], + "overhang_fan_speed": [ + "40" + ], + "overhang_fan_threshold": [ + "0%" + ], + "pressure_advance": [ + "0.035" + ], + "slow_down_layer_time": [ + "5" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], + "supertack_plate_temp": [ + "60" + ], + "temperature_vitrification": [ + "100" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "textured_cool_plate_temp_initial_layer": [ + "60" + ], + "textured_cool_plate_temp": [ + "60" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PA6-CF @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PA6-CF @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..fbe5870078 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PA6-CF @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "qTjsP8oR1sbh56Gh", + "name": "QIDI PA6-CF @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PA6-CF@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PA6-CF @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PA6-CF @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..016f9bb2e4 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PA6-CF @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "4kzkdzJmDYGic0aa", + "name": "QIDI PA6-CF @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PA6-CF@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PA6-CF @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PA6-CF @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..ba0d981c29 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PA6-CF @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "97peVPSildpjdDQj", + "name": "QIDI PA6-CF @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PA6-CF@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PA6-CF @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PA6-CF @X-Plus 5.json new file mode 100644 index 0000000000..9b6a66f61b --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PA6-CF @X-Plus 5.json @@ -0,0 +1,111 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_25", + "name": "QIDI PA6-CF@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "65" + ], + "box_temperature_range_low": [ + "0" + ], + "box_temperature": [ + "55" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "cool_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "eng_plate_temp": [ + "80" + ], + "fan_cooling_layer_time": [ + "10" + ], + "fan_max_speed": [ + "40" + ], + "fan_min_speed": [ + "20" + ], + "filament_adhesiveness_category": [ + "400" + ], + "filament_density": [ + "1.09" + ], + "filament_flow_ratio": [ + "0.96" + ], + "filament_max_volumetric_speed": [ + "8" + ], + "filament_type": [ + "PA6-CF" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "hot_plate_temp": [ + "80" + ], + "impact_strength_z": [ + "5.7" + ], + "nozzle_temperature_initial_layer": [ + "280" + ], + "nozzle_temperature_range_high": [ + "300" + ], + "nozzle_temperature_range_low": [ + "280" + ], + "nozzle_temperature": [ + "280" + ], + "overhang_fan_speed": [ + "40" + ], + "overhang_fan_threshold": [ + "0%" + ], + "pressure_advance": [ + "0.035" + ], + "slow_down_layer_time": [ + "5" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], + "supertack_plate_temp": [ + "60" + ], + "temperature_vitrification": [ + "100" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "textured_cool_plate_temp_initial_layer": [ + "60" + ], + "textured_cool_plate_temp": [ + "60" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PAHT-CF @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PAHT-CF @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..68d604414d --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PAHT-CF @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "3iw5hq0KFa9NU4b3", + "name": "QIDI PAHT-CF @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PAHT-CF@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PAHT-CF @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PAHT-CF @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..0ca7290e39 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PAHT-CF @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "7eBDjJbetxOC3aNT", + "name": "QIDI PAHT-CF @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PAHT-CF@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PAHT-CF @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PAHT-CF @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..b3b88d5f28 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PAHT-CF @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "hkIpB0QZOGZpoYFX", + "name": "QIDI PAHT-CF @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PAHT-CF@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PAHT-CF @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PAHT-CF @X-Plus 5.json new file mode 100644 index 0000000000..fcfccfe0f6 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PAHT-CF @X-Plus 5.json @@ -0,0 +1,111 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_30", + "name": "QIDI PAHT-CF@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "65" + ], + "box_temperature_range_low": [ + "0" + ], + "box_temperature": [ + "60" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "80" + ], + "cool_plate_temp": [ + "80" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "eng_plate_temp": [ + "80" + ], + "fan_cooling_layer_time": [ + "10" + ], + "fan_max_speed": [ + "40" + ], + "fan_min_speed": [ + "20" + ], + "filament_adhesiveness_category": [ + "400" + ], + "filament_density": [ + "1.2" + ], + "filament_flow_ratio": [ + "0.96" + ], + "filament_max_volumetric_speed": [ + "8" + ], + "filament_type": [ + "PAHT-CF" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "hot_plate_temp": [ + "80" + ], + "impact_strength_z": [ + "13.3" + ], + "nozzle_temperature_initial_layer": [ + "300" + ], + "nozzle_temperature_range_high": [ + "320" + ], + "nozzle_temperature_range_low": [ + "300" + ], + "nozzle_temperature": [ + "300" + ], + "overhang_fan_speed": [ + "40" + ], + "overhang_fan_threshold": [ + "0%" + ], + "pressure_advance": [ + "0.032" + ], + "slow_down_layer_time": [ + "5" + ], + "supertack_plate_temp_initial_layer": [ + "80" + ], + "supertack_plate_temp": [ + "80" + ], + "temperature_vitrification": [ + "180" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "textured_cool_plate_temp_initial_layer": [ + "80" + ], + "textured_cool_plate_temp": [ + "80" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PAHT-GF @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PAHT-GF @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..9638d33afc --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PAHT-GF @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "R7cctkITobdKgMkY", + "name": "QIDI PAHT-GF @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PAHT-GF@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PAHT-GF @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PAHT-GF @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..4d926ff370 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PAHT-GF @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "cVzpHjaSzsDm0bwG", + "name": "QIDI PAHT-GF @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PAHT-GF@X-Plus 5-Series", + "pressure_advance": [ + "0.015" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PAHT-GF @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PAHT-GF @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..ce49e3ef04 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PAHT-GF @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "N0aUkpcJD6CvODci", + "name": "QIDI PAHT-GF @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PAHT-GF@X-Plus 5-Series", + "pressure_advance": [ + "0.01" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PAHT-GF @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PAHT-GF @X-Plus 5.json new file mode 100644 index 0000000000..3c0f1056ed --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PAHT-GF @X-Plus 5.json @@ -0,0 +1,111 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_31", + "name": "QIDI PAHT-GF@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "65" + ], + "box_temperature_range_low": [ + "0" + ], + "box_temperature": [ + "60" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "80" + ], + "cool_plate_temp": [ + "80" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "eng_plate_temp": [ + "80" + ], + "fan_cooling_layer_time": [ + "10" + ], + "fan_max_speed": [ + "20" + ], + "fan_min_speed": [ + "20" + ], + "filament_adhesiveness_category": [ + "400" + ], + "filament_density": [ + "1.27" + ], + "filament_flow_ratio": [ + "0.96" + ], + "filament_max_volumetric_speed": [ + "10" + ], + "filament_type": [ + "PAHT-GF" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "hot_plate_temp": [ + "80" + ], + "impact_strength_z": [ + "13.3" + ], + "nozzle_temperature_initial_layer": [ + "300" + ], + "nozzle_temperature_range_high": [ + "320" + ], + "nozzle_temperature_range_low": [ + "300" + ], + "nozzle_temperature": [ + "300" + ], + "overhang_fan_speed": [ + "40" + ], + "overhang_fan_threshold": [ + "0%" + ], + "pressure_advance": [ + "0.027" + ], + "slow_down_layer_time": [ + "5" + ], + "supertack_plate_temp_initial_layer": [ + "80" + ], + "supertack_plate_temp": [ + "80" + ], + "temperature_vitrification": [ + "180" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "textured_cool_plate_temp_initial_layer": [ + "80" + ], + "textured_cool_plate_temp": [ + "80" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PC-ABS-FR @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PC-ABS-FR @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..e4842206ed --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PC-ABS-FR @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "uYwbMiemSo7LxiqR", + "name": "QIDI PC/ABS-FR @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PC/ABS-FR@X-Plus 5-Series", + "pressure_advance": [ + "0.042" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PC-ABS-FR @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PC-ABS-FR @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..35c317e8d3 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PC-ABS-FR @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "Fxkv0PN8RU17fkB0", + "name": "QIDI PC/ABS-FR @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PC/ABS-FR@X-Plus 5-Series", + "pressure_advance": [ + "0.031" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PC-ABS-FR @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PC-ABS-FR @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..456a93a6d0 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PC-ABS-FR @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "oKNGzYqymTnWQsBB", + "name": "QIDI PC/ABS-FR @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PC/ABS-FR@X-Plus 5-Series", + "pressure_advance": [ + "0.024" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PC-ABS-FR @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PC-ABS-FR @X-Plus 5.json new file mode 100644 index 0000000000..b6c6fb3b64 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PC-ABS-FR @X-Plus 5.json @@ -0,0 +1,111 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_34", + "name": "QIDI PC/ABS-FR@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "50" + ], + "chamber_temperatures": [ + "55" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "65" + ], + "cool_plate_temp": [ + "65" + ], + "eng_plate_temp_initial_layer": [ + "100" + ], + "eng_plate_temp": [ + "100" + ], + "during_print_exhaust_fan_speed": [ + "0" + ], + "fan_cooling_layer_time": [ + "30" + ], + "fan_max_speed": [ + "40" + ], + "fan_min_speed": [ + "10" + ], + "filament_adhesiveness_category": [ + "200" + ], + "filament_density": [ + "1.19" + ], + "filament_flow_ratio": [ + "0.92" + ], + "filament_max_volumetric_speed": [ + "8" + ], + "filament_type": [ + "PC-ABS-FR" + ], + "hot_plate_temp_initial_layer": [ + "100" + ], + "hot_plate_temp": [ + "100" + ], + "impact_strength_z": [ + "8" + ], + "nozzle_temperature_initial_layer": [ + "250" + ], + "nozzle_temperature_range_high": [ + "280" + ], + "nozzle_temperature_range_low": [ + "240" + ], + "nozzle_temperature": [ + "250" + ], + "overhang_fan_speed": [ + "90" + ], + "overhang_fan_threshold": [ + "75%" + ], + "pressure_advance": [ + "0.082" + ], + "slow_down_layer_time": [ + "4" + ], + "supertack_plate_temp_initial_layer": [ + "65" + ], + "supertack_plate_temp": [ + "65" + ], + "temperature_vitrification": [ + "100" + ], + "textured_plate_temp_initial_layer": [ + "100" + ], + "textured_plate_temp": [ + "100" + ], + "textured_cool_plate_temp_initial_layer": [ + "65" + ], + "textured_cool_plate_temp": [ + "65" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PEBA 95A @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PEBA 95A @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..5fa07706a9 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PEBA 95A @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "hgdi1oL4EFzh0h7h", + "name": "QIDI PEBA 95A @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PEBA 95A@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PEBA 95A @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PEBA 95A @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..f44261951e --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PEBA 95A @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "iNZpBz1iklW3sJlj", + "name": "QIDI PEBA 95A @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PEBA 95A@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PEBA 95A @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PEBA 95A @X-Plus 5.json new file mode 100644 index 0000000000..43410eabe1 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PEBA 95A @X-Plus 5.json @@ -0,0 +1,96 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_36", + "name": "QIDI PEBA 95A@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "cool_plate_temp_initial_layer": [ + "30" + ], + "cool_plate_temp": [ + "30" + ], + "eng_plate_temp_initial_layer": [ + "35" + ], + "eng_plate_temp": [ + "35" + ], + "fan_cooling_layer_time": [ + "100" + ], + "filament_adhesiveness_category": [ + "600" + ], + "filament_density": [ + "1.15" + ], + "filament_flow_ratio": [ + "1" + ], + "filament_max_volumetric_speed": [ + "6" + ], + "filament_retraction_length": [ + "0.8" + ], + "filament_type": [ + "PEBA" + ], + "filament_vendor": [ + "QIDI" + ], + "filament_z_hop": [ + "0" + ], + "hot_plate_temp_initial_layer": [ + "35" + ], + "hot_plate_temp": [ + "35" + ], + "impact_strength_z": [ + "88.7" + ], + "nozzle_temperature_initial_layer": [ + "250" + ], + "nozzle_temperature_range_high": [ + "260" + ], + "nozzle_temperature_range_low": [ + "230" + ], + "nozzle_temperature": [ + "250" + ], + "pressure_advance": [ + "0.04" + ], + "slow_down_layer_time": [ + "14" + ], + "supertack_plate_temp_initial_layer": [ + "30" + ], + "supertack_plate_temp": [ + "30" + ], + "temperature_vitrification": [ + "30" + ], + "textured_plate_temp_initial_layer": [ + "35" + ], + "textured_plate_temp": [ + "35" + ], + "textured_cool_plate_temp_initial_layer": [ + "30" + ], + "textured_cool_plate_temp": [ + "30" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PET-CF @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PET-CF @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..bfdd1aed31 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PET-CF @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "bILtWMlzBPi4Ft8a", + "name": "QIDI PET-CF @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PET-CF@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PET-CF @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PET-CF @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..cb78cf3afc --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PET-CF @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "E0Y6g3qpoERrgSUH", + "name": "QIDI PET-CF @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PET-CF@X-Plus 5-Series", + "pressure_advance": [ + "0.025" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PET-CF @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PET-CF @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..ae40d594ff --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PET-CF @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "e2HuHVJZck2nKANa", + "name": "QIDI PET-CF @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PET-CF@X-Plus 5-Series", + "pressure_advance": [ + "0.025" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PET-CF @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PET-CF @X-Plus 5.json new file mode 100644 index 0000000000..162cbec691 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PET-CF @X-Plus 5.json @@ -0,0 +1,111 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_37", + "name": "QIDI PET-CF@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "65" + ], + "box_temperature_range_low": [ + "0" + ], + "box_temperature": [ + "55" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "70" + ], + "cool_plate_temp": [ + "70" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "eng_plate_temp": [ + "80" + ], + "fan_cooling_layer_time": [ + "10" + ], + "fan_max_speed": [ + "40" + ], + "fan_min_speed": [ + "20" + ], + "filament_adhesiveness_category": [ + "800" + ], + "filament_density": [ + "1.3" + ], + "filament_flow_ratio": [ + "1" + ], + "filament_max_volumetric_speed": [ + "8" + ], + "filament_type": [ + "PET-CF" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "hot_plate_temp": [ + "80" + ], + "impact_strength_z": [ + "4.5" + ], + "nozzle_temperature_initial_layer": [ + "280" + ], + "nozzle_temperature_range_high": [ + "320" + ], + "nozzle_temperature_range_low": [ + "280" + ], + "nozzle_temperature": [ + "280" + ], + "overhang_fan_speed": [ + "40" + ], + "overhang_fan_threshold": [ + "0%" + ], + "pressure_advance": [ + "0.032" + ], + "slow_down_layer_time": [ + "5" + ], + "temperature_vitrification": [ + "185" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "supertack_plate_temp_initial_layer": [ + "70" + ], + "supertack_plate_temp": [ + "70" + ], + "textured_cool_plate_temp_initial_layer": [ + "70" + ], + "textured_cool_plate_temp": [ + "70" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PET-GF @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PET-GF @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..97029aeb25 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PET-GF @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "2NHbKFmgDoN1oHlo", + "name": "QIDI PET-GF @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PET-GF@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PET-GF @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PET-GF @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..480d13bb65 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PET-GF @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "cMlzOMKIIIro4H62", + "name": "QIDI PET-GF @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PET-GF@X-Plus 5-Series", + "pressure_advance": [ + "0.014" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PET-GF @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PET-GF @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..2cc0cfe2cb --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PET-GF @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "evxG7nCDQx2Y5UV5", + "name": "QIDI PET-GF @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PET-GF@X-Plus 5-Series", + "pressure_advance": [ + "0.01" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PET-GF @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PET-GF @X-Plus 5.json new file mode 100644 index 0000000000..84310856cc --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PET-GF @X-Plus 5.json @@ -0,0 +1,111 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_38", + "name": "QIDI PET-GF@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "50" + ], + "box_temperature_range_low": [ + "0" + ], + "box_temperature": [ + "50" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "70" + ], + "cool_plate_temp": [ + "70" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "eng_plate_temp": [ + "80" + ], + "fan_cooling_layer_time": [ + "10" + ], + "fan_max_speed": [ + "20" + ], + "fan_min_speed": [ + "20" + ], + "filament_adhesiveness_category": [ + "800" + ], + "filament_density": [ + "1.38" + ], + "filament_flow_ratio": [ + "0.97" + ], + "filament_max_volumetric_speed": [ + "10" + ], + "filament_type": [ + "PET-GF" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "hot_plate_temp": [ + "80" + ], + "impact_strength_z": [ + "4.5" + ], + "nozzle_temperature_initial_layer": [ + "300" + ], + "nozzle_temperature_range_high": [ + "320" + ], + "nozzle_temperature_range_low": [ + "280" + ], + "nozzle_temperature": [ + "300" + ], + "overhang_fan_speed": [ + "40" + ], + "overhang_fan_threshold": [ + "0%" + ], + "pressure_advance": [ + "0.022" + ], + "slow_down_layer_time": [ + "5" + ], + "supertack_plate_temp_initial_layer": [ + "70" + ], + "supertack_plate_temp": [ + "70" + ], + "temperature_vitrification": [ + "185" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "textured_cool_plate_temp_initial_layer": [ + "70" + ], + "textured_cool_plate_temp": [ + "70" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG Basic @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG Basic @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..6689cca71a --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG Basic @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "k4jOZFY5QV2bMKHK", + "name": "QIDI PETG Basic @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG Basic@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "1" + ], + "pressure_advance": [ + "0.054" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG Basic @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG Basic @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..c1b29f1b75 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG Basic @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "b55sLpGKCrWLiDPf", + "name": "QIDI PETG Basic @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG Basic@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG Basic @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG Basic @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..3be97a337f --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG Basic @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "XcxbsZ4d8Dh21WX0", + "name": "QIDI PETG Basic @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG Basic@X-Plus 5-Series", + "pressure_advance": [ + "0.04" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG Basic @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG Basic @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..92a57fb1b8 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG Basic @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "9MEoOjLU7AOCvb9X", + "name": "QIDI PETG Basic @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG Basic@X-Plus 5-Series", + "pressure_advance": [ + "0.04" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG Basic @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PETG Basic @X-Plus 5.json new file mode 100644 index 0000000000..46810f49c3 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG Basic @X-Plus 5.json @@ -0,0 +1,102 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_39", + "name": "QIDI PETG Basic@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "45" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "cool_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "eng_plate_temp": [ + "80" + ], + "fan_cooling_layer_time": [ + "30" + ], + "fan_max_speed": [ + "40" + ], + "fan_min_speed": [ + "10" + ], + "filament_adhesiveness_category": [ + "300" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_max_volumetric_speed": [ + "15" + ], + "filament_type": [ + "PETG" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "hot_plate_temp": [ + "80" + ], + "impact_strength_z": [ + "10.6" + ], + "nozzle_temperature_initial_layer": [ + "250" + ], + "nozzle_temperature_range_high": [ + "280" + ], + "nozzle_temperature_range_low": [ + "240" + ], + "nozzle_temperature": [ + "250" + ], + "overhang_fan_speed": [ + "90" + ], + "overhang_fan_threshold": [ + "10%" + ], + "pressure_advance": [ + "0.054" + ], + "slow_down_layer_time": [ + "12" + ], + "temperature_vitrification": [ + "70" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], + "supertack_plate_temp": [ + "60" + ], + "textured_cool_plate_temp_initial_layer": [ + "60" + ], + "textured_cool_plate_temp": [ + "60" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG Rapido @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG Rapido @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..220a6ce55a --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG Rapido @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "oi5JKM4fwEYHefs0", + "name": "QIDI PETG Rapido @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG Rapido@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "1" + ], + "pressure_advance": [ + "0.054" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG Rapido @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG Rapido @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..9fe3affac2 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG Rapido @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "SZOwb16vrcdtaDzJ", + "name": "QIDI PETG Rapido @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG Rapido@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG Rapido @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG Rapido @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..a550919113 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG Rapido @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "4LKxL4N0ccD6vZ60", + "name": "QIDI PETG Rapido @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG Rapido@X-Plus 5-Series", + "pressure_advance": [ + "0.04" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG Rapido @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG Rapido @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..b56b55d1ff --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG Rapido @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "rsNDDt3IFUOMYV3O", + "name": "QIDI PETG Rapido @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG Rapido@X-Plus 5-Series", + "pressure_advance": [ + "0.04" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG Rapido @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PETG Rapido @X-Plus 5.json new file mode 100644 index 0000000000..fd506d8e74 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG Rapido @X-Plus 5.json @@ -0,0 +1,102 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_41", + "name": "QIDI PETG Rapido@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "45" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "cool_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "eng_plate_temp": [ + "80" + ], + "fan_cooling_layer_time": [ + "20" + ], + "fan_max_speed": [ + "40" + ], + "fan_min_speed": [ + "20" + ], + "filament_adhesiveness_category": [ + "300" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_max_volumetric_speed": [ + "18" + ], + "filament_type": [ + "PETG" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "hot_plate_temp": [ + "80" + ], + "impact_strength_z": [ + "10.6" + ], + "nozzle_temperature_initial_layer": [ + "250" + ], + "nozzle_temperature_range_high": [ + "275" + ], + "nozzle_temperature_range_low": [ + "240" + ], + "nozzle_temperature": [ + "250" + ], + "overhang_fan_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "10%" + ], + "pressure_advance": [ + "0.054" + ], + "slow_down_layer_time": [ + "8" + ], + "temperature_vitrification": [ + "70" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], + "supertack_plate_temp": [ + "60" + ], + "textured_cool_plate_temp_initial_layer": [ + "60" + ], + "textured_cool_plate_temp": [ + "60" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG Tough @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG Tough @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..7708ebba0d --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG Tough @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "k4kLQheLeWOw8bwp", + "name": "QIDI PETG Tough @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG Tough@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "1" + ], + "pressure_advance": [ + "0.056" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG Tough @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG Tough @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..5c8c182a2b --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG Tough @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "WEyrSDtW2CJeLQmg", + "name": "QIDI PETG Tough @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG Tough@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG Tough @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG Tough @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..ad75cc5722 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG Tough @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "Morgo6GIzU3Voc0Q", + "name": "QIDI PETG Tough @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG Tough@X-Plus 5-Series", + "pressure_advance": [ + "0.04" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG Tough @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG Tough @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..37c0beddb9 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG Tough @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "DsoxBXKOjsENLEZf", + "name": "QIDI PETG Tough @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG Tough@X-Plus 5-Series", + "pressure_advance": [ + "0.04" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG Tough @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PETG Tough @X-Plus 5.json new file mode 100644 index 0000000000..3e9f0a1cdf --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG Tough @X-Plus 5.json @@ -0,0 +1,102 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_40", + "name": "QIDI PETG Tough@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "45" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "cool_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "eng_plate_temp": [ + "80" + ], + "fan_cooling_layer_time": [ + "30" + ], + "fan_max_speed": [ + "40" + ], + "fan_min_speed": [ + "10" + ], + "filament_adhesiveness_category": [ + "300" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_max_volumetric_speed": [ + "13" + ], + "filament_type": [ + "PETG" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "hot_plate_temp": [ + "80" + ], + "impact_strength_z": [ + "10.6" + ], + "nozzle_temperature_initial_layer": [ + "240" + ], + "nozzle_temperature_range_high": [ + "270" + ], + "nozzle_temperature_range_low": [ + "220" + ], + "nozzle_temperature": [ + "250" + ], + "overhang_fan_speed": [ + "90" + ], + "overhang_fan_threshold": [ + "10%" + ], + "pressure_advance": [ + "0.056" + ], + "slow_down_layer_time": [ + "8" + ], + "temperature_vitrification": [ + "70" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], + "supertack_plate_temp": [ + "60" + ], + "textured_cool_plate_temp_initial_layer": [ + "60" + ], + "textured_cool_plate_temp": [ + "60" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG Translucent @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG Translucent @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..03e72feba9 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG Translucent @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "2UJnG0rXeU2nCIQL", + "name": "QIDI PETG Translucent @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG Translucent@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "1" + ], + "pressure_advance": [ + "0.054" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG Translucent @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG Translucent @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..545bd42538 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG Translucent @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "S2DQ7jy5KqbiVNnV", + "name": "QIDI PETG Translucent @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG Translucent@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG Translucent @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG Translucent @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..963916f76e --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG Translucent @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "G33dlk73weGNFNwg", + "name": "QIDI PETG Translucent @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG Translucent@X-Plus 5-Series", + "pressure_advance": [ + "0.04" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG Translucent @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG Translucent @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..42bf38bde4 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG Translucent @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "zqI9F8vplNFfm2pa", + "name": "QIDI PETG Translucent @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG Translucent@X-Plus 5-Series", + "pressure_advance": [ + "0.04" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG Translucent @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PETG Translucent @X-Plus 5.json new file mode 100644 index 0000000000..62ea937028 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG Translucent @X-Plus 5.json @@ -0,0 +1,102 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_45", + "name": "QIDI PETG Translucent@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "45" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "cool_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "eng_plate_temp": [ + "80" + ], + "fan_cooling_layer_time": [ + "30" + ], + "fan_max_speed": [ + "30" + ], + "fan_min_speed": [ + "10" + ], + "filament_adhesiveness_category": [ + "300" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_max_volumetric_speed": [ + "8" + ], + "filament_type": [ + "PETG" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "hot_plate_temp": [ + "80" + ], + "impact_strength_z": [ + "10.6" + ], + "nozzle_temperature_initial_layer": [ + "250" + ], + "nozzle_temperature_range_high": [ + "280" + ], + "nozzle_temperature_range_low": [ + "240" + ], + "nozzle_temperature": [ + "250" + ], + "overhang_fan_speed": [ + "90" + ], + "overhang_fan_threshold": [ + "10%" + ], + "pressure_advance": [ + "0.054" + ], + "slow_down_layer_time": [ + "8" + ], + "temperature_vitrification": [ + "70" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], + "supertack_plate_temp": [ + "60" + ], + "textured_cool_plate_temp_initial_layer": [ + "60" + ], + "textured_cool_plate_temp": [ + "60" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG-CF @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG-CF @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..04176974f5 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG-CF @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "cXbQtMQKmAFqwmqK", + "name": "QIDI PETG-CF @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG-CF@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG-CF @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG-CF @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..ce16a13acf --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG-CF @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "YNIWSLXvW6kSPRFG", + "name": "QIDI PETG-CF @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG-CF@X-Plus 5-Series", + "pressure_advance": [ + "0.04" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG-CF @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG-CF @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..e1ce42ee0a --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG-CF @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "O0lwfGXO3BdPCask", + "name": "QIDI PETG-CF @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG-CF@X-Plus 5-Series", + "pressure_advance": [ + "0.04" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG-CF @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PETG-CF @X-Plus 5.json new file mode 100644 index 0000000000..fcddc636a8 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG-CF @X-Plus 5.json @@ -0,0 +1,102 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_42", + "name": "QIDI PETG-CF@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "45" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "cool_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "eng_plate_temp": [ + "80" + ], + "fan_cooling_layer_time": [ + "30" + ], + "fan_max_speed": [ + "40" + ], + "fan_min_speed": [ + "5" + ], + "filament_adhesiveness_category": [ + "300" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_max_volumetric_speed": [ + "11.5" + ], + "filament_type": [ + "PETG-CF" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "hot_plate_temp": [ + "80" + ], + "impact_strength_z": [ + "10.6" + ], + "nozzle_temperature_initial_layer": [ + "255" + ], + "nozzle_temperature_range_high": [ + "270" + ], + "nozzle_temperature_range_low": [ + "240" + ], + "nozzle_temperature": [ + "255" + ], + "overhang_fan_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "10%" + ], + "pressure_advance": [ + "0.048" + ], + "slow_down_layer_time": [ + "6" + ], + "temperature_vitrification": [ + "70" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], + "supertack_plate_temp": [ + "60" + ], + "textured_cool_plate_temp_initial_layer": [ + "60" + ], + "textured_cool_plate_temp": [ + "60" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG-GF @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG-GF @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..46b3b1176a --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG-GF @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "zfI1VXQ7E0re78CC", + "name": "QIDI PETG-GF @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG-GF@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG-GF @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG-GF @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..4af6b7db02 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG-GF @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "zYEbjZPipK4BgHRO", + "name": "QIDI PETG-GF @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG-GF@X-Plus 5-Series", + "pressure_advance": [ + "0.04" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG-GF @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PETG-GF @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..bf819e04a9 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG-GF @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "5ef246YwGEK0eVCr", + "name": "QIDI PETG-GF @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PETG-GF@X-Plus 5-Series", + "pressure_advance": [ + "0.04" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PETG-GF @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PETG-GF @X-Plus 5.json new file mode 100644 index 0000000000..fa65907d3d --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PETG-GF @X-Plus 5.json @@ -0,0 +1,102 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_43", + "name": "QIDI PETG-GF@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "45" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "cool_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "eng_plate_temp": [ + "80" + ], + "fan_cooling_layer_time": [ + "30" + ], + "fan_max_speed": [ + "50" + ], + "fan_min_speed": [ + "20" + ], + "filament_adhesiveness_category": [ + "300" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_max_volumetric_speed": [ + "8" + ], + "filament_type": [ + "PETG-GF" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "hot_plate_temp": [ + "80" + ], + "impact_strength_z": [ + "10.6" + ], + "nozzle_temperature_initial_layer": [ + "255" + ], + "nozzle_temperature_range_high": [ + "270" + ], + "nozzle_temperature_range_low": [ + "240" + ], + "nozzle_temperature": [ + "255" + ], + "overhang_fan_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "10%" + ], + "pressure_advance": [ + "0.056" + ], + "slow_down_layer_time": [ + "8" + ], + "temperature_vitrification": [ + "70" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], + "supertack_plate_temp": [ + "60" + ], + "textured_cool_plate_temp_initial_layer": [ + "60" + ], + "textured_cool_plate_temp": [ + "60" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Basic @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Basic @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..4ec7f05eb9 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Basic @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "sfrpgBzPufKwgBCT", + "name": "QIDI PLA Basic @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Basic@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "2" + ], + "pressure_advance": [ + "0.034" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Basic @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Basic @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..20dd378bd2 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Basic @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "6ZscfuWENwCkdSYE", + "name": "QIDI PLA Basic @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Basic@X-Plus 5-Series", + "pressure_advance": [ + "0.038" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Basic @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Basic @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..574e748c98 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Basic @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "pEry2GMq8UJEw7c7", + "name": "QIDI PLA Basic @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Basic@X-Plus 5-Series", + "pressure_advance": [ + "0.016" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Basic @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Basic @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..527eb00054 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Basic @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "MTq8xcAKCWABLI2k", + "name": "QIDI PLA Basic @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Basic@X-Plus 5-Series", + "pressure_advance": [ + "0.008" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Basic @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Basic @X-Plus 5.json new file mode 100644 index 0000000000..0ee194dfb3 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Basic @X-Plus 5.json @@ -0,0 +1,63 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_7", + "name": "QIDI PLA Basic@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "cool_plate_temp_initial_layer": [ + "45" + ], + "cool_plate_temp": [ + "45" + ], + "filament_adhesiveness_category": [ + "100" + ], + "filament_max_volumetric_speed": [ + "18" + ], + "filament_type": [ + "PLA" + ], + "impact_strength_z": [ + "13.8" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "nozzle_temperature_initial_layer": [ + "210" + ], + "nozzle_temperature": [ + "210" + ], + "overhang_fan_threshold": [ + "50%" + ], + "supertack_plate_temp_initial_layer": [ + "45" + ], + "supertack_plate_temp": [ + "45" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp_initial_layer": [ + "45" + ], + "textured_cool_plate_temp": [ + "45" + ], + "compatible_printers": [], + "close_additional_fan_first_x_layers": [ + "1" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Matte Basic @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Matte Basic @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..0078600f36 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Matte Basic @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "54iiCCwizthHtT1v", + "name": "QIDI PLA Matte Basic @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Matte Basic@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "2" + ], + "pressure_advance": [ + "0.034" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Matte Basic @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Matte Basic @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..d67aa77dc4 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Matte Basic @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "ojnl8sBbBL4QBL8q", + "name": "QIDI PLA Matte Basic @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Matte Basic@X-Plus 5-Series", + "pressure_advance": [ + "0.038" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Matte Basic @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Matte Basic @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..6575396e45 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Matte Basic @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "XtZarwF2HIipdHjN", + "name": "QIDI PLA Matte Basic @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Matte Basic@X-Plus 5-Series", + "pressure_advance": [ + "0.016" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Matte Basic @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Matte Basic @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..8c4584a70b --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Matte Basic @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "TRkCNQeoDvLBaJMt", + "name": "QIDI PLA Matte Basic @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Matte Basic@X-Plus 5-Series", + "pressure_advance": [ + "0.008" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Matte Basic @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Matte Basic @X-Plus 5.json new file mode 100644 index 0000000000..7796bf1faa --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Matte Basic @X-Plus 5.json @@ -0,0 +1,63 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_8", + "name": "QIDI PLA Matte Basic@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "cool_plate_temp_initial_layer": [ + "45" + ], + "cool_plate_temp": [ + "45" + ], + "filament_adhesiveness_category": [ + "100" + ], + "filament_max_volumetric_speed": [ + "18" + ], + "filament_type": [ + "PLA" + ], + "impact_strength_z": [ + "13.8" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "nozzle_temperature_initial_layer": [ + "210" + ], + "nozzle_temperature": [ + "210" + ], + "overhang_fan_threshold": [ + "50%" + ], + "supertack_plate_temp_initial_layer": [ + "45" + ], + "supertack_plate_temp": [ + "45" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp_initial_layer": [ + "45" + ], + "textured_cool_plate_temp": [ + "45" + ], + "compatible_printers": [], + "close_additional_fan_first_x_layers": [ + "1" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..3ea3dac12f --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "S56TixVPmXr4uFut", + "name": "QIDI PLA Rapido @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Rapido@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "2" + ], + "pressure_advance": [ + "0.034" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..178fcfd08d --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "KATHrxoL84i7MiVJ", + "name": "QIDI PLA Rapido @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Rapido@X-Plus 5-Series", + "pressure_advance": [ + "0.038" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..2ad1d18a60 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "YE6WmGAG5Ua6bbap", + "name": "QIDI PLA Rapido @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Rapido@X-Plus 5-Series", + "pressure_advance": [ + "0.016" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..ffc9edfb78 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "KPqnbBv0Np93O02u", + "name": "QIDI PLA Rapido @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Rapido@X-Plus 5-Series", + "pressure_advance": [ + "0.008" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido @X-Plus 5.json new file mode 100644 index 0000000000..1aa1803258 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido @X-Plus 5.json @@ -0,0 +1,60 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_1", + "name": "QIDI PLA Rapido@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "cool_plate_temp_initial_layer": [ + "45" + ], + "cool_plate_temp": [ + "45" + ], + "filament_adhesiveness_category": [ + "100" + ], + "filament_type": [ + "PLA" + ], + "impact_strength_z": [ + "13.8" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "nozzle_temperature_initial_layer": [ + "210" + ], + "nozzle_temperature": [ + "210" + ], + "overhang_fan_threshold": [ + "50%" + ], + "supertack_plate_temp_initial_layer": [ + "45" + ], + "supertack_plate_temp": [ + "45" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp_initial_layer": [ + "45" + ], + "textured_cool_plate_temp": [ + "45" + ], + "compatible_printers": [], + "close_additional_fan_first_x_layers": [ + "1" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Matte @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Matte @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..e469cf1fd1 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Matte @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "jlUpp5oaYSWwaNn1", + "name": "QIDI PLA Rapido Matte @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Rapido Matte@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "2" + ], + "pressure_advance": [ + "0.034" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Matte @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Matte @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..3f74323770 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Matte @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "E0gYAZUksoFlebuH", + "name": "QIDI PLA Rapido Matte @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Rapido Matte@X-Plus 5-Series", + "pressure_advance": [ + "0.034" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Matte @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Matte @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..f45479c37f --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Matte @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "juxpaK6EosaHj2ok", + "name": "QIDI PLA Rapido Matte @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Rapido Matte@X-Plus 5-Series", + "pressure_advance": [ + "0.016" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Matte @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Matte @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..7e4ec99248 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Matte @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "OlgI3WQA3QLhPsdw", + "name": "QIDI PLA Rapido Matte @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Rapido Matte@X-Plus 5-Series", + "pressure_advance": [ + "0.008" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Matte @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Matte @X-Plus 5.json new file mode 100644 index 0000000000..d8cbd06ea1 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Matte @X-Plus 5.json @@ -0,0 +1,57 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_2", + "name": "QIDI PLA Rapido Matte@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "additional_cooling_fan_speed": [ + "100" + ], + "filament_adhesiveness_category": [ + "100" + ], + "filament_density": [ + "1.42" + ], + "filament_type": [ + "PLA" + ], + "impact_strength_z": [ + "6.6" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "nozzle_temperature_initial_layer": [ + "210" + ], + "nozzle_temperature": [ + "210" + ], + "overhang_fan_threshold": [ + "50%" + ], + "supertack_plate_temp_initial_layer": [ + "45" + ], + "supertack_plate_temp": [ + "45" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp_initial_layer": [ + "45" + ], + "textured_cool_plate_temp": [ + "45" + ], + "compatible_printers": [], + "close_additional_fan_first_x_layers": [ + "1" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Metal @Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Metal @Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..7506c80541 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Metal @Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "t9qUv1IlI5VJ8LoV", + "name": "QIDI PLA Rapido Metal @Qidi X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Rapido Metal@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "2" + ], + "pressure_advance": [ + "0.038" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Metal @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Metal @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..e2be1216f3 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Metal @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "ARcrUoVsGSM0RB1a", + "name": "QIDI PLA Rapido Metal @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Rapido Metal@X-Plus 5-Series", + "pressure_advance": [ + "0.038" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Metal @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Metal @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..2b3fae0a1e --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Metal @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "A23HjUFAyt4GeHAW", + "name": "QIDI PLA Rapido Metal @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Rapido Metal@X-Plus 5-Series", + "pressure_advance": [ + "0.020" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Metal @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Metal @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..e6868b6772 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Metal @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "nMpSP1jmsQIUjGPb", + "name": "QIDI PLA Rapido Metal @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Rapido Metal@X-Plus 5-Series", + "pressure_advance": [ + "0.01" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Metal @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Metal @X-Plus 5.json new file mode 100644 index 0000000000..404b7cd1e8 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Rapido Metal @X-Plus 5.json @@ -0,0 +1,57 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_3", + "name": "QIDI PLA Rapido Metal@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "filament_type": [ + "PLA" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "filament_adhesiveness_category": [ + "100" + ], + "filament_density": [ + "1.20" + ], + "impact_strength_z": [ + "16.8" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "nozzle_temperature_initial_layer": [ + "210" + ], + "nozzle_temperature": [ + "210" + ], + "overhang_fan_threshold": [ + "50%" + ], + "supertack_plate_temp_initial_layer": [ + "45" + ], + "supertack_plate_temp": [ + "45" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp_initial_layer": [ + "45" + ], + "textured_cool_plate_temp": [ + "45" + ], + "compatible_printers": [], + "close_additional_fan_first_x_layers": [ + "1" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Silk @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Silk @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..dfd789a216 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Silk @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "IUU7TApRivYtrrt0", + "name": "QIDI PLA Silk @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Silk@X-Plus 5-Series", + "pressure_advance": [ + "0.034" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Silk @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Silk @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..286c1d3a36 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Silk @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "lx9Dqv6EMmZ0C2SD", + "name": "QIDI PLA Silk @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA Silk@X-Plus 5-Series", + "pressure_advance": [ + "0.021" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA Silk @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PLA Silk @X-Plus 5.json new file mode 100644 index 0000000000..57c0c3e1dc --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA Silk @X-Plus 5.json @@ -0,0 +1,84 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_4", + "name": "QIDI PLA Silk@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "additional_cooling_fan_speed": [ + "100" + ], + "filament_adhesiveness_category": [ + "100" + ], + "filament_density": [ + "1.24" + ], + "filament_max_volumetric_speed": [ + "7.5" + ], + "filament_type": [ + "PLA" + ], + "impact_strength_z": [ + "4.6" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "nozzle_temperature": [ + "220" + ], + "overhang_fan_threshold": [ + "50%" + ], + "supertack_plate_temp_initial_layer": [ + "45" + ], + "supertack_plate_temp": [ + "45" + ], + "cool_plate_temp_initial_layer": [ + "45" + ], + "cool_plate_temp": [ + "45" + ], + "eng_plate_temp_initial_layer": [ + "55" + ], + "eng_plate_temp": [ + "55" + ], + "hot_plate_temp_initial_layer": [ + "55" + ], + "hot_plate_temp": [ + "55" + ], + "textured_plate_temp_initial_layer": [ + "55" + ], + "textured_plate_temp": [ + "55" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp_initial_layer": [ + "45" + ], + "textured_cool_plate_temp": [ + "45" + ], + "compatible_printers": [], + "close_additional_fan_first_x_layers": [ + "1" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA-CF @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA-CF @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..c341f7c71d --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA-CF @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "5PocTwpY0SnNzcdc", + "name": "QIDI PLA-CF @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA-CF@X-Plus 5-Series", + "pressure_advance": [ + "0.034" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA-CF @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA-CF @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..5c08e7f67f --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA-CF @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "5NETSLSQlaoR2MYD", + "name": "QIDI PLA-CF @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA-CF@X-Plus 5-Series", + "pressure_advance": [ + "0.012" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA-CF @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PLA-CF @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..7435222e8b --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA-CF @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "setting_id": "AUO7iSMEa6GDvM7Y", + "name": "QIDI PLA-CF @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PLA-CF@X-Plus 5-Series", + "filament_max_volumetric_speed": [ + "18" + ], + "pressure_advance": [ + "0.008" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PLA-CF @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PLA-CF @X-Plus 5.json new file mode 100644 index 0000000000..1c4d8c99e3 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PLA-CF @X-Plus 5.json @@ -0,0 +1,75 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_5", + "name": "QIDI PLA-CF@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "additional_cooling_fan_speed": [ + "0" + ], + "cool_plate_temp_initial_layer": [ + "45" + ], + "cool_plate_temp": [ + "45" + ], + "filament_adhesiveness_category": [ + "100" + ], + "filament_density": [ + "1.25" + ], + "filament_flow_ratio": [ + "0.93" + ], + "filament_max_volumetric_speed": [ + "15" + ], + "filament_type": [ + "PLA-CF" + ], + "impact_strength_z": [ + "7.8" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "nozzle_temperature_range_high": [ + "250" + ], + "nozzle_temperature_range_low": [ + "210" + ], + "nozzle_temperature": [ + "220" + ], + "overhang_fan_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "50%" + ], + "pressure_advance": [ + "0.042" + ], + "supertack_plate_temp_initial_layer": [ + "45" + ], + "supertack_plate_temp": [ + "45" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp_initial_layer": [ + "45" + ], + "textured_cool_plate_temp": [ + "45" + ], + "compatible_printers": [], + "close_additional_fan_first_x_layers": [ + "1" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PPS-CF @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PPS-CF @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..2c2d573713 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PPS-CF @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "FwWut7sJIMbe87XC", + "name": "QIDI PPS-CF @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PPS-CF@X-Plus 5-Series", + "pressure_advance": [ + "0.03" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PPS-CF @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PPS-CF @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..9cc0556490 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PPS-CF @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "gDIuKCdgoP59Xc0G", + "name": "QIDI PPS-CF @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PPS-CF@X-Plus 5-Series", + "pressure_advance": [ + "0.021" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PPS-CF @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PPS-CF @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..573f8e17f8 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PPS-CF @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "D29sGCB9OI4cTn0S", + "name": "QIDI PPS-CF @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PPS-CF@X-Plus 5-Series", + "pressure_advance": [ + "0.008" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PPS-CF @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PPS-CF @X-Plus 5.json new file mode 100644 index 0000000000..c6be7046f6 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PPS-CF @X-Plus 5.json @@ -0,0 +1,117 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_44", + "name": "QIDI PPS-CF@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "65" + ], + "box_temperature_range_low": [ + "0" + ], + "box_temperature": [ + "65" + ], + "chamber_temperatures": [ + "60" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "80" + ], + "cool_plate_temp": [ + "80" + ], + "eng_plate_temp_initial_layer": [ + "110" + ], + "eng_plate_temp": [ + "110" + ], + "during_print_exhaust_fan_speed": [ + "0" + ], + "fan_cooling_layer_time": [ + "5" + ], + "fan_max_speed": [ + "30" + ], + "fan_min_speed": [ + "0" + ], + "filament_adhesiveness_category": [ + "801" + ], + "filament_density": [ + "1.3" + ], + "filament_flow_ratio": [ + "0.97" + ], + "filament_max_volumetric_speed": [ + "6" + ], + "filament_type": [ + "PPS-CF" + ], + "hot_plate_temp_initial_layer": [ + "110" + ], + "hot_plate_temp": [ + "110" + ], + "impact_strength_z": [ + "2.8" + ], + "nozzle_temperature_initial_layer": [ + "320" + ], + "nozzle_temperature_range_high": [ + "350" + ], + "nozzle_temperature_range_low": [ + "300" + ], + "nozzle_temperature": [ + "320" + ], + "overhang_fan_speed": [ + "40" + ], + "overhang_fan_threshold": [ + "0%" + ], + "pressure_advance": [ + "0.032" + ], + "slow_down_layer_time": [ + "2" + ], + "supertack_plate_temp_initial_layer": [ + "80" + ], + "supertack_plate_temp": [ + "80" + ], + "temperature_vitrification": [ + "180" + ], + "textured_plate_temp_initial_layer": [ + "110" + ], + "textured_plate_temp": [ + "110" + ], + "textured_cool_plate_temp_initial_layer": [ + "80" + ], + "textured_cool_plate_temp": [ + "80" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PPS-GF @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PPS-GF @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..de96ad5f7a --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PPS-GF @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "5C6dIIgQOt4NiTu0", + "name": "QIDI PPS-GF @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PPS-GF@X-Plus 5-Series", + "pressure_advance": [ + "0.03" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PPS-GF @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PPS-GF @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..470839ef59 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PPS-GF @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "aSd6UW4Mrlxpa15E", + "name": "QIDI PPS-GF @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PPS-GF@X-Plus 5-Series", + "pressure_advance": [ + "0.021" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PPS-GF @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI PPS-GF @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..6cc49dbd68 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PPS-GF @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "7JC2zAUgtGu5zSGo", + "name": "QIDI PPS-GF @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI PPS-GF@X-Plus 5-Series", + "pressure_advance": [ + "0.008" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI PPS-GF @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PPS-GF @X-Plus 5.json new file mode 100644 index 0000000000..757d4dbf3d --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI PPS-GF @X-Plus 5.json @@ -0,0 +1,117 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_46", + "name": "QIDI PPS-GF@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "65" + ], + "box_temperature_range_low": [ + "0" + ], + "box_temperature": [ + "65" + ], + "chamber_temperatures": [ + "0" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "80" + ], + "cool_plate_temp": [ + "80" + ], + "eng_plate_temp_initial_layer": [ + "90" + ], + "eng_plate_temp": [ + "90" + ], + "during_print_exhaust_fan_speed": [ + "0" + ], + "fan_cooling_layer_time": [ + "30" + ], + "fan_max_speed": [ + "90" + ], + "fan_min_speed": [ + "10" + ], + "filament_adhesiveness_category": [ + "801" + ], + "filament_density": [ + "1.3" + ], + "filament_flow_ratio": [ + "0.97" + ], + "filament_max_volumetric_speed": [ + "10" + ], + "filament_type": [ + "PPS-GF" + ], + "hot_plate_temp_initial_layer": [ + "90" + ], + "hot_plate_temp": [ + "90" + ], + "impact_strength_z": [ + "2.8" + ], + "nozzle_temperature_initial_layer": [ + "320" + ], + "nozzle_temperature_range_high": [ + "350" + ], + "nozzle_temperature_range_low": [ + "300" + ], + "nozzle_temperature": [ + "320" + ], + "overhang_fan_speed": [ + "50" + ], + "overhang_fan_threshold": [ + "0%" + ], + "pressure_advance": [ + "0.03" + ], + "slow_down_layer_time": [ + "6" + ], + "supertack_plate_temp_initial_layer": [ + "80" + ], + "supertack_plate_temp": [ + "80" + ], + "temperature_vitrification": [ + "180" + ], + "textured_plate_temp_initial_layer": [ + "90" + ], + "textured_plate_temp": [ + "90" + ], + "textured_cool_plate_temp_initial_layer": [ + "80" + ], + "textured_cool_plate_temp": [ + "80" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI Support For PAHT @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI Support For PAHT @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..5bd3b85fb9 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI Support For PAHT @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "oMvDD9rKJGcezwpq", + "name": "QIDI Support For PAHT @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI Support For PAHT@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI Support For PAHT @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI Support For PAHT @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..37f474533e --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI Support For PAHT @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "6BhbbuUP6HCVScDd", + "name": "QIDI Support For PAHT @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI Support For PAHT@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI Support For PAHT @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI Support For PAHT @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..f28e56c5b1 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI Support For PAHT @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "kA4fvvj6xZEhArXv", + "name": "QIDI Support For PAHT @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI Support For PAHT@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI Support For PAHT @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI Support For PAHT @X-Plus 5.json new file mode 100644 index 0000000000..ed1222daa9 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI Support For PAHT @X-Plus 5.json @@ -0,0 +1,114 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_32", + "name": "QIDI Support For PAHT@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "65" + ], + "box_temperature_range_low": [ + "0" + ], + "box_temperature": [ + "60" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "80" + ], + "cool_plate_temp": [ + "80" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "eng_plate_temp": [ + "80" + ], + "fan_cooling_layer_time": [ + "10" + ], + "fan_max_speed": [ + "60" + ], + "fan_min_speed": [ + "0" + ], + "filament_adhesiveness_category": [ + "800" + ], + "filament_density": [ + "1.26" + ], + "filament_flow_ratio": [ + "0.94" + ], + "filament_is_support": [ + "1" + ], + "filament_max_volumetric_speed": [ + "8" + ], + "filament_type": [ + "PAHT-S" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "hot_plate_temp": [ + "80" + ], + "impact_strength_z": [ + "4.5" + ], + "nozzle_temperature_initial_layer": [ + "280" + ], + "nozzle_temperature_range_high": [ + "280" + ], + "nozzle_temperature_range_low": [ + "260" + ], + "nozzle_temperature": [ + "280" + ], + "overhang_fan_speed": [ + "30" + ], + "overhang_fan_threshold": [ + "95%" + ], + "pressure_advance": [ + "0.02" + ], + "slow_down_layer_time": [ + "6" + ], + "temperature_vitrification": [ + "218" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "supertack_plate_temp_initial_layer": [ + "80" + ], + "supertack_plate_temp": [ + "80" + ], + "textured_cool_plate_temp_initial_layer": [ + "80" + ], + "textured_cool_plate_temp": [ + "80" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI Support For PET-PA @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI Support For PET-PA @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..615d9af041 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI Support For PET-PA @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "0xy82KbAJD5uBara", + "name": "QIDI Support For PET/PA @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI Support For PET/PA@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI Support For PET-PA @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI Support For PET-PA @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..1e0c81424c --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI Support For PET-PA @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "2oCGYFdC4dfpNUw4", + "name": "QIDI Support For PET/PA @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI Support For PET/PA@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI Support For PET-PA @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI Support For PET-PA @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..2d47d18707 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI Support For PET-PA @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "hFYahJkYIgmjcuPb", + "name": "QIDI Support For PET/PA @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI Support For PET/PA@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI Support For PET-PA @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI Support For PET-PA @X-Plus 5.json new file mode 100644 index 0000000000..346c3cc3eb --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI Support For PET-PA @X-Plus 5.json @@ -0,0 +1,114 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_33", + "name": "QIDI Support For PET/PA@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "65" + ], + "box_temperature_range_low": [ + "0" + ], + "box_temperature": [ + "55" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "70" + ], + "cool_plate_temp": [ + "70" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "eng_plate_temp": [ + "80" + ], + "fan_cooling_layer_time": [ + "10" + ], + "fan_max_speed": [ + "60" + ], + "fan_min_speed": [ + "0" + ], + "filament_adhesiveness_category": [ + "800" + ], + "filament_density": [ + "1.16" + ], + "filament_flow_ratio": [ + "0.91" + ], + "filament_is_support": [ + "1" + ], + "filament_max_volumetric_speed": [ + "8" + ], + "filament_type": [ + "PA-S" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "hot_plate_temp": [ + "80" + ], + "impact_strength_z": [ + "4.5" + ], + "nozzle_temperature_initial_layer": [ + "280" + ], + "nozzle_temperature_range_high": [ + "280" + ], + "nozzle_temperature_range_low": [ + "260" + ], + "nozzle_temperature": [ + "280" + ], + "overhang_fan_speed": [ + "30" + ], + "overhang_fan_threshold": [ + "95%" + ], + "pressure_advance": [ + "0.02" + ], + "slow_down_layer_time": [ + "6" + ], + "temperature_vitrification": [ + "168" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "supertack_plate_temp_initial_layer": [ + "70" + ], + "supertack_plate_temp": [ + "70" + ], + "textured_cool_plate_temp_initial_layer": [ + "70" + ], + "textured_cool_plate_temp": [ + "70" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI TPU 95A-HF @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI TPU 95A-HF @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..1f2db1eb25 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI TPU 95A-HF @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "ygLg4KVojo7YoG5K", + "name": "QIDI TPU 95A-HF @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI TPU 95A-HF@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI TPU 95A-HF @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI TPU 95A-HF @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..94e992d0e9 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI TPU 95A-HF @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "K98nPXclatc5HnIx", + "name": "QIDI TPU 95A-HF @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI TPU 95A-HF@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI TPU 95A-HF @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI TPU 95A-HF @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..e05faeed65 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI TPU 95A-HF @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "pTxBir6QC9KhWrLU", + "name": "QIDI TPU 95A-HF @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI TPU 95A-HF@X-Plus 5-Series", + "nozzle_temperature": [ + "220" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI TPU 95A-HF @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI TPU 95A-HF @X-Plus 5.json new file mode 100644 index 0000000000..4721ab171c --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI TPU 95A-HF @X-Plus 5.json @@ -0,0 +1,84 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_50", + "name": "QIDI TPU 95A-HF@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "cool_plate_temp_initial_layer": [ + "30" + ], + "cool_plate_temp": [ + "30" + ], + "eng_plate_temp_initial_layer": [ + "35" + ], + "eng_plate_temp": [ + "35" + ], + "filament_adhesiveness_category": [ + "600" + ], + "filament_density": [ + "1.15" + ], + "filament_flow_ratio": [ + "1" + ], + "filament_max_volumetric_speed": [ + "4" + ], + "filament_type": [ + "TPU" + ], + "filament_vendor": [ + "QIDI" + ], + "hot_plate_temp_initial_layer": [ + "35" + ], + "hot_plate_temp": [ + "35" + ], + "impact_strength_z": [ + "88.7" + ], + "nozzle_temperature_initial_layer": [ + "230" + ], + "nozzle_temperature_range_high": [ + "250" + ], + "nozzle_temperature_range_low": [ + "200" + ], + "nozzle_temperature": [ + "230" + ], + "pressure_advance": [ + "0.1" + ], + "supertack_plate_temp_initial_layer": [ + "30" + ], + "supertack_plate_temp": [ + "30" + ], + "temperature_vitrification": [ + "30" + ], + "textured_plate_temp_initial_layer": [ + "35" + ], + "textured_plate_temp": [ + "35" + ], + "textured_cool_plate_temp_initial_layer": [ + "30" + ], + "textured_cool_plate_temp": [ + "30" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI TPU-Aero @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI TPU-Aero @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..592ad5a51c --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI TPU-Aero @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "KIIwoI87n5MkT83b", + "name": "QIDI TPU-Aero @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI TPU-Aero@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI TPU-Aero @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI TPU-Aero @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..f18b67f608 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI TPU-Aero @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "eUNygrv7weqgX7e0", + "name": "QIDI TPU-Aero @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI TPU-Aero@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI TPU-Aero @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI TPU-Aero @X-Plus 5.json new file mode 100644 index 0000000000..b6eee90916 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI TPU-Aero @X-Plus 5.json @@ -0,0 +1,93 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_49", + "name": "QIDI TPU-Aero@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "cool_plate_temp_initial_layer": [ + "30" + ], + "cool_plate_temp": [ + "30" + ], + "eng_plate_temp_initial_layer": [ + "35" + ], + "eng_plate_temp": [ + "35" + ], + "fan_cooling_layer_time": [ + "100" + ], + "filament_adhesiveness_category": [ + "600" + ], + "filament_density": [ + "1.15" + ], + "filament_flow_ratio": [ + "0.5" + ], + "filament_max_volumetric_speed": [ + "6" + ], + "filament_retraction_length": [ + "0" + ], + "filament_type": [ + "TPU-AERO" + ], + "filament_vendor": [ + "QIDI" + ], + "hot_plate_temp_initial_layer": [ + "35" + ], + "hot_plate_temp": [ + "35" + ], + "impact_strength_z": [ + "88.7" + ], + "nozzle_temperature_initial_layer": [ + "250" + ], + "nozzle_temperature_range_high": [ + "270" + ], + "nozzle_temperature_range_low": [ + "230" + ], + "nozzle_temperature": [ + "250" + ], + "pressure_advance": [ + "0.03" + ], + "slow_down_layer_time": [ + "14" + ], + "supertack_plate_temp_initial_layer": [ + "30" + ], + "supertack_plate_temp": [ + "30" + ], + "temperature_vitrification": [ + "30" + ], + "textured_plate_temp_initial_layer": [ + "35" + ], + "textured_plate_temp": [ + "35" + ], + "textured_cool_plate_temp_initial_layer": [ + "30" + ], + "textured_cool_plate_temp": [ + "30" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI TPU-GF @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI TPU-GF @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..8e88b23151 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI TPU-GF @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "AtsowCGhOIw4GeRJ", + "name": "QIDI TPU-GF @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI TPU-GF@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI TPU-GF @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI TPU-GF @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..5a7687b51c --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI TPU-GF @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "wqk4I1KaRyLEm12W", + "name": "QIDI TPU-GF @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI TPU-GF@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI TPU-GF @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI TPU-GF @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..2fe800d6ab --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI TPU-GF @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "o2JjRsoi03ST3Wh2", + "name": "QIDI TPU-GF @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI TPU-GF@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI TPU-GF @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI TPU-GF @X-Plus 5.json new file mode 100644 index 0000000000..b3f2f71866 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI TPU-GF @X-Plus 5.json @@ -0,0 +1,84 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_15", + "name": "QIDI TPU-GF@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "cool_plate_temp_initial_layer": [ + "30" + ], + "cool_plate_temp": [ + "30" + ], + "eng_plate_temp_initial_layer": [ + "35" + ], + "eng_plate_temp": [ + "35" + ], + "filament_adhesiveness_category": [ + "600" + ], + "filament_density": [ + "1.15" + ], + "filament_flow_ratio": [ + "1" + ], + "filament_max_volumetric_speed": [ + "6" + ], + "filament_type": [ + "TPU-GF" + ], + "filament_vendor": [ + "QIDI" + ], + "hot_plate_temp_initial_layer": [ + "35" + ], + "hot_plate_temp": [ + "35" + ], + "impact_strength_z": [ + "88.7" + ], + "nozzle_temperature_initial_layer": [ + "240" + ], + "nozzle_temperature_range_high": [ + "270" + ], + "nozzle_temperature_range_low": [ + "240" + ], + "nozzle_temperature": [ + "240" + ], + "pressure_advance": [ + "0.1" + ], + "supertack_plate_temp_initial_layer": [ + "30" + ], + "supertack_plate_temp": [ + "30" + ], + "temperature_vitrification": [ + "30" + ], + "textured_plate_temp_initial_layer": [ + "35" + ], + "textured_plate_temp": [ + "35" + ], + "textured_cool_plate_temp_initial_layer": [ + "30" + ], + "textured_cool_plate_temp": [ + "30" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI UltraPA @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI UltraPA @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..ae7b99531e --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI UltraPA @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "RzZGtHGu2xKASwwu", + "name": "QIDI UltraPA @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI UltraPA@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI UltraPA @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI UltraPA @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..e9bfbe4362 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI UltraPA @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "0seF33ojhYF6lNM4", + "name": "QIDI UltraPA @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI UltraPA@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI UltraPA @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI UltraPA @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..6f03ff7b5a --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI UltraPA @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "vfr7Lv94OHi4KOCw", + "name": "QIDI UltraPA @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI UltraPA@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI UltraPA @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI UltraPA @X-Plus 5.json new file mode 100644 index 0000000000..9d86c06f95 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI UltraPA @X-Plus 5.json @@ -0,0 +1,99 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_24", + "name": "QIDI UltraPA@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "55" + ], + "box_temperature_range_low": [ + "0" + ], + "box_temperature": [ + "55" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "cool_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "eng_plate_temp": [ + "80" + ], + "fan_max_speed": [ + "40" + ], + "fan_min_speed": [ + "20" + ], + "filament_adhesiveness_category": [ + "400" + ], + "filament_density": [ + "1.21" + ], + "filament_flow_ratio": [ + "0.96" + ], + "filament_max_volumetric_speed": [ + "4" + ], + "filament_type": [ + "UltraPA" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "hot_plate_temp": [ + "80" + ], + "impact_strength_z": [ + "15.5" + ], + "nozzle_temperature_initial_layer": [ + "280" + ], + "nozzle_temperature_range_high": [ + "290" + ], + "nozzle_temperature_range_low": [ + "250" + ], + "nozzle_temperature": [ + "280" + ], + "pressure_advance": [ + "0.03" + ], + "slow_down_layer_time": [ + "15" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], + "supertack_plate_temp": [ + "60" + ], + "temperature_vitrification": [ + "170" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "textured_cool_plate_temp_initial_layer": [ + "60" + ], + "textured_cool_plate_temp": [ + "60" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI UltraPA-CF25 @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI UltraPA-CF25 @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..c88635f447 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI UltraPA-CF25 @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,11 @@ +{ + "type": "filament", + "setting_id": "8h1ITuLzHYf0J4NA", + "name": "QIDI UltraPA-CF25 @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI UltraPA-CF25@X-Plus 5-Series", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI UltraPA-CF25 @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI UltraPA-CF25 @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..66ac797bb8 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI UltraPA-CF25 @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "WK63qDyLkG4fznPW", + "name": "QIDI UltraPA-CF25 @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI UltraPA-CF25@X-Plus 5-Series", + "pressure_advance": [ + "0.022" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI UltraPA-CF25 @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI UltraPA-CF25 @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..282fed912a --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI UltraPA-CF25 @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "53sirpAAiDOx0c8P", + "name": "QIDI UltraPA-CF25 @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI UltraPA-CF25@X-Plus 5-Series", + "pressure_advance": [ + "0.02" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI UltraPA-CF25 @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI UltraPA-CF25 @X-Plus 5.json new file mode 100644 index 0000000000..73379541b3 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI UltraPA-CF25 @X-Plus 5.json @@ -0,0 +1,114 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_26", + "name": "QIDI UltraPA-CF25@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "box_temperature_range_high": [ + "65" + ], + "box_temperature_range_low": [ + "0" + ], + "box_temperature": [ + "60" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "cool_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "eng_plate_temp": [ + "80" + ], + "during_print_exhaust_fan_speed": [ + "0" + ], + "fan_cooling_layer_time": [ + "5" + ], + "fan_max_speed": [ + "40" + ], + "fan_min_speed": [ + "20" + ], + "filament_adhesiveness_category": [ + "400" + ], + "filament_density": [ + "1.23" + ], + "filament_flow_ratio": [ + "0.94" + ], + "filament_max_volumetric_speed": [ + "8" + ], + "filament_type": [ + "UltraPA-CF25" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "hot_plate_temp": [ + "80" + ], + "impact_strength_z": [ + "15.5" + ], + "nozzle_temperature_initial_layer": [ + "300" + ], + "nozzle_temperature_range_high": [ + "320" + ], + "nozzle_temperature_range_low": [ + "300" + ], + "nozzle_temperature": [ + "300" + ], + "overhang_fan_speed": [ + "40" + ], + "overhang_fan_threshold": [ + "0%" + ], + "pressure_advance": [ + "0.026" + ], + "slow_down_layer_time": [ + "2" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], + "supertack_plate_temp": [ + "60" + ], + "temperature_vitrification": [ + "230" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "textured_plate_temp": [ + "80" + ], + "textured_cool_plate_temp_initial_layer": [ + "60" + ], + "textured_cool_plate_temp": [ + "60" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI WOOD Rapido @Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI WOOD Rapido @Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..2232226b78 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI WOOD Rapido @Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "gIumq0YfeZrNDTZt", + "name": "QIDI WOOD Rapido @Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI WOOD Rapido@X-Plus 5-Series", + "pressure_advance": [ + "0.044" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI WOOD Rapido @Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI WOOD Rapido @Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..b9ecf201ba --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI WOOD Rapido @Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "qUjZ8AUjivOk8uCf", + "name": "QIDI WOOD Rapido @Qidi X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI WOOD Rapido@X-Plus 5-Series", + "pressure_advance": [ + "0.024" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI WOOD Rapido @Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/filament/X5/QIDI WOOD Rapido @Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..fc15698443 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI WOOD Rapido @Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "setting_id": "A2kztfG9PA7Qxh7r", + "name": "QIDI WOOD Rapido @Qidi X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "QIDI WOOD Rapido@X-Plus 5-Series", + "pressure_advance": [ + "0.012" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/filament/X5/QIDI WOOD Rapido @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI WOOD Rapido @X-Plus 5.json new file mode 100644 index 0000000000..30428c7422 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/QIDI WOOD Rapido @X-Plus 5.json @@ -0,0 +1,66 @@ +{ + "type": "filament", + "filament_id": "QD_4_1_6", + "name": "QIDI WOOD Rapido@X-Plus 5-Series", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_x5_common", + "additional_cooling_fan_speed": [ + "100" + ], + "box_temperature_range_high": [ + "45" + ], + "filament_adhesiveness_category": [ + "100" + ], + "filament_density": [ + "1.23" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_max_volumetric_speed": [ + "18" + ], + "filament_type": [ + "PLA" + ], + "impact_strength_z": [ + "5.6" + ], + "nozzle_temperature_range_high": [ + "220" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "nozzle_temperature_initial_layer": [ + "210" + ], + "nozzle_temperature": [ + "210" + ], + "overhang_fan_threshold": [ + "50%" + ], + "pressure_advance": [ + "0.044" + ], + "supertack_plate_temp_initial_layer": [ + "45" + ], + "supertack_plate_temp": [ + "45" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp_initial_layer": [ + "45" + ], + "textured_cool_plate_temp": [ + "45" + ], + "compatible_printers": [] +} diff --git a/resources/profiles/Qidi/filament/X5/fdm_filament_x5_common.json b/resources/profiles/Qidi/filament/X5/fdm_filament_x5_common.json new file mode 100644 index 0000000000..a161158dc4 --- /dev/null +++ b/resources/profiles/Qidi/filament/X5/fdm_filament_x5_common.json @@ -0,0 +1,255 @@ +{ + "type": "filament", + "name": "fdm_filament_x5_common", + "from": "system", + "instantiation": "false", + "activate_air_filtration": [ + "1" + ], + "additional_cooling_fan_speed": [ + "0" + ], + "additional_fan_full_speed_layer": [ + "0" + ], + "bed_type": [ + "Cool Plate" + ], + "box_temperature_range_high": [ + "0" + ], + "box_temperature_range_low": [ + "0" + ], + "box_temperature": [ + "0" + ], + "chamber_temperatures": [ + "0" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "close_additional_fan_first_x_layers": [ + "3" + ], + "complete_print_exhaust_fan_speed": [ + "0" + ], + "cool_plate_temp_initial_layer": [ + "45" + ], + "cool_plate_temp": [ + "45" + ], + "during_print_exhaust_fan_speed": [ + "100" + ], + "enable_pressure_advance": [ + "1" + ], + "eng_plate_temp_initial_layer": [ + "60" + ], + "eng_plate_temp": [ + "60" + ], + "fan_cooling_layer_time": [ + "60" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "filament_cost": [ + "0" + ], + "filament_cooling_before_tower": [ + "0" + ], + "filament_tower_interface_pre_extrusion_dist": [ + "10" + ], + "filament_tower_interface_pre_extrusion_length": [ + "0" + ], + "filament_tower_ironing_area": [ + "4" + ], + "filament_tower_interface_purge_volume": [ + "20" + ], + "filament_tower_interface_print_temp": [ + "-1" + ], + "filament_density": [ + "1.24" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_dev_ams_drying_ams_limitations": [ + "1" + ], + "filament_dev_ams_drying_temperature": [ + "40.0", + "40.0", + "40.0", + "40.0" + ], + "filament_dev_ams_drying_time": [ + "8.0", + "8.0", + "8.0", + "8.0" + ], + "filament_dev_drying_softening_temperature": [ + "40.0" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "45.0" + ], + "filament_dev_drying_cooling_temperature": [ + "35.0" + ], + "filament_dev_chamber_drying_bed_temperature": [ + "90.0" + ], + "filament_dev_chamber_drying_time": [ + "12.0" + ], + "filament_diameter": [ + "1.75" + ], + "filament_end_gcode": [ + "; Filament-specific end gcode \n;END gcode for filament" + ], + "filament_extruder_compatibility": [ + "0" + ], + "filament_flow_ratio": [ + "0.98" + ], + "filament_flush_temp": [ + "0" + ], + "filament_flush_volumetric_speed": [ + "0" + ], + "filament_max_volumetric_speed": [ + "24.5" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_retract_before_wipe": [ + "nil" + ], + "filament_retract_restart_extra": [ + "nil" + ], + "filament_retract_when_changing_layer": [ + "nil" + ], + "filament_retraction_length": [ + "nil" + ], + "filament_retraction_minimum_travel": [ + "nil" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_settings_id": [ + "" + ], + "filament_soluble": [ + "0" + ], + "filament_start_gcode": [ + "; Filament start gcode" + ], + "filament_type": [ + "PLA" + ], + "filament_vendor": [ + "QIDI" + ], + "filament_wipe_distance": [ + "nil" + ], + "filament_wipe": [ + "nil" + ], + "filament_z_hop_types": [ + "nil" + ], + "filament_z_hop": [ + "nil" + ], + "filament_ramming_travel_time": [ + "0" + ], + "filament_pre_cooling_temperature": [ + "0" + ], + "filament_ramming_volumetric_speed": [ + "-1" + ], + "filament_prime_volume": [ + "30" + ], + "full_fan_speed_layer": [ + "0" + ], + "hot_plate_temp_initial_layer": [ + "60" + ], + "hot_plate_temp": [ + "60" + ], + "nozzle_temperature_initial_layer": [ + "210" + ], + "nozzle_temperature": [ + "210" + ], + "overhang_fan_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "95%" + ], + "pressure_advance": [ + "0.042" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "slow_down_layer_time": [ + "8" + ], + "slow_down_min_speed": [ + "20" + ], + "supertack_plate_temp_initial_layer": [ + "45" + ], + "supertack_plate_temp": [ + "45" + ], + "temperature_vitrification": [ + "100" + ], + "textured_plate_temp_initial_layer": [ + "60" + ], + "textured_plate_temp": [ + "60" + ] +} diff --git a/resources/profiles/Qidi/machine/Qidi Q2 0.4 nozzle.json b/resources/profiles/Qidi/machine/Qidi Q2 0.4 nozzle.json index 6f3115dbd5..6a0a91b2ac 100644 --- a/resources/profiles/Qidi/machine/Qidi Q2 0.4 nozzle.json +++ b/resources/profiles/Qidi/machine/Qidi Q2 0.4 nozzle.json @@ -17,7 +17,7 @@ "cooling_tube_length": "0", "parking_pos_retraction": "0", "extra_loading_move": "5", - "change_filament_gcode": "G1 Z{max_layer_z + 3.0} F1200\nTOOL_CHANGE_START F=[current_extruder] T=[next_extruder]\nBUFFER_MONITORING ENABLE=0\nDISABLE_ALL_SENSOR\nM106 S255\nMOVE_TO_TRASH\n{if long_retractions_when_cut[previous_extruder]}\nG1 E-{retraction_distances_when_cut[previous_extruder]} F{old_filament_e_feedrate}\n{else}\nG1 E-10 F{old_filament_e_feedrate}\n{endif}\nM400\nCUT_FILAMENT T=[current_extruder]\nMOVE_TO_TRASH\nM106 P2 S0\nUNLOAD_T[current_extruder]\nT[next_extruder]\n{if nozzle_temperature_range_high[current_extruder] >= nozzle_temperature_range_high[next_extruder]}\nM104 S{nozzle_temperature_range_high[current_extruder]}\n{else}\nM104 S{nozzle_temperature_range_high[next_extruder]}\n{endif}\n; FLUSH_START\nM106 S25\nG1 E30 F300\n; FLUSH_END\n{if long_retractions_when_cut[previous_extruder]}\nG1 E{retraction_distances_when_cut[previous_extruder]} F{old_filament_e_feedrate}\n{endif}\n{if flush_length_1 > 1}\n; FLUSH_START\n{if flush_length_1 > 23.7}\nG1 E23.7 F{old_filament_e_feedrate}\nG1 E{(flush_length_1 - 23.7) * 0.02} F50\nG1 E{(flush_length_1 - 23.7) * 0.23} F{old_filament_e_feedrate}\nG1 E{(flush_length_1 - 23.7) * 0.02} F50\nG1 E{(flush_length_1 - 23.7) * 0.23} F{new_filament_e_feedrate}\nG1 E{(flush_length_1 - 23.7) * 0.02} F50\nG1 E{(flush_length_1 - 23.7) * 0.23} F{new_filament_e_feedrate}\nG1 E{(flush_length_1 - 23.7) * 0.02} F50\nG1 E{(flush_length_1 - 23.7) * 0.23} F{new_filament_e_feedrate}\n{else}\nG1 E{flush_length_1} F{old_filament_e_feedrate}\n{endif}\nG1 E-[old_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\n{if flush_length_2 > 1}\n; FLUSH_START\nG1 X92 F9000\nG1 E[old_retract_length_toolchange] F300\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E-[new_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\n{if flush_length_3 > 1}\n; FLUSH_START\nG1 X85 F9000\nG1 E[new_retract_length_toolchange] F300\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E-[new_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\n{if flush_length_4 > 1}\n; FLUSH_START\nG1 X92 F9000\nG1 E[new_retract_length_toolchange] F300\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E-[new_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\nM400\nM106 S255\nM104 S[new_filament_temp]\nINIT_SYNC_BUFFER_STATE\nBUFFER_MONITORING ENABLE=1\nG1 E10 F25 \nM109 S[new_filament_temp]\nG1 E-5 F1800\nCLEAR_OOZE\nTOOL_CHANGE_END\nG1 Y270 F8000\nM106 S0\nG1 E2 F1800\nENABLE_ALL_SENSOR\n", + "change_filament_gcode": "{if current_extruder != next_extruder}\nG1 Z{max_layer_z + 3.0} F1200\nTOOL_CHANGE_START F=[current_extruder] T=[next_extruder]\nBUFFER_MONITORING ENABLE=0\nDISABLE_ALL_SENSOR\nM106 S255\nMOVE_TO_TRASH\n{if long_retractions_when_cut[previous_extruder]}\nG1 E-{retraction_distances_when_cut[previous_extruder]} F{old_filament_e_feedrate}\n{else}\nG1 E-10 F{old_filament_e_feedrate}\n{endif}\nM400\nCUT_FILAMENT T=[current_extruder]\nMOVE_TO_TRASH\nM106 P2 S0\nUNLOAD_T[current_extruder]\nT[next_extruder]\n{if nozzle_temperature_range_high[current_extruder] >= nozzle_temperature_range_high[next_extruder]}\nM104 S{nozzle_temperature_range_high[current_extruder]}\n{else}\nM104 S{nozzle_temperature_range_high[next_extruder]}\n{endif}\n; FLUSH_START\nM106 S25\nG1 E30 F300\n; FLUSH_END\n{if long_retractions_when_cut[previous_extruder]}\nG1 E{retraction_distances_when_cut[previous_extruder]} F{old_filament_e_feedrate}\n{endif}\n{if flush_length_1 > 1}\n; FLUSH_START\n{if flush_length_1 > 23.7}\nG1 E23.7 F{old_filament_e_feedrate}\nG1 E{(flush_length_1 - 23.7) * 0.02} F50\nG1 E{(flush_length_1 - 23.7) * 0.23} F{old_filament_e_feedrate}\nG1 E{(flush_length_1 - 23.7) * 0.02} F50\nG1 E{(flush_length_1 - 23.7) * 0.23} F{new_filament_e_feedrate}\nG1 E{(flush_length_1 - 23.7) * 0.02} F50\nG1 E{(flush_length_1 - 23.7) * 0.23} F{new_filament_e_feedrate}\nG1 E{(flush_length_1 - 23.7) * 0.02} F50\nG1 E{(flush_length_1 - 23.7) * 0.23} F{new_filament_e_feedrate}\n{else}\nG1 E{flush_length_1} F{old_filament_e_feedrate}\n{endif}\nG1 E-[old_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\n{if flush_length_2 > 1}\n; FLUSH_START\nG1 X92 F9000\nG1 E[old_retract_length_toolchange] F300\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E-[new_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\n{if flush_length_3 > 1}\n; FLUSH_START\nG1 X85 F9000\nG1 E[new_retract_length_toolchange] F300\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E-[new_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\n{if flush_length_4 > 1}\n; FLUSH_START\nG1 X92 F9000\nG1 E[new_retract_length_toolchange] F300\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E-[new_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\nM400\nM106 S255\nM104 S[new_filament_temp]\nINIT_SYNC_BUFFER_STATE\nBUFFER_MONITORING ENABLE=1\nG1 E10 F25 \nM109 S[new_filament_temp]\nG1 E-5 F1800\nCLEAR_OOZE\nTOOL_CHANGE_END\nG1 Y270 F8000\nM106 S0\nG1 E2 F1800\nENABLE_ALL_SENSOR\n{endif}\n", "default_filament_profile": [ "QIDI PLA Rapido @Qidi Q2 0.4 nozzle" ], diff --git a/resources/profiles/Qidi/machine/Qidi Q2C 0.4 nozzle.json b/resources/profiles/Qidi/machine/Qidi Q2C 0.4 nozzle.json index fbf61a6af9..9d4683885f 100644 --- a/resources/profiles/Qidi/machine/Qidi Q2C 0.4 nozzle.json +++ b/resources/profiles/Qidi/machine/Qidi Q2C 0.4 nozzle.json @@ -17,7 +17,7 @@ "cooling_tube_length": "0", "parking_pos_retraction": "0", "extra_loading_move": "5", - "change_filament_gcode": "G1 Z{max_layer_z + 3.0} F1200\nTOOL_CHANGE_START F=[current_extruder] T=[next_extruder]\nBUFFER_MONITORING ENABLE=0\nDISABLE_ALL_SENSOR\nM106 S255\nMOVE_TO_TRASH\n{if long_retractions_when_cut[previous_extruder]}\nG1 E-{retraction_distances_when_cut[previous_extruder]} F{old_filament_e_feedrate}\n{else}\nG1 E-10 F{old_filament_e_feedrate}\n{endif}\nM400\nCUT_FILAMENT T=[current_extruder]\nMOVE_TO_TRASH\nM106 P2 S0\nUNLOAD_T[current_extruder]\nT[next_extruder]\n{if nozzle_temperature_range_high[current_extruder] >= nozzle_temperature_range_high[next_extruder]}\nM104 S{nozzle_temperature_range_high[current_extruder]}\n{else}\nM104 S{nozzle_temperature_range_high[next_extruder]}\n{endif}\n; FLUSH_START\nM106 S25\nG1 E30 F300\n; FLUSH_END\n{if long_retractions_when_cut[previous_extruder]}\nG1 E{retraction_distances_when_cut[previous_extruder]} F{old_filament_e_feedrate}\n{endif}\n{if flush_length_1 > 1}\n; FLUSH_START\n{if flush_length_1 > 23.7}\nG1 E23.7 F{old_filament_e_feedrate}\nG1 E{(flush_length_1 - 23.7) * 0.02} F50\nG1 E{(flush_length_1 - 23.7) * 0.23} F{old_filament_e_feedrate}\nG1 E{(flush_length_1 - 23.7) * 0.02} F50\nG1 E{(flush_length_1 - 23.7) * 0.23} F{new_filament_e_feedrate}\nG1 E{(flush_length_1 - 23.7) * 0.02} F50\nG1 E{(flush_length_1 - 23.7) * 0.23} F{new_filament_e_feedrate}\nG1 E{(flush_length_1 - 23.7) * 0.02} F50\nG1 E{(flush_length_1 - 23.7) * 0.23} F{new_filament_e_feedrate}\n{else}\nG1 E{flush_length_1} F{old_filament_e_feedrate}\n{endif}\nG1 E-[old_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\n{if flush_length_2 > 1}\n; FLUSH_START\nG1 X92 F9000\nG1 E[old_retract_length_toolchange] F300\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E-[new_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\n{if flush_length_3 > 1}\n; FLUSH_START\nG1 X85 F9000\nG1 E[new_retract_length_toolchange] F300\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E-[new_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\n{if flush_length_4 > 1}\n; FLUSH_START\nG1 X92 F9000\nG1 E[new_retract_length_toolchange] F300\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E-[new_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\nM400\nM106 S255\nM104 S[new_filament_temp]\nINIT_SYNC_BUFFER_STATE\nBUFFER_MONITORING ENABLE=1\nG1 E10 F25 \nM109 S[new_filament_temp]\nG1 E-5 F1800\nCLEAR_OOZE\nTOOL_CHANGE_END\nG1 Y270 F8000\nM106 S0\nG1 E2 F1800\nENABLE_ALL_SENSOR\n", + "change_filament_gcode": "{if current_extruder != next_extruder}\nG1 Z{max_layer_z + 3.0} F1200\nTOOL_CHANGE_START F=[current_extruder] T=[next_extruder]\nBUFFER_MONITORING ENABLE=0\nDISABLE_ALL_SENSOR\nM106 S255\nMOVE_TO_TRASH\n{if long_retractions_when_cut[previous_extruder]}\nG1 E-{retraction_distances_when_cut[previous_extruder]} F{old_filament_e_feedrate}\n{else}\nG1 E-10 F{old_filament_e_feedrate}\n{endif}\nM400\nCUT_FILAMENT T=[current_extruder]\nMOVE_TO_TRASH\nM106 P2 S0\nUNLOAD_T[current_extruder]\nT[next_extruder]\n{if nozzle_temperature_range_high[current_extruder] >= nozzle_temperature_range_high[next_extruder]}\nM104 S{nozzle_temperature_range_high[current_extruder]}\n{else}\nM104 S{nozzle_temperature_range_high[next_extruder]}\n{endif}\n; FLUSH_START\nM106 S25\nG1 E30 F300\n; FLUSH_END\n{if long_retractions_when_cut[previous_extruder]}\nG1 E{retraction_distances_when_cut[previous_extruder]} F{old_filament_e_feedrate}\n{endif}\n{if flush_length_1 > 1}\n; FLUSH_START\n{if flush_length_1 > 23.7}\nG1 E23.7 F{old_filament_e_feedrate}\nG1 E{(flush_length_1 - 23.7) * 0.02} F50\nG1 E{(flush_length_1 - 23.7) * 0.23} F{old_filament_e_feedrate}\nG1 E{(flush_length_1 - 23.7) * 0.02} F50\nG1 E{(flush_length_1 - 23.7) * 0.23} F{new_filament_e_feedrate}\nG1 E{(flush_length_1 - 23.7) * 0.02} F50\nG1 E{(flush_length_1 - 23.7) * 0.23} F{new_filament_e_feedrate}\nG1 E{(flush_length_1 - 23.7) * 0.02} F50\nG1 E{(flush_length_1 - 23.7) * 0.23} F{new_filament_e_feedrate}\n{else}\nG1 E{flush_length_1} F{old_filament_e_feedrate}\n{endif}\nG1 E-[old_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\n{if flush_length_2 > 1}\n; FLUSH_START\nG1 X92 F9000\nG1 E[old_retract_length_toolchange] F300\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E-[new_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\n{if flush_length_3 > 1}\n; FLUSH_START\nG1 X85 F9000\nG1 E[new_retract_length_toolchange] F300\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E-[new_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\n{if flush_length_4 > 1}\n; FLUSH_START\nG1 X92 F9000\nG1 E[new_retract_length_toolchange] F300\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E-[new_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\nM400\nM106 S255\nM104 S[new_filament_temp]\nINIT_SYNC_BUFFER_STATE\nBUFFER_MONITORING ENABLE=1\nG1 E10 F25 \nM109 S[new_filament_temp]\nG1 E-5 F1800\nCLEAR_OOZE\nTOOL_CHANGE_END\nG1 Y270 F8000\nM106 S0\nG1 E2 F1800\nENABLE_ALL_SENSOR\n{endif}\n", "default_filament_profile": [ "QIDI PLA Rapido @Qidi Q2C 0.4 nozzle" ], diff --git a/resources/profiles/Qidi/machine/Qidi X-Max 4 0.4 nozzle.json b/resources/profiles/Qidi/machine/Qidi X-Max 4 0.4 nozzle.json index be754eb338..ef26c0a620 100644 --- a/resources/profiles/Qidi/machine/Qidi X-Max 4 0.4 nozzle.json +++ b/resources/profiles/Qidi/machine/Qidi X-Max 4 0.4 nozzle.json @@ -13,7 +13,7 @@ "bed_exclude_area": [ "0x0, 16x0, 16x13, 0x13, 0x0, 0x0, 0x0, 0x0, 0x13, 6x13, 6x23, 0x23, 0x13, 0x13, 0x13, 0x13, 0x387, 53x387, 53x390, 0x390, 0x387, 0x387, 0x397, 0x390, 338x390, 338x384, 390x384, 390x390, 0x390" ], - "change_filament_gcode": "G1 Z{max_layer_z + 3.0} F1200\nTOOL_CHANGE_START F=[current_extruder] T=[next_extruder]\nDISABLE_ALL_SENSOR\nM104 S{old_filament_temp - 10}\nM106 S255\n{if long_retractions_when_cut[previous_extruder]}\nG1 E-{retraction_distances_when_cut[previous_extruder]} F{old_filament_e_feedrate}\n{else}\nG1 E-2 F{old_filament_e_feedrate}\n{endif}\nM400\nCUT_FILAMENT T=[current_extruder]\nMOVE_TO_TRASH\nM106 P2 S0\nUNLOAD_T[current_extruder]\nT[next_extruder]\nM106 S0\n{if nozzle_temperature_range_high[current_extruder] >= nozzle_temperature_range_high[next_extruder]}\nM104 S{nozzle_temperature_range_high[current_extruder]}\nM109.0 S{(nozzle_temperature_range_high[current_extruder])-25}\n{else}\nM104 S{nozzle_temperature_range_high[next_extruder]}\nM109.0 S{(nozzle_temperature_range_high[next_extruder])-25}\n{endif}\n{if long_retractions_when_cut[previous_extruder]}\nG1 E{retraction_distances_when_cut[previous_extruder]} F{old_filament_e_feedrate}\n{endif}\n{if flush_length_1 > 1}\n; FLUSH_START\nG1 Y403.5 F2000\nG1 E{flush_length_1} F{old_filament_e_feedrate * 0.5}\n; FLUSH_END\n{endif}\n{if flush_length_2 > 1}\n; FLUSH_START\nG1 E{flush_length_2} F{new_filament_e_feedrate * 0.5}\n; FLUSH_END\n{endif}\n{if flush_length_3 > 1}\n; FLUSH_START\nG1 E{flush_length_3} F{new_filament_e_feedrate * 0.5}\n; FLUSH_END\n{endif}\n{if flush_length_4 > 1}\n; FLUSH_START\nG1 E{flush_length_4} F{new_filament_e_feedrate * 0.5}\n; FLUSH_END\n{endif}\nM400\nM106 S180\nM104 S{new_filament_temp - 10}\nG1 E1 F10\nM109.1 S{new_filament_temp - 10}\nG1 E-4 F1000\nG4 P2000\nM204 S5000\nG1 Y403 F2000\nG1 X163 F8000\nG1 X145 F5000\nG1 X163 F3000\nG1 X145 F2000\nG1 X175 F6000\nG1 X163\nG1 X175\nG1 X163\nG1 X175\nG1 X163\nG1 X180 F8000\nG1 Y380\nG1 X116\nG4 P2000\nG1 Y403 F3000\nG1 X130\nG1 X100 F8000\nG1 Y380\nG1 X116\nG1 Y403 F3000\nG1 X130 F3000\nG1 X100 F8000\nG1 Y380\nM104 S[new_filament_temp]\nTOOL_CHANGE_END\nG1 E{new_retract_length_toolchange + 1} F{new_filament_e_feedrate}\nENABLE_ALL_SENSOR\n", + "change_filament_gcode": "{if current_extruder != next_extruder}\nG1 Z{max_layer_z + 3.0} F1200\nTOOL_CHANGE_START F=[current_extruder] T=[next_extruder]\nDISABLE_ALL_SENSOR\nM104 S{old_filament_temp - 10}\nM106 S255\n{if long_retractions_when_cut[previous_extruder]}\nG1 E-{retraction_distances_when_cut[previous_extruder]} F{old_filament_e_feedrate}\n{else}\nG1 E-2 F{old_filament_e_feedrate}\n{endif}\nM400\nCUT_FILAMENT T=[current_extruder]\nMOVE_TO_TRASH\nM106 P2 S0\nUNLOAD_T[current_extruder]\nT[next_extruder]\nM106 S0\n{if nozzle_temperature_range_high[current_extruder] >= nozzle_temperature_range_high[next_extruder]}\nM104 S{nozzle_temperature_range_high[current_extruder]}\nM109.0 S{(nozzle_temperature_range_high[current_extruder])-25}\n{else}\nM104 S{nozzle_temperature_range_high[next_extruder]}\nM109.0 S{(nozzle_temperature_range_high[next_extruder])-25}\n{endif}\n{if long_retractions_when_cut[previous_extruder]}\nG1 E{retraction_distances_when_cut[previous_extruder]} F{old_filament_e_feedrate}\n{endif}\n{if flush_length_1 > 1}\n; FLUSH_START\nG1 Y403.5 F2000\nG1 E{flush_length_1} F{old_filament_e_feedrate * 0.5}\n; FLUSH_END\n{endif}\n{if flush_length_2 > 1}\n; FLUSH_START\nG1 E{flush_length_2} F{new_filament_e_feedrate * 0.5}\n; FLUSH_END\n{endif}\n{if flush_length_3 > 1}\n; FLUSH_START\nG1 E{flush_length_3} F{new_filament_e_feedrate * 0.5}\n; FLUSH_END\n{endif}\n{if flush_length_4 > 1}\n; FLUSH_START\nG1 E{flush_length_4} F{new_filament_e_feedrate * 0.5}\n; FLUSH_END\n{endif}\nM400\nM106 S180\nM104 S{new_filament_temp - 10}\nG1 E1 F10\nM109.1 S{new_filament_temp - 10}\nG1 E-4 F1000\nG4 P2000\nM204 S5000\nG1 Y403 F2000\nG1 X163 F8000\nG1 X145 F5000\nG1 X163 F3000\nG1 X145 F2000\nG1 X175 F6000\nG1 X163\nG1 X175\nG1 X163\nG1 X175\nG1 X163\nG1 X180 F8000\nG1 Y380\nG1 X116\nG4 P2000\nG1 Y403 F3000\nG1 X130\nG1 X100 F8000\nG1 Y380\nG1 X116\nG1 Y403 F3000\nG1 X130 F3000\nG1 X100 F8000\nG1 Y380\nM104 S[new_filament_temp]\nTOOL_CHANGE_END\nG1 E{new_retract_length_toolchange + 1} F{new_filament_e_feedrate}\nENABLE_ALL_SENSOR\n{endif}\n", "default_filament_profile": [ "QIDI PLA Rapido @Qidi X-Max 4 0.4 nozzle" ], diff --git a/resources/profiles/Qidi/machine/Qidi X-Plus 4 0.4 nozzle.json b/resources/profiles/Qidi/machine/Qidi X-Plus 4 0.4 nozzle.json index c4e758a451..82e9da78cb 100644 --- a/resources/profiles/Qidi/machine/Qidi X-Plus 4 0.4 nozzle.json +++ b/resources/profiles/Qidi/machine/Qidi X-Plus 4 0.4 nozzle.json @@ -60,7 +60,7 @@ "2" ], "single_extruder_multi_material": "1", - "change_filament_gcode": "{if max_layer_z < 12}\nG1 Z15 F1200\n{else}\nG1 Z{max_layer_z + 3.0} F1200\n{endif}\nTOOL_CHANGE_START F=[current_extruder] T=[next_extruder]\nDISABLE_ALL_SENSOR\n{if long_retractions_when_cut[previous_extruder]}\nMOVE_TO_TRASH\nG1 E-{retraction_distances_when_cut[previous_extruder]} F{old_filament_e_feedrate}\nM400\n{else}\nG1 E-5 F{old_filament_e_feedrate}\n{endif}\nCUT_FILAMENT T=[current_extruder]\nMOVE_TO_TRASH\nM400\n{if nozzle_temperature_range_high[current_extruder] >= nozzle_temperature_range_high[next_extruder]}\nM104 S{nozzle_temperature_range_high[current_extruder]}\n{else}\nM104 S{nozzle_temperature_range_high[next_extruder]}\n{endif}\nM106 S0\nM106 P2 S0\nUNLOAD_T[current_extruder]\nG92 E0\nM83\nG1 E2 F50\nT[next_extruder]\n{if nozzle_temperature_range_high[current_extruder] >= nozzle_temperature_range_high[next_extruder]}\nSET_HEATER_TEMPERATURE HEATER=extruder TARGET={nozzle_temperature_range_high[current_extruder]} WAIT=1\n{else}\nSET_HEATER_TEMPERATURE HEATER=extruder TARGET={nozzle_temperature_range_high[next_extruder]} WAIT=1\n{endif}\n{if long_retractions_when_cut[previous_extruder]}\nG1 E{retraction_distances_when_cut[previous_extruder]} F{old_filament_e_feedrate}\n{endif}\nM400\nM106 S60\n; FLUSH_START\nG1 E1 F50\nG1 E{65.5 * 0.58} F{old_filament_e_feedrate}\nG1 E{65.5 * 0.02} F50\nG1 E{65.5 * 0.18} F{old_filament_e_feedrate}\nG1 E{65.5 * 0.02} F50\nG1 E{65.5 * 0.18} F{old_filament_e_feedrate}\nG1 E{65.5 * 0.02} F50\nG1 E-[old_retract_length_toolchange] F1800\n; FLUSH_END\n{if flush_length_1 > 1}\nM400\nM106 S255\nG91\nG1 X-5 F60\nG1 X5 F60\nG90\nCLEAR_FLUSH\nM400\nM106 S60\n; FLUSH_START\nG1 E[old_retract_length_toolchange] F300\nG1 E{flush_length_1 * 0.58} F{new_filament_e_feedrate}\nG1 E{flush_length_1 * 0.02} F50\nG1 E{flush_length_1 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_1 * 0.02} F50\nG1 E{flush_length_1 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_1 * 0.02} F50\nG1 E-[old_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\n{if flush_length_2 > 1}\nM400\nM106 S255\nG91\nG1 X-5 F60\nG1 X5 F60\nG90\nCLEAR_FLUSH\nM400\nM106 S60\n; FLUSH_START\nG1 E[old_retract_length_toolchange] F300\nG1 E{flush_length_2 * 0.58} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E-[new_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\n{if flush_length_3 > 1}\nM400\nM106 S255\nG91\nG1 X-5 F60\nG1 X5 F60\nG90\nCLEAR_FLUSH\nM400\nM106 S60\n; FLUSH_START\nG1 E[new_retract_length_toolchange] F300\nG1 E{flush_length_3 * 0.58} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E-[new_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\n{if flush_length_4 > 1}\nM400\nM106 S255\nG91\nG1 X-5 F60\nG1 X5 F60\nG90\nCLEAR_FLUSH\nM400\nM106 S60\n; FLUSH_START\nG1 E[new_retract_length_toolchange] F300\nG1 E{flush_length_4 * 0.58} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E-[new_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\nM104 S[new_filament_temp]\nM400\nM106 S255\nG91\nG1 X-5 F60\nG1 X5 F60\nG90\nM109 S[new_filament_temp]\nG92 E0\nM400\nCLEAR_FLUSH\nCLEAR_OOZE\nM400\nM106 S0\nTOOL_CHANGE_END\nG1 Y305 F9000\nENABLE_ALL_SENSOR", + "change_filament_gcode": "{if current_extruder != next_extruder}\n{if max_layer_z < 12}\nG1 Z15 F1200\n{else}\nG1 Z{max_layer_z + 3.0} F1200\n{endif}\nTOOL_CHANGE_START F=[current_extruder] T=[next_extruder]\nDISABLE_ALL_SENSOR\n{if long_retractions_when_cut[previous_extruder]}\nMOVE_TO_TRASH\nG1 E-{retraction_distances_when_cut[previous_extruder]} F{old_filament_e_feedrate}\nM400\n{else}\nG1 E-5 F{old_filament_e_feedrate}\n{endif}\nCUT_FILAMENT T=[current_extruder]\nMOVE_TO_TRASH\nM400\n{if nozzle_temperature_range_high[current_extruder] >= nozzle_temperature_range_high[next_extruder]}\nM104 S{nozzle_temperature_range_high[current_extruder]}\n{else}\nM104 S{nozzle_temperature_range_high[next_extruder]}\n{endif}\nM106 S0\nM106 P2 S0\nUNLOAD_T[current_extruder]\nG92 E0\nM83\nG1 E2 F50\nT[next_extruder]\n{if nozzle_temperature_range_high[current_extruder] >= nozzle_temperature_range_high[next_extruder]}\nSET_HEATER_TEMPERATURE HEATER=extruder TARGET={nozzle_temperature_range_high[current_extruder]} WAIT=1\n{else}\nSET_HEATER_TEMPERATURE HEATER=extruder TARGET={nozzle_temperature_range_high[next_extruder]} WAIT=1\n{endif}\n{if long_retractions_when_cut[previous_extruder]}\nG1 E{retraction_distances_when_cut[previous_extruder]} F{old_filament_e_feedrate}\n{endif}\nM400\nM106 S60\n; FLUSH_START\nG1 E1 F50\nG1 E{65.5 * 0.58} F{old_filament_e_feedrate}\nG1 E{65.5 * 0.02} F50\nG1 E{65.5 * 0.18} F{old_filament_e_feedrate}\nG1 E{65.5 * 0.02} F50\nG1 E{65.5 * 0.18} F{old_filament_e_feedrate}\nG1 E{65.5 * 0.02} F50\nG1 E-[old_retract_length_toolchange] F1800\n; FLUSH_END\n{if flush_length_1 > 1}\nM400\nM106 S255\nG91\nG1 X-5 F60\nG1 X5 F60\nG90\nCLEAR_FLUSH\nM400\nM106 S60\n; FLUSH_START\nG1 E[old_retract_length_toolchange] F300\nG1 E{flush_length_1 * 0.58} F{new_filament_e_feedrate}\nG1 E{flush_length_1 * 0.02} F50\nG1 E{flush_length_1 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_1 * 0.02} F50\nG1 E{flush_length_1 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_1 * 0.02} F50\nG1 E-[old_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\n{if flush_length_2 > 1}\nM400\nM106 S255\nG91\nG1 X-5 F60\nG1 X5 F60\nG90\nCLEAR_FLUSH\nM400\nM106 S60\n; FLUSH_START\nG1 E[old_retract_length_toolchange] F300\nG1 E{flush_length_2 * 0.58} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E{flush_length_2 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_2 * 0.02} F50\nG1 E-[new_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\n{if flush_length_3 > 1}\nM400\nM106 S255\nG91\nG1 X-5 F60\nG1 X5 F60\nG90\nCLEAR_FLUSH\nM400\nM106 S60\n; FLUSH_START\nG1 E[new_retract_length_toolchange] F300\nG1 E{flush_length_3 * 0.58} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E{flush_length_3 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_3 * 0.02} F50\nG1 E-[new_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\n{if flush_length_4 > 1}\nM400\nM106 S255\nG91\nG1 X-5 F60\nG1 X5 F60\nG90\nCLEAR_FLUSH\nM400\nM106 S60\n; FLUSH_START\nG1 E[new_retract_length_toolchange] F300\nG1 E{flush_length_4 * 0.58} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E{flush_length_4 * 0.18} F{new_filament_e_feedrate}\nG1 E{flush_length_4 * 0.02} F50\nG1 E-[new_retract_length_toolchange] F1800\n; FLUSH_END\n{endif}\nM104 S[new_filament_temp]\nM400\nM106 S255\nG91\nG1 X-5 F60\nG1 X5 F60\nG90\nM109 S[new_filament_temp]\nG92 E0\nM400\nCLEAR_FLUSH\nCLEAR_OOZE\nM400\nM106 S0\nTOOL_CHANGE_END\nG1 Y305 F9000\nENABLE_ALL_SENSOR\n{endif}", "is_support_multi_box": "0", "machine_pause_gcode": "PAUSE", "thumbnails": [ diff --git a/resources/profiles/Qidi/machine/Qidi X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/machine/Qidi X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..54fed33aea --- /dev/null +++ b/resources/profiles/Qidi/machine/Qidi X-Plus 5 0.2 nozzle.json @@ -0,0 +1,28 @@ +{ + "type": "machine", + "name": "Qidi X-Plus 5 0.2 nozzle", + "inherits": "Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "setting_id": "prjWhtWxCsXqcrHr", + "instantiation": "true", + "printer_model": "Qidi X-Plus 5", + "printer_variant": "0.2", + "default_filament_profile": [ + "QIDI PLA Rapido @Qidi X-Plus 5 0.2 nozzle" + ], + "default_print_profile": "0.10mm Standard @X-Plus 5 0.2 nozzle", + "max_layer_height": [ + "0.14" + ], + "min_layer_height": [ + "0.04" + ], + "nozzle_diameter": [ + "0.2" + ], + "printer_agent": "qidi", + "retraction_length": [ + "0.4" + ], + "support_box_temp_control": "1" +} diff --git a/resources/profiles/Qidi/machine/Qidi X-Plus 5 0.4 nozzle.json b/resources/profiles/Qidi/machine/Qidi X-Plus 5 0.4 nozzle.json new file mode 100644 index 0000000000..f23f86af2a --- /dev/null +++ b/resources/profiles/Qidi/machine/Qidi X-Plus 5 0.4 nozzle.json @@ -0,0 +1,88 @@ +{ + "type": "machine", + "name": "Qidi X-Plus 5 0.4 nozzle", + "inherits": "fdm_machine_x_common", + "from": "system", + "setting_id": "exeoc5LTdCkPeWLD", + "instantiation": "true", + "printer_model": "Qidi X-Plus 5", + "auxiliary_fan": "1", + "bed_exclude_area": [ + "0x0,9x0,9x13,0x13" + ], + "box_id": "4", + "change_filament_gcode": "{if current_extruder != next_extruder}\n{if max_layer_z + 3 > max_print_height}\nG0 Z{max_print_height} F1200\n{else}\nG0 Z{max_layer_z + 3} F1200\n{endif}\nTOOL_CHANGE_START F=[current_extruder] T=[next_extruder]\nDISABLE_ALL_SENSOR\nM104 S{old_filament_temp - 10}\nM106 S255\n{if long_retractions_when_cut[previous_extruder]}\nG1 E-{retraction_distances_when_cut[previous_extruder]} F{old_filament_e_feedrate}\n{else}\nG1 E-5 F{old_filament_e_feedrate}\n{endif}\nM400\nCUT_FILAMENT T=[current_extruder]\nMOVE_TO_TRASH\nM106 P2 S0\nUNLOAD_T[current_extruder]\nT[next_extruder]\nM106 S0\n{if nozzle_temperature_range_high[current_extruder] >= nozzle_temperature_range_high[next_extruder]}\nM104 S{nozzle_temperature_range_high[current_extruder]}\nM109.0 S{(nozzle_temperature_range_high[current_extruder])-25}\n{else}\nM104 S{nozzle_temperature_range_high[next_extruder]}\nM109.0 S{(nozzle_temperature_range_high[next_extruder])-25}\n{endif}\n{if long_retractions_when_cut[previous_extruder]}\nG1 E{retraction_distances_when_cut[previous_extruder]} F{old_filament_e_feedrate}\n{endif}\n{if flush_length_1 > 1}\n; FLUSH_START\nG1 E{flush_length_1} F{old_filament_e_feedrate * 0.5}\n; FLUSH_END\n{endif}\n{if flush_length_2 > 1}\n; FLUSH_START\nG1 E{flush_length_2} F{new_filament_e_feedrate * 0.5}\n; FLUSH_END\n{endif}\n{if flush_length_3 > 1}\n; FLUSH_START\nG1 E{flush_length_3} F{new_filament_e_feedrate * 0.5}\n; FLUSH_END\n{endif}\n{if flush_length_4 > 1}\n; FLUSH_START\nG1 E{flush_length_4} F{new_filament_e_feedrate * 0.5}\n; FLUSH_END\n{endif}\nM400\nM106 S180\nM104 S{new_filament_temp - 10}\nG1 E1 F10\nM109.1 S{new_filament_temp - 10}\nG1 E-4 F1000\nG4 P2000\nM204 S5000\nG1 X109 F8000\nG1 X95 F5000\nG1 X109 F8000\nG1 X95 F5000\nG1 X140 F10000\nG1 Y319\nG1 X110\nG4 P2000\nG1 Y339 F2000\nG1 X123 F6000\nG1 X110\nG1 X123\nG1 X110\nG1 X123\nG1 X110\nG1 X123\nG1 X95\nG1 Y319 F10000\nM104 S[new_filament_temp]\nTOOL_CHANGE_END\nG1 E{new_retract_length_toolchange} F{new_filament_e_feedrate}\nENABLE_ALL_SENSOR\n{endif}", + "default_bed_type": "Textured PEI Plate", + "default_filament_profile": [ + "QIDI PLA Rapido @Qidi X-Plus 5 0.4 nozzle" + ], + "default_print_profile": "0.20mm Standard @X-Plus 5", + "enable_long_retraction_when_cut": "2", + "extruder_clearance_dist_to_rod": "42", + "extruder_clearance_height_to_lid": "168", + "extruder_clearance_height_to_rod": "42", + "extruder_clearance_max_radius": "75", + "fan_direction": "left", + "gcode_flavor": "klipper", + "is_support_3mf": "1", + "is_support_mqtt": "1", + "is_support_multi_box": "1", + "is_support_polar_cooler": "1", + "is_support_timelapse": "1", + "layer_change_gcode": "{if timelapse_type == 1} ; timelapse with wipe tower\nG92 E0\nG1 E-[retraction_length] F1800\n{if layer_z + 0.4 > max_print_height}\nG2 Z{max_print_height} I0.86 J0.86 P1 F20000 ; spiral lift a little\n{else}\nG2 Z{layer_z + 0.4} I0.86 J0.86 P1 F20000 ; spiral lift a little\n{endif}\nMOVE_TO_TRASH\n{if layer_z <=25}\nG1 Z25\n{endif}\nG92 E0\nM400\nTIMELAPSE_TAKE_FRAME\nG1 E[retraction_length] F300\nG1 X140 F8000\nG1 Y319\n{if layer_z <=25}\nG1 Z[layer_z]\n{endif}\n{elsif timelapse_type == 0} ; timelapse without wipe tower\nTIMELAPSE_TAKE_FRAME\n{endif}\nG92 E0\nSET_PRINT_STATS_INFO CURRENT_LAYER={layer_num + 1}", + "machine_end_gcode": "SET_PRINT_MAIN_STATUS MAIN_STATUS=print_end\nDISABLE_BOX_HEATER\nM141 S0\nM140 S0\nDISABLE_ALL_SENSOR\nG1 E-3 F1800\n{if max_layer_z + 3 > max_print_height}\nG0 Z{max_print_height} F600\n{else}\nG0 Z{max_layer_z + 3} F600\n{endif}\nUNLOAD_FILAMENT T=[current_extruder]\nG0 Y320 F12000\nG0 X105 Y320 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}\nM104 S0\nPRINT_END", + "machine_max_acceleration_x": [ + "20000" + ], + "machine_max_acceleration_y": [ + "20000" + ], + "machine_max_jerk_e": [ + "4" + ], + "machine_max_jerk_x": [ + "9" + ], + "machine_max_jerk_y": [ + "9" + ], + "machine_max_jerk_z": [ + "4" + ], + "machine_max_speed_x": [ + "600" + ], + "machine_max_speed_y": [ + "600" + ], + "machine_max_speed_z": [ + "20" + ], + "machine_pause_gcode": "PAUSE", + "machine_start_gcode": ";===== PRINT_PHASE_INIT =====\nSET_PRINT_STATS_INFO TOTAL_LAYER=[total_layer_count]\nSET_PRINT_MAIN_STATUS MAIN_STATUS=print_start\nM220 S100\nM221 S100\nDISABLE_ALL_SENSOR\nM1002 R1\nM107\nCLEAR_PAUSE\nM140 S[bed_temperature_initial_layer_single]\nM141 S[chamber_temperature]\nG29.0\nSET_PRINT_SUB_STATUS SUB_STATUS=tool_head_reset\nG28\n\n;===== BOX_PREPAR =====\nSET_PRINT_SUB_STATUS SUB_STATUS=change_filament\nBOX_PRINT_START EXTRUDER=[initial_no_support_extruder] HOTENDTEMP={nozzle_temperature_range_high[initial_tool]}\nM400\nEXTRUSION_AND_FLUSH HOTEND=[nozzle_temperature_initial_layer]\n\n;===== CLEAR_NOZZLE =====\nSET_PRINT_SUB_STATUS SUB_STATUS=flush_filament\nG1 Z20 F480\nMOVE_TO_TRASH\n{if chamber_temperature[0] == 0}\nM106 P3 S[during_print_exhaust_fan_speed]\n{else}\nM106 P3 S0\n{endif}\nM1004\nM106 S0\nM109 S[nozzle_temperature_initial_layer]\nG92 E0\nM83\nG1 E5 F80\nG1 E200 F300\nM400\nM106 S255\nG1 E-3 F1000\nM104 S140\nSET_PRINT_SUB_STATUS SUB_STATUS=clear_nozzle\nM109.1 S{nozzle_temperature_initial_layer[0]-30}\nM204 S10000\nG1 X109 F10000\nG1 X95 F6000\nG1 X109 F10000\nG1 X95 F6000\nG1 X109 F10000\nG1 X95 F6000\nG1 Y318\nG1 X151 F15000\nG1 Y327\nG1 Z5 F480\nM400\nprobe samples=1\nG91\nG1 Z0.1\nG90\nM106 S255\nM109.1 S150\nG91\nG1 X20 F200\nG1 Y3\nG1 X-20\nG1 Y-3\nG1 X20\nG90\nG2 I0.5 J0.5 F480\nG2 I0.5 J0.5\nG2 I0.5 J0.5\nG1 Z10\nG1 Y318 F12000\nG1 X77\nG1 Y338 F2000\nG1 X95 F12000\nG1 X123\nG1 X110\nG1 X123\nG1 X110\nG1 X123\nG1 X110\nG1 X123\nG1 X110\nG1 X140\nG1 X160 Y160\nM106 S0\nSET_PRINT_SUB_STATUS SUB_STATUS=wait_bed_temp\nM190 S[bed_temperature_initial_layer_single]\nSET_PRINT_SUB_STATUS SUB_STATUS=wait_chamber_temp\nM191 S[chamber_temperature]\nG1 Y-2 F15000\nG1 X15\nG1 X-2 F5000\nG4 P1000\nG1 X-1 F1000\nG1 X-2 F5000\nG4 P1000\nG1 E-4 F1800\nG1 X15 F3000\nG1 X20 Y20 F15000\nSET_PRINT_SUB_STATUS SUB_STATUS=z_tilt_adjust\nZ_TILT_ADJUST\nSET_PRINT_SUB_STATUS SUB_STATUS=auto_bed_adjust\nG29\nM1002 A1\nG1 X160 Y160 Z10 F20000\nM1006 Z{10 - ((nozzle_temperature_initial_layer[initial_tool] - 130) / 14 - 5.0) / 100}\nG0 Y-1\nM109 S[nozzle_temperature_initial_layer]\nENABLE_ALL_SENSOR\n\n;===== PRINT_START =====\n; LAYER_HEIGHT: 0.2\nT[initial_tool]\nM140 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nM141 S[chamber_temperature]\nG4 P3000\nprobe samples=1\nG91\nG0 Z0.6 F480\nG90\nG1 X140 Y1 F20000\nG1 E5 F{filament_max_volumetric_speed[initial_no_support_extruder]/2/2.4053*60}\nG1 X180 E20 F{filament_max_volumetric_speed[initial_no_support_extruder]/2/2.4053*60}\nG1 Z1 F480\nSET_PRINT_MAIN_STATUS MAIN_STATUS=printing", + "nozzle_diameter": [ + "0.4" + ], + "nozzle_volume": [ + "125" + ], + "printable_area": [ + "0x0", + "320x0", + "320x320", + "0x320", + "0x0" + ], + "printable_height": "300", + "printer_agent": "qidi", + "printer_settings_id": "Qidi", + "retract_lift_below": [ + "299" + ], + "support_box_temp_control": "1", + "support_multi_bed_types": "1", + "thumbnail_size": [ + "50x50" + ], + "use_3mf": "1" +} diff --git a/resources/profiles/Qidi/machine/Qidi X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/machine/Qidi X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..649462eab9 --- /dev/null +++ b/resources/profiles/Qidi/machine/Qidi X-Plus 5 0.6 nozzle.json @@ -0,0 +1,31 @@ +{ + "type": "machine", + "name": "Qidi X-Plus 5 0.6 nozzle", + "inherits": "Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "setting_id": "C4EIoDQYYzDGANJA", + "instantiation": "true", + "printer_model": "Qidi X-Plus 5", + "printer_variant": "0.6", + "default_filament_profile": [ + "QIDI PLA Rapido @Qidi X-Plus 5 0.6 nozzle" + ], + "default_print_profile": "0.30mm Standard @X-Plus 5 0.6 nozzle", + "max_layer_height": [ + "0.42" + ], + "min_layer_height": [ + "0.12" + ], + "nozzle_diameter": [ + "0.6" + ], + "printer_agent": "qidi", + "retraction_length": [ + "1.4" + ], + "retraction_minimum_travel": [ + "3" + ], + "support_box_temp_control": "1" +} diff --git a/resources/profiles/Qidi/machine/Qidi X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/machine/Qidi X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..ed932902f9 --- /dev/null +++ b/resources/profiles/Qidi/machine/Qidi X-Plus 5 0.8 nozzle.json @@ -0,0 +1,31 @@ +{ + "type": "machine", + "name": "Qidi X-Plus 5 0.8 nozzle", + "inherits": "Qidi X-Plus 5 0.4 nozzle", + "from": "system", + "setting_id": "5eqkluxp0SrRzhV7", + "instantiation": "true", + "printer_model": "Qidi X-Plus 5", + "printer_variant": "0.8", + "default_filament_profile": [ + "QIDI PLA Rapido @Qidi X-Plus 5 0.8 nozzle" + ], + "default_print_profile": "0.40mm Standard @X-Plus 5 0.8 nozzle", + "max_layer_height": [ + "0.56" + ], + "min_layer_height": [ + "0.16" + ], + "nozzle_diameter": [ + "0.8" + ], + "printer_agent": "qidi", + "retract_length_toolchange": [ + "3" + ], + "retraction_length": [ + "3" + ], + "support_box_temp_control": "1" +} diff --git a/resources/profiles/Qidi/machine/Qidi X-Plus 5.json b/resources/profiles/Qidi/machine/Qidi X-Plus 5.json new file mode 100644 index 0000000000..afc1d9af54 --- /dev/null +++ b/resources/profiles/Qidi/machine/Qidi X-Plus 5.json @@ -0,0 +1,12 @@ +{ + "type": "machine_model", + "name": "Qidi X-Plus 5", + "model_id": "Qidi-XPlus-5", + "nozzle_diameter": "0.4;0.2;0.6;0.8", + "machine_tech": "FFF", + "family": "Qidi", + "bed_model": "qidi_xplus5_buildplate_model.stl", + "bed_texture": "qidi_xplus5_buildplate_texture.svg", + "hotend_model": "qidi_xseries_gen3_hotend.stl", + "default_materials": "Generic ABS @Qidi X-Plus 5 0.4 nozzle;Generic PLA @Qidi X-Plus 5 0.4 nozzle;QIDI ABS Odorless @Qidi X-Plus 5 0.4 nozzle;QIDI ABS Rapido @Qidi X-Plus 5 0.4 nozzle;QIDI PLA Rapido @Qidi X-Plus 5 0.2 nozzle;QIDI PLA Rapido @Qidi X-Plus 5 0.4 nozzle;QIDI PLA Rapido @Qidi X-Plus 5 0.6 nozzle;QIDI PLA Rapido @Qidi X-Plus 5 0.8 nozzle;QIDI PLA Rapido Matte @Qidi X-Plus 5 0.4 nozzle;QIDI PLA-CF @Qidi X-Plus 5 0.4 nozzle;Generic PLA Silk @Qidi X-Plus 5 0.4 nozzle;QIDI PLA Silk @Qidi X-Plus 5 0.4 nozzle;QIDI ASA @Qidi X-Plus 5 0.4 nozzle;QIDI PETG Basic @Qidi X-Plus 5 0.4 nozzle;QIDI PETG Rapido @Qidi X-Plus 5 0.4 nozzle;QIDI PETG Tough @Qidi X-Plus 5 0.4 nozzle;QIDI PETG Translucent @Qidi X-Plus 5 0.4 nozzle;QIDI PLA Basic @Qidi X-Plus 5 0.4 nozzle;QIDI PLA Matte Basic @Qidi X-Plus 5 0.4 nozzle;Generic PETG @Qidi X-Plus 5 0.4 nozzle" +} diff --git a/resources/profiles/Qidi/process/0.08mm High Quality @X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/process/0.08mm High Quality @X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..6ef2deee1a --- /dev/null +++ b/resources/profiles/Qidi/process/0.08mm High Quality @X-Plus 5 0.2 nozzle.json @@ -0,0 +1,70 @@ +{ + "type": "process", + "setting_id": "ojwggKwtZ95dDdGn", + "name": "0.08mm High Quality @X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "fdm_process_n_common", + "bottom_color_penetration_layers": "5", + "bottom_shell_layers": "5", + "bridge_flow": "1", + "default_acceleration": [ + "10000" + ], + "elefant_foot_compensation": "0.15", + "enable_arc_fitting": "0", + "initial_layer_infill_speed": [ + "70" + ], + "initial_layer_line_width": "0.25", + "initial_layer_print_height": "0.1", + "initial_layer_speed": [ + "40" + ], + "inner_wall_acceleration": [ + "0" + ], + "inner_wall_line_width": "0.22", + "inner_wall_speed": [ + "150" + ], + "internal_solid_infill_line_width": "0.22", + "layer_height": "0.08", + "line_width": "0.22", + "outer_wall_acceleration": [ + "5000" + ], + "outer_wall_line_width": "0.22", + "outer_wall_speed": [ + "100" + ], + "overhang_1_4_speed": [ + "60" + ], + "prime_tower_width": "60", + "prime_tower_brim_width": "-1", + "prime_tower_flat_ironing": "1", + "skin_infill_line_width": "0.22", + "skeleton_infill_line_width": "0.22", + "sparse_infill_line_width": "0.22", + "sparse_infill_pattern": "gyroid", + "sparse_infill_speed": [ + "100" + ], + "support_bottom_z_distance": "0.08", + "support_line_width": "0.22", + "support_top_z_distance": "0.08", + "top_color_penetration_layers": "7", + "top_shell_layers": "7", + "top_surface_line_width": "0.22", + "top_surface_speed": [ + "150" + ], + "travel_speed": [ + "500" + ], + "wall_loops": "4", + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/process/0.08mm High Quality @X-Plus 5.json b/resources/profiles/Qidi/process/0.08mm High Quality @X-Plus 5.json new file mode 100644 index 0000000000..5b76c79ef4 --- /dev/null +++ b/resources/profiles/Qidi/process/0.08mm High Quality @X-Plus 5.json @@ -0,0 +1,73 @@ +{ + "type": "process", + "setting_id": "pdbmnAHtrlkeD1D6", + "name": "0.08mm High Quality @X-Plus 5", + "from": "system", + "instantiation": "true", + "inherits": "fdm_process_n_common", + "bottom_color_penetration_layers": "7", + "bottom_shell_layers": "7", + "bridge_flow": "1", + "default_acceleration": [ + "4000" + ], + "elefant_foot_compensation": "0.15", + "enable_arc_fitting": "0", + "gap_infill_speed": [ + "210" + ], + "initial_layer_infill_speed": [ + "105" + ], + "initial_layer_speed": [ + "50" + ], + "inner_wall_acceleration": [ + "0" + ], + "inner_wall_speed": [ + "120" + ], + "internal_solid_infill_speed": [ + "150" + ], + "ironing_flow": "8%", + "layer_height": "0.08", + "outer_wall_acceleration": [ + "2000" + ], + "outer_wall_speed": [ + "60" + ], + "overhang_1_4_speed": [ + "60" + ], + "overhang_2_4_speed": [ + "30" + ], + "overhang_3_4_speed": [ + "10" + ], + "prime_tower_width": "60", + "prime_tower_brim_width": "-1", + "prime_tower_flat_ironing": "0", + "sparse_infill_speed": [ + "150" + ], + "sparse_infill_pattern": "gyroid", + "support_bottom_z_distance": "0.08", + "support_threshold_angle": "15", + "support_top_z_distance": "0.08", + "top_color_penetration_layers": "9", + "top_shell_layers": "9", + "top_shell_thickness": "1", + "top_surface_speed": [ + "150" + ], + "travel_speed": [ + "350" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/process/0.10mm Standard @X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/process/0.10mm Standard @X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..a8a9aa5f37 --- /dev/null +++ b/resources/profiles/Qidi/process/0.10mm Standard @X-Plus 5 0.2 nozzle.json @@ -0,0 +1,63 @@ +{ + "type": "process", + "setting_id": "3k8N3voNC9Kp0Ov6", + "name": "0.10mm Standard @X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "fdm_process_n_common", + "bottom_color_penetration_layers": "5", + "bottom_shell_layers": "5", + "bridge_flow": "1", + "elefant_foot_compensation": "0.15", + "enable_arc_fitting": "0", + "initial_layer_infill_speed": [ + "70" + ], + "initial_layer_line_width": "0.25", + "initial_layer_print_height": "0.1", + "initial_layer_speed": [ + "40" + ], + "inner_wall_acceleration": [ + "0" + ], + "inner_wall_line_width": "0.22", + "internal_solid_infill_line_width": "0.22", + "ironing_flow": "20%", + "ironing_speed": "20", + "layer_height": "0.1", + "line_width": "0.22", + "outer_wall_acceleration": [ + "5000" + ], + "outer_wall_line_width": "0.22", + "overhang_1_4_speed": [ + "60" + ], + "skin_infill_line_width": "0.22", + "outer_wall_speed": [ + "100" + ], + "prime_tower_width": "60", + "skeleton_infill_line_width": "0.22", + "sparse_infill_line_width": "0.22", + "sparse_infill_speed": [ + "100" + ], + "support_bottom_z_distance": "0.1", + "support_line_width": "0.22", + "support_top_z_distance": "0.1", + "top_color_penetration_layers": "7", + "top_shell_layers": "7", + "top_surface_line_width": "0.22", + "top_surface_speed": [ + "150" + ], + "travel_speed": [ + "500" + ], + "wall_loops": "4", + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/process/0.12mm Balanced Quality @X-Plus 5 0.2 nozzle.json b/resources/profiles/Qidi/process/0.12mm Balanced Quality @X-Plus 5 0.2 nozzle.json new file mode 100644 index 0000000000..1c1d304ccd --- /dev/null +++ b/resources/profiles/Qidi/process/0.12mm Balanced Quality @X-Plus 5 0.2 nozzle.json @@ -0,0 +1,66 @@ +{ + "type": "process", + "setting_id": "O20HfmdyTRg2xayA", + "name": "0.12mm Balanced Quality @X-Plus 5 0.2 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "fdm_process_n_common", + "bottom_color_penetration_layers": "5", + "bottom_shell_layers": "5", + "bridge_flow": "1", + "default_acceleration": [ + "10000" + ], + "elefant_foot_compensation": "0.15", + "enable_arc_fitting": "0", + "initial_layer_infill_speed": [ + "70" + ], + "initial_layer_line_width": "0.25", + "initial_layer_print_height": "0.1", + "initial_layer_speed": [ + "40" + ], + "inner_wall_acceleration": [ + "0" + ], + "inner_wall_line_width": "0.22", + "internal_solid_infill_line_width": "0.22", + "layer_height": "0.12", + "line_width": "0.22", + "outer_wall_acceleration": [ + "5000" + ], + "outer_wall_line_width": "0.22", + "outer_wall_speed": [ + "100" + ], + "overhang_1_4_speed": [ + "60" + ], + "prime_tower_width": "60", + "prime_tower_brim_width": "-1", + "prime_tower_flat_ironing": "1", + "skin_infill_line_width": "0.22", + "skeleton_infill_line_width": "0.22", + "sparse_infill_line_width": "0.22", + "sparse_infill_speed": [ + "100" + ], + "support_bottom_z_distance": "0.12", + "support_line_width": "0.22", + "support_top_z_distance": "0.12", + "top_color_penetration_layers": "7", + "top_shell_layers": "7", + "top_surface_line_width": "0.22", + "top_surface_speed": [ + "150" + ], + "travel_speed": [ + "500" + ], + "wall_loops": "4", + "compatible_printers": [ + "Qidi X-Plus 5 0.2 nozzle" + ] +} diff --git a/resources/profiles/Qidi/process/0.12mm High Quality @X-Plus 5.json b/resources/profiles/Qidi/process/0.12mm High Quality @X-Plus 5.json new file mode 100644 index 0000000000..a74aecb369 --- /dev/null +++ b/resources/profiles/Qidi/process/0.12mm High Quality @X-Plus 5.json @@ -0,0 +1,73 @@ +{ + "type": "process", + "setting_id": "xWoxLO8XiKAOEvPO", + "name": "0.12mm High Quality @X-Plus 5", + "from": "system", + "instantiation": "true", + "inherits": "fdm_process_n_common", + "bottom_color_penetration_layers": "5", + "bottom_shell_layers": "5", + "bridge_flow": "1", + "default_acceleration": [ + "4000" + ], + "elefant_foot_compensation": "0.15", + "enable_arc_fitting": "0", + "gap_infill_speed": [ + "230" + ], + "initial_layer_infill_speed": [ + "105" + ], + "initial_layer_speed": [ + "50" + ], + "inner_wall_acceleration": [ + "0" + ], + "inner_wall_speed": [ + "150" + ], + "internal_solid_infill_speed": [ + "180" + ], + "layer_height": "0.12", + "outer_wall_acceleration": [ + "2000" + ], + "outer_wall_speed": [ + "60" + ], + "overhang_1_4_speed": [ + "60" + ], + "overhang_2_4_speed": [ + "30" + ], + "overhang_3_4_speed": [ + "10" + ], + "prime_tower_width": "60", + "prime_tower_brim_width": "-1", + "prime_tower_flat_ironing": "1", + "sparse_infill_pattern": "gyroid", + "sparse_infill_speed": [ + "180" + ], + "support_bottom_z_distance": "0.12", + "support_threshold_angle": "20", + "support_top_z_distance": "0.12", + "top_color_penetration_layers": "7", + "top_shell_layers": "5", + "top_shell_thickness": "0.6", + "top_surface_speed": [ + "150" + ], + "travel_speed": [ + "350" + ], + "wall_loops": "2", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/process/0.16mm High Quality @X-Plus 5.json b/resources/profiles/Qidi/process/0.16mm High Quality @X-Plus 5.json new file mode 100644 index 0000000000..54f20f7324 --- /dev/null +++ b/resources/profiles/Qidi/process/0.16mm High Quality @X-Plus 5.json @@ -0,0 +1,65 @@ +{ + "type": "process", + "setting_id": "7hYw7osV25Xu2yiu", + "name": "0.16mm High Quality @X-Plus 5", + "from": "system", + "instantiation": "true", + "inherits": "fdm_process_n_common", + "bottom_color_penetration_layers": "4", + "bottom_shell_layers": "4", + "bridge_flow": "1", + "default_acceleration": [ + "4000" + ], + "elefant_foot_compensation": "0.15", + "enable_arc_fitting": "0", + "gap_infill_speed": [ + "250" + ], + "inner_wall_acceleration": [ + "0" + ], + "internal_solid_infill_speed": [ + "200" + ], + "ironing_flow": "25%", + "ironing_speed": "20", + "layer_height": "0.16", + "outer_wall_acceleration": [ + "2000" + ], + "outer_wall_speed": [ + "60" + ], + "overhang_1_4_speed": [ + "60" + ], + "overhang_2_4_speed": [ + "30" + ], + "overhang_3_4_speed": [ + "10" + ], + "prime_tower_width": "60", + "prime_tower_brim_width": "-1", + "prime_tower_flat_ironing": "1", + "sparse_infill_pattern": "gyroid", + "sparse_infill_speed": [ + "200" + ], + "support_bottom_z_distance": "0.16", + "support_threshold_angle": "25", + "support_top_z_distance": "0.16", + "top_color_penetration_layers": "6", + "top_shell_layers": "6", + "top_shell_thickness": "1.0", + "top_surface_speed": [ + "150" + ], + "travel_speed": [ + "350" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/process/0.16mm Standard @X-Plus 5.json b/resources/profiles/Qidi/process/0.16mm Standard @X-Plus 5.json new file mode 100644 index 0000000000..5399e693aa --- /dev/null +++ b/resources/profiles/Qidi/process/0.16mm Standard @X-Plus 5.json @@ -0,0 +1,62 @@ +{ + "type": "process", + "setting_id": "xP7mco0WGscmPNoV", + "name": "0.16mm Standard @X-Plus 5", + "from": "system", + "instantiation": "true", + "inherits": "fdm_process_n_common", + "bottom_color_penetration_layers": "4", + "bottom_shell_layers": "4", + "bridge_flow": "1", + "default_acceleration": [ + "10000" + ], + "elefant_foot_compensation": "0.15", + "enable_arc_fitting": "0", + "gap_infill_speed": [ + "250" + ], + "inner_wall_acceleration": [ + "0" + ], + "inner_wall_speed": [ + "300" + ], + "internal_solid_infill_speed": [ + "250" + ], + "layer_height": "0.16", + "outer_wall_acceleration": [ + "5000" + ], + "outer_wall_speed": [ + "200" + ], + "overhang_1_4_speed": [ + "60" + ], + "overhang_2_4_speed": [ + "30" + ], + "overhang_3_4_speed": [ + "10" + ], + "prime_tower_width": "60", + "prime_tower_brim_width": "-1", + "prime_tower_flat_ironing": "1", + "sparse_infill_speed": [ + "350" + ], + "support_bottom_z_distance": "0.16", + "support_threshold_angle": "25", + "support_top_z_distance": "0.16", + "top_color_penetration_layers": "6", + "top_shell_layers": "6", + "top_shell_thickness": "1.0", + "travel_speed": [ + "500" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/process/0.18mm Balanced Quality @X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/process/0.18mm Balanced Quality @X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..85d94d49a7 --- /dev/null +++ b/resources/profiles/Qidi/process/0.18mm Balanced Quality @X-Plus 5 0.6 nozzle.json @@ -0,0 +1,73 @@ +{ + "type": "process", + "setting_id": "kj7YrKOrYowWXXrY", + "name": "0.18mm Balanced Quality @X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "fdm_process_n_common", + "bridge_flow": "1", + "bridge_speed": [ + "30" + ], + "default_acceleration": [ + "10000" + ], + "elefant_foot_compensation": "0.15", + "enable_arc_fitting": "0", + "gap_infill_speed": [ + "50" + ], + "initial_layer_line_width": "0.62", + "initial_layer_print_height": "0.3", + "initial_layer_speed": [ + "50" + ], + "inner_wall_acceleration": [ + "0" + ], + "inner_wall_line_width": "0.62", + "inner_wall_speed": [ + "300" + ], + "internal_solid_infill_line_width": "0.62", + "internal_solid_infill_speed": [ + "250" + ], + "layer_height": "0.18", + "line_width": "0.62", + "outer_wall_acceleration": [ + "5000" + ], + "outer_wall_line_width": "0.62", + "outer_wall_speed": [ + "200" + ], + "overhang_1_4_speed": [ + "0" + ], + "overhang_2_4_speed": [ + "50" + ], + "prime_tower_width": "60", + "prime_tower_brim_width": "-1", + "prime_tower_flat_ironing": "1", + "skin_infill_line_width": "0.62", + "skeleton_infill_line_width": "0.62", + "sparse_infill_line_width": "0.62", + "sparse_infill_speed": [ + "350" + ], + "support_bottom_z_distance": "0.18", + "support_line_width": "0.62", + "support_top_z_distance": "0.18", + "top_surface_line_width": "0.62", + "top_surface_speed": [ + "200" + ], + "travel_speed": [ + "500" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/process/0.20mm High Quality @X-Plus 5.json b/resources/profiles/Qidi/process/0.20mm High Quality @X-Plus 5.json new file mode 100644 index 0000000000..4f8c7b4ef4 --- /dev/null +++ b/resources/profiles/Qidi/process/0.20mm High Quality @X-Plus 5.json @@ -0,0 +1,63 @@ +{ + "type": "process", + "setting_id": "azERQAEOdeNEo2z7", + "name": "0.20mm High Quality @X-Plus 5", + "from": "system", + "inherits": "fdm_process_n_common", + "instantiation": "true", + "bottom_shell_layers": "3", + "bridge_flow": "1", + "default_acceleration": [ + "4000" + ], + "elefant_foot_compensation": "0.15", + "enable_arc_fitting": "0", + "gap_infill_speed": [ + "250" + ], + "inner_wall_acceleration": [ + "0" + ], + "inner_wall_speed": [ + "150" + ], + "internal_solid_infill_speed": [ + "200" + ], + "outer_wall_acceleration": [ + "2000" + ], + "outer_wall_speed": [ + "60" + ], + "overhang_1_4_speed": [ + "60" + ], + "overhang_2_4_speed": [ + "30" + ], + "overhang_3_4_speed": [ + "10" + ], + "prime_tower_width": "60", + "prime_tower_brim_width": "-1", + "prime_tower_flat_ironing": "1", + "sparse_infill_speed": [ + "200" + ], + "sparse_infill_density": "15%", + "sparse_infill_pattern": "gyroid", + "top_color_penetration_layers": "5", + "top_shell_layers": "5", + "top_shell_thickness": "1.0", + "top_surface_speed": [ + "150" + ], + "travel_speed": [ + "350" + ], + "wall_loops": "2", + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/process/0.20mm Standard @X-Plus 5.json b/resources/profiles/Qidi/process/0.20mm Standard @X-Plus 5.json new file mode 100644 index 0000000000..decaceecae --- /dev/null +++ b/resources/profiles/Qidi/process/0.20mm Standard @X-Plus 5.json @@ -0,0 +1,47 @@ +{ + "type": "process", + "setting_id": "ug7kxxCLKKE7MoJW", + "name": "0.20mm Standard @X-Plus 5", + "from": "system", + "inherits": "fdm_process_n_common", + "instantiation": "true", + "bridge_flow": "1", + "default_acceleration": [ + "10000" + ], + "elefant_foot_compensation": "0.15", + "enable_arc_fitting": "0", + "gap_infill_speed": [ + "250" + ], + "inner_wall_speed": [ + "300" + ], + "inner_wall_acceleration": [ + "0" + ], + "internal_solid_infill_speed": [ + "250" + ], + "ironing_flow": "15%", + "outer_wall_acceleration": [ + "5000" + ], + "outer_wall_speed": [ + "200" + ], + "prime_tower_width": "60", + "prime_tower_flat_ironing": "1", + "sparse_infill_speed": [ + "350" + ], + "top_color_penetration_layers": "5", + "top_shell_layers": "5", + "top_shell_thickness": "1.0", + "travel_speed": [ + "500" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/process/0.24mm Balanced Quality @X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/process/0.24mm Balanced Quality @X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..16e25dcef3 --- /dev/null +++ b/resources/profiles/Qidi/process/0.24mm Balanced Quality @X-Plus 5 0.6 nozzle.json @@ -0,0 +1,71 @@ +{ + "type": "process", + "setting_id": "BWwslzDWus0iHZM8", + "name": "0.24mm Balanced Quality @X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "fdm_process_n_common", + "bridge_flow": "1", + "bridge_speed": [ + "30" + ], + "default_acceleration": [ + "10000" + ], + "elefant_foot_compensation": "0.15", + "enable_arc_fitting": "0", + "gap_infill_speed": [ + "50" + ], + "initial_layer_infill_speed": [ + "105" + ], + "initial_layer_line_width": "0.62", + "initial_layer_print_height": "0.3", + "initial_layer_speed": [ + "50" + ], + "inner_wall_acceleration": [ + "0" + ], + "inner_wall_line_width": "0.62", + "inner_wall_speed": [ + "300" + ], + "internal_solid_infill_line_width": "0.62", + "internal_solid_infill_speed": [ + "250" + ], + "layer_height": "0.24", + "line_width": "0.62", + "outer_wall_acceleration": [ + "5000" + ], + "outer_wall_line_width": "0.62", + "outer_wall_speed": [ + "200" + ], + "overhang_3_4_speed": [ + "30" + ], + "prime_tower_width": "60", + "prime_tower_brim_width": "-1", + "prime_tower_flat_ironing": "1", + "skin_infill_line_width": "0.62", + "skeleton_infill_line_width": "0.62", + "sparse_infill_line_width": "0.62", + "sparse_infill_speed": [ + "350" + ], + "support_line_width": "0.62", + "top_surface_line_width": "0.62", + "top_surface_speed": [ + "200" + ], + "travel_speed": [ + "500" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/process/0.24mm Balanced Quality @X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/process/0.24mm Balanced Quality @X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..e7d69b3f09 --- /dev/null +++ b/resources/profiles/Qidi/process/0.24mm Balanced Quality @X-Plus 5 0.8 nozzle.json @@ -0,0 +1,63 @@ +{ + "type": "process", + "setting_id": "TdXf6dJK5dAiFtIW", + "name": "0.24mm Balanced Quality @X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "fdm_process_n_common", + "bridge_flow": "1", + "bridge_speed": [ + "30" + ], + "default_acceleration": [ + "10000" + ], + "elefant_foot_compensation": "0.15", + "enable_arc_fitting": "0", + "gap_infill_speed": [ + "50" + ], + "initial_layer_line_width": "0.82", + "initial_layer_print_height": "0.4", + "initial_layer_speed": [ + "50" + ], + "inner_wall_acceleration": [ + "0" + ], + "inner_wall_line_width": "0.82", + "inner_wall_speed": [ + "300" + ], + "internal_solid_infill_line_width": "0.82", + "internal_solid_infill_speed": [ + "250" + ], + "layer_height": "0.24", + "line_width": "0.82", + "outer_wall_acceleration": [ + "5000" + ], + "outer_wall_line_width": "0.82", + "outer_wall_speed": [ + "200" + ], + "prime_tower_width": "60", + "prime_tower_brim_width": "-1", + "prime_tower_flat_ironing": "1", + "skin_infill_line_width": "0.82", + "skeleton_infill_line_width": "0.82", + "sparse_infill_line_width": "0.82", + "sparse_infill_speed": [ + "350" + ], + "support_line_width": "0.82", + "top_surface_line_width": "0.82", + "top_surface_pattern": "monotonic", + "travel_speed": [ + "500" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/process/0.24mm Standard @X-Plus 5.json b/resources/profiles/Qidi/process/0.24mm Standard @X-Plus 5.json new file mode 100644 index 0000000000..8a435e69e7 --- /dev/null +++ b/resources/profiles/Qidi/process/0.24mm Standard @X-Plus 5.json @@ -0,0 +1,50 @@ +{ + "type": "process", + "setting_id": "fiMTwk6ObF3WNpi5", + "name": "0.24mm Standard @X-Plus 5", + "from": "system", + "inherits": "fdm_process_n_common", + "instantiation": "true", + "bridge_flow": "1", + "default_acceleration": [ + "10000" + ], + "elefant_foot_compensation": "0.15", + "enable_arc_fitting": "0", + "gap_infill_speed": [ + "250" + ], + "inner_wall_acceleration": [ + "0" + ], + "inner_wall_speed": [ + "300" + ], + "internal_solid_infill_speed": [ + "250" + ], + "layer_height": "0.24", + "outer_wall_acceleration": [ + "5000" + ], + "outer_wall_speed": [ + "200" + ], + "prime_tower_width": "60", + "prime_tower_brim_width": "-1", + "prime_tower_flat_ironing": "1", + "sparse_infill_speed": [ + "350" + ], + "support_threshold_angle": "35", + "top_color_penetration_layers": "4", + "top_shell_layers": "4", + "top_shell_thickness": "1.0", + "top_surface_line_width": "0.45", + "travel_speed": [ + "500" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.4 nozzle" + ] +} diff --git a/resources/profiles/Qidi/process/0.30mm Standard @X-Plus 5 0.6 nozzle.json b/resources/profiles/Qidi/process/0.30mm Standard @X-Plus 5 0.6 nozzle.json new file mode 100644 index 0000000000..a297f33bac --- /dev/null +++ b/resources/profiles/Qidi/process/0.30mm Standard @X-Plus 5 0.6 nozzle.json @@ -0,0 +1,65 @@ +{ + "type": "process", + "setting_id": "ZHUAMUGv0HI3h8fQ", + "name": "0.30mm Standard @X-Plus 5 0.6 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "fdm_process_n_common", + "bridge_flow": "1", + "bridge_speed": [ + "30" + ], + "default_acceleration": [ + "10000" + ], + "elefant_foot_compensation": "0.15", + "enable_arc_fitting": "0", + "gap_infill_speed": [ + "50" + ], + "initial_layer_line_width": "0.62", + "initial_layer_print_height": "0.3", + "initial_layer_speed": [ + "50" + ], + "inner_wall_acceleration": [ + "0" + ], + "inner_wall_line_width": "0.62", + "inner_wall_speed": [ + "300" + ], + "internal_solid_infill_line_width": "0.62", + "internal_solid_infill_speed": [ + "250" + ], + "layer_height": "0.3", + "line_width": "0.62", + "outer_wall_acceleration": [ + "5000" + ], + "outer_wall_line_width": "0.62", + "outer_wall_speed": [ + "120" + ], + "prime_tower_width": "60", + "prime_tower_flat_ironing": "1", + "skin_infill_line_width": "0.62", + "skeleton_infill_line_width": "0.62", + "sparse_infill_line_width": "0.62", + "sparse_infill_speed": [ + "350" + ], + "support_line_width": "0.62", + "top_shell_layers": "4", + "top_surface_line_width": "0.62", + "top_surface_speed": [ + "200" + ], + "travel_speed": [ + "500" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.6 nozzle" + ] +} diff --git a/resources/profiles/Qidi/process/0.32mm Balanced Quality @X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/process/0.32mm Balanced Quality @X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..be02d140d6 --- /dev/null +++ b/resources/profiles/Qidi/process/0.32mm Balanced Quality @X-Plus 5 0.8 nozzle.json @@ -0,0 +1,72 @@ +{ + "type": "process", + "setting_id": "Vl4hMKR69J1JcquX", + "name": "0.32mm Balanced Quality @X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "fdm_process_n_common", + "bridge_flow": "1", + "bridge_speed": [ + "30" + ], + "default_acceleration": [ + "10000" + ], + "elefant_foot_compensation": "0.15", + "enable_arc_fitting": "0", + "gap_infill_speed": [ + "50" + ], + "initial_layer_infill_speed": [ + "105" + ], + "initial_layer_line_width": "0.82", + "initial_layer_print_height": "0.4", + "initial_layer_speed": [ + "50" + ], + "inner_wall_acceleration": [ + "0" + ], + "inner_wall_line_width": "0.82", + "inner_wall_speed": [ + "300" + ], + "internal_solid_infill_line_width": "0.82", + "internal_solid_infill_speed": [ + "250" + ], + "layer_height": "0.32", + "line_width": "0.82", + "outer_wall_acceleration": [ + "5000" + ], + "outer_wall_line_width": "0.82", + "outer_wall_speed": [ + "200" + ], + "overhang_3_4_speed": [ + "30" + ], + "prime_tower_width": "60", + "prime_tower_brim_width": "-1", + "prime_tower_flat_ironing": "1", + "skin_infill_line_width": "0.82", + "skeleton_infill_line_width": "0.82", + "sparse_infill_line_width": "0.82", + "sparse_infill_speed": [ + "350" + ], + "support_line_width": "0.82", + "top_surface_line_width": "0.82", + "top_surface_pattern": "monotonic", + "top_surface_speed": [ + "200" + ], + "travel_speed": [ + "500" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/process/0.40mm Standard @X-Plus 5 0.8 nozzle.json b/resources/profiles/Qidi/process/0.40mm Standard @X-Plus 5 0.8 nozzle.json new file mode 100644 index 0000000000..cb3f30c20e --- /dev/null +++ b/resources/profiles/Qidi/process/0.40mm Standard @X-Plus 5 0.8 nozzle.json @@ -0,0 +1,60 @@ +{ + "type": "process", + "setting_id": "6EBzl1Pg5Px36Cu6", + "name": "0.40mm Standard @X-Plus 5 0.8 nozzle", + "from": "system", + "instantiation": "true", + "inherits": "fdm_process_n_common", + "bridge_flow": "1", + "bridge_speed": [ + "30" + ], + "default_acceleration": [ + "10000" + ], + "elefant_foot_compensation": "0.15", + "enable_arc_fitting": "0", + "gap_infill_speed": [ + "50" + ], + "initial_layer_line_width": "0.82", + "initial_layer_print_height": "0.4", + "inner_wall_acceleration": [ + "0" + ], + "inner_wall_line_width": "0.82", + "inner_wall_speed": [ + "300" + ], + "internal_solid_infill_line_width": "0.82", + "internal_solid_infill_speed": [ + "250" + ], + "layer_height": "0.4", + "line_width": "0.82", + "outer_wall_acceleration": [ + "5000" + ], + "outer_wall_line_width": "0.82", + "prime_tower_width": "60", + "prime_tower_flat_ironing": "1", + "skin_infill_line_width": "0.82", + "skeleton_infill_line_width": "0.82", + "sparse_infill_line_width": "0.82", + "sparse_infill_speed": [ + "350" + ], + "support_line_width": "0.82", + "top_surface_line_width": "0.82", + "top_surface_pattern": "monotonic", + "top_surface_speed": [ + "200" + ], + "top_shell_layers": "4", + "travel_speed": [ + "500" + ], + "compatible_printers": [ + "Qidi X-Plus 5 0.8 nozzle" + ] +} diff --git a/resources/profiles/Qidi/qidi_xplus5_buildplate_model.stl b/resources/profiles/Qidi/qidi_xplus5_buildplate_model.stl new file mode 100644 index 0000000000..5e2d7ca485 Binary files /dev/null and b/resources/profiles/Qidi/qidi_xplus5_buildplate_model.stl differ diff --git a/resources/profiles/Qidi/qidi_xplus5_buildplate_texture.svg b/resources/profiles/Qidi/qidi_xplus5_buildplate_texture.svg new file mode 100644 index 0000000000..72a081620f --- /dev/null +++ b/resources/profiles/Qidi/qidi_xplus5_buildplate_texture.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="921.2999877929688" height="983.5999755859375" viewBox="0 0 921.2999877929688 983.5999755859375"><defs><clipPath id="master_svg0_5_0"><rect x="0" y="0" width="921.2999877929688" height="983.5999755859375" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_5_0)"><g><g><path d="M470.79998780703124,968.7000122L466.99999999703124,962.5L470.50000000703125,962.5L471.79998780703124,964.9000244C472.09997560703124,965.5,472.3999939070313,966.1000366,472.8999939070313,966.9000244L473.00000000703125,966.9000244C473.29998780703124,966.1000366,473.6000061070313,965.4000244,473.8999939070313,964.9000244L475.1000061070313,962.5L478.3999942070312,962.5L474.6000061070313,968.7999878L478.60000620703124,975.200012L475.1000061070313,975.200012L473.6000061070313,972.600037C473.20001220703125,971.9000244,472.8999939070313,971.3000488,472.50000000703125,970.500061L472.3999939070313,970.500061C472.1000061070313,971.3000488,471.69998170703127,971.9000854,471.3999939070313,972.600037L470.00000000703125,975.200012L466.70001220703125,975.200012L470.79998780703124,968.7000122Z" fill="#B4B4B4" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g><g><g><path d="M488.29998779296875,975.2999882070312L488.29998779296875,962.7000122070312L492.1999816929688,962.7000122070312C493.09997559296875,962.7000122070312,493.8999938929687,962.7999877930313,494.4999694929688,963.1000366170313C495.09994509296877,963.4000854470313,495.6999816929688,963.8000488070312,496.1999816929688,964.3000488070312C496.59997559296875,964.9000244070312,496.79998779296875,965.7000732070312,496.79998779296875,966.7000732070312C496.79998779296875,967.7000732070312,496.59997559296875,968.4000854070313,496.1999816929688,969.1000976070312C495.79998779296875,969.7000732070312,495.1999816929688,970.2000732070312,494.59997559296875,970.5001221070313C493.89996339296874,970.8001099070312,493.09997559296875,970.9001465070312,492.29998779296875,970.9001465070312L490.6999816929688,970.9001465070312L490.6999816929688,975.4001462070313L488.29998779296875,975.4001462070313L488.29998779296875,975.2999882070312ZM490.70001219296876,968.9000244070312L492.1000060929687,968.9000244070312C492.8999938929687,968.9000244070312,493.49999999296875,968.7000122070312,493.8999938929687,968.4000244070312C494.29998779296875,968.0000000070313,494.49999999296875,967.5000000070313,494.49999999296875,966.8000488070312C494.49999999296875,966.1000976070312,494.29998779296875,965.6000366070313,493.8999938929687,965.3000488070312C493.49999999296875,965.0000610070313,492.8999938929687,964.9000244070312,491.99999999296875,964.9000244070312L490.70001219296876,964.9000244070312L490.70001219296876,968.9000244070312Z" fill="#B4B4B4" fill-opacity="1" style="mix-blend-mode:passthrough"/></g><g><path d="M500.99999999296875,975.5999757578124C500.3999938929687,975.5999757578124,499.99999999296875,975.4999997578125,499.70001220296876,975.1999507578125C499.4000244129688,974.9999387578125,499.20001220296876,974.5999757578124,499.00000000296876,974.1999507578125C498.8999938929687,973.7999267578125,498.79998779296875,973.2999267578125,498.79998779296875,972.6999507578125L498.79998779296875,961.7999267578125L501.09997559296875,961.7999267578125L501.09997559296875,972.7999267578125C501.09997559296875,973.0999147578125,501.09997559296875,973.2999267578125,501.29998779296875,973.3999027578125C501.3999938929687,973.4998777578126,501.49999999296875,973.5999147578125,501.59997559296875,973.5999147578125C501.6999816929688,973.5999147578125,501.6999816929688,973.5999147578125,501.6999816929688,973.5999147578125C501.6999816929688,973.5999147578125,501.79998779296875,973.5999147578125,501.8999938929687,973.5999147578125L502.1999816929688,975.4999387578125C502.09997559296875,975.5999147578125,501.8999938929687,975.5999147578125,501.6999816929688,975.6999507578125C501.49999999296875,975.4999997578125,501.29998779296875,975.5999757578124,500.99999999296875,975.5999757578124Z" fill="#B4B4B4" fill-opacity="1" style="mix-blend-mode:passthrough"/></g><g><path d="M506.3999938859375,975.5999755578125C505.3999938859375,975.5999755578125,504.6999816859375,975.2999877578125,504.2999877959375,974.5999755578125C503.8999938959375,973.8999633578125,503.5999755859375,972.9999999578125,503.5999755859375,971.7999877578125L503.5999755859375,965.8999633778125L505.8999633859375,965.8999633778125L505.8999633859375,971.4999389578124C505.8999633859375,972.1999511578125,505.9999694859375,972.7999267578125,506.1999511859375,972.9999389578124C506.3999633859375,973.2999267578125,506.6999511859375,973.3999633578125,507.1999511859375,973.3999633578125C507.5999450859375,973.3999633578125,507.8999633859375,973.2999877578125,508.0999450859375,973.0999755578125C508.3999328859375,972.8999633578125,508.6999511859375,972.5999755578125,508.9999389859375,972.1999511578125L508.9999389859375,965.7999267578125L511.2999267859375,965.7999267578125L511.2999267859375,975.2999267578125L509.3999328859375,975.2999267578125L509.1999206859375,973.8999023578125L509.0999145859375,973.8999023578125C508.6999206859375,974.3999023578125,508.2999267859375,974.7999267578125,507.8999023859375,975.0999145578126C507.6000060859375,975.4000244578125,507.1000060859375,975.5999755578125,506.3999938859375,975.5999755578125Z" fill="#B4B4B4" fill-opacity="1" style="mix-blend-mode:passthrough"/></g><g><path d="M516.5000000210938,975.5999755507812C515.9000244210938,975.5999755507812,515.2999878210937,975.4999999507812,514.7000122210937,975.1999511507812C514.1000366210938,974.9999389507813,513.6000366210938,974.5999755507812,513.1000366210938,974.1999511507812L514.2000122210937,972.5999755507812C514.6000366210938,972.8999633507813,515.0000000210938,973.1999511507812,515.4000244210938,973.3999633507813C515.8000488210937,973.5999755507812,516.2000122210937,973.6999511507812,516.6000366210938,973.6999511507812C517.0000610210938,973.6999511507812,517.4000244210938,973.5999755507812,517.6000366210938,973.3999633507813C517.8000488210937,973.1999511507812,517.9000244210938,972.9999389507813,517.9000244210938,972.6999511507812C517.9000244210938,972.4999389507813,517.8000488210937,972.2999267507812,517.6000366210938,972.0999755507812C517.4000244210938,971.9000244507813,517.2000122210937,971.7999877507813,516.9000244210938,971.6999511507812C516.6000366210938,971.5999755507812,516.3000488210937,971.3999633507813,516.0000000210938,971.2999267507812C515.5999756210938,971.0999145507812,515.2999878210937,970.8999023507813,514.9000244210938,970.6999511507812C514.5000000210938,970.4999389507813,514.2000122210937,970.1999511507812,514.0000000010938,969.7999267507812C513.7999877910937,969.3999023507813,513.5999755810938,968.9999389507813,513.5999755810938,968.4999389507813C513.5999755810938,967.8999633507813,513.6999511710937,967.3999633507813,514.0000000010938,966.9999389507813C514.2999878210937,966.5999145507812,514.7000122210937,966.1999511707812,515.2000122210937,965.9999389607813C515.7000122210937,965.7999267607812,516.2999878210937,965.5999145507812,517.0000000210938,965.5999145507812C517.5999756210938,965.5999145507812,518.2000122210937,965.6998901367813,518.7000122210937,965.9999389607813C519.2000122210937,966.2999877907813,519.6000366210938,966.4999389607813,520.0000000210938,966.7999267507812L518.9000244210938,968.2999267507812C518.6000366210938,968.0999145507812,518.3000488210937,967.8999023507813,518.0000000210938,967.6999511507812C517.7000122210937,967.5999755507812,517.4000244210938,967.4999389507813,517.0000000210938,967.4999389507813C516.5999756210938,967.4999389507813,516.2999878210937,967.5999145507812,516.0999756210938,967.7999267507812C515.8999634210937,967.9999389507813,515.7999878210937,968.1999511507812,515.7999878210937,968.4999389507813C515.7999878210937,968.6999511507812,515.8999634210937,968.8999633507813,516.0000000210938,969.0999145507812C516.2000122210937,969.2999267507812,516.4000244210938,969.3999023507813,516.7000122210937,969.4999389507813C517.0000000210938,969.5999755507812,517.2999878210937,969.6999511507812,517.6000366210938,969.8999633507813C517.9000244210938,969.9999389507813,518.2000122210937,970.0999755507812,518.5000610210938,970.2999877507813C518.8001098210938,970.4999999507812,519.1000366210938,970.5999755507812,519.3000488210937,970.8999633507813C519.5000610210938,971.0999755507812,519.7000732210937,971.3999633507813,519.9000244210938,971.6999511507812C520.0000000210938,971.9999389507813,520.1000366210938,972.3999633507813,520.1000366210938,972.7999267507812C520.1000366210938,973.3999023507813,520.0000610210938,973.8999023507813,519.7000122210937,974.2999267507812C519.4000244210938,974.7999267507812,519.0000000210938,975.0999145507812,518.5000000210938,975.3999023507813C518.0000000210938,975.6998905507812,517.2000122210937,975.5999755507812,516.5000000210938,975.5999755507812Z" fill="#B4B4B4" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g><g><g><path d="M526.0999755859375,974.099975171875L527.3999633859376,972.199951171875C528.1999511859375,972.899963171875,529.0999755859375,973.499939171875,530.1999511859375,973.499939171875C531.5999755859375,973.499939171875,532.4999389859375,972.799927171875,532.4999389859375,971.499938971875C532.4999389859375,970.199951171875,531.5999145859375,969.499938971875,530.2999267859375,969.499938971875C529.4999389859375,969.499938971875,529.0999145859375,969.699951171875,528.2999267859375,970.199951171875L527.0999145459375,969.299926771875L527.3999023859375,962.699951171875L534.7999267859375,962.699951171875L534.7999267859375,965.199951171875L529.7999267859375,965.199951171875L529.5999145859375,967.599975571875C530.1998901859375,967.399963371875,530.5999145859375,967.299987771875,531.1998901859375,967.299987771875C533.3999023859375,967.299987771875,535.2998656859374,968.599975571875,535.2998656859374,971.299987771875C535.2998656859374,974.200012171875,533.0998534859375,975.799988171875,530.5998534859375,975.799988171875C528.4999999859375,975.900024171875,527.0999755859375,975.099975171875,526.0999755859375,974.099975171875Z" fill="#B4B4B4" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g><g><g><path d="M478.8999938964844,969.7999877789063L478.8999938964844,967.3999633789062L485.6000060964844,967.3999633789062L485.6000060964844,969.7999877789063L478.8999938964844,969.7999877789063Z" fill="#B4B4B4" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g><g><g><g><g><path d="M433.3999939035156,967.2999877929688L431.6000061035156,967.2999877929688C431.30001831351564,967.2999877929688,431.1000061035156,967.5000000029687,431.1000061035156,967.7999877929688L431.1000061035156,974.5999755929688C431.1000061035156,974.8999633929687,431.30001831351564,975.0999755929688,431.6000061035156,975.0999755929688L433.3999939035156,975.0999755929688C433.6999817035156,975.0999755929688,433.8999939035156,974.8999633929687,433.8999939035156,974.5999755929688L433.8999939035156,967.7999877929688C433.8999939035156,967.5000000029687,433.7000122035156,967.2999877929688,433.3999939035156,967.2999877929688Z" fill="#B4B4B4" fill-opacity="1" style="mix-blend-mode:passthrough"/></g><g><path d="M432.7000122035156,965.5999755929688L432.3000183035156,965.5999755929688C431.7000122035156,965.5999755929688,431.1000061035156,965.0999755929688,431.1000061035156,964.3999633929687L431.1000061035156,963.9999999929687C431.1000061035156,963.4000244129687,431.6000061035156,962.7999877929688,432.3000183035156,962.7999877929688L432.7000122035156,962.7999877929688C433.3000183035156,962.7999877929688,433.9000244035156,963.2999877929688,433.9000244035156,963.9999999929687L433.9000244035156,964.4000243929687C433.8999939035156,965.0999755929688,433.3999939035156,965.5999755929688,432.7000122035156,965.5999755929688Z" fill="#B4B4B4" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g><g><g><path d="M454.00000000703125,967.2999877929688L452.20001220703125,967.2999877929688C451.90002441703126,967.2999877929688,451.70001220703125,967.5000000029687,451.70001220703125,967.7999877929688L451.70001220703125,974.5999755929688C451.70001220703125,974.8999633929687,451.90002441703126,975.0999755929688,452.20001220703125,975.0999755929688L454.00000000703125,975.0999755929688C454.29998780703124,975.0999755929688,454.50000000703125,974.8999633929687,454.50000000703125,974.5999755929688L454.50000000703125,967.7999877929688C454.50000000703125,967.5000000029687,454.20001220703125,967.2999877929688,454.00000000703125,967.2999877929688Z" fill="#B4B4B4" fill-opacity="1" style="mix-blend-mode:passthrough"/></g><g><path d="M453.2999877894531,965.5999755929688L452.8999938894531,965.5999755929688C452.2999877894531,965.5999755929688,451.6999816894531,965.0999755929688,451.6999816894531,964.3999633929687L451.6999816894531,963.9999999929687C451.6999816894531,963.4000244129687,452.1999816894531,962.7999877929688,452.8999938894531,962.7999877929688L453.2999877894531,962.7999877929688C453.8999938894531,962.7999877929688,454.4999999894531,963.2999877929688,454.4999999894531,963.9999999929687L454.4999999894531,964.4000243929687C454.4999999894531,965.0999755929688,453.8999938894531,965.5999755929688,453.2999877894531,965.5999755929688Z" fill="#B4B4B4" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g><g><path d="M444.29998778242185,962.9000244140625L443.59997558242185,962.9000244140625L442.4999694824219,962.9000244140625L440.2999572824219,962.9000244140625L439.2999572824219,962.9000244140625L437.09994507242186,962.9000244140625C436.79995727242186,962.9000244140625,436.5999450684219,963.1000366240625,436.5999450684219,963.4000244140625L436.5999450684219,965.2000122140626C436.5999450684219,965.5000000140625,436.79995727242186,965.7000122140626,437.09994507242186,965.7000122140626L439.2999572824219,965.7000122140626L442.3999633824219,965.7000122140626L442.6999511824219,965.7000122140626L443.4999389824219,965.7000122140626L444.09994508242187,965.7000122140626C445.1999511824219,965.7000122140626,445.9999389824219,966.6000366140624,445.9999389824219,967.6000366140624L445.9999389824219,969.0000610140625L445.9999389824219,970.4000854140625C445.9999389824219,971.5000610140625,445.09994508242187,972.3001098140625,444.09994508242187,972.3001098140625L442.6999511824219,972.3001098140625L441.3999633824219,972.3001098140625L441.2999572824219,972.3001098140625L439.7999572824219,972.3001098140625C439.4999694824219,972.3001098140625,439.2999572824219,972.1000976140625,439.2999572824219,971.8001098140625L439.2999572824219,970.3001098140625L439.2999572824219,970.0000000140625L439.2999572824219,969.9000244140625L439.2999572824219,969.5000000140625C439.2999572824219,969.2000122140626,439.09994508242187,969.0000000140625,438.7999572824219,969.0000000140625L436.9999694824219,969.0000000140625C436.6999816924219,969.0000000140625,436.4999694824219,969.2000122140626,436.4999694824219,969.5000000140625L436.4999694824219,969.9000244140625L436.4999694824219,974.7000124140625C436.4999694824219,975.0000004140625,436.6999816924219,975.2000124140625,436.9999694824219,975.2000124140625L437.1999816924219,975.2000124140625L439.29998778242185,975.2000124140625L440.79998778242185,975.2000124140625C440.8999938824219,975.2000124140625,440.99999998242185,975.2000124140625,441.09997558242185,975.2000124140625L444.1999816824219,975.2000124140625C446.6999814824219,975.2000124140625,448.7999874824219,973.1000364140625,448.7999874824219,970.6000366140624L448.7999874824219,969.0000000140625L448.7999874824219,967.5000000140625C448.89999348242185,964.9000244140625,446.89999348242185,962.9000244140625,444.29998778242185,962.9000244140625Z" fill="#B4B4B4" fill-opacity="1" style="mix-blend-mode:passthrough"/></g><g><g><path d="M423.7999877859375,962.9000244140625L420.6999816859375,962.9000244140625C418.1999816859375,962.9000244140625,416.0999755859375,965.0000000140625,416.0999755859375,967.5000000140625L416.0999755859375,968.0999756140625L416.0999755859375,969.0999756140625L416.0999755859375,970.5000000140625L416.0999755859375,970.5999756140625C416.0999755859375,973.0999754140626,418.1999816859375,975.1999514140625,420.6999816859375,975.1999514140625L421.6999816859375,975.1999514140625C421.9999694859375,975.1999514140625,422.1999816859375,974.9999394140625,422.1999816859375,974.6999514140625L422.1999816859375,972.8999634140625C422.1999816859375,972.5999756140625,421.9999694859375,972.3999634140625,421.6999816859375,972.3999634140625L420.7999877859375,972.3999634140625C419.6999816859375,972.3999634140625,418.8999938859375,971.4999390140625,418.8999938859375,970.4999390140625L418.8999938859375,969.0000000140625L418.8999938859375,968.0000000140625L418.8999938859375,967.5999756140625C418.8999938859375,966.5000000140625,419.7999877859375,965.6999512140625,420.7999877859375,965.6999512140625L423.5999755859375,965.6999512140625C424.6999816859375,965.6999512140625,425.4999694859375,966.5999756140625,425.4999694859375,967.5999756140625L425.4999694859375,968.5000000140625C425.4999694859375,968.7999878140625,425.6999816859375,969.0000000140625,425.9999694859375,969.0000000140625L427.7999575859375,969.0000000140625C428.0999445859375,969.0000000140625,428.2999575859375,968.7999878140625,428.2999575859375,968.5000000140625L428.2999575859375,967.5000000140625C428.3999935859375,964.9000244140625,426.2999875859375,962.9000244140625,423.7999877859375,962.9000244140625Z" fill="#B4B4B4" fill-opacity="1" style="mix-blend-mode:passthrough"/></g><g><g><path d="M428.20001222734373,973L424.30001832734376,969.099975586C424.20001222734373,969,424.10000612734376,969,424.00003052734377,969L421.50003051734376,969C421.10003662134375,969,420.90002441734373,969.5,421.20004272734377,969.79998779L426.30004882734374,974.8999634C426.50006102734375,975.0999756,426.80004882734374,975.0999756,427.00006102734375,974.8999634L428.30004882734374,973.5999756C428.39999392734376,973.5,428.39999392734376,973.2000122,428.20001222734373,973Z" fill="#B4B4B4" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></g></g><g><g><g><path d="M399.99999979296877,976.4000244492188L399.99999979296877,974.4000244492188L399.99999979296877,971.2000124492188C399.99999979296877,970.9000244492188,400.2000117929687,970.5000004492188,400.49999979296877,970.4000244492188L404.7000117929687,967.9000244492188L406.6000057929688,966.8000484492187L406.6000057929688,963.7000732492188C406.6000057929688,963.0000610492187,406.2000117929687,962.4000854492188,405.6000057929688,962.0000610492187L396.89999379296876,957.0000610392187C396.29998779296875,956.7000732422188,395.6000057929688,956.7000732422188,394.99999999296875,957.0000610392187L386.29998779296875,962.0000610492187C385.6999816929688,962.3000488492188,385.29998779296875,963.0000610492187,385.29998779296875,963.7000732492188L385.29998779296875,973.7000734492187C385.29998779296875,974.4000854492188,385.6999816929688,975.0000614492187,386.29998779296875,975.4000854492188L394.99999999296875,980.4000854492188C395.6000057929688,980.7000734492187,396.29998779296875,980.7000734492187,396.89999379296876,980.4000854492188L399.89999379296876,978.7000734492187L399.89999379296876,976.4000244492188L399.99999979296877,976.4000244492188ZM396.99999979296877,967.0000004492188C396.39999379296876,967.2999874492187,395.99999979296877,968.0000004492188,395.99999979296877,968.7000124492188L395.99999979296877,977.0000004492188C395.99999979296877,977.7000124492188,395.20001219296876,978.2000124492188,394.49999999296875,977.7999874492187L387.79998779296875,973.8999634492187C387.49999999296875,973.6999514492187,387.29998779296875,973.3999634492187,387.29998779296875,973.0999754492187L387.29998779296875,964.1999511492188C387.29998779296875,963.8999633492188,387.49999999296875,963.4999389492187,387.79998779296875,963.3999633492188L395.49999979296877,958.9999389492187C395.79998779296875,958.7999267492188,396.2000117929687,958.7999267492188,396.49999979296877,958.9999389492187L402.29998779296875,962.2999267492188C402.89999379296876,962.6999511492188,402.89999379296876,963.5999145492187,402.29998779296875,963.9999389492187L396.99999979296877,967.0000004492188Z" fill="#B4B4B4" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g><g><path d="M408.00000000351565,965.999999999625L406.7000122035156,966.799987790625L406.7000122035156,969.499999990625L406.7000122035156,969.799987790625L406.7000122035156,973.099975590625L406.7000122035156,974.199951190625C406.7000122035156,974.499938990625,406.50000000351565,974.899963390625,406.2000122035156,974.999938990625L406.1000061035156,975.099914590625L406.00000000351565,975.199890090625L404.79998780351565,975.899902390625L404.00000000351565,976.399901890625L400.1000061035156,978.699889890625L400.1000061035156,979.699889890625L400.1000061035156,980.199889890625L400.1000061035156,980.299865890625C400.1000061035156,980.999877890625,400.8999938935156,981.499877890625,401.50000000351565,981.199889890625L402.29998780351565,980.699889890625L409.00000000351565,976.799865890625C409.29998780351565,976.599853890625,409.50000000351565,976.299865890625,409.50000000351565,975.999877890625L409.50000000351565,967.099853490625C409.3999939035156,966.099975590625,408.6000061035156,965.700012210625,408.00000000351565,965.999999999625Z" fill="#B4B4B4" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></g></g></svg> \ No newline at end of file diff --git a/resources/profiles/Snapmaker.json b/resources/profiles/Snapmaker.json index cdc7f0fe81..65b1d12a3a 100644 --- a/resources/profiles/Snapmaker.json +++ b/resources/profiles/Snapmaker.json @@ -1,6 +1,6 @@ { "name": "Snapmaker", - "version": "02.04.00.07", + "version": "02.04.00.10", "force_update": "0", "description": "Snapmaker configurations", "machine_model_list": [ @@ -426,10 +426,6 @@ "name": "0.16 Optimal @Snapmaker U1 (0.4 nozzle)", "sub_path": "process/0.16 Optimal @Snapmaker U1 (0.4 nozzle).json" }, - { - "name": "0.20 Bambu Support W @Snapmaker U1 (0.4 nozzle)", - "sub_path": "process/0.20 Bambu Support W @Snapmaker U1 (0.4 nozzle).json" - }, { "name": "0.20 Quality @Snapmaker U1 (0.4 nozzle)", "sub_path": "process/0.20 Quality @Snapmaker U1 (0.4 nozzle).json" @@ -486,6 +482,66 @@ "name": "fdm_process_U1_0.8_common", "sub_path": "process/fdm_process_U1_0.8_common.json" }, + { + "name": "fdm_process_U1_0.06_nozzle_0.2", + "sub_path": "process/fdm_process_U1_0.06_nozzle_0.2.json" + }, + { + "name": "fdm_process_U1_0.08_nozzle_0.2", + "sub_path": "process/fdm_process_U1_0.08_nozzle_0.2.json" + }, + { + "name": "fdm_process_U1_0.10_nozzle_0.2", + "sub_path": "process/fdm_process_U1_0.10_nozzle_0.2.json" + }, + { + "name": "fdm_process_U1_0.12_nozzle_0.2", + "sub_path": "process/fdm_process_U1_0.12_nozzle_0.2.json" + }, + { + "name": "fdm_process_U1_0.14_nozzle_0.2", + "sub_path": "process/fdm_process_U1_0.14_nozzle_0.2.json" + }, + { + "name": "fdm_process_U1_0.18_nozzle_0.6", + "sub_path": "process/fdm_process_U1_0.18_nozzle_0.6.json" + }, + { + "name": "fdm_process_U1_0.24_nozzle_0.6", + "sub_path": "process/fdm_process_U1_0.24_nozzle_0.6.json" + }, + { + "name": "fdm_process_U1_0.24_nozzle_0.8", + "sub_path": "process/fdm_process_U1_0.24_nozzle_0.8.json" + }, + { + "name": "fdm_process_U1_0.30_nozzle_0.6", + "sub_path": "process/fdm_process_U1_0.30_nozzle_0.6.json" + }, + { + "name": "fdm_process_U1_0.32_nozzle_0.8", + "sub_path": "process/fdm_process_U1_0.32_nozzle_0.8.json" + }, + { + "name": "fdm_process_U1_0.36_nozzle_0.6", + "sub_path": "process/fdm_process_U1_0.36_nozzle_0.6.json" + }, + { + "name": "fdm_process_U1_0.40_nozzle_0.8", + "sub_path": "process/fdm_process_U1_0.40_nozzle_0.8.json" + }, + { + "name": "fdm_process_U1_0.42_nozzle_0.6", + "sub_path": "process/fdm_process_U1_0.42_nozzle_0.6.json" + }, + { + "name": "fdm_process_U1_0.48_nozzle_0.8", + "sub_path": "process/fdm_process_U1_0.48_nozzle_0.8.json" + }, + { + "name": "fdm_process_U1_0.56_nozzle_0.8", + "sub_path": "process/fdm_process_U1_0.56_nozzle_0.8.json" + }, { "name": "0.06 High Quality @Snapmaker U1 (0.2 nozzle)", "sub_path": "process/0.06 High Quality @Snapmaker U1 (0.2 nozzle).json" @@ -1508,10 +1564,6 @@ "name": "Snapmaker PLA Metal @U1", "sub_path": "filament/Snapmaker PLA Metal @U1.json" }, - { - "name": "Snapmaker PLA Silk @U1", - "sub_path": "filament/Snapmaker PLA Silk @U1.json" - }, { "name": "Snapmaker PLA Silk", "sub_path": "filament/Snapmaker PLA Silk.json" @@ -1671,6 +1723,214 @@ { "name": "Snapmaker PLA Matte @U1 base", "sub_path": "filament/Snapmaker PLA Matte @U1 base.json" + }, + { + "name": "Polymaker PLA @U1 base", + "sub_path": "filament/Polymaker PLA @U1 base.json" + }, + { + "name": "Polymaker Silk PLA Family @U1", + "sub_path": "filament/Polymaker Silk PLA Family @U1.json" + }, + { + "name": "Polymaker Tough PLA Family @U1", + "sub_path": "filament/Polymaker Tough PLA Family @U1.json" + }, + { + "name": "Snapmaker Breakaway Support For PLA @U1 0.2 nozzle", + "sub_path": "filament/Snapmaker Breakaway Support For PLA @U1 0.2 nozzle.json" + }, + { + "name": "Snapmaker Breakaway Support For PLA @U1 0.6 nozzle", + "sub_path": "filament/Snapmaker Breakaway Support For PLA @U1 0.6 nozzle.json" + }, + { + "name": "Snapmaker Breakaway Support For PLA @U1 0.8 nozzle", + "sub_path": "filament/Snapmaker Breakaway Support For PLA @U1 0.8 nozzle.json" + }, + { + "name": "Snapmaker PETG HF @U1 base2", + "sub_path": "filament/Snapmaker PETG HF @U1 base2.json" + }, + { + "name": "Snapmaker PETG Translucent @U1 base", + "sub_path": "filament/Snapmaker PETG Translucent @U1 base.json" + }, + { + "name": "Snapmaker PLA Basic @U1 base", + "sub_path": "filament/Snapmaker PLA Basic @U1 base.json" + }, + { + "name": "Snapmaker PLA Full Spectrum @U1 0.4 nozzle", + "sub_path": "filament/Snapmaker PLA Full Spectrum @U1 0.4 nozzle.json" + }, + { + "name": "Snapmaker PLA Glow @U1 base", + "sub_path": "filament/Snapmaker PLA Glow @U1 base.json" + }, + { + "name": "Snapmaker PLA Matte @U1 base2", + "sub_path": "filament/Snapmaker PLA Matte @U1 base2.json" + }, + { + "name": "Snapmaker PLA Silk @U1 0.2 nozzle", + "sub_path": "filament/Snapmaker PLA Silk @U1 0.2 nozzle.json" + }, + { + "name": "Snapmaker PLA Silk @U1 0.6 nozzle", + "sub_path": "filament/Snapmaker PLA Silk @U1 0.6 nozzle.json" + }, + { + "name": "Snapmaker PLA Silk @U1 0.8 nozzle", + "sub_path": "filament/Snapmaker PLA Silk @U1 0.8 nozzle.json" + }, + { + "name": "Snapmaker PLA SnapSpeed @U1 base2", + "sub_path": "filament/Snapmaker PLA SnapSpeed @U1 base2.json" + }, + { + "name": "Snapmaker PLA Translucent @U1 base", + "sub_path": "filament/Snapmaker PLA Translucent @U1 base.json" + }, + { + "name": "Snapmaker PLA Wood @U1 0.4 nozzle", + "sub_path": "filament/Snapmaker PLA Wood @U1 0.4 nozzle.json" + }, + { + "name": "Snapmaker PLA Wood @U1 0.6 nozzle", + "sub_path": "filament/Snapmaker PLA Wood @U1 0.6 nozzle.json" + }, + { + "name": "Snapmaker PLA Wood @U1 0.8 nozzle", + "sub_path": "filament/Snapmaker PLA Wood @U1 0.8 nozzle.json" + }, + { + "name": "Snapmaker PLA-CF @U1 0.4 nozzle", + "sub_path": "filament/Snapmaker PLA-CF @U1 0.4 nozzle.json" + }, + { + "name": "Snapmaker PLA-CF @U1 0.6 nozzle", + "sub_path": "filament/Snapmaker PLA-CF @U1 0.6 nozzle.json" + }, + { + "name": "Snapmaker PLA-CF @U1 0.8 nozzle", + "sub_path": "filament/Snapmaker PLA-CF @U1 0.8 nozzle.json" + }, + { + "name": "Snapmaker PVA @U1 0.6 nozzle", + "sub_path": "filament/Snapmaker PVA @U1 0.6 nozzle.json" + }, + { + "name": "Snapmaker PVA @U1 0.8 nozzle", + "sub_path": "filament/Snapmaker PVA @U1 0.8 nozzle.json" + }, + { + "name": "Snapmaker TPU 90A @U1", + "sub_path": "filament/Snapmaker TPU 90A @U1.json" + }, + { + "name": "Snapmaker TPU 90A @U1 0.6 nozzle", + "sub_path": "filament/Snapmaker TPU 90A @U1 0.6 nozzle.json" + }, + { + "name": "Snapmaker TPU 90A @U1 0.8 nozzle", + "sub_path": "filament/Snapmaker TPU 90A @U1 0.8 nozzle.json" + }, + { + "name": "Snapmaker TPU 95A HF @U1", + "sub_path": "filament/Snapmaker TPU 95A HF @U1.json" + }, + { + "name": "Snapmaker TPU 95A HF @U1 0.6 nozzle", + "sub_path": "filament/Snapmaker TPU 95A HF @U1 0.6 nozzle.json" + }, + { + "name": "Snapmaker TPU 95A HF @U1 0.8 nozzle", + "sub_path": "filament/Snapmaker TPU 95A HF @U1 0.8 nozzle.json" + }, + { + "name": "Snapmaker PLA Silk @U1", + "sub_path": "filament/Snapmaker PLA Silk @U1.json" + }, + { + "name": "Polymaker General PLA Family @U1", + "sub_path": "filament/Polymaker General PLA Family @U1.json" + }, + { + "name": "Snapmaker PETG HF @U1 0.2 nozzle", + "sub_path": "filament/Snapmaker PETG HF @U1 0.2 nozzle.json" + }, + { + "name": "Snapmaker PETG HF @U1 0.6 nozzle", + "sub_path": "filament/Snapmaker PETG HF @U1 0.6 nozzle.json" + }, + { + "name": "Snapmaker PETG HF @U1 0.8 nozzle", + "sub_path": "filament/Snapmaker PETG HF @U1 0.8 nozzle.json" + }, + { + "name": "Snapmaker PETG Translucent @U1 0.2 nozzle", + "sub_path": "filament/Snapmaker PETG Translucent @U1 0.2 nozzle.json" + }, + { + "name": "Snapmaker PETG Translucent @U1 0.4 nozzle", + "sub_path": "filament/Snapmaker PETG Translucent @U1 0.4 nozzle.json" + }, + { + "name": "Snapmaker PETG Translucent @U1 0.6 nozzle", + "sub_path": "filament/Snapmaker PETG Translucent @U1 0.6 nozzle.json" + }, + { + "name": "Snapmaker PETG Translucent @U1 0.8 nozzle", + "sub_path": "filament/Snapmaker PETG Translucent @U1 0.8 nozzle.json" + }, + { + "name": "Snapmaker PLA Basic @U1", + "sub_path": "filament/Snapmaker PLA Basic @U1.json" + }, + { + "name": "Snapmaker PLA Glow @U1 0.4 nozzle", + "sub_path": "filament/Snapmaker PLA Glow @U1 0.4 nozzle.json" + }, + { + "name": "Snapmaker PLA Matte @U1 0.2 nozzle", + "sub_path": "filament/Snapmaker PLA Matte @U1 0.2 nozzle.json" + }, + { + "name": "Snapmaker PLA Matte @U1 0.6 nozzle", + "sub_path": "filament/Snapmaker PLA Matte @U1 0.6 nozzle.json" + }, + { + "name": "Snapmaker PLA Matte @U1 0.8 nozzle", + "sub_path": "filament/Snapmaker PLA Matte @U1 0.8 nozzle.json" + }, + { + "name": "Snapmaker PLA SnapSpeed @U1 0.2 nozzle", + "sub_path": "filament/Snapmaker PLA SnapSpeed @U1 0.2 nozzle.json" + }, + { + "name": "Snapmaker PLA SnapSpeed @U1 0.6 nozzle", + "sub_path": "filament/Snapmaker PLA SnapSpeed @U1 0.6 nozzle.json" + }, + { + "name": "Snapmaker PLA SnapSpeed @U1 0.8 nozzle", + "sub_path": "filament/Snapmaker PLA SnapSpeed @U1 0.8 nozzle.json" + }, + { + "name": "Snapmaker PLA Translucent @U1 0.2 nozzle", + "sub_path": "filament/Snapmaker PLA Translucent @U1 0.2 nozzle.json" + }, + { + "name": "Snapmaker PLA Translucent @U1 0.4 nozzle", + "sub_path": "filament/Snapmaker PLA Translucent @U1 0.4 nozzle.json" + }, + { + "name": "Snapmaker PLA Translucent @U1 0.6 nozzle", + "sub_path": "filament/Snapmaker PLA Translucent @U1 0.6 nozzle.json" + }, + { + "name": "Snapmaker PLA Translucent @U1 0.8 nozzle", + "sub_path": "filament/Snapmaker PLA Translucent @U1 0.8 nozzle.json" } ], "machine_list": [ diff --git a/resources/profiles/Snapmaker/filament/Polymaker General PLA Family @U1.json b/resources/profiles/Snapmaker/filament/Polymaker General PLA Family @U1.json new file mode 100644 index 0000000000..c706ba776d --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Polymaker General PLA Family @U1.json @@ -0,0 +1,32 @@ +{ + "type": "filament", + "name": "Polymaker General PLA Family @U1", + "inherits": "Polymaker PLA @U1 base", + "from": "system", + "setting_id": "thXfSaUkOAKJ50ey", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.4 nozzle)" + ], + "enable_pressure_advance": [ + "0" + ], + "filament_max_volumetric_speed": [ + "15" + ], + "filament_vendor": [ + "Polymaker" + ], + "nozzle_temperature_range_high": [ + "230" + ], + "temperature_vitrification": [ + "62" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Polymaker PLA @U1 base.json b/resources/profiles/Snapmaker/filament/Polymaker PLA @U1 base.json new file mode 100644 index 0000000000..b27c48f720 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Polymaker PLA @U1 base.json @@ -0,0 +1,107 @@ +{ + "type": "filament", + "name": "Polymaker PLA @U1 base", + "inherits": "fdm_filament_pla", + "from": "system", + "filament_id": "OGFL99", + "instantiation": "false", + "compatible_printers": [ + "Snapmaker U1 (0.4 nozzle)" + ], + "activate_air_filtration": [ + "0" + ], + "chamber_temperatures": [ + "0" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "35" + ], + "cool_plate_temp_initial_layer": [ + "35" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "filament_cost": [ + "20" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_max_volumetric_speed": [ + "12" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_length": [ + "nil" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_vendor": [ + "Generic" + ], + "filament_scarf_seam_type": [ + "none" + ], + "filament_scarf_height": [ + "10%" + ], + "filament_scarf_gap": [ + "15%" + ], + "filament_scarf_length": [ + "10" + ], + "filament_shrink": [ + "100%" + ], + "hot_plate_temp": [ + "55" + ], + "hot_plate_temp_initial_layer": [ + "55" + ], + "nozzle_temperature": [ + "220" + ], + "overhang_fan_threshold": [ + "50%" + ], + "supertack_plate_temp": [ + "45" + ], + "supertack_plate_temp_initial_layer": [ + "45" + ], + "slow_down_layer_time": [ + "4" + ], + "slow_down_min_speed": [ + "10" + ], + "temperature_vitrification": [ + "45" + ], + "textured_plate_temp": [ + "55" + ], + "textured_plate_temp_initial_layer": [ + "55" + ], + "filament_start_gcode": [ + "; Filament gcode\n" + ], + "filament_end_gcode": [ + "; filament end gcode\n" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Polymaker Silk PLA Family @U1.json b/resources/profiles/Snapmaker/filament/Polymaker Silk PLA Family @U1.json new file mode 100644 index 0000000000..b6bd119022 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Polymaker Silk PLA Family @U1.json @@ -0,0 +1,32 @@ +{ + "type": "filament", + "name": "Polymaker Silk PLA Family @U1", + "inherits": "Polymaker PLA @U1 base", + "from": "system", + "setting_id": "h8vIYpXbxFtHPV6e", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.4 nozzle)" + ], + "filament_density": [ + "1.34" + ], + "filament_vendor": [ + "Polymaker" + ], + "nozzle_temperature": [ + "230" + ], + "nozzle_temperature_initial_layer": [ + "230" + ], + "nozzle_temperature_range_high": [ + "230" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Polymaker Tough PLA Family @U1.json b/resources/profiles/Snapmaker/filament/Polymaker Tough PLA Family @U1.json new file mode 100644 index 0000000000..e5c2c7d7f6 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Polymaker Tough PLA Family @U1.json @@ -0,0 +1,32 @@ +{ + "type": "filament", + "name": "Polymaker Tough PLA Family @U1", + "inherits": "Polymaker PLA @U1 base", + "from": "system", + "setting_id": "d6CC81Es2hp46bto", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.4 nozzle)" + ], + "filament_density": [ + "1.23" + ], + "filament_flow_ratio": [ + "0.96" + ], + "filament_vendor": [ + "Polymaker" + ], + "slow_down_layer_time": [ + "6" + ], + "temperature_vitrification": [ + "55" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Polymaker/Fiberon ASA-CF08 @Snapmaker U1.json b/resources/profiles/Snapmaker/filament/Polymaker/Fiberon ASA-CF08 @Snapmaker U1.json index 31799163c6..b38925e01b 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/Fiberon ASA-CF08 @Snapmaker U1.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/Fiberon ASA-CF08 @Snapmaker U1.json @@ -11,7 +11,6 @@ "filament_minimal_purge_on_wipe_tower": [ "15" ], - "pressure_advance": [ "0.05" ] diff --git a/resources/profiles/Snapmaker/filament/Polymaker/Fiberon PA12-CF10 @Snapmaker U1.json b/resources/profiles/Snapmaker/filament/Polymaker/Fiberon PA12-CF10 @Snapmaker U1.json index fd99eee105..1bb569c291 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/Fiberon PA12-CF10 @Snapmaker U1.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/Fiberon PA12-CF10 @Snapmaker U1.json @@ -17,7 +17,6 @@ "filament_z_hop": [ "0.0" ], - "enable_pressure_advance": [ "1" ], diff --git a/resources/profiles/Snapmaker/filament/Polymaker/Fiberon PA6-CF20 @Snapmaker U1.json b/resources/profiles/Snapmaker/filament/Polymaker/Fiberon PA6-CF20 @Snapmaker U1.json index 1b227b2fd0..d6106faa50 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/Fiberon PA6-CF20 @Snapmaker U1.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/Fiberon PA6-CF20 @Snapmaker U1.json @@ -17,7 +17,6 @@ "filament_z_hop": [ "0.0" ], - "enable_pressure_advance": [ "1" ], diff --git a/resources/profiles/Snapmaker/filament/Polymaker/Fiberon PETG-ESD @Snapmaker U1.json b/resources/profiles/Snapmaker/filament/Polymaker/Fiberon PETG-ESD @Snapmaker U1.json index cfe6890a80..6e1797904c 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/Fiberon PETG-ESD @Snapmaker U1.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/Fiberon PETG-ESD @Snapmaker U1.json @@ -17,7 +17,6 @@ "supertack_plate_temp_initial_layer": [ "70" ], - "pressure_advance": [ "0.04" ] diff --git a/resources/profiles/Snapmaker/filament/Polymaker/PolyLite Dual PLA @0.2 nozzle.json b/resources/profiles/Snapmaker/filament/Polymaker/PolyLite Dual PLA @0.2 nozzle.json index 707d5ae54b..f4703d6c40 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/PolyLite Dual PLA @0.2 nozzle.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/PolyLite Dual PLA @0.2 nozzle.json @@ -1,10 +1,10 @@ { "type": "filament", - "from": "system", - "instantiation": "true", "name": "PolyLite Dual PLA @0.2 nozzle", - "setting_id": "jhmy4YHi9MuL0DWu", "inherits": "PolyLite PLA @0.2 nozzle", + "from": "system", + "setting_id": "jhmy4YHi9MuL0DWu", + "instantiation": "true", "compatible_printers": [ "Snapmaker A250 Dual (0.2 nozzle)", "Snapmaker A250 Dual BKit (0.2 nozzle)", diff --git a/resources/profiles/Snapmaker/filament/Polymaker/PolyLite J1 PLA @0.2 nozzle.json b/resources/profiles/Snapmaker/filament/Polymaker/PolyLite J1 PLA @0.2 nozzle.json index 1a4422472c..e1ce855c61 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/PolyLite J1 PLA @0.2 nozzle.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/PolyLite J1 PLA @0.2 nozzle.json @@ -1,10 +1,10 @@ { "type": "filament", - "from": "system", - "instantiation": "true", "name": "PolyLite J1 PLA @0.2 nozzle", - "setting_id": "jFlRSxx0KvN3BOUu", "inherits": "PolyLite PLA @0.2 nozzle", + "from": "system", + "setting_id": "jFlRSxx0KvN3BOUu", + "instantiation": "true", "compatible_printers": [ "Snapmaker J1 (0.2 nozzle)" ] diff --git a/resources/profiles/Snapmaker/filament/Polymaker/PolyLite J1 PLA.json b/resources/profiles/Snapmaker/filament/Polymaker/PolyLite J1 PLA.json index 6c5017d0a8..55d39a0832 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/PolyLite J1 PLA.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/PolyLite J1 PLA.json @@ -1,10 +1,10 @@ { "type": "filament", - "from": "system", - "instantiation": "true", "name": "PolyLite J1 PLA", - "setting_id": "wcpOTTMyZNPFKyXr", "inherits": "PolyLite PLA @base", + "from": "system", + "setting_id": "wcpOTTMyZNPFKyXr", + "instantiation": "true", "compatible_printers": [ "Snapmaker J1 (0.4 nozzle)", "Snapmaker J1 (0.6 nozzle)", diff --git a/resources/profiles/Snapmaker/filament/Polymaker/PolyLite PETG @Base.json b/resources/profiles/Snapmaker/filament/Polymaker/PolyLite PETG @Base.json index cd5f75a463..579940a5b2 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/PolyLite PETG @Base.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/PolyLite PETG @Base.json @@ -1,4 +1,9 @@ { + "type": "filament", + "name": "PolyLite PETG @Base", + "inherits": "fdm_filament_petg", + "from": "system", + "instantiation": "false", "filament_density": [ "1.25" ], @@ -18,11 +23,6 @@ "0" ], "description": "", - "inherits": "fdm_filament_petg", - "name": "PolyLite PETG @Base", - "type": "filament", - "instantiation": "false", - "from": "system", "filament_vendor": [ "Polymaker" ] diff --git a/resources/profiles/Snapmaker/filament/Polymaker/PolyLite PETG @Snapmaker U1.json b/resources/profiles/Snapmaker/filament/Polymaker/PolyLite PETG @Snapmaker U1.json index 47cf37385d..f51cc5eeec 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/PolyLite PETG @Snapmaker U1.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/PolyLite PETG @Snapmaker U1.json @@ -17,7 +17,6 @@ "supertack_plate_temp_initial_layer": [ "70" ], - "pressure_advance": [ "0.05" ] diff --git a/resources/profiles/Snapmaker/filament/Polymaker/PolyLite PETG Translucent @Snapmaker U1.json b/resources/profiles/Snapmaker/filament/Polymaker/PolyLite PETG Translucent @Snapmaker U1.json index 6089dd66c4..a3efeee371 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/PolyLite PETG Translucent @Snapmaker U1.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/PolyLite PETG Translucent @Snapmaker U1.json @@ -17,7 +17,6 @@ "supertack_plate_temp_initial_layer": [ "70" ], - "pressure_advance": [ "0.05" ] diff --git a/resources/profiles/Snapmaker/filament/Polymaker/PolyLite PLA @0.2 nozzle.json b/resources/profiles/Snapmaker/filament/Polymaker/PolyLite PLA @0.2 nozzle.json index c3897e63c2..fcceb14014 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/PolyLite PLA @0.2 nozzle.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/PolyLite PLA @0.2 nozzle.json @@ -1,10 +1,10 @@ { "type": "filament", - "from": "system", - "instantiation": "true", "name": "PolyLite PLA @0.2 nozzle", - "setting_id": "s9mdMaFca8SHfji9", "inherits": "PolyLite PLA @base", + "from": "system", + "setting_id": "s9mdMaFca8SHfji9", + "instantiation": "true", "compatible_printers": [ "Snapmaker A250 (0.2 nozzle)", "Snapmaker A250 BKit (0.2 nozzle)", diff --git a/resources/profiles/Snapmaker/filament/Polymaker/PolyLite PLA @base.json b/resources/profiles/Snapmaker/filament/Polymaker/PolyLite PLA @base.json index 4173572637..3a4621b838 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/PolyLite PLA @base.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/PolyLite PLA @base.json @@ -1,10 +1,10 @@ { "type": "filament", - "from": "system", - "instantiation": "false", "name": "PolyLite PLA @base", - "filament_id": "1393866034", "inherits": "fdm_filament_pla", + "from": "system", + "filament_id": "1393866034", + "instantiation": "false", "filament_flow_ratio": [ "0.95" ], diff --git a/resources/profiles/Snapmaker/filament/Polymaker/PolyTerra Dual PLA @0.2 nozzle.json b/resources/profiles/Snapmaker/filament/Polymaker/PolyTerra Dual PLA @0.2 nozzle.json index b45068dd52..3f8cbc7674 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/PolyTerra Dual PLA @0.2 nozzle.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/PolyTerra Dual PLA @0.2 nozzle.json @@ -1,10 +1,10 @@ { "type": "filament", - "from": "system", - "instantiation": "true", "name": "PolyTerra Dual PLA @0.2 nozzle", - "setting_id": "UQYgjH1uVxf3d2Nv", "inherits": "PolyTerra PLA @0.2 nozzle", + "from": "system", + "setting_id": "UQYgjH1uVxf3d2Nv", + "instantiation": "true", "compatible_printers": [ "Snapmaker A250 Dual (0.2 nozzle)", "Snapmaker A250 Dual BKit (0.2 nozzle)", diff --git a/resources/profiles/Snapmaker/filament/Polymaker/PolyTerra J1 PLA @0.2 nozzle.json b/resources/profiles/Snapmaker/filament/Polymaker/PolyTerra J1 PLA @0.2 nozzle.json index dd4f969f62..9cb7060139 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/PolyTerra J1 PLA @0.2 nozzle.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/PolyTerra J1 PLA @0.2 nozzle.json @@ -1,10 +1,10 @@ { "type": "filament", - "from": "system", - "instantiation": "true", "name": "PolyTerra J1 PLA @0.2 nozzle", - "setting_id": "mxKHxGmBZzPLS82O", "inherits": "PolyTerra PLA @0.2 nozzle", + "from": "system", + "setting_id": "mxKHxGmBZzPLS82O", + "instantiation": "true", "compatible_printers": [ "Snapmaker J1 (0.2 nozzle)" ] diff --git a/resources/profiles/Snapmaker/filament/Polymaker/PolyTerra J1 PLA.json b/resources/profiles/Snapmaker/filament/Polymaker/PolyTerra J1 PLA.json index 7b66438058..2583b2a2c3 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/PolyTerra J1 PLA.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/PolyTerra J1 PLA.json @@ -1,10 +1,10 @@ { "type": "filament", - "from": "system", - "instantiation": "true", "name": "PolyTerra J1 PLA", - "setting_id": "4MZWZX7QPBzxdEGj", "inherits": "PolyTerra PLA @base", + "from": "system", + "setting_id": "4MZWZX7QPBzxdEGj", + "instantiation": "true", "compatible_printers": [ "Snapmaker J1 (0.4 nozzle)", "Snapmaker J1 (0.6 nozzle)", diff --git a/resources/profiles/Snapmaker/filament/Polymaker/PolyTerra PLA @0.2 nozzle.json b/resources/profiles/Snapmaker/filament/Polymaker/PolyTerra PLA @0.2 nozzle.json index 07d45a7633..e70ee3d5a0 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/PolyTerra PLA @0.2 nozzle.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/PolyTerra PLA @0.2 nozzle.json @@ -1,10 +1,10 @@ { "type": "filament", - "from": "system", - "instantiation": "true", "name": "PolyTerra PLA @0.2 nozzle", - "setting_id": "6W1ygT08NordBdIW", "inherits": "PolyTerra PLA @base", + "from": "system", + "setting_id": "6W1ygT08NordBdIW", + "instantiation": "true", "compatible_printers": [ "Snapmaker A250 (0.2 nozzle)", "Snapmaker A250 BKit (0.2 nozzle)", diff --git a/resources/profiles/Snapmaker/filament/Polymaker/Polymaker HT-PLA @Base.json b/resources/profiles/Snapmaker/filament/Polymaker/Polymaker HT-PLA @Base.json index 7c05254a2e..76b1d2020c 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/Polymaker HT-PLA @Base.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/Polymaker HT-PLA @Base.json @@ -1,4 +1,9 @@ { + "type": "filament", + "name": "Polymaker HT-PLA @Base", + "inherits": "fdm_filament_pla", + "from": "system", + "instantiation": "false", "filament_density": [ "1.28" ], @@ -18,11 +23,6 @@ "0" ], "description": "", - "inherits": "fdm_filament_pla", - "name": "Polymaker HT-PLA @Base", - "type": "filament", - "instantiation": "false", - "from": "system", "filament_vendor": [ "Polymaker" ] diff --git a/resources/profiles/Snapmaker/filament/Polymaker/Polymaker HT-PLA-GF @Base.json b/resources/profiles/Snapmaker/filament/Polymaker/Polymaker HT-PLA-GF @Base.json index 3a64237122..06019b562c 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/Polymaker HT-PLA-GF @Base.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/Polymaker HT-PLA-GF @Base.json @@ -1,4 +1,9 @@ { + "type": "filament", + "name": "Polymaker HT-PLA-GF @Base", + "inherits": "fdm_filament_pla", + "from": "system", + "instantiation": "false", "filament_density": [ "1.34" ], @@ -18,11 +23,6 @@ "0" ], "description": "", - "inherits": "fdm_filament_pla", - "name": "Polymaker HT-PLA-GF @Base", - "type": "filament", - "instantiation": "false", - "from": "system", "filament_vendor": [ "Polymaker" ] diff --git a/resources/profiles/Snapmaker/filament/Polymaker/Polymaker PETG @Base.json b/resources/profiles/Snapmaker/filament/Polymaker/Polymaker PETG @Base.json index 0965bd153b..3692eac7d5 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/Polymaker PETG @Base.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/Polymaker PETG @Base.json @@ -1,4 +1,9 @@ { + "type": "filament", + "name": "Polymaker PETG @Base", + "inherits": "fdm_filament_petg", + "from": "system", + "instantiation": "false", "filament_density": [ "1.3" ], @@ -18,11 +23,6 @@ "0" ], "description": "", - "inherits": "fdm_filament_petg", - "name": "Polymaker PETG @Base", - "type": "filament", - "instantiation": "false", - "from": "system", "filament_vendor": [ "Polymaker" ] diff --git a/resources/profiles/Snapmaker/filament/Polymaker/Polymaker PLA Pro @Base.json b/resources/profiles/Snapmaker/filament/Polymaker/Polymaker PLA Pro @Base.json index 072fa53c93..d69ccc79f2 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/Polymaker PLA Pro @Base.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/Polymaker PLA Pro @Base.json @@ -1,4 +1,9 @@ { + "type": "filament", + "name": "Polymaker PLA Pro @Base", + "inherits": "fdm_filament_pla", + "from": "system", + "instantiation": "false", "filament_density": [ "1.23" ], @@ -18,11 +23,6 @@ "0" ], "description": "", - "inherits": "fdm_filament_pla", - "name": "Polymaker PLA Pro @Base", - "type": "filament", - "instantiation": "false", - "from": "system", "filament_vendor": [ "Polymaker" ] diff --git a/resources/profiles/Snapmaker/filament/Snapmaker ABS @U1.json b/resources/profiles/Snapmaker/filament/Snapmaker ABS @U1.json index 083432ebd5..fe6c59ba80 100644 --- a/resources/profiles/Snapmaker/filament/Snapmaker ABS @U1.json +++ b/resources/profiles/Snapmaker/filament/Snapmaker ABS @U1.json @@ -7,5 +7,8 @@ "instantiation": "true", "compatible_printers": [ "Snapmaker U1 (0.4 nozzle)" + ], + "filament_retract_length_toolchange": [ + "5" ] } diff --git a/resources/profiles/Snapmaker/filament/Snapmaker ASA @U1.json b/resources/profiles/Snapmaker/filament/Snapmaker ASA @U1.json index 0c5d22723a..67bde29e8d 100644 --- a/resources/profiles/Snapmaker/filament/Snapmaker ASA @U1.json +++ b/resources/profiles/Snapmaker/filament/Snapmaker ASA @U1.json @@ -7,5 +7,8 @@ "instantiation": "true", "compatible_printers": [ "Snapmaker U1 (0.4 nozzle)" + ], + "filament_retract_length_toolchange": [ + "5" ] } diff --git a/resources/profiles/Snapmaker/filament/Snapmaker Breakaway Support For PLA @U1 0.2 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker Breakaway Support For PLA @U1 0.2 nozzle.json new file mode 100644 index 0000000000..db331b0daa --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker Breakaway Support For PLA @U1 0.2 nozzle.json @@ -0,0 +1,104 @@ +{ + "type": "filament", + "name": "Snapmaker Breakaway Support For PLA @U1 0.2 nozzle", + "inherits": "Snapmaker Breakaway Support @base", + "from": "system", + "setting_id": "mJoaud6wulT4p8SA", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.2 nozzle)" + ], + "filament_type": [ + "PLA" + ], + "enable_pressure_advance": [ + "0" + ], + "additional_cooling_fan_speed": [ + "70" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "fan_max_speed": [ + "30" + ], + "fan_min_speed": [ + "20" + ], + "filament_cost": [ + "69.98" + ], + "filament_density": [ + "1.3" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_flow_ratio": [ + "1" + ], + "filament_max_volumetric_speed": [ + "0.5" + ], + "filament_retract_length_toolchange": [ + "10" + ], + "filament_retraction_length": [ + "nil" + ], + "filament_retraction_speed": [ + "nil" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "nozzle_temperature": [ + "240" + ], + "nozzle_temperature_initial_layer": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "pressure_advance": [ + "0.2" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ], + "filament_cooling_moves": [ + "4" + ], + "filament_loading_speed": [ + "28" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "5" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_unloading_speed": [ + "90" + ], + "filament_unloading_speed_start": [ + "100" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker Breakaway Support For PLA @U1 0.6 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker Breakaway Support For PLA @U1 0.6 nozzle.json new file mode 100644 index 0000000000..248c57f6d0 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker Breakaway Support For PLA @U1 0.6 nozzle.json @@ -0,0 +1,104 @@ +{ + "type": "filament", + "name": "Snapmaker Breakaway Support For PLA @U1 0.6 nozzle", + "inherits": "Snapmaker Breakaway Support @base", + "from": "system", + "setting_id": "rKUUREJNhstIU0I2", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.6 nozzle)" + ], + "filament_type": [ + "PLA" + ], + "enable_pressure_advance": [ + "0" + ], + "filament_cost": [ + "69.98" + ], + "filament_density": [ + "1.3" + ], + "filament_flow_ratio": [ + "0.95" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "nozzle_temperature": [ + "210" + ], + "nozzle_temperature_initial_layer": [ + "210" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "pressure_advance": [ + "0.02" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ], + "additional_cooling_fan_speed": [ + "70" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "fan_min_speed": [ + "100" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_retract_length_toolchange": [ + "10" + ], + "filament_retraction_length": [ + "nil" + ], + "filament_retraction_minimum_travel": [ + "1" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_wipe_distance": [ + "2" + ], + "filament_cooling_moves": [ + "4" + ], + "filament_loading_speed": [ + "28" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "20" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_unloading_speed": [ + "90" + ], + "filament_unloading_speed_start": [ + "100" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker Breakaway Support For PLA @U1 0.8 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker Breakaway Support For PLA @U1 0.8 nozzle.json new file mode 100644 index 0000000000..47cf80d076 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker Breakaway Support For PLA @U1 0.8 nozzle.json @@ -0,0 +1,98 @@ +{ + "type": "filament", + "name": "Snapmaker Breakaway Support For PLA @U1 0.8 nozzle", + "inherits": "Snapmaker Breakaway Support @base", + "from": "system", + "setting_id": "hQ8jQ5GKQKGKuPew", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.8 nozzle)" + ], + "filament_type": [ + "PLA" + ], + "enable_pressure_advance": [ + "0" + ], + "additional_cooling_fan_speed": [ + "70" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "filament_cost": [ + "69.98" + ], + "filament_density": [ + "1.3" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_flow_ratio": [ + "0.93" + ], + "filament_retract_length_toolchange": [ + "10" + ], + "filament_retraction_length": [ + "3" + ], + "filament_retraction_speed": [ + "nil" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "nozzle_temperature": [ + "210" + ], + "nozzle_temperature_initial_layer": [ + "210" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "pressure_advance": [ + "0.015" + ], + "textured_plate_temp_initial_layer": [ + "65" + ], + "filament_cooling_moves": [ + "4" + ], + "filament_loading_speed": [ + "28" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "20" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_unloading_speed": [ + "90" + ], + "filament_unloading_speed_start": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "textured_plate_temp": [ + "65" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker Breakaway Support For PLA @U1.json b/resources/profiles/Snapmaker/filament/Snapmaker Breakaway Support For PLA @U1.json index 97b2f8b07f..9e5d63a63c 100644 --- a/resources/profiles/Snapmaker/filament/Snapmaker Breakaway Support For PLA @U1.json +++ b/resources/profiles/Snapmaker/filament/Snapmaker Breakaway Support For PLA @U1.json @@ -10,5 +10,83 @@ ], "filament_type": [ "PLA" + ], + "enable_pressure_advance": [ + "1" + ], + "additional_cooling_fan_speed": [ + "70" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "fan_min_speed": [ + "100" + ], + "filament_cooling_moves": [ + "4" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_flow_ratio": [ + "1" + ], + "filament_loading_speed": [ + "28" + ], + "filament_minimal_purge_on_wipe_tower": [ + "20" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "20" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_retract_length_toolchange": [ + "5" + ], + "filament_retraction_length": [ + "1" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_unloading_speed": [ + "90" + ], + "filament_unloading_speed_start": [ + "100" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "nozzle_temperature": [ + "210" + ], + "nozzle_temperature_initial_layer": [ + "210" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "pressure_advance": [ + "0.03" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" ] } diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 0.2 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 0.2 nozzle.json new file mode 100644 index 0000000000..f1c3aa30a1 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 0.2 nozzle.json @@ -0,0 +1,108 @@ +{ + "type": "filament", + "name": "Snapmaker PETG HF @U1 0.2 nozzle", + "inherits": "Snapmaker PETG HF @U1 base2", + "from": "system", + "setting_id": "Ujul2YTBwfldFiFd", + "instantiation": "true", + "filament_max_volumetric_speed": [ + "1", + "1" + ], + "filament_ramming_travel_time": [ + "0", + "0" + ], + "long_retractions_when_ec": [ + "0", + "0" + ], + "retraction_distances_when_ec": [ + "0", + "0" + ], + "compatible_printers": [ + "Snapmaker U1 (0.2 nozzle)" + ], + "additional_cooling_fan_speed": [ + "70" + ], + "eng_plate_temp": [ + "80" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "fan_cooling_layer_time": [ + "20" + ], + "fan_max_speed": [ + "60" + ], + "fan_min_speed": [ + "30" + ], + "filament_end_gcode": [ + "\n\n" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "3" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_retract_length_toolchange": [ + "5" + ], + "filament_retraction_length": [ + "1.5" + ], + "filament_start_gcode": [ + "" + ], + "filament_vendor": [ + "Snapmaker" + ], + "hot_plate_temp": [ + "80" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "nozzle_temperature": [ + "245" + ], + "nozzle_temperature_initial_layer": [ + "245" + ], + "nozzle_temperature_range_low": [ + "230" + ], + "overhang_fan_speed": [ + "100" + ], + "slow_down_layer_time": [ + "10" + ], + "slow_down_min_speed": [ + "20" + ], + "supertack_plate_temp": [ + "60" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], + "textured_plate_temp": [ + "80" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "filament_z_hop_types": [ + "Slope Lift" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 0.6 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 0.6 nozzle.json new file mode 100644 index 0000000000..66a535b7fb --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 0.6 nozzle.json @@ -0,0 +1,113 @@ +{ + "type": "filament", + "name": "Snapmaker PETG HF @U1 0.6 nozzle", + "inherits": "Snapmaker PETG HF @U1 base2", + "from": "system", + "setting_id": "2FfQZdp5xunngnF0", + "instantiation": "true", + "filament_ramming_travel_time": [ + "0", + "0" + ], + "long_retractions_when_ec": [ + "0", + "0" + ], + "retraction_distances_when_ec": [ + "0", + "0" + ], + "compatible_printers": [ + "Snapmaker U1 (0.6 nozzle)" + ], + "additional_cooling_fan_speed": [ + "70" + ], + "eng_plate_temp": [ + "80" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "fan_cooling_layer_time": [ + "20" + ], + "fan_max_speed": [ + "60" + ], + "fan_min_speed": [ + "30" + ], + "filament_end_gcode": [ + "\n" + ], + "filament_max_volumetric_speed": [ + "20" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "20" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_retract_length_toolchange": [ + "5" + ], + "filament_retraction_length": [ + "2.5" + ], + "filament_start_gcode": [ + "" + ], + "filament_vendor": [ + "Snapmaker" + ], + "filament_wipe": [ + "1" + ], + "filament_wipe_distance": [ + "1" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "hot_plate_temp": [ + "80" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "nozzle_temperature": [ + "245" + ], + "nozzle_temperature_initial_layer": [ + "245" + ], + "nozzle_temperature_range_low": [ + "230" + ], + "overhang_fan_speed": [ + "100" + ], + "slow_down_layer_time": [ + "10" + ], + "slow_down_min_speed": [ + "20" + ], + "supertack_plate_temp": [ + "60" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], + "textured_plate_temp": [ + "80" + ], + "textured_plate_temp_initial_layer": [ + "80" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 0.8 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 0.8 nozzle.json new file mode 100644 index 0000000000..04446a623e --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 0.8 nozzle.json @@ -0,0 +1,110 @@ +{ + "type": "filament", + "name": "Snapmaker PETG HF @U1 0.8 nozzle", + "inherits": "Snapmaker PETG HF @U1 base2", + "from": "system", + "setting_id": "ebANa67V4B2nwUaX", + "instantiation": "true", + "filament_ramming_travel_time": [ + "0", + "0" + ], + "long_retractions_when_ec": [ + "0", + "0" + ], + "retraction_distances_when_ec": [ + "0", + "0" + ], + "compatible_printers": [ + "Snapmaker U1 (0.8 nozzle)" + ], + "additional_cooling_fan_speed": [ + "70" + ], + "eng_plate_temp": [ + "80" + ], + "eng_plate_temp_initial_layer": [ + "80" + ], + "fan_cooling_layer_time": [ + "20" + ], + "fan_max_speed": [ + "60" + ], + "fan_min_speed": [ + "30" + ], + "filament_max_volumetric_speed": [ + "20" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "20" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_retract_length_toolchange": [ + "5" + ], + "filament_retraction_length": [ + "1" + ], + "filament_start_gcode": [ + "" + ], + "filament_vendor": [ + "Snapmaker" + ], + "filament_wipe": [ + "1" + ], + "filament_wipe_distance": [ + "2" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "hot_plate_temp": [ + "80" + ], + "hot_plate_temp_initial_layer": [ + "80" + ], + "nozzle_temperature": [ + "245" + ], + "nozzle_temperature_initial_layer": [ + "245" + ], + "nozzle_temperature_range_low": [ + "230" + ], + "overhang_fan_speed": [ + "100" + ], + "slow_down_layer_time": [ + "10" + ], + "slow_down_min_speed": [ + "20" + ], + "supertack_plate_temp": [ + "60" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], + "textured_plate_temp": [ + "80" + ], + "textured_plate_temp_initial_layer": [ + "80" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 base2.json b/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 base2.json new file mode 100644 index 0000000000..0ad75759a1 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PETG HF @U1 base2.json @@ -0,0 +1,279 @@ +{ + "type": "filament", + "name": "Snapmaker PETG HF @U1 base2", + "inherits": "fdm_filament_petg", + "from": "system", + "filament_id": "GFG96", + "instantiation": "false", + "cool_plate_temp": [ + "0" + ], + "cool_plate_temp_initial_layer": [ + "0" + ], + "eng_plate_temp": [ + "70" + ], + "eng_plate_temp_initial_layer": [ + "70" + ], + "filament_type": [ + "PETG" + ], + "fan_cooling_layer_time": [ + "30" + ], + "fan_max_speed": [ + "40" + ], + "filament_cost": [ + "24.99" + ], + "filament_density": [ + "1.28" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_max_volumetric_speed": [ + "16" + ], + "hot_plate_temp": [ + "70" + ], + "hot_plate_temp_initial_layer": [ + "70" + ], + "nozzle_temperature_range_high": [ + "270" + ], + "nozzle_temperature": [ + "220" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "overhang_fan_speed": [ + "90" + ], + "overhang_fan_threshold": [ + "10%" + ], + "slow_down_layer_time": [ + "25" + ], + "textured_plate_temp": [ + "70" + ], + "textured_plate_temp_initial_layer": [ + "70" + ], + "filament_start_gcode": [ + "; filament start gcode\n{if (bed_temperature[current_extruder] >80)||(bed_temperature_initial_layer[current_extruder] >80)}M106 P3 S255\n{elsif (bed_temperature[current_extruder] >60)||(bed_temperature_initial_layer[current_extruder] >60)}M106 P3 S180\n{endif}\n\n{if activate_air_filtration[current_extruder] && support_air_filtration}\nM106 P3 S{during_print_exhaust_fan_speed_num[current_extruder]} \n{endif}" + ], + "activate_air_filtration": [ + "0" + ], + "additional_cooling_fan_speed": [ + "0" + ], + "chamber_temperatures": [ + "0" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "counter_coef_1": [ + "0" + ], + "counter_coef_2": [ + "0.008" + ], + "counter_coef_3": [ + "-0.041" + ], + "counter_limit_min": [ + "-0.035" + ], + "counter_limit_max": [ + "0.033" + ], + "circle_compensation_speed": [ + "200" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "diameter_limit": [ + "50" + ], + "fan_min_speed": [ + "20" + ], + "filament_cooling_before_tower": [ + "0" + ], + "filament_dev_ams_drying_ams_limitations": [ + "0" + ], + "filament_dev_ams_drying_temperature": [ + "40.0" + ], + "filament_dev_ams_drying_time": [ + "8.0" + ], + "filament_dev_drying_softening_temperature": [ + "40.0" + ], + "filament_dev_ams_drying_heat_distortion_temperature": [ + "45.0" + ], + "filament_dev_drying_cooling_temperature": [ + "35.0" + ], + "filament_dev_chamber_drying_bed_temperature": [ + "90.0" + ], + "filament_dev_chamber_drying_time": [ + "12.0" + ], + "filament_flush_temp": [ + "0" + ], + "filament_flush_volumetric_speed": [ + "0" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_long_retractions_when_ec": [ + "nil" + ], + "filament_ramming_volumetric_speed": [ + "-1" + ], + "filament_ramming_volumetric_speed_nc": [ + "-1" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_printable": [ + "3" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_distances_when_ec": [ + "nil" + ], + "filament_retraction_length": [ + "nil" + ], + "filament_retraction_minimum_travel": [ + "nil" + ], + "filament_vendor": [ + "Generic" + ], + "filament_prime_volume": [ + "45" + ], + "filament_prime_volume_nc": [ + "60" + ], + "filament_extruder_variant": [ + "Direct Drive Standard" + ], + "filament_scarf_seam_type": [ + "none" + ], + "filament_scarf_height": [ + "10%" + ], + "filament_scarf_gap": [ + "0%" + ], + "filament_scarf_length": [ + "10" + ], + "filament_shrink": [ + "100%" + ], + "filament_pre_cooling_temperature": [ + "0" + ], + "filament_pre_cooling_temperature_nc": [ + "0" + ], + "filament_ramming_travel_time": [ + "0" + ], + "filament_ramming_travel_time_nc": [ + "0" + ], + "filament_retract_length_nc": [ + "14" + ], + "hole_coef_1": [ + "0" + ], + "hole_coef_2": [ + "-0.008" + ], + "hole_coef_3": [ + "0.23415" + ], + "hole_limit_min": [ + "0.088" + ], + "hole_limit_max": [ + "0.22" + ], + "impact_strength_z": [ + "10" + ], + "long_retractions_when_ec": [ + "0" + ], + "retraction_distances_when_ec": [ + "0" + ], + "supertack_plate_temp": [ + "70" + ], + "supertack_plate_temp_initial_layer": [ + "70" + ], + "no_slow_down_for_cooling_on_outwalls": [ + "0" + ], + "slow_down_min_speed": [ + "10" + ], + "temperature_vitrification": [ + "70" + ], + "filament_change_length": [ + "10" + ], + "filament_velocity_adaptation_factor": [ + "1" + ], + "compatible_printers": [], + "filament_end_gcode": [ + "; filament end gcode \n\n" + ], + "filament_adaptive_volumetric_speed": [ + "0" + ], + "volumetric_speed_coefficients": [ + "0 0 0 0 0 0" + ], + "filament_adhesiveness_category": [ + "300" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PETG Translucent @U1 0.2 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PETG Translucent @U1 0.2 nozzle.json new file mode 100644 index 0000000000..ee6234f406 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PETG Translucent @U1 0.2 nozzle.json @@ -0,0 +1,110 @@ +{ + "type": "filament", + "name": "Snapmaker PETG Translucent @U1 0.2 nozzle", + "inherits": "Snapmaker PETG Translucent @U1 base", + "from": "system", + "setting_id": "UTpedqYJysaoe3OI", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.2 nozzle)" + ], + "additional_cooling_fan_speed": [ + "20" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "fan_max_speed": [ + "40" + ], + "fan_min_speed": [ + "20" + ], + "filament_cooling_moves": [ + "4" + ], + "filament_cost": [ + "25" + ], + "filament_density": [ + "1.25" + ], + "filament_flow_ratio": [ + "0.98" + ], + "filament_loading_speed": [ + "28" + ], + "filament_max_volumetric_speed": [ + "1" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "5" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_retract_length_toolchange": [ + "10" + ], + "filament_retraction_length": [ + "1" + ], + "filament_retraction_minimum_travel": [ + "nil" + ], + "filament_retraction_speed": [ + "50" + ], + "filament_unloading_speed": [ + "90" + ], + "filament_unloading_speed_start": [ + "100" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "hot_plate_temp": [ + "80" + ], + "nozzle_temperature": [ + "245" + ], + "nozzle_temperature_initial_layer": [ + "250" + ], + "nozzle_temperature_range_low": [ + "230" + ], + "overhang_fan_threshold": [ + "10%" + ], + "pressure_advance": [ + "0.23" + ], + "slow_down_min_speed": [ + "10" + ], + "textured_plate_temp": [ + "80" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "enable_pressure_advance": [ + "0" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PETG Translucent @U1 0.4 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PETG Translucent @U1 0.4 nozzle.json new file mode 100644 index 0000000000..e0e60a0e14 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PETG Translucent @U1 0.4 nozzle.json @@ -0,0 +1,98 @@ +{ + "type": "filament", + "name": "Snapmaker PETG Translucent @U1 0.4 nozzle", + "inherits": "Snapmaker PETG Translucent @U1 base", + "from": "system", + "setting_id": "k6UQJJASzFSfePqN", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.4 nozzle)" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "fan_max_speed": [ + "30" + ], + "fan_min_speed": [ + "10" + ], + "filament_cooling_moves": [ + "4" + ], + "filament_cost": [ + "24.99" + ], + "filament_flow_ratio": [ + "0.97" + ], + "filament_loading_speed": [ + "28" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "20" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_retract_length_toolchange": [ + "5" + ], + "filament_retraction_length": [ + "1" + ], + "filament_retraction_minimum_travel": [ + "nil" + ], + "filament_unloading_speed": [ + "90" + ], + "filament_unloading_speed_start": [ + "100" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "hot_plate_temp": [ + "80" + ], + "nozzle_temperature": [ + "250" + ], + "nozzle_temperature_initial_layer": [ + "250" + ], + "nozzle_temperature_range_low": [ + "230" + ], + "overhang_fan_threshold": [ + "10%" + ], + "pressure_advance": [ + "0.04" + ], + "slow_down_min_speed": [ + "10" + ], + "textured_plate_temp": [ + "80" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "filament_density": [ + "1.25" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PETG Translucent @U1 0.6 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PETG Translucent @U1 0.6 nozzle.json new file mode 100644 index 0000000000..14f59104f8 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PETG Translucent @U1 0.6 nozzle.json @@ -0,0 +1,107 @@ +{ + "type": "filament", + "name": "Snapmaker PETG Translucent @U1 0.6 nozzle", + "inherits": "Snapmaker PETG Translucent @U1 base", + "from": "system", + "setting_id": "FlcbnP3kWiJLMDMl", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.6 nozzle)" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "fan_max_speed": [ + "60" + ], + "fan_min_speed": [ + "20" + ], + "filament_cost": [ + "25" + ], + "filament_density": [ + "1.25" + ], + "filament_flow_ratio": [ + "0.98" + ], + "filament_max_volumetric_speed": [ + "16" + ], + "hot_plate_temp": [ + "80" + ], + "nozzle_temperature": [ + "245" + ], + "nozzle_temperature_initial_layer": [ + "250" + ], + "nozzle_temperature_range_low": [ + "230" + ], + "overhang_fan_threshold": [ + "10%" + ], + "pressure_advance": [ + "0.02" + ], + "slow_down_min_speed": [ + "10" + ], + "textured_plate_temp": [ + "80" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "filament_cooling_moves": [ + "4" + ], + "filament_loading_speed": [ + "28" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "20" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_retract_length_toolchange": [ + "10" + ], + "filament_retraction_length": [ + "1.2" + ], + "filament_retraction_minimum_travel": [ + "1" + ], + "filament_unloading_speed": [ + "90" + ], + "filament_unloading_speed_start": [ + "100" + ], + "filament_wipe_distance": [ + "2" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "enable_pressure_advance": [ + "0" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PETG Translucent @U1 0.8 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PETG Translucent @U1 0.8 nozzle.json new file mode 100644 index 0000000000..e746141f06 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PETG Translucent @U1 0.8 nozzle.json @@ -0,0 +1,104 @@ +{ + "type": "filament", + "name": "Snapmaker PETG Translucent @U1 0.8 nozzle", + "inherits": "Snapmaker PETG Translucent @U1 base", + "from": "system", + "setting_id": "aJk3TnFTU3uQGjd3", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.8 nozzle)" + ], + "close_fan_the_first_x_layers": [ + "3" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "fan_max_speed": [ + "60" + ], + "fan_min_speed": [ + "20" + ], + "filament_cost": [ + "25" + ], + "filament_density": [ + "1.25" + ], + "filament_flow_ratio": [ + "0.97" + ], + "filament_max_volumetric_speed": [ + "16" + ], + "hot_plate_temp": [ + "80" + ], + "nozzle_temperature": [ + "245" + ], + "nozzle_temperature_initial_layer": [ + "250" + ], + "nozzle_temperature_range_low": [ + "230" + ], + "overhang_fan_threshold": [ + "10%" + ], + "pressure_advance": [ + "0.02" + ], + "slow_down_min_speed": [ + "10" + ], + "textured_plate_temp": [ + "80" + ], + "textured_plate_temp_initial_layer": [ + "80" + ], + "filament_cooling_moves": [ + "4" + ], + "filament_loading_speed": [ + "28" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "30" + ], + "filament_multitool_ramming_volume": [ + "10" + ], + "filament_retract_length_toolchange": [ + "10" + ], + "filament_retraction_length": [ + "1" + ], + "filament_retraction_minimum_travel": [ + "nil" + ], + "filament_unloading_speed": [ + "90" + ], + "filament_unloading_speed_start": [ + "100" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "enable_pressure_advance": [ + "0" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PETG Translucent @U1 base.json b/resources/profiles/Snapmaker/filament/Snapmaker PETG Translucent @U1 base.json new file mode 100644 index 0000000000..eb18fb4838 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PETG Translucent @U1 base.json @@ -0,0 +1,8 @@ +{ + "type": "filament", + "name": "Snapmaker PETG Translucent @U1 base", + "inherits": "fdm_filament_petg", + "from": "system", + "filament_id": "PETG_TRANSLUCENT_001", + "instantiation": "false" +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Basic @U1 base.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Basic @U1 base.json new file mode 100644 index 0000000000..adf7624e1f --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Basic @U1 base.json @@ -0,0 +1,127 @@ +{ + "type": "filament", + "name": "Snapmaker PLA Basic @U1 base", + "from": "system", + "filament_id": "1417031127011", + "instantiation": "false", + "filament_end_gcode": [ + "" + ], + "filament_retraction_length": [ + "nil" + ], + "filament_loading_speed_start": [ + "35" + ], + "filament_loading_speed": [ + "35" + ], + "filament_unloading_speed_start": [ + "35" + ], + "filament_unloading_speed": [ + "35" + ], + "filament_load_time": [ + "2" + ], + "filament_unload_time": [ + "2" + ], + "filament_cooling_moves": [ + "2" + ], + "filament_cooling_initial_speed": [ + "35" + ], + "filament_cooling_final_speed": [ + "60" + ], + "fan_cooling_layer_time": [ + "100" + ], + "filament_max_volumetric_speed": [ + "12" + ], + "filament_type": [ + "PLA" + ], + "filament_density": [ + "1.24" + ], + "filament_cost": [ + "20" + ], + "cool_plate_temp": [ + "60" + ], + "eng_plate_temp": [ + "60" + ], + "hot_plate_temp": [ + "60" + ], + "textured_plate_temp": [ + "60" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "60" + ], + "hot_plate_temp_initial_layer": [ + "60" + ], + "textured_plate_temp_initial_layer": [ + "60" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "overhang_fan_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "50%" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "nozzle_temperature": [ + "220" + ], + "temperature_vitrification": [ + "60" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "nozzle_temperature_range_high": [ + "230" + ], + "slow_down_min_speed": [ + "10" + ], + "slow_down_layer_time": [ + "4" + ], + "additional_cooling_fan_speed": [ + "70" + ], + "filament_start_gcode": [ + "; filament start gcode\n" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Basic @U1.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Basic @U1.json new file mode 100644 index 0000000000..4b68672fa5 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Basic @U1.json @@ -0,0 +1,284 @@ +{ + "type": "filament", + "name": "Snapmaker PLA Basic @U1", + "inherits": "Snapmaker PLA Basic @U1 base", + "from": "system", + "setting_id": "ZtUJN4JpkR0MLpiY", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.4 nozzle)" + ], + "activate_air_filtration": [ + "0" + ], + "activate_chamber_temp_control": [ + "0" + ], + "adaptive_pressure_advance": [ + "0" + ], + "adaptive_pressure_advance_bridges": [ + "0" + ], + "adaptive_pressure_advance_model": [ + "0,0,0\n0,0,0" + ], + "adaptive_pressure_advance_overhangs": [ + "0" + ], + "additional_cooling_fan_speed": [ + "70" + ], + "chamber_temperature": [ + "0" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "compatible_printers_condition": "", + "compatible_prints": [], + "compatible_prints_condition": "", + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "60" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "dont_slow_down_outer_wall": [ + "0" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "enable_overhang_bridge_fan": [ + "1" + ], + "enable_pressure_advance": [ + "0" + ], + "eng_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "60" + ], + "fan_cooling_layer_time": [ + "100" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "filament_cooling_final_speed": [ + "3.5" + ], + "filament_cooling_initial_speed": [ + "10" + ], + "filament_cooling_moves": [ + "2" + ], + "filament_cost": [ + "25.4" + ], + "filament_density": [ + "1.32" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_diameter": [ + "1.75" + ], + "filament_end_gcode": [ + "; filament end gcode \n" + ], + "filament_flow_ratio": [ + "0.98" + ], + "filament_is_support": [ + "0" + ], + "filament_loading_speed": [ + "10" + ], + "filament_loading_speed_start": [ + "50" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_max_volumetric_speed": [ + "15" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "25" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_notes": [ + "" + ], + "filament_ramming_parameters": [ + "120 100 6.6 6.8 7.2 7.6 7.9 8.2 8.7 9.4 9.9 10.0| 0.05 6.6 0.45 6.8 0.95 7.8 1.45 8.3 1.95 9.7 2.45 10 2.95 7.6 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6" + ], + "filament_retract_before_wipe": [ + "nil" + ], + "filament_retract_length_toolchange": [ + "10" + ], + "filament_retract_lift_above": [ + "nil" + ], + "filament_retract_lift_below": [ + "nil" + ], + "filament_retract_lift_enforce": [ + "nil" + ], + "filament_retract_restart_extra": [ + "nil" + ], + "filament_retract_when_changing_layer": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_length": [ + "nil" + ], + "filament_retraction_minimum_travel": [ + "nil" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_shrink": [ + "100%" + ], + "filament_shrinkage_compensation_z": [ + "100%" + ], + "filament_soluble": [ + "0" + ], + "filament_stamping_distance": [ + "45" + ], + "filament_stamping_loading_speed": [ + "29" + ], + "filament_start_gcode": [ + "" + ], + "filament_toolchange_delay": [ + "0" + ], + "filament_type": [ + "PLA" + ], + "filament_unloading_speed": [ + "100" + ], + "filament_unloading_speed_start": [ + "100" + ], + "filament_vendor": [ + "Snapmaker" + ], + "filament_wipe": [ + "nil" + ], + "filament_wipe_distance": [ + "nil" + ], + "filament_z_hop": [ + "nil" + ], + "filament_z_hop_types": [ + "nil" + ], + "full_fan_speed_layer": [ + "0" + ], + "hot_plate_temp": [ + "65" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "idle_temperature": [ + "0" + ], + "nozzle_temperature": [ + "220" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "overhang_fan_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "50%" + ], + "pellet_flow_coefficient": [ + "0.4157" + ], + "pressure_advance": [ + "0.02" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "required_nozzle_HRC": [ + "0" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "slow_down_layer_time": [ + "4" + ], + "slow_down_min_speed": [ + "20" + ], + "support_material_interface_fan_speed": [ + "-1" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp": [ + "40" + ], + "textured_cool_plate_temp_initial_layer": [ + "40" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Full Spectrum @U1 0.4 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Full Spectrum @U1 0.4 nozzle.json new file mode 100644 index 0000000000..990e67a79e --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Full Spectrum @U1 0.4 nozzle.json @@ -0,0 +1,284 @@ +{ + "type": "filament", + "name": "Snapmaker PLA Full Spectrum @U1 0.4 nozzle", + "inherits": "Snapmaker PLA Basic @U1 base", + "from": "system", + "setting_id": "iybaFWrXKOsmXgMJ", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.4 nozzle)" + ], + "activate_air_filtration": [ + "0" + ], + "activate_chamber_temp_control": [ + "0" + ], + "adaptive_pressure_advance": [ + "0" + ], + "adaptive_pressure_advance_bridges": [ + "0" + ], + "adaptive_pressure_advance_model": [ + "0,0,0\n0,0,0" + ], + "adaptive_pressure_advance_overhangs": [ + "0" + ], + "additional_cooling_fan_speed": [ + "70" + ], + "chamber_temperature": [ + "0" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "compatible_printers_condition": "", + "compatible_prints": [], + "compatible_prints_condition": "", + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "60" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "dont_slow_down_outer_wall": [ + "0" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "enable_overhang_bridge_fan": [ + "1" + ], + "enable_pressure_advance": [ + "0" + ], + "eng_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "60" + ], + "fan_cooling_layer_time": [ + "100" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "filament_cooling_final_speed": [ + "3.5" + ], + "filament_cooling_initial_speed": [ + "10" + ], + "filament_cooling_moves": [ + "2" + ], + "filament_cost": [ + "25.4" + ], + "filament_density": [ + "1.32" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_diameter": [ + "1.75" + ], + "filament_end_gcode": [ + "; filament end gcode \n" + ], + "filament_flow_ratio": [ + "0.98" + ], + "filament_is_support": [ + "0" + ], + "filament_loading_speed": [ + "10" + ], + "filament_loading_speed_start": [ + "50" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_max_volumetric_speed": [ + "15" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "25" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_notes": [ + "" + ], + "filament_ramming_parameters": [ + "120 100 6.6 6.8 7.2 7.6 7.9 8.2 8.7 9.4 9.9 10.0| 0.05 6.6 0.45 6.8 0.95 7.8 1.45 8.3 1.95 9.7 2.45 10 2.95 7.6 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6" + ], + "filament_retract_before_wipe": [ + "nil" + ], + "filament_retract_length_toolchange": [ + "10" + ], + "filament_retract_lift_above": [ + "nil" + ], + "filament_retract_lift_below": [ + "nil" + ], + "filament_retract_lift_enforce": [ + "nil" + ], + "filament_retract_restart_extra": [ + "nil" + ], + "filament_retract_when_changing_layer": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_length": [ + "nil" + ], + "filament_retraction_minimum_travel": [ + "nil" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_shrink": [ + "100%" + ], + "filament_shrinkage_compensation_z": [ + "100%" + ], + "filament_soluble": [ + "0" + ], + "filament_stamping_distance": [ + "45" + ], + "filament_stamping_loading_speed": [ + "29" + ], + "filament_start_gcode": [ + "" + ], + "filament_toolchange_delay": [ + "0" + ], + "filament_type": [ + "PLA" + ], + "filament_unloading_speed": [ + "100" + ], + "filament_unloading_speed_start": [ + "100" + ], + "filament_vendor": [ + "Snapmaker" + ], + "filament_wipe": [ + "nil" + ], + "filament_wipe_distance": [ + "nil" + ], + "filament_z_hop": [ + "nil" + ], + "filament_z_hop_types": [ + "nil" + ], + "full_fan_speed_layer": [ + "0" + ], + "hot_plate_temp": [ + "65" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "idle_temperature": [ + "0" + ], + "nozzle_temperature": [ + "220" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "overhang_fan_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "50%" + ], + "pellet_flow_coefficient": [ + "0.4157" + ], + "pressure_advance": [ + "0.02" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "required_nozzle_HRC": [ + "0" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "slow_down_layer_time": [ + "4" + ], + "slow_down_min_speed": [ + "20" + ], + "support_material_interface_fan_speed": [ + "-1" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp": [ + "40" + ], + "textured_cool_plate_temp_initial_layer": [ + "40" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Glow @U1 0.4 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Glow @U1 0.4 nozzle.json new file mode 100644 index 0000000000..907781cd2c --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Glow @U1 0.4 nozzle.json @@ -0,0 +1,95 @@ +{ + "type": "filament", + "name": "Snapmaker PLA Glow @U1 0.4 nozzle", + "inherits": "Snapmaker PLA Glow @U1 base", + "from": "system", + "setting_id": "ee1YNNr8CwKetuvY", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.4 nozzle)" + ], + "additional_cooling_fan_speed": [ + "80" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "filament_cooling_final_speed": [ + "3.4" + ], + "filament_cooling_initial_speed": [ + "2.2" + ], + "filament_cooling_moves": [ + "4" + ], + "filament_cost": [ + "20" + ], + "filament_flow_ratio": [ + "0.97" + ], + "filament_loading_speed": [ + "28" + ], + "filament_loading_speed_start": [ + "3" + ], + "filament_max_volumetric_speed": [ + "16" + ], + "filament_minimal_purge_on_wipe_tower": [ + "30" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "20" + ], + "filament_multitool_ramming_volume": [ + "10" + ], + "filament_retract_length_toolchange": [ + "5" + ], + "filament_retraction_length": [ + "0.4" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_unloading_speed": [ + "90" + ], + "filament_unloading_speed_start": [ + "100" + ], + "filament_z_hop": [ + "0.2" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "overhang_fan_threshold": [ + "50%" + ], + "slow_down_layer_time": [ + "4" + ], + "slow_down_min_speed": [ + "20" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Glow @U1 base.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Glow @U1 base.json new file mode 100644 index 0000000000..ad3c1f4bbb --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Glow @U1 base.json @@ -0,0 +1,44 @@ +{ + "type": "filament", + "name": "Snapmaker PLA Glow @U1 base", + "inherits": "fdm_filament_pla", + "from": "system", + "filament_id": "SPGLOW001", + "instantiation": "false", + "filament_end_gcode": [ + "" + ], + "filament_retraction_length": [ + "nil" + ], + "filament_loading_speed_start": [ + "35" + ], + "filament_loading_speed": [ + "35" + ], + "filament_unloading_speed_start": [ + "35" + ], + "filament_unloading_speed": [ + "35" + ], + "filament_load_time": [ + "2" + ], + "filament_unload_time": [ + "2" + ], + "filament_cooling_moves": [ + "2" + ], + "filament_cooling_initial_speed": [ + "35" + ], + "filament_cooling_final_speed": [ + "60" + ], + "nozzle_temperature": [ + "220" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Matte @U1 0.2 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Matte @U1 0.2 nozzle.json new file mode 100644 index 0000000000..2a8c1e40c3 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Matte @U1 0.2 nozzle.json @@ -0,0 +1,287 @@ +{ + "type": "filament", + "name": "Snapmaker PLA Matte @U1 0.2 nozzle", + "inherits": "Snapmaker PLA Matte @U1 base2", + "from": "system", + "setting_id": "d3c48DeNk7VDeeHf", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.2 nozzle)" + ], + "activate_air_filtration": [ + "0" + ], + "activate_chamber_temp_control": [ + "0" + ], + "adaptive_pressure_advance": [ + "0" + ], + "adaptive_pressure_advance_bridges": [ + "0" + ], + "adaptive_pressure_advance_model": [ + "0,0,0\n0,0,0" + ], + "adaptive_pressure_advance_overhangs": [ + "0" + ], + "chamber_temperature": [ + "0" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "compatible_printers_condition": "", + "compatible_prints": [], + "compatible_prints_condition": "", + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "35" + ], + "cool_plate_temp_initial_layer": [ + "35" + ], + "default_filament_colour": [ + "" + ], + "dont_slow_down_outer_wall": [ + "0" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "enable_overhang_bridge_fan": [ + "1" + ], + "enable_pressure_advance": [ + "0" + ], + "eng_plate_temp": [ + "45" + ], + "eng_plate_temp_initial_layer": [ + "45" + ], + "fan_cooling_layer_time": [ + "100" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "filament_cost": [ + "24.99" + ], + "filament_density": [ + "1.26" + ], + "filament_diameter": [ + "1.75" + ], + "filament_end_gcode": [ + " " + ], + "filament_is_support": [ + "0" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_notes": [ + "" + ], + "filament_ramming_parameters": [ + "120 100 6.6 6.8 7.2 7.6 7.9 8.2 8.7 9.4 9.9 10.0| 0.05 6.6 0.45 6.8 0.95 7.8 1.45 8.3 1.95 9.7 2.45 10 2.95 7.6 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6" + ], + "filament_retract_before_wipe": [ + "nil" + ], + "filament_retract_length_toolchange": [ + "2" + ], + "filament_retract_lift_above": [ + "nil" + ], + "filament_retract_lift_below": [ + "nil" + ], + "filament_retract_lift_enforce": [ + "nil" + ], + "filament_retract_restart_extra": [ + "nil" + ], + "filament_retract_when_changing_layer": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_length": [ + "nil" + ], + "filament_retraction_minimum_travel": [ + "nil" + ], + "filament_shrink": [ + "100%" + ], + "filament_shrinkage_compensation_z": [ + "100%" + ], + "filament_soluble": [ + "0" + ], + "filament_start_gcode": [ + " " + ], + "filament_toolchange_delay": [ + "0" + ], + "filament_type": [ + "PLA" + ], + "filament_unloading_speed_start": [ + "100" + ], + "filament_vendor": [ + "Snapmaker" + ], + "filament_wipe": [ + "nil" + ], + "filament_wipe_distance": [ + "nil" + ], + "filament_z_hop": [ + "nil" + ], + "full_fan_speed_layer": [ + "0" + ], + "idle_temperature": [ + "0" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "overhang_fan_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "50%" + ], + "pellet_flow_coefficient": [ + "0.4157" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "required_nozzle_HRC": [ + "0" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "slow_down_layer_time": [ + "4" + ], + "slow_down_min_speed": [ + "20" + ], + "support_material_interface_fan_speed": [ + "-1" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp": [ + "40" + ], + "textured_cool_plate_temp_initial_layer": [ + "40" + ], + "filament_max_volumetric_speed": [ + "2" + ], + "hot_plate_temp": [ + "65" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ], + "additional_cooling_fan_speed": [ + "80" + ], + "filament_cooling_final_speed": [ + "3.5" + ], + "filament_cooling_initial_speed": [ + "10" + ], + "filament_cooling_moves": [ + "2" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_flow_ratio": [ + "1.05" + ], + "filament_loading_speed": [ + "10" + ], + "filament_loading_speed_start": [ + "50" + ], + "filament_minimal_purge_on_wipe_tower": [ + "5" + ], + "filament_multitool_ramming_flow": [ + "4" + ], + "filament_multitool_ramming_volume": [ + "4" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_stamping_distance": [ + "45" + ], + "filament_stamping_loading_speed": [ + "29" + ], + "filament_unloading_speed": [ + "100" + ], + "filament_z_hop_types": [ + "nil" + ], + "nozzle_temperature": [ + "215" + ], + "nozzle_temperature_initial_layer": [ + "215" + ], + "pressure_advance": [ + "0.025" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Matte @U1 0.6 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Matte @U1 0.6 nozzle.json new file mode 100644 index 0000000000..9de7615ae5 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Matte @U1 0.6 nozzle.json @@ -0,0 +1,287 @@ +{ + "type": "filament", + "name": "Snapmaker PLA Matte @U1 0.6 nozzle", + "inherits": "Snapmaker PLA Matte @U1 base2", + "from": "system", + "setting_id": "l3E1WaljDn2uoTqW", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.6 nozzle)" + ], + "activate_air_filtration": [ + "0" + ], + "activate_chamber_temp_control": [ + "0" + ], + "adaptive_pressure_advance": [ + "0" + ], + "adaptive_pressure_advance_bridges": [ + "0" + ], + "adaptive_pressure_advance_model": [ + "0,0,0\n0,0,0" + ], + "adaptive_pressure_advance_overhangs": [ + "0" + ], + "chamber_temperature": [ + "0" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "compatible_printers_condition": "", + "compatible_prints": [], + "compatible_prints_condition": "", + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "35" + ], + "cool_plate_temp_initial_layer": [ + "35" + ], + "default_filament_colour": [ + "" + ], + "dont_slow_down_outer_wall": [ + "0" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "enable_overhang_bridge_fan": [ + "1" + ], + "enable_pressure_advance": [ + "0" + ], + "eng_plate_temp": [ + "45" + ], + "eng_plate_temp_initial_layer": [ + "45" + ], + "fan_cooling_layer_time": [ + "100" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "filament_cost": [ + "24.99" + ], + "filament_density": [ + "1.26" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_diameter": [ + "1.75" + ], + "filament_end_gcode": [ + " " + ], + "filament_is_support": [ + "0" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_max_volumetric_speed": [ + "20" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_notes": [ + "" + ], + "filament_ramming_parameters": [ + "120 100 6.6 6.8 7.2 7.6 7.9 8.2 8.7 9.4 9.9 10.0| 0.05 6.6 0.45 6.8 0.95 7.8 1.45 8.3 1.95 9.7 2.45 10 2.95 7.6 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6" + ], + "filament_retract_before_wipe": [ + "nil" + ], + "filament_retract_length_toolchange": [ + "10" + ], + "filament_retract_lift_above": [ + "nil" + ], + "filament_retract_lift_below": [ + "nil" + ], + "filament_retract_lift_enforce": [ + "nil" + ], + "filament_retract_restart_extra": [ + "nil" + ], + "filament_retract_when_changing_layer": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_minimum_travel": [ + "nil" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_shrink": [ + "100%" + ], + "filament_shrinkage_compensation_z": [ + "100%" + ], + "filament_soluble": [ + "0" + ], + "filament_start_gcode": [ + " " + ], + "filament_toolchange_delay": [ + "0" + ], + "filament_type": [ + "PLA" + ], + "filament_unloading_speed_start": [ + "100" + ], + "filament_vendor": [ + "Snapmaker" + ], + "filament_wipe": [ + "nil" + ], + "filament_wipe_distance": [ + "nil" + ], + "filament_z_hop": [ + "nil" + ], + "filament_z_hop_types": [ + "nil" + ], + "full_fan_speed_layer": [ + "0" + ], + "idle_temperature": [ + "0" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "overhang_fan_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "50%" + ], + "pellet_flow_coefficient": [ + "0.4157" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "required_nozzle_HRC": [ + "0" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "slow_down_layer_time": [ + "4" + ], + "slow_down_min_speed": [ + "20" + ], + "support_material_interface_fan_speed": [ + "-1" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp": [ + "40" + ], + "textured_cool_plate_temp_initial_layer": [ + "40" + ], + "textured_plate_temp": [ + "45" + ], + "textured_plate_temp_initial_layer": [ + "45" + ], + "additional_cooling_fan_speed": [ + "80" + ], + "filament_cooling_final_speed": [ + "3.5" + ], + "filament_cooling_initial_speed": [ + "10" + ], + "filament_cooling_moves": [ + "2" + ], + "filament_flow_ratio": [ + "0.99" + ], + "filament_loading_speed": [ + "10" + ], + "filament_loading_speed_start": [ + "50" + ], + "filament_multitool_ramming_flow": [ + "40" + ], + "filament_multitool_ramming_volume": [ + "10" + ], + "filament_retraction_length": [ + "0.6" + ], + "filament_stamping_distance": [ + "45" + ], + "filament_stamping_loading_speed": [ + "29" + ], + "filament_unloading_speed": [ + "100" + ], + "hot_plate_temp": [ + "65" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "nozzle_temperature": [ + "215" + ], + "nozzle_temperature_initial_layer": [ + "215" + ], + "pressure_advance": [ + "0.015" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Matte @U1 0.8 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Matte @U1 0.8 nozzle.json new file mode 100644 index 0000000000..251b2a47b6 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Matte @U1 0.8 nozzle.json @@ -0,0 +1,287 @@ +{ + "type": "filament", + "name": "Snapmaker PLA Matte @U1 0.8 nozzle", + "inherits": "Snapmaker PLA Matte @U1 base2", + "from": "system", + "setting_id": "NLPd4AA0seW3NXdy", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.8 nozzle)" + ], + "activate_air_filtration": [ + "0" + ], + "activate_chamber_temp_control": [ + "0" + ], + "adaptive_pressure_advance": [ + "0" + ], + "adaptive_pressure_advance_bridges": [ + "0" + ], + "adaptive_pressure_advance_model": [ + "0,0,0\n0,0,0" + ], + "adaptive_pressure_advance_overhangs": [ + "0" + ], + "chamber_temperature": [ + "0" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "compatible_printers_condition": "", + "compatible_prints": [], + "compatible_prints_condition": "", + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "35" + ], + "cool_plate_temp_initial_layer": [ + "35" + ], + "default_filament_colour": [ + "" + ], + "dont_slow_down_outer_wall": [ + "0" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "enable_overhang_bridge_fan": [ + "1" + ], + "enable_pressure_advance": [ + "0" + ], + "eng_plate_temp": [ + "45" + ], + "eng_plate_temp_initial_layer": [ + "45" + ], + "fan_cooling_layer_time": [ + "100" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "filament_cost": [ + "24.99" + ], + "filament_density": [ + "1.26" + ], + "filament_diameter": [ + "1.75" + ], + "filament_end_gcode": [ + " " + ], + "filament_flow_ratio": [ + "0.98" + ], + "filament_is_support": [ + "0" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_max_volumetric_speed": [ + "20" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_notes": [ + "" + ], + "filament_ramming_parameters": [ + "120 100 6.6 6.8 7.2 7.6 7.9 8.2 8.7 9.4 9.9 10.0| 0.05 6.6 0.45 6.8 0.95 7.8 1.45 8.3 1.95 9.7 2.45 10 2.95 7.6 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6" + ], + "filament_retract_before_wipe": [ + "nil" + ], + "filament_retract_length_toolchange": [ + "10" + ], + "filament_retract_lift_above": [ + "nil" + ], + "filament_retract_lift_below": [ + "nil" + ], + "filament_retract_lift_enforce": [ + "nil" + ], + "filament_retract_restart_extra": [ + "nil" + ], + "filament_retract_when_changing_layer": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_minimum_travel": [ + "nil" + ], + "filament_shrink": [ + "100%" + ], + "filament_shrinkage_compensation_z": [ + "100%" + ], + "filament_soluble": [ + "0" + ], + "filament_start_gcode": [ + " " + ], + "filament_toolchange_delay": [ + "0" + ], + "filament_type": [ + "PLA" + ], + "filament_unloading_speed_start": [ + "100" + ], + "filament_vendor": [ + "Snapmaker" + ], + "filament_wipe": [ + "nil" + ], + "filament_wipe_distance": [ + "nil" + ], + "filament_z_hop": [ + "nil" + ], + "filament_z_hop_types": [ + "nil" + ], + "full_fan_speed_layer": [ + "0" + ], + "idle_temperature": [ + "0" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "overhang_fan_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "50%" + ], + "pellet_flow_coefficient": [ + "0.4157" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "required_nozzle_HRC": [ + "0" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "slow_down_layer_time": [ + "4" + ], + "slow_down_min_speed": [ + "20" + ], + "support_material_interface_fan_speed": [ + "-1" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp": [ + "40" + ], + "textured_cool_plate_temp_initial_layer": [ + "40" + ], + "nozzle_temperature": [ + "215" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ], + "additional_cooling_fan_speed": [ + "80" + ], + "filament_cooling_final_speed": [ + "3.5" + ], + "filament_cooling_initial_speed": [ + "10" + ], + "filament_cooling_moves": [ + "2" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_loading_speed": [ + "10" + ], + "filament_loading_speed_start": [ + "50" + ], + "filament_multitool_ramming_flow": [ + "40" + ], + "filament_multitool_ramming_volume": [ + "10" + ], + "filament_retraction_length": [ + "0.6" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_stamping_distance": [ + "45" + ], + "filament_stamping_loading_speed": [ + "29" + ], + "filament_unloading_speed": [ + "100" + ], + "hot_plate_temp": [ + "65" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "nozzle_temperature_initial_layer": [ + "215" + ], + "pressure_advance": [ + "0.015" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Matte @U1 base2.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Matte @U1 base2.json new file mode 100644 index 0000000000..6c0dcf26fd --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Matte @U1 base2.json @@ -0,0 +1,127 @@ +{ + "type": "filament", + "name": "Snapmaker PLA Matte @U1 base2", + "from": "system", + "filament_id": "141703112701", + "instantiation": "false", + "filament_end_gcode": [ + "" + ], + "filament_retraction_length": [ + "nil" + ], + "filament_loading_speed_start": [ + "35" + ], + "filament_loading_speed": [ + "35" + ], + "filament_unloading_speed_start": [ + "35" + ], + "filament_unloading_speed": [ + "35" + ], + "filament_load_time": [ + "2" + ], + "filament_unload_time": [ + "2" + ], + "filament_cooling_moves": [ + "2" + ], + "filament_cooling_initial_speed": [ + "35" + ], + "filament_cooling_final_speed": [ + "60" + ], + "fan_cooling_layer_time": [ + "100" + ], + "filament_max_volumetric_speed": [ + "12" + ], + "filament_type": [ + "PLA" + ], + "filament_density": [ + "1.24" + ], + "filament_cost": [ + "20" + ], + "cool_plate_temp": [ + "60" + ], + "eng_plate_temp": [ + "60" + ], + "hot_plate_temp": [ + "60" + ], + "textured_plate_temp": [ + "60" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "60" + ], + "hot_plate_temp_initial_layer": [ + "60" + ], + "textured_plate_temp_initial_layer": [ + "60" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "overhang_fan_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "50%" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "nozzle_temperature": [ + "220" + ], + "temperature_vitrification": [ + "60" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "nozzle_temperature_range_high": [ + "230" + ], + "slow_down_min_speed": [ + "10" + ], + "slow_down_layer_time": [ + "4" + ], + "additional_cooling_fan_speed": [ + "70" + ], + "filament_start_gcode": [ + "; filament start gcode\n" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Matte @U1.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Matte @U1.json index fcb1d74c92..a71c43f9b0 100644 --- a/resources/profiles/Snapmaker/filament/Snapmaker PLA Matte @U1.json +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Matte @U1.json @@ -27,7 +27,7 @@ "0" ], "additional_cooling_fan_speed": [ - "70" + "80" ], "chamber_temperature": [ "0" @@ -47,9 +47,6 @@ "cool_plate_temp_initial_layer": [ "60" ], - "default_filament_colour": [ - "" - ], "dont_slow_down_outer_wall": [ "0" ], @@ -60,7 +57,7 @@ "1" ], "enable_pressure_advance": [ - "1" + "0" ], "eng_plate_temp": [ "60" @@ -102,7 +99,7 @@ "; filament end gcode \n" ], "filament_flow_ratio": [ - "1.01" + "1" ], "filament_is_support": [ "0" @@ -117,7 +114,7 @@ "nil" ], "filament_max_volumetric_speed": [ - "20" + "22" ], "filament_minimal_purge_on_wipe_tower": [ "15" @@ -140,6 +137,9 @@ "filament_retract_before_wipe": [ "nil" ], + "filament_retract_length_toolchange": [ + "5" + ], "filament_retract_lift_above": [ "nil" ], @@ -216,19 +216,19 @@ "0" ], "hot_plate_temp": [ - "55" + "65" ], "hot_plate_temp_initial_layer": [ - "55" + "65" ], "idle_temperature": [ "0" ], "nozzle_temperature": [ - "220" + "215" ], "nozzle_temperature_initial_layer": [ - "220" + "215" ], "nozzle_temperature_range_high": [ "240" @@ -276,9 +276,9 @@ "40" ], "textured_plate_temp": [ - "60" + "65" ], "textured_plate_temp_initial_layer": [ - "60" + "65" ] } diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Metal @U1 base.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Metal @U1 base.json index a530b9208e..9fba864f40 100644 --- a/resources/profiles/Snapmaker/filament/Snapmaker PLA Metal @U1 base.json +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Metal @U1 base.json @@ -46,8 +46,5 @@ ], "nozzle_temperature": [ "220" - ], - "default_filament_colour": [ - "" ] } diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Silk @U1 0.2 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Silk @U1 0.2 nozzle.json new file mode 100644 index 0000000000..5eb860506c --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Silk @U1 0.2 nozzle.json @@ -0,0 +1,285 @@ +{ + "type": "filament", + "name": "Snapmaker PLA Silk @U1 0.2 nozzle", + "inherits": "Snapmaker PLA Basic @U1 base", + "from": "system", + "setting_id": "Q5xH6oGGlbXr14Pa", + "filament_id": "11813638720", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.2 nozzle)" + ], + "activate_air_filtration": [ + "0" + ], + "activate_chamber_temp_control": [ + "0" + ], + "adaptive_pressure_advance": [ + "0" + ], + "adaptive_pressure_advance_bridges": [ + "0" + ], + "adaptive_pressure_advance_model": [ + "0,0,0\n0,0,0" + ], + "adaptive_pressure_advance_overhangs": [ + "0" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "chamber_temperature": [ + "0" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "compatible_printers_condition": "", + "compatible_prints": [], + "compatible_prints_condition": "", + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "60" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "enable_overhang_bridge_fan": [ + "1" + ], + "enable_pressure_advance": [ + "0" + ], + "eng_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "60" + ], + "fan_cooling_layer_time": [ + "100" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "filament_cost": [ + "25.4" + ], + "filament_density": [ + "1.32" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_diameter": [ + "1.75" + ], + "filament_end_gcode": [ + "; filament end gcode \n" + ], + "filament_flow_ratio": [ + "1.02" + ], + "filament_is_support": [ + "0" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "3" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_notes": [ + "" + ], + "filament_ramming_parameters": [ + "120 100 6.6 6.8 7.2 7.6 7.9 8.2 8.7 9.4 9.9 10.0| 0.05 6.6 0.45 6.8 0.95 7.8 1.45 8.3 1.95 9.7 2.45 10 2.95 7.6 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6" + ], + "filament_retract_before_wipe": [ + "nil" + ], + "filament_retract_length_toolchange": [ + "5" + ], + "filament_retract_lift_above": [ + "nil" + ], + "filament_retract_lift_below": [ + "nil" + ], + "filament_retract_lift_enforce": [ + "nil" + ], + "filament_retract_restart_extra": [ + "nil" + ], + "filament_retract_when_changing_layer": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_minimum_travel": [ + "nil" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_shrink": [ + "100%" + ], + "filament_shrinkage_compensation_z": [ + "100%" + ], + "filament_soluble": [ + "0" + ], + "filament_start_gcode": [ + "" + ], + "filament_toolchange_delay": [ + "0" + ], + "filament_type": [ + "PLA" + ], + "filament_unloading_speed_start": [ + "100" + ], + "filament_vendor": [ + "Snapmaker" + ], + "filament_wipe": [ + "1" + ], + "filament_wipe_distance": [ + "nil" + ], + "filament_z_hop": [ + "nil" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "full_fan_speed_layer": [ + "0" + ], + "idle_temperature": [ + "0" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "overhang_fan_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "50%" + ], + "pellet_flow_coefficient": [ + "0.4157" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "required_nozzle_HRC": [ + "0" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "slow_down_layer_time": [ + "18" + ], + "slow_down_min_speed": [ + "20" + ], + "support_material_interface_fan_speed": [ + "-1" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp": [ + "40" + ], + "textured_cool_plate_temp_initial_layer": [ + "40" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ], + "dont_slow_down_outer_wall": [ + "1" + ], + "filament_cooling_final_speed": [ + "3.4" + ], + "filament_cooling_initial_speed": [ + "2.2" + ], + "filament_cooling_moves": [ + "4" + ], + "filament_loading_speed": [ + "28" + ], + "filament_loading_speed_start": [ + "3" + ], + "filament_max_volumetric_speed": [ + "2" + ], + "filament_retraction_length": [ + "0.4" + ], + "filament_stamping_distance": [ + "0" + ], + "filament_stamping_loading_speed": [ + "0" + ], + "filament_unloading_speed": [ + "90" + ], + "hot_plate_temp": [ + "65" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "nozzle_temperature": [ + "230" + ], + "nozzle_temperature_initial_layer": [ + "230" + ], + "pressure_advance": [ + "0.015" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Silk @U1 0.6 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Silk @U1 0.6 nozzle.json new file mode 100644 index 0000000000..5df6ff5a3a --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Silk @U1 0.6 nozzle.json @@ -0,0 +1,285 @@ +{ + "type": "filament", + "name": "Snapmaker PLA Silk @U1 0.6 nozzle", + "inherits": "Snapmaker PLA Basic @U1 base", + "from": "system", + "setting_id": "2pieDQoz9PiDCnU1", + "filament_id": "11813638720", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.6 nozzle)" + ], + "activate_air_filtration": [ + "0" + ], + "activate_chamber_temp_control": [ + "0" + ], + "adaptive_pressure_advance": [ + "0" + ], + "adaptive_pressure_advance_bridges": [ + "0" + ], + "adaptive_pressure_advance_model": [ + "0,0,0\n0,0,0" + ], + "adaptive_pressure_advance_overhangs": [ + "0" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "chamber_temperature": [ + "0" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "compatible_printers_condition": "", + "compatible_prints": [], + "compatible_prints_condition": "", + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "60" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "enable_overhang_bridge_fan": [ + "1" + ], + "enable_pressure_advance": [ + "0" + ], + "eng_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "60" + ], + "fan_cooling_layer_time": [ + "100" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "filament_cost": [ + "25.4" + ], + "filament_density": [ + "1.32" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_diameter": [ + "1.75" + ], + "filament_end_gcode": [ + "; filament end gcode \n" + ], + "filament_flow_ratio": [ + "0.97" + ], + "filament_is_support": [ + "0" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "40" + ], + "filament_multitool_ramming_volume": [ + "10" + ], + "filament_notes": [ + "" + ], + "filament_ramming_parameters": [ + "120 100 6.6 6.8 7.2 7.6 7.9 8.2 8.7 9.4 9.9 10.0| 0.05 6.6 0.45 6.8 0.95 7.8 1.45 8.3 1.95 9.7 2.45 10 2.95 7.6 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6" + ], + "filament_retract_before_wipe": [ + "nil" + ], + "filament_retract_length_toolchange": [ + "10" + ], + "filament_retract_lift_above": [ + "nil" + ], + "filament_retract_lift_below": [ + "nil" + ], + "filament_retract_lift_enforce": [ + "nil" + ], + "filament_retract_restart_extra": [ + "nil" + ], + "filament_retract_when_changing_layer": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_minimum_travel": [ + "nil" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_shrink": [ + "100%" + ], + "filament_shrinkage_compensation_z": [ + "100%" + ], + "filament_soluble": [ + "0" + ], + "filament_start_gcode": [ + "" + ], + "filament_toolchange_delay": [ + "0" + ], + "filament_type": [ + "PLA" + ], + "filament_unloading_speed_start": [ + "100" + ], + "filament_vendor": [ + "Snapmaker" + ], + "filament_wipe": [ + "1" + ], + "filament_wipe_distance": [ + "1" + ], + "filament_z_hop": [ + "nil" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "full_fan_speed_layer": [ + "0" + ], + "idle_temperature": [ + "0" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "overhang_fan_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "50%" + ], + "pellet_flow_coefficient": [ + "0.4157" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "required_nozzle_HRC": [ + "0" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "slow_down_layer_time": [ + "15" + ], + "slow_down_min_speed": [ + "20" + ], + "support_material_interface_fan_speed": [ + "-1" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp": [ + "40" + ], + "textured_cool_plate_temp_initial_layer": [ + "40" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ], + "dont_slow_down_outer_wall": [ + "0" + ], + "filament_cooling_final_speed": [ + "3.4" + ], + "filament_cooling_initial_speed": [ + "2.2" + ], + "filament_cooling_moves": [ + "4" + ], + "filament_loading_speed": [ + "28" + ], + "filament_loading_speed_start": [ + "3" + ], + "filament_max_volumetric_speed": [ + "10" + ], + "filament_retraction_length": [ + "0.4" + ], + "filament_stamping_distance": [ + "0" + ], + "filament_stamping_loading_speed": [ + "0" + ], + "filament_unloading_speed": [ + "90" + ], + "hot_plate_temp": [ + "65" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "nozzle_temperature": [ + "230" + ], + "nozzle_temperature_initial_layer": [ + "230" + ], + "pressure_advance": [ + "0.015" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Silk @U1 0.8 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Silk @U1 0.8 nozzle.json new file mode 100644 index 0000000000..0758372ea0 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Silk @U1 0.8 nozzle.json @@ -0,0 +1,285 @@ +{ + "type": "filament", + "name": "Snapmaker PLA Silk @U1 0.8 nozzle", + "inherits": "Snapmaker PLA Basic @U1 base", + "from": "system", + "setting_id": "f3H0JFUnds0mkKCN", + "filament_id": "11813638720", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.8 nozzle)" + ], + "activate_air_filtration": [ + "0" + ], + "activate_chamber_temp_control": [ + "0" + ], + "adaptive_pressure_advance": [ + "0" + ], + "adaptive_pressure_advance_bridges": [ + "0" + ], + "adaptive_pressure_advance_model": [ + "0,0,0\n0,0,0" + ], + "adaptive_pressure_advance_overhangs": [ + "0" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "chamber_temperature": [ + "0" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "compatible_printers_condition": "", + "compatible_prints": [], + "compatible_prints_condition": "", + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "60" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "enable_overhang_bridge_fan": [ + "1" + ], + "enable_pressure_advance": [ + "0" + ], + "eng_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "60" + ], + "fan_cooling_layer_time": [ + "100" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "filament_cost": [ + "25.4" + ], + "filament_density": [ + "1.32" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_diameter": [ + "1.75" + ], + "filament_end_gcode": [ + "; filament end gcode \n" + ], + "filament_flow_ratio": [ + "0.97" + ], + "filament_is_support": [ + "0" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "40" + ], + "filament_multitool_ramming_volume": [ + "10" + ], + "filament_notes": [ + "" + ], + "filament_ramming_parameters": [ + "120 100 6.6 6.8 7.2 7.6 7.9 8.2 8.7 9.4 9.9 10.0| 0.05 6.6 0.45 6.8 0.95 7.8 1.45 8.3 1.95 9.7 2.45 10 2.95 7.6 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6" + ], + "filament_retract_before_wipe": [ + "nil" + ], + "filament_retract_length_toolchange": [ + "10" + ], + "filament_retract_lift_above": [ + "nil" + ], + "filament_retract_lift_below": [ + "nil" + ], + "filament_retract_lift_enforce": [ + "nil" + ], + "filament_retract_restart_extra": [ + "nil" + ], + "filament_retract_when_changing_layer": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_minimum_travel": [ + "nil" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_shrink": [ + "100%" + ], + "filament_shrinkage_compensation_z": [ + "100%" + ], + "filament_soluble": [ + "0" + ], + "filament_start_gcode": [ + "" + ], + "filament_toolchange_delay": [ + "0" + ], + "filament_type": [ + "PLA" + ], + "filament_unloading_speed_start": [ + "100" + ], + "filament_vendor": [ + "Snapmaker" + ], + "filament_wipe": [ + "1" + ], + "filament_wipe_distance": [ + "1" + ], + "filament_z_hop": [ + "nil" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "full_fan_speed_layer": [ + "0" + ], + "idle_temperature": [ + "0" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "overhang_fan_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "50%" + ], + "pellet_flow_coefficient": [ + "0.4157" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "required_nozzle_HRC": [ + "0" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "slow_down_layer_time": [ + "15" + ], + "slow_down_min_speed": [ + "20" + ], + "support_material_interface_fan_speed": [ + "-1" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp": [ + "40" + ], + "textured_cool_plate_temp_initial_layer": [ + "40" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ], + "dont_slow_down_outer_wall": [ + "0" + ], + "filament_cooling_final_speed": [ + "3.4" + ], + "filament_cooling_initial_speed": [ + "2.2" + ], + "filament_cooling_moves": [ + "4" + ], + "filament_loading_speed": [ + "28" + ], + "filament_loading_speed_start": [ + "3" + ], + "filament_max_volumetric_speed": [ + "10" + ], + "filament_retraction_length": [ + "0.4" + ], + "filament_stamping_distance": [ + "0" + ], + "filament_stamping_loading_speed": [ + "0" + ], + "filament_unloading_speed": [ + "90" + ], + "hot_plate_temp": [ + "65" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "nozzle_temperature": [ + "230" + ], + "nozzle_temperature_initial_layer": [ + "230" + ], + "pressure_advance": [ + "0.015" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Silk @U1 base.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Silk @U1 base.json index 12d65834ea..227fa034dc 100644 --- a/resources/profiles/Snapmaker/filament/Snapmaker PLA Silk @U1 base.json +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Silk @U1 base.json @@ -55,8 +55,5 @@ ], "nozzle_temperature": [ "230" - ], - "default_filament_colour": [ - "" ] } diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Silk @U1.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Silk @U1.json index 7fdf44e792..c68904ffd9 100644 --- a/resources/profiles/Snapmaker/filament/Snapmaker PLA Silk @U1.json +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Silk @U1.json @@ -1,11 +1,285 @@ { "type": "filament", "name": "Snapmaker PLA Silk @U1", - "inherits": "Snapmaker PLA Silk @U1 base", + "inherits": "Snapmaker PLA Basic @U1 base", "from": "system", "setting_id": "9PkHSwtXFM0zPWth", + "filament_id": "11813638720", "instantiation": "true", "compatible_printers": [ "Snapmaker U1 (0.4 nozzle)" + ], + "activate_air_filtration": [ + "0" + ], + "activate_chamber_temp_control": [ + "0" + ], + "adaptive_pressure_advance": [ + "0" + ], + "adaptive_pressure_advance_bridges": [ + "0" + ], + "adaptive_pressure_advance_model": [ + "0,0,0\n0,0,0" + ], + "adaptive_pressure_advance_overhangs": [ + "0" + ], + "additional_cooling_fan_speed": [ + "70" + ], + "chamber_temperature": [ + "0" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "compatible_printers_condition": "", + "compatible_prints": [], + "compatible_prints_condition": "", + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "60" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "enable_overhang_bridge_fan": [ + "1" + ], + "enable_pressure_advance": [ + "0" + ], + "eng_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "60" + ], + "fan_cooling_layer_time": [ + "100" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "filament_cost": [ + "25.4" + ], + "filament_density": [ + "1.32" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_diameter": [ + "1.75" + ], + "filament_end_gcode": [ + "; filament end gcode \n" + ], + "filament_flow_ratio": [ + "0.98" + ], + "filament_is_support": [ + "0" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "25" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_notes": [ + "" + ], + "filament_ramming_parameters": [ + "120 100 6.6 6.8 7.2 7.6 7.9 8.2 8.7 9.4 9.9 10.0| 0.05 6.6 0.45 6.8 0.95 7.8 1.45 8.3 1.95 9.7 2.45 10 2.95 7.6 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6" + ], + "filament_retract_before_wipe": [ + "nil" + ], + "filament_retract_length_toolchange": [ + "5" + ], + "filament_retract_lift_above": [ + "nil" + ], + "filament_retract_lift_below": [ + "nil" + ], + "filament_retract_lift_enforce": [ + "nil" + ], + "filament_retract_restart_extra": [ + "nil" + ], + "filament_retract_when_changing_layer": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_minimum_travel": [ + "nil" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_shrink": [ + "100%" + ], + "filament_shrinkage_compensation_z": [ + "100%" + ], + "filament_soluble": [ + "0" + ], + "filament_start_gcode": [ + "" + ], + "filament_toolchange_delay": [ + "0" + ], + "filament_type": [ + "PLA" + ], + "filament_unloading_speed_start": [ + "100" + ], + "filament_vendor": [ + "Snapmaker" + ], + "filament_wipe": [ + "nil" + ], + "filament_wipe_distance": [ + "nil" + ], + "filament_z_hop": [ + "nil" + ], + "filament_z_hop_types": [ + "nil" + ], + "full_fan_speed_layer": [ + "0" + ], + "idle_temperature": [ + "0" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "overhang_fan_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "50%" + ], + "pellet_flow_coefficient": [ + "0.4157" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "required_nozzle_HRC": [ + "0" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "slow_down_layer_time": [ + "4" + ], + "slow_down_min_speed": [ + "20" + ], + "support_material_interface_fan_speed": [ + "-1" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp": [ + "40" + ], + "textured_cool_plate_temp_initial_layer": [ + "40" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ], + "dont_slow_down_outer_wall": [ + "1" + ], + "filament_cooling_final_speed": [ + "3.4" + ], + "filament_cooling_initial_speed": [ + "2.2" + ], + "filament_cooling_moves": [ + "4" + ], + "filament_loading_speed": [ + "28" + ], + "filament_loading_speed_start": [ + "3" + ], + "filament_max_volumetric_speed": [ + "10" + ], + "filament_retraction_length": [ + "0.2" + ], + "filament_stamping_distance": [ + "0" + ], + "filament_stamping_loading_speed": [ + "0" + ], + "filament_unloading_speed": [ + "90" + ], + "hot_plate_temp": [ + "65" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "nozzle_temperature": [ + "230" + ], + "nozzle_temperature_initial_layer": [ + "230" + ], + "pressure_advance": [ + "0.015" ] } diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA SnapSpeed @U1 0.2 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA SnapSpeed @U1 0.2 nozzle.json new file mode 100644 index 0000000000..7fcb20f31b --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA SnapSpeed @U1 0.2 nozzle.json @@ -0,0 +1,50 @@ +{ + "type": "filament", + "name": "Snapmaker PLA SnapSpeed @U1 0.2 nozzle", + "inherits": "Snapmaker PLA SnapSpeed @U1 base2", + "from": "system", + "setting_id": "jsAgNNiUC6DoO7rz", + "instantiation": "true", + "filament_deretraction_speed": [ + "30" + ], + "filament_flow_ratio": [ + "1.01" + ], + "filament_max_volumetric_speed": [ + "2" + ], + "filament_multitool_ramming_flow": [ + "30" + ], + "filament_retract_length_toolchange": [ + "5" + ], + "filament_retraction_speed": [ + "30" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "hot_plate_temp": [ + "65" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "pressure_advance": [ + "0.2" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ], + "compatible_printers": [ + "Snapmaker U1 (0.2 nozzle)" + ], + "enable_pressure_advance": [ + "0" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA SnapSpeed @U1 0.6 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA SnapSpeed @U1 0.6 nozzle.json new file mode 100644 index 0000000000..18fca8b4fd --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA SnapSpeed @U1 0.6 nozzle.json @@ -0,0 +1,41 @@ +{ + "type": "filament", + "name": "Snapmaker PLA SnapSpeed @U1 0.6 nozzle", + "inherits": "Snapmaker PLA SnapSpeed @U1 base2", + "from": "system", + "setting_id": "aKudBpQBj3MZZZav", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.6 nozzle)" + ], + "filament_deretraction_speed": [ + "30" + ], + "filament_flow_ratio": [ + "0.97" + ], + "filament_multitool_ramming_flow": [ + "30" + ], + "filament_retract_length_toolchange": [ + "5" + ], + "filament_retraction_length": [ + "1.6" + ], + "filament_retraction_speed": [ + "30" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ], + "enable_pressure_advance": [ + "0" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA SnapSpeed @U1 0.8 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA SnapSpeed @U1 0.8 nozzle.json new file mode 100644 index 0000000000..2ebadbd2b2 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA SnapSpeed @U1 0.8 nozzle.json @@ -0,0 +1,41 @@ +{ + "type": "filament", + "name": "Snapmaker PLA SnapSpeed @U1 0.8 nozzle", + "inherits": "Snapmaker PLA SnapSpeed @U1 base2", + "from": "system", + "setting_id": "sWoInox4hzxzIHfe", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.8 nozzle)" + ], + "filament_deretraction_speed": [ + "30" + ], + "filament_multitool_ramming_flow": [ + "30" + ], + "filament_retract_length_toolchange": [ + "5" + ], + "filament_retraction_length": [ + "1.6" + ], + "filament_retraction_speed": [ + "30" + ], + "nozzle_temperature": [ + "215" + ], + "pressure_advance": [ + "0.018" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ], + "enable_pressure_advance": [ + "0" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA SnapSpeed @U1 base2.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA SnapSpeed @U1 base2.json new file mode 100644 index 0000000000..84889e3d9b --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA SnapSpeed @U1 base2.json @@ -0,0 +1,281 @@ +{ + "type": "filament", + "name": "Snapmaker PLA SnapSpeed @U1 base2", + "from": "system", + "filament_id": "141703112701", + "instantiation": "false", + "activate_air_filtration": [ + "0" + ], + "activate_chamber_temp_control": [ + "0" + ], + "adaptive_pressure_advance": [ + "0" + ], + "adaptive_pressure_advance_bridges": [ + "0" + ], + "adaptive_pressure_advance_model": [ + "0,0,0\n0,0,0" + ], + "adaptive_pressure_advance_overhangs": [ + "0" + ], + "additional_cooling_fan_speed": [ + "70" + ], + "chamber_temperature": [ + "0" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "compatible_printers": [], + "compatible_printers_condition": "", + "compatible_prints": [], + "compatible_prints_condition": "", + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "35" + ], + "cool_plate_temp_initial_layer": [ + "35" + ], + "default_filament_colour": [ + "" + ], + "dont_slow_down_outer_wall": [ + "0" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "enable_overhang_bridge_fan": [ + "1" + ], + "enable_pressure_advance": [ + "1" + ], + "eng_plate_temp": [ + "45" + ], + "eng_plate_temp_initial_layer": [ + "45" + ], + "fan_cooling_layer_time": [ + "100" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "filament_cooling_final_speed": [ + "3.4" + ], + "filament_cooling_initial_speed": [ + "2.2" + ], + "filament_cooling_moves": [ + "4" + ], + "filament_cost": [ + "24.99" + ], + "filament_density": [ + "1.26" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_diameter": [ + "1.75" + ], + "filament_end_gcode": [ + " " + ], + "filament_flow_ratio": [ + "0.98" + ], + "filament_is_support": [ + "0" + ], + "filament_loading_speed": [ + "28" + ], + "filament_loading_speed_start": [ + "3" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_max_volumetric_speed": [ + "20" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "20" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_notes": [ + "" + ], + "filament_ramming_parameters": [ + "120 100 6.6 6.8 7.2 7.6 7.9 8.2 8.7 9.4 9.9 10.0| 0.05 6.6 0.45 6.8 0.95 7.8 1.45 8.3 1.95 9.7 2.45 10 2.95 7.6 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6" + ], + "filament_retract_before_wipe": [ + "nil" + ], + "filament_retract_lift_above": [ + "nil" + ], + "filament_retract_lift_below": [ + "nil" + ], + "filament_retract_lift_enforce": [ + "nil" + ], + "filament_retract_restart_extra": [ + "nil" + ], + "filament_retract_when_changing_layer": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_length": [ + "nil" + ], + "filament_retraction_minimum_travel": [ + "nil" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_shrink": [ + "100%" + ], + "filament_shrinkage_compensation_z": [ + "100%" + ], + "filament_soluble": [ + "0" + ], + "filament_stamping_distance": [ + "0" + ], + "filament_stamping_loading_speed": [ + "0" + ], + "filament_start_gcode": [ + " " + ], + "filament_toolchange_delay": [ + "0" + ], + "filament_type": [ + "PLA" + ], + "filament_unloading_speed": [ + "90" + ], + "filament_unloading_speed_start": [ + "100" + ], + "filament_vendor": [ + "snapmaker" + ], + "filament_wipe": [ + "nil" + ], + "filament_wipe_distance": [ + "nil" + ], + "filament_z_hop": [ + "nil" + ], + "filament_z_hop_types": [ + "nil" + ], + "full_fan_speed_layer": [ + "0" + ], + "hot_plate_temp": [ + "55" + ], + "hot_plate_temp_initial_layer": [ + "55" + ], + "idle_temperature": [ + "0" + ], + "nozzle_temperature": [ + "220" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "overhang_fan_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "50%" + ], + "pellet_flow_coefficient": [ + "0.4157" + ], + "pressure_advance": [ + "0.02" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "required_nozzle_HRC": [ + "0" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "slow_down_layer_time": [ + "4" + ], + "slow_down_min_speed": [ + "20" + ], + "support_material_interface_fan_speed": [ + "-1" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp": [ + "40" + ], + "textured_cool_plate_temp_initial_layer": [ + "40" + ], + "textured_plate_temp": [ + "45" + ], + "textured_plate_temp_initial_layer": [ + "45" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA SnapSpeed @U1.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA SnapSpeed @U1.json index 96d86d2a81..2525f5b2a9 100644 --- a/resources/profiles/Snapmaker/filament/Snapmaker PLA SnapSpeed @U1.json +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA SnapSpeed @U1.json @@ -33,13 +33,13 @@ "0" ], "close_fan_the_first_x_layers": [ - "3" + "1" ], "compatible_printers_condition": "", "compatible_prints": [], "compatible_prints_condition": "", "complete_print_exhaust_fan_speed": [ - "80" + "70" ], "cool_plate_temp": [ "60" @@ -47,20 +47,17 @@ "cool_plate_temp_initial_layer": [ "60" ], - "default_filament_colour": [ - "" - ], "dont_slow_down_outer_wall": [ "0" ], "during_print_exhaust_fan_speed": [ - "60" + "70" ], "enable_overhang_bridge_fan": [ "1" ], "enable_pressure_advance": [ - "1" + "0" ], "eng_plate_temp": [ "60" @@ -102,7 +99,7 @@ "; filament end gcode \n" ], "filament_flow_ratio": [ - "0.99" + "0.966" ], "filament_is_support": [ "0" @@ -117,7 +114,7 @@ "nil" ], "filament_max_volumetric_speed": [ - "18" + "20" ], "filament_minimal_purge_on_wipe_tower": [ "15" @@ -126,7 +123,7 @@ "1" ], "filament_multitool_ramming_flow": [ - "40" + "30" ], "filament_multitool_ramming_volume": [ "5" @@ -140,6 +137,9 @@ "filament_retract_before_wipe": [ "nil" ], + "filament_retract_length_toolchange": [ + "5" + ], "filament_retract_lift_above": [ "nil" ], @@ -159,13 +159,13 @@ "nil" ], "filament_retraction_length": [ - "0.8" + "1.2" ], "filament_retraction_minimum_travel": [ "nil" ], "filament_retraction_speed": [ - "30" + "nil" ], "filament_shrink": [ "100%" @@ -207,19 +207,19 @@ "nil" ], "filament_z_hop": [ - "nil" + "0.4" ], "filament_z_hop_types": [ - "nil" + "Slope Lift" ], "full_fan_speed_layer": [ "0" ], "hot_plate_temp": [ - "55" + "65" ], "hot_plate_temp_initial_layer": [ - "55" + "65" ], "idle_temperature": [ "0" @@ -231,7 +231,7 @@ "220" ], "nozzle_temperature_range_high": [ - "230" + "240" ], "nozzle_temperature_range_low": [ "190" @@ -246,7 +246,7 @@ "0.4157" ], "pressure_advance": [ - "0.026" + "0.02" ], "reduce_fan_stop_start_freq": [ "1" @@ -267,7 +267,7 @@ "-1" ], "temperature_vitrification": [ - "60" + "45" ], "textured_cool_plate_temp": [ "40" @@ -276,9 +276,9 @@ "40" ], "textured_plate_temp": [ - "60" + "65" ], "textured_plate_temp_initial_layer": [ - "60" + "65" ] } diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Translucent @U1 0.2 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Translucent @U1 0.2 nozzle.json new file mode 100644 index 0000000000..6271b7af42 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Translucent @U1 0.2 nozzle.json @@ -0,0 +1,284 @@ +{ + "type": "filament", + "name": "Snapmaker PLA Translucent @U1 0.2 nozzle", + "inherits": "Snapmaker PLA Translucent @U1 base", + "from": "system", + "setting_id": "zIg9XLrieYNBduyn", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.2 nozzle)" + ], + "activate_air_filtration": [ + "0" + ], + "activate_chamber_temp_control": [ + "0" + ], + "adaptive_pressure_advance": [ + "0" + ], + "adaptive_pressure_advance_bridges": [ + "0" + ], + "adaptive_pressure_advance_model": [ + "0,0,0\n0,0,0" + ], + "adaptive_pressure_advance_overhangs": [ + "0" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "chamber_temperature": [ + "0" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "compatible_printers_condition": "", + "compatible_prints": [], + "compatible_prints_condition": "", + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "60" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "dont_slow_down_outer_wall": [ + "0" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "enable_overhang_bridge_fan": [ + "1" + ], + "enable_pressure_advance": [ + "0" + ], + "eng_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "60" + ], + "fan_cooling_layer_time": [ + "100" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "filament_cooling_final_speed": [ + "3.4" + ], + "filament_cooling_initial_speed": [ + "2.2" + ], + "filament_cooling_moves": [ + "4" + ], + "filament_cost": [ + "25" + ], + "filament_density": [ + "1.22" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_diameter": [ + "1.75" + ], + "filament_end_gcode": [ + "; filament end gcode \n" + ], + "filament_flow_ratio": [ + "1.02" + ], + "filament_is_support": [ + "0" + ], + "filament_loading_speed": [ + "28" + ], + "filament_loading_speed_start": [ + "3" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_max_volumetric_speed": [ + "1.6" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "5" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_notes": [ + "" + ], + "filament_ramming_parameters": [ + "120 100 6.6 6.8 7.2 7.6 7.9 8.2 8.7 9.4 9.9 10.0| 0.05 6.6 0.45 6.8 0.95 7.8 1.45 8.3 1.95 9.7 2.45 10 2.95 7.6 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6" + ], + "filament_retract_before_wipe": [ + "0%" + ], + "filament_retract_length_toolchange": [ + "5" + ], + "filament_retract_lift_above": [ + "nil" + ], + "filament_retract_lift_below": [ + "nil" + ], + "filament_retract_lift_enforce": [ + "nil" + ], + "filament_retract_restart_extra": [ + "nil" + ], + "filament_retract_when_changing_layer": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_length": [ + "0.2" + ], + "filament_retraction_minimum_travel": [ + "nil" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_shrink": [ + "100%" + ], + "filament_shrinkage_compensation_z": [ + "100%" + ], + "filament_soluble": [ + "0" + ], + "filament_stamping_distance": [ + "0" + ], + "filament_stamping_loading_speed": [ + "0" + ], + "filament_start_gcode": [ + "; filament start gcode\n" + ], + "filament_toolchange_delay": [ + "0" + ], + "filament_type": [ + "PLA" + ], + "filament_unloading_speed": [ + "90" + ], + "filament_unloading_speed_start": [ + "100" + ], + "filament_vendor": [ + "Snapmaker" + ], + "filament_wipe": [ + "1" + ], + "filament_wipe_distance": [ + "1" + ], + "filament_z_hop": [ + "nil" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "full_fan_speed_layer": [ + "0" + ], + "hot_plate_temp": [ + "65" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "idle_temperature": [ + "0" + ], + "nozzle_temperature": [ + "220" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "200" + ], + "overhang_fan_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "50%" + ], + "pellet_flow_coefficient": [ + "0.4157" + ], + "pressure_advance": [ + "0.15" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "required_nozzle_HRC": [ + "0" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "slow_down_layer_time": [ + "10" + ], + "slow_down_min_speed": [ + "20" + ], + "support_material_interface_fan_speed": [ + "-1" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp": [ + "40" + ], + "textured_cool_plate_temp_initial_layer": [ + "40" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Translucent @U1 0.4 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Translucent @U1 0.4 nozzle.json new file mode 100644 index 0000000000..7d4270ef31 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Translucent @U1 0.4 nozzle.json @@ -0,0 +1,284 @@ +{ + "type": "filament", + "name": "Snapmaker PLA Translucent @U1 0.4 nozzle", + "inherits": "Snapmaker PLA Translucent @U1 base", + "from": "system", + "setting_id": "RqtrcLcy124ICGEc", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.4 nozzle)" + ], + "activate_air_filtration": [ + "0" + ], + "activate_chamber_temp_control": [ + "0" + ], + "adaptive_pressure_advance": [ + "0" + ], + "adaptive_pressure_advance_bridges": [ + "0" + ], + "adaptive_pressure_advance_model": [ + "0,0,0\n0,0,0" + ], + "adaptive_pressure_advance_overhangs": [ + "0" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "chamber_temperature": [ + "0" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "compatible_printers_condition": "", + "compatible_prints": [], + "compatible_prints_condition": "", + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "60" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "dont_slow_down_outer_wall": [ + "0" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "enable_overhang_bridge_fan": [ + "1" + ], + "enable_pressure_advance": [ + "1" + ], + "eng_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "60" + ], + "fan_cooling_layer_time": [ + "100" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "filament_cooling_final_speed": [ + "3.4" + ], + "filament_cooling_initial_speed": [ + "2.2" + ], + "filament_cooling_moves": [ + "4" + ], + "filament_cost": [ + "25.4" + ], + "filament_density": [ + "1.32" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_diameter": [ + "1.75" + ], + "filament_end_gcode": [ + "; filament end gcode \n" + ], + "filament_flow_ratio": [ + "0.97" + ], + "filament_is_support": [ + "0" + ], + "filament_loading_speed": [ + "8" + ], + "filament_loading_speed_start": [ + "3" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_max_volumetric_speed": [ + "12" + ], + "filament_minimal_purge_on_wipe_tower": [ + "50" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "20" + ], + "filament_multitool_ramming_volume": [ + "10" + ], + "filament_notes": [ + "" + ], + "filament_ramming_parameters": [ + "120 100 6.6 6.8 7.2 7.6 7.9 8.2 8.7 9.4 9.9 10.0| 0.05 6.6 0.45 6.8 0.95 7.8 1.45 8.3 1.95 9.7 2.45 10 2.95 7.6 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6" + ], + "filament_retract_before_wipe": [ + "nil" + ], + "filament_retract_length_toolchange": [ + "5" + ], + "filament_retract_lift_above": [ + "nil" + ], + "filament_retract_lift_below": [ + "nil" + ], + "filament_retract_lift_enforce": [ + "nil" + ], + "filament_retract_restart_extra": [ + "nil" + ], + "filament_retract_when_changing_layer": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_length": [ + "0.2" + ], + "filament_retraction_minimum_travel": [ + "nil" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_shrink": [ + "100%" + ], + "filament_shrinkage_compensation_z": [ + "100%" + ], + "filament_soluble": [ + "0" + ], + "filament_stamping_distance": [ + "0" + ], + "filament_stamping_loading_speed": [ + "0" + ], + "filament_start_gcode": [ + "; filament start gcode\n" + ], + "filament_toolchange_delay": [ + "0" + ], + "filament_type": [ + "PLA" + ], + "filament_unloading_speed": [ + "90" + ], + "filament_unloading_speed_start": [ + "100" + ], + "filament_vendor": [ + "Snapmaker" + ], + "filament_wipe": [ + "nil" + ], + "filament_wipe_distance": [ + "nil" + ], + "filament_z_hop": [ + "nil" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "full_fan_speed_layer": [ + "0" + ], + "hot_plate_temp": [ + "65" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "idle_temperature": [ + "0" + ], + "nozzle_temperature": [ + "220" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "190" + ], + "overhang_fan_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "50%" + ], + "pellet_flow_coefficient": [ + "0.4157" + ], + "pressure_advance": [ + "0.02" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "required_nozzle_HRC": [ + "0" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "slow_down_layer_time": [ + "8" + ], + "slow_down_min_speed": [ + "20" + ], + "support_material_interface_fan_speed": [ + "-1" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp": [ + "40" + ], + "textured_cool_plate_temp_initial_layer": [ + "40" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Translucent @U1 0.6 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Translucent @U1 0.6 nozzle.json new file mode 100644 index 0000000000..af3f95c757 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Translucent @U1 0.6 nozzle.json @@ -0,0 +1,284 @@ +{ + "type": "filament", + "name": "Snapmaker PLA Translucent @U1 0.6 nozzle", + "inherits": "Snapmaker PLA Translucent @U1 base", + "from": "system", + "setting_id": "OZHemzEcvzFMN8fL", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.6 nozzle)" + ], + "activate_air_filtration": [ + "0" + ], + "activate_chamber_temp_control": [ + "0" + ], + "adaptive_pressure_advance": [ + "0" + ], + "adaptive_pressure_advance_bridges": [ + "0" + ], + "adaptive_pressure_advance_model": [ + "0,0,0\n0,0,0" + ], + "adaptive_pressure_advance_overhangs": [ + "0" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "chamber_temperature": [ + "0" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "compatible_printers_condition": "", + "compatible_prints": [], + "compatible_prints_condition": "", + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "60" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "dont_slow_down_outer_wall": [ + "0" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "enable_overhang_bridge_fan": [ + "1" + ], + "enable_pressure_advance": [ + "0" + ], + "eng_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "60" + ], + "fan_cooling_layer_time": [ + "100" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "filament_cooling_final_speed": [ + "3.4" + ], + "filament_cooling_initial_speed": [ + "2.2" + ], + "filament_cooling_moves": [ + "4" + ], + "filament_cost": [ + "25" + ], + "filament_density": [ + "1.22" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_diameter": [ + "1.75" + ], + "filament_end_gcode": [ + "; filament end gcode \n" + ], + "filament_flow_ratio": [ + "0.98" + ], + "filament_is_support": [ + "0" + ], + "filament_loading_speed": [ + "28" + ], + "filament_loading_speed_start": [ + "3" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_max_volumetric_speed": [ + "12" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "20" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_notes": [ + "" + ], + "filament_ramming_parameters": [ + "120 100 6.6 6.8 7.2 7.6 7.9 8.2 8.7 9.4 9.9 10.0| 0.05 6.6 0.45 6.8 0.95 7.8 1.45 8.3 1.95 9.7 2.45 10 2.95 7.6 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6" + ], + "filament_retract_before_wipe": [ + "0%" + ], + "filament_retract_length_toolchange": [ + "10" + ], + "filament_retract_lift_above": [ + "nil" + ], + "filament_retract_lift_below": [ + "nil" + ], + "filament_retract_lift_enforce": [ + "nil" + ], + "filament_retract_restart_extra": [ + "nil" + ], + "filament_retract_when_changing_layer": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_length": [ + "0.5" + ], + "filament_retraction_minimum_travel": [ + "nil" + ], + "filament_retraction_speed": [ + "50" + ], + "filament_shrink": [ + "100%" + ], + "filament_shrinkage_compensation_z": [ + "100%" + ], + "filament_soluble": [ + "0" + ], + "filament_stamping_distance": [ + "0" + ], + "filament_stamping_loading_speed": [ + "0" + ], + "filament_start_gcode": [ + "; filament start gcode\n" + ], + "filament_toolchange_delay": [ + "0" + ], + "filament_type": [ + "PLA" + ], + "filament_unloading_speed": [ + "90" + ], + "filament_unloading_speed_start": [ + "100" + ], + "filament_vendor": [ + "Snapmaker" + ], + "filament_wipe": [ + "nil" + ], + "filament_wipe_distance": [ + "nil" + ], + "filament_z_hop": [ + "nil" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "full_fan_speed_layer": [ + "0" + ], + "hot_plate_temp": [ + "65" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "idle_temperature": [ + "0" + ], + "nozzle_temperature": [ + "220" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "200" + ], + "overhang_fan_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "50%" + ], + "pellet_flow_coefficient": [ + "0.4157" + ], + "pressure_advance": [ + "0.02" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "required_nozzle_HRC": [ + "0" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "slow_down_layer_time": [ + "8" + ], + "slow_down_min_speed": [ + "20" + ], + "support_material_interface_fan_speed": [ + "-1" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp": [ + "40" + ], + "textured_cool_plate_temp_initial_layer": [ + "40" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Translucent @U1 0.8 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Translucent @U1 0.8 nozzle.json new file mode 100644 index 0000000000..a10a4b2fb1 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Translucent @U1 0.8 nozzle.json @@ -0,0 +1,284 @@ +{ + "type": "filament", + "name": "Snapmaker PLA Translucent @U1 0.8 nozzle", + "inherits": "Snapmaker PLA Translucent @U1 base", + "from": "system", + "setting_id": "IPehCg9fKawLorB5", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.8 nozzle)" + ], + "activate_air_filtration": [ + "0" + ], + "activate_chamber_temp_control": [ + "0" + ], + "adaptive_pressure_advance": [ + "0" + ], + "adaptive_pressure_advance_bridges": [ + "0" + ], + "adaptive_pressure_advance_model": [ + "0,0,0\n0,0,0" + ], + "adaptive_pressure_advance_overhangs": [ + "0" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "chamber_temperature": [ + "0" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "compatible_printers_condition": "", + "compatible_prints": [], + "compatible_prints_condition": "", + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "60" + ], + "cool_plate_temp_initial_layer": [ + "60" + ], + "dont_slow_down_outer_wall": [ + "0" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "enable_overhang_bridge_fan": [ + "1" + ], + "enable_pressure_advance": [ + "0" + ], + "eng_plate_temp": [ + "60" + ], + "eng_plate_temp_initial_layer": [ + "60" + ], + "fan_cooling_layer_time": [ + "100" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "filament_cooling_final_speed": [ + "3.4" + ], + "filament_cooling_initial_speed": [ + "2.2" + ], + "filament_cooling_moves": [ + "4" + ], + "filament_cost": [ + "25" + ], + "filament_density": [ + "1.22" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_diameter": [ + "1.75" + ], + "filament_end_gcode": [ + "; filament end gcode \n" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_is_support": [ + "0" + ], + "filament_loading_speed": [ + "28" + ], + "filament_loading_speed_start": [ + "3" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_max_volumetric_speed": [ + "12" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "20" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_notes": [ + "" + ], + "filament_ramming_parameters": [ + "120 100 6.6 6.8 7.2 7.6 7.9 8.2 8.7 9.4 9.9 10.0| 0.05 6.6 0.45 6.8 0.95 7.8 1.45 8.3 1.95 9.7 2.45 10 2.95 7.6 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6" + ], + "filament_retract_before_wipe": [ + "0%" + ], + "filament_retract_length_toolchange": [ + "10" + ], + "filament_retract_lift_above": [ + "nil" + ], + "filament_retract_lift_below": [ + "nil" + ], + "filament_retract_lift_enforce": [ + "nil" + ], + "filament_retract_restart_extra": [ + "nil" + ], + "filament_retract_when_changing_layer": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_length": [ + "1" + ], + "filament_retraction_minimum_travel": [ + "nil" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_shrink": [ + "100%" + ], + "filament_shrinkage_compensation_z": [ + "100%" + ], + "filament_soluble": [ + "0" + ], + "filament_stamping_distance": [ + "0" + ], + "filament_stamping_loading_speed": [ + "0" + ], + "filament_start_gcode": [ + "; filament start gcode\n" + ], + "filament_toolchange_delay": [ + "0" + ], + "filament_type": [ + "PLA" + ], + "filament_unloading_speed": [ + "90" + ], + "filament_unloading_speed_start": [ + "100" + ], + "filament_vendor": [ + "Snapmaker" + ], + "filament_wipe": [ + "nil" + ], + "filament_wipe_distance": [ + "nil" + ], + "filament_z_hop": [ + "nil" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "full_fan_speed_layer": [ + "0" + ], + "hot_plate_temp": [ + "65" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "idle_temperature": [ + "0" + ], + "nozzle_temperature": [ + "220" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "nozzle_temperature_range_low": [ + "200" + ], + "overhang_fan_speed": [ + "100" + ], + "overhang_fan_threshold": [ + "50%" + ], + "pellet_flow_coefficient": [ + "0.4157" + ], + "pressure_advance": [ + "0.02" + ], + "reduce_fan_stop_start_freq": [ + "1" + ], + "required_nozzle_HRC": [ + "0" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "slow_down_layer_time": [ + "8" + ], + "slow_down_min_speed": [ + "20" + ], + "support_material_interface_fan_speed": [ + "-1" + ], + "temperature_vitrification": [ + "45" + ], + "textured_cool_plate_temp": [ + "40" + ], + "textured_cool_plate_temp_initial_layer": [ + "40" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Translucent @U1 base.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Translucent @U1 base.json new file mode 100644 index 0000000000..4f4842ae1d --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Translucent @U1 base.json @@ -0,0 +1,44 @@ +{ + "type": "filament", + "name": "Snapmaker PLA Translucent @U1 base", + "inherits": "fdm_filament_pla", + "from": "system", + "filament_id": "SPTR001", + "instantiation": "false", + "filament_end_gcode": [ + "" + ], + "filament_retraction_length": [ + "nil" + ], + "filament_loading_speed_start": [ + "35" + ], + "filament_loading_speed": [ + "35" + ], + "filament_unloading_speed_start": [ + "35" + ], + "filament_unloading_speed": [ + "35" + ], + "filament_load_time": [ + "2" + ], + "filament_unload_time": [ + "2" + ], + "filament_cooling_moves": [ + "2" + ], + "filament_cooling_initial_speed": [ + "35" + ], + "filament_cooling_final_speed": [ + "60" + ], + "nozzle_temperature": [ + "220" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Wood @U1 0.4 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Wood @U1 0.4 nozzle.json new file mode 100644 index 0000000000..4768ac2783 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Wood @U1 0.4 nozzle.json @@ -0,0 +1,108 @@ +{ + "type": "filament", + "name": "Snapmaker PLA Wood @U1 0.4 nozzle", + "inherits": "fdm_filament_pla", + "from": "system", + "setting_id": "XvizDDHbds3bVrzi", + "filament_id": "GFL9922", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.4 nozzle)" + ], + "filament_start_gcode": [ + "" + ], + "enable_pressure_advance": [ + "1" + ], + "filament_end_gcode": [ + "\n" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_vendor": [ + "Snapmaker" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ], + "hot_plate_temp": [ + "65" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "filament_flow_ratio": [ + "0.97" + ], + "filament_max_volumetric_speed": [ + "18" + ], + "filament_multitool_ramming_flow": [ + "40" + ], + "filament_multitool_ramming_volume": [ + "10" + ], + "filament_retract_length_toolchange": [ + "5" + ], + "filament_retraction_length": [ + "0.6" + ], + "pressure_advance": [ + "0.025" + ], + "slow_down_layer_time": [ + "4" + ], + "activate_air_filtration": [ + "0" + ], + "chamber_temperatures": [ + "0" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "35" + ], + "cool_plate_temp_initial_layer": [ + "35" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "filament_cost": [ + "20" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_speed": [ + "nil" + ], + "nozzle_temperature": [ + "220" + ], + "overhang_fan_threshold": [ + "50%" + ], + "slow_down_min_speed": [ + "20" + ], + "temperature_vitrification": [ + "45" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Wood @U1 0.6 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Wood @U1 0.6 nozzle.json new file mode 100644 index 0000000000..43d306ebae --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Wood @U1 0.6 nozzle.json @@ -0,0 +1,114 @@ +{ + "type": "filament", + "name": "Snapmaker PLA Wood @U1 0.6 nozzle", + "inherits": "fdm_filament_pla", + "from": "system", + "setting_id": "Kp77DCcitdmoyjqm", + "filament_id": "GFL9922", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.6 nozzle)" + ], + "filament_start_gcode": [ + "" + ], + "enable_pressure_advance": [ + "0" + ], + "filament_end_gcode": [ + "\n" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_vendor": [ + "Snapmaker" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ], + "hot_plate_temp": [ + "65" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "filament_flow_ratio": [ + "0.98" + ], + "filament_max_volumetric_speed": [ + "18" + ], + "filament_multitool_ramming_flow": [ + "40" + ], + "filament_multitool_ramming_volume": [ + "10" + ], + "filament_retract_length_toolchange": [ + "5" + ], + "filament_retraction_length": [ + "0.4" + ], + "pressure_advance": [ + "0.014" + ], + "slow_down_layer_time": [ + "4" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "activate_air_filtration": [ + "0" + ], + "chamber_temperatures": [ + "0" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "35" + ], + "cool_plate_temp_initial_layer": [ + "35" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "filament_cost": [ + "20" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_speed": [ + "nil" + ], + "nozzle_temperature": [ + "220" + ], + "overhang_fan_threshold": [ + "50%" + ], + "slow_down_min_speed": [ + "20" + ], + "temperature_vitrification": [ + "45" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA Wood @U1 0.8 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA Wood @U1 0.8 nozzle.json new file mode 100644 index 0000000000..da788a35fb --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA Wood @U1 0.8 nozzle.json @@ -0,0 +1,114 @@ +{ + "type": "filament", + "name": "Snapmaker PLA Wood @U1 0.8 nozzle", + "inherits": "fdm_filament_pla", + "from": "system", + "setting_id": "nBYnNiqwLo6nAsqi", + "filament_id": "GFL9922", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.8 nozzle)" + ], + "filament_start_gcode": [ + "" + ], + "enable_pressure_advance": [ + "0" + ], + "filament_end_gcode": [ + "\n" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_vendor": [ + "Snapmaker" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ], + "hot_plate_temp": [ + "65" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "filament_flow_ratio": [ + "0.965" + ], + "filament_max_volumetric_speed": [ + "20" + ], + "filament_multitool_ramming_flow": [ + "40" + ], + "filament_multitool_ramming_volume": [ + "10" + ], + "filament_retract_length_toolchange": [ + "5" + ], + "filament_retraction_length": [ + "0.4" + ], + "pressure_advance": [ + "0.007" + ], + "slow_down_layer_time": [ + "4" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "activate_air_filtration": [ + "0" + ], + "chamber_temperatures": [ + "0" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "35" + ], + "cool_plate_temp_initial_layer": [ + "35" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "filament_cost": [ + "20" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_speed": [ + "nil" + ], + "nozzle_temperature": [ + "220" + ], + "overhang_fan_threshold": [ + "50%" + ], + "slow_down_min_speed": [ + "20" + ], + "temperature_vitrification": [ + "45" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA-CF @U1 0.4 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA-CF @U1 0.4 nozzle.json new file mode 100644 index 0000000000..b986a7244a --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA-CF @U1 0.4 nozzle.json @@ -0,0 +1,132 @@ +{ + "type": "filament", + "name": "Snapmaker PLA-CF @U1 0.4 nozzle", + "inherits": "fdm_filament_pla", + "from": "system", + "setting_id": "2INwruC3ZVkBTdUT", + "filament_id": "GFL98111", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.4 nozzle)" + ], + "enable_pressure_advance": [ + "1" + ], + "filament_end_gcode": [ + "" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "20" + ], + "filament_start_gcode": [ + "" + ], + "filament_vendor": [ + "Snapmaker" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ], + "hot_plate_temp": [ + "65" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "filament_flow_ratio": [ + "0.98" + ], + "filament_max_volumetric_speed": [ + "15" + ], + "filament_minimal_purge_on_wipe_tower": [ + "50" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_retract_length_toolchange": [ + "5" + ], + "filament_retraction_length": [ + "1" + ], + "filament_type": [ + "PLA-CF" + ], + "filament_z_hop": [ + "0.4" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "nozzle_temperature": [ + "240" + ], + "nozzle_temperature_initial_layer": [ + "240" + ], + "nozzle_temperature_range_high": [ + "250" + ], + "nozzle_temperature_range_low": [ + "200" + ], + "pressure_advance": [ + "0.01" + ], + "activate_air_filtration": [ + "0" + ], + "chamber_temperatures": [ + "0" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "45" + ], + "cool_plate_temp_initial_layer": [ + "45" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "filament_cost": [ + "20" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_speed": [ + "nil" + ], + "overhang_fan_threshold": [ + "50%" + ], + "required_nozzle_HRC": [ + "40" + ], + "slow_down_layer_time": [ + "7" + ], + "slow_down_min_speed": [ + "20" + ], + "temperature_vitrification": [ + "45" + ], + "additional_cooling_fan_speed": [ + "0" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA-CF @U1 0.6 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA-CF @U1 0.6 nozzle.json new file mode 100644 index 0000000000..dcb6392634 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA-CF @U1 0.6 nozzle.json @@ -0,0 +1,138 @@ +{ + "type": "filament", + "name": "Snapmaker PLA-CF @U1 0.6 nozzle", + "inherits": "fdm_filament_pla", + "from": "system", + "setting_id": "CKfdL85SvbOLzMuP", + "filament_id": "GFL98111", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.6 nozzle)" + ], + "enable_pressure_advance": [ + "0" + ], + "filament_end_gcode": [ + "" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "25" + ], + "filament_start_gcode": [ + "" + ], + "filament_vendor": [ + "Snapmaker" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ], + "hot_plate_temp": [ + "65" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "filament_flow_ratio": [ + "0.98" + ], + "filament_max_volumetric_speed": [ + "18" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_retract_length_toolchange": [ + "10" + ], + "filament_retraction_length": [ + "2" + ], + "filament_type": [ + "PLA-CF" + ], + "filament_z_hop": [ + "nil" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "nozzle_temperature": [ + "220" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "nozzle_temperature_range_high": [ + "250" + ], + "nozzle_temperature_range_low": [ + "210" + ], + "pressure_advance": [ + "0.015" + ], + "filament_cost": [ + "35" + ], + "filament_density": [ + "1.22" + ], + "slow_down_layer_time": [ + "8" + ], + "filament_wipe_distance": [ + "2" + ], + "activate_air_filtration": [ + "0" + ], + "chamber_temperatures": [ + "0" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "45" + ], + "cool_plate_temp_initial_layer": [ + "45" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_speed": [ + "nil" + ], + "overhang_fan_threshold": [ + "50%" + ], + "required_nozzle_HRC": [ + "40" + ], + "slow_down_min_speed": [ + "20" + ], + "temperature_vitrification": [ + "45" + ], + "additional_cooling_fan_speed": [ + "0" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA-CF @U1 0.8 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA-CF @U1 0.8 nozzle.json new file mode 100644 index 0000000000..1e6e961497 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA-CF @U1 0.8 nozzle.json @@ -0,0 +1,135 @@ +{ + "type": "filament", + "name": "Snapmaker PLA-CF @U1 0.8 nozzle", + "inherits": "fdm_filament_pla", + "from": "system", + "setting_id": "fhhnfJm4dN9FXpWQ", + "filament_id": "GFL98111", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.8 nozzle)" + ], + "enable_pressure_advance": [ + "0" + ], + "filament_end_gcode": [ + "" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "25" + ], + "filament_start_gcode": [ + "" + ], + "filament_vendor": [ + "Snapmaker" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ], + "hot_plate_temp": [ + "65" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "filament_flow_ratio": [ + "0.98" + ], + "filament_max_volumetric_speed": [ + "18" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_retract_length_toolchange": [ + "10" + ], + "filament_retraction_length": [ + "3" + ], + "filament_type": [ + "PLA-CF" + ], + "filament_z_hop": [ + "nil" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "nozzle_temperature": [ + "220" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "nozzle_temperature_range_high": [ + "250" + ], + "nozzle_temperature_range_low": [ + "210" + ], + "pressure_advance": [ + "0.015" + ], + "filament_cost": [ + "35" + ], + "filament_density": [ + "1.22" + ], + "slow_down_layer_time": [ + "8" + ], + "activate_air_filtration": [ + "0" + ], + "chamber_temperatures": [ + "0" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "45" + ], + "cool_plate_temp_initial_layer": [ + "45" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_retraction_speed": [ + "nil" + ], + "overhang_fan_threshold": [ + "50%" + ], + "required_nozzle_HRC": [ + "40" + ], + "slow_down_min_speed": [ + "20" + ], + "temperature_vitrification": [ + "45" + ], + "additional_cooling_fan_speed": [ + "0" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA-CF @U1 base.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA-CF @U1 base.json index 617a8c23ea..63f877d965 100644 --- a/resources/profiles/Snapmaker/filament/Snapmaker PLA-CF @U1 base.json +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA-CF @U1 base.json @@ -59,9 +59,6 @@ "temperature_vitrification": [ "150" ], - "default_filament_colour": [ - "" - ], "filament_type": [ "PLA-CF" ], diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PLA-CF @U1.json b/resources/profiles/Snapmaker/filament/Snapmaker PLA-CF @U1.json index ec04c8108d..9091258467 100644 --- a/resources/profiles/Snapmaker/filament/Snapmaker PLA-CF @U1.json +++ b/resources/profiles/Snapmaker/filament/Snapmaker PLA-CF @U1.json @@ -7,5 +7,8 @@ "instantiation": "true", "compatible_printers": [ "Snapmaker U1 (0.4 nozzle)" + ], + "filament_retract_length_toolchange": [ + "5" ] } diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PVA @U1 0.6 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PVA @U1 0.6 nozzle.json new file mode 100644 index 0000000000..b28e0627ad --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PVA @U1 0.6 nozzle.json @@ -0,0 +1,113 @@ +{ + "type": "filament", + "name": "Snapmaker PVA @U1 0.6 nozzle", + "inherits": "Snapmaker PVA @U1 base", + "from": "system", + "setting_id": "lIqgnfeZdAm8G0TH", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.6 nozzle)" + ], + "enable_pressure_advance": [ + "0" + ], + "additional_cooling_fan_speed": [ + "70" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "filament_cost": [ + "79.98" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_flow_ratio": [ + "0.98" + ], + "filament_retract_length_toolchange": [ + "10" + ], + "filament_retraction_length": [ + "nil" + ], + "filament_retraction_minimum_travel": [ + "1" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_soluble": [ + "1" + ], + "filament_wipe_distance": [ + "2" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "nozzle_temperature": [ + "230" + ], + "nozzle_temperature_initial_layer": [ + "230" + ], + "nozzle_temperature_range_low": [ + "210" + ], + "pressure_advance": [ + "0.03" + ], + "slow_down_layer_time": [ + "7" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ], + "filament_cooling_final_speed": [ + "3.4" + ], + "filament_cooling_initial_speed": [ + "2.2" + ], + "filament_cooling_moves": [ + "4" + ], + "filament_loading_speed": [ + "28" + ], + "filament_loading_speed_start": [ + "3" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "20" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_unloading_speed": [ + "90" + ], + "filament_unloading_speed_start": [ + "100" + ], + "fan_min_speed": [ + "100" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PVA @U1 0.8 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker PVA @U1 0.8 nozzle.json new file mode 100644 index 0000000000..6572a60078 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker PVA @U1 0.8 nozzle.json @@ -0,0 +1,107 @@ +{ + "type": "filament", + "name": "Snapmaker PVA @U1 0.8 nozzle", + "inherits": "Snapmaker PVA @U1 base", + "from": "system", + "setting_id": "WxtuvwWxy73Gi0IK", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.8 nozzle)" + ], + "enable_pressure_advance": [ + "0" + ], + "additional_cooling_fan_speed": [ + "70" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "fan_min_speed": [ + "100" + ], + "filament_cost": [ + "79.98" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_flow_ratio": [ + "0.96" + ], + "filament_retract_length_toolchange": [ + "10" + ], + "filament_retraction_length": [ + "nil" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_soluble": [ + "1" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "nozzle_temperature": [ + "225" + ], + "nozzle_temperature_initial_layer": [ + "225" + ], + "nozzle_temperature_range_low": [ + "205" + ], + "pressure_advance": [ + "0.03" + ], + "slow_down_layer_time": [ + "7" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" + ], + "filament_cooling_final_speed": [ + "3.4" + ], + "filament_cooling_initial_speed": [ + "2.2" + ], + "filament_cooling_moves": [ + "4" + ], + "filament_loading_speed": [ + "28" + ], + "filament_loading_speed_start": [ + "3" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_flow": [ + "20" + ], + "filament_multitool_ramming_volume": [ + "5" + ], + "filament_unloading_speed": [ + "90" + ], + "filament_unloading_speed_start": [ + "100" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker PVA @U1.json b/resources/profiles/Snapmaker/filament/Snapmaker PVA @U1.json index f9a33d7bcb..5ed5d243ac 100644 --- a/resources/profiles/Snapmaker/filament/Snapmaker PVA @U1.json +++ b/resources/profiles/Snapmaker/filament/Snapmaker PVA @U1.json @@ -7,5 +7,95 @@ "instantiation": "true", "compatible_printers": [ "Snapmaker U1 (0.4 nozzle)" + ], + "enable_pressure_advance": [ + "1" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "pressure_advance": [ + "0.03" + ], + "additional_cooling_fan_speed": [ + "70" + ], + "close_fan_the_first_x_layers": [ + "1" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "fan_min_speed": [ + "100" + ], + "filament_cooling_final_speed": [ + "3.4" + ], + "filament_cooling_initial_speed": [ + "2.2" + ], + "filament_cooling_moves": [ + "4" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_loading_speed": [ + "28" + ], + "filament_loading_speed_start": [ + "3" + ], + "filament_max_volumetric_speed": [ + "5" + ], + "filament_minimal_purge_on_wipe_tower": [ + "20" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_volume": [ + "0.1" + ], + "filament_retract_length_toolchange": [ + "4" + ], + "filament_retraction_length": [ + "0.4" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_unloading_speed": [ + "90" + ], + "filament_unloading_speed_start": [ + "100" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "hot_plate_temp_initial_layer": [ + "65" + ], + "nozzle_temperature": [ + "240" + ], + "nozzle_temperature_initial_layer": [ + "240" + ], + "slow_down_layer_time": [ + "10" + ], + "textured_plate_temp": [ + "65" + ], + "textured_plate_temp_initial_layer": [ + "65" ] } diff --git a/resources/profiles/Snapmaker/filament/Snapmaker TPU 90A @U1 0.6 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker TPU 90A @U1 0.6 nozzle.json new file mode 100644 index 0000000000..6ae8a5b675 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker TPU 90A @U1 0.6 nozzle.json @@ -0,0 +1,150 @@ +{ + "type": "filament", + "name": "Snapmaker TPU 90A @U1 0.6 nozzle", + "inherits": "fdm_filament_tpu", + "from": "system", + "setting_id": "uQCplebxmpBpzsnk", + "filament_id": "GFU99", + "instantiation": "true", + "filament_max_volumetric_speed": [ + "3.2" + ], + "compatible_printers": [ + "Snapmaker U1 (0.6 nozzle)" + ], + "filament_start_gcode": [ + "; filament start gcode\n" + ], + "filament_end_gcode": [ + "; filament end gcode \n\n" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_volume": [ + "0.1" + ], + "pressure_advance": [ + "0.02" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "enable_pressure_advance": [ + "0" + ], + "filament_deretraction_speed": [ + "20" + ], + "filament_flow_ratio": [ + "1.093" + ], + "filament_multitool_ramming_flow": [ + "5" + ], + "filament_retract_length_toolchange": [ + "4" + ], + "filament_retraction_length": [ + "0.6" + ], + "filament_retraction_speed": [ + "20" + ], + "filament_vendor": [ + "Snapmaker" + ], + "nozzle_temperature": [ + "220" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "slow_down_layer_time": [ + "14" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "filament_wipe": [ + "1" + ], + "filament_wipe_distance": [ + "0" + ], + "activate_air_filtration": [ + "0" + ], + "chamber_temperatures": [ + "0" + ], + "cool_plate_temp": [ + "30" + ], + "cool_plate_temp_initial_layer": [ + "30" + ], + "eng_plate_temp": [ + "30" + ], + "eng_plate_temp_initial_layer": [ + "30" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "filament_cost": [ + "20" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_retract_when_changing_layer": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_z_hop": [ + "nil" + ], + "hot_plate_temp": [ + "35" + ], + "hot_plate_temp_initial_layer": [ + "35" + ], + "overhang_fan_speed": [ + "100" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "temperature_vitrification": [ + "30" + ], + "textured_plate_temp": [ + "35" + ], + "textured_plate_temp_initial_layer": [ + "35" + ], + "nozzle_temperature_range_low": [ + "200" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker TPU 90A @U1 0.8 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker TPU 90A @U1 0.8 nozzle.json new file mode 100644 index 0000000000..45f0f7a0e9 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker TPU 90A @U1 0.8 nozzle.json @@ -0,0 +1,144 @@ +{ + "type": "filament", + "name": "Snapmaker TPU 90A @U1 0.8 nozzle", + "inherits": "fdm_filament_tpu", + "from": "system", + "setting_id": "w4Hpl2qNPmgNvaeM", + "filament_id": "GFU99", + "instantiation": "true", + "filament_max_volumetric_speed": [ + "3.5" + ], + "compatible_printers": [ + "Snapmaker U1 (0.8 nozzle)" + ], + "filament_start_gcode": [ + "; filament start gcode\n" + ], + "filament_end_gcode": [ + "; filament end gcode \n\n" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_volume": [ + "0.1" + ], + "pressure_advance": [ + "0.02" + ], + "additional_cooling_fan_speed": [ + "70" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "enable_pressure_advance": [ + "0" + ], + "filament_deretraction_speed": [ + "20" + ], + "filament_flow_ratio": [ + "1.095" + ], + "filament_multitool_ramming_flow": [ + "5" + ], + "filament_retract_length_toolchange": [ + "4" + ], + "filament_retraction_length": [ + "1.2" + ], + "filament_retraction_speed": [ + "20" + ], + "filament_vendor": [ + "Snapmaker" + ], + "nozzle_temperature": [ + "220" + ], + "nozzle_temperature_initial_layer": [ + "220" + ], + "slow_down_layer_time": [ + "14" + ], + "nozzle_temperature_range_high": [ + "240" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "activate_air_filtration": [ + "0" + ], + "chamber_temperatures": [ + "0" + ], + "cool_plate_temp": [ + "30" + ], + "cool_plate_temp_initial_layer": [ + "30" + ], + "eng_plate_temp": [ + "30" + ], + "eng_plate_temp_initial_layer": [ + "30" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "filament_cost": [ + "20" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_retract_when_changing_layer": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_z_hop": [ + "nil" + ], + "hot_plate_temp": [ + "35" + ], + "hot_plate_temp_initial_layer": [ + "35" + ], + "overhang_fan_speed": [ + "100" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "temperature_vitrification": [ + "30" + ], + "textured_plate_temp": [ + "35" + ], + "textured_plate_temp_initial_layer": [ + "35" + ], + "nozzle_temperature_range_low": [ + "200" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker TPU 90A @U1.json b/resources/profiles/Snapmaker/filament/Snapmaker TPU 90A @U1.json new file mode 100644 index 0000000000..e89397849b --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker TPU 90A @U1.json @@ -0,0 +1,141 @@ +{ + "type": "filament", + "name": "Snapmaker TPU 90A @U1", + "inherits": "fdm_filament_tpu", + "from": "system", + "setting_id": "T4MmgICRrSzvmCZ5", + "filament_id": "GFU99", + "instantiation": "true", + "filament_max_volumetric_speed": [ + "3.2" + ], + "compatible_printers": [ + "Snapmaker U1 (0.4 nozzle)" + ], + "filament_start_gcode": [ + "; filament start gcode\n" + ], + "filament_end_gcode": [ + "; filament end gcode \n\n" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_volume": [ + "0.1" + ], + "pressure_advance": [ + "0.4" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "complete_print_exhaust_fan_speed": [ + "100" + ], + "during_print_exhaust_fan_speed": [ + "100" + ], + "enable_pressure_advance": [ + "0" + ], + "filament_deretraction_speed": [ + "15" + ], + "filament_flow_ratio": [ + "1.045" + ], + "filament_multitool_ramming_flow": [ + "5" + ], + "filament_retract_length_toolchange": [ + "2" + ], + "filament_retraction_length": [ + "1.2" + ], + "filament_retraction_speed": [ + "15" + ], + "filament_vendor": [ + "Snapmaker" + ], + "nozzle_temperature": [ + "230" + ], + "nozzle_temperature_initial_layer": [ + "230" + ], + "slow_down_layer_time": [ + "14" + ], + "activate_air_filtration": [ + "0" + ], + "chamber_temperatures": [ + "0" + ], + "cool_plate_temp": [ + "30" + ], + "cool_plate_temp_initial_layer": [ + "30" + ], + "eng_plate_temp": [ + "30" + ], + "eng_plate_temp_initial_layer": [ + "30" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "filament_cost": [ + "20" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_retract_when_changing_layer": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_z_hop": [ + "nil" + ], + "filament_z_hop_types": [ + "nil" + ], + "hot_plate_temp": [ + "35" + ], + "hot_plate_temp_initial_layer": [ + "35" + ], + "overhang_fan_speed": [ + "100" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "temperature_vitrification": [ + "30" + ], + "textured_plate_temp": [ + "35" + ], + "textured_plate_temp_initial_layer": [ + "35" + ], + "nozzle_temperature_range_low": [ + "200" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker TPU 95A @U1 base.json b/resources/profiles/Snapmaker/filament/Snapmaker TPU 95A @U1 base.json index a6d8c41b5d..3c51439206 100644 --- a/resources/profiles/Snapmaker/filament/Snapmaker TPU 95A @U1 base.json +++ b/resources/profiles/Snapmaker/filament/Snapmaker TPU 95A @U1 base.json @@ -43,9 +43,6 @@ "filament_retraction_speed": [ "nil" ], - "filament_settings_id": [ - "" - ], "filament_soluble": [ "0" ], diff --git a/resources/profiles/Snapmaker/filament/Snapmaker TPU 95A HF @U1 0.6 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker TPU 95A HF @U1 0.6 nozzle.json new file mode 100644 index 0000000000..1085c84e6f --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker TPU 95A HF @U1 0.6 nozzle.json @@ -0,0 +1,147 @@ +{ + "type": "filament", + "name": "Snapmaker TPU 95A HF @U1 0.6 nozzle", + "inherits": "fdm_filament_tpu", + "from": "system", + "setting_id": "Q2svXpkEqg0dCdq3", + "filament_id": "GFU99", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.6 nozzle)" + ], + "filament_start_gcode": [ + "; filament start gcode\n" + ], + "filament_end_gcode": [ + "; filament end gcode \n\n" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_volume": [ + "0.1" + ], + "enable_pressure_advance": [ + "0" + ], + "fan_cooling_layer_time": [ + "100" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_flow_ratio": [ + "1.067" + ], + "filament_max_volumetric_speed": [ + "12" + ], + "filament_multitool_ramming_flow": [ + "10" + ], + "filament_retract_length_toolchange": [ + "8" + ], + "filament_retraction_length": [ + "0.8" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_vendor": [ + "Snapmaker" + ], + "nozzle_temperature": [ + "215" + ], + "nozzle_temperature_initial_layer": [ + "215" + ], + "overhang_fan_threshold": [ + "95%" + ], + "pressure_advance": [ + "0.14" + ], + "slow_down_layer_time": [ + "8" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "activate_air_filtration": [ + "0" + ], + "chamber_temperatures": [ + "0" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "30" + ], + "cool_plate_temp_initial_layer": [ + "30" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "eng_plate_temp": [ + "30" + ], + "eng_plate_temp_initial_layer": [ + "30" + ], + "filament_cost": [ + "20" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_retract_when_changing_layer": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_z_hop": [ + "nil" + ], + "hot_plate_temp": [ + "35" + ], + "hot_plate_temp_initial_layer": [ + "35" + ], + "overhang_fan_speed": [ + "100" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "temperature_vitrification": [ + "30" + ], + "textured_plate_temp": [ + "35" + ], + "textured_plate_temp_initial_layer": [ + "35" + ], + "nozzle_temperature_range_low": [ + "200" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker TPU 95A HF @U1 0.8 nozzle.json b/resources/profiles/Snapmaker/filament/Snapmaker TPU 95A HF @U1 0.8 nozzle.json new file mode 100644 index 0000000000..bef4c7712f --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker TPU 95A HF @U1 0.8 nozzle.json @@ -0,0 +1,147 @@ +{ + "type": "filament", + "name": "Snapmaker TPU 95A HF @U1 0.8 nozzle", + "inherits": "fdm_filament_tpu", + "from": "system", + "setting_id": "RjsE08h3l37cAkAs", + "filament_id": "GFU99", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.8 nozzle)" + ], + "filament_start_gcode": [ + "; filament start gcode\n" + ], + "filament_end_gcode": [ + "; filament end gcode \n\n" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_volume": [ + "0.1" + ], + "enable_pressure_advance": [ + "0" + ], + "fan_cooling_layer_time": [ + "100" + ], + "fan_max_speed": [ + "100" + ], + "fan_min_speed": [ + "100" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_flow_ratio": [ + "1.045" + ], + "filament_max_volumetric_speed": [ + "16" + ], + "filament_multitool_ramming_flow": [ + "15" + ], + "filament_retract_length_toolchange": [ + "8" + ], + "filament_retraction_length": [ + "1" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_vendor": [ + "Snapmaker" + ], + "nozzle_temperature": [ + "215" + ], + "nozzle_temperature_initial_layer": [ + "215" + ], + "overhang_fan_threshold": [ + "95%" + ], + "pressure_advance": [ + "0.12" + ], + "slow_down_layer_time": [ + "12" + ], + "additional_cooling_fan_speed": [ + "100" + ], + "filament_z_hop_types": [ + "Slope Lift" + ], + "activate_air_filtration": [ + "0" + ], + "chamber_temperatures": [ + "0" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "30" + ], + "cool_plate_temp_initial_layer": [ + "30" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "eng_plate_temp": [ + "30" + ], + "eng_plate_temp_initial_layer": [ + "30" + ], + "filament_cost": [ + "20" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_retract_when_changing_layer": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_z_hop": [ + "nil" + ], + "hot_plate_temp": [ + "35" + ], + "hot_plate_temp_initial_layer": [ + "35" + ], + "overhang_fan_speed": [ + "100" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "temperature_vitrification": [ + "30" + ], + "textured_plate_temp": [ + "35" + ], + "textured_plate_temp_initial_layer": [ + "35" + ], + "nozzle_temperature_range_low": [ + "200" + ] +} diff --git a/resources/profiles/Snapmaker/filament/Snapmaker TPU 95A HF @U1.json b/resources/profiles/Snapmaker/filament/Snapmaker TPU 95A HF @U1.json new file mode 100644 index 0000000000..5e0696d906 --- /dev/null +++ b/resources/profiles/Snapmaker/filament/Snapmaker TPU 95A HF @U1.json @@ -0,0 +1,144 @@ +{ + "type": "filament", + "name": "Snapmaker TPU 95A HF @U1", + "inherits": "fdm_filament_tpu", + "from": "system", + "setting_id": "KwcwmV2UmZUgCBoz", + "filament_id": "GFU99", + "instantiation": "true", + "compatible_printers": [ + "Snapmaker U1 (0.4 nozzle)" + ], + "filament_start_gcode": [ + "; filament start gcode\n" + ], + "filament_end_gcode": [ + "; filament end gcode \n\n" + ], + "filament_multitool_ramming": [ + "1" + ], + "filament_multitool_ramming_volume": [ + "0.1" + ], + "enable_pressure_advance": [ + "0" + ], + "fan_cooling_layer_time": [ + "40" + ], + "fan_max_speed": [ + "50" + ], + "fan_min_speed": [ + "10" + ], + "filament_deretraction_speed": [ + "nil" + ], + "filament_flow_ratio": [ + "1.067" + ], + "filament_max_volumetric_speed": [ + "9" + ], + "filament_multitool_ramming_flow": [ + "10" + ], + "filament_retract_length_toolchange": [ + "6" + ], + "filament_retraction_length": [ + "1" + ], + "filament_retraction_speed": [ + "nil" + ], + "filament_vendor": [ + "Snapmaker" + ], + "nozzle_temperature": [ + "230" + ], + "nozzle_temperature_initial_layer": [ + "230" + ], + "overhang_fan_threshold": [ + "25%" + ], + "pressure_advance": [ + "0.23" + ], + "slow_down_layer_time": [ + "10" + ], + "activate_air_filtration": [ + "0" + ], + "chamber_temperatures": [ + "0" + ], + "complete_print_exhaust_fan_speed": [ + "70" + ], + "cool_plate_temp": [ + "30" + ], + "cool_plate_temp_initial_layer": [ + "30" + ], + "during_print_exhaust_fan_speed": [ + "70" + ], + "eng_plate_temp": [ + "30" + ], + "eng_plate_temp_initial_layer": [ + "30" + ], + "filament_cost": [ + "20" + ], + "filament_long_retractions_when_cut": [ + "nil" + ], + "filament_minimal_purge_on_wipe_tower": [ + "15" + ], + "filament_retract_when_changing_layer": [ + "nil" + ], + "filament_retraction_distances_when_cut": [ + "nil" + ], + "filament_z_hop": [ + "nil" + ], + "filament_z_hop_types": [ + "nil" + ], + "hot_plate_temp": [ + "35" + ], + "hot_plate_temp_initial_layer": [ + "35" + ], + "overhang_fan_speed": [ + "100" + ], + "slow_down_for_layer_cooling": [ + "1" + ], + "temperature_vitrification": [ + "30" + ], + "textured_plate_temp": [ + "35" + ], + "textured_plate_temp_initial_layer": [ + "35" + ], + "nozzle_temperature_range_low": [ + "200" + ] +} diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json index 8a5cbc56d8..183e125c73 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.2 nozzle).json @@ -1,13 +1,54 @@ { "type": "machine", - "setting_id": "CwJeuh1rxZcjvkXh", "name": "Snapmaker U1 (0.2 nozzle)", - "from": "system", - "instantiation": "true", "inherits": "fdm_U1", + "from": "system", + "setting_id": "CwJeuh1rxZcjvkXh", + "instantiation": "true", "printer_model": "Snapmaker U1", "printer_variant": "0.2", - "default_print_profile": "0.10 Standard @Snapmaker U1 (0.2 nozzle)", + "auxiliary_fan": "1", + "change_filament_gcode": ";===== date: 20260607 =====================\n; Change Tool[previous_extruder] -> Tool[next_extruder] (layer [layer_num])\n; max_layer_z [max_layer_z]\n; max_print_height [max_print_height]\n; print_sequence [print_sequence]\n\n{\nlocal move_z = 1.5;\nlocal max_speed_toolchange = 350.0;\nlocal wait_for_extruder_temp = true;\nposition[2] = position[2] + 2.0;\nlocal speed_toolchange = max_speed_toolchange;\n\nif travel_speed < max_speed_toolchange then\n speed_toolchange = travel_speed;\nendif\n\nif print_sequence == \"by object\" then\n\n if max_layer_z < ((max_print_height - z_offset) - 2) then\n move_z = z_offset + min(((max_layer_z - z_offset) + 2), max_print_height);\n endif\n\nendif\n\n\"G91\nG1 Z\" + move_z + \" F600\nG90\n\";\n\"G1 F\" + (speed_toolchange * 60) + \"\n\";\nif wait_for_extruder_temp and not((layer_num < 0) and (next_extruder == initial_tool)) then\n \"\n\";\n \"; \" + layer_num + \"\n\";\n if layer_num == 0 then\n \"M109 S\" + first_layer_temperature[next_extruder] + \" T\" + next_extruder + \"\n\";\n else\n \"M109 S\" + temperature[next_extruder] + \" T\" + next_extruder + \"\n\";\n endif\nendif\n\"M400\" + \"\n\";\n\"T\" + next_extruder + \"\n\";\nif filament_type[next_extruder] == \"PVA\" then\n\"SET_VELOCITY_LIMIT ACCEL=3000\n\";\nelse\nendif\nif previous_extruder != next_extruder and initial_extruder != next_extruder then\n\"SM_PRINT_PREEXTRUDE_FILAMENT INDEX=\" + next_extruder + \"\n\";\nendif\n\"G90\n\";\n}\n", + "deretraction_speed": [ + "30", + "30", + "30", + "30" + ], + "extruder_colour": [ + "#FCE94F", + "#FCE94F", + "#FCE94F", + "#FCE94F" + ], + "extruder_offset": [ + "0x0", + "0x0", + "0x0", + "0x0" + ], + "host_type": "octoprint", + "long_retractions_when_cut": [ + "0", + "0", + "0", + "0" + ], + "machine_end_gcode": ";===== date: 20260605 =====================\n; layer [layer_num]\n; max_layer_z [max_layer_z]\n; print_sequence [print_sequence]\n\n{if print_sequence == \"by object\"}\n{\nlocal move_z = max_print_height;\n\nif max_layer_z < ((max_print_height - z_offset) - 2) then\n move_z = z_offset + min(((max_layer_z - z_offset) + 2), max_print_height);\nendif\n}\n\nG91\nG1 X2 Y2 Z1 F24000\nG90\nG1 Z{move_z} F600\n{endif}\n\nPRINT_END\nTIMELAPSE_STOP\n", + "machine_max_jerk_z": [ + "3", + "0.4" + ], + "machine_max_speed_e": [ + "30", + "25" + ], + "machine_max_speed_z": [ + "20", + "12" + ], + "machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0\n{endif} \n\n\n;===== 画起始线 =================\nG90\nG1 Z1.5\nG0 X85 Y1 Z2 F18000\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\nG1 Z0.2\nM83\nG1 X185 E15 F360\nG1 Z1.5\n\nG90\nM106 S0", + "machine_tool_change_time": "5", "max_layer_height": [ "0.14", "0.14", @@ -26,5 +67,125 @@ "0.2", "0.2" ], - "nozzle_type": "hardened_steel" + "nozzle_type": "hardened_steel", + "printable_area": [ + "0.5x1", + "270.5x1", + "270.5x271", + "0.5x271" + ], + "printable_height": "270.05", + "retract_before_wipe": [ + "0%", + "0%", + "0%", + "0%" + ], + "retract_length_toolchange": [ + "10", + "10", + "10", + "10" + ], + "retract_lift_above": [ + "0", + "0", + "0", + "0" + ], + "retract_lift_below": [ + "269", + "269", + "269", + "269" + ], + "retract_lift_enforce": [ + "All Surfaces", + "All Surfaces", + "All Surfaces", + "All Surfaces" + ], + "retract_restart_extra": [ + "0", + "0", + "0", + "0" + ], + "retract_restart_extra_toolchange": [ + "0", + "0", + "0", + "0" + ], + "retract_when_changing_layer": [ + "1", + "1", + "1", + "1" + ], + "retraction_distances_when_cut": [ + "18", + "18", + "18", + "18" + ], + "retraction_length": [ + "0.4", + "0.4", + "0.4", + "0.4" + ], + "retraction_minimum_travel": [ + "1", + "1", + "1", + "1" + ], + "retraction_speed": [ + "30", + "30", + "30", + "30" + ], + "thumbnails": "48x48/PNG, 300x300/PNG", + "travel_slope": [ + "3", + "3", + "3", + "3" + ], + "wipe": [ + "1", + "1", + "1", + "1" + ], + "wipe_distance": [ + "2", + "2", + "2", + "2" + ], + "z_hop": [ + "0.4", + "0.4", + "0.4", + "0.4" + ], + "z_hop_types": [ + "Auto Lift", + "Auto Lift", + "Auto Lift", + "Auto Lift" + ], + "enable_filament_ramming": "0", + "extruder_clearance_height_to_rod": "27.5", + "extruder_clearance_radius": "72.5", + "machine_load_filament_time": "0", + "machine_unload_filament_time": "0", + "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\nTIMELAPSE_TAKE_FRAME\nDEFECT_DETECTION_DETECT", + "machine_pause_gcode": "M600", + "nozzle_volume": "143", + "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER={layer_num+1}", + "default_print_profile": "0.10 Standard @Snapmaker U1 (0.2 nozzle)" } diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json index 1105a7ec1d..6d2ec2cfe6 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4 nozzle).json @@ -1,14 +1,20 @@ { "type": "machine", - "setting_id": "UeTP4RqAd7xAMHVE", "name": "Snapmaker U1 (0.4 nozzle)", - "from": "system", - "instantiation": "true", "inherits": "fdm_U1", + "from": "system", + "setting_id": "UeTP4RqAd7xAMHVE", + "instantiation": "true", "printer_model": "Snapmaker U1", "printer_variant": "0.4", "auxiliary_fan": "1", "change_filament_gcode": ";===== date: 20260607 =====================\n; Change Tool[previous_extruder] -> Tool[next_extruder] (layer [layer_num])\n; max_layer_z [max_layer_z]\n; max_print_height [max_print_height]\n; print_sequence [print_sequence]\n\n{\nlocal move_z = 1.5;\nlocal max_speed_toolchange = 350.0;\nlocal wait_for_extruder_temp = true;\nposition[2] = position[2] + 2.0;\nlocal speed_toolchange = max_speed_toolchange;\n\nif travel_speed < max_speed_toolchange then\n speed_toolchange = travel_speed;\nendif\n\nif print_sequence == \"by object\" then\n\n if max_layer_z < ((max_print_height - z_offset) - 2) then\n move_z = z_offset + min(((max_layer_z - z_offset) + 2), max_print_height);\n endif\n\nendif\n\n\"G91\nG1 Z\" + move_z + \" F600\nG90\n\";\n\"G1 F\" + (speed_toolchange * 60) + \"\n\";\nif wait_for_extruder_temp and not((layer_num < 0) and (next_extruder == initial_tool)) then\n \"\n\";\n \"; \" + layer_num + \"\n\";\n if layer_num == 0 then\n \"M109 S\" + first_layer_temperature[next_extruder] + \" T\" + next_extruder + \"\n\";\n else\n \"M109 S\" + temperature[next_extruder] + \" T\" + next_extruder + \"\n\";\n endif\nendif\n\"M400\" + \"\n\";\n\"T\" + next_extruder + \"\n\";\nif filament_type[next_extruder] == \"PVA\" then\n\"SET_VELOCITY_LIMIT ACCEL=3000\n\";\nelse\nendif\nif previous_extruder != next_extruder and initial_extruder != next_extruder then\n\"SM_PRINT_PREEXTRUDE_FILAMENT INDEX=\" + next_extruder + \"\n\";\nendif\n\"G90\n\";\n}\n", + "deretraction_speed": [ + "30", + "30", + "30", + "30" + ], "extruder_colour": [ "#FCE94F", "#FCE94F", @@ -29,38 +35,6 @@ "0" ], "machine_end_gcode": ";===== date: 20260605 =====================\n; layer [layer_num]\n; max_layer_z [max_layer_z]\n; print_sequence [print_sequence]\n\n{if print_sequence == \"by object\"}\n{\nlocal move_z = max_print_height;\n\nif max_layer_z < ((max_print_height - z_offset) - 2) then\n move_z = z_offset + min(((max_layer_z - z_offset) + 2), max_print_height);\nendif\n}\n\nG91\nG1 X2 Y2 Z1 F24000\nG90\nG1 Z{move_z} F600\n{endif}\n\nPRINT_END\nTIMELAPSE_STOP\n", - "machine_max_acceleration_extruding": [ - "25000", - "25000" - ], - "machine_max_acceleration_retracting": [ - "5000", - "5000" - ], - "machine_max_acceleration_travel": [ - "25000", - "25000" - ], - "machine_max_acceleration_x": [ - "25000", - "25000" - ], - "machine_max_acceleration_y": [ - "25000", - "25000" - ], - "machine_max_acceleration_z": [ - "500", - "200" - ], - "machine_max_jerk_x": [ - "9", - "9" - ], - "machine_max_jerk_y": [ - "9", - "9" - ], "machine_max_jerk_z": [ "3", "0.4" @@ -69,22 +43,11 @@ "30", "25" ], - "machine_max_speed_x": [ - "1000", - "200" - ], - "machine_max_speed_y": [ - "1000", - "200" - ], "machine_max_speed_z": [ "20", "12" ], - "resonance_avoidance": "1", - "min_resonance_avoidance_speed": "40", - "max_resonance_avoidance_speed": "90", - "machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20251222 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count}\nSET_PRINT_STATS_INFO CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\n\nG28 X Y\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nG28 Z\n\n;===== 热床调平 =================\n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0\n; Original upstream: BED_MESH_CALIBRATE PROBE_COUNT=11,11\n\n;===== 画起始线 =================\nG90\nG1 Z1.5\nG0 X10 Y3 Z2 F18000\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\nG1 Z0.2\nM83\nG1 X110 E15 F360\nG1 Z1.5\n\nG90\nM106 S0", + "machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0\n{endif} \n\n\n;===== 画起始线 =================\nG90\nG1 Z1.5\nG0 X85 Y1 Z2 F18000\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\nG1 Z0.2\nM83\nG1 X185 E15 F360\nG1 Z1.5\n\nG90\nM106 S0", "machine_tool_change_time": "5", "max_layer_height": [ "0.32", @@ -104,7 +67,7 @@ "0.4", "0.4" ], - "nozzle_type": "stainless_steel", + "nozzle_type": "hardened_steel", "printable_area": [ "0.5x1", "270.5x1", @@ -112,7 +75,6 @@ "0.5x271" ], "printable_height": "270.05", - "printer_settings_id": "MyToolChanger 0.4 nozzle - Copy", "retract_before_wipe": [ "0%", "0%", @@ -168,10 +130,10 @@ "18" ], "retraction_length": [ - "0.8", - "0.8", - "0.8", - "0.8" + "1.5", + "1.5", + "1.5", + "1.5" ], "retraction_minimum_travel": [ "1", @@ -180,16 +142,10 @@ "1" ], "retraction_speed": [ - "40", - "40", - "40", - "40" - ], - "deretraction_speed": [ - "35", - "35", - "35", - "35" + "30", + "30", + "30", + "30" ], "thumbnails": "48x48/PNG, 300x300/PNG", "travel_slope": [ @@ -228,17 +184,11 @@ "machine_load_filament_time": "0", "machine_unload_filament_time": "0", "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\nTIMELAPSE_TAKE_FRAME\nDEFECT_DETECTION_DETECT", - "z_hop_when_prime": [ - "0", - "0", - "0", - "0" - ], - "ramming_pressure_advance_value": "0.02", - "tool_change_temprature_wait": "0", - "printer_notes": "", + "default_print_profile": "0.20 Standard @Snapmaker U1 (0.4 nozzle)", "machine_pause_gcode": "M600", - "default_bed_type": "Textured PEI Plate", - "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count}\nSET_PRINT_STATS_INFO CURRENT_LAYER={layer_num+1}", - "nozzle_volume": "143" + "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER={layer_num+1}", + "nozzle_volume": "143", + "resonance_avoidance": "1", + "min_resonance_avoidance_speed": "40", + "max_resonance_avoidance_speed": "90" } diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4+0.6 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4+0.6 nozzle).json index f637b2a592..c9866329bd 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4+0.6 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.4+0.6 nozzle).json @@ -1,10 +1,10 @@ { "type": "machine", - "setting_id": "O6AMxX1Ptbtv4zCK", "name": "Snapmaker U1 (0.4+0.6 nozzle)", - "from": "system", - "instantiation": "true", "inherits": "fdm_U1", + "from": "system", + "setting_id": "O6AMxX1Ptbtv4zCK", + "instantiation": "true", "printer_model": "Snapmaker U1", "printer_variant": "0.4+0.6", "default_print_profile": "0.20 Standard @Snapmaker U1 (0.4+0.6 nozzle)", diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json index 60e699d108..a6cb0d0bd3 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.6 nozzle).json @@ -1,18 +1,59 @@ { "type": "machine", - "setting_id": "1OseO7RSPgE4CRKO", "name": "Snapmaker U1 (0.6 nozzle)", - "from": "system", - "instantiation": "true", "inherits": "fdm_U1", + "from": "system", + "setting_id": "1OseO7RSPgE4CRKO", + "instantiation": "true", "printer_model": "Snapmaker U1", "printer_variant": "0.6", - "default_print_profile": "0.20 Standard @Snapmaker U1 (0.6 nozzle)", + "auxiliary_fan": "1", + "change_filament_gcode": ";===== date: 20260607 =====================\n; Change Tool[previous_extruder] -> Tool[next_extruder] (layer [layer_num])\n; max_layer_z [max_layer_z]\n; max_print_height [max_print_height]\n; print_sequence [print_sequence]\n\n{\nlocal move_z = 1.5;\nlocal max_speed_toolchange = 350.0;\nlocal wait_for_extruder_temp = true;\nposition[2] = position[2] + 2.0;\nlocal speed_toolchange = max_speed_toolchange;\n\nif travel_speed < max_speed_toolchange then\n speed_toolchange = travel_speed;\nendif\n\nif print_sequence == \"by object\" then\n\n if max_layer_z < ((max_print_height - z_offset) - 2) then\n move_z = z_offset + min(((max_layer_z - z_offset) + 2), max_print_height);\n endif\n\nendif\n\n\"G91\nG1 Z\" + move_z + \" F600\nG90\n\";\n\"G1 F\" + (speed_toolchange * 60) + \"\n\";\nif wait_for_extruder_temp and not((layer_num < 0) and (next_extruder == initial_tool)) then\n \"\n\";\n \"; \" + layer_num + \"\n\";\n if layer_num == 0 then\n \"M109 S\" + first_layer_temperature[next_extruder] + \" T\" + next_extruder + \"\n\";\n else\n \"M109 S\" + temperature[next_extruder] + \" T\" + next_extruder + \"\n\";\n endif\nendif\n\"M400\" + \"\n\";\n\"T\" + next_extruder + \"\n\";\nif filament_type[next_extruder] == \"PVA\" then\n\"SET_VELOCITY_LIMIT ACCEL=3000\n\";\nelse\nendif\nif previous_extruder != next_extruder and initial_extruder != next_extruder then\n\"SM_PRINT_PREEXTRUDE_FILAMENT INDEX=\" + next_extruder + \"\n\";\nendif\n\"G90\n\";\n}\n", + "deretraction_speed": [ + "30", + "30", + "30", + "30" + ], + "extruder_colour": [ + "#FCE94F", + "#FCE94F", + "#FCE94F", + "#FCE94F" + ], + "extruder_offset": [ + "0x0", + "0x0", + "0x0", + "0x0" + ], + "host_type": "octoprint", + "long_retractions_when_cut": [ + "0", + "0", + "0", + "0" + ], + "machine_end_gcode": ";===== date: 20260605 =====================\n; layer [layer_num]\n; max_layer_z [max_layer_z]\n; print_sequence [print_sequence]\n\n{if print_sequence == \"by object\"}\n{\nlocal move_z = max_print_height;\n\nif max_layer_z < ((max_print_height - z_offset) - 2) then\n move_z = z_offset + min(((max_layer_z - z_offset) + 2), max_print_height);\nendif\n}\n\nG91\nG1 X2 Y2 Z1 F24000\nG90\nG1 Z{move_z} F600\n{endif}\n\nPRINT_END\nTIMELAPSE_STOP\n", + "machine_max_jerk_z": [ + "3", + "0.4" + ], + "machine_max_speed_e": [ + "30", + "25" + ], + "machine_max_speed_z": [ + "20", + "12" + ], + "machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0\n{endif} \n\n\n;===== 画起始线 =================\nG90\nG1 Z1.5\nG0 X85 Y1 Z2 F18000\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\nG1 Z0.2\nM83\nG1 X185 E15 F360\nG1 Z1.5\n\nG90\nM106 S0", + "machine_tool_change_time": "5", "max_layer_height": [ - "0.48", - "0.48", - "0.48", - "0.48" + "0.42", + "0.42", + "0.42", + "0.42" ], "min_layer_height": [ "0.12", @@ -26,5 +67,125 @@ "0.6", "0.6" ], - "nozzle_type": "stainless_steel" + "nozzle_type": "stainless_steel", + "printable_area": [ + "0.5x1", + "270.5x1", + "270.5x271", + "0.5x271" + ], + "printable_height": "270.05", + "retract_before_wipe": [ + "0%", + "0%", + "0%", + "0%" + ], + "retract_length_toolchange": [ + "10", + "10", + "10", + "10" + ], + "retract_lift_above": [ + "0", + "0", + "0", + "0" + ], + "retract_lift_below": [ + "269", + "269", + "269", + "269" + ], + "retract_lift_enforce": [ + "All Surfaces", + "All Surfaces", + "All Surfaces", + "All Surfaces" + ], + "retract_restart_extra": [ + "0", + "0", + "0", + "0" + ], + "retract_restart_extra_toolchange": [ + "0", + "0", + "0", + "0" + ], + "retract_when_changing_layer": [ + "1", + "1", + "1", + "1" + ], + "retraction_distances_when_cut": [ + "18", + "18", + "18", + "18" + ], + "retraction_length": [ + "1.4", + "1.4", + "1.4", + "1.4" + ], + "retraction_minimum_travel": [ + "3", + "3", + "3", + "3" + ], + "retraction_speed": [ + "30", + "30", + "30", + "30" + ], + "thumbnails": "48x48/PNG, 300x300/PNG", + "travel_slope": [ + "3", + "3", + "3", + "3" + ], + "wipe": [ + "1", + "1", + "1", + "1" + ], + "wipe_distance": [ + "1", + "1", + "1", + "1" + ], + "z_hop": [ + "0.4", + "0.4", + "0.4", + "0.4" + ], + "z_hop_types": [ + "Auto Lift", + "Auto Lift", + "Auto Lift", + "Auto Lift" + ], + "enable_filament_ramming": "0", + "extruder_clearance_height_to_rod": "27.5", + "extruder_clearance_radius": "72.5", + "machine_load_filament_time": "0", + "machine_unload_filament_time": "0", + "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\nTIMELAPSE_TAKE_FRAME\nDEFECT_DETECTION_DETECT", + "machine_pause_gcode": "M600", + "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER={layer_num+1}", + "nozzle_volume": "143", + "default_print_profile": "0.30 Standard @Snapmaker U1 (0.6 nozzle)" } diff --git a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json index b775c2c964..ef4da1a516 100644 --- a/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json +++ b/resources/profiles/Snapmaker/machine/Snapmaker U1 (0.8 nozzle).json @@ -1,13 +1,54 @@ { "type": "machine", - "setting_id": "WTkhQtyDO06YY6AG", "name": "Snapmaker U1 (0.8 nozzle)", - "from": "system", - "instantiation": "true", "inherits": "fdm_U1", + "from": "system", + "setting_id": "WTkhQtyDO06YY6AG", + "instantiation": "true", "printer_model": "Snapmaker U1", "printer_variant": "0.8", - "default_print_profile": "0.40 Standard @Snapmaker U1 (0.8 nozzle)", + "auxiliary_fan": "1", + "change_filament_gcode": ";===== date: 20260607 =====================\n; Change Tool[previous_extruder] -> Tool[next_extruder] (layer [layer_num])\n; max_layer_z [max_layer_z]\n; max_print_height [max_print_height]\n; print_sequence [print_sequence]\n\n{\nlocal move_z = 1.5;\nlocal max_speed_toolchange = 350.0;\nlocal wait_for_extruder_temp = true;\nposition[2] = position[2] + 2.0;\nlocal speed_toolchange = max_speed_toolchange;\n\nif travel_speed < max_speed_toolchange then\n speed_toolchange = travel_speed;\nendif\n\nif print_sequence == \"by object\" then\n\n if max_layer_z < ((max_print_height - z_offset) - 2) then\n move_z = z_offset + min(((max_layer_z - z_offset) + 2), max_print_height);\n endif\n\nendif\n\n\"G91\nG1 Z\" + move_z + \" F600\nG90\n\";\n\"G1 F\" + (speed_toolchange * 60) + \"\n\";\nif wait_for_extruder_temp and not((layer_num < 0) and (next_extruder == initial_tool)) then\n \"\n\";\n \"; \" + layer_num + \"\n\";\n if layer_num == 0 then\n \"M109 S\" + first_layer_temperature[next_extruder] + \" T\" + next_extruder + \"\n\";\n else\n \"M109 S\" + temperature[next_extruder] + \" T\" + next_extruder + \"\n\";\n endif\nendif\n\"M400\" + \"\n\";\n\"T\" + next_extruder + \"\n\";\nif filament_type[next_extruder] == \"PVA\" then\n\"SET_VELOCITY_LIMIT ACCEL=3000\n\";\nelse\nendif\nif previous_extruder != next_extruder and initial_extruder != next_extruder then\n\"SM_PRINT_PREEXTRUDE_FILAMENT INDEX=\" + next_extruder + \"\n\";\nendif\n\"G90\n\";\n}\n", + "deretraction_speed": [ + "30", + "30", + "30", + "30" + ], + "extruder_colour": [ + "#FCE94F", + "#FCE94F", + "#FCE94F", + "#FCE94F" + ], + "extruder_offset": [ + "0x0", + "0x0", + "0x0", + "0x0" + ], + "host_type": "octoprint", + "long_retractions_when_cut": [ + "0", + "0", + "0", + "0" + ], + "machine_end_gcode": ";===== date: 20260605 =====================\n; layer [layer_num]\n; max_layer_z [max_layer_z]\n; print_sequence [print_sequence]\n\n{if print_sequence == \"by object\"}\n{\nlocal move_z = max_print_height;\n\nif max_layer_z < ((max_print_height - z_offset) - 2) then\n move_z = z_offset + min(((max_layer_z - z_offset) + 2), max_print_height);\nendif\n}\n\nG91\nG1 X2 Y2 Z1 F24000\nG90\nG1 Z{move_z} F600\n{endif}\n\nPRINT_END\nTIMELAPSE_STOP\n", + "machine_max_jerk_z": [ + "3", + "0.4" + ], + "machine_max_speed_e": [ + "30", + "25" + ], + "machine_max_speed_z": [ + "20", + "12" + ], + "machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0\n{endif} \n\n\n;===== 画起始线 =================\nG90\nG1 Z1.5\nG0 X85 Y1 Z2 F18000\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\nG1 Z0.2\nM83\nG1 X185 E15 F360\nG1 Z1.5\n\nG90\nM106 S0", + "machine_tool_change_time": "5", "max_layer_height": [ "0.56", "0.56", @@ -26,5 +67,125 @@ "0.8", "0.8" ], - "nozzle_type": "hardened_steel" + "nozzle_type": "hardened_steel", + "printable_area": [ + "0.5x1", + "270.5x1", + "270.5x271", + "0.5x271" + ], + "printable_height": "270.05", + "retract_before_wipe": [ + "0%", + "0%", + "0%", + "0%" + ], + "retract_length_toolchange": [ + "10", + "10", + "10", + "10" + ], + "retract_lift_above": [ + "0", + "0", + "0", + "0" + ], + "retract_lift_below": [ + "269", + "269", + "269", + "269" + ], + "retract_lift_enforce": [ + "All Surfaces", + "All Surfaces", + "All Surfaces", + "All Surfaces" + ], + "retract_restart_extra": [ + "0", + "0", + "0", + "0" + ], + "retract_restart_extra_toolchange": [ + "0", + "0", + "0", + "0" + ], + "retract_when_changing_layer": [ + "1", + "1", + "1", + "1" + ], + "retraction_distances_when_cut": [ + "18", + "18", + "18", + "18" + ], + "retraction_length": [ + "1.5", + "1.5", + "1.5", + "1.5" + ], + "retraction_minimum_travel": [ + "1", + "1", + "1", + "1" + ], + "retraction_speed": [ + "30", + "30", + "30", + "30" + ], + "thumbnails": "48x48/PNG, 300x300/PNG", + "travel_slope": [ + "3", + "3", + "3", + "3" + ], + "wipe": [ + "1", + "1", + "1", + "1" + ], + "wipe_distance": [ + "2", + "2", + "2", + "2" + ], + "z_hop": [ + "0.4", + "0.4", + "0.4", + "0.4" + ], + "z_hop_types": [ + "Auto Lift", + "Auto Lift", + "Auto Lift", + "Auto Lift" + ], + "enable_filament_ramming": "0", + "extruder_clearance_height_to_rod": "27.5", + "extruder_clearance_radius": "72.5", + "machine_load_filament_time": "0", + "machine_unload_filament_time": "0", + "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\nTIMELAPSE_TAKE_FRAME\nDEFECT_DETECTION_DETECT", + "machine_pause_gcode": "M600", + "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER={layer_num+1}", + "nozzle_volume": "143", + "default_print_profile": "0.40 Standard @Snapmaker U1 (0.8 nozzle)" } diff --git a/resources/profiles/Snapmaker/machine/fdm_U1.json b/resources/profiles/Snapmaker/machine/fdm_U1.json index 73fe5b74ef..0b3d89a303 100644 --- a/resources/profiles/Snapmaker/machine/fdm_U1.json +++ b/resources/profiles/Snapmaker/machine/fdm_U1.json @@ -176,8 +176,6 @@ "Normal Lift", "Normal Lift" ], - "bed_mesh_max": "267,267", - "bed_mesh_min": "3,3", "purge_in_prime_tower": "0", "machine_pause_gcode": "M601", "change_filament_gcode": "", @@ -185,13 +183,16 @@ "scan_first_layer": "0", "nozzle_type": "undefine", "auxiliary_fan": "0", + "support_multi_bed_types": "1", "default_bed_type": "Textured PEI Plate", - "printer_agent": "snapmaker", "printable_area": [ "0.5x1", "270.5x1", "270.5x271", "0.5x271" ], - "printable_height": "270.05" + "printable_height": "270.05", + "bed_mesh_min": "3,3", + "bed_mesh_max": "267,267", + "printer_agent": "snapmaker" } diff --git a/resources/profiles/Snapmaker/process/0.06 High Quality @Snapmaker U1 (0.2 nozzle).json b/resources/profiles/Snapmaker/process/0.06 High Quality @Snapmaker U1 (0.2 nozzle).json index 996b127b77..3dec06228e 100644 --- a/resources/profiles/Snapmaker/process/0.06 High Quality @Snapmaker U1 (0.2 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.06 High Quality @Snapmaker U1 (0.2 nozzle).json @@ -1,43 +1,31 @@ { - "type": "process", - "name": "0.06 High Quality @Snapmaker U1 (0.2 nozzle)", - "inherits": "fdm_process_U1_0.2_common", - "from": "system", - "instantiation": "true", - "layer_height": "0.06", - "initial_layer_print_height": "0.1", - "wall_loops": "4", - "bottom_shell_layers": "5", - "top_shell_layers": "7", - "bridge_flow": "1", - "initial_layer_speed": "40", - "initial_layer_infill_speed": "70", - "sparse_infill_speed": "100", - "top_surface_speed": "150", - "support_top_z_distance": "0.06", - "support_bottom_z_distance": "0.06", - "setting_id": "3SCofR6VrVyo3vJp", - "description": "Compared with the default profile of a 0.2 mm nozzle, it has a smaller layer lines, lower speeds and acceleration, and the sparse infill pattern is Gyroid. So, it results in minimal layer lines and much higher printing quality, but much longer printing time.", - "default_acceleration": "4000", - "elefant_foot_compensation": "0.15", - "outer_wall_acceleration": "2000", - "outer_wall_speed": "60", - "sparse_infill_pattern": "gyroid", - "smooth_coefficient": "150", - "overhang_totally_speed": "50", - "compatible_printers": [ - "Snapmaker U1 (0.2 nozzle)" - ], - "enable_arc_fitting": "0", - "ooze_prevention": "1", - "precise_outer_wall": "0", - "prime_tower_brim_width": "5", - "prime_tower_width": "30", - "prime_volume": "13.5", - "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150", - "support_type": "tree(auto)", - "wipe_tower_extra_rib_length": "8", - "wipe_tower_extra_spacing": "120%", - "wipe_tower_wall_type": "rib" + "type": "process", + "name": "0.06 High Quality @Snapmaker U1 (0.2 nozzle)", + "inherits": "fdm_process_U1_0.06_nozzle_0.2", + "from": "system", + "setting_id": "3SCofR6VrVyo3vJp", + "instantiation": "true", + "description": "Compared with the default profile of a 0.2 mm nozzle, it has a smaller layer lines, lower speeds and acceleration, and the sparse infill pattern is Gyroid. So, it results in minimal layer lines and much higher printing quality, but much longer printing time.", + "default_acceleration": "4000", + "elefant_foot_compensation": "0.15", + "outer_wall_acceleration": "2000", + "outer_wall_speed": "60", + "sparse_infill_pattern": "gyroid", + "smooth_coefficient": "150", + "overhang_totally_speed": "50", + "compatible_printers": [ + "Snapmaker U1 (0.2 nozzle)" + ], + "enable_arc_fitting": "0", + "ooze_prevention": "1", + "precise_outer_wall": "1", + "prime_tower_brim_width": "5", + "prime_tower_width": "30", + "prime_volume": "13.5", + "slowdown_for_curled_perimeters": "0", + "standby_temperature_delta": "-150", + "support_type": "tree(auto)", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_extra_spacing": "120%", + "wipe_tower_wall_type": "rib" } diff --git a/resources/profiles/Snapmaker/process/0.06 Standard @Snapmaker U1 (0.2 nozzle).json b/resources/profiles/Snapmaker/process/0.06 Standard @Snapmaker U1 (0.2 nozzle).json index 85e50ec87f..7c80e048f6 100644 --- a/resources/profiles/Snapmaker/process/0.06 Standard @Snapmaker U1 (0.2 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.06 Standard @Snapmaker U1 (0.2 nozzle).json @@ -1,39 +1,27 @@ { - "type": "process", - "name": "0.06 Standard @Snapmaker U1 (0.2 nozzle)", - "inherits": "fdm_process_U1_0.2_common", - "from": "system", - "instantiation": "true", - "layer_height": "0.06", - "initial_layer_print_height": "0.1", - "wall_loops": "4", - "bottom_shell_layers": "5", - "top_shell_layers": "7", - "bridge_flow": "1", - "initial_layer_speed": "40", - "initial_layer_infill_speed": "70", - "sparse_infill_speed": "100", - "top_surface_speed": "150", - "support_top_z_distance": "0.06", - "support_bottom_z_distance": "0.06", - "setting_id": "997RaiNHd2YhpaRB", - "description": "Compared with the default profile of 0.2 mm nozzle, it has a smaller layer height, and results in minimal layer lines and higher printing quality, but shorter printing time.", - "elefant_foot_compensation": "0.15", - "smooth_coefficient": "150", - "overhang_totally_speed": "50", - "compatible_printers": [ - "Snapmaker U1 (0.2 nozzle)" - ], - "enable_arc_fitting": "0", - "ooze_prevention": "1", - "precise_outer_wall": "0", - "prime_tower_brim_width": "5", - "prime_tower_width": "30", - "prime_volume": "13.5", - "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150", - "support_type": "tree(auto)", - "wipe_tower_extra_rib_length": "8", - "wipe_tower_extra_spacing": "120%", - "wipe_tower_wall_type": "rib" + "type": "process", + "name": "0.06 Standard @Snapmaker U1 (0.2 nozzle)", + "inherits": "fdm_process_U1_0.06_nozzle_0.2", + "from": "system", + "setting_id": "997RaiNHd2YhpaRB", + "instantiation": "true", + "description": "Compared with the default profile of 0.2 mm nozzle, it has a smaller layer height, and results in minimal layer lines and higher printing quality, but shorter printing time.", + "elefant_foot_compensation": "0.15", + "smooth_coefficient": "150", + "overhang_totally_speed": "50", + "compatible_printers": [ + "Snapmaker U1 (0.2 nozzle)" + ], + "enable_arc_fitting": "0", + "ooze_prevention": "1", + "precise_outer_wall": "1", + "prime_tower_brim_width": "5", + "prime_tower_width": "30", + "prime_volume": "13.5", + "slowdown_for_curled_perimeters": "0", + "standby_temperature_delta": "-150", + "support_type": "tree(auto)", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_extra_spacing": "120%", + "wipe_tower_wall_type": "rib" } diff --git a/resources/profiles/Snapmaker/process/0.08 Extra Fine @Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/process/0.08 Extra Fine @Snapmaker U1 (0.4 nozzle).json index aeffe99f2a..b8a8e126aa 100644 --- a/resources/profiles/Snapmaker/process/0.08 Extra Fine @Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.08 Extra Fine @Snapmaker U1 (0.4 nozzle).json @@ -12,6 +12,19 @@ "Snapmaker U1 (0.4 nozzle)" ], "ooze_prevention": "1", + "prime_tower_width": "30", + "prime_volume": "18", "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150" + "standby_temperature_delta": "-150", + "wipe_tower_filament": "0", + "prime_tower_brim_width": "5", + "wipe_tower_cone_angle": "15", + "wipe_tower_extra_spacing": "120%", + "enable_arc_fitting": "0", + "outer_wall_speed": "100", + "precise_outer_wall": "1", + "support_type": "tree(auto)", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_wall_type": "rib", + "gap_fill_target": "topbottom" } diff --git a/resources/profiles/Snapmaker/process/0.08 High Quality @Snapmaker U1 (0.2 nozzle).json b/resources/profiles/Snapmaker/process/0.08 High Quality @Snapmaker U1 (0.2 nozzle).json index fe0fc0e87d..27d3903f06 100644 --- a/resources/profiles/Snapmaker/process/0.08 High Quality @Snapmaker U1 (0.2 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.08 High Quality @Snapmaker U1 (0.2 nozzle).json @@ -1,43 +1,31 @@ { - "type": "process", - "name": "0.08 High Quality @Snapmaker U1 (0.2 nozzle)", - "inherits": "fdm_process_U1_0.2_common", - "from": "system", - "instantiation": "true", - "layer_height": "0.08", - "initial_layer_print_height": "0.1", - "wall_loops": "4", - "bottom_shell_layers": "5", - "top_shell_layers": "7", - "bridge_flow": "1", - "initial_layer_speed": "40", - "initial_layer_infill_speed": "70", - "sparse_infill_speed": "100", - "top_surface_speed": "150", - "support_top_z_distance": "0.08", - "support_bottom_z_distance": "0.08", - "setting_id": "6ExpMU3Wq4J1R7wy", - "description": "Compared with the default profile of a 0.2 mm nozzle, it has a smaller layer lines, lower speeds and acceleration, and the sparse infill pattern is Gyroid. So, it results in almost invisible layer lines and much higher printing quality, but much longer printing time.", - "default_acceleration": "4000", - "elefant_foot_compensation": "0.15", - "outer_wall_acceleration": "2000", - "outer_wall_speed": "60", - "sparse_infill_pattern": "gyroid", - "smooth_coefficient": "150", - "overhang_totally_speed": "50", - "compatible_printers": [ - "Snapmaker U1 (0.2 nozzle)" - ], - "enable_arc_fitting": "0", - "ooze_prevention": "1", - "precise_outer_wall": "0", - "prime_tower_brim_width": "5", - "prime_tower_width": "30", - "prime_volume": "18", - "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150", - "support_type": "tree(auto)", - "wipe_tower_extra_rib_length": "8", - "wipe_tower_extra_spacing": "120%", - "wipe_tower_wall_type": "rib" + "type": "process", + "name": "0.08 High Quality @Snapmaker U1 (0.2 nozzle)", + "inherits": "fdm_process_U1_0.08_nozzle_0.2", + "from": "system", + "setting_id": "6ExpMU3Wq4J1R7wy", + "instantiation": "true", + "description": "Compared with the default profile of a 0.2 mm nozzle, it has a smaller layer lines, lower speeds and acceleration, and the sparse infill pattern is Gyroid. So, it results in almost invisible layer lines and much higher printing quality, but much longer printing time.", + "default_acceleration": "4000", + "elefant_foot_compensation": "0.15", + "outer_wall_acceleration": "2000", + "outer_wall_speed": "60", + "sparse_infill_pattern": "gyroid", + "smooth_coefficient": "150", + "overhang_totally_speed": "50", + "compatible_printers": [ + "Snapmaker U1 (0.2 nozzle)" + ], + "enable_arc_fitting": "0", + "ooze_prevention": "1", + "precise_outer_wall": "1", + "prime_tower_brim_width": "5", + "prime_tower_width": "30", + "prime_volume": "18", + "slowdown_for_curled_perimeters": "0", + "standby_temperature_delta": "-150", + "support_type": "tree(auto)", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_extra_spacing": "120%", + "wipe_tower_wall_type": "rib" } diff --git a/resources/profiles/Snapmaker/process/0.08 High Quality @Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/process/0.08 High Quality @Snapmaker U1 (0.4 nozzle).json index b86156d841..677e6742d9 100644 --- a/resources/profiles/Snapmaker/process/0.08 High Quality @Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.08 High Quality @Snapmaker U1 (0.4 nozzle).json @@ -21,6 +21,18 @@ "Snapmaker U1 (0.4 nozzle)" ], "ooze_prevention": "1", + "prime_tower_width": "30", + "prime_volume": "18", "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150" + "standby_temperature_delta": "-150", + "wipe_tower_filament": "0", + "prime_tower_brim_width": "5", + "wipe_tower_cone_angle": "15", + "wipe_tower_extra_spacing": "120%", + "enable_arc_fitting": "0", + "precise_outer_wall": "1", + "support_type": "tree(auto)", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_wall_type": "rib", + "gap_fill_target": "topbottom" } diff --git a/resources/profiles/Snapmaker/process/0.08 Standard @Snapmaker U1 (0.2 nozzle).json b/resources/profiles/Snapmaker/process/0.08 Standard @Snapmaker U1 (0.2 nozzle).json index 472176ff40..b4728bd60f 100644 --- a/resources/profiles/Snapmaker/process/0.08 Standard @Snapmaker U1 (0.2 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.08 Standard @Snapmaker U1 (0.2 nozzle).json @@ -1,39 +1,27 @@ { - "type": "process", - "name": "0.08 Standard @Snapmaker U1 (0.2 nozzle)", - "inherits": "fdm_process_U1_0.2_common", - "from": "system", - "instantiation": "true", - "layer_height": "0.08", - "initial_layer_print_height": "0.1", - "wall_loops": "4", - "bottom_shell_layers": "5", - "top_shell_layers": "7", - "bridge_flow": "1", - "initial_layer_speed": "40", - "initial_layer_infill_speed": "70", - "sparse_infill_speed": "100", - "top_surface_speed": "150", - "support_top_z_distance": "0.08", - "support_bottom_z_distance": "0.08", - "setting_id": "loFrCYH9ux2L5JpZ", - "description": "Compared with the default profile of a 0.2 mm nozzle, it has a smaller layer height, and results in almost invisible layer lines and higher printing quality, but shorter printing time.", - "elefant_foot_compensation": "0.15", - "smooth_coefficient": "150", - "overhang_totally_speed": "50", - "compatible_printers": [ - "Snapmaker U1 (0.2 nozzle)" - ], - "enable_arc_fitting": "0", - "ooze_prevention": "1", - "precise_outer_wall": "0", - "prime_tower_brim_width": "5", - "prime_tower_width": "30", - "prime_volume": "18", - "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150", - "support_type": "tree(auto)", - "wipe_tower_extra_rib_length": "8", - "wipe_tower_extra_spacing": "120%", - "wipe_tower_wall_type": "rib" + "type": "process", + "name": "0.08 Standard @Snapmaker U1 (0.2 nozzle)", + "inherits": "fdm_process_U1_0.08_nozzle_0.2", + "from": "system", + "setting_id": "loFrCYH9ux2L5JpZ", + "instantiation": "true", + "description": "Compared with the default profile of a 0.2 mm nozzle, it has a smaller layer height, and results in almost invisible layer lines and higher printing quality, but shorter printing time.", + "elefant_foot_compensation": "0.15", + "smooth_coefficient": "150", + "overhang_totally_speed": "50", + "compatible_printers": [ + "Snapmaker U1 (0.2 nozzle)" + ], + "enable_arc_fitting": "0", + "ooze_prevention": "1", + "precise_outer_wall": "1", + "prime_tower_brim_width": "5", + "prime_tower_width": "30", + "prime_volume": "18", + "slowdown_for_curled_perimeters": "0", + "standby_temperature_delta": "-150", + "support_type": "tree(auto)", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_extra_spacing": "120%", + "wipe_tower_wall_type": "rib" } diff --git a/resources/profiles/Snapmaker/process/0.10 High Quality @Snapmaker U1 (0.2 nozzle).json b/resources/profiles/Snapmaker/process/0.10 High Quality @Snapmaker U1 (0.2 nozzle).json index 0f15ce7c5f..80b2120807 100644 --- a/resources/profiles/Snapmaker/process/0.10 High Quality @Snapmaker U1 (0.2 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.10 High Quality @Snapmaker U1 (0.2 nozzle).json @@ -1,43 +1,31 @@ { - "type": "process", - "name": "0.10 High Quality @Snapmaker U1 (0.2 nozzle)", - "inherits": "fdm_process_U1_0.2_common", - "from": "system", - "instantiation": "true", - "layer_height": "0.1", - "initial_layer_print_height": "0.1", - "wall_loops": "4", - "bottom_shell_layers": "5", - "top_shell_layers": "7", - "bridge_flow": "1", - "initial_layer_speed": "40", - "initial_layer_infill_speed": "70", - "sparse_infill_speed": "100", - "top_surface_speed": "150", - "support_top_z_distance": "0.1", - "support_bottom_z_distance": "0.1", - "setting_id": "BfLpmnJnSBYBFInR", - "description": "Compared with the default profile of a 0.2 mm nozzle, it has lower speeds and acceleration, and the sparse infill pattern is Gyroid. So, it results in much higher printing quality, but a much longer printing time.", - "default_acceleration": "4000", - "elefant_foot_compensation": "0.15", - "outer_wall_acceleration": "2000", - "outer_wall_speed": "60", - "sparse_infill_pattern": "gyroid", - "smooth_coefficient": "150", - "overhang_totally_speed": "50", - "compatible_printers": [ - "Snapmaker U1 (0.2 nozzle)" - ], - "enable_arc_fitting": "0", - "ooze_prevention": "1", - "precise_outer_wall": "0", - "prime_tower_brim_width": "5", - "prime_tower_width": "28", - "prime_volume": "25", - "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150", - "support_type": "tree(auto)", - "wipe_tower_extra_rib_length": "8", - "wipe_tower_extra_spacing": "120%", - "wipe_tower_wall_type": "rib" + "type": "process", + "name": "0.10 High Quality @Snapmaker U1 (0.2 nozzle)", + "inherits": "fdm_process_U1_0.10_nozzle_0.2", + "from": "system", + "setting_id": "BfLpmnJnSBYBFInR", + "instantiation": "true", + "description": "Compared with the default profile of a 0.2 mm nozzle, it has lower speeds and acceleration, and the sparse infill pattern is Gyroid. So, it results in much higher printing quality, but a much longer printing time.", + "default_acceleration": "4000", + "elefant_foot_compensation": "0.15", + "outer_wall_acceleration": "2000", + "outer_wall_speed": "60", + "sparse_infill_pattern": "gyroid", + "smooth_coefficient": "150", + "overhang_totally_speed": "50", + "compatible_printers": [ + "Snapmaker U1 (0.2 nozzle)" + ], + "enable_arc_fitting": "0", + "ooze_prevention": "1", + "precise_outer_wall": "1", + "prime_tower_brim_width": "5", + "prime_tower_width": "28", + "prime_volume": "25", + "slowdown_for_curled_perimeters": "0", + "standby_temperature_delta": "-150", + "support_type": "tree(auto)", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_extra_spacing": "120%", + "wipe_tower_wall_type": "rib" } diff --git a/resources/profiles/Snapmaker/process/0.10 Standard @Snapmaker U1 (0.2 nozzle).json b/resources/profiles/Snapmaker/process/0.10 Standard @Snapmaker U1 (0.2 nozzle).json index 460bf8b6fd..80739205ac 100644 --- a/resources/profiles/Snapmaker/process/0.10 Standard @Snapmaker U1 (0.2 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.10 Standard @Snapmaker U1 (0.2 nozzle).json @@ -1,41 +1,29 @@ { - "type": "process", - "name": "0.10 Standard @Snapmaker U1 (0.2 nozzle)", - "inherits": "fdm_process_U1_0.2_common", - "from": "system", - "instantiation": "true", - "layer_height": "0.1", - "initial_layer_print_height": "0.1", - "wall_loops": "4", - "bottom_shell_layers": "5", - "top_shell_layers": "7", - "bridge_flow": "1", - "initial_layer_speed": "40", - "initial_layer_infill_speed": "70", - "sparse_infill_speed": "100", - "top_surface_speed": "150", - "support_top_z_distance": "0.1", - "support_bottom_z_distance": "0.1", - "setting_id": "SOq962UWv1ml1Mk0", - "description": "It has a small layer height, and results in almost negligible layer lines and high printing quality. It is suitable for most general printing cases.", - "elefant_foot_compensation": "0.15", - "smooth_coefficient": "150", - "overhang_totally_speed": "50", - "compatible_printers": [ - "Snapmaker U1 (0.2 nozzle)" - ], - "enable_arc_fitting": "0", - "ooze_prevention": "1", - "precise_outer_wall": "0", - "preheat_time": "31", - "prime_tower_brim_width": "6", - "prime_tower_width": "28", - "prime_volume": "25", - "standby_temperature_delta": "-150", - "support_threshold_angle": "35", - "support_type": "tree(auto)", - "wipe_tower_extra_rib_length": "8", - "wipe_tower_extra_spacing": "120%", - "wipe_tower_wall_type": "rib", - "slowdown_for_curled_perimeters": "0" + "type": "process", + "name": "0.10 Standard @Snapmaker U1 (0.2 nozzle)", + "inherits": "fdm_process_U1_0.10_nozzle_0.2", + "from": "system", + "setting_id": "SOq962UWv1ml1Mk0", + "instantiation": "true", + "description": "It has a small layer height, and results in almost negligible layer lines and high printing quality. It is suitable for most general printing cases.", + "elefant_foot_compensation": "0.15", + "smooth_coefficient": "150", + "overhang_totally_speed": "50", + "compatible_printers": [ + "Snapmaker U1 (0.2 nozzle)" + ], + "enable_arc_fitting": "0", + "ooze_prevention": "1", + "precise_outer_wall": "1", + "preheat_time": "31", + "prime_tower_brim_width": "6", + "prime_tower_width": "28", + "prime_volume": "25", + "standby_temperature_delta": "-150", + "support_threshold_angle": "35", + "support_type": "tree(auto)", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_extra_spacing": "120%", + "wipe_tower_wall_type": "rib", + "slowdown_for_curled_perimeters": "0" } diff --git a/resources/profiles/Snapmaker/process/0.12 Fine @Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/process/0.12 Fine @Snapmaker U1 (0.4 nozzle).json index 42823b3f0b..2da54a6dc3 100644 --- a/resources/profiles/Snapmaker/process/0.12 Fine @Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.12 Fine @Snapmaker U1 (0.4 nozzle).json @@ -13,5 +13,18 @@ ], "ooze_prevention": "1", "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150" + "standby_temperature_delta": "-150", + "wipe_tower_filament": "0", + "prime_tower_brim_width": "5", + "prime_tower_width": "30", + "prime_volume": "27", + "wipe_tower_cone_angle": "15", + "wipe_tower_extra_spacing": "120%", + "enable_arc_fitting": "0", + "precise_outer_wall": "1", + "support_threshold_angle": "25", + "support_type": "tree(auto)", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_wall_type": "rib", + "gap_fill_target": "topbottom" } diff --git a/resources/profiles/Snapmaker/process/0.12 High Quality @Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/process/0.12 High Quality @Snapmaker U1 (0.4 nozzle).json index 8564a41b24..9302a40421 100644 --- a/resources/profiles/Snapmaker/process/0.12 High Quality @Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.12 High Quality @Snapmaker U1 (0.4 nozzle).json @@ -20,5 +20,20 @@ "compatible_printers": [ "Snapmaker U1 (0.4 nozzle)" ], - "slowdown_for_curled_perimeters": "0" + "prime_tower_width": "30", + "prime_volume": "27", + "slowdown_for_curled_perimeters": "0", + "wipe_tower_filament": "0", + "prime_tower_brim_width": "5", + "wipe_tower_cone_angle": "15", + "wipe_tower_extra_spacing": "120%", + "enable_arc_fitting": "0", + "ooze_prevention": "1", + "precise_outer_wall": "1", + "standby_temperature_delta": "-150", + "support_threshold_angle": "25", + "support_type": "tree(auto)", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_wall_type": "rib", + "gap_fill_target": "topbottom" } diff --git a/resources/profiles/Snapmaker/process/0.12 Standard @Snapmaker U1 (0.2 nozzle).json b/resources/profiles/Snapmaker/process/0.12 Standard @Snapmaker U1 (0.2 nozzle).json index de68cc0dd0..cc93a97e99 100644 --- a/resources/profiles/Snapmaker/process/0.12 Standard @Snapmaker U1 (0.2 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.12 Standard @Snapmaker U1 (0.2 nozzle).json @@ -1,39 +1,27 @@ { - "type": "process", - "name": "0.12 Standard @Snapmaker U1 (0.2 nozzle)", - "inherits": "fdm_process_U1_0.2_common", - "from": "system", - "instantiation": "true", - "layer_height": "0.12", - "initial_layer_print_height": "0.1", - "wall_loops": "4", - "bottom_shell_layers": "5", - "top_shell_layers": "7", - "bridge_flow": "1", - "initial_layer_speed": "40", - "initial_layer_infill_speed": "70", - "sparse_infill_speed": "100", - "top_surface_speed": "150", - "support_top_z_distance": "0.12", - "support_bottom_z_distance": "0.12", - "setting_id": "KHC1zpxpuvaUEzvf", - "description": "Compared with the default profile of a 0.2 mm nozzle, it has a slightly bigger layer height, and results in almost negligible layer lines, and slightly shorter printing time.", - "elefant_foot_compensation": "0.15", - "smooth_coefficient": "150", - "overhang_totally_speed": "50", - "compatible_printers": [ - "Snapmaker U1 (0.2 nozzle)" - ], - "enable_arc_fitting": "0", - "ooze_prevention": "1", - "precise_outer_wall": "0", - "prime_tower_brim_width": "5", - "prime_tower_width": "28", - "prime_volume": "27", - "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150", - "support_type": "tree(auto)", - "wipe_tower_extra_rib_length": "8", - "wipe_tower_extra_spacing": "120%", - "wipe_tower_wall_type": "rib" + "type": "process", + "name": "0.12 Standard @Snapmaker U1 (0.2 nozzle)", + "inherits": "fdm_process_U1_0.12_nozzle_0.2", + "from": "system", + "setting_id": "KHC1zpxpuvaUEzvf", + "instantiation": "true", + "description": "Compared with the default profile of a 0.2 mm nozzle, it has a slightly bigger layer height, and results in almost negligible layer lines, and slightly shorter printing time.", + "elefant_foot_compensation": "0.15", + "smooth_coefficient": "150", + "overhang_totally_speed": "50", + "compatible_printers": [ + "Snapmaker U1 (0.2 nozzle)" + ], + "enable_arc_fitting": "0", + "ooze_prevention": "1", + "precise_outer_wall": "1", + "prime_tower_brim_width": "5", + "prime_tower_width": "28", + "prime_volume": "27", + "slowdown_for_curled_perimeters": "0", + "standby_temperature_delta": "-150", + "support_type": "tree(auto)", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_extra_spacing": "120%", + "wipe_tower_wall_type": "rib" } diff --git a/resources/profiles/Snapmaker/process/0.14 Standard @Snapmaker U1 (0.2 nozzle).json b/resources/profiles/Snapmaker/process/0.14 Standard @Snapmaker U1 (0.2 nozzle).json index c265d55e67..54513eb14b 100644 --- a/resources/profiles/Snapmaker/process/0.14 Standard @Snapmaker U1 (0.2 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.14 Standard @Snapmaker U1 (0.2 nozzle).json @@ -1,39 +1,27 @@ { - "type": "process", - "name": "0.14 Standard @Snapmaker U1 (0.2 nozzle)", - "inherits": "fdm_process_U1_0.2_common", - "from": "system", - "instantiation": "true", - "layer_height": "0.14", - "initial_layer_print_height": "0.1", - "wall_loops": "4", - "bottom_shell_layers": "5", - "top_shell_layers": "7", - "bridge_flow": "1", - "initial_layer_speed": "40", - "initial_layer_infill_speed": "70", - "sparse_infill_speed": "100", - "top_surface_speed": "150", - "support_top_z_distance": "0.14", - "support_bottom_z_distance": "0.14", - "setting_id": "NGJoV0n6T510y9wM", - "description": "Compared with the default profile of a 0.2 mm nozzle, it has a bigger layer height, and results in slightly visible layer lines, but shorter printing time.", - "elefant_foot_compensation": "0.15", - "smooth_coefficient": "150", - "overhang_totally_speed": "50", - "compatible_printers": [ - "Snapmaker U1 (0.2 nozzle)" - ], - "enable_arc_fitting": "0", - "ooze_prevention": "1", - "precise_outer_wall": "0", - "prime_tower_brim_width": "5", - "prime_tower_width": "30", - "prime_volume": "32", - "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150", - "support_type": "tree(auto)", - "wipe_tower_extra_rib_length": "8", - "wipe_tower_extra_spacing": "120%", - "wipe_tower_wall_type": "rib" + "type": "process", + "name": "0.14 Standard @Snapmaker U1 (0.2 nozzle)", + "inherits": "fdm_process_U1_0.14_nozzle_0.2", + "from": "system", + "setting_id": "NGJoV0n6T510y9wM", + "instantiation": "true", + "description": "Compared with the default profile of a 0.2 mm nozzle, it has a bigger layer height, and results in slightly visible layer lines, but shorter printing time.", + "elefant_foot_compensation": "0.15", + "smooth_coefficient": "150", + "overhang_totally_speed": "50", + "compatible_printers": [ + "Snapmaker U1 (0.2 nozzle)" + ], + "enable_arc_fitting": "0", + "ooze_prevention": "1", + "precise_outer_wall": "1", + "prime_tower_brim_width": "5", + "prime_tower_width": "30", + "prime_volume": "32", + "slowdown_for_curled_perimeters": "0", + "standby_temperature_delta": "-150", + "support_type": "tree(auto)", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_extra_spacing": "120%", + "wipe_tower_wall_type": "rib" } diff --git a/resources/profiles/Snapmaker/process/0.16 High Quality @Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/process/0.16 High Quality @Snapmaker U1 (0.4 nozzle).json index e66f9f3beb..416d85881c 100644 --- a/resources/profiles/Snapmaker/process/0.16 High Quality @Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.16 High Quality @Snapmaker U1 (0.4 nozzle).json @@ -21,6 +21,19 @@ "Snapmaker U1 (0.4 nozzle)" ], "ooze_prevention": "1", + "prime_tower_width": "30", + "prime_volume": "36", "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150" + "standby_temperature_delta": "-150", + "wipe_tower_filament": "0", + "prime_tower_brim_width": "5", + "wipe_tower_cone_angle": "15", + "wipe_tower_extra_spacing": "120%", + "enable_arc_fitting": "0", + "precise_outer_wall": "1", + "support_threshold_angle": "30", + "support_type": "tree(auto)", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_wall_type": "rib", + "gap_fill_target": "topbottom" } diff --git a/resources/profiles/Snapmaker/process/0.16 Optimal @Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/process/0.16 Optimal @Snapmaker U1 (0.4 nozzle).json index 2817871fad..6871a3e120 100644 --- a/resources/profiles/Snapmaker/process/0.16 Optimal @Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.16 Optimal @Snapmaker U1 (0.4 nozzle).json @@ -12,6 +12,18 @@ "Snapmaker U1 (0.4 nozzle)" ], "ooze_prevention": "1", + "prime_tower_width": "30", + "prime_volume": "36", "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150" + "standby_temperature_delta": "-150", + "wipe_tower_filament": "0", + "prime_tower_brim_width": "5", + "wipe_tower_cone_angle": "15", + "wipe_tower_extra_spacing": "120%", + "enable_arc_fitting": "0", + "precise_outer_wall": "1", + "support_type": "tree(auto)", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_wall_type": "rib", + "gap_fill_target": "topbottom" } diff --git a/resources/profiles/Snapmaker/process/0.18 Standard @Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/process/0.18 Standard @Snapmaker U1 (0.6 nozzle).json index e68cf86d75..77b756bffc 100644 --- a/resources/profiles/Snapmaker/process/0.18 Standard @Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.18 Standard @Snapmaker U1 (0.6 nozzle).json @@ -1,42 +1,32 @@ { - "type": "process", - "name": "0.18 Standard @Snapmaker U1 (0.6 nozzle)", - "inherits": "fdm_process_U1_0.6_common", - "from": "system", - "instantiation": "true", - "layer_height": "0.25", - "initial_layer_print_height": "0.3", - "bridge_flow": "1", - "initial_layer_speed": "35", - "initial_layer_infill_speed": "55", - "sparse_infill_speed": "100", - "top_surface_speed": "150", - "bridge_speed": "30", - "overhang_3_4_speed": "15", - "support_top_z_distance": "0.18", - "support_bottom_z_distance": "0.18", - "setting_id": "XGSrVgnsUFv6uCRs", - "description": "Compared with the default profile of a 0.6 mm nozzle, it has a smaller layer height, and results in less apparent layer lines and higher printing quality, but longer printing time.", - "elefant_foot_compensation": "0.15", - "smooth_coefficient": "150", - "overhang_totally_speed": "50", - "compatible_printers": [ - "Snapmaker U1 (0.6 nozzle)" - ], - "filter_out_gap_fill": "1", - "gap_fill_target": "topbottom", - "internal_bridge_speed": "100%", - "prime_tower_brim_width": "5", - "prime_tower_width": "30", - "prime_volume": "41", - "seam_gap": "15%", - "wipe_tower_extra_rib_length": "8", - "wipe_tower_extra_spacing": "120%", - "wipe_tower_wall_type": "rib", - "enable_arc_fitting": "0", - "ooze_prevention": "1", - "precise_outer_wall": "0", - "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150", - "support_type": "tree(auto)" + "type": "process", + "name": "0.18 Standard @Snapmaker U1 (0.6 nozzle)", + "inherits": "fdm_process_U1_0.18_nozzle_0.6", + "from": "system", + "setting_id": "XGSrVgnsUFv6uCRs", + "instantiation": "true", + "description": "Compared with the default profile of a 0.6 mm nozzle, it has a smaller layer height, and results in less apparent layer lines and higher printing quality, but longer printing time.", + "elefant_foot_compensation": "0.15", + "smooth_coefficient": "150", + "overhang_totally_speed": "50", + "compatible_printers": [ + "Snapmaker U1 (0.6 nozzle)" + ], + "filter_out_gap_fill": "1", + "gap_fill_target": "topbottom", + "internal_bridge_speed": "100%", + "layer_height": "0.25", + "prime_tower_brim_width": "5", + "prime_tower_width": "30", + "prime_volume": "41", + "seam_gap": "15%", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_extra_spacing": "120%", + "wipe_tower_wall_type": "rib", + "enable_arc_fitting": "0", + "ooze_prevention": "1", + "precise_outer_wall": "1", + "slowdown_for_curled_perimeters": "0", + "standby_temperature_delta": "-150", + "support_type": "tree(auto)" } diff --git a/resources/profiles/Snapmaker/process/0.20 Bambu Support W @Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/process/0.20 Bambu Support W @Snapmaker U1 (0.4 nozzle).json deleted file mode 100644 index 5de608c68d..0000000000 --- a/resources/profiles/Snapmaker/process/0.20 Bambu Support W @Snapmaker U1 (0.4 nozzle).json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "type": "process", - "name": "0.20 Bambu Support W @Snapmaker U1 (0.4 nozzle)", - "inherits": "fdm_process_U1_0.20", - "from": "system", - "setting_id": "8OkVMuElKExBdftG", - "instantiation": "true", - "enable_support": "1", - "support_interface_top_layers": "3", - "support_top_z_distance": "0.2", - "support_interface_loop_pattern": "1", - "support_interface_spacing": "0", - "support_interface_speed": "80", - "support_filament": "0", - "support_interface_filament": "0", - "enable_prime_tower": "1", - "compatible_printers": [ - "Snapmaker U1 (0.4 nozzle)" - ], - "ooze_prevention": "1", - "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150" -} diff --git a/resources/profiles/Snapmaker/process/0.20 Quality @Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/process/0.20 Quality @Snapmaker U1 (0.4 nozzle).json index ac7fc102bc..da92006d49 100644 --- a/resources/profiles/Snapmaker/process/0.20 Quality @Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.20 Quality @Snapmaker U1 (0.4 nozzle).json @@ -12,6 +12,7 @@ "Snapmaker U1 (0.4 nozzle)" ], "ooze_prevention": "1", + "prime_tower_width": "30", "slowdown_for_curled_perimeters": "0", "standby_temperature_delta": "-150", "wipe_tower_filament": "0", diff --git a/resources/profiles/Snapmaker/process/0.20 Standard @Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/process/0.20 Standard @Snapmaker U1 (0.4 nozzle).json index 243718507b..5d873d5efd 100644 --- a/resources/profiles/Snapmaker/process/0.20 Standard @Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.20 Standard @Snapmaker U1 (0.4 nozzle).json @@ -12,6 +12,8 @@ "Snapmaker U1 (0.4 nozzle)" ], "ooze_prevention": "1", + "prime_tower_width": "30", + "prime_volume": "45", "slowdown_for_curled_perimeters": "0", "standby_temperature_delta": "-150", "wipe_tower_filament": "0", diff --git a/resources/profiles/Snapmaker/process/0.20 Standard @Snapmaker U1 (0.4+0.6 nozzle).json b/resources/profiles/Snapmaker/process/0.20 Standard @Snapmaker U1 (0.4+0.6 nozzle).json index 52e1aa9349..63bb6ea170 100644 --- a/resources/profiles/Snapmaker/process/0.20 Standard @Snapmaker U1 (0.4+0.6 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.20 Standard @Snapmaker U1 (0.4+0.6 nozzle).json @@ -20,6 +20,7 @@ "smooth_coefficient": "150", "overhang_totally_speed": "50", "ooze_prevention": "1", + "prime_tower_width": "30", "slowdown_for_curled_perimeters": "0", "standby_temperature_delta": "-150", "wipe_tower_filament": "0", diff --git a/resources/profiles/Snapmaker/process/0.20 Standard @Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/process/0.20 Standard @Snapmaker U1 (0.6 nozzle).json index f5ab699928..d1bb174a62 100644 --- a/resources/profiles/Snapmaker/process/0.20 Standard @Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.20 Standard @Snapmaker U1 (0.6 nozzle).json @@ -13,6 +13,7 @@ "Snapmaker U1 (0.6 nozzle)" ], "ooze_prevention": "1", + "prime_tower_width": "30", "slowdown_for_curled_perimeters": "0", "standby_temperature_delta": "-150", "wipe_tower_filament": "0", diff --git a/resources/profiles/Snapmaker/process/0.20 Strength @Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/process/0.20 Strength @Snapmaker U1 (0.4 nozzle).json index 4defa45a16..91c5995b92 100644 --- a/resources/profiles/Snapmaker/process/0.20 Strength @Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.20 Strength @Snapmaker U1 (0.4 nozzle).json @@ -15,6 +15,18 @@ "Snapmaker U1 (0.4 nozzle)" ], "ooze_prevention": "1", + "prime_tower_width": "30", + "prime_volume": "45", "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150" + "standby_temperature_delta": "-150", + "wipe_tower_filament": "0", + "prime_tower_brim_width": "5", + "wipe_tower_cone_angle": "15", + "wipe_tower_extra_spacing": "120%", + "enable_arc_fitting": "0", + "precise_outer_wall": "1", + "support_type": "tree(auto)", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_wall_type": "rib", + "gap_fill_target": "topbottom" } diff --git a/resources/profiles/Snapmaker/process/0.20 Support @Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/process/0.20 Support @Snapmaker U1 (0.4 nozzle).json index 36a3172b48..0cb85cc26d 100644 --- a/resources/profiles/Snapmaker/process/0.20 Support @Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.20 Support @Snapmaker U1 (0.4 nozzle).json @@ -12,6 +12,8 @@ "Snapmaker U1 (0.4 nozzle)" ], "ooze_prevention": "1", + "prime_tower_width": "40", + "prime_volume": "15", "slowdown_for_curled_perimeters": "0", "standby_temperature_delta": "-150", "wipe_tower_filament": "0", diff --git a/resources/profiles/Snapmaker/process/0.20 Support W @Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/process/0.20 Support W @Snapmaker U1 (0.4 nozzle).json index 37ba596b63..3d1887bea9 100644 --- a/resources/profiles/Snapmaker/process/0.20 Support W @Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.20 Support W @Snapmaker U1 (0.4 nozzle).json @@ -18,6 +18,8 @@ "Snapmaker U1 (0.4 nozzle)" ], "ooze_prevention": "1", + "prime_tower_width": "50", + "prime_volume": "38", "slowdown_for_curled_perimeters": "0", "standby_temperature_delta": "-150" } diff --git a/resources/profiles/Snapmaker/process/0.24 Draft @Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/process/0.24 Draft @Snapmaker U1 (0.4 nozzle).json index a4c10a8ad1..f98007b6e1 100644 --- a/resources/profiles/Snapmaker/process/0.24 Draft @Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.24 Draft @Snapmaker U1 (0.4 nozzle).json @@ -12,6 +12,18 @@ "Snapmaker U1 (0.4 nozzle)" ], "ooze_prevention": "1", + "prime_tower_width": "30", + "prime_volume": "54", "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150" + "standby_temperature_delta": "-150", + "wipe_tower_filament": "0", + "prime_tower_brim_width": "5", + "wipe_tower_cone_angle": "15", + "wipe_tower_extra_spacing": "120%", + "enable_arc_fitting": "0", + "precise_outer_wall": "1", + "support_type": "tree(auto)", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_wall_type": "rib", + "gap_fill_target": "topbottom" } diff --git a/resources/profiles/Snapmaker/process/0.24 Standard @Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/process/0.24 Standard @Snapmaker U1 (0.6 nozzle).json index b8a8427baf..7a9f4e2154 100644 --- a/resources/profiles/Snapmaker/process/0.24 Standard @Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.24 Standard @Snapmaker U1 (0.6 nozzle).json @@ -1,41 +1,32 @@ { - "type": "process", - "name": "0.24 Standard @Snapmaker U1 (0.6 nozzle)", - "inherits": "fdm_process_U1_0.6_common", - "from": "system", - "instantiation": "true", - "layer_height": "0.24", - "initial_layer_print_height": "0.3", - "bridge_flow": "1", - "initial_layer_speed": "35", - "initial_layer_infill_speed": "55", - "sparse_infill_speed": "100", - "top_surface_speed": "150", - "bridge_speed": "30", - "overhang_3_4_speed": "15", - "setting_id": "BziJYuA5U5gWm6FM", - "description": "Compared with the default profile of a 0.6 mm nozzle, it has a smaller layer height, and results in less apparent layer lines and slight higher printing quality, but longer printing time.", - "elefant_foot_compensation": "0.15", - "smooth_coefficient": "150", - "overhang_totally_speed": "50", - "compatible_printers": [ - "Snapmaker U1 (0.6 nozzle)" - ], - "filter_out_gap_fill": "1", - "gap_fill_target": "topbottom", - "internal_bridge_speed": "100%", - "prime_tower_brim_width": "5", - "prime_tower_width": "30", - "prime_volume": "55", - "seam_gap": "15%", - "wipe_tower_extra_rib_length": "8", - "wipe_tower_extra_spacing": "120%", - "wipe_tower_wall_type": "rib", - "enable_arc_fitting": "0", - "ooze_prevention": "1", - "precise_outer_wall": "0", - "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150", - "support_threshold_angle": "35", - "support_type": "tree(auto)" + "type": "process", + "name": "0.24 Standard @Snapmaker U1 (0.6 nozzle)", + "inherits": "fdm_process_U1_0.24_nozzle_0.6", + "from": "system", + "setting_id": "BziJYuA5U5gWm6FM", + "instantiation": "true", + "description": "Compared with the default profile of a 0.6 mm nozzle, it has a smaller layer height, and results in less apparent layer lines and slight higher printing quality, but longer printing time.", + "elefant_foot_compensation": "0.15", + "smooth_coefficient": "150", + "overhang_totally_speed": "50", + "compatible_printers": [ + "Snapmaker U1 (0.6 nozzle)" + ], + "filter_out_gap_fill": "1", + "gap_fill_target": "topbottom", + "internal_bridge_speed": "100%", + "prime_tower_brim_width": "5", + "prime_tower_width": "30", + "prime_volume": "55", + "seam_gap": "15%", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_extra_spacing": "120%", + "wipe_tower_wall_type": "rib", + "enable_arc_fitting": "0", + "ooze_prevention": "1", + "precise_outer_wall": "1", + "slowdown_for_curled_perimeters": "0", + "standby_temperature_delta": "-150", + "support_threshold_angle": "35", + "support_type": "tree(auto)" } diff --git a/resources/profiles/Snapmaker/process/0.24 Standard @Snapmaker U1 (0.8 nozzle).json b/resources/profiles/Snapmaker/process/0.24 Standard @Snapmaker U1 (0.8 nozzle).json index 1b13e745d2..d9e1a2bf94 100644 --- a/resources/profiles/Snapmaker/process/0.24 Standard @Snapmaker U1 (0.8 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.24 Standard @Snapmaker U1 (0.8 nozzle).json @@ -1,43 +1,32 @@ { - "type": "process", - "name": "0.24 Standard @Snapmaker U1 (0.8 nozzle)", - "inherits": "fdm_process_U1_0.8_common", - "from": "system", - "instantiation": "true", - "layer_height": "0.24", - "initial_layer_print_height": "0.4", - "bridge_flow": "1", - "top_surface_pattern": "monotonic", - "initial_layer_speed": "35", - "initial_layer_infill_speed": "55", - "sparse_infill_speed": "100", - "top_surface_speed": "150", - "bridge_speed": "30", - "overhang_3_4_speed": "25", - "overhang_4_4_speed": "5", - "setting_id": "QcJ5p9h0eYUb91ak", - "description": "Compared with the default profile of a 0.6 mm nozzle, it has a smaller layer height, and results in less apparent layer lines and slight higher printing quality, but longer printing time.", - "elefant_foot_compensation": "0.15", - "smooth_coefficient": "150", - "overhang_totally_speed": "50", - "compatible_printers": [ - "Snapmaker U1 (0.8 nozzle)" - ], - "filter_out_gap_fill": "1", - "gap_fill_target": "topbottom", - "internal_bridge_speed": "100%", - "min_width_top_surface": "90", - "prime_tower_brim_width": "5", - "prime_tower_width": "30", - "prime_volume": "54", - "seam_gap": "15%", - "wipe_tower_extra_rib_length": "8", - "wipe_tower_extra_spacing": "120%", - "wipe_tower_wall_type": "rib", - "enable_arc_fitting": "0", - "ooze_prevention": "1", - "precise_outer_wall": "0", - "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150", - "support_type": "tree(auto)" + "type": "process", + "name": "0.24 Standard @Snapmaker U1 (0.8 nozzle)", + "inherits": "fdm_process_U1_0.24_nozzle_0.8", + "from": "system", + "setting_id": "QcJ5p9h0eYUb91ak", + "instantiation": "true", + "description": "Compared with the default profile of a 0.6 mm nozzle, it has a smaller layer height, and results in less apparent layer lines and slight higher printing quality, but longer printing time.", + "elefant_foot_compensation": "0.15", + "smooth_coefficient": "150", + "overhang_totally_speed": "50", + "compatible_printers": [ + "Snapmaker U1 (0.8 nozzle)" + ], + "filter_out_gap_fill": "1", + "gap_fill_target": "topbottom", + "internal_bridge_speed": "100%", + "min_width_top_surface": "90", + "prime_tower_brim_width": "5", + "prime_tower_width": "30", + "prime_volume": "54", + "seam_gap": "15%", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_extra_spacing": "120%", + "wipe_tower_wall_type": "rib", + "enable_arc_fitting": "0", + "ooze_prevention": "1", + "precise_outer_wall": "1", + "slowdown_for_curled_perimeters": "0", + "standby_temperature_delta": "-150", + "support_type": "tree(auto)" } diff --git a/resources/profiles/Snapmaker/process/0.28 Extra Draft @Snapmaker U1 (0.4 nozzle).json b/resources/profiles/Snapmaker/process/0.28 Extra Draft @Snapmaker U1 (0.4 nozzle).json index 4adf06f100..4382b11e41 100644 --- a/resources/profiles/Snapmaker/process/0.28 Extra Draft @Snapmaker U1 (0.4 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.28 Extra Draft @Snapmaker U1 (0.4 nozzle).json @@ -11,5 +11,19 @@ "compatible_printers": [ "Snapmaker U1 (0.4 nozzle)" ], - "slowdown_for_curled_perimeters": "0" + "prime_tower_width": "30", + "prime_volume": "63", + "slowdown_for_curled_perimeters": "0", + "wipe_tower_filament": "0", + "prime_tower_brim_width": "5", + "wipe_tower_cone_angle": "15", + "wipe_tower_extra_spacing": "120%", + "enable_arc_fitting": "0", + "ooze_prevention": "1", + "precise_outer_wall": "1", + "standby_temperature_delta": "-150", + "support_type": "tree(auto)", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_wall_type": "rib", + "gap_fill_target": "topbottom" } diff --git a/resources/profiles/Snapmaker/process/0.30 Draft @Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/process/0.30 Draft @Snapmaker U1 (0.6 nozzle).json index 8f8e6a1d15..52eb63a64d 100644 --- a/resources/profiles/Snapmaker/process/0.30 Draft @Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.30 Draft @Snapmaker U1 (0.6 nozzle).json @@ -13,6 +13,8 @@ "Snapmaker U1 (0.6 nozzle)" ], "ooze_prevention": "1", + "prime_tower_width": "30", + "prime_volume": "68", "slowdown_for_curled_perimeters": "0", "standby_temperature_delta": "-150", "wipe_tower_filament": "0", diff --git a/resources/profiles/Snapmaker/process/0.30 Standard @Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/process/0.30 Standard @Snapmaker U1 (0.6 nozzle).json index 3ed454a7d5..e2d5c192bb 100644 --- a/resources/profiles/Snapmaker/process/0.30 Standard @Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.30 Standard @Snapmaker U1 (0.6 nozzle).json @@ -1,40 +1,31 @@ { - "type": "process", - "name": "0.30 Standard @Snapmaker U1 (0.6 nozzle)", - "inherits": "fdm_process_U1_0.6_common", - "from": "system", - "instantiation": "true", - "layer_height": "0.3", - "initial_layer_print_height": "0.3", - "bridge_flow": "1", - "initial_layer_speed": "35", - "initial_layer_infill_speed": "55", - "sparse_infill_speed": "100", - "top_surface_speed": "150", - "bridge_speed": "30", - "overhang_3_4_speed": "15", - "setting_id": "TWxp6qgz1DJa8Ngo", - "description": "It has a big layer height, and results in apparent layer lines and ordinary printing quality and printing time.", - "elefant_foot_compensation": "0.15", - "smooth_coefficient": "150", - "overhang_totally_speed": "50", - "compatible_printers": [ - "Snapmaker U1 (0.6 nozzle)" - ], - "filter_out_gap_fill": "1", - "gap_fill_target": "topbottom", - "internal_bridge_speed": "100%", - "prime_tower_brim_width": "5", - "prime_tower_width": "30", - "prime_volume": "68", - "seam_gap": "15%", - "wipe_tower_extra_rib_length": "8", - "wipe_tower_extra_spacing": "120%", - "wipe_tower_wall_type": "rib", - "ooze_prevention": "1", - "precise_outer_wall": "0", - "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150", - "support_threshold_angle": "35", - "support_type": "tree(auto)" + "type": "process", + "name": "0.30 Standard @Snapmaker U1 (0.6 nozzle)", + "inherits": "fdm_process_U1_0.30_nozzle_0.6", + "from": "system", + "setting_id": "TWxp6qgz1DJa8Ngo", + "instantiation": "true", + "description": "It has a big layer height, and results in apparent layer lines and ordinary printing quality and printing time.", + "elefant_foot_compensation": "0.15", + "smooth_coefficient": "150", + "overhang_totally_speed": "50", + "compatible_printers": [ + "Snapmaker U1 (0.6 nozzle)" + ], + "filter_out_gap_fill": "1", + "gap_fill_target": "topbottom", + "internal_bridge_speed": "100%", + "prime_tower_brim_width": "5", + "prime_tower_width": "30", + "prime_volume": "68", + "seam_gap": "15%", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_extra_spacing": "120%", + "wipe_tower_wall_type": "rib", + "ooze_prevention": "1", + "precise_outer_wall": "1", + "slowdown_for_curled_perimeters": "0", + "standby_temperature_delta": "-150", + "support_threshold_angle": "35", + "support_type": "tree(auto)" } diff --git a/resources/profiles/Snapmaker/process/0.30 Strength @Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/process/0.30 Strength @Snapmaker U1 (0.6 nozzle).json index ce6563d960..c216eb09d2 100644 --- a/resources/profiles/Snapmaker/process/0.30 Strength @Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.30 Strength @Snapmaker U1 (0.6 nozzle).json @@ -1,42 +1,33 @@ { - "type": "process", - "name": "0.30 Strength @Snapmaker U1 (0.6 nozzle)", - "inherits": "fdm_process_U1_0.6_common", - "from": "system", - "instantiation": "true", - "layer_height": "0.3", - "initial_layer_print_height": "0.3", - "bridge_flow": "1", - "initial_layer_speed": "35", - "initial_layer_infill_speed": "55", - "sparse_infill_speed": "100", - "top_surface_speed": "150", - "bridge_speed": "30", - "overhang_3_4_speed": "15", - "setting_id": "CBaL0vgwtgsd6Snj", - "description": "Compared with the default profile of a 0.6 mm nozzle, it has more wall loops and a higher sparse infill density. So, it results in higher strength of the prints, but more filament consumption and longer printing time.", - "elefant_foot_compensation": "0.15", - "sparse_infill_density": "25%", - "wall_loops": "4", - "smooth_coefficient": "150", - "overhang_totally_speed": "50", - "compatible_printers": [ - "Snapmaker U1 (0.6 nozzle)" - ], - "filter_out_gap_fill": "1", - "gap_fill_target": "topbottom", - "internal_bridge_speed": "100%", - "prime_tower_width": "30", - "prime_volume": "68", - "seam_gap": "15%", - "wipe_tower_extra_rib_length": "8", - "wipe_tower_wall_type": "rib", - "enable_arc_fitting": "0", - "ooze_prevention": "1", - "precise_outer_wall": "0", - "prime_tower_brim_width": "5", - "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150", - "support_type": "tree(auto)", - "wipe_tower_extra_spacing": "120%" + "type": "process", + "name": "0.30 Strength @Snapmaker U1 (0.6 nozzle)", + "inherits": "fdm_process_U1_0.30_nozzle_0.6", + "from": "system", + "setting_id": "CBaL0vgwtgsd6Snj", + "instantiation": "true", + "description": "Compared with the default profile of a 0.6 mm nozzle, it has more wall loops and a higher sparse infill density. So, it results in higher strength of the prints, but more filament consumption and longer printing time.", + "elefant_foot_compensation": "0.15", + "sparse_infill_density": "25%", + "wall_loops": "4", + "smooth_coefficient": "150", + "overhang_totally_speed": "50", + "compatible_printers": [ + "Snapmaker U1 (0.6 nozzle)" + ], + "filter_out_gap_fill": "1", + "gap_fill_target": "topbottom", + "internal_bridge_speed": "100%", + "prime_tower_width": "30", + "prime_volume": "68", + "seam_gap": "15%", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_wall_type": "rib", + "enable_arc_fitting": "0", + "ooze_prevention": "1", + "precise_outer_wall": "1", + "prime_tower_brim_width": "5", + "slowdown_for_curled_perimeters": "0", + "standby_temperature_delta": "-150", + "support_type": "tree(auto)", + "wipe_tower_extra_spacing": "120%" } diff --git a/resources/profiles/Snapmaker/process/0.32 Standard @Snapmaker U1 (0.8 nozzle).json b/resources/profiles/Snapmaker/process/0.32 Standard @Snapmaker U1 (0.8 nozzle).json index 47ac276acc..d2c20559fd 100644 --- a/resources/profiles/Snapmaker/process/0.32 Standard @Snapmaker U1 (0.8 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.32 Standard @Snapmaker U1 (0.8 nozzle).json @@ -1,44 +1,34 @@ { - "type": "process", - "name": "0.32 Standard @Snapmaker U1 (0.8 nozzle)", - "inherits": "fdm_process_U1_0.8_common", - "from": "system", - "instantiation": "true", - "layer_height": "0.32", - "initial_layer_print_height": "0.4", - "bridge_flow": "0.7", - "top_surface_pattern": "monotonic", - "initial_layer_speed": "35", - "initial_layer_infill_speed": "55", - "sparse_infill_speed": "100", - "top_surface_speed": "150", - "bridge_speed": "30", - "overhang_3_4_speed": "25", - "overhang_4_4_speed": "5", - "setting_id": "iVRz2B0VBIBL1xVd", - "description": "Compared with the default profile of a 0.8 mm nozzle, it has a slightly smaller layer height, and results in slightly less but still apparent layer lines and slightly higher printing quality, but longer printing time in some printing cases.", - "elefant_foot_compensation": "0.15", - "smooth_coefficient": "150", - "overhang_totally_speed": "50", - "compatible_printers": [ - "Snapmaker U1 (0.8 nozzle)" - ], - "filter_out_gap_fill": "1", - "gap_fill_target": "topbottom", - "internal_bridge_speed": "100%", - "prime_tower_brim_width": "5", - "prime_tower_width": "30", - "prime_volume": "72", - "seam_gap": "15%", - "wipe_tower_extra_rib_length": "8", - "wipe_tower_extra_spacing": "120%", - "wipe_tower_wall_type": "rib", - "bridge_density": "70%", - "enable_arc_fitting": "0", - "ooze_prevention": "1", - "precise_outer_wall": "0", - "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150", - "support_threshold_angle": "35", - "support_type": "tree(auto)" + "type": "process", + "name": "0.32 Standard @Snapmaker U1 (0.8 nozzle)", + "inherits": "fdm_process_U1_0.32_nozzle_0.8", + "from": "system", + "setting_id": "iVRz2B0VBIBL1xVd", + "instantiation": "true", + "description": "Compared with the default profile of a 0.8 mm nozzle, it has a slightly smaller layer height, and results in slightly less but still apparent layer lines and slightly higher printing quality, but longer printing time in some printing cases.", + "elefant_foot_compensation": "0.15", + "smooth_coefficient": "150", + "overhang_totally_speed": "50", + "compatible_printers": [ + "Snapmaker U1 (0.8 nozzle)" + ], + "filter_out_gap_fill": "1", + "gap_fill_target": "topbottom", + "internal_bridge_speed": "100%", + "prime_tower_brim_width": "5", + "prime_tower_width": "30", + "prime_volume": "72", + "seam_gap": "15%", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_extra_spacing": "120%", + "wipe_tower_wall_type": "rib", + "bridge_density": "70%", + "bridge_flow": "0.7", + "enable_arc_fitting": "0", + "ooze_prevention": "1", + "precise_outer_wall": "1", + "slowdown_for_curled_perimeters": "0", + "standby_temperature_delta": "-150", + "support_threshold_angle": "35", + "support_type": "tree(auto)" } diff --git a/resources/profiles/Snapmaker/process/0.36 Standard @Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/process/0.36 Standard @Snapmaker U1 (0.6 nozzle).json index ddd97a76c3..44ea3bb3f4 100644 --- a/resources/profiles/Snapmaker/process/0.36 Standard @Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.36 Standard @Snapmaker U1 (0.6 nozzle).json @@ -1,41 +1,32 @@ { - "type": "process", - "name": "0.36 Standard @Snapmaker U1 (0.6 nozzle)", - "inherits": "fdm_process_U1_0.6_common", - "from": "system", - "instantiation": "true", - "layer_height": "0.36", - "initial_layer_print_height": "0.3", - "bridge_flow": "1", - "initial_layer_speed": "35", - "initial_layer_infill_speed": "55", - "sparse_infill_speed": "100", - "top_surface_speed": "150", - "bridge_speed": "30", - "overhang_3_4_speed": "15", - "setting_id": "C0PiCFd3P222knX9", - "description": "Compared with the default profile of a 0.6 mm nozzle, it has a bigger layer height, and results in more apparent layer lines and lower printing quality, but shorter printing time in some printing cases.", - "elefant_foot_compensation": "0.15", - "smooth_coefficient": "150", - "overhang_totally_speed": "50", - "compatible_printers": [ - "Snapmaker U1 (0.6 nozzle)" - ], - "filter_out_gap_fill": "1", - "gap_fill_target": "topbottom", - "internal_bridge_speed": "100%", - "prime_tower_brim_width": "5", - "prime_tower_width": "30", - "prime_volume": "81", - "seam_gap": "15%", - "wipe_tower_extra_rib_length": "8", - "wipe_tower_extra_spacing": "120%", - "wipe_tower_wall_type": "rib", - "enable_arc_fitting": "0", - "ooze_prevention": "1", - "precise_outer_wall": "0", - "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150", - "support_threshold_angle": "35", - "support_type": "tree(auto)" + "type": "process", + "name": "0.36 Standard @Snapmaker U1 (0.6 nozzle)", + "inherits": "fdm_process_U1_0.36_nozzle_0.6", + "from": "system", + "setting_id": "C0PiCFd3P222knX9", + "instantiation": "true", + "description": "Compared with the default profile of a 0.6 mm nozzle, it has a bigger layer height, and results in more apparent layer lines and lower printing quality, but shorter printing time in some printing cases.", + "elefant_foot_compensation": "0.15", + "smooth_coefficient": "150", + "overhang_totally_speed": "50", + "compatible_printers": [ + "Snapmaker U1 (0.6 nozzle)" + ], + "filter_out_gap_fill": "1", + "gap_fill_target": "topbottom", + "internal_bridge_speed": "100%", + "prime_tower_brim_width": "5", + "prime_tower_width": "30", + "prime_volume": "81", + "seam_gap": "15%", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_extra_spacing": "120%", + "wipe_tower_wall_type": "rib", + "enable_arc_fitting": "0", + "ooze_prevention": "1", + "precise_outer_wall": "1", + "slowdown_for_curled_perimeters": "0", + "standby_temperature_delta": "-150", + "support_threshold_angle": "35", + "support_type": "tree(auto)" } diff --git a/resources/profiles/Snapmaker/process/0.40 Extra Draft @Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/process/0.40 Extra Draft @Snapmaker U1 (0.6 nozzle).json index 265c91eb35..37238fb0ea 100644 --- a/resources/profiles/Snapmaker/process/0.40 Extra Draft @Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.40 Extra Draft @Snapmaker U1 (0.6 nozzle).json @@ -13,6 +13,8 @@ "Snapmaker U1 (0.6 nozzle)" ], "ooze_prevention": "1", + "prime_tower_width": "30", + "prime_volume": "90", "slowdown_for_curled_perimeters": "0", "standby_temperature_delta": "-150", "wipe_tower_filament": "0", diff --git a/resources/profiles/Snapmaker/process/0.40 Standard @Snapmaker U1 (0.8 nozzle).json b/resources/profiles/Snapmaker/process/0.40 Standard @Snapmaker U1 (0.8 nozzle).json index 24edfabf6d..8d00eb9fab 100644 --- a/resources/profiles/Snapmaker/process/0.40 Standard @Snapmaker U1 (0.8 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.40 Standard @Snapmaker U1 (0.8 nozzle).json @@ -1,45 +1,35 @@ { - "type": "process", - "name": "0.40 Standard @Snapmaker U1 (0.8 nozzle)", - "inherits": "fdm_process_U1_0.8_common", - "from": "system", - "instantiation": "true", - "layer_height": "0.4", - "initial_layer_print_height": "0.4", - "bridge_flow": "0.8", - "top_surface_pattern": "monotonic", - "initial_layer_speed": "35", - "initial_layer_infill_speed": "55", - "sparse_infill_speed": "100", - "top_surface_speed": "150", - "bridge_speed": "30", - "overhang_3_4_speed": "25", - "overhang_4_4_speed": "5", - "setting_id": "VqkPgUtZi159BKpJ", - "description": "It has a very big layer height, and results in very apparent layer lines, low printing quality and general printing time.", - "elefant_foot_compensation": "0.15", - "smooth_coefficient": "150", - "overhang_totally_speed": "50", - "compatible_printers": [ - "Snapmaker U1 (0.8 nozzle)" - ], - "filter_out_gap_fill": "1", - "gap_fill_target": "topbottom", - "internal_bridge_speed": "100%", - "prime_tower_brim_width": "5", - "prime_tower_width": "30", - "prime_volume": "90", - "seam_gap": "15%", - "wipe_tower_extra_rib_length": "8", - "wipe_tower_extra_spacing": "120%", - "wipe_tower_wall_type": "rib", - "bridge_density": "60%", - "enable_arc_fitting": "0", - "min_width_top_surface": "200%", - "ooze_prevention": "1", - "precise_outer_wall": "0", - "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150", - "support_threshold_angle": "40", - "support_type": "tree(auto)" + "type": "process", + "name": "0.40 Standard @Snapmaker U1 (0.8 nozzle)", + "inherits": "fdm_process_U1_0.40_nozzle_0.8", + "from": "system", + "setting_id": "VqkPgUtZi159BKpJ", + "instantiation": "true", + "description": "It has a very big layer height, and results in very apparent layer lines, low printing quality and general printing time.", + "elefant_foot_compensation": "0.15", + "smooth_coefficient": "150", + "overhang_totally_speed": "50", + "compatible_printers": [ + "Snapmaker U1 (0.8 nozzle)" + ], + "filter_out_gap_fill": "1", + "gap_fill_target": "topbottom", + "internal_bridge_speed": "100%", + "prime_tower_brim_width": "5", + "prime_tower_width": "30", + "prime_volume": "90", + "seam_gap": "15%", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_extra_spacing": "120%", + "wipe_tower_wall_type": "rib", + "bridge_density": "60%", + "bridge_flow": "0.8", + "enable_arc_fitting": "0", + "min_width_top_surface": "200%", + "ooze_prevention": "1", + "precise_outer_wall": "1", + "slowdown_for_curled_perimeters": "0", + "standby_temperature_delta": "-150", + "support_threshold_angle": "40", + "support_type": "tree(auto)" } diff --git a/resources/profiles/Snapmaker/process/0.42 Standard @Snapmaker U1 (0.6 nozzle).json b/resources/profiles/Snapmaker/process/0.42 Standard @Snapmaker U1 (0.6 nozzle).json index d65ba8e688..d3e4bf7f07 100644 --- a/resources/profiles/Snapmaker/process/0.42 Standard @Snapmaker U1 (0.6 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.42 Standard @Snapmaker U1 (0.6 nozzle).json @@ -1,41 +1,32 @@ { - "type": "process", - "name": "0.42 Standard @Snapmaker U1 (0.6 nozzle)", - "inherits": "fdm_process_U1_0.6_common", - "from": "system", - "instantiation": "true", - "layer_height": "0.42", - "initial_layer_print_height": "0.3", - "bridge_flow": "1", - "initial_layer_speed": "35", - "initial_layer_infill_speed": "55", - "sparse_infill_speed": "100", - "top_surface_speed": "150", - "bridge_speed": "30", - "overhang_3_4_speed": "15", - "setting_id": "SVJzhQA3KQetRZmo", - "description": "Compared with the default profile of a 0.6 mm nozzle, it has a bigger layer height, and results in much more apparent layer lines and much lower printing quality, but shorter printing time in some printing cases.", - "elefant_foot_compensation": "0.15", - "smooth_coefficient": "150", - "overhang_totally_speed": "50", - "compatible_printers": [ - "Snapmaker U1 (0.6 nozzle)" - ], - "filter_out_gap_fill": "1", - "gap_fill_target": "topbottom", - "internal_bridge_speed": "100%", - "precise_z_height": "0", - "prime_tower_brim_width": "5", - "prime_tower_width": "30", - "prime_volume": "95", - "seam_gap": "15%", - "wipe_tower_extra_rib_length": "8", - "wipe_tower_extra_spacing": "120%", - "wipe_tower_wall_type": "rib", - "enable_arc_fitting": "0", - "ooze_prevention": "1", - "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150", - "support_threshold_angle": "35", - "support_type": "tree(auto)" + "type": "process", + "name": "0.42 Standard @Snapmaker U1 (0.6 nozzle)", + "inherits": "fdm_process_U1_0.42_nozzle_0.6", + "from": "system", + "setting_id": "SVJzhQA3KQetRZmo", + "instantiation": "true", + "description": "Compared with the default profile of a 0.6 mm nozzle, it has a bigger layer height, and results in much more apparent layer lines and much lower printing quality, but shorter printing time in some printing cases.", + "elefant_foot_compensation": "0.15", + "smooth_coefficient": "150", + "overhang_totally_speed": "50", + "compatible_printers": [ + "Snapmaker U1 (0.6 nozzle)" + ], + "filter_out_gap_fill": "1", + "gap_fill_target": "topbottom", + "internal_bridge_speed": "100%", + "precise_z_height": "0", + "prime_tower_brim_width": "5", + "prime_tower_width": "30", + "prime_volume": "95", + "seam_gap": "15%", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_extra_spacing": "120%", + "wipe_tower_wall_type": "rib", + "enable_arc_fitting": "0", + "ooze_prevention": "1", + "slowdown_for_curled_perimeters": "0", + "standby_temperature_delta": "-150", + "support_threshold_angle": "35", + "support_type": "tree(auto)" } diff --git a/resources/profiles/Snapmaker/process/0.48 Standard @Snapmaker U1 (0.8 nozzle).json b/resources/profiles/Snapmaker/process/0.48 Standard @Snapmaker U1 (0.8 nozzle).json index 08e054619e..d6cdf607dd 100644 --- a/resources/profiles/Snapmaker/process/0.48 Standard @Snapmaker U1 (0.8 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.48 Standard @Snapmaker U1 (0.8 nozzle).json @@ -1,43 +1,32 @@ { - "type": "process", - "name": "0.48 Standard @Snapmaker U1 (0.8 nozzle)", - "inherits": "fdm_process_U1_0.8_common", - "from": "system", - "instantiation": "true", - "layer_height": "0.48", - "initial_layer_print_height": "0.4", - "bridge_flow": "1", - "top_surface_pattern": "monotonic", - "initial_layer_speed": "35", - "initial_layer_infill_speed": "55", - "sparse_infill_speed": "100", - "top_surface_speed": "150", - "bridge_speed": "30", - "overhang_3_4_speed": "25", - "overhang_4_4_speed": "5", - "setting_id": "5H6JWChFEo3zqoHQ", - "description": "Compared with the default profile of a 0.8 mm nozzle, it has a bigger layer height, and results in very apparent layer lines and much lower printing quality, but shorter printing time in some printing cases.", - "elefant_foot_compensation": "0.15", - "smooth_coefficient": "150", - "overhang_totally_speed": "50", - "compatible_printers": [ - "Snapmaker U1 (0.8 nozzle)" - ], - "filter_out_gap_fill": "1", - "gap_fill_target": "topbottom", - "internal_bridge_speed": "100%", - "prime_tower_brim_width": "5", - "prime_tower_width": "30", - "prime_volume": "108", - "seam_gap": "15%", - "wipe_tower_extra_rib_length": "8", - "wipe_tower_extra_spacing": "120%", - "wipe_tower_wall_type": "rib", - "enable_arc_fitting": "0", - "ooze_prevention": "1", - "precise_outer_wall": "0", - "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150", - "support_threshold_angle": "35", - "support_type": "tree(auto)" + "type": "process", + "name": "0.48 Standard @Snapmaker U1 (0.8 nozzle)", + "inherits": "fdm_process_U1_0.48_nozzle_0.8", + "from": "system", + "setting_id": "5H6JWChFEo3zqoHQ", + "instantiation": "true", + "description": "Compared with the default profile of a 0.8 mm nozzle, it has a bigger layer height, and results in very apparent layer lines and much lower printing quality, but shorter printing time in some printing cases.", + "elefant_foot_compensation": "0.15", + "smooth_coefficient": "150", + "overhang_totally_speed": "50", + "compatible_printers": [ + "Snapmaker U1 (0.8 nozzle)" + ], + "filter_out_gap_fill": "1", + "gap_fill_target": "topbottom", + "internal_bridge_speed": "100%", + "prime_tower_brim_width": "5", + "prime_tower_width": "30", + "prime_volume": "108", + "seam_gap": "15%", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_extra_spacing": "120%", + "wipe_tower_wall_type": "rib", + "enable_arc_fitting": "0", + "ooze_prevention": "1", + "precise_outer_wall": "1", + "slowdown_for_curled_perimeters": "0", + "standby_temperature_delta": "-150", + "support_threshold_angle": "35", + "support_type": "tree(auto)" } diff --git a/resources/profiles/Snapmaker/process/0.56 Standard @Snapmaker U1 (0.8 nozzle).json b/resources/profiles/Snapmaker/process/0.56 Standard @Snapmaker U1 (0.8 nozzle).json index 86b571d1f5..9f67114c20 100644 --- a/resources/profiles/Snapmaker/process/0.56 Standard @Snapmaker U1 (0.8 nozzle).json +++ b/resources/profiles/Snapmaker/process/0.56 Standard @Snapmaker U1 (0.8 nozzle).json @@ -1,41 +1,31 @@ { - "type": "process", - "name": "0.56 Standard @Snapmaker U1 (0.8 nozzle)", - "inherits": "fdm_process_U1_0.8_common", - "from": "system", - "instantiation": "true", - "layer_height": "0.56", - "initial_layer_print_height": "0.4", - "bridge_flow": "1", - "top_surface_pattern": "monotonic", - "initial_layer_speed": "35", - "initial_layer_infill_speed": "55", - "sparse_infill_speed": "100", - "top_surface_speed": "150", - "bridge_speed": "30", - "overhang_3_4_speed": "25", - "overhang_4_4_speed": "5", - "setting_id": "JZEIXOUT33c7MrZj", - "description": "Compared with the default profile of a 0.8 mm nozzle, it has a much bigger layer height, and results in extremely apparent layer lines and much lower printing quality, but much shorter printing time in some printing cases.", - "elefant_foot_compensation": "0.15", - "smooth_coefficient": "150", - "overhang_totally_speed": "50", - "compatible_printers": [ - "Snapmaker U1 (0.8 nozzle)" - ], - "filter_out_gap_fill": "1", - "gap_fill_target": "topbottom", - "prime_tower_brim_width": "5", - "prime_tower_width": "30", - "prime_volume": "126", - "wipe_tower_extra_rib_length": "8", - "wipe_tower_extra_spacing": "120%", - "wipe_tower_wall_type": "rib", - "enable_arc_fitting": "0", - "ooze_prevention": "1", - "precise_outer_wall": "0", - "slowdown_for_curled_perimeters": "0", - "standby_temperature_delta": "-150", - "support_threshold_angle": "35", - "support_type": "tree(auto)" + "type": "process", + "name": "0.56 Standard @Snapmaker U1 (0.8 nozzle)", + "inherits": "fdm_process_U1_0.56_nozzle_0.8", + "from": "system", + "setting_id": "JZEIXOUT33c7MrZj", + "instantiation": "true", + "description": "Compared with the default profile of a 0.8 mm nozzle, it has a much bigger layer height, and results in extremely apparent layer lines and much lower printing quality, but much shorter printing time in some printing cases.", + "elefant_foot_compensation": "0.15", + "smooth_coefficient": "150", + "overhang_totally_speed": "50", + "compatible_printers": [ + "Snapmaker U1 (0.8 nozzle)" + ], + "filter_out_gap_fill": "1", + "gap_fill_target": "topbottom", + "layer_height": "0.56", + "prime_tower_brim_width": "5", + "prime_tower_width": "30", + "prime_volume": "126", + "wipe_tower_extra_rib_length": "8", + "wipe_tower_extra_spacing": "120%", + "wipe_tower_wall_type": "rib", + "enable_arc_fitting": "0", + "ooze_prevention": "1", + "precise_outer_wall": "1", + "slowdown_for_curled_perimeters": "0", + "standby_temperature_delta": "-150", + "support_threshold_angle": "35", + "support_type": "tree(auto)" } diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1.json b/resources/profiles/Snapmaker/process/fdm_process_U1.json index 5d89a91866..72dab0c16b 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1.json @@ -33,7 +33,6 @@ "wall_loops": "2", "inner_wall_line_width": "0.45", "inner_wall_speed": "40", - "print_settings_id": "", "raft_layers": "0", "seam_position": "nearest", "skirt_distance": "2", diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.06_nozzle_0.2.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.06_nozzle_0.2.json new file mode 100644 index 0000000000..240fd4f425 --- /dev/null +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.06_nozzle_0.2.json @@ -0,0 +1,27 @@ +{ + "type": "process", + "name": "fdm_process_U1_0.06_nozzle_0.2", + "inherits": "fdm_process_U1_common", + "from": "system", + "instantiation": "false", + "layer_height": "0.06", + "initial_layer_print_height": "0.1", + "wall_loops": "4", + "bottom_shell_layers": "5", + "top_shell_layers": "7", + "bridge_flow": "1", + "line_width": "0.22", + "outer_wall_line_width": "0.22", + "initial_layer_line_width": "0.25", + "sparse_infill_line_width": "0.22", + "inner_wall_line_width": "0.22", + "internal_solid_infill_line_width": "0.22", + "support_line_width": "0.22", + "top_surface_line_width": "0.22", + "initial_layer_speed": "40", + "initial_layer_infill_speed": "70", + "sparse_infill_speed": "100", + "top_surface_speed": "150", + "support_top_z_distance": "0.06", + "support_bottom_z_distance": "0.06" +} diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.08_nozzle_0.2.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.08_nozzle_0.2.json new file mode 100644 index 0000000000..41a824fa3a --- /dev/null +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.08_nozzle_0.2.json @@ -0,0 +1,27 @@ +{ + "type": "process", + "name": "fdm_process_U1_0.08_nozzle_0.2", + "inherits": "fdm_process_U1_common", + "from": "system", + "instantiation": "false", + "layer_height": "0.08", + "initial_layer_print_height": "0.1", + "wall_loops": "4", + "bottom_shell_layers": "5", + "top_shell_layers": "7", + "bridge_flow": "1", + "line_width": "0.22", + "outer_wall_line_width": "0.22", + "initial_layer_line_width": "0.25", + "sparse_infill_line_width": "0.22", + "inner_wall_line_width": "0.22", + "internal_solid_infill_line_width": "0.22", + "support_line_width": "0.22", + "top_surface_line_width": "0.22", + "initial_layer_speed": "40", + "initial_layer_infill_speed": "70", + "sparse_infill_speed": "100", + "top_surface_speed": "150", + "support_top_z_distance": "0.08", + "support_bottom_z_distance": "0.08" +} diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.10_nozzle_0.2.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.10_nozzle_0.2.json new file mode 100644 index 0000000000..5ca58d7bd1 --- /dev/null +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.10_nozzle_0.2.json @@ -0,0 +1,27 @@ +{ + "type": "process", + "name": "fdm_process_U1_0.10_nozzle_0.2", + "inherits": "fdm_process_U1_common", + "from": "system", + "instantiation": "false", + "layer_height": "0.1", + "initial_layer_print_height": "0.1", + "wall_loops": "4", + "bottom_shell_layers": "5", + "top_shell_layers": "7", + "bridge_flow": "1", + "line_width": "0.22", + "outer_wall_line_width": "0.22", + "initial_layer_line_width": "0.25", + "sparse_infill_line_width": "0.22", + "inner_wall_line_width": "0.22", + "internal_solid_infill_line_width": "0.22", + "support_line_width": "0.22", + "top_surface_line_width": "0.22", + "initial_layer_speed": "40", + "initial_layer_infill_speed": "70", + "sparse_infill_speed": "100", + "top_surface_speed": "150", + "support_top_z_distance": "0.1", + "support_bottom_z_distance": "0.1" +} diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.12_nozzle_0.2.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.12_nozzle_0.2.json new file mode 100644 index 0000000000..8c70dd9dee --- /dev/null +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.12_nozzle_0.2.json @@ -0,0 +1,27 @@ +{ + "type": "process", + "name": "fdm_process_U1_0.12_nozzle_0.2", + "inherits": "fdm_process_U1_common", + "from": "system", + "instantiation": "false", + "layer_height": "0.12", + "initial_layer_print_height": "0.1", + "wall_loops": "4", + "bottom_shell_layers": "5", + "top_shell_layers": "7", + "bridge_flow": "1", + "line_width": "0.22", + "outer_wall_line_width": "0.22", + "initial_layer_line_width": "0.25", + "sparse_infill_line_width": "0.22", + "inner_wall_line_width": "0.22", + "internal_solid_infill_line_width": "0.22", + "support_line_width": "0.22", + "top_surface_line_width": "0.22", + "initial_layer_speed": "40", + "initial_layer_infill_speed": "70", + "sparse_infill_speed": "100", + "top_surface_speed": "150", + "support_top_z_distance": "0.12", + "support_bottom_z_distance": "0.12" +} diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.14_nozzle_0.2.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.14_nozzle_0.2.json new file mode 100644 index 0000000000..1ac42c9ffa --- /dev/null +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.14_nozzle_0.2.json @@ -0,0 +1,27 @@ +{ + "type": "process", + "name": "fdm_process_U1_0.14_nozzle_0.2", + "inherits": "fdm_process_U1_common", + "from": "system", + "instantiation": "false", + "layer_height": "0.14", + "initial_layer_print_height": "0.1", + "wall_loops": "4", + "bottom_shell_layers": "5", + "top_shell_layers": "7", + "bridge_flow": "1", + "line_width": "0.22", + "outer_wall_line_width": "0.22", + "initial_layer_line_width": "0.25", + "sparse_infill_line_width": "0.22", + "inner_wall_line_width": "0.22", + "internal_solid_infill_line_width": "0.22", + "support_line_width": "0.22", + "top_surface_line_width": "0.22", + "initial_layer_speed": "40", + "initial_layer_infill_speed": "70", + "sparse_infill_speed": "100", + "top_surface_speed": "150", + "support_top_z_distance": "0.14", + "support_bottom_z_distance": "0.14" +} diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.18_nozzle_0.6.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.18_nozzle_0.6.json new file mode 100644 index 0000000000..f4725a73c7 --- /dev/null +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.18_nozzle_0.6.json @@ -0,0 +1,26 @@ +{ + "type": "process", + "name": "fdm_process_U1_0.18_nozzle_0.6", + "inherits": "fdm_process_U1_common", + "from": "system", + "instantiation": "false", + "layer_height": "0.18", + "initial_layer_print_height": "0.3", + "bridge_flow": "1", + "line_width": "0.62", + "outer_wall_line_width": "0.62", + "initial_layer_line_width": "0.62", + "sparse_infill_line_width": "0.62", + "inner_wall_line_width": "0.62", + "internal_solid_infill_line_width": "0.62", + "support_line_width": "0.62", + "top_surface_line_width": "0.62", + "initial_layer_speed": "35", + "initial_layer_infill_speed": "55", + "sparse_infill_speed": "100", + "top_surface_speed": "150", + "bridge_speed": "30", + "overhang_3_4_speed": "15", + "support_top_z_distance": "0.18", + "support_bottom_z_distance": "0.18" +} diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.24_nozzle_0.6.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.24_nozzle_0.6.json new file mode 100644 index 0000000000..a014a375b3 --- /dev/null +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.24_nozzle_0.6.json @@ -0,0 +1,24 @@ +{ + "type": "process", + "name": "fdm_process_U1_0.24_nozzle_0.6", + "inherits": "fdm_process_U1_common", + "from": "system", + "instantiation": "false", + "layer_height": "0.24", + "initial_layer_print_height": "0.3", + "bridge_flow": "1", + "line_width": "0.62", + "outer_wall_line_width": "0.62", + "initial_layer_line_width": "0.62", + "sparse_infill_line_width": "0.62", + "inner_wall_line_width": "0.62", + "internal_solid_infill_line_width": "0.62", + "support_line_width": "0.62", + "top_surface_line_width": "0.62", + "initial_layer_speed": "35", + "initial_layer_infill_speed": "55", + "sparse_infill_speed": "100", + "top_surface_speed": "150", + "bridge_speed": "30", + "overhang_3_4_speed": "15" +} diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.24_nozzle_0.8.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.24_nozzle_0.8.json new file mode 100644 index 0000000000..e958e9a2ee --- /dev/null +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.24_nozzle_0.8.json @@ -0,0 +1,26 @@ +{ + "type": "process", + "name": "fdm_process_U1_0.24_nozzle_0.8", + "inherits": "fdm_process_U1_common", + "from": "system", + "instantiation": "false", + "layer_height": "0.24", + "initial_layer_print_height": "0.4", + "bridge_flow": "1", + "line_width": "0.82", + "outer_wall_line_width": "0.82", + "initial_layer_line_width": "0.82", + "sparse_infill_line_width": "0.82", + "inner_wall_line_width": "0.82", + "internal_solid_infill_line_width": "0.82", + "support_line_width": "0.82", + "top_surface_line_width": "0.82", + "top_surface_pattern": "monotonic", + "initial_layer_speed": "35", + "initial_layer_infill_speed": "55", + "sparse_infill_speed": "100", + "top_surface_speed": "150", + "bridge_speed": "30", + "overhang_3_4_speed": "25", + "overhang_4_4_speed": "5" +} diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.30_nozzle_0.6.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.30_nozzle_0.6.json new file mode 100644 index 0000000000..a44a9ca05a --- /dev/null +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.30_nozzle_0.6.json @@ -0,0 +1,24 @@ +{ + "type": "process", + "name": "fdm_process_U1_0.30_nozzle_0.6", + "inherits": "fdm_process_U1_common", + "from": "system", + "instantiation": "false", + "layer_height": "0.3", + "initial_layer_print_height": "0.3", + "bridge_flow": "1", + "line_width": "0.62", + "outer_wall_line_width": "0.62", + "initial_layer_line_width": "0.62", + "sparse_infill_line_width": "0.62", + "inner_wall_line_width": "0.62", + "internal_solid_infill_line_width": "0.62", + "support_line_width": "0.62", + "top_surface_line_width": "0.62", + "initial_layer_speed": "35", + "initial_layer_infill_speed": "55", + "sparse_infill_speed": "100", + "top_surface_speed": "150", + "bridge_speed": "30", + "overhang_3_4_speed": "15" +} diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.32_nozzle_0.8.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.32_nozzle_0.8.json new file mode 100644 index 0000000000..67d9ecdb62 --- /dev/null +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.32_nozzle_0.8.json @@ -0,0 +1,26 @@ +{ + "type": "process", + "name": "fdm_process_U1_0.32_nozzle_0.8", + "inherits": "fdm_process_U1_common", + "from": "system", + "instantiation": "false", + "layer_height": "0.32", + "initial_layer_print_height": "0.4", + "bridge_flow": "1", + "line_width": "0.82", + "outer_wall_line_width": "0.82", + "initial_layer_line_width": "0.82", + "sparse_infill_line_width": "0.82", + "inner_wall_line_width": "0.82", + "internal_solid_infill_line_width": "0.82", + "support_line_width": "0.82", + "top_surface_line_width": "0.82", + "top_surface_pattern": "monotonic", + "initial_layer_speed": "35", + "initial_layer_infill_speed": "55", + "sparse_infill_speed": "100", + "top_surface_speed": "150", + "bridge_speed": "30", + "overhang_3_4_speed": "25", + "overhang_4_4_speed": "5" +} diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.36_nozzle_0.6.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.36_nozzle_0.6.json new file mode 100644 index 0000000000..314965b603 --- /dev/null +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.36_nozzle_0.6.json @@ -0,0 +1,24 @@ +{ + "type": "process", + "name": "fdm_process_U1_0.36_nozzle_0.6", + "inherits": "fdm_process_U1_common", + "from": "system", + "instantiation": "false", + "layer_height": "0.36", + "initial_layer_print_height": "0.3", + "bridge_flow": "1", + "line_width": "0.62", + "outer_wall_line_width": "0.62", + "initial_layer_line_width": "0.62", + "sparse_infill_line_width": "0.62", + "inner_wall_line_width": "0.62", + "internal_solid_infill_line_width": "0.62", + "support_line_width": "0.62", + "top_surface_line_width": "0.62", + "initial_layer_speed": "35", + "initial_layer_infill_speed": "55", + "sparse_infill_speed": "100", + "top_surface_speed": "150", + "bridge_speed": "30", + "overhang_3_4_speed": "15" +} diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.40_nozzle_0.8.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.40_nozzle_0.8.json new file mode 100644 index 0000000000..ec19b56fa5 --- /dev/null +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.40_nozzle_0.8.json @@ -0,0 +1,26 @@ +{ + "type": "process", + "name": "fdm_process_U1_0.40_nozzle_0.8", + "inherits": "fdm_process_U1_common", + "from": "system", + "instantiation": "false", + "layer_height": "0.4", + "initial_layer_print_height": "0.4", + "bridge_flow": "1", + "line_width": "0.82", + "outer_wall_line_width": "0.82", + "initial_layer_line_width": "0.82", + "sparse_infill_line_width": "0.82", + "inner_wall_line_width": "0.82", + "internal_solid_infill_line_width": "0.82", + "support_line_width": "0.82", + "top_surface_line_width": "0.82", + "top_surface_pattern": "monotonic", + "initial_layer_speed": "35", + "initial_layer_infill_speed": "55", + "sparse_infill_speed": "100", + "top_surface_speed": "150", + "bridge_speed": "30", + "overhang_3_4_speed": "25", + "overhang_4_4_speed": "5" +} diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.42_nozzle_0.6.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.42_nozzle_0.6.json new file mode 100644 index 0000000000..18c1e7d79b --- /dev/null +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.42_nozzle_0.6.json @@ -0,0 +1,24 @@ +{ + "type": "process", + "name": "fdm_process_U1_0.42_nozzle_0.6", + "inherits": "fdm_process_U1_common", + "from": "system", + "instantiation": "false", + "layer_height": "0.42", + "initial_layer_print_height": "0.3", + "bridge_flow": "1", + "line_width": "0.62", + "outer_wall_line_width": "0.62", + "initial_layer_line_width": "0.62", + "sparse_infill_line_width": "0.62", + "inner_wall_line_width": "0.62", + "internal_solid_infill_line_width": "0.62", + "support_line_width": "0.62", + "top_surface_line_width": "0.62", + "initial_layer_speed": "35", + "initial_layer_infill_speed": "55", + "sparse_infill_speed": "100", + "top_surface_speed": "150", + "bridge_speed": "30", + "overhang_3_4_speed": "15" +} diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.48_nozzle_0.8.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.48_nozzle_0.8.json new file mode 100644 index 0000000000..8d5632b790 --- /dev/null +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.48_nozzle_0.8.json @@ -0,0 +1,26 @@ +{ + "type": "process", + "name": "fdm_process_U1_0.48_nozzle_0.8", + "inherits": "fdm_process_U1_common", + "from": "system", + "instantiation": "false", + "layer_height": "0.48", + "initial_layer_print_height": "0.4", + "bridge_flow": "1", + "line_width": "0.82", + "outer_wall_line_width": "0.82", + "initial_layer_line_width": "0.82", + "sparse_infill_line_width": "0.82", + "inner_wall_line_width": "0.82", + "internal_solid_infill_line_width": "0.82", + "support_line_width": "0.82", + "top_surface_line_width": "0.82", + "top_surface_pattern": "monotonic", + "initial_layer_speed": "35", + "initial_layer_infill_speed": "55", + "sparse_infill_speed": "100", + "top_surface_speed": "150", + "bridge_speed": "30", + "overhang_3_4_speed": "25", + "overhang_4_4_speed": "5" +} diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.56_nozzle_0.8.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.56_nozzle_0.8.json new file mode 100644 index 0000000000..7a5c92932d --- /dev/null +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.56_nozzle_0.8.json @@ -0,0 +1,26 @@ +{ + "type": "process", + "name": "fdm_process_U1_0.56_nozzle_0.8", + "inherits": "fdm_process_U1_common", + "from": "system", + "instantiation": "false", + "layer_height": "0.56", + "initial_layer_print_height": "0.4", + "bridge_flow": "1", + "line_width": "0.82", + "outer_wall_line_width": "0.82", + "initial_layer_line_width": "0.82", + "sparse_infill_line_width": "0.82", + "inner_wall_line_width": "0.82", + "internal_solid_infill_line_width": "0.82", + "support_line_width": "0.82", + "top_surface_line_width": "0.82", + "top_surface_pattern": "monotonic", + "initial_layer_speed": "35", + "initial_layer_infill_speed": "55", + "sparse_infill_speed": "100", + "top_surface_speed": "150", + "bridge_speed": "30", + "overhang_3_4_speed": "25", + "overhang_4_4_speed": "5" +} diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_0.6_common.json b/resources/profiles/Snapmaker/process/fdm_process_U1_0.6_common.json index 93119a9370..40ea900ba2 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1_0.6_common.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_0.6_common.json @@ -7,7 +7,7 @@ "line_width": "0.62", "outer_wall_line_width": "0.62", "inner_wall_line_width": "0.62", - "initial_layer_line_width": "0.72", + "initial_layer_line_width": "0.62", "sparse_infill_line_width": "0.62", "internal_solid_infill_line_width": "0.62", "support_line_width": "0.62", diff --git a/resources/profiles/Snapmaker/process/fdm_process_U1_common.json b/resources/profiles/Snapmaker/process/fdm_process_U1_common.json index 0af8c229a7..462ddb53ae 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_U1_common.json +++ b/resources/profiles/Snapmaker/process/fdm_process_U1_common.json @@ -13,8 +13,7 @@ "compatible_printers_condition": "", "draft_shield": "disabled", "elefant_foot_compensation": "0", - "enable_arc_fitting": "1", - "exclude_object": "1", + "enable_arc_fitting": "0", "outer_wall_acceleration": "5000", "wall_infill_order": "inner wall/outer wall/infill", "line_width": "0.42", @@ -46,7 +45,7 @@ "internal_solid_infill_speed": "150", "initial_layer_infill_speed": "60", "resolution": "0.012", - "support_type": "normal(auto)", + "support_type": "tree(auto)", "support_style": "default", "support_top_z_distance": "0.2", "support_bottom_z_distance": "0.2", @@ -68,11 +67,9 @@ "travel_speed": "500", "enable_prime_tower": "1", "wipe_tower_no_sparse_layers": "0", - "wipe_tower_cone_angle": "30", - "wipe_tower_wall_type": "rib", - "wipe_tower_extra_rib_length": "0", "prime_tower_width": "35", - "prime_volume": "30", "wall_generator": "arachne", - "compatible_printers": [] + "compatible_printers": [], + "wipe_tower_extra_rib_length": "8", + "exclude_object": "1" } diff --git a/resources/profiles/Snapmaker/process/fdm_process_a400.json b/resources/profiles/Snapmaker/process/fdm_process_a400.json index 39e7565f0b..f8ca481f9f 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_a400.json +++ b/resources/profiles/Snapmaker/process/fdm_process_a400.json @@ -5,7 +5,7 @@ "from": "system", "instantiation": "false", "initial_layer_print_height": "0.2", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "initial_layer_infill_speed": "75", "outer_wall_speed": "100", "inner_wall_speed": "160", diff --git a/resources/profiles/Snapmaker/process/fdm_process_common.json b/resources/profiles/Snapmaker/process/fdm_process_common.json index 6fb4fa2de4..458a0f6162 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_common.json +++ b/resources/profiles/Snapmaker/process/fdm_process_common.json @@ -110,7 +110,7 @@ "top_surface_jerk": "2", "travel_jerk": "4", "enable_support": "0", - "support_type": "normal(auto)", + "support_type": "tree(auto)", "support_style": "snug", "support_threshold_angle": "30", "support_on_build_plate_only": "1", diff --git a/resources/profiles/Snapmaker/process/fdm_process_idex.json b/resources/profiles/Snapmaker/process/fdm_process_idex.json index 7e04710bd3..6aeabfdaa7 100644 --- a/resources/profiles/Snapmaker/process/fdm_process_idex.json +++ b/resources/profiles/Snapmaker/process/fdm_process_idex.json @@ -5,7 +5,7 @@ "from": "system", "instantiation": "false", "initial_layer_print_height": "0.2", - "enable_arc_fitting": "1", + "enable_arc_fitting": "0", "initial_layer_infill_speed": "75", "outer_wall_speed": "120", "inner_wall_speed": "250", diff --git a/resources/profiles/Voron/machine/fdm_klipper_common.json b/resources/profiles/Voron/machine/fdm_klipper_common.json index 4d40f3d168..b4ffd1a4bb 100644 --- a/resources/profiles/Voron/machine/fdm_klipper_common.json +++ b/resources/profiles/Voron/machine/fdm_klipper_common.json @@ -116,7 +116,7 @@ "deretraction_speed": [ "30" ], - "z_hop_types": "Normal Lift", + "z_hop_types": "Slope Lift", "silent_mode": "0", "single_extruder_multi_material": "1", "change_filament_gcode": "", diff --git a/resources/profiles/re3D.json b/resources/profiles/re3D.json index e0acc669d0..94cca3c2ef 100644 --- a/resources/profiles/re3D.json +++ b/resources/profiles/re3D.json @@ -1,214 +1,219 @@ { - "name": "re3D", - "version": "02.04.00.03", - "force_update": "0", - "description": "re3D configurations", - "machine_model_list": [ - { - "name": "re3D Gigabot 4", - "sub_path": "machine/re3D Gigabot 4.json" - }, - { - "name": "re3D Gigabot 4 XLT", - "sub_path": "machine/re3D Gigabot 4 XLT.json" - }, - { - "name": "re3D GigabotX 2", - "sub_path": "machine/re3D GigabotX 2.json" - }, - { - "name": "re3D GigabotX 2 XLT", - "sub_path": "machine/re3D GigabotX 2 XLT.json" - }, - { - "name": "re3D Terabot 4", - "sub_path": "machine/re3D Terabot 4.json" - }, - { - "name": "re3D TerabotX 2", - "sub_path": "machine/re3D TerabotX 2.json" - } - ], - "process_list": [ - { - "name": "fdm_process_common", - "sub_path": "process/fdm_process_common.json" - }, - { - "name": "fdm_process_re3D_common", - "sub_path": "process/fdm_process_re3D_common.json" - }, - { - "name": "fgf_process_re3D_common", - "sub_path": "process/fgf_process_re3D_common.json" - }, - { - "name": "0.2 Fine", - "sub_path": "process/0.2mm Fine @re3D fdm 0.4.json" - }, - { - "name": "0.26 Standard", - "sub_path": "process/0.26mm Standard @re3D fdm 0.4.json" - }, - { - "name": "0.3 Fine", - "sub_path": "process/0.3mm Fine @re3D fdm 0.8.json" - }, - { - "name": "0.32 Draft", - "sub_path": "process/0.32mm Draft @re3D fdm 0.4.json" - }, - { - "name": "0.4 Standard", - "sub_path": "process/0.4mm Draft @re3D fdm 0.8.json" - }, - { - "name": "0.6 Standard", - "sub_path": "process/0.6mm Standard @re3D fgf 0.8.json" - }, - { - "name": "1.0 Standard", - "sub_path": "process/1.0mm Standard @re3D fgf 1.75.json" - } - ], - "filament_list": [ - { - "name": "fdm_filament_common", - "sub_path": "filament/fdm_filament_common.json" - }, - { - "name": "fdm_filament_pc", - "sub_path": "filament/fdm_filament_pc.json" - }, - { - "name": "fdm_filament_pet", - "sub_path": "filament/fdm_filament_pet.json" - }, - { - "name": "fdm_filament_pla", - "sub_path": "filament/fdm_filament_pla.json" - }, - { - "name": "re3D PC", - "sub_path": "filament/re3D PC.json" - }, - { - "name": "re3D PC @0.4 nozzle", - "sub_path": "filament/re3D PC @0.4 nozzle.json" - }, - { - "name": "re3D PC @0.8 nozzle", - "sub_path": "filament/re3D PC @0.8 nozzle.json" - }, - { - "name": "re3D Greengate rPETG", - "sub_path": "filament/re3D Greengate rPETG.json" - }, - { - "name": "re3D Greengate rPETG @0.8 nozzle", - "sub_path": "filament/re3D Greengate rPETG @0.8 nozzle.json" - }, - { - "name": "re3D Greengate rPETG @1.75 nozzle", - "sub_path": "filament/re3D Greengate rPETG @1.75 nozzle.json" - }, - { - "name": "re3D PETG", - "sub_path": "filament/re3D PETG.json" - }, - { - "name": "re3D PETG @0.4 nozzle", - "sub_path": "filament/re3D PETG @0.4 nozzle.json" - }, - { - "name": "re3D PETG @0.8 nozzle", - "sub_path": "filament/re3D PETG @0.8 nozzle.json" - }, - { - "name": "re3D rPP", - "sub_path": "filament/re3D rPP.json" - }, - { - "name": "re3D rPP @0.8 nozzle", - "sub_path": "filament/re3D rPP @0.8 nozzle.json" - }, - { - "name": "re3D rPP @1.75 nozzle", - "sub_path": "filament/re3D rPP @1.75 nozzle.json" - }, - { - "name": "re3D PLA", - "sub_path": "filament/re3D PLA.json" - }, - { - "name": "re3D PLA @0.4 nozzle", - "sub_path": "filament/re3D PLA @0.4 nozzle.json" - }, - { - "name": "re3D PLA @0.8 nozzle", - "sub_path": "filament/re3D PLA @0.8 nozzle.json" - } - ], - "machine_list": [ - { - "name": "fdm_machine_common", - "sub_path": "machine/fdm_machine_common.json" - }, - { - "name": "fdm_re3D_common", - "sub_path": "machine/fdm_re3D_common.json" - }, - { - "name": "fgf_re3D_common", - "sub_path": "machine/fgf_re3D_common.json" - }, - { - "name": "re3D Gigabot 4 0.4 nozzle", - "sub_path": "machine/re3D Gigabot 4 0.4 nozzle.json" - }, - { - "name": "re3D Gigabot 4 0.8 nozzle", - "sub_path": "machine/re3D Gigabot 4 0.8 nozzle.json" - }, - { - "name": "re3D Gigabot 4 XLT 0.4 nozzle", - "sub_path": "machine/re3D Gigabot 4 XLT 0.4 nozzle.json" - }, - { - "name": "re3D Gigabot 4 XLT 0.8 nozzle", - "sub_path": "machine/re3D Gigabot 4 XLT 0.8 nozzle.json" - }, - { - "name": "re3D Terabot 4 0.4 nozzle", - "sub_path": "machine/re3D Terabot 4 0.4 nozzle.json" - }, - { - "name": "re3D Terabot 4 0.8 nozzle", - "sub_path": "machine/re3D Terabot 4 0.8 nozzle.json" - }, - { - "name": "re3D GigabotX 2 0.8 nozzle", - "sub_path": "machine/re3D GigabotX 2 0.8 nozzle.json" - }, - { - "name": "re3D GigabotX 2 1.75 nozzle", - "sub_path": "machine/re3D GigabotX 2 1.75 nozzle.json" - }, - { - "name": "re3D GigabotX 2 XLT 0.8 nozzle", - "sub_path": "machine/re3D GigabotX 2 XLT 0.8 nozzle.json" - }, - { - "name": "re3D GigabotX 2 XLT 1.75 nozzle", - "sub_path": "machine/re3D GigabotX 2 XLT 1.75 nozzle.json" - }, - { - "name": "re3D TerabotX 2 0.8 nozzle", - "sub_path": "machine/re3D TerabotX 2 0.8 nozzle.json" - }, - { - "name": "re3D TerabotX 2 1.75 nozzle", - "sub_path": "machine/re3D TerabotX 2 1.75 nozzle.json" - } - ] -} + "name": "re3D", + "url": "", + "version": "03.00.08", + "force_update": "0", + "description": "re3D configurations", + "machine_model_list": [ + { + "name": "re3D Gigabot 4", + "sub_path": "machine/re3D Gigabot 4.json" + }, + { + "name": "re3D Gigabot 4 XLT", + "sub_path": "machine/re3D Gigabot 4 XLT.json" + }, + { + "name": "re3D GigabotX 2", + "sub_path": "machine/re3D GigabotX 2.json" + }, + { + "name": "re3D GigabotX 2 XLT", + "sub_path": "machine/re3D GigabotX 2 XLT.json" + }, + { + "name": "re3D Terabot 4", + "sub_path": "machine/re3D Terabot 4.json" + }, + { + "name": "re3D TerabotX 2", + "sub_path": "machine/re3D TerabotX 2.json" + } + ], + "process_list": [ + { + "name": "fdm_process_common", + "sub_path": "process/fdm_process_common.json" + }, + { + "name": "fdm_process_re3D_common", + "sub_path": "process/fdm_process_re3D_common.json" + }, + { + "name": "fgf_process_re3D_common", + "sub_path": "process/fgf_process_re3D_common.json" + }, + { + "name": "0.2 Fine", + "sub_path": "process/0.2mm Fine @re3D fdm 0.4.json" + }, + { + "name": "0.26 Standard", + "sub_path": "process/0.26mm Standard @re3D fdm 0.4.json" + }, + { + "name": "0.32 Draft", + "sub_path": "process/0.32mm Draft @re3D fdm 0.4.json" + }, + { + "name": "0.3 Fine", + "sub_path": "process/0.3mm Fine @re3D fdm 0.8.json" + }, + { + "name": "0.4 Standard", + "sub_path": "process/0.4mm Draft @re3D fdm 0.8.json" + }, + { + "name": "1.0 Standard", + "sub_path": "process/1.0mm Standard @re3D fgf 1.75.json" + }, + { + "name": "0.6 Standard", + "sub_path": "process/0.6mm Standard @re3D fgf 0.8.json" + } + ], + "filament_list": [ + { + "name": "fdm_filament_common", + "sub_path": "filament/fdm_filament_common.json" + }, + { + "name": "fdm_filament_pla", + "sub_path": "filament/fdm_filament_pla.json" + }, + { + "name": "fdm_filament_pet", + "sub_path": "filament/fdm_filament_pet.json" + }, + { + "name": "fdm_filament_pp", + "sub_path": "filament/fdm_filament_pp.json" + }, + { + "name": "fdm_filament_pc", + "sub_path": "filament/fdm_filament_pc.json" + }, + { + "name": "re3D PLA", + "sub_path": "filament/re3D PLA.json" + }, + { + "name": "re3D PETG", + "sub_path": "filament/re3D PETG.json" + }, + { + "name": "re3D PC", + "sub_path": "filament/re3D PC.json" + }, + { + "name": "re3D rPETG", + "sub_path": "filament/re3D rPETG.json" + }, + { + "name": "re3D rPP", + "sub_path": "filament/re3D rPP.json" + }, + { + "name": "re3D PLA @0.4 nozzle", + "sub_path": "filament/re3D PLA @0.4 nozzle.json" + }, + { + "name": "re3D PLA @0.8 nozzle", + "sub_path": "filament/re3D PLA @0.8 nozzle.json" + }, + { + "name": "re3D PETG @0.4 nozzle", + "sub_path": "filament/re3D PETG @0.4 nozzle.json" + }, + { + "name": "re3D PETG @0.8 nozzle", + "sub_path": "filament/re3D PETG @0.8 nozzle.json" + }, + { + "name": "re3D PC @0.4 nozzle", + "sub_path": "filament/re3D PC @0.4 nozzle.json" + }, + { + "name": "re3D PC @0.8 nozzle", + "sub_path": "filament/re3D PC @0.8 nozzle.json" + }, + { + "name": "re3D rPETG @0.8 nozzle", + "sub_path": "filament/re3D rPETG @0.8 nozzle.json" + }, + { + "name": "re3D rPETG @1.75 nozzle", + "sub_path": "filament/re3D rPETG @1.75 nozzle.json" + }, + { + "name": "re3D rPP @0.8 nozzle", + "sub_path": "filament/re3D rPP @0.8 nozzle.json" + }, + { + "name": "re3D rPP @1.75 nozzle", + "sub_path": "filament/re3D rPP @1.75 nozzle.json" + } + ], + "machine_list": [ + { + "name": "fdm_machine_common", + "sub_path": "machine/fdm_machine_common.json" + }, + { + "name": "fdm_re3D_common", + "sub_path": "machine/fdm_re3D_common.json" + }, + { + "name": "fgf_re3D_common", + "sub_path": "machine/fgf_re3D_common.json" + }, + { + "name": "re3D Gigabot 4 0.4 nozzle", + "sub_path": "machine/re3D Gigabot 4 0.4 nozzle.json" + }, + { + "name": "re3D Gigabot 4 0.8 nozzle", + "sub_path": "machine/re3D Gigabot 4 0.8 nozzle.json" + }, + { + "name": "re3D Gigabot 4 XLT 0.4 nozzle", + "sub_path": "machine/re3D Gigabot 4 XLT 0.4 nozzle.json" + }, + { + "name": "re3D Gigabot 4 XLT 0.8 nozzle", + "sub_path": "machine/re3D Gigabot 4 XLT 0.8 nozzle.json" + }, + { + "name": "re3D GigabotX 2 0.8 nozzle", + "sub_path": "machine/re3D GigabotX 2 0.8 nozzle.json" + }, + { + "name": "re3D GigabotX 2 1.75 nozzle", + "sub_path": "machine/re3D GigabotX 2 1.75 nozzle.json" + }, + { + "name": "re3D GigabotX 2 XLT 0.8 nozzle", + "sub_path": "machine/re3D GigabotX 2 XLT 0.8 nozzle.json" + }, + { + "name": "re3D GigabotX 2 XLT 1.75 nozzle", + "sub_path": "machine/re3D GigabotX 2 XLT 1.75 nozzle.json" + }, + { + "name": "re3D Terabot 4 0.4 nozzle", + "sub_path": "machine/re3D Terabot 4 0.4 nozzle.json" + }, + { + "name": "re3D Terabot 4 0.8 nozzle", + "sub_path": "machine/re3D Terabot 4 0.8 nozzle.json" + }, + { + "name": "re3D TerabotX 2 0.8 nozzle", + "sub_path": "machine/re3D TerabotX 2 0.8 nozzle.json" + }, + { + "name": "re3D TerabotX 2 1.75 nozzle", + "sub_path": "machine/re3D TerabotX 2 1.75 nozzle.json" + } + ] +} \ No newline at end of file diff --git a/resources/profiles/re3D/filament/fdm_filament_pp.json b/resources/profiles/re3D/filament/fdm_filament_pp.json new file mode 100644 index 0000000000..16a1c9705d --- /dev/null +++ b/resources/profiles/re3D/filament/fdm_filament_pp.json @@ -0,0 +1,10 @@ +{ + "type": "filament", + "name": "fdm_filament_pp", + "from": "system", + "instantiation": "false", + "inherits": "fdm_filament_common", + "filament_type": [ + "PP" + ] +} \ No newline at end of file diff --git a/resources/profiles/re3D/filament/re3D PC @0.4 nozzle.json b/resources/profiles/re3D/filament/re3D PC @0.4 nozzle.json index 47a3a44f34..d3ec58f07c 100644 --- a/resources/profiles/re3D/filament/re3D PC @0.4 nozzle.json +++ b/resources/profiles/re3D/filament/re3D PC @0.4 nozzle.json @@ -9,6 +9,9 @@ "filament_settings_id": [ "re3D PC @0.4 nozzle" ], + "filament_type": [ + "PC" + ], "compatible_printers": [ "re3D Gigabot 4 0.4 nozzle", "re3D Gigabot 4 XLT 0.4 nozzle", diff --git a/resources/profiles/re3D/filament/re3D PC @0.8 nozzle.json b/resources/profiles/re3D/filament/re3D PC @0.8 nozzle.json index c1f329f295..2f4f743d07 100644 --- a/resources/profiles/re3D/filament/re3D PC @0.8 nozzle.json +++ b/resources/profiles/re3D/filament/re3D PC @0.8 nozzle.json @@ -9,6 +9,9 @@ "filament_settings_id": [ "re3D PC @0.8 nozzle" ], + "filament_type": [ + "PC" + ], "compatible_printers": [ "re3D Gigabot 4 0.8 nozzle", "re3D Gigabot 4 XLT 0.8 nozzle", diff --git a/resources/profiles/re3D/filament/re3D PETG @0.4 nozzle.json b/resources/profiles/re3D/filament/re3D PETG @0.4 nozzle.json index c275a7517f..d85397a1c0 100644 --- a/resources/profiles/re3D/filament/re3D PETG @0.4 nozzle.json +++ b/resources/profiles/re3D/filament/re3D PETG @0.4 nozzle.json @@ -17,6 +17,9 @@ "filament_vendor": [ "re3D" ], + "filament_type": [ + "PETG" + ], "close_fan_the_first_x_layers": [ "2" ], diff --git a/resources/profiles/re3D/filament/re3D PETG @0.8 nozzle.json b/resources/profiles/re3D/filament/re3D PETG @0.8 nozzle.json index 148f6540fe..c45d4a84e5 100644 --- a/resources/profiles/re3D/filament/re3D PETG @0.8 nozzle.json +++ b/resources/profiles/re3D/filament/re3D PETG @0.8 nozzle.json @@ -14,6 +14,9 @@ "re3D Gigabot 4 XLT 0.8 nozzle", "re3D Terabot 4 0.8 nozzle" ], + "filament_type": [ + "PETG" + ], "filament_vendor": [ "re3D" ], diff --git a/resources/profiles/re3D/filament/re3D PLA @0.4 nozzle.json b/resources/profiles/re3D/filament/re3D PLA @0.4 nozzle.json index 1719807f7f..a3630d0111 100644 --- a/resources/profiles/re3D/filament/re3D PLA @0.4 nozzle.json +++ b/resources/profiles/re3D/filament/re3D PLA @0.4 nozzle.json @@ -9,6 +9,9 @@ "filament_settings_id": [ "re3D PLA @0.4 nozzle" ], + "filament_type": [ + "PLA" + ], "compatible_printers": [ "re3D Gigabot 4 0.4 nozzle", "re3D Gigabot 4 XLT 0.4 nozzle", diff --git a/resources/profiles/re3D/filament/re3D PLA @0.8 nozzle.json b/resources/profiles/re3D/filament/re3D PLA @0.8 nozzle.json index 01e7abbc9a..002e18f67c 100644 --- a/resources/profiles/re3D/filament/re3D PLA @0.8 nozzle.json +++ b/resources/profiles/re3D/filament/re3D PLA @0.8 nozzle.json @@ -9,6 +9,9 @@ "filament_settings_id": [ "re3D PLA @0.8 nozzle" ], + "filament_type": [ + "PLA" + ], "compatible_printers": [ "re3D Gigabot 4 0.8 nozzle", "re3D Gigabot 4 XLT 0.8 nozzle", diff --git a/resources/profiles/re3D/filament/re3D Greengate rPETG @0.8 nozzle.json b/resources/profiles/re3D/filament/re3D rPETG @0.8 nozzle.json similarity index 89% rename from resources/profiles/re3D/filament/re3D Greengate rPETG @0.8 nozzle.json rename to resources/profiles/re3D/filament/re3D rPETG @0.8 nozzle.json index 028bf042bb..0ac0d012f5 100644 --- a/resources/profiles/re3D/filament/re3D Greengate rPETG @0.8 nozzle.json +++ b/resources/profiles/re3D/filament/re3D rPETG @0.8 nozzle.json @@ -1,14 +1,21 @@ { "type": "filament", "filament_id": "GFG01", - "setting_id": "fRX555Prkdu5ESIp", - "name": "re3D Greengate rPETG @0.8 nozzle", + "setting_id": "WlELqPVnuL7DaTxs", + "name": "re3D rPETG @0.8 nozzle", "from": "system", + "renamed_from": "re3D Greengate rPETG @0.8 nozzle", "instantiation": "true", "inherits": "fdm_filament_pet", "nozzle_temperature_initial_layer": [ "0" ], + "filament_type": [ + "PETG" + ], + "filament_vendor": [ + "re3D" + ], "nozzle_temperature": [ "0" ], diff --git a/resources/profiles/re3D/filament/re3D Greengate rPETG @1.75 nozzle.json b/resources/profiles/re3D/filament/re3D rPETG @1.75 nozzle.json similarity index 89% rename from resources/profiles/re3D/filament/re3D Greengate rPETG @1.75 nozzle.json rename to resources/profiles/re3D/filament/re3D rPETG @1.75 nozzle.json index 5ae3d832ff..8cd090fc06 100644 --- a/resources/profiles/re3D/filament/re3D Greengate rPETG @1.75 nozzle.json +++ b/resources/profiles/re3D/filament/re3D rPETG @1.75 nozzle.json @@ -1,14 +1,21 @@ { "type": "filament", "filament_id": "GFG01", - "setting_id": "6aPU6CYS2cmODkok", - "name": "re3D Greengate rPETG @1.75 nozzle", + "setting_id": "sHzO2S3mmE6Iqs2O", + "name": "re3D rPETG @1.75 nozzle", "from": "system", + "renamed_from": "re3D Greengate rPETG @1.75 nozzle", "instantiation": "true", "inherits": "fdm_filament_pet", "nozzle_temperature_initial_layer": [ "0" ], + "filament_type": [ + "PETG" + ], + "filament_vendor": [ + "re3D" + ], "nozzle_temperature": [ "0" ], diff --git a/resources/profiles/re3D/filament/re3D Greengate rPETG.json b/resources/profiles/re3D/filament/re3D rPETG.json similarity index 78% rename from resources/profiles/re3D/filament/re3D Greengate rPETG.json rename to resources/profiles/re3D/filament/re3D rPETG.json index de56af4f02..fe19f12539 100644 --- a/resources/profiles/re3D/filament/re3D Greengate rPETG.json +++ b/resources/profiles/re3D/filament/re3D rPETG.json @@ -1,13 +1,14 @@ { "type": "filament", - "name": "re3D Greengate rPETG", + "name": "re3D rPETG", "from": "system", + "renamed_from": "re3D Greengate rPETG", "instantiation": "true", "inherits": "fdm_filament_pet", "filament_id": "GFG01", - "setting_id": "SdugLw5oy9GB7NID", + "setting_id": "6aESuJ2S2Kogd4Lq", "filament_settings_id": [ - "re3D Greengate rPETG" + "re3D rPETG" ], "compatible_printers": [ "re3D GigabotX 2 0.8 nozzle", diff --git a/resources/profiles/re3D/filament/re3D rPP @0.8 nozzle.json b/resources/profiles/re3D/filament/re3D rPP @0.8 nozzle.json index a49f19e128..5f5467e86d 100644 --- a/resources/profiles/re3D/filament/re3D rPP @0.8 nozzle.json +++ b/resources/profiles/re3D/filament/re3D rPP @0.8 nozzle.json @@ -5,7 +5,7 @@ "name": "re3D rPP @0.8 nozzle", "from": "system", "instantiation": "true", - "inherits": "fdm_filament_pet", + "inherits": "fdm_filament_pp", "filament_type": [ "PP" ], diff --git a/resources/profiles/re3D/filament/re3D rPP @1.75 nozzle.json b/resources/profiles/re3D/filament/re3D rPP @1.75 nozzle.json index 0bde56c596..2e5a3f4386 100644 --- a/resources/profiles/re3D/filament/re3D rPP @1.75 nozzle.json +++ b/resources/profiles/re3D/filament/re3D rPP @1.75 nozzle.json @@ -5,7 +5,7 @@ "name": "re3D rPP @1.75 nozzle", "from": "system", "instantiation": "true", - "inherits": "fdm_filament_pet", + "inherits": "fdm_filament_pp", "filament_type": [ "PP" ], diff --git a/resources/profiles/re3D/filament/re3D rPP.json b/resources/profiles/re3D/filament/re3D rPP.json index bf96aa51e6..f7244f5985 100644 --- a/resources/profiles/re3D/filament/re3D rPP.json +++ b/resources/profiles/re3D/filament/re3D rPP.json @@ -3,10 +3,7 @@ "name": "re3D rPP", "from": "system", "instantiation": "true", - "inherits": "fdm_filament_pet", - "filament_type": [ - "PP" - ], + "inherits": "fdm_filament_pp", "filament_id": "GFG02", "setting_id": "8PB62qm5Cd3SHLf4", "filament_settings_id": [ diff --git a/resources/profiles/re3D/machine/re3D Gigabot 4 0.4 nozzle.json b/resources/profiles/re3D/machine/re3D Gigabot 4 0.4 nozzle.json index a0cbd3c0e2..d14d3c260e 100644 --- a/resources/profiles/re3D/machine/re3D Gigabot 4 0.4 nozzle.json +++ b/resources/profiles/re3D/machine/re3D Gigabot 4 0.4 nozzle.json @@ -6,7 +6,7 @@ "machine_tech": "FFF", "model_id": "GB4", "printer_model": "re3D Gigabot 4", - "bed_texture": "Gigabot 4_buildplate_texture.png", + "bed_texture": "re3D Gigabot 4_buildplate_texture.svg", "default_materials": "re3D PETG;re3D PLA;re3D PC", "printable_area": [ "0x0", diff --git a/resources/profiles/re3D/machine/re3D Gigabot 4 0.8 nozzle.json b/resources/profiles/re3D/machine/re3D Gigabot 4 0.8 nozzle.json index e5d847e62c..9bc78b36ad 100644 --- a/resources/profiles/re3D/machine/re3D Gigabot 4 0.8 nozzle.json +++ b/resources/profiles/re3D/machine/re3D Gigabot 4 0.8 nozzle.json @@ -6,7 +6,7 @@ "machine_tech": "FFF", "model_id": "GB4", "printer_model": "re3D Gigabot 4", - "bed_texture": "Gigabot 4_buildplate_texture.png", + "bed_texture": "re3D Gigabot 4_buildplate_texture.svg", "default_materials": "re3D PETG;re3D PLA;re3D PC", "printable_area": [ "0x0", diff --git a/resources/profiles/re3D/machine/re3D Gigabot 4 XLT 0.4 nozzle.json b/resources/profiles/re3D/machine/re3D Gigabot 4 XLT 0.4 nozzle.json index 8a5e118bff..56b4030476 100644 --- a/resources/profiles/re3D/machine/re3D Gigabot 4 XLT 0.4 nozzle.json +++ b/resources/profiles/re3D/machine/re3D Gigabot 4 XLT 0.4 nozzle.json @@ -6,7 +6,7 @@ "machine_tech": "FFF", "model_id": "GB4", "printer_model": "re3D Gigabot 4 XLT", - "bed_texture": "Gigabot 4 XLT_buildplate_texture.png", + "bed_texture": "re3D Gigabot 4 XLT_buildplate_texture.svg", "default_materials": "re3D PETG;re3D PLA;re3D PC", "printable_area": [ "0x0", diff --git a/resources/profiles/re3D/machine/re3D Gigabot 4 XLT 0.8 nozzle.json b/resources/profiles/re3D/machine/re3D Gigabot 4 XLT 0.8 nozzle.json index a3fabc985d..a14e730b30 100644 --- a/resources/profiles/re3D/machine/re3D Gigabot 4 XLT 0.8 nozzle.json +++ b/resources/profiles/re3D/machine/re3D Gigabot 4 XLT 0.8 nozzle.json @@ -6,7 +6,7 @@ "machine_tech": "FFF", "model_id": "GB4", "printer_model": "re3D Gigabot 4 XLT", - "bed_texture": "Gigabot 4 XLT_buildplate_texture.png", + "bed_texture": "re3D Gigabot 4 XLT_buildplate_texture.svg", "default_materials": "re3D PETG;re3D PLA;re3D PC", "printable_area": [ "0x0", diff --git a/resources/profiles/re3D/machine/re3D GigabotX 2 0.8 nozzle.json b/resources/profiles/re3D/machine/re3D GigabotX 2 0.8 nozzle.json index 8260ee5a1d..f6bac17a0d 100644 --- a/resources/profiles/re3D/machine/re3D GigabotX 2 0.8 nozzle.json +++ b/resources/profiles/re3D/machine/re3D GigabotX 2 0.8 nozzle.json @@ -6,7 +6,7 @@ "machine_tech": "FGF", "model_id": "re3D GBX2", "printer_model": "re3D GigabotX 2", - "bed_texture": "Gigabot 4_buildplate_texture.png", + "bed_texture": "re3D GigabotX 2_buildplate_texture.svg", "printable_area": [ "0x0", "552x0", @@ -25,7 +25,7 @@ "0.3" ], "default_filament_profile": [ - "re3D Greengate rPETG @0.8 nozzle" + "re3D rPETG @0.8 nozzle" ], "default_print_profile": "0.6 Standard", "printer_settings_id": "re3d_gbx_08", diff --git a/resources/profiles/re3D/machine/re3D GigabotX 2 1.75 nozzle.json b/resources/profiles/re3D/machine/re3D GigabotX 2 1.75 nozzle.json index 8a89e9d858..585c219bc7 100644 --- a/resources/profiles/re3D/machine/re3D GigabotX 2 1.75 nozzle.json +++ b/resources/profiles/re3D/machine/re3D GigabotX 2 1.75 nozzle.json @@ -7,7 +7,7 @@ "machine_tech": "FGF", "model_id": "re3D GBX2", "printer_model": "re3D GigabotX 2", - "bed_texture": "Gigabot 4_buildplate_texture.png", + "bed_texture": "re3D GigabotX 2_buildplate_texture.svg", "printable_area": [ "0x0", "552x0", @@ -26,7 +26,7 @@ "0.6" ], "default_filament_profile": [ - "re3D Greengate rPETG @1.75 nozzle" + "re3D rPETG @1.75 nozzle" ], "default_print_profile": "1.0 Standard", "printer_settings_id": "re3d_gbx_175", diff --git a/resources/profiles/re3D/machine/re3D GigabotX 2 XLT 0.8 nozzle.json b/resources/profiles/re3D/machine/re3D GigabotX 2 XLT 0.8 nozzle.json index 2d81331c5c..863d8e6403 100644 --- a/resources/profiles/re3D/machine/re3D GigabotX 2 XLT 0.8 nozzle.json +++ b/resources/profiles/re3D/machine/re3D GigabotX 2 XLT 0.8 nozzle.json @@ -6,7 +6,7 @@ "machine_tech": "FGF", "model_id": "re3D GBX2 XLT", "printer_model": "re3D GigabotX 2 XLT", - "bed_texture": "GigabotX 2 XLT_buildplate_texture.png", + "bed_texture": "re3D GigabotX 2 XLT_buildplate_texture.svg", "printable_area": [ "0x0", "552x0", @@ -25,7 +25,7 @@ "0.3" ], "default_filament_profile": [ - "re3D Greengate rPETG @0.8 nozzle" + "re3D rPETG @0.8 nozzle" ], "default_print_profile": "0.6 Standard", "printer_settings_id": "re3d_gbx_xlt_08", diff --git a/resources/profiles/re3D/machine/re3D GigabotX 2 XLT 1.75 nozzle.json b/resources/profiles/re3D/machine/re3D GigabotX 2 XLT 1.75 nozzle.json index 63503c01d8..5784b4e673 100644 --- a/resources/profiles/re3D/machine/re3D GigabotX 2 XLT 1.75 nozzle.json +++ b/resources/profiles/re3D/machine/re3D GigabotX 2 XLT 1.75 nozzle.json @@ -7,7 +7,7 @@ "machine_tech": "FGF", "model_id": "re3D GBX2 XLT", "printer_model": "re3D GigabotX 2 XLT", - "bed_texture": "GigabotX 2 XLT_buildplate_texture.png", + "bed_texture": "re3D GigabotX 2 XLT_buildplate_texture.svg", "printable_area": [ "0x0", "552x0", @@ -26,7 +26,7 @@ "0.6" ], "default_filament_profile": [ - "re3D Greengate rPETG @1.75 nozzle" + "re3D rPETG @1.75 nozzle" ], "default_print_profile": "1.0 Standard", "printer_settings_id": "re3d_gbx_xlt_175", diff --git a/resources/profiles/re3D/machine/re3D GigabotX 2 XLT.json b/resources/profiles/re3D/machine/re3D GigabotX 2 XLT.json index 09a2702f3a..99800331d9 100644 --- a/resources/profiles/re3D/machine/re3D GigabotX 2 XLT.json +++ b/resources/profiles/re3D/machine/re3D GigabotX 2 XLT.json @@ -9,5 +9,5 @@ "bed_model": "", "bed_texture": "re3D GigabotX 2 XLT_buildplate_texture.svg", "hotend_model": "GBX-HOTEND.stl", - "default_materials": "re3D Greengate rPETG;re3D rPP;" + "default_materials": "re3D rPETG;re3D rPP;" } diff --git a/resources/profiles/re3D/machine/re3D GigabotX 2.json b/resources/profiles/re3D/machine/re3D GigabotX 2.json index cc1470ee05..3a1db130b0 100644 --- a/resources/profiles/re3D/machine/re3D GigabotX 2.json +++ b/resources/profiles/re3D/machine/re3D GigabotX 2.json @@ -9,5 +9,5 @@ "bed_model": "", "bed_texture": "re3D GigabotX 2_buildplate_texture.svg", "hotend_model": "GBX-HOTEND.stl", - "default_materials": "re3D Greengate rPETG;re3D rPP;" + "default_materials": "re3D rPETG;re3D rPP;" } diff --git a/resources/profiles/re3D/machine/re3D Terabot 4 0.4 nozzle.json b/resources/profiles/re3D/machine/re3D Terabot 4 0.4 nozzle.json index 105b0f2621..d24ba564a2 100644 --- a/resources/profiles/re3D/machine/re3D Terabot 4 0.4 nozzle.json +++ b/resources/profiles/re3D/machine/re3D Terabot 4 0.4 nozzle.json @@ -6,7 +6,7 @@ "machine_tech": "FFF", "model_id": "TB4", "printer_model": "re3D Terabot 4", - "bed_texture": "Gigabot 4_buildplate_texture.png", + "bed_texture": "re3D Terabot 4_buildplate_texture.svg", "default_materials": "re3D PETG;re3D PLA;re3D PC", "printable_area": [ "0x0", diff --git a/resources/profiles/re3D/machine/re3D Terabot 4 0.8 nozzle.json b/resources/profiles/re3D/machine/re3D Terabot 4 0.8 nozzle.json index bd83a5ef45..6255e84896 100644 --- a/resources/profiles/re3D/machine/re3D Terabot 4 0.8 nozzle.json +++ b/resources/profiles/re3D/machine/re3D Terabot 4 0.8 nozzle.json @@ -6,7 +6,7 @@ "machine_tech": "FFF", "model_id": "TB4", "printer_model": "re3D Terabot 4", - "bed_texture": "Gigabot 4_buildplate_texture.png", + "bed_texture": "re3D Terabot 4_buildplate_texture.svg", "default_materials": "re3D PETG;re3D PLA;re3D PC", "printable_area": [ "0x0", diff --git a/resources/profiles/re3D/machine/re3D TerabotX 2 0.8 nozzle.json b/resources/profiles/re3D/machine/re3D TerabotX 2 0.8 nozzle.json index daa97c2e88..5622a813c1 100644 --- a/resources/profiles/re3D/machine/re3D TerabotX 2 0.8 nozzle.json +++ b/resources/profiles/re3D/machine/re3D TerabotX 2 0.8 nozzle.json @@ -6,7 +6,7 @@ "machine_tech": "FGF", "model_id": "re3D TBX2", "printer_model": "re3D TerabotX 2", - "bed_texture": "Gigabot 4_buildplate_texture.png", + "bed_texture": "re3D TerabotX 2_buildplate_texture.svg", "printable_area": [ "0x0", "892x0", @@ -25,7 +25,7 @@ "0.3" ], "default_filament_profile": [ - "re3D Greengate rPETG @0.8 nozzle" + "re3D rPETG @0.8 nozzle" ], "default_print_profile": "0.6 Standard", "printer_settings_id": "re3d_tbx2_08", diff --git a/resources/profiles/re3D/machine/re3D TerabotX 2 1.75 nozzle.json b/resources/profiles/re3D/machine/re3D TerabotX 2 1.75 nozzle.json index 401833977a..398ab006bd 100644 --- a/resources/profiles/re3D/machine/re3D TerabotX 2 1.75 nozzle.json +++ b/resources/profiles/re3D/machine/re3D TerabotX 2 1.75 nozzle.json @@ -7,7 +7,7 @@ "machine_tech": "FGF", "model_id": "re3D TBX2", "printer_model": "re3D TerabotX 2", - "bed_texture": "Gigabot 4_buildplate_texture.png", + "bed_texture": "re3D TerabotX 2_buildplate_texture.svg", "printable_area": [ "0x0", "892x0", @@ -26,7 +26,7 @@ "0.6" ], "default_filament_profile": [ - "re3D Greengate rPETG @1.75 nozzle" + "re3D rPETG @1.75 nozzle" ], "default_print_profile": "1.0 Standard", "printer_settings_id": "re3d_tbx2_175", diff --git a/resources/profiles/re3D/machine/re3D TerabotX 2.json b/resources/profiles/re3D/machine/re3D TerabotX 2.json index 9d8d0b2cb4..f541a62ad7 100644 --- a/resources/profiles/re3D/machine/re3D TerabotX 2.json +++ b/resources/profiles/re3D/machine/re3D TerabotX 2.json @@ -9,5 +9,5 @@ "bed_model": "", "bed_texture": "re3D TerabotX 2_buildplate_texture.svg", "hotend_model": "GBX-HOTEND.stl", - "default_materials": "re3D Greengate rPETG;re3D rPP;" + "default_materials": "re3D rPETG;re3D rPP;" } diff --git a/resources/profiles/re3D/process/0.26mm Standard @re3D fdm 0.4.json b/resources/profiles/re3D/process/0.26mm Standard @re3D fdm 0.4.json index 7d443c9730..2ab80cdf26 100644 --- a/resources/profiles/re3D/process/0.26mm Standard @re3D fdm 0.4.json +++ b/resources/profiles/re3D/process/0.26mm Standard @re3D fdm 0.4.json @@ -22,5 +22,35 @@ "top_surface_line_width": "0.48", "support_line_width": "0.48", "support_top_z_distance": "0.2", - "support_bottom_z_distance": "0.2" -} \ No newline at end of file + "support_bottom_z_distance": "0.2", + "bridge_speed": "50", + "internal_bridge_speed": "150%", + "default_acceleration": "5000", + "outer_wall_speed": "60", + "initial_layer_speed": "50", + "gap_infill_speed": "60", + "sparse_infill_speed": "80", + "inner_wall_speed": "80", + "internal_solid_infill_speed": "80", + "support_interface_speed": "30", + "support_speed": "90", + "top_surface_speed": "50", + "travel_speed": "300", + "top_surface_acceleration": "2500", + "initial_layer_acceleration": "1000", + "travel_acceleration": "5000", + "inner_wall_acceleration": "5000", + "ironing_speed": "25", + "overhang_1_4_speed": "0", + "overhang_2_4_speed": "50", + "overhang_3_4_speed": "30", + "overhang_4_4_speed": "10", + "initial_layer_infill_speed": "50", + "outer_wall_acceleration": "2500", + "bridge_acceleration": "3500", + "sparse_infill_acceleration": "5000", + "internal_solid_infill_acceleration": "5000", + "initial_layer_travel_speed": "100%", + "small_perimeter_speed": "50%", + "filename_format": "{input_filename_base}_{\"E\"}{initial_tool}_{filament_type[initial_tool]}_{print_time}.gcode" +} diff --git a/resources/profiles/re3D/process/0.2mm Fine @re3D fdm 0.4.json b/resources/profiles/re3D/process/0.2mm Fine @re3D fdm 0.4.json index 2734cf3334..10013e9c36 100644 --- a/resources/profiles/re3D/process/0.2mm Fine @re3D fdm 0.4.json +++ b/resources/profiles/re3D/process/0.2mm Fine @re3D fdm 0.4.json @@ -22,5 +22,32 @@ "top_surface_line_width": "0.44", "support_line_width": "0.48", "support_top_z_distance": "0.2", - "support_bottom_z_distance": "0.2" -} \ No newline at end of file + "support_bottom_z_distance": "0.2", + "outer_wall_speed": "60", + "initial_layer_speed": "50", + "gap_infill_speed": "60", + "sparse_infill_speed": "80", + "inner_wall_speed": "80", + "internal_solid_infill_speed": "80", + "support_interface_speed": "30", + "support_speed": "90", + "top_surface_speed": "50", + "travel_speed": "300", + "top_surface_acceleration": "2500", + "initial_layer_acceleration": "1000", + "travel_acceleration": "5000", + "inner_wall_acceleration": "5000", + "ironing_speed": "25", + "overhang_1_4_speed": "0", + "overhang_2_4_speed": "50", + "overhang_3_4_speed": "30", + "overhang_4_4_speed": "10", + "initial_layer_infill_speed": "50", + "outer_wall_acceleration": "2500", + "bridge_acceleration": "3500", + "sparse_infill_acceleration": "5000", + "internal_solid_infill_acceleration": "5000", + "initial_layer_travel_speed": "100%", + "small_perimeter_speed": "50%", + "filename_format": "{input_filename_base}_{\"E\"}{initial_tool}_{filament_type[initial_tool]}_{print_time}.gcode" +} diff --git a/resources/profiles/re3D/process/0.32mm Draft @re3D fdm 0.4.json b/resources/profiles/re3D/process/0.32mm Draft @re3D fdm 0.4.json index 3ce98e2e7e..1b14349f81 100644 --- a/resources/profiles/re3D/process/0.32mm Draft @re3D fdm 0.4.json +++ b/resources/profiles/re3D/process/0.32mm Draft @re3D fdm 0.4.json @@ -22,5 +22,32 @@ "top_surface_line_width": "0.48", "support_line_width": "0.48", "support_top_z_distance": "0.35", - "support_bottom_z_distance": "0.35" -} \ No newline at end of file + "support_bottom_z_distance": "0.35", + "outer_wall_speed": "60", + "initial_layer_speed": "50", + "gap_infill_speed": "60", + "sparse_infill_speed": "80", + "inner_wall_speed": "80", + "internal_solid_infill_speed": "80", + "support_interface_speed": "30", + "support_speed": "90", + "top_surface_speed": "50", + "travel_speed": "300", + "top_surface_acceleration": "2500", + "initial_layer_acceleration": "1000", + "travel_acceleration": "5000", + "inner_wall_acceleration": "5000", + "ironing_speed": "25", + "overhang_1_4_speed": "0", + "overhang_2_4_speed": "50", + "overhang_3_4_speed": "30", + "overhang_4_4_speed": "10", + "initial_layer_infill_speed": "50", + "outer_wall_acceleration": "2500", + "bridge_acceleration": "3500", + "sparse_infill_acceleration": "5000", + "internal_solid_infill_acceleration": "5000", + "initial_layer_travel_speed": "100%", + "small_perimeter_speed": "50%", + "filename_format": "{input_filename_base}_{\"E\"}{initial_tool}_{filament_type[initial_tool]}_{print_time}.gcode" +} diff --git a/resources/profiles/re3D/process/0.3mm Fine @re3D fdm 0.8.json b/resources/profiles/re3D/process/0.3mm Fine @re3D fdm 0.8.json index 27c2452d19..2b35f2c1f8 100644 --- a/resources/profiles/re3D/process/0.3mm Fine @re3D fdm 0.8.json +++ b/resources/profiles/re3D/process/0.3mm Fine @re3D fdm 0.8.json @@ -22,5 +22,32 @@ "top_surface_line_width": "1", "support_line_width": "1", "support_top_z_distance": "0.24", - "support_bottom_z_distance": "0.24" -} \ No newline at end of file + "support_bottom_z_distance": "0.24", + "outer_wall_speed": "60", + "initial_layer_speed": "50", + "gap_infill_speed": "60", + "sparse_infill_speed": "80", + "inner_wall_speed": "80", + "internal_solid_infill_speed": "80", + "support_interface_speed": "30", + "support_speed": "90", + "top_surface_speed": "50", + "travel_speed": "300", + "top_surface_acceleration": "2500", + "initial_layer_acceleration": "1000", + "travel_acceleration": "5000", + "inner_wall_acceleration": "5000", + "ironing_speed": "25", + "overhang_1_4_speed": "0", + "overhang_2_4_speed": "50", + "overhang_3_4_speed": "30", + "overhang_4_4_speed": "10", + "initial_layer_infill_speed": "50", + "outer_wall_acceleration": "2500", + "bridge_acceleration": "3500", + "sparse_infill_acceleration": "5000", + "internal_solid_infill_acceleration": "5000", + "initial_layer_travel_speed": "100%", + "small_perimeter_speed": "50%", + "filename_format": "{input_filename_base}_{\"E\"}{initial_tool}_{filament_type[initial_tool]}_{print_time}.gcode" +} diff --git a/resources/profiles/re3D/process/0.4mm Draft @re3D fdm 0.8.json b/resources/profiles/re3D/process/0.4mm Draft @re3D fdm 0.8.json index d46316ef4e..d00f8bce44 100644 --- a/resources/profiles/re3D/process/0.4mm Draft @re3D fdm 0.8.json +++ b/resources/profiles/re3D/process/0.4mm Draft @re3D fdm 0.8.json @@ -22,5 +22,32 @@ "top_surface_line_width": "1", "support_line_width": "1", "support_top_z_distance": "0.42", - "support_bottom_z_distance": "0.42" -} \ No newline at end of file + "support_bottom_z_distance": "0.42", + "outer_wall_speed": "60", + "initial_layer_speed": "50", + "gap_infill_speed": "60", + "sparse_infill_speed": "80", + "inner_wall_speed": "80", + "internal_solid_infill_speed": "80", + "support_interface_speed": "30", + "support_speed": "90", + "top_surface_speed": "50", + "travel_speed": "300", + "top_surface_acceleration": "2500", + "initial_layer_acceleration": "1000", + "travel_acceleration": "5000", + "inner_wall_acceleration": "5000", + "ironing_speed": "25", + "overhang_1_4_speed": "0", + "overhang_2_4_speed": "50", + "overhang_3_4_speed": "30", + "overhang_4_4_speed": "10", + "initial_layer_infill_speed": "50", + "outer_wall_acceleration": "2500", + "bridge_acceleration": "3500", + "sparse_infill_acceleration": "5000", + "internal_solid_infill_acceleration": "5000", + "initial_layer_travel_speed": "100%", + "small_perimeter_speed": "50%", + "filename_format": "{input_filename_base}_{\"E\"}{initial_tool}_{filament_type[initial_tool]}_{print_time}.gcode" +} diff --git a/resources/profiles/re3D/process/fdm_process_common.json b/resources/profiles/re3D/process/fdm_process_common.json index 164ff9d58a..dcd5c826ae 100644 --- a/resources/profiles/re3D/process/fdm_process_common.json +++ b/resources/profiles/re3D/process/fdm_process_common.json @@ -6,34 +6,27 @@ "adaptive_layer_height": "0", "reduce_crossing_wall": "0", "bridge_flow": "0.95", - "bridge_speed": "10", "brim_width": "5", "compatible_printers": [], "print_sequence": "by layer", - "default_acceleration": "0", "bridge_no_support": "0", "elefant_foot_compensation": "0.1", "outer_wall_line_width": "0.4", - "outer_wall_speed": "25", "line_width": "0.4", "infill_direction": "45", "sparse_infill_density": "15%", "sparse_infill_pattern": "crosshatch", "initial_layer_line_width": "0.4", "initial_layer_print_height": "0.2", - "initial_layer_speed": "15", - "gap_infill_speed": "25", "infill_combination": "0", "sparse_infill_line_width": "0.4", "infill_wall_overlap": "25%", - "sparse_infill_speed": "70", "interface_shells": "0", "detect_overhang_wall": "0", "reduce_infill_retraction": "0", "filename_format": "{input_filename_base}.gcode", "wall_loops": "3", "inner_wall_line_width": "0.4", - "inner_wall_speed": "40", "print_settings_id": "", "raft_layers": "0", "seam_position": "nearest", @@ -41,7 +34,6 @@ "skirt_height": "2", "minimum_sparse_infill_area": "0", "internal_solid_infill_line_width": "0.4", - "internal_solid_infill_speed": "60", "spiral_mode": "0", "standby_temperature_delta": "-20", "enable_support": "0", @@ -53,16 +45,12 @@ "support_interface_loop_pattern": "0", "support_interface_top_layers": "2", "support_interface_spacing": "0", - "support_interface_speed": "80", "support_base_pattern": "rectilinear", "support_base_pattern_spacing": "2", - "support_speed": "40", "support_threshold_angle": "30", "support_object_xy_distance": "0.5", "detect_thin_wall": "0", "top_surface_line_width": "0.4", - "top_surface_speed": "35", - "travel_speed": "150", "enable_prime_tower": "1", "prime_tower_width": "60", "xy_hole_compensation": "0", diff --git a/resources/profiles/re3D/process/fdm_process_re3D_common.json b/resources/profiles/re3D/process/fdm_process_re3D_common.json index a1547448e3..ea169330f4 100644 --- a/resources/profiles/re3D/process/fdm_process_re3D_common.json +++ b/resources/profiles/re3D/process/fdm_process_re3D_common.json @@ -1,116 +1,88 @@ { - "type": "process", - "name": "fdm_process_re3D_common", - "from": "system", - "instantiation": "false", - "inherits": "fdm_process_common", - "adaptive_layer_height": "0", - "reduce_crossing_wall": "1", - "bridge_flow": "0.985", - "bridge_speed": "25", - "brim_width": "8", - "print_sequence": "by layer", - "default_acceleration": "5000", - "bridge_no_support": "0", - "elefant_foot_compensation": "0", - "outer_wall_speed": "120", - "sparse_infill_density": "15%", - "sparse_infill_pattern": "rectilinear", - "initial_layer_speed": "50", - "gap_infill_speed": "30", - "infill_combination": "0", - "infill_wall_overlap": "25%", - "sparse_infill_speed": "50", - "detect_overhang_wall": "1", - "reduce_infill_retraction": "0", - "filename_format": "{input_filename_base}.gcode", - "wall_loops": "3", - "inner_wall_speed": "40", - "wall_generator": "arachne", - "raft_layers": "0", - "seam_position": "nearest", - "skirt_distance": "8", - "skirt_height": "1", - "minimum_sparse_infill_area": "0", - "internal_solid_infill_speed": "40", - "spiral_mode": "0", - "standby_temperature_delta": "-75", - "enable_support": "1", - "support_filament": "0", - "support_interface_filament": "0", - "support_on_build_plate_only": "0", - "support_interface_loop_pattern": "0", - "support_interface_top_layers": "2", - "support_interface_spacing": "0.05", - "support_interface_speed": "80", - "support_base_pattern": "rectilinear", - "support_base_pattern_spacing": "2", - "support_speed": "40", - "support_threshold_angle": "30", - "support_object_xy_distance": "0.5", - "detect_thin_wall": "0", - "top_surface_speed": "30", - "travel_speed": "300", - "enable_prime_tower": "0", - "prime_tower_width": "60", - "xy_hole_compensation": "0", - "xy_contour_compensation": "0", - "max_travel_detour_distance": "0", - "bottom_surface_pattern": "monotonic", - "bottom_shell_layers": "3", - "bottom_shell_thickness": "0", - "brim_object_gap": "0.1", - "compatible_printers_condition": "", - "top_surface_acceleration": "500", - "draft_shield": "disabled", - "enable_arc_fitting": "1", - "wall_infill_order": "inner wall/outer wall/infill", - "infill_direction": "45", - "initial_layer_acceleration": "500", - "travel_acceleration": "5000", - "inner_wall_acceleration": "5000", - "interface_shells": "0", - "ironing_flow": "10%", - "ironing_spacing": "0.1", - "ironing_speed": "20", - "ironing_type": "no ironing", - "overhang_1_4_speed": "45", - "overhang_2_4_speed": "35", - "overhang_3_4_speed": "25", - "overhang_4_4_speed": "15", - "print_settings_id": "fdm_process_re3D_common", - "skirt_loops": "2", - "resolution": "0.0", - "support_type": "normal(auto)", - "support_style": "snug", - "support_interface_bottom_layers": "2", - "tree_support_branch_angle": "45", - "tree_support_wall_count": "0", - "top_surface_pattern": "monotonicline", - "top_shell_layers": "4", - "top_shell_thickness": "0.6", - "initial_layer_infill_speed": "50", - "wipe_tower_no_sparse_layers": "0", - "precise_outer_wall": "0", - "outer_wall_acceleration": "2500", - "bridge_acceleration": "5000", - "sparse_infill_acceleration": "5000", - "internal_solid_infill_acceleration": "5000", - "accel_to_decel_enable": "0", - "prime_volume": "200", - "ooze_prevention": "1", - "preheat_time": "30", - "initial_layer_travel_speed": "100", - "slow_down_layers": "2", - "small_perimeter_speed": "20", - "small_perimeter_threshold": "10", - "exclude_object": "1", - "compatible_printers": [ - "re3D Gigabot 4 0.4 nozzle", - "re3D Gigabot 4 0.8 nozzle", - "re3D Gigabot 4 XLT 0.4 nozzle", - "re3D Gigabot 4 XLT 0.8 nozzle", - "re3D Terabot 4 0.4 nozzle", - "re3D Terabot 4 0.8 nozzle" - ] -} \ No newline at end of file + "type": "process", + "name": "fdm_process_re3D_common", + "from": "system", + "instantiation": "false", + "inherits": "fdm_process_common", + "adaptive_layer_height": "0", + "reduce_crossing_wall": "1", + "bridge_flow": "0.985", + "brim_width": "8", + "print_sequence": "by layer", + "bridge_no_support": "0", + "elefant_foot_compensation": "0", + "sparse_infill_density": "15%", + "sparse_infill_pattern": "rectilinear", + "infill_combination": "0", + "infill_wall_overlap": "25%", + "detect_overhang_wall": "1", + "reduce_infill_retraction": "0", + "filename_format": "{input_filename_base}.gcode", + "wall_loops": "3", + "wall_generator": "arachne", + "raft_layers": "0", + "seam_position": "nearest", + "skirt_distance": "8", + "skirt_height": "1", + "minimum_sparse_infill_area": "0", + "spiral_mode": "0", + "standby_temperature_delta": "-75", + "enable_support": "1", + "support_filament": "0", + "support_interface_filament": "0", + "support_on_build_plate_only": "0", + "support_interface_loop_pattern": "0", + "support_interface_top_layers": "2", + "support_interface_spacing": "0.05", + "support_base_pattern": "rectilinear", + "support_base_pattern_spacing": "2", + "support_threshold_angle": "30", + "support_object_xy_distance": "0.5", + "detect_thin_wall": "0", + "enable_prime_tower": "0", + "prime_tower_width": "60", + "xy_hole_compensation": "0", + "xy_contour_compensation": "0", + "max_travel_detour_distance": "0", + "bottom_surface_pattern": "monotonic", + "bottom_shell_layers": "3", + "bottom_shell_thickness": "0", + "brim_object_gap": "0.1", + "compatible_printers_condition": "", + "draft_shield": "disabled", + "enable_arc_fitting": "1", + "wall_infill_order": "inner wall/outer wall/infill", + "infill_direction": "45", + "interface_shells": "0", + "ironing_flow": "10%", + "ironing_spacing": "0.1", + "ironing_type": "no ironing", + "print_settings_id": "fdm_process_re3D_common", + "skirt_loops": "2", + "resolution": "0.0", + "support_type": "normal(auto)", + "support_style": "snug", + "support_interface_bottom_layers": "2", + "tree_support_branch_angle": "45", + "tree_support_wall_count": "0", + "top_surface_pattern": "monotonicline", + "top_shell_layers": "4", + "top_shell_thickness": "0.6", + "wipe_tower_no_sparse_layers": "0", + "precise_outer_wall": "0", + "accel_to_decel_enable": "0", + "prime_volume": "200", + "ooze_prevention": "1", + "preheat_time": "30", + "slow_down_layers": "2", + "small_perimeter_threshold": "10", + "exclude_object": "1", + "compatible_printers": [ + "re3D Gigabot 4 0.4 nozzle", + "re3D Gigabot 4 0.8 nozzle", + "re3D Gigabot 4 XLT 0.4 nozzle", + "re3D Gigabot 4 XLT 0.8 nozzle", + "re3D Terabot 4 0.4 nozzle", + "re3D Terabot 4 0.8 nozzle" + ] +} diff --git a/scripts/build_flatpak_with_docker.sh b/scripts/build_flatpak_with_docker.sh index 9e8805af2f..eedfe53692 100755 --- a/scripts/build_flatpak_with_docker.sh +++ b/scripts/build_flatpak_with_docker.sh @@ -122,6 +122,9 @@ sed "/name: OrcaSlicer/{ \1 git_commit_hash: \"$GIT_COMMIT_HASH\"| }" > "$MANIFEST_DOCKER" +# ---------- pack deps/ ---------- +./scripts/flatpak/make_deps_tar.sh + # ---------- run build in Docker ---------- DOCKER="${DOCKER:-docker}" diff --git a/scripts/build_preset_cache.bat b/scripts/build_preset_cache.bat new file mode 100644 index 0000000000..82f3e02723 --- /dev/null +++ b/scripts/build_preset_cache.bat @@ -0,0 +1,141 @@ +@echo off +rem Build the per-vendor system preset caches (one <vendor>.opc per vendor) by +rem running the generate_system_cache.exe dev tool against a profiles directory, +rem and make every profiles directory named on the command line ship-ready: +rem install the caches into it and delete the preset JSONs they replace, so a +rem build ships one copy of its presets instead of two. +rem +rem scripts\build_preset_cache.bat [build_dir] [target_dir ...] +rem +rem build_dir defaults to "build" +rem target_dir profiles directories to ship into. Caches are generated into +rem the source tree's resources\profiles, which is what every +rem packaging step copies from; a target may be that same +rem directory, which then only gets pruned. +rem --prune-source +rem allow a target that is the directory the caches were generated +rem into (resources\profiles). Pruning it deletes the checkout's +rem own preset JSONs, which is a packaging step - not something a +rem build should do to a working tree by surprise. CI passes it. +rem +rem Shipping deletes, so it is a CI packaging step. A vendor's own <vendor>.json +rem goes along with its preset JSONs: the cache carries the vendor profile and +rem the version it was built at, so discovery, version checks and installing all +rem read it there. Only a vendor that has a cache is pruned, so non-vendor JSONs +rem (blacklist.json) are left alone, as are the vendor directories themselves - +rem thumbnails, covers and bed models still live there. +rem +rem set CONFIG=<cfg> to pin the build config for multi-config generators +rem (default: the config of the tool already in the build tree, else Release) +setlocal enabledelayedexpansion + +set "REPO_ROOT=%~dp0.." + +set "PRUNE_SOURCE=" +:parse_flags +if /i "%~1"=="--prune-source" ( + set "PRUNE_SOURCE=1" + shift + goto :parse_flags +) + +set "BUILD_DIR=%~1" +if "%BUILD_DIR%"=="" set "BUILD_DIR=build" +if not exist "%BUILD_DIR%\" ( + echo ERROR: build tree not found: %BUILD_DIR% 1>&2 + exit /b 1 +) +if not "%~1"=="" shift + +rem Newest match wins: a stale binary silently produces a stale cache layout. +call :find_tool +if not defined CONFIG ( + for %%c in (Debug Release RelWithDebInfo MinSizeRel) do ( + echo !TOOL! | findstr /i "\\%%c\\" >nul && set "CONFIG=%%c" + ) +) +if not defined CONFIG set "CONFIG=Release" + +echo Building generate_system_cache in %BUILD_DIR% (%CONFIG%) +cmake --build "%BUILD_DIR%" --config %CONFIG% --target generate_system_cache +if errorlevel 1 ( + echo ERROR: could not build generate_system_cache - configure the build tree with -DORCA_TOOLS=ON: 1>&2 + echo cmake -S "%REPO_ROOT%" -B "%BUILD_DIR%" -DORCA_TOOLS=ON 1>&2 + exit /b 1 +) +call :find_tool +if not defined TOOL ( + echo ERROR: generate_system_cache.exe not found under %BUILD_DIR% - build with -DORCA_TOOLS=ON 1>&2 + exit /b 1 +) + +set "PROFILES=%REPO_ROOT%\resources\profiles" +if not exist "%PROFILES%\" ( + echo ERROR: profiles directory not found: %PROFILES% 1>&2 + exit /b 1 +) +for %%d in ("%PROFILES%") do set "PROFILES=%%~fd" + +rem Add the slicer's runtime DLL directory to PATH so generate_system_cache.exe +rem can resolve its dependencies (TKernel.dll etc.) without a full install step. +set "DLL_DIR=" +for /f "delims=" %%f in ('dir /s /b "%BUILD_DIR%\TKernel.dll" 2^>nul') do ( + if not defined DLL_DIR set "DLL_DIR=%%~dpf" +) +if defined DLL_DIR set "PATH=%DLL_DIR%;%PATH%" + +echo Generating per-vendor preset caches in %PROFILES% +rem Start clean so vendors that went away - and caches written by older tool +rem versions - don't linger next to the freshly generated ones. +del /q "%PROFILES%\*.opc" 2>nul +del /q "%PROFILES%\*.cache" 2>nul +"%TOOL%" --path "%PROFILES%" --log_level 2 +if errorlevel 1 exit /b %errorlevel% + +:next_target +if "%~1"=="" exit /b 0 +call :ship "%~1" +if errorlevel 1 exit /b 1 +shift +goto :next_target + +:ship +set "TARGET=%~1" +if not exist "%TARGET%\" ( + echo ERROR: profiles directory not found: %TARGET% 1>&2 + exit /b 1 +) +for %%d in ("%TARGET%") do set "TARGET=%%~fd" +if /i "%TARGET%"=="%PROFILES%" if not defined PRUNE_SOURCE ( + echo %TARGET%: skipped - this is where the caches were generated. + echo Pass --prune-source to prune it; that deletes this checkout's preset JSONs. + exit /b 0 +) +if /i not "%TARGET%"=="%PROFILES%" copy /y "%PROFILES%\*.opc" "%TARGET%\" >nul + +set /a SHIPPED=0 +set /a PRUNED=0 +for %%c in ("%PROFILES%\*.opc") do ( + set /a SHIPPED+=1 + set "VENDOR=%%~nc" + if exist "%TARGET%\!VENDOR!.json" ( + del /q "%TARGET%\!VENDOR!.json" + set /a PRUNED+=1 + ) + if exist "%TARGET%\!VENDOR!\" ( + for /f %%n in ('dir /s /b "%TARGET%\!VENDOR!\*.json" 2^>nul ^| find /c /v ""') do set /a PRUNED+=%%n + del /s /q "%TARGET%\!VENDOR!\*.json" >nul 2>&1 + rem Deepest first, so a directory the delete above emptied goes too; rd + rem refuses the ones still holding covers or meshes. + for /f "delims=" %%d in ('dir /s /b /ad "%TARGET%\!VENDOR!" 2^>nul ^| sort /r') do rd "%%d" 2>nul + ) +) +echo %TARGET%: !SHIPPED! caches, dropped !PRUNED! preset JSONs +exit /b 0 + +:find_tool +set "TOOL=" +for /f "delims=" %%f in ('dir /s /b /o-d "%BUILD_DIR%\generate_system_cache.exe" 2^>nul') do ( + if not defined TOOL set "TOOL=%%f" +) +exit /b 0 diff --git a/scripts/build_preset_cache.sh b/scripts/build_preset_cache.sh new file mode 100755 index 0000000000..7a874f7e06 --- /dev/null +++ b/scripts/build_preset_cache.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +# Build the per-vendor system preset caches (one <vendor>.opc per vendor) by +# running the generate_system_cache dev tool against a profiles directory, and +# make every profiles directory named on the command line ship-ready: install +# the caches into it and delete the preset JSONs they replace, so a build ships +# one copy of its presets instead of two. +# +# ./scripts/build_preset_cache.sh # caches into resources/profiles +# ./scripts/build_preset_cache.sh -b build/arm64 # search this build tree for the tool +# ./scripts/build_preset_cache.sh <dir> [<dir> ...] # and ship into these profiles dirs +# +# Caches are generated into the source tree's resources/profiles, which is what +# every packaging step copies from. Shipping deletes, so it is a CI packaging +# step: pass packaged output directories, or the checkout of a build that is +# about to be packaged from it. +# +# A vendor's own <vendor>.json goes along with its preset JSONs: the cache +# carries the vendor profile and the version it was built at, so discovery, +# version checks and installing all read it there. A shipped vendor is its cache +# and nothing else. Only a vendor that has a cache is pruned, so an ungenerated +# vendor keeps its JSONs and is simply parsed at startup; non-vendor JSONs +# (blacklist.json) are left alone, as are the vendor directories themselves — +# thumbnails, covers and bed models still live there. +# +# -b <dir> build tree holding the tool +# (default: build/arm64, build/x86_64, or build — first that exists) +# -p <dir> profiles directory to generate caches into +# (default: <repo>/resources/profiles) +# -c <cfg> build config for multi-config generators +# (default: the config of the tool already in the build tree, else +# the build tree's CMAKE_BUILD_TYPE) +# -n skip the rebuild and run the tool already in the build tree +# -l <level> tool log level (default: 2) +# --prune-source +# allow a target that is the directory the caches were generated +# into (resources/profiles). Pruning it deletes the checkout's own +# preset JSONs, which is a packaging step - not something a build +# should do to a working tree by surprise. +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd -P)" +build_dir="" +profiles_dir="" +config="" +build_tool=1 +log_level=2 +prune_source=0 + +# getopts does not do long options; pull this one out first. +args=() +for arg in "$@"; do + if [ "$arg" = "--prune-source" ]; then prune_source=1; else args+=("$arg"); fi +done +set -- ${args+"${args[@]}"} + +while getopts "b:p:c:l:nh" opt; do + case $opt in + b) build_dir="$OPTARG" ;; + p) profiles_dir="$OPTARG" ;; + c) config="$OPTARG" ;; + n) build_tool=0 ;; + l) log_level="$OPTARG" ;; + h) sed -n '2,${/^#/!q;p;}' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) exit 1 ;; + esac +done +shift $((OPTIND - 1)) + +if [ -z "$build_dir" ]; then + for candidate in "$repo_root/build/arm64" "$repo_root/build/x86_64" "$repo_root/build"; do + if [ -d "$candidate" ]; then build_dir="$candidate"; break; fi + done +fi +if [ -z "$build_dir" ] || [ ! -d "$build_dir" ]; then + echo "ERROR: build tree not found (pass -b <build_dir>)" >&2 + exit 1 +fi + +# Newest match wins: multi-config trees keep one binary per config, and a stale +# one silently produces a stale cache layout. +find_tool() { + local best="" f + while IFS= read -r f; do + [ -n "$f" ] || continue + if [ -z "$best" ] || [ "$f" -nt "$best" ]; then best="$f"; fi + done < <(find "$build_dir" -name generate_system_cache -type f 2>/dev/null) + printf '%s' "$best" +} + +tool=$(find_tool) +if [ -z "$config" ]; then + case "$tool" in + */Debug/*) config=Debug ;; + */Release/*) config=Release ;; + */RelWithDebInfo/*) config=RelWithDebInfo ;; + */MinSizeRel/*) config=MinSizeRel ;; + *) config=$(sed -n 's/^CMAKE_BUILD_TYPE:[A-Z]*=\(.\+\)$/\1/p' "$build_dir/CMakeCache.txt" 2>/dev/null | head -1 || true) ;; + esac +fi + +if [ "$build_tool" = 1 ]; then + echo "Building generate_system_cache in $build_dir${config:+ ($config)}" + build_args=(--build "$build_dir" --target generate_system_cache) + if [ -n "$config" ]; then build_args+=(--config "$config"); fi + if ! cmake "${build_args[@]}"; then + echo "ERROR: could not build generate_system_cache — configure the build tree with -DORCA_TOOLS=ON:" >&2 + echo " cmake -S \"$repo_root\" -B \"$build_dir\" -DORCA_TOOLS=ON" >&2 + exit 1 + fi + tool=$(find_tool) +fi + +if [ -z "$tool" ]; then + echo "ERROR: generate_system_cache not found under $build_dir — build with -DORCA_TOOLS=ON" >&2 + exit 1 +fi + +if [ -z "$profiles_dir" ]; then profiles_dir="$repo_root/resources/profiles"; fi +if [ ! -d "$profiles_dir" ]; then + echo "ERROR: profiles directory not found: $profiles_dir" >&2 + exit 1 +fi +profiles_dir=$(cd "$profiles_dir" && pwd -P) + +# Start clean so vendors that went away — and caches written by older tool +# versions — don't linger next to the freshly generated ones. +echo "Generating per-vendor preset caches in $profiles_dir" +rm -f "$profiles_dir"/*.opc "$profiles_dir"/*.cache +"$tool" --path "$profiles_dir" --log_level "$log_level" + +for target in "$@"; do + resolved=$(cd "$target" 2>/dev/null && pwd -P) || { + echo "ERROR: profiles directory not found: $target" >&2 + exit 1 + } + if [ "$resolved" = "$profiles_dir" ] && [ "$prune_source" -eq 0 ]; then + echo "$resolved: skipped - this is where the caches were generated." + echo " Pass --prune-source to prune it; that deletes this checkout's preset JSONs." + continue + fi + if [ "$resolved" != "$profiles_dir" ]; then + cp "$profiles_dir"/*.opc "$resolved"/ + fi + + pruned=0 + shipped=0 + for cache in "$profiles_dir"/*.opc; do + vendor=$(basename "$cache" .opc) + shipped=$(( shipped + 1 )) + if [ -f "$resolved/$vendor.json" ]; then + rm -f "$resolved/$vendor.json" + pruned=$(( pruned + 1 )) + fi + [ -d "$resolved/$vendor" ] || continue + n=$(find "$resolved/$vendor" -name '*.json' | wc -l) + find "$resolved/$vendor" -name '*.json' -delete + find "$resolved/$vendor" -type d -empty -delete + pruned=$(( pruned + n )) + done + echo "$resolved: $shipped caches, dropped $pruned preset JSONs" +done diff --git a/scripts/check_profile.bat b/scripts/check_profile.bat new file mode 100644 index 0000000000..231fa721d4 --- /dev/null +++ b/scripts/check_profile.bat @@ -0,0 +1,6 @@ +@echo off +REM Runs check_profile.ps1, the Windows twin of check_profile.sh, from cmd. +REM -ExecutionPolicy Bypass is needed because a Windows client defaults to Restricted, +REM which refuses to run a checked-out .ps1 at all. +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0check_profile.ps1" %* +exit /b %ERRORLEVEL% diff --git a/scripts/check_profile.ps1 b/scripts/check_profile.ps1 new file mode 100644 index 0000000000..5c5dcb20c8 --- /dev/null +++ b/scripts/check_profile.ps1 @@ -0,0 +1,564 @@ +<# +.SYNOPSIS + Local twin of the "Check profiles" CI job (.github/workflows/check_profiles.yml), for Windows. + +.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 + 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. + + extra_json_check scripts/orca_extra_profile_check.py + validate_system validator -p <profiles> -l <level> + validate_slice validator -p <profiles> -s -l <level> + validate_filament_subtypes validator -p <profiles> -l <level> -f + validate_custom validator against every released custom-preset fixture + + Everything that has to be downloaded - the profile validator and the custom-preset fixture + archives - lands under <repo>\.test\check_profiles and is reused on the next run. That + directory also holds one log per check plus a copy of the comment CI would post on the PR. + + resources\profiles\user, which the validator creates as its data dir but a CI checkout never + has, is moved aside for the duration of the run and restored on exit. Only one run per work + dir at a time. + + x64 and ARM64 hosts are both supported. A locally built validator is chosen by the machine + type in its PE header rather than by the name of its build tree, so build\ (x64) and + build-arm64\ side by side resolve correctly; the published nightly is x64 only and runs + under emulation on ARM64. + +.PARAMETER ProfilesDir + Profile tree to validate (default: resources\profiles). extra_json_check always looks at the + tree next to the script, so this only redirects the validator checks. + +.PARAMETER Validator + OrcaSlicer_profile_validator.exe to use; also $env:ORCA_PROFILE_VALIDATOR. Default: the local + build*\ Release build (then RelWithDebInfo, MinSizeRel, Debug) for this architecture, else + the nightly release build is downloaded. + +.PARAMETER Download + Ignore local builds and use the downloaded nightly validator. + +.PARAMETER Refresh + Re-download the validator and fixtures instead of using the cache. + +.PARAMETER WorkDir + Downloads, logs and fixture trees (default: .test\check_profiles). Point it somewhere short, + such as D:\t, if a fixture tree trips Windows' 260-character path limit. + +.PARAMETER LogLevel + Validator log level (default: 2, as in CI). + +.PARAMETER Checks + Checks to run, by name (default: all of them, in the order listed above). + +.EXAMPLE + scripts\check_profile.bat + +.EXAMPLE + powershell -ExecutionPolicy Bypass -File scripts\check_profile.ps1 validate_system validate_slice +#> + +# PositionalBinding is off so that the check names are the only positional arguments; left on, +# a bare "validate_system" would bind to whichever named parameter came next in this block. +[CmdletBinding(PositionalBinding = $false)] +param( + [Alias('p')] [string] $ProfilesDir, + [string] $Validator, + [string] $WorkDir, + [Alias('l')] [int] $LogLevel = 2, + [switch] $Download, + [switch] $Refresh, + [Alias('h')] [switch] $Help, + [Parameter(Position = 0, ValueFromRemainingArguments = $true)] [string[]] $Checks +) + +$ErrorActionPreference = 'Stop' + +# Windows PowerShell 5.1 still negotiates TLS 1.0/1.1, which github.com refuses. +[Net.ServicePointManager]::SecurityProtocol = + [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + +$ValidatorReleaseUrl = 'https://github.com/OrcaSlicer/OrcaSlicer/releases/download/nightly-builds' +$FixtureReleaseUrl = 'https://github.com/OrcaSlicer/OrcaSlicer-profile-validator/releases/download/fixture-archive' + +$RepoRoot = Split-Path -Parent $PSScriptRoot + +# PROCESSOR_ARCHITECTURE reports the architecture of the *shell*, so a 32-bit PowerShell on a +# 64-bit OS says x86; ARCHITEW6432 is the machine's own in that case. +$HostArch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE } +$HostArch = switch ($HostArch) { + 'ARM64' { 'arm64' } + 'AMD64' { 'x64' } + default { 'x86' } +} + +$AllChecks = @('extra_json_check', 'validate_system', 'validate_slice', 'validate_filament_subtypes', 'validate_custom') + +$script:LogWriter = $null +$script:Python = '' + +# ---------------------------------------------------------------------------- helpers + +function Die([string] $Message) { + Write-Host "check_profile.ps1: $Message" -ForegroundColor Red + exit 2 +} + +# UTF-8 without a BOM, so a log reads the same here as the artifact CI uploads. +function New-LogWriter([string] $Path) { + New-Object IO.StreamWriter($Path, $false, (New-Object Text.UTF8Encoding($false))) +} + +# Output of the check being run: shown, and kept in that check's log. +function Write-CheckLog([string] $Text) { + Write-Host $Text + if ($script:LogWriter) { $script:LogWriter.WriteLine($Text) } +} + +# Runs a program and returns its exit code, streaming stdout and stderr into the current check's +# log. -OutFile sends that output to a file of its own instead, silently. +function Invoke-Tool { + param([string] $Exe, [string[]] $Arguments, [string] $OutFile) + + # Under 'Stop', 2>&1 turns every stderr line of a native command into a terminating error. + # Assigning the preference here scopes it to this function, so it undoes itself on return. + $ErrorActionPreference = 'Continue' + + $writer = if ($OutFile) { New-LogWriter $OutFile } else { $null } + try { + & $Exe @Arguments 2>&1 | ForEach-Object { + if ($writer) { $writer.WriteLine("$_") } else { Write-CheckLog "$_" } + } + return $LASTEXITCODE + } catch { + if ($writer) { $writer.WriteLine("$_") } else { Write-CheckLog "$_" } + return 1 + } finally { + if ($writer) { $writer.Dispose() } + } +} + +# The first $Limit characters of a log, as CI truncates them for the PR comment. +function Get-LogHead([string] $Path, [int] $Limit) { + $text = if (Test-Path -LiteralPath $Path) { [IO.File]::ReadAllText($Path) } else { '' } + if (-not $text) { return 'No output captured' } + if ($text.Length -gt $Limit) { return $text.Substring(0, $Limit) } + return $text +} + +# ---------------------------------------------------------------------------- arguments + +if ($Help) { Get-Help $PSCommandPath -Detailed; exit 0 } + +foreach ($name in $Checks) { + if ($name -like '-*') { Die "unknown option '$name' (try -Help)" } + if ($AllChecks -notcontains $name) { Die "unknown check '$name' (try -Help)" } +} +if (-not $Checks) { $Checks = $AllChecks } + +if (-not $ProfilesDir) { $ProfilesDir = Join-Path $RepoRoot 'resources\profiles' } +if (-not (Test-Path -LiteralPath $ProfilesDir -PathType Container)) { Die "profile directory not found: $ProfilesDir" } +$ProfilesDir = (Resolve-Path -LiteralPath $ProfilesDir).Path + +if (-not $WorkDir) { $WorkDir = Join-Path $RepoRoot '.test\check_profiles' } +$LogDir = Join-Path $WorkDir 'logs' +try { New-Item -ItemType Directory -Force -Path $LogDir | Out-Null } catch { Die "cannot create ${LogDir}: $_" } +$WorkDir = (Resolve-Path -LiteralPath $WorkDir).Path +$LogDir = Join-Path $WorkDir 'logs' + +if (-not $Validator) { $Validator = $env:ORCA_PROFILE_VALIDATOR } + +# ------------------------------------------------------------------- clean profile tree + +# The validator points its data dir at the profile tree, so it creates - and, with -g, fills - +# <profiles>\user. A CI checkout never has that directory, and anything left in it from an +# earlier local run would be loaded as user presets and validated too. Move it aside for the +# duration of the run so what gets checked is what CI checks. +$script:StashedUserDir = '' + +function Push-UserPresets { + $user = Join-Path $ProfilesDir 'user' + if (-not (Test-Path -LiteralPath $user -PathType Container)) { return } + $script:StashedUserDir = Join-Path $WorkDir "user-presets-$PID" + Remove-Item -LiteralPath $script:StashedUserDir -Recurse -Force -ErrorAction SilentlyContinue + try { Move-Item -LiteralPath $user -Destination $script:StashedUserDir } + catch { $script:StashedUserDir = ''; Die "cannot move $user aside: $_" } + Write-Host "moved $user aside for the run (restored on exit)" +} + +function Pop-UserPresets { + # The validator leaves an empty user\default\{filament,machine,process} skeleton behind. + # Prune it directory by directory, never wholesale: one that holds a real file survives and + # is reported instead of being deleted. Runs even when nothing was stashed, so a tree that + # had no user\ before the run does not gain one. + $user = Join-Path $ProfilesDir 'user' + Remove-EmptyDirs $user + if (-not $script:StashedUserDir) { return } + if (Test-Path -LiteralPath $user) { + Write-Host "$user is not empty; your presets stay in $($script:StashedUserDir)" + } else { + Move-Item -LiteralPath $script:StashedUserDir -Destination $user + } + $script:StashedUserDir = '' +} + +function Remove-EmptyDirs([string] $Path) { + if (-not (Test-Path -LiteralPath $Path -PathType Container)) { return } + Get-ChildItem -LiteralPath $Path -Recurse -Directory -Force | + Sort-Object { $_.FullName.Length } -Descending | + ForEach-Object { + if (-not (Get-ChildItem -LiteralPath $_.FullName -Force)) { + Remove-Item -LiteralPath $_.FullName -Force + } + } + if (-not (Get-ChildItem -LiteralPath $Path -Force)) { Remove-Item -LiteralPath $Path -Force } +} + +# ---------------------------------------------------------------------------- downloads + +# Cached; -Refresh, or -Force, pulls a new copy. Release assets never change, so caching them is +# safe; the fixture manifest is the index that grows with every release, and CI re-reads it on +# every run. +function Save-Asset { + param([string] $Url, [string] $Dest, [switch] $Force) + + if (-not $Refresh -and -not $Force -and (Test-Path -LiteralPath $Dest -PathType Leaf) -and + (Get-Item -LiteralPath $Dest).Length -gt 0) { + return + } + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Dest) | Out-Null + Write-Host "downloading $(Split-Path -Leaf $Dest) ..." + + # Invoke-WebRequest spends most of a large download repainting its progress bar. + $ProgressPreference = 'SilentlyContinue' + $part = "$Dest.part" + for ($attempt = 1; ; $attempt++) { + try { + Invoke-WebRequest -Uri $Url -OutFile $part -UseBasicParsing + break + } catch { + if ($attempt -ge 3) { + Remove-Item -LiteralPath $part -Force -ErrorAction SilentlyContinue + throw "download failed: $Url" + } + } + } + Move-Item -LiteralPath $part -Destination $Dest -Force +} + +function Get-Sha256([string] $Path) { + return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash +} + +# Directories a built validator can sit in, best first: build_win.bat names its trees build, +# build-dbginfo, build-minsize and build-dbg, each optionally -clang and -arm64 suffixed, and +# CMake puts the binary in src\<config>. Each pattern is matched both directly under a build root +# and one level down (build\x64), for trees laid out the way the macOS ones are. +function Get-ValidatorSearchDirs { + foreach ($config in 'Release', 'RelWithDebInfo', 'MinSizeRel', 'Debug') { + Join-Path $RepoRoot "build*\src\$config" + Join-Path $RepoRoot "build*\*\src\$config" + } + Join-Path $RepoRoot 'build*\src' + Join-Path $RepoRoot 'build*\*\src' +} + +# The machine type from the PE header, which is what actually decides whether an .exe can run +# here - the name of the build tree only says what it was meant to be. +function Get-ExeArch([string] $Path) { + try { + $stream = [IO.File]::OpenRead($Path) + try { + $reader = New-Object IO.BinaryReader($stream) + $stream.Position = 0x3C # e_lfanew: offset of the PE header + $stream.Position = $reader.ReadInt32() + if ($reader.ReadUInt32() -ne 0x00004550) { return '' } # "PE\0\0" + switch ($reader.ReadUInt16()) { + 0x8664 { 'x64' } + 0xAA64 { 'arm64' } + 0x014C { 'x86' } + default { '' } + } + } finally { $stream.Dispose() } + } catch { '' } +} + +# First locally built OrcaSlicer_profile_validator.exe, in the order above. An x86 build, and on +# ARM64 an x64 one, is kept only as a fallback: it runs, but emulated. An ARM64 build on an x64 +# host does not run at all and is never offered. +function Find-LocalValidator { + $emulated = '' + foreach ($pattern in (Get-ValidatorSearchDirs)) { + foreach ($dir in @(Resolve-Path -Path $pattern -ErrorAction SilentlyContinue)) { + $candidate = Join-Path $dir.Path 'OrcaSlicer_profile_validator.exe' + if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) { continue } + $arch = Get-ExeArch $candidate + if (-not $arch -or $arch -eq $HostArch) { return $candidate } + if (-not $emulated -and ($arch -eq 'x86' -or $HostArch -eq 'arm64')) { $emulated = $candidate } + } + } + if ($emulated) { Write-Host "no $HostArch build found, falling back to $emulated (emulated)" } + return $emulated +} + +# The nightly release build, same one CI uses. Windows ships the bare .exe, x64 only. +function Save-NightlyValidator { + if ($HostArch -ne 'x64') { + Write-Host "the nightly Windows validator is x64; it runs here under emulation" + } + $exe = Join-Path $WorkDir 'validator\OrcaSlicer_profile_validator.exe' + Save-Asset -Url "$ValidatorReleaseUrl/OrcaSlicer_profile_validator_Windows_nightly.exe" -Dest $exe + return $exe +} + +function Resolve-Validator { + if ($Validator) { + if (-not (Test-Path -LiteralPath $Validator -PathType Leaf)) { Die "validator not found: $Validator" } + return (Resolve-Path -LiteralPath $Validator).Path + } + if (-not $Download) { + $local = Find-LocalValidator + if ($local) { + Write-Host "using locally built validator: $local" + return $local + } + } + try { $downloaded = Save-NightlyValidator } catch { Die "could not obtain a profile validator: $_" } + Write-Host "using downloaded validator: $downloaded" + return $downloaded +} + +# python3 is rarely on PATH on Windows: the py launcher is the reliable way in, and a bare +# `python` may be the Store stub, which prints an advert and exits non-zero. Probe each for the +# interpreter it actually resolves to, and use that. +function Resolve-Python { + $ErrorActionPreference = 'Continue' + if ($script:Python) { return $script:Python } + foreach ($candidate in 'py -3', 'python', 'python3') { + $words = $candidate -split ' ' + $exe = Get-Command $words[0] -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $exe) { continue } + $leading = @($words | Select-Object -Skip 1) + $found = & $exe.Source @leading -c 'import sys; print(sys.executable)' 2>$null + if ($LASTEXITCODE -eq 0 -and $found) { + $script:Python = "$found" + return $script:Python + } + } + Die 'no Python 3 found; install it (or the py launcher) and re-run' +} + +# ---------------------------------------------------------------------------- checks + +$CheckBodies = @{ + + extra_json_check = { + Invoke-Tool -Exe (Resolve-Python) -Arguments @((Join-Path $RepoRoot 'scripts\orca_extra_profile_check.py')) + } + + validate_system = { + Invoke-Tool -Exe $Validator -Arguments @('-p', $ProfilesDir, '-l', "$LogLevel") + } + + # Slices a two-colour cube through every printer so all custom g-code (incl. + # change_filament_gcode) is expanded - catches undefined-placeholder / invalid-flow bugs the + # static checks cannot see. + validate_slice = { + Invoke-Tool -Exe $Validator -Arguments @('-p', $ProfilesDir, '-s', '-l', "$LogLevel") + } + + validate_filament_subtypes = { + Invoke-Tool -Exe $Validator -Arguments @('-p', $ProfilesDir, '-l', "$LogLevel", '-f') + } + + # Every released fixture is a snapshot of user presets saved by that OrcaSlicer version; each + # is unpacked over the current system profiles and validated, so a profile change that would + # break an existing user's presets fails here. + validate_custom = { + $fixturesDir = Join-Path $WorkDir 'profile-fixtures' + $outputDir = Join-Path $WorkDir 'custom-preset-validation' + New-Item -ItemType Directory -Force -Path $fixturesDir, $outputDir | Out-Null + + $manifestPath = Join-Path $fixturesDir 'manifest.json' + Save-Asset -Url "$FixtureReleaseUrl/manifest.json" -Dest $manifestPath -Force + + $manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json + $entries = if ($manifest -is [array]) { $manifest } else { $manifest.fixtures } + $fixtures = @($entries | Where-Object { $_.version -and $_.asset }) + if (-not $fixtures) { + Write-CheckLog "No custom preset fixtures found in $FixtureReleaseUrl/manifest.json" + return 1 + } + + $status = 0 + $failedLogs = @() + $summary = @('## Custom Preset Fixture Validation', '', '| Version | Status | Log |', '| --- | --- | --- |') + + foreach ($fixture in $fixtures) { + $version = $fixture.version + $asset = $fixture.asset + $fixtureZip = Join-Path $fixturesDir $asset + $profileTree = Join-Path $outputDir "profiles-$version" + $logPath = Join-Path $outputDir "$version.log" + $assetUrl = "$FixtureReleaseUrl/$([uri]::EscapeDataString($asset))" + + Save-Asset -Url $assetUrl -Dest $fixtureZip + + $expected = $fixture.asset_sha256 + if ($expected -and $expected -ne '<sha256>' -and (Get-Sha256 $fixtureZip) -ne $expected.ToUpperInvariant()) { + # A cached zip can be stale or truncated; the release asset itself is immutable, + # so deleting it forces Save-Asset to pull a fresh copy. + Write-Host "checksum mismatch for $asset, re-downloading" + Remove-Item -LiteralPath $fixtureZip -Force + Save-Asset -Url $assetUrl -Dest $fixtureZip + $actual = Get-Sha256 $fixtureZip + if ($actual -ne $expected.ToUpperInvariant()) { + Write-CheckLog "${asset}: expected $expected, got $actual" + return 1 + } + } + + Write-Host "validating custom presets from $version ..." + Remove-Item -LiteralPath $profileTree -Recurse -Force -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Force -Path $profileTree | Out-Null + # Piped rather than copied through <profiles>\*, so a vendor directory whose name + # holds a wildcard character is still copied by its literal path. + Get-ChildItem -LiteralPath $ProfilesDir -Force | Copy-Item -Destination $profileTree -Recurse -Force + Remove-Item -LiteralPath (Join-Path $profileTree 'user') -Recurse -Force -ErrorAction SilentlyContinue + Expand-Archive -LiteralPath $fixtureZip -DestinationPath $profileTree -Force + + $result = Invoke-Tool -Exe $Validator -Arguments @('-p', $profileTree, '-l', "$LogLevel") -OutFile $logPath + if ($result -eq 0) { + $summary += "| $version | PASS | $version.log |" + # Only failures are worth keeping; each tree is a full copy of resources\profiles. + Remove-Item -LiteralPath $profileTree -Recurse -Force + } else { + $summary += "| $version | FAIL | $version.log |" + $failedLogs += $logPath + $status = 1 + } + } + + [IO.File]::WriteAllLines((Join-Path $outputDir 'summary.md'), [string[]] $summary) + $summary | ForEach-Object { Write-CheckLog $_ } + + if ($failedLogs) { + Write-CheckLog '' + Write-CheckLog '## Failed Fixture Logs' + foreach ($logPath in $failedLogs) { + Write-CheckLog '' + Write-CheckLog "### $([IO.Path]::GetFileNameWithoutExtension($logPath))" + Write-CheckLog '```' + Write-CheckLog (Get-LogHead $logPath 12000) + Write-CheckLog '```' + } + } + return $status + } +} + +# Heading CI puts above this check's log in the PR comment. +$CommentHeadings = @{ + extra_json_check = '### Extra JSON Check Failed' + validate_system = '### System Profile Validation Failed' + validate_slice = '### Slice Validation Failed (custom g-code expansion)' + validate_filament_subtypes = '### Filament Subtype Validation Failed' + validate_custom = '### Custom Preset Validation Failed' +} + +# ---------------------------------------------------------------------------- run + +function Invoke-Check([string] $Name) { + $log = Join-Path $LogDir "$Name.log" + Write-Host '' + Write-Host "==> $Name" -ForegroundColor Cyan + + $script:LogWriter = New-LogWriter $log + try { + $result = & $CheckBodies[$Name] | Select-Object -Last 1 + } catch { + Write-CheckLog "$_" + $result = 1 + } finally { + $script:LogWriter.Dispose() + $script:LogWriter = $null + } + + if ([int] $result -eq 0) { + Write-Host " $Name passed" -ForegroundColor Green + return $true + } + Write-Host " $Name failed (exit $result)" -ForegroundColor Red + return $false +} + +# The fixture trees under the work dir are shared scratch space keyed by fixture version, so a +# second run would delete a tree the first one is validating. +$LockDir = Join-Path $WorkDir '.lock' +try { New-Item -ItemType Directory -Path $LockDir -ErrorAction Stop | Out-Null } +catch { Die "another run is using $WorkDir (pass -WorkDir, or remove $LockDir if no run is active)" } + +# The validator writes UTF-8, but PowerShell decodes a child process's output using the console +# code page, which mangles the accented and CJK preset names in its messages. +$PreviousOutputEncoding = [Console]::OutputEncoding + +try { + [Console]::OutputEncoding = New-Object Text.UTF8Encoding($false) + Push-UserPresets + + if ($Checks | Where-Object { $_ -ne 'extra_json_check' }) { $Validator = Resolve-Validator } + + $results = [ordered] @{} + foreach ($name in $AllChecks) { + if ($Checks -contains $name) { $results[$name] = Invoke-Check $name } + } + + Write-Host '' + Write-Host '==> summary' -ForegroundColor Cyan + foreach ($name in $results.Keys) { + if ($results[$name]) { + Write-Host " PASS $name" -ForegroundColor Green + } else { + Write-Host " FAIL $name ($(Join-Path $LogDir "$name.log"))" -ForegroundColor Red + } + } + + $failed = @($results.Keys | Where-Object { -not $results[$_] }) + if (-not $failed) { + Remove-Item -LiteralPath (Join-Path $WorkDir 'pr_comment.md') -Force -ErrorAction SilentlyContinue + Write-Host '' + Write-Host "All checks passed. Logs: $LogDir" -ForegroundColor Green + exit 0 + } + + # The comment check_profiles_comment.yml would post when something fails. + $comment = @( + # Marker matched by check_profiles_comment.yml to delete prior comments. + '<!-- profile-validation-comment -->' + '## :x: Profile Validation Errors' + '' + foreach ($name in $failed) { + $CommentHeadings[$name] + '' + '```' + Get-LogHead (Join-Path $LogDir "$name.log") 30000 + '```' + '' + } + '---' + '*Please fix the above errors and push a new commit.*' + ) + $commentPath = Join-Path $WorkDir 'pr_comment.md' + [IO.File]::WriteAllLines($commentPath, [string[]] $comment) + + Write-Host '' + Write-Host "One or more profile checks failed. Logs: $LogDir" -ForegroundColor Red + Write-Host "The comment CI would post: $commentPath" + exit 1 +} finally { + Pop-UserPresets + Remove-Item -LiteralPath $LockDir -Force -ErrorAction SilentlyContinue + [Console]::OutputEncoding = $PreviousOutputEncoding +} diff --git a/scripts/check_profile.sh b/scripts/check_profile.sh new file mode 100755 index 0000000000..9b841d0612 --- /dev/null +++ b/scripts/check_profile.sh @@ -0,0 +1,527 @@ +#!/usr/bin/env bash +# +# 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 +# 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 <repo>/.test/check_profiles/ and is reused on the next run. That +# directory also holds one log per check plus a copy of the comment CI would post on the PR. +# +# resources/profiles/user, which the validator creates as its data dir but a CI checkout never +# has, is moved aside for the duration of the run and restored on exit. Only one run per work +# dir at a time. +# +# Usage: scripts/check_profile.sh [OPTION]... [CHECK]... + +# The check_* functions run through run_check, which dispatches on the check name, so +# ShellCheck cannot see that they (and what they call) are used. +# shellcheck disable=SC2329 + +set -uo pipefail + +VALIDATOR_RELEASE_URL="https://github.com/OrcaSlicer/OrcaSlicer/releases/download/nightly-builds" +FIXTURE_RELEASE_URL="https://github.com/OrcaSlicer/OrcaSlicer-profile-validator/releases/download/fixture-archive" + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)" + +HOST_ARCH="$(uname -m)" + +PROFILES_DIR="${REPO_ROOT}/resources/profiles" +WORK_DIR="${REPO_ROOT}/.test/check_profiles" +VALIDATOR="${ORCA_PROFILE_VALIDATOR:-}" +LOG_LEVEL=2 +PREFER_DOWNLOAD=0 +REFRESH=0 + +ALL_CHECKS=(extra_json_check validate_system validate_slice validate_filament_subtypes validate_custom) +CHECKS=() +# "<check><TAB>pass|fail" per check that ran; a string rather than an array because bash 3.2 +# (still the /bin/bash on macOS) cannot expand an empty array under `set -u`. +RESULTS="" + +usage() { + cat <<EOF +Run the profile checks from .github/workflows/check_profiles.yml locally. + +Usage: scripts/check_profile.sh [OPTION]... [CHECK]... + +Checks (default: all, in this order): + extra_json_check scripts/orca_extra_profile_check.py + validate_system validator -p <profiles> -l <level> + validate_slice validator -p <profiles> -s -l <level> + validate_filament_subtypes validator -p <profiles> -l <level> -f + validate_custom validator against every released custom-preset fixture + +Options: + -p, --profiles DIR profile tree to validate (default: resources/profiles) + --validator BIN OrcaSlicer_profile_validator to use; also \$ORCA_PROFILE_VALIDATOR. + Default: the local build*/ Release build (then RelWithDebInfo, then + Debug) for this architecture, else the nightly release build is + downloaded for this platform + --download ignore local builds and use the downloaded nightly validator + --refresh re-download the validator and fixtures instead of using the cache + --work-dir DIR downloads, logs and fixture trees (default: .test/check_profiles) + -l, --log-level N validator log level (default: ${LOG_LEVEL}, as in CI) + -h, --help show this help + +Note: extra_json_check always looks at the tree next to the script +(<repo>/resources/profiles); --profiles only redirects the validator checks. +EOF +} + +msg() { printf '%s\n' "$*" >&2; } +die() { printf 'check_profile.sh: %s\n' "$*" >&2; exit 2; } + +if [ -t 1 ]; then + C_RED=$'\033[91m'; C_GREEN=$'\033[92m'; C_BOLD=$'\033[1m'; C_RESET=$'\033[0m' +else + C_RED=''; C_GREEN=''; C_BOLD=''; C_RESET='' +fi + +# ---------------------------------------------------------------------------- arguments + +while [ $# -gt 0 ]; do + case "$1" in + -h|--help) usage; exit 0 ;; + -p|--profiles) [ $# -ge 2 ] || die "$1 needs a directory"; PROFILES_DIR="$2"; shift 2 ;; + --validator) [ $# -ge 2 ] || die "$1 needs a path"; VALIDATOR="$2"; shift 2 ;; + --work-dir) [ $# -ge 2 ] || die "$1 needs a directory"; WORK_DIR="$2"; shift 2 ;; + -l|--log-level) [ $# -ge 2 ] || die "$1 needs a number"; LOG_LEVEL="$2"; shift 2 ;; + --download) PREFER_DOWNLOAD=1; shift ;; + --refresh) REFRESH=1; shift ;; + -*) die "unknown option '$1' (try --help)" ;; + *) + known=0 + for check in "${ALL_CHECKS[@]}"; do + [ "$1" = "${check}" ] && known=1 + done + [ "${known}" -eq 1 ] || die "unknown check '$1' (try --help)" + CHECKS[${#CHECKS[@]}]="$1" + shift + ;; + esac +done + +[ "${#CHECKS[@]}" -gt 0 ] || CHECKS=("${ALL_CHECKS[@]}") + +[ -d "${PROFILES_DIR}" ] || die "profile directory not found: ${PROFILES_DIR}" +PROFILES_DIR="$(cd -- "${PROFILES_DIR}" && pwd)" + +LOG_DIR="${WORK_DIR}/logs" +mkdir -p "${LOG_DIR}" || die "cannot create ${LOG_DIR}" +WORK_DIR="$(cd -- "${WORK_DIR}" && pwd)" +LOG_DIR="${WORK_DIR}/logs" + +wants() { + local check + for check in "${CHECKS[@]}"; do + [ "${check}" = "$1" ] && return 0 + done + return 1 +} + +# ------------------------------------------------------------------- clean profile tree + +# The validator points its data dir at the profile tree, so it creates - and, with -g, fills - +# <profiles>/user. A CI checkout never has that directory, and anything left in it from an +# earlier local run would be loaded as user presets and validated too. Move it aside for the +# duration of the run so what gets checked is what CI checks. +STASHED_USER_DIR="" + +stash_user_presets() { + [ -d "${PROFILES_DIR}/user" ] || return 0 + STASHED_USER_DIR="${WORK_DIR}/user-presets-$$" + rm -rf "${STASHED_USER_DIR}" + mv "${PROFILES_DIR}/user" "${STASHED_USER_DIR}" || { STASHED_USER_DIR=""; die "cannot move ${PROFILES_DIR}/user aside"; } + msg "moved ${PROFILES_DIR}/user aside for the run (restored on exit)" +} + +restore_user_presets() { + # The validator leaves an empty user/default/{filament,machine,process} skeleton behind. + # Prune it with rmdir, never rm -rf: a directory that holds a real file survives and is + # reported instead of being deleted. Runs even when nothing was stashed, so a tree that had + # no user/ before the run does not gain one. + find "${PROFILES_DIR}/user" -depth -type d -exec rmdir {} + 2>/dev/null + [ -n "${STASHED_USER_DIR}" ] || return 0 + if [ -d "${PROFILES_DIR}/user" ]; then + msg "${PROFILES_DIR}/user is not empty; your presets stay in ${STASHED_USER_DIR}" + STASHED_USER_DIR="" + return 0 + fi + mv "${STASHED_USER_DIR}" "${PROFILES_DIR}/user" + STASHED_USER_DIR="" +} + +# The fixture trees under the work dir are shared scratch space keyed by fixture version, so a +# second run would delete a tree the first one is validating. +LOCK_DIR="${WORK_DIR}/.lock" +mkdir "${LOCK_DIR}" 2>/dev/null || + die "another run is using ${WORK_DIR} (pass --work-dir, or remove ${LOCK_DIR} if no run is active)" + +cleanup() { + restore_user_presets + rmdir "${LOCK_DIR}" 2>/dev/null +} + +# Installed only once the lock is ours, so a refused start never releases someone else's. +trap cleanup EXIT +trap 'cleanup; exit 130' INT TERM + +stash_user_presets + +# ---------------------------------------------------------------------------- downloads + +# fetch URL DEST [force] - cached; --refresh, or a non-empty third argument, forces a new +# download. Release assets never change, so caching them is safe; the fixture manifest is the +# index that grows with every release, and CI re-reads it on every run. +fetch() { + local url="$1" dest="$2" force="${3:-}" + if [ "${REFRESH}" -eq 0 ] && [ -z "${force}" ] && [ -s "${dest}" ]; then + return 0 + fi + mkdir -p "$(dirname -- "${dest}")" || return 1 + msg "downloading $(basename -- "${dest}") ..." + if ! curl -fsSL --retry 3 -o "${dest}.part" "${url}"; then + rm -f "${dest}.part" + msg "download failed: ${url}" + return 1 + fi + mv -f "${dest}.part" "${dest}" +} + +sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | cut -d' ' -f1 + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | cut -d' ' -f1 + else + return 1 + fi +} + +# Directories a built validator can sit in, best first: every Ninja Multi-Config Release tree, +# then RelWithDebInfo, then Debug, then a single-config generator's plain src/. Each pattern is +# matched both directly under a build root (build/) and one level down (build/arm64, build/x64), +# and unmatched globs are dropped by the caller's -d test. +validator_search_dirs() { + local config + for config in Release RelWithDebInfo Debug; do + printf '%s\n' "${REPO_ROOT}"/build*/src/"${config}" "${REPO_ROOT}"/build*/*/src/"${config}" + done + printf '%s\n' "${REPO_ROOT}"/build*/src "${REPO_ROOT}"/build*/*/src +} + +# A build tree named for the other architecture (build/x86_64 on an arm64 host, build/arm64 on +# an x64 one) is a last resort: on Linux it will not run at all, on macOS it goes via Rosetta. +# Takes a repo-relative path - an absolute one would also match an arch name in the checkout path. +is_foreign_arch_dir() { + case "${HOST_ARCH}" in + arm64|aarch64) case "$1" in *x86_64*|*x86-64*|*x64*|*amd64*) return 0 ;; esac ;; + x86_64|amd64) case "$1" in *arm64*|*aarch64*) return 0 ;; esac ;; + esac + return 1 +} + +# First locally built OrcaSlicer_profile_validator, in the order above. macOS puts it in an .app +# bundle, Linux and Windows next to the other binaries. +find_local_validator() { + local dir candidate foreign="" + while IFS= read -r dir; do + [ -d "${dir}" ] || continue + for candidate in \ + "${dir}/OrcaSlicer_profile_validator" \ + "${dir}/OrcaSlicer_profile_validator.exe" \ + "${dir}/OrcaSlicer_profile_validator.app/Contents/MacOS/OrcaSlicer_profile_validator"; do + [ -f "${candidate}" ] && [ -x "${candidate}" ] || continue + if ! is_foreign_arch_dir "${dir#"${REPO_ROOT}"/}"; then + printf '%s\n' "${candidate}" + return 0 + fi + [ -n "${foreign}" ] || foreign="${candidate}" + done + done <<EOF +$(validator_search_dirs) +EOF + [ -n "${foreign}" ] || return 1 + msg "no ${HOST_ARCH} build found, falling back to ${foreign}" + printf '%s\n' "${foreign}" +} + +# The nightly release build, same one CI uses. Linux ships the bare binary, macOS a .dmg +# holding the signed .app, Windows an .exe. +download_validator() { + local dest="${WORK_DIR}/validator" binary dmg app mounted app_src + case "$(uname -s)" in + Linux*) + case "${HOST_ARCH}" in + arm64|aarch64) msg "the nightly Linux validator is x86_64; build it locally for ${HOST_ARCH}" ;; + esac + binary="${dest}/OrcaSlicer_profile_validator" + fetch "${VALIDATOR_RELEASE_URL}/OrcaSlicer_profile_validator_Linux_Ubuntu2404_nightly" "${binary}" || return 1 + chmod +x "${binary}" || return 1 + ;; + Darwin*) + dmg="${dest}/OrcaSlicer_profile_validator.dmg" + app="${dest}/OrcaSlicer_profile_validator.app" + binary="${app}/Contents/MacOS/OrcaSlicer_profile_validator" + fetch "${VALIDATOR_RELEASE_URL}/OrcaSlicer_profile_validator_Mac_universal_nightly.dmg" "${dmg}" || return 1 + if [ ! -x "${binary}" ] || [ "${REFRESH}" -eq 1 ]; then + mounted="${dest}/mnt" + rm -rf "${mounted}" "${app}" + mkdir -p "${mounted}" || return 1 + hdiutil attach -nobrowse -readonly -mountpoint "${mounted}" "${dmg}" >/dev/null || return 1 + app_src="$(find "${mounted}" -maxdepth 1 -name '*.app' -print 2>/dev/null | head -n 1)" + if [ -n "${app_src}" ]; then + cp -R "${app_src}" "${app}" + fi + hdiutil detach "${mounted}" >/dev/null 2>&1 + rmdir "${mounted}" 2>/dev/null + [ -x "${binary}" ] || { msg "no validator app inside ${dmg}"; return 1; } + fi + ;; + MINGW*|MSYS*|CYGWIN*) + binary="${dest}/OrcaSlicer_profile_validator.exe" + fetch "${VALIDATOR_RELEASE_URL}/OrcaSlicer_profile_validator_Windows_nightly.exe" "${binary}" || return 1 + chmod +x "${binary}" || return 1 + ;; + *) + msg "no nightly validator published for $(uname -s); build it (-DORCA_TOOLS=ON) and pass --validator" + return 1 + ;; + esac + printf '%s\n' "${binary}" +} + +resolve_validator() { + if [ -n "${VALIDATOR}" ]; then + [ -x "${VALIDATOR}" ] || die "validator not executable: ${VALIDATOR}" + return 0 + fi + if [ "${PREFER_DOWNLOAD}" -eq 0 ]; then + VALIDATOR="$(find_local_validator)" + if [ -n "${VALIDATOR}" ]; then + msg "using locally built validator: ${VALIDATOR}" + return 0 + fi + fi + VALIDATOR="$(download_validator)" || die "could not obtain a profile validator" + msg "using downloaded validator: ${VALIDATOR}" +} + +# ---------------------------------------------------------------------------- checks + +check_extra_json_check() { + python3 "${REPO_ROOT}/scripts/orca_extra_profile_check.py" +} + +check_validate_system() { + "${VALIDATOR}" -p "${PROFILES_DIR}" -l "${LOG_LEVEL}" +} + +# Slices a two-colour cube through every printer so all custom g-code (incl. change_filament_gcode) +# is expanded - catches undefined-placeholder / invalid-flow bugs the static checks cannot see. +check_validate_slice() { + "${VALIDATOR}" -p "${PROFILES_DIR}" -s -l "${LOG_LEVEL}" +} + +check_validate_filament_subtypes() { + "${VALIDATOR}" -p "${PROFILES_DIR}" -l "${LOG_LEVEL}" -f +} + +# Every released fixture is a snapshot of user presets saved by that OrcaSlicer version; each is +# unpacked over the current system profiles and validated, so a profile change that would break +# an existing user's presets fails here. +check_validate_custom() { + local fixtures_dir="${WORK_DIR}/profile-fixtures" + local output_dir="${WORK_DIR}/custom-preset-validation" + local summary="${output_dir}/summary.md" + local status=0 failed_logs="" + local version asset expected_sha256 asset_url fixture_zip profile_tree log_path actual_sha256 result + + command -v unzip >/dev/null 2>&1 || { msg "unzip is required for validate_custom"; return 1; } + mkdir -p "${fixtures_dir}" "${output_dir}" || return 1 + + fetch "${FIXTURE_RELEASE_URL}/manifest.json" "${fixtures_dir}/manifest.json" force || return 1 + + MANIFEST_PATH="${fixtures_dir}/manifest.json" python3 - > "${fixtures_dir}/fixtures.tsv" <<'PY' +import json +import os + +with open(os.environ["MANIFEST_PATH"], encoding="utf-8") as fh: + manifest = json.load(fh) + +if isinstance(manifest, dict): + entries = manifest.get("fixtures", []) +else: + entries = manifest + +for entry in entries: + version = entry.get("version", "") + asset = entry.get("asset", "") + sha256 = entry.get("asset_sha256", "") + if not version or not asset: + continue + print(f"{version}\t{asset}\t{sha256}") +PY + + if [ ! -s "${fixtures_dir}/fixtures.tsv" ]; then + echo "No custom preset fixtures found in ${FIXTURE_RELEASE_URL}/manifest.json" + return 1 + fi + + { + echo "## Custom Preset Fixture Validation" + echo "" + echo "| Version | Status | Log |" + echo "| --- | --- | --- |" + } > "${summary}" + + while IFS=$'\t' read -r version asset expected_sha256; do + [ -n "${version}" ] || continue + fixture_zip="${fixtures_dir}/${asset}" + profile_tree="${output_dir}/profiles-${version}" + log_path="${output_dir}/${version}.log" + + asset_url="${FIXTURE_RELEASE_URL}/$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "${asset}")" + fetch "${asset_url}" "${fixture_zip}" || return 1 + + if [ -n "${expected_sha256}" ] && [ "${expected_sha256}" != "<sha256>" ]; then + actual_sha256="$(sha256_of "${fixture_zip}")" + if [ -z "${actual_sha256}" ]; then + msg "no sha256 tool available, skipping checksum of ${asset}" + elif [ "${actual_sha256}" != "${expected_sha256}" ]; then + # A cached zip can be stale or truncated; the release asset itself is immutable, + # so deleting it forces fetch to pull a fresh copy. + msg "checksum mismatch for ${asset}, re-downloading" + rm -f "${fixture_zip}" + fetch "${asset_url}" "${fixture_zip}" || return 1 + actual_sha256="$(sha256_of "${fixture_zip}")" + [ "${actual_sha256}" = "${expected_sha256}" ] || { msg "${asset}: expected ${expected_sha256}, got ${actual_sha256}"; return 1; } + fi + fi + + msg "validating custom presets from ${version} ..." + rm -rf "${profile_tree}" + mkdir -p "${profile_tree}" || return 1 + cp -a "${PROFILES_DIR}/." "${profile_tree}/" || return 1 + rm -rf "${profile_tree}/user" + unzip -q "${fixture_zip}" -d "${profile_tree}" || return 1 + + "${VALIDATOR}" -p "${profile_tree}" -l "${LOG_LEVEL}" > "${log_path}" 2>&1 + result=$? + + if [ "${result}" -eq 0 ]; then + echo "| ${version} | PASS | ${version}.log |" >> "${summary}" + # Only failures are worth keeping; each tree is a full copy of resources/profiles. + rm -rf "${profile_tree}" + else + echo "| ${version} | FAIL | ${version}.log |" >> "${summary}" + failed_logs="${failed_logs}${log_path}"$'\n' + status=1 + fi + done < "${fixtures_dir}/fixtures.tsv" + + cat "${summary}" + if [ -n "${failed_logs}" ]; then + echo "" + echo "## Failed Fixture Logs" + while IFS= read -r log_path; do + [ -n "${log_path}" ] || continue + echo "" + echo "### $(basename "${log_path}" .log)" + echo '```' + head -c 12000 "${log_path}" || echo "No output captured" + echo '```' + done <<EOF +${failed_logs} +EOF + fi + + return "${status}" +} + +# Heading CI puts above this check's log in the PR comment. +comment_heading() { + case "$1" in + extra_json_check) echo "### Extra JSON Check Failed" ;; + 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" ;; + validate_custom) echo "### Custom Preset Validation Failed" ;; + esac +} + +# ---------------------------------------------------------------------------- run + +run_check() { + local name="$1" + local log="${LOG_DIR}/${name}.log" + local result + printf '\n%s==> %s%s\n' "${C_BOLD}" "${name}" "${C_RESET}" + "check_${name}" 2>&1 | tee "${log}" + result="${PIPESTATUS[0]}" + if [ "${result}" -eq 0 ]; then + printf '%s %s passed%s\n' "${C_GREEN}" "${name}" "${C_RESET}" + RESULTS="${RESULTS}${name}"$'\t'"pass"$'\n' + else + printf '%s %s failed (exit %s)%s\n' "${C_RED}" "${name}" "${result}" "${C_RESET}" + RESULTS="${RESULTS}${name}"$'\t'"fail"$'\n' + fi +} + +if wants validate_system || wants validate_slice || wants validate_filament_subtypes || wants validate_custom; then + resolve_validator +fi + +for check in "${ALL_CHECKS[@]}"; do + wants "${check}" && run_check "${check}" +done + +# Summary, plus the comment check_profiles_comment.yml would post when something fails. +failed=0 +printf '\n%s==> summary%s\n' "${C_BOLD}" "${C_RESET}" +while IFS=$'\t' read -r name result; do + [ -n "${name}" ] || continue + if [ "${result}" = "pass" ]; then + printf '%s PASS%s %s\n' "${C_GREEN}" "${C_RESET}" "${name}" + else + printf '%s FAIL%s %s (%s)\n' "${C_RED}" "${C_RESET}" "${name}" "${LOG_DIR}/${name}.log" + failed=1 + fi +done <<EOF +${RESULTS} +EOF + +if [ "${failed}" -eq 0 ]; then + rm -f "${WORK_DIR}/pr_comment.md" + printf '\n%sAll checks passed.%s Logs: %s\n' "${C_GREEN}" "${C_RESET}" "${LOG_DIR}" + exit 0 +fi + +{ + # Marker matched by check_profiles_comment.yml to delete prior comments. + echo "<!-- profile-validation-comment -->" + echo "## :x: Profile Validation Errors" + echo "" + while IFS=$'\t' read -r name result; do + [ "${result}" = "fail" ] || continue + comment_heading "${name}" + echo "" + echo '```' + head -c 30000 "${LOG_DIR}/${name}.log" || echo "No output captured" + echo '```' + echo "" + done <<INNER +${RESULTS} +INNER + echo "---" + echo "*Please fix the above errors and push a new commit.*" +} > "${WORK_DIR}/pr_comment.md" + +printf '\n%sOne or more profile checks failed.%s Logs: %s\n' "${C_RED}" "${C_RESET}" "${LOG_DIR}" +printf 'The comment CI would post: %s\n' "${WORK_DIR}/pr_comment.md" +exit 1 diff --git a/scripts/flatpak/.gitignore b/scripts/flatpak/.gitignore index 3b01f15d01..21abf7a876 100644 --- a/scripts/flatpak/.gitignore +++ b/scripts/flatpak/.gitignore @@ -1,3 +1,4 @@ builddir .flatpak-builder *.docker.yml +deps.tar* diff --git a/scripts/flatpak/check_manifest_cacheable.sh b/scripts/flatpak/check_manifest_cacheable.sh new file mode 100755 index 0000000000..e82190ca81 --- /dev/null +++ b/scripts/flatpak/check_manifest_cacheable.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# Check a Flatpak manifest for `type: dir` sources at or before the orca_deps +# module. +# +# Usage: check_manifest_cacheable.sh [manifest] +# Defaults to com.orcaslicer.OrcaSlicer.yml next to this script. +# +# Exits 0 when none are found, 1 when any are, listing them as file:line, and +# 2 when the manifest is missing or has no orca_deps module. + +set -euo pipefail + +anchor=orca_deps +manifest=${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/com.orcaslicer.OrcaSlicer.yml} + +module_re='^ - name: ' +dir_re='(^|[-{,[:space:]])type:[[:space:]]*dir([,}[:space:]]|$)' + +if [ ! -f "$manifest" ]; then + echo "$manifest: no such file" >&2 + exit 2 +fi + +anchor_start=$(grep -n "${module_re}${anchor}[[:space:]]*$" "$manifest" | cut -d: -f1 || true) +if [ -z "$anchor_start" ]; then + echo "$manifest: no module named '$anchor'; this check needs updating" >&2 + exit 2 +fi + +# First module header after the anchor, or EOF if the anchor is last. +anchor_end=$(grep -n "$module_re" "$manifest" | cut -d: -f1 | + awk -v s="$anchor_start" '$1 > s { print $1; exit }') +[ -n "$anchor_end" ] || anchor_end=$(awk 'END { print NR + 1 }' "$manifest") + +violations=$(awk -v e="$anchor_end" -v f="$manifest" -v mre="$module_re" -v dre="$dir_re" ' + { sub(/\r$/, "") } + /^[[:space:]]*#/ { next } + $0 ~ mre { module = $3 } + NR < e && $0 ~ dre { + printf "%s:%d: %s (module %s)\n", f, NR, $0, module + }' "$manifest") + +if [ -n "$violations" ]; then + printf '%s\n' "$violations" >&2 + echo >&2 + echo "flatpak-builder cannot checksum a directory, so each of these makes" >&2 + echo "$anchor and every module after it rebuild from scratch on every run." >&2 + echo "See the $anchor sources in this manifest for the tarball used instead." >&2 + exit 1 +fi + +echo "manifest OK: no 'type: dir' source at or before $anchor" diff --git a/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml b/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml index c33425f23f..00aa430f84 100644 --- a/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml +++ b/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml @@ -144,6 +144,7 @@ modules: env: BUILD_DIR: deps/build_flatpak build-commands: + - tar xf deps.tar - | cmake -S deps -B $BUILD_DIR \ -DFLATPAK=ON \ @@ -169,10 +170,10 @@ modules: - /libpython/include sources: - # OrcaSlicer deps/ directory (avoids copying .git from worktree) - - type: dir - path: ../../deps - dest: deps + # flatpak-builder cannot checksum a directory, so a `type: dir` here would + # rebuild every dependency on every run. Generated by make_deps_tar.sh. + - type: file + path: deps.tar # --------------------------------------------------------------- # Pre-downloaded dependency archives @@ -276,6 +277,12 @@ modules: sha256: 27b72ba2d5ff3d0a9814ad40d4cb88f8dc89a35491c0866d952473f8f9416b77 dest: external-packages/Draco + # Assimp 5.4.3 + - type: file + url: https://github.com/assimp/assimp/archive/refs/tags/v5.4.3.tar.gz + sha256: 66dfbaee288f2bc43172440a55d0235dfc7bf885dda6435c038e8000e79582cb + dest: external-packages/Assimp + # OpenSSL 1.1.1w (GNOME SDK has 3.x; OrcaSlicer requires 1.1.x) - type: file url: https://github.com/openssl/openssl/archive/OpenSSL_1_1_1w.tar.gz @@ -312,6 +319,12 @@ modules: sha256: 0ba163956f2d468b19a91b96c5aba66ee9610843ea41dda628ea44cdafde7db7 dest: external-packages/wxInspector + # FFmpeg n7.0.3 + - type: file + url: https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n7.0.3.tar.gz + sha256: deedcabe339165214a3637df4c86a507aef0d793cf8774ff68735f4737e8ddbc + dest: external-packages/FFMPEG + # --------------------------------------------------------------- # Fallback archives for deps normally provided by the GNOME SDK. # These are only used if find_package() fails to locate them. @@ -347,6 +360,7 @@ modules: - | cmake . -B build_flatpak \ -DFLATPAK=ON \ + -DORCA_TOOLS=ON \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH=/app \ -DCMAKE_INSTALL_PREFIX=/app \ @@ -357,6 +371,13 @@ modules: - ./scripts/run_gettext.sh - cmake --build build_flatpak --target install -j$FLATPAK_BUILDER_N_JOBS + # Per-vendor preset caches. On the other platforms CI runs this script + # itself; the flatpak is built inside flatpak-builder and the generator + # only exists in here, so the swap is a build step instead, against the + # profiles the install above copied into /app. + - cmake --build build_flatpak --target generate_system_cache -j$FLATPAK_BUILDER_N_JOBS + - ./scripts/build_preset_cache.sh -n -b build_flatpak /app/share/OrcaSlicer/profiles + cleanup: - /include @@ -403,6 +424,9 @@ modules: - type: file path: ../run_gettext.sh dest: scripts + - type: file + path: ../build_preset_cache.sh + dest: scripts # AppData metainfo for GNOME Software & Co. - type: file diff --git a/scripts/flatpak/make_deps_tar.sh b/scripts/flatpak/make_deps_tar.sh new file mode 100755 index 0000000000..e561da2e7b --- /dev/null +++ b/scripts/flatpak/make_deps_tar.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Pack deps/ into scripts/flatpak/deps.tar for the Flatpak manifest's orca_deps +# module. +# +# Usage: make_deps_tar.sh +# Requires GNU tar. On macOS, brew install gnu-tar. +# +# The archive is byte-reproducible. Member order, mtimes, ownership and the +# group/other write bits are pinned, so identical deps/ contents always produce +# an identical file. deps/build* and deps/DL_CACHE are excluded. +# +# Prints the output path, size and sha256. + +set -euo pipefail + +if [ "$#" -ne 0 ]; then + echo "usage: ${0##*/}" >&2 + exit 2 +fi + +tar_bin=$(command -v gtar || command -v tar || true) +tar_version=$([ -n "$tar_bin" ] && "$tar_bin" --version 2>/dev/null || true) +case $tar_version in + *"GNU tar"*) ;; + *) echo "${0##*/}: needs GNU tar; on macOS run 'brew install gnu-tar'" >&2 + exit 2 ;; +esac + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +repo_root=$(cd "$script_dir/../.." && pwd) +out=$script_dir/deps.tar +tmp=$out.tmp +trap 'rm -f "$tmp"' EXIT + +"$tar_bin" --format=gnu \ + --sort=name \ + --mtime=@0 \ + --owner=0 --group=0 --numeric-owner \ + --mode=go-w \ + --exclude='deps/build*' \ + --exclude='deps/DL_CACHE' \ + -cf "$tmp" \ + -C "$repo_root" deps + +mv -f "$tmp" "$out" + +if command -v sha256sum >/dev/null 2>&1; then + sum=$(sha256sum "$out" | cut -d' ' -f1) +elif command -v shasum >/dev/null 2>&1; then + sum=$(shasum -a 256 "$out" | cut -d' ' -f1) +else + sum=unavailable +fi + +echo "Wrote $out ($(du -h "$out" | cut -f1), sha256 $sum)" diff --git a/scripts/flatpak/setup_env_ubuntu24.04.sh b/scripts/flatpak/setup_env_ubuntu24.04.sh index a0ccbb9b32..74b7629f60 100755 --- a/scripts/flatpak/setup_env_ubuntu24.04.sh +++ b/scripts/flatpak/setup_env_ubuntu24.04.sh @@ -8,8 +8,9 @@ flatpak install flathub org.gnome.Platform//50 org.gnome.Sdk//50 org.freedesktop ## # in OrcaSlicer folder, run following command to build Orca -# # First time build -# flatpak-builder --state-dir=.flatpak-builder --keep-build-dirs --user --force-clean build-dir scripts/flatpak/com.orcaslicer.OrcaSlicer.yml -# # Subsequent builds (only rebuilding OrcaSlicer) +# # First time build +# ./scripts/flatpak/make_deps_tar.sh && flatpak-builder --state-dir=.flatpak-builder --keep-build-dirs --user --force-clean build-dir scripts/flatpak/com.orcaslicer.OrcaSlicer.yml + +# # Subsequent builds (only rebuilding OrcaSlicer; run make_deps_tar.sh first if deps/ changed) # flatpak-builder --state-dir=.flatpak-builder --keep-build-dirs --user build-dir scripts/flatpak/com.orcaslicer.OrcaSlicer.yml --build-only=OrcaSlicer \ No newline at end of file diff --git a/scripts/linux.d/arch b/scripts/linux.d/arch index ead963e9a6..dc554b10b5 100644 --- a/scripts/linux.d/arch +++ b/scripts/linux.d/arch @@ -25,6 +25,9 @@ export REQUIRED_DEV_PACKAGES=( wayland-protocols webkit2gtk-4.1 wget + pkgconf + yasm + nasm ) if [[ -n "$UPDATE_LIB" ]] diff --git a/scripts/linux.d/cachyos b/scripts/linux.d/cachyos index d491747ace..6137a3ef5c 100644 --- a/scripts/linux.d/cachyos +++ b/scripts/linux.d/cachyos @@ -25,6 +25,9 @@ export REQUIRED_DEV_PACKAGES=( wayland-protocols webkit2gtk wget + pkgconf + yasm + nasm ) if [[ -n "$UPDATE_LIB" ]] diff --git a/scripts/linux.d/clear-linux-os b/scripts/linux.d/clear-linux-os index 149e805549..41ca68e835 100644 --- a/scripts/linux.d/clear-linux-os +++ b/scripts/linux.d/clear-linux-os @@ -20,6 +20,7 @@ export REQUIRED_BUNDLES=( perl-basic texinfo wget + nasm ) if [[ -n "$UPDATE_LIB" ]] diff --git a/scripts/linux.d/debian b/scripts/linux.d/debian index f4d500760e..c33b24d2b7 100644 --- a/scripts/linux.d/debian +++ b/scripts/linux.d/debian @@ -27,6 +27,9 @@ REQUIRED_DEV_PACKAGES=( ninja-build texinfo wget + pkgconf + yasm + nasm ) if [[ -n "$UPDATE_LIB" ]] diff --git a/scripts/linux.d/fedora b/scripts/linux.d/fedora index ca4eceaee3..993ea0b10a 100644 --- a/scripts/linux.d/fedora +++ b/scripts/linux.d/fedora @@ -31,6 +31,9 @@ REQUIRED_DEV_PACKAGES=( webkit2gtk4.1-devel wget libcurl-devel + pkgconf + yasm + nasm ) if [[ -n "$UPDATE_LIB" ]] diff --git a/scripts/linux.d/gentoo b/scripts/linux.d/gentoo index f172ac92c3..f8554542ff 100644 --- a/scripts/linux.d/gentoo +++ b/scripts/linux.d/gentoo @@ -32,6 +32,9 @@ REQUIRED_DEV_PACKAGES=( sys-devel/m4 virtual/libudev x11-libs/gtk+:3 + dev-util/pkgconf + dev-lang/yasm + dev-lang/nasm ) if [[ -n "$UPDATE_LIB" ]] diff --git a/scripts/linux.d/suse b/scripts/linux.d/suse index 1f4db45f46..682960650b 100644 --- a/scripts/linux.d/suse +++ b/scripts/linux.d/suse @@ -30,6 +30,9 @@ REQUIRED_DEV_PACKAGES=( webkit2gtk4-devel wget libcurl-devel + pkgconf + yasm + nasm ) if [[ -n "$UPDATE_LIB" ]] diff --git a/scripts/orca_extra_profile_check.py b/scripts/orca_extra_profile_check.py index cdfc8544a4..07ce3d69d3 100644 --- a/scripts/orca_extra_profile_check.py +++ b/scripts/orca_extra_profile_check.py @@ -46,12 +46,16 @@ def no_duplicates_object_pairs_hook(pairs): return seen # NOTE: currently Orca expects compatible_printers to be a defined in every instantiation profile, inheritation is not supported in Profile page -def check_filament_compatible_printers(vendor_folder): +def check_filament_compatible_printers(vendor, vendor_folder): """ Checks JSON files in the vendor folder for missing or empty 'compatible_printers' when 'instantiation' is flagged as true. + In the OrcaFilamentLibrary 'compatible_printers' is optional: a profile without it is generic and + offered on every printer, while a profile that lists printers supersedes the generic one there. + Parameters: + vendor (str): The vendor name the folder belongs to. vendor_folder (str or Path): The directory to search for JSON profile files. Returns: @@ -115,7 +119,7 @@ def check_filament_compatible_printers(vendor_folder): for profile in profiles.values(): instantiation = str(profile['content'].get("instantiation", "")).lower() == "true" - if instantiation: + if instantiation and vendor != 'OrcaFilamentLibrary': try: compatible_printers = get_property(profile, "compatible_printers") if not compatible_printers or (isinstance(compatible_printers, list) and not compatible_printers): @@ -571,7 +575,7 @@ def main(): vendor_path = profiles_dir / vendor_name if args.check_filaments or not (args.check_materials and not args.check_filaments): - errors_found += check_filament_compatible_printers(vendor_path / "filament") + errors_found += check_filament_compatible_printers(vendor_name, vendor_path / "filament") if args.check_materials: new_errors, new_warnings = check_machine_default_materials(profiles_dir, vendor_name) diff --git a/scripts/test_build_win.ps1 b/scripts/test_build_win.ps1 new file mode 100644 index 0000000000..563430559f --- /dev/null +++ b/scripts/test_build_win.ps1 @@ -0,0 +1,907 @@ +<# +.SYNOPSIS + Tests build_win.bat's option handling and the commands it generates. + +.DESCRIPTION + Cases run the script with --dry-run, so nothing is configured, built or + deleted and the suite finishes in seconds. Each case asserts on the exit + code and on the command lines the script echoes. + + Adding a case means adding one row to $cases. A bare string starts a new + group. Defaults: ExpectExit is 0 and --dry-run is appended, so a row only + states what is unusual about it. + + Name what the case proves, in words + Args arguments, as an array + ExpectExit expected exit code (default 0) + DryRun append --dry-run (default $true) + First regex the first output line must match + Env environment for this case only + Contains literal strings the output must have + NotContains literal strings it must not have + Match regexes; each must match at least one output line + NotMatch regexes; none may match any output line + NotExists paths that must not exist after the case runs + +.PARAMETER Name + Run only the cases whose name matches this regex. Headings with no + matching case are not printed, and a pattern that matches nothing is a + failure rather than an empty pass. + +.EXAMPLE + powershell -File scripts/test_build_win.ps1 + +.EXAMPLE + powershell -File scripts/test_build_win.ps1 -Name solution +#> +[CmdletBinding()] +param( + [string] $Script, + [string] $Name +) + +$ErrorActionPreference = 'Stop' + +if (-not $Script) { + $here = $PSScriptRoot + if (-not $here) { $here = Split-Path -Parent $MyInvocation.MyCommand.Path } + $Script = Join-Path (Split-Path -Parent $here) 'build_win.bat' +} +if (-not (Test-Path $Script)) { throw "build_win.bat not found at $Script" } + +# cmd resumes a batch file by byte offset after `call :label`, and with LF +# endings that offset lands wrong and the label lookup fails. .gitattributes +# pins CRLF; this catches a checkout or an editor that did not honour it. +if ((Get-Content -Raw $Script) -match "(?<!`r)`n") { + throw "$Script has LF line endings; cmd needs CRLF to resume after call :label" +} +$Script = (Resolve-Path $Script).Path + +# Read the long options out of the script itself, so this cannot go stale when +# an option is added. Field order is: add_arg <var> <type> <short> <long>. +$longFlags = @( + Select-String -Path $Script -Pattern '^call :add_arg \S+ \S+ \S+ (\S+) ' | + ForEach-Object { '--' + $_.Matches[0].Groups[1].Value } +) +if ($longFlags.Count -lt 20) { throw "only found $($longFlags.Count) options in $Script; the parser above is wrong" } + +# A winget that always fails, so the prerequisite failure path runs without +# touching the machine. It has to be an .exe: a .bat invoked without `call` +# transfers control and never comes back, which would end the script instead. +# where.exe returns 1 when its patterns match nothing and never prompts. +$fixtures = Join-Path ([IO.Path]::GetTempPath()) 'build_win_test_fixtures' +New-Item -ItemType Directory -Force -Path $fixtures | Out-Null +Copy-Item "$env:SystemRoot\System32\where.exe" (Join-Path $fixtures 'winget.exe') -Force +$stubPath = "$fixtures;C:\Windows\system32;C:\Windows" + +# Stand-in ninjas that only report a version, so the 1.12 boundary in the +# progress format can be exercised on a machine whose real ninja is newer. +# A dry run skips the dev shell, so PATH here is what the script sees. +$ninjaPaths = @{} +foreach ($v in @{ old = '1.11.1'; new = '1.12.0' }.GetEnumerator()) { + $d = Join-Path $fixtures "ninja-$($v.Key)" + New-Item -ItemType Directory -Force -Path $d | Out-Null + Set-Content -Path (Join-Path $d 'ninja.bat') -Encoding ascii -Value @('@echo off', "echo $($v.Value)") + $ninjaPaths[$v.Key] = "$d;$env:PATH" +} + +# A clang-cl earlier on PATH than the Visual Studio one, which is what the +# compiler used to resolve to. Nothing runs it; the script only locates it. +$clangDir = Join-Path $fixtures 'clang' +New-Item -ItemType Directory -Force -Path $clangDir | Out-Null +Copy-Item "$env:SystemRoot\System32\where.exe" (Join-Path $clangDir 'clang-cl.exe') -Force +$clangOnPath = "$clangDir;$env:PATH" + +# ProgramFiles(x86) is where the script looks for vswhere, so an empty one +# stands in for a machine whose Visual Studio has no clang toolset. +$noVs = Join-Path $fixtures 'no-vs' +New-Item -ItemType Directory -Force -Path $noVs | Out-Null + +# A build directory that already holds a classic solution, for the case where +# what is on disk disagrees with what the generator would write. +$slnDir = Join-Path $fixtures 'sln' +New-Item -ItemType Directory -Force -Path $slnDir | Out-Null +Set-Content -Path (Join-Path $slnDir 'OrcaSlicer.sln') -Value '' -Encoding ascii + +# The pack stamp is checked against real dates, so a locale-dependent parse +# in the script cannot pass by looking date-shaped. Yesterday is accepted too, +# so a run that crosses midnight does not flake. +$dateStamps = @((Get-Date -Format 'yyyyMMdd'), (Get-Date).AddDays(-1).ToString('yyyyMMdd')) +$stampPattern = '_(' + ($dateStamps -join '|') + ')\.zip$' + +$cases = @( + 'argument handling' + @{ Name = 'no arguments prints help'; Args = @(); DryRun = $false + Contains = @('Usage: build_win.bat [options]', '--clang-cl') } + @{ Name = "--help lists all $($longFlags.Count) options the script defines"; Args = @('--help'); DryRun = $false + Contains = $longFlags } + @{ Name = 'help is grouped and shows usage, examples and environment'; Args = @('--help'); DryRun = $false + Contains = @('Usage: build_win.bat [options]', 'Actions:', 'Build configuration:', 'Toolchain:', + 'How much gets rebuilt:', 'Paths and extra arguments:', 'Diagnostics:', + 'Examples:', 'Environment:') } + @{ Name = 'the environment section shows what to set'; Args = @('--help'); DryRun = $false + Contains = @('ORCA_DEPS_CMAKE_ARGS', 'ORCA_SLICER_CMAKE_ARGS', 'ORCA_UPDATER_SIG_KEY', 'NINJA_STATUS', + 'set ORCA_SLICER_CMAKE_ARGS=-DSLIC3R_PCH=OFF', '(PowerShell)', 'debugscript') } + @{ Name = 'section headers do not widen the flag column'; Args = @('--help'); DryRun = $false + Match = @('^ -d, --deps +Download') } + # Windows Terminal opens at 120 columns and wraps at 120, so 119 is the + # limit. Anyone still on the old conhost gets 80 and will see wrapping. + @{ Name = 'every help line fits a 120 column console'; Args = @('--help'); DryRun = $false + NotMatch = @('^.{120,}$') } + @{ Name = 'an unknown long option is rejected'; Args = @('--nonsense'); ExpectExit = 1 + Contains = @('Failed to find arg') } + @{ Name = 'an unknown short option is rejected'; Args = @('-Z'); ExpectExit = 1 + Contains = @('Failed to find arg') } + @{ Name = 'a bare argument is rejected'; Args = @('deps'); ExpectExit = 1 + Contains = @('Unknown argument') } + @{ Name = 'an unknown architecture is rejected'; Args = @('-d', '--arch', 'sparc'); ExpectExit = 1 + Contains = @('Unknown architecture') } + @{ Name = 'a string option without a value is rejected'; Args = @('-d', '--arch'); ExpectExit = 1 + Contains = @('requires a value') } + @{ Name = 'short options can be bundled'; Args = @('-dx') + Contains = @('-G "Ninja Multi-Config"', '--target deps') } + + 'generator and compiler selection' + @{ Name = 'deps default to the Visual Studio generator'; Args = @('-d') + Contains = @('-G "Visual Studio', '-A x64', '--target deps') + NotContains = @('Ninja', 'clang-cl') } + @{ Name = '-x selects Ninja without changing compiler'; Args = @('-d', '-x') + Contains = @('-G "Ninja Multi-Config"') + NotContains = @('clang-cl', '-A x64') } + @{ Name = '-l -x builds with clang-cl under Ninja'; Args = @('-d', '-l', '-x') + Contains = @('-G "Ninja Multi-Config"') + Match = @('-DCMAKE_C_COMPILER="[^"]+/clang-cl\.exe"', '-DCMAKE_CXX_COMPILER="[^"]+/clang-cl\.exe"') } + # PATH order used to decide the compiler. VsDevCmd appends the Visual + # Studio LLVM directory to the end of PATH, so a standalone LLVM already + # there was resolved instead, and an old one failed the compiler check. + @{ Name = 'the compiler is resolved from Visual Studio, not PATH'; Args = @('-s', '-l', '-x') + Env = @{ PATH = $clangOnPath } + Match = @('^Compiler: .*/VC/Tools/Llvm/[^/]+/bin/clang-cl\.exe$') } + @{ Name = 'msvc names no compiler, having resolved none'; Args = @('-s') + NotContains = @('Compiler: ') } + @{ Name = '-l without -x names none either, the toolset picks it'; Args = @('-s', '-l') + NotContains = @('Compiler: ') } + # An empty ProgramFiles(x86) puts vswhere out of reach, which is a machine + # whose Visual Studio has no clang toolset. + @{ Name = 'without a Visual Studio clang the one on PATH is used and named'; Args = @('-s', '-l', '-x') + Env = @{ 'ProgramFiles(x86)' = $noVs; PATH = $clangOnPath } + Contains = @('Visual Studio has no clang-cl') + Match = @('^Compiler: .*/clang/clang-cl\.exe$') } + @{ Name = 'no clang-cl anywhere stops before configuring'; Args = @('-s', '-l', '-x'); ExpectExit = 1 + Env = @{ 'ProgramFiles(x86)' = $noVs; PATH = 'C:\Windows\system32;C:\Windows' } + Contains = @('No clang-cl found', '--install-vs ide -l') + NotContains = @('cmake -B') } + # Only a configure passes the compiler to CMake, so an action that does + # not configure resolves none, and cannot start needing one installed. + @{ Name = 'packing resolves no compiler'; Args = @('-p', '-l', '-x') + Contains = @('Packing the dependencies') + NotContains = @('Compiler: ') } + @{ Name = '--no-configure resolves none either'; Args = @('-s', '-l', '-x', '--no-configure') + Contains = @('cmake --build "build-clang"') + NotContains = @('Compiler: ') } + @{ Name = '-l alone uses the ClangCL toolset on the VS generator'; Args = @('-d', '-l') + Contains = @('-G "Visual Studio', '-T ClangCL') + NotContains = @('-DCMAKE_C_COMPILER') } + # --msvc and --msbuild name the defaults, so a caller can be explicit and a + # contradictory pair can be caught rather than silently resolved. + @{ Name = '--msvc is the compiler default spelled out'; Args = @('-d', '--msvc') + Contains = @('-G "Visual Studio', '-A x64') + NotContains = @('clang-cl') } + @{ Name = '--msbuild is the generator default spelled out'; Args = @('-d', '--msbuild') + Contains = @('-G "Visual Studio') + NotContains = @('Ninja') } + @{ Name = '--msvc with -x gives Ninja driving cl'; Args = @('-d', '--msvc', '-x') + Contains = @('-G "Ninja Multi-Config"') + NotContains = @('clang-cl') } + @{ Name = '--msbuild with -l gives the VS generator and the ClangCL toolset'; Args = @('-d', '--msbuild', '-l') + Contains = @('-G "Visual Studio', '-T ClangCL') } + # A developer with a standalone LLVM points at it, and the path is passed + # with forward slashes so CMake cannot read a backslash as an escape. + @{ Name = '--clang-path names the compiler, quoted for its spaces'; Args = @('-d', '-x', '--clang-path', 'C:\Program Files\LLVM\bin\clang-cl.exe') + Contains = @('-DCMAKE_C_COMPILER="C:/Program Files/LLVM/bin/clang-cl.exe"', + '-DCMAKE_CXX_COMPILER="C:/Program Files/LLVM/bin/clang-cl.exe"') } + @{ Name = '--clang-path beats the Visual Studio clang'; Args = @('-s', '-x', '--clang-path', 'C:\Program Files\LLVM\bin\clang-cl.exe') + Contains = @('Compiler: C:/Program Files/LLVM/bin/clang-cl.exe') } + @{ Name = '--clang-path is a clang request on its own'; Args = @('-d', '-x', '--clang-path', 'C:\Program Files\LLVM\bin\clang-cl.exe') + Contains = @('deps/build-clang') } + @{ Name = '--clang-path needs Ninja to take effect'; Args = @('-d', '--clang-path', 'C:\Program Files\LLVM\bin\clang-cl.exe'); ExpectExit = 1 + Contains = @('needs the Ninja generator') } + # `exist` is true for a directory too, and a directory would reach CMake + # as the compiler. + @{ Name = '--clang-path must name the exe, not its folder'; Args = @('-d', '-x', '--clang-path', 'C:\Program Files\LLVM'); ExpectExit = 1 + Contains = @('is a directory') } + @{ Name = 'a clang-cl that is not there is caught early'; Args = @('-d', '-x', '--clang-path', 'C:\nope\clang-cl.exe'); ExpectExit = 1 + Contains = @('No clang-cl at') + NotContains = @('cmake -S deps') } + @{ Name = '--clang-path contradicting --msvc is rejected'; Args = @('-d', '-x', '--msvc', '--clang-path', 'C:\Program Files\LLVM\bin\clang-cl.exe'); ExpectExit = 1 + Contains = @('select different compilers') } + @{ Name = '--clang-cl and --msvc together are rejected'; Args = @('-d', '-l', '--msvc'); ExpectExit = 1 + Contains = @('select different compilers') } + @{ Name = '--ninja and --msbuild together are rejected'; Args = @('-d', '-x', '--msbuild'); ExpectExit = 1 + Contains = @('select different generators') } + # One option with a value, driven by a table, rather than a flag per + # release. Adding a release should not need a new flag. + @{ Name = '--vs 2019 pins that release and skips autodetect'; Args = @('-d', '--vs', '2019') + Contains = @('-G "Visual Studio 16 2019"') + NotContains = @('Detecting Visual Studio') } + @{ Name = '--vs 2022 pins that release'; Args = @('-d', '--vs', '2022') + Contains = @('-G "Visual Studio 17 2022"') } + @{ Name = '--vs 2026 pins that release'; Args = @('-d', '--vs', '2026') + Contains = @('-G "Visual Studio 18 2026"') } + @{ Name = 'an unknown release is rejected and the known ones listed'; Args = @('-d', '--vs', '2015'); ExpectExit = 1 + Contains = @('Unknown Visual Studio release', '2019, 2022, 2026') } + @{ Name = '--vs and --ninja together are rejected'; Args = @('-d', '--vs', '2022', '-x'); ExpectExit = 1 + Contains = @('select different generators') } + @{ Name = 'without --vs the release is autodetected'; Args = @('-d') + Contains = @('Detecting Visual Studio') } + + 'architecture' + @{ Name = 'x64 is the default'; Args = @('-d') + Contains = @('Configuration: Release, x64') + NotContains = @('build-arm64') } + @{ Name = 'arm64 sets the generator platform and deps tree'; Args = @('-d', '--arch', 'arm64') + Contains = @('-A ARM64', 'deps/build-arm64') } + @{ Name = 'the architecture is matched case-insensitively'; Args = @('-d', '--arch', 'ARM64') + Contains = @('-A ARM64', 'deps/build-arm64') } + @{ Name = 'arm64 under Ninja has no -A but keeps the arm64 tree'; Args = @('-d', '--arch', 'arm64', '-x', '-l') + Contains = @('deps/build-clang-arm64') + Match = @('-DCMAKE_C_COMPILER="[^"]+/clang-cl\.exe"') + NotContains = @('-A ') } + + 'build configurations' + # One option with a value, driven by a table, so Release is named rather + # than being whatever is left when no flag is passed. + @{ Name = 'release is the default'; Args = @('-s') + Contains = @('-DCMAKE_BUILD_TYPE=Release', 'cmake -B "build" ') } + @{ Name = '--config release is the default spelled out'; Args = @('-s', '--config', 'release') + Contains = @('-DCMAKE_BUILD_TYPE=Release', 'cmake -B "build" ') } + @{ Name = '--config debug builds into build-dbg'; Args = @('-s', '--config', 'debug') + Contains = @('-DCMAKE_BUILD_TYPE=Debug', 'cmake -B "build-dbg" ') } + @{ Name = '--config relwithdebinfo builds into build-dbginfo'; Args = @('-s', '--config', 'relwithdebinfo') + Contains = @('-DCMAKE_BUILD_TYPE=RelWithDebInfo', 'cmake -B "build-dbginfo" ') } + @{ Name = '--config minsizerel builds into build-minsize'; Args = @('-s', '--config', 'minsizerel') + Contains = @('-DCMAKE_BUILD_TYPE=MinSizeRel', 'cmake -B "build-minsize" ') } + # Batch variable names are case-insensitive, so the table lookup is too. + @{ Name = 'the configuration name is matched case-insensitively'; Args = @('-s', '--config', 'RelWithDebInfo') + Contains = @('-DCMAKE_BUILD_TYPE=RelWithDebInfo') } + @{ Name = 'an unknown configuration is rejected and the known ones listed'; Args = @('-s', '--config', 'bogus'); ExpectExit = 1 + Contains = @('Unknown configuration', 'release, debug, relwithdebinfo, minsizerel') } + # Release, RelWithDebInfo and MinSizeRel all link the /MD dependencies, so + # one tree serves all three. Debug is /MDd and cannot share. + @{ Name = 'minsizerel builds against the release deps'; Args = @('-d', '-s', '--config', 'minsizerel') + Contains = @('cmake -S deps -B "deps/build"', 'cmake -B "build-minsize" ') + NotContains = @('deps/build-minsize') } + @{ Name = 'relwithdebinfo builds against them too'; Args = @('-d', '-s', '-l', '--config', 'relwithdebinfo') + Contains = @('cmake -S deps -B "deps/build-clang"', 'cmake -B "build-dbginfo-clang" ') + NotContains = @('deps/build-dbginfo') } + @{ Name = 'debug keeps a dependency tree of its own'; Args = @('-d', '--config', 'debug') + Contains = @('cmake -S deps -B "deps/build-dbg"') } + # The build tree and the deps tree now have different names, so the + # derivation from the binary directory name cannot work and it is named. + @{ Name = 'a shared deps tree is named outright'; Args = @('-s', '-l', '--config', 'relwithdebinfo') + Match = @('-DDEP_BUILD_DIR="[A-Za-z]:\\.*\\deps\\build-clang"') } + @{ Name = 'configuration and arch combine into build-dbg-arm64'; Args = @('-s', '--config', 'debug', '--arch', 'arm64') + Contains = @('cmake -B "build-dbg-arm64" ') } + + 'what gets built' + @{ Name = 'the deps target defaults to deps'; Args = @('-d') + Contains = @('--target deps') } + @{ Name = '-t overrides the deps target'; Args = @('-d', '-t', 'dep_Boost') + Contains = @('--target dep_Boost') } + @{ Name = 'unit tests are off unless asked for'; Args = @('-s') + Contains = @('-DBUILD_TESTS=OFF') } + @{ Name = '--tests turns the unit tests on'; Args = @('-s', '--tests') + Contains = @('-DBUILD_TESTS=ON') } + @{ Name = '-a enables ASAN for the slicer'; Args = @('-s', '-a') + Contains = @('-DSLIC3R_ASAN=ON') } + @{ Name = 'the slicer build runs gettext'; Args = @('-s') + Contains = @('run_gettext.bat') } + # tools\7z.exe needs a 7z.dll beside it, which the repo does not carry, + # so the pack falls back to the bsdtar Windows ships. Either is correct; + # what matters is that one is chosen and handed the right names. + @{ Name = '-p packs the deps tree with whichever archiver is usable'; Args = @('-d', '-p') + Match = @('^\+ .*(7z\.exe a|tar\.exe -a -c -f) OrcaSlicer_dep_win-\S+\.zip OrcaSlicer_dep$') } + # The bundle is only good for what built it, so it carries the same three + # axes as the tree. Release x64 on cl keeps the historical plain name. + @{ Name = 'the bundle name carries compiler and deps flavour'; Args = @('-p', '-l', '--config', 'debug', '--arch', 'arm64') + Contains = @('OrcaSlicer_dep_win-ARM64-clang-dbg_') } + @{ Name = 'a relwithdebinfo pack is the release bundle'; Args = @('-p', '--config', 'relwithdebinfo') + Contains = @('OrcaSlicer_dep_win-x64_') + NotContains = @('-dbg', 'dbginfo') } + @{ Name = 'a plain release bundle keeps its old name'; Args = @('-p') + Contains = @('OrcaSlicer_dep_win-x64_') + NotContains = @('-clang', '-Release') } + @{ Name = 'the bundle is stamped with today, not a shuffled date'; Args = @('-p') + Match = @($stampPattern) } + # powershell.exe is not in System32 itself, so a trimmed PATH used to + # leave the stamp empty and the bundle named OrcaSlicer_dep_win-x64_.zip. + @{ Name = 'the bundle is stamped even with a bare PATH'; Args = @('-p') + Env = @{ PATH = 'C:\Windows\system32;C:\Windows' } + Match = @($stampPattern) } + @{ Name = '-p packs without rebuilding'; Args = @('-p') + Match = @('^\+ .*(7z\.exe a|tar\.exe -a -c -f) ') + NotContains = @('cmake -S deps') } + @{ Name = 'deps and slicer build in one invocation'; Args = @('-d', '-s', '-x', '-l') + Contains = @('cmake -S deps', 'cmake -B "build-clang" ') } + + 'the developer loop' + @{ Name = '--slicer-target builds one target'; Args = @('-s', '--slicer-target', 'libslic3r') + Contains = @('--config Release --target libslic3r') } + @{ Name = '--no-configure skips the slicer configure'; Args = @('-s', '--no-configure') + Contains = @('cmake --build "build"') + NotContains = @('cmake -B "build" ') } + @{ Name = '--no-configure skips the deps configure'; Args = @('-d', '--no-configure') + Contains = @('cmake --build "deps/build"') + NotContains = @('cmake -S deps') } + @{ Name = '--no-gettext skips the translation step'; Args = @('-s', '--no-gettext') + Contains = @('cmake --build "build"') + NotContains = @('run_gettext') } + # Installing copies the whole tree again for a layout only releases need, + # so it is asked for rather than assumed. + @{ Name = 'nothing is installed unless asked'; Args = @('-s') + Contains = @('run_gettext') + NotContains = @('--target install') } + @{ Name = '-i adds the install step'; Args = @('-s', '-i') + Contains = @('--target install') } + # install(TARGETS OrcaSlicer) has no OPTIONAL, so this would fail partway + # through a build instead of before it. + @{ Name = 'installing a tree with no executable is refused'; Args = @('-s', '-i', '--slicer-target', 'glad'); ExpectExit = 1 + Contains = @('--install needs the executable') + NotContains = @('cmake --build') } + # Without -s there is no install step, so there is nothing to refuse. + @{ Name = 'the same flags without a slicer build are left alone'; Args = @('-d', '-i', '--slicer-target', 'glad') + Contains = @('cmake -S deps') + NotContains = @('--install needs the executable') } + @{ Name = 'naming the executable target is allowed'; Args = @('-s', '-i', '--slicer-target', 'OrcaSlicer') + Contains = @('--target install') } + # Ninja counts compilers, so -j goes on the command line. MSBuild's -j + # counts projects while /MP still runs one cl per core inside each, so the + # cap goes to CL_MPCount in the environment instead. A dry run can only + # show that no -j is passed. + @{ Name = '-j on Ninja passes -j to cmake'; Args = @('-s', '-x', '-j', '4') + Contains = @('Parallel jobs: 4', '--config Release -j 4') } + @{ Name = '-j on Ninja reaches the deps build'; Args = @('-d', '-x', '-j', '2') + Contains = @('--target deps -j 2') } + @{ Name = '-j on MSBuild does not pass -j, which would count projects'; Args = @('-s', '-j', '4') + Contains = @('Parallel jobs: 4') + NotContains = @('-j 4') } + @{ Name = '-j on MSBuild leaves the deps build without -j too'; Args = @('-d', '-j', '2') + Contains = @('--target deps') + NotContains = @('-j 2') } + @{ Name = 'no -j means no job limit'; Args = @('-s') + NotContains = @('parallel jobs', '-j ') } + @{ Name = 'a non-numeric -j is rejected'; Args = @('-s', '-j', 'abc'); ExpectExit = 1 + Contains = @('Invalid --jobs value') } + @{ Name = 'a zero -j is rejected'; Args = @('-s', '-j', '0'); ExpectExit = 1 + Contains = @('Invalid --jobs value') } + @{ Name = 'the loop options combine into a single build command'; Args = @('-s', '-x', '--no-configure', '--no-gettext', '--slicer-target', 'libslic3r_tests', '-j', '8') + Contains = @('--target libslic3r_tests -j 8') + NotContains = @('cmake -B "build" ', 'run_gettext', '--target install') } + + 'one tree per configuration, compiler and architecture' + # CMake resets its cache and carries on when the compiler changes under an + # existing tree, leaving stamps from the old toolchain. Give each its own. + @{ Name = 'clang builds land in their own trees'; Args = @('-d', '-s', '-l') + Contains = @('cmake -S deps -B "deps/build-clang"', 'cmake -B "build-clang" ') } + @{ Name = 'MSVC keeps the historical plain names'; Args = @('-d', '-s', '--msvc') + Contains = @('cmake -S deps -B "deps/build"', 'cmake -B "build" ') + NotContains = @('build-clang') } + @{ Name = 'configuration, compiler and arch all name the tree'; Args = @('-d', '-s', '-l', '--config', 'debug', '--arch', 'arm64') + Contains = @('cmake -S deps -B "deps/build-dbg-clang-arm64"', 'cmake -B "build-dbg-clang-arm64" ') } + + 'locating the dependency tree' + # Without --deps-dir nothing is passed, so CMakeLists derives the path from + # the binary directory name as it always has. + @{ Name = 'the deps path is left to CMake by default'; Args = @('-s') + NotContains = @('-DDEP_BUILD_DIR') } + @{ Name = '--deps-dir tells the slicer where the deps are'; Args = @('-s', '--deps-dir', 'D:\orca-deps') + Contains = @('-DDEP_BUILD_DIR="D:\orca-deps"') } + @{ Name = '--deps-dir also redirects the deps build'; Args = @('-d', '--deps-dir', 'D:\orca-deps') + Contains = @('cmake -S deps -B "D:\orca-deps"', 'cmake --build "D:\orca-deps"') + NotContains = @('deps/build') } + # Paths are quoted throughout, so one with spaces survives the whole run. + @{ Name = 'a deps path with spaces survives'; Args = @('-d', '-s', '--deps-dir', 'C:\Program Files\deps') + Contains = @('-B "C:\Program Files\deps"', '-DDEP_BUILD_DIR="C:\Program Files\deps"') } + @{ Name = '--deps-dir also redirects the pack'; Args = @('-p', '--deps-dir', 'D:\orca-deps') + Contains = @('cd /d "D:\orca-deps"') } + @{ Name = 'pack uses an absolute default path'; Args = @('-p') + Match = @('^\+ cd /d "[A-Za-z]:\\.*\\deps\\build"') } + # CMakeLists derives DEP_BUILD_DIR from the build directory's name, so + # pointing the build elsewhere has to name the deps tree outright. + @{ Name = '--build-dir moves the slicer build'; Args = @('-s', '-l', '-x', '--build-dir', 'out/build/x64-clang') + Contains = @('cmake -B "out/build/x64-clang" ') } + @{ Name = '--build-dir still names the deps tree'; Args = @('-s', '-l', '-x', '--build-dir', 'out/build/x64-clang') + Match = @('-DDEP_BUILD_DIR="[A-Za-z]:\\.*\\deps\\build-clang"') } + @{ Name = '--deps-dir wins over the derived tree'; Args = @('-s', '--build-dir', 'out/build/x64-clang', '--deps-dir', 'D:\orca-deps') + Contains = @('-DDEP_BUILD_DIR="D:\orca-deps"') } + # A value ending in a backslash escapes the closing quote it is spliced + # into, so cmake would receive D:\tree" and swallow the next argument. + @{ Name = 'a trailing backslash is trimmed off a path'; Args = @('-s', '--build-dir', 'D:\tree\') + Contains = @('cmake -B "D:\tree" ') } + @{ Name = 'a build directory with spaces survives'; Args = @('-s', '--build-dir', 'C:\Program Files\tree') + Contains = @('cmake -B "C:\Program Files\tree" ') } + @{ Name = 'the default build names no deps tree'; Args = @('-s') + NotContains = @('DEP_BUILD_DIR') } + + 'saying what mode and toolchain are in play' + @{ Name = 'a dry run announces itself before the first command'; Args = @('-u') + First = '^Dry run: printing commands without running them\.$' } + @{ Name = 'the announcement leads even a full build'; Args = @('-ds') + First = '^Dry run: ' } + # Autodetect only runs for the Visual Studio generator, and only when no + # release was pinned, so it needs a Visual Studio on the machine. + @{ Name = 'the detected Visual Studio names its release year'; Args = @('-s') + Match = @('^Detected Visual Studio \d+ \(20\d\d\)$') } + @{ Name = 'pinning a release skips detection'; Args = @('-s', '--vs', '2022') + NotContains = @('Detected Visual Studio') } + + 'an action has to be asked for' + # Neither can happen without building the slicer, so they stand alone the + # way --install-vs does. + @{ Name = '--run-tests is an action on its own'; Args = @('--run-tests') + Contains = @('-DBUILD_TESTS=ON', 'ctest --test-dir') + NotContains = @('Nothing to do') } + @{ Name = '--tests is too'; Args = @('--tests') + Contains = @('-DBUILD_TESTS=ON') + NotContains = @('Nothing to do', 'ctest --test-dir') } + # Naming an action means that action, not a fuller build. + @{ Name = 'they do not add a slicer build to one already asked for'; Args = @('-d', '--tests') + Contains = @('cmake -S deps') + NotContains = @('cmake -B "build"') } + @{ Name = 'shaping options alone are not an action'; Args = @('--config', 'debug'); ExpectExit = 1 + Contains = @('Nothing to do.') + NotContains = @('Build completed') } + @{ Name = '-j alone is not an action either'; Args = @('-j', '4'); ExpectExit = 1 + Contains = @('Nothing to do.') } + @{ Name = '--install-vs counts as an action on its own'; Args = @('--install-vs', 'ide') + NotContains = @('Nothing to do.') } + + 'diagnostics' + @{ Name = '-v asks cmake for the command lines'; Args = @('-s', '-v') + Contains = @('--verbose') } + @{ Name = '-v applies to the deps build too'; Args = @('-d', '-v') + Contains = @('--target deps', '--verbose') } + # ninja's own default shows neither a percentage nor a time. %w and %W + # need ninja 1.12, and an unknown placeholder is fatal, so the format is + # chosen from the version rather than hardcoded. + @{ Name = '-v names the ninja progress format'; Args = @('-s', '-x', '-v') + Match = @('^Ninja progress format: \[.*%p.*\]') } + @{ Name = 'a ninja older than 1.12 gets the format it understands'; Args = @('-s', '-x', '-v') + Env = @{ PATH = $ninjaPaths['old'] } + Contains = @('Ninja progress format: [%s/%t %p :: %e]') } + @{ Name = 'ninja 1.12 gets elapsed and remaining'; Args = @('-s', '-x', '-v') + Env = @{ PATH = $ninjaPaths['new'] } + Contains = @('Ninja progress format: [%f/%t %p :: %w / %W]') } + @{ Name = 'a format you set yourself is left alone'; Args = @('-s', '-x', '-v') + Env = @{ NINJA_STATUS = '[mine] ' } + Contains = @('Ninja progress format: [mine]') } + @{ Name = 'MSBuild builds mention no ninja format'; Args = @('-s', '-v') + NotContains = @('Ninja progress format') } + @{ Name = 'builds are quiet without -v'; Args = @('-s') + NotContains = @('--verbose') } + + 'installing prerequisites' + # -u installs CMake, Perl and Git. Visual Studio is a separate ask, because + # most people already have it, and which one you want is a real choice. + @{ Name = '-u alone installs no Visual Studio'; Args = @('-u') + Contains = @('Kitware.CMake', 'StrawberryPerl', 'Git.Git') + NotContains = @('Microsoft.VisualStudio') } + # Asking for Visual Studio is asking to install prerequisites, so it does + # not also need -u; requiring both meant --install-vs alone did nothing. + @{ Name = '--install-vs works without -u'; Args = @('--install-vs', 'ide') + Contains = @('id=Microsoft.VisualStudio.Community', 'Kitware.CMake', 'Git.Git') } + @{ Name = '--install-vs buildtools asks for the build tools'; Args = @('-u', '--install-vs', 'buildtools') + Contains = @('id=Microsoft.VisualStudio.BuildTools') + NotContains = @('VC.CoreIde') } + @{ Name = '--install-vs ide asks for Community with the IDE component'; Args = @('-u', '--install-vs', 'ide') + Contains = @('id=Microsoft.VisualStudio.Community', 'VC.CoreIde') } + @{ Name = 'an unknown edition is rejected and the known ones listed'; Args = @('-u', '--install-vs', 'bogus'); ExpectExit = 1 + Contains = @('Unknown Visual Studio edition', 'buildtools, ide') } + @{ Name = '--vs picks which release to install'; Args = @('-u', '--vs', '2022', '--install-vs', 'buildtools') + Contains = @('id=Microsoft.VisualStudio.2022.BuildTools') } + @{ Name = '-l adds the clang compiler and the MSBuild toolset'; Args = @('-u', '--install-vs', 'buildtools', '-l') + Contains = @('VC.Llvm.Clang ', 'VC.Llvm.ClangToolset') } + # CMake 4.x breaks Boost.Context on ARM64, so the installer pins 3.31 there + # and leaves x64 on the current release. + @{ Name = 'CMake is pinned to 3.31 when installing for arm64'; Args = @('-u', '--arch', 'arm64') + Contains = @('Kitware.CMake --version 3.31.8') } + @{ Name = 'CMake is not pinned for x64'; Args = @('-u', '--arch', 'x64') + Contains = @('Kitware.CMake') + NotContains = @('--version') } + @{ Name = 'installing for arm64 asks for the ARM64 toolset'; Args = @('--install-vs', 'buildtools', '--arch', 'arm64') + Contains = @('Microsoft.VisualStudio.Component.VC.Tools.ARM64') } + @{ Name = 'an x64 install asks only for the x64 toolset'; Args = @('--install-vs', 'buildtools') + Contains = @('Microsoft.VisualStudio.Component.VC.Tools.x86.x64') + NotContains = @('VC.Tools.ARM64') } + + 'dry run changes nothing' + # clean_tree resolves the path before removing it, so the line echoed is + # absolute. It is the last thing printed before a directory goes. + @{ Name = '-c echoes the deps rmdir rather than running it'; Args = @('-d', '-c') + Match = @('^\+ rmdir /S /Q "[A-Za-z]:\\.*\\deps\\build"$') } + @{ Name = '-c removes the tree --deps-dir named, not the default one'; Args = @('-d', '-c', '--deps-dir', 'D:\orca-deps') + Match = @('^\+ rmdir /S /Q "D:\\orca-deps"$') + NotContains = @('\deps\build') } + @{ Name = '-c echoes the slicer rmdir rather than running it'; Args = @('-s', '-c') + Match = @('^\+ rmdir /S /Q "[A-Za-z]:\\.*\\build"$') } + @{ Name = 'cleaning a slicer build leaves the deps tree alone'; Args = @('-s', '-c') + NotMatch = @('rmdir.*\\deps\\') } + @{ Name = 'cleaning both builds removes both trees'; Args = @('-d', '-s', '-c') + Match = @('^\+ rmdir /S /Q "[A-Za-z]:\\[^"]*\\deps\\build"$', + '^\+ rmdir /S /Q "[A-Za-z]:\\(?!.*\\deps\\)[^"]*\\build"$') } + + # Nothing below should be reachable. They are a floor under a bug that + # hands clean_tree a path far shorter than it looks. + @{ Name = 'a configuration whose lookup comes back empty is rejected'; Args = @('-d', '-c', '--config', 'release '); ExpectExit = 1 + Contains = @('Unknown configuration') + NotContains = @('rmdir') } + @{ Name = 'a drive root is refused'; Args = @('-d', '-c', '--deps-dir', 'D:\'); ExpectExit = 1 + Contains = @('that is a drive root') + NotContains = @('+ rmdir') } + @{ Name = 'the repository itself is refused'; Args = @('-d', '-c', '--deps-dir', '.'); ExpectExit = 1 + Contains = @('that is the repository itself') + NotContains = @('+ rmdir') } + @{ Name = '-k echoes the taskkills rather than running them'; Args = @('-k') + Contains = @('+ taskkill /F /IM MSBuild.exe', '+ taskkill /F /IM cl.exe') } + @{ Name = '-k also covers the Ninja toolchain'; Args = @('-k') + Contains = @('+ taskkill /F /IM ninja.exe', '+ taskkill /F /IM clang-cl.exe') } + @{ Name = '-k announces the dry run like every other action'; Args = @('-k') + First = '^Dry run: ' } + @{ Name = '-u echoes the winget installs rather than running them'; Args = @('-u') + Contains = @('+ winget install', 'Kitware.CMake') + NotContains = @('cmake -S deps') } + # Not a dry run: winget is a stub that fails, so nothing is installed. + @{ Name = 'a failed install is reported, not claimed as success'; Args = @('-u') + DryRun = $false; ExpectExit = 1 + Env = @{ PATH = $stubPath } + Contains = @('Failed to install:', 'CMake', 'Perl', 'Git') + NotContains = @('Installed the prerequisites') } + # The reason belongs inside the frame. Every command this path ran had + # already succeeded, so naming the last one would point at the wrong thing. + @{ Name = 'a failed install names the reason, not the last command'; Args = @('-u') + DryRun = $false; ExpectExit = 1 + Env = @{ PATH = $stubPath } + Contains = @('####', 'Failed to install:') + NotContains = @('Failed: winget') } + @{ Name = 'a dry run does not claim the install happened'; Args = @('-u') + Contains = @('Dry run: nothing was installed.') + NotContains = @('are in place') } + @{ Name = '-u with a build defers that build, not a fuller one'; Args = @('-u', '-d') + Contains = @('build_win.bat -d') + NotContains = @('build_win.bat -ds') } + @{ Name = '-u on its own suggests the whole build'; Args = @('-u') + Contains = @('build_win.bat -ds') } + @{ Name = 'a dry run only echoes, it never configures'; Args = @('-d') + Contains = @('+ cmake') + NotContains = @('CMake Error', 'Configuring done') } + + 'extra configure arguments' + # --deps-args and --slicer-args are declared "rawstring", so a value that + # looks like an option is allowed through. Plain string options still + # reject one, since there it almost always means a forgotten value. + @{ Name = '--deps-args reaches the deps configure'; Args = @('-d', '--deps-args', 'FOO') + Contains = @('-DCMAKE_BUILD_TYPE=Release FOO') } + @{ Name = '--slicer-args reaches the slicer configure'; Args = @('-s', '--slicer-args', 'BAZ') + Contains = @('BAZ') } + @{ Name = '--deps-args accepts a value that starts with a dash'; Args = @('-d', '--deps-args', '-DFOO') + Contains = @('-DFOO') } + @{ Name = 'a plain string option still rejects a dash-leading value'; Args = @('-d', '--deps-target', '--tests'); ExpectExit = 1 + Contains = @('looks like another option') } + # cmd splits arguments on "=" as well as spaces, so an unquoted -D reaches + # the script as two arguments however it was invoked. + @{ Name = 'an unquoted value containing = is rejected'; Args = @('-d', '--deps-args', 'FOO=BAR'); ExpectExit = 1 + Contains = @('Unknown argument') } + # The environment overrides exist for that reason; nothing tokenises them. + @{ Name = 'ORCA_DEPS_CMAKE_ARGS reaches the deps configure'; Args = @('-d') + Env = @{ ORCA_DEPS_CMAKE_ARGS = '-DFOO=BAR -DBAZ=QUX' } + Contains = @('-DFOO=BAR -DBAZ=QUX') } + @{ Name = 'ORCA_SLICER_CMAKE_ARGS reaches the slicer configure'; Args = @('-s') + Env = @{ ORCA_SLICER_CMAKE_ARGS = '-DWANTED=1' } + Contains = @('-DWANTED=1') } + @{ Name = 'the deps override does not leak into the slicer configure'; Args = @('-s') + Env = @{ ORCA_DEPS_CMAKE_ARGS = '-DDEPSONLY=1' } + NotContains = @('-DDEPSONLY=1') } + @{ Name = 'the help points at the environment for a spaced argument'; Args = @('--help'); DryRun = $false + Contains = @('Neither form supports a value containing an ampersand') } + @{ Name = 'the help lists the environment overrides'; Args = @('--help'); DryRun = $false + Contains = @('Environment:', 'ORCA_DEPS_CMAKE_ARGS', 'ORCA_SLICER_CMAKE_ARGS') } + + 'running the unit tests' + @{ Name = '--tests builds them without running them'; Args = @('-s', '--tests') + Contains = @('-DBUILD_TESTS=ON') + NotContains = @('ctest') } + @{ Name = '--run-tests builds and runs them'; Args = @('-s', '--run-tests') + Contains = @('-DBUILD_TESTS=ON', 'ctest --test-dir "build/tests" -C Release --output-on-failure') } + @{ Name = '--run-tests follows the build type and directory'; Args = @('-s', '--run-tests', '--config', 'debug') + Contains = @('ctest --test-dir "build-dbg/tests" -C Debug') } + @{ Name = 'no tests are run by default'; Args = @('-s') + NotContains = @('ctest') } + + 'failures are reported' + @{ Name = 'a missing cmake is caught and exits non-zero'; Args = @('-d'); ExpectExit = 1 + Env = @{ PATH = 'C:\Windows\system32;C:\Windows' } + Contains = @('CMake was not found') } + @{ Name = 'packing does not need cmake, only an archiver'; Args = @('-p') + Env = @{ PATH = 'C:\Windows\system32;C:\Windows' } + NotContains = @('CMake was not found') } + # Not a dry run: a cd to a missing drive is a real failure inside a + # parenthesised block, which is where exit /b silently loses its code. + # Without the jump to :die this exits 0 and a failed build reads as a + # successful one. + @{ Name = 'a failure inside a build block reaches the caller'; Args = @('-p', '--deps-dir', 'Z:\nope') + DryRun = $false; ExpectExit = 1 + Contains = @('Exit code 1.', '####') + NotContains = @('Build completed', 'Try') } + # The retry follows the stage that failed. Offering it for the whole run + # would clean a dependency tree that was not at fault. + @{ Name = 'a failure names a retry scoped to the stage that failed'; Args = @('-d', '-s', '--deps-dir', 'Z:\nope') + DryRun = $false; ExpectExit = 1 + Contains = @('build_win.bat -d --deps-dir "Z:\nope" -c') + NotContains = @('build_win.bat -ds') } + # CMake's own failure here is hundreds of lines about package resolution. + @{ Name = 'a missing dependency tree is named, not left to CMake'; Args = @('-s', '--deps-dir', 'Z:\nope') + DryRun = $false; ExpectExit = 1 + Contains = @('Dependencies not found at', 'Build them with build_win.bat -d --deps-dir "Z:\nope"') + NotContains = @('cmake -B', 'Try') } + # Every other suggestion carries the flags that reproduce the run; a bare + # -d would point at the MSVC tree after a clang build. + @{ Name = 'the missing-deps hint names this toolchain'; Args = @('-s', '-l', '-x', '--deps-dir', 'deps/not-built') + DryRun = $false; ExpectExit = 1 + Contains = @('Build them with build_win.bat -d -l -x --deps-dir "deps/not-built"') + NotExists = @('deps/not-built') } + # A dry run configures nothing, so it must not depend on which trees happen + # to exist on the machine running the suite. + @{ Name = 'a dry run does not check for the deps tree'; Args = @('-s', '--deps-dir', 'Z:\nope') + Contains = @('cmake -B "build"') + NotContains = @('Dependencies not found') } + # -d is about to build them, so there is nothing to report yet. + @{ Name = 'building the deps in the same run skips the check'; Args = @('-d', '-s', '--deps-dir', 'Z:\nope') + DryRun = $false; ExpectExit = 1 + NotContains = @('Dependencies not found') } + # A configure fails before any compiler runs, so -v has nothing to show. + @{ Name = 'a configure failure is not offered a verbose rebuild'; Args = @('-d', '--deps-dir', 'Z:\nope') + DryRun = $false; ExpectExit = 1 + Contains = @('-c discard that tree') + NotContains = @('-v show the failing') } + # A bare --no-configure succeeds on a machine that already has a usable + # build tree, so name one that cannot exist instead. + @{ Name = 'a build failure is'; Args = @('-s', '--no-configure', '--build-dir', 'deps/no-such-tree') + DryRun = $false; ExpectExit = 1 + Contains = @('-v show the failing') + NotExists = @('deps/no-such-tree') } + # --build-dir names the slicer tree, which a deps failure has nothing to do + # with. --deps-dir stays, because that is the tree that failed. + @{ Name = 'a deps retry leaves out the slicer tree'; Args = @('-d', '-s', '--deps-dir', 'Z:\nope', '--build-dir', 'D:\b') + DryRun = $false; ExpectExit = 1 + Contains = @('build_win.bat -d --deps-dir "Z:\nope" -c') + NotContains = @('--build-dir') } + @{ Name = 'an unknown configuration stays a single line'; Args = @('-s', '--config', 'bogus'); ExpectExit = 1 + Contains = @('Unknown configuration') + NotContains = @('####', 'Try') } + @{ Name = 'a bad --jobs value is not framed either'; Args = @('-s', '-j', 'x'); ExpectExit = 1 + Contains = @('Invalid --jobs value') + NotContains = @('####') } + + 'the summary says what to do next' + # Every suggested command carries the flags that reproduce this run. + @{ Name = 'a deps build points at the slicer build'; Args = @('-d', '-l') + Contains = @('Build the slicer build_win.bat -s -l') + NotContains = @('Run it') } + @{ Name = 'a ninja slicer build offers a single target'; Args = @('-s', '-l', '-x') + Contains = @('Rebuild after edits build_win.bat -s -l -x --no-configure', 'Rebuild one target') + NotContains = @('Solution', 'Open in Visual Studio') } + @{ Name = 'a visual studio build names the solution instead'; Args = @('-s') + Contains = @('Solution ', 'Open in Visual Studio build\OrcaSlicer.sln', + 'Rebuild after edits build_win.bat -s --no-configure') + NotContains = @('Rebuild one target') } + @{ Name = 'the configuration and architecture come back'; Args = @('-s', '-l', '-x', '--config', 'debug', '--arch', 'arm64') + Contains = @('build_win.bat -s -l -x --config debug --arch arm64 --no-configure') } + @{ Name = 'the tree overrides come back quoted'; Args = @('-s', '--deps-dir', 'D:\d', '--build-dir', 'D:\b') + Contains = @('--deps-dir "D:\d" --build-dir "D:\b"') } + @{ Name = 'a pinned visual studio release comes back'; Args = @('-s', '--vs', '2022') + Contains = @('build_win.bat -s --vs 2022 --no-configure') } + # Autodetection writes what it found into the same variable, so a detected + # release must not come back as though it had been asked for. + @{ Name = 'a detected release does not'; Args = @('-s') + NotContains = @('--vs') } + @{ Name = 'the binary is named in the build tree it was built in'; Args = @('-s', '-l', '-x') + Contains = @('build-clang\src\Release\orca-slicer.exe') } + @{ Name = 'installing names the installed copy instead'; Args = @('-s', '-l', '-x', '-i') + Contains = @('build-clang\OrcaSlicer\orca-slicer.exe') } + # -i changes where the binary lands, so a rebuild that dropped it would + # leave the path above pointing at a stale copy. + @{ Name = 'the rebuild suggestion keeps -i'; Args = @('-s', '-l', '-x', '-i') + Contains = @('Rebuild after edits build_win.bat -s -l -x -i --no-configure') } + # A deps retry has no install step to repeat. + @{ Name = 'a deps retry drops it'; Args = @('-d', '-s', '-i', '--deps-dir', 'Z:\nope') + DryRun = $false; ExpectExit = 1 + Contains = @('build_win.bat -d --deps-dir "Z:\nope" -c') + NotContains = @('-d -i') } + # Naming a target builds it and its dependencies, not its dependents, so + # the binary on disk is whatever the last full build left there. + @{ Name = 'a single-target build does not claim the whole binary'; Args = @('-s', '-l', '-x', '--slicer-target', 'glad') + Contains = @('Target glad', 'Relink the binary build_win.bat -s -l -x --no-configure') + NotContains = @('Run it', 'orca-slicer.exe', 'Rebuild after edits') } + # The executable has a target of its own, and naming that one does relink. + @{ Name = 'naming the executable target still claims the binary'; Args = @('-s', '-l', '-x', '--slicer-target', 'OrcaSlicer') + Contains = @('Run it', 'orca-slicer.exe', 'Rebuild after edits') + NotContains = @('Target OrcaSlicer', 'Relink the binary') } + @{ Name = '--run-tests offers the ctest line'; Args = @('-s', '-l', '-x', '--run-tests') + Contains = @('Re-run the tests ctest --test-dir build-clang/tests -C Release') } + @{ Name = 'packing names the bundle and how to use it'; Args = @('-p', '-l') + Contains = @('Bundle ', 'Share the bundle') } + # The line supplies its own tree, so the one this run used must not ride + # along and contradict it. + @{ Name = 'the bundle line names one tree, not two'; Args = @('-s', '-p', '-l', '-x', '--deps-dir', 'D:\shared') + Contains = @('then build_win.bat -s -l -x --deps-dir <path>') + NotContains = @('--deps-dir "D:\shared" --deps-dir') } + @{ Name = 'installing prerequisites suggests the build that follows'; Args = @('-u', '-l') + Contains = @('Restart this shell', 'build_win.bat -ds -l') + NotContains = @('Run it') } + # Everything below the header line is worked out the same way in either + # run, which is why a dry run can cover it. + @{ Name = 'a dry run does not claim a build happened'; Args = @('-s', '-l', '-x') + Contains = @('Dry run: nothing was built.') + NotContains = @('Build completed in') } + # --no-configure is the iteration loop and still gets the block; four + # lines after a rebuild is not enough to be worth suppressing. + @{ Name = '--no-configure still gets the summary'; Args = @('-s', '-l', '-x', '--no-configure') + Contains = @('Next', 'Rebuild after edits') } + + 'pointing at the solution' + # The extension follows the generator, so these two pin the release and a + # build directory that cannot already hold a solution of either kind. + @{ Name = 'the 2026 generator gets the XML solution'; Args = @('-s', '--vs', '2026', '--build-dir', 'D:\tree') + Contains = @('Solution D:\tree\OrcaSlicer.slnx', 'Open in Visual Studio D:\tree\OrcaSlicer.slnx') } + @{ Name = 'the releases before it get the classic one'; Args = @('-s', '--vs', '2022', '--build-dir', 'D:\tree') + Contains = @('Solution D:\tree\OrcaSlicer.sln', 'Open in Visual Studio D:\tree\OrcaSlicer.sln') } + @{ Name = 'a solution already on disk wins over the generator'; Args = @('-s', '--vs', '2026', '--build-dir', $slnDir) + Match = @('^ Solution .*\\OrcaSlicer\.sln$') } + # Extension-agnostic from here: these cases are about the directory, and + # the release is whatever is installed. + @{ Name = 'the VS generator says where the solution is'; Args = @('-s') + Match = @('^ Solution .*\\build\\OrcaSlicer\.slnx?$') } + @{ Name = 'the solution path follows the configuration'; Args = @('-s', '--config', 'debug') + Match = @('^ Solution .*\\build-dbg\\OrcaSlicer\.slnx?$') } + @{ Name = 'the solution line survives an install'; Args = @('-s', '-i') + Contains = @(' Solution ') } + # The path is resolved, not pasted onto the repository root, so it is + # right whether --build-dir came absolute or with forward slashes. + @{ Name = 'a moved build still prints one real path'; Args = @('-s', '--build-dir', 'out/build/x64-clang') + Match = @('^ Solution [A-Za-z]:\\[^/]+\\OrcaSlicer\.slnx?$') } + @{ Name = 'an absolute --build-dir is not glued onto the repo root'; Args = @('-s', '--build-dir', 'D:\tree') + Match = @('^ Solution D:\\tree\\OrcaSlicer\.slnx?$') } +) + +function Invoke-BuildScript { + param([string[]] $Arguments, [hashtable] $Environment) + + $saved = @{} + if ($Environment) { + foreach ($key in $Environment.Keys) { + $saved[$key] = [Environment]::GetEnvironmentVariable($key) + Set-Item -Path "env:$key" -Value $Environment[$key] + } + } + try { + # 'Stop' turns a native command's stderr into a terminating error, and + # a case that exercises a real failure writes to stderr. Let the output + # through and judge the run by its exit code instead. The assignment is + # scoped to this function, so the rest of the suite keeps 'Stop'. + $ErrorActionPreference = 'Continue' + if ($Arguments.Count -eq 0) { + $out = & $Script 2>&1 | Out-String + } else { + $out = & $Script @Arguments 2>&1 | Out-String + } + return [pscustomobject]@{ Output = $out; Exit = $LASTEXITCODE } + } finally { + foreach ($key in $saved.Keys) { + if ($null -eq $saved[$key]) { Remove-Item -Path "env:$key" -ErrorAction SilentlyContinue } + else { Set-Item -Path "env:$key" -Value $saved[$key] } + } + } +} + +$knownFields = @( + 'Name', 'Args', 'ExpectExit', 'DryRun', 'First', 'Env', + 'Contains', 'NotContains', 'Match', 'NotMatch', 'NotExists' +) + +function Test-Case { + param([hashtable] $Case) + + # Read fields with the indexer, not dot notation. A hashtable exposes its + # own members too, so $Case.Contains returns the Contains *method* whenever + # the case has no key by that name. + $argv = @($Case['Args']) + if (-not $Case.ContainsKey('DryRun') -or $Case['DryRun']) { $argv += '--dry-run' } + + $expect = 0 + if ($Case.ContainsKey('ExpectExit')) { $expect = $Case['ExpectExit'] } + + $result = Invoke-BuildScript -Arguments $argv -Environment $Case['Env'] + + $problems = @() + + # A misspelled field is silently ignored by the checks below, which + # leaves the case asserting nothing at all and passing. + foreach ($field in $Case.Keys) { + if ($knownFields -notcontains $field) { $problems += "unknown field '$field'" } + } + + if ($result.Exit -ne $expect) { $problems += "exit $($result.Exit), expected $expect" } + foreach ($needle in $Case['Contains']) { + if (-not $result.Output.Contains($needle)) { $problems += "missing '$needle'" } + } + foreach ($needle in $Case['NotContains']) { + if ($result.Output.Contains($needle)) { $problems += "unexpected '$needle'" } + } + $lines = $result.Output -split "`r?`n" + if ($Case['First'] -and $lines[0] -notmatch $Case['First']) { + $problems += "first line was '$($lines[0])'" + } + foreach ($pattern in $Case['Match']) { + if (@($lines | Where-Object { $_ -match $pattern }).Count -eq 0) { + $problems += "no line matching /$pattern/" + } + } + foreach ($pattern in $Case['NotMatch']) { + foreach ($line in @($lines | Where-Object { $_ -match $pattern })) { + $problems += "line matches /$pattern/: $line" + } + } + # Output cannot show what a run did not create. + foreach ($path in $Case['NotExists']) { + $full = Join-Path (Split-Path -Parent $Script) $path + if (Test-Path $full) { + $problems += "created '$path'" + } + } + return ,$problems +} + +$pass = 0 +$failed = @() +# Held back so a filtered run does not print headings for groups it skipped. +$heading = $null + +foreach ($case in $cases) { + if ($case -is [string]) { + $heading = $case + continue + } + if ($Name -and $case['Name'] -notmatch $Name) { continue } + if ($heading) { + Write-Host '' + Write-Host $heading -ForegroundColor Cyan + $heading = $null + } + + $problems = Test-Case -Case $case + if ($problems.Count -eq 0) { + $pass++ + Write-Host (' ok ' + $case['Name']) + } else { + $failed += $case['Name'] + Write-Host (' FAIL ' + $case['Name']) -ForegroundColor Red + foreach ($problem in $problems) { Write-Host (' ' + $problem) -ForegroundColor Red } + Write-Host (' args: ' + (@($case['Args']) -join ' ')) + } +} + +Remove-Item -Recurse -Force $fixtures -ErrorAction SilentlyContinue + +Write-Host '' +# A pattern that matched nothing has proved nothing, so do not report it as +# a clean run. +if ($Name -and $pass -eq 0 -and $failed.Count -eq 0) { + Write-Host "no case matched /$Name/" -ForegroundColor Red + exit 1 +} +Write-Host "$pass passed, $($failed.Count) failed" +if ($failed.Count -gt 0) { + foreach ($name in $failed) { Write-Host " failed: $name" -ForegroundColor Red } + exit 1 +} +exit 0 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 79b49cfd16..f0de57f12c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -33,7 +33,8 @@ if (SLIC3R_GUI) set (wxWidgets_CONFIG_OPTIONS "--toolkit=gtk${SLIC3R_GTK}") find_package(wxWidgets 3.3 REQUIRED COMPONENTS base core adv html gl aui net media webview) else () - find_package(wxWidgets 3.3 CONFIG REQUIRED COMPONENTS html adv gl core base webview aui net media) + # propgrid is required by wxInspector. + find_package(wxWidgets 3.3 CONFIG REQUIRED COMPONENTS html adv gl core base webview aui net media propgrid) endif () if(UNIX) @@ -75,7 +76,7 @@ if (SLIC3R_GUI) list(FILTER wxWidgets_LIBRARIES EXCLUDE REGEX expat) list(APPEND wxWidgets_LIBRARIES ${EXPAT_LIBRARIES}) endif () - + # This is an issue in the new wxWidgets cmake build, doesn't deal with librt find_library(LIBRT rt) if(LIBRT) @@ -90,6 +91,16 @@ if (SLIC3R_GUI) # list(REMOVE_ITEM wxWidgets_LIBRARIES oleacc) find_package(wxInspector REQUIRED) + + # wxInspector's exported interface names the release wxWidgets import + # libraries, which a Debug build cannot link. wx is linked above instead. + get_target_property(_wxinspector_interface wxInspector::wxInspector INTERFACE_LINK_LIBRARIES) + if (_wxinspector_interface) + list(FILTER _wxinspector_interface EXCLUDE REGEX "wx(base|msw)3[0-9]u[_.]") + set_target_properties(wxInspector::wxInspector PROPERTIES + INTERFACE_LINK_LIBRARIES "${_wxinspector_interface}") + endif () + list(APPEND wxWidgets_LIBRARIES "wxInspector::wxInspector") message(STATUS "wx libs: ${wxWidgets_LIBRARIES}") @@ -256,14 +267,6 @@ if (WIN32) VERBATIM ) endforeach () - - if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") - orcaslicer_copy_dlls(COPY_DLLS "Debug" "d" output_dlls_Debug) - elseif("${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo") - orcaslicer_copy_dlls(COPY_DLLS "RelWithDebInfo" "" output_dlls_Release) - else() - orcaslicer_copy_dlls(COPY_DLLS "Release" "" output_dlls_Release) - endif() else () file(TO_NATIVE_PATH "${CMAKE_CURRENT_BINARY_DIR}/resources" WIN_RESOURCES_SYMLINK) add_custom_command(TARGET OrcaSlicer POST_BUILD @@ -279,8 +282,39 @@ if (WIN32) COMMENT "Copying Python runtime into the build tree" VERBATIM) + if (CMAKE_CONFIGURATION_TYPES) + # Multi-config generators (Visual Studio, Ninja Multi-Config): copy per config. + foreach (cfg ${CMAKE_CONFIGURATION_TYPES}) + if ("${cfg}" STREQUAL "Debug") + orcaslicer_copy_dlls(COPY_DLLS "Debug" "d" output_dlls_Debug) + elseif("${cfg}" STREQUAL "RelWithDebInfo") + orcaslicer_copy_dlls(COPY_DLLS "RelWithDebInfo" "" output_dlls_RelWithDebInfo) + else() + orcaslicer_copy_dlls(COPY_DLLS "${cfg}" "" output_dlls_${cfg}) + endif() + endforeach() + else() + # Single-config generators (Ninja): use CMAKE_BUILD_TYPE. + if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") + orcaslicer_copy_dlls(COPY_DLLS "Debug" "d" output_dlls_Debug) + elseif("${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo") + orcaslicer_copy_dlls(COPY_DLLS "RelWithDebInfo" "" output_dlls_RelWithDebInfo) + else() + orcaslicer_copy_dlls(COPY_DLLS "Release" "" output_dlls_Release) + endif() + endif() else () + if (NOT APPLE) + set(output_sos_Release "") + set(output_sos_Debug "") + add_custom_target(OrcaSlicerSosCopy ALL DEPENDS OrcaSlicer) + if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") + orcaslicer_copy_sos(OrcaSlicerSosCopy "Debug" "d" output_sos_Debug) + else() + orcaslicer_copy_sos(OrcaSlicerSosCopy "Release" "" output_sos_Release) + endif() + endif() if (APPLE AND NOT CMAKE_MACOSX_BUNDLE) # On OSX, the name of the binary matches the name of the Application. add_custom_command(TARGET OrcaSlicer POST_BUILD @@ -359,5 +393,9 @@ if (WIN32) install(FILES ${output_dlls_${build_type}} DESTINATION ".") install(DIRECTORY "${CMAKE_PREFIX_PATH}/libpython/" DESTINATION "python") else () + if (APPLE) + else() + install(FILES ${output_sos_${build_type}} DESTINATION "${CMAKE_INSTALL_PREFIX}") + endif() install(TARGETS OrcaSlicer RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR}) endif () diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index e0a15209a3..dcd5ec0082 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -3,7 +3,9 @@ #define _WIN32_WINNT 0x0502 // The standard Windows includes. #define WIN32_LEAN_AND_MEAN + #ifndef NOMINMAX #define NOMINMAX + #endif #include <Windows.h> #include <wchar.h> #include <commctrl.h> @@ -1791,8 +1793,9 @@ int CLI::run(int argc, char **argv) old_printable_area = config.option<ConfigOptionPoints>("printable_area", true)->values; old_exclude_area = config.option<ConfigOptionPoints>("bed_exclude_area", true)->values; if (old_printable_area.size() >= 4) { - old_printable_width = (int)(old_printable_area[2].x() - old_printable_area[0].x()); - old_printable_depth = (int)(old_printable_area[2].y() - old_printable_area[0].y()); + BoundingBoxf old_printable_bbox(old_printable_area); + old_printable_width = static_cast<int>(old_printable_bbox.size().x()); + old_printable_depth = static_cast<int>(old_printable_bbox.size().y()); } old_printable_height = (int)(config.opt_float("printable_height")); @@ -1967,7 +1970,7 @@ int CLI::run(int argc, char **argv) } } - auto load_config_file = [config_substitution_rule](const std::string& file, DynamicPrintConfig& config, std::string& config_type, + auto load_config_file = [](const std::string& file, DynamicPrintConfig& config, std::string& config_type, std::string& config_name, std::string& filament_id, std::string& config_from) { if (! boost::filesystem::exists(file)) { boost::nowide::cerr << __FUNCTION__<< ": can not find setting file: " << file << std::endl; @@ -2341,8 +2344,9 @@ int CLI::run(int argc, char **argv) Pointfs orig_printable_area; orig_printable_area = config.option<ConfigOptionPoints>("printable_area", true)->values; if (orig_printable_area.size() >= 4) { - orig_printable_width = (int)(orig_printable_area[2].x() - orig_printable_area[0].x()); - orig_printable_depth = (int)(orig_printable_area[2].y() - orig_printable_area[0].y()); + BoundingBoxf orig_printable_bbox(orig_printable_area); + orig_printable_width = static_cast<int>(orig_printable_bbox.size().x()); + orig_printable_depth = static_cast<int>(orig_printable_bbox.size().y()); } orig_printable_height = (int)(config.opt_float("printable_height")); BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(":%1%, check printable size: old_printable_width=%2%, orig_printable_width=%3%, old_printable_depth=%4%, orig_printable_depth=%5%, old_printable_height=%6%, orig_printable_height=%7%") @@ -3136,7 +3140,25 @@ int CLI::run(int argc, char **argv) std::vector<int> old_variant_counts(filament_count, 1), new_variant_counts; ConfigOptionInts* filament_self_index_opt = m_print_config.option<ConfigOptionInts>("filament_self_index"); - if (!filament_self_index_opt) { + bool need_regenerate_self_index = !filament_self_index_opt; + if (filament_self_index_opt) { + // a filament_self_index carried over from a stale project can disagree with the + // current filament_count. old_start_indice/old_variant_counts below are sized to + // filament_count and walked with 1-based group indices, so an index above + // filament_count overruns old_start_indice[++k], and a non-positive first index + // writes old_variant_counts[-1] - both heap corruption. + int max_self_index = 0, min_self_index = 1; + for (int v : filament_self_index_opt->values) { + max_self_index = std::max(max_self_index, v); + min_self_index = std::min(min_self_index, v); + } + if (max_self_index > filament_count || min_self_index < 1) { + BOOST_LOG_TRIVIAL(warning) << boost::format("filament_self_index range [%1%, %2%] is invalid for filament_count %3%, regenerating") + % min_self_index % max_self_index % filament_count; + need_regenerate_self_index = true; + } + } + if (need_regenerate_self_index) { filament_self_index_opt = m_print_config.option<ConfigOptionInts>("filament_self_index", true); std::vector<int>& filament_self_indice = filament_self_index_opt->values; filament_self_indice.resize(filament_count); @@ -3730,6 +3752,8 @@ int CLI::run(int argc, char **argv) double height_to_lid = m_print_config.opt_float("extruder_clearance_height_to_lid"); double height_to_rod = m_print_config.opt_float("extruder_clearance_height_to_rod"); double clearance_radius = m_print_config.opt_float("extruder_clearance_radius"); + double nozzle_height = m_print_config.opt_float("nozzle_height"); + Vec2d align_center = m_print_config.option<ConfigOptionPoint>("best_object_pos")->value; int shared_printable_width = 0, shared_printable_depth = 0, shared_printable_height = 0, shared_center_x = 0, shared_center_y = 0; //double plate_stride; std::string bed_texture; @@ -3740,8 +3764,11 @@ int CLI::run(int argc, char **argv) if (m_print_config.opt<ConfigOptionFloatsNullable>("extruder_printable_height")) { current_extruder_print_heights = m_print_config.opt<ConfigOptionFloatsNullable>("extruder_printable_height")->values; } - current_printable_width = current_printable_area[2].x() - current_printable_area[0].x(); - current_printable_depth = current_printable_area[2].y() - current_printable_area[0].y(); + { + BoundingBoxf current_printable_bbox(current_printable_area); + current_printable_width = static_cast<int>(current_printable_bbox.size().x()); + current_printable_depth = static_cast<int>(current_printable_bbox.size().y()); + } current_printable_height = print_height; if (old_printable_width == 0) old_printable_width = current_printable_width; @@ -3942,6 +3969,11 @@ int CLI::run(int argc, char **argv) ConfigOptionFloats *wipe_x_option = dynamic_cast<ConfigOptionFloats *>(print_config.option("wipe_tower_x")); ConfigOptionFloats *wipe_y_option = dynamic_cast<ConfigOptionFloats *>(print_config.option("wipe_tower_y")); + // get_at() silently clamps an out-of-range index to entry 0 - make the reuse visible + if (static_cast<size_t>(plate_index) >= wipe_x_option->values.size() || static_cast<size_t>(plate_index) >= wipe_y_option->values.size()) { + BOOST_LOG_TRIVIAL(warning) << boost::format("plate %1%: wipe_tower_x/y only has %2%/%3% entries, reusing entry 0's position") + %(plate_index+1) %wipe_x_option->values.size() %wipe_y_option->values.size(); + } plate_obj_size_info.wipe_x = wipe_x_option->get_at(plate_index); plate_obj_size_info.wipe_y = wipe_y_option->get_at(plate_index); @@ -4137,8 +4169,9 @@ int CLI::run(int argc, char **argv) temp_extruder_print_heights = config.option<ConfigOptionFloatsNullable>("extruder_printable_height", true)->values; if (temp_printable_area.size() >= 4) { - printer_plate.printable_width = (int)(temp_printable_area[2].x() - temp_printable_area[0].x()); - printer_plate.printable_depth = (int)(temp_printable_area[2].y() - temp_printable_area[0].y()); + BoundingBoxf temp_printable_bbox(temp_printable_area); + printer_plate.printable_width = static_cast<int>(temp_printable_bbox.size().x()); + printer_plate.printable_depth = static_cast<int>(temp_printable_bbox.size().y()); printer_plate.printable_height = (int)(config.opt_float("printable_height")); } if (temp_exclude_area.size() >= 4) { @@ -4786,6 +4819,8 @@ int CLI::run(int argc, char **argv) arrange_cfg.clearance_height_to_rod = height_to_rod; arrange_cfg.clearance_height_to_lid = height_to_lid; arrange_cfg.clearance_radius = clearance_radius; + arrange_cfg.nozzle_height = nozzle_height; + arrange_cfg.align_center = align_center; arrange_cfg.printable_height = print_height; arrange_cfg.min_obj_distance = 0; if (arrange_cfg.is_seq_print) { @@ -5164,7 +5199,8 @@ int CLI::run(int argc, char **argv) Vec3d wipe_tower_size = cur_plate->estimate_wipe_tower_size(m_print_config, w, v, new_extruder_count, filaments_cnt, false, enable_wrapping); Vec3d plate_origin = cur_plate->get_origin(); - int plate_width, plate_depth, plate_height; + int plate_width, plate_depth; + double plate_height; partplate_list.get_plate_size(plate_width, plate_depth, plate_height); float depth = wipe_tower_size(1); float margin = 15.f, wp_brim_width = 0.f; @@ -5236,6 +5272,8 @@ int CLI::run(int argc, char **argv) arrange_cfg.clearance_height_to_rod = height_to_rod; arrange_cfg.clearance_height_to_lid = height_to_lid; arrange_cfg.clearance_radius = clearance_radius; + arrange_cfg.nozzle_height = nozzle_height; + arrange_cfg.align_center = align_center; arrange_cfg.printable_height = print_height; arrange_cfg.min_obj_distance = 0; if (arrange_cfg.is_seq_print) { @@ -6495,7 +6533,6 @@ int CLI::run(int argc, char **argv) bool need_create_thumbnail_group = false, need_create_no_light_group = false, need_create_top_group = false; // get type and color for platedata - auto* filament_types = dynamic_cast<const ConfigOptionStrings*>(m_print_config.option("filament_type")); const ConfigOptionStrings* filament_color = dynamic_cast<const ConfigOptionStrings *>(m_print_config.option("filament_colour")); auto* filament_id = dynamic_cast<const ConfigOptionStrings*>(m_print_config.option("filament_ids")); const ConfigOptionFloats* nozzle_diameter_option = dynamic_cast<const ConfigOptionFloats *>(m_print_config.option("nozzle_diameter")); @@ -6514,10 +6551,11 @@ int CLI::run(int argc, char **argv) plate_data->nozzle_diameters = nozzle_diameter_str; for (auto it = plate_data->slice_filaments_info.begin(); it != plate_data->slice_filaments_info.end(); it++) { + // get_at() on an empty vector option is UB - these can be unpopulated on a from-scratch slice std::string display_filament_type; it->type = m_print_config.get_filament_type(display_filament_type, it->id); - it->color = filament_color ? filament_color->get_at(it->id) : "#FFFFFF"; - it->filament_id = filament_id?filament_id->get_at(it->id):""; + it->color = (filament_color && !filament_color->values.empty()) ? filament_color->get_at(it->id) : "#FFFFFF"; + it->filament_id = (filament_id && !filament_id->values.empty()) ? filament_id->get_at(it->id) : ""; } if (!plate_data->plate_thumbnail.is_valid()) { @@ -7054,7 +7092,7 @@ int CLI::run(int argc, char **argv) gcode_viewer.render_calibration_thumbnail(*calibration_data, cali_thumbnail_width, cali_thumbnail_height, calibration_params, partplate_list, opengl_mgr); //generate_calibration_thumbnail(*calibration_data, thumbnail_width, thumbnail_height, calibration_params); - //*plate_bboxes[index] = p->generate_first_layer_bbox(); + // *plate_bboxes[index] = p->generate_first_layer_bbox(); calibration_thumbnails.push_back(calibration_data);*/ PlateBBoxData* plate_bbox = new PlateBBoxData(); @@ -7309,6 +7347,10 @@ bool CLI::setup(int argc, char **argv) m_config.option(optdef.first, true); set_data_dir(m_config.opt_string("datadir")); + if (!data_dir().empty() && !boost::filesystem::exists(data_dir())) { + boost::nowide::cerr << "Could not create data directory: " << data_dir() << std::endl; + return false; + } //FIXME Validating at this stage most likely does not make sense, as the config is not fully initialized yet. if (!validity.empty()) { @@ -7382,7 +7424,7 @@ void CLI::print_help(bool include_print_options, PrinterTechnology printer_techn << std::endl << "Print setting priorities:" << std::endl << "\t1) setting values from the command line (highest priority)"<< std::endl - << "\t2) setting values loaded with --load_settings and --load_filaments" << std::endl + << "\t2) setting values loaded with --load-settings and --load-filaments" << std::endl << "\t3) setting values loaded from 3mf(lowest priority)" << std::endl; /*if (include_print_options) { @@ -7421,6 +7463,10 @@ bool CLI::export_models(IO::ExportFormat format, std::string path_dir) for (ModelObject* model_object : model.objects) { const std::string path = this->output_filepath(*model_object, index++, format, path_dir); + if (path.empty()) { + boost::nowide::cerr << "Could not create output directory for STL export" << std::endl; + return false; + } success = Slic3r::store_stl(path.c_str(), model_object, true); if (success) BOOST_LOG_TRIVIAL(info) << "Model successfully exported to " << path << std::endl; @@ -7546,8 +7592,19 @@ std::string CLI::output_filepath(const ModelObject &object, unsigned int index, output_path = subdir + "/"+file_name; boost::filesystem::path subdir_path(subdir); - if (!boost::filesystem::exists(subdir_path)) - boost::filesystem::create_directory(subdir_path); + if (!boost::filesystem::exists(subdir_path)) { + try { + boost::filesystem::create_directories(subdir_path); + } catch (const boost::filesystem::filesystem_error &ex) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to create output directory " << subdir_path.string() << ": " << ex.what(); + } + if (!boost::filesystem::exists(subdir_path)) { + // Directory creation failed and won't succeed on a retry (same path, same cause) - + // signal failure now instead of letting every object in the model repeat the same + // doomed attempt and fail with a less specific "export failed" error later. + return std::string(); + } + } return output_path; } diff --git a/src/OrcaSlicer_app_msvc.cpp b/src/OrcaSlicer_app_msvc.cpp index b1e498f4e4..35568a9cfa 100644 --- a/src/OrcaSlicer_app_msvc.cpp +++ b/src/OrcaSlicer_app_msvc.cpp @@ -2,7 +2,9 @@ #define _WIN32_WINNT 0x0502 // The standard Windows includes. #define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX #define NOMINMAX +#endif #include <Windows.h> #include <shellapi.h> #include <wchar.h> diff --git a/src/dev-utils/BaseException.cpp b/src/dev-utils/BaseException.cpp index f33c8f635f..048eadf151 100644 --- a/src/dev-utils/BaseException.cpp +++ b/src/dev-utils/BaseException.cpp @@ -9,6 +9,7 @@ #include <boost/format.hpp> #include <mutex> +#include "git_commit_hash.h" #include "libslic3r_version.h" static std::string g_log_folder; @@ -39,7 +40,7 @@ CBaseException::CBaseException(HANDLE hProcess, WORD wPID, LPCTSTR lpSymbolPath, output_file->open(log_filename, std::ios::out | std::ios::app); // Output app build info in crash log so we could look for the correct PDB files - OutputString(_T("%s\n\n"), _T(SLIC3R_APP_NAME " " SoftFever_VERSION " Build " GIT_COMMIT_HASH)); + OutputString(_T("%s\n\n"), _T(SLIC3R_APP_NAME " " SoftFever_VERSION " Build " GIT_COMMIT_HASH GIT_COMMIT_SUFFIX)); } } @@ -69,7 +70,7 @@ void CBaseException::OutputString(LPCTSTR lpszFormat, ...) //WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), szBuf, _tcslen(szBuf), NULL, NULL); //output it to the current directory of binary - std::string output_str = textconv_helper::T2A_(szBuf); + std::string output_str = static_cast<const char*>(textconv_helper::T2A_(szBuf)); *output_file << output_str; output_file->flush(); } diff --git a/src/dev-utils/CMakeLists.txt b/src/dev-utils/CMakeLists.txt index e3534a024a..2cfce6a7c5 100644 --- a/src/dev-utils/CMakeLists.txt +++ b/src/dev-utils/CMakeLists.txt @@ -20,6 +20,16 @@ if (SLIC3R_ENC_CHECK) ) endif() +if (ORCA_TOOLS) + set(_DEV_DEFS -DBOOST_ALL_NO_LIB -DBOOST_USE_WINAPI_VERSION=0x602 -DBOOST_SYSTEM_USE_UTF8) + + # generate_system_cache: pre-generates per-vendor <vendor>.opc files under resources/profiles for CI bundling. + add_executable(generate_system_cache generate_system_cache.cpp) + target_link_libraries(generate_system_cache libslic3r boost_headeronly) + target_compile_definitions(generate_system_cache PRIVATE ${_DEV_DEFS}) + +endif() + # Function that adds source file encoding check to a target # using the above encoding-check binary diff --git a/src/dev-utils/generate_system_cache.cpp b/src/dev-utils/generate_system_cache.cpp new file mode 100644 index 0000000000..426ccee997 --- /dev/null +++ b/src/dev-utils/generate_system_cache.cpp @@ -0,0 +1,84 @@ +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Preset.hpp" +#include "libslic3r/Utils.hpp" + +#include <boost/algorithm/string/predicate.hpp> +#include <boost/filesystem.hpp> +#include <boost/log/trivial.hpp> +#include <boost/program_options.hpp> +#include <iostream> + +using namespace Slic3r; +namespace fs = boost::filesystem; +namespace po = boost::program_options; + +int main(int argc, char* argv[]) +{ + po::options_description desc("OrcaSlicer System Cache Generator\nUsage"); + // clang-format off + desc.add_options() + ("help,h", "Show help") +#ifdef __APPLE__ + ("path,p", po::value<std::string>()->default_value("../../../../../../../resources/profiles"), "Path to profiles directory") +#else + ("path,p", po::value<std::string>()->default_value("../../../resources/profiles"), "Path to profiles directory") +#endif + ("log_level,l", po::value<int>()->default_value(2), "Log level (0=trace, 2=info, 4=error)"); + // clang-format on + + po::variables_map vm; + try { + po::store(po::parse_command_line(argc, argv, desc), vm); + if (vm.count("help")) { std::cout << desc << "\n"; return 0; } + po::notify(vm); + } catch (const po::error& e) { + std::cerr << "Error: " << e.what() << "\n" << desc << "\n"; + return 1; + } + + const std::string profiles_path = vm["path"].as<std::string>(); + const int log_level = vm["log_level"].as<int>(); + + if (!fs::exists(profiles_path) || !fs::is_directory(profiles_path)) { + std::cerr << "Error: '" << profiles_path << "' is not a valid directory\n"; + return 1; + } + + set_logging_level(log_level); + set_data_dir(profiles_path); + set_resources_dir(fs::path(profiles_path).parent_path().make_preferred().string()); + + const fs::path user_dir = fs::path(data_dir()) / PRESET_USER_DIR; + if (!fs::exists(user_dir)) + fs::create_directories(user_dir); + + AppConfig app_config; + app_config.set("preset_folder", "default"); + + auto preset_bundle = std::make_unique<PresetBundle>(); + preset_bundle->set_is_validation_mode(true); + preset_bundle->set_default_suppressed(true); + preset_bundle->set_generate_vendor_caches(true); + + std::cout << "Loading system presets from: " << profiles_path << "\n"; + + try { + // In validation mode data_dir() is the profiles directory set above, so the + // loader writes each <vendor>.opc next to its <vendor>.json as it parses it. + preset_bundle->load_presets(app_config, ForwardCompatibilitySubstitutionRule::EnableSilent); + } catch (const std::exception& ex) { + std::cerr << "Failed to load presets: " << ex.what() << "\n"; + return 1; + } + + size_t cache_count = 0; + for (auto& entry : fs::directory_iterator(profiles_path)) + if (boost::iends_with(entry.path().string(), ".opc")) + ++ cache_count; + if (cache_count == 0) { + std::cerr << "No vendor cache files were generated under " << profiles_path << "\n"; + return 1; + } + std::cout << "Generated " << cache_count << " vendor cache file(s) under " << profiles_path << "\n"; + return 0; +} diff --git a/src/dev-utils/platform/unix/build_linux_image.sh.in b/src/dev-utils/platform/unix/build_linux_image.sh.in index 85134b764d..873cf2e1b1 100755 --- a/src/dev-utils/platform/unix/build_linux_image.sh.in +++ b/src/dev-utils/platform/unix/build_linux_image.sh.in @@ -83,6 +83,10 @@ copy_shared_object_to_dir() { src_real="$(readlink -f "$src")" dst_name="$(basename "$src_real")" mkdir -p "$dst_dir" + if [ "$src_real" = "$dst_dir/$dst_name" ]; then + # Already bundled; the dependency resolved from the bundle directory. + return 0 + fi cp -fL "$src_real" "$dst_dir/$dst_name" if [ -L "$src" ]; then @@ -96,12 +100,23 @@ copy_shared_object_to_dir() { } bundle_dependency_closure() { - local dst_dir="$1" + local dst_dir + dst_dir="$(cd -- "$1" && pwd)" shift local -a queue=("$@") - local target dep dep_real copied_path + local target dep dep_real dep_key copied_path declare -A seen=() + # Dependencies are resolved with ldd, which only searches the default + # loader path. Deps-built shared libraries (e.g. the FFmpeg stack) are not + # installed there and carry no RUNPATH of their own, so once copied into + # the bundle ldd can no longer resolve one sibling from another + # (libavcodec -> libavutil) and reports it as missing. Extend the loader + # path with the bundle directory plus the source directories of files + # already bundled, so every library that was resolved once keeps resolving + # for its own dependencies. The audit script does the same + # (scripts/check_appimage_libs.sh). + local -a search_dirs=("$dst_dir") while [ ${#queue[@]} -gt 0 ]; do target="${queue[0]}" @@ -122,17 +137,24 @@ bundle_dependency_closure() { continue fi - if [ -n "${seen[$dep_real]}" ]; then + # Key dedup on the bundled file rather than the source path: once + # ldd resolves a library from the bundle directory (via the + # LD_LIBRARY_PATH above) its path is a dst_dir path, which differs + # from the source path the first resolution returned. Keying on + # the source path would re-copy the file onto itself. + dep_key="$dst_dir/$(basename "$dep_real")" + if [ -n "${seen[$dep_key]}" ]; then continue fi - seen[$dep_real]=1 + seen[$dep_key]=1 copy_shared_object_to_dir "$dep" "$dst_dir" + search_dirs+=("$(dirname "$dep_real")") copied_path="$dst_dir/$(basename "$dep_real")" if [ -e "$copied_path" ]; then queue+=("$copied_path") fi - done < <(appimage_list_direct_dependencies "$target") + done < <(LD_LIBRARY_PATH="$(IFS=:; printf '%s' "${search_dirs[*]}")${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" appimage_list_direct_dependencies "$target") done } diff --git a/src/libslic3r/AABBMesh.cpp b/src/libslic3r/AABBMesh.cpp index a23fd69f68..be026f69d7 100644 --- a/src/libslic3r/AABBMesh.cpp +++ b/src/libslic3r/AABBMesh.cpp @@ -54,12 +54,12 @@ public: int & i, Eigen::Matrix<double, 1, 3> &closest) { - size_t idx_unsigned = 0; - Vec3d closest_vec3d(closest); - double dist = + size_t idx_unsigned { 0 }; + Vec3d closest_vec3d { Vec3d::Zero() }; + const double dist { AABBTreeIndirect::squared_distance_to_indexed_triangle_set( its.vertices, its.indices, m_tree, point, idx_unsigned, - closest_vec3d); + closest_vec3d) }; i = int(idx_unsigned); closest = closest_vec3d; return dist; @@ -311,10 +311,9 @@ AABBMesh::hit_result IndexedMesh::filter_hits( double AABBMesh::squared_distance(const Vec3d &p, int& i, Vec3d& c) const { - double sqdst = 0; - Eigen::Matrix<double, 1, 3> pp = p; - Eigen::Matrix<double, 1, 3> cc; - sqdst = m_aabb->squared_distance(*m_tm, pp, i, cc); + const Eigen::Matrix<double, 1, 3> pp { p }; + Eigen::Matrix<double, 1, 3> cc { Vec3d::Zero() }; + const double sqdst { m_aabb->squared_distance(*m_tm, pp, i, cc) }; c = cc; return sqdst; } diff --git a/src/libslic3r/AABBTreeIndirect.hpp b/src/libslic3r/AABBTreeIndirect.hpp index 035982d876..483ca8efc4 100644 --- a/src/libslic3r/AABBTreeIndirect.hpp +++ b/src/libslic3r/AABBTreeIndirect.hpp @@ -229,7 +229,7 @@ public: m_bbox(bbox.min - Point(SCALED_EPSILON, SCALED_EPSILON), bbox.max + Point(SCALED_EPSILON, SCALED_EPSILON)) {} size_t idx() const { return m_idx; } const BoundingBox& bbox() const { return m_bbox; } - Point centroid() const { return (m_bbox.min() + m_bbox.max() / 2); } + Point centroid() const { return (m_bbox.min() + m_bbox.max()) / 2; } private: size_t m_idx; BoundingBox m_bbox; diff --git a/src/libslic3r/AABBTreeLines.hpp b/src/libslic3r/AABBTreeLines.hpp index 97ad1bdf44..52c4cdf545 100644 --- a/src/libslic3r/AABBTreeLines.hpp +++ b/src/libslic3r/AABBTreeLines.hpp @@ -31,8 +31,9 @@ namespace AABBTreeLines { inline VectorType closest_point_to_origin(size_t primitive_index, ScalarType& squared_distance) const { Vec<LineType::Dim, typename LineType::Scalar> nearest_point; + Vec<LineType::Dim, typename LineType::Scalar> cast_origin = origin.template cast<typename LineType::Scalar>(); const LineType& line = lines[primitive_index]; - squared_distance = line_alg::distance_to_squared(line, origin.template cast<typename LineType::Scalar>(), &nearest_point); + squared_distance = line_alg::distance_to_squared(line, cast_origin, &nearest_point); return nearest_point.template cast<ScalarType>(); } }; diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 159d9bbeda..29eb96aa5b 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -280,6 +280,9 @@ void AppConfig::set_defaults() set(SETTING_OPENGL_FPS_CAP, std::to_string(fps_cap)); } + // The getter already defaults, parses and clamps; write back what it resolves to. + set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(get_plugin_pages_visible_count())); + if (get(SETTING_OPENGL_SHOW_FPS_OVERLAY).empty()) set_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY, false); @@ -626,6 +629,12 @@ void AppConfig::set_defaults() set_bool("window_buttons_on_left", false); #endif + if (get("use_printer_agents").empty()) + { + // false = legacy behavior using print hosts + set_bool("use_printer_agents", false); + } + // Remove legacy window positions/sizes erase("app", "main_frame_maximized"); erase("app", "main_frame_pos"); @@ -847,6 +856,10 @@ std::string AppConfig::load() local_machine.dev_ip = p["dev_ip"].get<std::string>(); if (p.contains("printer_type")) local_machine.printer_type = p["printer_type"].get<std::string>(); + if (p.contains("printer_agent_id")) + local_machine.printer_agent_id = p["printer_agent_id"].get<std::string>(); + if (p.contains("access_code")) + local_machine.access_code = p["access_code"].get<std::string>(); m_local_machines[local_machine.dev_id] = local_machine; } } else { @@ -877,7 +890,7 @@ std::string AppConfig::load() } } } - } catch(std::exception err) { + } catch(const std::exception &err) { BOOST_LOG_TRIVIAL(info) << format("parse app config \"%1%\", error: %2%", AppConfig::loading_path(), err.what()); return err.what(); @@ -1059,6 +1072,8 @@ void AppConfig::save() m_json["dev_name"] = local_machine.second.dev_name; m_json["dev_ip"] = local_machine.second.dev_ip; m_json["printer_type"] = local_machine.second.printer_type; + m_json["printer_agent_id"] = local_machine.second.printer_agent_id; + m_json["access_code"] = local_machine.second.access_code; j["local_machines"][local_machine.first] = m_json; } @@ -1425,6 +1440,7 @@ void AppConfig::set_mouse_device(const std::string& name, double translation_spe it->second["invert_yaw"] = invert_yaw ? "1" : "0"; it->second["invert_pitch"] = invert_pitch ? "1" : "0"; it->second["invert_roll"] = invert_roll ? "1" : "0"; + m_dirty = true; } std::vector<std::string> AppConfig::get_mouse_device_names() const @@ -1624,6 +1640,22 @@ void AppConfig::set_network_plugin_version(const std::string& version) set(SETTING_NETWORK_PLUGIN_VERSION, version); } +int AppConfig::get_plugin_pages_visible_count() const +{ + std::string value = get(SETTING_PLUGIN_PAGES_VISIBLE_COUNT); + if (value.empty()) + return PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT; + + int visible_count = PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT; + try { + visible_count = std::stoi(value); + } + catch (...) { + return PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT; + } + return std::clamp(visible_count, PLUGIN_PAGES_VISIBLE_COUNT_MIN, PLUGIN_PAGES_VISIBLE_COUNT_MAX); +} + std::vector<std::string> AppConfig::get_skipped_network_versions() const { std::vector<std::string> result; diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index 2c83ebb488..0a278f4f1f 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -41,6 +41,11 @@ using namespace nlohmann; #define SETTING_OPENGL_PHONG_SSAO "opengl_phong_ssao" #define SETTING_OPENGL_PHONG_SMOOTH_NORMALS "opengl_phong_smooth_normals" +#define SETTING_PLUGIN_PAGES_VISIBLE_COUNT "plugin_pages_visible_count" +#define PLUGIN_PAGES_VISIBLE_COUNT_MIN 1 +#define PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT 5 +#define PLUGIN_PAGES_VISIBLE_COUNT_MAX 10 + #if defined(_WIN32) || defined(_WIN64) #define BAMBU_NETWORK_AGENT_VERSION_LEGACY "01.10.01.09" #else @@ -61,10 +66,19 @@ struct BBLocalMachine std::string dev_ip; std::string dev_id; /* serial number */ std::string printer_type; /* model_id */ + std::string printer_agent_id; /* id of the IPrinterAgent that discovered/bound this device, e.g. "bbl"; empty for entries persisted before this field existed */ + // Access code, scoped to printer_agent_id above - so a code saved while bound under one + // printer agent isn't treated as valid for a different, independent agent talking to the + // same physical dev_id. Empty for entries persisted before this field existed; those fall + // back to the legacy flat "access_code"/"user_access_code" AppConfig sections (BBL-only, + // since BBL was the only agent when they were saved) - see + // get_access_code_with_legacy_fallback() in DevManager.cpp. + std::string access_code; bool operator==(const BBLocalMachine& other) const { - return dev_name == other.dev_name && dev_ip == other.dev_ip && dev_id == other.dev_id && printer_type == other.printer_type; + return dev_name == other.dev_name && dev_ip == other.dev_ip && dev_id == other.dev_id && printer_type == other.printer_type && + printer_agent_id == other.printer_agent_id && access_code == other.access_code; } bool operator!=(const BBLocalMachine& other) const { return !operator==(other); } }; @@ -374,6 +388,10 @@ public: std::string get_network_plugin_version() const; void set_network_plugin_version(const std::string& version); + // Number of plugin pages shown as fixed tabs before the rest are collapsed into a + // dropdown on the last tab. + int get_plugin_pages_visible_count() const; + std::vector<std::string> get_skipped_network_versions() const; void add_skipped_network_version(const std::string& version); bool is_network_version_skipped(const std::string& version) const; diff --git a/src/libslic3r/Arachne/WallToolPaths.cpp b/src/libslic3r/Arachne/WallToolPaths.cpp index 0a59619560..724016bcb1 100644 --- a/src/libslic3r/Arachne/WallToolPaths.cpp +++ b/src/libslic3r/Arachne/WallToolPaths.cpp @@ -154,8 +154,8 @@ void simplify(Polygon &thiss, const int64_t smallest_line_segment_squared, const //h^2 = L^2 / b^2 [factor the divisor] const int64_t height_2 = double(area_removed_so_far) * double(area_removed_so_far) / double(base_length_2); // Orca: The value of `height_2` is squared, so we need to compare it with the squared value - if ((height_2 <= Slic3r::sqr(scaled<coord_t>(0.005)) //Almost exactly colinear (barring rounding errors). - && Line::distance_to_infinite(current, previous, next) <= scaled<double>(0.005))) // make sure that height_2 is not small because of cancellation of positive and negative areas + if ((height_2 <= Slic3r::sqr(colinear_vertex_tolerance()) //Almost exactly colinear (barring rounding errors). + && Line::distance_to_infinite(current, previous, next) <= double(colinear_vertex_tolerance()))) // make sure that height_2 is not small because of cancellation of positive and negative areas continue; if (length2 < smallest_line_segment_squared diff --git a/src/libslic3r/Arachne/WallToolPaths.hpp b/src/libslic3r/Arachne/WallToolPaths.hpp index dde3dc785f..d7120a088c 100644 --- a/src/libslic3r/Arachne/WallToolPaths.hpp +++ b/src/libslic3r/Arachne/WallToolPaths.hpp @@ -23,14 +23,14 @@ inline coord_t meshfix_maximum_extrusion_area_deviation() { return scaled<coo class WallToolPathsParams { public: - float min_bead_width; - float min_feature_size; - float min_length_factor; - float wall_transition_length; - float wall_transition_angle; - float wall_transition_filter_deviation; - int wall_distribution_count; - bool is_top_or_bottom_layer; + float min_bead_width = 0.f; + float min_feature_size = 0.f; + float min_length_factor = 0.5f; + float wall_transition_length = 0.f; + float wall_transition_angle = 10.f; + float wall_transition_filter_deviation = 0.f; + int wall_distribution_count = 1; + bool is_top_or_bottom_layer = false; coord_t wall_maximum_resolution = meshfix_maximum_resolution(); coord_t wall_maximum_deviation = meshfix_maximum_deviation(); diff --git a/src/libslic3r/Arachne/utils/ExtrusionLine.cpp b/src/libslic3r/Arachne/utils/ExtrusionLine.cpp index eebd5d5d1c..66bb707ebe 100644 --- a/src/libslic3r/Arachne/utils/ExtrusionLine.cpp +++ b/src/libslic3r/Arachne/utils/ExtrusionLine.cpp @@ -133,8 +133,8 @@ void ExtrusionLine::simplify(const int64_t smallest_line_segment_squared, const const auto height_2 = int64_t(double(area_removed_so_far) * double(area_removed_so_far) / double(base_length_2)); const int64_t extrusion_area_error = calculateExtrusionAreaDeviationError(previous, current, next); // Orca: The value of `height_2` is squared, so we need to compare it with the squared value - if ((height_2 <= Slic3r::sqr(scaled<coord_t>(0.005)) // Almost exactly colinear (barring rounding errors). - && Line::distance_to_infinite(current.p, previous.p, next.p) <= scaled<double>(0.005)) // Make sure that height_2 is not small because of cancellation of positive and negative areas + if ((height_2 <= Slic3r::sqr(colinear_vertex_tolerance()) // Almost exactly colinear (barring rounding errors). + && Line::distance_to_infinite(current.p, previous.p, next.p) <= double(colinear_vertex_tolerance())) // Make sure that height_2 is not small because of cancellation of positive and negative areas // We shouldn't remove middle junctions of colinear segments if the area changed for the C-P segment is exceeding the maximum allowed && extrusion_area_error <= maximum_extrusion_area_deviation) { diff --git a/src/libslic3r/Arachne/utils/ExtrusionLine.hpp b/src/libslic3r/Arachne/utils/ExtrusionLine.hpp index 21791000f0..72e008cef1 100644 --- a/src/libslic3r/Arachne/utils/ExtrusionLine.hpp +++ b/src/libslic3r/Arachne/utils/ExtrusionLine.hpp @@ -32,6 +32,14 @@ class Flow; namespace Slic3r::Arachne { +// ORCA: Tolerance of the "almost exactly colinear" early-out shared by the two simplify() passes +// (this file and WallToolPaths.cpp). That test drops a vertex regardless of the user's Maximum wall +// resolution/deviation, so it has to stay at the scale of coordinate rounding noise. A larger value +// silently decimates finely tessellated curves: on a circle, one vertex may be removed whenever the +// sagitta of the resulting chord falls below the tolerance, which halves the point count and turns +// smooth arcs into corners the firmware has to decelerate through. +inline coord_t colinear_vertex_tolerance() { return coord_t(SCALED_EPSILON); } + /*! * Represents a polyline (not just a line) that is to be extruded with variable * line width. diff --git a/src/libslic3r/ArcFitter.cpp b/src/libslic3r/ArcFitter.cpp index cdfd708b10..46dd12931e 100644 --- a/src/libslic3r/ArcFitter.cpp +++ b/src/libslic3r/ArcFitter.cpp @@ -57,24 +57,24 @@ void ArcFitter::do_arc_fitting(const Points& points, std::vector<PathFittingData //BBS: can be fit as arc, then save arc data temperarily last_arc = target_arc; if (back_index == points.size() - 1) { - result.emplace_back(std::move(PathFittingData{ front_index, + result.emplace_back(PathFittingData{ front_index, back_index, last_arc.direction == ArcDirection::Arc_Dir_CCW ? EMovePathType::Arc_move_ccw : EMovePathType::Arc_move_cw, - last_arc })); + last_arc }); front_index = back_index; } } else { if (back_index - front_index > 2) { //BBS: althought current point_stack can't be fit as arc, //but previous must can be fit if removing the top in stack, so save last arc - result.emplace_back(std::move(PathFittingData{ front_index, + result.emplace_back(PathFittingData{ front_index, back_index - 1, last_arc.direction == ArcDirection::Arc_Dir_CCW ? EMovePathType::Arc_move_ccw : EMovePathType::Arc_move_cw, - last_arc })); + last_arc }); } else { //BBS: save the first segment as line move when 3 point-line can't be fit as arc move if (result.empty() || result.back().path_type != EMovePathType::Linear_move) - result.emplace_back(std::move(PathFittingData{front_index, front_index + 1, EMovePathType::Linear_move, ArcSegment()})); + result.emplace_back(PathFittingData{front_index, front_index + 1, EMovePathType::Linear_move, ArcSegment()}); else if(result.back().path_type == EMovePathType::Linear_move) result.back().end_point_index = front_index + 1; } @@ -87,7 +87,7 @@ void ArcFitter::do_arc_fitting(const Points& points, std::vector<PathFittingData //BBS: handle the remain data if (front_index != back_index) { if (result.empty() || result.back().path_type != EMovePathType::Linear_move) - result.emplace_back(std::move(PathFittingData{front_index, back_index, EMovePathType::Linear_move, ArcSegment()})); + result.emplace_back(PathFittingData{front_index, back_index, EMovePathType::Linear_move, ArcSegment()}); else if (result.back().path_type == EMovePathType::Linear_move) result.back().end_point_index = back_index; } diff --git a/src/libslic3r/Arrange.cpp b/src/libslic3r/Arrange.cpp index 48e6950a82..fd496537f4 100644 --- a/src/libslic3r/Arrange.cpp +++ b/src/libslic3r/Arrange.cpp @@ -797,7 +797,7 @@ public: } }); - m_pck.unfitIndicator([this](std::string name) { + m_pck.unfitIndicator([](std::string name) { BOOST_LOG_TRIVIAL(debug) << "arrange progress: " + name; }); diff --git a/src/libslic3r/BoundingBox.cpp b/src/libslic3r/BoundingBox.cpp index a2a510b64c..cf5441dace 100644 --- a/src/libslic3r/BoundingBox.cpp +++ b/src/libslic3r/BoundingBox.cpp @@ -8,6 +8,8 @@ namespace Slic3r { template BoundingBoxBase<Point, Points>::BoundingBoxBase(const Points &points); +template void BoundingBoxBase<Point, Points>::construct<0, BoundingBox, Points::const_iterator>(BoundingBox&, Points::const_iterator, Points::const_iterator); +template void BoundingBoxBase<Point, Points>::construct<1, BoundingBox, Points::const_iterator>(BoundingBox&, Points::const_iterator, Points::const_iterator); template BoundingBoxBase<Vec2d>::BoundingBoxBase(const std::vector<Vec2d> &points); template BoundingBox3Base<Vec3d>::BoundingBox3Base(const std::vector<Vec3d> &points); diff --git a/src/libslic3r/BuildVolume.cpp b/src/libslic3r/BuildVolume.cpp index 1ac200d9c8..15e6668bae 100644 --- a/src/libslic3r/BuildVolume.cpp +++ b/src/libslic3r/BuildVolume.cpp @@ -13,7 +13,6 @@ BuildVolume::BuildVolume(const std::vector<Vec2d> &printable_area, const double : m_bed_shape(printable_area), m_max_print_height(printable_height), m_extruder_shapes(extruder_areas), m_extruder_printable_height(extruder_printable_heights) { assert(printable_height >= 0); - //assert(extruder_printable_heights.size() == extruder_areas.size()); m_polygon = Polygon::new_scale(printable_area); assert(m_polygon.is_counter_clockwise()); @@ -86,6 +85,9 @@ BuildVolume::BuildVolume(const std::vector<Vec2d> &printable_area, const double m_shared_volume.data[2] = m_bboxf.max.x(); m_shared_volume.data[3] = m_bboxf.max.y(); m_shared_volume.zs[1] = m_bboxf.max.z(); + if (extruder_printable_heights.size() < m_extruder_shapes.size()) + BOOST_LOG_TRIVIAL(warning) << boost::format("extruder_printable_height has only %1% entries but extruder_printable_area has %2%, falling back to the bed printable_height for the missing ones") + % extruder_printable_heights.size() % m_extruder_shapes.size(); for (unsigned int index = 0; index < m_extruder_shapes.size(); index++) { std::vector<Vec2d>& extruder_shape = m_extruder_shapes[index]; @@ -100,7 +102,9 @@ BuildVolume::BuildVolume(const std::vector<Vec2d> &printable_area, const double return; } - if ((extruder_shape == printable_area)&&(extruder_printable_heights[index] == printable_height)) { + const double extruder_height = index < extruder_printable_heights.size() ? extruder_printable_heights[index] : printable_height; + + if ((extruder_shape == printable_area)&&(extruder_height == printable_height)) { extruder_volume.same_with_bed = true; extruder_volume.type = m_type; extruder_volume.bbox = m_bbox; @@ -113,7 +117,7 @@ BuildVolume::BuildVolume(const std::vector<Vec2d> &printable_area, const double double poly_area = poly.area(); extruder_volume.bbox = get_extents(poly); BoundingBoxf temp_bboxf = get_extents(extruder_shape); - extruder_volume.bboxf = BoundingBoxf3{ to_3d(temp_bboxf.min, 0.), to_3d(temp_bboxf.max, extruder_printable_heights[index]) }; + extruder_volume.bboxf = BoundingBoxf3{ to_3d(temp_bboxf.min, 0.), to_3d(temp_bboxf.max, extruder_height) }; if (extruder_shape.size() >= 4 && std::abs((poly_area - double(extruder_volume.bbox.size().x()) * double(extruder_volume.bbox.size().y()))) < sqr(SCALED_EPSILON)) { diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 812d28e088..2880a3cc6b 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -149,6 +149,8 @@ set(lisbslic3r_sources Fill/FillConcentric.hpp Fill/FillConcentricInternal.cpp Fill/FillConcentricInternal.hpp + Fill/FillCornerSmoothing.cpp + Fill/FillCornerSmoothing.hpp Fill/Fill.cpp Fill/FillCrossHatch.cpp Fill/FillCrossHatch.hpp @@ -177,6 +179,17 @@ set(lisbslic3r_sources Fill/Lightning/Layer.hpp Fill/Lightning/TreeNode.cpp Fill/Lightning/TreeNode.hpp + FilamentMixer.cpp + FilamentMixer.hpp + FilamentMixerModel.hpp + ColorDecomposeRecipe.cpp + ColorDecomposeRecipe.hpp + TexturePainting.hpp + TexturePainting.cpp + TextureToColor/TextureToColor.hpp + TextureToColor/TextureToColor.cpp + TextureToColor/ColorUtils.hpp + TextureToColor/ColorUtils.cpp Flow.cpp Flow.hpp FlushVolCalc.cpp @@ -192,6 +205,9 @@ set(lisbslic3r_sources format.hpp Format/OBJ.cpp Format/OBJ.hpp + Format/AssimpImport.hpp + Format/AssimpImport.cpp + Format/ResourcePathUtils.hpp Format/objparser.cpp Format/objparser.hpp Format/SL1.cpp @@ -346,6 +362,8 @@ set(lisbslic3r_sources Polyline.hpp PresetBundle.cpp PresetBundle.hpp + PresetCacheFormat.cpp + PresetCacheFormat.hpp Preset.cpp Preset.hpp PrincipalComponents2D.cpp @@ -505,6 +523,7 @@ cmake_policy(SET CMP0011 NEW) set(CMAKE_POLICY_DEFAULT_CMP0167 NEW) find_package(CGAL REQUIRED) find_package(OpenCV REQUIRED core) +find_package(assimp REQUIRED) unset(CMAKE_POLICY_DEFAULT_CMP0167) cmake_policy(POP) @@ -545,7 +564,7 @@ target_compile_definitions(libslic3r PUBLIC -DUSE_TBB -DTBB_USE_CAPTURED_EXCEPTI if (USE_SLIC3R_CONSOLE_LOG) target_compile_definitions(libslic3r PRIVATE $<$<CONFIG:RelWithDebInfo>:SLIC3R_CONSOLE_LOG>) endif() -target_include_directories(libslic3r PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(libslic3r PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/TextureToColor PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) target_include_directories(libslic3r SYSTEM PUBLIC ${EXPAT_INCLUDE_DIRS}) # Find the OCCT and related libraries @@ -593,6 +612,7 @@ target_link_libraries(libslic3r libnest2d miniz opencv_world + assimp::assimp PRIVATE ${CMAKE_DL_LIBS} ${EXPAT_LIBRARIES} diff --git a/src/libslic3r/Clipper2Utils.cpp b/src/libslic3r/Clipper2Utils.cpp index d389d25209..f119fc3924 100644 --- a/src/libslic3r/Clipper2Utils.cpp +++ b/src/libslic3r/Clipper2Utils.cpp @@ -13,8 +13,8 @@ Slic3r::Polylines Paths64_to_polylines(const Clipper2Lib::Paths64& in) Slic3r::Points points; points.reserve(path64.size()); for (const Clipper2Lib::Point64& point64 : path64) - points.emplace_back(std::move(Slic3r::Point(point64.x, point64.y))); - out.emplace_back(std::move(Slic3r::Polyline(points))); + points.emplace_back(Slic3r::Point(point64.x, point64.y)); + out.emplace_back(Slic3r::Polyline(points)); } return out; } @@ -29,7 +29,7 @@ Clipper2Lib::Paths64 Slic3rPoints_to_Paths64(const Container& in) Clipper2Lib::Path64 path; path.reserve(item.size()); for (const Slic3r::Point& point : item.points) - path.emplace_back(std::move(Clipper2Lib::Point64(point.x(), point.y()))); + path.emplace_back(Clipper2Lib::Point64(point.x(), point.y())); out.emplace_back(std::move(path)); } return out; @@ -44,7 +44,7 @@ Points Path64ToPoints(const Clipper2Lib::Path64& path64) { Points points; points.reserve(path64.size()); - for (const Clipper2Lib::Point64 &point64 : path64) points.emplace_back(std::move(Slic3r::Point(point64.x, point64.y))); + for (const Clipper2Lib::Point64 &point64 : path64) points.emplace_back(Slic3r::Point(point64.x, point64.y)); return points; } @@ -99,7 +99,7 @@ Clipper2Lib::Paths64 Slic3rPolygons_to_Paths64(const Polygons &in) for (const Polygon &poly : in) { Clipper2Lib::Path64 path; path.reserve(poly.points.size()); - for (const Slic3r::Point &point : poly.points) path.emplace_back(std::move(Clipper2Lib::Point64(point.x(), point.y()))); + for (const Slic3r::Point &point : poly.points) path.emplace_back(Clipper2Lib::Point64(point.x(), point.y())); out.emplace_back(std::move(path)); } return out; @@ -114,7 +114,7 @@ Clipper2Lib::Paths64 Slic3rExPolygons_to_Paths64(const ExPolygons& in) const auto &poly = expolygon.contour_or_hole(i); Clipper2Lib::Path64 path; path.reserve(poly.points.size()); - for (const Slic3r::Point &point : poly.points) path.emplace_back(std::move(Clipper2Lib::Point64(point.x(), point.y()))); + for (const Slic3r::Point &point : poly.points) path.emplace_back(Clipper2Lib::Point64(point.x(), point.y())); out.emplace_back(std::move(path)); } } @@ -134,8 +134,8 @@ Polylines _clipper2_pl_open(Clipper2Lib::ClipType clipType, const Slic3r::Polyli Slic3r::Polylines out; out.reserve(solution.size() + solution_open.size()); - polylines_append(out, std::move(Paths64_to_polylines(solution))); - polylines_append(out, std::move(Paths64_to_polylines(solution_open))); + polylines_append(out, Paths64_to_polylines(solution)); + polylines_append(out, Paths64_to_polylines(solution_open)); return out; } diff --git a/src/libslic3r/ColorDecomposeRecipe.cpp b/src/libslic3r/ColorDecomposeRecipe.cpp new file mode 100644 index 0000000000..f2ceb1860a --- /dev/null +++ b/src/libslic3r/ColorDecomposeRecipe.cpp @@ -0,0 +1,530 @@ +#include "ColorDecomposeRecipe.hpp" + +#include "FilamentMixer.hpp" +#include "Utils.hpp" +#include "nlohmann/json.hpp" + +#include <algorithm> +#include <cmath> +#include <cstdio> +#include <fstream> +#include <limits> +#include <utility> + +namespace Slic3r { +namespace { + +struct LabColor { + double l{0.0}; + double a{0.0}; + double b{0.0}; +}; + +struct StandardRecipeEntry { + ColorDecomposeRecipeMode mode{ColorDecomposeRecipeMode::CMYW}; + std::string material; + std::string source; + std::vector<std::string> component_keys; + std::vector<std::string> component_hexes; + std::vector<int> ratios; + std::string measured_hex; + LabColor measured_lab; +}; + +static double srgb_to_linear(double v) +{ + v /= 255.0; + return v <= 0.04045 ? v / 12.92 : std::pow((v + 0.055) / 1.055, 2.4); +} + +static double xyz_to_lab_component(double v) +{ + constexpr double eps = 216.0 / 24389.0; + constexpr double kappa = 24389.0 / 27.0; + return v > eps ? std::cbrt(v) : (kappa * v + 16.0) / 116.0; +} + +static LabColor rgb_to_lab(const ColorDecomposeRgb& rgb) +{ + const double r = srgb_to_linear(rgb.r); + const double g = srgb_to_linear(rgb.g); + const double b = srgb_to_linear(rgb.b); + + const double x = (0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / 0.95047; + const double y = (0.2126729 * r + 0.7151522 * g + 0.0721750 * b); + const double z = (0.0193339 * r + 0.1191920 * g + 0.9503041 * b) / 1.08883; + + const double fx = xyz_to_lab_component(x); + const double fy = xyz_to_lab_component(y); + const double fz = xyz_to_lab_component(z); + + return {116.0 * fy - 16.0, 500.0 * (fx - fy), 200.0 * (fy - fz)}; +} + +static std::string lab_to_srgb_hex(const LabColor& lab) +{ + constexpr double Xn = 0.95047, Yn = 1.0, Zn = 1.08883; + + auto f_inv = [](double t) -> double { + constexpr double eps = 216.0 / 24389.0; + constexpr double kappa = 24389.0 / 27.0; + const double t3 = t * t * t; + return t3 > eps ? t3 : (t * 116.0 - 16.0) / kappa; + }; + + const double fy = (lab.l + 16.0) / 116.0; + const double fx = lab.a / 500.0 + fy; + const double fz = fy - lab.b / 200.0; + + const double X = Xn * f_inv(fx); + const double Y = Yn * f_inv(fy); + const double Z = Zn * f_inv(fz); + + double r = 3.2406 * X - 1.5372 * Y - 0.4986 * Z; + double g = -0.9689 * X + 1.8758 * Y + 0.0415 * Z; + double b = 0.0557 * X - 0.2040 * Y + 1.0570 * Z; + + auto gamma = [](double c) -> double { + c = std::max(0.0, std::min(1.0, c)); + return c <= 0.0031308 ? 12.92 * c : 1.055 * std::pow(c, 1.0 / 2.4) - 0.055; + }; + auto u8 = [&](double c) -> int { + return std::max(0, std::min(255, static_cast<int>(std::lround(gamma(c) * 255.0)))); + }; + + char buf[8]; + std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", u8(r), u8(g), u8(b)); + return std::string(buf); +} + +static double delta_e76(const LabColor& a, const LabColor& b) +{ + return std::sqrt(std::pow(a.l - b.l, 2.0) + std::pow(a.a - b.a, 2.0) + std::pow(a.b - b.b, 2.0)); +} + +static bool material_matches(const std::string& a, const std::string& b) +{ + if (a.empty() || b.empty()) + return false; + return a == b || a == b + " Basic" || b == a + " Basic"; +} + +static std::vector<std::vector<int>> ratio_grid(size_t n) +{ + std::vector<std::vector<int>> out; + if (n == 2) { + for (int a = 20; a <= 80; a += 5) + out.push_back({a, 100 - a}); + } else if (n == 3) { + for (int a = 20; a <= 60; a += 5) + for (int b = 20; b <= 80 - a; b += 5) { + const int c = 100 - a - b; + if (c >= 20) + out.push_back({a, b, c}); + } + } + return out; +} + +static ColorDecomposeRecipeMode parse_mode(const std::string& s) +{ + if (s == "RYBW" || s == "RGBY") + return ColorDecomposeRecipeMode::RYBW; + return ColorDecomposeRecipeMode::CMYW; +} + +static std::vector<StandardRecipeEntry> load_standard_entries() +{ + std::vector<StandardRecipeEntry> entries; + const std::string path = resources_dir() + "/filament_mixing/standard_color_recipes.json"; + std::ifstream ifs(path); + if (!ifs) + return entries; + + nlohmann::json root = nlohmann::json::parse(ifs, nullptr, false); + if (root.is_discarded() || !root.contains("entries") || !root["entries"].is_array()) + return entries; + + for (const auto& item : root["entries"]) { + if (!item.is_object()) + continue; + StandardRecipeEntry entry; + entry.mode = parse_mode(item.value("mode", "CMYW")); + entry.material = item.value("material", ""); + entry.source = item.value("source", ""); + entry.measured_hex = item.value("measured_rgb", ""); + + if (item.contains("components") && item["components"].is_array()) { + for (const auto& comp : item["components"]) { + if (comp.is_object()) { + entry.component_keys.push_back(comp.value("key", "")); + entry.component_hexes.push_back(comp.value("rgb", "")); + } + } + } + if (item.contains("ratios") && item["ratios"].is_array()) { + for (const auto& ratio : item["ratios"]) { + if (ratio.is_number_integer()) + entry.ratios.push_back(ratio.get<int>()); + } + } + if (item.contains("measured_lab") && item["measured_lab"].is_array() && item["measured_lab"].size() >= 3) { + entry.measured_lab = { + item["measured_lab"][0].get<double>(), + item["measured_lab"][1].get<double>(), + item["measured_lab"][2].get<double>() + }; + } else { + ColorDecomposeRgb measured_rgb; + if (!color_decompose_hex_to_rgb(entry.measured_hex, measured_rgb)) + continue; + entry.measured_lab = rgb_to_lab(measured_rgb); + } + + if (entry.component_hexes.size() >= 2 && entry.component_hexes.size() == entry.ratios.size() && + !entry.measured_hex.empty()) + entries.push_back(std::move(entry)); + } + return entries; +} + +static const std::vector<StandardRecipeEntry>& standard_entries() +{ + static const std::vector<StandardRecipeEntry> entries = load_standard_entries(); + return entries; +} + +static void evaluate_candidate(const ColorDecomposeRgb& target, + const std::vector<std::string>& hexes, + const std::vector<int>& ratios, + const std::vector<unsigned int>& indices, + ColorDecomposeRecipeMode mode, + double& best_score, + ColorDecomposeRecipeResult& best) +{ + const std::string mixed = blend_color_multi(hexes, ratios); + ColorDecomposeRgb mixed_rgb; + if (!color_decompose_hex_to_rgb(mixed, mixed_rgb)) + return; + + const double score = delta_e76(rgb_to_lab(target), rgb_to_lab(mixed_rgb)); + if (score >= best_score) + return; + + best_score = score; + best.valid = true; + best.mode = mode; + best.matched_color_hex = mixed; + best.components.clear(); + for (size_t i = 0; i < hexes.size(); ++i) { + ColorDecomposeRecipeComponent comp; + comp.color_hex = hexes[i]; + comp.ratio = ratios[i]; + comp.filament_index = i < indices.size() ? indices[i] : 0; + best.components.push_back(comp); + } +} + +} // namespace + +std::string color_decompose_rgb_to_hex(const ColorDecomposeRgb& rgb) +{ + char buf[8]; + std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", rgb.r, rgb.g, rgb.b); + return std::string(buf); +} + +bool color_decompose_hex_to_rgb(const std::string& hex, ColorDecomposeRgb& out) +{ + if (hex.size() < 7 || hex[0] != '#') + return false; + unsigned r = 0, g = 0, b = 0; + if (std::sscanf(hex.c_str(), "#%02x%02x%02x", &r, &g, &b) != 3) + return false; + out = {static_cast<unsigned char>(r), static_cast<unsigned char>(g), static_cast<unsigned char>(b)}; + return true; +} + +ColorDecomposeRecipeResult recommend_from_physical_filaments( + const ColorDecomposeRgb& target, + const std::vector<ColorDecomposePhysicalFilament>& physical_filaments, + const std::string& preferred_material_type) +{ + std::vector<ColorDecomposePhysicalFilament> candidates; + for (const auto& filament : physical_filaments) { + if (filament.is_mixed) + continue; + ColorDecomposeRgb ignored; + if (!color_decompose_hex_to_rgb(filament.color_hex, ignored)) + continue; + if (preferred_material_type.empty() || material_matches(filament.type, preferred_material_type)) + candidates.push_back(filament); + } + + // Early exit: if a material-matched candidate has the exact target color, + // return it as 100%. Downstream rejects single-component results (no mixed + // slot created), which is correct -- the color already exists. + const std::string target_hex = color_decompose_rgb_to_hex(target); + for (const auto& cand : candidates) { + ColorDecomposeRgb cand_rgb; + if (!color_decompose_hex_to_rgb(cand.color_hex, cand_rgb)) + continue; + if (color_decompose_rgb_to_hex(cand_rgb) == target_hex) { + ColorDecomposeRecipeResult exact; + exact.valid = true; + exact.mode = ColorDecomposeRecipeMode::MaterialList; + exact.matched_color_hex = cand.color_hex; + ColorDecomposeRecipeComponent comp; + comp.color_hex = cand.color_hex; + comp.ratio = 100; + comp.filament_index = cand.filament_index; + exact.components.push_back(comp); + return exact; + } + } + + if (candidates.size() < 2) + candidates = physical_filaments; + candidates.erase(std::remove_if(candidates.begin(), candidates.end(), [](const auto& filament) { + if (filament.is_mixed) + return true; + ColorDecomposeRgb ignored; + return !color_decompose_hex_to_rgb(filament.color_hex, ignored); + }), candidates.end()); + + constexpr size_t kMaxCandidates = 8; + if (candidates.size() > kMaxCandidates) { + const LabColor target_lab = rgb_to_lab(target); + std::sort(candidates.begin(), candidates.end(), + [&target_lab](const ColorDecomposePhysicalFilament& a, const ColorDecomposePhysicalFilament& b) { + ColorDecomposeRgb rgb_a, rgb_b; + color_decompose_hex_to_rgb(a.color_hex, rgb_a); + color_decompose_hex_to_rgb(b.color_hex, rgb_b); + return delta_e76(target_lab, rgb_to_lab(rgb_a)) + < delta_e76(target_lab, rgb_to_lab(rgb_b)); + }); + candidates.resize(kMaxCandidates); + } + + ColorDecomposeRecipeResult best; + double best_score = std::numeric_limits<double>::max(); + + for (size_t i = 0; i < candidates.size(); ++i) { + for (size_t j = i + 1; j < candidates.size(); ++j) { + const std::vector<std::string> hexes = {candidates[i].color_hex, candidates[j].color_hex}; + const std::vector<unsigned int> indices = {candidates[i].filament_index, candidates[j].filament_index}; + for (const auto& ratios : ratio_grid(2)) + evaluate_candidate(target, hexes, ratios, indices, ColorDecomposeRecipeMode::MaterialList, best_score, best); + + for (size_t k = j + 1; k < candidates.size(); ++k) { + const std::vector<std::string> hexes3 = {candidates[i].color_hex, candidates[j].color_hex, candidates[k].color_hex}; + const std::vector<unsigned int> indices3 = {candidates[i].filament_index, candidates[j].filament_index, candidates[k].filament_index}; + for (const auto& ratios : ratio_grid(3)) + evaluate_candidate(target, hexes3, ratios, indices3, ColorDecomposeRecipeMode::MaterialList, best_score, best); + } + } + } + + return best; +} + +ColorDecomposeRecipeResult lookup_standard_recipe( + const ColorDecomposeRgb& target, + ColorDecomposeRecipeMode mode, + const std::string& preferred_material_type) +{ + const LabColor target_lab = rgb_to_lab(target); + ColorDecomposeRecipeResult best; + double best_score = std::numeric_limits<double>::max(); + + auto consider = [&](bool require_material_match) { + for (const StandardRecipeEntry& entry : standard_entries()) { + if (entry.mode != mode) + continue; + if (require_material_match && !material_matches(entry.material, preferred_material_type)) + continue; + if (!require_material_match && !preferred_material_type.empty() && material_matches(entry.material, preferred_material_type)) + continue; + + const double score = delta_e76(target_lab, entry.measured_lab); + if (score >= best_score) + continue; + + best_score = score; + best.valid = true; + best.mode = mode; + best.matched_color_hex = entry.measured_hex; + best.components.clear(); + for (size_t i = 0; i < entry.component_hexes.size(); ++i) { + ColorDecomposeRecipeComponent comp; + comp.color_hex = entry.component_hexes[i]; + comp.base_color = i < entry.component_keys.size() ? entry.component_keys[i] : ""; + comp.ratio = entry.ratios[i]; + comp.filament_index = 0; + best.components.push_back(comp); + } + } + }; + + consider(true); + if (!best.valid) + consider(false); + return best; +} + +std::string lookup_measured_blend_color(const std::vector<std::string>& component_hexes, + const std::vector<int>& ratios) +{ + if (component_hexes.size() < 2 || component_hexes.size() != ratios.size()) + return {}; + + auto normalize_hex = [](const std::string& hex) -> std::string { + ColorDecomposeRgb rgb; + if (!color_decompose_hex_to_rgb(hex, rgb)) + return {}; + char buf[8]; + std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", rgb.r, rgb.g, rgb.b); + return std::string(buf); + }; + + // Stage 1: canonicalize input by sorting (hex, ratio) pairs so matching + // is independent of the caller's component order. + const size_t n = component_hexes.size(); + std::vector<std::pair<std::string, int>> in_pairs; + in_pairs.reserve(n); + for (size_t i = 0; i < n; ++i) { + std::string nh = normalize_hex(component_hexes[i]); + if (nh.empty()) + return {}; + in_pairs.emplace_back(std::move(nh), ratios[i]); + } + std::sort(in_pairs.begin(), in_pairs.end()); + + std::vector<std::string> in_hexes; + std::vector<int> in_ratios; + in_hexes.reserve(n); + in_ratios.reserve(n); + for (const auto& p : in_pairs) { + in_hexes.push_back(p.first); + in_ratios.push_back(p.second); + } + + // Normalize ratios to sum=100 (callers may pass arbitrary weights, + // e.g. MixedFilamentDialog uses ratio*10000). + { + int sum = 0; + for (int r : in_ratios) sum += r; + if (sum > 0 && sum != 100) { + int new_sum = 0; + for (size_t i = 0; i < in_ratios.size(); ++i) { + in_ratios[i] = static_cast<int>(std::lround( + static_cast<double>(in_ratios[i]) * 100.0 / static_cast<double>(sum))); + new_sum += in_ratios[i]; + } + if (new_sum != 100) { + auto it = std::max_element(in_ratios.begin(), in_ratios.end()); + *it += (100 - new_sum); + } + } + } + + // Fall back to polynomial model for ratios outside the measured range. + { + bool out_of_range = false; + if (n == 2) { + for (int r : in_ratios) + if (r < 20 || r > 80) { out_of_range = true; break; } + } else { + for (int r : in_ratios) + if (r < 20) { out_of_range = true; break; } + } + if (out_of_range) + return {}; + } + + // Stage 2: collect anchors with the same component hex set; try exact match. + struct Anchor { + std::vector<int> ratios; + LabColor lab; + std::string hex; + }; + std::vector<Anchor> anchors; + + for (const StandardRecipeEntry& entry : standard_entries()) { + if (entry.source != "measured" && entry.source != "interpolated") + continue; + if (entry.component_hexes.size() != n) + continue; + + std::vector<std::pair<std::string, int>> e_pairs; + e_pairs.reserve(n); + for (size_t i = 0; i < n; ++i) + e_pairs.emplace_back(normalize_hex(entry.component_hexes[i]), entry.ratios[i]); + std::sort(e_pairs.begin(), e_pairs.end()); + + bool same_set = true; + for (size_t i = 0; i < n; ++i) + if (e_pairs[i].first != in_hexes[i]) { same_set = false; break; } + if (!same_set) + continue; + + Anchor a; + a.ratios.reserve(n); + for (const auto& p : e_pairs) a.ratios.push_back(p.second); + a.lab = entry.measured_lab; + a.hex = entry.measured_hex; + + if (a.ratios == in_ratios) + return a.hex; + + anchors.push_back(std::move(a)); + } + + if (anchors.size() < 2) + return {}; + + // Stage 3: interpolation in Lab space. + if (n == 2) { + // 1D linear interpolation along ratio[0]. + std::sort(anchors.begin(), anchors.end(), + [](const Anchor& a, const Anchor& b) { return a.ratios[0] < b.ratios[0]; }); + const double x = static_cast<double>(in_ratios[0]); + size_t lo = 0; + while (lo + 2 < anchors.size() && static_cast<double>(anchors[lo + 1].ratios[0]) <= x) + ++lo; + const Anchor& a0 = anchors[lo]; + const Anchor& a1 = anchors[lo + 1]; + const double span = static_cast<double>(a1.ratios[0] - a0.ratios[0]); + const double t = span > 0.0 ? (x - static_cast<double>(a0.ratios[0])) / span : 0.0; + return lab_to_srgb_hex({a0.lab.l + t * (a1.lab.l - a0.lab.l), + a0.lab.a + t * (a1.lab.a - a0.lab.a), + a0.lab.b + t * (a1.lab.b - a0.lab.b)}); + } + + // 3+ color: IDW (p=2) with 3 nearest anchors in the (ratio[0], ratio[1]) plane. + const double ra = static_cast<double>(in_ratios[0]); + const double rb = static_cast<double>(in_ratios[1]); + std::vector<std::pair<double, const Anchor*>> dists; + dists.reserve(anchors.size()); + for (const Anchor& a : anchors) { + const double d = std::sqrt(std::pow(ra - static_cast<double>(a.ratios[0]), 2.0) + + std::pow(rb - static_cast<double>(a.ratios[1]), 2.0)); + if (d == 0.0) + return a.hex; + dists.emplace_back(d, &a); + } + const size_t k = std::min(static_cast<size_t>(3), dists.size()); + std::partial_sort(dists.begin(), dists.begin() + k, dists.end(), + [](const auto& a, const auto& b) { return a.first < b.first; }); + double num_l = 0.0, num_a = 0.0, num_b = 0.0, den = 0.0; + for (size_t j = 0; j < k; ++j) { + const double w = 1.0 / (dists[j].first * dists[j].first); + num_l += w * dists[j].second->lab.l; + num_a += w * dists[j].second->lab.a; + num_b += w * dists[j].second->lab.b; + den += w; + } + return lab_to_srgb_hex({num_l / den, num_a / den, num_b / den}); +} + +} // namespace Slic3r diff --git a/src/libslic3r/ColorDecomposeRecipe.hpp b/src/libslic3r/ColorDecomposeRecipe.hpp new file mode 100644 index 0000000000..146bf322a2 --- /dev/null +++ b/src/libslic3r/ColorDecomposeRecipe.hpp @@ -0,0 +1,64 @@ +#ifndef SLIC3R_COLOR_DECOMPOSE_RECIPE_HPP +#define SLIC3R_COLOR_DECOMPOSE_RECIPE_HPP + +#include <string> +#include <vector> + +namespace Slic3r { + +enum class ColorDecomposeRecipeMode { + MaterialList, + CMYW, + RYBW +}; + +struct ColorDecomposeRgb { + unsigned char r{0}; + unsigned char g{0}; + unsigned char b{0}; +}; + +struct ColorDecomposePhysicalFilament { + std::string color_hex; + std::string name; + std::string type; + bool is_mixed{false}; + unsigned int filament_index{0}; // 1-based physical filament index +}; + +struct ColorDecomposeRecipeComponent { + std::string color_hex; + std::string base_color; + int ratio{0}; + unsigned int filament_index{0}; // 1-based for physical filaments, 0 for standard base colors +}; + +struct ColorDecomposeRecipeResult { + bool valid{false}; + ColorDecomposeRecipeMode mode{ColorDecomposeRecipeMode::MaterialList}; + std::string matched_color_hex; + std::vector<ColorDecomposeRecipeComponent> components; +}; + +std::string color_decompose_rgb_to_hex(const ColorDecomposeRgb& rgb); +bool color_decompose_hex_to_rgb(const std::string& hex, ColorDecomposeRgb& out); + +ColorDecomposeRecipeResult recommend_from_physical_filaments( + const ColorDecomposeRgb& target, + const std::vector<ColorDecomposePhysicalFilament>& physical_filaments, + const std::string& preferred_material_type); + +ColorDecomposeRecipeResult lookup_standard_recipe( + const ColorDecomposeRgb& target, + ColorDecomposeRecipeMode mode, + const std::string& preferred_material_type); + +// Look up the measured blend color for an exact (component_hexes, ratios) match +// in the standard color recipe table. Returns the measured hex color if found +// with reliable source data ("measured" or "interpolated"), empty string otherwise. +std::string lookup_measured_blend_color(const std::vector<std::string>& component_hexes, + const std::vector<int>& ratios); + +} // namespace Slic3r + +#endif // SLIC3R_COLOR_DECOMPOSE_RECIPE_HPP diff --git a/src/libslic3r/Config.cpp b/src/libslic3r/Config.cpp index a43f659be6..a2756a9f8e 100644 --- a/src/libslic3r/Config.cpp +++ b/src/libslic3r/Config.cpp @@ -1715,6 +1715,36 @@ const ConfigOption* DynamicConfig::optptr(const t_config_option_key &opt_key) co return (it == options.end()) ? nullptr : it->second.get(); } +// ConfigOptionBool(s)::deserialize only understands "1" and "0", but scripts commonly spell CLI +// flags as --opt=true or --opt=no. Map the usual spellings onto what deserialize() accepts, per +// comma-separated item so vector options keep working, and pass anything else through unchanged +// so a genuine typo is still reported as invalid. +static std::string normalize_cli_bool_value(const std::string &value) +{ + static const char* true_values[] = { "1", "true", "yes", "on", "enabled" }; + static const char* false_values[] = { "0", "false", "no", "off", "disabled" }; + + auto matches = [](const std::string &item, const char* const* candidates, size_t count) { + return std::any_of(candidates, candidates + count, [&item](const char* candidate) { return boost::iequals(item, candidate); }); + }; + + std::string normalized; + std::istringstream is(value); + std::string item; + while (std::getline(is, item, ',')) { + boost::trim(item); + if (! normalized.empty()) + normalized += ","; + if (matches(item, true_values, std::size(true_values))) + normalized += "1"; + else if (matches(item, false_values, std::size(false_values))) + normalized += "0"; + else + normalized += item; + } + return normalized; +} + bool DynamicConfig::read_cli(int argc, const char* const argv[], t_config_option_keys* extra, t_config_option_keys* keys) { // cache the CLI option => opt_key mapping @@ -1812,17 +1842,32 @@ bool DynamicConfig::read_cli(int argc, const char* const argv[], t_config_option // to the end of the value. if (opt_base->type() == coBools && value.empty()) static_cast<ConfigOptionBools*>(opt_base)->values.push_back(!no); - else + else { // Deserialize any other vector value (ConfigOptionInts, Floats, Percents, Points) the same way // they get deserialized from an .ini file. For ConfigOptionStrings, that means that the C-style unescape // will be applied for values enclosed in quotes, while values non-enclosed in quotes are left to be // unescaped by the calling shell. - opt_vector->deserialize(value, true); + const std::string vector_value = opt_base->type() == coBools ? normalize_cli_bool_value(value) : value; + bool deserialized = false; + try { + deserialized = opt_vector->deserialize(vector_value, true); + } catch (const std::exception &ex) { + // e.g. "nil" deserialized into a non-nullable vector option throws instead of + // returning false - treat that the same as any other invalid value here. + deserialized = false; + } + if (! deserialized) { + boost::nowide::cerr << "Invalid value for option --" << token.c_str() << std::endl; + return false; + } + } } else if (opt_base->type() == coBool) { if (value.empty()) static_cast<ConfigOptionBool*>(opt_base)->value = !no; - else - opt_base->deserialize(value); + else if (! opt_base->deserialize(normalize_cli_bool_value(value))) { + boost::nowide::cerr << "Invalid value for option --" << token.c_str() << std::endl; + return false; + } } else if (opt_base->type() == coString) { // Do not unescape single string values, the unescaping is left to the calling shell. static_cast<ConfigOptionString*>(opt_base)->value = value; @@ -2031,7 +2076,8 @@ const double& DynamicConfig::opt_float(const t_config_option_key &opt_key, unsig return opt_floats_nullable->get_at(idx); } else { assert(false); - return 0; + static const double zero = 0.0; + return zero; } } diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index 6f4117d249..9e4344820d 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -28,6 +28,9 @@ #include <cereal/access.hpp> #include <cereal/types/base_class.hpp> +// The serialize() members below archive ConfigOption hierarchies through +// cereal::base_class, whose registration machinery lives in polymorphic.hpp. +#include <cereal/types/polymorphic.hpp> namespace Slic3r { struct FloatOrPercent @@ -2273,6 +2276,8 @@ public: plugin_picker, // Raw JSON string value, edited through a dialog behind a button rather than in the row. plugin_config, + // PrinterAgentChoice + printer_agent_select, }; // Identifier of this option. It is stored here so that it is accessible through the by_serialization_key_ordinal map. @@ -2980,6 +2985,8 @@ public: const double & opt_float(const t_config_option_key &opt_key, unsigned int idx) const; double & opt_float_nullable(const t_config_option_key &opt_key, unsigned int idx) { return this->option<ConfigOptionFloatsNullable>(opt_key)->get_at(idx); } const double & opt_float_nullable(const t_config_option_key &opt_key, unsigned int idx) const { return dynamic_cast<const ConfigOptionFloatsNullable *>(this->option(opt_key))->get_at(idx); } + FloatOrPercent & opt_float_or_percent_nullable(const t_config_option_key &opt_key, unsigned int idx) { return this->option<ConfigOptionFloatsOrPercentsNullable>(opt_key)->get_at(idx); } + const FloatOrPercent & opt_float_or_percent_nullable(const t_config_option_key &opt_key, unsigned int idx) const { return dynamic_cast<const ConfigOptionFloatsOrPercentsNullable *>(this->option(opt_key))->get_at(idx); } int& opt_int(const t_config_option_key &opt_key) { return this->option<ConfigOptionInt>(opt_key)->value; } int opt_int(const t_config_option_key &opt_key) const { return dynamic_cast<const ConfigOptionInt*>(this->option(opt_key))->value; } diff --git a/src/libslic3r/Extruder.cpp b/src/libslic3r/Extruder.cpp index 1e250b707e..b1b6ca347a 100644 --- a/src/libslic3r/Extruder.cpp +++ b/src/libslic3r/Extruder.cpp @@ -220,12 +220,12 @@ double Extruder::retract_restart_extra() const double Extruder::retract_length_toolchange() const { - return m_config->retract_length_toolchange.get_at(extruder_id()); + return m_config->retract_length_toolchange.get_at(m_config_index); } double Extruder::retract_restart_extra_toolchange() const { - return m_config->retract_restart_extra_toolchange.get_at(extruder_id()); + return m_config->retract_restart_extra_toolchange.get_at(m_config_index); } double Extruder::travel_slope() const diff --git a/src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp b/src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp index 11e2d081d2..97f8f743fb 100644 --- a/src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp +++ b/src/libslic3r/Feature/FuzzySkin/FuzzySkin.cpp @@ -682,7 +682,7 @@ Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perim return fuzzified; } -void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, const bool is_contour) +void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, const bool is_contour, const bool closed) { const auto slice_z = perimeter_generator.slice_z; const auto& regions = perimeter_generator.regions_by_fuzzify; @@ -690,7 +690,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato const auto& config = regions.begin()->first; const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, extrusion->inset_idx, is_contour); if (fuzzify) - fuzzy_extrusion_line(extrusion->junctions, slice_z, config); + fuzzy_extrusion_line(extrusion->junctions, slice_z, config, closed); } else { // Merge regions that produce identical fuzzy effects (differ only in type). // When the style (e.g. External) and a painted region (All) both fuzzify this loop @@ -701,10 +701,19 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato // Fast path: single merged region — apply directly without splitting if (merged_regions.size() == 1 && merged_regions.front().expolygons.empty()) { - fuzzy_extrusion_line(extrusion->junctions, slice_z, *merged_regions.front().config); + fuzzy_extrusion_line(extrusion->junctions, slice_z, *merged_regions.front().config, closed); return; } + // Open path means this is a thin wall that collapsed into a single thick line, in this case the path will go exactly + // between the middle two sides of the object. And since the paint segmentation never goes beyond the middle line because + // it uses voronoi diagram, we need to expand the segmentation a little bit to make sure it covers the path. + if (!closed) { + for (auto& r : merged_regions) { + r.expolygons = offset_ex(r.expolygons, perimeter_generator.ext_perimeter_flow.scaled_width() / 10); + } + } + #ifdef DEBUG_FUZZY { int i = 0; @@ -752,7 +761,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato // Fuzzy splitted extrusion if (std::all_of(splitted.begin(), splitted.end(), [](const Algorithm::SplitLineJunction& j) { return j.clipped; })) { // The entire polygon is fuzzified - fuzzy_extrusion_line(extrusion->junctions, slice_z, *r.config); + fuzzy_extrusion_line(extrusion->junctions, slice_z, *r.config, closed); continue; } else { const auto current_ext = extrusion->junctions; @@ -803,7 +812,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato } //Orca: ensure the loop is closed after fuzzy - if (!extrusion->junctions.empty() && extrusion->junctions.front().p != extrusion->junctions.back().p) { + if (closed && !extrusion->junctions.empty() && extrusion->junctions.front().p != extrusion->junctions.back().p) { extrusion->junctions.back().p = extrusion->junctions.front().p; extrusion->junctions.back().w = extrusion->junctions.front().w; } diff --git a/src/libslic3r/Feature/FuzzySkin/FuzzySkin.hpp b/src/libslic3r/Feature/FuzzySkin/FuzzySkin.hpp index e099139c90..51d503a3c9 100644 --- a/src/libslic3r/Feature/FuzzySkin/FuzzySkin.hpp +++ b/src/libslic3r/Feature/FuzzySkin/FuzzySkin.hpp @@ -16,7 +16,7 @@ void group_region_by_fuzzify(PerimeterGenerator& g); bool should_fuzzify(const FuzzySkinConfig& config, int layer_id, size_t loop_idx, bool is_contour); Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perimeter_generator, size_t loop_idx, bool is_contour); -void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, bool is_contour); +void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, bool is_contour, bool closed = true); } // namespace Slic3r::Feature::FuzzySkin diff --git a/src/libslic3r/FilamentGroup.cpp b/src/libslic3r/FilamentGroup.cpp index 97da94652c..07a4cc2449 100644 --- a/src/libslic3r/FilamentGroup.cpp +++ b/src/libslic3r/FilamentGroup.cpp @@ -1021,7 +1021,7 @@ namespace Slic3r if (FGMode::MatchMode == ctx.group_info.mode) return calc_filament_group_for_match(cost); } - catch (const FilamentGroupException& e) { + catch (const FilamentGroupException&) { } return calc_filament_group_for_flush(cost); diff --git a/src/libslic3r/FilamentMixer.cpp b/src/libslic3r/FilamentMixer.cpp new file mode 100644 index 0000000000..66640498e6 --- /dev/null +++ b/src/libslic3r/FilamentMixer.cpp @@ -0,0 +1,829 @@ +#include "FilamentMixer.hpp" + +#include <algorithm> +#include <cassert> +#include <cctype> +#include <cmath> +#include <cstdio> +#include <limits> +#include <set> +#include <sstream> +#include <numeric> + +#include <boost/log/trivial.hpp> + +#include "ColorDecomposeRecipe.hpp" +#include "FilamentMixerModel.hpp" +#include "LocalesUtils.hpp" + +namespace Slic3r { +namespace { + +inline float clamp01(float x) +{ + return std::max(0.0f, std::min(1.0f, x)); +} + +inline float srgb_to_linear(float x) +{ + return (x >= 0.04045f) ? std::pow((x + 0.055f) / 1.055f, 2.4f) : x / 12.92f; +} + +inline float linear_to_srgb(float x) +{ + return (x >= 0.0031308f) ? (1.055f * std::pow(x, 1.0f / 2.4f) - 0.055f) : (12.92f * x); +} + +inline unsigned char to_u8(float x) +{ + const float clamped = clamp01(x); + return static_cast<unsigned char>(clamped * 255.0f + 0.5f); +} + +inline float to_f01(unsigned char x) +{ + return static_cast<float>(x) / 255.0f; +} + +} // namespace + +void filament_mixer_lerp(unsigned char r1, unsigned char g1, unsigned char b1, + unsigned char r2, unsigned char g2, unsigned char b2, + float t, + unsigned char* out_r, unsigned char* out_g, unsigned char* out_b) +{ + ::filament_mixer::lerp(r1, g1, b1, r2, g2, b2, t, out_r, out_g, out_b); +} + +void filament_mixer_lerp_float(float r1, float g1, float b1, + float r2, float g2, float b2, + float t, + float* out_r, float* out_g, float* out_b) +{ + unsigned char ur = 0, ug = 0, ub = 0; + filament_mixer_lerp(to_u8(r1), to_u8(g1), to_u8(b1), + to_u8(r2), to_u8(g2), to_u8(b2), + t, &ur, &ug, &ub); + *out_r = to_f01(ur); + *out_g = to_f01(ug); + *out_b = to_f01(ub); +} + +void filament_mixer_lerp_linear_float(float r1, float g1, float b1, + float r2, float g2, float b2, + float t, + float* out_r, float* out_g, float* out_b) +{ + const float sr1 = linear_to_srgb(clamp01(r1)); + const float sg1 = linear_to_srgb(clamp01(g1)); + const float sb1 = linear_to_srgb(clamp01(b1)); + const float sr2 = linear_to_srgb(clamp01(r2)); + const float sg2 = linear_to_srgb(clamp01(g2)); + const float sb2 = linear_to_srgb(clamp01(b2)); + + float out_sr = 0.0f, out_sg = 0.0f, out_sb = 0.0f; + filament_mixer_lerp_float(sr1, sg1, sb1, sr2, sg2, sb2, t, &out_sr, &out_sg, &out_sb); + + *out_r = srgb_to_linear(clamp01(out_sr)); + *out_g = srgb_to_linear(clamp01(out_sg)); + *out_b = srgb_to_linear(clamp01(out_sb)); +} + +static bool parse_hex(const std::string &hex, unsigned char &r, unsigned char &g, unsigned char &b) +{ + if (hex.size() < 7 || hex[0] != '#') return false; + unsigned rv = 0, gv = 0, bv = 0; + if (std::sscanf(hex.c_str(), "#%02x%02x%02x", &rv, &gv, &bv) != 3) return false; + r = (unsigned char)rv; g = (unsigned char)gv; b = (unsigned char)bv; + return true; +} + +std::string blend_color(const std::string& hex_a, const std::string& hex_b, float ratio_b) +{ + unsigned char r1 = 128, g1 = 128, b1 = 128; + unsigned char r2 = 128, g2 = 128, b2 = 128; + parse_hex(hex_a, r1, g1, b1); + parse_hex(hex_b, r2, g2, b2); + + unsigned char mr = 0, mg = 0, mb = 0; + filament_mixer_lerp(r1, g1, b1, r2, g2, b2, ratio_b, &mr, &mg, &mb); + + char buf[8]; + std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", mr, mg, mb); + return std::string(buf); +} + +std::string blend_color_multi(const std::vector<std::string> &hex_colors, + const std::vector<int> &weights) +{ + if (hex_colors.size() >= 2 && hex_colors.size() == weights.size()) { + std::string measured = lookup_measured_blend_color(hex_colors, weights); + if (!measured.empty()) + return measured; + } + + if (hex_colors.empty()) + return "#000000"; + if (hex_colors.size() == 1) { + unsigned char cr = 128, cg = 128, cb = 128; + parse_hex(hex_colors.front(), cr, cg, cb); + char buf[8]; + std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", cr, cg, cb); + return std::string(buf); + } + + assert(hex_colors.size() == weights.size()); + + unsigned char r = 128, g = 128, b = 128; + int accumulated = 0; + + for (size_t i = 0; i < hex_colors.size() && i < weights.size(); ++i) { + if (weights[i] <= 0) + continue; + unsigned char cr = 128, cg = 128, cb = 128; + parse_hex(hex_colors[i], cr, cg, cb); + if (accumulated == 0) { + r = cr; g = cg; b = cb; + accumulated = weights[i]; + } else { + const int new_total = accumulated + weights[i]; + const float t = static_cast<float>(weights[i]) / static_cast<float>(new_total); + filament_mixer_lerp(r, g, b, cr, cg, cb, t, &r, &g, &b); + accumulated = new_total; + } + } + + if (accumulated == 0) + return "#000000"; + + char buf[8]; + std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", r, g, b); + return std::string(buf); +} + +std::vector<unsigned int> parse_mixed_components(const std::string &str) +{ + std::vector<unsigned int> components; + if (str.empty()) + return components; + std::istringstream ss(str); + std::string token; + while (std::getline(ss, token, ',')) { + try { + int val = std::stoi(token); + if (val >= 0) + components.push_back(static_cast<unsigned int>(val)); + } catch (...) {} + } + return components; +} + +namespace { + +// Parse a token that may represent a finite double or "use default" (empty / "nan"). +// Returns NaN on either explicit sentinel or any parse error. +inline double parse_tangent_token(const std::string& tok) +{ + if (tok.empty()) return std::numeric_limits<double>::quiet_NaN(); + std::string lower(tok.size(), '\0'); + std::transform(tok.begin(), tok.end(), lower.begin(), + [](unsigned char c) { return static_cast<char>(std::tolower(c)); }); + if (lower == "nan") return std::numeric_limits<double>::quiet_NaN(); + try { + const double v = std::stod(tok); + if (!std::isfinite(v)) return std::numeric_limits<double>::quiet_NaN(); + return v; + } catch (...) { + return std::numeric_limits<double>::quiet_NaN(); + } +} + +// Split a "a,b,c,d" segment on commas, preserving empty tokens (so "0.5,0.4,," yields +// {"0.5","0.4","",""}). Used by the gradient-curve parser to distinguish NaN tangents +// from a malformed segment. +inline std::vector<std::string> split_commas(const std::string& seg) +{ + std::vector<std::string> out; + size_t start = 0; + while (true) { + const size_t comma = seg.find(',', start); + if (comma == std::string::npos) { + out.emplace_back(seg.substr(start)); + return out; + } + out.emplace_back(seg.substr(start, comma - start)); + start = comma + 1; + } +} + +} // namespace + +// Default Fritsch-Carlson PCHIP tangents for a sorted-by-x anchor list. m has size n +// matching the anchor count; for n == 1 the tangent is 0; for n == 2 both endpoint +// tangents equal the single secant (degenerates to linear). +std::vector<double> compute_pchip_default_tangents(const std::vector<GradientAnchor>& pts) +{ + const size_t n = pts.size(); + std::vector<double> m(n, 0.0); + if (n < 2) return m; + + std::vector<double> d(n - 1); + for (size_t i = 0; i + 1 < n; ++i) { + const double h = std::max(1e-12, pts[i + 1].x - pts[i].x); + d[i] = (pts[i + 1].y - pts[i].y) / h; + } + + m[0] = d[0]; + m[n - 1] = d[n - 2]; + for (size_t i = 1; i + 1 < n; ++i) + m[i] = 0.5 * (d[i - 1] + d[i]); + + // Fritsch-Carlson monotonic guard: kill flats then rescale steep tangents so the + // resulting cubic never overshoots [min, max] of the surrounding anchors. + for (size_t i = 0; i + 1 < n; ++i) { + if (d[i] == 0.0) { + m[i] = 0.0; + m[i + 1] = 0.0; + continue; + } + const double a = m[i] / d[i]; + const double b = m[i + 1] / d[i]; + const double s = a * a + b * b; + if (s > 9.0) { + const double tau = 3.0 / std::sqrt(s); + m[i] = tau * a * d[i]; + m[i + 1] = tau * b * d[i]; + } + } + return m; +} + +GradientCurve parse_gradient_curve(const std::string& s) +{ + GradientCurve curve; + if (s.empty()) + return curve; + + CNumericLocalesSetter c_locale_setter; + std::istringstream ss(s); + std::string segment; + while (std::getline(ss, segment, '|')) { + if (segment.empty()) + continue; + const auto fields = split_commas(segment); + // 2-field legacy form -> (x, y), tangents stay NaN. + // 4-field form -> (x, y, m_in, m_out), empty / "nan" tokens preserved as NaN. + if (fields.size() != 2 && fields.size() != 4) { + BOOST_LOG_TRIVIAL(warning) << "parse_gradient_curve: ignoring malformed segment \"" + << segment << "\" (expected 2 or 4 comma-separated fields, got " + << fields.size() << ")"; + continue; + } + try { + double x = std::stod(fields[0]); + double y = std::stod(fields[1]); + x = std::max(0.0, std::min(1.0, x)); + y = std::max(kGradientMinRatio, std::min(kGradientMaxRatio, y)); + GradientAnchor a; + a.x = x; + a.y = y; + if (fields.size() == 4) { + a.m_in = parse_tangent_token(fields[2]); + a.m_out = parse_tangent_token(fields[3]); + } + curve.points.push_back(a); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "parse_gradient_curve: ignoring unparseable segment \"" + << segment << "\": " << e.what(); + } + } + + if (curve.points.size() < 2) { + if (!curve.points.empty()) + BOOST_LOG_TRIVIAL(warning) << "parse_gradient_curve: only " + << curve.points.size() << " valid point(s), need at least 2; discarding"; + curve.points.clear(); + return curve; + } + + std::sort(curve.points.begin(), curve.points.end(), + [](const GradientAnchor& a, const GradientAnchor& b) { + return a.x < b.x; + }); + return curve; +} + +std::string serialize_gradient_curve(const GradientCurve& c) +{ + if (c.points.empty()) + return std::string{}; + + CNumericLocalesSetter c_locale_setter; + std::string out; + char buf[128]; + for (size_t i = 0; i < c.points.size(); ++i) { + if (i > 0) out += '|'; + const auto& a = c.points[i]; + const bool has_in = std::isfinite(a.m_in); + const bool has_out = std::isfinite(a.m_out); + if (has_in || has_out) { + // Emit empty tokens for NaN slots so the legacy parser would still split + // four fields; the new parser interprets empty tokens as "use PCHIP default". + char in_buf[32] = {0}; + char out_buf[32] = {0}; + if (has_in) std::snprintf(in_buf, sizeof(in_buf), "%.4f", a.m_in); + if (has_out) std::snprintf(out_buf, sizeof(out_buf), "%.4f", a.m_out); + std::snprintf(buf, sizeof(buf), "%.4f,%.4f,%s,%s", + a.x, a.y, in_buf, out_buf); + } else { + // 4-field form is only emitted when at least one tangent is finite; the + // 2-field form is emitted otherwise so the JSON payload stays minimal + // and remains readable by older clients that only know (x, y) pairs. + std::snprintf(buf, sizeof(buf), "%.4f,%.4f", a.x, a.y); + } + out += buf; + } + return out; +} + +double sample_gradient_curve(const GradientCurve& c, double t) +{ + const auto& pts = c.points; + if (pts.size() < 2) + return 0.5; + if (t <= pts.front().x) + return pts.front().y; + if (t >= pts.back().x) + return pts.back().y; + + // PCHIP defaults are computed for every call; control point counts are typically + // tiny (< 16) so the allocation cost is negligible compared to any actual rendering + // or G-code work that drives the sampler. + const std::vector<double> m_def = compute_pchip_default_tangents(pts); + const size_t n = pts.size(); + + // Linear scan to locate the interval [pts[i].x, pts[i+1].x] containing t. Cheap + // and avoids the upper_bound boilerplate; n is small. + for (size_t i = 1; i < n; ++i) { + const double x0 = pts[i - 1].x; + const double x1 = pts[i].x; + if (t > x1) continue; + + const double y0 = pts[i - 1].y; + const double y1 = pts[i].y; + const double h = std::max(1e-12, x1 - x0); + const double m_left = std::isfinite(pts[i - 1].m_out) ? pts[i - 1].m_out : m_def[i - 1]; + const double m_right = std::isfinite(pts[i].m_in) ? pts[i].m_in : m_def[i]; + + const double u = (t - x0) / h; + const double u2 = u * u; + const double u3 = u2 * u; + const double h00 = 2.0 * u3 - 3.0 * u2 + 1.0; + const double h10 = u3 - 2.0 * u2 + u; + const double h01 = -2.0 * u3 + 3.0 * u2; + const double h11 = u3 - u2; + double y = h00 * y0 + h10 * h * m_left + + h01 * y1 + h11 * h * m_right; + // Defensive clamp in case tangent overrides on legacy curves push the + // single-segment Hermite slightly outside the anchor band. + if (y < kGradientMinRatio) y = kGradientMinRatio; + if (y > kGradientMaxRatio) y = kGradientMaxRatio; + return y; + } + return pts.back().y; +} + +std::vector<double> parse_mixed_ratios(const std::string &str, size_t n_components) +{ + CNumericLocalesSetter c_locale_setter; + std::vector<double> ratios; + if (!str.empty()) { + std::istringstream ss(str); + std::string token; + while (std::getline(ss, token, ',')) { + try { + double val = std::stod(token); + if (val > 0.0) + ratios.push_back(val); + } catch (...) {} + } + } + + if (ratios.size() != n_components || n_components == 0) { + ratios.assign(n_components, n_components > 0 ? 1.0 / n_components : 0.0); + return ratios; + } + + double sum = std::accumulate(ratios.begin(), ratios.end(), 0.0); + if (sum > 0.0 && std::abs(sum - 1.0) > 1e-6) { + for (double &r : ratios) + r /= sum; + } + return ratios; +} + +bool has_any_mixed_filament(const std::vector<unsigned char> &is_mixed) +{ + for (unsigned char v : is_mixed) + if (v) return true; + return false; +} + +std::vector<size_t> check_mixed_filament_integrity( + const std::vector<unsigned char> &is_mixed, + const std::vector<std::string> &comp_strs, + size_t num_physical) +{ + std::vector<size_t> broken; + for (size_t i = 0; i < is_mixed.size(); ++i) { + if (!is_mixed[i]) continue; + if (i >= comp_strs.size() || comp_strs[i].empty()) { + broken.push_back(i); + continue; + } + auto comps = parse_mixed_components(comp_strs[i]); + if (comps.size() < 2) { + broken.push_back(i); + continue; + } + for (unsigned int c : comps) { + if (c < 1 || c > num_physical) { + broken.push_back(i); + break; + } + } + } + return broken; +} + +std::vector<unsigned int> expand_mixed_filaments( + const std::vector<unsigned int> &extruders_0based, + const std::vector<unsigned char> &is_mixed, + const std::vector<std::string> &comp_strs) +{ + std::vector<unsigned int> result; + for (unsigned int ext : extruders_0based) { + if (ext < is_mixed.size() && is_mixed[ext] && ext < comp_strs.size()) { + auto comps = parse_mixed_components(comp_strs[ext]); + for (unsigned int c : comps) + if (c >= 1) result.push_back(c - 1); + } else { + result.push_back(ext); + } + } + std::sort(result.begin(), result.end()); + result.erase(std::unique(result.begin(), result.end()), result.end()); + return result; +} + +void remap_mixed_components_on_delete( + const std::vector<unsigned char> &is_mixed, + std::vector<std::string> &comp_strs, + unsigned int del_1based) +{ + for (size_t i = 0; i < is_mixed.size(); ++i) { + if (!is_mixed[i]) continue; + if (i >= comp_strs.size() || comp_strs[i].empty()) continue; + + auto comps = parse_mixed_components(comp_strs[i]); + std::ostringstream ss; + for (size_t j = 0; j < comps.size(); ++j) { + if (j > 0) ss << ','; + if (comps[j] == del_1based) + ss << 0; + else if (comps[j] > del_1based) + ss << (comps[j] - 1); + else + ss << comps[j]; + } + comp_strs[i] = ss.str(); + } +} + +std::vector<size_t> check_mixed_filament_type_consistency( + const std::vector<unsigned char> &is_mixed, + const std::vector<std::string> &comp_strs, + const std::vector<std::string> &filament_types) +{ + std::vector<size_t> result; + for (size_t i = 0; i < is_mixed.size(); ++i) { + if (!is_mixed[i]) continue; + if (i >= comp_strs.size() || comp_strs[i].empty()) continue; + auto comps = parse_mixed_components(comp_strs[i]); + if (comps.size() < 2) continue; + + std::string ref_type; + bool mismatch = false; + for (unsigned int c : comps) { + if (c == 0) continue; // sentinel for deleted component + size_t idx = static_cast<size_t>(c) - 1; // 1-based -> 0-based + if (idx >= filament_types.size()) continue; + if (ref_type.empty()) + ref_type = filament_types[idx]; + else if (filament_types[idx] != ref_type) { + mismatch = true; + break; + } + } + if (mismatch) + result.push_back(i); + } + return result; +} + +void expand_mixed_slots_in_unprintables( + std::vector<std::set<int>> &unprintables, + const std::vector<unsigned char> &is_mixed, + const std::vector<std::string> &comp_strs) +{ + for (auto &unprintable_set : unprintables) { + std::set<int> expanded; + for (int fid : unprintable_set) { + if (fid >= 0 && (size_t)fid < is_mixed.size() && is_mixed[fid] + && (size_t)fid < comp_strs.size()) { + auto comps = parse_mixed_components(comp_strs[fid]); + for (unsigned int c : comps) + if (c >= 1) expanded.insert((int)(c - 1)); + } else { + expanded.insert(fid); + } + } + unprintable_set = std::move(expanded); + } +} + +void sanitize_mixed_gradient_curve_array(std::vector<std::string>& vals) +{ + for (size_t i = 0; i < vals.size(); ++i) { + if (vals[i].empty()) + continue; + // parse_gradient_curve returns empty for both "empty input" and "<2 valid points"; + // we already skipped empty, so an empty result means a corrupted single-point slot. + if (parse_gradient_curve(vals[i]).empty()) { + BOOST_LOG_TRIVIAL(warning) << "sanitize_mixed_gradient_curve_array: slot " + << i << " curve \"" << vals[i] + << "\" has fewer than 2 valid points; clearing to linear"; + vals[i].clear(); + } + } +} + +bool try_parse_mixed_components_strict(const std::string &str, + std::vector<unsigned int> &components, + std::string &err) +{ + components.clear(); + if (str.empty()) { + err = "empty component list"; + return false; + } + std::istringstream ss(str); + std::string token; + while (std::getline(ss, token, ',')) { + if (token.empty()) { + err = "empty component index"; + return false; + } + try { + const long val = std::stol(token); + if (val < 1) { + err = "component index must be >= 1 (got " + token + ")"; + return false; + } + components.push_back(static_cast<unsigned int>(val)); + } catch (...) { + err = "invalid component index \"" + token + "\""; + return false; + } + } + if (components.size() < 2) { + err = "at least 2 components required (got " + std::to_string(components.size()) + ")"; + return false; + } + std::set<unsigned int> seen; + for (unsigned int c : components) { + if (!seen.insert(c).second) { + err = "duplicate component index " + std::to_string(c); + return false; + } + } + return true; +} + +bool try_parse_mixed_ratios_strict(const std::string &str, + size_t n_components, + std::string &err) +{ + if (str.empty()) + return true; + + CNumericLocalesSetter c_locale_setter; + std::vector<double> ratios; + std::istringstream ss(str); + std::string token; + while (std::getline(ss, token, ',')) { + if (token.empty()) { + err = "empty ratio value"; + return false; + } + try { + const double val = std::stod(token); + if (!(val > 0.0)) { + err = "ratio must be positive (got " + token + ")"; + return false; + } + ratios.push_back(val); + } catch (...) { + err = "invalid ratio \"" + token + "\""; + return false; + } + } + if (ratios.size() != n_components) { + err = "expected " + std::to_string(n_components) + " ratio(s), got " + + std::to_string(ratios.size()); + return false; + } + return true; +} + +bool validate_gradient_range_strict(const std::string &str, std::string &err) +{ + if (str.empty()) + return true; + + CNumericLocalesSetter c_locale_setter; + float v0 = 0.f, v1 = 0.f; + if (std::sscanf(str.c_str(), "%f,%f", &v0, &v1) != 2) { + err = "expected two comma-separated floats, e.g. \"0.10,0.90\""; + return false; + } + if (!(v0 > 0.f && v0 < 1.f && v1 > 0.f && v1 < 1.f)) { + err = "start and end ratios must be in (0, 1)"; + return false; + } + return true; +} + +static void append_error(std::map<std::string, std::string> &errors, + const std::string &key, + const std::string &msg) +{ + auto it = errors.find(key); + if (it == errors.end()) + errors.emplace(key, msg); + else + it->second += "; " + msg; +} + +static bool has_mixed_sub_params_specified( + const std::vector<std::string> &comp_strs, + const std::vector<std::string> &ratio_strs, + const std::vector<unsigned char> &gradient_flags) +{ + for (const std::string &s : comp_strs) + if (!s.empty()) return true; + for (const std::string &s : ratio_strs) + if (!s.empty()) return true; + for (unsigned char g : gradient_flags) + if (g) return true; + return false; +} + +static bool mixed_string_array_was_specified(const std::vector<std::string> &vals) +{ + for (const std::string &s : vals) + if (!s.empty()) + return true; + return false; +} + +static bool mixed_bool_array_was_specified(const std::vector<unsigned char> &vals) +{ + for (unsigned char v : vals) + if (v) + return true; + return false; +} + +static void check_mixed_array_size_required(std::map<std::string, std::string> &errors, + const std::string &opt_key, + size_t actual_size, + size_t expected_size) +{ + if (actual_size != expected_size) { + append_error(errors, opt_key, + "array size " + std::to_string(actual_size) + + " does not match filament slot count " + std::to_string(expected_size)); + } +} + +std::map<std::string, std::string> validate_mixed_filament_params( + const std::vector<unsigned char> &is_mixed, + const std::vector<std::string> &comp_strs, + const std::vector<std::string> &ratio_strs, + const std::vector<unsigned char> &gradient_flags, + const std::vector<std::string> &gradient_range_strs, + const std::vector<std::string> &gradient_curve_strs) +{ + std::map<std::string, std::string> errors; + + if (has_mixed_sub_params_specified(comp_strs, ratio_strs, gradient_flags) + && !has_any_mixed_filament(is_mixed)) { + append_error(errors, "filament_is_mixed", + "must be set when mixed filament parameters are specified"); + return errors; + } + + if (!has_any_mixed_filament(is_mixed)) + return errors; + + const size_t slot_count = is_mixed.size(); + + // Rule 1: mixed filament model → components & ratios arrays must cover every slot. + check_mixed_array_size_required(errors, "filament_mixed_components", comp_strs.size(), slot_count); + check_mixed_array_size_required(errors, "filament_mixed_sublayer_ratios", ratio_strs.size(), slot_count); + + // Rule 2: gradient passed (any slot true) → gradient & range arrays must cover every slot. + const bool gradient_specified = mixed_bool_array_was_specified(gradient_flags); + if (gradient_specified) { + check_mixed_array_size_required(errors, "filament_mixed_gradient", gradient_flags.size(), slot_count); + check_mixed_array_size_required(errors, "filament_mixed_gradient_range", gradient_range_strs.size(), slot_count); + } + + // Rule 3: curve passed (any non-empty entry) → curve array must cover every slot. + const bool curve_specified = mixed_string_array_was_specified(gradient_curve_strs); + if (curve_specified) + check_mixed_array_size_required(errors, "filament_mixed_gradient_curve", gradient_curve_strs.size(), slot_count); + + size_t num_physical = 0; + for (unsigned char v : is_mixed) + if (!v) ++num_physical; + + for (size_t i = 0; i < is_mixed.size(); ++i) { + if (!is_mixed[i]) + continue; + + const std::string slot = "slot " + std::to_string(i + 1); + const std::string comp_str = i < comp_strs.size() ? comp_strs[i] : ""; + + std::vector<unsigned int> components; + std::string comp_err; + if (!try_parse_mixed_components_strict(comp_str, components, comp_err)) { + append_error(errors, "filament_mixed_components", slot + ": " + comp_err); + continue; + } + + for (unsigned int c : components) { + if (c > num_physical) { + append_error(errors, "filament_mixed_components", + slot + ": component " + std::to_string(c) + + " out of range (max physical filament index is " + + std::to_string(num_physical) + ")"); + break; + } + if (c == i + 1) { + append_error(errors, "filament_mixed_components", + slot + ": cannot reference itself as a component"); + break; + } + const size_t idx0 = static_cast<size_t>(c - 1); + if (idx0 < is_mixed.size() && is_mixed[idx0]) { + append_error(errors, "filament_mixed_components", + slot + ": component " + std::to_string(c) + + " references a mixed filament slot"); + break; + } + } + + std::string ratio_err; + const std::string ratio_str = i < ratio_strs.size() ? ratio_strs[i] : ""; + if (!try_parse_mixed_ratios_strict(ratio_str, components.size(), ratio_err)) + append_error(errors, "filament_mixed_sublayer_ratios", slot + ": " + ratio_err); + + const bool gradient_on = i < gradient_flags.size() && gradient_flags[i]; + if (gradient_on) { + if (components.size() != 2) { + append_error(errors, "filament_mixed_gradient", + slot + ": gradient requires exactly 2 components"); + } + + if (gradient_specified) { + std::string range_err; + const std::string range_str = i < gradient_range_strs.size() ? gradient_range_strs[i] : ""; + if (!validate_gradient_range_strict(range_str, range_err)) + append_error(errors, "filament_mixed_gradient_range", slot + ": " + range_err); + } + + if (curve_specified) { + const std::string curve_str = i < gradient_curve_strs.size() ? gradient_curve_strs[i] : ""; + if (!curve_str.empty() && parse_gradient_curve(curve_str).empty()) + append_error(errors, "filament_mixed_gradient_curve", + slot + ": invalid curve (need at least 2 valid control points)"); + } + } + } + + return errors; +} + +} // namespace Slic3r diff --git a/src/libslic3r/FilamentMixer.hpp b/src/libslic3r/FilamentMixer.hpp new file mode 100644 index 0000000000..81ddd29e46 --- /dev/null +++ b/src/libslic3r/FilamentMixer.hpp @@ -0,0 +1,164 @@ +#ifndef SLIC3R_FILAMENT_MIXER_HPP +#define SLIC3R_FILAMENT_MIXER_HPP + +#include <limits> +#include <map> +#include <set> +#include <string> +#include <utility> +#include <vector> + +namespace Slic3r { + +// Photoshop-style gradient curve control point in [0,1] x [0,1]. +// (x, y) is the anchor position; (m_in, m_out) are optional cubic Hermite tangent +// overrides. NaN means "use the PCHIP-computed default", which is the case for plain +// anchors loaded from old 2-field 3MF projects or freshly added via a quick click. +// A press-and-drag on a curve segment populates m_out of its left anchor and m_in of +// its right anchor so the segment bends without inserting a new anchor. +struct GradientAnchor { + double x = 0.0; + double y = 0.0; + double m_in = std::numeric_limits<double>::quiet_NaN(); + double m_out = std::numeric_limits<double>::quiet_NaN(); +}; + +// Sorted list of GradientAnchor; x in [0,1], y in [kGradientMinRatio, kGradientMaxRatio]. +// Empty means "no custom curve" (callers should fall back to the linear range). +struct GradientCurve { + std::vector<GradientAnchor> points; + bool empty() const { return points.empty(); } +}; + +// Reserved blend ratio range. Anchor y values (= component 0's ratio) are constrained +// to this band so the mixed filament never reaches pure 0% / 100% of either physical +// component, which keeps both extruders flowing and avoids degenerate transitions. +// Both the editor and the sampler enforce this clamp. +constexpr double kGradientMinRatio = 0.1; +constexpr double kGradientMaxRatio = 0.9; + +// Parse "x0,y0[,m_in0,m_out0]|x1,y1[,m_in1,m_out1]|..." into a GradientCurve. +// (Anchors are pipe-separated; the fields within an anchor are comma-separated.) +// Accepts both the legacy 2-field form (tangents -> NaN) and the new 4-field form +// (empty token or "nan" preserved as NaN). Returns an empty curve when the input is +// empty or unparsable. Points are clamped to [0,1] for (x, y) and re-sorted by x. +GradientCurve parse_gradient_curve(const std::string& s); + +// Serialize a GradientCurve back to a string. Emits 4 fields per anchor when any +// tangent override is finite; emits 2 fields when both tangents are NaN so unchanged +// projects stay byte-identical with the legacy format. Returns "" when empty. +std::string serialize_gradient_curve(const GradientCurve& c); + +// Sample the curve at t in [0,1] using cubic Hermite with Fritsch-Carlson PCHIP +// default tangents, optionally overridden per anchor via m_in / m_out. Returns the +// clamped end values when t is outside the control point range. Returns 0.5 when the +// curve has fewer than 2 points (a safety fallback; callers should check empty()). +double sample_gradient_curve(const GradientCurve& c, double t); + +// Compute Fritsch-Carlson PCHIP default tangents for a sorted-by-x anchor list. +// Result size == pts.size(). Useful for callers that need to know what tangent the +// sampler would synthesize when m_in / m_out are NaN (e.g. the GUI's segment-bend +// interaction that inserts a virtual anchor and reads back the surrounding tangents). +std::vector<double> compute_pchip_default_tangents(const std::vector<GradientAnchor>& pts); + +void filament_mixer_lerp(unsigned char r1, unsigned char g1, unsigned char b1, + unsigned char r2, unsigned char g2, unsigned char b2, + float t, + unsigned char* out_r, unsigned char* out_g, unsigned char* out_b); + +void filament_mixer_lerp_float(float r1, float g1, float b1, + float r2, float g2, float b2, + float t, + float* out_r, float* out_g, float* out_b); + +void filament_mixer_lerp_linear_float(float r1, float g1, float b1, + float r2, float g2, float b2, + float t, + float* out_r, float* out_g, float* out_b); + +// Blend two hex colors ("#RRGGBB") by ratio (0.0 ~ 1.0 for color_b). +// Returns "#RRGGBB" string. +std::string blend_color(const std::string& hex_a, const std::string& hex_b, float ratio_b); + +// Blend N hex colors by integer weights using polynomial pigment mixing. +// Pairwise accumulation via filament_mixer_lerp. Returns "#RRGGBB". +std::string blend_color_multi(const std::vector<std::string> &hex_colors, + const std::vector<int> &weights); + +// Parse comma-separated 1-based component IDs, e.g. "1,3" → {1, 3}. +std::vector<unsigned int> parse_mixed_components(const std::string &str); + +// Parse comma-separated ratio values, e.g. "0.7,0.3" → {0.7, 0.3}. +// Returns equal ratios (1/n each) when str is empty or invalid. +// Normalizes so the sum equals 1.0. +std::vector<double> parse_mixed_ratios(const std::string &str, size_t n_components); + +// Returns true if any element in is_mixed is true. +// ConfigOptionBools stores values as std::vector<unsigned char>. +bool has_any_mixed_filament(const std::vector<unsigned char> &is_mixed); + +// Check which mixed filament slots have broken component references. +// Returns 0-based indices of mixed slots whose components reference +// filaments beyond num_physical (i.e., deleted filaments). +std::vector<size_t> check_mixed_filament_integrity( + const std::vector<unsigned char> &is_mixed, + const std::vector<std::string> &comp_strs, + size_t num_physical); + +// Expand mixed filament slots in an extruder list to their physical components. +// Input/output are 0-based indices. Non-mixed slots pass through unchanged. +// Result is sorted and deduplicated. +std::vector<unsigned int> expand_mixed_filaments( + const std::vector<unsigned int> &extruders_0based, + const std::vector<unsigned char> &is_mixed, + const std::vector<std::string> &comp_strs); + +// Remap mixed filament component references after a physical filament is deleted. +// del_1based: the 1-based index of the deleted physical filament. +// For each mixed slot: +// - if component == del_1based -> replace with 0 (sentinel for deleted/unselected) +// - if component > del_1based -> decrement by 1 +void remap_mixed_components_on_delete( + const std::vector<unsigned char> &is_mixed, + std::vector<std::string> &comp_strs, + unsigned int del_1based); + +// Check which mixed filament slots have type-mismatched components. +// filament_types: type strings for physical filaments (0-based, size == num_physical). +// Component IDs in comp_strs are 1-based; the function converts to 0-based to look up types. +// Returns 0-based config indices of mixed slots with mismatched component types. +std::vector<size_t> check_mixed_filament_type_consistency( + const std::vector<unsigned char> &is_mixed, + const std::vector<std::string> &comp_strs, + const std::vector<std::string> &filament_types); + +// Expand mixed-slot IDs in geometric unprintable sets to their physical component IDs. +// Each set entry that corresponds to a mixed slot is replaced by the slot's component +// IDs (0-based). Non-mixed entries pass through unchanged. +void expand_mixed_slots_in_unprintables( + std::vector<std::set<int>> &unprintables, + const std::vector<unsigned char> &is_mixed, + const std::vector<std::string> &comp_strs); + +// Clear any non-empty gradient-curve slot that parses to fewer than 2 control points. +// Heals per-slot arrays corrupted by the legacy "|" separator collision between +// PresetBundle::export_selections / load_selections (which used "|" as the inter-slot +// delimiter) and serialize_gradient_curve / parse_gradient_curve (which use "|" as the +// intra-slot control-point delimiter). Such a round-trip splits a multi-point curve +// across adjacent slots, leaving single-point entries that fail MakerWorld's strict +// "curve needs >= 2 points" check. Clearing them falls back to the linear range. +void sanitize_mixed_gradient_curve_array(std::vector<std::string>& vals); + +// Validate mixed-color (混色) parameters. Returns error messages keyed by option name. +// Slot details are included in the message text (1-based slot index). +std::map<std::string, std::string> validate_mixed_filament_params( + const std::vector<unsigned char> &is_mixed, + const std::vector<std::string> &comp_strs, + const std::vector<std::string> &ratio_strs, + const std::vector<unsigned char> &gradient_flags, + const std::vector<std::string> &gradient_range_strs, + const std::vector<std::string> &gradient_curve_strs); + +} // namespace Slic3r + +#endif // SLIC3R_FILAMENT_MIXER_HPP diff --git a/src/libslic3r/FilamentMixerModel.hpp b/src/libslic3r/FilamentMixerModel.hpp new file mode 100644 index 0000000000..89b299471b --- /dev/null +++ b/src/libslic3r/FilamentMixerModel.hpp @@ -0,0 +1,819 @@ +/* + * FilamentMixer — Header-only C++ pigment color mixer + * + * Filament mixer implementation using a degree-4 polynomial regression + * trained to approximate Mixbox behavior (Mean Delta-E ~2.07). + * This library does not include Mixbox source code, binaries, or data files. + * + * Usage: + * #include "FilamentMixerModel.hpp" + * + * unsigned char r, g, b; + * filament_mixer::lerp(0, 33, 133, 252, 211, 0, 0.5f, &r, &g, &b); + * // r=47, g=141, b=56 (blue + yellow → green) + * + * No dependencies beyond the C++ standard library. + * + * MIT License + * + * Copyright (c) 2026 Justin Hayes + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef FILAMENT_MIXER_MODEL_HPP +#define FILAMENT_MIXER_MODEL_HPP + +#include <algorithm> +#include <cmath> +#include <cstdint> + +namespace filament_mixer { +namespace detail { + +// BEGIN AUTO-GENERATED COEFFICIENTS +// Auto-generated by scripts/export_poly_coefficients.py +// Do not edit manually. +// Degree-4 polynomial, 330 features, 7 inputs + +static const int POLY_DEGREE = 4; +static const int N_FEATURES = 330; +static const int N_INPUTS = 7; + +static const int POWERS[330][7] = { + {0, 0, 0, 0, 0, 0, 0}, + {1, 0, 0, 0, 0, 0, 0}, + {0, 1, 0, 0, 0, 0, 0}, + {0, 0, 1, 0, 0, 0, 0}, + {0, 0, 0, 1, 0, 0, 0}, + {0, 0, 0, 0, 1, 0, 0}, + {0, 0, 0, 0, 0, 1, 0}, + {0, 0, 0, 0, 0, 0, 1}, + {2, 0, 0, 0, 0, 0, 0}, + {1, 1, 0, 0, 0, 0, 0}, + {1, 0, 1, 0, 0, 0, 0}, + {1, 0, 0, 1, 0, 0, 0}, + {1, 0, 0, 0, 1, 0, 0}, + {1, 0, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 0, 0, 1}, + {0, 2, 0, 0, 0, 0, 0}, + {0, 1, 1, 0, 0, 0, 0}, + {0, 1, 0, 1, 0, 0, 0}, + {0, 1, 0, 0, 1, 0, 0}, + {0, 1, 0, 0, 0, 1, 0}, + {0, 1, 0, 0, 0, 0, 1}, + {0, 0, 2, 0, 0, 0, 0}, + {0, 0, 1, 1, 0, 0, 0}, + {0, 0, 1, 0, 1, 0, 0}, + {0, 0, 1, 0, 0, 1, 0}, + {0, 0, 1, 0, 0, 0, 1}, + {0, 0, 0, 2, 0, 0, 0}, + {0, 0, 0, 1, 1, 0, 0}, + {0, 0, 0, 1, 0, 1, 0}, + {0, 0, 0, 1, 0, 0, 1}, + {0, 0, 0, 0, 2, 0, 0}, + {0, 0, 0, 0, 1, 1, 0}, + {0, 0, 0, 0, 1, 0, 1}, + {0, 0, 0, 0, 0, 2, 0}, + {0, 0, 0, 0, 0, 1, 1}, + {0, 0, 0, 0, 0, 0, 2}, + {3, 0, 0, 0, 0, 0, 0}, + {2, 1, 0, 0, 0, 0, 0}, + {2, 0, 1, 0, 0, 0, 0}, + {2, 0, 0, 1, 0, 0, 0}, + {2, 0, 0, 0, 1, 0, 0}, + {2, 0, 0, 0, 0, 1, 0}, + {2, 0, 0, 0, 0, 0, 1}, + {1, 2, 0, 0, 0, 0, 0}, + {1, 1, 1, 0, 0, 0, 0}, + {1, 1, 0, 1, 0, 0, 0}, + {1, 1, 0, 0, 1, 0, 0}, + {1, 1, 0, 0, 0, 1, 0}, + {1, 1, 0, 0, 0, 0, 1}, + {1, 0, 2, 0, 0, 0, 0}, + {1, 0, 1, 1, 0, 0, 0}, + {1, 0, 1, 0, 1, 0, 0}, + {1, 0, 1, 0, 0, 1, 0}, + {1, 0, 1, 0, 0, 0, 1}, + {1, 0, 0, 2, 0, 0, 0}, + {1, 0, 0, 1, 1, 0, 0}, + {1, 0, 0, 1, 0, 1, 0}, + {1, 0, 0, 1, 0, 0, 1}, + {1, 0, 0, 0, 2, 0, 0}, + {1, 0, 0, 0, 1, 1, 0}, + {1, 0, 0, 0, 1, 0, 1}, + {1, 0, 0, 0, 0, 2, 0}, + {1, 0, 0, 0, 0, 1, 1}, + {1, 0, 0, 0, 0, 0, 2}, + {0, 3, 0, 0, 0, 0, 0}, + {0, 2, 1, 0, 0, 0, 0}, + {0, 2, 0, 1, 0, 0, 0}, + {0, 2, 0, 0, 1, 0, 0}, + {0, 2, 0, 0, 0, 1, 0}, + {0, 2, 0, 0, 0, 0, 1}, + {0, 1, 2, 0, 0, 0, 0}, + {0, 1, 1, 1, 0, 0, 0}, + {0, 1, 1, 0, 1, 0, 0}, + {0, 1, 1, 0, 0, 1, 0}, + {0, 1, 1, 0, 0, 0, 1}, + {0, 1, 0, 2, 0, 0, 0}, + {0, 1, 0, 1, 1, 0, 0}, + {0, 1, 0, 1, 0, 1, 0}, + {0, 1, 0, 1, 0, 0, 1}, + {0, 1, 0, 0, 2, 0, 0}, + {0, 1, 0, 0, 1, 1, 0}, + {0, 1, 0, 0, 1, 0, 1}, + {0, 1, 0, 0, 0, 2, 0}, + {0, 1, 0, 0, 0, 1, 1}, + {0, 1, 0, 0, 0, 0, 2}, + {0, 0, 3, 0, 0, 0, 0}, + {0, 0, 2, 1, 0, 0, 0}, + {0, 0, 2, 0, 1, 0, 0}, + {0, 0, 2, 0, 0, 1, 0}, + {0, 0, 2, 0, 0, 0, 1}, + {0, 0, 1, 2, 0, 0, 0}, + {0, 0, 1, 1, 1, 0, 0}, + {0, 0, 1, 1, 0, 1, 0}, + {0, 0, 1, 1, 0, 0, 1}, + {0, 0, 1, 0, 2, 0, 0}, + {0, 0, 1, 0, 1, 1, 0}, + {0, 0, 1, 0, 1, 0, 1}, + {0, 0, 1, 0, 0, 2, 0}, + {0, 0, 1, 0, 0, 1, 1}, + {0, 0, 1, 0, 0, 0, 2}, + {0, 0, 0, 3, 0, 0, 0}, + {0, 0, 0, 2, 1, 0, 0}, + {0, 0, 0, 2, 0, 1, 0}, + {0, 0, 0, 2, 0, 0, 1}, + {0, 0, 0, 1, 2, 0, 0}, + {0, 0, 0, 1, 1, 1, 0}, + {0, 0, 0, 1, 1, 0, 1}, + {0, 0, 0, 1, 0, 2, 0}, + {0, 0, 0, 1, 0, 1, 1}, + {0, 0, 0, 1, 0, 0, 2}, + {0, 0, 0, 0, 3, 0, 0}, + {0, 0, 0, 0, 2, 1, 0}, + {0, 0, 0, 0, 2, 0, 1}, + {0, 0, 0, 0, 1, 2, 0}, + {0, 0, 0, 0, 1, 1, 1}, + {0, 0, 0, 0, 1, 0, 2}, + {0, 0, 0, 0, 0, 3, 0}, + {0, 0, 0, 0, 0, 2, 1}, + {0, 0, 0, 0, 0, 1, 2}, + {0, 0, 0, 0, 0, 0, 3}, + {4, 0, 0, 0, 0, 0, 0}, + {3, 1, 0, 0, 0, 0, 0}, + {3, 0, 1, 0, 0, 0, 0}, + {3, 0, 0, 1, 0, 0, 0}, + {3, 0, 0, 0, 1, 0, 0}, + {3, 0, 0, 0, 0, 1, 0}, + {3, 0, 0, 0, 0, 0, 1}, + {2, 2, 0, 0, 0, 0, 0}, + {2, 1, 1, 0, 0, 0, 0}, + {2, 1, 0, 1, 0, 0, 0}, + {2, 1, 0, 0, 1, 0, 0}, + {2, 1, 0, 0, 0, 1, 0}, + {2, 1, 0, 0, 0, 0, 1}, + {2, 0, 2, 0, 0, 0, 0}, + {2, 0, 1, 1, 0, 0, 0}, + {2, 0, 1, 0, 1, 0, 0}, + {2, 0, 1, 0, 0, 1, 0}, + {2, 0, 1, 0, 0, 0, 1}, + {2, 0, 0, 2, 0, 0, 0}, + {2, 0, 0, 1, 1, 0, 0}, + {2, 0, 0, 1, 0, 1, 0}, + {2, 0, 0, 1, 0, 0, 1}, + {2, 0, 0, 0, 2, 0, 0}, + {2, 0, 0, 0, 1, 1, 0}, + {2, 0, 0, 0, 1, 0, 1}, + {2, 0, 0, 0, 0, 2, 0}, + {2, 0, 0, 0, 0, 1, 1}, + {2, 0, 0, 0, 0, 0, 2}, + {1, 3, 0, 0, 0, 0, 0}, + {1, 2, 1, 0, 0, 0, 0}, + {1, 2, 0, 1, 0, 0, 0}, + {1, 2, 0, 0, 1, 0, 0}, + {1, 2, 0, 0, 0, 1, 0}, + {1, 2, 0, 0, 0, 0, 1}, + {1, 1, 2, 0, 0, 0, 0}, + {1, 1, 1, 1, 0, 0, 0}, + {1, 1, 1, 0, 1, 0, 0}, + {1, 1, 1, 0, 0, 1, 0}, + {1, 1, 1, 0, 0, 0, 1}, + {1, 1, 0, 2, 0, 0, 0}, + {1, 1, 0, 1, 1, 0, 0}, + {1, 1, 0, 1, 0, 1, 0}, + {1, 1, 0, 1, 0, 0, 1}, + {1, 1, 0, 0, 2, 0, 0}, + {1, 1, 0, 0, 1, 1, 0}, + {1, 1, 0, 0, 1, 0, 1}, + {1, 1, 0, 0, 0, 2, 0}, + {1, 1, 0, 0, 0, 1, 1}, + {1, 1, 0, 0, 0, 0, 2}, + {1, 0, 3, 0, 0, 0, 0}, + {1, 0, 2, 1, 0, 0, 0}, + {1, 0, 2, 0, 1, 0, 0}, + {1, 0, 2, 0, 0, 1, 0}, + {1, 0, 2, 0, 0, 0, 1}, + {1, 0, 1, 2, 0, 0, 0}, + {1, 0, 1, 1, 1, 0, 0}, + {1, 0, 1, 1, 0, 1, 0}, + {1, 0, 1, 1, 0, 0, 1}, + {1, 0, 1, 0, 2, 0, 0}, + {1, 0, 1, 0, 1, 1, 0}, + {1, 0, 1, 0, 1, 0, 1}, + {1, 0, 1, 0, 0, 2, 0}, + {1, 0, 1, 0, 0, 1, 1}, + {1, 0, 1, 0, 0, 0, 2}, + {1, 0, 0, 3, 0, 0, 0}, + {1, 0, 0, 2, 1, 0, 0}, + {1, 0, 0, 2, 0, 1, 0}, + {1, 0, 0, 2, 0, 0, 1}, + {1, 0, 0, 1, 2, 0, 0}, + {1, 0, 0, 1, 1, 1, 0}, + {1, 0, 0, 1, 1, 0, 1}, + {1, 0, 0, 1, 0, 2, 0}, + {1, 0, 0, 1, 0, 1, 1}, + {1, 0, 0, 1, 0, 0, 2}, + {1, 0, 0, 0, 3, 0, 0}, + {1, 0, 0, 0, 2, 1, 0}, + {1, 0, 0, 0, 2, 0, 1}, + {1, 0, 0, 0, 1, 2, 0}, + {1, 0, 0, 0, 1, 1, 1}, + {1, 0, 0, 0, 1, 0, 2}, + {1, 0, 0, 0, 0, 3, 0}, + {1, 0, 0, 0, 0, 2, 1}, + {1, 0, 0, 0, 0, 1, 2}, + {1, 0, 0, 0, 0, 0, 3}, + {0, 4, 0, 0, 0, 0, 0}, + {0, 3, 1, 0, 0, 0, 0}, + {0, 3, 0, 1, 0, 0, 0}, + {0, 3, 0, 0, 1, 0, 0}, + {0, 3, 0, 0, 0, 1, 0}, + {0, 3, 0, 0, 0, 0, 1}, + {0, 2, 2, 0, 0, 0, 0}, + {0, 2, 1, 1, 0, 0, 0}, + {0, 2, 1, 0, 1, 0, 0}, + {0, 2, 1, 0, 0, 1, 0}, + {0, 2, 1, 0, 0, 0, 1}, + {0, 2, 0, 2, 0, 0, 0}, + {0, 2, 0, 1, 1, 0, 0}, + {0, 2, 0, 1, 0, 1, 0}, + {0, 2, 0, 1, 0, 0, 1}, + {0, 2, 0, 0, 2, 0, 0}, + {0, 2, 0, 0, 1, 1, 0}, + {0, 2, 0, 0, 1, 0, 1}, + {0, 2, 0, 0, 0, 2, 0}, + {0, 2, 0, 0, 0, 1, 1}, + {0, 2, 0, 0, 0, 0, 2}, + {0, 1, 3, 0, 0, 0, 0}, + {0, 1, 2, 1, 0, 0, 0}, + {0, 1, 2, 0, 1, 0, 0}, + {0, 1, 2, 0, 0, 1, 0}, + {0, 1, 2, 0, 0, 0, 1}, + {0, 1, 1, 2, 0, 0, 0}, + {0, 1, 1, 1, 1, 0, 0}, + {0, 1, 1, 1, 0, 1, 0}, + {0, 1, 1, 1, 0, 0, 1}, + {0, 1, 1, 0, 2, 0, 0}, + {0, 1, 1, 0, 1, 1, 0}, + {0, 1, 1, 0, 1, 0, 1}, + {0, 1, 1, 0, 0, 2, 0}, + {0, 1, 1, 0, 0, 1, 1}, + {0, 1, 1, 0, 0, 0, 2}, + {0, 1, 0, 3, 0, 0, 0}, + {0, 1, 0, 2, 1, 0, 0}, + {0, 1, 0, 2, 0, 1, 0}, + {0, 1, 0, 2, 0, 0, 1}, + {0, 1, 0, 1, 2, 0, 0}, + {0, 1, 0, 1, 1, 1, 0}, + {0, 1, 0, 1, 1, 0, 1}, + {0, 1, 0, 1, 0, 2, 0}, + {0, 1, 0, 1, 0, 1, 1}, + {0, 1, 0, 1, 0, 0, 2}, + {0, 1, 0, 0, 3, 0, 0}, + {0, 1, 0, 0, 2, 1, 0}, + {0, 1, 0, 0, 2, 0, 1}, + {0, 1, 0, 0, 1, 2, 0}, + {0, 1, 0, 0, 1, 1, 1}, + {0, 1, 0, 0, 1, 0, 2}, + {0, 1, 0, 0, 0, 3, 0}, + {0, 1, 0, 0, 0, 2, 1}, + {0, 1, 0, 0, 0, 1, 2}, + {0, 1, 0, 0, 0, 0, 3}, + {0, 0, 4, 0, 0, 0, 0}, + {0, 0, 3, 1, 0, 0, 0}, + {0, 0, 3, 0, 1, 0, 0}, + {0, 0, 3, 0, 0, 1, 0}, + {0, 0, 3, 0, 0, 0, 1}, + {0, 0, 2, 2, 0, 0, 0}, + {0, 0, 2, 1, 1, 0, 0}, + {0, 0, 2, 1, 0, 1, 0}, + {0, 0, 2, 1, 0, 0, 1}, + {0, 0, 2, 0, 2, 0, 0}, + {0, 0, 2, 0, 1, 1, 0}, + {0, 0, 2, 0, 1, 0, 1}, + {0, 0, 2, 0, 0, 2, 0}, + {0, 0, 2, 0, 0, 1, 1}, + {0, 0, 2, 0, 0, 0, 2}, + {0, 0, 1, 3, 0, 0, 0}, + {0, 0, 1, 2, 1, 0, 0}, + {0, 0, 1, 2, 0, 1, 0}, + {0, 0, 1, 2, 0, 0, 1}, + {0, 0, 1, 1, 2, 0, 0}, + {0, 0, 1, 1, 1, 1, 0}, + {0, 0, 1, 1, 1, 0, 1}, + {0, 0, 1, 1, 0, 2, 0}, + {0, 0, 1, 1, 0, 1, 1}, + {0, 0, 1, 1, 0, 0, 2}, + {0, 0, 1, 0, 3, 0, 0}, + {0, 0, 1, 0, 2, 1, 0}, + {0, 0, 1, 0, 2, 0, 1}, + {0, 0, 1, 0, 1, 2, 0}, + {0, 0, 1, 0, 1, 1, 1}, + {0, 0, 1, 0, 1, 0, 2}, + {0, 0, 1, 0, 0, 3, 0}, + {0, 0, 1, 0, 0, 2, 1}, + {0, 0, 1, 0, 0, 1, 2}, + {0, 0, 1, 0, 0, 0, 3}, + {0, 0, 0, 4, 0, 0, 0}, + {0, 0, 0, 3, 1, 0, 0}, + {0, 0, 0, 3, 0, 1, 0}, + {0, 0, 0, 3, 0, 0, 1}, + {0, 0, 0, 2, 2, 0, 0}, + {0, 0, 0, 2, 1, 1, 0}, + {0, 0, 0, 2, 1, 0, 1}, + {0, 0, 0, 2, 0, 2, 0}, + {0, 0, 0, 2, 0, 1, 1}, + {0, 0, 0, 2, 0, 0, 2}, + {0, 0, 0, 1, 3, 0, 0}, + {0, 0, 0, 1, 2, 1, 0}, + {0, 0, 0, 1, 2, 0, 1}, + {0, 0, 0, 1, 1, 2, 0}, + {0, 0, 0, 1, 1, 1, 1}, + {0, 0, 0, 1, 1, 0, 2}, + {0, 0, 0, 1, 0, 3, 0}, + {0, 0, 0, 1, 0, 2, 1}, + {0, 0, 0, 1, 0, 1, 2}, + {0, 0, 0, 1, 0, 0, 3}, + {0, 0, 0, 0, 4, 0, 0}, + {0, 0, 0, 0, 3, 1, 0}, + {0, 0, 0, 0, 3, 0, 1}, + {0, 0, 0, 0, 2, 2, 0}, + {0, 0, 0, 0, 2, 1, 1}, + {0, 0, 0, 0, 2, 0, 2}, + {0, 0, 0, 0, 1, 3, 0}, + {0, 0, 0, 0, 1, 2, 1}, + {0, 0, 0, 0, 1, 1, 2}, + {0, 0, 0, 0, 1, 0, 3}, + {0, 0, 0, 0, 0, 4, 0}, + {0, 0, 0, 0, 0, 3, 1}, + {0, 0, 0, 0, 0, 2, 2}, + {0, 0, 0, 0, 0, 1, 3}, + {0, 0, 0, 0, 0, 0, 4} +}; + +static const double COEF[330][3] = { + {8.70954844857314666e-12, 1.27926950848359881e-09, -2.06865474316332923e-09}, + {1.05783308354771544e+00, -8.02119209663359686e-03, -7.88705651445470723e-02}, + {1.35905954452774837e-02, 8.71267975138422468e-01, 1.04898760410704936e-01}, + {-4.16452026099768252e-02, 1.75465381596434100e-02, 1.00224594702931546e+00}, + {4.50321316661211821e-02, -7.11409155427628892e-02, 3.91232300778902690e-03}, + {1.76675507851922452e-02, -1.32709276116036640e-01, 6.36935270589509828e-02}, + {-5.23434830565911030e-02, 3.77681739012521722e-02, -2.08691145087504179e-02}, + {-2.33722556520224792e-03, -1.57542611462692145e-03, -3.05158628452478807e-03}, + {-8.87678609044812990e-04, 3.83194388837734693e-04, 1.37779212442523083e-03}, + {-2.11519042076831979e-03, 5.82337362515735358e-04, 2.24055108941204821e-04}, + {4.61545125563611917e-04, 7.72869451707915893e-04, -1.10800630143346882e-03}, + {1.05937484157345879e-03, -3.14448681732842211e-04, -1.75129182446198098e-03}, + {1.49045689016363055e-03, -2.09220860101674106e-04, 5.93100338908187697e-04}, + {-3.51246656293852696e-04, -8.20743017485394289e-04, 5.71854064480802862e-04}, + {-9.18204643629581319e-01, -2.27788122702773155e-01, 6.39980793022790623e-02}, + {9.24243491377523679e-05, 7.32841332381495400e-04, -1.55219718415109450e-03}, + {7.13695056804217989e-04, -8.46467621879685712e-05, 6.50202947442505750e-04}, + {1.66640864747485983e-03, -1.24492362771216523e-04, 2.68236502346156410e-04}, + {-7.20253644860527516e-04, 7.81434220384157334e-04, 1.12661089007361367e-03}, + {-6.83033334365238206e-05, 7.27742627159490762e-04, -1.78048843835204584e-03}, + {-3.13431571993316588e-02, -8.57604034845650287e-01, -2.57225920656276863e-01}, + {-6.47867200595898341e-05, -1.16688982572457655e-03, 1.14174511750260031e-03}, + {-5.00713925613324338e-04, -6.87598082111323477e-04, 6.20598069880440176e-04}, + {-8.56716727659588957e-05, 9.74478786593559361e-04, -1.65892838405139512e-03}, + {6.53468478750158263e-04, 7.51662000672516676e-04, -6.73196326298856570e-04}, + {-4.42539011000103941e-02, -2.01965359697350230e-02, -9.94663493761314355e-01}, + {-7.39107395392403087e-04, 5.28870828612476996e-04, 1.00947183860234540e-03}, + {-2.06577300933763214e-03, 9.60215813758718011e-04, -3.27993888180819421e-04}, + {3.47783280638377555e-04, 8.41824316850705743e-04, -8.87458944147930993e-04}, + {1.20960551709587905e+00, -7.07660818059813873e-02, -8.56332806008946491e-03}, + {2.11116509318935269e-04, 7.68490846994171776e-04, -1.63228995491542417e-03}, + {6.47698075356516103e-04, -4.20589129268072884e-04, 1.18354001300614896e-03}, + {-2.78795945253848716e-02, 1.22199201000304547e+00, -2.07383075858847743e-01}, + {-5.32457386680677347e-05, -9.58027320315790677e-04, 9.89667309649038679e-04}, + {-9.03932426306289782e-02, -4.00969232187064692e-02, 1.26285611182120072e+00}, + {-2.19453630740322871e-03, -1.21893190049422620e-03, -1.92293368093085417e-03}, + {1.72950845415964505e-06, -8.93952511560151819e-09, -6.14874900641340649e-06}, + {8.02644554976326974e-06, -6.42543741723487294e-06, -6.07103419227907060e-06}, + {3.20307552755319525e-06, -4.83533743093466500e-06, 9.13563764113473065e-07}, + {-2.18105804067510178e-06, 6.19595552598436322e-07, 5.21392855381760945e-06}, + {-2.43310123604345563e-06, 2.17201813434465818e-06, 1.94098874242362718e-07}, + {-1.56293672065252465e-06, 3.95256011818110372e-06, 1.68792962079201969e-06}, + {-1.37567295252127852e-03, 3.59746071987262106e-04, 7.38927139000157259e-05}, + {4.27822004137219658e-06, -8.80187479967658548e-07, 2.29453131891411977e-06}, + {7.68758937964332534e-06, 2.40909410585557829e-07, 4.69351234070854509e-06}, + {-2.87166709944317033e-06, 7.60223902901142716e-07, 4.57864913314467992e-06}, + {-4.01295140267654560e-06, 2.65929275888376483e-06, -2.36575067819565221e-06}, + {2.32693030513910805e-07, 2.28814396769890308e-06, 1.83526107699893970e-07}, + {-2.18213927011287265e-03, 1.65013083920367864e-03, 2.31992998847323087e-04}, + {-7.70829764693697905e-06, 4.23888841240673345e-07, 7.30018322002944087e-06}, + {-1.23111329452911533e-06, 1.50076529718910084e-06, -1.91139744928209288e-06}, + {-1.68872756433485760e-06, 1.03254236824697979e-06, -1.72081108163607555e-06}, + {1.64276928199709460e-06, -4.96350219553231067e-07, -1.46349385185670297e-06}, + {1.12731767057843682e-03, 5.03104281148445223e-04, 1.36398977654308994e-03}, + {-1.05449609518089293e-06, -4.06952115309007489e-07, 3.53062441379482783e-06}, + {-1.98745923822574166e-06, 4.98021943693208180e-07, 3.92645061370218429e-06}, + {-1.55569377977005097e-07, -4.00262856484093037e-07, -2.49609122397048688e-06}, + {2.18005022830924673e-03, -4.10275057064835439e-05, -2.59776311836759947e-04}, + {5.41337439827552225e-07, -1.88603932528607146e-06, -2.06428606152470051e-06}, + {-6.03243799807140491e-06, -3.75067864464502022e-06, -3.05702776851046742e-06}, + {2.30038011634901016e-03, -1.32581161861259635e-03, -1.07680096899188406e-03}, + {4.46773877910556887e-06, 1.85008408528524772e-08, -2.72851357570281713e-06}, + {-1.49177636513049289e-03, -1.91426739654176659e-04, -1.71206384332753194e-03}, + {2.31661325589237743e-02, 2.26540538563063554e-01, 5.42330337046266139e-02}, + {-1.40563059963100256e-06, -4.50551806294901061e-06, 8.87542894832671347e-06}, + {-1.66780916452391459e-06, 4.12065434881171526e-06, -3.55865035776836702e-06}, + {2.71536622051954390e-07, -3.08564858926584692e-06, -1.52164363662402047e-06}, + {2.66659632027280158e-06, -1.19436686895073481e-06, -3.25738306279285683e-06}, + {-1.43666282346327501e-06, -2.51923473623639690e-06, 5.21205120344175876e-06}, + {2.82954522469612199e-04, -1.59147454710008968e-03, 1.27685773978167098e-03}, + {-3.99471240294241303e-06, 9.97323772325767188e-08, -5.28196823261495307e-06}, + {-6.39858432699424995e-06, -4.59897864440506933e-06, -2.39736149785715891e-06}, + {2.89457420106498109e-06, -3.10427512149489757e-06, 9.75553221437691631e-07}, + {-8.96518259720091581e-07, -5.53996694461914366e-06, 1.03733964032237669e-05}, + {8.82130497168875905e-04, -2.33618402105562365e-03, 1.35100410641244379e-03}, + {-2.14088521029685841e-06, 2.59005410360388117e-06, -9.78713171504927426e-08}, + {-4.50668337071552516e-06, 3.58808570076458002e-06, -1.56159349007541082e-06}, + {-1.52345101244247272e-06, 2.21066768791959578e-06, -2.19555898547246775e-06}, + {2.07334042074768356e-03, -1.56333498489329517e-03, -5.53762940364141767e-04}, + {2.22151748134440108e-06, -4.74729938900429749e-07, -3.46744150304684889e-06}, + {2.95389009221172505e-06, -2.96312023445686329e-06, -9.00385068308695580e-07}, + {-6.47780848348620771e-04, 2.38772263398574292e-03, -8.93908589731968019e-04}, + {9.69501567645025819e-07, 2.41432205872957328e-06, 5.56908291093893837e-07}, + {-6.33392066185247586e-04, 2.38613844267241120e-03, -1.05383725637261472e-03}, + {6.76250135616376785e-02, -5.57799579151454852e-02, 1.83393652374666566e-01}, + {3.53986894266120067e-06, 5.92996717102502093e-06, -7.32378536156402804e-06}, + {5.69667193362453916e-06, 1.20219201908705218e-06, -4.56663805956276925e-06}, + {7.11494218295222192e-07, 2.93069858359131137e-06, 1.23210839732268429e-07}, + {-3.41917893741799928e-06, -1.47435291776966751e-06, 1.07397354370819542e-06}, + {7.30931882734254710e-04, 1.15433149094644884e-03, -2.40026982569019722e-03}, + {-1.22780859907432871e-06, 2.29287908084027789e-06, 1.84270754640877832e-06}, + {7.71579140080615178e-07, 2.92378122615943208e-06, -1.91800935486416413e-07}, + {-3.76107279903559188e-07, -1.83159743461489867e-06, 8.17089655984204466e-07}, + {-1.10830882430058061e-03, -5.10908079549339251e-04, -1.77835176235151705e-03}, + {-1.26839781743699406e-06, -2.86942252006448415e-06, 4.47464983859263005e-06}, + {-1.44518716284694482e-06, -7.03360635528004451e-06, 1.04898109513258675e-05}, + {-4.98687888007460470e-04, 1.86990180752567262e-03, -1.24341018156770089e-03}, + {-2.90479801332704790e-06, -9.24272269110706229e-07, 7.56354222045119151e-07}, + {-1.16451534008294149e-03, -2.34216801827852273e-03, 4.91479264672447288e-03}, + {-7.70970926241258958e-02, 9.35855573900774423e-02, 1.50623807158846906e-01}, + {1.14039905307547484e-06, -1.80664235182388840e-07, -5.15527441317074897e-06}, + {7.50559587697416375e-06, -6.23982034686780714e-06, -5.01245198064126721e-06}, + {2.37840954889385892e-06, -4.15663063190341991e-06, 1.93118829429697603e-06}, + {-1.54903048110950777e-03, 2.65832194444263125e-04, 5.34401520444913940e-04}, + {4.00040634507183718e-06, -2.43965474694277443e-06, 2.88683251413283937e-06}, + {7.72301916160400559e-06, -9.54300275625495457e-07, 5.50777546561020959e-06}, + {-2.28103126593574368e-03, 1.02658341009706066e-03, 1.22010567464172614e-03}, + {-6.32818026002207601e-06, 9.83088209200334157e-07, 5.24316808343458507e-06}, + {1.37175660779395581e-03, 4.01188715721313943e-04, 7.59370199245276625e-04}, + {-3.33184694847917573e-01, 7.82846225823195241e-02, -9.94270054263078074e-02}, + {-1.70108770909324636e-06, -5.10749831734438279e-06, 9.80267482880020635e-06}, + {-1.79301365419055891e-06, 4.44839673308561508e-06, -3.83837422072638712e-06}, + {1.71911692904483371e-04, -1.56077480341044431e-03, 1.30725115579017584e-03}, + {-3.55763938679129477e-06, 1.20558966207589408e-06, -5.94340114624253291e-06}, + {1.02325453537648178e-03, -1.52640960762801372e-03, 3.10973117856692537e-04}, + {3.81842873295820109e-03, -3.02114884453467680e-01, 2.78264587142456665e-01}, + {3.46123498726202961e-06, 5.05929187103208375e-06, -6.85764673719752027e-06}, + {4.47228353489932293e-04, 9.60672217798415784e-04, -2.19382758010531077e-03}, + {2.22711833124298791e-01, -4.14141995162802465e-02, -4.27998216564745015e-01}, + {-1.78271151817048783e-03, -9.81039111371464307e-04, -1.37513011841553174e-03}, + {3.35305394560947434e-10, -1.26710751613412498e-09, 3.54248685940916630e-09}, + {-9.26917423371698135e-09, 6.21190912597491263e-09, 1.86942252233812667e-08}, + {-1.56687696151180944e-09, -5.44315731376698864e-09, 1.93822974337010123e-09}, + {7.52897716393974292e-10, -3.48923168136394679e-10, -5.94217786087369859e-10}, + {2.52116855170569920e-10, -2.48216903975251313e-09, 1.01699001303634518e-09}, + {3.72215577457146729e-09, 4.51910314724912610e-10, -6.15361639422218332e-09}, + {-2.62088816666700142e-07, 3.23631086683010168e-07, 8.85302852722882894e-07}, + {-1.30537319842360944e-08, 1.46808588619151692e-08, 2.67574040702101001e-09}, + {-1.23991327621864045e-08, 2.61298349069072344e-08, -4.58919307373337193e-09}, + {5.03079244928983371e-09, -6.73783119575777079e-10, -1.13935871848269699e-08}, + {9.09065785148488459e-09, -1.04304054004966673e-08, -3.23123813816827976e-09}, + {9.55627910137479830e-10, -1.41129563591135820e-08, -1.75594400131373618e-09}, + {-1.05549669436946769e-07, 8.47284096194811896e-08, 6.70761880091491625e-07}, + {-5.92079330008488114e-10, 6.31702118392141188e-09, -4.51534448719925763e-09}, + {-1.04033970327321867e-09, 4.67775485013532943e-09, 2.79348504744758586e-09}, + {5.38758108958869997e-09, -9.55380699552144108e-09, 6.16488249338686956e-11}, + {1.12057409185073453e-09, -3.00645183748393663e-09, -2.14940637510707688e-09}, + {-6.27004681934967278e-07, 8.59159786402940127e-07, 2.73192537668387470e-07}, + {7.36784189214745311e-10, -8.12761968838060511e-10, -2.43226564583531868e-09}, + {1.25546123497244366e-09, -6.98609614602219153e-10, -5.29894812750786315e-09}, + {-8.88351475714088679e-10, 1.37132565025677167e-09, 1.92497813869541012e-09}, + {6.10992637326349119e-07, -6.13496367368217277e-07, -2.19901889726877020e-06}, + {-8.59090437677068053e-11, 2.72772732179404898e-09, 1.54554039011323141e-09}, + {-4.58798915525804318e-10, 4.54384851966693759e-09, 3.63189350816028877e-09}, + {9.93115786933340683e-08, 1.63700862245048928e-07, -1.71397937400244449e-07}, + {-1.62985361318312982e-09, -3.10762126448649312e-09, 1.76193495557419588e-09}, + {6.27207737564569601e-07, -1.49343052365004934e-06, 8.16168870109573730e-08}, + {1.42518738380244172e-03, -3.47531891583186285e-04, -2.98661838800559913e-04}, + {8.98157254125564464e-09, -8.24242643235328920e-09, -5.34769730234363472e-09}, + {-2.17776999489327494e-08, -4.47141107473569832e-09, -1.10218517090920898e-08}, + {3.19614509858290319e-09, -3.32861183754973311e-09, 9.92016746526047655e-11}, + {-2.91660393059167689e-09, 5.59829099744391101e-09, 1.70080685646389895e-09}, + {1.22479524179014421e-09, 9.20737683318684219e-09, -1.10618757209746121e-10}, + {7.70594587548882257e-09, -1.33267446898667659e-06, 4.52812675308736368e-07}, + {9.46080642993951670e-09, -1.95483249032513129e-08, -1.23592694620255905e-08}, + {-2.02330094345448686e-09, 1.18198534293512125e-10, 2.34746776184291406e-09}, + {4.00839940406516604e-09, -4.80716730311137042e-09, 5.25802457129742606e-09}, + {-2.53115202408782380e-09, 2.05563177591017165e-10, 5.46003270374129102e-09}, + {3.24841319972028232e-08, -1.24284705839720552e-06, 4.97326549863015555e-07}, + {1.37729661009444726e-09, -1.67903983772088594e-09, -5.62083748989472554e-09}, + {-3.53256937590806785e-10, 4.49320892992322030e-09, -4.02300486673778934e-09}, + {2.48976475547557641e-09, -6.97256366533061112e-09, 1.43185084622299286e-09}, + {-4.38617299338556199e-09, 9.45081248826811111e-08, -2.91197460585562728e-07}, + {3.24429103026879773e-09, -1.71647943601749287e-09, 2.71076100455402980e-09}, + {3.86933235105302309e-09, -2.82628156988984358e-09, 8.24455756442965537e-09}, + {-7.46614068323353530e-07, 1.27696340529665289e-06, 6.88413034833322557e-07}, + {-5.78118683480788320e-09, 1.34319005917760137e-09, -1.15898873831454807e-09}, + {4.42686972671260670e-07, 6.41810588767341775e-07, -1.16058405342719939e-08}, + {2.24399192788231686e-03, -1.35129336477888174e-03, -7.39944244498236844e-04}, + {7.47869199901884940e-09, -2.68762612165573955e-09, -7.41584788022109365e-09}, + {1.80867308283150230e-09, -2.21500551234043996e-09, 1.86995768869380186e-09}, + {-5.05514829302056157e-09, 4.74048706539109688e-09, 2.52998993977016085e-09}, + {1.32441967115592973e-09, 5.70339246663831290e-09, 7.13448300437846683e-10}, + {1.19767475292940212e-06, 6.72445227582811568e-07, -1.97500319605841551e-06}, + {-1.70612399208458498e-09, 1.07145120553653328e-09, 1.73225882249550267e-09}, + {1.15369127445807962e-09, -5.80362996549510513e-09, 9.33515653667171819e-10}, + {3.38692740520230018e-09, 3.72531013675958533e-09, -3.18062756687886861e-09}, + {1.14787653780236421e-06, -1.84917201319622368e-06, -2.44834286920736499e-07}, + {1.45558928799083276e-09, 1.12720083267348059e-09, 9.00940544390493869e-10}, + {2.09654001104286891e-09, 4.92913422578400429e-09, 3.04938074791039071e-10}, + {3.54033623213741155e-07, 1.07259516691213860e-06, -6.03027205987524684e-07}, + {-2.72038239157446071e-09, -1.60070143945256760e-09, 6.03853855807301443e-10}, + {-2.03235662485238069e-06, -1.03151962834260348e-06, 1.99637918628457062e-06}, + {-1.26261175077493210e-03, -4.98503988506484859e-04, -1.03875859619143593e-03}, + {6.43182729298530376e-10, 8.01776645076301975e-10, -1.83589794755523172e-09}, + {4.01805119037978997e-09, -5.63673552278487477e-10, -1.09102650663883693e-08}, + {-1.48648961195707585e-09, 5.01067861508053269e-09, 2.99132781045319263e-09}, + {-8.91404754824534629e-07, 7.49163968581634775e-07, 2.12542215183124383e-06}, + {2.38642574451608525e-09, -3.47605810802065207e-09, 3.86935566920598717e-10}, + {-2.80031986488182838e-09, -4.25160427697246490e-11, 2.24182921879090280e-09}, + {-1.26991357818351247e-07, -1.45348284568834647e-07, 5.68792533226815389e-07}, + {1.39227229745131353e-09, -1.84849578699353145e-09, 2.24967258190267305e-09}, + {-1.15462500328497586e-06, 1.84347590761761086e-06, 3.64918716654494962e-07}, + {-2.09357112083411985e-03, 1.60820400301404873e-05, 2.27418117008655948e-04}, + {-1.04484803378768198e-08, 4.86043558178828050e-09, 2.00996588123336650e-09}, + {1.44040971927772432e-08, 1.42223015309195233e-09, 1.99778974613318283e-09}, + {-1.62414574166394599e-07, -1.31976785339561840e-06, 4.43918084507000099e-07}, + {3.73061943836905385e-09, 1.00036822436866402e-08, -1.05450977117005351e-09}, + {-2.06551932971539565e-07, -9.72167971235462190e-07, 4.28861904300768815e-07}, + {-2.16051814014425313e-03, 1.48780488507118812e-03, 7.79940397419977911e-04}, + {-4.80544204428667854e-09, -1.09870773590259319e-09, 6.58876991984844174e-09}, + {1.31575045692056136e-06, 4.32430764481131318e-07, -1.55255090541518703e-06}, + {1.28823975640215602e-03, 4.04521283440268135e-04, 1.76186984141882253e-03}, + {-1.09767251093991436e-01, -4.94112205838347640e-02, -5.43102978164306804e-02}, + {7.93691223854864347e-10, 1.54639511196208446e-08, -1.71518303448969789e-08}, + {2.56523843833456056e-09, -2.31047392329486456e-09, -4.29758133398648601e-09}, + {-9.87725901069325118e-09, 4.28127375218245732e-09, 2.02888056355376989e-09}, + {3.21762172461603768e-10, -5.82937505211322815e-09, 3.88293127512318037e-09}, + {1.63250610252241302e-09, -7.02161705168347083e-09, 3.46592492032893329e-09}, + {-1.44272117683086343e-07, -4.40408510988914148e-07, 5.92746408872857344e-07}, + {2.71961467235293242e-09, -1.47466668633244868e-08, 2.89637452632884873e-08}, + {1.47637712476396399e-08, 1.16406781783262581e-09, 2.04904540557215853e-09}, + {-5.53709807865621073e-09, 7.05512286092169205e-09, 1.56159114805820565e-09}, + {5.29268649740455288e-09, 2.10616986628942016e-08, -3.03219004488264332e-08}, + {1.79978890693655025e-07, 7.95085399132693105e-07, -4.78366567607801940e-07}, + {-4.03847393894152251e-10, 2.90357085597214848e-09, 1.12992165623992946e-09}, + {2.99031871486832301e-09, -1.37951879780606745e-09, 2.41048263988075107e-09}, + {1.26882357398550027e-09, 1.30631467101793852e-09, 7.99574240151201820e-10}, + {-1.41169562567489137e-08, 1.27148955713198356e-06, -2.89386439707162157e-07}, + {-2.68794415198003733e-09, 8.73673404455654889e-10, 2.89557382238125882e-09}, + {-4.90264437380538709e-09, 1.89207244316591527e-09, 2.25393465003165261e-09}, + {-3.58274654665979853e-08, 2.91386646529383231e-07, -4.98477764412919022e-08}, + {1.65722165851311942e-09, -1.11673743863338615e-09, -4.14131162695952071e-09}, + {-1.47751280626939874e-07, -2.41471865000848773e-07, -8.53552350049691100e-07}, + {-2.24352957583577790e-04, 1.60900273524284708e-03, -1.32260753549593617e-03}, + {2.05497643901431104e-09, 1.38702982710459111e-08, -3.09887516689033582e-09}, + {3.39770491949997755e-09, 9.41613393506957053e-09, -7.09844738544518350e-10}, + {7.86209687630989862e-10, 1.93556837224662104e-10, -6.58630930350234678e-09}, + {-6.86841181152253455e-10, -5.57194149153339424e-09, 1.41214109156129197e-09}, + {2.59516074158083754e-07, 1.30703181255419770e-06, -4.02454784192984860e-07}, + {-5.79425202262839889e-10, 4.05071760856134944e-09, 3.02384985106929349e-09}, + {4.00677924866643664e-09, -2.25614611715219127e-09, 7.52819043214891792e-09}, + {2.34003759425061020e-09, 5.27462258592681366e-09, -2.05723854618256041e-10}, + {2.29340174767722615e-07, 1.05507868574435809e-06, -4.45904844964539748e-07}, + {-3.91634245866523401e-09, 1.07849931763048801e-09, 1.85542686770290288e-09}, + {-6.62166513287765213e-09, 3.86355018811013196e-09, -1.87861701195224384e-09}, + {1.32112240848469842e-07, 4.39339645861430705e-08, -1.59384598983486336e-06}, + {2.02488462108796341e-09, -1.48427112267590644e-09, -4.32055485832805175e-09}, + {-4.27701540045566375e-07, -1.46229443391283215e-06, -2.38186369433401879e-07}, + {-9.86744509368740232e-04, 1.91104095070606826e-03, -8.17774843405986713e-04}, + {2.06891823117949514e-10, -2.64060942556376688e-09, 1.86419366055012858e-09}, + {8.33785634979378187e-09, -1.00697171434571686e-08, -2.84106664583116952e-09}, + {5.07057938692323518e-09, -9.56246298811080919e-09, -6.33399999117045809e-11}, + {-6.78808357162941078e-08, -2.21612941845184680e-07, 9.42031624998063144e-08}, + {-3.04300065007145903e-09, 5.64120231083542478e-09, 1.65718606892628628e-09}, + {3.76240642807612602e-09, -4.58941407446844529e-09, 5.06162500801821125e-09}, + {7.25149885354159363e-07, -1.18149759075966698e-06, -6.82406347277120240e-07}, + {-4.84358128605144600e-09, 4.56893046833772853e-09, 2.67044331092591847e-09}, + {-2.54939737986958903e-07, -1.06106228658746360e-06, 5.04013386790069795e-07}, + {-2.17097468872509735e-03, 1.41624400187313607e-03, 8.11305605779899562e-04}, + {2.24635331169675823e-10, -6.02144184513875302e-09, 4.15827878380570226e-09}, + {-4.55408258326350790e-09, 6.20319154376325343e-09, 2.08760821823750220e-09}, + {2.10871853867367065e-07, -4.29346688506603014e-07, 1.15683623843482186e-07}, + {1.00732072683129559e-09, 3.88267751283422058e-11, -6.73798626615873530e-09}, + {5.34506627847264326e-09, -8.01262819982717645e-08, 1.60888846226225901e-06}, + {5.83419066552946048e-04, -2.36474094848551555e-03, 8.79373865688287898e-04}, + {-4.85158746510450101e-10, -6.78789624508624456e-09, 4.95385649168511577e-09}, + {3.47485142271342085e-07, 5.60944792101468470e-07, -4.35887910682497548e-07}, + {5.75824910919892421e-04, -2.18618554413632388e-03, 1.22736498224538170e-03}, + {-2.51838883195707221e-02, -8.23487774284355212e-02, 3.33658831723806573e-02}, + {-8.70167529698484543e-09, -1.37080219501928280e-08, 1.80728228771354082e-08}, + {-4.67111571644807100e-09, -2.72041008123058425e-09, 7.06648883852523113e-09}, + {7.26183221906172727e-10, -6.77816339167414128e-09, 4.52883232651690726e-09}, + {5.28852302228433047e-09, 6.47161005340457507e-09, -8.67298467766008940e-09}, + {-2.25465519365641853e-07, -6.46057585221293529e-07, 3.48151143400587948e-07}, + {-1.30051025504229756e-09, -3.25062288891730944e-09, 2.01775679498084060e-09}, + {-5.12724809831333062e-09, 9.33902577666956280e-10, -6.96327353416625883e-10}, + {-3.10810940873373909e-09, -7.49756534634826721e-10, 6.87357185058523612e-10}, + {-1.52109221995821997e-06, -4.22908767925417317e-07, 1.38629667568307413e-06}, + {1.42955317028459206e-09, -7.02968461219199980e-10, -3.81617160094549490e-09}, + {2.53707400921232562e-09, -1.60727622877665510e-09, -4.18765366827500429e-09}, + {-2.14750738948554787e-07, -6.40554276953864132e-07, 3.76128531993924486e-07}, + {3.83073214815787821e-09, 4.50296289838947317e-10, 2.29523194894554194e-09}, + {4.76340728555735282e-07, 6.83235613037347367e-07, -4.72205395646296822e-07}, + {-6.10651996176347607e-04, -1.06790499934057291e-03, 2.29083496655867842e-03}, + {3.95497823379997726e-09, 1.38236928154400474e-09, -6.26218820548585242e-09}, + {1.11904936705986557e-09, -1.37869946362223494e-08, -9.34049783699042457e-10}, + {1.25499246411697740e-09, -2.73635453185150368e-09, -2.91506864740637139e-09}, + {-3.59882924006599270e-07, 1.32511373732895413e-06, -1.55110207063907657e-07}, + {1.07068498511608823e-09, 8.92087770321126072e-09, 2.62826524433101838e-10}, + {-2.69316546841480431e-09, 9.61138280075601870e-10, 5.19946977139973399e-09}, + {-5.92563579700916554e-07, -1.05071339539294234e-06, 1.56249964602256375e-07}, + {1.32198180180509439e-09, 5.16087961255351502e-09, 8.46339526239248130e-10}, + {2.07323220008381881e-06, 1.02309267446332522e-06, -2.07661522726165781e-06}, + {1.31402366846389393e-03, 3.78229792813366064e-04, 1.77496793932758741e-03}, + {8.59301428624004160e-10, -6.83071707530125138e-09, 3.36249680876754553e-09}, + {5.27310424491833629e-09, 2.09999085065692981e-08, -3.10459945807028959e-08}, + {-8.88666080375855039e-08, 4.60897593930476024e-07, 7.41576575386676540e-07}, + {-4.85540663230921155e-10, -5.58243438975036810e-09, 7.40450811775872353e-10}, + {4.03141117225058743e-07, 1.52035531639227450e-06, 9.06206514897367477e-08}, + {5.61075629915620496e-04, -2.05847905628765053e-03, 1.12849817492909434e-03}, + {5.11216541321246609e-09, 7.26292920250060092e-09, -8.97145741030058730e-09}, + {-4.26211688914213127e-07, -7.03366608210270750e-07, 6.27995585866791828e-07}, + {1.15309052943982646e-03, 2.34474318844151959e-03, -4.91856748507475423e-03}, + {1.01104427799588961e-01, -4.22361682938472982e-02, -1.88750007538552200e-01}, + {3.94738332298860684e-10, -7.81372397340440727e-10, 4.06815717224340290e-09}, + {-8.61483928638051566e-09, 5.37427180535843263e-09, 1.81738104426676372e-08}, + {-8.48011268844706123e-10, -5.33803143354383280e-09, 2.99703953494934172e-10}, + {3.89154099408092063e-07, -2.44166311268514957e-07, -8.03240371135063858e-07}, + {-1.20249536439409610e-08, 1.48908931921210019e-08, 1.88292573199966284e-09}, + {-1.16401289163015065e-08, 2.57866422936903206e-08, -5.27022399332555125e-09}, + {1.37065399911928676e-07, 2.16494406102361175e-08, -7.63924557662179482e-07}, + {-6.94754161319199870e-10, 6.65038621394664631e-09, -4.31779645371221932e-09}, + {4.72542155592614588e-07, -7.58546986886782931e-07, -2.35913417925837088e-07}, + {1.46133817312113241e-03, -3.25193103208258009e-04, -3.06625181254991741e-04}, + {9.35794082672593210e-09, -7.92923574022275091e-09, -5.41426242728348939e-09}, + {-2.15279239157428748e-08, -4.16754339024882903e-09, -1.12896482995505920e-08}, + {2.60645369870582400e-10, 1.44616071127263122e-06, -3.63334053799999057e-07}, + {9.17105741349288905e-09, -2.02295233654725681e-08, -1.20002956877085509e-08}, + {-1.27759226226098477e-07, 1.28193771791124470e-06, -5.83097827522305323e-07}, + {2.26880791869919426e-03, -1.34042850080092401e-03, -7.65092051285704835e-04}, + {7.03374036792325796e-09, -2.53508958270032281e-09, -7.66132998708535240e-09}, + {-9.71978722189015265e-07, -5.57836512454779054e-07, 1.96329328074063003e-06}, + {-1.26115140811304343e-03, -4.81792074617704632e-04, -1.06803272537897391e-03}, + {1.19419564863885497e-01, 5.07766738901840875e-02, 4.87642090320925953e-02}, + {1.14090414893297520e-09, 1.56073433760228752e-08, -1.78054684078429726e-08}, + {3.03285130343056153e-09, -1.58615337531031741e-09, -4.94928394101368241e-09}, + {2.64483280249840080e-07, 2.97155396291660413e-07, -5.41608085095034164e-07}, + {2.68757552324139226e-09, -1.41400907649469332e-08, 2.93255796729452456e-08}, + {-2.11094617584561828e-07, -6.56355695552793272e-07, 3.72180321686621518e-07}, + {-2.55073452371079590e-04, 1.57943859317488818e-03, -1.29154484940938240e-03}, + {1.40049266628139435e-09, 1.40747080656922208e-08, -2.58792021839981956e-09}, + {-2.12330362681090179e-07, -1.30522733223815968e-06, 5.84417623253341567e-07}, + {-9.33144849909676392e-04, 1.90305575962152547e-03, -8.35564417983726418e-04}, + {1.81624805201406961e-02, 6.84911174969819458e-02, -2.28291882522520390e-02}, + {-8.25231299961259879e-09, -1.40227519596081152e-08, 1.78809529925716415e-08}, + {1.90689491530449118e-07, 7.01057736002264065e-07, -4.26430629252294580e-07}, + {-5.85146839837499930e-04, -1.07311215649546045e-03, 2.31986890222730339e-03}, + {-1.05962397073886522e-01, 5.51532131360410807e-02, 1.87542648909451215e-01}, + {-1.37499370823599516e-03, -8.49619409242363438e-04, -1.18180356709159952e-03} +}; + +static const double INTERCEPT[3] = { + -1.29208772400146188e+00, + 6.62251952866635918e+00, + -1.35908984683965173e-01 +}; +// END AUTO-GENERATED COEFFICIENTS + +inline void compute_poly_features(const double x[7], double out[330]) { + for (int i = 0; i < N_FEATURES; ++i) { + double val = 1.0; + for (int j = 0; j < N_INPUTS; ++j) { + if (POWERS[i][j] != 0) { + double base = x[j]; + int exp = POWERS[i][j]; + // Fast integer exponentiation (max exp = 4) + double p = 1.0; + for (int e = 0; e < exp; ++e) + p *= base; + val *= p; + } + } + out[i] = val; + } +} + +} // namespace detail + +struct RGB { + unsigned char r, g, b; +}; + +/** + * Mix two RGB colors using polynomial pigment mixing. + * + * This performs polynomial pigment-style RGB interpolation. + * + * @param r1,g1,b1 First color (0-255) + * @param r2,g2,b2 Second color (0-255) + * @param t Mixing ratio: 0.0 = all color1, 1.0 = all color2 + * @param out_r,out_g,out_b Output color (0-255) + */ +inline void lerp(unsigned char r1, unsigned char g1, unsigned char b1, + unsigned char r2, unsigned char g2, unsigned char b2, + float t, + unsigned char* out_r, unsigned char* out_g, unsigned char* out_b) { + // Clamp t + if (t <= 0.0f) { + *out_r = r1; *out_g = g1; *out_b = b1; + return; + } + if (t >= 1.0f) { + *out_r = r2; *out_g = g2; *out_b = b2; + return; + } + + double x[7] = { + static_cast<double>(r1), static_cast<double>(g1), static_cast<double>(b1), + static_cast<double>(r2), static_cast<double>(g2), static_cast<double>(b2), + static_cast<double>(t) + }; + + double features[330]; + detail::compute_poly_features(x, features); + + // Dot product: features @ COEF + INTERCEPT + for (int c = 0; c < 3; ++c) { + double sum = detail::INTERCEPT[c]; + for (int i = 0; i < detail::N_FEATURES; ++i) { + sum += features[i] * detail::COEF[i][c]; + } + // Clamp to [0, 255] and truncate (matches numpy astype(int) behavior) + int val = static_cast<int>(sum); + if (val < 0) val = 0; + if (val > 255) val = 255; + + if (c == 0) *out_r = static_cast<unsigned char>(val); + else if (c == 1) *out_g = static_cast<unsigned char>(val); + else *out_b = static_cast<unsigned char>(val); + } +} + +/** + * Convenience overload returning an RGB struct. + */ +inline RGB lerp(unsigned char r1, unsigned char g1, unsigned char b1, + unsigned char r2, unsigned char g2, unsigned char b2, + float t) { + RGB result; + lerp(r1, g1, b1, r2, g2, b2, t, &result.r, &result.g, &result.b); + return result; +} + +} // namespace filament_mixer + +#endif // FILAMENT_MIXER_MODEL_HPP diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index 88d87ddb26..0888e1bb55 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -970,9 +970,9 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p region_config.sparse_infill_rotate_template.value); params.fixed_angle = !region_config.sparse_infill_rotate_template.value.empty(); - // Orca: special case; apply smoothing factor only for Hilbert Curve sparse infill. - // FillHilbertCurve::generate clamps and validates the value itself. - if (params.pattern == ipHilbertCurve) + // Orca: the smoothing factor only applies to the sparse infill patterns that + // implement it. The fills clamp and validate the value themselves. + if (is_smoothable_infill_pattern(params.pattern, params.multiline)) params.smooth_factor = 0.01 * region_config.sparse_infill_smooth_factor.value; } else { const bool top_layer_direction_set = surface.is_top() && region_config.top_layer_direction.value >= 0.; diff --git a/src/libslic3r/Fill/Fill3DHoneycomb.cpp b/src/libslic3r/Fill/Fill3DHoneycomb.cpp index 5908f854de..ad5f8918fd 100644 --- a/src/libslic3r/Fill/Fill3DHoneycomb.cpp +++ b/src/libslic3r/Fill/Fill3DHoneycomb.cpp @@ -2,6 +2,7 @@ #include "../ShortestPath.hpp" #include "../Surface.hpp" #include "FillBase.hpp" +#include "FillCornerSmoothing.hpp" #include "Fill3DHoneycomb.hpp" namespace Slic3r { @@ -271,6 +272,9 @@ void Fill3DHoneycomb::_fill_surface_single( for (Polyline &pl : polylines){ pl.translate(bb.min); pl.simplify(5 * spacing); // simplify to 5x line width + // Orca: round the corners of the octahedral wave. The layers where the wave degenerates to a + // straight line have no corner to round. + smooth_polyline_corners(pl, params.smooth_factor, scaled<double>(params.resolution)); } // Apply multiline offset if needed diff --git a/src/libslic3r/Fill/FillConcentric.cpp b/src/libslic3r/Fill/FillConcentric.cpp index a75d2ed7d3..1882f7a656 100644 --- a/src/libslic3r/Fill/FillConcentric.cpp +++ b/src/libslic3r/Fill/FillConcentric.cpp @@ -5,6 +5,7 @@ #include "Arachne/WallToolPaths.hpp" #include "FillConcentric.hpp" +#include "FillCornerSmoothing.hpp" #include <libslic3r/ShortestPath.hpp> namespace Slic3r { @@ -32,12 +33,32 @@ void FillConcentric::_fill_surface_single( Polygons loops = to_polygons(contracted); - ExPolygons last { std::move(contracted) }; + ExPolygons last { contracted }; while (! last.empty()) { last = offset2_ex(last, -(distance + min_spacing/2), +min_spacing/2); append(loops, to_polygons(last)); } + // Orca: round the corners of the loops. Unlike the other patterns these are never clipped to the + // fill region - they are its offsets - so a corner may only be rounded where the curve replacing it + // stays inside. Rounding cuts toward the inside of the turn, which around a hole, at a concave + // feature or across a thin region is outside the fill and would put the extrusion over a wall. + // The reach is capped at half the distance between two loops as well: a loop is as long as the + // object, and a corner cut by half of its side would swallow the neighbouring loops. + auto corner_stays_inside = [&contracted](const Vec2d &from, const Vec2d &to) { + // The straight chord between the ends of the curve is the deepest the curve can cut. + for (const double t : { 0.25, 0.5, 0.75 }) { + const Vec2d sample = from + t * (to - from); + const Point point(coord_t(sample.x()), coord_t(sample.y())); + if (std::none_of(contracted.begin(), contracted.end(), + [&point](const ExPolygon ®ion) { return region.contains(point); })) + return false; + } + return true; + }; + smooth_polygons_corners(loops, params.smooth_factor, scaled<double>(params.resolution), 0.5 * distance, + corner_stays_inside); + // generate paths from the outermost to the innermost, to avoid // adhesion problems of the first central tiny loops loops = union_pt_chained_outside_in(loops); diff --git a/src/libslic3r/Fill/FillCornerSmoothing.cpp b/src/libslic3r/Fill/FillCornerSmoothing.cpp new file mode 100644 index 0000000000..dbce39d572 --- /dev/null +++ b/src/libslic3r/Fill/FillCornerSmoothing.cpp @@ -0,0 +1,242 @@ +#include <array> + +#include "FillCornerSmoothing.hpp" + +namespace Slic3r { + +// Turns sharper than this are left untouched: both ends of the curve replacing such a corner nearly +// coincide, so the corner would be rounded into a degenerate loop instead of a hairpin. +static constexpr const double min_smoothed_turn_cosine = -0.9; + +// The control points are expressed in the (incoming, outgoing) basis of the corner, which is not +// orthonormal for turns other than a right angle. +using QuinticBezier = std::array<Vec2d, 6>; + +static bool is_bezier_flat(const QuinticBezier &curve, const Vec2d &incoming, const Vec2d &outgoing, const double deviation) +{ + // A Bezier curve stays inside the convex hull of its control points. Therefore, keeping every + // control point within a deviation-wide strip around the endpoint chord conservatively bounds the + // flattening error. The cross product is the perpendicular distance scaled by the chord length; + // comparing squared values avoids a square root. + auto in_plane = [&incoming, &outgoing](const Vec2d &c) { return c.x() * incoming + c.y() * outgoing; }; + const Vec2d chord = in_plane(curve.back() - curve.front()); + const double chord_length_sq = chord.squaredNorm(); + const double max_cross_sq = deviation * deviation * chord_length_sq; + + for (size_t i = 1; i + 1 < curve.size(); ++i) { + const Vec2d offset = in_plane(curve[i] - curve.front()); + const double cross = chord.x() * offset.y() - chord.y() * offset.x(); + if (cross * cross > max_cross_sq) + return false; + } + return true; +} + +static void subdivide_bezier(const QuinticBezier &curve, QuinticBezier &left, QuinticBezier &right) +{ + // Split the curve at t = 0.5 using de Casteljau's algorithm. Each averaging level contributes one + // control point to the left half and one to the right half; the latter is filled backwards to keep + // both resulting control polygons in their original parameter direction. + QuinticBezier subdivision = curve; + left.front() = subdivision.front(); + right.back() = subdivision.back(); + for (size_t level = 1; level < curve.size(); ++level) { + for (size_t i = 0; i + level < curve.size(); ++i) + subdivision[i] = 0.5 * (subdivision[i] + subdivision[i + 1]); + left[level] = subdivision.front(); + right[curve.size() - level - 1] = subdivision[curve.size() - level - 1]; + } +} + +static void flatten_bezier( + const QuinticBezier &curve, const Vec2d &incoming, const Vec2d &outgoing, const double deviation, std::vector<Vec2d> &output) +{ + // Subdivide to at least depth 1 so a rounded corner cannot collapse to a single diagonal chord. + // A uniform subdivision depth keeps samples at equal parameter intervals t = k / 2^depth, + // avoiding abrupt segment-length jumps at adaptive-depth boundaries. + static constexpr size_t max_depth = 16; + + std::vector<QuinticBezier> subcurves(2); + subdivide_bezier(curve, subcurves[0], subcurves[1]); + + for (size_t depth = 1; depth < max_depth; ++depth) { + bool all_flat = true; + for (const QuinticBezier &c : subcurves) + if (!is_bezier_flat(c, incoming, outgoing, deviation)) { + all_flat = false; + break; + } + if (all_flat) + break; + std::vector<QuinticBezier> finer(subcurves.size() * 2); + for (size_t i = 0; i < subcurves.size(); ++i) + subdivide_bezier(subcurves[i], finer[i * 2], finer[i * 2 + 1]); + subcurves = std::move(finer); + } + + // The curve start is deliberately omitted so it can be shared with the straight leg feeding into it. + output.clear(); + output.reserve(subcurves.size()); + for (const QuinticBezier &c : subcurves) + output.emplace_back(c.back()); +} + +const std::vector<Vec2d>& CornerSmoother::curve_coefficients( + const double corner_distance, const Vec2d &incoming, const Vec2d &outgoing) +{ + const double cosine = incoming.dot(outgoing); + // Corners of the same size and turn angle are congruent, so they flatten identically. An infill + // path walks over the very same corner over and over again, the Hilbert curve over a single one. + if (m_has_cached_coefficients && corner_distance == m_cached_distance && cosine == m_cached_cosine) + return m_cached_coefficients; + + // One canonical corner running from -corner_distance along the incoming leg to corner_distance + // along the outgoing one. At each end, the first three control points are collinear and equally + // spaced: the tangent follows the adjoining straight leg and the second derivative is zero. The + // endpoint curvature is therefore zero, giving G2 joins to both legs. + const double d = corner_distance; + const QuinticBezier corner_curve {{ + {-d, 0.}, {-0.7 * d, 0.}, {-0.4 * d, 0.}, {0., 0.4 * d}, {0., 0.7 * d}, {0., d} + }}; + // Retain a finite positive tolerance if the smoother was set up with an invalid one. + const double deviation = m_tolerance > 0. && std::isfinite(m_tolerance) ? m_tolerance : EPSILON; + flatten_bezier(corner_curve, incoming, outgoing, deviation, m_cached_coefficients); + + m_cached_distance = corner_distance; + m_cached_cosine = cosine; + m_has_cached_coefficients = true; + return m_cached_coefficients; +} + +bool CornerSmoother::is_on_straight_run(const Vec2d &previous, const Vec2d &vertex, const Vec2d &next) +{ + const Vec2d incoming_leg = vertex - previous; + const Vec2d outgoing_leg = next - vertex; + const double incoming_length = incoming_leg.norm(); + const double outgoing_length = outgoing_leg.norm(); + // A vertex repeating one of its neighbours carries no direction of its own. + if (incoming_length < EPSILON || outgoing_length < EPSILON) + return true; + + const Vec2d incoming = incoming_leg / incoming_length; + const Vec2d outgoing = outgoing_leg / outgoing_length; + return incoming.dot(outgoing) > 0. && + std::abs(incoming.x() * outgoing.y() - incoming.y() * outgoing.x()) < EPSILON; +} + +void CornerSmoother::round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next) +{ + m_corner_points.clear(); + + const Vec2d incoming_leg = corner - previous; + const Vec2d outgoing_leg = next - corner; + const double incoming_length = incoming_leg.norm(); + const double outgoing_length = outgoing_leg.norm(); + if (incoming_length < EPSILON || outgoing_length < EPSILON) { + m_corner_points.emplace_back(corner); + return; + } + + const Vec2d incoming = incoming_leg / incoming_length; + const Vec2d outgoing = outgoing_leg / outgoing_length; + const double cross = incoming.x() * outgoing.y() - incoming.y() * outgoing.x(); + // A collinear vertex is no corner at all, and a hairpin cannot be rounded, see above. + if (std::abs(cross) < EPSILON || incoming.dot(outgoing) < min_smoothed_turn_cosine) { + m_corner_points.emplace_back(corner); + return; + } + + // Consuming at most half of the shorter leg keeps the curves of two adjacent corners apart. + double corner_distance = m_corner_distance_ratio * std::min(incoming_length, outgoing_length); + if (m_max_corner_distance > 0.) + corner_distance = std::min(corner_distance, m_max_corner_distance); + + const Vec2d curve_start = corner - corner_distance * incoming; + const Vec2d curve_end = corner + corner_distance * outgoing; + if (m_corner_filter && !m_corner_filter(curve_start, curve_end)) { + m_corner_points.emplace_back(corner); + return; + } + + const std::vector<Vec2d> &coefficients = curve_coefficients(corner_distance, incoming, outgoing); + m_corner_points.reserve(coefficients.size() + 1); + m_corner_points.emplace_back(curve_start); + for (const Vec2d &coefficient : coefficients) + m_corner_points.emplace_back(corner + coefficient.x() * incoming + coefficient.y() * outgoing); +} + +// Rounds the corners of a scaled point sequence. A polygon closes implicitly, so all of its vertices +// are corners; a polyline is an open path that keeps both of its ends, even where they coincide - a +// path returning to where it started retraces its way back and is not a loop. +static Points smooth_corners(const Points &points, const bool polygon, CornerSmoother &smoother) +{ + // A polygon has no free ends, so its first vertex is a corner like any other. Rounding it takes + // feeding the smoother the last vertex first, whose own output point is then dropped again. + size_t skip = polygon ? 1 : 0; + + Points smoothed; + smoothed.reserve(2 * points.size()); + auto emit = [&smoothed, &skip](const Vec2d &point) { + if (skip > 0) { + --skip; + return; + } + smoothed.emplace_back(coord_t(std::floor(point.x() + 0.5)), coord_t(std::floor(point.y() + 0.5))); + }; + + if (polygon) + smoother.push(points.back().cast<double>(), emit); + for (const Point &point : points) + smoother.push(point.cast<double>(), emit); + if (polygon) + // Wrap the first vertex around, so that the last one is a corner as well. + smoother.push(points.front().cast<double>(), emit); + smoother.flush(emit); + + if (polygon) + // The flushed point is the wrapped first vertex, which a polygon does not store. + smoothed.pop_back(); + return smoothed; +} + +void smooth_polyline_corners(Polyline &polyline, const double smooth_factor, const double tolerance, + const double max_corner_distance, const CornerFilter &corner_filter) +{ + CornerSmoother smoother(smooth_factor, tolerance, max_corner_distance, corner_filter); + if (!smoother.enabled() || polyline.size() < 3) + return; + + polyline.points = smooth_corners(polyline.points, false, smoother); + // Rounding back to the integer grid may collapse neighbouring samples of a curve. + polyline.remove_duplicate_points(); +} + +void smooth_polylines_corners(Polylines &polylines, const double smooth_factor, const double tolerance, + const double max_corner_distance, const CornerFilter &corner_filter) +{ + if (sanitize_smooth_factor(smooth_factor) == 0.) + return; + for (Polyline &polyline : polylines) + smooth_polyline_corners(polyline, smooth_factor, tolerance, max_corner_distance, corner_filter); +} + +void smooth_polygons_corners(Polygons &polygons, const double smooth_factor, const double tolerance, + const double max_corner_distance, const CornerFilter &corner_filter) +{ + CornerSmoother smoother(smooth_factor, tolerance, max_corner_distance, corner_filter); + if (!smoother.enabled()) + return; + + for (Polygon &polygon : polygons) { + if (polygon.size() < 3) + continue; + polygon.points = smooth_corners(polygon.points, true, smoother); + polygon.remove_duplicate_points(); + // The curves of the first and of the last corner may have met on the segment they share. A + // polygon closes implicitly, so it must not repeat its first vertex at the end. + if (polygon.points.size() > 1 && polygon.points.front() == polygon.points.back()) + polygon.points.pop_back(); + } +} + +} // namespace Slic3r diff --git a/src/libslic3r/Fill/FillCornerSmoothing.hpp b/src/libslic3r/Fill/FillCornerSmoothing.hpp new file mode 100644 index 0000000000..7f2ead229a --- /dev/null +++ b/src/libslic3r/Fill/FillCornerSmoothing.hpp @@ -0,0 +1,131 @@ +#pragma once + +#include <algorithm> +#include <array> +#include <cmath> +#include <functional> +#include <vector> + +#include "../libslic3r.h" +#include "../Point.hpp" +#include "../Polygon.hpp" +#include "../Polyline.hpp" + +namespace Slic3r { + +// Orca: NaN or infinite factors disable the smoothing, everything else is clamped to <0, 1>. +inline double sanitize_smooth_factor(double smooth_factor) +{ + return std::isfinite(smooth_factor) ? std::clamp(smooth_factor, 0., 1.) : 0.; +} + +// Decides whether a corner may be replaced by the curve that leaves the path at `from` and rejoins it +// at `to`, both in the coordinate system of the pushed points. Rounding cuts toward the inside of the +// turn, so a path that is not clipped to the fill region afterwards needs this to stay inside it. +using CornerFilter = std::function<bool(const Vec2d &from, const Vec2d &to)>; + +// Orca: Replaces the sharp vertices of an infill path with curves that join the adjoining straight +// legs with a continuous curvature, so the toolhead does not have to stop in every corner. +// Points are pushed one by one, because the plane path fills produce their path on the fly, and +// every point of the smoothed path is handed over to the caller supplied emit callback. +// Fully smoothed adjacent corners meet at the midpoint of the segment they share, so the emitted +// points may collapse onto each other once rounded to the integer grid of the caller. Dropping such +// duplicates is left to the caller, which is the only one knowing that grid. +class CornerSmoother +{ +public: + // tolerance is the maximum chordal deviation of the flattened curves, in the units of the pushed + // points. max_corner_distance caps how far a curve may reach along a leg, in the same units; it + // bounds how far a rounded corner moves away from the original path, which matters where the legs + // are much longer than the spacing of the pattern. Zero leaves the reach uncapped. + CornerSmoother(double smooth_factor, double tolerance, double max_corner_distance = 0., + CornerFilter corner_filter = {}) + : m_corner_distance_ratio(0.5 * sanitize_smooth_factor(smooth_factor)), m_tolerance(tolerance), + m_max_corner_distance(max_corner_distance), m_corner_filter(std::move(corner_filter)) + {} + + bool enabled() const { return m_corner_distance_ratio > 0.; } + + template<typename Emit> void push(const Vec2d &point, Emit &emit) + { + if (m_held == 0) { + // The first point of a path is an end, not a corner, and stays where it is. + emit(point); + m_window[m_held++] = point; + return; + } + if (m_held > 1 && is_on_straight_run(m_window[m_held - 2], m_window[m_held - 1], point)) { + // The newest vertex only splits a straight leg, so the leg runs on to this point instead. + m_window[m_held - 1] = point; + return; + } + if (m_held < 3) { + m_window[m_held++] = point; + return; + } + // Both legs of the middle vertex are complete now, so its curve can no longer grow. + emit_corner(m_window[0], m_window[1], m_window[2], emit); + m_window[0] = m_window[1]; + m_window[1] = m_window[2]; + m_window[2] = point; + } + + // Emits the last point of the path and prepares the smoother for a new one. + template<typename Emit> void flush(Emit &emit) + { + if (m_held > 2) + emit_corner(m_window[0], m_window[1], m_window[2], emit); + if (m_held > 1) + emit(m_window[m_held - 1]); + m_held = 0; + } + +private: + template<typename Emit> void emit_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next, Emit &emit) + { + round_corner(previous, corner, next); + for (const Vec2d &corner_point : m_corner_points) + emit(corner_point); + } + + // Tells a vertex that only continues a straight leg (or repeats its predecessor) from a corner. + // A path doubling back on itself is not one, that vertex is a hairpin and stays where it is. + static bool is_on_straight_run(const Vec2d &previous, const Vec2d &vertex, const Vec2d &next); + // Fills m_corner_points with the points replacing the corner vertex. + void round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next); + // Flattens the canonical corner curve of the given size and turn into coordinates of the + // (incoming, outgoing) basis of the corner. Cached, as an infill path repeats the same corner. + const std::vector<Vec2d>& curve_coefficients(double corner_distance, const Vec2d &incoming, const Vec2d &outgoing); + + // Fraction of the shorter adjoining leg consumed on each side of a corner. Half of a leg is the + // maximum, otherwise the curves of two adjacent corners would overlap. + const double m_corner_distance_ratio; + const double m_tolerance; + const double m_max_corner_distance; + const CornerFilter m_corner_filter; + std::vector<Vec2d> m_corner_points; + // Cached flattening of the last corner, valid for corners of the same size and turn angle. + std::vector<Vec2d> m_cached_coefficients; + double m_cached_distance { 0. }; + double m_cached_cosine { 0. }; + bool m_has_cached_coefficients { false }; + + // The corners seen last, kept free of vertices that merely split a straight leg. The middle one + // is rounded once the third arrives, which is what makes its outgoing leg final. + std::array<Vec2d, 3> m_window { Vec2d::Zero(), Vec2d::Zero(), Vec2d::Zero() }; + // How many of them are filled in. + int m_held { 0 }; +}; + +// Rounds the corners of already scaled paths in place. Paths of less than three points are left alone. +// Both ends of a polyline are kept where they are, even when they coincide: such a path retraces its +// way back and joining its ends would turn it into a loop. See CornerSmoother for max_corner_distance. +void smooth_polyline_corners(Polyline &polyline, double smooth_factor, double tolerance, + double max_corner_distance = 0., const CornerFilter &corner_filter = {}); +void smooth_polylines_corners(Polylines &polylines, double smooth_factor, double tolerance, + double max_corner_distance = 0., const CornerFilter &corner_filter = {}); +// Polygons close implicitly, so every one of their vertices is a corner. +void smooth_polygons_corners(Polygons &polygons, double smooth_factor, double tolerance, + double max_corner_distance = 0., const CornerFilter &corner_filter = {}); + +} // namespace Slic3r diff --git a/src/libslic3r/Fill/FillCrossHatch.cpp b/src/libslic3r/Fill/FillCrossHatch.cpp index 571095eca4..98be5ef46b 100644 --- a/src/libslic3r/Fill/FillCrossHatch.cpp +++ b/src/libslic3r/Fill/FillCrossHatch.cpp @@ -3,6 +3,7 @@ #include "../Surface.hpp" #include <cmath> #include "FillBase.hpp" +#include "FillCornerSmoothing.hpp" #include "FillCrossHatch.hpp" namespace Slic3r { @@ -205,6 +206,9 @@ void FillCrossHatch ::_fill_surface_single( // shift the pattern to the actual space for (Polyline &pl : polylines) { pl.translate(bb.min); } + // Orca: round the corners of the transition layers. The repeat layers are straight lines and stay as they are. + smooth_polylines_corners(polylines, params.smooth_factor, scaled<double>(params.resolution)); + // Apply multiline offset if needed multiline_fill(polylines, params, spacing); diff --git a/src/libslic3r/Fill/FillHoneycomb.cpp b/src/libslic3r/Fill/FillHoneycomb.cpp index a595cdb664..82679541da 100644 --- a/src/libslic3r/Fill/FillHoneycomb.cpp +++ b/src/libslic3r/Fill/FillHoneycomb.cpp @@ -2,6 +2,7 @@ #include "../ShortestPath.hpp" #include "../Surface.hpp" +#include "FillCornerSmoothing.hpp" #include "FillHoneycomb.hpp" namespace Slic3r { @@ -70,6 +71,9 @@ void FillHoneycomb::_fill_surface_single( } p.rotate(-direction.first, m.hex_center); p.simplify(5 * spacing); // simplify to 5x line width + // Orca: round the corners of the honeycomb cells. Done before the clipping, so that the + // curves are cut by the region boundary just like the sharp path would be. + smooth_polyline_corners(p, params.smooth_factor, scaled<double>(params.resolution)); all_polylines.push_back(p); } } diff --git a/src/libslic3r/Fill/FillLightning.cpp b/src/libslic3r/Fill/FillLightning.cpp index 7937b9d129..77031b42e0 100644 --- a/src/libslic3r/Fill/FillLightning.cpp +++ b/src/libslic3r/Fill/FillLightning.cpp @@ -2,6 +2,7 @@ #include "../Print.hpp" #include "../ShortestPath.hpp" #include "FillBase.hpp" +#include "FillCornerSmoothing.hpp" #include "FillLightning.hpp" #include "Lightning/Generator.hpp" @@ -17,6 +18,19 @@ void Filler::_fill_surface_single( const Layer &layer = generator->getTreesForLayer(this->layer_id); Polylines fill_lines = layer.convertToLines(to_polygons(expolygon), scaled<coord_t>(0.5 * this->spacing - this->overlap)); + // Orca: round the turns of the branches. Hairpins are left sharp, as they cannot be rounded, and + // the reach is capped: cutting a corner moves the branch, and a branch is as long as the object + // rather than as long as one cell of a pattern, so half of a leg would merge it with its neighbour + // instead of rounding the turn between them. Half the distance between two branches keeps them + // apart. With more than one line per infill wall the branches are printed as outlines drawn around + // them, and the outlines of branches that run into each other merge into a single one; moving a + // branch by more than a fraction of its printed width breaks such an outline up into separate + // loops, so that width bounds the reach as well. + const double branch_width = scaled<double>(this->spacing) * params.multiline; + const double branch_spacing = branch_width / std::max(double(params.density), EPSILON); + const double max_reach = 0.5 * (params.multiline > 1 ? branch_width : branch_spacing); + smooth_polylines_corners(fill_lines, params.smooth_factor, scaled<double>(params.resolution), max_reach); + // Apply multiline offset if needed multiline_fill(fill_lines, params, spacing); diff --git a/src/libslic3r/Fill/FillPlanePath.cpp b/src/libslic3r/Fill/FillPlanePath.cpp index 7c4f285ac6..577aef0600 100644 --- a/src/libslic3r/Fill/FillPlanePath.cpp +++ b/src/libslic3r/Fill/FillPlanePath.cpp @@ -2,6 +2,7 @@ #include "../ShortestPath.hpp" #include "../Surface.hpp" +#include "FillCornerSmoothing.hpp" #include "FillPlanePath.hpp" namespace Slic3r { @@ -288,145 +289,60 @@ static void generate_hilbert_curve(coord_t min_x, coord_t min_y, coord_t max_x, } } -using QuinticBezier = std::array<Vec2d, 6>; - -static bool is_bezier_flat(const QuinticBezier &curve, const double deviation) -{ - // A Bezier curve stays inside the convex hull of its control points. Therefore, keeping every - // control point within a deviation-wide strip around the endpoint chord conservatively bounds the - // flattening error. The cross product is the perpendicular distance scaled by the chord length; - // comparing squared values avoids a square root. - const Vec2d chord = curve.back() - curve.front(); - const double chord_length_sq = chord.squaredNorm(); - const double max_cross_sq = deviation * deviation * chord_length_sq; - - for (size_t i = 1; i + 1 < curve.size(); ++i) { - const Vec2d offset = curve[i] - curve.front(); - const double cross = chord.x() * offset.y() - chord.y() * offset.x(); - if (cross * cross > max_cross_sq) - return false; - } - return true; -} - -static void subdivide_bezier(const QuinticBezier &curve, QuinticBezier &left, QuinticBezier &right) -{ - // Split the curve at t = 0.5 using de Casteljau's algorithm. Each averaging level contributes one - // control point to the left half and one to the right half; the latter is filled backwards to keep - // both resulting control polygons in their original parameter direction. - QuinticBezier subdivision = curve; - left.front() = subdivision.front(); - right.back() = subdivision.back(); - for (size_t level = 1; level < curve.size(); ++level) { - for (size_t i = 0; i + level < curve.size(); ++i) - subdivision[i] = 0.5 * (subdivision[i] + subdivision[i + 1]); - left[level] = subdivision.front(); - right[curve.size() - level - 1] = subdivision[curve.size() - level - 1]; - } -} - -static void flatten_bezier(const QuinticBezier &curve, const double deviation, std::vector<Vec2d> &output) -{ - // Subdivide to at least depth 1 so a rounded corner cannot collapse to a single diagonal chord. - // A uniform subdivision depth keeps samples at equal parameter intervals t = k / 2^depth, - // avoiding abrupt segment-length jumps at adaptive-depth boundaries. - static constexpr size_t max_depth = 16; - - std::vector<QuinticBezier> subcurves(2); - subdivide_bezier(curve, subcurves[0], subcurves[1]); - - for (size_t depth = 1; depth < max_depth; ++depth) { - bool all_flat = true; - for (const QuinticBezier &c : subcurves) - if (!is_bezier_flat(c, deviation)) { - all_flat = false; - break; - } - if (all_flat) - break; - std::vector<QuinticBezier> finer(subcurves.size() * 2); - for (size_t i = 0; i < subcurves.size(); ++i) - subdivide_bezier(subcurves[i], finer[i * 2], finer[i * 2 + 1]); - subcurves = std::move(finer); - } - - // The curve start is deliberately omitted so consecutive curve pieces can share it without duplication. - output.reserve(output.size() + subcurves.size()); - for (const QuinticBezier &c : subcurves) - output.emplace_back(c.back()); -} - +// Rounds the corners of the generated path on its way to the infill output. template<typename Output> -static void generate_smooth_hilbert_curve( - coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, - const double corner_distance, Output &output) +class SmoothingPolylineOutput { - // A Hilbert curve is defined on a square grid whose side is a power of two. As in the unsmoothed - // generator, expand the larger requested dimension to the next valid Hilbert grid size. The output - // clipper or the later region intersection removes the padded part of the traversal. - size_t sz = 2; - const size_t sz0 = std::max(max_x + 1 - min_x, max_y + 1 - min_y); - while (sz < sz0) - sz <<= 1; +public: + SmoothingPolylineOutput(Output &output, const double smooth_factor, const double tolerance) + : m_output(output), m_smoother(smooth_factor, tolerance) {} - const size_t point_count = sz * sz; - output.reserve(point_count); + void reserve(size_t n) { m_output.reserve(n); } + void add_point(const Vec2d &pt) { auto emit = emitter(); m_smoother.push(pt, emit); } + // The smoother holds back the last point of the path until it knows there is no corner left to round. + void finish() { auto emit = emitter(); m_smoother.flush(emit); } - // The caller normalizes resolution to the unit Hilbert grid; retain a finite positive tolerance - // if this helper is invoked with an invalid resolution. - const double deviation = resolution > 0. && std::isfinite(resolution) ? resolution : EPSILON; - // Construct one canonical 90-degree corner from (-corner_distance, 0) to (0, corner_distance). - // At each end, the first three control points are collinear and equally spaced: the tangent follows - // the adjoining straight leg and the second derivative is zero. The endpoint curvature is therefore - // zero, giving G2 joins to both legs. Every Hilbert turn is an oriented copy of this curve, so flatten - // it only once to the requested chordal-deviation tolerance. - const QuinticBezier corner_curve {{ - {-corner_distance, 0.}, {-0.7 * corner_distance, 0.}, {-0.4 * corner_distance, 0.}, - {0., 0.4 * corner_distance}, {0., 0.7 * corner_distance}, {0., corner_distance} - }}; - std::vector<Vec2d> curve_coefficients; - flatten_bezier(corner_curve, deviation, curve_coefficients); - - auto translated_point = [min_x, min_y](size_t idx) { - Point p = hilbert_n_to_xy(idx); - return Point(p.x() + min_x, p.y() + min_y); - }; - auto to_vec2d = [](const Point &p) { return Vec2d(double(p.x()), double(p.y())); }; - bool has_last_output = false; - Vec2d last_output; - // Fully smoothed adjacent corners may meet at the same segment midpoint. Suppress such duplicates - // to avoid emitting zero-length extrusion segments. - auto add_point = [&output, &has_last_output, &last_output](const Vec2d &point) { - if (!has_last_output || point.x() != last_output.x() || point.y() != last_output.y()) { - output.add_point(point); - last_output = point; - has_last_output = true; - } - }; - - Vec2d previous = to_vec2d(translated_point(0)); - Vec2d corner = to_vec2d(translated_point(1)); - add_point(previous); - // Replace each non-collinear Hilbert vertex by the canonical curve expressed in the local basis of - // its incoming and outgoing unit vectors. Collinear vertices remain part of the straight polyline. - for (size_t i = 1; i + 1 < point_count; ++i) { - const Vec2d next = to_vec2d(translated_point(i + 1)); - const Vec2d incoming = (corner - previous).normalized(); - const Vec2d outgoing = (next - corner).normalized(); - const double cross = incoming.x() * outgoing.y() - incoming.y() * outgoing.x(); - - if (std::abs(cross) < EPSILON) { - add_point(corner); - } else { - add_point(corner - corner_distance * incoming); - for (const Vec2d &coefficient : curve_coefficients) - add_point(corner + coefficient.x() * incoming + coefficient.y() * outgoing); - } - - previous = corner; - corner = next; +private: + // The curves of two adjacent corners meet at the midpoint of the segment they share, where they + // may round to the very same output point. Drop those, they would be zero length extrusions. + auto emitter() + { + return [this](const Vec2d &pt) { + const Point snapped = m_output.scaled(pt); + if (m_has_last_snapped && snapped == m_last_snapped) + return; + m_last_snapped = snapped; + m_has_last_snapped = true; + m_output.add_point(pt); + }; } - add_point(corner); + + Output &m_output; + CornerSmoother m_smoother; + Point m_last_snapped { Point::Zero() }; + bool m_has_last_snapped { false }; +}; + +// Runs the path generator against the concrete output type, optionally through the corner smoother. +// The outputs do not share a virtual add_point(), so the type has to be resolved here. +template<typename GenerateFn> +static void generate_path(InfillPolylineOutput &output, const FillParams ¶ms, const double resolution, GenerateFn generate) +{ + const double smooth_factor = sanitize_smooth_factor(params.smooth_factor); + auto run = [smooth_factor, resolution, &generate](auto &out) { + if (smooth_factor == 0.) { + generate(out); + } else { + SmoothingPolylineOutput<std::remove_reference_t<decltype(out)>> smoothing(out, smooth_factor, resolution); + generate(smoothing); + smoothing.finish(); + } + }; + + if (output.clips()) + run(static_cast<InfillPolylineClipper&>(output)); + else + run(output); } void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double /* resolution */, InfillPolylineOutput &output) @@ -440,19 +356,8 @@ void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coo void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, const FillParams ¶ms, InfillPolylineOutput &output) { - const double smooth_factor = std::isfinite(params.smooth_factor) ? - std::clamp(params.smooth_factor, 0., 1.) : 0.; - if (smooth_factor == 0.) { - this->generate(min_x, min_y, max_x, max_y, resolution, output); - return; - } - - const double corner_distance = 0.5 * smooth_factor; - if (output.clips()) - generate_smooth_hilbert_curve( - min_x, min_y, max_x, max_y, resolution, corner_distance, static_cast<InfillPolylineClipper&>(output)); - else - generate_smooth_hilbert_curve(min_x, min_y, max_x, max_y, resolution, corner_distance, output); + generate_path(output, params, resolution, + [min_x, min_y, max_x, max_y](auto &out) { generate_hilbert_curve(min_x, min_y, max_x, max_y, out); }); } template<typename Output> @@ -495,4 +400,11 @@ void FillOctagramSpiral::generate(coord_t min_x, coord_t min_y, coord_t max_x, c generate_octagram_spiral(min_x, min_y, max_x, max_y, output); } +void FillOctagramSpiral::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, + const FillParams ¶ms, InfillPolylineOutput &output) +{ + generate_path(output, params, resolution, + [min_x, min_y, max_x, max_y](auto &out) { generate_octagram_spiral(min_x, min_y, max_x, max_y, out); }); +} + } // namespace Slic3r diff --git a/src/libslic3r/Fill/FillPlanePath.hpp b/src/libslic3r/Fill/FillPlanePath.hpp index b4b25b73ae..a1e9068ca9 100644 --- a/src/libslic3r/Fill/FillPlanePath.hpp +++ b/src/libslic3r/Fill/FillPlanePath.hpp @@ -21,10 +21,10 @@ public: void add_point(const Vec2d& pt) { m_out.emplace_back(this->scaled(pt)); } Points&& result() { return std::move(m_out); } virtual bool clips() const { return false; } - -protected: + // The output grid the generated points are snapped to. const Point scaled(const Vec2d& fpt) const { return { coord_t(floor(fpt.x() * m_scale_out + 0.5)), coord_t(floor(fpt.y() * m_scale_out + 0.5)) }; } +protected: // Output polyline. Points m_out; @@ -93,6 +93,8 @@ public: protected: bool centered() const override { return true; } void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, InfillPolylineOutput &output) override; + void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, + const FillParams ¶ms, InfillPolylineOutput &output) override; }; } // namespace Slic3r diff --git a/src/libslic3r/Fill/FillRectilinear.cpp b/src/libslic3r/Fill/FillRectilinear.cpp index 138b50bc88..db340748ab 100644 --- a/src/libslic3r/Fill/FillRectilinear.cpp +++ b/src/libslic3r/Fill/FillRectilinear.cpp @@ -18,6 +18,7 @@ #include "../ShortestPath.hpp" #include "../VariableWidth.hpp" +#include "FillCornerSmoothing.hpp" #include "FillRectilinear.hpp" // #define SLIC3R_DEBUG @@ -2394,12 +2395,7 @@ static std::vector<MonotonicRegionLink> chain_monotonic_regions( } // Probability (unnormalized) of traversing a link between two monotonic regions. - auto path_probability = [ -#ifndef __APPLE__ - // clang complains when capturing constexpr constants. - pheromone_alpha, pheromone_beta -#endif // __APPLE__ - ](AntPath &path) { + auto path_probability = [](AntPath &path) { return pow(path.pheromone, pheromone_alpha) * pow(path.visibility, pheromone_beta); }; @@ -3364,6 +3360,10 @@ bool FillRectilinear::fill_surface_trapezoidal( for (Polyline &pl : polylines) pl.translate(rotate_vector.second); + // Orca: round the corners of the trapezoids. The straight base lines of the triangular family + // have no corner to round. + smooth_polylines_corners(polylines, params.smooth_factor, scaled<double>(params.resolution)); + // Apply multiline fill multiline_fill(polylines, params, spacing); @@ -3576,7 +3576,7 @@ Polylines FillLateralHoneycomb::fill_surface(const Surface *surface, const FillP // | // | // 0 --+-- - // / \ + // ⟋ ⟍ // why inverted? // it makes determining some of the properties easier // and the two angled legs provide additional horizontal stiffness diff --git a/src/libslic3r/Fill/Lightning/TreeNode.cpp b/src/libslic3r/Fill/Lightning/TreeNode.cpp index 982d47b10e..3d57ebae4a 100644 --- a/src/libslic3r/Fill/Lightning/TreeNode.cpp +++ b/src/libslic3r/Fill/Lightning/TreeNode.cpp @@ -351,19 +351,23 @@ void Node::convertToPolylines(Polylines &output, const coord_t line_overlap) con { Polylines result; result.emplace_back(); - convertToPolylines(0, result); + // Orca: the layers are filled in parallel, so they would consume a shared generator in a + // different order every run, and a model would not slice the same way twice. Each tree seeds + // its own from where it is rooted; one constant seed would start them all on the same pick. + std::mt19937_64 rng { uint64_t(PointHash{}(m_p)) }; + convertToPolylines(0, result, rng); removeJunctionOverlap(result, line_overlap); append(output, std::move(result)); } -void Node::convertToPolylines(size_t long_line_idx, Polylines &output) const +void Node::convertToPolylines(size_t long_line_idx, Polylines &output, std::mt19937_64 &rng) const { if (m_children.empty()) { output[long_line_idx].points.push_back(m_p); return; } - size_t first_child_idx = rand() % m_children.size(); - m_children[first_child_idx]->convertToPolylines(long_line_idx, output); + const size_t first_child_idx = rng() % m_children.size(); + m_children[first_child_idx]->convertToPolylines(long_line_idx, output, rng); output[long_line_idx].points.push_back(m_p); for (size_t idx_offset = 1; idx_offset < m_children.size(); idx_offset++) { @@ -371,7 +375,7 @@ void Node::convertToPolylines(size_t long_line_idx, Polylines &output) const const Node& child = *m_children[child_idx]; output.emplace_back(); size_t child_line_idx = output.size() - 1; - child.convertToPolylines(child_line_idx, output); + child.convertToPolylines(child_line_idx, output, rng); output[child_line_idx].points.emplace_back(m_p); } } diff --git a/src/libslic3r/Fill/Lightning/TreeNode.hpp b/src/libslic3r/Fill/Lightning/TreeNode.hpp index 14aa5e4888..95559524ba 100644 --- a/src/libslic3r/Fill/Lightning/TreeNode.hpp +++ b/src/libslic3r/Fill/Lightning/TreeNode.hpp @@ -7,6 +7,7 @@ #include <functional> #include <memory> #include <optional> +#include <random> #include <vector> #include "../../EdgeGrid.hpp" @@ -259,8 +260,9 @@ protected: * * \param long_line a reference to a polyline in \p output which to continue building on in the recursion * \param output all branches in this tree connected into polylines + * \param rng the generator the junctions draw from, carried through the recursion */ - void convertToPolylines(size_t long_line_idx, Polylines &output) const; + void convertToPolylines(size_t long_line_idx, Polylines &output, std::mt19937_64 &rng) const; void removeJunctionOverlap(Polylines &polylines, coord_t line_overlap) const; diff --git a/src/libslic3r/Format/AssimpImport.cpp b/src/libslic3r/Format/AssimpImport.cpp new file mode 100644 index 0000000000..f0ae99506a --- /dev/null +++ b/src/libslic3r/Format/AssimpImport.cpp @@ -0,0 +1,327 @@ +#include "AssimpImport.hpp" + +#include "../TexturePainting.hpp" +#include "ResourcePathUtils.hpp" + +#include <assimp/Importer.hpp> +#include <assimp/config.h> +#include <assimp/material.h> +#include <assimp/postprocess.h> +#include <assimp/scene.h> + +#include <boost/algorithm/string/predicate.hpp> +#include <boost/filesystem.hpp> +#include <boost/log/trivial.hpp> +#include <boost/nowide/fstream.hpp> + +#include <array> +#include <cstdint> +#include <limits> +#include <sstream> +#include <string> +#include <vector> + +namespace Slic3r { +namespace { + +void clear_textured_mesh(TexturedMesh& out) +{ + out.vertices.clear(); + out.indices.clear(); + out.uvs.clear(); + out.uv_coords.clear(); + out.uv_indices.clear(); + out.textures.clear(); + out.material_ids.clear(); + out.material_texture_map.clear(); + out.material_colors.clear(); +} + +void set_error_message(std::string* error_message, const std::string& message) +{ + if (error_message) + *error_message = message; +} + +bool is_fbx_path(const std::string& path) +{ + return boost::algorithm::iends_with(path, ".fbx"); +} + +bool should_flip_uvs(const std::string& path) +{ + return boost::algorithm::iends_with(path, ".fbx") || + boost::algorithm::iends_with(path, ".glb"); +} + +unsigned int assimp_import_flags(const std::string& path) +{ + unsigned int flags = aiProcess_Triangulate + | aiProcess_GenNormals + | aiProcess_PreTransformVertices + | aiProcess_SortByPType; + if (should_flip_uvs(path)) + flags |= aiProcess_FlipUVs; + return flags; +} + +void configure_importer(Assimp::Importer& importer, const std::string& path, unsigned int flags) +{ + importer.SetPropertyInteger(AI_CONFIG_PP_SBP_REMOVE, + aiPrimitiveType_POINT | aiPrimitiveType_LINE); + + if (flags & aiProcess_PreTransformVertices) + importer.SetPropertyBool(AI_CONFIG_PP_PTV_KEEP_HIERARCHY, true); + + if (is_fbx_path(path)) { + importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ALL_GEOMETRY_LAYERS, true); + importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_MATERIALS, true); + importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_TEXTURES, true); + importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ANIMATIONS, false); + importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_LIGHTS, false); + importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_CAMERAS, false); + } +} + +bool read_external_texture_file(const boost::filesystem::path& path, TextureImage& out) +{ + boost::nowide::ifstream file(path.string(), std::ios::binary | std::ios::ate); + if (!file.is_open()) + return false; + + const std::streamoff size = file.tellg(); + if (size <= 0) + return false; + if (static_cast<uintmax_t>(size) > static_cast<uintmax_t>(std::numeric_limits<size_t>::max())) + return false; + + file.seekg(0); + out.width = -1; + out.height = -1; + out.channels = 0; + out.data.resize(static_cast<size_t>(size)); + file.read(reinterpret_cast<char*>(out.data.data()), size); + if (!file && !file.eof()) { + out.data.clear(); + return false; + } + return true; +} + +bool read_embedded_texture(const aiTexture& texture, TextureImage& out) +{ + out.data.clear(); + if (texture.mHeight == 0) { + if (texture.mWidth == 0) + return false; + out.width = -1; + out.height = -1; + out.channels = 0; + out.data.assign( + reinterpret_cast<const unsigned char*>(texture.pcData), + reinterpret_cast<const unsigned char*>(texture.pcData) + texture.mWidth); + return !out.data.empty(); + } + + if (texture.mWidth == 0 || texture.mHeight == 0) + return false; + if (texture.mWidth > static_cast<unsigned int>(std::numeric_limits<int>::max()) || + texture.mHeight > static_cast<unsigned int>(std::numeric_limits<int>::max())) { + return false; + } + const size_t width = static_cast<size_t>(texture.mWidth); + const size_t height = static_cast<size_t>(texture.mHeight); + if (width > std::numeric_limits<size_t>::max() / height || + width * height > std::numeric_limits<size_t>::max() / 4) { + return false; + } + + out.width = static_cast<int>(texture.mWidth); + out.height = static_cast<int>(texture.mHeight); + out.channels = 4; + const size_t pixel_count = width * height; + out.data.resize(pixel_count * 4); + for (size_t i = 0; i < pixel_count; ++i) { + const aiTexel& texel = texture.pcData[i]; + out.data[i * 4 + 0] = texel.r; + out.data[i * 4 + 1] = texel.g; + out.data[i * 4 + 2] = texel.b; + out.data[i * 4 + 3] = texel.a; + } + return !out.data.empty(); +} + +bool get_material_texture(const aiMaterial& material, aiString& texture_path) +{ + if (material.GetTextureCount(aiTextureType_DIFFUSE) > 0 && + material.GetTexture(aiTextureType_DIFFUSE, 0, &texture_path) == AI_SUCCESS) { + return true; + } + + if (material.GetTextureCount(aiTextureType_BASE_COLOR) > 0 && + material.GetTexture(aiTextureType_BASE_COLOR, 0, &texture_path) == AI_SUCCESS) { + return true; + } + + return false; +} + +std::array<float, 4> get_material_color(const aiMaterial& material) +{ + aiColor4D color(1.f, 1.f, 1.f, 1.f); + if (material.Get(AI_MATKEY_BASE_COLOR, color) == AI_SUCCESS) + return {color.r, color.g, color.b, color.a}; + if (material.Get(AI_MATKEY_COLOR_DIFFUSE, color) == AI_SUCCESS) + return {color.r, color.g, color.b, color.a}; + return {1.f, 1.f, 1.f, 1.f}; +} + +bool collect_mesh(const aiMesh& mesh, size_t& vertex_offset, TexturedMesh& out, std::string& error) +{ + if (mesh.mNumVertices > static_cast<size_t>(std::numeric_limits<int>::max()) - vertex_offset) { + error = "Assimp mesh has too many vertices for TexturedMesh indices"; + return false; + } + + for (unsigned int i = 0; i < mesh.mNumVertices; ++i) { + const aiVector3D& v = mesh.mVertices[i]; + out.vertices.push_back({v.x, v.y, v.z}); + + if (mesh.HasTextureCoords(0)) { + const aiVector3D& uv = mesh.mTextureCoords[0][i]; + out.uvs.push_back({uv.x, uv.y}); + } else { + out.uvs.push_back({0.f, 0.f}); + } + } + + const int material_index = static_cast<int>(mesh.mMaterialIndex); + for (unsigned int i = 0; i < mesh.mNumFaces; ++i) { + const aiFace& face = mesh.mFaces[i]; + if (face.mNumIndices != 3) + continue; + if (face.mIndices[0] >= mesh.mNumVertices || + face.mIndices[1] >= mesh.mNumVertices || + face.mIndices[2] >= mesh.mNumVertices) { + error = "Assimp mesh face index is out of bounds"; + return false; + } + out.indices.push_back({ + static_cast<int>(static_cast<size_t>(face.mIndices[0]) + vertex_offset), + static_cast<int>(static_cast<size_t>(face.mIndices[1]) + vertex_offset), + static_cast<int>(static_cast<size_t>(face.mIndices[2]) + vertex_offset)}); + out.material_ids.push_back(material_index); + } + + vertex_offset += mesh.mNumVertices; + return true; +} + +void collect_materials(const aiScene& scene, const boost::filesystem::path& base_dir, TexturedMesh& out) +{ + out.material_texture_map.assign(scene.mNumMaterials, -1); + out.material_colors.assign(scene.mNumMaterials, {1.f, 1.f, 1.f, 1.f}); + + for (unsigned int material_index = 0; material_index < scene.mNumMaterials; ++material_index) { + const aiMaterial* material = scene.mMaterials[material_index]; + if (!material) + continue; + + out.material_colors[material_index] = get_material_color(*material); + + aiString texture_path; + if (!get_material_texture(*material, texture_path)) + continue; + + TextureImage image; + const aiTexture* embedded_texture = scene.GetEmbeddedTexture(texture_path.C_Str()); + if (embedded_texture) { + if (!read_embedded_texture(*embedded_texture, image)) + continue; + } else { + const boost::filesystem::path resolved = resource_path::resolve_external_resource_path( + base_dir, texture_path.C_Str(), "Assimp texture"); + if (resolved.empty()) { + BOOST_LOG_TRIVIAL(warning) << "AssimpImport: texture file not found: " + << texture_path.C_Str(); + continue; + } + if (!read_external_texture_file(resolved, image)) { + BOOST_LOG_TRIVIAL(warning) << "AssimpImport: failed to read texture: " + << resolved; + continue; + } + } + + out.material_texture_map[material_index] = static_cast<int>(out.textures.size()); + out.textures.push_back(std::move(image)); + } +} + +std::string scene_failure_summary(const std::string& path, const char* assimp_error) +{ + std::ostringstream ss; + ss << "Assimp failed to import " << path; + if (assimp_error && assimp_error[0] != '\0') + ss << ": " << assimp_error; + return ss.str(); +} + +} // namespace + +bool load_assimp_textured_model(const std::string& path, TexturedMesh& out, std::string* error_message) +{ + clear_textured_mesh(out); + + Assimp::Importer importer; + const unsigned int flags = assimp_import_flags(path); + configure_importer(importer, path, flags); + + const aiScene* scene = importer.ReadFile(path, flags); + if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) { + const std::string message = scene_failure_summary(path, importer.GetErrorString()); + BOOST_LOG_TRIVIAL(error) << "AssimpImport: " << message; + set_error_message(error_message, message); + return false; + } + + if (scene->mNumMeshes == 0) { + const std::string message = "Assimp scene has no meshes: " + path; + BOOST_LOG_TRIVIAL(error) << "AssimpImport: " << message; + set_error_message(error_message, message); + return false; + } + + size_t vertex_offset = 0; + for (unsigned int mesh_index = 0; mesh_index < scene->mNumMeshes; ++mesh_index) { + const aiMesh* mesh = scene->mMeshes[mesh_index]; + if (!mesh || !mesh->HasPositions()) + continue; + std::string mesh_error; + if (!collect_mesh(*mesh, vertex_offset, out, mesh_error)) { + const std::string message = mesh_error + ": " + path; + BOOST_LOG_TRIVIAL(error) << "AssimpImport: " << message; + set_error_message(error_message, message); + clear_textured_mesh(out); + return false; + } + } + + if (out.vertices.empty() || out.indices.empty()) { + const std::string message = "Assimp extracted no valid triangles: " + path; + BOOST_LOG_TRIVIAL(error) << "AssimpImport: " << message; + set_error_message(error_message, message); + clear_textured_mesh(out); + return false; + } + + collect_materials(*scene, boost::filesystem::path(path).parent_path(), out); + + BOOST_LOG_TRIVIAL(info) << "AssimpImport: loaded " << out.vertices.size() + << " vertices, " << out.indices.size() + << " triangles, " << out.textures.size() + << " textures from " << path; + return true; +} + +} // namespace Slic3r diff --git a/src/libslic3r/Format/AssimpImport.hpp b/src/libslic3r/Format/AssimpImport.hpp new file mode 100644 index 0000000000..80c3e2dc91 --- /dev/null +++ b/src/libslic3r/Format/AssimpImport.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include <string> + +namespace Slic3r { + +struct TexturedMesh; + +bool load_assimp_textured_model(const std::string& path, TexturedMesh& out, std::string* error_message = nullptr); + +} // namespace Slic3r diff --git a/src/libslic3r/Format/OBJ.cpp b/src/libslic3r/Format/OBJ.cpp index 71f7d1e7e2..10abe8e4de 100644 --- a/src/libslic3r/Format/OBJ.cpp +++ b/src/libslic3r/Format/OBJ.cpp @@ -1,6 +1,8 @@ #include "../libslic3r.h" #include "../Model.hpp" #include "../TriangleMesh.hpp" +#include "../TexturePainting.hpp" +#include "ResourcePathUtils.hpp" #include "OBJ.hpp" #include "objparser.hpp" @@ -8,6 +10,7 @@ #include <string> #include <boost/log/trivial.hpp> +#include <boost/nowide/fstream.hpp> #ifdef _WIN32 #define DIR_SEPARATOR '\\' @@ -21,7 +24,7 @@ namespace Slic3r { -bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::string &message) +bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::string &message, ObjParser::MtlData *out_mtl) { if (meshptr == nullptr) return false; @@ -53,9 +56,9 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s boost::filesystem::path temp_mtl_path(mtl_file); mtl_path = temp_mtl_path; } - auto _mtl_path = mtl_name_is_path ? mtl_abs_path.string().c_str() : mtl_path.string().c_str(); + const std::string _mtl_path = (mtl_name_is_path ? mtl_abs_path : mtl_path).string(); if (boost::filesystem::exists(mtl_name_is_path ? mtl_abs_path : mtl_path)) { - if (!ObjParser::mtlparse(_mtl_path, mtl_data)) { + if (!ObjParser::mtlparse(_mtl_path.c_str(), mtl_data)) { BOOST_LOG_TRIVIAL(error) << "load_obj:load_mtl: failed to parse " << _mtl_path; message = _L("load mtl in obj: failed to parse"); return false; @@ -98,6 +101,7 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s its.indices.reserve(num_faces + num_quads); if (exist_mtl) { obj_info.is_single_mtl = data.usemtls.size() == 1 && mtl_data.new_mtl_unmap.size() == 1; + obj_info.usemtls = data.usemtls; obj_info.face_colors.reserve(num_faces + num_quads); } bool has_color = data.has_vertex_color; @@ -210,14 +214,17 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s } if (meshptr->volume() < 0) meshptr->flip_triangles(); + // Hand the parsed material table back so callers can build a TexturedMesh from it. + if (out_mtl) + *out_mtl = mtl_data; return true; } -bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &message, const char *object_name_in) +bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &message, const char *object_name_in, ObjParser::MtlData *out_mtl) { TriangleMesh mesh; - bool ret = load_obj(path, &mesh, obj_info, message); + bool ret = load_obj(path, &mesh, obj_info, message, out_mtl); if (ret) { std::string object_name; @@ -232,6 +239,144 @@ bool load_obj(const char *path, Model *model, ObjInfo& obj_info, std::string &me return ret; } +bool obj_to_textured_mesh( + const ObjInfo& obj_info, + const indexed_triangle_set& its, + const ObjParser::MtlData& mtl_data, + const std::string& obj_directory, + TexturedMesh& out) +{ + if (its.vertices.empty() || its.indices.empty() || !obj_info.has_uv_png) + return false; + + const size_t nv = its.vertices.size(); + const size_t nf = its.indices.size(); + + // 1. Copy vertices + out.vertices.resize(nv); + for (size_t i = 0; i < nv; ++i) + out.vertices[i] = {its.vertices[i].x(), its.vertices[i].y(), its.vertices[i].z()}; + + // 2. Copy face indices + out.indices.resize(nf); + for (size_t i = 0; i < nf; ++i) + out.indices[i] = {its.indices[i][0], its.indices[i][1], its.indices[i][2]}; + + // 3. Build per-face UV (uv_coords + uv_indices) + // OBJ UV convention: V=0 at bottom (OpenGL); texture sampling expects V=0 at top (like glTF/OpenCV). + // Flip V here so downstream code works uniformly. + if (!obj_info.uvs.empty()) { + const size_t uv_face_count = obj_info.uvs.size(); + out.uv_coords.resize(uv_face_count * 3); + out.uv_indices.resize(nf); + for (size_t fi = 0; fi < nf; ++fi) { + if (fi < uv_face_count) { + int base = static_cast<int>(fi * 3); + out.uv_coords[base + 0] = {obj_info.uvs[fi][0].x(), 1.f - obj_info.uvs[fi][0].y()}; + out.uv_coords[base + 1] = {obj_info.uvs[fi][1].x(), 1.f - obj_info.uvs[fi][1].y()}; + out.uv_coords[base + 2] = {obj_info.uvs[fi][2].x(), 1.f - obj_info.uvs[fi][2].y()}; + out.uv_indices[fi] = {base, base + 1, base + 2}; + } else { + out.uv_indices[fi] = {0, 0, 0}; + } + } + } + + // 4. Build material list and load textures from disk + // Map: material name -> material index + std::map<std::string, int> mtl_name_to_idx; + for (size_t i = 0; i < mtl_data.mtl_orders.size(); ++i) + mtl_name_to_idx[mtl_data.mtl_orders[i]] = static_cast<int>(i); + + const int num_materials = static_cast<int>(mtl_data.mtl_orders.size()); + out.material_colors.resize(num_materials, {1.f, 1.f, 1.f, 1.f}); + out.material_texture_map.resize(num_materials, -1); + + // Map: texture filename -> index in out.textures + std::map<std::string, int> png_to_tex_idx; + + for (int mi = 0; mi < num_materials; ++mi) { + const std::string& name = mtl_data.mtl_orders[mi]; + auto it = mtl_data.new_mtl_unmap.find(name); + if (it == mtl_data.new_mtl_unmap.end()) + continue; + const auto& mtl = *(it->second); + + // Material color from Kd + out.material_colors[mi] = {mtl.Kd[0], mtl.Kd[1], mtl.Kd[2], mtl.Tr}; + + // Texture from map_Kd + if (mtl.map_Kd.empty()) + continue; + + auto tex_it = png_to_tex_idx.find(mtl.map_Kd); + if (tex_it != png_to_tex_idx.end()) { + out.material_texture_map[mi] = tex_it->second; + continue; + } + + // Resolve texture file path. + const boost::filesystem::path requested_tex_path(mtl.map_Kd); + const boost::filesystem::path tex_path = requested_tex_path.is_absolute() ? + resource_path::resolve_existing_path_case_insensitive(requested_tex_path, "obj_to_textured_mesh: map_Kd") : + resource_path::resolve_existing_relative_path_case_insensitive( + boost::filesystem::path(obj_directory), requested_tex_path, "obj_to_textured_mesh: map_Kd"); + + if (tex_path.empty()) { + BOOST_LOG_TRIVIAL(warning) << "obj_to_textured_mesh: texture not found: " << requested_tex_path; + continue; + } + + // Read raw file bytes + boost::nowide::ifstream file(tex_path.string(), std::ios::binary | std::ios::ate); + if (!file.is_open()) + continue; + auto file_size = file.tellg(); + if (file_size <= 0) + continue; + file.seekg(0, std::ios::beg); + + TextureImage ti; + ti.data.resize(static_cast<size_t>(file_size)); + file.read(reinterpret_cast<char*>(ti.data.data()), file_size); + ti.width = -1; + ti.height = -1; + ti.channels = 0; + + int new_idx = static_cast<int>(out.textures.size()); + out.textures.push_back(std::move(ti)); + png_to_tex_idx[mtl.map_Kd] = new_idx; + out.material_texture_map[mi] = new_idx; + } + + // 5. Build per-face material_ids from usemtls ranges + out.material_ids.resize(nf, -1); + if (!obj_info.usemtls.empty()) { + for (size_t fi = 0; fi < nf; ++fi) { + int face_idx = static_cast<int>(fi); + for (size_t k = 0; k < obj_info.usemtls.size(); ++k) { + const auto& um = obj_info.usemtls[k]; + if (face_idx >= um.face_start && face_idx <= um.face_end) { + auto name_it = mtl_name_to_idx.find(um.name); + if (name_it != mtl_name_to_idx.end()) + out.material_ids[fi] = name_it->second; + break; + } + } + } + } + + if (out.textures.empty()) { + BOOST_LOG_TRIVIAL(warning) << "obj_to_textured_mesh: no textures loaded"; + return false; + } + + BOOST_LOG_TRIVIAL(info) << "obj_to_textured_mesh: " << nf << " faces, " + << out.textures.size() << " textures, " + << num_materials << " materials"; + return true; +} + bool store_obj(const char *path, TriangleMesh *mesh) { //FIXME returning false even if write failed. diff --git a/src/libslic3r/Format/OBJ.hpp b/src/libslic3r/Format/OBJ.hpp index 2d4370c99a..c103326af6 100644 --- a/src/libslic3r/Format/OBJ.hpp +++ b/src/libslic3r/Format/OBJ.hpp @@ -1,6 +1,7 @@ #ifndef slic3r_Format_OBJ_hpp_ #define slic3r_Format_OBJ_hpp_ #include "libslic3r/Color.hpp" +#include "objparser.hpp" #include <unordered_map> namespace Slic3r { @@ -18,6 +19,7 @@ struct ObjInfo { std::map<std::string,bool> pngs; std::unordered_map<int, std::string> uv_map_pngs; bool has_uv_png{false}; + std::vector<ObjParser::ObjUseMtl> usemtls; // material spans, for texture import }; struct ObjDialogInOut @@ -32,8 +34,18 @@ struct ObjDialogInOut std::string lost_material_name{""}; }; typedef std::function<void(ObjDialogInOut &in_out)> ObjImportColorFn; -extern bool load_obj(const char *path, TriangleMesh *mesh, ObjInfo &vertex_colors, std::string &message); -extern bool load_obj(const char *path, Model *model, ObjInfo &vertex_colors, std::string &message, const char *object_name = nullptr); +extern bool load_obj(const char *path, TriangleMesh *mesh, ObjInfo &vertex_colors, std::string &message, ObjParser::MtlData *out_mtl = nullptr); +extern bool load_obj(const char *path, Model *model, ObjInfo &vertex_colors, std::string &message, const char *object_name = nullptr, ObjParser::MtlData *out_mtl = nullptr); + +struct TexturedMesh; +// Build a TexturedMesh (vertices + per-face UVs + the texture files named by map_Kd) from a +// parsed OBJ plus its material table, so the texture-to-color importer can sample face colours. +extern bool obj_to_textured_mesh( + const ObjInfo& obj_info, + const indexed_triangle_set& its, + const ObjParser::MtlData& mtl_data, + const std::string& obj_directory, + TexturedMesh& out); extern bool store_obj(const char *path, TriangleMesh *mesh); extern bool store_obj(const char *path, ModelObject *model); diff --git a/src/libslic3r/Format/ResourcePathUtils.hpp b/src/libslic3r/Format/ResourcePathUtils.hpp new file mode 100644 index 0000000000..d82b92bd45 --- /dev/null +++ b/src/libslic3r/Format/ResourcePathUtils.hpp @@ -0,0 +1,240 @@ +#ifndef slic3r_Format_ResourcePathUtils_hpp_ +#define slic3r_Format_ResourcePathUtils_hpp_ + +#include <algorithm> +#include <cctype> +#include <cstddef> +#include <string> +#include <vector> + +#include <boost/filesystem.hpp> +#include <boost/log/trivial.hpp> + +namespace Slic3r { +namespace resource_path { + +inline std::string ascii_lower_copy(const std::string& value) +{ + std::string lowered; + lowered.reserve(value.size()); + for (unsigned char ch : value) + lowered.push_back(static_cast<char>(std::tolower(ch))); + return lowered; +} + +inline boost::filesystem::path portable_path_copy(const boost::filesystem::path& value) +{ + std::string portable = value.string(); + std::replace(portable.begin(), portable.end(), '\\', '/'); + return boost::filesystem::path(portable); +} + +inline int hex_digit_value(char ch) +{ + if (ch >= '0' && ch <= '9') return ch - '0'; + if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10; + if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10; + return -1; +} + +// Byte-level percent decoding. Per RFC 3986 the %XX byte stream is expected to be +// UTF-8 when produced from URIs / Assimp aiString; this function performs no +// transcoding, so callers must treat both input and output as raw UTF-8 bytes. +inline std::string percent_decode_copy(const std::string& value) +{ + std::string decoded; + decoded.reserve(value.size()); + for (std::size_t i = 0; i < value.size(); ++i) { + if (value[i] == '%' && i + 2 < value.size()) { + const int hi = hex_digit_value(value[i + 1]); + const int lo = hex_digit_value(value[i + 2]); + if (hi >= 0 && lo >= 0) { + decoded.push_back(static_cast<char>((hi << 4) | lo)); + i += 2; + continue; + } + } + decoded.push_back(value[i]); + } + return decoded; +} + +inline std::string strip_file_uri_prefix_copy(const std::string& value) +{ + const std::string lower = ascii_lower_copy(value); + if (lower.rfind("file://", 0) != 0) + return value; + + std::string path = value.substr(7); + if (ascii_lower_copy(path).rfind("localhost/", 0) == 0) + path.erase(0, std::string("localhost").size()); + else if (!path.empty() && path.front() != '/') + path = "//" + path; + + // file:///C:/... should become C:/..., while file:///tmp/... keeps /tmp/... + if (path.size() >= 3 && path[0] == '/' && std::isalpha(static_cast<unsigned char>(path[1])) && path[2] == ':') + path.erase(path.begin()); + return path; +} + +inline bool file_uri_has_remote_authority(const std::string& value) +{ + const std::string lower = ascii_lower_copy(value); + if (lower.rfind("file://", 0) != 0) + return false; + + const std::string path = value.substr(7); + if (path.empty() || path.front() == '/') + return false; + + const std::size_t slash = path.find('/'); + const std::string authority = path.substr(0, slash); + return ascii_lower_copy(authority) != "localhost"; +} + +inline bool looks_like_windows_absolute_path(const boost::filesystem::path& path) +{ + const std::string portable = portable_path_copy(path).string(); + return portable.size() >= 3 + && std::isalpha(static_cast<unsigned char>(portable[0])) + && portable[1] == ':' + && portable[2] == '/'; +} + +inline boost::filesystem::path filename_from_portable_path(const boost::filesystem::path& value) +{ + const boost::filesystem::path portable = portable_path_copy(value); + return portable.filename(); +} + +inline boost::filesystem::path find_child_case_insensitive( + const boost::filesystem::path& directory, + const boost::filesystem::path& requested_name, + const char* context) +{ + if (!boost::filesystem::exists(directory) || !boost::filesystem::is_directory(directory)) + return {}; + + const std::string requested_lower = ascii_lower_copy(requested_name.filename().string()); + std::vector<boost::filesystem::path> matches; + + boost::system::error_code ec; + for (boost::filesystem::directory_iterator it(directory, ec), end; !ec && it != end; it.increment(ec)) { + if (ascii_lower_copy(it->path().filename().string()) == requested_lower) + matches.push_back(it->path()); + } + + if (matches.size() == 1) + return matches.front(); + + if (matches.size() > 1) { + BOOST_LOG_TRIVIAL(warning) << context << ": ambiguous case-insensitive resource match for " + << requested_name << " in " << directory; + } + + return {}; +} + +inline boost::filesystem::path resolve_existing_path_case_insensitive( + const boost::filesystem::path& requested_path, + const char* context = "resource_path") +{ + const boost::filesystem::path normalized_path = portable_path_copy(requested_path); + + if (normalized_path.empty()) + return {}; + + if (boost::filesystem::exists(normalized_path)) + return normalized_path; + + boost::filesystem::path current; + bool initialized = false; + + for (const boost::filesystem::path& part : normalized_path) { + if (part == normalized_path.root_name() || part == normalized_path.root_directory()) { + current /= part; + initialized = true; + continue; + } + + if (!initialized) { + current = boost::filesystem::current_path(); + initialized = true; + } + + boost::filesystem::path exact = current / part; + if (boost::filesystem::exists(exact)) { + current = exact; + continue; + } + + boost::filesystem::path matched = find_child_case_insensitive(current, part, context); + if (matched.empty()) + return {}; + + BOOST_LOG_TRIVIAL(info) << context << ": resolved resource path case-insensitively from " + << exact << " to " << matched; + current = matched; + } + + return boost::filesystem::exists(current) ? current : boost::filesystem::path(); +} + +inline boost::filesystem::path resolve_existing_relative_path_case_insensitive( + const boost::filesystem::path& base_dir, + const boost::filesystem::path& resource_path, + const char* context = "resource_path") +{ + const boost::filesystem::path requested = resource_path.is_absolute() ? resource_path : base_dir / resource_path; + return resolve_existing_path_case_insensitive(requested, context); +} + +// Resolve a resource path that originated outside our own code (e.g. a glTF/FBX +// material texture reference or a file:// URI inside a 3MF descriptor). +// +// `raw_path` is expected to be UTF-8 regardless of host platform: file URIs are +// UTF-8 by spec, and Assimp aiString uses UTF-8 internally. Cross-platform +// correctness on Windows additionally relies on the process having called +// boost::nowide::nowide_filesystem() during startup (see src/BambuStudio.cpp), +// which imbues boost::filesystem::path with a UTF-8 codecvt so that +// `path(std::string)` constructs from UTF-8 byte sequences. Callers that bypass +// the main entry point (standalone CLI tools, unit tests) must reproduce that +// setup themselves before invoking this helper. +inline boost::filesystem::path resolve_external_resource_path( + const boost::filesystem::path& base_dir, + const std::string& raw_path, + const char* context = "resource_path", + bool allow_basename_fallback = true) +{ + if (raw_path.empty()) + return {}; + + const bool remote_file_uri = file_uri_has_remote_authority(raw_path); + const std::string decoded_path = percent_decode_copy(strip_file_uri_prefix_copy(raw_path)); + const boost::filesystem::path requested = portable_path_copy(boost::filesystem::path(decoded_path)); + + boost::filesystem::path resolved = (requested.is_absolute() || looks_like_windows_absolute_path(requested)) ? + resolve_existing_path_case_insensitive(requested, context) : + resolve_existing_relative_path_case_insensitive(base_dir, requested, context); + if (!resolved.empty()) + return resolved; + + if (!allow_basename_fallback || remote_file_uri) + return {}; + + const boost::filesystem::path basename = filename_from_portable_path(requested); + if (basename.empty()) + return {}; + + resolved = resolve_existing_relative_path_case_insensitive(base_dir, basename, context); + if (!resolved.empty()) { + BOOST_LOG_TRIVIAL(info) << context << ": resolved resource by basename from " + << requested << " to " << resolved; + } + return resolved; +} + +} // namespace resource_path +} // namespace Slic3r + +#endif /* slic3r_Format_ResourcePathUtils_hpp_ */ diff --git a/src/libslic3r/Format/STEP.cpp b/src/libslic3r/Format/STEP.cpp index 8b07286c5b..22e39b207f 100644 --- a/src/libslic3r/Format/STEP.cpp +++ b/src/libslic3r/Format/STEP.cpp @@ -111,14 +111,19 @@ bool StepPreProcessor::isUtf8File(const char* path) bool StepPreProcessor::isUtf8(const std::string str) { size_t num = 0; - int i = 0; + size_t i = 0; while (i < str.length()) { - if ((str[i] & 0x80) == 0x00) { + const unsigned char lead = static_cast<unsigned char>(str[i]); + if ((lead & 0x80) == 0x00) { i++; - } else if ((num = preNum(str[i])) > 2) { + // preNum() counts the leading 1 bits, and a multi-byte sequence is 2 to 4 + // bytes long, so anything outside that range is not a lead byte. + } else if ((num = preNum(lead)) >= 2 && num <= 4) { + if (i + num > str.length()) + return false; i++; - for (int j = 0; j < num - 1; j++) { - if ((str[i] & 0xc0) != 0x80) + for (size_t j = 0; j < num - 1; j++) { + if ((static_cast<unsigned char>(str[i]) & 0xc0) != 0x80) return false; i++; } @@ -132,15 +137,20 @@ bool StepPreProcessor::isUtf8(const std::string str) bool StepPreProcessor::isGBK(const std::string str) { size_t i = 0; while (i < str.length()) { - if (str[i] <= 0x7f) { + // char is signed here, so every byte compares <= 0x7f unless widened first. + const unsigned char lead = static_cast<unsigned char>(str[i]); + if (lead <= 0x7f) { i++; continue; } else { - if (str[i] >= 0x81 && - str[i] <= 0xfe && - str[i + 1] >= 0x40 && - str[i + 1] <= 0xfe && - str[i + 1] != 0xf7) { + if (i + 1 >= str.length()) + return false; + const unsigned char trail = static_cast<unsigned char>(str[i + 1]); + if (lead >= 0x81 && + lead <= 0xfe && + trail >= 0x40 && + trail <= 0xfe && + trail != 0xf7) { i += 2; continue; } @@ -586,7 +596,7 @@ Step::Step_Status Step::mesh(Model* model, for (Standard_Integer aNodeIter = 1; aNodeIter <= aTriangulation->NbNodes(); ++aNodeIter) { gp_Pnt aPnt = aTriangulation->Node(aNodeIter); aPnt.Transform(aTrsf); - points.emplace_back(std::move(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z()))); + points.emplace_back(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z())); } // BBS: copy triangles const TopAbs_Orientation anOrientation = anExpSF.Current().Orientation(); @@ -712,7 +722,7 @@ unsigned int Step::get_triangle_num(double linear_deflection, double angle_defle return 0; } } - } catch(Exception e) { + } catch(const Exception &) { return 0; } diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 3000adb441..b0cbb1fd50 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -4,6 +4,7 @@ #include "../Preset.hpp" #include "../Utils.hpp" #include "../LocalesUtils.hpp" +#include "../FilamentMixer.hpp" #include "../GCode.hpp" #include "../Geometry.hpp" #include "../GCode/ThumbnailData.hpp" @@ -246,6 +247,8 @@ static constexpr const char* BUILD_TAG = "build"; static constexpr const char* ITEM_TAG = "item"; static constexpr const char* METADATA_TAG = "metadata"; static constexpr const char* FILAMENT_TAG = "filament"; +static constexpr const char* MIXED_FILAMENT_TAG = "mixed_filament"; +static constexpr const char* MIXED_FILAMENT_COMPONENTS_TAG = "components"; static constexpr const char* SLICE_WARNING_TAG = "warning"; static constexpr const char* WARNING_MSG_TAG = "msg"; static constexpr const char *FILAMENT_ID_TAG = "id"; @@ -1315,6 +1318,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) bool _handle_end_config_metadata(); bool _handle_start_config_filament(const char** attributes, unsigned int num_attributes); + bool _handle_start_config_mixed_filament(const char** attributes, unsigned int num_attributes); bool _handle_end_config_filament(); bool _handle_start_config_warning(const char** attributes, unsigned int num_attributes); @@ -2694,6 +2698,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) return; } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", load project config file successfully from %1%\n") %dest_file; + + // Heal any gradient-curve slots corrupted by the legacy "|" separator collision + // (see FilamentMixer::sanitize_mixed_gradient_curve_array). The 3MF JSON itself + // is safe (";" + C-style escape), but older projects saved through the buggy + // export_selections/load_selections path may already carry single-point entries + // that fail MakerWorld's "curve needs >= 2 points" check. + if (auto* curve_opt = config.option<ConfigOptionStrings>("filament_mixed_gradient_curve")) + Slic3r::sanitize_mixed_gradient_curve_array(curve_opt->values); } } @@ -3511,6 +3523,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) res = _handle_start_config_plater_instance(attributes, num_attributes); else if (::strcmp(FILAMENT_TAG, name) == 0) res = _handle_start_config_filament(attributes, num_attributes); + else if (::strcmp(MIXED_FILAMENT_TAG, name) == 0) + res = _handle_start_config_mixed_filament(attributes, num_attributes); else if (::strcmp(SLICE_WARNING_TAG, name) == 0) res = _handle_start_config_warning(attributes, num_attributes); else if (::strcmp(NOZZLE_TAG, name) == 0) @@ -4684,6 +4698,23 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) return true; } + bool _BBS_3MF_Importer::_handle_start_config_mixed_filament(const char** attributes, unsigned int num_attributes) + { + if (m_curr_plater) { + std::string id = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_ID_TAG); + std::string type = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_TYPE_TAG); + std::string color = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_COLOR_TAG); + std::string components = bbs_get_attribute_value_string(attributes, num_attributes, MIXED_FILAMENT_COMPONENTS_TAG); + PlateMixedFilamentInfo mixed_info; + mixed_info.id = atoi(id.c_str()); + mixed_info.type = type; + mixed_info.color = color; + mixed_info.components = components; + m_curr_plater->mixed_filaments_info.push_back(mixed_info); + } + return true; + } + bool _BBS_3MF_Importer::_handle_end_config_filament() { // do nothing @@ -8488,6 +8519,17 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) << FILAMENT_USED_FOR_SUPPORT << "=\"" << std::boolalpha << it->used_for_support << "\"/>\n"; } + // Mixed (virtual) filaments used by this plate. These are resolved to physical + // components before g-code statistics, so they are not present in the <filament> + // list above and are recorded separately here. + for (auto it = plate_data->mixed_filaments_info.begin(); it != plate_data->mixed_filaments_info.end(); it++) + { + stream << " <" << MIXED_FILAMENT_TAG << " " << FILAMENT_ID_TAG << "=\"" << std::to_string(it->id) << "\" " + << FILAMENT_TYPE_TAG << "=\"" << it->type << "\" " + << FILAMENT_COLOR_TAG << "=\"" << it->color << "\" " + << MIXED_FILAMENT_COMPONENTS_TAG << "=\"" << it->components << "\"/>\n"; + } + for (auto it = plate_data->warnings.begin(); it != plate_data->warnings.end(); it++) { stream << " <" << SLICE_WARNING_TAG << " msg=\"" << it->msg << "\" level=\"" << std::to_string(it->level) << "\" error_code =\"" << it->error_code << "\" />\n"; } @@ -8921,7 +8963,7 @@ private: BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " inital and interval = " << m_interval; m_next_backup = boost::get_system_time() + boost::posix_time::seconds(m_interval); boost::unique_lock lock(m_mutex); - m_thread = std::move(boost::thread(boost::ref(*this))); + m_thread = boost::thread(boost::ref(*this)); } ~_BBS_Backup_Manager() { diff --git a/src/libslic3r/Format/bbs_3mf.hpp b/src/libslic3r/Format/bbs_3mf.hpp index 9c697a14fc..7f5bb8c78d 100644 --- a/src/libslic3r/Format/bbs_3mf.hpp +++ b/src/libslic3r/Format/bbs_3mf.hpp @@ -48,6 +48,18 @@ public: }; +// Mixed (virtual) filament used by a plate. Mixed filaments are virtual slots that get +// resolved to their physical components before g-code statistics, so they never appear in +// slice_filaments_info. They are recorded here separately so a plate's mixed-color usage +// can be recovered from slice_info. +struct PlateMixedFilamentInfo +{ + int id{0}; // 1-based virtual filament slot id + std::string type; + std::string color; // blended display color, "#RRGGBB" + std::string components; // 1-based physical component ids, comma separated, e.g. "1,3" +}; + //BBS: define plate data list related structures struct PlateData { @@ -89,6 +101,8 @@ struct PlateData std::string first_layer_time; std::string plate_name; std::vector<FilamentInfo> slice_filaments_info; + // Mixed (virtual) filaments used by this plate; empty when no mixed filament is used. + std::vector<PlateMixedFilamentInfo> mixed_filaments_info; std::vector<size_t> skipped_objects; DynamicPrintConfig config; bool is_support_used {false}; diff --git a/src/libslic3r/Format/objparser.cpp b/src/libslic3r/Format/objparser.cpp index 82bf2b4963..886fa423bd 100644 --- a/src/libslic3r/Format/objparser.cpp +++ b/src/libslic3r/Format/objparser.cpp @@ -262,12 +262,9 @@ static bool obj_parseline(const char *line, ObjData &data) } face_index_count++; } - if (face_index_count == 3) {//tri - data.usemtls.back().face_end++; - } else if (face_index_count == 4) {//quad - data.usemtls.back().face_end++; - data.usemtls.back().face_end++; - } + if (face_index_count >= 3) { + data.usemtls.back().face_end += face_index_count - 2; + } } vertex.coordIdx = -1; vertex.normalIdx = -1; @@ -374,6 +371,107 @@ static bool obj_parseline(const char *line, ObjData &data) return true; } static std::string cur_mtl_name = ""; +static bool mtl_is_space(char c) +{ + return c == ' ' || c == '\t' || c == '\r'; +} + +static const char* mtl_skip_ws(const char *line) +{ + while (mtl_is_space(*line)) + ++line; + return line; +} + +static const char* mtl_skip_token(const char *line) +{ + while (*line != 0 && !mtl_is_space(*line)) + ++line; + return line; +} + +static bool mtl_token_equals(const char *begin, const char *end, const char *token) +{ + const size_t len = static_cast<size_t>(end - begin); + return strlen(token) == len && strncmp(begin, token, len) == 0; +} + +static std::string mtl_trim_value(const char *line) +{ + const char *begin = mtl_skip_ws(line); + const char *end = begin + strlen(begin); + while (end > begin && mtl_is_space(*(end - 1))) + --end; + return std::string(begin, end); +} + +static bool mtl_skip_numeric_token(const char *&line) +{ + const char *begin = mtl_skip_ws(line); + if (*begin == 0) + return false; + char *endptr = 0; + strtod(begin, &endptr); + if (endptr == begin || (!mtl_is_space(*endptr) && *endptr != 0)) + return false; + line = mtl_skip_ws(endptr); + return true; +} + +static bool mtl_skip_required_tokens(const char *&line, int count) +{ + for (int i = 0; i < count; ++i) { + line = mtl_skip_ws(line); + if (*line == 0) + return false; + line = mtl_skip_token(line); + } + line = mtl_skip_ws(line); + return true; +} + +static std::string mtl_parse_texture_name(const char *line) +{ + const char *original = mtl_skip_ws(line); + const char *current = original; + + while (*current == '-') { + const char *option_begin = current; + const char *option_end = mtl_skip_token(current); + current = option_end; + + if (mtl_token_equals(option_begin, option_end, "-o") || + mtl_token_equals(option_begin, option_end, "-s") || + mtl_token_equals(option_begin, option_end, "-t")) { + int skipped = 0; + while (skipped < 3 && mtl_skip_numeric_token(current)) + ++skipped; + if (skipped == 0) + return mtl_trim_value(original); + continue; + } + + int option_args = -1; + if (mtl_token_equals(option_begin, option_end, "-mm")) + option_args = 2; + else if (mtl_token_equals(option_begin, option_end, "-bm") || + mtl_token_equals(option_begin, option_end, "-boost") || + mtl_token_equals(option_begin, option_end, "-texres") || + mtl_token_equals(option_begin, option_end, "-clamp") || + mtl_token_equals(option_begin, option_end, "-blendu") || + mtl_token_equals(option_begin, option_end, "-blendv") || + mtl_token_equals(option_begin, option_end, "-cc") || + mtl_token_equals(option_begin, option_end, "-imfchan") || + mtl_token_equals(option_begin, option_end, "-type")) + option_args = 1; + + if (option_args < 0 || !mtl_skip_required_tokens(current, option_args)) + return mtl_trim_value(original); + } + + return mtl_trim_value(current); +} + static bool mtl_parseline(const char *line, MtlData &data) { if (*line == 0) return true; @@ -394,13 +492,14 @@ static bool mtl_parseline(const char *line, MtlData &data) ObjNewMtl new_mtl; cur_mtl_name = line; data.new_mtl_unmap[cur_mtl_name] = std::make_shared<ObjNewMtl>(); + data.mtl_orders.emplace_back(cur_mtl_name); break; } case 'm': { if (*(line++) != 'a' || *(line++) != 'p' || *(line++) != '_' || *(line++) != 'K' || *(line++) != 'd') return false; EATWS(); if (data.new_mtl_unmap.find(cur_mtl_name) != data.new_mtl_unmap.end()) { - data.new_mtl_unmap[cur_mtl_name]->map_Kd = line; + data.new_mtl_unmap[cur_mtl_name]->map_Kd = mtl_parse_texture_name(line); } break; } diff --git a/src/libslic3r/Format/objparser.hpp b/src/libslic3r/Format/objparser.hpp index 48493de3de..58afd015a8 100644 --- a/src/libslic3r/Format/objparser.hpp +++ b/src/libslic3r/Format/objparser.hpp @@ -122,6 +122,9 @@ struct MtlData // Version of the data structure for load / store in the private binary format. int version; std::unordered_map<std::string, std::shared_ptr<ObjNewMtl>> new_mtl_unmap; + // Material names in declaration order. new_mtl_unmap is unordered, but OBJ material + // indices are positional, so texture import needs the original order. + std::vector<std::string> mtl_orders; }; extern bool objparse(const char *path, ObjData &data); extern bool mtlparse(const char *path, MtlData &data); diff --git a/src/libslic3r/Format/svg.cpp b/src/libslic3r/Format/svg.cpp index 7bfd73b987..7b720e62ef 100644 --- a/src/libslic3r/Format/svg.cpp +++ b/src/libslic3r/Format/svg.cpp @@ -352,7 +352,7 @@ bool load_svg(const char *path, Model *model, std::string &message) for (Standard_Integer aNodeIter = 1; aNodeIter <= aTriangulation->NbNodes(); ++aNodeIter) { gp_Pnt aPnt = aTriangulation->Node(aNodeIter); aPnt.Transform(aTrsf); - points.emplace_back(std::move(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z()))); + points.emplace_back(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z())); } // BBS: copy triangles const TopAbs_Orientation anOrientation = anExpSF.Current().Orientation(); diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index f5db2e8349..e9cdb620e0 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -768,30 +768,31 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print) return changes; } + // Clearance the tower-approach router keeps around the tower: the avoid box is + // inflated by this much before routing, and the inflated corners must stay on the + // bed for a route to be generated at all. + static constexpr float wipe_tower_routing_clearance = 2.f; + // BBS // start_pos refers to the last position before the wipe_tower. // end_pos refers to the wipe tower's start_pos. // using the print coordinate system - Polyline WipeTowerIntegration::generate_path_to_wipe_tower(const Point& start_pos,const Point &end_pos , const BoundingBox& avoid_polygon , const BoundingBox& printer_bbx) const + Polyline WipeTowerIntegration::generate_path_to_wipe_tower(const Point& start_pos,const Point &end_pos , const BoundingBox& avoid_polygon , const Polygons& bed_polygons) const { Polyline res; - coord_t alpha = scaled(2.f); // offset distance + coord_t alpha = scaled(wipe_tower_routing_clearance); // offset distance BoundingBox avoid_polygon_inner = avoid_polygon; avoid_polygon_inner.offset(alpha); coord_t width = avoid_polygon_inner.max[0] - avoid_polygon_inner.min[0]; - Polygon bed_polygon = printer_bbx.polygon(); Vec2f v(1, 0); // the first print direction of end_pos. if (abs(end_pos[0] - avoid_polygon_inner.min[0]) < width / 2) v = -v; // judge whether the wipe tower's infill goes to the left or right. - // Judge whether the avoid_polygon_inner is outside the printer_bbx. + // Judge whether the avoid_polygon_inner is outside the bed. The real printable + // outline is tested (not its bounding box), so on circular/custom beds corners + // hanging off the bed are rejected. // If so, do nothing and just go directly to the end_pos. - bool is_bbx_in_bed = true; Points avoid_points = avoid_polygon_inner.polygon().points; - for (auto &wipe_tower_bbx_p : avoid_points) { - if (ClipperLib::PointInPolygon(wipe_tower_bbx_p, bed_polygon.points) != 1) { - is_bbx_in_bed = false; - break; - } - } + const bool is_bbx_in_bed = std::all_of(avoid_points.begin(), avoid_points.end(), + [&bed_polygons](const Point &pt) { return contains(bed_polygons, pt, /*border_result=*/false); }); if (!is_bbx_in_bed) { res.points.push_back(end_pos); return res; @@ -898,27 +899,17 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print) return Eigen::Rotation2Df(alpha) * (pt + m_rib_offset) + m_wipe_tower_pos; } - // Printable-area bounds for tower-approach routing, in object coordinates (shared by - // the BBL avoid-perimeter path in append_tcr and the Type2 skip-points router). - // Multi-nozzle: clamp the travel bounds to the region every extruder can reach - // (get_extruder_shared_printable_polygon) instead of the full bed. Gated on the - // multi-nozzle predicate so every existing single/dual printer keeps the historic - // full-printable_area routing byte-identical. - BoundingBox WipeTowerIntegration::printer_travel_bounds(GCode &gcodegen) const + // Bed outline the tower-approach router plans against, in object coordinates. The real + // outline is returned, not its bounding box, so the router's containment tests fail off + // the bed on circular/custom shapes; the multi-nozzle narrowing lives in the accessor. + Polygons WipeTowerIntegration::shared_printable_area(GCode &gcodegen) const { - const Vec2f plate_origin_2d(m_plate_origin(0), m_plate_origin(1)); - BoundingBox printer_bbx; - if (is_multi_nozzle_printer(gcodegen.m_config)) { - printer_bbx = get_extents(gcodegen.m_print->get_extruder_shared_printable_polygon()); - printer_bbx.min = wipe_tower_point_to_object_point(gcodegen, unscaled<float>(printer_bbx.min) + plate_origin_2d); - printer_bbx.max = wipe_tower_point_to_object_point(gcodegen, unscaled<float>(printer_bbx.max) + plate_origin_2d); - } else { - Points bed_points; - for (const auto& p : gcodegen.m_config.printable_area.values) - bed_points.push_back(wipe_tower_point_to_object_point(gcodegen, p.cast<float>() + plate_origin_2d)); - printer_bbx = BoundingBox(bed_points); - } - return printer_bbx; + // The frame change is a pure translation, so transform the origin once. + const Point offset = wipe_tower_point_to_object_point(gcodegen, Vec2f(m_plate_origin(0), m_plate_origin(1))); + Polygons bed_polygons = gcodegen.m_print->get_extruder_shared_printable_polygon(); + for (Polygon &poly : bed_polygons) + poly.translate(offset); + return bed_polygons; } // With skip points enabled the Type2 tower wall has an opening at each toolchange's @@ -933,15 +924,37 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print) if (!WipeTower2::use_gap_wall(gcodegen.m_config)) return {}; const Vec2f plate_origin_2d(m_plate_origin(0), m_plate_origin(1)); - // Transform the tower-local bbx corners exactly like the tcr points; a rotated - // tower gets a conservative axis-aligned envelope. - Polygon avoid_points = scaled(m_wipe_tower_bbx).polygon(); - for (auto& p : avoid_points.points) - p = wipe_tower_point_to_object_point(gcodegen, transform_wt2_pt(unscale(p).cast<float>()) + plate_origin_2d); - BoundingBox avoid_bbx(avoid_points.points); - if (avoid_bbx.contains(route_start)) + // Transform tower-local corners exactly like the tcr points; a rotated tower gets a + // conservative axis-aligned envelope from the result. + auto tower_polygon = [&](const BoundingBoxf &bbx) { + Polygon poly = scaled(bbx).polygon(); + for (Point &p : poly.points) + p = wipe_tower_point_to_object_point(gcodegen, transform_wt2_pt(unscale(p).cast<float>()) + plate_origin_2d); + return poly; + }; + // The avoid envelope covers the first-layer brim (and rib flare), which a travel may + // cross freely: early-out only when the approach already starts over the tower body + // itself, so a start between the wall and the brim edge still gets routed in through + // the wall opening. Test the rotated polygon, not its bounding box — at angles off the + // axes the box's corner triangles cover most of the brim ring. + const float body_width = gcodegen.m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib ? m_wipe_tower_depth : m_right; + if (tower_polygon(BoundingBoxf(Vec2d(0., 0.), Vec2d(body_width, m_wipe_tower_depth))).contains(route_start)) return {}; - Polyline travel_polyline = generate_path_to_wipe_tower(route_start, start_wipe_pos, avoid_bbx, printer_travel_bounds(gcodegen)); + + const Polygons bed = shared_printable_area(gcodegen); + BoundingBox avoid_bbx = get_extents(tower_polygon(m_wipe_tower_bbx)); + // The inflated corners must stay on the bed for the router to generate a route at all: + // clamp the box against the bed shrunk by the clearance the router adds, so a tower + // parked near the bed edge is still routed along the clamped side instead of always + // travelling straight across the tower. + BoundingBox clamp_bbx = get_extents(bed); + clamp_bbx.offset(-(scaled(wipe_tower_routing_clearance) + SCALED_EPSILON)); + avoid_bbx.min = avoid_bbx.min.cwiseMax(clamp_bbx.min); + avoid_bbx.max = avoid_bbx.max.cwiseMin(clamp_bbx.max); + if (avoid_bbx.min.x() >= avoid_bbx.max.x() || avoid_bbx.min.y() >= avoid_bbx.max.y()) + return {}; + + Polyline travel_polyline = generate_path_to_wipe_tower(route_start, start_wipe_pos, avoid_bbx, bed); std::string gcode; // The polyline's last point is start_wipe_pos itself — emitted by the caller. for (size_t i = 0; i + 1 < travel_polyline.points.size(); ++i) @@ -1137,8 +1150,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print) float old_retract_length = (old_filament_id != -1) ? full_config.retraction_length.get_at(old_fi) : 0; float new_retract_length = full_config.retraction_length.get_at(new_fi); - float old_retract_length_toolchange = (old_filament_id != -1) ? full_config.retract_length_toolchange.get_at(old_filament_id) : 0; - float new_retract_length_toolchange = full_config.retract_length_toolchange.get_at(new_filament_id); + float old_retract_length_toolchange = (old_filament_id != -1) ? full_config.retract_length_toolchange.get_at(old_fi) : 0; + float new_retract_length_toolchange = full_config.retract_length_toolchange.get_at(new_fi); int old_filament_temp = (old_filament_id != -1) ? (gcodegen.on_first_layer()? full_config.nozzle_temperature_initial_layer.get_at(old_fi) : full_config.nozzle_temperature.get_at(old_fi)) : 210; int new_filament_temp = gcodegen.on_first_layer() ? full_config.nozzle_temperature_initial_layer.get_at(new_fi) : full_config.nozzle_temperature.get_at(new_fi); Vec3d nozzle_pos = gcode_writer.get_position(); @@ -1322,7 +1335,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print) Vec2f gcode_last_pos2d{gcode_last_pos[0], gcode_last_pos[1]}; Point gcode_last_pos2d_object = gcodegen.gcode_to_point(gcode_last_pos2d.cast<double>() + plate_origin_2d.cast<double>()); Point start_wipe_pos = wipe_tower_point_to_object_point(gcodegen, tool_change_start_pos + plate_origin_2d); - BoundingBox avoid_bbx, printer_bbx = printer_travel_bounds(gcodegen); + BoundingBox avoid_bbx; { // set avoid_bbx avoid_bbx = scaled(m_wipe_tower_bbx); @@ -1334,7 +1347,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print) avoid_bbx = BoundingBox(avoid_points.points); } std::string travel_to_wipe_tower_gcode; - Polyline travel_polyline = generate_path_to_wipe_tower(gcode_last_pos2d_object, start_wipe_pos, avoid_bbx, printer_bbx); + Polyline travel_polyline = generate_path_to_wipe_tower(gcode_last_pos2d_object, start_wipe_pos, avoid_bbx, shared_printable_area(gcodegen)); for (size_t i = 0; i < travel_polyline.points.size(); ++i) { const auto &p = travel_polyline.points[i]; @@ -1554,7 +1567,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print) interface_temp = gcodegen.config().nozzle_temperature_range_high.get_at(new_extruder_id); toolchange_temp_override = interface_temp; } - toolchange_gcode_str = gcodegen.set_extruder(new_extruder_id, tcr.print_z, false, toolchange_temp_override); // TODO: toolchange_z vs print_z + toolchange_gcode_str = gcodegen.set_extruder(new_extruder_id, tcr.print_z, false, toolchange_temp_override, + WipeTower2::wait_for_temp_enabled(gcodegen.m_config)); // TODO: toolchange_z vs print_z if (!travel_to_tower_now && !tcr.priming && WipeTower2::use_gap_wall(gcodegen.m_config)) { // The tool changed in place (multi-tool printer without ramming), so the // tower entry is the tcr's own positioning move — a straight line across @@ -1705,7 +1719,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print) std::string trimmed = line; trimmed.erase(0, trimmed.find_first_not_of(" \t")); bool skip_line = false; - if (boost::starts_with(trimmed, "M109")) { + if (boost::starts_with(trimmed, "M109") && trimmed.find(WipeTower2::wait_for_temp_tag()) == std::string::npos) { bool matches_extruder = true; if (trimmed.find('T') != std::string::npos) matches_extruder = trimmed.find(t_token) != std::string::npos; @@ -2884,6 +2898,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato DoExport::init_gcode_processor(print.config(), m_processor, m_silent_time_estimator_enabled, print.get_layered_nozzle_group_result()); const bool is_bbl_printers = print.is_BBL_printer(); + const bool skip_config_block = print.config().gcode_skip_config_block; const WipeTowerType wipe_tower_type = print.wipe_tower_type(); m_calib_config.clear(); // resets analyzer's tracking data @@ -3059,7 +3074,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // as configuration key / value pairs to be parsable by older versions of // PrusaSlicer G-code viewer. { - if (is_bbl_printers) { + if (is_bbl_printers && !skip_config_block) { file.write("; CONFIG_BLOCK_START\n"); std::string full_config; append_full_config(print, full_config); @@ -4086,23 +4101,25 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder) .c_str()); file.write("\n"); - file.write("; CONFIG_BLOCK_START\n"); - std::string full_config; - append_full_config(print, full_config); - if (!full_config.empty()) - file.write(full_config); + if (!skip_config_block) { + file.write("; CONFIG_BLOCK_START\n"); + std::string full_config; + append_full_config(print, full_config); + if (!full_config.empty()) + file.write(full_config); - // SoftFever: write compatiple info - int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type); - file.write_format("; first_layer_bed_temperature = %d\n", first_layer_bed_temperature); - file.write_format("; bed_shape = %s\n", print.full_print_config().opt_serialize("printable_area").c_str()); - file.write_format("; first_layer_temperature = %d\n", print.config().nozzle_temperature_initial_layer.get_at(0)); - file.write_format("; first_layer_height = %.3f\n", print.config().initial_layer_print_height.value); - - //SF TODO -// file.write_format("; variable_layer_height = %d\n", print.ad.adaptive_layer_height ? 1 : 0); - - file.write("; CONFIG_BLOCK_END\n\n"); + // SoftFever: write compatiple info + int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type); + file.write_format("; first_layer_bed_temperature = %d\n", first_layer_bed_temperature); + file.write_format("; bed_shape = %s\n", print.full_print_config().opt_serialize("printable_area").c_str()); + file.write_format("; first_layer_temperature = %d\n", print.config().nozzle_temperature_initial_layer.get_at(0)); + file.write_format("; first_layer_height = %.3f\n", print.config().initial_layer_print_height.value); + + //SF TODO +// file.write_format("; variable_layer_height = %d\n", print.ad.adaptive_layer_height ? 1 : 0); + + file.write("; CONFIG_BLOCK_END\n\n"); + } // !skip_config_block } file.write("\n"); @@ -4178,6 +4195,8 @@ void GCode::export_layer_filaments(GCodeProcessorResult* result) } } + result->used_mixed_filaments = m_print->get_slice_used_mixed_filaments(); + result->optimal_assignment.clear(); result->optimal_assignment.reserve(filament_map.size()); for (int nozzle_id : filament_map) @@ -5508,7 +5527,7 @@ LayerResult GCode::process_layer( // add tag for processor gcode += ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Layer_Change) + "\n"; // export layer z - char buf[64]; + char buf[80]; sprintf(buf, print.is_BBL_printer() ? "; Z_HEIGHT: %g\n" : ";Z:%g\n", print_z); gcode += buf; // export layer height @@ -5987,9 +6006,16 @@ LayerResult GCode::process_layer( const WipingExtrusions::ExtruderPerCopy *entity_overrides = nullptr; if (! layer_tools.has_extruder(correct_extruder_id)) { - // this entity is not overridden, but its extruder is not in layer_tools - we'll print it - // by last extruder on this layer (could happen e.g. when a wiping object is taller than others - dontcare extruders are eradicated from layer_tools) - correct_extruder_id = layer_tools.extruders.back(); + // A mixed-color slot is absent from layer_tools.extruders by design: + // resolve_mixed_filaments() replaced it with its physical components, + // and the sublayer block emits its geometry separately. Reassigning it + // to the last extruder here would print it in the wrong colour, so only + // fall back for genuinely stale (dontcare) extruders. + if (!layer_tools.is_mixed_slot(correct_extruder_id)) { + // this entity is not overridden, but its extruder is not in layer_tools - we'll print it + // by last extruder on this layer (could happen e.g. when a wiping object is taller than others - dontcare extruders are eradicated from layer_tools) + correct_extruder_id = layer_tools.extruders.back(); + } } printing_extruders.clear(); if (is_anything_overridden && use_overrides) { @@ -6077,7 +6103,16 @@ LayerResult GCode::process_layer( const bool island_level_ordering = print.config().print_sequence != PrintSequence::ByObject && single_object_instance_idx == size_t(-1) && print.config().print_order != PrintOrder::AsObjectList; - for (unsigned int filament_id : layer_tools.extruders) { + // A mixed-color slot is absent from layer_tools.extruders by design: resolve_mixed_filaments() + // replaced it with its physical components. Its geometry is still keyed under the slot in + // by_extruder though, and the sublayer emitter looks the plan up by slot id, so append the + // slots here. Appending rather than merging leaves the flush-optimized order untouched. + std::vector<unsigned int> plan_filaments = layer_tools.extruders; + for (const auto &grp : layer_tools.mixed_sub_layer_groups) + if (std::find(plan_filaments.begin(), plan_filaments.end(), grp.mixed_slot_0based) == plan_filaments.end()) + plan_filaments.push_back(grp.mixed_slot_0based); + + for (unsigned int filament_id : plan_filaments) { auto objects_by_extruder_it = by_extruder.find(filament_id); if (objects_by_extruder_it == by_extruder.end()) continue; @@ -6258,8 +6293,22 @@ LayerResult GCode::process_layer( } if (print.config().print_sequence == PrintSequence::ByLayer && m_enable_exclude_object && print.config().support_object_skip_flush.value) { - std::vector<size_t> filament_instances_id; - for (InstanceToPrint &instance : filament_to_print_instances[extruder_id].first) filament_instances_id.emplace_back(instance.label_object_id); + std::set<size_t> all_label_ids; + for (InstanceToPrint &instance : filament_to_print_instances[extruder_id].first) + all_label_ids.insert(instance.label_object_id); + // This extruder may also be printing sub-layers on behalf of a mixed slot, whose + // instances live under the slot id. Their labels belong in the same skip set, or + // exclude-object would not skip that geometry. + for (const auto &grp : layer_tools.mixed_sub_layer_groups) + for (unsigned int comp : grp.components_0based) + if (comp == extruder_id) { + auto mit = filament_to_print_instances.find(grp.mixed_slot_0based); + if (mit != filament_to_print_instances.end()) + for (const InstanceToPrint &inst : mit->second.first) + all_label_ids.insert(inst.label_object_id); + break; + } + std::vector<size_t> filament_instances_id(all_label_ids.begin(), all_label_ids.end()); m_filament_instances_code = _encode_label_ids_to_base64(filament_instances_id); } @@ -6540,6 +6589,318 @@ LayerResult GCode::process_layer( } } } + + // Mixed-color sublayer extrusion: if this extruder is a component of a mixed sublayer + // group, extrude the mixed slot's geometry at the appropriate sub-Z with scaled flow. + // Ported from BambuStudio and adapted to Orca's instance loop and its finer-grained + // per-role region filament options. + for (const auto &grp : layer_tools.mixed_sub_layer_groups) { + int sub_idx = -1; + for (size_t k = 0; k < grp.components_0based.size(); ++k) { + if (grp.components_0based[k] == extruder_id) { + sub_idx = static_cast<int>(k); + break; + } + } + if (sub_idx < 0) + continue; + + auto mixed_instances_it = filament_to_print_instances.find(grp.mixed_slot_0based); + if (mixed_instances_it == filament_to_print_instances.end() || mixed_instances_it->second.first.empty()) + continue; + + double lh = grp.layer_height > 0. ? grp.layer_height : static_cast<double>(height); + double cumulative_h = 0.0; + for (int i = 0; i < sub_idx; ++i) + cumulative_h += grp.sub_heights[i]; + double default_sub_h = grp.sub_heights[sub_idx]; + double default_sub_z = print_z - lh + cumulative_h + default_sub_h; + + m_sub_layer_flow_ratio = default_sub_h / lh; + m_sub_layer_height = default_sub_h; + m_nominal_z = default_sub_z; + + gcode += this->set_extruder(extruder_id, default_sub_z); + + for (InstanceToPrint &instance_to_print : mixed_instances_it->second.first) { + const bool use_per_volume = grp.is_gradient + && !grp.per_volume_gradient.empty() + && std::any_of(grp.per_volume_gradient.begin(), grp.per_volume_gradient.end(), + [&](const auto &kv) { return kv.first.obj == &instance_to_print.print_object; }); + + // --- Shared instance preamble (mirrors Orca's main instance loop) --- + const LayerToPrint &layer_to_print = layers[instance_to_print.layer_id]; + const auto &inst = instance_to_print.print_object.instances()[instance_to_print.instance_id]; + + bool object_layer_over_raft = layer_to_print.object_layer && layer_to_print.object_layer->id() > 0 && + instance_to_print.print_object.slicing_parameters().raft_layers() == layer_to_print.object_layer->id(); + m_config.apply(print.default_region_config()); + m_config.apply(instance_to_print.print_object.config(), true); + m_layer = layer_to_print.layer(); + m_object_layer_over_raft = object_layer_over_raft; + if (m_config.reduce_crossing_wall) + m_avoid_crossing_perimeters.init_layer(*m_layer); + + if (this->config().gcode_label_objects) { + gcode += std::string("; printing object ") + instance_to_print.print_object.model_object()->name + + " id:" + std::to_string(instance_to_print.print_object.get_id()) + " copy " + + std::to_string(inst.id) + "\n"; + } + if (m_enable_exclude_object) { + if (is_BBL_Printer()) { + m_writer.set_object_start_str( + std::string("; start printing object, unique label id: ") + + std::to_string(instance_to_print.label_object_id) + "\n" + "M624 " + + _encode_label_ids_to_base64({instance_to_print.label_object_id}) + "\n"); + } else { + const auto gflavor = print.config().gcode_flavor.value; + if (gflavor == gcfKlipper) { + m_writer.set_object_start_str(std::string("EXCLUDE_OBJECT_START NAME=") + + get_instance_name(&instance_to_print.print_object, inst.id) + "\n"); + } else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) { + m_writer.set_object_start_str(std::string("M486 S") + std::to_string(inst.unique_id) + "\n"); + } + } + } + + m_extrusion_quality_estimator.set_current_object(&instance_to_print.print_object); + + const Point &offset = inst.shift; + std::pair<const PrintObject*, Point> this_object_copy(&instance_to_print.print_object, offset); + if (m_last_obj_copy != this_object_copy) + m_avoid_crossing_perimeters.use_external_mp_once(); + m_last_obj_copy = this_object_copy; + this->set_origin(unscale(offset)); + + // --- Build emission plan --- + // Each entry represents one travel_to_z + extrude pass. Per-object mode produces + // exactly 1 entry (all regions, single sub_z); per-volume mode produces N entries + // for tagged volumes plus an optional entry for untagged residue. + struct SubLayerEmitEntry { + double sub_h; + double sub_z; + std::function<bool(size_t region_idx)> region_filter; + bool skip = false; + }; + std::vector<SubLayerEmitEntry> emit_plan; + + auto compute_sub_zh = [&](double r1, double r2, double &out_sub_h, double &out_sub_z) { + std::vector<double> sub_heights_local(grp.components_0based.size()); + for (size_t ci = 0; ci < grp.components_0based.size(); ++ci) + sub_heights_local[ci] = (static_cast<int>(ci) == grp.gradient_first_sorted_idx) ? r1 * lh : r2 * lh; + double cum = 0.0; + for (int ci = 0; ci < sub_idx; ++ci) + cum += sub_heights_local[ci]; + out_sub_h = sub_heights_local[sub_idx]; + out_sub_z = print_z - lh + cum + out_sub_h; + }; + + auto gradient_ratios = [](const auto &g) -> std::pair<double, double> { + double t = (g.total_layers > 0) ? (2.0 * g.current_idx + 1.0) / (2.0 * g.total_layers) : 0.5; + // Custom curve wins over linear range when present; OFF path stays bit-identical. + double r1 = g.curve.empty() + ? (g.gradient_start + (g.gradient_end - g.gradient_start) * t) + : sample_gradient_curve(g.curve, t); + return {r1, 1.0 - r1}; + }; + + // Orca splits BBS's three role filaments into five; a region belongs to the slot + // when any of its roles is assigned to it. + auto region_uses_slot = [](const PrintRegionConfig &rcfg, unsigned int slot_1b) { + return (unsigned int)rcfg.outer_wall_filament_id.value == slot_1b + || (unsigned int)rcfg.inner_wall_filament_id.value == slot_1b + || (unsigned int)rcfg.sparse_infill_filament_id.value == slot_1b + || (unsigned int)rcfg.internal_solid_filament_id.value == slot_1b + || (unsigned int)rcfg.top_surface_filament_id.value == slot_1b + || (unsigned int)rcfg.bottom_surface_filament_id.value == slot_1b; + }; + + double obj_sub_z = default_sub_z; + + if (use_per_volume) { + const PrintObject *po = &instance_to_print.print_object; + const unsigned int slot_1b = grp.mixed_slot_0based + 1; + + // Discover tagged volumes and untagged presence for this instance. + std::set<ObjectID> tagged_volumes_present; + bool has_untagged_for_slot = false; + for (ObjectByExtruder::Island &island : instance_to_print.object_by_extruder.islands) { + for (size_t r = 0; r < island.by_region.size(); ++r) { + const auto ®ion = island.by_region[r]; + if (region.perimeters.empty() && region.infills.empty()) + continue; + const PrintRegion &pr = print.get_print_region(r); + if (!region_uses_slot(pr.config(), slot_1b)) + continue; + ObjectID vid = pr.gradient_volume_id(); + if (vid.valid()) + tagged_volumes_present.insert(vid); + else + has_untagged_for_slot = true; + } + } + + // One entry per tagged volume. + for (const ObjectID &target_vid : tagged_volumes_present) { + auto vg_it = grp.per_volume_gradient.find({po, target_vid}); + if (vg_it == grp.per_volume_gradient.end()) + continue; + const auto &vg = vg_it->second; + auto [r1, r2] = gradient_ratios(vg); + + bool vol_no_split = false; + bool skip_entry = false; + const size_t n = grp.components_0based.size(); + if (n == 2 && vg.current_idx + 1 == vg.total_layers) { + const size_t dom_idx = (r1 >= r2) ? 0 : 1; + const unsigned int first_sorted_comp = grp.components_0based[grp.gradient_first_sorted_idx]; + const unsigned int other_comp = grp.components_0based[1 - grp.gradient_first_sorted_idx]; + const unsigned int dom_0b = (dom_idx == 0) ? first_sorted_comp : other_comp; + const unsigned int oth_0b = (dom_idx == 0) ? other_comp : first_sorted_comp; + if (dom_0b < oth_0b) { + vol_no_split = true; + if (extruder_id != dom_0b) + skip_entry = true; + } + } + + double vol_sub_h = default_sub_h; + double vol_sub_z = default_sub_z; + if (vol_no_split) { + vol_sub_h = lh; + vol_sub_z = print_z; + } else { + compute_sub_zh(r1, r2, vol_sub_h, vol_sub_z); + } + + emit_plan.push_back({vol_sub_h, vol_sub_z, + [target_vid, &print](size_t r) { + return print.get_print_region(r).gradient_volume_id() == target_vid; + }, + skip_entry}); + } + + // Optional entry for untagged regions (modifier / painted / fuzzy_skin). + if (has_untagged_for_slot) { + double obj_sub_h = default_sub_h; + auto og_it = grp.per_object_gradient.find(po); + if (og_it != grp.per_object_gradient.end()) { + auto [r1, r2] = gradient_ratios(og_it->second); + compute_sub_zh(r1, r2, obj_sub_h, obj_sub_z); + } + emit_plan.push_back({obj_sub_h, obj_sub_z, + [&print](size_t r) { + return !print.get_print_region(r).gradient_volume_id().valid(); + }, + false}); + } + } else { + // Legacy per-object path: single entry, no region filter. + double legacy_sub_h = default_sub_h; + obj_sub_z = default_sub_z; + if (grp.is_gradient) { + auto og_it = grp.per_object_gradient.find(&instance_to_print.print_object); + if (og_it != grp.per_object_gradient.end()) { + auto [r1, r2] = gradient_ratios(og_it->second); + compute_sub_zh(r1, r2, legacy_sub_h, obj_sub_z); + } + } + emit_plan.push_back({legacy_sub_h, obj_sub_z, nullptr, false}); + } + + // --- Unified emission loop --- + auto plan_has_infill = [](const std::vector<ObjectByExtruder::Island::Region> &by_region) { + for (const auto &r : by_region) + if (!r.infills.empty()) + return true; + return false; + }; + + for (auto &entry : emit_plan) { + if (entry.skip) + continue; + m_sub_layer_flow_ratio = entry.sub_h / lh; + m_sub_layer_height = entry.sub_h; + m_nominal_z = entry.sub_z; + // Use the same lazy-Z mechanism as change_layer(): set the flag so travel_to + // fires even when m_last_pos coincides with the first extrusion point, + // ensuring Z reaches sub_z via the combined XY+Z move. + m_need_change_layer_lift_z = true; + + for (ObjectByExtruder::Island &island : instance_to_print.object_by_extruder.islands) { + const auto &src = island.by_region; + std::vector<ObjectByExtruder::Island::Region> subset_storage; + if (entry.region_filter) { + subset_storage.resize(src.size()); + for (size_t r = 0; r < src.size(); ++r) + if (entry.region_filter(r)) + subset_storage[r] = src[r]; + } + const auto &by_region_specific = entry.region_filter ? subset_storage : src; + + // Orca resolves infill-first per region inside extrude_perimeters() + // (unlike BBS, which branches on a single global flag), so mirror the + // main instance loop's ordering exactly. + gcode += this->extrude_perimeters(print, by_region_specific, first_layer, false); + if (!has_wipe_tower && need_insert_timelapse_gcode_for_traditional + && printer_structure == PrinterStructure::psI3 + && !has_insert_timelapse_gcode && plan_has_infill(by_region_specific)) { + gcode += this->retract(false, false, auto_lift_type, true); + gcode += insert_timelapse_gcode(); + has_insert_timelapse_gcode = true; + } + gcode += this->extrude_infill(print, by_region_specific, false); + gcode += this->extrude_perimeters(print, by_region_specific, first_layer, true); + // ironing + gcode += this->extrude_infill(print, by_region_specific, true); + } + } + + // --- Shared support --- + if (instance_to_print.object_by_extruder.support && !instance_to_print.object_by_extruder.support->empty()) { + if (use_per_volume) { + m_nominal_z = obj_sub_z; + m_need_change_layer_lift_z = true; + } + ExtrusionRole support_role = instance_to_print.object_by_extruder.support_extrusion_role; + gcode += this->extrude_support(*instance_to_print.object_by_extruder.support, support_role); + // Make sure ironing is the last (Orca names this role erIroning, not erSupportIroning). + if (support_role == erMixed || support_role == erSupportMaterialInterface) + gcode += this->extrude_support(*instance_to_print.object_by_extruder.support, erIroning); + } + + // --- Shared instance footer (mirrors Orca's main instance loop) --- + if (!m_writer.is_object_start_str_empty()) { + m_writer.set_object_start_str(""); + } else if (m_enable_exclude_object) { + if (is_BBL_Printer()) { + m_writer.set_object_end_str(std::string("; stop printing object, unique label id: ") + + std::to_string(instance_to_print.label_object_id) + "\n" + + "M625\n"); + } else { + const auto gflavor = print.config().gcode_flavor.value; + if (gflavor == gcfKlipper) { + m_writer.set_object_end_str(std::string("EXCLUDE_OBJECT_END NAME=") + + get_instance_name(&instance_to_print.print_object, inst.id) + "\n"); + } else if (gflavor == gcfMarlinLegacy || gflavor == gcfMarlinFirmware || gflavor == gcfRepRapFirmware) { + m_writer.set_object_end_str(std::string("M486 S-1\n")); + } + } + } + } + + m_sub_layer_flow_ratio = 0.0; + m_sub_layer_height = 0.0; + } + // Flush any pending object end label before leaving the sublayer block, otherwise the + // wipe tower's add_object_end_labels may consume it into a local temp string and the + // M625 would be lost for BBL printers. + if (!layer_tools.mixed_sub_layer_groups.empty()) { + m_writer.add_object_end_labels(gcode); + m_nominal_z = print_z; + m_need_change_layer_lift_z = true; + } + } if (first_layer) { for (auto iter = by_extruder.begin(); iter != by_extruder.end(); ++iter) { @@ -6709,6 +7070,7 @@ void GCode::append_full_config(const Print &print, std::string &str) "farthest_point_timelapse"sv, "compatible_printers"sv, "compatible_prints"sv, + "filament_colour_type"sv, "print_host"sv, "print_host_webui"sv, "printhost_apikey"sv, @@ -7616,6 +7978,15 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } } + // Mixed-color sublayer: this path belongs to one sub-layer of a split layer, so scale the + // flow down to that sub-layer's share of the nominal layer height and report the sub-height + // as the effective extrusion height. Inert (ratio == 0) outside the sublayer emission block. + float effective_height = path.height; + if (m_sub_layer_flow_ratio > 0.0) { + _mm3_per_mm *= m_sub_layer_flow_ratio; + effective_height = static_cast<float>(m_sub_layer_height); + } + // Effective extrusion length per distance unit = (filament_flow_ratio/cross_section) * mm3_per_mm / print flow ratio // m_writer.extruder()->e_per_mm3() below is (filament flow ratio / cross-sectional area) double e_per_mm = m_writer.filament()->e_per_mm3() * _mm3_per_mm; @@ -7633,8 +8004,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, if (sloped) { speed = std::min(speed, m_config.scarf_joint_speed.get_abs_value(speed)); } - } - else if(path.role() == erInternalBridgeInfill) { + } else if(path.role() == erInternalBridgeInfill) { speed = m_config.get_abs_value_at("internal_bridge_speed", get_nozzle_config_index(m_writer.filament()->id())); } else if (path.role() == erOverhangPerimeter || path.role() == erSupportTransition || path.role() == erBridgeInfill) { speed = NOZZLE_CONFIG(bridge_speed); @@ -7645,7 +8015,10 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } else if (path.role() == erTopSolidInfill) { speed = NOZZLE_CONFIG(top_surface_speed); } else if (path.role() == erIroning) { - speed = m_config.get_abs_value("ironing_speed"); + const size_t filament_idx = get_filament_config_index(m_writer.filament()->id()); + speed = m_config.filament_ironing_speed.is_nil(filament_idx) + ? m_config.get_abs_value("ironing_speed") + : m_config.filament_ironing_speed.get_at(filament_idx); } else if (path.role() == erBottomSurface) { speed = NOZZLE_CONFIG(initial_layer_infill_speed); } else if (path.role() == erGapFill) { @@ -7824,9 +8197,10 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } variable_speed = std::any_of(new_points.begin(), new_points.end(), [speed](const ProcessedPoint &p) { return fabs(double(p.speed) - speed) > 1; }); // Ignore small speed variations (under 1mm/sec) - if (!NOZZLE_CONFIG(enable_overhang_speed) && FILAMENT_CONFIG(enable_overhang_bridge_fan) && m_enable_cooling_markers) { - for (ProcessedPoint &point : new_points) - point.speed = speed; + if (FILAMENT_CONFIG(enable_overhang_bridge_fan) && m_enable_cooling_markers) { + if (!NOZZLE_CONFIG(enable_overhang_speed)) + for (ProcessedPoint &point : new_points) + point.speed = speed; variable_speed = new_points.size() > 1; } } @@ -7913,8 +8287,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, gcode += buf; } - if (last_was_wipe_tower || std::abs(m_last_height - path.height) > EPSILON) { - m_last_height = path.height; + if (last_was_wipe_tower || std::abs(m_last_height - effective_height) > EPSILON) { + m_last_height = effective_height; sprintf(buf, ";%s%g\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height).c_str(), m_last_height); gcode += buf; } @@ -8706,7 +9080,7 @@ bool GCode::needs_retraction(const Polyline &travel, ExtrusionRole role, LiftTyp continue; Polygons temp; - temp.emplace_back(std::move(instance_bbox.polygon())); + temp.emplace_back(instance_bbox.polygon()); if (intersection_pl(travel, temp).empty()) continue; @@ -8936,7 +9310,7 @@ void GCode::update_placeholder_parser_with_variant_params() } } -std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bool by_object, int toolchange_temp_override) +std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bool by_object, int toolchange_temp_override, bool defer_temp_wait) { int new_extruder_id = get_extruder_id(new_filament_id); if (!m_writer.need_toolchange(new_filament_id)) @@ -9035,7 +9409,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo // per-layer nozzle grouping; resolve the column instead of indexing by the filament id. size_t new_fi = get_filament_config_index((int)new_filament_id); float new_retract_length = m_config.retraction_length.get_at(new_fi); - float new_retract_length_toolchange = m_config.retract_length_toolchange.get_at(new_filament_id); + float new_retract_length_toolchange = m_config.retract_length_toolchange.get_at(new_fi); int new_filament_temp = this->on_first_layer() ? m_config.nozzle_temperature_initial_layer.get_at(new_fi) : m_config.nozzle_temperature.get_at(new_fi); // BBS: if print_z == 0 use first layer temperature if (abs(print_z) < EPSILON) @@ -9043,6 +9417,24 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo if (toolchange_temp_override > 0) new_filament_temp = toolchange_temp_override; + // With wait_for_temp_on_wipe_tower the blocking M109 is deferred to the wipe tower, so raise + // the incoming filament's target here — ahead of the tool change rather than after it — and + // let the heat-up overlap the change itself as well as the travel to the tower. The command + // always carries an explicit tool index (the option is off for single extruder MM, so the + // writer emits one), leaving the outgoing filament that pre_toolchange just dropped to its + // standby temperature alone. nozzle_temperature == 0 means "use the first layer temperature". + if (defer_temp_wait) { + // Target what the tower will wait on. It waits on the first layer temperature not only on + // the first layer but also while priming, which runs before any layer is set: there + // on_first_layer() is false and print_z is the initial layer height, so neither test above + // catches it. nozzle_temperature == 0 means "use the first layer temperature" as well. + int preheat_temp = new_filament_temp; + if (toolchange_temp_override <= 0 && (m_layer == nullptr || preheat_temp <= 0)) + preheat_temp = m_config.nozzle_temperature_initial_layer.get_at(new_fi); + if (preheat_temp > 0) + gcode += m_writer.set_temperature(preheat_temp, false, new_filament_id); + } + Vec3d nozzle_pos = m_writer.get_position(); float old_retract_length, old_retract_length_toolchange, wipe_volume; int old_filament_temp, old_filament_e_feedrate; @@ -9066,7 +9458,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo // gap-filled carry-forward, so its current-layer column matches the nozzle it occupies. size_t old_fi = get_filament_config_index(old_filament_id); old_retract_length = m_config.retraction_length.get_at(old_fi); - old_retract_length_toolchange = m_config.retract_length_toolchange.get_at(old_filament_id); + old_retract_length_toolchange = m_config.retract_length_toolchange.get_at(old_fi); old_filament_temp = this->on_first_layer()? m_config.nozzle_temperature_initial_layer.get_at(old_fi) : m_config.nozzle_temperature.get_at(old_fi); //During the filament change, the extruder will extrude an extra length of grab_length for the corresponding detection, so the purge can reduce this length. @@ -9346,8 +9738,10 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo } check_add_eol(gcode); } - // Set the new extruder to the operating temperature. - if (m_ooze_prevention.enable) + // Set the new extruder to the operating temperature. With defer_temp_wait the target was + // already raised before the tool change and the blocking wait belongs to the wipe tower + // generator, so there is nothing left to restore here. + if (m_ooze_prevention.enable && !defer_temp_wait) gcode += m_ooze_prevention.post_toolchange(*this); if (m_config.enable_pressure_advance.get_at(new_filament_id)) { diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 47abf6be85..990bf0fee7 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -130,11 +130,11 @@ public: private: WipeTowerIntegration& operator=(const WipeTowerIntegration&); std::string append_tcr(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const; - Polyline generate_path_to_wipe_tower(const Point &start_pos, const Point &end_pos, const BoundingBox &avoid_polygon, const BoundingBox &printer_bbx) const; + Polyline generate_path_to_wipe_tower(const Point &start_pos, const Point &end_pos, const BoundingBox &avoid_polygon, const Polygons &bed_polygons) const; std::string append_tcr2(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const; std::string travel_to_tower_gap(GCode &gcodegen, const Point &route_start, const Point &start_wipe_pos) const; Vec2f transform_wt2_pt(const Vec2f &pt) const; - BoundingBox printer_travel_bounds(GCode &gcodegen) const; + Polygons shared_printable_area(GCode &gcodegen) const; // Postprocesses gcode: rotates and moves G1 extrusions and returns result std::string post_process_wipe_tower_moves(const WipeTower::ToolChangeResult& tcr, const Vec2f& translation, float angle) const; @@ -262,7 +262,7 @@ public: std::string retract(bool toolchange = false, bool is_last_retraction = false, LiftType lift_type = LiftType::NormalLift, bool apply_instantly = false, ExtrusionRole role = erNone); // extra_retract forwards a PETG pre-extrusion over-extrusion; default 0 -> identical to the plain deretract. std::string unretract(float extra_retract = 0.f) { return m_writer.unlift() + m_writer.unretract(extra_retract); } - std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1); + std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1, bool defer_temp_wait = false); bool is_BBL_Printer(); WipeTowerType wipe_tower_type(); @@ -747,6 +747,11 @@ private: Print* m_curr_print = nullptr; unsigned int m_toolchange_count; coordf_t m_nominal_z; + // Mixed-color sublayer state. Non-zero only while emitting a mixed slot's sub-layer: + // scales extrusion flow to the sub-layer's share of the nominal layer height, and + // reports that sub-height as the effective extrusion height. Reset to 0 afterwards. + double m_sub_layer_flow_ratio = 0.0; + double m_sub_layer_height = 0.0; bool m_need_change_layer_lift_z = false; int m_start_gcode_filament = -1; std::string m_filament_instances_code; diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp index 3bca1f8df9..e55b89212e 100644 --- a/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -1024,28 +1024,29 @@ std::string CoolingBuffer::apply_layer_cooldown( } if (need_set_fan) { + const auto set_fan = [&](int speed) { + if (m_current_fan_speed != speed) { + new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, speed, part_cooling_fan_min_pwm); + m_current_fan_speed = speed; + } + }; if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){ - new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, overhang_fan_speed, part_cooling_fan_min_pwm); - m_current_fan_speed = overhang_fan_speed; + set_fan(overhang_fan_speed); } else if (fan_speed_change_requests[CoolingLine::TYPE_INTERNAL_BRIDGE_FAN_START]){ // ORCA: Add support for separate internal bridge fan speed control - new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, internal_bridge_fan_speed, part_cooling_fan_min_pwm); - m_current_fan_speed = internal_bridge_fan_speed; + set_fan(internal_bridge_fan_speed); } else if (fan_speed_change_requests[CoolingLine::TYPE_SUPPORT_INTERFACE_FAN_START]){ - new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, supp_interface_fan_speed, part_cooling_fan_min_pwm); - m_current_fan_speed = supp_interface_fan_speed; + set_fan(supp_interface_fan_speed); } else if (fan_speed_change_requests[CoolingLine::TYPE_IRONING_FAN_START]){ - new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, ironing_fan_speed, part_cooling_fan_min_pwm); - m_current_fan_speed = ironing_fan_speed; + set_fan(ironing_fan_speed); } else if(fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] && m_current_fan_speed != -1){ new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_current_fan_speed, part_cooling_fan_min_pwm); fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] = false; } else { - new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_fan_speed, part_cooling_fan_min_pwm); - m_current_fan_speed = m_fan_speed; + set_fan(m_fan_speed); } need_set_fan = false; } diff --git a/src/libslic3r/GCode/ExtrusionProcessor.hpp b/src/libslic3r/GCode/ExtrusionProcessor.hpp index b282af8f4e..1d65e83f3e 100644 --- a/src/libslic3r/GCode/ExtrusionProcessor.hpp +++ b/src/libslic3r/GCode/ExtrusionProcessor.hpp @@ -19,6 +19,7 @@ #include <algorithm> #include <cmath> #include <cstddef> +#include <functional> #include <limits> #include <numeric> #include <unordered_map> @@ -39,7 +40,11 @@ std::vector<ExtendedPoint<L::Dim>> estimate_points_properties(const POINTS& const AABBTreeLines::LinesDistancer<L>& unscaled_prev_layer, float flow_width, float max_line_length = -1.0f, - float min_distance = -1.0f) + float min_distance = -1.0f, + // Maps an overhang distance onto the speed it will be printed at. Interior sampling + // needs it to tell which of the points it could add would change the G-code, and is + // skipped without it. + const std::function<float(float)>& distance_to_speed = {}) { bool looped = input_points.front() == input_points.back(); std::function<size_t(size_t,size_t)> get_prev_index = [](size_t idx, size_t count) { @@ -120,6 +125,107 @@ std::vector<ExtendedPoint<L::Dim>> estimate_points_properties(const POINTS& points.push_back(next_point); } + // ORCA: Interior sampling + // The passes below infer the support under a span from its endpoints alone, so an interior that is supported + // differently from both ends is invisible to them: the outer perimeter of an overhang whose ends are caged by + // full height walls reads as supported along its whole length. Probe the interior, keep the samples the + // endpoint interpolation fails to predict, and bisect either side of each one, so a span that is only partly + // unsupported gets points where its support actually changes instead of one reading spread across all of it. + if (PREV_LAYER_BOUNDARY_OFFSET && ADD_INTERSECTIONS && min_distance > 0 && distance_to_speed) { + // Probe at least this densely before treating matching samples as evidence that a span is uniform. The + // segmentation pass below only splits lines of 2mm or more, and every pass here drops points closer + // together than min_spacing, so finer discovery would not produce a more precise speed transition. + const double max_probe_spacing = std::max(2., 4. * min_spacing); + // A backstop for that length test, which on a non-finite length would never be met. + constexpr int max_bisection_depth = 10; + // Whether two readings are interchangeable. A segment is printed at the lower of the speeds its ends + // read, so a sample that agrees on speed with what is already known cannot change the G-code, whatever + // its distance says. The distances themselves are far too coarse a stand-in for this: the speed sections + // interpolate, so readings a small fraction of min_distance apart can still be tens of mm/s apart. + // The tolerance matches the one GCode.cpp applies when it decides a path has a variable speed at all. + auto same_speed = [&distance_to_speed](float a, float b) { + return std::abs(distance_to_speed(a) - distance_to_speed(b)) <= 1.f; + }; + // Whether the first reading is printed slower than the second, once they are known to differ. + auto prints_slower = [&distance_to_speed](float a, float b) { return distance_to_speed(a) < distance_to_speed(b); }; + + // Part of a segment still to bisect: its positions along the segment and bisections left. + struct Subspan { double t0, t1; int depth; }; + + std::vector<ExtendedPoint<L::Dim>> sampled_points; // Populated lazily, on the first insertion + std::vector<std::pair<double, float>> interior; // Samples of one segment, keyed by position along it + std::vector<Subspan> pending; + + for (size_t point_idx = 0; point_idx + 1 < points.size(); ++point_idx) { + const ExtendedPoint<L::Dim>& curr = points[point_idx]; + const ExtendedPoint<L::Dim>& next = points[point_idx + 1]; + const Vec step = next.position - curr.position; + const double line_len = step.norm(); + + interior.clear(); + if (line_len >= max_probe_spacing) + pending.push_back({0., 1., max_bisection_depth}); + + while (!pending.empty()) { + const Subspan subspan = pending.back(); + pending.pop_back(); + if (subspan.depth <= 0 || (subspan.t1 - subspan.t0) * line_len < max_probe_spacing) + continue; + + const double t = 0.5 * (subspan.t0 + subspan.t1); + auto [distance, nearest_line, x] = unscaled_prev_layer.template distance_from_lines_extra<SIGNED_DISTANCE>( + (curr.position + t * step).template cast<AABBScalar>()); + const float sampled = float(distance + boundary_offset); + + interior.emplace_back(t, sampled); + pending.push_back({subspan.t0, t, subspan.depth - 1}); + pending.push_back({t, subspan.t1, subspan.depth - 1}); + } + + if (!interior.empty()) { + std::sort(interior.begin(), interior.end(), + [](const std::pair<double, float>& l, const std::pair<double, float>& r) { return l.first < r.first; }); + // Coarse probing keeps every sample it took until this pass can see which ones bracket a speed + // transition. Matching samples cannot be discarded during discovery: one may be the last + // supported point before a narrow unsupported pocket found by a later probe. + size_t kept = 0; + for (size_t i = 0; i < interior.size(); ++i) { + const float sample = interior[i].second; + const bool at_start = kept == 0; // Nothing kept yet, so the segment's own start precedes it + const bool at_end = i + 1 == interior.size(); // And nothing follows the last sample but the segment's end + const float before = at_start ? curr.distance : interior[kept - 1].second; + const float after = at_end ? next.distance : interior[i + 1].second; + // A sample is worth a point in the path only where it prints at a different speed from the + // readings either side of it. Differing from one of the segment's own ends is not enough on + // its own where the sample is the faster of the two: the segmentation pass below already + // ends the slowdown an end reads, at a distance taken from how far out that end is rather + // than from wherever bisection happened to stop, and a point here would leave the span + // beside the end too short for that pass to run at all. Support an end cannot account for, + // where the interior is the slower reading, is exactly what this pass is here to find. + const bool worth_before = !same_speed(sample, before) && (!at_start || prints_slower(sample, before)); + const bool worth_after = !same_speed(sample, after) && (!at_end || prints_slower(sample, after)); + if (worth_before || worth_after) + interior[kept++] = interior[i]; + } + interior.resize(kept); + } + + if (!interior.empty() && sampled_points.empty()) { + sampled_points.reserve(points.size() + 8); + sampled_points.assign(points.begin(), points.begin() + point_idx + 1); + } + if (!sampled_points.empty()) { + // Only a sub-span of max_probe_spacing or more is ever bisected, so these sit at least + // 2 * min_spacing apart, and need none of the filtering the passes either side of this one do. + for (const auto& [t, distance] : interior) + sampled_points.push_back({curr.position + t * step, distance}); + sampled_points.push_back(next); + } + } + if (!sampled_points.empty()) + points = std::move(sampled_points); + } + // Segmentation handling if (PREV_LAYER_BOUNDARY_OFFSET && ADD_INTERSECTIONS) { std::vector<ExtendedPoint<L::Dim>> new_points; @@ -362,9 +468,28 @@ public: smallest_distance_with_lower_speed=-1.f; // Orca: Pass to the point properties estimator the smallest ovehang distance that triggers a slowdown (smallest_distance_with_lower_speed) + auto calculate_speed = [&speed_sections, &original_speed](float distance) { + float final_speed; + if (distance <= speed_sections.front().first) { + final_speed = original_speed; + } else if (distance >= speed_sections.back().first) { + final_speed = speed_sections.back().second; + } else { + size_t section_idx = 0; + while (distance > speed_sections[section_idx + 1].first) { + section_idx++; + } + float t = (distance - speed_sections[section_idx].first) / + (speed_sections[section_idx + 1].first - speed_sections[section_idx].first); + t = std::clamp(t, 0.0f, 1.0f); + final_speed = (1.0f - t) * speed_sections[section_idx].second + t * speed_sections[section_idx + 1].second; + } + return round(final_speed); + }; + std::vector<ExtendedPoint<3>> extended_points = estimate_points_properties<true, true, true, true>(path.polyline.points, prev_layer_boundaries[current_object], path.width, -1, - smallest_distance_with_lower_speed); + smallest_distance_with_lower_speed, calculate_speed); const auto width_inv = 1.0f / path.width; std::vector<ProcessedPoint> processed_points; processed_points.reserve(extended_points.size()); @@ -423,25 +548,6 @@ public: } } - auto calculate_speed = [&speed_sections, &original_speed](float distance) { - float final_speed; - if (distance <= speed_sections.front().first) { - final_speed = original_speed; - } else if (distance >= speed_sections.back().first) { - final_speed = speed_sections.back().second; - } else { - size_t section_idx = 0; - while (distance > speed_sections[section_idx + 1].first) { - section_idx++; - } - float t = (distance - speed_sections[section_idx].first) / - (speed_sections[section_idx + 1].first - speed_sections[section_idx].first); - t = std::clamp(t, 0.0f, 1.0f); - final_speed = (1.0f - t) * speed_sections[section_idx].second + t * speed_sections[section_idx + 1].second; - } - return round(final_speed); - }; - float extrusion_speed = std::min(calculate_speed(curr.distance), calculate_speed(next.distance)); // ORCA: Clamp resulting speed to lowest of calculated speed based on the overhang values and the current speed // Fixes bug where resulting overhang speed is higher than the current speed due to (for example) volumetric flow limits. diff --git a/src/libslic3r/GCode/FanMover.cpp b/src/libslic3r/GCode/FanMover.cpp index 112066bb85..9d77d1d04c 100644 --- a/src/libslic3r/GCode/FanMover.cpp +++ b/src/libslic3r/GCode/FanMover.cpp @@ -170,14 +170,13 @@ void FanMover::_put_in_middle_G1(std::list<BufferData>::iterator item_to_split, void FanMover::_print_in_middle_G1(BufferData& line_to_split, float nb_sec, const std::string &line_to_write) { if (nb_sec < line_to_split.time * 0.1) { - // doesn't really need to be split, print it after - m_process_output += line_to_split.raw + "\n"; + // Doesn't need to be split: the insertion point is at the start. m_process_output += line_to_write + (line_to_write.back() == '\n'?"":"\n"); - } else if (nb_sec > line_to_split.time * 0.9) { - // doesn't really need to be split, print it before - //will also print before if line_to_split.time == 0 - m_process_output += line_to_write + (line_to_write.back() == '\n' ? "" : "\n"); m_process_output += line_to_split.raw + "\n"; + } else if (nb_sec > line_to_split.time * 0.9) { + // Doesn't need to be split: the insertion point is at the end. + m_process_output += line_to_split.raw + "\n"; + m_process_output += line_to_write + (line_to_write.back() == '\n' ? "" : "\n"); }else if(line_to_split.raw.size() > 2 && line_to_split.raw[0] == 'G' && line_to_split.raw[1] == '1' && line_to_split.raw[2] == ' ') { float percent = nb_sec / line_to_split.time; diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index cebfe486cb..cd6c494f33 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -298,6 +298,7 @@ void GCodeProcessor::TimeMachine::State::reset() //BBS enter_direction = { 0.0f, 0.0f, 0.0f }; exit_direction = { 0.0f, 0.0f, 0.0f }; + jd_unit_vec = { 0.0f, 0.0f, 0.0f, 0.0f }; } void GCodeProcessor::TimeMachine::CustomGCodeTime::reset() @@ -531,7 +532,7 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P const float height = interpolate ? lerp(prev_move.height, curr_move.height, t) : curr_move.height; // ORCA: Fix issue with flow rate changes being visualized incorrectly const float mm3_per_mm = curr_move.mm3_per_mm; - const float fan_speed = interpolate ? lerp(prev_move.fan_speed, curr_move.fan_speed, t) : curr_move.fan_speed; + const float fan_speed = curr_move.fan_speed; const float temperature = interpolate ? lerp(prev_move.temperature, curr_move.temperature, t) : curr_move.temperature; actual_speed_moves.push_back({ block.move_id, @@ -562,7 +563,7 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P const float height = interpolate ? lerp(prev_move.height, curr_move.height, t) : curr_move.height; // ORCA: Fix issue with flow rate changes being visualized incorrectly const float mm3_per_mm = curr_move.mm3_per_mm; - const float fan_speed = interpolate ? lerp(prev_move.fan_speed, curr_move.fan_speed, t) : curr_move.fan_speed; + const float fan_speed = curr_move.fan_speed; const float temperature = interpolate ? lerp(prev_move.temperature, curr_move.temperature, t) : curr_move.temperature; actual_speed_moves.push_back({ block.move_id, @@ -1272,7 +1273,7 @@ void GCodeProcessor::run_post_process() // add lines M73 to exported gcode auto process_line_move = [ // Lambdas, mostly for string formatting, all with an empty capture block. - time_in_minutes, format_time_float, format_line_M73_main, format_line_M73_stop_int, format_line_M73_stop_float, time_in_last_minute,format_line_exhaust_fan_control, + time_in_minutes, format_time_float, format_line_M73_main, format_line_M73_stop_int, format_line_M73_stop_float, time_in_last_minute, &self = std::as_const(m_time_processor), // Caches, to be modified &g1_times_cache_it, &last_exported_main, &last_exported_stop, @@ -2542,6 +2543,7 @@ void GCodeProcessorResult::reset() { spiral_vase_mode = false; layer_filaments.clear(); filament_change_sequence.clear(); + used_mixed_filaments.clear(); nozzle_change_sequence.clear(); optimal_assignment.clear(); filament_change_count_map.clear(); @@ -5036,6 +5038,10 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes if (!is_extrusion_only_move(delta_pos)) curr.enter_direction = curr.enter_direction / norm; curr.exit_direction = curr.enter_direction; + curr.jd_unit_vec = Vec4f(static_cast<float>(delta_pos[X]), + static_cast<float>(delta_pos[Y]), + static_cast<float>(delta_pos[Z]), + static_cast<float>(delta_pos[E])).normalized(); TimeBlock block; block.move_type = type; @@ -5118,22 +5124,32 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes block.acceleration = acceleration; - // calculates block exit feedrate - curr.safe_feedrate = block.feedrate_profile.cruise; + static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f; + const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD; - for (unsigned char a = X; a <= E; ++a) { - float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a)); - if (curr.abs_axis_feedrate[a] > axis_max_jerk) - curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk); + // Orca: junction deviation where the firmware uses it (Klipper always, Marlin 2 with M205 J). + // Negative leaves the classic jerk path below unchanged. + const float vmax_junction_jd = calc_vmax_junction_deviation(block, prev, curr, has_prev_move, + static_cast<PrintEstimatedStatistics::ETimeMode>(i)); + const bool use_junction_deviation = vmax_junction_jd >= 0.0f; + + // calculates block exit feedrate. Junction deviation has no per axis jerk floor, so a move is + // free to start from rest. + curr.safe_feedrate = use_junction_deviation ? 0.0f : block.feedrate_profile.cruise; + + if (!use_junction_deviation) { + for (unsigned char a = X; a <= E; ++a) { + float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a)); + if (curr.abs_axis_feedrate[a] > axis_max_jerk) + curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk); + } } block.feedrate_profile.exit = curr.safe_feedrate; - static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f; - // calculates block entry feedrate - float vmax_junction = curr.safe_feedrate; - if (!blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD) { + float vmax_junction = use_junction_deviation ? vmax_junction_jd : curr.safe_feedrate; + if (!use_junction_deviation && has_prev_move) { bool prev_speed_larger = prev.feedrate > block.feedrate_profile.cruise; float smaller_speed_factor = prev_speed_larger ? (block.feedrate_profile.cruise / prev.feedrate) : (prev.feedrate / block.feedrate_profile.cruise); // Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting. @@ -5288,7 +5304,7 @@ void GCodeProcessor::process_VG1(const GCodeReader::GCodeLine& line) float filament_radius = 0.5f * filament_diameter; float area_filament_cross_section = static_cast<float>(M_PI) * sqr(filament_radius); - auto absolute_position = [this, area_filament_cross_section](Axis axis, const GCodeReader::GCodeLine& lineG1) { + auto absolute_position = [this](Axis axis, const GCodeReader::GCodeLine& lineG1) { bool is_relative = (m_global_positioning_type == EPositioningType::Relative); if (axis == E) is_relative |= (m_e_local_positioning_type == EPositioningType::Relative); @@ -5400,6 +5416,10 @@ void GCodeProcessor::process_VG1(const GCodeReader::GCodeLine& line) if (!is_extrusion_only_move(delta_pos)) curr.enter_direction = curr.enter_direction / norm; curr.exit_direction = curr.enter_direction; + curr.jd_unit_vec = Vec4f(static_cast<float>(delta_pos[X]), + static_cast<float>(delta_pos[Y]), + static_cast<float>(delta_pos[Z]), + static_cast<float>(delta_pos[E])).normalized(); TimeBlock block; block.move_type = type; @@ -5480,22 +5500,32 @@ void GCodeProcessor::process_VG1(const GCodeReader::GCodeLine& line) block.acceleration = acceleration; - // calculates block exit feedrate - curr.safe_feedrate = block.feedrate_profile.cruise; + static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f; + const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD; - for (unsigned char a = X; a <= E; ++a) { - float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a)); - if (curr.abs_axis_feedrate[a] > axis_max_jerk) - curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk); + // Orca: junction deviation where the firmware uses it (Klipper always, Marlin 2 with M205 J). + // Negative leaves the classic jerk path below unchanged. + const float vmax_junction_jd = calc_vmax_junction_deviation(block, prev, curr, has_prev_move, + static_cast<PrintEstimatedStatistics::ETimeMode>(i)); + const bool use_junction_deviation = vmax_junction_jd >= 0.0f; + + // calculates block exit feedrate. Junction deviation has no per axis jerk floor, so a move is + // free to start from rest. + curr.safe_feedrate = use_junction_deviation ? 0.0f : block.feedrate_profile.cruise; + + if (!use_junction_deviation) { + for (unsigned char a = X; a <= E; ++a) { + float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a)); + if (curr.abs_axis_feedrate[a] > axis_max_jerk) + curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk); + } } block.feedrate_profile.exit = curr.safe_feedrate; - static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f; - // calculates block entry feedrate - float vmax_junction = curr.safe_feedrate; - if (!blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD) { + float vmax_junction = use_junction_deviation ? vmax_junction_jd : curr.safe_feedrate; + if (!use_junction_deviation && has_prev_move) { bool prev_speed_larger = prev.feedrate > block.feedrate_profile.cruise; float smaller_speed_factor = prev_speed_larger ? (block.feedrate_profile.cruise / prev.feedrate) : (prev.feedrate / block.feedrate_profile.cruise); // Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting. @@ -5725,7 +5755,7 @@ void GCodeProcessor::process_G2_G3(const GCodeReader::GCodeLine& line, bool cloc if (travel_length < 0.001) return; - auto adjust_target = [this, area_filament_cross_section](const AxisCoords& target, const AxisCoords& prev_position) { + auto adjust_target = [this](const AxisCoords& target, const AxisCoords& prev_position) { AxisCoords ret = target; if (m_global_positioning_type == EPositioningType::Relative) { for (unsigned char a = X; a <= E; ++a) { @@ -6976,7 +7006,7 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type, get_acceleration(normal_mode)); const float junction_deviation = get_option_value(m_time_processor.machine_limits.machine_max_junction_deviation, normal_mode_id); const bool use_jd_jerk = (m_flavor == gcfMarlinFirmware && junction_deviation > 0.0f); - const auto axis_jerk_for_preview = [this, normal_mode, use_jd_jerk, move_acceleration](Axis axis) { + const auto axis_jerk_for_preview = [this, use_jd_jerk, move_acceleration](Axis axis) { return use_jd_jerk ? get_axis_max_jerk_with_jd(normal_mode, axis, move_acceleration) : get_axis_max_jerk(normal_mode, axis); }; const float jerk_x = axis_jerk_for_preview(X); @@ -7168,6 +7198,91 @@ float GCodeProcessor::get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeM return get_axis_max_jerk_with_jd(mode, axis, get_acceleration(mode)); } +float GCodeProcessor::get_junction_deviation(PrintEstimatedStatistics::ETimeMode mode, float acceleration) const +{ + const size_t id = static_cast<size_t>(mode); + + // Klipper has no classic jerk: jd = scv^2 * (sqrt(2) - 1) / max_accel + // (toolhead.py::_calc_junction_deviation). Passing the block acceleration back in makes it cancel + // in calc_vmax_junction_deviation(), leaving the identity v == scv at a 90 degree corner. + if (m_flavor == gcfKlipper) { + // machine_max_jerk_x holds the square corner velocity; process_SET_VELOCITY_LIMIT() writes it. + const float scv = get_option_value(m_time_processor.machine_limits.machine_max_jerk_x, id); + if (scv <= 0.0f || acceleration <= 0.0f) + return 0.0f; + return sqr(scv) * (std::sqrt(2.0f) - 1.0f) / acceleration; + } + + // Marlin 2 plans with junction deviation only when M205 J > 0; classic jerk leaves it at 0. + if (m_flavor == gcfMarlinFirmware) + return get_option_value(m_time_processor.machine_limits.machine_max_junction_deviation, id); + + return 0.0f; +} + +float GCodeProcessor::calc_junction_acceleration(const TimeBlock& block, const Vec4f& junction_unit_vec, + PrintEstimatedStatistics::ETimeMode mode) const +{ + float junction_acceleration = block.acceleration; + for (unsigned char a = X; a <= E; ++a) { + if (junction_unit_vec[a] == 0.0f) + continue; + const float axis_max_acceleration = get_axis_max_acceleration(mode, static_cast<Axis>(a), m_machine_config_idx); + if (axis_max_acceleration > 0.0f) + junction_acceleration = std::min(junction_acceleration, std::abs(axis_max_acceleration / junction_unit_vec[a])); + } + return junction_acceleration; +} + +// Ported from PrusaSlicer (src/libslic3r/GCode/GCodeProcessor.cpp). +float GCodeProcessor::calc_vmax_junction_deviation(const TimeBlock& block, const TimeMachine::State& prev, + const TimeMachine::State& curr, bool has_prev_move, + PrintEstimatedStatistics::ETimeMode mode) const +{ + const float junction_deviation = get_junction_deviation(mode, block.acceleration); + if (junction_deviation <= 0.0f) + return -1.0f; // classic jerk machine, the caller keeps its own computation + if (!has_prev_move) + return 0.0f; // starts from rest, the planner raises this on the reverse pass + + // -1 for a straight continuation, +1 for a full reversal. Half angle identity, no acos()/sin(). + // Both vectors are unit length over XYZE, so this really is a cosine: scaling by 1 / distance + // instead, as PrusaSlicer does, leaves an E term that makes extruding corners look straighter + // than they are. Marlin normalizes over XYZE for any extruding move (planner.cpp, esteps > 0) + // and Klipper keeps E out of the cosine entirely (toolhead.py::Move.calc_junction); both agree + // that the corner is planned by its geometry, and normalizing matches them to within 1e-5. + float junction_cos_theta = (-prev.jd_unit_vec).dot(curr.jd_unit_vec); + if (junction_cos_theta > 0.999999f) + return 0.0f; // the path doubles back, the machine has to stop + junction_cos_theta = std::max(junction_cos_theta, -0.999999f); // guards the division below + + const float sin_theta_d2 = std::sqrt(0.5f * (1.0f - junction_cos_theta)); // always positive + const Vec4f junction_vec = curr.jd_unit_vec - prev.jd_unit_vec; + const float junction_vec_norm = junction_vec.norm(); + const Vec4f junction_unit_vec = (junction_vec_norm > 0.0f) ? Vec4f(junction_vec / junction_vec_norm) + : Vec4f(0.0f, 0.0f, 0.0f, 0.0f); + const float junction_acceleration = calc_junction_acceleration(block, junction_unit_vec, mode); + + float vmax_junction_sqr = (junction_acceleration * junction_deviation * sin_theta_d2) / (1.0f - sin_theta_d2); + + // Marlin's JD_HANDLE_SMALL_SEGMENTS: a short move through a shallow corner is treated as an arc and + // capped by the centripetal acceleration it needs. Klipper has no equivalent. + if (m_flavor != gcfKlipper && block.distance < 1.0f && junction_cos_theta < -0.7071067812f) { + // Fast acos(-t), max. error +-0.033rad. MinMax polynomial by W. Randolph Franklin: + // https://wrf.ecse.rpi.edu/Research/Short_Notes/arcsin/onlyelem.html + const float neg = junction_cos_theta < 0.0f ? -1.0f : 1.0f; + const float t = neg * junction_cos_theta; + const float asinx = 0.032843707f + t * (-1.451838349f + t * (29.66153956f + t * (-131.1123477f + + t * (262.8130562f + t * (-242.7199627f + t * (84.31466202f)))))); + const float junction_theta = float(0.5 * M_PI) + neg * asinx; // acos(-t), bottoms out at 0.033 + vmax_junction_sqr = std::min(vmax_junction_sqr, (block.distance * junction_acceleration) / junction_theta); + } + + // Never faster than either of the two moves the junction joins. + vmax_junction_sqr = std::min(vmax_junction_sqr, std::min(sqr(block.feedrate_profile.cruise), sqr(prev.feedrate))); + return std::sqrt(vmax_junction_sqr); +} + float GCodeProcessor::get_axis_max_jerk(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const { const size_t id = static_cast<size_t>(mode); diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index f5bec9e826..0f211f133e 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -306,6 +306,9 @@ class Print; std::unordered_map<std::vector<unsigned int>, std::vector<std::pair<int, int>>,FilamentSequenceHash> layer_filaments; std::vector<unsigned int> nozzle_change_sequence; std::vector<unsigned int> filament_change_sequence; + // 0-based mixed (virtual) filament slots actually used on this plate. + // Recorded before resolve_mixed_filaments expands them to physical components. + std::vector<unsigned int> used_mixed_filaments; std::vector<int> optimal_assignment; // first key stores `from` filament, second keys stores the `to` filament std::map<std::pair<int,int>, int > filament_change_count_map; @@ -357,6 +360,7 @@ class Print; printer_extruder_id = other.printer_extruder_id; layer_filaments = other.layer_filaments; filament_change_sequence = other.filament_change_sequence; + used_mixed_filaments = other.used_mixed_filaments; nozzle_change_sequence = other.nozzle_change_sequence; optimal_assignment = other.optimal_assignment; filament_change_count_map = other.filament_change_count_map; @@ -637,6 +641,9 @@ class Print; //For line move, there are same. For arc move, there are different. Vec3f enter_direction; Vec3f exit_direction; + // Orca: move direction over all four axes, unit length. Used by + // calc_vmax_junction_deviation(); see there for why E is normalized in. + Vec4f jd_unit_vec; void reset(); }; @@ -1488,6 +1495,16 @@ class Print; float get_axis_max_acceleration(PrintEstimatedStatistics::ETimeMode mode, Axis axis, int machine_idx) const; float get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeMode mode, Axis axis, float acceleration) const; float get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const; + // Orca: junction deviation for a block at the given acceleration, 0 for a classic jerk machine. + float get_junction_deviation(PrintEstimatedStatistics::ETimeMode mode, float acceleration) const; + // Orca: acceleration along the junction direction, clamped by the per axis limits. + float calc_junction_acceleration(const TimeBlock& block, const Vec4f& junction_unit_vec, + PrintEstimatedStatistics::ETimeMode mode) const; + // Orca: entry speed from the junction deviation model, which limits a corner by its angle alone + // and is therefore isotropic, unlike per axis jerk. Negative means classic jerk applies instead. + float calc_vmax_junction_deviation(const TimeBlock& block, const TimeMachine::State& prev, + const TimeMachine::State& curr, bool has_prev_move, + PrintEstimatedStatistics::ETimeMode mode) const; float get_axis_max_jerk(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const; Vec3f get_xyz_max_jerk(PrintEstimatedStatistics::ETimeMode mode) const; float get_retract_acceleration(PrintEstimatedStatistics::ETimeMode mode) const; diff --git a/src/libslic3r/GCode/ThumbnailData.hpp b/src/libslic3r/GCode/ThumbnailData.hpp index 1a41c7486e..bc37ad40c4 100644 --- a/src/libslic3r/GCode/ThumbnailData.hpp +++ b/src/libslic3r/GCode/ThumbnailData.hpp @@ -32,7 +32,7 @@ using ThumbnailsList = std::vector<ThumbnailData>; struct ThumbnailsParams { - const Vec2ds sizes; + const Vec2ds sizes{}; bool printable_only; bool parts_only; bool show_bed; @@ -78,7 +78,7 @@ struct PlateBBoxData int first_extruder = 0; float nozzle_diameter = 0.4; std::string bed_type; - float first_layer_time; + float first_layer_time = 0.0f; // version 1: use view type ColorPrint (filament color) // version 2: use view type FilamentId (filament id) int version = 2; diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index f37025d4e7..0a97e7ac41 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -7,6 +7,8 @@ #include "GCode/ToolOrderUtils.hpp" #include "FilamentGroupUtils.hpp" #include "MultiNozzleUtils.hpp" +#include "FilamentMixer.hpp" +#include "LocalesUtils.hpp" #include "Utils.hpp" #include "I18N.hpp" @@ -22,8 +24,13 @@ #endif #include <cassert> +#include <cstdio> #include <limits> #include <algorithm> +#include <map> +#include <numeric> +#include <queue> +#include <set> #include <unordered_map> #include <libslic3r.h> @@ -84,22 +91,28 @@ bool check_filament_printable_after_group(const std::vector<unsigned int> &used_ } // Return a zero based extruder from the region, or extruder_override if overriden. +// The region accessors below resolve mixed-color slots to the physical filament chosen for this +// layer by resolve_mixed_filaments(), because a virtual slot id is never a real tool. resolve_mixed() +// returns its argument unchanged for every filament that is not a mixed slot. unsigned int LayerTools::wall_extruder_id(const PrintRegion ®ion) const { assert(region.config().outer_wall_filament_id.value > 0); - return ((this->extruder_override == 0) ? region.config().outer_wall_filament_id.value : this->extruder_override) - 1; + unsigned int result = ((this->extruder_override == 0) ? region.config().outer_wall_filament_id.value : this->extruder_override) - 1; + return resolve_mixed(result); } unsigned int LayerTools::sparse_infill_filament_id(const PrintRegion ®ion) const { assert(region.config().sparse_infill_filament_id.value > 0); - return ((this->extruder_override == 0) ? region.config().sparse_infill_filament_id.value : this->extruder_override) - 1; + unsigned int result = ((this->extruder_override == 0) ? region.config().sparse_infill_filament_id.value : this->extruder_override) - 1; + return resolve_mixed(result); } unsigned int LayerTools::internal_solid_filament_id(const PrintRegion ®ion) const { assert(region.config().internal_solid_filament_id.value > 0); - return ((this->extruder_override == 0) ? region.config().internal_solid_filament_id.value : this->extruder_override) - 1; + unsigned int result = ((this->extruder_override == 0) ? region.config().internal_solid_filament_id.value : this->extruder_override) - 1; + return resolve_mixed(result); } // Returns a zero based extruder this eec should be printed with, according to PrintRegion config or extruder_override if overriden. @@ -135,7 +148,8 @@ unsigned int LayerTools::extruder(const ExtrusionEntityCollection &extrusions, c } else extruder = this->extruder_override; - return (extruder == 0) ? 0 : extruder - 1; + unsigned int result = (extruder == 0) ? 0 : extruder - 1; + return resolve_mixed(result); } static double calc_max_layer_height(const PrintConfig &config, double max_object_layer_height) @@ -402,7 +416,9 @@ void ToolOrdering::sort_and_build_data(const Print& print, unsigned int first_ex // if first extruder is -1, we can decide the first layer tool order before doing reorder function // so we shouldn't reorder first layer in reorder function bool reorder_first_layer = (first_extruder != (unsigned int)(-1)); + this->resolve_mixed_filaments(print.config()); reorder_extruders_for_minimum_flush_volume(reorder_first_layer); + this->enforce_mixed_component_order(); m_sorted = true; double max_layer_height = 0.; @@ -422,6 +438,9 @@ void ToolOrdering::sort_and_build_data(const Print& print, unsigned int first_ex this->fill_wipe_tower_partitions(print.config(), object_bottom_z, max_layer_height); if (this->insert_wipe_tower_extruder()) { reorder_extruders_for_minimum_flush_volume(reorder_first_layer); + // Orca reorders a second time here (BBS has no such path); re-enforce so the + // mixed sub-layer component order survives the extra pass. + this->enforce_mixed_component_order(); this->fill_wipe_tower_partitions(print.config(), object_bottom_z, max_layer_height); } @@ -433,7 +452,9 @@ void ToolOrdering::sort_and_build_data(const PrintObject& object , unsigned int // if first extruder is -1, we can decide the first layer tool order before doing reorder function // so we shouldn't reorder first layer in reorder function bool reorder_first_layer = (first_extruder != (unsigned int)(-1)); + this->resolve_mixed_filaments(object.print()->config()); reorder_extruders_for_minimum_flush_volume(reorder_first_layer); + this->enforce_mixed_component_order(); m_sorted = true; double max_layer_height = calc_max_layer_height(object.print()->config(), object.config().layer_height); @@ -441,6 +462,9 @@ void ToolOrdering::sort_and_build_data(const PrintObject& object , unsigned int this->fill_wipe_tower_partitions(object.print()->config(), object.layers().front()->print_z - object.layers().front()->height, max_layer_height); if (this->insert_wipe_tower_extruder()) { reorder_extruders_for_minimum_flush_volume(reorder_first_layer); + // Orca reorders a second time here (BBS has no such path); re-enforce so the + // mixed sub-layer component order survives the extra pass. + this->enforce_mixed_component_order(); this->fill_wipe_tower_partitions(object.print()->config(), object.layers().front()->print_z - object.layers().front()->height, max_layer_height); } @@ -723,6 +747,38 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto it_per_layer_extruder_override = per_layer_extruder_switches.begin(); unsigned int extruder_override = 0; + // Pre-compute 1-based IDs of mixed filament slots for per-object tracking. + // mixed_slots_1based covers ALL mixed slots (needed by calc_slot_lh for + // accurate layer height when a slot skips layers). gradient_slots_1based + // and per_part_slots_1based are subsets for gradient-specific logic. + std::set<unsigned int> mixed_slots_1based; + std::set<unsigned int> gradient_slots_1based; + std::set<unsigned int> per_part_slots_1based; + { + const PrintConfig &cfg = object.print()->config(); + const auto &is_mixed = cfg.filament_is_mixed.values; + const auto &grad_flags = cfg.filament_mixed_gradient.values; + const auto &per_part_flags = cfg.filament_mixed_gradient_per_part.values; + const auto &comp_strs = cfg.filament_mixed_components.values; + for (size_t i = 0; i < is_mixed.size(); ++i) { + if (!is_mixed[i]) + continue; + auto comps = parse_mixed_components(i < comp_strs.size() ? comp_strs[i] : ""); + if (comps.size() < 2) + continue; + mixed_slots_1based.insert(static_cast<unsigned int>(i + 1)); + // Gradient/per-part are only defined for 2-component slots; keep their + // tracking limited to them (mirrors the is_gradient guard at resolve time). + if (comps.size() != 2) + continue; + if (i >= grad_flags.size() || !grad_flags[i]) + continue; + gradient_slots_1based.insert(static_cast<unsigned int>(i + 1)); + if (i < per_part_flags.size() && per_part_flags[i]) + per_part_slots_1based.insert(static_cast<unsigned int>(i + 1)); + } + } + // BBS: collect first layer extruders of an object's wall, which will be used by brim generator int layerCount = 0; std::vector<int> firstLayerExtruders; @@ -732,6 +788,9 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto for (auto layer : object.layers()) { LayerTools &layer_tools = this->tools_for_layer(layer->print_z); + m_object_all_layer_indices[&object].push_back( + static_cast<size_t>(&layer_tools - m_layer_tools.data())); + // Override extruder with the next for (; it_per_layer_extruder_override != per_layer_extruder_switches.end() && it_per_layer_extruder_override->first < layer->print_z + EPSILON; ++ it_per_layer_extruder_override) extruder_override = (int)it_per_layer_extruder_override->second; @@ -739,6 +798,9 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto // Store the current extruder override (set to zero if no overriden), so that layer_tools.wiping_extrusions().is_overridable_and_mark() will use it. layer_tools.extruder_override = extruder_override; + // Snapshot extruders before this object's regions to track new additions. + const size_t ext_snapshot = layer_tools.extruders.size(); + // What extruders are required to print this object layer? for (const LayerRegion *layerm : layer->regions()) { const PrintRegion ®ion = layerm->region(); @@ -805,6 +867,54 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto if (has_internal_solid || has_top_solid_surface || has_bottom_surface || has_infill) layer_tools.has_object = true; } + + // Record mixed slot usage for this object at this layer. + // All mixed slots are tracked (not just gradient) so that calc_slot_lh + // can compute accurate layer heights even when a slot skips layers. + if (!mixed_slots_1based.empty()) { + size_t layer_idx = static_cast<size_t>(&layer_tools - m_layer_tools.data()); + std::set<unsigned int> seen; + for (size_t ei = ext_snapshot; ei < layer_tools.extruders.size(); ++ei) { + unsigned int ext_1based = layer_tools.extruders[ei]; + if (mixed_slots_1based.count(ext_1based) && seen.insert(ext_1based).second) + m_mixed_object_layers[ext_1based - 1][&object].push_back(layer_idx); + } + } + + // Per-part gradient: walk LayerRegions and record which (slot, ModelVolume) pairs + // contributed to this layer. Only regions tagged by PrintApply.cpp's get_create_region + // (i.e. gradient_volume_id().valid()) are considered, so this loop is a strict no-op + // unless per_part_gradient is enabled for at least one slot AND the corresponding + // ModelObject has >=2 model-part volumes using that slot. The per-object pass above is + // unaffected — both run the same layer's data through orthogonal containers. + if (!per_part_slots_1based.empty()) { + size_t layer_idx = static_cast<size_t>(&layer_tools - m_layer_tools.data()); + std::set<std::pair<unsigned int, ObjectID>> vol_seen; + for (const LayerRegion *layerm : layer->regions()) { + if (layerm->slices.empty()) + continue; + const PrintRegion ®ion = layerm->region(); + ObjectID vol_id = region.gradient_volume_id(); + if (! vol_id.valid()) + continue; + const PrintRegionConfig &rcfg = region.config(); + // Orca splits BBS's three role slots into five; cover them all so a mixed + // slot used by any role is tracked. + const unsigned int role_slots[5] = { + static_cast<unsigned int>(rcfg.outer_wall_filament_id.value), + static_cast<unsigned int>(rcfg.inner_wall_filament_id.value), + static_cast<unsigned int>(rcfg.sparse_infill_filament_id.value), + static_cast<unsigned int>(rcfg.top_surface_filament_id.value), + static_cast<unsigned int>(rcfg.bottom_surface_filament_id.value), + }; + for (unsigned int ext_1based : role_slots) { + if (ext_1based >= 1 + && per_part_slots_1based.count(ext_1based) + && vol_seen.insert({ext_1based, vol_id}).second) + m_gradient_volume_layers[ext_1based - 1][{&object, vol_id}].push_back(layer_idx); + } + } + } layerCount++; } @@ -903,7 +1013,7 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_ //FIXME this is a hack to get the ball rolling. for (LayerTools < : m_layer_tools) - lt.has_wipe_tower |= (lt.has_object && (config.timelapse_type == TimelapseType::tlSmooth || lt.wipe_tower_partitions > 0)) + lt.has_wipe_tower |= ((lt.has_object || lt.has_support) && (config.timelapse_type == TimelapseType::tlSmooth || lt.wipe_tower_partitions > 0)) || lt.print_z < object_bottom_z + EPSILON; // Test for a raft, insert additional wipe tower layer to fill in the raft separation gap. @@ -944,6 +1054,84 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_ } } + // Ensure wipe tower vertical continuity: + // + // (1) Any existing LayerTools sandwiched between two has_wipe_tower layers must itself be a + // wipe-tower layer. The LayerTools entry already exists, but it has neither object nor + // support geometry (has_object == false && has_support == false), so the marking pass + // above leaves has_wipe_tower == false. Happens e.g. when one object is fully floating + // above another and the support_top_z_distance / support_bottom_z_distance gap leaves an + // interior layer with no object and no support (e.g. B top z=20.4, A first layer z=20.8, + // the z=20.6 LayerTools entry exists but stays unmarked). + // + // (2) When two adjacent has_wipe_tower layers are farther apart than max_layer_height and no + // LayerTools entry exists between them, insert virtual wipe-tower-only layers to bridge + // the gap. Happens with raft: BambuStudio's raft contact layer can be thicker than + // max_layer_height (e.g. raft base top z=0.2, raft contact top z=0.5 — gap 0.3 > 0.28), + // and there is no LayerTools entry between those two z values. + // + // wipe_tower_partitions has already been max-propagated downward above, so partition counts + // on the filled-in / inserted layers stay consistent. + { + int first_wt_idx = -1; + int last_wt_idx = -1; + for (int i = 0; i < (int)m_layer_tools.size(); ++i) + if (m_layer_tools[i].has_wipe_tower) { + if (first_wt_idx < 0) first_wt_idx = i; + last_wt_idx = i; + } + for (int i = first_wt_idx + 1; i < last_wt_idx; ++i) { + LayerTools < = m_layer_tools[i]; + lt.has_wipe_tower = true; + // GCode::process_layer emits wipe-tower G-code inside `for (extruder_id : layer_tools.extruders)`. + // An empty extruders vector here would silently skip wipe tower output, leaving the tower + // physically floating. Seed from the nearest non-empty neighbor so the loop actually runs. + if (lt.extruders.empty()) { + unsigned int seed_extruder = 0; + bool found_seed = false; + for (int j = i - 1; j >= 0; --j) + if (!m_layer_tools[j].extruders.empty()) { + seed_extruder = m_layer_tools[j].extruders.back(); + found_seed = true; + break; + } + if (!found_seed) + for (int j = i + 1; j < (int)m_layer_tools.size(); ++j) + if (!m_layer_tools[j].extruders.empty()) { + seed_extruder = m_layer_tools[j].extruders.front(); + found_seed = true; + break; + } + if (found_seed) + lt.extruders.push_back(seed_extruder); + } + } + + // Walk adjacent has_wipe_tower pairs and split oversized gaps. Re-evaluate the same i + // after each insertion so very large gaps get split into multiple layers. + for (int i = 0; i + 1 < (int)m_layer_tools.size(); ) { + LayerTools < = m_layer_tools[i]; + LayerTools <_next = m_layer_tools[i + 1]; + if (!lt.has_wipe_tower || !lt_next.has_wipe_tower) { + ++i; + continue; + } + coordf_t gap = lt_next.print_z - lt.print_z; + if (gap <= max_layer_height + EPSILON) { + ++i; + continue; + } + LayerTools lt_new(0.5 * (lt.print_z + lt_next.print_z)); + lt_new.has_wipe_tower = true; + if (!lt_next.extruders.empty()) + lt_new.extruders.push_back(lt_next.extruders.front()); + else if (!lt.extruders.empty()) + lt_new.extruders.push_back(lt.extruders.back()); + lt_new.wipe_tower_partitions = lt_next.wipe_tower_partitions; + m_layer_tools.insert(m_layer_tools.begin() + i + 1, lt_new); + } + } + // If the model contains empty layers (such as https://github.com/prusa3d/Slic3r/issues/1266), there might be layers // that were not marked as has_wipe_tower, even when they should have been. This produces a crash with soluble supports // and maybe other problems. We will therefore go through layer_tools and detect and fix this. @@ -1945,6 +2133,605 @@ MultiNozzleUtils::LayeredNozzleGroupResult ToolOrdering::build_sequential_group_ return result ? *result : MultiNozzleUtils::LayeredNozzleGroupResult(); } +static double snap_to_simple_fraction(double r, int max_denom = 10) +{ + double best_r = r; + double best_err = 1.0; + for (int q = 1; q <= max_denom; ++q) { + int p = (int)std::round(r * q); + if (p < 0) p = 0; + if (p > q) p = q; + double candidate = (double)p / q; + double err = std::abs(candidate - r); + if (err < best_err) { + best_err = err; + best_r = candidate; + } + } + return best_r; +} + +void ToolOrdering::resolve_mixed_filaments(const PrintConfig &config) +{ + const auto &is_mixed = config.filament_is_mixed.values; + const auto &comp_strs = config.filament_mixed_components.values; + const auto &ratio_strs = config.filament_mixed_sublayer_ratios.values; + + // Capture mixed slots that actually appear on layers before they are expanded to + // physical components. Assigned-but-unused mixed slots never enter layer_tools. + m_used_mixed_filaments.clear(); + if (has_any_mixed_filament(is_mixed)) { + std::set<unsigned int> used; + for (const LayerTools < : m_layer_tools) + for (unsigned int ext : lt.extruders) + if (ext < is_mixed.size() && is_mixed[ext]) + used.insert(ext); + m_used_mixed_filaments.assign(used.begin(), used.end()); + } + + if (!has_any_mixed_filament(is_mixed)) + return; + + const bool sublayer_enabled = config.enable_mixed_color_sublayer.value; + + struct SlotInfo { + std::vector<unsigned int> components; // 1-based + std::vector<double> ratios; + std::vector<long long> accum; // deficit accumulator (integer, unit: 1e-6 mm) + }; + std::vector<SlotInfo> slots(is_mixed.size()); + for (size_t i = 0; i < is_mixed.size(); ++i) { + if (!is_mixed[i]) + continue; + slots[i].components = parse_mixed_components(i < comp_strs.size() ? comp_strs[i] : ""); + if (slots[i].components.size() < 2) { + slots[i].components.clear(); + continue; + } + for (unsigned int cid : slots[i].components) { + unsigned int idx0 = cid - 1; + if (idx0 >= is_mixed.size() || (idx0 < is_mixed.size() && is_mixed[idx0])) { + slots[i].components.clear(); + break; + } + } + if (slots[i].components.empty()) + continue; + slots[i].ratios = parse_mixed_ratios( + i < ratio_strs.size() ? ratio_strs[i] : "", slots[i].components.size()); + if (!sublayer_enabled) { + for (double &r : slots[i].ratios) + r = snap_to_simple_fraction(r); + double sum = 0; + for (double r : slots[i].ratios) sum += r; + if (sum > 0) + for (double &r : slots[i].ratios) r /= sum; + } + slots[i].accum.assign(slots[i].components.size(), 0LL); + } + + // Parse gradient settings per slot + const auto &gradient_flags = config.filament_mixed_gradient.values; + const auto &gradient_range_strs = config.filament_mixed_gradient_range.values; + const auto &gradient_curve_strs = config.filament_mixed_gradient_curve.values; + struct GradientInfo { + double start = 0.10; + double end_val = 0.90; + GradientCurve curve; // empty -> use linear (start, end_val); non-empty wins + }; + std::vector<bool> is_gradient(is_mixed.size(), false); + std::vector<GradientInfo> gradient_info(is_mixed.size()); + for (size_t i = 0; i < is_mixed.size(); ++i) { + if (!is_mixed[i] || slots[i].components.size() != 2) + continue; + if (i >= gradient_flags.size() || !gradient_flags[i]) + continue; + is_gradient[i] = true; + if (i < gradient_range_strs.size() && !gradient_range_strs[i].empty()) { + CNumericLocalesSetter c_locale_setter; + float v0 = 0, v1 = 0; + if (std::sscanf(gradient_range_strs[i].c_str(), "%f,%f", &v0, &v1) == 2 && + v0 > 0 && v0 < 1.0 && v1 > 0 && v1 < 1.0) { + gradient_info[i].start = v0; + gradient_info[i].end_val = v1; + } + } + if (i < gradient_curve_strs.size() && !gradient_curve_strs[i].empty()) + gradient_info[i].curve = parse_gradient_curve(gradient_curve_strs[i]); + } + + // Pass 1: identify continuous runs for each gradient slot (Per-Run). + // A "run" is a maximal sequence of consecutive layers where the slot appears. + struct GradientRunInfo { + std::vector<size_t> run_lengths; + int current_run = -1; + size_t current_idx = 0; + bool prev_appeared = false; + bool last_absent_was_relevant = false; + }; + std::map<unsigned int, GradientRunInfo> gradient_runs; + for (size_t i = 0; i < is_mixed.size(); ++i) + if (is_gradient[i]) gradient_runs[static_cast<unsigned int>(i)] = {}; + + // Build per-slot sets of all layer indices where any slot-owning object has a + // layer. Used by gradient run detection (a gap is real only if the slot is + // absent at a layer belonging to one of its own objects) and by calc_slot_lh + // to keep prev_relevant_z_for_slot current even when a slot skips many layers. + std::map<unsigned int, std::set<size_t>> slot_relevant_layers; + for (auto &[slot_idx, obj_map] : m_mixed_object_layers) { + for (auto &[obj, _] : obj_map) { + auto it = m_object_all_layer_indices.find(obj); + if (it != m_object_all_layer_indices.end()) + slot_relevant_layers[slot_idx].insert(it->second.begin(), it->second.end()); + } + } + + if (!gradient_runs.empty()) { + for (size_t li = 0; li < m_layer_tools.size(); ++li) { + if (li == 0) continue; + const auto < = m_layer_tools[li]; + for (auto &[slot, run] : gradient_runs) { + bool here = std::find(lt.extruders.begin(), lt.extruders.end(), slot) != lt.extruders.end(); + if (here) { + bool real_gap = false; + if (!run.prev_appeared && !run.run_lengths.empty()) { + real_gap = run.last_absent_was_relevant; + } + if (run.run_lengths.empty() || real_gap) + run.run_lengths.push_back(0); + run.run_lengths.back()++; + run.last_absent_was_relevant = false; + } else if (!run.run_lengths.empty()) { + auto rel_it = slot_relevant_layers.find(slot); + if (rel_it != slot_relevant_layers.end() && rel_it->second.count(li)) + run.last_absent_was_relevant = true; + } + run.prev_appeared = here; + } + } + for (auto &[slot, run] : gradient_runs) { + run.current_run = -1; + run.current_idx = 0; + run.prev_appeared = false; + run.last_absent_was_relevant = false; + } + } + + // Per-object gradient: pre-compute per-object runs (respecting Z gaps within each object). + struct PerObjRunState { + std::vector<size_t> run_start_offsets; // index into layer_indices where each run starts + std::vector<size_t> run_lengths; + int current_run = -1; + size_t current_idx = 0; + }; + + // Detect whether a gap between two consecutive gradient-slot appearances is a + // real run break. A gap is real only if the object has its own layer inside the + // gap that does NOT use the gradient slot (i.e. the slot was genuinely absent). + // Uses lower_bound to skip global indices that don't belong to the object. + auto has_real_gap = [](size_t prev_idx, size_t cur_idx, + const std::set<size_t>& obj_set, + const std::set<size_t>& slot_set) -> bool { + for (auto it = obj_set.lower_bound(prev_idx + 1); + it != obj_set.end() && *it < cur_idx; ++it) { + if (!slot_set.count(*it)) + return true; + } + return false; + }; + + // Segment a sorted list of layer indices into runs, using has_real_gap to decide + // where to break. Shared by the per-object and per-volume paths below. + auto segment_runs = [&](const std::vector<size_t>& layer_indices, + const std::set<size_t>& obj_set, + const std::set<size_t>& slot_set) -> PerObjRunState { + PerObjRunState st; + for (size_t i = 0; i < layer_indices.size(); ++i) { + bool new_run = (i == 0) || + has_real_gap(layer_indices[i - 1], layer_indices[i], obj_set, slot_set); + if (new_run) { + st.run_start_offsets.push_back(i); + st.run_lengths.push_back(0); + } + st.run_lengths.back()++; + } + return st; + }; + + std::map<unsigned int, std::map<const PrintObject*, PerObjRunState>> per_obj_runs; + for (auto &[slot, obj_map] : m_mixed_object_layers) { + if (slot >= is_gradient.size() || !is_gradient[slot]) + continue; + for (auto &[obj, layer_indices] : obj_map) { + sort_remove_duplicates(layer_indices); + // Erase layer 0 — this mutation is also relied upon by the Pass 2 binary_search below. + if (!layer_indices.empty() && layer_indices.front() == 0) + layer_indices.erase(layer_indices.begin()); + + const auto &all_obj_layers = m_object_all_layer_indices[obj]; + std::set<size_t> all_obj_set(all_obj_layers.begin(), all_obj_layers.end()); + std::set<size_t> grad_set(layer_indices.begin(), layer_indices.end()); + + per_obj_runs[slot][obj] = segment_runs(layer_indices, all_obj_set, grad_set); + } + } + + // Per-volume gradient: mirror the per-object run-segmentation logic above for + // m_gradient_volume_layers. When per_part_gradient is off (or no qualifying volume exists), + // m_gradient_volume_layers is empty and per_vol_runs ends up empty too — so all subsequent + // checks of `per_vol_runs.find(slot) != end()` will fail and the legacy per-object path + // remains the only path taken. + using VolumeKey = LayerTools::MixedSubLayerGroup::VolumeKey; + std::map<unsigned int, std::map<VolumeKey, PerObjRunState>> per_vol_runs; + for (auto &[slot, vol_map] : m_gradient_volume_layers) { + if (slot >= is_gradient.size() || !is_gradient[slot]) + continue; + for (auto &[vkey, layer_indices] : vol_map) { + sort_remove_duplicates(layer_indices); + if (!layer_indices.empty() && layer_indices.front() == 0) + layer_indices.erase(layer_indices.begin()); + + const auto &all_obj_layers = m_object_all_layer_indices[vkey.obj]; + std::set<size_t> all_obj_set(all_obj_layers.begin(), all_obj_layers.end()); + std::set<size_t> vol_grad_set(layer_indices.begin(), layer_indices.end()); + + per_vol_runs[slot][vkey] = segment_runs(layer_indices, all_obj_set, vol_grad_set); + } + } + // Pass 2: resolve per layer + coordf_t prev_print_z = 0.; + // Track last print_z per mixed slot so that layer height is computed from the + // slot's own previous appearance, not from a global Z that may include layers + // belonging only to other objects with different layer heights. + std::map<unsigned int, coordf_t> prev_print_z_for_slot; + // Track the last Z where a slot-owning object had ANY layer (regardless of + // whether the slot was present). Used to detect genuine gaps: if the slot was + // absent but its owner objects had layers, prev_relevant_z advances while + // prev_print_z_for_slot stays stale. Taking the max of both gives correct lh. + std::map<unsigned int, coordf_t> prev_relevant_z_for_slot; + + // Compute the effective layer height for a mixed slot by choosing the best + // reference Z among: (1) the slot's own last Z, (2) the last Z where the + // slot's owning object had any layer, (3) the global previous Z as fallback + // when the slot appears for the first time. + auto calc_slot_lh = [&](unsigned int ext, coordf_t print_z) -> double { + auto slot_pz_it = prev_print_z_for_slot.find(ext); + auto rel_pz_it = prev_relevant_z_for_slot.find(ext); + coordf_t base_z = prev_print_z; + if (slot_pz_it != prev_print_z_for_slot.end()) { + base_z = slot_pz_it->second; + if (rel_pz_it != prev_relevant_z_for_slot.end()) + base_z = std::max(base_z, rel_pz_it->second); + } + double lh = print_z - base_z; + return (lh > 0.) ? lh : 0.2; // 0.2mm safety fallback; should not trigger in normal operation + }; + + for (LayerTools < : m_layer_tools) { + size_t layer_idx = static_cast<size_t>(< - m_layer_tools.data()); + + // Update gradient run state (skip first layer to match counting). + if (layer_idx > 0) { + for (auto &[slot, run] : gradient_runs) { + bool here = std::find(lt.extruders.begin(), lt.extruders.end(), slot) != lt.extruders.end(); + if (here) { + if (!run.prev_appeared) { + if (run.last_absent_was_relevant || run.current_run < 0) { + run.current_run++; + run.current_idx = 0; + } + } + run.last_absent_was_relevant = false; + } else { + auto rel_it = slot_relevant_layers.find(slot); + if (rel_it != slot_relevant_layers.end() && rel_it->second.count(layer_idx)) + run.last_absent_was_relevant = true; + } + run.prev_appeared = here; + } + } + + std::vector<unsigned int> new_extruders; + for (unsigned int ext : lt.extruders) { + if (ext >= slots.size() || slots[ext].components.empty()) { + new_extruders.push_back(ext); + continue; + } + auto &s = slots[ext]; + + // Skip sublayer splitting for the first layer to preserve bed adhesion. + if (sublayer_enabled && layer_idx > 0) { + double lh = calc_slot_lh(ext, lt.print_z); + size_t n = s.components.size(); + + std::vector<double> sub_heights; + bool gradient_last_no_split = false; + unsigned int gradient_last_dominant_0b = 0; + if (is_gradient[ext] && n == 2) { + auto gr_it = gradient_runs.find(ext); + if (gr_it != gradient_runs.end() && gr_it->second.current_run >= 0 && + static_cast<size_t>(gr_it->second.current_run) < gr_it->second.run_lengths.size()) { + auto &run = gr_it->second; + size_t N = run.run_lengths[run.current_run]; + size_t idx = run.current_idx++; + double t = (N > 0) ? (2.0 * idx + 1.0) / (2.0 * N) : 0.5; + // Custom curve wins over linear range when present; OFF path stays bit-identical. + double r1 = gradient_info[ext].curve.empty() + ? (gradient_info[ext].start + (gradient_info[ext].end_val - gradient_info[ext].start) * t) + : sample_gradient_curve(gradient_info[ext].curve, t); + double r2 = 1.0 - r1; + sub_heights.push_back(r1 * lh); + sub_heights.push_back(r2 * lh); + // The sublayer split path sorts components by physical ID ascending; + // the higher-ID component ends up on top (visible surface). If the + // gradient's dominant component has the lower physical ID, splitting + // would put the non-dominant color on the visible top surface. In + // that case, skip the split and print this final run-layer as pure + // dominant color to preserve the gradient appearance. + if (idx == N - 1) { + // When r1 == r2 (exactly 50/50), component[0] is treated as dominant. + size_t dominant = (r1 >= r2) ? 0 : 1; + unsigned int dom_0b = s.components[dominant] - 1; + unsigned int oth_0b = s.components[1 - dominant] - 1; + if (dom_0b < oth_0b) { + gradient_last_no_split = true; + gradient_last_dominant_0b = dom_0b; + } + } + } else { + for (double r : s.ratios) + sub_heights.push_back(r * lh); + } + } else { + for (double r : s.ratios) + sub_heights.push_back(r * lh); + } + + // Per-part gradient: when this slot has any qualifying volume, the global + // no-split short-circuit must NOT bypass MixedSubLayerGroup creation — each + // volume needs its own no-split decision in GCode.cpp (a per-volume "last + // run-layer" can occur on a different layer index than the per-object one). We + // still keep the per-object short-circuit when per_vol_runs[ext] is empty, which + // covers the legacy path bit-identically. + bool per_vol_active_for_slot = per_vol_runs.find(ext) != per_vol_runs.end() + && !per_vol_runs[ext].empty(); + + if (gradient_last_no_split && !per_vol_active_for_slot) { + lt.mixed_filament_resolution[ext] = gradient_last_dominant_0b; + new_extruders.push_back(gradient_last_dominant_0b); + prev_print_z_for_slot[ext] = lt.print_z; + continue; + } + + LayerTools::MixedSubLayerGroup grp; + grp.mixed_slot_0based = ext; + grp.layer_height = lh; + grp.is_gradient = is_gradient[ext]; + for (size_t k = 0; k < s.components.size(); ++k) { + unsigned int comp_0based = s.components[k] - 1; + grp.components_0based.push_back(comp_0based); + } + grp.sub_heights = sub_heights; + + // Write gradient metadata (run-aware). Both per_object_gradient and + // per_volume_gradient are populated independently from their own run-state + // machines; the GCode emitter chooses per-region: + // - tagged region (gradient_volume_id valid) -> per_volume_gradient[{obj, vol}] + // - untagged region (modifier / painted / etc.) -> per_object_gradient[obj] + // Populating both keeps the per-object run state correct even when per-volume + // takes over for the same (slot, obj), and lets untagged geometry (which is + // never split per-volume) keep its per-object gradient ratios. + if (grp.is_gradient) { + auto vol_runs_slot_it = per_vol_runs.find(ext); + if (vol_runs_slot_it != per_vol_runs.end()) { + auto vol_slot_it = m_gradient_volume_layers.find(ext); + for (auto &[vkey, st] : vol_runs_slot_it->second) { + auto &layer_indices = vol_slot_it->second[vkey]; + if (!std::binary_search(layer_indices.begin(), layer_indices.end(), layer_idx)) + continue; + if (st.current_run < 0 || + st.current_idx >= st.run_lengths[st.current_run]) { + st.current_run++; + st.current_idx = 0; + } + size_t run_N = st.run_lengths[st.current_run]; + size_t run_idx = st.current_idx++; + grp.per_volume_gradient[vkey] = { + run_N, + run_idx, + gradient_info[ext].start, + gradient_info[ext].end_val, + gradient_info[ext].curve, + }; + } + } + + auto runs_slot_it = per_obj_runs.find(ext); + if (runs_slot_it != per_obj_runs.end()) { + auto slot_it = m_mixed_object_layers.find(ext); + for (auto &[obj, st] : runs_slot_it->second) { + auto &layer_indices = slot_it->second[obj]; + if (!std::binary_search(layer_indices.begin(), layer_indices.end(), layer_idx)) + continue; + if (st.current_run < 0 || + st.current_idx >= st.run_lengths[st.current_run]) { + st.current_run++; + st.current_idx = 0; + } + size_t run_N = st.run_lengths[st.current_run]; + size_t run_idx = st.current_idx++; + grp.per_object_gradient[obj] = { + run_N, + run_idx, + gradient_info[ext].start, + gradient_info[ext].end_val, + gradient_info[ext].curve, + }; + } + } + } + + if (grp.components_0based.size() > 1) { + unsigned int first_comp_0based = s.components[0] - 1; + std::vector<size_t> idx(grp.components_0based.size()); + std::iota(idx.begin(), idx.end(), 0); + std::sort(idx.begin(), idx.end(), [&](size_t a, size_t b) { + return grp.components_0based[a] < grp.components_0based[b]; + }); + std::vector<unsigned int> sorted_comps; + std::vector<double> sorted_heights; + for (size_t i : idx) { + sorted_comps.push_back(grp.components_0based[i]); + sorted_heights.push_back(grp.sub_heights[i]); + } + grp.components_0based = std::move(sorted_comps); + grp.sub_heights = std::move(sorted_heights); + if (grp.is_gradient) { + for (size_t i = 0; i < grp.components_0based.size(); ++i) { + if (grp.components_0based[i] == first_comp_0based) { + grp.gradient_first_sorted_idx = static_cast<int>(i); + break; + } + } + } + } + + for (unsigned int comp : grp.components_0based) + new_extruders.push_back(comp); + lt.mixed_sub_layer_groups.push_back(std::move(grp)); + prev_print_z_for_slot[ext] = lt.print_z; + } else { + // Deficit Round-Robin: pick one component per layer. + // Weight by layer height so volume ratios stay accurate + // even with adaptive layer heights. + double lh = calc_slot_lh(ext, lt.print_z); + long long lh_i = std::llround(lh * 1e6); + + // For 2-component gradient on the first layer, use the gradient's + // starting ratio instead of the configured mixing ratio so the + // selected filament matches the gradient's "from" end. + // Only affects the first layer; when sublayer splitting is enabled + // (required for gradient), layers 1+ take the sublayer path and + // do not touch the DRR accumulator. + if (layer_idx == 0 && is_gradient[ext] && s.components.size() == 2) { + double r0 = gradient_info[ext].start; + s.accum[0] += std::llround(r0 * lh_i); + s.accum[1] += std::llround((1.0 - r0) * lh_i); + } else { + for (size_t k = 0; k < s.ratios.size(); ++k) + s.accum[k] += std::llround(s.ratios[k] * lh_i); + } + size_t sel = 0; + for (size_t k = 1; k < s.accum.size(); ++k) + if (s.accum[k] > s.accum[sel]) + sel = k; + s.accum[sel] -= lh_i; + unsigned int resolved = s.components[sel] - 1; + lt.mixed_filament_resolution[ext] = resolved; + new_extruders.push_back(resolved); + prev_print_z_for_slot[ext] = lt.print_z; + } + } + lt.extruders = new_extruders; + sort_remove_duplicates(lt.extruders); + + // Update prev_relevant_z: for each slot that has relevant-layer tracking, + // advance if the current layer belongs to a slot-owning object. + for (auto &[slot, rel_set] : slot_relevant_layers) { + if (rel_set.count(layer_idx)) + prev_relevant_z_for_slot[slot] = lt.print_z; + } + + prev_print_z = lt.print_z; + } +} + +void ToolOrdering::enforce_mixed_component_order() +{ + for (LayerTools < : m_layer_tools) { + if (lt.mixed_sub_layer_groups.empty()) + continue; + + // Build a set of extruders present in lt.extruders for fast lookup. + std::set<unsigned int> ext_set(lt.extruders.begin(), lt.extruders.end()); + + // 1. Build DAG from mixed group constraints. + // For each group [c0, c1, c2, ...], add edges c0->c1, c1->c2, ... + // Only between components that are both present in lt.extruders. + // Use an edge set to avoid duplicate edges inflating in-degree. + std::map<unsigned int, std::vector<unsigned int>> adj; + std::map<unsigned int, int> in_degree; + std::set<std::pair<unsigned int, unsigned int>> edge_set; + + for (unsigned int ext : lt.extruders) + in_degree[ext] = 0; + + for (const auto &grp : lt.mixed_sub_layer_groups) { + for (size_t i = 0; i + 1 < grp.components_0based.size(); ++i) { + unsigned int a = grp.components_0based[i]; + unsigned int b = grp.components_0based[i + 1]; + if (!ext_set.count(a) || !ext_set.count(b)) + continue; + if (edge_set.insert({a, b}).second) { + adj[a].push_back(b); + in_degree[b] += 1; + } + } + } + + // 2. Record original position (from flush optimizer) as priority. + std::map<unsigned int, size_t> orig_pos; + for (size_t i = 0; i < lt.extruders.size(); ++i) + orig_pos[lt.extruders[i]] = i; + + // 3. Kahn's topological sort with priority queue (prefer original position). + auto cmp = [&orig_pos](unsigned int lhs, unsigned int rhs) { + return orig_pos[lhs] > orig_pos[rhs]; // min-heap by orig_pos + }; + std::priority_queue<unsigned int, std::vector<unsigned int>, decltype(cmp)> pq(cmp); + + for (unsigned int ext : lt.extruders) { + if (in_degree[ext] == 0) + pq.push(ext); + } + + std::vector<unsigned int> ordered; + ordered.reserve(lt.extruders.size()); + while (!pq.empty()) { + unsigned int ext = pq.top(); + pq.pop(); + ordered.push_back(ext); + if (auto it = adj.find(ext); it != adj.end()) { + for (unsigned int next : it->second) { + if (--in_degree[next] == 0) + pq.push(next); + } + } + } + + // Safety: if topological sort didn't produce all elements, keep original order. + if (ordered.size() != lt.extruders.size()) + ordered = lt.extruders; + + // 4. Verify: every mixed group's component order is preserved as subsequence. + for (const auto &grp : lt.mixed_sub_layer_groups) { + size_t prev_pos = 0; + bool valid = true; + for (unsigned int c : grp.components_0based) { + if (!ext_set.count(c)) + continue; + auto it = std::find(ordered.begin() + prev_pos, ordered.end(), c); + if (it == ordered.end()) { valid = false; break; } + prev_pos = (it - ordered.begin()) + 1; + } + assert(valid && "enforce_mixed_component_order: mixed group subsequence violated"); + (void)valid; + } + + lt.extruders = ordered; + } +} + void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer) { const PrintConfig* print_config = m_print_config_ptr; @@ -1998,6 +2785,17 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first std::vector<unsigned int> used_filaments = collect_sorted_used_filaments(layer_filaments); std::vector<std::set<int>>geometric_unprintables = m_print->get_geometric_unprintable_filaments(); + + // Unprintable sets are keyed by filament id, but a mixed-color slot is virtual: what actually + // reaches the nozzle are its components. Expand the slot to those components so a geometric + // restriction is applied to the filaments really being printed. No-op without mixed filaments. + { + const auto &is_mixed = m_print->config().filament_is_mixed.values; + const auto &comp_strs = m_print->config().filament_mixed_components.values; + if (has_any_mixed_filament(is_mixed)) + expand_mixed_slots_in_unprintables(geometric_unprintables, is_mixed, comp_strs); + } + std::vector<std::set<int>>physical_unprintables = m_print->get_physical_unprintable_filaments(used_filaments); auto filament_unprintable_volumes = m_print->get_filament_unprintable_flow(used_filaments); diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index c77b152fe9..4dc08c0e8b 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -5,12 +5,16 @@ #include "../libslic3r.h" +#include <functional> +#include <map> #include <utility> #include <boost/container/small_vector.hpp> #include "../FilamentGroup.hpp" +#include "../FilamentMixer.hpp" #include "../MultiNozzleUtils.hpp" #include "../ExtrusionEntity.hpp" +#include "../ObjectID.hpp" #include "../PrintConfig.hpp" namespace Slic3r { @@ -172,6 +176,65 @@ public: // Custom G-code (color change, extruder switch, pause) to be performed before this layer starts to print. const CustomGCode::Item *custom_gcode = nullptr; + // 0-based mixed filament slot → 0-based resolved physical filament for this layer. + // Populated by ToolOrdering::resolve_mixed_filaments(). Empty when no mixed filaments. + std::map<unsigned int, unsigned int> mixed_filament_resolution; + + unsigned int resolve_mixed(unsigned int filament_0based) const { + auto it = mixed_filament_resolution.find(filament_0based); + return (it != mixed_filament_resolution.end()) ? it->second : filament_0based; + } + + struct MixedSubLayerGroup { + unsigned int mixed_slot_0based; + std::vector<unsigned int> components_0based; + std::vector<double> sub_heights; // per-component, sum ≈ layer_height + double layer_height = 0.; // the actual lh used to compute sub_heights + bool is_gradient = false; + int gradient_first_sorted_idx = 0; // index of "first" config component after sorting + + struct ObjectGradient { + size_t total_layers; + size_t current_idx; + double gradient_start; + double gradient_end; + GradientCurve curve; // empty -> linear fallback (start, end); non-empty wins + }; + std::map<const PrintObject*, ObjectGradient> per_object_gradient; + + // Per-volume gradient: same metadata layout as ObjectGradient but keyed by + // (PrintObject*, ModelVolume id). Populated only when filament_mixed_gradient_per_part is + // enabled for this slot AND the corresponding ModelObject contains >=2 model-part volumes + // using this slot. When non-empty for a given (PrintObject*), GCode emission takes the + // per-volume path for tagged regions; untagged regions (modifier/painted/fuzzy_skin) still + // use per_object_gradient. Both maps are populated in parallel to keep run states correct. + struct VolumeKey { + const PrintObject* obj; + ObjectID volume_id; + bool operator<(const VolumeKey &o) const { + if (obj != o.obj) return std::less<const PrintObject*>{}(obj, o.obj); + return volume_id < o.volume_id; + } + bool operator==(const VolumeKey &o) const { + return obj == o.obj && volume_id == o.volume_id; + } + }; + using VolumeGradient = ObjectGradient; + std::map<VolumeKey, VolumeGradient> per_volume_gradient; + }; + std::vector<MixedSubLayerGroup> mixed_sub_layer_groups; + + const MixedSubLayerGroup* mixed_group_by_slot(unsigned int slot_id) const { + for (const auto &g : mixed_sub_layer_groups) + if (g.mixed_slot_0based == slot_id) + return &g; + return nullptr; + } + + bool is_mixed_slot(unsigned int slot_id) const { + return mixed_group_by_slot(slot_id) != nullptr; + } + WipingExtrusions& wiping_extrusions() { m_wiping_extrusions.set_layer_tools_ptr(this); return m_wiping_extrusions; @@ -227,6 +290,9 @@ public: // For a multi-material print, the printing extruders are ordered in the order they shall be primed. const std::vector<unsigned int>& all_extruders() const { return m_all_printing_extruders; } + // 0-based mixed (virtual) slots that appeared on layers before resolve_mixed_filaments + // expanded them to physical components. + const std::vector<unsigned int>& used_mixed_filaments() const { return m_used_mixed_filaments; } // Find LayerTools with the closest print_z. const LayerTools& tools_for_layer(coordf_t print_z) const; @@ -299,6 +365,8 @@ private: void mark_skirt_layers(const PrintConfig &config, coordf_t max_layer_height); void collect_extruder_statistics(bool prime_multi_material); void reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer); + void resolve_mixed_filaments(const PrintConfig &config); + void enforce_mixed_component_order(); // BBS std::vector<unsigned int> generate_first_layer_tool_order(const Print& print); @@ -311,8 +379,26 @@ private: unsigned int m_last_printing_extruder = (unsigned int)-1; // All extruders, which extrude some material over m_layer_tools. std::vector<unsigned int> m_all_printing_extruders; + std::vector<unsigned int> m_used_mixed_filaments; const DynamicPrintConfig* m_print_full_config = nullptr; const PrintConfig* m_print_config_ptr = nullptr; + + // Per-object gradient tracking: slot(0-based) -> PrintObject* -> list of layer indices + // where that object uses the slot. Populated by collect_extruders, consumed by resolve_mixed_filaments. + std::map<unsigned int, std::map<const PrintObject*, std::vector<size_t>>> m_mixed_object_layers; + + // All layer indices (in m_layer_tools) where each object has any layer. + // Used by gradient run detection to distinguish real gaps (object has a layer + // that doesn't use the slot) from spurious gaps (another object's layer). + std::map<const PrintObject*, std::vector<size_t>> m_object_all_layer_indices; + + // Per-volume gradient tracking: slot(0-based) -> (PrintObject*, ModelVolume id) -> list of + // layer indices where the given volume contributes to the slot. Populated by collect_extruders + // alongside m_mixed_object_layers when per_part gradient is enabled for the slot AND the + // ModelObject has >=2 model-part volumes using the slot. Empty for all other configurations, + // which keeps every legacy per-object code path bit-identical (loops over an empty map are + // no-ops; downstream emission falls through to the per-object branch). + std::map<unsigned int, std::map<LayerTools::MixedSubLayerGroup::VolumeKey, std::vector<size_t>>> m_gradient_volume_layers; const PrintObject* m_print_object_ptr = nullptr; Print* m_print; bool m_sorted = false; diff --git a/src/libslic3r/GCode/WipeTower.cpp b/src/libslic3r/GCode/WipeTower.cpp index 3d7353eadf..8aff5f4a3f 100644 --- a/src/libslic3r/GCode/WipeTower.cpp +++ b/src/libslic3r/GCode/WipeTower.cpp @@ -617,6 +617,18 @@ Polygon generate_rectange_polygon(const Vec2f &wt_box_min ,const Vec2f & wt_box_ return res; } +const char* flush_planner_queue_command(GCodeFlavor flavor) +{ + return flavor == gcfKlipper ? "M400\n" : "G4 S0\n"; +} + +std::string wait_command(GCodeFlavor flavor, float seconds) +{ + if (flavor == gcfKlipper) + return "G4 P" + std::to_string(std::lround(seconds * 1000.f)) + "\n"; + return "G4 S" + Slic3r::float_to_string_decimal_point(seconds, 3) + "\n"; +} + class WipeTowerWriter { public: @@ -1145,7 +1157,7 @@ public: { if (time==0.f) return *this; - m_gcode += "G4 S" + Slic3r::float_to_string_decimal_point(time, 3) + "\n"; + m_gcode += wait_command(m_gcode_flavor, time); return *this; } @@ -1190,7 +1202,7 @@ public: WipeTowerWriter& flush_planner_queue() { - m_gcode += "G4 S0\n"; + m_gcode += flush_planner_queue_command(m_gcode_flavor); return *this; } @@ -1333,6 +1345,8 @@ public: { std::string buffer; if (wait_for_moves) + // Not flush_planner_queue_command(): this BBL precool path wants M400, which every + // flavor it reaches understands, not the zero dwell the other flavors flush with. buffer += "M400\n"; buffer += "M104"; if (target_extruder != -1) @@ -1616,25 +1630,56 @@ float WipeTower::get_auto_brim_by_height(float max_height) { return 8.f; } -Vec2f WipeTower::move_box_inside_box(const BoundingBox &box1, const BoundingBox &box2,int scaled_offset) +Vec2f WipeTower::move_box_inside_polygon(const BoundingBox &box, const Polygons &polygons, coord_t offset) { - Vec2f res{0, 0}; - if (box1.size()[0] >= box2.size()[0]- 2*scaled_offset || box1.size()[1] >= box2.size()[1]-2*scaled_offset) return res; + if (polygons.empty()) return Vec2f{0.f, 0.f}; - if (box1.max[0] > box2.max[0] - scaled_offset) { - res[0] = unscaled<float>((box2.max[0] - scaled_offset) - box1.max[0]); - } - else if (box1.min[0] < box2.min[0] + scaled_offset) { - res[0] = unscaled<float>((box2.min[0] + scaled_offset) - box1.min[0]); + const BoundingBox bed = get_extents(polygons); + // No position fits the footprint. + if (box.size().x() >= bed.size().x() - 2 * offset || box.size().y() >= bed.size().y() - 2 * offset) + return Vec2f{0.f, 0.f}; + + // Clamp against the bounding box first, moving only along the axis that is violated so a dragged + // prime tower slides along the bed edge instead of jumping inwards. + Point shift(0, 0); + for (int axis = 0; axis < 2; ++axis) { + if (box.max[axis] > bed.max[axis] - offset) + shift[axis] = (bed.max[axis] - offset) - box.max[axis]; + else if (box.min[axis] < bed.min[axis] + offset) + shift[axis] = (bed.min[axis] + offset) - box.min[axis]; } - if (box1.max[1] > box2.max[1] - scaled_offset) { - res[1] = unscaled<float>((box2.max[1] - scaled_offset) - box1.max[1]); + // A bed that fills its own bounding box is fully clamped by that, so every rectangular bed — all + // but the delta-style profiles — stops here and keeps its historic placement, including when a + // negative margin lets the footprint hang over the edge. The tolerance is relative because an + // exact rectangle loses a few ulps once the areas are squared world coordinates. + double area = 0.; + for (const Polygon &poly : polygons) area += std::abs(poly.area()); + const double bed_area = double(bed.size().x()) * double(bed.size().y()); + if (area >= bed_area * (1. - EPSILON)) return unscaled<float>(shift); + + // Clamp a negative margin (an auto brim width that has not been resolved yet) to zero: padding by + // it would shrink the footprint and hand back a position the validation still rejects. The + // epsilon lets the move's round trip through millimeters land on the outline without counting as + // a violation. + BoundingBox padded = box.inflated(std::max<coord_t>(offset, 0) - SCALED_EPSILON); + padded.translate(shift); + auto fits = [&padded, &polygons](const Point &move) { + BoundingBox moved = padded; + moved.translate(move); + return diff(Polygons{moved.polygon()}, polygons).empty(); + }; + if (fits(Point(0, 0))) return unscaled<float>(shift); + + // Walk towards the middle of the bed. On every non-rectangular bed we ship, the fitting positions + // form a convex region around it, so bisecting stops just inside the outline. + Point lo(0, 0), hi = bed.center() - padded.center(); + if (!fits(hi)) return unscaled<float>(shift); + for (int i = 0; i < 12; ++i) { + const Point mid = (lo + hi) / 2; + if (fits(mid)) hi = mid; else lo = mid; } - else if (box1.min[1] < box2.min[1] + scaled_offset) { - res[1] = unscaled<float>((box2.min[1] + scaled_offset) - box1.min[1]); - } - return res; + return unscaled<float>(Point(shift + hi)); } Polygon WipeTower::rib_section(float width, float depth, float rib_length, float rib_width,bool fillet_wall) @@ -4038,7 +4083,7 @@ void WipeTower::toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinat } return time * 60.f; }; - auto estimate_wipe_time = [&estimate_time_kernel, & cleaning_box, &target_speed, &x_to_wipe, &xr, &xl, &dy, &WipeSpeedMap, &solid_tool_toolchange](int begin_line) -> float { + auto estimate_wipe_time = [&estimate_time_kernel, & cleaning_box, &x_to_wipe, &xr, &xl, &dy, &solid_tool_toolchange](int begin_line) -> float { int n = std::ceil(x_to_wipe / (xr - xl)); if (solid_tool_toolchange) n = (cleaning_box.lu[1] - cleaning_box.ld[1]) / dy; float total_time = estimate_time_kernel(n); @@ -5066,7 +5111,7 @@ Polygon WipeTower::generate_rib_polygon(const box_coordinates &wt_box) Polygon WipeTower::generate_support_wall_new(WipeTowerWriter &writer, const box_coordinates &wt_box, double feedrate, bool first_layer,bool rib_wall, bool extrude_perimeter, bool skip_points) { - auto get_closet_idx = [this, &writer](Polylines &pls) -> std::pair<int,int> { + auto get_closet_idx = [&writer](Polylines &pls) -> std::pair<int,int> { Vec2f anchor{writer.x(), writer.y()}; int closestIndex = -1; int closestPl = -1; diff --git a/src/libslic3r/GCode/WipeTower.hpp b/src/libslic3r/GCode/WipeTower.hpp index 66e8acf1c4..045c82cbf3 100644 --- a/src/libslic3r/GCode/WipeTower.hpp +++ b/src/libslic3r/GCode/WipeTower.hpp @@ -26,6 +26,12 @@ enum GCodeFlavor : unsigned char; Polylines construct_gap_for_skip_points( const Polygon& polygon, const std::vector<Vec2f>& skip_points, float wt_width, float gap_length, Polygon& insert_skip_polygon); +// Klipper acts on commands the instant it parses them, and its G4 reads only P (milliseconds), +// so the zero-second and seconds-valued dwells every other flavor uses neither synchronize nor +// pause there. Both defined in WipeTower.cpp, shared by WipeTower and WipeTower2. +const char* flush_planner_queue_command(GCodeFlavor flavor); // finish queued moves, e.g. around M104/M109 +std::string wait_command(GCodeFlavor flavor, float seconds); // pause for `seconds` + class WipeTower { public: @@ -39,7 +45,11 @@ public: static TriangleMesh its_make_rib_tower(float width, float depth, float height, float rib_length, float rib_width, bool fillet_wall); static TriangleMesh its_make_rib_brim(const Polygon& brim, float layer_height); static Polygon rib_section(float width, float depth, float rib_length, float rib_width, bool fillet_wall); - static Vec2f move_box_inside_box(const BoundingBox &box1, const BoundingBox &box2, int offset = 0); + // Translation that brings a footprint inside the printable outline, padded by offset. The prime + // tower is validated against the real outline (see layered_print_cleareance_valid), so clamping + // against the bounding box alone would leave it off a delta or hexagonal bed. box and polygons + // must share one scaled coordinate frame; the translation comes back in millimeters. + static Vec2f move_box_inside_polygon(const BoundingBox &box, const Polygons &polygons, coord_t offset = 0); static Polygon rounding_polygon(Polygon &polygon, double rounding = 2., double angle_tol = 30. / 180. * PI); struct Extrusion { diff --git a/src/libslic3r/GCode/WipeTower2.cpp b/src/libslic3r/GCode/WipeTower2.cpp index 51ab155dd9..ee0f9c375a 100644 --- a/src/libslic3r/GCode/WipeTower2.cpp +++ b/src/libslic3r/GCode/WipeTower2.cpp @@ -386,7 +386,8 @@ public: } WipeTowerWriter2& switch_filament_monitoring(bool enable) { - m_gcode += std::string("G4 S0\n") + "M591 " + (enable ? "R" : "S0") + "\n"; + flush_planner_queue(); + m_gcode += enable ? "M591 R\n" : "M591 S0\n"; return *this; } @@ -412,6 +413,7 @@ public: const Vec2f& pos() const { return m_current_pos; } const Vec2f start_pos_rotated() const { return m_start_pos; } const Vec2f pos_rotated() const { return this->rotate(m_current_pos); } + const Vec2f rotated(const Vec2f &pt) const { return this->rotate(pt); } float elapsed_time() const { return m_elapsed_time; } float get_and_reset_used_filament_length() { float temp = m_used_filament_length; m_used_filament_length = 0.f; return temp; } @@ -623,10 +625,13 @@ public: } // Set extruder temperature, don't wait by default. - WipeTowerWriter2& set_extruder_temp(int temperature, bool wait = false) + WipeTowerWriter2& set_extruder_temp(int temperature, bool wait = false, const std::string& comment = std::string()) { - m_gcode += "G4 S0\n"; // to flush planner queue - m_gcode += "M" + std::to_string(wait ? 109 : 104) + " S" + std::to_string(temperature) + "\n"; + flush_planner_queue(); + m_gcode += "M" + std::to_string(wait ? 109 : 104) + " S" + std::to_string(temperature); + if (!comment.empty()) + m_gcode += " " + comment; + m_gcode += "\n"; return *this; } @@ -635,7 +640,7 @@ public: { if (time==0.f) return *this; - m_gcode += "G4 S" + Slic3r::float_to_string_decimal_point(time, 3) + "\n"; + m_gcode += wait_command(m_gcode_flavor, time); return *this; } @@ -677,8 +682,8 @@ public: } WipeTowerWriter2& flush_planner_queue() - { - m_gcode += "G4 S0\n"; + { + m_gcode += flush_planner_queue_command(m_gcode_flavor); return *this; } @@ -1005,6 +1010,13 @@ bool WipeTower2::use_gap_wall(const PrintConfig& config) return config.prime_tower_skip_points.value && config.wipe_tower_wall_type.value != wtwCone; } +bool WipeTower2::wait_for_temp_enabled(const PrintConfig& config) +{ + // SEMM runs its own unload/load temperature sequence; the GUI hides the option + // there but a profile may still carry it set. + return config.wait_for_temp_on_wipe_tower.value && !config.single_extruder_multi_material.value; +} + WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& default_region_config,int plate_idx, Vec3d plate_origin, const std::vector<std::vector<float>>& wiping_matrix, size_t initial_tool) : m_semm(config.single_extruder_multi_material.value), m_enable_filament_ramming(config.enable_filament_ramming.value), @@ -1034,7 +1046,8 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau m_wall_type((int)config.wipe_tower_wall_type), m_use_gap_wall(use_gap_wall(config)), m_enable_tower_interface_features(config.enable_tower_interface_features.value), - m_enable_tower_interface_cooldown_during_tower(config.enable_tower_interface_cooldown_during_tower.value) + m_enable_tower_interface_cooldown_during_tower(config.enable_tower_interface_cooldown_during_tower.value), + m_wait_for_temp_on_wipe_tower(wait_for_temp_enabled(config)) { // Read absolute value of first layer speed, if given as percentage, // it is taken over following default. Speeds from config are not @@ -1084,6 +1097,7 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau m_bed_bottom_left = m_bed_shape == RectangularBed ? Vec2f(bed_points.front().x(), bed_points.front().y()) : Vec2f::Zero(); + m_bed_polygon = Polygon::new_scale(bed_points); } @@ -1235,7 +1249,7 @@ std::vector<WipeTower::ToolChangeResult> WipeTower2::prime( unsigned int tool = tools[idx_tool]; m_left_to_right = true; - toolchange_Change(writer, tool, m_filpar[tool].material); // Select the tool, set a speed override for soluble and flex materials. + toolchange_Change(writer, tool, m_filpar[tool].material, m_filpar[tool].first_layer_temperature, false); // Select the tool, set a speed override for soluble and flex materials. toolchange_Load(writer, cleaning_box); // Prime the tool. if (idx_tool + 1 == tools.size()) { // Last tool should not be unloaded, but it should be wiped enough to become of a pure color. @@ -1354,13 +1368,19 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool) toolchange_Unload(writer, cleaning_box, m_filpar[m_current_tool].material, (is_first_layer() ? m_filpar[m_current_tool].first_layer_temperature : m_filpar[m_current_tool].temperature), new_tool_temp); - toolchange_Change(writer, tool, m_filpar[tool].material); // Change the tool, set a speed override for soluble and flex materials. + // Wait-at-tower target: the interface temp when an interface boost applies on this layer, + // otherwise the print temp (nozzle_temperature == 0 means "use the first layer temp"). + int wait_for_temp = interface_layer && m_filpar[tool].interface_print_temperature > 0 ? + m_filpar[tool].interface_print_temperature : + (is_first_layer() || m_filpar[tool].temperature == 0 ? m_filpar[tool].first_layer_temperature : m_filpar[tool].temperature); + toolchange_Change(writer, tool, m_filpar[tool].material, wait_for_temp, true); // Change the tool, set a speed override for soluble and flex materials. toolchange_Load(writer, cleaning_box); writer.travel(writer.x(), writer.y()-m_perimeter_width); // cooling and loading were done a bit down the road int base_temp = is_first_layer() ? m_filpar[tool].first_layer_temperature : m_filpar[tool].temperature; if (interface_layer) { int interface_temp = m_filpar[tool].interface_print_temperature; - if (interface_temp > 0 && interface_temp != base_temp) + // With wait-for-temp-on-wipe-tower the toolchange already blocked for the interface temp. + if (interface_temp > 0 && interface_temp != base_temp && !m_wait_for_temp_on_wipe_tower) writer.set_extruder_temp(interface_temp, true); if (m_enable_tower_interface_cooldown_during_tower && interface_temp > 0 && interface_temp != base_temp) writer.set_extruder_temp(base_temp, false); @@ -1670,7 +1690,9 @@ void WipeTower2::toolchange_Unload( void WipeTower2::toolchange_Change( WipeTowerWriter2 &writer, const size_t new_tool, - const std::string& new_material) + const std::string& new_material, + const int wait_for_temp, + const bool wait_beside_tower) { // Ask the writer about how much of the old filament we consumed: if (m_current_tool < m_used_filament_length.size()) @@ -1684,6 +1706,90 @@ void WipeTower2::toolchange_Change( if (m_is_mk4mmu3) writer.switch_filament_monitoring(true); + const bool wait_for_temp_here = m_wait_for_temp_on_wipe_tower && wait_for_temp > 0; + + // The Tn above was issued without a blocking temperature wait (GCode::set_extruder only raises + // the target, ahead of the Tn); block here, before the deretraction below, which must not + // extrude on a cold nozzle. Like the Bambu H2C, park beside the tower for the heat-up so drool lands + // next to it instead of on its top surface — nearest x side first, then that side clamped + // toward the bed edge, then the far side, in place if all would leave the bed. Raw + // pre-rotated moves (see the repositioning move below) keep the writer's tracked position + // at the tower entry. The tag keeps the + // interface-temp deduplication pass in append_tcr2 from stripping the M109. + if (wait_for_temp_here && wait_beside_tower) { + // The rib wall and the stabilization cone bulge past the nominal width rectangle + // (widest near the bottom), and the first-layer brim is printed around the wall later + // in the layer — clear the widest of them, not just the rectangle, so the park point + // and its drool stay off the tower. + float min_x = 0.f, max_x = m_wipe_tower_width; + if (m_wall_type == (int)wtwRib) { + WipeTower::box_coordinates wt_box(Vec2f(0.f, 0.f), m_wipe_tower_width, m_layer_info->depth + m_perimeter_width); + const BoundingBox rib_bbox = get_extents(generate_rib_polygon(wt_box)); // the fillet stays within this bbox + min_x = std::min(min_x, unscaled<float>(rib_bbox.min.x())); + max_x = std::max(max_x, unscaled<float>(rib_bbox.max.x())); + } else if (m_wall_type == (int)wtwCone) { + const double support_scale = get_wipe_tower_cone_base(m_wipe_tower_width, m_wipe_tower_height, m_wipe_tower_depth, + m_wipe_tower_cone_angle).second; + const double z = m_no_sparse_layers ? (m_current_height + m_layer_info->height) : m_layer_info->z; + const double r = std::tan(Geometry::deg2rad(m_wipe_tower_cone_angle / 2.f)) * (m_wipe_tower_height - z); + const double w = m_layer_info->depth + m_perimeter_width; + if (r > 0.5 * w + 0.01) { // same guard as generate_support_cone_wall + const float bulge = float(std::sqrt(r * r - 0.25 * w * w) / support_scale); + min_x = std::min(min_x, m_wipe_tower_width / 2.f - bulge); + max_x = std::max(max_x, m_wipe_tower_width / 2.f + bulge); + } + } + if (is_first_layer()) { + const float brim = m_wipe_tower_brim_width < 0.f ? WipeTower::get_auto_brim_by_height(m_wipe_tower_height) : + m_wipe_tower_brim_width; + min_x -= brim; + max_x += brim; + } + constexpr float gap = 2.f; + constexpr float min_gap = 0.5f; + const bool on_left = writer.x() < m_wipe_tower_width / 2.f; + const float near_x = on_left ? min_x - gap : max_x + gap; + const float far_x = on_left ? max_x + gap : min_x - gap; + const Eigen::Rotation2Df to_bed(float(Geometry::deg2rad(m_wipe_tower_rotation_angle))); + auto park_pt_on_bed = [this, &writer, to_bed](float side_x) { + const Vec2f bed_pt = to_bed * (writer.rotated(Vec2f(side_x, writer.y())) + m_rib_offset) + m_wipe_tower_pos; + return m_bed_polygon.contains(Point::new_scale(bed_pt.x(), bed_pt.y())); + }; + float park_x = near_x; + bool have_park = park_pt_on_bed(near_x); + if (!have_park) { + // The ideal near point hangs off the bed: pull it back to the bed edge as long + // as that still clears the tower envelope by min_gap (the BBL tower clamps its + // stop_pos against the bed the same way in append_tcr). Bisection, because with + // tower rotation and non-rectangular beds the bed edge is not axis-aligned. + const float limit_x = on_left ? min_x - min_gap : max_x + min_gap; + if (park_pt_on_bed(limit_x)) { + float on = limit_x, off = near_x; + for (int i = 0; i < 8; ++i) { + const float mid = 0.5f * (on + off); + if (park_pt_on_bed(mid)) + on = mid; + else + off = mid; + } + park_x = on; + have_park = true; + } + } + if (!have_park && park_pt_on_bed(far_x)) { + park_x = far_x; + have_park = true; + } + if (have_park) { + const Vec2f stop = writer.rotated(Vec2f(park_x, writer.y())); + writer.feedrate(m_travel_speed * 60.f) + .append(std::string("G1 X") + Slic3r::float_to_string_decimal_point(stop.x()) + + " Y" + Slic3r::float_to_string_decimal_point(stop.y()) + + never_skip_tag() + "\n"); + } + writer.set_extruder_temp(wait_for_temp, true, wait_for_temp_tag()); + } + // Travel to where we assume we are. Custom toolchange or some special T code handling (parking extruder etc) // gcode could have left the extruder somewhere, we cannot just start extruding. We should also inform the // postprocessor that we absolutely want to have this in the gcode, even if it thought it is the same as before. @@ -1694,6 +1800,10 @@ void WipeTower2::toolchange_Change( + never_skip_tag() + "\n" ); + // Priming has no tower to park beside — wait right at the priming line instead. + if (wait_for_temp_here && !wait_beside_tower) + writer.set_extruder_temp(wait_for_temp, true, wait_for_temp_tag()); + writer.append("[deretraction_from_wipe_tower_generator]"); // The toolchange Tn command will be inserted later, only in case that the user does @@ -2020,31 +2130,68 @@ std::pair<double, double> WipeTower2::get_wipe_tower_cone_base(double width, dou } // Static method to extract wipe_volumes[from][to] from the configuration. -std::vector<std::vector<float>> WipeTower2::extract_wipe_volumes(const PrintConfig& config) +// Takes a ConfigBase so the GUI's wipe tower size estimate can pass the plate's +// DynamicPrintConfig directly instead of materializing a full PrintConfig per call. +std::vector<std::vector<float>> WipeTower2::extract_wipe_volumes(const ConfigBase& config) { - // Get wiping matrix to get number of extruders and convert vector<double> to vector<float>: - std::vector<float> wiping_matrix(cast<float>(config.flush_volumes_matrix.values)); - auto scale = config.flush_multiplier.get_at(0); + // flush_volumes_matrix holds one filaments x filaments block per nozzle (written by + // PresetBundle::update_multi_material_filament_presets), so the filament count is + // sqrt(size / nozzles). One tower serves every nozzle and the filament to nozzle assignment is + // only decided later by ToolOrdering, so fold the blocks with std::max: the depth reserved here + // has to cover the worst nozzle. With a single nozzle the fold has one term. + const std::vector<double> &raw_matrix = config.option<ConfigOptionFloats>("flush_volumes_matrix")->values; + const auto *nozzle_diameter = config.option<ConfigOptionFloats>("nozzle_diameter"); + size_t nozzle_nums = (nozzle_diameter == nullptr || nozzle_diameter->values.empty()) ? 1 : nozzle_diameter->values.size(); + unsigned int number_of_extruders = (unsigned int)(sqrt(raw_matrix.size() / nozzle_nums) + EPSILON); + if (size_t(number_of_extruders) * number_of_extruders * nozzle_nums != raw_matrix.size()) { + // Saved for a different nozzle count (older project, or the printer was just switched): + // fall back to reading the whole option as one block, as this did before. + nozzle_nums = 1; + number_of_extruders = (unsigned int)(sqrt(raw_matrix.size()) + EPSILON); + } // The values shall only be used when SEMM is enabled. The purging for other printers // is determined by filament_minimal_purge_on_wipe_tower. - if (! config.purge_in_prime_tower.value || ! config.single_extruder_multi_material.value) - std::fill(wiping_matrix.begin(), wiping_matrix.end(), 0.f); + const bool purge = config.option<ConfigOptionBool>("purge_in_prime_tower")->value + && config.option<ConfigOptionBool>("single_extruder_multi_material")->value; - // Extract purging volumes for each extruder pair: - std::vector<std::vector<float>> wipe_volumes; - const unsigned int number_of_extruders = (unsigned int)(sqrt(wiping_matrix.size())+EPSILON); - for (size_t i = 0; i<number_of_extruders; ++i) - wipe_volumes.push_back(std::vector<float>(wiping_matrix.begin()+i*number_of_extruders, wiping_matrix.begin()+(i+1)*number_of_extruders)); + // Extract purging volumes for each extruder pair, each nozzle's block scaled by its own multiplier: + std::vector<std::vector<float>> wipe_volumes(number_of_extruders, std::vector<float>(number_of_extruders, 0.f)); + if (purge) { + const auto *multiplier = config.option<ConfigOptionFloats>("flush_multiplier"); + for (size_t nozzle_id = 0; nozzle_id < nozzle_nums; ++nozzle_id) { + const std::vector<double> block = get_flush_volumes_matrix(raw_matrix, nozzle_id, nozzle_nums); + const double scale = multiplier->get_at(nozzle_id); + for (unsigned int i = 0; i<number_of_extruders; ++i) + for (unsigned int j = 0; j<number_of_extruders; ++j) + wipe_volumes[i][j] = std::max<float>(wipe_volumes[i][j], float(block[size_t(i) * number_of_extruders + j]) * scale); + } + } // Also include filament_minimal_purge_on_wipe_tower. This is needed for the preview. + const auto *minimal_purge = config.option<ConfigOptionFloats>("filament_minimal_purge_on_wipe_tower"); for (unsigned int i = 0; i<number_of_extruders; ++i) for (unsigned int j = 0; j<number_of_extruders; ++j) - wipe_volumes[i][j] = std::max<float>(wipe_volumes[i][j] * scale, config.filament_minimal_purge_on_wipe_tower.get_at(j)); + wipe_volumes[i][j] = std::max<float>(wipe_volumes[i][j], minimal_purge->get_at(j)); return wipe_volumes; } +float WipeTower2::estimate_semm_flush_volume(const ConfigBase& config, size_t filaments_cnt) +{ + const std::vector<std::vector<float>> wipe_volumes = extract_wipe_volumes(config); + if (wipe_volumes.empty()) // an empty flush matrix would make the average below 0/0 + return 0.f; + float maximum = 0.f; + for (const std::vector<float> &v : wipe_volumes) + maximum += *std::max_element(v.begin(), v.end()); + maximum = maximum * filaments_cnt / wipe_volumes.size(); + + // Orca: it's overshooting a bit, so let's reduce it a bit + maximum *= 0.6; + return maximum; +} + static float get_wipe_depth(float volume, float layer_height, float perimeter_width, float extra_flow, float extra_spacing, float width) { float length_to_extrude = (volume_to_length(volume, perimeter_width, layer_height)) / extra_flow; diff --git a/src/libslic3r/GCode/WipeTower2.hpp b/src/libslic3r/GCode/WipeTower2.hpp index 3efa884202..5b1a474b5d 100644 --- a/src/libslic3r/GCode/WipeTower2.hpp +++ b/src/libslic3r/GCode/WipeTower2.hpp @@ -17,13 +17,20 @@ namespace Slic3r class WipeTowerWriter2; class PrintRegionConfig; +class ConfigBase; class WipeTower2 { public: static const std::string never_skip_tag() { return "_GCODE_WIPE_TOWER_NEVER_SKIP_TAG"; } + // Marks the wait-for-temp-on-wipe-tower M109 so the interface-temp deduplication pass + // in WipeTowerIntegration::append_tcr2 does not strip it. + static const std::string wait_for_temp_tag() { return ";_WAIT_FOR_TEMP_ON_WIPE_TOWER"; } static std::pair<double, double> get_wipe_tower_cone_base(double width, double height, double depth, double angle_deg); - static std::vector<std::vector<float>> extract_wipe_volumes(const PrintConfig& config); + static std::vector<std::vector<float>> extract_wipe_volumes(const ConfigBase& config); + // Estimated total flush volume of a SEMM print with the given number of filaments, + // used to reserve wipe tower space before the tower is generated. + static float estimate_semm_flush_volume(const ConfigBase& config, size_t filaments_cnt); // Construct ToolChangeResult from current state of WipeTower2 and WipeTowerWriter2. @@ -38,6 +45,11 @@ public: // Shared with the entry routing in GCode.cpp so the router and the tower agree. static bool use_gap_wall(const PrintConfig& config); + // Whether the blocking toolchange temperature wait moves onto the wipe tower. + // Shared with the defer flag in GCode.cpp append_tcr2 so the deferral and the + // tower's tagged M109 can never disagree. + static bool wait_for_temp_enabled(const PrintConfig& config); + // x -- x coordinates of wipe tower in mm ( left bottom corner ) // y -- y coordinates of wipe tower in mm ( left bottom corner ) // width -- width of wipe tower in mm ( default 60 mm - leave as it is ) @@ -227,6 +239,7 @@ private: size_t m_first_layer_idx = size_t(-1); bool m_enable_tower_interface_features = false; bool m_enable_tower_interface_cooldown_during_tower = false; + bool m_wait_for_temp_on_wipe_tower = false; bool m_prev_layer_had_interface = false; bool m_current_layer_has_interface = false; @@ -263,6 +276,7 @@ private: } m_bed_shape; float m_bed_width; // width of the bed bounding box Vec2f m_bed_bottom_left; // bottom-left corner coordinates (for rectangular beds) + Polygon m_bed_polygon; // printable_area contour (scaled) float m_perimeter_width = 0.4f * Width_To_Nozzle_Ratio; // Width of an extrusion line, also a perimeter spacing for 100% infill. float m_extrusion_flow = 0.038f; //0.029f;// Extrusion flow is derived from m_perimeter_width, layer height and filament diameter. @@ -385,7 +399,9 @@ private: void toolchange_Change( WipeTowerWriter2 &writer, const size_t new_tool, - const std::string& new_material); + const std::string& new_material, + const int wait_for_temp, + const bool wait_beside_tower); void toolchange_Load( WipeTowerWriter2 &writer, diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index e3d1c30362..0e80cc1fb7 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -3,6 +3,7 @@ #include "I18N.hpp" #include "PrintConfig.hpp" #include "ClipperUtils.hpp" +#include "Geometry/ArcWelder.hpp" #include "Line.hpp" #include <algorithm> #include <iomanip> @@ -1018,45 +1019,48 @@ std::string GCodeWriter::_spiral_travel_to_z(double z, const Vec2d &ij_offset, c } if (!this->config.enable_arc_fitting) { // Orca: if arc fitting is disabled, approximate the arc with small linear segments - std::ostringstream oss; const double z_start = m_pos(2); // starting Z height - // -------------------------------------------------------------------- - // Determine number of segments based on Resolution - // -------------------------------------------------------------------- - const double ref_resolution = 0.01; // reference resolution in mm - const double ref_segments = 8.0; // reference number of segments at reference resolution - - // number of linear segments to use for approximating the arc, clamp between 4 and 16 - const int segments = std::clamp(int(std::round(ref_segments * (ref_resolution / m_resolution))), 4, 16); - // -------------------------------------------------------------------- - const double px = m_pos(0) - m_x_offset; // take plate offset into consideration const double py = m_pos(1) - m_y_offset; // take plate offset into consideration const double cx = px + ij_offset(0); // center x const double cy = py + ij_offset(1); // center y const double radius = ij_offset.norm(); // radius + + // Number of linear segments approximating the circle, chosen so that a chord never deviates + // from the true arc by more than the slicing resolution. A resolution of 0 means "no + // simplification", which has no finite segment count, so it takes the upper bound. + constexpr size_t min_segments = 8; // keep a small spiral visibly round + constexpr size_t max_segments = 128; // bound the emitted G-code + const int segments = int(m_resolution > 0. ? + std::clamp(Geometry::ArcWelder::arc_discretization_steps(radius, 2. * M_PI, m_resolution), min_segments, max_segments) : + max_segments); + const double a0 = std::atan2(py - cy, px - cx); // start angle - const double delta = 2.0 * M_PI; // CCW full circle - if (full_gcode_comment) - oss << ";" << comment << "\n"; + auto emit_point = [&output](const Vec3d &point) { + GCodeG1Formatter w; + w.emit_xyz(point); + output += w.string(); + }; - oss << "G1 F" << (speed * 60.0) << "\n"; // set feedrate + output.reserve(size_t(segments) * 40); // ~40 characters per emitted G1 line + + GCodeG1Formatter w; // set feedrate + w.emit_f(speed * 60.0); + w.emit_comment(GCodeWriter::full_gcode_comment, comment); + output += w.string(); // approximate the arc with small linear segments (without the last point which is added later to ensure exactness) for (int i = 1; i < segments; ++i) { - double t = double(i) / segments; // parametric position along arc - double a = a0 + delta * t; // CCW arc param - double x = cx + radius * std::cos(a); // point on circle - double y = cy + radius * std::sin(a); // point on circle - double zz = z_start + (z - z_start) * t; // interpolated Z height - - oss << "G1 X" << x << " Y" << y << " Z" << zz << "\n"; + const double t = double(i) / segments; // parametric position along arc + const double a = a0 + 2. * M_PI * t; // CCW arc param, full circle + emit_point(Vec3d(cx + radius * std::cos(a), // point on circle + cy + radius * std::sin(a), + z_start + (z - z_start) * t)); // interpolated Z height } - oss << "G1 X" << px << " Y" << py << " Z" << z << "\n"; // final point to ensure exactness - output = oss.str(); + emit_point(Vec3d(px, py, z)); // final point to ensure exactness } else { // Orca: if arc fitting is enabled emit a G2/G3 command for the spiral lift output = std::string("G17") + (full_gcode_comment ? " ; XY plane for arc\n" : "\n"); diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index fcb63c3e96..8247ab757a 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -165,7 +165,6 @@ public: //BBS - unsigned int m_last_additional_fan_speed; int m_last_bed_temperature; bool m_last_bed_temperature_reached; double m_lifted; diff --git a/src/libslic3r/Layer.cpp b/src/libslic3r/Layer.cpp index b3a145bed0..87ad11bcf8 100644 --- a/src/libslic3r/Layer.cpp +++ b/src/libslic3r/Layer.cpp @@ -210,6 +210,12 @@ void Layer::make_perimeters() if (! (*it)->slices.empty()) { LayerRegion* other_layerm = *it; const PrintRegion &other_region = other_layerm->region(); + // Per-part gradient tags a region with its owning ModelVolume; merging two + // differently-tagged regions would collapse volumes that need independent + // gradient runs. Both tags are invalid unless per-part gradient is on, so + // this is a no-op for every other configuration. + if (this_region.gradient_volume_id() != other_region.gradient_volume_id()) + continue; if (is_perimeter_compatible(*m_object->print(), this_region, other_region)) { other_layerm->perimeters.clear(); diff --git a/src/libslic3r/LocalesUtils.cpp b/src/libslic3r/LocalesUtils.cpp index d321072335..308752cc62 100644 --- a/src/libslic3r/LocalesUtils.cpp +++ b/src/libslic3r/LocalesUtils.cpp @@ -53,7 +53,7 @@ bool is_decimal_separator_point() double string_to_double_decimal_point(const std::string_view str, size_t* pos /* = nullptr*/) { - double out; + double out = 0.; size_t p = fast_float::from_chars(str.data(), str.data() + str.size(), out).ptr - str.data(); if (pos) *pos = p; diff --git a/src/libslic3r/Measure.hpp b/src/libslic3r/Measure.hpp index 614b443131..0374c5d4aa 100644 --- a/src/libslic3r/Measure.hpp +++ b/src/libslic3r/Measure.hpp @@ -33,15 +33,6 @@ public: SurfaceFeature(const Vec3d& pt) : m_type{SurfaceFeatureType::Point}, m_pt1{pt} {} - SurfaceFeature(const SurfaceFeature& sf){ - this->clone(sf); - volume = sf.volume; - plane_indices = sf.plane_indices; - world_tran = sf.world_tran; - world_plane_features = sf.world_plane_features; - origin_surface_feature = sf.origin_surface_feature; - } - void clone(const SurfaceFeature &sf) { m_type = sf.get_type(); @@ -94,7 +85,7 @@ public: void* volume{nullptr}; std::vector<int>* plane_indices{nullptr}; - Transform3d world_tran; + Transform3d world_tran = Transform3d::Identity(); std::shared_ptr<std::vector<SurfaceFeature>> world_plane_features{nullptr}; std::shared_ptr<SurfaceFeature> origin_surface_feature{nullptr}; diff --git a/src/libslic3r/MeshBoolean.cpp b/src/libslic3r/MeshBoolean.cpp index 8b50681370..7497f92abf 100644 --- a/src/libslic3r/MeshBoolean.cpp +++ b/src/libslic3r/MeshBoolean.cpp @@ -352,7 +352,7 @@ void segment(CGALMesh& src, std::vector<CGALMesh>& dst, double smoothing_alpha = //} //else { - dst.emplace_back(std::move(CGALMesh(out))); + dst.emplace_back(CGALMesh(out)); } } //if (mesh_merged.is_empty() == false) { @@ -371,7 +371,7 @@ std::vector<TriangleMesh> segment(const TriangleMesh& src, double smoothing_alph std::vector<TriangleMesh> out_meshes; for (auto& outf_cgal_mesh: out_cgal_meshes) { - out_meshes.emplace_back(std::move(cgal_to_triangle_mesh(outf_cgal_mesh.m))); + out_meshes.emplace_back(cgal_to_triangle_mesh(outf_cgal_mesh.m)); } return out_meshes; diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 1177a5227d..8f1dd8926c 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -1,6 +1,8 @@ #include "Model.hpp" #include "libslic3r.h" #include "BuildVolume.hpp" +#include "TexturePainting.hpp" +#include "Format/AssimpImport.hpp" #include "ClipperUtils.hpp" #include "Exception.hpp" #include "Model.hpp" @@ -104,6 +106,7 @@ Model& Model::assign_copy(const Model &rhs) this->mk_version = rhs.mk_version; this->md_name = rhs.md_name; this->md_value = rhs.md_value; + this->texture_mesh = rhs.texture_mesh; return *this; } @@ -139,6 +142,7 @@ Model& Model::assign_copy(Model &&rhs) this->mk_version = rhs.mk_version; this->md_name = rhs.md_name; this->md_value = rhs.md_value; + this->texture_mesh = std::move(rhs.texture_mesh); this->backup_path = std::move(rhs.backup_path); this->object_backup_id_map = std::move(rhs.object_backup_id_map); this->next_object_backup_id = rhs.next_object_backup_id; @@ -239,6 +243,27 @@ _finished: // BBS: add part plate related logic // BBS: backup & restore // Loading model from a file, it may be a simple geometry file as STL or OBJ, however it may be a project file as well. +// Build a plain geometry ModelObject from a textured mesh. The texture itself is carried +// separately on Model::texture_mesh and consumed by the texture import dialog. +static void add_textured_mesh_to_model(Model& model, const TexturedMesh& tex_mesh, const std::string& input_file) +{ + std::string object_name = boost::filesystem::path(input_file).filename().string(); + + indexed_triangle_set its; + its.vertices.resize(tex_mesh.vertices.size()); + for (size_t i = 0; i < tex_mesh.vertices.size(); ++i) + its.vertices[i] = Vec3f(tex_mesh.vertices[i][0], tex_mesh.vertices[i][1], tex_mesh.vertices[i][2]); + its.indices.resize(tex_mesh.indices.size()); + for (size_t i = 0; i < tex_mesh.indices.size(); ++i) + its.indices[i] = Vec3i32(tex_mesh.indices[i][0], tex_mesh.indices[i][1], tex_mesh.indices[i][2]); + + its_merge_vertices(its); + its_remove_degenerate_faces(its); + its_compactify_vertices(its); + + model.add_object(object_name.c_str(), input_file.c_str(), TriangleMesh(std::move(its))); +} + Model Model::read_from_file(const std::string& input_file, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, @@ -281,32 +306,85 @@ Model Model::read_from_file(const std::string& result = load_stl(input_file.c_str(), &model, nullptr, stlFn,256); else if (boost::algorithm::iends_with(input_file, ".obj")) { ObjInfo obj_info; - result = load_obj(input_file.c_str(), &model, obj_info, message); - if (result){ - ObjDialogInOut in_out; - in_out.model = &model; - in_out.lost_material_name = obj_info.lost_material_name; + ObjParser::MtlData mtl_data; + result = load_obj(input_file.c_str(), &model, obj_info, message, nullptr, &mtl_data); + if (result && obj_info.has_uv_png && !obj_info.uvs.empty() && !model.objects.empty()) { + // Textured OBJ: hand the mesh + materials to the texture-to-color importer + // instead of the flat per-face colour dialog. + auto tex_mesh = std::make_shared<TexturedMesh>(); + std::string obj_dir = boost::filesystem::path(input_file).parent_path().string(); + if (obj_to_textured_mesh(obj_info, + model.objects.back()->volumes[0]->mesh().its, + mtl_data, obj_dir, *tex_mesh)) { + model.texture_mesh = tex_mesh; + } + } + else if (result && !model.objects.empty() && !model.objects.back()->volumes.empty()) { + // Vertex-colour and MTL face-colour OBJs also go through the texture-to-color + // importer (as precomputed per-face colors) instead of the flat + // per-face colour dialog, matching the uv_png branch above. + auto build_tex_mesh_geometry = [&]() { + auto tex_mesh = std::make_shared<TexturedMesh>(); + const auto& its = model.objects.back()->volumes[0]->mesh().its; + tex_mesh->vertices.resize(its.vertices.size()); + for (size_t i = 0; i < its.vertices.size(); ++i) + tex_mesh->vertices[i] = {its.vertices[i].x(), its.vertices[i].y(), its.vertices[i].z()}; + tex_mesh->indices.resize(its.indices.size()); + for (size_t i = 0; i < its.indices.size(); ++i) + tex_mesh->indices[i] = {its.indices[i][0], its.indices[i][1], its.indices[i][2]}; + return tex_mesh; + }; if (obj_info.vertex_colors.size() > 0) { - if (objFn) { // 1.result is ok and pop up a dialog - in_out.input_colors = std::move(obj_info.vertex_colors); - in_out.is_single_color = false; - in_out.deal_vertex_color = true; - objFn(in_out); + auto tex_mesh = build_tex_mesh_geometry(); + const auto& its = model.objects.back()->volumes[0]->mesh().its; + tex_mesh->precomputed_face_colors.resize(its.indices.size()); + for (size_t i = 0; i < its.indices.size(); ++i) { + const auto& f = its.indices[i]; + auto avg = [&](int ch) -> std::size_t { + float v = (obj_info.vertex_colors[f[0]][ch] + + obj_info.vertex_colors[f[1]][ch] + + obj_info.vertex_colors[f[2]][ch]) / 3.0f * 255.0f; + return (std::size_t) std::clamp(v, 0.0f, 255.0f); + }; + tex_mesh->precomputed_face_colors[i] = {avg(0), avg(1), avg(2)}; } - } else if (obj_info.face_colors.size() > 0 && obj_info.has_uv_png == false) { // mtl file - if (objFn) { // 1.result is ok and pop up a dialog - in_out.input_colors = std::move(obj_info.face_colors); - in_out.is_single_color = obj_info.is_single_mtl; - in_out.deal_vertex_color = false; - objFn(in_out); + tex_mesh->precomputed_vertex_colors = obj_info.vertex_colors; + model.texture_mesh = tex_mesh; + } else if (obj_info.face_colors.size() > 0 && obj_info.has_uv_png == false) { + auto tex_mesh = build_tex_mesh_geometry(); + const size_t nf = tex_mesh->indices.size(); + tex_mesh->precomputed_face_colors.resize(nf); + for (size_t i = 0; i < nf; ++i) { + if (i < obj_info.face_colors.size()) { + const auto& c = obj_info.face_colors[i]; + tex_mesh->precomputed_face_colors[i] = { + (std::size_t) std::clamp(c[0] * 255.0f, 0.0f, 255.0f), + (std::size_t) std::clamp(c[1] * 255.0f, 0.0f, 255.0f), + (std::size_t) std::clamp(c[2] * 255.0f, 0.0f, 255.0f) + }; + } else { + tex_mesh->precomputed_face_colors[i] = {128, 128, 128}; + } } - } /*else if (obj_info.has_uv_png && obj_info.uvs.size() > 0) { - boost::filesystem::path full_path(input_file); - std::string obj_directory = full_path.parent_path().string(); - obj_info.obj_dircetory = obj_directory; - result = false; - message = _L("Importing obj with png function is developing."); - }*/ + model.texture_mesh = tex_mesh; + } + } + } + else if (boost::algorithm::iends_with(input_file, ".glb") || + boost::algorithm::iends_with(input_file, ".gltf") || + boost::algorithm::iends_with(input_file, ".fbx")) { + // These formats can carry material/texture data, so they go through the textured + // import path: the geometry becomes a normal object and the texture is handed to the + // texture-to-color dialog via Model::texture_mesh. + auto tex_mesh = std::make_shared<TexturedMesh>(); + result = load_assimp_textured_model(input_file, *tex_mesh, &message); + if (result) { + model.texture_mesh = tex_mesh; + add_textured_mesh_to_model(model, *tex_mesh, input_file); + } else if (!message.empty()) { + BOOST_LOG_TRIVIAL(error) << "Assimp: failed to load model: " << message + << ", path=" << input_file; + message = _L("The file format is incompatible and cannot be parsed."); } } else if (boost::algorithm::iends_with(input_file, ".svg")) @@ -578,6 +656,7 @@ void Model::clear_objects() this->objects.clear(); object_backup_id_map.clear(); next_object_backup_id = 1; + texture_mesh.reset(); } // BBS: backup, reuse objects @@ -798,7 +877,7 @@ void Model::convert_multipart_object(unsigned int max_extruders) // Revert the centering operation. trafo_volume.set_offset(trafo_volume.get_offset() - o->origin_translation); int counter = 1; - auto copy_volume = [o, v, max_extruders, &counter, &extruder_counter](ModelVolume *new_v) { + auto copy_volume = [o, v, &counter](ModelVolume *new_v) { assert(new_v != nullptr); new_v->name = (counter > 1) ? o->name + "_" + std::to_string(counter++) : o->name; //BBS: Use extruder priority: volumn > object > default @@ -2576,7 +2655,8 @@ void ModelVolume::update_extruder_count(size_t extruder_count) } } -void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id) +void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id, + const std::vector<unsigned char> &filament_is_mixed) { std::vector<int> used_extruders = get_extruders(); for (int extruder_id : used_extruders) { @@ -2587,8 +2667,22 @@ void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_cou } // Same stale-assignment cleanup as update_extruder_count, for the filament-delete path. // Ported from BambuStudio (STUDIO-15763). - if (extruder_id() > extruder_count) { - this->config.erase("extruder"); + size_t eid = extruder_id(); + // Judge out-of-range against the post-remap id, mirroring update_filament_values_for_items_when_delete_filament. + // Using the pre-remap eid would wrongly erase a high extruder that should remap (e.g. 5 -> 4 after + // deleting filament 1); update_filament_values_for_items_when_delete_filament would then skip it + // (!has("extruder")) and the volume would fall back to the object default color. + size_t remapped = eid; + if (eid == filament_id) + remapped = (replace_filament_id > 0) ? (size_t)replace_filament_id : 1; + else if (eid > filament_id) + remapped = eid - 1; + if (remapped > extruder_count) { + // filament_is_mixed is the pre-delete snapshot; index it with the ORIGINAL eid (1-based), + // not remapped, so we check whether this volume's current slot is a mixed slot. + bool is_mixed = !filament_is_mixed.empty() && eid >= 1 && (eid - 1) < filament_is_mixed.size() && filament_is_mixed[eid - 1]; + if (!is_mixed) + this->config.erase("extruder"); } } @@ -3243,9 +3337,9 @@ double Model::findMaxSpeed(const ModelObject* object) { if (objectKey == "outer_wall_speed") externalPerimeterSpeedObj = object->config.get().opt_float_nullable(objectKey, 0); if (objectKey == "small_perimeter_speed") - smallPerimeterSpeedObj = object->config.get().opt_float_nullable(objectKey, 0); + smallPerimeterSpeedObj = object->config.get().opt_float_or_percent_nullable(objectKey, 0).get_abs_value(externalPerimeterSpeedObj); if (objectKey == "small_support_perimeter_speed") - smallSupportPerimeterSpeedObj = object->config.get().opt_float_nullable(objectKey, 0); + smallSupportPerimeterSpeedObj = object->config.get().opt_float_or_percent_nullable(objectKey, 0).get_abs_value(supportSpeedObj); } objMaxSpeed = std::max(perimeterSpeedObj, std::max(externalPerimeterSpeedObj, std::max(infillSpeedObj, std::max(solidInfillSpeedObj, std::max(topSolidInfillSpeedObj, std::max(supportSpeedObj, std::max(smallPerimeterSpeedObj, std::max(smallSupportPerimeterSpeedObj, objMaxSpeed)))))))); if (objMaxSpeed <= 0) objMaxSpeed = 250.; @@ -3495,6 +3589,15 @@ void FacetsAnnotation::get_facets(const ModelVolume& mv, std::vector<indexed_tri selector.get_facets(facets_per_type); } +void FacetsAnnotation::shift_states_above(const ModelVolume &mv, EnforcerBlockerType threshold, int delta) +{ + if (empty()) return; + TriangleSelector selector(mv.mesh()); + selector.deserialize(m_data, false); + selector.shift_states_above(threshold, delta); + this->set(selector); +} + void FacetsAnnotation::set_enforcer_block_type_limit(const ModelVolume &mv, EnforcerBlockerType max_type, EnforcerBlockerType to_delete_filament, diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 2d46bc4cdf..6834c7a59b 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -47,6 +47,8 @@ namespace cereal { } namespace Slic3r { + +struct TexturedMesh; enum class ConversionType; class BuildVolume; @@ -740,6 +742,9 @@ public: EnforcerBlockerType max_type, EnforcerBlockerType to_delete_filament = EnforcerBlockerType::NONE, EnforcerBlockerType replace_filament = EnforcerBlockerType::NONE); + // Shift painted filament indices >= threshold by delta. Used when a physical filament is + // inserted ahead of existing slots (mixed-color slots are kept at the end of the list). + void shift_states_above(const ModelVolume &mv, EnforcerBlockerType threshold, int delta); indexed_triangle_set get_facets_strict(const ModelVolume& mv, EnforcerBlockerType type) const; bool has_facets(const ModelVolume& mv, EnforcerBlockerType type) const; bool empty() const { return m_data.triangles_to_split.empty(); } @@ -932,7 +937,8 @@ public: // BBS std::vector<int> get_extruders() const; void update_extruder_count(size_t extruder_count); - void update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id = -1); + void update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id = -1, + const std::vector<unsigned char> &filament_is_mixed = {}); // Split this volume, append the result to the object owning this volume. // Return the number of volumes created from this one. @@ -1549,6 +1555,10 @@ public: std::shared_ptr<ModelInfo> model_info = nullptr; std::shared_ptr<ModelProfileInfo> profile_info = nullptr; + // Textured mesh data for texture-to-painting import. Populated by the loader when a mesh + // arrives with usable UVs and a texture map; consumed (and reset) by the import dialog. + std::shared_ptr<TexturedMesh> texture_mesh; + //makerlab information std::string mk_name; std::string mk_version; diff --git a/src/libslic3r/OpenVDBUtils.cpp b/src/libslic3r/OpenVDBUtils.cpp index 72c7668a45..c72607f14f 100644 --- a/src/libslic3r/OpenVDBUtils.cpp +++ b/src/libslic3r/OpenVDBUtils.cpp @@ -1,4 +1,6 @@ +#ifndef NOMINMAX #define NOMINMAX +#endif #include "OpenVDBUtils.hpp" #ifdef _MSC_VER diff --git a/src/libslic3r/Orient.cpp b/src/libslic3r/Orient.cpp index ae1954087d..7d54048176 100644 --- a/src/libslic3r/Orient.cpp +++ b/src/libslic3r/Orient.cpp @@ -39,7 +39,6 @@ namespace orientation { float height_to_bottom_hull_ratio = 0; // affects stability, the lower the better float unprintability = 0; Eigen::VectorXf areas_cooling; - CostItems(CostItems const & other) = default; CostItems() = default; static std::string field_names() { return " overhang, bottom, bothull, contour, A_laf, A_prj, unprintability"; diff --git a/src/libslic3r/Orient.hpp b/src/libslic3r/Orient.hpp index 30dbdd3a20..370f23d4fb 100644 --- a/src/libslic3r/Orient.hpp +++ b/src/libslic3r/Orient.hpp @@ -48,100 +48,50 @@ struct OrientMesh { }; -// params for minimizing support area -struct OrientParamsArea { - float TAR_A = 0.015f; - float TAR_B = 0.177f; - float RELATIVE_F = 20; - float CONTOUR_F = 0.5f; - float BOTTOM_F = 2.5f; - float BOTTOM_HULL_F = 0.1f; - float TAR_C = 0.1f; - float TAR_D = 1; - float TAR_E = 0.0115f; - float FIRST_LAY_H = 0.2f;//0.0475; - float VECTOR_TOL = -0.00083f; - float NEGL_FACE_SIZE = 0.01f; - float ASCENT = -0.5f; - float PLAFOND_ADV = 0.0599f; - float CONTOUR_AMOUNT = 0.0182427f; - float OV_H = 2.574f; - float height_offset = 2.3728f; - float height_log = 0.041375f; - float height_log_k = 1.9325457f; - float LAF_MAX = 0.999f; // cos(1.4\degree) for low angle face 0.9997f - float LAF_MIN = 0.97f; // cos(14\degree) 0.9703f - float TAR_LAF = 0.001f; //0.01f - float TAR_PROJ_AREA = 0.1f; - float BOTTOM_MIN = 0.1f; // min bottom area. If lower than it the object may be unstable - float BOTTOM_MAX = 2000; // max bottom area. If get to it the object is stable enough (further increase bottom area won't do more help) - float height_to_bottom_hull_ratio_MIN = 1; - float BOTTOM_HULL_MAX = 2000;// max bottom hull area - float APPERANCE_FACE_SUPP=3; // penalty of generating supports on appearance face - - float overhang_angle = 60.f; - bool use_low_angle_face = true; - bool min_volume = false; - Eigen::Vector3f fun_dir; - - /// Allow parallel execution. - bool parallel = true; - - /// Progress indicator callback called when an object gets packed. - /// The unsigned argument is the number of items remaining to pack. - std::function<void(unsigned, std::string)> progressind = {}; - - /// A predicate returning true if abort is needed. - std::function<bool(void)> stopcondition = {}; - - OrientParamsArea() = default; -}; - struct OrientParams { - float TAR_A = 0.01f;//0.128f; - float TAR_B = 0.177f; - float RELATIVE_F= 6.610621027964314f; - float CONTOUR_F = 0.23228623269775997f; - float BOTTOM_F = 1.167152017941474f; - float BOTTOM_HULL_F = 0.1f; - float TAR_C = 0.24308070476924726f; - float TAR_D = 0.6284515508160871f; - float TAR_E = 0;//0.032157292647062234; - float FIRST_LAY_H = 0.2f;//0.029; - float VECTOR_TOL = -0.0011163303070972383f; - float NEGL_FACE_SIZE = 0.1f; - float ASCENT= -0.5f; - float PLAFOND_ADV = 0.04079208948120519f; - float CONTOUR_AMOUNT = 0.0101472219892684f; - float OV_H = 1.0370178217794535f; - float height_offset = 2.7417608343142073f; - float height_log = 0.06442030687034085f; - float height_log_k = 0.3933594673063997f; - float LAF_MAX = 0.999f; // cos(1.4\degree) for low angle face //0.9997f; - float LAF_MIN= 0.9703f; // cos(14\degree) 0.9703f; - float TAR_LAF = 0.01f; //0.1f - float TAR_PROJ_AREA = 0.1f; - float BOTTOM_MIN = 0.1f; // min bottom area. If lower than it the objects may be unstable - float BOTTOM_MAX = 2000; //400 - float height_to_bottom_hull_ratio_MIN = 1; - float BOTTOM_HULL_MAX = 2000;// max bottom hull area to clip //600 - float APPERANCE_FACE_SUPP=3; // penalty of generating supports on appearance face - - float overhang_angle = 60.f; - bool use_low_angle_face = true; - bool min_volume = false; - Eigen::Vector3f fun_dir; + float TAR_A { 0.01f }; // 0.128f; + float TAR_B { 0.177f }; + float RELATIVE_F { 6.610621027964314f }; + float CONTOUR_F { 0.23228623269775997f }; + float BOTTOM_F { 1.167152017941474f }; + float BOTTOM_HULL_F { 0.1f }; + float TAR_C { 0.24308070476924726f }; + float TAR_D { 0.6284515508160871f }; + float TAR_E { 0}; // 0.032157292647062234; + float FIRST_LAY_H { 0.2f}; // 0.029; + float VECTOR_TOL { -0.0011163303070972383f }; + float NEGL_FACE_SIZE { 0.1f }; + float ASCENT { -0.5f }; + float PLAFOND_ADV { 0.04079208948120519f }; + float CONTOUR_AMOUNT { 0.0101472219892684f }; + float OV_H { 1.0370178217794535f }; + float height_offset { 2.7417608343142073f }; + float height_log { 0.06442030687034085f }; + float height_log_k { 0.3933594673063997f }; + float LAF_MAX { 0.999f }; // cos(1.4\degree) for low angle face //0.9997f; + float LAF_MIN { 0.9703f }; // cos(14\degree) 0.9703f; + float TAR_LAF { 0.01f }; // 0.1f + float TAR_PROJ_AREA { 0.1f }; + float BOTTOM_MIN { 0.1f }; // min bottom area. If lower than it the objects may be unstable + float BOTTOM_MAX { 2000 }; // 400 + float height_to_bottom_hull_ratio_MIN { 1 }; + float BOTTOM_HULL_MAX { 2000 }; // max bottom hull area to clip //600 + float APPERANCE_FACE_SUPP { 3 }; // penalty of generating supports on appearance face + float overhang_angle { 60.f }; + bool use_low_angle_face { true }; + bool min_volume { false }; + Eigen::Vector3f fun_dir {}; /// Allow parallel execution. - bool parallel = false; + bool parallel { false }; /// Progress indicator callback called when an object gets packed. /// The unsigned argument is the number of items remaining to pack. - std::function<void(unsigned, std::string)> progressind = {}; + std::function<void(unsigned, std::string)> progressind {}; /// A predicate returning true if abort is needed. - std::function<bool(void)> stopcondition = {}; + std::function<bool(void)> stopcondition {}; OrientParams() = default; }; diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index ad4d615807..a6b38889a5 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -229,6 +229,22 @@ static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perime // Append thin walls to the nearest-neighbor search (only for first iteration) if (! thin_walls.empty()) { + // Orca: apply fuzzy skin to thin walls as well + for (auto& thin_wall : thin_walls) { + // First, we convert the ThickPolyline into Arachne::ExtrusionLine so we could reuse our existing fuzzy code + Arachne::ExtrusionLine el(0, true); + el.junctions.reserve(thin_wall.points.size()); + for (int i = 0; i < thin_wall.points.size(); i++) { + el.junctions.emplace_back(thin_wall.points[i], thin_wall.width[i], 0); + } + + // Then we fuzzy it + apply_fuzzy_skin(&el, perimeter_generator, true, thin_wall.is_closed()); + + // Then convert the result back to ThickPolyline + thin_wall = Arachne::to_thick_polyline(el); + } + variable_width(thin_walls, erExternalPerimeter, perimeter_generator.ext_perimeter_flow, coll.entities); thin_walls.clear(); } @@ -392,7 +408,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p ExtrusionRole role = is_external ? erExternalPerimeter : erPerimeter; const bool is_contour = !extrusion->is_closed || pg_extrusion.is_contour; - apply_fuzzy_skin(extrusion, perimeter_generator, is_contour); + apply_fuzzy_skin(extrusion, perimeter_generator, is_contour, extrusion->is_closed); ExtrusionPaths paths; // detect overhanging/bridging perimeters @@ -1977,7 +1993,7 @@ void PerimeterGenerator::process_no_bridge(Surfaces& all_surfaces, coord_t perim ExPolygons unsupported = diff_ex(last, *this->lower_slices, ApplySafetyOffset::Yes); if (!unsupported.empty()) { //remove small overhangs - ExPolygons unsupported_filtered = offset2_ex(unsupported, double(-perimeter_spacing), double(perimeter_spacing)); + ExPolygons unsupported_filtered = opening_ex(unsupported, perimeter_spacing); if (!unsupported_filtered.empty()) { //to_draw.insert(to_draw.end(), last.begin(), last.end()); @@ -2090,35 +2106,40 @@ void PerimeterGenerator::process_no_bridge(Surfaces& all_surfaces, coord_t perim //TODO: add other polys as holes inside this one (-margin) } else { // if(this->config->counterbore_hole_bridging.value == chbBridges) // Orca: Partial counterbore bridging is mask-based. Preserve the supported - // remainder (`last`) and use simplified BridgeDetector coverage to derive the + // remainder and use simplified BridgeDetector coverage to derive the // bridgeable counterbore span. The span is grown from supported material, - // shrunk back, stripped from `last`, and expanded back. It is then prevented - // from intruding deeper into `last` than the explicit anchor overlap. - // Finally, add the allowed anchor band from `last` then remove the + // shrunk back, stripped from the remaining normal surface, and expanded back. + // It is then prevented from intruding deeper into it than the explicit anchor overlap. + // Finally, add the allowed anchor band from it then remove the // narrow hole-side wall contact, which must remain unbridgeable. - last = diff_ex(last, unsupported_filtered, ApplySafetyOffset::Yes); + const ExPolygons remaining = diff_ex(last, unsupported_filtered, ApplySafetyOffset::Yes); ExPolygons bridgeable_filtered; + for (ExPolygon& poly : bridgeable) { poly.simplify(perimeter_spacing, &bridgeable_filtered); } bridgeable_filtered = opening_ex(bridgeable_filtered, ext_perimeter_width); // Get rid of coarseness of the resulted bridgeable area by using the original supported area as reference. - // This is to avoid keeping tiny bridgeable areas that are far from the supported area, or protrude into it. - bridgeable_filtered = union_ex(offset_ex(last, perimeter_spacing), bridgeable_filtered); + // This is to avoid keeping tiny bridgeable areas that are far from the supported area, or protrude into it. + bridgeable_filtered = union_ex(offset_ex(remaining, perimeter_spacing), bridgeable_filtered); bridgeable_filtered = offset_ex(bridgeable_filtered, -perimeter_spacing); - bridgeable_filtered = diff_ex(bridgeable_filtered, last, ApplySafetyOffset::Yes); - bridgeable_filtered = opening_ex(bridgeable_filtered, perimeter_spacing); // filter noise from the diff_ex + bridgeable_filtered = diff_ex(bridgeable_filtered, remaining, ApplySafetyOffset::Yes); + bridgeable_filtered = opening_ex(bridgeable_filtered, ext_perimeter_width / 2); // filter noise from the diff_ex bridgeable_filtered = offset_ex(bridgeable_filtered, perimeter_spacing); // restore the size to the original bridgeable area // Safety measure: Keep the bridge mask from intruding deeper into the - // supported anchor region (`last`) than the explicit anchor overlap. - bridgeable_filtered = diff_ex(bridgeable_filtered, offset_ex(last, -bridge_anchor_offset)); + // supported anchor region than the explicit anchor overlap. + bridgeable_filtered = diff_ex(bridgeable_filtered, offset_ex(remaining, -bridge_anchor_offset)); - ExPolygons bridge_anchor_areas = intersection_ex(last, offset_ex(unsupported_filtered, bridge_anchor_offset)); + ExPolygons bridge_anchor_areas = intersection_ex(remaining, offset_ex(unsupported_filtered, bridge_anchor_offset)); unsupported_filtered = union_ex(bridgeable_filtered, bridge_anchor_areas); // add bridge anchor unsupported_filtered = opening_ex(unsupported_filtered, bridge_anchor_offset); // remove anchor area from hole-side walls, it must remain unbridgeable + + // update 'last' only if we have a valid bridgeable area, otherwise we will lose the original unsupported area + if (!unsupported_filtered.empty()) + last = remaining; // TODO: Fix the case with thin outer walls around the bridge (1~2 walls) where classic wall // might generate two walls in a tiny space or non at all if "Detect thin walls" is not activated } diff --git a/src/libslic3r/PlaceholderParser.cpp b/src/libslic3r/PlaceholderParser.cpp index e3a4037590..3e29e0c172 100644 --- a/src/libslic3r/PlaceholderParser.cpp +++ b/src/libslic3r/PlaceholderParser.cpp @@ -1791,6 +1791,8 @@ namespace client // from UTF8 to UTF16 don't bail out. msg += boost::nowide::narrow(boost::nowide::widen(error_line)); msg += '\n'; + // The error dialog (MsgDialog.cpp) renders this excerpt monospaced. It recognizes a source + // line directly above a caret line of spaces and a single '^'. for (size_t i = 0; i < error_pos; ++ i) msg += ' '; msg += "^\n"; diff --git a/src/libslic3r/Point.hpp b/src/libslic3r/Point.hpp index 039f361eaa..21e5355fc5 100644 --- a/src/libslic3r/Point.hpp +++ b/src/libslic3r/Point.hpp @@ -195,7 +195,6 @@ public: Point(int64_t x, int32_t y) : Vec2crd(coord_t(x), coord_t(y)) {} Point(int32_t x, int64_t y) : Vec2crd(coord_t(x), coord_t(y)) {} Point(double x, double y) : Vec2crd(coord_t(std::round(x)), coord_t(std::round(y))) {} - Point(const Point &rhs) { *this = rhs; } explicit Point(const Vec2d& rhs) : Vec2crd(coord_t(std::round(rhs.x())), coord_t(std::round(rhs.y()))) {} // This constructor allows you to construct Point from Eigen expressions // This constructor has to be implicit (non-explicit) to allow implicit conversion from Eigen expressions. @@ -278,7 +277,6 @@ public: Point3(int32_t x, int32_t y, int32_t z = 0) : Vec3crd(coord_t(x), coord_t(y), coord_t(z)) {} Point3(int64_t x, int64_t y, int64_t z = 0) : Vec3crd(coord_t(x), coord_t(y), coord_t(z)) {} Point3(double x, double y, double z = 0.0) : Vec3crd(coord_t(std::round(x)), coord_t(std::round(y)), coord_t(std::round(z))) {} - Point3(const Point3 &rhs) { *this = rhs; } explicit Point3(const Vec2crd& vec2crd, coord_t z = 0) : Vec3crd(vec2crd.x(), vec2crd.y(), z) {} explicit Point3(const Vec3crd &vec3crd) : Vec3crd(vec3crd) {} // This constructor allows you to construct Point from Eigen expressions diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 2821bef0af..235ea66ad2 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -8,7 +8,9 @@ #ifdef _MSC_VER #define WIN32_LEAN_AND_MEAN + #ifndef NOMINMAX #define NOMINMAX + #endif #include <Windows.h> #endif /* _MSC_VER */ @@ -147,6 +149,9 @@ Semver get_version_from_json(std::string file_path) return Semver(); //throw ConfigurationError(format("Failed loading configuration file \"%1%\": %2%", file_path, err.what())); } + catch(...) { + return Semver(); + } } //BBS: add a function to load the key-values from xxx.json @@ -261,18 +266,28 @@ void extend_default_config_length(DynamicPrintConfig& config, const bool set_nil } }; + // The four variant sets are immutable after static init and probed for every + // key of every preset loaded; one merged map makes that a single lookup. + // emplace keeps the first insertion, preserving the first-set-wins priority + // of the else-if chain this replaces. + static const std::unordered_map<std::string, int> variant_class = [] { + std::unordered_map<std::string, int> m; + for (const std::string& k : print_options_with_variant) m.emplace(k, 0); + for (const std::string& k : filament_options_with_variant) m.emplace(k, 1); + for (const std::string& k : printer_options_with_variant_1) m.emplace(k, 2); + for (const std::string& k : printer_options_with_variant_2) m.emplace(k, 3); + return m; + }(); + for(auto& key :config.keys()){ - if(auto iter = print_options_with_variant.find(key); iter != print_options_with_variant.end()){ - replace_nil_and_resize(key, process_variant_length); - } - else if(auto iter = filament_options_with_variant.find(key); iter != filament_options_with_variant.end()){ - replace_nil_and_resize(key, filament_variant_length); - } - else if(auto iter = printer_options_with_variant_1.find(key); iter != printer_options_with_variant_1.end()){ - replace_nil_and_resize(key, machine_variant_length); - } - else if(auto iter = printer_options_with_variant_2.find(key); iter != printer_options_with_variant_2.end()){ - replace_nil_and_resize(key, machine_variant_length * 2); + auto iter = variant_class.find(key); + if (iter == variant_class.end()) + continue; + switch (iter->second) { + case 0: replace_nil_and_resize(key, process_variant_length); break; + case 1: replace_nil_and_resize(key, filament_variant_length); break; + case 2: replace_nil_and_resize(key, machine_variant_length); break; + case 3: replace_nil_and_resize(key, machine_variant_length * 2); break; } } } @@ -968,15 +983,19 @@ BedType Preset::get_default_bed_type(PresetBundle* preset_bundle) if (config.has("default_bed_type") && !config.opt_string("default_bed_type").empty()) { try { std::string str_bed_type = config.opt_string("default_bed_type"); - - // Try parsing as integer first (legacy format) + BedType bed_type; + if (ConfigOptionEnum<BedType>::from_string(str_bed_type, bed_type) && + bed_type > btDefault && bed_type < btCount) { + return bed_type; + } + + // Try parsing as integer (legacy format) int bed_type_value = atoi(str_bed_type.c_str()); - if (bed_type_value > 0) { + if (bed_type_value > 0 && bed_type_value < BedType::btCount) { return BedType(bed_type_value); } - else { - BOOST_LOG_TRIVIAL(error) << "default_bed_type: invalid bed type: " << str_bed_type; - } + + BOOST_LOG_TRIVIAL(error) << "default_bed_type: invalid bed type: " << str_bed_type; return BedType::btPEI; } catch(...) { @@ -1170,6 +1189,7 @@ static std::vector<std::string> s_Preset_print_options{ "flush_into_infill", "flush_into_objects", "flush_into_support", + "enable_mixed_color_sublayer", "tree_support_branch_angle", "tree_support_angle_slow", "tree_support_wall_count", @@ -1351,6 +1371,8 @@ static std::vector<std::string> s_Preset_filament_options {/*"filament_colour", "filament_retraction_length", "filament_retraction_minimum_travel", "filament_retraction_speed", + "filament_retract_length_toolchange", + "filament_retract_restart_extra_toolchange", "filament_wipe", "filament_z_hop", "filament_z_hop_types", @@ -1404,7 +1426,7 @@ static std::vector<std::string> s_Preset_machine_limits_options { static std::vector<std::string> s_Preset_printer_options { "printer_technology", "printable_area", "extruder_printable_area", "support_parallel_printheads", "parallel_printheads_count", "parallel_printheads_bed_exclude_areas", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "gcode_flavor", - "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs", + "gcode_skip_config_block", "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs", "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", "printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type", "printable_height", "extruder_printable_height", "extruder_clearance_radius", "extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod", @@ -1421,7 +1443,7 @@ static std::vector<std::string> s_Preset_printer_options { "use_relative_e_distances", "extruder_type", "use_firmware_retraction", "printer_notes", "grab_length", "support_object_skip_flush", "physical_extruder_map", "cooling_tube_retraction", - "cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "wipe_tower_type", "purge_in_prime_tower", "enable_filament_ramming", "tool_change_on_wipe_tower", + "cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "wipe_tower_type", "purge_in_prime_tower", "enable_filament_ramming", "tool_change_on_wipe_tower", "wait_for_temp_on_wipe_tower", "z_offset", "disable_m73", "preferred_orientation", "emit_machine_limits_to_gcode", "pellet_modded_printer", "support_multi_bed_types", "use_3mf", "default_bed_type", "bed_mesh_min","bed_mesh_max","bed_mesh_probe_distance", "adaptive_bed_mesh_margin", "enable_long_retraction_when_cut","long_retractions_when_cut","retraction_distances_when_cut", "bed_temperature_formula", "nozzle_flush_dataset", @@ -3784,12 +3806,14 @@ void PresetCollection::update_library_profile_excluded_from() } // Check all presets that has the same alias as the filament presets with empty compatible_printers in Orca Filament Library. + // A printer specific profile supersedes the generic one, no matter whether it lives in a vendor bundle or in the + // library itself. for (const Preset& preset : m_presets) { - if (preset.vendor == nullptr || preset.vendor->name == PresetBundle::ORCA_FILAMENT_LIBRARY) + if (preset.vendor == nullptr) continue; const auto* compatible_printers = dynamic_cast<const ConfigOptionStrings*>(preset.config.option("compatible_printers")); - // All profiles in concrete vendor profile shouldn't have empty compatible_printers, but here we check it for safety. + // Profiles with empty compatible_printers are the generic ones, they never supersede anything. if (compatible_printers == nullptr || compatible_printers->values.empty()) continue; auto itr = excluded_froms.find(preset.alias); diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index b88b5a5ed5..22089be0b6 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -131,6 +131,10 @@ public: PrinterVariant() {} PrinterVariant(const std::string &name) : name(name) {} std::string name; + + // All fields, declaration order — keep in sync; bump CACHE_VERSION on change. + template<class Archive> + void serialize(Archive& ar) { ar(name); } // PrinterVariant }; struct PrinterModel { @@ -139,7 +143,7 @@ public: std::string name; //BBS: this is internal id for the printer. Currently only used for searching in database std::string model_id; - PrinterTechnology technology; + PrinterTechnology technology = ptFFF; std::string family; std::vector<PrinterVariant> variants; std::vector<std::string> default_materials; @@ -162,6 +166,17 @@ public: } const PrinterVariant* variant(const std::string &name) const { return const_cast<PrinterModel*>(this)->variant(name); } + + // All fields, declaration order — keep in sync; bump CACHE_VERSION on change. + template<class Archive> + void serialize(Archive& ar) // PrinterModel + { + ar(id, name, model_id, technology, family, variants, default_materials, + not_support_bed_types, bed_model, bed_texture, image_bed_type, + bottom_texture_end_name, use_double_extruder_default_texture, + bottom_texture_rect, bottom_texture_rect_longer, middle_texture_rect, + hotend_model); + } }; std::vector<PrinterModel> models; @@ -173,6 +188,14 @@ public: bool valid() const { return ! name.empty() && ! id.empty() && config_version.valid(); } + // All fields, declaration order — keep in sync; bump CACHE_VERSION on change. + template<class Archive> + void serialize(Archive& ar) // VendorProfile + { + ar(name, id, config_version, config_update_url, changelog_url, + models, default_filaments, default_sla_materials); + } + // Load VendorProfile from an ini file. // If `load_all` is false, only the header with basic info (name, version, URLs) is loaded. static VendorProfile from_ini(const boost::filesystem::path &path, bool load_all=true); @@ -427,10 +450,10 @@ public: Preset(Type type, const std::string &name, bool is_default = false) : type(type), is_default(is_default), name(name) {} protected: - Preset() = default; - friend class PresetCollection; friend class PresetBundle; + + Preset() = default; }; bool is_compatible_with_print (const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_print, const PresetWithVendorProfile &active_printer); @@ -817,13 +840,12 @@ public: protected: PresetCollection() = default; - // Copy constructor and copy operators are not to be used from outside PresetBundle, - // as the Profile::vendor points to an instance of VendorProfile stored at parent PresetBundle! - PresetCollection(const PresetCollection &other) = default; - //BBS: add operator= logic insteadof default + // Deleted by the std::recursive_mutex member. PresetBundle copies by assignment. + PresetCollection(const PresetCollection &other) = delete; + //BBS: hand-written because m_mutex cannot be copy-assigned. PresetCollection& operator=(const PresetCollection &other); - // After copying a collection with the default operators above, call this function - // to adjust Profile::vendor pointers. + // Copying leaves every Preset::vendor pointing into the source bundle's vendor map. + // This re-points them at the matching entries in vendors. void update_vendor_ptrs_after_copy(const VendorMap &vendors); // Select a preset, if it exists. If it does not exist, select an invalid (-1) index. @@ -961,7 +983,8 @@ public: bool only_default_printers() const; private: PrinterPresetCollection() = default; - PrinterPresetCollection(const PrinterPresetCollection &other) = default; + // Deleted along with the base copy constructor. + PrinterPresetCollection(const PrinterPresetCollection &other) = delete; PrinterPresetCollection& operator=(const PrinterPresetCollection &other) = default; friend class PresetBundle; diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 01cbc43bc2..2dfc967e3f 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -1,8 +1,13 @@ #include <cassert> +#include <chrono> #include <ctime> +#include <sstream> #include "PresetBundle.hpp" + +#include "PresetCacheFormat.hpp" #include "PrintConfig.hpp" +#include "FilamentMixer.hpp" #include "libslic3r.h" #include "I18N.hpp" #include "Utils.hpp" @@ -49,7 +54,6 @@ static std::vector<std::string> s_project_options { "filament_multi_colour", "wipe_tower_x", "wipe_tower_y", - "wipe_tower_rotation_angle", "curr_bed_type", "flush_multiplier", // Fast-purge mode: project-level purge control, inert at Default. @@ -68,7 +72,17 @@ static std::vector<std::string> s_project_options { // whether dynamic per-nozzle filament mapping is active. Persisted with the project and // restored from a saved 3mf; reset to false on load and set true only by live device sync. "has_filament_switcher", - "enable_filament_dynamic_map" + "enable_filament_dynamic_map", + // Mixed-color filament slots. Project-level parallel arrays indexed like filament_colour: + // which slots are virtual mixes, their component filaments, blend ratios and the optional + // Z-gradient description. Kept with the project so a saved 3mf round-trips the mix setup. + "filament_is_mixed", + "filament_mixed_components", + "filament_mixed_sublayer_ratios", + "filament_mixed_gradient", + "filament_mixed_gradient_range", + "filament_mixed_gradient_curve", + "filament_mixed_gradient_per_part" }; //Orca: add custom as default @@ -308,16 +322,20 @@ std::string PresetBundle::find_preset_vendor(const std::string &preset_name, Pre return ""; } - // Iterate through vendor JSON files in the system directory - for (auto& dir_entry : fs::directory_iterator(system_dir)) { - std::string vendor_file = dir_entry.path().string(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Checking vendor: " << vendor_file; - if (!Slic3r::is_json_file(vendor_file)) + // A vendor is named by its profile or, where the build ships preset caches + // instead of the raw profile JSONs, by its cache alone. + for (const std::string& vendor_name : vendor_names_in(system_dir)) { + const fs::path vendor_json = system_dir / (vendor_name + ".json"); + if (! fs::exists(vendor_json)) { + if (VendorCacheFile::carries_preset((system_dir / (vendor_name + ".opc")).string(), vendor_name, type, preset_name)) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Found preset " << preset_name + << " in vendor cache " << vendor_name; + return vendor_name; + } continue; - - // Get vendor name (filename without .json extension) - std::string vendor_name = dir_entry.path().filename().string(); - vendor_name.erase(vendor_name.size() - 5); // Remove ".json" + } + const std::string vendor_file = vendor_json.string(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Checking vendor: " << vendor_file; try { // Load and parse the vendor JSON file @@ -564,6 +582,8 @@ PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, Forward //BBS: add config related logs BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, substitution_rule %1%, preferred printer_model_id %2%")%substitution_rule%preferred_selection.printer_model_id; + const auto startup_t0 = std::chrono::steady_clock::now(); + //BBS: change system config to json std::tie(substitutions, errors_cummulative) = this->load_system_presets_from_json(substitution_rule); @@ -589,6 +609,12 @@ PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, Forward set_calibrate_printer(""); + { + const auto total_ms = std::chrono::duration_cast<std::chrono::milliseconds>( + std::chrono::steady_clock::now() - startup_t0).count(); + BOOST_LOG_TRIVIAL(info) << "PresetBundle: all presets loaded in " << total_ms << " ms"; + } + //BBS: add config related logs BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" finished, returned substitutions %1%")%substitutions.size(); return substitutions; @@ -1001,6 +1027,8 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For bundles.m_bundles.clear(); bundles.WriteUnlock(); + const auto user_load_t0 = std::chrono::steady_clock::now(); + // Load bundle metadata from _local directory first fs::path local_dir(folder / PRESET_LOCAL_DIR); if (fs::exists(local_dir)) { @@ -1019,7 +1047,6 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For metadata.filament_presets.clear(); metadata.printer_presets.clear(); - // Add the profiles this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) { metadata.print_presets.push_back(preset.name); }, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); @@ -1056,7 +1083,6 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For metadata.printer_presets.clear(); metadata.is_subscribed = true; - // Load presets from bundle (same logic as __local__) this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) { metadata.print_presets.push_back(preset.name); }, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); @@ -1077,34 +1103,41 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For } } - // BBS do not load sla_print - // BBS: change directoties by design - try { - std::string print_selected_preset_name = prints.get_selected_preset().name; - this->prints.load_presets(dir_user_presets, PRESET_PRINT_NAME, substitutions, substitution_rule); - prints.select_preset_by_name(print_selected_preset_name, false); - } catch (const std::runtime_error &err) { - errors_cummulative += err.what(); + // BBS: change directories by design + + { + const auto json_t0 = std::chrono::steady_clock::now(); + try { + std::string sel = prints.get_selected_preset().name; + this->prints.load_presets(dir_user_presets, PRESET_PRINT_NAME, substitutions, substitution_rule); + prints.select_preset_by_name(sel, false); + } catch (const std::runtime_error& err) { errors_cummulative += err.what(); } + try { + std::string sel = filaments.get_selected_preset().name; + this->filaments.load_presets(dir_user_presets, PRESET_FILAMENT_NAME, substitutions, substitution_rule); + filaments.select_preset_by_name(sel, false); + } catch (const std::runtime_error& err) { errors_cummulative += err.what(); } + try { + std::string sel = printers.get_selected_preset().name; + this->printers.load_presets(dir_user_presets, PRESET_PRINTER_NAME, substitutions, substitution_rule); + printers.select_preset_by_name(sel, false); + } catch (const std::runtime_error& err) { errors_cummulative += err.what(); } + if (!errors_cummulative.empty()) throw Slic3r::RuntimeError(errors_cummulative); + + const auto json_ms = std::chrono::duration_cast<std::chrono::milliseconds>( + std::chrono::steady_clock::now() - json_t0).count(); + BOOST_LOG_TRIVIAL(info) << "PresetBundle: user presets loaded from JSON in " << json_ms << " ms"; } - try { - std::string filament_selected_preset_name = filaments.get_selected_preset().name; - this->filaments.load_presets(dir_user_presets, PRESET_FILAMENT_NAME, substitutions, substitution_rule); - filaments.select_preset_by_name(filament_selected_preset_name, false); - } catch (const std::runtime_error &err) { - errors_cummulative += err.what(); + + { + const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>( + std::chrono::steady_clock::now() - user_load_t0).count(); + BOOST_LOG_TRIVIAL(info) << "PresetBundle: user + bundle presets loaded in " << ms << " ms"; } - try { - std::string printer_selected_preset_name = printers.get_selected_preset().name; - this->printers.load_presets(dir_user_presets, PRESET_PRINTER_NAME, substitutions, substitution_rule); - printers.select_preset_by_name(printer_selected_preset_name, false); - } catch (const std::runtime_error &err) { - errors_cummulative += err.what(); - } - if (!errors_cummulative.empty()) throw Slic3r::RuntimeError(errors_cummulative); + this->update_multi_material_filament_presets(); this->update_compatible(PresetSelectCompatibleType::Never); - set_calibrate_printer(""); return PresetsConfigSubstitutions(); @@ -1210,13 +1243,10 @@ bool PresetBundle::apply_vendor_config( : std::map<std::string, std::string>(); // Find vendors that need installation - const auto vendor_dir = (fs::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred(); - std::vector<std::string> install_bundles; for (const auto &it : new_vendors) { if (it.second.size() > 0) { - auto vendor_file = vendor_dir / (it.first + ".json"); - if (!fs::exists(vendor_file)) { + if (!is_vendor_installed(it.first)) { install_bundles.emplace_back(it.first); } } @@ -2127,7 +2157,7 @@ int PresetBundle::validate_presets(const std::string &file_name, DynamicPrintCon void PresetBundle::remove_users_preset(AppConfig &config, std::map<std::string, std::map<std::string, std::string>> *my_presets) { - auto check_removed = [my_presets, this](Preset &preset) -> bool { + auto check_removed = [my_presets](Preset &preset) -> bool { if (my_presets == nullptr) return true; if (my_presets->find(preset.name) != my_presets->end()) return false; if (!preset.sync_info.empty()) return false; // syncing, not remove @@ -2224,6 +2254,16 @@ void PresetBundle::remove_users_preset(AppConfig &config, std::map<std::string, } } +// m_printer_hold_alias survives reset() (and a cache body that failed partway +// in), so every full-bundle rebuild clears all five collections' maps by hand. +void PresetBundle::clear_printer_hold_aliases() +{ + this->prints.m_printer_hold_alias.clear(); + this->sla_prints.m_printer_hold_alias.clear(); + this->filaments.m_printer_hold_alias.clear(); + this->sla_materials.m_printer_hold_alias.clear(); + this->printers.m_printer_hold_alias.clear(); +} //BBS: add json related logic, load system presets from json std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule) @@ -2243,22 +2283,19 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre if (validation_mode) dir = (boost::filesystem::path(data_dir())).make_preferred(); + const auto load_t0 = std::chrono::steady_clock::now(); + + // The vendors below are loaded whole and against each other — the filament + // library first, then every other vendor with it as the base — so each parse + // is complete enough to be worth caching. + m_generate_vendor_caches = m_generate_vendor_caches || ! validation_mode; + PresetsConfigSubstitutions substitutions; std::string errors_cummulative; - bool first = true; - std::vector<std::string> vendor_names; - // store all vendor names in vendor_names - for (auto& dir_entry : boost::filesystem::directory_iterator(dir)) { - std::string vendor_file = dir_entry.path().string(); - if (!Slic3r::is_json_file(vendor_file)) - continue; - - std::string vendor_name = dir_entry.path().filename().string(); - - // Remove the .json suffix. - vendor_name.erase(vendor_name.size() - 5); - vendor_names.push_back(vendor_name); - } + bool first = true; + // Sorted, so any duplicate-preset warning below comes out in the same order on + // every run. + const std::set<std::string> vendor_names = vendor_names_in(dir); // Separate ORCA_FILAMENT_LIBRARY from other vendors. It must be loaded // first because other vendors' filaments may inherit from it via the // `base_bundle` lookup in parse_subfile. The remaining vendors are @@ -2274,8 +2311,13 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre } // Step 1: Load ORCA_FILAMENT_LIBRARY into `this` synchronously. - if (!orca_lib_vendor.empty()) { + if (! orca_lib_vendor.empty()) { try { + // Match a fresh launch before parsing: hold aliases and the error + // counter survive reset(), and would otherwise carry prior-cycle + // state into this load. + this->clear_printer_hold_aliases(); + this->m_errors = 0; append(substitutions, this->load_vendor_configs_from_json(dir.string(), orca_lib_vendor, PresetBundle::LoadSystem, compatibility_rule).first); first = false; } catch (const std::runtime_error &err) { @@ -2298,10 +2340,10 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre for (size_t i = range.begin(); i < range.end(); ++i) { auto bundle = std::make_unique<PresetBundle>(); bundle->set_is_validation_mode(validation_mode); + bundle->set_generate_vendor_caches(m_generate_vendor_caches); try { auto result = bundle->load_vendor_configs_from_json( - dir.string(), other_vendors[i], PresetBundle::LoadSystem, - compatibility_rule, this); + dir.string(), other_vendors[i], PresetBundle::LoadSystem, compatibility_rule, this); parallel_substitutions[i] = std::move(result.first); parallel_bundles[i] = std::move(bundle); } catch (const std::runtime_error &err) { @@ -2346,6 +2388,11 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre } this->update_system_maps(); + + const auto load_ms = std::chrono::duration_cast<std::chrono::milliseconds>( + std::chrono::steady_clock::now() - load_t0).count(); + BOOST_LOG_TRIVIAL(info) << "PresetBundle: " << vendor_names.size() << " vendor(s) loaded in " << load_ms << " ms"; + //BBS: add config related logs BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, errors_cummulative %1%")%errors_cummulative; return std::make_pair(std::move(substitutions), errors_cummulative); @@ -2668,6 +2715,79 @@ void PresetBundle::load_installed_sla_materials(AppConfig &config) preset.set_visible_from_appconfig(config); } +// Mixed-color filament metadata is project state saved in the 3mf, also mirrored into the app +// config so the last session's mixes are back before any project is opened. It is kept in the +// per-printer snapshot next to the filament list it indexes (filament_%02u/filament_colors), +// because that list is rebuilt on every printer selection and the component ids are 1-based +// indices into exactly that list. Missing keys clear the arrays, so one printer never inherits +// another's mixes; fallback_to_global also reads the shared "presets" keys an older config +// layout used, which export_selections drops on the next save. +static void load_mixed_filament_settings(DynamicPrintConfig &project_config, AppConfig &config, + const std::string &printer_name, size_t n_filaments, + bool fallback_to_global) +{ + auto raw_value = [&](const char *key, bool &found) -> std::string { + if (config.has_printer_setting(printer_name, key)) { + found = true; + return config.get_printer_setting(printer_name, key); + } + if (fallback_to_global && config.has("presets", key)) { + found = true; + return config.get("presets", key); + } + found = false; + return std::string{}; + }; + std::vector<std::string> parts; + auto load_bools = [&](const char *key) { + auto &vals = project_config.option<ConfigOptionBools>(key)->values; + vals.clear(); + bool found = false; + const std::string s = raw_value(key, found); + if (found && !s.empty()) { + boost::algorithm::split(parts, s, boost::algorithm::is_any_of(",")); + for (const auto &p : parts) vals.push_back(p == "1"); + } + vals.resize(n_filaments, false); + }; + auto load_strings = [&](const char *key) { + auto &vals = project_config.option<ConfigOptionStrings>(key)->values; + vals.clear(); + bool found = false; + const std::string s = raw_value(key, found); + if (found && !s.empty()) { + boost::algorithm::split(parts, s, boost::algorithm::is_any_of("|")); + vals = parts; + } + vals.resize(n_filaments, std::string{}); + }; + + load_bools("filament_is_mixed"); + load_strings("filament_mixed_components"); + load_strings("filament_mixed_sublayer_ratios"); + load_bools("filament_mixed_gradient"); + load_strings("filament_mixed_gradient_range"); + load_bools("filament_mixed_gradient_per_part"); + + // The gradient curve is the one array whose values contain '|' themselves (it separates the + // control points), so it is stored C-style escaped rather than '|'-joined. + { + auto &vals = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_curve")->values; + vals.clear(); + bool found = false; + const std::string s = raw_value("filament_mixed_gradient_curve", found); + if (found && !s.empty()) { + std::vector<std::string> curves; + if (unescape_strings_cstyle(s, curves)) + vals = std::move(curves); + } + vals.resize(n_filaments, std::string{}); + // Heal legacy corruption: clear any non-empty slot that ended up with < 2 points + // (e.g. a curve split across slots by the old "|" delimiter). Falls back to linear. + Slic3r::sanitize_mixed_gradient_curve_array(vals); + } +} + void PresetBundle::update_selections(AppConfig &config) { std::string initial_printer_profile_name = printers.get_selected_preset_name(); @@ -2748,6 +2868,9 @@ void PresetBundle::update_selections(AppConfig &config) auto flush_multipliers = matrix | boost::adaptors::transformed(boost::lexical_cast<double, std::string>); project_config.option<ConfigOptionFloats>("flush_multiplier")->values = std::vector<double>(flush_multipliers.begin(), flush_multipliers.end()); } + // No global fallback here: on a printer change the legacy shared keys describe another + // printer's filament list, so absent per-printer keys must clear the mixes, not revive them. + load_mixed_filament_settings(project_config, config, initial_printer_profile_name, filament_presets.size(), false); // Update visibility of presets based on their compatibility with the active printer. // Always try to select a compatible print and filament preset to the current printer preset, @@ -2797,6 +2920,16 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p // If executed due to a Config Wizard update, preferred_printer contains the first newly installed printer, otherwise nullptr. const Preset *preferred_printer = printers.find_system_preset_by_model_and_variant(preferred_selection.printer_model_id, preferred_selection.printer_variant); printers.select_preset_by_name(preferred_printer ? preferred_printer->name : initial_printer_profile_name, true); + Preset &selected_printer = printers.get_edited_preset(); + if (selected_printer.printer_technology() == ptFFF) { + BedType bed_type = selected_printer.get_default_bed_type(this); + const std::string saved_bed_type = config.get_printer_setting(selected_printer.name, "curr_bed_type"); + const int saved_bed_type_value = atoi(saved_bed_type.c_str()); + if (saved_bed_type_value > btDefault && saved_bed_type_value < btCount) + bed_type = static_cast<BedType>(saved_bed_type_value); + project_config.set_key_value("curr_bed_type", new ConfigOptionEnum<BedType>(bed_type)); + config.set("curr_bed_type", std::to_string(static_cast<int>(bed_type))); + } CNumericLocalesSetter locales_setter; // Orca: load from orca_presets @@ -2898,6 +3031,7 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p auto flush_multipliers = matrix | boost::adaptors::transformed(boost::lexical_cast<double, std::string>); project_config.option<ConfigOptionFloats>("flush_multiplier")->values = std::vector<double>(flush_multipliers.begin(), flush_multipliers.end()); } + load_mixed_filament_settings(project_config, config, initial_printer_profile_name, filament_presets.size(), true); // Update visibility of presets based on their compatibility with the active printer. // Always try to select a compatible print and filament preset to the current printer preset, @@ -3032,6 +3166,32 @@ void PresetBundle::export_selections(AppConfig &config) "|"); config.set_printer_setting(printer_name, "flush_multiplier", flush_multiplier_str); + // Mixed-color filament metadata goes into the per-printer snapshot next to the filament list + // it indexes (see load_mixed_filament_settings). Bools are ','-joined and the component, ratio + // and range strings '|'-joined; the gradient curve is escaped instead, as it contains '|'. + auto join_bools = [](const std::vector<unsigned char> &vals) { + std::string s; + for (size_t i = 0; i < vals.size(); ++i) { + if (i > 0) s += ","; + s += (vals[i] ? "1" : "0"); + } + return s; + }; + if (auto *opt = project_config.option<ConfigOptionBools>("filament_is_mixed")) + config.set_printer_setting(printer_name, "filament_is_mixed", join_bools(opt->values)); + if (auto *opt = project_config.option<ConfigOptionStrings>("filament_mixed_components")) + config.set_printer_setting(printer_name, "filament_mixed_components", boost::algorithm::join(opt->values, "|")); + if (auto *opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios")) + config.set_printer_setting(printer_name, "filament_mixed_sublayer_ratios", boost::algorithm::join(opt->values, "|")); + if (auto *opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient")) + config.set_printer_setting(printer_name, "filament_mixed_gradient", join_bools(opt->values)); + if (auto *opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_range")) + config.set_printer_setting(printer_name, "filament_mixed_gradient_range", boost::algorithm::join(opt->values, "|")); + if (auto *opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_curve")) + config.set_printer_setting(printer_name, "filament_mixed_gradient_curve", escape_strings_cstyle(opt->values)); + if (auto *opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient_per_part")) + config.set_printer_setting(printer_name, "filament_mixed_gradient_per_part", join_bools(opt->values)); + // BBS //config.set("presets", "sla_print", sla_prints.get_selected_preset_name()); //config.set("presets", "sla_material", sla_materials.get_selected_preset_name()); @@ -3040,46 +3200,6 @@ void PresetBundle::export_selections(AppConfig &config) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": printer %1%, print %2%, filaments[0] %3% ")%printers.get_selected_preset_name() % prints.get_selected_preset_name() %filament_presets[0]; } -// BBS -void PresetBundle::set_num_filaments(unsigned int n, std::vector<std::string> new_colors) { - int old_filament_count = this->filament_presets.size(); - if (n > old_filament_count && old_filament_count != 0) - filament_presets.resize(n, filament_presets.back()); - else { - filament_presets.resize(n); - } - ConfigOptionStrings* filament_color = project_config.option<ConfigOptionStrings>("filament_colour"); - ConfigOptionStrings *filament_multi_color = project_config.option<ConfigOptionStrings>("filament_multi_colour"); - ConfigOptionStrings* filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type"); - ConfigOptionInts* filament_map = project_config.option<ConfigOptionInts>("filament_map"); - ConfigOptionInts* filament_nozzle_map = project_config.option<ConfigOptionInts>("filament_nozzle_map"); - ConfigOptionInts* filament_volume_map = project_config.option<ConfigOptionInts>("filament_volume_map"); - - filament_color->resize(n); - // Sync filament multi colour - filament_multi_color->values.resize(n); - for (size_t i = 0; i < n; i++) { - filament_multi_color->values[i] = filament_color->values[i]; - } - filament_color_type->resize(n); - filament_map->values.resize(n, 1); - filament_nozzle_map->values.resize(n, 0); - filament_volume_map->values.resize(n, static_cast<int>(NozzleVolumeType::nvtStandard)); - ams_multi_color_filment.resize(n); - - // BBS set new filament color to new_color - if (old_filament_count < n) { - if (!new_colors.empty()) { - for (int i = old_filament_count; i < n; i++) { - filament_color->values[i] = new_colors[i - old_filament_count]; - filament_multi_color->values[i] = new_colors[i - old_filament_count]; - filament_color_type->values[i] = "1"; // default color type - } - } - } - - update_multi_material_filament_presets(); -} void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) { unsigned old_filament_count = this->filament_presets.size(); @@ -3095,6 +3215,11 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) ConfigOptionInts* filament_nozzle_map = project_config.option<ConfigOptionInts>("filament_nozzle_map"); ConfigOptionInts* filament_volume_map = project_config.option<ConfigOptionInts>("filament_volume_map"); + // Which slots are new is a fact about the arrays below, not about filament_presets: + // update_multi_material_filament_presets() tops that list up to the nozzle count on its own, + // so it can already sit at the new size while every array below is still at the old one. + const size_t old_slot_count = filament_color->values.size(); + filament_color->resize(n); // Sync filament multi colour filament_multi_color->values.resize(n); @@ -3107,14 +3232,29 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) filament_volume_map->values.resize(n, static_cast<int>(NozzleVolumeType::nvtStandard)); ams_multi_color_filment.resize(n); + // Mixed-color metadata is a parallel per-filament array set, so it has to grow and shrink + // with the filament count exactly like filament_colour above. + if (auto* opt = project_config.option<ConfigOptionBools>("filament_is_mixed")) + opt->values.resize(n, false); + if (auto* opt = project_config.option<ConfigOptionStrings>("filament_mixed_components")) + opt->values.resize(n, std::string{}); + if (auto* opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios")) + opt->values.resize(n, std::string{}); + if (auto* opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient")) + opt->values.resize(n, false); + if (auto* opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_range")) + opt->values.resize(n, std::string{}); + if (auto* opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_curve")) + opt->values.resize(n, std::string{}); + if (auto* opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient_per_part")) + opt->values.resize(n, false); + //BBS set new filament color to new_color - if (old_filament_count < n) { - if (!new_color.empty()) { - for (unsigned i = old_filament_count; i < n; i++) { - filament_color->values[i] = new_color; - filament_multi_color->values[i] = new_color; - filament_color_type->values[i] = "1"; // default color type - } + if (!new_color.empty()) { + for (size_t i = old_slot_count; i < n; i++) { + filament_color->values[i] = new_color; + filament_multi_color->values[i] = new_color; + filament_color_type->values[i] = "1"; // default color type } } @@ -3179,9 +3319,69 @@ void PresetBundle::update_num_filaments(unsigned int to_del_flament_id) erase_or_resize(filament_color_type->values); erase_or_resize(ams_multi_color_filment); + // Mixed-color metadata. Component IDs reference other slots by 1-based index, so a deleted + // *physical* filament must be remapped out of every mix before the arrays themselves shrink. + // Deleting a mixed slot needs no remap (nothing references a mixed slot as a component). + { + auto *is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed"); + auto *comp_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components"); + if (is_mixed_opt && comp_opt) { + bool del_is_physical = (to_del_flament_id >= is_mixed_opt->values.size() + || !is_mixed_opt->values[to_del_flament_id]); + if (del_is_physical) + remap_mixed_components_on_delete(is_mixed_opt->values, comp_opt->values, + to_del_flament_id + 1); + } + if (is_mixed_opt) + erase_or_resize(is_mixed_opt->values); + if (comp_opt) + erase_or_resize(comp_opt->values); + } + if (auto *opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios")) + erase_or_resize(opt->values); + if (auto *opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient")) + erase_or_resize(opt->values); + if (auto *opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_range")) + erase_or_resize(opt->values); + if (auto *opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_curve")) + erase_or_resize(opt->values); + if (auto *opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient_per_part")) + erase_or_resize(opt->values); + update_multi_material_filament_presets(to_del_flament_id); } +bool PresetBundle::is_mixed_filament(size_t idx) const +{ + auto *opt = project_config.option<ConfigOptionBools>("filament_is_mixed"); + return opt && idx < opt->values.size() && opt->values[idx]; +} + +size_t PresetBundle::num_mixed_filaments() const +{ + auto *opt = project_config.option<ConfigOptionBools>("filament_is_mixed"); + return opt == nullptr ? 0 : size_t(std::count(opt->values.begin(), opt->values.end(), true)); +} + +// Counted off the mixed flags, not filament_presets: that list is topped up to the nozzle count on +// its own, so it can sit a slot ahead of the arrays that describe slots. Unlike the sibling +// physical_filament_config_indices(), which bounds by filament_presets, this ignores that top-up. +size_t PresetBundle::num_physical_filaments() const +{ + const auto *opt = project_config.option<ConfigOptionBools>("filament_is_mixed"); + return opt == nullptr ? filament_presets.size() + : size_t(std::count(opt->values.begin(), opt->values.end(), false)); +} + +std::vector<size_t> PresetBundle::physical_filament_config_indices() const +{ + std::vector<size_t> indices; + for (size_t i = 0; i < filament_presets.size(); ++i) + if (!is_mixed_filament(i)) + indices.push_back(i); + return indices; +} + void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info) { @@ -3222,7 +3422,7 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info) combox_info.ams_names.push_back(ams_name); continue; } - iter = std::find_if(filaments.begin(), filaments.end(), [&filament_type](auto &f) { return f.is_compatible && f.is_system; }); + iter = std::find_if(filaments.begin(), filaments.end(), [](auto &f) { return f.is_compatible && f.is_system; }); if (iter == filaments.end()) continue; } @@ -3387,6 +3587,63 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi ConfigOptionStrings *filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type"); ConfigOptionInts * filament_map = project_config.option<ConfigOptionInts>("filament_map"); ConfigOptionInts * filament_volume_map = project_config.option<ConfigOptionInts>("filament_volume_map"); + + // Snapshot and temporarily strip mixed filament slots so AMS sync operates on physical + // filaments only. A mixed slot is virtual and has no tray to sync against; leaving it in + // would let AMS mapping overwrite it and would break the physical-first slot ordering the + // rest of the feature relies on. The slots are re-appended verbatim after the sync. + struct MixedSlotSnapshot { + std::string preset; + std::string color; + std::string color_type; + std::string mixed_components; + std::string mixed_sublayer_ratios; + bool mixed_gradient = false; + std::string mixed_gradient_range; + std::string mixed_gradient_curve; + bool mixed_gradient_per_part = false; + }; + std::vector<MixedSlotSnapshot> mixed_snapshots; + auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed"); + auto* mixed_comp_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components"); + auto* mixed_ratios_opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios"); + auto* mixed_gradient_opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient"); + auto* mixed_grad_range_opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_range"); + auto* mixed_grad_curve_opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_curve"); + auto* mixed_per_part_opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient_per_part"); + if (is_mixed_opt) { + for (size_t i = 0; i < is_mixed_opt->values.size() && i < this->filament_presets.size(); ++i) { + if (!is_mixed_opt->values[i]) + continue; + MixedSlotSnapshot snap; + snap.preset = this->filament_presets[i]; + snap.color = (i < filament_color->values.size()) ? filament_color->values[i] : ""; + snap.color_type = (i < filament_color_type->values.size()) ? filament_color_type->values[i] : ""; + if (mixed_comp_opt && i < mixed_comp_opt->values.size()) snap.mixed_components = mixed_comp_opt->values[i]; + if (mixed_ratios_opt && i < mixed_ratios_opt->values.size()) snap.mixed_sublayer_ratios = mixed_ratios_opt->values[i]; + if (mixed_gradient_opt && i < mixed_gradient_opt->values.size()) snap.mixed_gradient = mixed_gradient_opt->values[i]; + if (mixed_grad_range_opt && i < mixed_grad_range_opt->values.size()) snap.mixed_gradient_range = mixed_grad_range_opt->values[i]; + if (mixed_grad_curve_opt && i < mixed_grad_curve_opt->values.size()) snap.mixed_gradient_curve = mixed_grad_curve_opt->values[i]; + if (mixed_per_part_opt && i < mixed_per_part_opt->values.size()) snap.mixed_gradient_per_part = mixed_per_part_opt->values[i]; + mixed_snapshots.push_back(snap); + } + if (!mixed_snapshots.empty()) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": stripping " << mixed_snapshots.size() << " mixed filament slot(s) before AMS sync"; + size_t phys_count = this->filament_presets.size() - mixed_snapshots.size(); + this->filament_presets.resize(phys_count); + filament_color->values.resize(phys_count); + filament_color_type->values.resize(phys_count); + filament_map->values.resize(phys_count, 1); + is_mixed_opt->values.resize(phys_count); + if (mixed_comp_opt) mixed_comp_opt->values.resize(phys_count); + if (mixed_ratios_opt) mixed_ratios_opt->values.resize(phys_count); + if (mixed_gradient_opt) mixed_gradient_opt->values.resize(phys_count); + if (mixed_grad_range_opt) mixed_grad_range_opt->values.resize(phys_count); + if (mixed_grad_curve_opt) mixed_grad_curve_opt->values.resize(phys_count); + if (mixed_per_part_opt) mixed_per_part_opt->values.resize(phys_count); + } + } + if (color_only) { auto get_map_index = [&ams_infos](const std::vector<AMSMapInfo> &infos, const AMSMapInfo &temp) { for (int i = 0; i < infos.size(); i++) { @@ -3550,7 +3807,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi return -1; }; for (size_t i = 0; i < need_append_colors.size(); i++){ - if (exist_filament_presets.size() >= size_t(EnforcerBlockerType::ExtruderMax)){ + if (exist_filament_presets.size() >= MAXIMUM_AMS_SYNC_FILAMENT_NUMBER){ break; } auto idx = get_idx_in_array(exist_filament_presets, exist_colors, need_append_colors[i].filament_preset, need_append_colors[i].filament_color); @@ -3642,6 +3899,34 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi if (support_interface_filament_opt->value > filament_color_type->values.size()) support_interface_filament_opt->value = 0; } + // Re-append mixed filament slots that were stripped before AMS sync + if (!mixed_snapshots.empty()) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": re-appending " << mixed_snapshots.size() << " mixed filament slot(s) after AMS sync"; + size_t new_phys_count = this->filament_presets.size(); + if (is_mixed_opt) is_mixed_opt->values.resize(new_phys_count, (unsigned char)false); + if (mixed_comp_opt) mixed_comp_opt->values.resize(new_phys_count); + if (mixed_ratios_opt) mixed_ratios_opt->values.resize(new_phys_count); + if (mixed_gradient_opt) mixed_gradient_opt->values.resize(new_phys_count, (unsigned char)false); + if (mixed_grad_range_opt) mixed_grad_range_opt->values.resize(new_phys_count); + if (mixed_grad_curve_opt) mixed_grad_curve_opt->values.resize(new_phys_count); + if (mixed_per_part_opt) mixed_per_part_opt->values.resize(new_phys_count, (unsigned char)false); + + for (auto& snap : mixed_snapshots) { + this->filament_presets.push_back(snap.preset); + filament_color->values.push_back(snap.color); + filament_color_type->values.push_back(snap.color_type); + ams_multi_color_filment.push_back({snap.color}); + filament_map->values.push_back(1); + if (is_mixed_opt) is_mixed_opt->values.push_back((unsigned char)true); + if (mixed_comp_opt) mixed_comp_opt->values.push_back(snap.mixed_components); + if (mixed_ratios_opt) mixed_ratios_opt->values.push_back(snap.mixed_sublayer_ratios); + if (mixed_gradient_opt) mixed_gradient_opt->values.push_back((unsigned char)snap.mixed_gradient); + if (mixed_grad_range_opt) mixed_grad_range_opt->values.push_back(snap.mixed_gradient_range); + if (mixed_grad_curve_opt) mixed_grad_curve_opt->values.push_back(snap.mixed_gradient_curve); + if (mixed_per_part_opt) mixed_per_part_opt->values.push_back((unsigned char)snap.mixed_gradient_per_part); + } + } + // Update ams_multi_color_filment update_filament_multi_color(); update_multi_material_filament_presets(); @@ -4760,30 +5045,287 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": finished"); } +// Orca: load one source-form preset entry — parsed from its JSON subfile just +// now, or deserialized from the vendor's cache; the code is shared so a +// cache-loaded bundle cannot come out different from a JSON-loaded one. +// Resolves `inherits` against the presets loaded before this one +// (config_maps) or against base_bundle's filament library, flattens, validates +// and registers the preset. Returns the reason loading failed, empty on +// success. +std::string PresetBundle::load_vendor_preset( + const CachedPreset& entry, + const std::string& path, const std::string& vendor_name, + const PresetBundle* base_bundle, + LoadConfigBundleAttributes flags, + ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions, + std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, std::string>& filament_id_maps, + PresetCollection* presets_collection, size_t& count, bool is_from_lib, + const std::set<std::string>* retain_configs) +{ + const VendorProfile* current_vendor_profile = &this->vendors.at(vendor_name); + const std::string subfile = path + "/" + vendor_name + "/" + entry.sub_path; + const std::string& preset_name = entry.name; + std::string alias_name, filament_id = entry.filament_id; + std::vector<std::string> renamed_from = entry.renamed_from; + DynamicPrintConfig config; + const DynamicPrintConfig* default_config = nullptr; + std::string reason; + + //check whether it inherits other preset or not + if (! entry.inherits.empty()) { + auto it2 = config_maps.find(entry.inherits); + if (it2 != config_maps.end()) + default_config = &(it2->second); + if (default_config == nullptr && base_bundle != nullptr) { + auto base_it2 = base_bundle->m_config_maps.find(entry.inherits); + if (base_it2 != base_bundle->m_config_maps.end()) + default_config = &(base_it2->second); + } + if (default_config != nullptr) { + if (filament_id.empty() && (presets_collection->type() == Preset::TYPE_FILAMENT)) { + auto filament_id_map_iter = filament_id_maps.find(entry.inherits); + if (filament_id_map_iter != filament_id_maps.end()) { + filament_id = filament_id_map_iter->second; + } + if (filament_id.empty() && base_bundle != nullptr) { + auto base_filament_id_map_iter = base_bundle->m_filament_id_maps.find(entry.inherits); + if (base_filament_id_map_iter != base_bundle->m_filament_id_maps.end()) { + filament_id = base_filament_id_map_iter->second; + } + } + } + } + else { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not find inherits " << entry.inherits << " for " << preset_name; + // throw ConfigurationError(format("can not find inherits %1% for %2%", inherits, preset_name)); + reason = "Can not find inherits: " + entry.inherits; + return reason; + } + } + else { + if (presets_collection->type() == Preset::TYPE_PRINTER) + default_config = &presets_collection->default_preset_for(entry.config_src).config; + else + default_config = &presets_collection->default_preset().config; + } + config = *default_config; + config.apply(entry.config_src); + extend_default_config_length(config, true, *default_config); + if (entry.instantiation == "false" && "Template" != vendor_name) { + // Report configuration fields, which are misplaced into a wrong group. + std::string incorrect_keys = Preset::remove_invalid_keys(config, *default_config); + if (!incorrect_keys.empty()) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": The config " << subfile << " contains incorrect keys: " << incorrect_keys + << ", which were removed"; + } + + if (retain_configs == nullptr || retain_configs->count(preset_name) != 0) + config_maps.emplace(preset_name, std::move(config)); + if ((presets_collection->type() == Preset::TYPE_FILAMENT) && (!filament_id.empty())) + filament_id_maps.emplace(preset_name, filament_id); + return reason; + } + if (config.has("alias")) + alias_name = (dynamic_cast<const ConfigOptionString *>(config.option("alias")))->value; + Preset::normalize(config); + + // Report configuration fields, which are misplaced into a wrong group. + std::string incorrect_keys = Preset::remove_invalid_keys(config, *default_config); + if (!incorrect_keys.empty()) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": The config " << subfile << " contains incorrect keys: " << incorrect_keys + << ", which were removed"; + } + + if (presets_collection->type() == Preset::TYPE_PRINTER) { + // Filter out printer presets, which are not mentioned in the vendor profile. + // These presets are considered not installed. + auto printer_model = config.opt_string("printer_model"); + if (printer_model.empty()) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" defines no printer model, it will be ignored."; + reason = std::string("can not find printer_model"); + return reason; + } + auto printer_variant = config.opt_string("printer_variant"); + if (printer_variant.empty()) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" defines no printer variant, it will be ignored."; + reason = std::string("can not find printer_variant"); + return reason; + } + auto it_model = std::find_if(current_vendor_profile->models.cbegin(), current_vendor_profile->models.cend(), + [&](const VendorProfile::PrinterModel &m) { return m.id == printer_model; } + ); + if (it_model == current_vendor_profile->models.end()) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" defines invalid printer model \"" << printer_model << "\", it will be ignored."; + reason = std::string("can not find printer model in vendor profile"); + return reason; + } + auto it_variant = it_model->variant(printer_variant); + if (it_variant == nullptr) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" defines invalid printer variant \"" << printer_variant << "\", it will be ignored."; + reason = std::string("can not find printer_variant in vendor profile"); + return reason; + } + // An instantiation printer profile's nozzle_diameter must match the numeric (diameter) + // prefix of its printer_variant: "0.4" -> {0.4}, "0.8HF" -> {0.8} (a trailing + // non-numeric suffix such as "HF"/"HS" distinguishes a hardware sub-variant and is + // ignored here), and for multi-nozzle printers "0.4+0.6" -> {0.4, 0.6}. + // Note: a variant may legitimately repeat across presets of the same model (e.g. speed + // modes, IDEX copy/mirror, or different control boards), so only the diameter is + // validated, not variant uniqueness. Validation-only so the app keeps loading existing + // profiles unchanged. + if (validation_mode && entry.instantiation == "true") { + const auto *nd = config.option<ConfigOptionFloats>("nozzle_diameter"); + std::set<double> nozzles, variant_nozzles; + if (nd != nullptr) + nozzles.insert(nd->values.begin(), nd->values.end()); + std::vector<std::string> variant_tokens; + boost::algorithm::split(variant_tokens, printer_variant, boost::algorithm::is_any_of("+")); + bool variant_ok = true; // printer_variant is already guaranteed non-empty above + for (const std::string &tok : variant_tokens) { + size_t consumed = 0; + double d = string_to_double_decimal_point(tok, &consumed); + // Require a leading numeric diameter; a trailing suffix (e.g. "HF") is allowed. + if (consumed == 0) { variant_ok = false; break; } + variant_nozzles.insert(d); + } + if (!variant_ok || variant_nozzles != nozzles) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" has printer_variant \"" << printer_variant << + "\" that does not match its nozzle_diameter \"" << (nd ? nd->serialize() : std::string()) << "\". " + "printer_variant must begin with the nozzle diameter, optionally followed by a non-numeric suffix " + "(e.g. \"0.4\", \"0.8HF\"); for multi-nozzle printers, join the per-nozzle diameters with \"+\" in " + "nozzle order (e.g. \"0.4+0.6\")."; + } + } + } + const Preset *preset_existing = presets_collection->find_preset(preset_name, false); + if (preset_existing != nullptr) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" has already been loaded from another Config Bundle."; + reason = std::string("duplicated defines"); + return reason; + } + + auto file_path = (boost::filesystem::path(data_dir()) /PRESET_SYSTEM_DIR/ vendor_name / entry.sub_path).make_preferred(); + if(validation_mode) + file_path = (boost::filesystem::path(data_dir()) / vendor_name / entry.sub_path).make_preferred(); + + // Load the preset into the list of presets, save it to disk. + Preset &loaded = presets_collection->load_preset(file_path.string(), preset_name, std::move(config), false); + if (flags.has(LoadConfigBundleAttribute::LoadSystem)) { + loaded.is_system = true; + loaded.vendor = current_vendor_profile; + loaded.version = current_vendor_profile->config_version; + loaded.description = entry.description; + loaded.setting_id = entry.setting_id; + // Derive the preset setting_id on the fly when a profile ships without one, + // matching scripts/assign_vendor_setting_ids.py. Only instantiated presets + // carry an id; non-instantiated base profiles return earlier above. This never + // touches the per-user cloud-sync setting_id written into user .info files. + if (loaded.setting_id.empty() && entry.instantiation == "true") + loaded.setting_id = generate_preset_setting_id( + vendor_name, Preset::get_type_string(presets_collection->type()), preset_name); + loaded.filament_id = filament_id; + loaded.m_from_orca_filament_lib = is_from_lib; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << __LINE__ << ", " << loaded.name << " load filament_id: " << filament_id; + if (presets_collection->type() == Preset::TYPE_FILAMENT) { + if (filament_id.empty() && "Template" != vendor_name) { + ++m_errors; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": can not find filament_id for " << preset_name; + //throw ConfigurationError(format("can not find inherits %1% for %2%", inherits, preset_name)); + reason = "Can not find filament_id for " + preset_name; + return reason; + } + else { + filament_id_maps.emplace(preset_name, filament_id); + } + } + } + + // Derive the profile logical name aka alias from the preset name if the alias was not stated explicitely. + if (alias_name.empty()) { + size_t end_pos = preset_name.find_first_of("@"); + if (end_pos != std::string::npos) { + alias_name = preset_name.substr(0, end_pos); + if (renamed_from.empty()) + // Add the preset name with the '@' character removed into the "renamed_from" list. + renamed_from.emplace_back(alias_name + preset_name.substr(end_pos + 1)); + boost::trim_right(alias_name); + } + } + if (alias_name.empty()) + loaded.alias = preset_name; + else { + loaded.alias = std::move(alias_name); + filaments.set_printer_hold_alias(loaded.alias, loaded); + } + loaded.renamed_from = std::move(renamed_from); + if (! substitution_context.empty()) + substitutions.push_back({ + preset_name, presets_collection->type(), PresetConfigSubstitutions::Source::ConfigBundle, + std::string(), std::move(substitution_context.substitutions) }); + if (retain_configs == nullptr || retain_configs->count(preset_name) != 0) + config_maps.emplace(preset_name, loaded.config); + ++count; + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", got preset %1%, from %2%")%loaded.name %subfile; + return reason; +} + //BBS: Load a config bundle file from json std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_from_json( - const std::string &path, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle) + const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle) { // Enable substitutions for user config bundle, throw an exception when loading a system profile. ConfigSubstitutionContext substitution_context { compatibility_rule }; PresetsConfigSubstitutions substitutions; + // Errors already on this bundle when the load began; the cache stamp below + // counts only what this parse adds. + const int errors_at_entry = m_errors; //BBS: add config related logs - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, path %1%, compatibility_rule %2%")%path.c_str()%compatibility_rule; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, path %1%, compatibility_rule %2%")%dir.c_str()%compatibility_rule; if (flags.has(LoadConfigBundleAttribute::ResetUserProfile) || flags.has(LoadConfigBundleAttribute::LoadSystem)) // Reset this bundle, delete user profile files if SaveImported. this->reset(flags.has(LoadConfigBundleAttribute::SaveImported)); + // Orca: only a whole-vendor load has a cache — the vendor-only and filament-only + // scans want a slice of one. Validation reads the JSONs whatever is cached. + const boost::filesystem::path dir_path(dir); + const bool cacheable = flags.has(LoadConfigBundleAttribute::LoadSystem) && ! flags.has(LoadConfigBundleAttribute::LoadFilamentOnly); + if (cacheable && ! validation_mode && this->load_vendor_cache(dir_path, vendor_name, base_bundle)) { + size_t presets_loaded = 0; + for (const PresetCollection* coll : std::initializer_list<const PresetCollection*>{ + &this->prints, &this->sla_prints, &this->filaments, &this->sla_materials, &this->printers }) + presets_loaded += coll->m_presets.size() - coll->m_num_default_presets; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", %1% served from its preset cache, %2% presets")%vendor_name%presets_loaded; + return std::make_pair(std::move(substitutions), presets_loaded); + } + // 1) load the vroot json and construct the vendor profile VendorProfile vendor_profile(vendor_name); - std::string root_file = path + "/" + vendor_name + ".json"; + std::string root_file = dir + "/" + vendor_name + ".json"; std::vector<std::pair<std::string, std::string>> machine_model_subfiles; std::vector<std::pair<std::string, std::string>> process_subfiles; std::vector<std::pair<std::string, std::string>> filament_subfiles; std::vector<std::pair<std::string, std::string>> machine_subfiles; auto get_name_and_subpath = [this](json::iterator& it, std::vector<std::pair<std::string, std::string>>& subfile_map) { if (it.value().is_array()) { - for (auto iter1 = it.value().begin(); iter1 != it.value().end(); iter1++) { + size_t index = 0; + for (auto iter1 = it.value().begin(); iter1 != it.value().end(); iter1++, index++) { if (iter1.value().is_object()) { std::string name, subpath; for (auto iter2 = iter1.value().begin(); iter2 != iter1.value().end(); iter2++) { @@ -4803,7 +5345,10 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_ subfile_map.push_back(std::make_pair(name, subpath)); } else { ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": invalid type for " << iter1.key(); + // An array element has no key, and asking one for it throws + // nlohmann's invalid_iterator — not a parse_error, so it would + // escape the catch around this parse. Say where it is instead. + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": invalid type for " << it.key() << "[" << index << "]"; } } } else { @@ -4824,7 +5369,7 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_ if (! config_version) { ++m_errors; throw ConfigurationError((boost::format("vendor %1%'s config version: %2% invalid\nSuggest cleaning the directory %3% firstly") - % vendor_name % version_str % path).str()); + % vendor_name % version_str % dir).str()); } else { vendor_profile.config_version = std::move(*config_version); } @@ -4862,7 +5407,7 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_ catch(nlohmann::detail::parse_error &err) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": parse "<<root_file<<" got a nlohmann::detail::parse_error, reason = " << err.what(); throw ConfigurationError((boost::format("Failed loading configuration file %1%: %2%\nSuggest cleaning the directory %3% firstly") - %root_file %err.what() % path).str()); + %root_file %err.what() % dir).str()); //goto __error_process; } @@ -4875,7 +5420,7 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_ //2) paste the machine model for (auto& machine_model : machine_model_subfiles) { - std::string subfile = path + "/" + vendor_name + "/" + machine_model.second; + std::string subfile = dir + "/" + vendor_name + "/" + machine_model.second; VendorProfile::PrinterModel model; model.id = machine_model.first; try { @@ -4977,7 +5522,7 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_ catch(nlohmann::detail::parse_error &err) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": parse "<< subfile <<" got a nlohmann::detail::parse_error, reason = " << err.what(); throw ConfigurationError((boost::format("Failed loading configuration file %1%: %2%\nSuggest cleaning the directory %3% firstly") - %subfile %err.what() % path).str()); + %subfile %err.what() % dir).str()); } if (! model.id.empty() && ! model.variants.empty()) @@ -4986,7 +5531,6 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_ //insert the vendor profile this->vendors.emplace(vendor_name, vendor_profile); - const VendorProfile* current_vendor_profile = &this->vendors[vendor_name]; BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", loaded vendor profile, name %1%, id %2%, version %3%")%vendor_profile.name%vendor_profile.id%vendor_profile.config_version.to_string(); @@ -4997,123 +5541,65 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_ PresetCollection *presets = nullptr; size_t presets_loaded = 0; - auto parse_subfile = [this, path, vendor_name, presets_loaded, current_vendor_profile, base_bundle]( + // Parse one subfile into a source-form entry — everything the JSON states, + // nothing resolved. Loading the entry (load_vendor_preset) is the + // same code whether the entry was parsed just now or deserialized from the + // vendor's cache. + auto parse_subfile = [this, dir, vendor_name]( ConfigSubstitutionContext& substitution_context, - PresetsConfigSubstitutions& substitutions, - LoadConfigBundleAttributes& flags, - std::pair<std::string, std::string>& subfile_iter, - std::map<std::string, DynamicPrintConfig>& config_maps, - std::map<std::string, std::string>& filament_id_maps, - PresetCollection* presets_collection, - size_t& count, bool is_from_lib = false) -> std::string { + const std::pair<std::string, std::string>& subfile_iter, + CachedPreset& entry) -> std::string { - std::string subfile = path + "/" + vendor_name + "/" + subfile_iter.second; - // Load the print, filament or printer preset. - std::string preset_name; - DynamicPrintConfig config; - std::string alias_name, inherits, description, instantiation, setting_id, filament_id; - std::vector<std::string> renamed_from; - const DynamicPrintConfig* default_config = nullptr; - std::string reason; + std::string subfile = dir + "/" + vendor_name + "/" + subfile_iter.second; + std::string reason; try { std::map<std::string, std::string> key_values; substitution_context.substitutions.clear(); //parse the json elements - DynamicPrintConfig config_src; - std::string _renamed_from_str; - config_src.load_from_json(subfile, substitution_context, false, key_values, reason); + entry.sub_path = subfile_iter.second; + entry.config_src.load_from_json(subfile, substitution_context, false, key_values, reason); if (!reason.empty()) { ++m_errors; BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": load config file "<<subfile<<" Failed!"; return reason; } - preset_name = key_values[BBL_JSON_KEY_NAME]; - description = key_values[BBL_JSON_KEY_DESCRIPTION]; + entry.name = key_values[BBL_JSON_KEY_NAME]; + entry.description = key_values[BBL_JSON_KEY_DESCRIPTION]; if(key_values.find(BBL_JSON_KEY_INSTANTIATION) == key_values.end()) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Missing instantiation attribute for " << preset_name; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Missing instantiation attribute for " << entry.name; ++m_errors; } - instantiation = key_values[BBL_JSON_KEY_INSTANTIATION]; - if(instantiation != "false" && instantiation != "true"){ - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Missing instantiation attribute for " << preset_name; + entry.instantiation = key_values[BBL_JSON_KEY_INSTANTIATION]; + if(entry.instantiation != "false" && entry.instantiation != "true"){ + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Missing instantiation attribute for " << entry.name; ++m_errors; } auto setting_it = key_values.find(BBL_JSON_KEY_SETTING_ID); if (setting_it != key_values.end()) - setting_id = setting_it->second; + entry.setting_id = setting_it->second; auto filament_it = key_values.find(BBL_JSON_KEY_FILAMENT_ID); if (filament_it != key_values.end()) - filament_id = filament_it->second; - //check whether it inherits other preset or not + entry.filament_id = filament_it->second; auto it1 = key_values.find(BBL_JSON_KEY_INHERITS); if (it1 != key_values.end()) { - inherits = it1->second; - auto it2 = config_maps.find(inherits); - default_config = nullptr; - if (it2 != config_maps.end()) - default_config = &(it2->second); - if(default_config == nullptr && base_bundle != nullptr) { - auto base_it2 = base_bundle->m_config_maps.find(inherits); - if (base_it2 != base_bundle->m_config_maps.end()) - default_config = &(base_it2->second); - } - if (default_config != nullptr) { - if (filament_id.empty() && (presets_collection->type() == Preset::TYPE_FILAMENT)) { - auto filament_id_map_iter = filament_id_maps.find(inherits); - if (filament_id_map_iter != filament_id_maps.end()) { - filament_id = filament_id_map_iter->second; - } - if (filament_id.empty() && base_bundle != nullptr) { - auto filament_id_map_iter = base_bundle->m_filament_id_maps.find(inherits); - if (filament_id_map_iter != base_bundle->m_filament_id_maps.end()) { - filament_id = filament_id_map_iter->second; - } - } - } - } - else { + entry.inherits = it1->second; + // An `inherits` key naming nothing can never resolve; fail it + // here so install can key off the empty string as "no inherits". + if (entry.inherits.empty()) { ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not find inherits " << inherits << " for " << preset_name; - // throw ConfigurationError(format("can not find inherits %1% for %2%", inherits, preset_name)); - reason = "Can not find inherits: " + inherits; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not find inherits " << entry.inherits << " for " << entry.name; + reason = "Can not find inherits: " + entry.inherits; return reason; } } - else { - if (presets_collection->type() == Preset::TYPE_PRINTER) - default_config = &presets_collection->default_preset_for(config_src).config; - else - default_config = &presets_collection->default_preset().config; - } - config = *default_config; - config.apply(config_src); - extend_default_config_length(config, true, *default_config); - if (instantiation == "false" && "Template" != vendor_name) { - // Report configuration fields, which are misplaced into a wrong group. - std::string incorrect_keys = Preset::remove_invalid_keys(config, *default_config); - if (!incorrect_keys.empty()) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": The config " << subfile << " contains incorrect keys: " << incorrect_keys - << ", which were removed"; - } - - config_maps.emplace(preset_name, std::move(config)); - if ((presets_collection->type() == Preset::TYPE_FILAMENT) && (!filament_id.empty())) - filament_id_maps.emplace(preset_name, filament_id); - return reason; - } - if (config.has("alias")) - alias_name = (dynamic_cast<const ConfigOptionString *>(config.option("alias")))->value; - if (key_values.find(ORCA_JSON_KEY_RENAMED_FROM) != key_values.end()) { - if (!unescape_strings_cstyle(key_values[ORCA_JSON_KEY_RENAMED_FROM], renamed_from)) { - BOOST_LOG_TRIVIAL(error) << "Error in a Config \"" << path << "\": The preset \"" << preset_name + if (!unescape_strings_cstyle(key_values[ORCA_JSON_KEY_RENAMED_FROM], entry.renamed_from)) { + BOOST_LOG_TRIVIAL(error) << "Error in a Config \"" << dir << "\": The preset \"" << entry.name << "\" contains invalid \"renamed_from\" key, which is being ignored."; } } - Preset::normalize(config); } catch(nlohmann::detail::parse_error &err) { ++m_errors; @@ -5121,195 +5607,60 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_ reason = std::string("json parse error") + err.what(); return reason; } - - // Report configuration fields, which are misplaced into a wrong group. - std::string incorrect_keys = Preset::remove_invalid_keys(config, *default_config); - if (!incorrect_keys.empty()) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": The config " << subfile << " contains incorrect keys: " << incorrect_keys - << ", which were removed"; - } - - if (presets_collection->type() == Preset::TYPE_PRINTER) { - // Filter out printer presets, which are not mentioned in the vendor profile. - // These presets are considered not installed. - auto printer_model = config.opt_string("printer_model"); - if (printer_model.empty()) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" defines no printer model, it will be ignored."; - reason = std::string("can not find printer_model"); - return reason; - } - auto printer_variant = config.opt_string("printer_variant"); - if (printer_variant.empty()) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" defines no printer variant, it will be ignored."; - reason = std::string("can not find printer_variant"); - return reason; - } - auto it_model = std::find_if(current_vendor_profile->models.cbegin(), current_vendor_profile->models.cend(), - [&](const VendorProfile::PrinterModel &m) { return m.id == printer_model; } - ); - if (it_model == current_vendor_profile->models.end()) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" defines invalid printer model \"" << printer_model << "\", it will be ignored."; - reason = std::string("can not find printer model in vendor profile"); - return reason; - } - auto it_variant = it_model->variant(printer_variant); - if (it_variant == nullptr) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" defines invalid printer variant \"" << printer_variant << "\", it will be ignored."; - reason = std::string("can not find printer_variant in vendor profile"); - return reason; - } - // An instantiation printer profile's nozzle_diameter must match the numeric (diameter) - // prefix of its printer_variant: "0.4" -> {0.4}, "0.8HF" -> {0.8} (a trailing - // non-numeric suffix such as "HF"/"HS" distinguishes a hardware sub-variant and is - // ignored here), and for multi-nozzle printers "0.4+0.6" -> {0.4, 0.6}. - // Note: a variant may legitimately repeat across presets of the same model (e.g. speed - // modes, IDEX copy/mirror, or different control boards), so only the diameter is - // validated, not variant uniqueness. Validation-only so the app keeps loading existing - // profiles unchanged. - if (validation_mode && instantiation == "true") { - const auto *nd = config.option<ConfigOptionFloats>("nozzle_diameter"); - std::set<double> nozzles, variant_nozzles; - if (nd != nullptr) - nozzles.insert(nd->values.begin(), nd->values.end()); - std::vector<std::string> variant_tokens; - boost::algorithm::split(variant_tokens, printer_variant, boost::algorithm::is_any_of("+")); - bool variant_ok = true; // printer_variant is already guaranteed non-empty above - for (const std::string &tok : variant_tokens) { - size_t consumed = 0; - double d = string_to_double_decimal_point(tok, &consumed); - // Require a leading numeric diameter; a trailing suffix (e.g. "HF") is allowed. - if (consumed == 0) { variant_ok = false; break; } - variant_nozzles.insert(d); - } - if (!variant_ok || variant_nozzles != nozzles) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" has printer_variant \"" << printer_variant << - "\" that does not match its nozzle_diameter \"" << (nd ? nd->serialize() : std::string()) << "\". " - "printer_variant must begin with the nozzle diameter, optionally followed by a non-numeric suffix " - "(e.g. \"0.4\", \"0.8HF\"); for multi-nozzle printers, join the per-nozzle diameters with \"+\" in " - "nozzle order (e.g. \"0.4+0.6\")."; - } - } - } - const Preset *preset_existing = presets_collection->find_preset(preset_name, false); - if (preset_existing != nullptr) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << - preset_name << "\" has already been loaded from another Config Bundle."; - reason = std::string("duplicated defines"); - return reason; - } - - auto file_path = (boost::filesystem::path(data_dir()) /PRESET_SYSTEM_DIR/ vendor_name / subfile_iter.second).make_preferred(); - if(validation_mode) - file_path = (boost::filesystem::path(data_dir()) / vendor_name / subfile_iter.second).make_preferred(); - - // Load the preset into the list of presets, save it to disk. - Preset &loaded = presets_collection->load_preset(file_path.string(), preset_name, std::move(config), false); - if (flags.has(LoadConfigBundleAttribute::LoadSystem)) { - loaded.is_system = true; - loaded.vendor = current_vendor_profile; - loaded.version = current_vendor_profile->config_version; - loaded.description = description; - loaded.setting_id = setting_id; - // Derive the preset setting_id on the fly when a profile ships without one, - // matching scripts/assign_vendor_setting_ids.py. Only instantiated presets - // carry an id; non-instantiated base profiles return earlier above. This never - // touches the per-user cloud-sync setting_id written into user .info files. - if (loaded.setting_id.empty() && instantiation == "true") - loaded.setting_id = generate_preset_setting_id( - vendor_name, Preset::get_type_string(presets_collection->type()), preset_name); - loaded.filament_id = filament_id; - loaded.m_from_orca_filament_lib = is_from_lib; - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << __LINE__ << ", " << loaded.name << " load filament_id: " << filament_id; - if (presets_collection->type() == Preset::TYPE_FILAMENT) { - if (filament_id.empty() && "Template" != vendor_name) { - ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": can not find filament_id for " << preset_name; - //throw ConfigurationError(format("can not find inherits %1% for %2%", inherits, preset_name)); - reason = "Can not find filament_id for " + preset_name; - return reason; - } - else { - filament_id_maps.emplace(preset_name, filament_id); - } - } - } - - // Derive the profile logical name aka alias from the preset name if the alias was not stated explicitely. - if (alias_name.empty()) { - size_t end_pos = preset_name.find_first_of("@"); - if (end_pos != std::string::npos) { - alias_name = preset_name.substr(0, end_pos); - if (renamed_from.empty()) - // Add the preset name with the '@' character removed into the "renamed_from" list. - renamed_from.emplace_back(alias_name + preset_name.substr(end_pos + 1)); - boost::trim_right(alias_name); - } - } - if (alias_name.empty()) - loaded.alias = preset_name; - else { - loaded.alias = std::move(alias_name); - filaments.set_printer_hold_alias(loaded.alias, loaded); - } - loaded.renamed_from = std::move(renamed_from); - if (! substitution_context.empty()) - substitutions.push_back({ - preset_name, presets_collection->type(), PresetConfigSubstitutions::Source::ConfigBundle, - std::string(), std::move(substitution_context.substitutions) }); - config_maps.emplace(preset_name, loaded.config); - ++count; - //BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", got preset %1%, from %2%")%loaded.name %subfile; return reason; }; std::map<std::string, DynamicPrintConfig> configs; std::map<std::string, std::string> filament_id_maps; + // Orca: whether to (re)write the vendor's cache after this parse, leaving it + // in step with the profile so the next run reads it instead. It is written + // where the vendor was looked for, even when the profile came from resources, + // and stamped with the version that profile claims — a profile without one + // cannot be judged for staleness later, and a cache nothing can invalidate is + // worse than none. + const bool will_cache = cacheable && m_generate_vendor_caches && vendor_profile.config_version.valid(); + VendorCacheData cache_data; + // Errors added by install are counted apart: a cache load runs install again, + // so the parse_errors stamped into the cache must hold only what a cache load + // will not recount. + int install_errors = 0; + auto load_subfiles = [&](std::vector<std::pair<std::string, std::string>>& subfiles, + std::vector<CachedPreset>& entries, const char* kind, bool is_from_lib = false) { + configs.clear(); + filament_id_maps.clear(); + for (auto& subfile : subfiles) { + CachedPreset entry; + std::string reason = parse_subfile(substitution_context, subfile, entry); + if (reason.empty()) { + const int errors_before_install = m_errors; + reason = load_vendor_preset(entry, dir, vendor_name, base_bundle, flags, + substitution_context, substitutions, configs, filament_id_maps, presets, + presets_loaded, is_from_lib); + install_errors += m_errors - errors_before_install; + } + if (!reason.empty()) { + ++m_errors; + //parse error + std::string subfile_path = dir + "/" + vendor_name + "/" + subfile.second; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", got error when parse %1% setting from %2%") % kind % subfile_path; + throw ConfigurationError((boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % subfile_path % dir).str()); + } + if (will_cache) + entries.emplace_back(std::move(entry)); + } + }; + + // The section order below — process, filaments (with the ORCA-lib map copy), + // printers — is mirrored by load_vendor_cache's install loops; keep the two + // in lockstep. //3.1) paste the process presets = &this->prints; - configs.clear(); - filament_id_maps.clear(); - for (auto& subfile : process_subfiles) - { - std::string reason = parse_subfile(substitution_context, substitutions, flags, subfile, configs, filament_id_maps, presets, presets_loaded); - if (!reason.empty()) { - ++m_errors; - //parse error - std::string subfile_path = path + "/" + vendor_name + "/" + subfile.second; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", got error when parse process setting from %1%") % subfile_path; - throw ConfigurationError((boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % subfile_path % path).str()); - } - } + load_subfiles(process_subfiles, cache_data.process_entries, "process"); //3.2) paste the filaments presets = &this->filaments; - configs.clear(); - filament_id_maps.clear(); const auto is_orca_lib = vendor_name == ORCA_FILAMENT_LIBRARY; - for (auto& subfile : filament_subfiles) - { - std::string reason = parse_subfile(substitution_context, substitutions, flags, subfile, configs, filament_id_maps, presets, - presets_loaded, is_orca_lib); - if (!reason.empty()) { - ++m_errors; - //parse error - std::string subfile_path = path + "/" + vendor_name + "/" + subfile.second; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", got error when parse filament setting from %1%") % subfile_path; - throw ConfigurationError((boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % subfile_path % path).str()); - } - } + load_subfiles(filament_subfiles, cache_data.filament_entries, "filament", is_orca_lib); if (is_orca_lib) { m_config_maps = configs; m_filament_id_maps = filament_id_maps; @@ -5317,18 +5668,16 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_ //3.3) paste the printers presets = &this->printers; - configs.clear(); - filament_id_maps.clear(); - for (auto& subfile : machine_subfiles) - { - std::string reason = parse_subfile(substitution_context, substitutions, flags, subfile, configs, filament_id_maps, presets, presets_loaded); - if (!reason.empty()) { - ++m_errors; - //parse error - std::string subfile_path = path + "/" + vendor_name + "/" + subfile.second; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", got error when parse printer setting from %1%") % subfile_path; - throw ConfigurationError((boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % subfile_path % path).str()); - } + load_subfiles(machine_subfiles, cache_data.machine_entries, "printer"); + + if (will_cache) { + // Clamped: the count is a difference of three tallies, and a stamp that + // wrapped would be added to every future load of this vendor. + cache_data.parse_errors = uint64_t(std::max(0, m_errors - errors_at_entry - install_errors)); + cache_data.vendors = this->vendors; + if (! VendorCacheFile::save((dir_path / (vendor_name + ".opc")).string(), vendor_name, + vendor_profile.config_version.to_string(), cache_data)) + BOOST_LOG_TRIVIAL(warning) << "PresetBundle: failed to save vendor cache for " << vendor_name; } //BBS: add config related logs @@ -5378,7 +5727,7 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam f_multiplier.resize(nozzle_nums, 1.f); } - if ( (num_filaments * num_filaments) != size_t(old_matrix.size() / old_nozzle_nums) ) { + if (old_matrix.size() != num_filaments * num_filaments * nozzle_nums) { // First verify if purging volumes presets for each extruder matches number of extruders std::vector<double>& filaments = this->project_config.option<ConfigOptionFloats>("flush_volumes_vector")->values; while (filaments.size() < 2* num_filaments) { @@ -5953,4 +6302,94 @@ bool BundleMetadata::save_to_json(const std::string& path) const return false; } } +// ---- Per-vendor preset cache: install into this bundle ------------------- +// The file format itself lives in PresetCacheFormat.cpp (VendorCacheFile). + +bool PresetBundle::load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const PresetBundle* base_bundle) +{ + // A vendor is loaded from where it is installed and nowhere else; resources + // reaches the app by being installed into `dir` first. The cache there is + // judged against the profile beside it — or, where the cache is the whole + // of the installation, against nothing, since nothing on disk can then be + // newer than it. That state is Semver::inf(), which no real profile carries. + const boost::filesystem::path profile = dir / (vendor_name + ".json"); + const Semver version = boost::filesystem::exists(profile) ? get_version_from_json(profile.string()) + : Semver::inf(); + return this->load_vendor_cache((dir / (vendor_name + ".opc")).string(), vendor_name, version, base_bundle); +} + +bool PresetBundle::load_vendor_cache(const std::string& cache_path, const std::string& expected_vendor_name, + const Semver& expected_vendor_version, const PresetBundle* base_bundle) +{ + // What this bundle had counted before the cache was tried. The caller + // measures its own parse against this same baseline, so a rejection must + // put it back rather than reset it to zero. + const int errors_at_entry = this->m_errors; + // Read and validated before this bundle is touched: a rejected file leaves + // no state to roll back. + VendorCacheData data; + if (! VendorCacheFile::load(cache_path, expected_vendor_name, expected_vendor_version, data)) + return false; + try { + const std::string& vendor_name = expected_vendor_name; // VendorCacheFile::load checked they match + this->vendors = std::move(data.vendors); + + // What the parse counted before install took over; install recounts its + // own below, so m_errors comes out as a JSON parse would leave it. + m_errors += int(data.parse_errors); + + // Install the entries exactly as load_vendor_configs_from_json installs + // them straight after parsing — same code, same order. The substitution + // context stays empty (the entries were substituted when they were + // parsed), so no substitutions are reported, as before. + ConfigSubstitutionContext substitution_context { ForwardCompatibilitySubstitutionRule::EnableSilent }; + PresetsConfigSubstitutions substitutions; + std::map<std::string, DynamicPrintConfig> configs; + std::map<std::string, std::string> filament_id_maps; + const std::string path = boost::filesystem::path(cache_path).parent_path().string(); + size_t count = 0; + auto install_entries = [&](const std::vector<CachedPreset>& entries, PresetCollection* presets, bool is_from_lib) { + configs.clear(); + filament_id_maps.clear(); + // Only configs of presets that other entries inherit are ever looked + // up again; registering just those skips one full config copy for + // every leaf preset. The library's filaments are all retained — they + // become the m_config_maps other vendors resolve against. + std::set<std::string> inherited; + for (const CachedPreset& entry : entries) + if (! entry.inherits.empty()) + inherited.insert(entry.inherits); + const std::set<std::string>* retain_configs = is_from_lib ? nullptr : &inherited; + for (const CachedPreset& entry : entries) { + const std::string reason = load_vendor_preset(entry, path, vendor_name, + base_bundle, LoadConfigBundleAttribute::LoadSystem, substitution_context, substitutions, + configs, filament_id_maps, presets, count, is_from_lib, retain_configs); + if (! reason.empty()) + throw std::runtime_error("entry " + entry.name + " failed to install: " + reason); + } + }; + install_entries(data.process_entries, &this->prints, false); + const bool is_orca_lib = vendor_name == ORCA_FILAMENT_LIBRARY; + install_entries(data.filament_entries, &this->filaments, is_orca_lib); + if (is_orca_lib) { + m_config_maps = configs; + m_filament_id_maps = filament_id_maps; + } + install_entries(data.machine_entries, &this->printers, false); + return true; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "PresetBundle: rejecting vendor cache " << cache_path << ": " << e.what(); + // Restore a clean state so the caller can fall back to the JSON parse. + this->reset(false); + this->vendors.clear(); + this->m_config_maps.clear(); + this->m_filament_id_maps.clear(); + this->m_errors = errors_at_entry; + // A failure partway through installing may have left presets in some + // collections with hold aliases already registered. + this->clear_printer_hold_aliases(); + return false; + } +} + } // namespace Slic3r diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 685687975b..6e7e07b26e 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -2,10 +2,12 @@ #define slic3r_PresetBundle_hpp_ #include "Preset.hpp" +#include "PresetCacheFormat.hpp" #include "AppConfig.hpp" #include "enum_bitmask.hpp" #include <memory> +#include <set> #include <shared_mutex> #include <unordered_map> #include <optional> @@ -170,6 +172,31 @@ struct PresetBundleMetadata class PresetBundle { public: + // ---- Per-vendor preset cache -------------------------------------------- + // One cache file per vendor (plus the Orca filament library), stamped with + // the vendor's own profile version rather than a directory scan. The bytes + // on disk are VendorCacheFile's business (PresetCacheFormat.hpp); what + // lives here is how a cache's contents install into a bundle. + + // The cache is not something a caller loads from: a vendor is loaded with + // load_vendor_configs_from_json, which comes from the cache whenever one covers + // it. What is public here is what the cache's own tests drive directly. + + // Load a per-vendor cache into this bundle by installing its entries, with + // base_bundle's filament library as the inheritance base. Rejects (returns + // false, with this bundle left clean) unless VendorCacheFile::load accepts + // the file — see its contract for the version and identity checks — and + // every entry installs. Options this build no longer defines are dropped, + // not fatal — the payload names its own keys. + bool load_vendor_cache(const std::string& cache_path, const std::string& expected_vendor_name, + const Semver& expected_vendor_version, const PresetBundle* base_bundle = nullptr); + + // Enable writing a per-vendor cache after a JSON parse (off by default). Cache + // content is pure parse output, so the guard is policy, not correctness: only + // the deliberate generators (load_system_presets_from_json, the cache build + // tool) write files, not every incidental load a dialog performs. + void set_generate_vendor_caches(bool enable) { m_generate_vendor_caches = enable; } + static DynamicPrintConfig construct_full_config(Preset &in_printer_preset, Preset &in_print_preset, const DynamicPrintConfig &project_config, @@ -299,8 +326,9 @@ public: // Export selections (current print, current filaments, current printer) into config.ini void export_selections(AppConfig &config); - // BBS - void set_num_filaments(unsigned int n, std::vector<std::string> new_colors); + // n is the total slot count, and growth appends at the raw tail - which is where the mixed + // slots live. A caller adding physical filaments has to add num_mixed_filaments() on top and + // then move the new slots ahead of the mixed tail, as Sidebar::add_custom_filament does. void set_num_filaments(unsigned int n, std::string new_col = ""); void update_num_filaments(unsigned int to_del_flament_id); @@ -444,8 +472,12 @@ public: /*std::pair<PresetsConfigSubstitutions, size_t> load_configbundle( const std::string &path, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule);*/ //Orca: load config bundle from json, pass the base bundle to support cross vendor inheritance + // Orca: `dir` is where the vendor is looked for — its own directory, whether or + // not the profile JSONs are still there. A whole-vendor load comes from the + // vendor's preset cache whenever one covers the profile on disk, and is parsed + // from the JSONs in `dir` only when none does. Nothing here reads resources. std::pair<PresetsConfigSubstitutions, size_t> load_vendor_configs_from_json( - const std::string &path, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr); + const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr); // Export a config bundle file containing all the presets and the names of the active presets. //void export_configbundle(const std::string &path, bool export_system_settings = false, bool export_physical_printers = false); @@ -466,6 +498,14 @@ public: // Read out the number of extruders from an active printer preset, // update size and content of filament_presets. void update_multi_material_filament_presets(size_t to_delete_filament_id = size_t(-1)); + // Mixed-color filament slots: virtual slots realized from 2-3 physical filaments. + bool is_mixed_filament(size_t idx) const; + std::vector<size_t> physical_filament_config_indices() const; + // How many slots are mixed. They sit at the tail of the filament list and have no nozzle of + // their own, so any resize driven by the printer's extruder count has to add this on top. + size_t num_mixed_filaments() const; + // How many slots hold a real filament, i.e. everything ahead of the mixed tail. + size_t num_physical_filaments() const; void on_extruders_count_changed(int extruder_count); @@ -517,11 +557,49 @@ public: // Orca: for validation only. bool has_errors(bool check_duplicate_filament_subtypes = false) const; + // Errors the last load recorded. What the cache's error accounting promises — + // a cache-served vendor reports what its parse would — is pinned against this. + int error_count() const { return m_errors; } + // Orca: for validation only. Flag any system preset whose inherits / compatible_printers / // compatible_prints references a deleted (unknown) or renamed (old) preset name. bool check_preset_references() const; + // Merge one vendor's presets with the other vendor's presets, report duplicates. + // Public so per-vendor-cache consumers (e.g. the setup wizard) can assemble a + // bundle out of several per-vendor caches loaded into separate PresetBundle instances. + std::vector<std::string> merge_presets(PresetBundle &&other); + private: + // Load one vendor from the preset cache installed in `dir`, judged against + // the vendor profile there. False, with this bundle left clean, when there + // is no usable cache and the vendor has to be parsed. This is how + // load_vendor_configs_from_json reads a cache. + bool load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const PresetBundle* base_bundle); + + // Load one source-form preset entry into this bundle: resolve `inherits`, + // flatten, validate and register the preset. Returns the reason loading + // failed, empty on success. See the definition for the sharing contract + // between the JSON parse and the cache load. + // retain_configs, when non-null, names the only presets registered into + // config_maps (a full config copy each). The cache load passes the names its + // entries inherit — the only ones ever looked up again; the JSON parse + // retains all, not knowing what later subfiles inherit. + std::string load_vendor_preset(const CachedPreset& entry, + const std::string& path, const std::string& vendor_name, + const PresetBundle* base_bundle, + LoadConfigBundleAttributes flags, + ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions, + std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, std::string>& filament_id_maps, + PresetCollection* presets_collection, size_t& count, bool is_from_lib, + const std::set<std::string>* retain_configs = nullptr); + + // Clear every collection's m_printer_hold_alias, which reset() leaves alone. + void clear_printer_hold_aliases(); + + // Whether to (re)write a per-vendor cache after a JSON parse. + bool m_generate_vendor_caches { false }; + // Orca: validation only - flag any printer with two or more compatible // filament presets sharing one filament_id (ambiguous AMS subtype match). bool check_duplicate_filament_subtypes() const; @@ -529,8 +607,6 @@ private: //std::pair<PresetsConfigSubstitutions, std::string> load_system_presets(ForwardCompatibilitySubstitutionRule compatibility_rule); //BBS: add json related logic std::pair<PresetsConfigSubstitutions, std::string> load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule); - // Merge one vendor's presets with the other vendor's presets, report duplicates. - std::vector<std::string> merge_presets(PresetBundle &&other); // Update the multicolor information for filaments. void update_filament_multi_color(); // Update renamed_from and alias maps of system profiles. diff --git a/src/libslic3r/PresetCacheFormat.cpp b/src/libslic3r/PresetCacheFormat.cpp new file mode 100644 index 0000000000..accebba7b4 --- /dev/null +++ b/src/libslic3r/PresetCacheFormat.cpp @@ -0,0 +1,588 @@ +#include "libslic3r/PresetCacheFormat.hpp" + +#include <algorithm> +#include <memory> +#include <sstream> +#include <stdexcept> +#include <utility> + +#include <boost/crc.hpp> +#include <boost/filesystem.hpp> +#include <boost/iostreams/device/array.hpp> +#include <boost/iostreams/stream.hpp> +#include <boost/log/trivial.hpp> +#include <boost/nowide/fstream.hpp> +#include <cereal/types/map.hpp> +#include <cereal/types/set.hpp> + +#include "libslic3r/Utils.hpp" + +namespace Slic3r { + +CacheDictionary::CacheDictionary() +{ + // ENUM_UNNAMED is index 0 and always the empty name. + m_enum_values.emplace_back(); +} + +// The ints an enum option holds — one for a coEnum, the whole vector for coEnums. +static std::vector<int> enum_ints(const ConfigOptionDef& def, const ConfigOption* opt) +{ + if (def.type == coEnum) + return { opt->getInt() }; + return static_cast<const ConfigOptionInts*>(opt)->values; +} + +// The name this build gives one of those ints, empty where it has none — a +// nullable option's nil, or a definition carrying no enum_keys_map. Enums are +// written by name so a build that reorders an enum's values still reads it right. +static std::string enum_name_of(const ConfigOptionDef& def, int value) +{ + if (def.enum_keys_map != nullptr) + for (const auto& kvp : *def.enum_keys_map) + if (kvp.second == value) + return kvp.first; + return {}; +} + +void CacheDictionary::collect(const DynamicPrintConfig& config) +{ + for (auto it = config.cbegin(); it != config.cend(); ++ it) { + const ConfigOptionDef* def = print_config_def.get(it->first); + if (def == nullptr) + continue; // save_config does not write it either + if (m_key_index.try_emplace(it->first, uint16_t(m_keys.size())).second) { + m_keys.push_back(it->first); + m_types.push_back(uint16_t(def->type)); + } + if (def->type != coEnum && def->type != coEnums) + continue; + for (int value : enum_ints(*def, it->second.get())) { + std::string name = enum_name_of(*def, value); + if (! name.empty() && m_enum_index.try_emplace(name, uint16_t(m_enum_values.size())).second) + m_enum_values.push_back(std::move(name)); + } + } +} + +uint16_t CacheDictionary::key_index(const t_config_option_key& key) const +{ + auto it = m_key_index.find(key); + if (it == m_key_index.end()) + throw std::runtime_error("preset cache: option " + key + " was never collected into the dictionary"); + return it->second; +} + +uint16_t CacheDictionary::enum_index(const std::string& name) const +{ + if (name.empty()) + return ENUM_UNNAMED; + auto it = m_enum_index.find(name); + return it == m_enum_index.end() ? ENUM_UNNAMED : it->second; +} + +void CacheDictionary::save(cereal::BinaryOutputArchive& ar) const +{ + // Checked here rather than left to the caller: an index that wrapped would + // be written silently, and nothing downstream could tell. + if (m_keys.size() > MAX_ENTRIES || m_enum_values.size() > MAX_ENTRIES) + throw std::runtime_error("preset cache: the option dictionary outgrew the uint16 it is indexed with"); + ar(m_keys, m_types, m_enum_values); +} + +void CacheDictionary::load(cereal::BinaryInputArchive& ar) +{ + ar(m_keys, m_types, m_enum_values); + if (m_keys.size() != m_types.size()) + throw std::runtime_error("preset cache: dictionary key and type tables differ in length"); + if (m_keys.size() > MAX_ENTRIES || m_enum_values.size() > MAX_ENTRIES) + throw std::runtime_error("preset cache: dictionary is larger than the uint16 it is indexed with"); + if (m_enum_values.empty() || ! m_enum_values.front().empty()) + throw std::runtime_error("preset cache: dictionary is missing its unnamed-enum slot"); + // Resolved once per file: every option read after this is a vector index. + m_defs.resize(m_keys.size()); + for (size_t i = 0; i < m_keys.size(); ++ i) { + const ConfigOptionDef* def = print_config_def.get(m_keys[i]); + m_defs[i] = (def != nullptr && uint16_t(def->type) == m_types[i]) ? def : nullptr; + } +} + +// ---- one config ----------------------------------------------------------- + +static void save_enum_option(cereal::BinaryOutputArchive& ar, const ConfigOptionDef& def, + const ConfigOption* opt, const CacheDictionary& dict) +{ + const std::vector<int> values = enum_ints(def, opt); + ar(uint32_t(values.size())); + for (int value : values) { + const uint16_t idx = dict.enum_index(enum_name_of(def, value)); + ar(idx); + if (idx == CacheDictionary::ENUM_UNNAMED) + ar(int32_t(value)); + } +} + +// `config` may be null, in which case the option is read and dropped. +static void load_enum_option(cereal::BinaryInputArchive& ar, ConfigOptionType type, + const ConfigOptionDef* def, DynamicPrintConfig* config, + const CacheDictionary& dict) +{ + uint32_t cnt = 0; + ar(cnt); + if (type == coEnum && cnt != 1) + throw std::runtime_error("preset cache: a scalar enum carrying more than one value"); + // Every element is read whatever happens, so the stream stays in sync and + // whatever follows this option still loads. + bool usable = def != nullptr && config != nullptr; + std::vector<int> values; + values.reserve(cnt); + for (uint32_t i = 0; i < cnt; ++ i) { + uint16_t idx = 0; + ar(idx); + if (! dict.valid_enum_index(idx)) + throw std::runtime_error("preset cache: enum value index past the end of the dictionary"); + if (idx == CacheDictionary::ENUM_UNNAMED) { + // An int the writer could not name — a nil, or an option whose + // definition carried no enum_keys_map. It travels verbatim. + int32_t raw = 0; + ar(raw); + values.push_back(int(raw)); + continue; + } + if (! usable) + continue; // the index above was this element's whole payload + if (def->enum_keys_map == nullptr) { + usable = false; // this build no longer maps this option's names + continue; + } + const auto it = def->enum_keys_map->find(dict.enum_name_at(idx)); + if (it == def->enum_keys_map->end()) { + usable = false; // a value this build dropped: the option goes with it + continue; + } + values.push_back(it->second); + } + if (! usable) + return; + if (type == coEnum) { + config->set_key_value(def->opt_key, new ConfigOptionEnumGeneric(def->enum_keys_map, values.front())); + } else { + auto* opt = def->nullable ? static_cast<ConfigOptionInts*>(new ConfigOptionEnumsGenericNullable(def->enum_keys_map)) + : static_cast<ConfigOptionInts*>(new ConfigOptionEnumsGeneric(def->enum_keys_map)); + opt->values = std::move(values); + config->set_key_value(def->opt_key, opt); + } +} + +void save_config(cereal::BinaryOutputArchive& ar, const DynamicPrintConfig& config, const CacheDictionary& dict) +{ + struct Written { uint16_t idx; const ConfigOptionDef* def; const ConfigOption* opt; }; + std::vector<Written> written; + written.reserve(config.size()); + for (auto it = config.cbegin(); it != config.cend(); ++ it) + if (const ConfigOptionDef* def = print_config_def.get(it->first)) + written.push_back({ dict.key_index(it->first), def, it->second.get() }); + + ar(uint32_t(written.size())); + for (const Written& w : written) { + ar(w.idx); + if (w.def->type == coEnum || w.def->type == coEnums) + save_enum_option(ar, *w.def, w.opt, dict); + else + w.def->save_option_to_archive(ar, w.opt); + } +} + +// `config` null means: read everything, keep nothing. +static void read_config(cereal::BinaryInputArchive& ar, DynamicPrintConfig* config, const CacheDictionary& dict) +{ + uint32_t cnt = 0; + ar(cnt); + if (config != nullptr) + config->clear(); + // Reused across the loop: constructing a ConfigOptionDef per dropped option + // would allocate its strings and vectors for nothing. + ConfigOptionDef scratch; + for (uint32_t i = 0; i < cnt; ++ i) { + uint16_t idx = 0; + ar(idx); + if (! dict.valid_key_index(idx)) + throw std::runtime_error("preset cache: option index past the end of the dictionary"); + const ConfigOptionType type = dict.type_at(idx); + const ConfigOptionDef* def = dict.def_at(idx); + if (type == coEnum || type == coEnums) { + load_enum_option(ar, type, def, config, dict); + } else if (def != nullptr && config != nullptr) { + config->set_key_value(def->opt_key, def->load_option_from_archive(ar)); + } else { + // Read by the type the writer recorded, then drop: the same outcome + // a JSON profile gets for an option this build no longer has. + scratch.type = type; + std::unique_ptr<ConfigOption> discard(scratch.load_option_from_archive(ar)); + } + } +} + +void load_config(cereal::BinaryInputArchive& ar, DynamicPrintConfig& config, const CacheDictionary& dict) +{ + read_config(ar, &config, dict); +} + +void skip_config(cereal::BinaryInputArchive& ar, const CacheDictionary& dict) +{ + read_config(ar, nullptr, dict); +} + +// ---- The per-vendor cache file (<vendor>.opc) ----------------------------- + +namespace { + +#pragma pack(push, 1) +struct CacheFileHeader { + uint32_t magic; + uint32_t version; + uint64_t data_size; + uint32_t crc32; +}; +#pragma pack(pop) +static_assert(sizeof(CacheFileHeader) == 20, "CacheFileHeader must be 20 bytes"); + +constexpr uint32_t CACHE_MAGIC = 0x4F52435A; // "ORCZ" +// Bump when the wire format changes in a way the payload cannot describe +// itself out of: reordering, removing or retyping a field of a hand-written +// serialize() (VendorProfile and its nested types, CachedPreset via +// save_entries below), or a change to the cache's own layout or the +// meaning of its stamps. Option-schema drift is NOT such a change — the +// dictionary handles it, which is why this no longer moves every release. +constexpr uint32_t CACHE_VERSION = 1; + +// A stamp-string read that refuses an absurd length before allocating anything. +// The stamps are read from files named from the outside (peek_version is +// pointed at whatever <vendor>.opc a directory holds), so the length word may +// be arbitrary bytes — and a resize to a garbage 64-bit length does not fail as +// a catchable bad_alloc here, it takes the app down through the out-of-memory +// handler. A vendor name or profile version is a short token; anything longer +// is not a cache this build wrote. +std::string read_bounded_string(cereal::BinaryInputArchive& ar) +{ + constexpr uint64_t MAX_STAMP_LEN = 1024; + cereal::size_type len = 0; + ar(cereal::make_size_tag(len)); + if (uint64_t(len) > MAX_STAMP_LEN) + throw std::runtime_error("preset cache: string length out of bounds"); + std::string s(size_t(len), '\0'); + ar(cereal::binary_data(s.data(), size_t(len))); + return s; +} + +// The prologue every cache reader starts with: the format version, then the +// vendor's identity. Returns the vendor version stamped on a body this build can +// read, empty on anything else — which is the same answer as "not this vendor". +std::string read_cache_stamps(cereal::BinaryInputArchive& ar, const std::string& expected_vendor_name) +{ + // The version is judged before anything variable-length is read: on a body + // that is not a per-vendor cache of this version, the bytes where a string + // length would sit may be arbitrary framing. + uint32_t cache_version = 0; + ar(cache_version); + if (cache_version != CACHE_VERSION) + return {}; + const std::string vendor_name = read_bounded_string(ar); + const std::string vendor_version = read_bounded_string(ar); + if (vendor_name != expected_vendor_name) + return {}; + return vendor_version; +} + +// A cache stays usable as long as it was built from a vendor profile at least +// as new as the one now on disk. Profiles whose version is invalid cannot be +// judged this way and are never served from cache; where no profile sits +// beside the cache at all, nothing can be newer than it — that state is passed +// as Semver::inf(), which no real profile can carry (an invalid version could +// not say it apart from "profile there but unjudgeable", and zero would +// collide with a genuine "0.0.0"). This is the serve rule; the install rule +// (cache_covers in PresetBundle.cpp) deliberately reads an unjudgeable profile +// the other way, so the two are not one function. +bool cache_covers_version(const std::string& cached, const Semver& on_disk) +{ + if (on_disk == Semver::inf()) + return true; // before parsing `cached`: nothing exists that the stamp must cover + if (! on_disk.valid()) + return false; + const auto cached_ver = Semver::parse(cached); + return cached_ver && *cached_ver >= on_disk; +} + +// CachedPreset on the wire: all fields, declaration order, in one place. +// `config` writes, reads or skips the config sitting in the middle of that +// order — the three things a reader can want to do with it — so save, load and +// the name peek below cannot drift apart. Keep in sync with the struct in +// PresetCacheFormat.hpp and bump CACHE_VERSION on change. Written here rather +// than as a serialize() member because the config needs the file's dictionary, +// which cereal cannot thread through one. +template<class Archive, class Entry, class ConfigFn> +void visit_entry(Archive& ar, Entry& e, ConfigFn&& config) +{ + ar(e.name, e.sub_path); + config(); + ar(e.inherits, e.description, e.instantiation, e.setting_id, e.filament_id, e.renamed_from); +} + +// The count comes from a file that has already passed magic and CRC, but a +// reserve is a promise to allocate: cap it and let push_back grow the rest. +constexpr uint32_t MAX_RESERVED_ENTRIES = 4096; + +void save_entries(cereal::BinaryOutputArchive& ar, + const std::vector<CachedPreset>& entries, + const CacheDictionary& dict) +{ + ar(uint32_t(entries.size())); + for (const CachedPreset& e : entries) + visit_entry(ar, e, [&] { save_config(ar, e.config_src, dict); }); +} + +void load_entries(cereal::BinaryInputArchive& ar, + std::vector<CachedPreset>& entries, + const CacheDictionary& dict) +{ + uint32_t cnt = 0; + ar(cnt); + entries.clear(); + entries.reserve(std::min(cnt, MAX_RESERVED_ENTRIES)); + for (uint32_t i = 0; i < cnt; ++ i) { + CachedPreset e; + visit_entry(ar, e, [&] { load_config(ar, e.config_src, dict); }); + entries.push_back(std::move(e)); + } +} + +// Read a raw cache body: verify magic, size, CRC. +bool read_cache_blob(const std::string& path, std::string& out_blob) +{ + try { + boost::nowide::ifstream ifs(path, std::ios::binary); + if (!ifs.is_open()) + return false; + CacheFileHeader fhdr; + if (!ifs.read(reinterpret_cast<char*>(&fhdr), sizeof(fhdr))) + return false; + if (fhdr.magic != CACHE_MAGIC) + return false; + // data_size is 8 bytes from a file nothing has authenticated yet, and + // it is about to size an allocation. The body is the whole of the file + // behind the header — anything else is not a cache this build wrote. + ifs.seekg(0, std::ios::end); + const std::streamoff file_size = ifs.tellg(); + if (file_size < std::streamoff(sizeof(fhdr)) || + fhdr.data_size == 0 || + fhdr.data_size != uint64_t(file_size) - sizeof(fhdr)) + return false; + ifs.seekg(sizeof(fhdr), std::ios::beg); + out_blob.assign(fhdr.data_size, '\0'); + if (!ifs.read(&out_blob[0], static_cast<std::streamsize>(fhdr.data_size))) + return false; + boost::crc_32_type crc; + crc.process_bytes(out_blob.data(), out_blob.size()); + if (crc.checksum() != fhdr.crc32) { + BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: CRC mismatch: " << path; + return false; + } + return true; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: read failed (" << path << "): " << e.what(); + return false; + } +} + +// Write a cache body behind the standard 20-byte file header. False when the +// file could not be opened or written whole. +bool write_cache_blob(const std::string& path, const std::string& blob) +{ + boost::crc_32_type crc; + crc.process_bytes(blob.data(), blob.size()); + // Written beside the target and moved into place, as AppConfig::save does: + // a cache is truncated and rewritten in full, so a write that dies partway + // would otherwise leave a header claiming more body than the file holds. + // The PID suffix also keeps two instances writing the same vendor from + // interleaving. + const std::string tmp_path = path + "." + std::to_string(get_current_pid()) + ".tmp"; + try { + boost::filesystem::create_directories(boost::filesystem::path(path).parent_path()); + { + boost::nowide::ofstream ofs(tmp_path, std::ios::binary | std::ios::trunc); + if (!ofs.is_open()) { + BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: cannot open for writing: " << tmp_path; + return false; + } + CacheFileHeader fhdr; + fhdr.magic = CACHE_MAGIC; + fhdr.version = CACHE_VERSION; + fhdr.data_size = static_cast<uint64_t>(blob.size()); + fhdr.crc32 = crc.checksum(); + ofs.write(reinterpret_cast<const char*>(&fhdr), sizeof(fhdr)); + ofs.write(blob.data(), static_cast<std::streamsize>(blob.size())); + ofs.close(); // flush; close() raises failbit on error + if (! ofs.good()) { + BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << tmp_path << ")"; + boost::system::error_code ec; + boost::filesystem::remove(tmp_path, ec); + return false; + } + } + if (const std::error_code ec = rename_file(tmp_path, path)) { + BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: could not move " << tmp_path << " into place: " << ec.message(); + boost::system::error_code rm; + boost::filesystem::remove(tmp_path, rm); + return false; + } + return true; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << path << "): " << e.what(); + boost::system::error_code ec; + boost::filesystem::remove(tmp_path, ec); + return false; + } +} + +} // anonymous namespace + +// static +bool VendorCacheFile::save(const std::string& path, const std::string& vendor_name, + const std::string& vendor_version, const VendorCacheData& data) +{ + try { + // Collected before anything is written: the dictionary sits ahead of the + // entries so a reader resolves it once and then indexes. + CacheDictionary dict; + for (const std::vector<CachedPreset>* entries : { &data.process_entries, &data.filament_entries, &data.machine_entries }) + for (const CachedPreset& e : *entries) + dict.collect(e.config_src); + + std::ostringstream body(std::ios::binary); + { + cereal::BinaryOutputArchive ar(body); + ar(CACHE_VERSION); + ar(vendor_name, vendor_version); + dict.save(ar); + ar(data.vendors); + save_entries(ar, data.process_entries, dict); + save_entries(ar, data.filament_entries, dict); + save_entries(ar, data.machine_entries, dict); + ar(data.parse_errors); + } + return write_cache_blob(path, body.str()); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: failed to save vendor cache " << path << ": " << e.what(); + return false; + } +} + +// static +bool VendorCacheFile::load(const std::string& path, const std::string& expected_vendor_name, + const Semver& expected_vendor_version, VendorCacheData& data) +{ + std::string blob; + if (! read_cache_blob(path, blob)) + return false; + try { + // Read in place: an istringstream would copy the blob once more just to + // stream over it. + boost::iostreams::stream<boost::iostreams::array_source> body(blob.data(), blob.size()); + cereal::BinaryInputArchive ar(body); + const std::string vendor_version = read_cache_stamps(ar, expected_vendor_name); + if (vendor_version.empty() || ! cache_covers_version(vendor_version, expected_vendor_version)) + return false; + CacheDictionary dict; + dict.load(ar); + ar(data.vendors); + load_entries(ar, data.process_entries, dict); + load_entries(ar, data.filament_entries, dict); + load_entries(ar, data.machine_entries, dict); + ar(data.parse_errors); + if (data.vendors.find(expected_vendor_name) == data.vendors.end()) + throw std::runtime_error("vendor cache does not carry its own vendor profile"); + return true; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: rejecting vendor cache " << path << ": " << e.what(); + return false; + } +} + +// static +std::string VendorCacheFile::peek_version(const std::string& path, const std::string& expected_vendor_name) +{ + try { + boost::nowide::ifstream ifs(path, std::ios::binary); + CacheFileHeader fhdr; + if (! ifs.read(reinterpret_cast<char*>(&fhdr), sizeof(fhdr)) || fhdr.magic != CACHE_MAGIC) + return {}; + // Only the head of the body is read, and its CRC left unverified: the + // stamps sit at the front, and this answers "what version is this?" + // without paying for tens of megabytes. Callers that need to know the + // file is whole use usable_version instead. + std::string head(static_cast<size_t>(std::min<uint64_t>(fhdr.data_size, 1024)), '\0'); + if (! ifs.read(&head[0], static_cast<std::streamsize>(head.size()))) + return {}; + std::istringstream body(head, std::ios::binary); + cereal::BinaryInputArchive ar(body); + return read_cache_stamps(ar, expected_vendor_name); + } catch (const std::exception&) { + return {}; + } +} + +// static +Semver VendorCacheFile::usable_version(const std::string& path, const std::string& expected_vendor_name) +{ + std::string blob; + if (! read_cache_blob(path, blob)) + return Semver::invalid(); + try { + boost::iostreams::stream<boost::iostreams::array_source> body(blob.data(), blob.size()); + cereal::BinaryInputArchive ar(body); + const auto ver = Semver::parse(read_cache_stamps(ar, expected_vendor_name)); + return ver ? *ver : Semver::invalid(); + } catch (const std::exception&) { + return Semver::invalid(); + } +} + +// static +bool VendorCacheFile::carries_preset(const std::string& path, const std::string& vendor_name, + Preset::Type type, const std::string& preset_name) +{ + std::string blob; + if (! read_cache_blob(path, blob)) + return false; + try { + boost::iostreams::stream<boost::iostreams::array_source> body(blob.data(), blob.size()); + cereal::BinaryInputArchive ar(body); + if (read_cache_stamps(ar, vendor_name).empty()) + return false; + CacheDictionary dict; + dict.load(ar); + VendorMap vendors; + ar(vendors); + // Reused: every entry overwrites it, and only its name is ever looked at. + CachedPreset entry; + // Written in this order by save. The list that could carry the preset + // is the last one worth reading. + for (Preset::Type kind : { Preset::TYPE_PRINT, Preset::TYPE_FILAMENT, Preset::TYPE_PRINTER }) { + uint32_t cnt = 0; + ar(cnt); + for (uint32_t i = 0; i < cnt; ++ i) { + visit_entry(ar, entry, [&] { skip_config(ar, dict); }); + if (kind == type && entry.name == preset_name) + return true; + } + if (kind == type) + return false; + } + return false; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: could not read preset names from " << path << ": " << e.what(); + return false; + } +} + +} // namespace Slic3r diff --git a/src/libslic3r/PresetCacheFormat.hpp b/src/libslic3r/PresetCacheFormat.hpp new file mode 100644 index 0000000000..b200ec9911 --- /dev/null +++ b/src/libslic3r/PresetCacheFormat.hpp @@ -0,0 +1,192 @@ +#ifndef slic3r_PresetCacheFormat_hpp_ +#define slic3r_PresetCacheFormat_hpp_ + +#include <cstdint> +#include <string> +#include <unordered_map> +#include <vector> + +#include <cereal/archives/binary.hpp> +#include <cereal/types/string.hpp> +#include <cereal/types/vector.hpp> + +#include "libslic3r/Config.hpp" +#include "libslic3r/Preset.hpp" +#include "libslic3r/PrintConfig.hpp" +#include "libslic3r/Semver.hpp" + +namespace Slic3r { + +// How the preset cache writes a DynamicPrintConfig. +// +// Not through the global cereal hooks in PrintConfig.hpp: those key an option by +// its serialization_key_ordinal, which ConfigDef::add assigns by declaration +// order at static-init time. Inserting one option into the middle of +// PrintConfig.cpp shifts every later ordinal, and the lookup on the way back in +// then SUCCEEDS on the wrong option — where the two share a type, and hundreds +// of coFloat/coBool/coInt options do, the bytes deserialize cleanly into the +// wrong key. Silently wrong print settings, no error. Those hooks are also the +// undo/redo wire format, where the process cannot change underneath them, so +// they stay as they are and the cache keys by name instead. +// +// Names are not repeated per preset. Each cache file carries one dictionary of +// the distinct opt_keys it uses, the type each was written as, and the distinct +// enum value names; an option on the wire is then a uint16 index into it plus +// its value. The dictionary is resolved to this build's option definitions once +// per file, after which reading an option is a vector index. +class CacheDictionary +{ +public: + CacheDictionary(); + + // Index reserved in the enum table for an int the writing build could not + // name — a nullable option's nil, or a definition carrying no + // enum_keys_map. The raw int32 follows it on the wire and is loaded + // verbatim, so those values survive too. + static constexpr uint16_t ENUM_UNNAMED = 0; + + // ---- writing ---- + + // Record every key and enum value `config` uses. Call for every config that + // will be written, before writing the dictionary. + void collect(const DynamicPrintConfig& config); + + uint16_t key_index(const t_config_option_key& key) const; + // ENUM_UNNAMED for an empty name or one that was never collected. + uint16_t enum_index(const std::string& name) const; + + // ---- reading ---- + + // The definition an index resolves to in THIS build, or nullptr where the + // key is unknown here or is now defined with a different type. A nullptr + // entry's value is still read — using type_at(idx), the type the writer + // recorded — and then dropped, which is what a JSON profile gets for an + // option this build no longer has. + const ConfigOptionDef* def_at(uint16_t idx) const { return m_defs[idx]; } + ConfigOptionType type_at(uint16_t idx) const { return ConfigOptionType(m_types[idx]); } + const std::string& enum_name_at(uint16_t idx) const { return m_enum_values[idx]; } + // m_defs, not m_keys: only load() sizes it, so this is false for every index + // on a dictionary that was collected rather than read. + bool valid_key_index(uint16_t idx) const { return size_t(idx) < m_defs.size(); } + bool valid_enum_index(uint16_t idx) const { return size_t(idx) < m_enum_values.size(); } + + // The layout these two agree on is covered by CACHE_VERSION (PresetCacheFormat.cpp); + // bump it when they change. + // Throws when either table outgrew the uint16 the wire format indexes it + // with. Both are bounded by the option count (912 at the time of writing), so + // that is a build-time failure in CI, not a runtime one. + void save(cereal::BinaryOutputArchive& ar) const; + // Throws on a dictionary that cannot be indexed as written. + void load(cereal::BinaryInputArchive& ar); + +private: + // Indices are uint16, so a table may hold at most this many entries. + static constexpr size_t MAX_ENTRIES = 0xFFFF; + + std::vector<std::string> m_keys; + // ConfigOptionType, as written. Sixteen bits, not eight: coVectorType is + // 0x4000, so every vector type — coFloats, coEnums, coStrings — is above + // 255, and a byte would fold each one onto its scalar counterpart. + std::vector<uint16_t> m_types; + std::vector<std::string> m_enum_values; // [ENUM_UNNAMED] is always empty + + // Writing. + std::unordered_map<std::string, uint16_t> m_key_index; + std::unordered_map<std::string, uint16_t> m_enum_index; + // Reading, resolved once by load(). + std::vector<const ConfigOptionDef*> m_defs; +}; + +// One config, keyed through `dict`. Options print_config_def does not know are +// not written: nothing could give them a type on the way back in. +void save_config(cereal::BinaryOutputArchive& ar, const DynamicPrintConfig& config, const CacheDictionary& dict); +// Throws only on a payload that cannot be indexed; an option this build cannot +// place is dropped, not fatal. +void load_config(cereal::BinaryInputArchive& ar, DynamicPrintConfig& config, const CacheDictionary& dict); +// Consume one config without building it, for a reader that only wants what +// comes after. +void skip_config(cereal::BinaryInputArchive& ar, const CacheDictionary& dict); + +// One preset as its JSON subfile states it: the config diff, the name of the +// preset it inherits, and the parse metadata — everything the parse phase of +// load_vendor_configs_from_json extracts and nothing it derives. Inheritance +// is resolved when the entry is installed, against whatever filament library +// is loaded then, so a cache carries no other vendor's values and no other +// vendor's update can make it stale. +// Written and read by visit_entry in PresetCacheFormat.cpp, which lists every +// field below in this order — once, for the save, the load and the name peek alike. +struct CachedPreset +{ + std::string name; + std::string sub_path; // path under the vendor's directory + DynamicPrintConfig config_src; // the preset's own diff, nothing inherited + std::string inherits; + std::string description; + std::string instantiation; // "true"/"false" as stated; anything else was already counted as a parse error + std::string setting_id; + std::string filament_id; + std::vector<std::string> renamed_from; +}; + +// What one per-vendor cache file carries besides its stamps: the vendor profile +// map, the presets in source form, and how many errors their parse counted. +struct VendorCacheData +{ + VendorMap vendors; + std::vector<CachedPreset> process_entries; + std::vector<CachedPreset> filament_entries; + std::vector<CachedPreset> machine_entries; + uint64_t parse_errors = 0; +}; + +// A per-vendor preset cache file (<vendor>.opc): a 20-byte header (magic, format +// version, body size, CRC) framing one cereal body — stamps (format version, +// vendor name, vendor profile version), the option dictionary, then the +// VendorCacheData. Everything about those bytes lives here; when a vendor is +// served from its cache, and how entries install into a bundle, is +// PresetBundle's business. +class VendorCacheFile +{ +public: + // Save one vendor (vendor_name at vendor_version). False when the file + // could not be written whole. + static bool save(const std::string& path, const std::string& vendor_name, + const std::string& vendor_version, const VendorCacheData& data); + + // Read a whole cache into `data`. False — with `data` in an unspecified + // state — unless the file is a cache this build wrote, its CRC holds, it + // names this vendor, it was built from a vendor profile at least as new as + // `expected_vendor_version`, and it carries its own vendor profile. An + // invalid expected version (a profile whose version + // cannot be judged) is never served from cache; Semver::inf() (no profile + // beside the cache at all) accepts whatever is cached. + static bool load(const std::string& path, const std::string& expected_vendor_name, + const Semver& expected_vendor_version, VendorCacheData& data); + + // Read the profile version a cache was stamped with, without deserializing + // its presets. Empty if the file is unreadable, not a cache this build + // understands, or not this vendor's. This is how an installed vendor's + // version is known when only its cache is installed. + static std::string peek_version(const std::string& path, const std::string& expected_vendor_name); + + // The profile version an installed cache can actually be served at, or an + // invalid Semver when the file is not a cache this build can read. Unlike + // peek_version this verifies the body's CRC, at the cost of reading the + // whole file: where the cache is the vendor's whole installation, "a file + // is there" is not enough to call it installed, and a vendor wrongly + // believed installed is never repaired. + static Semver usable_version(const std::string& path, const std::string& expected_vendor_name); + + // Whether a cache carries a preset of `type` under `preset_name`, without + // installing any of them. False when the file is not a cache this build can + // read. The three kinds are written in one stream, so reaching the machines + // means reading past the processes and filaments — their configs are consumed + // and dropped rather than built. This is how a build that ships caches instead + // of preset JSONs answers "which vendor carries this preset?". + static bool carries_preset(const std::string& path, const std::string& vendor_name, + Preset::Type type, const std::string& preset_name); +}; + +} // namespace Slic3r + +#endif // slic3r_PresetCacheFormat_hpp_ diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 371f1e68ea..e96304e874 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -5,6 +5,7 @@ #include "Brim.hpp" #include "ClipperUtils.hpp" #include "Extruder.hpp" +#include "FilamentMixer.hpp" #include "Flow.hpp" #include "Geometry/ConvexHull.hpp" #include "I18N.hpp" @@ -282,6 +283,14 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "wipe_tower_x" || opt_key == "wipe_tower_y" || opt_key == "wipe_tower_rotation_angle") { + // The tower gcode itself is position-independent (position and rotation are applied + // at export), except that the wait_for_temp_on_wipe_tower park bakes a bed-relative + // side choice into it (WipeTower2::toolchange_Change) — regenerate it when the tower + // moves. Gating on the old config is safe: both inputs of wait_for_temp_enabled + // invalidate psWipeTower themselves when they are part of the same diff. + if ((opt_key == "wipe_tower_x" || opt_key == "wipe_tower_y" || opt_key == "wipe_tower_rotation_angle") + && WipeTower2::wait_for_temp_enabled(m_config)) + steps.emplace_back(psWipeTower); steps.emplace_back(psSkirtBrim); } else if ( opt_key == "slicing_pipeline_plugin" @@ -382,6 +391,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "wiping_volumes_extruders" || opt_key == "enable_filament_ramming" || opt_key == "tool_change_on_wipe_tower" + || opt_key == "wait_for_temp_on_wipe_tower" || opt_key == "purge_in_prime_tower" || opt_key == "z_offset" || opt_key == "support_multi_bed_types" @@ -556,7 +566,7 @@ std::vector<unsigned int> Print::extruders(bool conside_custom_gcode) const // If a wipe tower filament is explicitly set, ensure it participates in tool ordering. if (has_wipe_tower() && config().wipe_tower_filament != 0 && extruders.size() > 1) { - assert(config().wipe_tower_filament > 0 && config().wipe_tower_filament < int(config().nozzle_diameter.size())); + assert(config().wipe_tower_filament > 0 && config().wipe_tower_filament <= int(config().filament_diameter.size())); extruders.emplace_back(config().wipe_tower_filament - 1); // config value is 1-based } @@ -1039,13 +1049,14 @@ static StringObjectException layered_print_cleareance_valid(const Print &print, wipe_tower_convex_hull.points.emplace_back(scale_(x + width), scale_(y)); wipe_tower_convex_hull.points.emplace_back(scale_(x + width), scale_(y + depth)); wipe_tower_convex_hull.points.emplace_back(scale_(x), scale_(y + depth)); - wipe_tower_convex_hull.rotate(a); + wipe_tower_convex_hull.rotate(Geometry::deg2rad(a), Point(scale_(x), scale_(y))); convex_hulls_temp.push_back(wipe_tower_convex_hull); } else { //here, wipe_tower_polygon is not always convex. Polygon wipe_tower_polygon; if (print.wipe_tower_data().wipe_tower_mesh_data) wipe_tower_polygon = print.wipe_tower_data().wipe_tower_mesh_data->bottom; + wipe_tower_polygon.rotate(Geometry::deg2rad(a)); wipe_tower_polygon.translate(Point(scale_(x), scale_(y))); convex_hulls_temp.push_back(wipe_tower_polygon); } @@ -1064,6 +1075,22 @@ static StringObjectException layered_print_cleareance_valid(const Print &print, if (print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, convex_hulls_temp).empty()) { return {L("Prime Tower") + L(" is too close to clumping detection area, and collisions will be caused.\n")}; } + // Skip the containment check for towers that will never be printed (single-filament + // prints without smooth timelapse keep the config's tower position but emit nothing). + // Pre-generation only the body square is tested — the auto-brim estimate can overshoot + // the generated brim by several mm and must not hard-fail a print that physically fits. + // Post-generation the mesh bottom already includes the real brim, so the exact + // footprint is tested. + if (filaments_count > 1 || print.enable_timelapse_print()) { + // The shared printable polygon is plate-local, while the tower polygons above are + // already shifted by the plate origin. + Polygons printable_polys = print.get_extruder_shared_printable_polygon(); + const Point plate_shift(scale_(plate_origin.x()), scale_(plate_origin.y())); + for (Polygon &p : printable_polys) + p.translate(plate_shift); + if (!diff(convex_hulls_temp, printable_polys).empty()) + return {L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n")}; + } return {}; } @@ -1301,6 +1328,19 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin if (extruders.empty()) return { L("No extrusions under current settings.") }; + // Orca: a gradient mixed filament only renders its gradient with "Mixed color sublayer" on; + // without it ToolOrdering::resolve_mixed_filaments prints one whole component per layer and + // the gradient is dropped silently. extruders() already covers painting, height ranges, + // per-feature filament ids and supports, and still lists mixed slots under their own id here. + if (!m_config.enable_mixed_color_sublayer.value) { + const auto &is_mixed = m_config.filament_is_mixed.values; + const auto &gradient = m_config.filament_mixed_gradient.values; + if (std::any_of(extruders.begin(), extruders.end(), [&](unsigned int e) { + return e < is_mixed.size() && is_mixed[e] && e < gradient.size() && gradient[e]; })) + warn(L("A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed."), + "enable_mixed_color_sublayer"); + } + if (nozzles < 2 && extruders.size() > 1) { auto ret = check_multi_filament_valid(*this); if (!ret.string.empty()) @@ -1362,6 +1402,13 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin // #4043 if (total_copies_count > 1 && m_config.print_sequence != PrintSequence::ByObject) return {L("Please select \"By object\" print sequence to print multiple objects in spiral vase mode."), nullptr, "spiral_mode"}; + // A mixed (virtual) filament always resolves to multiple physical components, which + // spiral vase cannot print. + const auto &is_mixed = m_config.filament_is_mixed.values; + for (const PrintObject *object : m_objects) + for (unsigned int ext : object->object_extruders()) + if (ext < is_mixed.size() && is_mixed[ext]) + return {L("Spiral (vase) mode does not work when an object contains more than one material."), nullptr, "spiral_mode"}; assert(m_objects.size() == 1); const auto all_regions = m_objects.front()->all_regions(); if (all_regions.size() > 1) { @@ -1438,6 +1485,17 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin } if (this->has_wipe_tower() && ! m_objects.empty()) { + // Orca: wipe_tower_filament (issue #10971) is inserted into the tool order after + // resolve_mixed_filaments has expanded every mixed (virtual) slot, so a mixed slot here + // would reach the G-code as a tool change to a slot no nozzle carries. The GUI hides + // mixed slots from the option; this guards loaded projects and the CLI. + if (m_config.wipe_tower_filament > 0) { + const auto &is_mixed = m_config.filament_is_mixed.values; + const size_t wipe_idx = size_t(m_config.wipe_tower_filament - 1); + if (wipe_idx < is_mixed.size() && is_mixed[wipe_idx]) + return { L("The wipe tower filament cannot be a mixed filament."), nullptr, "wipe_tower_filament" }; + } + // Make sure all extruders use same diameter filament and have the same nozzle diameter // EPSILON comparison is used for nozzles and 10 % tolerance is used for filaments double first_nozzle_diam = m_config.nozzle_diameter.get_at(extruders.front()); @@ -2230,7 +2288,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) obj->clear_shared_object(); //add the print_object share check logic - auto is_print_object_the_same = [this](const PrintObject* object1, const PrintObject* object2) -> bool{ + auto is_print_object_the_same = [](const PrintObject* object1, const PrintObject* object2) -> bool{ if (object1->trafo().matrix() != object2->trafo().matrix()) return false; const ModelObject* model_obj1 = object1->model_object(); @@ -2559,18 +2617,31 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) std::vector<const PrintInstance*>::const_iterator print_object_instance_sequential_active; std::vector<std::pair<coordf_t, std::vector<GCode::LayerToPrint>>> layers_to_print = GCode::collect_layers_to_print(*this); std::vector<unsigned int> printExtruders; + // Per-object first-layer mixed-slot resolutions for the by-object remap below + // (BBS reads them from m_sequential_print_data->object_tool_ordering_map). + std::map<ObjectID, std::map<unsigned int, unsigned int>> seq_mixed_resolution; // Cleared on every process so a print-sequence or selector-mode change can never leave // stale object pointers behind; repopulated below only by the sequential selector path. m_sequential_dynamic_orderings.clear(); if (this->config().print_sequence == PrintSequence::ByObject) { // Order object instances for sequential print. print_object_instances_ordering = sort_object_instances_by_model_order(*this); + // A mixed slot is virtual; only its components reach a nozzle. These per-object orderings + // are unsorted (no resolve_mixed_filaments), so expand the slots here for the grouping, the + // unprintable sets and the slice-used lists. Because the expansion happens here rather than + // on the sorted orderings, the first-layer used set lists every component of a mixed slot, + // not just the one layer 0 resolves to. No-op without mixed filaments. + const auto &is_mixed = m_config.filament_is_mixed.values; + const auto &comp_strs = m_config.filament_mixed_components.values; + const bool has_mixed = has_any_mixed_filament(is_mixed); std::vector<unsigned int> first_layer_used_filaments; std::vector<std::vector<unsigned int>> all_filaments; for (print_object_instance_sequential_active = print_object_instances_ordering.begin(); print_object_instance_sequential_active != print_object_instances_ordering.end(); ++print_object_instance_sequential_active) { tool_ordering = ToolOrdering(*(*print_object_instance_sequential_active)->print_object, initial_extruder_id); for (size_t idx = 0; idx < tool_ordering.layer_tools().size(); ++idx) { - auto& layer_filament = tool_ordering.layer_tools()[idx].extruders; + auto layer_filament = tool_ordering.layer_tools()[idx].extruders; + if (has_mixed) + layer_filament = expand_mixed_filaments(layer_filament, is_mixed, comp_strs); all_filaments.emplace_back(layer_filament); if (idx == 0) first_layer_used_filaments.insert(first_layer_used_filaments.end(), layer_filament.begin(), layer_filament.end()); @@ -2582,6 +2653,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) auto physical_unprintables = this->get_physical_unprintable_filaments(used_filaments); auto geometric_unprintables = this->get_geometric_unprintable_filaments(); + if (has_mixed) + expand_mixed_slots_in_unprintables(geometric_unprintables, is_mixed, comp_strs); auto filament_unprintable_volumes = this->get_filament_unprintable_flow(used_filaments); // Selector (per-layer regroup) prints skip the static grouping: their print-wide result // is stitched from the per-object plans after the ordering loop below. @@ -2633,6 +2706,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) std::vector<std::vector<int>> nozzle_map_per_layer; std::vector<std::vector<unsigned int>> stitched_layer_filaments; print_object_instance_sequential_active = print_object_instances_ordering.begin(); + std::vector<unsigned int> used_mixed_filaments; for (; print_object_instance_sequential_active != print_object_instances_ordering.end(); ++print_object_instance_sequential_active) { const PrintObject *print_object = (*print_object_instance_sequential_active)->print_object; if (dynamic_reorder) { @@ -2661,11 +2735,18 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) } else { tool_ordering = ToolOrdering(*print_object, initial_extruder_id); tool_ordering.sort_and_build_data(*print_object, initial_extruder_id); + if (!tool_ordering.layer_tools().empty()) + seq_mixed_resolution[print_object->id()] = tool_ordering.layer_tools().front().mixed_filament_resolution; } + // Only sorted orderings have run resolve_mixed_filaments, so only they know which + // mixed slots actually print. + append(used_mixed_filaments, tool_ordering.used_mixed_filaments()); if ((initial_extruder_id = tool_ordering.first_extruder()) != static_cast<unsigned int>(-1)) { append(printExtruders, tool_ordering.tools_for_layer(layers_to_print.front().first).extruders); } } + sort_remove_duplicates(used_mixed_filaments); + this->set_slice_used_mixed_filaments(used_mixed_filaments); if (dynamic_reorder && m_objects.size() > 1) { // Stitch the per-object plans into one print-wide selector result. A single-object // sequential print publishes (and writes back) from its own ordering instead: the @@ -2686,6 +2767,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) first_layer_used_filaments = tool_ordering.layer_tools().front().extruders; this->set_slice_used_filaments(first_layer_used_filaments, tool_ordering.all_extruders()); + this->set_slice_used_mixed_filaments(tool_ordering.used_mixed_filaments()); has_wipe_tower = this->has_wipe_tower() && tool_ordering.has_wipe_tower(); initial_extruder_id = tool_ordering.first_extruder(); print_object_instances_ordering = chain_print_object_instances(*this); @@ -2693,6 +2775,28 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) } auto objectExtruderMap = getObjectExtruderMap(*this); + // Resolve mixed filament virtual slots to physical components so brim + // extruder matching works correctly (mixed slot IDs are not present + // in printExtruders after ToolOrdering::resolve_mixed_filaments). + { + const LayerTools *first_lt = nullptr; + if (m_config.print_sequence != PrintSequence::ByObject && !tool_ordering.layer_tools().empty()) + first_lt = &tool_ordering.layer_tools().front(); + for (auto &[obj_id, ext_1based] : objectExtruderMap) { + if (ext_1based == 0) + continue; + const std::map<unsigned int, unsigned int> *resolution = nullptr; + if (first_lt) + resolution = &first_lt->mixed_filament_resolution; + else if (auto obj_it = seq_mixed_resolution.find(obj_id); obj_it != seq_mixed_resolution.end()) + resolution = &obj_it->second; + if (resolution) { + auto it = resolution->find(ext_1based - 1); + if (it != resolution->end()) + ext_1based = it->second + 1; + } + } + } std::vector<std::pair<ObjectID, unsigned int>> objPrintVec; for (const PrintInstance* instance : print_object_instances_ordering) { const ObjectID& print_object_ID = instance->print_object->id(); @@ -3686,7 +3790,7 @@ std::vector<Polygons> Print::get_extruder_printable_polygons() const Polygons ploys = {Polygon::new_scale(e_printable_area)}; extruder_printable_polys.emplace_back(ploys); } - return std::move(extruder_printable_polys); + return extruder_printable_polys; } std::vector<Polygons> Print::get_extruder_unprintable_polygons() const @@ -3699,7 +3803,7 @@ std::vector<Polygons> Print::get_extruder_unprintable_polygons() const Polygons ploys = diff(printable_poly, Polygon::new_scale(e_printable_area)); extruder_unprintable_polys.emplace_back(ploys); } - return std::move(extruder_unprintable_polys); + return extruder_unprintable_polys; } size_t Print::get_extruder_id(unsigned int filament_id) const @@ -3750,6 +3854,14 @@ bool Print::is_dynamic_group_reorder() const const bool enabled = opt && opt->value; if (!enabled || m_config.filament_map_mode != FilamentMapMode::fmmAutoForFlush || m_config.nozzle_diameter.size() <= 1) return false; + + // Dynamic regrouping and mixed-color slots are incompatible: a mixed slot is resolved to + // different physical components per layer, so a group assignment made up-front would be wrong. + const auto &is_mixed = m_config.filament_is_mixed.values; + for (unsigned int filament_id : extruders()) { + if (filament_id < is_mixed.size() && is_mixed[filament_id]) + return false; + } return true; } @@ -3918,6 +4030,12 @@ const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const double volume = wipe_volume * filament_depth_count; if (m_config.nozzle_diameter.values.size() == 2) volume += filament_change_volume * (int) (filaments_cnt / 2); + // Sizing should take into account currently set wiping volumes. + // For a long time, the initial preview would just use 900/width per toolchange (15mm on a 60mm wide tower) + // and it worked well enough. Let's try to do slightly better by accounting for the purging volumes. + const bool semm_flush = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material; + if (semm_flush) volume = WipeTower2::estimate_semm_flush_volume(m_config, filaments_cnt); + if (m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib) { double depth = std::sqrt(volume / layer_height * extra_spacing); if (need_wipe_tower || filaments_cnt > 1) { @@ -3929,30 +4047,16 @@ const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const } } else { - double width = m_config.prime_tower_width; - if (m_config.purge_in_prime_tower && m_config.single_extruder_multi_material) { - // Calculating depth should take into account currently set wiping volumes. - // For a long time, the initial preview would just use 900/width per toolchange (15mm on a 60mm wide tower) - // and it worked well enough. Let's try to do slightly better by accounting for the purging volumes. - std::vector<std::vector<float>> wipe_volumes = WipeTower2::extract_wipe_volumes(m_config); - std::vector<float> max_wipe_volumes; - for (const std::vector<float> &v : wipe_volumes) - max_wipe_volumes.emplace_back(*std::max_element(v.begin(), v.end())); - float maximum = std::accumulate(max_wipe_volumes.begin(), max_wipe_volumes.end(), 0.f); - maximum = maximum * filaments_cnt / max_wipe_volumes.size(); - - // Orca: it's overshooting a bit, so let's reduce it a bit - maximum *= 0.6; - const_cast<Print *>(this)->m_wipe_tower_data.depth = maximum / (layer_height * width); - } else { - double depth = volume / (layer_height * width) * extra_spacing; - if (need_wipe_tower || m_wipe_tower_data.depth > EPSILON) { + double width = m_config.prime_tower_width; + double depth = volume / (layer_height * width); + // The flush volumes already hold the spacing between wipes. + if (!semm_flush) depth *= extra_spacing; + if (need_wipe_tower || depth > EPSILON) { float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height); depth = std::max((double) min_wipe_tower_depth, depth); } const_cast<Print *>(this)->m_wipe_tower_data.depth = depth; - } - const_cast<Print *>(this)->m_wipe_tower_data.brim_width = m_config.prime_tower_brim_width; + const_cast<Print *>(this)->m_wipe_tower_data.brim_width = m_config.prime_tower_brim_width; } if (m_config.prime_tower_brim_width < 0) const_cast<Print *>(this)->m_wipe_tower_data.brim_width = WipeTower::get_auto_brim_by_height(max_height); } @@ -3981,38 +4085,36 @@ void Print::_make_wipe_tower() return; // Check whether there are any layers in m_tool_ordering, which are marked with has_wipe_tower, - // they print neither object, nor support. These layers are above the raft and below the object, and they - // shall be added to the support layers to be printed. - // see https://github.com/prusa3d/PrusaSlicer/issues/607 + // they print neither object, nor support. Each such layer needs a virtual support layer + // counterpart in m_objects.front() so that GCode::collect_layers_to_print picks it up and the + // wipe tower G-code is actually emitted for that z. Such layers appear in two scenarios: + // - above the raft, between raft top and the first real object layer + // (see https://github.com/prusa3d/PrusaSlicer/issues/607); + // - between two real wipe-tower layers, when one object is fully floating above another and + // the support_top_z_distance / support_bottom_z_distance gap leaves interior z values with + // neither object nor support (continuity fill in ToolOrdering::fill_wipe_tower_partitions). + // The previous implementation only handled the first contiguous run starting at the first + // virtual layer, which made the second scenario silently produce empty wipe-tower layers. { - size_t idx_begin = size_t(-1); - size_t idx_end = m_wipe_tower_data.tool_ordering.layer_tools().size(); - // Find the first wipe tower layer, which does not have a counterpart in an object or a support layer. + auto &support_layers = m_objects.front()->support_layers(); + auto it_layer = support_layers.begin(); + const size_t idx_end = m_wipe_tower_data.tool_ordering.layer_tools().size(); for (size_t i = 0; i < idx_end; ++ i) { - const LayerTools < = m_wipe_tower_data.tool_ordering.layer_tools()[i]; - if (lt.has_wipe_tower && ! lt.has_object && ! lt.has_support) { - idx_begin = i; - break; - } - } - if (idx_begin != size_t(-1)) { - // Find the position in m_objects.first()->support_layers to insert these new support layers. - double wipe_tower_new_layer_print_z_first = m_wipe_tower_data.tool_ordering.layer_tools()[idx_begin].print_z; - auto it_layer = m_objects.front()->support_layers().begin(); - auto it_end = m_objects.front()->support_layers().end(); - for (; it_layer != it_end && (*it_layer)->print_z - EPSILON < wipe_tower_new_layer_print_z_first; ++ it_layer); - // Find the stopper of the sequence of wipe tower layers, which do not have a counterpart in an object or a support layer. - for (size_t i = idx_begin; i < idx_end; ++ i) { - LayerTools < = const_cast<LayerTools&>(m_wipe_tower_data.tool_ordering.layer_tools()[i]); - if (! (lt.has_wipe_tower && ! lt.has_object && ! lt.has_support)) - break; - lt.has_support = true; - // Insert the new support layer. - double height = lt.print_z - (i == 0 ? 0. : m_wipe_tower_data.tool_ordering.layer_tools()[i-1].print_z); - //FIXME the support layer ID is set to -1, as Vojtech hopes it is not being used anyway. - it_layer = m_objects.front()->insert_support_layer(it_layer, -1, 0, height, lt.print_z, lt.print_z - 0.5 * height); + LayerTools < = const_cast<LayerTools&>(m_wipe_tower_data.tool_ordering.layer_tools()[i]); + if (! (lt.has_wipe_tower && ! lt.has_object && ! lt.has_support)) + continue; + while (it_layer != support_layers.end() && (*it_layer)->print_z + EPSILON < lt.print_z) ++ it_layer; + if (it_layer != support_layers.end() && std::abs((*it_layer)->print_z - lt.print_z) < EPSILON) { + lt.has_support = true; + ++ it_layer; + continue; } + lt.has_support = true; + double height = lt.print_z - (i == 0 ? 0. : m_wipe_tower_data.tool_ordering.layer_tools()[i-1].print_z); + //FIXME the support layer ID is set to -1, as Vojtech hopes it is not being used anyway. + it_layer = m_objects.front()->insert_support_layer(it_layer, -1, 0, height, lt.print_z, lt.print_z - 0.5 * height); + ++ it_layer; } } this->throw_if_canceled(); @@ -5597,7 +5699,7 @@ int Print::load_cached_data(const std::string& directory) return CLI_IMPORT_CACHE_NOT_FOUND; } - auto find_region = [this](PrintObject* object, size_t config_hash) -> const PrintRegion* { + auto find_region = [](PrintObject* object, size_t config_hash) -> const PrintRegion* { int regions_count = object->num_printing_regions(); for (int index = 0; index < regions_count; index++ ) { @@ -5809,7 +5911,7 @@ BoundingBoxf3 PrintInstance::get_bounding_box() const { Polygon PrintInstance::get_convex_hull_2d() { Polygon poly = print_object->model_object()->convex_hull_2d(model_instance->get_matrix()); - poly.douglas_peucker(0.1); + poly.douglas_peucker(scale_(0.1)); return poly; } diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index b38a0ca058..9e20061501 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -117,9 +117,9 @@ class PrintRegion public: PrintRegion() = default; PrintRegion(const PrintRegionConfig &config); - PrintRegion(const PrintRegionConfig &config, const size_t config_hash, int print_object_region_id = -1) : m_config(config), m_config_hash(config_hash), m_print_object_region_id(print_object_region_id) {} + PrintRegion(const PrintRegionConfig &config, const size_t config_hash, int print_object_region_id = -1, ObjectID gradient_volume_id = ObjectID()) : m_config(config), m_config_hash(config_hash), m_print_object_region_id(print_object_region_id), m_gradient_volume_id(gradient_volume_id) {} PrintRegion(PrintRegionConfig &&config); - PrintRegion(PrintRegionConfig &&config, const size_t config_hash, int print_object_region_id = -1) : m_config(std::move(config)), m_config_hash(config_hash), m_print_object_region_id(print_object_region_id) {} + PrintRegion(PrintRegionConfig &&config, const size_t config_hash, int print_object_region_id = -1, ObjectID gradient_volume_id = ObjectID()) : m_config(std::move(config)), m_config_hash(config_hash), m_print_object_region_id(print_object_region_id), m_gradient_volume_id(gradient_volume_id) {} ~PrintRegion() = default; // Methods NOT modifying the PrintRegion's state: @@ -129,6 +129,10 @@ public: // Identifier of this PrintRegion in the list of Print::m_print_regions. int print_region_id() const throw() { return m_print_region_id; } int print_object_region_id() const throw() { return m_print_object_region_id; } + // Volume identity used to differentiate same-config regions when per-part gradient is enabled. + // Default-constructed (invalid) means this region is not tied to a specific volume — preserves + // existing behavior for all paths not using per_part_gradient. + ObjectID gradient_volume_id() const throw() { return m_gradient_volume_id; } // 1-based extruder identifier for this region and role. unsigned int extruder(FlowRole role) const; Flow flow(const PrintObject &object, FlowRole role, double layer_height, bool first_layer = false) const; @@ -158,6 +162,10 @@ private: int m_print_region_id { -1 }; int m_print_object_region_id { -1 }; int m_ref_cnt { 0 }; + // Per-part gradient: when non-invalid, this region belongs exclusively to one ModelVolume, + // letting same-color volumes within a combined ModelObject be tracked separately for gradient + // emission. Default invalid -> region keying behaves exactly as before. + ObjectID m_gradient_volume_id; }; inline bool operator==(const PrintRegion &lhs, const PrintRegion &rhs) { return lhs.config_hash() == rhs.config_hash() && lhs.config() == rhs.config(); } @@ -306,6 +314,11 @@ public: Transform3d trafo_bboxes; std::vector<ObjectID> cached_volume_ids; + // Per-part gradient: the slot_per_part_enabled bit vector that produced these regions. + // Print::apply compares it against the current one to detect a change that PrintRegionConfig + // alone would not reveal, and regenerates the regions when it differs. + std::vector<bool> last_slot_per_part_enabled; + void ref_cnt_inc() { ++ m_ref_cnt; } void ref_cnt_dec() { if (-- m_ref_cnt == 0) delete this; } void clear() { @@ -930,8 +943,8 @@ public: // If preview_data is not null, the preview_data is filled in for the G-code visualization (not used by the command line Slic3r). std::string export_gcode(const std::string& path_template, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb = nullptr); //return 0 means successful - int export_cached_data(const std::string& dir_path, bool with_space=false); - int load_cached_data(const std::string& directory); + int export_cached_data(const std::string& dir_path, bool with_space=false) override; + int load_cached_data(const std::string& directory) override; // methods for handling state bool is_step_done(PrintStep step) const { return Inherited::is_step_done(step); } @@ -1075,6 +1088,10 @@ public: m_slice_used_filaments = used_filaments; } std::vector<unsigned int> get_slice_used_filaments(bool first_layer) const { return first_layer ? m_slice_used_filaments_first_layer : m_slice_used_filaments;} + void set_slice_used_mixed_filaments(const std::vector<unsigned int> &used_mixed_filaments) { + m_slice_used_mixed_filaments = used_mixed_filaments; + } + const std::vector<unsigned int>& get_slice_used_mixed_filaments() const { return m_slice_used_mixed_filaments; } /** * @brief Determines the unprintable filaments for each extruder based on its physical attributes @@ -1342,6 +1359,8 @@ private: std::vector<unsigned int> m_slice_used_filaments; std::vector<unsigned int> m_slice_used_filaments_first_layer; + // 0-based mixed (virtual) filament slots actually used on this plate. + std::vector<unsigned int> m_slice_used_mixed_filaments; //BBS: plate's origin Vec3d m_origin {0, 0, 0}; diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index e2e9bc737d..90d60b5917 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1,6 +1,7 @@ #include "ClipperUtils.hpp" #include "Model.hpp" #include "Print.hpp" +#include "FilamentMixer.hpp" #include <boost/log/trivial.hpp> #include <cfloat> @@ -559,9 +560,11 @@ static inline bool model_volume_solid_or_modifier(const ModelVolume &mv) static inline Transform3f trafo_for_bbox(const Transform3d &object_trafo, const Transform3d &volume_trafo) { - Transform3d m = object_trafo * volume_trafo; - m.translation().x() = 0.; - m.translation().y() = 0.; + // Orca: Keep the volume's local XY offset for multipart overlap checks, but remove the object's bed placement. + Transform3d object_trafo_local = object_trafo; + object_trafo_local.translation().x() = 0.; + object_trafo_local.translation().y() = 0.; + Transform3d m = object_trafo_local * volume_trafo; return m.cast<float>(); } @@ -886,7 +889,12 @@ bool verify_update_print_object_regions( size_t hash = regions[i]->config_hash(); size_t j = i; for (++ j; j < regions.size() && regions[j]->config_hash() == hash; ++ j) - if (regions[i]->config() == regions[j]->config()) { + // Same config but different gradient_volume_id is intentional (per-part gradient + // splitting) and must NOT be flagged as a merge. When per-part is off all regions + // carry an invalid (default) gradient_volume_id, so the AND condition is always + // true and behavior matches the legacy check. + if (regions[i]->config() == regions[j]->config() + && regions[i]->gradient_volume_id() == regions[j]->gradient_volume_id()) { // Regions were merged. We need to reslice. return false; } @@ -978,7 +986,10 @@ static PrintObjectRegions* generate_print_object_regions( const float xy_contour_compensation, const std::vector<unsigned int> &painting_extruders, std::vector<int> &variant_index, - const bool has_painted_fuzzy_skin) + const bool has_painted_fuzzy_skin, + // Per-part gradient: slot_per_part_enabled[s-1] is true when mixed slot s has + // filament_mixed_gradient_per_part on. Empty / all-false preserves legacy behavior. + const std::vector<bool> &slot_per_part_enabled = {}) { // Reuse the old object or generate a new one. auto out = print_object_regions_old ? std::unique_ptr<PrintObjectRegions>(print_object_regions_old) : std::make_unique<PrintObjectRegions>(); @@ -1013,19 +1024,71 @@ static PrintObjectRegions* generate_print_object_regions( update_volume_bboxes(layer_ranges_regions, out->cached_volume_ids, model_volumes, out->trafo_bboxes, is_mm_painted ? 0.f : std::max(0.f, xy_contour_compensation)); std::vector<PrintRegion*> region_set; - auto get_create_region = [®ion_set, &all_regions](PrintRegionConfig &&config) -> PrintRegion* { + // Look up or create a PrintRegion. The optional volume_tag, when valid (non-zero ObjectID), + // keys the region to one ModelVolume so two volumes with identical settings still get + // separate regions — needed so each part can run its own gradient. A default (invalid) + // tag reproduces the previous lookup exactly. + auto get_create_region = [®ion_set, &all_regions](PrintRegionConfig &&config, ObjectID volume_tag = ObjectID()) -> PrintRegion* { size_t hash = config.hash(); - auto it = Slic3r::lower_bound_by_predicate(region_set.begin(), region_set.end(), [&config, hash](const PrintRegion* l) { - return l->config_hash() < hash || (l->config_hash() == hash && l->config() < config); }); - if (it != region_set.end() && (*it)->config_hash() == hash && (*it)->config() == config) + auto it = Slic3r::lower_bound_by_predicate(region_set.begin(), region_set.end(), [&config, hash, volume_tag](const PrintRegion* l) { + return l->config_hash() < hash || (l->config_hash() == hash && l->config() < config) + || (l->config_hash() == hash && l->config() == config && l->gradient_volume_id() < volume_tag); }); + if (it != region_set.end() && (*it)->config_hash() == hash && (*it)->config() == config + && (*it)->gradient_volume_id() == volume_tag) return *it; // Insert into a sorted array, it has O(n) complexity, but the calling algorithm has an O(n^2*log(n)) complexity anyways. - all_regions.emplace_back(std::make_unique<PrintRegion>(std::move(config), hash, int(all_regions.size()))); + all_regions.emplace_back(std::make_unique<PrintRegion>(std::move(config), hash, int(all_regions.size()), volume_tag)); PrintRegion *region = all_regions.back().get(); region_set.emplace(it, region); return region; }; + // Per-part gradient: count how many model-part volumes in this object use each + // per-part-enabled gradient slot. Only slots with at least 2 users get their volumes + // tagged — a single-user slot gains nothing from per-volume splitting and would only + // inflate the region count. Empty slot_per_part_enabled leaves this empty, so + // compute_volume_tag below always returns an invalid tag and nothing changes. + std::vector<int> per_part_volume_users; + if (!slot_per_part_enabled.empty()) { + per_part_volume_users.assign(slot_per_part_enabled.size(), 0); + for (const ModelVolume *mv : model_volumes) { + if (! mv->is_model_part()) + continue; + const DynamicPrintConfig *range_cfg = layer_ranges_regions.empty() ? nullptr : layer_ranges_regions.front().config; + PrintRegionConfig vol_cfg = region_config_from_model_volume(default_region_config, range_cfg, *mv, num_extruders, variant_index); + for (unsigned int s_1based : { (unsigned int)vol_cfg.outer_wall_filament_id.value, + (unsigned int)vol_cfg.inner_wall_filament_id.value, + (unsigned int)vol_cfg.sparse_infill_filament_id.value, + (unsigned int)vol_cfg.internal_solid_filament_id.value, + (unsigned int)vol_cfg.top_surface_filament_id.value, + (unsigned int)vol_cfg.bottom_surface_filament_id.value }) { + if (s_1based >= 1 + && size_t(s_1based - 1) < slot_per_part_enabled.size() + && slot_per_part_enabled[s_1based - 1]) + ++per_part_volume_users[s_1based - 1]; + } + } + } + auto compute_volume_tag = [&](const PrintRegionConfig &cfg, const ModelVolume &mv) -> ObjectID { + if (per_part_volume_users.empty()) + return ObjectID(); + auto qualifies = [&](unsigned int s_1based) { + return s_1based >= 1 + && size_t(s_1based - 1) < slot_per_part_enabled.size() + && slot_per_part_enabled[s_1based - 1] + && per_part_volume_users[s_1based - 1] >= 2; + }; + if (qualifies((unsigned int)cfg.outer_wall_filament_id.value) + || qualifies((unsigned int)cfg.inner_wall_filament_id.value) + || qualifies((unsigned int)cfg.sparse_infill_filament_id.value) + || qualifies((unsigned int)cfg.internal_solid_filament_id.value) + || qualifies((unsigned int)cfg.top_surface_filament_id.value) + || qualifies((unsigned int)cfg.bottom_surface_filament_id.value)) { + return mv.id(); + } + return ObjectID(); + }; + // Chain the regions in the order they are stored in the volumes list. for (int volume_id = 0; volume_id < int(model_volumes.size()); ++ volume_id) { const ModelVolume &volume = *model_volumes[volume_id]; @@ -1034,9 +1097,11 @@ static PrintObjectRegions* generate_print_object_regions( if (const PrintObjectRegions::BoundingBox *bbox = find_volume_extents(layer_range, volume); bbox) { if (volume.is_model_part()) { // Add a model volume, assign an existing region or generate a new one. + PrintRegionConfig vol_cfg = region_config_from_model_volume(default_region_config, layer_range.config, volume, num_extruders, variant_index); + ObjectID volume_tag = compute_volume_tag(vol_cfg, volume); layer_range.volume_regions.push_back({ &volume, -1, - get_create_region(region_config_from_model_volume(default_region_config, layer_range.config, volume, num_extruders, variant_index)), + get_create_region(std::move(vol_cfg), volume_tag), bbox }); } else if (volume.is_negative_volume()) { @@ -1121,6 +1186,12 @@ static PrintObjectRegions* generate_print_object_regions( } } + + // Save the slot_per_part_enabled bit vector that produced these regions, so the guard in + // Print::apply can detect changes on the next call even when PrintRegionConfig did not + // change. Always written — including an empty vector — so the snapshot always reflects + // the exact input used to generate the current regions. + out->last_slot_per_part_enabled = slot_per_part_enabled; return out.release(); } @@ -1141,6 +1212,17 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ std::vector <unsigned int> used_filaments = this->extruders(true); std::unordered_set <unsigned int> used_filament_set(used_filaments.begin(), used_filaments.end()); + // A mixed slot is virtual: the filaments actually consumed are its components, so add them + // to the used set or they would be treated as unused and stripped from the config. + { + auto* is_mixed_opt = new_full_config.option<ConfigOptionBools>("filament_is_mixed"); + auto* comp_strs_opt = new_full_config.option<ConfigOptionStrings>("filament_mixed_components"); + if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) { + auto expanded = expand_mixed_filaments(used_filaments, is_mixed_opt->values, comp_strs_opt->values); + used_filament_set.insert(expanded.begin(), expanded.end()); + } + } + //new_full_config.normalize_fdm(used_filaments); new_full_config.normalize_fdm_1(); t_config_option_keys changed_keys = new_full_config.normalize_fdm_2(objects().size(), used_filaments.size()); @@ -1802,6 +1884,29 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ update_filament_self_index_cache(); } + // Per-part gradient: compute the per-slot enable bit vector once for this Print::apply pass. + // Used by generate_print_object_regions to decide which volumes deserve their own PrintRegion. + std::vector<bool> slot_per_part_enabled; + { + const auto &is_mixed_vec = m_config.filament_is_mixed.values; + const auto &grad_vec = m_config.filament_mixed_gradient.values; + const auto &per_part_vec = m_config.filament_mixed_gradient_per_part.values; + const auto &components_vec = m_config.filament_mixed_components.values; + slot_per_part_enabled.assign(is_mixed_vec.size(), false); + for (size_t i = 0; i < is_mixed_vec.size(); ++i) { + if (! is_mixed_vec[i]) + continue; + std::vector<unsigned int> comps = parse_mixed_components(i < components_vec.size() ? components_vec[i] : ""); + if (comps.size() != 2) + continue; + if (i >= grad_vec.size() || ! grad_vec[i]) + continue; + if (i >= per_part_vec.size() || ! per_part_vec[i]) + continue; + slot_per_part_enabled[i] = true; + } + } + // All regions now have distinct settings. // Check whether applying the new region config defaults we would get different regions, // update regions or create regions from scratch. @@ -1828,7 +1933,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ for (const ModelVolume *volume : volumes) { const std::vector<bool> &volume_used_facet_states = volume->mmu_segmentation_facets.get_data().used_states; - assert(volume_used_facet_states.size() == used_facet_states.size()); + // Paint data saved before the painted state range was extended deserializes a + // shorter used_states vector, so merge over the common prefix. for (size_t state_idx = 0; state_idx < std::min(volume_used_facet_states.size(), used_facet_states.size()); ++state_idx) used_facet_states[state_idx] |= volume_used_facet_states[state_idx]; } @@ -1862,6 +1968,15 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ update_apply_status((*it)->invalidate_state_by_config_options(old_config, new_config, diff_keys)); }, print_variant_index)) { + // Per-part gradient: PrintRegionConfig alone cannot reveal a change in which slots + // have per-part enabled, so compare against the snapshot taken when these regions + // were generated and regenerate on any difference (slot toggled, per-part moved + // between slots, eligibility changed via components / gradient / is_mixed). + if (print_object_regions->last_slot_per_part_enabled != slot_per_part_enabled) { + invalidate(); + model_object_status.print_object_regions_status = ModelObjectStatus::PrintObjectRegionsStatus::PartiallyValid; + print_regions_reshuffled = true; + } // Regions are valid, just keep them. } else { // Regions were reshuffled. @@ -1884,7 +1999,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ print_object.is_mm_painted() ? 0.f : float(print_object.config().xy_contour_compensation.value), painting_extruders, print_variant_index, - print_object.is_fuzzy_skin_painted()); + print_object.is_fuzzy_skin_painted(), + slot_per_part_enabled); } for (auto it = it_print_object; it != it_print_object_end; ++it) if ((*it)->m_shared_regions) { diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index cc991f91cc..100089fd24 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -2,6 +2,7 @@ #include "PrintConfigConstants.hpp" #include "ClipperUtils.hpp" #include "Config.hpp" +#include "FilamentMixer.hpp" #include "MaterialType.hpp" #include "I18N.hpp" #include "format.hpp" @@ -72,6 +73,8 @@ const std::vector<std::string> filament_extruder_override_keys = { "filament_deretraction_speed", "filament_retract_restart_extra", //not in filament_options_with_variant, added on 20250816 "filament_retraction_minimum_travel", + "filament_retract_length_toolchange", + "filament_retract_restart_extra_toolchange", // BBS: floats "filament_wipe_distance", // bools @@ -1127,8 +1130,7 @@ void PrintConfigDef::init_fff_params() // BBS def = this->add("supertack_plate_temp", coInts); def->label = L("Other layers"); - def->tooltip = L("Bed temperature for layers except the initial one. " - "A value of 0 means the filament does not support printing on the Cool Plate SuperTack."); + def->tooltip = L("This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack."); def->sidetext = L(u8"\u2103" /* °C */); // degrees Celsius, CIS languages need translation def->full_label = L("Bed temperature"); def->min = 0; @@ -2767,7 +2769,7 @@ void PrintConfigDef::init_fff_params() def = this->add("fan_cooling_layer_time", coFloats); def->label = L("Layer time"); def->tooltip = L("The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time."); - def->sidetext = L("s"); // seconds, CIS languages need translation + def->sidetext = L_CONTEXT("s", "second"); // seconds, CIS languages need translation def->min = 0; def->max = 1000; def->mode = comSimple; @@ -2920,7 +2922,7 @@ void PrintConfigDef::init_fff_params() def->label = L("Filament load time"); def->tooltip = L("Time to load new filament when switch filament. It's usually applicable for single-extruder multi-material machines. " "For tool changers or multi-tool machines, it's typically 0. For statistics only."); - def->sidetext = L("s"); // seconds, CIS languages need translation + def->sidetext = L_CONTEXT("s", "second"); // seconds, CIS languages need translation def->min = 0; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0.0)); @@ -2929,7 +2931,7 @@ void PrintConfigDef::init_fff_params() def->label = L("Filament unload time"); def->tooltip = L("Time to unload old filament when switch filament. It's usually applicable for single-extruder multi-material machines. " "For tool changers or multi-tool machines, it's typically 0. For statistics only."); - def->sidetext = L("s"); // seconds, CIS languages need translation + def->sidetext = L_CONTEXT("s", "second"); // seconds, CIS languages need translation def->min = 0; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0.0)); @@ -2938,7 +2940,7 @@ void PrintConfigDef::init_fff_params() def->label = L("Tool change time"); def->tooltip = L("Time taken to switch tools. It's usually applicable for tool changers or multi-tool machines. " "For single-extruder multi-material machines, it's typically 0. For statistics only."); - def->sidetext = L("s"); // seconds, CIS languages need translation + def->sidetext = L_CONTEXT("s", "second"); // seconds, CIS languages need translation def->min = 0; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat { 0. }); @@ -3084,7 +3086,7 @@ void PrintConfigDef::init_fff_params() def->tooltip = L("Time to wait after the filament is unloaded. " "May help to get reliable tool changes with flexible materials " "that may need more time to shrink to original dimensions."); - def->sidetext = L("s"); // seconds, CIS languages need translation + def->sidetext = L_CONTEXT("s", "second"); // seconds, CIS languages need translation def->min = 0; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloats { 0. }); @@ -3261,6 +3263,62 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBools { false }); + // Mixed-color filament. A slot flagged here is virtual: it is not loaded into any + // physical extruder, but resolved at slicing time into the physical filaments listed + // in filament_mixed_components, blended either by splitting each layer into + // sub-layers or by alternating whole layers (see enable_mixed_color_sublayer). + def = this->add("filament_is_mixed", coBools); + def->label = L("Is mixed filament"); + def->tooltip = L("Whether this filament slot is a mixed filament composed of multiple physical filaments"); + def->mode = comDevelop; + def->set_default_value(new ConfigOptionBools{false}); + + def = this->add("filament_mixed_components", coStrings); + def->label = L("Mixed filament components"); + def->tooltip = L("Comma-separated 1-based indices of component filaments, e.g. \"1,3\""); + def->mode = comDevelop; + def->set_default_value(new ConfigOptionStrings{""}); + + def = this->add("filament_mixed_sublayer_ratios", coStrings); + def->label = L("Mixed filament sublayer ratios"); + def->tooltip = L("Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\""); + def->mode = comDevelop; + def->set_default_value(new ConfigOptionStrings{""}); + + def = this->add("filament_mixed_gradient", coBools); + def->label = L("Mixed filament gradient"); + def->tooltip = L("Enable Z-direction gradient mode for mixed filament sub-layers. " + "When enabled, the sub-layer ratios vary linearly across layers."); + def->mode = comDevelop; + def->set_default_value(new ConfigOptionBools{false}); + + def = this->add("filament_mixed_gradient_range", coStrings); + def->label = L("Mixed filament gradient range"); + def->tooltip = L("Start and end ratios for the first component in gradient mode. " + "Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%."); + def->mode = comDevelop; + def->set_default_value(new ConfigOptionStrings{""}); + + def = this->add("filament_mixed_gradient_curve", coStrings); + def->label = L("Mixed filament gradient curve"); + def->tooltip = L("Optional Photoshop-style custom curve mapping Z progress to the first " + "component ratio. Encoded as pipe-separated control points, " + "either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override " + "is needed (empty token or \"nan\" means use PCHIP default). " + "x in [0,1]; y is clamped to the configured ratio range, " + "e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear " + "gradient_range is used instead."); + def->mode = comDevelop; + def->set_default_value(new ConfigOptionStrings{""}); + + def = this->add("filament_mixed_gradient_per_part", coBools); + def->label = L("Mixed filament per-part gradient"); + def->tooltip = L("When gradient mode is enabled, apply the gradient to each part of an " + "assembly independently rather than treating the whole assembly as one " + "Z range."); + def->mode = comDevelop; + def->set_default_value(new ConfigOptionBools{false}); + // defined in bits // 0 means cannot support, 1 means support // 0 bit: can support in left extruder @@ -3467,9 +3525,8 @@ void PrintConfigDef::init_fff_params() def = this->add("sparse_infill_smooth_factor", coPercent); def->label = L("Sparse infill smooth factor"); def->category = L("Strength"); - def->tooltip = L("Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, " - "while 100% produces the largest possible curves between adjacent infill lines. " - "Currently applies only to the Hilbert Curve."); + def->tooltip = L("Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, " + "while 100% produces the largest possible curves between adjacent infill lines."); def->sidetext = "%"; def->min = 0; def->max = 100; @@ -4148,7 +4205,7 @@ void PrintConfigDef::init_fff_params() "\nIt won't move fan commands from custom G-code (they act as a sort of 'barrier')." "\nIt won't move fan commands into the start G-code if the 'only custom start G-code' is activated." "\nUse 0 to deactivate."); - def->sidetext = L("s"); // seconds, CIS languages need translation + def->sidetext = L_CONTEXT("s", "second"); // seconds, CIS languages need translation def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0)); @@ -4164,7 +4221,7 @@ void PrintConfigDef::init_fff_params() "\nThis is useful for fans where a low PWM/power may be insufficient to get the fan started spinning from a stop, or to " "get the fan up to speed faster." "\nSet to 0 to deactivate."); - def->sidetext = L("s"); // seconds, CIS languages need translation + def->sidetext = L_CONTEXT("s", "second"); // seconds, CIS languages need translation def->min = 0; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0)); @@ -4259,6 +4316,15 @@ void PrintConfigDef::init_fff_params() def->readonly = false; def->set_default_value(new ConfigOptionEnum<GCodeFlavor>(gcfMarlinLegacy)); + def = this->add("gcode_skip_config_block", coBool); + def->label = L("Skip G-code config block"); + def->tooltip = L("Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. " + "This can help with printers whose firmware crashes when parsing these comment lines " + "(e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, " + "so importing it back into OrcaSlicer will not restore the configuration."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("pellet_modded_printer", coBool); def->label = L("Pellet Modded Printer"); def->tooltip = L("Enable this option if your printer uses pellets instead of filaments."); @@ -4292,7 +4358,7 @@ void PrintConfigDef::init_fff_params() "slow down."); def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(0)); - + //BBS def = this->add("infill_combination", coBool); def->label = L("Infill combination"); @@ -4799,7 +4865,7 @@ void PrintConfigDef::init_fff_params() def->label = L("Ironing expansion"); def->category = L("Quality"); def->tooltip = L("Expand or contract the ironing area."); - def->sidetext = L("mm"); + def->sidetext = L("mm"); // millimeters, CIS languages need translation def->min = -100; def->max = 100; def->mode = comExpert; @@ -4836,7 +4902,7 @@ void PrintConfigDef::init_fff_params() def->category = L("Quality"); def->tooltip = L("Minimum Z-layer height.\n" "Also controls the slicing plane."); - def->sidetext = L("mm"); + def->sidetext = L("mm"); // millimeters, CIS languages need translation def->min = 0; def->max = 100; def->mode = comExpert; @@ -5035,7 +5101,7 @@ void PrintConfigDef::init_fff_params() def->category = L("Machine limits"); def->readonly = false; def->tooltip = L("The allowed maximum output force of Y axis"); - def->sidetext = L("N"); + def->sidetext = L_CONTEXT("N", "Newton"); // Newtons, CIS languages need translation def->min = 0; def->mode = comDevelop; def->set_default_value(new ConfigOptionFloat(0)); @@ -5045,7 +5111,7 @@ void PrintConfigDef::init_fff_params() def->category = L("Machine limits"); def->readonly = false; def->tooltip = L("The machine bed mass load of Y axis"); - def->sidetext = L("g"); + def->sidetext = L_CONTEXT("g", "gram"); // grams, CIS languages need translation def->min = 0; def->mode = comDevelop; def->set_default_value(new ConfigOptionFloat(0)); @@ -5055,7 +5121,7 @@ void PrintConfigDef::init_fff_params() def->category = L("Machine limits"); def->readonly = false; def->tooltip = L("The allowed max printed mass on a plate"); - def->sidetext = L("g"); + def->sidetext = L_CONTEXT("g", "gram"); // grams, CIS languages need translation def->min = 0; def->mode = comDevelop; def->set_default_value(new ConfigOptionFloat(0)); @@ -5414,7 +5480,7 @@ void PrintConfigDef::init_fff_params() def = this->add("reduce_infill_retraction", coBool); def->label = L("Reduce infill retraction"); - def->tooltip = L("Don\'t retract when the travel is entirely within an infill area. That means the oozing can\'t been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped."); + def->tooltip = L("Don\'t retract when the travel is entirely within an infill area. That means the oozing can\'t been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped."); def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(false)); @@ -5669,7 +5735,7 @@ void PrintConfigDef::init_fff_params() def = this->add("retract_after_wipe", coPercents); def->label = L("Retract amount after wipe"); // xgettext:no-c-format, no-boost-format - def->tooltip = L("The length of fast retraction after wipe, relative to retraction length.\n" + def->tooltip = L("This is the length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value."); def->sidetext = "%"; def->mode = comExpert; @@ -5725,19 +5791,17 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionFloatsNullable{10}); def = this->add("retract_length_toolchange", coFloats); - def->label = L("Length"); - //def->full_label = L("Retraction Length (Toolchange)"); - def->full_label = "Retraction Length (Toolchange)"; - //def->tooltip = L("When retraction is triggered before changing tool, filament is pulled back " - // "by the specified amount (the length is measured on raw filament, before it enters " - // "the extruder)."); + def->label = L("Retraction Length (Toolchange)"); + def->tooltip = L("When retraction is triggered before changing tool, filament is pulled back " + "by the specified amount (the length is measured on raw filament, before it enters " + "the extruder)."); def->sidetext = L("mm"); // millimeters, CIS languages need translation def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloats { 10. }); def = this->add("z_hop", coFloats); def->label = L("Z-hop height"); - def->tooltip = L("Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing."); + def->tooltip = L("Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing."); def->sidetext = L("mm"); // millimeters, CIS languages need translation def->mode = comSimple; def->min = 0; @@ -5984,7 +6048,7 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionFloats { 0. }); def = this->add("retract_restart_extra_toolchange", coFloats); - def->label = L("Extra length on restart"); + def->label = L("Extra length on restart (Toolchange)"); def->tooltip = L("When the retraction is compensated after changing tool, the extruder will push " "this additional amount of filament."); def->sidetext = L("mm"); // millimeters, CIS languages need translation @@ -6315,7 +6379,7 @@ void PrintConfigDef::init_fff_params() def->label = L("Layer time"); def->tooltip = L("The printing speed in exported G-code will be slowed down when the estimated layer time is " "shorter than this value in order to get better cooling for these layers."); - def->sidetext = L("s"); // seconds, CIS languages need translation + def->sidetext = L_CONTEXT("s", "second"); // seconds, CIS languages need translation def->min = 0; def->max = 1000; def->mode = comSimple; @@ -6468,7 +6532,7 @@ void PrintConfigDef::init_fff_params() def->label = L("Preheat time"); def->tooltip = L("To reduce the waiting time after tool change, Orca can preheat the next tool while the current tool is still in use. " "This setting specifies the time in seconds to preheat the next tool. Orca will insert a M104 command to preheat the tool in advance."); - def->sidetext = L("s"); // seconds, CIS languages need translation + def->sidetext = L_CONTEXT("s", "second"); // seconds, CIS languages need translation def->min = 0; def->max = 120; def->mode = comAdvanced; @@ -6561,6 +6625,17 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(false)); + def = this->add("wait_for_temp_on_wipe_tower", coBool); + def->label = L("Wait for temperature on wipe tower"); + def->tooltip = L("Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe " + "tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on " + "the tower instead of the model, and the travel overlaps with the heating. " + "Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. " + "The firmware or tool change macro must not wait for the temperature itself. " + "When disabled, the temperature wait is issued right after the tool change command."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("wipe_tower_no_sparse_layers", coBool); def->label = L("No sparse layers (beta)"); @@ -7383,6 +7458,14 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloats { 1. }); + def = this->add("enable_mixed_color_sublayer", coBool); + def->label = L("Mixed color sublayer"); + def->tooltip = L("Enable mixed color sublayer splitting. When enabled, layers containing mixed color " + "filaments will be split into sub-layers to achieve color mixing effects."); + def->category = L("Quality"); + def->mode = comSimple; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("enable_prime_tower", coBool); def->label = L("Enable"); def->tooltip = L("The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects."); @@ -7941,7 +8024,7 @@ void PrintConfigDef::init_fff_params() def = this->add("machine_hotend_change_time", coFloat); def->label = L("Hotend change time"); def->tooltip = L("Time to change hotend."); - def->sidetext = L("s"); + def->sidetext = L_CONTEXT("s", "second"); def->min = 0; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0.0)); @@ -8154,10 +8237,12 @@ void PrintConfigDef::init_extruder_option_keys() "long_retractions_when_cut", "retract_after_wipe", "retract_before_wipe", + "retract_length_toolchange", "retract_lift_above", "retract_lift_below", "retract_lift_enforce", "retract_restart_extra", + "retract_restart_extra_toolchange", "retract_when_changing_layer", "retraction_distances_when_cut", "retraction_length", @@ -9245,6 +9330,8 @@ std::set<std::string> filament_options_with_variant = { "filament_retract_lift_below", "filament_retract_lift_enforce", "filament_retract_restart_extra", + "filament_retract_length_toolchange", + "filament_retract_restart_extra_toolchange", "filament_retraction_speed", "filament_deretraction_speed", "filament_retraction_minimum_travel", @@ -9582,7 +9669,15 @@ t_config_option_keys DynamicPrintConfig::normalize_fdm_2(int num_objects, int us ConfigOptionBool *enable_wrapping_opt = this->option<ConfigOptionBool>("enable_wrapping_detection"); bool enable_wrapping = enable_wrapping_opt != nullptr && enable_wrapping_opt->value; - if (!is_smooth_timelapse && !enable_wrapping && (used_filaments == 1 || (ps_opt->value == PrintSequence::ByObject && num_objects > 1))) { + bool has_mixed_filament = false; + { + auto *mixed_opt = this->option<ConfigOptionBools>("filament_is_mixed"); + if (mixed_opt) + has_mixed_filament = has_any_mixed_filament(mixed_opt->values); + } + if (!is_smooth_timelapse && !enable_wrapping + && ( (used_filaments == 1 && !has_mixed_filament) + || (ps_opt->value == PrintSequence::ByObject && num_objects > 1))) { if (ept_opt->value) { ept_opt->value = false; changed_keys.push_back("enable_prime_tower"); @@ -9770,7 +9865,15 @@ std::string DynamicPrintConfig::get_filament_type(std::string &displayed_filamen auto* filament_type = dynamic_cast<const ConfigOptionStrings*>(this->option("filament_type")); auto* filament_is_support = dynamic_cast<const ConfigOptionBools*>(this->option("filament_is_support")); - if (!filament_type) + // get_at() on an empty vector option is undefined behavior (.front() of an empty vector), + // and e.g. filament_id is never populated on a CLI from-scratch slice - treat an empty + // option the same as a missing one. + if (filament_id && filament_id->values.empty()) + filament_id = nullptr; + if (filament_is_support && filament_is_support->values.empty()) + filament_is_support = nullptr; + + if (!filament_type || filament_type->values.empty()) return ""; if (!filament_is_support) { @@ -9995,6 +10098,10 @@ int DynamicPrintConfig::update_values_from_single_to_multi(DynamicPrintConfig& m for (int index = 0; index < variant_count; index++) { + //variant_count is the variant column width, src_opt the value array; + //they disagree when the source was authored at a different width + if (index >= (int)src_opt->values.size()) + break; if (opt->values[index] > src_opt->values[index]) opt->values[index] = src_opt->values[index]; } @@ -10012,6 +10119,8 @@ int DynamicPrintConfig::update_values_from_single_to_multi(DynamicPrintConfig& m for (int index = 0; index < variant_count; index++) { + if (index >= (int)src_opt->values.size()) + break; if (opt->values[index].value > src_opt->values[index].value) opt->values[index] = src_opt->values[index]; } @@ -10206,6 +10315,10 @@ int DynamicPrintConfig::update_values_from_multi_to_multi(DynamicPrintConfig& ne for(auto idx : variant_indices){ assert(idx < old_count); + //the counts come from the variant columns, the arrays from the options; + //they disagree when a config was authored at a different variant width + if (idx >= old_count || new_variant_index >= (int)opt->values.size()) + continue; if (old_values[idx] < opt->values[new_variant_index]) opt->values[new_variant_index] = old_values[idx]; } @@ -10236,6 +10349,10 @@ int DynamicPrintConfig::update_values_from_multi_to_multi(DynamicPrintConfig& ne for(auto idx : variant_indices){ assert(idx < old_count); + //the counts come from the variant columns, the arrays from the options; + //they disagree when a config was authored at a different variant width + if (idx >= old_count || new_variant_index >= (int)opt->values.size()) + continue; if (old_values[idx] < opt->values[new_variant_index]) opt->values[new_variant_index] = old_values[idx]; } @@ -10266,6 +10383,8 @@ int DynamicPrintConfig::update_values_from_multi_to_multi(DynamicPrintConfig& ne for(auto idx : variant_indices){ assert(idx < old_count); + if (idx >= old_count || new_variant_index >= (int)opt->values.size()) + continue; if (old_values[idx]) //enabled opt->values[new_variant_index] = old_values[idx]; } @@ -10306,6 +10425,15 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector<st same_variant_indices.emplace_back(indices); } + //dst_values below is the destination PRINT preset's per-variant row, sized to its own + //print_extruder_variant; dst_extruder_variants is the PRINTER's list. They disagree until + //the print preset is re-selected, so size the row to the variant count before indexing it. + const size_t dst_variant_count = dst_extruder_variants.size(); + if (dst_variant_count == 0) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", Line %1%: empty destination variant list")%__LINE__; + return -1; + } + t_config_option_keys keys = this->keys(); for(auto& key : keys){ if(key_sets.find(key) == key_sets.end()) @@ -10321,7 +10449,13 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector<st { ConfigOptionFloatsNullable* opt = this->option<ConfigOptionFloatsNullable>(key); auto src_values = opt->values; - auto dst_values = dst_config.option<ConfigOptionFloatsNullable>(key) ->values; + const auto* dst_opt = dst_config.option<ConfigOptionFloatsNullable>(key); + if(!dst_opt){ + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: %2% missing from destination config")%__LINE__%key; + break; + } + auto dst_values = dst_opt->values; + dst_values.resize(dst_variant_count, ConfigOptionFloatsNullable::nil_value()); for(size_t dst_idx =0; dst_idx < same_variant_indices.size(); ++dst_idx){ auto& indices = same_variant_indices[dst_idx]; if(indices.empty()) @@ -10329,7 +10463,7 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector<st bool has_value = false; double target_value = std::numeric_limits<double>::max(); for(auto idx : indices){ - if(opt && idx < opt->values.size() && !opt->is_nil(idx)){ + if(idx < (int)opt->values.size() && !opt->is_nil(idx)){ has_value = true; target_value = std::min(target_value, src_values[idx]); } @@ -10345,7 +10479,13 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector<st { ConfigOptionFloatsOrPercentsNullable* opt = this->option<ConfigOptionFloatsOrPercentsNullable>(key); auto src_values = opt->values; - auto dst_values = dst_config.option<ConfigOptionFloatsOrPercentsNullable>(key) ->values; + const auto* dst_opt = dst_config.option<ConfigOptionFloatsOrPercentsNullable>(key); + if(!dst_opt){ + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: %2% missing from destination config")%__LINE__%key; + break; + } + auto dst_values = dst_opt->values; + dst_values.resize(dst_variant_count, ConfigOptionFloatsOrPercentsNullable::nil_value()); for(size_t dst_idx =0; dst_idx < same_variant_indices.size(); ++dst_idx){ auto& indices = same_variant_indices[dst_idx]; if(indices.empty()) @@ -10353,7 +10493,7 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector<st bool has_value = false; FloatOrPercent target_value{9999.f, true}; for(auto idx : indices){ - if(opt && !opt->is_nil(idx)){ + if(idx < (int)opt->values.size() && !opt->is_nil(idx)){ has_value = true; target_value = src_values[idx].value < target_value.value ? src_values[idx] : target_value; } @@ -10369,15 +10509,21 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector<st { ConfigOptionBoolsNullable* opt = this->option<ConfigOptionBoolsNullable>(key); auto src_values = opt->values; - auto dst_values = dst_config.option<ConfigOptionBoolsNullable>(key) ->values; + const auto* dst_opt = dst_config.option<ConfigOptionBoolsNullable>(key); + if(!dst_opt){ + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: %2% missing from destination config")%__LINE__%key; + break; + } + auto dst_values = dst_opt->values; + dst_values.resize(dst_variant_count, ConfigOptionBoolsNullable::nil_value()); for(size_t dst_idx =0; dst_idx < same_variant_indices.size(); ++dst_idx){ auto indices = same_variant_indices[dst_idx]; if(indices.empty()) continue; bool has_value = false; - bool target_value; + bool target_value = false; for(auto idx : indices){ - if(opt && !opt->is_nil(idx)){ + if(idx < (int)opt->values.size() && !opt->is_nil(idx)){ has_value = true; target_value = src_values[idx]; break; @@ -10402,6 +10548,16 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector<st } +void set_variant_override(ConfigOptionVectorBase &target, const ConfigOptionVectorBase &source, + const std::vector<int> &variant_index, int stride) +{ + // A single-value object or region override applies to every nozzle variant. + std::vector<int> indices = variant_index; + if (source.size() == 1 && !source.is_nil(0)) + std::fill(indices.begin(), indices.end(), 0); + target.set_to_index(&source, indices, stride); +} + //used for object/region config //use the smallest of multiple to single @@ -11479,7 +11635,7 @@ void update_static_print_config_from_dynamic(ConfigBase& config, const DynamicPr else { ConfigOptionVectorBase* opt_vec_src = static_cast<ConfigOptionVectorBase*>(opt_src); const ConfigOptionVectorBase* opt_vec_dest = static_cast<const ConfigOptionVectorBase*>(opt_dest); - opt_vec_src->set_to_index(opt_vec_dest, variant_index, stride); + set_variant_override(*opt_vec_src, *opt_vec_dest, variant_index, stride); } } } @@ -11720,6 +11876,23 @@ std::map<std::string, std::string> validate(const FullPrintConfig &cfg, bool und } } + // Mixed-color (混色) parameter validation. + { + const auto &is_mixed = cfg.filament_is_mixed.values; + const auto &comp_strs = cfg.filament_mixed_components.values; + const auto &ratio_strs = cfg.filament_mixed_sublayer_ratios.values; + const auto &gradient_flags = cfg.filament_mixed_gradient.values; + const auto &range_strs = cfg.filament_mixed_gradient_range.values; + const auto &curve_strs = cfg.filament_mixed_gradient_curve.values; + + std::map<std::string, std::string> mixed_errors = validate_mixed_filament_params( + is_mixed, comp_strs, ratio_strs, gradient_flags, + range_strs, curve_strs); + for (const auto &kv : mixed_errors) + if (error_message.find(kv.first) == error_message.end()) + error_message.emplace(kv.first, kv.second); + } + // The configuration is valid. return error_message; } @@ -11825,13 +11998,11 @@ CLIActionsConfigDef::CLIActionsConfigDef() def = this->add("load_defaultfila", coBool); def->label = L("Load default filaments"); def->tooltip = L("Load first filament as default for those not loaded."); - def->cli_params = "option"; def->set_default_value(new ConfigOptionBool(false)); def = this->add("min_save", coBool); def->label = L("Minimum save"); def->tooltip = L("Export 3MF with minimum size."); - def->cli_params = "option"; def->set_default_value(new ConfigOptionBool(false)); def = this->add("mtcpp", coInt); @@ -11857,7 +12028,6 @@ CLIActionsConfigDef::CLIActionsConfigDef() def = this->add("normative_check", coBool); def->label = L("Normative check"); def->tooltip = L("Check the normative items."); - def->cli_params = "option"; def->set_default_value(new ConfigOptionBool(true)); /*def = this->add("help_fff", coBool); @@ -12124,7 +12294,7 @@ CLIMiscConfigDef::CLIMiscConfigDef() def->cli_params = "level"; def->set_default_value(new ConfigOptionInt(1)); - def = this->add("logfile", coInt); + def = this->add("logfile", coString); def->label = L("Log file"); def->tooltip = L("Redirects debug logging to file.\n"); def->cli_params = "file"; @@ -12172,7 +12342,6 @@ CLIMiscConfigDef::CLIMiscConfigDef() def = this->add("skip_modified_gcodes", coBool); def->label = L("Skip modified G-code in 3MF"); def->tooltip = L("Skip the modified G-code in 3MF from printer or filament presets."); - def->cli_params = "option"; def->set_default_value(new ConfigOptionBool(false)); def = this->add("makerlab_name", coString); @@ -12202,14 +12371,12 @@ CLIMiscConfigDef::CLIMiscConfigDef() def = this->add("allow_newer_file", coBool); def->label = L("Allow 3MF with newer version to be sliced"); def->tooltip = L("Allow 3MF with newer version to be sliced."); - def->cli_params = "option"; def->set_default_value(new ConfigOptionBool(false)); def = this->add("allow_mix_temp", coBool); // internal use only, don't need translation def->label = "Allow filaments with high/low temperature to be printed together"; def->tooltip = "Allow filaments with high/low temperature to be printed together."; - def->cli_params = "option"; def->set_default_value(new ConfigOptionBool(false)); } diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 90aa1adb3d..330151c4d3 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -146,6 +146,29 @@ inline bool is_separable_infill_pattern(InfillPattern pattern) } } +// Orca: Infill patterns that round their corners by the "sparse_infill_smooth_factor" option. +// Grid, Triangles and Tri-hexagon only do so in their trapezoidal form, which is generated with more +// than one line per infill wall; a single line makes them plain crossing lines with nothing to round. +inline bool is_smoothable_infill_pattern(InfillPattern pattern, int multiline = 1) +{ + switch (pattern) { + case ipHilbertCurve: + case ipOctagramSpiral: + case ipLightning: + case ipHoneycomb: + case ip3DHoneycomb: + case ipConcentric: + case ipCrossHatch: + return true; + case ipGrid: + case ipTriangles: + case ipStars: + return multiline > 1; + default: + return false; + } +} + enum class IroningType { NoIroning, TopSurfaces, @@ -842,6 +865,9 @@ extern std::set<std::string> printer_options_with_variant_1; extern std::set<std::string> printer_options_with_variant_2; extern std::set<std::string> empty_options; +void set_variant_override(ConfigOptionVectorBase &target, const ConfigOptionVectorBase &source, + const std::vector<int> &variant_index, int stride = 1); + extern std::set<std::string> filament_dev_options; extern void update_static_print_config_from_dynamic(ConfigBase& config, const DynamicPrintConfig& dest_config, std::vector<int> variant_index, std::set<std::string>& key_set1, int stride = 1); @@ -1512,6 +1538,14 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionStrings, filament_colour)) ((ConfigOptionStrings, filament_vendor)) ((ConfigOptionBools, filament_is_support)) + // Mixed-color filament: a virtual slot realized from 2-3 physical filaments. + ((ConfigOptionBools, filament_is_mixed)) + ((ConfigOptionStrings, filament_mixed_components)) + ((ConfigOptionStrings, filament_mixed_sublayer_ratios)) + ((ConfigOptionBools, filament_mixed_gradient)) + ((ConfigOptionStrings, filament_mixed_gradient_range)) + ((ConfigOptionStrings, filament_mixed_gradient_curve)) + ((ConfigOptionBools, filament_mixed_gradient_per_part)) ((ConfigOptionInts, filament_printable)) ((ConfigOptionInts, filament_extruder_compatibility)) ((ConfigOptionFloats, filament_change_length)) @@ -1547,7 +1581,7 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionBool, gcode_add_line_number)) ((ConfigOptionBool, bbl_bed_temperature_gcode)) ((ConfigOptionEnum<GCodeFlavor>, gcode_flavor)) - + ((ConfigOptionBool, gcode_skip_config_block)) ((ConfigOptionFloat, time_cost)) ((ConfigOptionString, layer_change_gcode)) ((ConfigOptionString, time_lapse_gcode)) @@ -1660,6 +1694,7 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionBool, purge_in_prime_tower)) ((ConfigOptionBool, enable_filament_ramming)) ((ConfigOptionBool, tool_change_on_wipe_tower)) + ((ConfigOptionBool, wait_for_temp_on_wipe_tower)) ((ConfigOptionBool, support_multi_bed_types)) ((ConfigOptionBool, use_3mf)) @@ -1811,6 +1846,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionInts, nozzle_temperature_range_low)) ((ConfigOptionInts, nozzle_temperature_range_high)) ((ConfigOptionFloats, wipe_distance)) + ((ConfigOptionBool, enable_mixed_color_sublayer)) ((ConfigOptionBool, enable_prime_tower)) ((ConfigOptionBool, prime_tower_enable_framework)) // BBS: change wipe_tower_x and wipe_tower_y data type to floats to add partplate logic @@ -2393,6 +2429,55 @@ static void set_flush_volumes_matrix(std::vector<T> &out_matrix, const std::vect } } +template<class T> +static bool has_zero_flush_volume_for_used_filaments(const std::vector<T> &fv_matrix, + const std::vector<T> &flush_multipliers, + const std::vector<int> &used_filaments) +{ + if (used_filaments.size() < 2 || flush_multipliers.empty()) + return false; + + if (fv_matrix.size() % flush_multipliers.size() != 0) + return false; + + const size_t matrix_len = fv_matrix.size() / flush_multipliers.size(); + const size_t row_len = size_t(std::sqrt(double(matrix_len))); + if (row_len < 2 || row_len * row_len != matrix_len) + return false; + + std::vector<int> filtered_filaments; + filtered_filaments.reserve(used_filaments.size()); + for (int filament_id : used_filaments) { + if (filament_id <= 0 || filament_id > int(row_len)) + continue; + if (std::find(filtered_filaments.begin(), filtered_filaments.end(), filament_id) == filtered_filaments.end()) + filtered_filaments.push_back(filament_id); + } + if (filtered_filaments.size() < 2) + return false; + + for (T multiplier : flush_multipliers) { + if (multiplier == 0) + return true; + } + + for (size_t nozzle_idx = 0; nozzle_idx < flush_multipliers.size(); nozzle_idx++) { + const size_t block_offset = nozzle_idx * matrix_len; + for (int from_id : filtered_filaments) { + for (int to_id : filtered_filaments) { + if (from_id == to_id) + continue; + + const size_t matrix_idx = block_offset + size_t(from_id - 1) * row_len + size_t(to_id - 1); + if (matrix_idx < fv_matrix.size() && fv_matrix[matrix_idx] == 0) + return true; + } + } + } + + return false; +} + size_t get_extruder_index(const GCodeConfig& config, unsigned int filament_id); } // namespace Slic3r @@ -2412,7 +2497,8 @@ namespace cereal { archive(serialization_key_ordinal); assert(serialization_key_ordinal > 0); auto it = Slic3r::print_config_def.by_serialization_key_ordinal.find(serialization_key_ordinal); - assert(it != Slic3r::print_config_def.by_serialization_key_ordinal.end()); + if (it == Slic3r::print_config_def.by_serialization_key_ordinal.end()) + throw std::runtime_error("VendorCache: unknown serialization_key_ordinal " + std::to_string(serialization_key_ordinal) + " - cache is stale"); config.set_key_value(it->second->opt_key, it->second->load_option_from_archive(archive)); } } diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index b2a92f11a6..a228bb7436 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -906,7 +906,7 @@ void PrintObject::detect_overhangs_for_lift() Layer& lower_layer = *layer.lower_layer; ExPolygons overhangs = diff_ex(layer.lslices, offset_ex(lower_layer.lslices, scale_(min_overlap))); - layer.loverhangs = std::move(offset2_ex(overhangs, -0.1f * scale_(line_width), 0.1f * scale_(line_width))); + layer.loverhangs = offset2_ex(overhangs, -0.1f * scale_(line_width), 0.1f * scale_(line_width)); layer.loverhangs_bbox = get_extents(layer.loverhangs); } }); @@ -2666,7 +2666,7 @@ void PrintObject::bridge_over_infill() // SECTION to gather and filter surfaces for expanding, and then cluster them by layer { tbb::concurrent_vector<CandidateSurface> candidate_surfaces; - tbb::parallel_for(tbb::blocked_range<size_t>(0, this->layers().size()), [po = static_cast<const PrintObject *>(this), &candidate_surfaces, has_lightning_infill](tbb::blocked_range<size_t> r) { + tbb::parallel_for(tbb::blocked_range<size_t>(0, this->layers().size()), [po = static_cast<const PrintObject *>(this), &candidate_surfaces](tbb::blocked_range<size_t> r) { PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); for (size_t lidx = r.begin(); lidx < r.end(); lidx++) { const Layer *layer = po->get_layer(lidx); @@ -3812,7 +3812,7 @@ static void apply_to_print_region_config(PrintRegionConfig &out, const DynamicPr else { ConfigOptionVectorBase* opt_vec_src = static_cast<ConfigOptionVectorBase*>(my_opt); const ConfigOptionVectorBase* opt_vec_dest = static_cast<const ConfigOptionVectorBase*>(it->second.get()); - opt_vec_src->set_to_index(opt_vec_dest, variant_index, 1); + set_variant_override(*opt_vec_src, *opt_vec_dest, variant_index); } } } diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 07eb885eda..d16e381604 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -304,11 +304,16 @@ static std::vector<std::vector<ExPolygons>> slices_to_regions( float z = zs[z_idx]; int idx_first_printable_region = -1; bool complex = false; + std::vector<int> printable_region_ids; for (int idx_region = 0; idx_region < int(layer_range.volume_regions.size()); ++ idx_region) { const PrintObjectRegions::VolumeRegion ®ion = layer_range.volume_regions[idx_region]; if (region.bbox->min().z() <= z && region.bbox->max().z() >= z) { - if (idx_first_printable_region == -1 && region.model_volume->is_model_part()) + if (region.model_volume->is_model_part()) + printable_region_ids.push_back(idx_region); + + if (idx_first_printable_region == -1 && region.model_volume->is_model_part()) { idx_first_printable_region = idx_region; + } else if (idx_first_printable_region != -1) { // Test for overlap with some other region. for (int idx_region2 = idx_first_printable_region; idx_region2 < idx_region; ++ idx_region2) { @@ -324,8 +329,10 @@ static std::vector<std::vector<ExPolygons>> slices_to_regions( if (complex) zs_complex.push_back({ z_idx, z }); else if (idx_first_printable_region >= 0) { - const PrintObjectRegions::VolumeRegion ®ion = layer_range.volume_regions[idx_first_printable_region]; - slices_by_region[region.region->print_object_region_id()][z_idx] = std::move(volume_slices_find_by_id(volume_slices, region.model_volume->id()).slices[z_idx]); + for (int printable_region_id : printable_region_ids) { + const PrintObjectRegions::VolumeRegion ®ion = layer_range.volume_regions[printable_region_id]; + append(slices_by_region[region.region->print_object_region_id()][z_idx], std::move(volume_slices_find_by_id(volume_slices, region.model_volume->id()).slices[z_idx])); + } } } } @@ -527,7 +534,7 @@ bool groupingVolumes(std::vector<VolumeSlices> objSliceByVolume, std::vector<gro } tbb::parallel_for(tbb::blocked_range<int>(0, osvIndex.size()), - [&osvIndex, &objSliceByVolume, &offsetValue, &resolution](const tbb::blocked_range<int>& range) { + [&osvIndex, &objSliceByVolume, &resolution](const tbb::blocked_range<int>& range) { for (auto k = range.begin(); k != range.end(); ++k) { for (ExPolygon& poly_ex : objSliceByVolume[osvIndex[k][0]].slices[osvIndex[k][1]]) poly_ex.douglas_peucker(resolution); @@ -535,7 +542,7 @@ bool groupingVolumes(std::vector<VolumeSlices> objSliceByVolume, std::vector<gro }); tbb::parallel_for(tbb::blocked_range<int>(0, osvIndex.size()), - [&osvIndex, &objSliceByVolume,&offsetValue, &resolution](const tbb::blocked_range<int>& range) { + [&osvIndex, &objSliceByVolume,&offsetValue](const tbb::blocked_range<int>& range) { for (auto k = range.begin(); k != range.end(); ++k) { objSliceByVolume[osvIndex[k][0]].slices[osvIndex[k][1]] = offset_ex(objSliceByVolume[osvIndex[k][0]].slices[osvIndex[k][1]], offsetValue); } diff --git a/src/libslic3r/SLA/Hollowing.hpp b/src/libslic3r/SLA/Hollowing.hpp index b57513fe72..f6beaadb24 100644 --- a/src/libslic3r/SLA/Hollowing.hpp +++ b/src/libslic3r/SLA/Hollowing.hpp @@ -44,9 +44,6 @@ struct DrainHole : pos(p), normal(n), radius(r), height(h), failed(fl) {} - 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)); } diff --git a/src/libslic3r/SLA/IndexedMesh.cpp b/src/libslic3r/SLA/IndexedMesh.cpp index b879e3f48b..65d9d90b34 100644 --- a/src/libslic3r/SLA/IndexedMesh.cpp +++ b/src/libslic3r/SLA/IndexedMesh.cpp @@ -56,12 +56,12 @@ public: int & i, Eigen::Matrix<double, 1, 3> &closest) { - size_t idx_unsigned = 0; - Vec3d closest_vec3d(closest); - double dist = + size_t idx_unsigned { 0 }; + Vec3d closest_vec3d { Vec3d::Zero() }; + const double dist { AABBTreeIndirect::squared_distance_to_indexed_triangle_set( its.vertices, its.indices, m_tree, point, idx_unsigned, - closest_vec3d); + closest_vec3d) }; i = int(idx_unsigned); closest = closest_vec3d; return dist; diff --git a/src/libslic3r/SLA/SupportTreeBuilder.cpp b/src/libslic3r/SLA/SupportTreeBuilder.cpp index 86339d2acf..4080c4fc3f 100644 --- a/src/libslic3r/SLA/SupportTreeBuilder.cpp +++ b/src/libslic3r/SLA/SupportTreeBuilder.cpp @@ -1,4 +1,6 @@ +#ifndef NOMINMAX #define NOMINMAX +#endif #include <libslic3r/SLA/SupportTreeBuilder.hpp> #include <libslic3r/SLA/SupportTreeBuildsteps.hpp> diff --git a/src/libslic3r/Semver.hpp b/src/libslic3r/Semver.hpp index 4d64b1c7db..d3683b4eb8 100644 --- a/src/libslic3r/Semver.hpp +++ b/src/libslic3r/Semver.hpp @@ -190,6 +190,19 @@ public: os << self.to_string(); return os; } + + // cereal: round-trip through the standard 3-part string (major.minor.patch). + // to_string() uses a BBS 4-part format that semver_parse() cannot read back. + template<class Archive> + std::string save_minimal(const Archive&) const { return to_string_sf(); } + template<class Archive> + void load_minimal(const Archive&, const std::string& s) { + auto v = Semver::parse(s); + if (! v) + throw std::runtime_error("Semver: cannot parse serialized version: " + s); + *this = std::move(*v); + } + private: semver_t ver; diff --git a/src/libslic3r/Shape/TextShape.cpp b/src/libslic3r/Shape/TextShape.cpp index dce731af19..4f32b9d857 100644 --- a/src/libslic3r/Shape/TextShape.cpp +++ b/src/libslic3r/Shape/TextShape.cpp @@ -199,7 +199,7 @@ static void MakeMesh(TopoDS_Shape& theSolid, TriangleMesh& theMesh) for (Standard_Integer aNodeIter = 1; aNodeIter <= aTriangulation->NbNodes(); ++aNodeIter) { gp_Pnt aPnt = aTriangulation->Node(aNodeIter); aPnt.Transform(aTrsf); - points.emplace_back(std::move(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z()))); + points.emplace_back(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z())); } //BBS: copy triangles const TopAbs_Orientation anOrientation = anExpSF.Current().Orientation(); diff --git a/src/libslic3r/Support/SupportCommon.cpp b/src/libslic3r/Support/SupportCommon.cpp index 15640cb0f2..e734608803 100644 --- a/src/libslic3r/Support/SupportCommon.cpp +++ b/src/libslic3r/Support/SupportCommon.cpp @@ -65,11 +65,13 @@ std::pair<SupportGeneratorLayersPtr, SupportGeneratorLayersPtr> generate_interfa const bool smooth_supports = support_params.support_style != smsGrid; SupportGeneratorLayersPtr &interface_layers = base_and_interface_layers.first; SupportGeneratorLayersPtr &base_interface_layers = base_and_interface_layers.second; - // The user-facing interface layer counts include the contact layer. Internally, - // contact layers are generated separately, so only the remaining layers are - // projected into intermediate interface/base-interface layers here. - const size_t num_top_interface_layers = support_params.has_top_contacts ? support_params.num_top_interface_layers - 1 : 0; - const size_t num_bottom_interface_layers = support_params.has_bottom_contacts ? support_params.num_bottom_interface_layers - 1 : 0; + // Contacts printed separately consume one requested interface layer. Organic + // bottom contacts are projection seeds and are not printed separately. + const bool organic_tree = support_params.support_style == smsTreeOrganic; + const size_t num_top_interface_layers = support_params.has_top_contacts ? + support_params.num_top_interface_layers - 1 : 0; + const size_t num_bottom_interface_layers = support_params.has_bottom_contacts ? + support_params.num_bottom_interface_layers - (organic_tree ? 0 : 1) : 0; const size_t num_top_base_interface_layers = std::min(support_params.num_top_base_interface_layers, num_top_interface_layers); const size_t num_bottom_base_interface_layers = std::min(support_params.num_bottom_base_interface_layers, num_bottom_interface_layers); const size_t num_top_interface_layers_only = num_top_interface_layers - num_top_base_interface_layers; @@ -132,7 +134,7 @@ std::pair<SupportGeneratorLayersPtr, SupportGeneratorLayersPtr> generate_interfa return nullptr; }; tbb::parallel_for(tbb::blocked_range<int>(0, int(intermediate_layers.size())), - [&bottom_contacts, &top_contacts, &top_interface_layers, &top_base_interface_layers, &intermediate_layers, &insert_layer, &support_params, + [&bottom_contacts, &top_contacts, &top_interface_layers, &top_base_interface_layers, &intermediate_layers, &insert_layer, num_top_interface_layers, num_bottom_interface_layers, num_top_base_interface_layers, num_bottom_base_interface_layers, num_top_interface_layers_only, num_bottom_interface_layers_only, snug_supports, &interface_layers, &base_interface_layers](const tbb::blocked_range<int>& range) { @@ -1234,10 +1236,6 @@ static void modulate_extrusion_by_overlapping_layers( (fragment_end.is_start ? &polyline.points.front() : &polyline.points.back()); } private: - ExtrusionPathFragmentEndPointAccessor& operator=(const ExtrusionPathFragmentEndPointAccessor&) { - return *this; - } - const std::vector<ExtrusionPathFragment> &m_path_fragments; }; const coord_t search_radius = 7; @@ -1656,28 +1654,32 @@ void generate_support_toolpaths( if (top_contact_layer.could_merge(interface_layer) && ! raft_layer) top_contact_layer.merge(std::move(interface_layer)); } - if (!bottom_interfaces && support_params.can_merge_support_regions) { - if (base_layer.could_merge(bottom_contact_layer)) - base_layer.merge(std::move(bottom_contact_layer)); - else if (base_layer.empty() && ! bottom_contact_layer.empty() && ! bottom_contact_layer.layer->bridging) - base_layer = std::move(bottom_contact_layer); - } else if (bottom_contact_layer.could_merge(top_contact_layer) && ! raft_layer) { - if (top_interfaces && bottom_interfaces) { - top_contact_layer.merge(std::move(bottom_contact_layer)); - } else if (bottom_interfaces) { - top_contact_layer.set_polygons_to_extrude( - diff(top_contact_layer.polygons_to_extrude(), bottom_contact_layer.polygons_to_extrude())); - } else { - bottom_contact_layer.set_polygons_to_extrude( - diff(bottom_contact_layer.polygons_to_extrude(), top_contact_layer.polygons_to_extrude())); - } - } else if (bottom_contact_layer.could_merge(interface_layer) && ! organic_tree) { - const bool interface_layer_is_bottom = interface_layer.layer->layer_type == SupporLayerType::BottomInterface; - if (bottom_interfaces && interface_layer_is_bottom) { - bottom_contact_layer.merge(std::move(interface_layer)); - } else { - bottom_contact_layer.set_polygons_to_extrude( - diff(bottom_contact_layer.polygons_to_extrude(), interface_layer.polygons_to_extrude())); + // Orca: Organic bottom contacts are projection seeds, not same-layer toolpaths. + // Do not merge them into another same-layer support region. + if (!organic_tree) { + if (!bottom_interfaces && support_params.can_merge_support_regions) { + if (base_layer.could_merge(bottom_contact_layer)) + base_layer.merge(std::move(bottom_contact_layer)); + else if (base_layer.empty() && ! bottom_contact_layer.empty() && ! bottom_contact_layer.layer->bridging) + base_layer = std::move(bottom_contact_layer); + } else if (bottom_contact_layer.could_merge(top_contact_layer) && ! raft_layer) { + if (top_interfaces && bottom_interfaces) { + top_contact_layer.merge(std::move(bottom_contact_layer)); + } else if (bottom_interfaces) { + top_contact_layer.set_polygons_to_extrude( + diff(top_contact_layer.polygons_to_extrude(), bottom_contact_layer.polygons_to_extrude())); + } else { + bottom_contact_layer.set_polygons_to_extrude( + diff(bottom_contact_layer.polygons_to_extrude(), top_contact_layer.polygons_to_extrude())); + } + } else if (bottom_contact_layer.could_merge(interface_layer)) { + const bool interface_layer_is_bottom = interface_layer.layer->layer_type == SupporLayerType::BottomInterface; + if (bottom_interfaces && interface_layer_is_bottom) { + bottom_contact_layer.merge(std::move(interface_layer)); + } else { + bottom_contact_layer.set_polygons_to_extrude( + diff(bottom_contact_layer.polygons_to_extrude(), interface_layer.polygons_to_extrude())); + } } } diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index cd60489e75..12ac275592 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -1588,7 +1588,7 @@ static inline std::tuple<Polygons, Polygons, double> detect_contacts( // Cache support trimming polygons derived from lower layer polygons, possible merged with "on build plate only" trimming polygons. auto slices_margin_update = - [&slices_margin, &layer, &lower_layer, &lower_layer_polygons, buildplate_only, has_enforcer, &annotations, layer_id] + [&slices_margin, &lower_layer, &lower_layer_polygons, buildplate_only, has_enforcer, &annotations, layer_id] (float slices_margin_offset, float no_interface_offset) { if (slices_margin.offset != slices_margin_offset) { slices_margin.offset = slices_margin_offset; diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index 7a769c77d1..70c33aab0d 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -32,7 +32,7 @@ namespace Slic3r::TreeSupport3D using namespace std::literals; // or warning -// had to use a define beacuse the macro processing inside macro BOOST_LOG_TRIVIAL() +// had to use a define because the macro processing inside macro BOOST_LOG_TRIVIAL() #define error_level_not_in_cache debug //FIXME Machine border is currently ignored. diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index d4a43a4767..2b06f11244 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -842,7 +842,7 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) // normal overhang ExPolygons lower_layer_offseted = offset_ex(lower_polys, support_offset_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS); - overhangs_all_layers[layer_nr] = std::move(diff_ex(curr_polys, lower_layer_offseted)); + overhangs_all_layers[layer_nr] = diff_ex(curr_polys, lower_layer_offseted); double duration{ std::chrono::duration_cast<second_>(clock_::now() - t0).count() }; if (duration > 30 || overhangs_all_layers[layer_nr].size() > 100) { @@ -1396,7 +1396,7 @@ void TreeSupport::generate_toolpaths() raft_areas.push_back(expoly); } - raft_areas = std::move(offset_ex(raft_areas, scale_(object_config.raft_first_layer_expansion))); + raft_areas = offset_ex(raft_areas, scale_(object_config.raft_first_layer_expansion)); size_t layer_nr = 0; for (; layer_nr < m_slicing_params.base_raft_layers; layer_nr++) { @@ -1522,9 +1522,9 @@ void TreeSupport::generate_toolpaths() erSupportMaterialInterface : erSupportMaterial; make_perimeter_and_inner_brim(ts_layer->support_fills.entities, poly, wall_count, flow, brim_role); - polys = std::move(offset_ex(poly, -flow.scaled_spacing())); + polys = offset_ex(poly, -flow.scaled_spacing()); } else if (area_group.type == SupportLayer::Roof1stLayer) { - polys = std::move(offset_ex(poly, 0.5*support_flow.scaled_width())); + polys = offset_ex(poly, 0.5*support_flow.scaled_width()); } else { polys.push_back(poly); @@ -2269,7 +2269,7 @@ void TreeSupport::draw_circles() // Inside the gap: remove only the part overlapping the contact surface, keep the rest. if (bottom_gap_height > EPSILON && layer_bottom_z < band_gap_top - EPSILON) { any_gap_cleared = true; - comp_poly = std::move(diff_ex(comp_poly, band.surfaces)); + comp_poly = diff_ex(comp_poly, band.surfaces); } // Overlaps interface band @@ -2304,7 +2304,7 @@ void TreeSupport::draw_circles() ExPolygons comp_interface = band_ex.empty() ? ExPolygons {} : intersection_ex(comp_poly, band_ex); if (!comp_interface.empty()) { append(new_floor_areas, comp_interface); - comp_poly = std::move(diff_ex(comp_poly, offset_ex(comp_interface, 10))); + comp_poly = diff_ex(comp_poly, offset_ex(comp_interface, 10)); } } @@ -2396,7 +2396,7 @@ void TreeSupport::draw_circles() ts_layer->lslices.emplace_back(*expoly); } - ts_layer->lslices = std::move(union_ex(ts_layer->lslices)); + ts_layer->lslices = union_ex(ts_layer->lslices); //Must update bounding box which is used in avoid crossing perimeter ts_layer->lslices_bboxes.clear(); ts_layer->lslices_bboxes.reserve(ts_layer->lslices.size()); @@ -2474,7 +2474,7 @@ void TreeSupport::draw_circles() if (global_lightning_infill) { //search overhangs globally - overhang = std::move(diff_ex(offset_ex(base_areas_lower, -2.0 * scale_(support_extrusion_width)), base_areas)); + overhang = diff_ex(offset_ex(base_areas_lower, -2.0 * scale_(support_extrusion_width)), base_areas); } else { @@ -2485,13 +2485,13 @@ void TreeSupport::draw_circles() Polygon rev_hole = hole; rev_hole.make_counter_clockwise(); ExPolygons ex_hole; - ex_hole.emplace_back(std::move(ExPolygon(rev_hole))); + ex_hole.emplace_back(ExPolygon(rev_hole)); for (auto& other_area : base_areas) //if (&other_area != &base_area) - ex_hole = std::move(diff_ex(ex_hole, other_area)); - overhang = std::move(union_ex(overhang, ex_hole)); + ex_hole = diff_ex(ex_hole, other_area); + overhang = union_ex(overhang, ex_hole); } - overhang = std::move(intersection_ex(overhang, offset_ex(base_areas_lower, -0.5 * scale_(support_extrusion_width)))); + overhang = intersection_ex(overhang, offset_ex(base_areas_lower, -0.5 * scale_(support_extrusion_width))); } overhangs.emplace_back(to_polygons(overhang)); @@ -2667,7 +2667,7 @@ void TreeSupport::drop_nodes() SupportNode::diameter_angle_scale_factor = diameter_angle_scale_factor; float DO_NOT_MOVER_UNDER_MM = is_slim ? 0 : 5; // do not move contact points under 5mm - auto get_max_move_dist = [this, &config, tan_angle, wall_count, support_extrusion_width](const SupportNode *node, int power = 1) { + auto get_max_move_dist = [this, tan_angle, support_extrusion_width](const SupportNode *node, int power = 1) { if (node->max_move_dist == 0) { node->radius = get_radius(node); node->max_move_dist = std::min(tan_angle * node->height, support_extrusion_width); @@ -2746,7 +2746,7 @@ void TreeSupport::drop_nodes() m_object->print()->set_status(60 + int(10 * (1 - float(layer_nr) / contact_nodes.size())), _u8L("Generating support"));// (boost::format(_u8L("Support: propagate branches at layer %d")) % layer_nr).str()); - Polygons layer_contours = std::move(m_ts_data->get_contours_with_holes(obj_layer_nr)); + Polygons layer_contours = m_ts_data->get_contours_with_holes(obj_layer_nr); //std::unordered_map<Line, bool, LineHash>& mst_line_x_layer_contour_cache = m_mst_line_x_layer_contour_caches[layer_nr]; tbb::concurrent_unordered_map<Line, bool, LineHash> mst_line_x_layer_contour_cache; auto is_line_cut_by_contour = [&mst_line_x_layer_contour_cache,&layer_contours](Point a, Point b) @@ -3763,7 +3763,7 @@ const ExPolygons& TreeSupportData::calculate_avoidance(const RadiusLayerPair& ke } const ExPolygons &collision = get_collision(radius, layer_nr); avoidance_areas.insert(avoidance_areas.end(), collision.begin(), collision.end()); - avoidance_areas = std::move(union_ex(avoidance_areas)); + avoidance_areas = union_ex(avoidance_areas); auto ret = m_avoidance_cache.insert({key, std::move(avoidance_areas)}); //assert(ret.second); return ret.first->second; diff --git a/src/libslic3r/Support/TreeSupport.hpp b/src/libslic3r/Support/TreeSupport.hpp index e0446ad5f1..61e030ef86 100644 --- a/src/libslic3r/Support/TreeSupport.hpp +++ b/src/libslic3r/Support/TreeSupport.hpp @@ -204,8 +204,9 @@ public: clear_nodes(); } - TreeSupportData(TreeSupportData&&) = default; - TreeSupportData& operator=(TreeSupportData&&) = default; + // Deleted by the tbb::spin_mutex member. + TreeSupportData(TreeSupportData&&) = delete; + TreeSupportData& operator=(TreeSupportData&&) = delete; TreeSupportData(const TreeSupportData&) = delete; TreeSupportData& operator=(const TreeSupportData&) = delete; diff --git a/src/libslic3r/TexturePainting.cpp b/src/libslic3r/TexturePainting.cpp new file mode 100644 index 0000000000..9f83f282cd --- /dev/null +++ b/src/libslic3r/TexturePainting.cpp @@ -0,0 +1,726 @@ +#include "TexturePainting.hpp" + +#include <algorithm> +#include <cmath> +#include <map> +#include <set> +#include <utility> + +#include <opencv2/core.hpp> +#include <opencv2/imgcodecs.hpp> +#include <opencv2/imgproc.hpp> + +#include <boost/log/trivial.hpp> + +#include "TextureToColor/TextureToColor.hpp" +#include "TextureToColor/ColorUtils.hpp" + +#include "Model.hpp" +#include "TriangleMesh.hpp" +#include "TriangleSelector.hpp" + +namespace Slic3r { + +static cv::Mat decode_texture_image(const TextureImage& img) { + if (img.data.empty()) + return {}; + + // Raw encoded image data (PNG/JPEG) from glTF loader: width == -1 + if (img.width <= 0 || img.height <= 0) { + std::vector<unsigned char> buf(img.data.begin(), img.data.end()); + cv::Mat raw(1, static_cast<int>(buf.size()), CV_8UC1, buf.data()); + cv::Mat decoded = cv::imdecode(raw, cv::IMREAD_COLOR); + return decoded; + } + + int cv_type = (img.channels == 4) ? CV_8UC4 : CV_8UC3; + std::vector<unsigned char> pixel_buf(img.data.begin(), img.data.end()); + cv::Mat src(img.height, img.width, cv_type, pixel_buf.data()); + + cv::Mat bgr; + if (img.channels == 4) + cv::cvtColor(src, bgr, cv::COLOR_RGBA2BGR); + else if (img.channels == 3) + cv::cvtColor(src, bgr, cv::COLOR_RGB2BGR); + else + return {}; + + return bgr; +} + +static void build_tex2color_mesh( + const TexturedMesh& textured, + tex2color::TriMesh& mesh, + std::vector<std::vector<Vec2f>>& uv_coords) +{ + const size_t nv = textured.vertices.size(); + const size_t nf = textured.indices.size(); + + mesh.vertices.resize(nv); + for (size_t i = 0; i < nv; ++i) { + mesh.vertices[i] = Vec3f( + textured.vertices[i][0], + textured.vertices[i][1], + textured.vertices[i][2]); + } + + mesh.indices.resize(nf); + for (size_t i = 0; i < nf; ++i) { + mesh.indices[i] = Vec3i32( + textured.indices[i][0], + textured.indices[i][1], + textured.indices[i][2]); + } + + uv_coords.resize(nf); + for (size_t fi = 0; fi < nf; ++fi) { + uv_coords[fi].resize(3); + for (int vi = 0; vi < 3; ++vi) { + if (textured.has_face_uvs()) { + int uv_idx = textured.uv_indices[fi][vi]; + if (uv_idx >= 0 && static_cast<size_t>(uv_idx) < textured.uv_coords.size()) { + uv_coords[fi][vi] = Vec2f( + textured.uv_coords[uv_idx][0], + textured.uv_coords[uv_idx][1]); + } else { + uv_coords[fi][vi] = Vec2f(0.f, 0.f); + } + } else { + int vtx_idx = textured.indices[fi][vi]; + if (vtx_idx >= 0 && static_cast<size_t>(vtx_idx) < textured.uvs.size()) { + uv_coords[fi][vi] = Vec2f( + textured.uvs[vtx_idx][0], + textured.uvs[vtx_idx][1]); + } else { + uv_coords[fi][vi] = Vec2f(0.f, 0.f); + } + } + } + } +} + +static void extract_painted_mesh( + const tex2color::TriMesh& color_mesh, + const std::vector<std::array<std::size_t,3>>& face_colors, + PaintedMesh& painted) +{ + const size_t nv = color_mesh.vertices.size(); + const size_t nf = color_mesh.indices.size(); + + painted.vertices.resize(nv); + for (size_t i = 0; i < nv; ++i) { + const auto& v = color_mesh.vertices[i]; + painted.vertices[i] = {v.x(), v.y(), v.z()}; + } + + painted.indices.resize(nf); + for (size_t i = 0; i < nf; ++i) { + const auto& f = color_mesh.indices[i]; + painted.indices[i] = {f[0], f[1], f[2]}; + } + + painted.face_colors = face_colors; + + std::set<std::array<std::size_t,3>> unique_colors(face_colors.begin(), face_colors.end()); + painted.cluster_colors.assign(unique_colors.begin(), unique_colors.end()); +} + +// Build a vertically-stacked atlas from multiple textures and remap per-face UVs. +// +// Sub-textures are laid out left-aligned (x=0) at successive y offsets, with +// atlas_w taken as the maximum width across all sub-textures. UVs must therefore +// be remapped on BOTH axes so that faces belonging to a sub-texture narrower +// than atlas_w sample inside that sub-texture's region (left side of the atlas) +// instead of the right-side zero-padding. Materials that carry only a baseColor +// (no map_Kd / glTF baseColorTexture) get their own 1x1 swatch at the bottom of +// the atlas so their faces sample the correct flat colour rather than being +// silently aliased onto textures[0]. +static bool build_multi_texture_atlas( + const TexturedMesh& textured, + cv::Mat& out_atlas, + std::vector<std::vector<Vec2f>>& out_uv_coords) +{ + std::vector<cv::Mat> decoded; + decoded.reserve(textured.textures.size()); + for (const auto& ti : textured.textures) + decoded.push_back(decode_texture_image(ti)); + + const bool has_mapping = !textured.material_texture_map.empty(); + const size_t nf = textured.indices.size(); + + auto resolve_tex_idx = [&](int mat_idx) -> int { + if (!has_mapping || mat_idx < 0 + || static_cast<size_t>(mat_idx) >= textured.material_texture_map.size()) + return -1; + const int ti = textured.material_texture_map[mat_idx]; + if (ti < 0 || static_cast<size_t>(ti) >= decoded.size() || decoded[ti].empty()) + return -1; + return ti; + }; + + // Determine atlas width (max width across all textures) and per-texture row offsets. + int atlas_w = 0; + int atlas_h = 0; + std::vector<int> y_offsets(decoded.size(), 0); + int first_usable_tex = -1; + for (size_t i = 0; i < decoded.size(); ++i) { + if (decoded[i].empty()) continue; + if (first_usable_tex < 0) first_usable_tex = static_cast<int>(i); + y_offsets[i] = atlas_h; + atlas_w = std::max(atlas_w, decoded[i].cols); + atlas_h += decoded[i].rows; + } + if (atlas_w == 0 || atlas_h == 0) + return false; + + // Collect materials that have a baseColor but no usable texture so we can + // route their faces to a dedicated 1x1 solid swatch instead of aliasing + // them onto textures[0]. + std::map<int, int> mat_solid_y; // mat_idx -> y row in atlas + std::map<int, std::array<float,4>> mat_solid_color; // mat_idx -> baseColor (RGBA) + for (size_t fi = 0; fi < nf; ++fi) { + const int mat_idx = (fi < textured.material_ids.size()) ? textured.material_ids[fi] : -1; + if (mat_idx < 0) continue; + if (resolve_tex_idx(mat_idx) >= 0) continue; + if (static_cast<size_t>(mat_idx) >= textured.material_colors.size()) continue; + if (mat_solid_y.find(mat_idx) != mat_solid_y.end()) continue; + mat_solid_y[mat_idx] = atlas_h++; + mat_solid_color[mat_idx] = textured.material_colors[mat_idx]; + } + + out_atlas = cv::Mat::zeros(atlas_h, atlas_w, CV_8UC3); + for (size_t i = 0; i < decoded.size(); ++i) { + if (decoded[i].empty()) continue; + cv::Mat roi = out_atlas(cv::Rect(0, y_offsets[i], decoded[i].cols, decoded[i].rows)); + decoded[i].copyTo(roi); + } + for (const auto& kv : mat_solid_color) { + const auto& c = kv.second; + // OpenCV stores BGR; baseColor is RGBA in [0,1]. + out_atlas.at<cv::Vec3b>(mat_solid_y[kv.first], 0) = cv::Vec3b( + static_cast<uchar>(std::clamp(c[2] * 255.f, 0.f, 255.f)), + static_cast<uchar>(std::clamp(c[1] * 255.f, 0.f, 255.f)), + static_cast<uchar>(std::clamp(c[0] * 255.f, 0.f, 255.f))); + } + + out_uv_coords.resize(nf); + for (size_t fi = 0; fi < nf; ++fi) { + const int mat_idx = (fi < textured.material_ids.size()) ? textured.material_ids[fi] : -1; + const int tex_idx = resolve_tex_idx(mat_idx); + + // Pick the atlas region this face samples from. + int y_off = 0, x_off = 0, th = atlas_h, tw = atlas_w; + bool use_solid = false; + if (tex_idx >= 0) { + y_off = y_offsets[tex_idx]; + th = decoded[tex_idx].rows; + tw = decoded[tex_idx].cols; + } else if (mat_idx >= 0 && mat_solid_y.count(mat_idx) > 0) { + y_off = mat_solid_y[mat_idx]; + th = 1; + tw = 1; + use_solid = true; + } else if (first_usable_tex >= 0) { + // Last-resort fallback: faces without a material or without any + // baseColor still need somewhere to sample; the first usable + // texture preserves legacy behaviour and, with the per-axis + // remapping below, no longer aliases onto the zero-padded right + // margin even when sub-textures have unequal widths. + y_off = y_offsets[first_usable_tex]; + th = decoded[first_usable_tex].rows; + tw = decoded[first_usable_tex].cols; + } + + out_uv_coords[fi].resize(3); + for (int vi = 0; vi < 3; ++vi) { + float u = 0.f, v = 0.f; + if (textured.has_face_uvs()) { + int uv_idx = textured.uv_indices[fi][vi]; + if (uv_idx >= 0 && static_cast<size_t>(uv_idx) < textured.uv_coords.size()) { + u = textured.uv_coords[uv_idx][0]; + v = textured.uv_coords[uv_idx][1]; + } + } else { + int vtx_idx = textured.indices[fi][vi]; + if (vtx_idx >= 0 && static_cast<size_t>(vtx_idx) < textured.uvs.size()) { + u = textured.uvs[vtx_idx][0]; + v = textured.uvs[vtx_idx][1]; + } + } + if (use_solid) { + // Aim at the centre of the 1x1 swatch so bilinear sampling + // (in tex2color) cannot drift into neighbouring rows. + const float u_atlas = (x_off + 0.5f) / static_cast<float>(atlas_w); + const float v_atlas = (y_off + 0.5f) / static_cast<float>(atlas_h); + out_uv_coords[fi][vi] = Vec2f(u_atlas, v_atlas); + } else { + // Wrap to [0,1) on both axes (OBJ tile UVs may step outside + // the unit square), then scale by the sub-texture extents so + // samples land inside its actual region. Without scaling u, + // any sub-texture narrower than atlas_w would have all its + // faces sampled from the right-side zero-padding. + u = u - std::floor(u); + v = v - std::floor(v); + const float u_atlas = (x_off + u * tw) / static_cast<float>(atlas_w); + const float v_atlas = (y_off + v * th) / static_cast<float>(atlas_h); + out_uv_coords[fi][vi] = Vec2f(u_atlas, v_atlas); + } + } + } + return true; +} + +bool texture_to_painting( + const TexturedMesh& textured, + PaintedMesh& painted, + const TexturePaintingSettings& settings, + PaintProgressCallback progress, + PaintCancelCallback cancel) +{ + if (textured.vertices.empty() || textured.indices.empty() || textured.textures.empty()) + return false; + + cv::Mat texture; + tex2color::TriMesh input_mesh; + std::vector<std::vector<Vec2f>> uv_coords; + + const bool multi_tex = textured.textures.size() > 1 && !textured.material_texture_map.empty(); + + if (multi_tex) { + if (!build_multi_texture_atlas(textured, texture, uv_coords)) + return false; + // Build mesh geometry (atlas UVs already computed above) + const size_t nv = textured.vertices.size(); + const size_t nf = textured.indices.size(); + input_mesh.vertices.resize(nv); + for (size_t i = 0; i < nv; ++i) + input_mesh.vertices[i] = Vec3f( + textured.vertices[i][0], textured.vertices[i][1], textured.vertices[i][2]); + input_mesh.indices.resize(nf); + for (size_t i = 0; i < nf; ++i) + input_mesh.indices[i] = Vec3i32( + textured.indices[i][0], textured.indices[i][1], textured.indices[i][2]); + } else { + texture = decode_texture_image(textured.textures[0]); + if (texture.empty()) + return false; + build_tex2color_mesh(textured, input_mesh, uv_coords); + } + + tex2color::TextureToColorSettings algo_settings; + algo_settings.target_colors_num = settings.target_colors_num; + algo_settings.smooth_weight = settings.smooth_weight; + algo_settings.oversampling_iters = settings.oversampling_iters; + switch (settings.mesh_repair_decision) { + case TexturePaintingSettings::MeshRepairDecision::Ask: + algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::Ask; + break; + case TexturePaintingSettings::MeshRepairDecision::RepairAndImport: + algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::RepairAndImport; + break; + case TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair: + default: + algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::ImportWithoutRepair; + break; + } + + tex2color::AlgoProgressCallback algo_progress = nullptr; + if (progress) { + algo_progress = [&progress](tex2color::AlgoProgress p) { + progress(p.percent, p.message); + }; + } + + tex2color::AlgoCancelCallback algo_cancel = nullptr; + if (cancel) { + algo_cancel = [&cancel]() -> bool { return cancel(); }; + } + + tex2color::TriMesh color_mesh; + std::vector<std::array<std::size_t,3>> face_colors; + algo_settings.mesh_repair_decision_required = settings.mesh_repair_decision_required; + algo_settings.mesh_repair_callback = settings.mesh_repair_callback; + + bool ok = tex2color::TextureToColor( + input_mesh, uv_coords, texture, + color_mesh, face_colors, + algo_settings, algo_progress, algo_cancel); + + if (!ok) + return false; + + extract_painted_mesh(color_mesh, face_colors, painted); + return true; +} + +bool face_colors_to_painting( + const TexturedMesh& mesh, + PaintedMesh& painted, + const TexturePaintingSettings& settings, + PaintProgressCallback progress, + PaintCancelCallback cancel) +{ + if (mesh.vertices.empty() || mesh.indices.empty() || mesh.precomputed_face_colors.empty()) + return false; + + // Build tex2color::TriMesh from input geometry + tex2color::TriMesh input_mesh; + input_mesh.vertices.resize(mesh.vertices.size()); + for (size_t i = 0; i < mesh.vertices.size(); ++i) + input_mesh.vertices[i] = Vec3f(mesh.vertices[i][0], mesh.vertices[i][1], mesh.vertices[i][2]); + input_mesh.indices.resize(mesh.indices.size()); + for (size_t i = 0; i < mesh.indices.size(); ++i) + input_mesh.indices[i] = Vec3i32(mesh.indices[i][0], mesh.indices[i][1], mesh.indices[i][2]); + + // Forward settings to tex2color + tex2color::TextureToColorSettings algo_settings; + algo_settings.target_colors_num = settings.target_colors_num; + algo_settings.smooth_weight = settings.smooth_weight; + switch (settings.mesh_repair_decision) { + case TexturePaintingSettings::MeshRepairDecision::Ask: + algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::Ask; + break; + case TexturePaintingSettings::MeshRepairDecision::RepairAndImport: + algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::RepairAndImport; + break; + case TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair: + default: + algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::ImportWithoutRepair; + break; + } + algo_settings.mesh_repair_decision_required = settings.mesh_repair_decision_required; + algo_settings.mesh_repair_callback = settings.mesh_repair_callback; + + tex2color::AlgoProgressCallback algo_progress = nullptr; + if (progress) { + algo_progress = [&progress](tex2color::AlgoProgress p) { + progress(p.percent, p.message); + }; + } + tex2color::AlgoCancelCallback algo_cancel = nullptr; + if (cancel) { + algo_cancel = [&cancel]() -> bool { return cancel(); }; + } + + tex2color::TriMesh out_mesh; + std::vector<std::array<std::size_t,3>> out_face_colors; + bool ok = tex2color::ClusterAndSmooth( + input_mesh, mesh.precomputed_face_colors, out_mesh, out_face_colors, + algo_settings, algo_progress, algo_cancel, + mesh.precomputed_vertex_colors); + + if (!ok) + return false; + + extract_painted_mesh(out_mesh, out_face_colors, painted); + return true; +} + +double compute_delta_e( + const std::array<std::size_t,3>& rgb1, + const std::array<float,4>& rgba2) +{ + return tex2color::color_utils::calc_rgb_color_difference_by_ciede2000( + rgb1, + { + static_cast<std::size_t>(rgba2[0] * 255.0f), + static_cast<std::size_t>(rgba2[1] * 255.0f), + static_cast<std::size_t>(rgba2[2] * 255.0f) + }); +} + +std::vector<FilamentMatch> match_clusters_to_filaments( + const std::vector<std::array<std::size_t,3>>& cluster_colors, + const std::vector<std::array<float,4>>& filament_colors, + const std::vector<std::string>& /*filament_names*/) +{ + std::vector<FilamentMatch> matches(cluster_colors.size()); + + for (size_t ci = 0; ci < cluster_colors.size(); ++ci) { + matches[ci].cluster_index = static_cast<int>(ci); + matches[ci].cluster_color = cluster_colors[ci]; + matches[ci].delta_e = 1e9; + + for (size_t fi = 0; fi < filament_colors.size(); ++fi) { + double de = compute_delta_e(cluster_colors[ci], filament_colors[fi]); + if (de < matches[ci].delta_e) { + matches[ci].delta_e = de; + matches[ci].filament_index = static_cast<int>(fi); + matches[ci].filament_color = filament_colors[fi]; + } + } + } + return matches; +} + +bool apply_painted_mesh_to_volume( + const PaintedMesh& painted, + const std::vector<FilamentMatch>& matches, + ModelVolume& volume) +{ + if (painted.face_colors.empty() || matches.empty()) + return false; + + const auto& cluster_colors = painted.cluster_colors; + std::map<std::array<std::size_t,3>, int> color_to_filament; + for (const auto& m : matches) { + if (m.cluster_index >= 0 && m.cluster_index < (int)cluster_colors.size() && m.filament_index >= 0) + color_to_filament[cluster_colors[m.cluster_index]] = m.filament_index; + } + + indexed_triangle_set its; + its.vertices.resize(painted.vertices.size()); + for (size_t i = 0; i < painted.vertices.size(); ++i) { + its.vertices[i] = Vec3f( + painted.vertices[i][0], + painted.vertices[i][1], + painted.vertices[i][2]); + } + its.indices.resize(painted.indices.size()); + for (size_t i = 0; i < painted.indices.size(); ++i) { + its.indices[i] = Vec3i32( + painted.indices[i][0], + painted.indices[i][1], + painted.indices[i][2]); + } + + TriangleMesh new_mesh(std::move(its)); + + // The volume already went through ModelObject::add_volume -> + // center_geometry_after_creation, which translated its mesh by + // -source.mesh_offset (and folded that shift into the volume + // transformation). The painted mesh, however, is derived from the + // raw textured mesh and is therefore expressed in the original + // un-centered coordinate frame. Reuse the exact recorded shift to + // align it -- do NOT compute it from the bounding-box centers of + // the two meshes: tex2color::TextureToColor performs subdivision + // and CGAL polygon-soup repair, so the painted vertex count and + // bbox no longer match the original textured mesh and a bbox- + // center alignment would silently displace the geometry. + // + // If the model has been scaled by Model::convert_from_meters / + // convert_from_imperial_units after load, the painted mesh fed + // here is already in millimetres (Model::convert_* also scales + // texture_mesh in place) while source.mesh_offset was recorded + // before the conversion and therefore still lives in the original + // pre-scaled frame. Bring it into the same frame as the painted + // vertices so the alignment shift below stays correct on the + // textured-import path. This compensation is scoped to this + // function so that other (non-textured) import paths are not + // affected. + Vec3d mesh_offset = volume.source.mesh_offset; + double unit_scale = 1.0; + if (volume.source.is_converted_from_meters) + unit_scale = 1000.0; + else if (volume.source.is_converted_from_inches) + unit_scale = 25.4; + if (unit_scale != 1.0) + mesh_offset *= unit_scale; + + if (!mesh_offset.isApprox(Vec3d::Zero())) + new_mesh.translate(-mesh_offset.cast<float>()); + new_mesh.set_init_shift(mesh_offset); + + // Log bbox drift for diagnostics. Subdivision + CGAL polygon-soup + // repair routinely changes vertex count and bbox, so moderate drift + // is expected and must not block the apply. + if (!new_mesh.empty() && !volume.mesh().empty()) { + const Vec3d new_center = new_mesh.bounding_box().center(); + const Vec3d cur_center = volume.mesh().bounding_box().center(); + const double diag = volume.mesh().bounding_box().size().norm(); + const double drift = (new_center - cur_center).norm(); + if (drift > 0.05 * std::max(1.0, diag)) + BOOST_LOG_TRIVIAL(warning) + << "apply_painted_mesh_to_volume: painted bbox center drifted by " + << drift << " (bbox diag=" << diag + << ", unit_scale=" << unit_scale + << ", from_meters=" << volume.source.is_converted_from_meters + << ", from_inches=" << volume.source.is_converted_from_inches << ")"; + else if (drift > 1e-3 * std::max(1.0, diag)) + BOOST_LOG_TRIVIAL(info) + << "apply_painted_mesh_to_volume: minor bbox drift " + << drift << " (bbox diag=" << diag + << ", unit_scale=" << unit_scale << ")"; + } + + volume.set_mesh(std::move(new_mesh)); + volume.calculate_convex_hull(); + + // Re-center the replaced mesh so its bbox center sits at the origin, + // matching what center_geometry_after_creation did for the original mesh. + // CGAL repair / subdivision may shift the bbox center (drift); without + // re-centering, the volume offset (which was computed for the original + // centered mesh) no longer matches, causing the model to float or clip. + // Pass false to keep source.mesh_offset unchanged. + volume.center_geometry_after_creation(false); + volume.invalidate_convex_hull_2d(); + + // Mesh geometry has been replaced; any per-face annotation indexed + // against the previous triangle set is now stale. mmu_segmentation_facets + // is rewritten below from the new selector; reset the others so future + // import paths that carry support / seam / fuzzy_skin painting cannot + // leak indices from the old mesh into the new one. + volume.supported_facets.reset(); + volume.fuzzy_skin_facets.reset(); + volume.seam_facets.reset(); + + if (ModelObject* obj = volume.get_object()) + obj->invalidate_bounding_box(); + + TriangleSelector selector(volume.mesh()); + for (size_t fi = 0; fi < painted.face_colors.size() && fi < (size_t)volume.mesh().its.indices.size(); ++fi) { + auto it = color_to_filament.find(painted.face_colors[fi]); + if (it != color_to_filament.end()) { + int extruder_idx = it->second; + auto state = static_cast<EnforcerBlockerType>( + static_cast<int>(EnforcerBlockerType::Extruder1) + extruder_idx); + if (state <= EnforcerBlockerType::ExtruderMax) + selector.set_facet(static_cast<int>(fi), state); + } + } + + volume.mmu_segmentation_facets.set(selector); + return true; +} + +bool decode_texture_to_pixels( + const TextureImage& img, + std::vector<unsigned char>& out_pixels, + int& out_w, int& out_h) +{ + cv::Mat decoded = decode_texture_image(img); + if (decoded.empty()) + return false; + + // decoded is BGR, CV_8UC3 + out_w = decoded.cols; + out_h = decoded.rows; + size_t nbytes = (size_t)out_w * out_h * 3; + out_pixels.resize(nbytes); + + if (decoded.isContinuous()) { + std::memcpy(out_pixels.data(), decoded.data, nbytes); + } else { + for (int r = 0; r < out_h; ++r) + std::memcpy(out_pixels.data() + r * out_w * 3, decoded.ptr(r), out_w * 3); + } + return true; +} + +// Sample face color from texture using 3 explicit UV values (centroid + bilinear). +static std::array<std::size_t,3> sample_face_from_uvs( + const cv::Mat& tex, + const std::array<float,2>& uv0, + const std::array<float,2>& uv1, + const std::array<float,2>& uv2) +{ + float cu = (uv0[0] + uv1[0] + uv2[0]) / 3.f; + float cv_val = (uv0[1] + uv1[1] + uv2[1]) / 3.f; + + cu = cu - std::floor(cu); + cv_val = cv_val - std::floor(cv_val); + + float fx = cu * (tex.cols - 1); + float fy = cv_val * (tex.rows - 1); + + int x0 = std::clamp(static_cast<int>(fx), 0, tex.cols - 1); + int y0 = std::clamp(static_cast<int>(fy), 0, tex.rows - 1); + int x1 = std::min(x0 + 1, tex.cols - 1); + int y1 = std::min(y0 + 1, tex.rows - 1); + + float wx = fx - x0; + float wy = fy - y0; + + const int ch = tex.channels(); + auto sample = [&](int row, int col) -> std::array<float,3> { + const uchar* ptr = tex.data + row * tex.step[0] + col * ch; + return {static_cast<float>(ptr[2]), static_cast<float>(ptr[1]), static_cast<float>(ptr[0])}; + }; + + auto c00 = sample(y0, x0); + auto c10 = sample(y0, x1); + auto c01 = sample(y1, x0); + auto c11 = sample(y1, x1); + + std::array<std::size_t,3> color; + for (int i = 0; i < 3; ++i) { + float top = c00[i] * (1.f - wx) + c10[i] * wx; + float bot = c01[i] * (1.f - wx) + c11[i] * wx; + color[i] = static_cast<std::size_t>(std::clamp(top * (1.f - wy) + bot * wy, 0.f, 255.f)); + } + return color; +} + +// Legacy overload: look up UVs from per-vertex array by vertex indices. +static std::array<std::size_t,3> sample_face_from_texture( + const cv::Mat& tex, + const std::vector<std::array<float,2>>& uvs, + const std::array<int,3>& face) +{ + std::array<float,2> uv0 = {0.f, 0.f}, uv1 = {0.f, 0.f}, uv2 = {0.f, 0.f}; + if (face[0] >= 0 && static_cast<size_t>(face[0]) < uvs.size()) uv0 = uvs[face[0]]; + if (face[1] >= 0 && static_cast<size_t>(face[1]) < uvs.size()) uv1 = uvs[face[1]]; + if (face[2] >= 0 && static_cast<size_t>(face[2]) < uvs.size()) uv2 = uvs[face[2]]; + return sample_face_from_uvs(tex, uv0, uv1, uv2); +} + +bool sample_original_face_colors( + const TexturedMesh& textured, + std::vector<std::array<std::size_t,3>>& out_face_colors) +{ + if (textured.indices.empty()) + return false; + + // Decode all textures up front + std::vector<cv::Mat> decoded_textures; + decoded_textures.reserve(textured.textures.size()); + for (const auto& ti : textured.textures) { + decoded_textures.push_back(decode_texture_image(ti)); + } + + const bool has_mapping = !textured.material_texture_map.empty(); + const size_t nf = textured.indices.size(); + out_face_colors.resize(nf); + + for (size_t fi = 0; fi < nf; ++fi) { + int mat_idx = (fi < textured.material_ids.size()) ? textured.material_ids[fi] : -1; + + int tex_idx = -1; + if (has_mapping && mat_idx >= 0 && static_cast<size_t>(mat_idx) < textured.material_texture_map.size()) + tex_idx = textured.material_texture_map[mat_idx]; + else if (!decoded_textures.empty()) + tex_idx = 0; // fallback: single-texture model + + if (tex_idx >= 0 && static_cast<size_t>(tex_idx) < decoded_textures.size() + && !decoded_textures[tex_idx].empty()) { + if (textured.has_face_uvs()) { + const auto& ui = textured.uv_indices[fi]; + auto get_uv = [&](int vi) -> std::array<float,2> { + int idx = ui[vi]; + if (idx >= 0 && static_cast<size_t>(idx) < textured.uv_coords.size()) + return textured.uv_coords[idx]; + return {0.f, 0.f}; + }; + out_face_colors[fi] = sample_face_from_uvs( + decoded_textures[tex_idx], get_uv(0), get_uv(1), get_uv(2)); + } else { + out_face_colors[fi] = sample_face_from_texture( + decoded_textures[tex_idx], textured.uvs, textured.indices[fi]); + } + } else if (has_mapping && mat_idx >= 0 + && static_cast<size_t>(mat_idx) < textured.material_colors.size()) { + // No texture — use baseColorFactor as solid color + const auto& c = textured.material_colors[mat_idx]; + out_face_colors[fi] = { + static_cast<std::size_t>(std::clamp(c[0] * 255.f, 0.f, 255.f)), + static_cast<std::size_t>(std::clamp(c[1] * 255.f, 0.f, 255.f)), + static_cast<std::size_t>(std::clamp(c[2] * 255.f, 0.f, 255.f)) + }; + } else { + out_face_colors[fi] = {192, 192, 192}; // default gray + } + } + return true; +} + +} // namespace Slic3r diff --git a/src/libslic3r/TexturePainting.hpp b/src/libslic3r/TexturePainting.hpp new file mode 100644 index 0000000000..fc4688620b --- /dev/null +++ b/src/libslic3r/TexturePainting.hpp @@ -0,0 +1,137 @@ +#pragma once + +#include <array> +#include <cstddef> +#include <functional> +#include <memory> +#include <string> +#include <vector> + +struct indexed_triangle_set; + +namespace Slic3r { + +class TriangleMesh; +class ModelVolume; + +struct TextureImage { + int width = 0; + int height = 0; + int channels = 4; + std::vector<unsigned char> data; +}; + +struct TexturedMesh { + std::vector<std::array<float,3>> vertices; + std::vector<std::array<int,3>> indices; + std::vector<std::array<float,2>> uvs; + std::vector<TextureImage> textures; + std::vector<int> material_ids; + // material index -> index in textures[] (-1 if no texture, use material_colors) + std::vector<int> material_texture_map; + // per-material baseColorFactor (RGBA 0-1), indexed by material index + std::vector<std::array<float,4>> material_colors; + + // Per-face independent UV support (for OBJ where the same vertex can have + // different texture coordinates on different faces). + std::vector<std::array<float,2>> uv_coords; // UV coordinate pool + std::vector<std::array<int,3>> uv_indices; // per-face UV indices into uv_coords + + bool has_face_uvs() const { return !uv_indices.empty() && !uv_coords.empty(); } + + // Pre-computed per-face colors (e.g. from OBJ vertex colors or MTL Kd). + // When non-empty, the pipeline skips texture decode/sample/oversample and + // consumes these instead of sampling a texture. + // Each entry is {R, G, B} in [0..255]. + std::vector<std::array<std::size_t,3>> precomputed_face_colors; + + // Per-vertex colors from OBJ (RGBA, [0..1]), indexed by vertex index. + // On a low-poly mesh these are quantized into a small palette and the mesh is + // split along the resulting cluster boundaries, so color borders stay sharp + // instead of being averaged away into a single color per face. + std::vector<std::array<float,4>> precomputed_vertex_colors; +}; + +struct PaintedMesh { + std::vector<std::array<float,3>> vertices; + std::vector<std::array<int,3>> indices; + std::vector<std::array<std::size_t,3>> face_colors; // per-face RGB [0..255] + std::vector<std::array<std::size_t,3>> cluster_colors; +}; + +using PaintProgressCallback = std::function<void(int percent, const char* message)>; +using PaintCancelCallback = std::function<bool()>; +using PaintMeshRepairCallback = std::function<bool(const indexed_triangle_set& mesh, + indexed_triangle_set& repaired_mesh, + std::function<void(const char* message, unsigned progress)> progress_callback, + std::function<bool()> cancel_callback, + std::string* error_message)>; + +struct TexturePaintingSettings { + std::size_t target_colors_num = 4; + double smooth_weight = 0.5; + std::size_t oversampling_iters = 0; + enum class MeshRepairDecision { + Ask, + ImportWithoutRepair, + RepairAndImport + }; + MeshRepairDecision mesh_repair_decision = MeshRepairDecision::ImportWithoutRepair; + bool* mesh_repair_decision_required = nullptr; + PaintMeshRepairCallback mesh_repair_callback; +}; + +struct FilamentMatch { + int cluster_index = -1; + int filament_index = -1; + double delta_e = 0.0; + std::array<std::size_t,3> cluster_color = {0,0,0}; + std::array<float,4> filament_color = {0,0,0,1}; +}; + +bool texture_to_painting( + const TexturedMesh& textured, + PaintedMesh& painted, + const TexturePaintingSettings& settings = {}, + PaintProgressCallback progress = nullptr, + PaintCancelCallback cancel = nullptr); +// Turn pre-computed per-face colors into a painted mesh, skipping texture decode +// and UV sampling. A low-poly mesh that also carries precomputed_vertex_colors is +// split along quantized color boundaries, which replaces its geometry. +bool face_colors_to_painting( + const TexturedMesh& mesh, + PaintedMesh& painted, + const TexturePaintingSettings& settings = {}, + PaintProgressCallback progress = nullptr, + PaintCancelCallback cancel = nullptr); + + +std::vector<FilamentMatch> match_clusters_to_filaments( + const std::vector<std::array<std::size_t,3>>& cluster_colors, + const std::vector<std::array<float,4>>& filament_colors, + const std::vector<std::string>& filament_names); + +double compute_delta_e( + const std::array<std::size_t,3>& rgb1, + const std::array<float,4>& rgba2); + +bool apply_painted_mesh_to_volume( + const PaintedMesh& painted, + const std::vector<FilamentMatch>& matches, + ModelVolume& volume); + +// Decode a TextureImage (which may contain raw PNG/JPEG bytes) into BGR pixel data. +// On success, populates out_pixels (BGR, 3 bytes/pixel) and sets out_w/out_h. +bool decode_texture_to_pixels( + const TextureImage& img, + std::vector<unsigned char>& out_pixels, + int& out_w, int& out_h); + +// Sample per-face colors from the correct texture per material_ids. +// Uses material_texture_map / material_colors for multi-material GLBs. +// Falls back to textures[0] when the mapping is absent. +bool sample_original_face_colors( + const TexturedMesh& textured, + std::vector<std::array<std::size_t,3>>& out_face_colors); + +} // namespace Slic3r diff --git a/src/libslic3r/TextureToColor/Callbacks.hpp b/src/libslic3r/TextureToColor/Callbacks.hpp new file mode 100644 index 0000000000..70084f585d --- /dev/null +++ b/src/libslic3r/TextureToColor/Callbacks.hpp @@ -0,0 +1,15 @@ +#pragma once +#include <functional> + +namespace Slic3r { namespace tex2color { + +struct AlgoProgress { + int percent = 0; + const char* message = ""; +}; + +using AlgoProgressCallback = std::function<void(AlgoProgress)>; +using AlgoCancelCallback = std::function<bool()>; + +} // namespace tex2color +} // namespace Slic3r diff --git a/src/libslic3r/TextureToColor/CgalUtils.hpp b/src/libslic3r/TextureToColor/CgalUtils.hpp new file mode 100644 index 0000000000..109d454827 --- /dev/null +++ b/src/libslic3r/TextureToColor/CgalUtils.hpp @@ -0,0 +1,173 @@ +#pragma once +#include "TriMesh.hpp" +#include <CGAL/Exact_predicates_inexact_constructions_kernel.h> +#include <CGAL/Surface_mesh.h> +#include <CGAL/Polygon_mesh_processing/repair.h> +#include <chrono> +#include <cstdio> +#include <map> +#include <queue> +#include <unordered_map> +#include <unordered_set> +#include <vector> + +namespace Slic3r { namespace tex2color { +namespace cgalutils { + +using Kernel = CGAL::Exact_predicates_inexact_constructions_kernel; +using CGALMesh = CGAL::Surface_mesh<Kernel::Point_3>; + +inline CGALMesh trimesh_to_cgal(const TriMesh& mesh) { + CGALMesh cm; + std::vector<CGALMesh::Vertex_index> vmap(mesh.vertices.size()); + for (size_t i = 0; i < mesh.vertices.size(); ++i) + vmap[i] = cm.add_vertex(Kernel::Point_3(mesh.vertices[i].x(), mesh.vertices[i].y(), mesh.vertices[i].z())); + for (const auto& f : mesh.indices) { + cm.add_face(vmap[f[0]], vmap[f[1]], vmap[f[2]]); + } + return cm; +} + +inline TriMesh cgal_to_trimesh(const CGALMesh& cm) { + TriMesh mesh; + std::map<CGALMesh::Vertex_index, size_t> vmap; + size_t idx = 0; + for (auto v : cm.vertices()) { + if (!cm.is_valid(v) || cm.is_removed(v)) continue; + auto p = cm.point(v); + mesh.vertices.push_back(Vec3f((float)p.x(), (float)p.y(), (float)p.z())); + vmap[v] = idx++; + } + for (auto f : cm.faces()) { + if (!cm.is_valid(f) || cm.is_removed(f)) continue; + auto h = cm.halfedge(f); + auto v0 = cm.target(h); + auto v1 = cm.target(cm.next(h)); + auto v2 = cm.target(cm.next(cm.next(h))); + mesh.indices.push_back(Vec3i32((int)vmap[v0], (int)vmap[v1], (int)vmap[v2])); + } + return mesh; +} + +inline bool is_mesh_halfedge_compatible(const TriMesh& mesh) { + std::vector<std::unordered_set<std::size_t>> vtx_to_adj_faces(mesh.vertices.size()); + std::size_t edge_id = 0; + std::vector<std::unordered_set<std::size_t>> edge_to_faces; + std::vector<std::unordered_set<std::size_t>> vtx_to_prev_vtxs(mesh.vertices.size()); + std::vector<std::unordered_set<std::size_t>> vtx_to_next_vtxs(mesh.vertices.size()); + std::vector<std::unordered_map<std::size_t, std::size_t>> vtx_vtx_to_edge(mesh.vertices.size()); + + for (std::size_t fid = 0; fid < mesh.indices.size(); ++fid) { + const TriFace& face = mesh.indices[fid]; + if (face[0] == face[1] || face[1] == face[2] || face[2] == face[0]) { + return false; + } + for (std::size_t i = 0; i < 3; ++i) { + if (static_cast<std::size_t>(face[i]) >= mesh.vertices.size()) { + return false; + } + vtx_to_adj_faces[face[i]].insert(fid); + + std::size_t prev_vtx = face[(i + 2) % 3]; + std::size_t next_vtx = face[(i + 1) % 3]; + + if (vtx_to_prev_vtxs[face[i]].count(prev_vtx)) { + return false; + } + vtx_to_prev_vtxs[face[i]].insert(prev_vtx); + + if (vtx_to_next_vtxs[face[i]].count(next_vtx)) { + return false; + } + vtx_to_next_vtxs[face[i]].insert(next_vtx); + } + + for (std::size_t i = 0; i < 3; ++i) { + std::size_t va = face[i]; + std::size_t vb = face[(i + 1) % 3]; + if (!vtx_vtx_to_edge[va].count(vb)) { + vtx_vtx_to_edge[va][vb] = edge_id; + vtx_vtx_to_edge[vb][va] = edge_id; + ++edge_id; + edge_to_faces.emplace_back(std::unordered_set<std::size_t>()); + } + edge_to_faces[vtx_vtx_to_edge[va][vb]].insert(fid); + } + } + + for (std::size_t vid = 0; vid < mesh.vertices.size(); ++vid) { + if (vtx_to_adj_faces[vid].empty()) { + continue; + } + std::unordered_set<std::size_t> visited_faces; + std::queue<std::size_t> face_queue; + face_queue.push(*(vtx_to_adj_faces[vid].begin())); + visited_faces.insert(*(vtx_to_adj_faces[vid].begin())); + while (!face_queue.empty()) { + std::size_t fid = face_queue.front(); + face_queue.pop(); + const TriFace& face = mesh.indices[fid]; + for (std::size_t i = 0; i < 3; ++i) { + if (static_cast<std::size_t>(face[i]) != vid) { + continue; + } + std::size_t v_next = face[(i + 1) % 3]; + std::size_t v_prev = face[(i + 2) % 3]; + for (std::size_t nbr : {v_next, v_prev}) { + std::size_t eid = vtx_vtx_to_edge[vid][nbr]; + for (std::size_t adj_fid : edge_to_faces[eid]) { + if (!visited_faces.count(adj_fid) && vtx_to_adj_faces[vid].count(adj_fid)) { + visited_faces.insert(adj_fid); + face_queue.push(adj_fid); + } + } + } + break; + } + } + + for (std::size_t fid : vtx_to_adj_faces[vid]) { + if (!visited_faces.count(fid)) { + return false; + } + } + } + + return true; +} + +inline bool convert_trimesh_to_cgal(const TriMesh& mesh, CGALMesh& cgal_mesh) { + cgal_mesh = trimesh_to_cgal(mesh); + return cgal_mesh.number_of_faces() > 0 || mesh.indices.empty(); +} + +inline bool convert_trimesh_to_cgal( + const TriMesh& mesh, const std::vector<Vec2f>& vertex_uvs, + CGALMesh& cgal_mesh, std::vector<Vec2f>& cgal_vertex_uvs) +{ + cgal_mesh.clear(); + std::vector<CGALMesh::Vertex_index> vmap(mesh.vertices.size()); + cgal_vertex_uvs.clear(); + + for (size_t i = 0; i < mesh.vertices.size(); ++i) { + vmap[i] = cgal_mesh.add_vertex(Kernel::Point_3( + mesh.vertices[i].x(), mesh.vertices[i].y(), mesh.vertices[i].z())); + } + + cgal_vertex_uvs.resize(cgal_mesh.num_vertices()); + for (size_t i = 0; i < mesh.vertices.size(); ++i) { + if (i < vertex_uvs.size()) + cgal_vertex_uvs[vmap[i]] = vertex_uvs[i]; + else + cgal_vertex_uvs[vmap[i]] = Vec2f(0.f, 0.f); + } + + for (const auto& f : mesh.indices) + cgal_mesh.add_face(vmap[f[0]], vmap[f[1]], vmap[f[2]]); + + return true; +} + +} // namespace cgalutils +} // namespace tex2color +} // namespace Slic3r diff --git a/src/libslic3r/TextureToColor/ColorUtils.cpp b/src/libslic3r/TextureToColor/ColorUtils.cpp new file mode 100644 index 0000000000..7127a5d364 --- /dev/null +++ b/src/libslic3r/TextureToColor/ColorUtils.cpp @@ -0,0 +1,1643 @@ +#include "ColorUtils.hpp" + +#include <algorithm> +#include <array> +#include <atomic> +#include <chrono> +#include <cmath> +#include <cstdio> +#include <iostream> +#include <numeric> + +#include <boost/next_prior.hpp> + +#include <CGAL/AABB_face_graph_triangle_primitive.h> +#include <CGAL/AABB_traits.h> +#include <CGAL/AABB_tree.h> +#include <CGAL/Exact_predicates_inexact_constructions_kernel.h> +#include <CGAL/Point_3.h> +#include <CGAL/Polygon_mesh_processing/compute_normal.h> +#include <CGAL/Polygon_mesh_processing/intersection.h> +#include <CGAL/Polygon_mesh_processing/remesh.h> +#include <CGAL/Polygon_mesh_processing/repair_self_intersections.h> +#include <CGAL/Polygon_mesh_processing/self_intersections.h> +#include <CGAL/Surface_mesh.h> +#include <CGAL/mesh_segmentation.h> +#include <tbb/parallel_for.h> + +#include "CgalUtils.hpp" +#include "libslic3r/AABBTreeIndirect.hpp" +#include <boost/log/trivial.hpp> + +namespace Slic3r { namespace tex2color { +namespace color_utils { + +// #define DEBUG_FLAG + +#ifndef M_PI +#define M_PI 3.1415926535897932 +#endif + +#ifndef EPSILON +#define EPSILON 1e-6 +#endif + +#ifndef DOUBLE_LIMITS +#define DOUBLE_LIMITS +#define Double_MAX std::numeric_limits<double>::max() +#define Double_MIN -std::numeric_limits<double>::max() +#endif // !DOUBLE_LIMITS + +namespace PMP = CGAL::Polygon_mesh_processing; + +using cgalutils::CGALMesh; +using CGALKernel = cgalutils::Kernel; + +static constexpr double TOPO_SMOOTH_WEIGHT_THRESHOLD = 0.3; + +namespace detail { +template<typename Mesh> +double average_edge_length_impl(const Mesh& m) { + double total = 0.0; + size_t count = 0; + for (auto e : m.edges()) { + auto h = m.halfedge(e); + auto p0 = m.point(m.source(h)); + auto p1 = m.point(m.target(h)); + total += std::sqrt(CGAL::squared_distance(p0, p1)); + ++count; + } + return count > 0 ? total / count : 1.0; +} +} // namespace detail + +typedef CGAL::Aff_transformation_3<CGALKernel> Affine_transformation_3; +typedef boost::graph_traits<CGALMesh>::halfedge_descriptor halfedge_descriptor; +typedef boost::graph_traits<CGALMesh>::edge_descriptor edge_descriptor; +typedef boost::graph_traits<CGALMesh>::vertex_descriptor vertex_descriptor; +typedef CGAL::AABB_face_graph_triangle_primitive<CGALMesh> Primitive; +typedef CGAL::AABB_traits<CGALKernel, Primitive> Traits; +typedef CGAL::AABB_tree<Traits> Tree; +typedef CGALMesh::template Property_map<vertex_descriptor, CGALKernel::Vector_3> VNMap; + +static inline ColorDouble convert_rgb_uint_to_rgb_double(const Color& color) { + return ColorDouble{static_cast<double>(color[0]), static_cast<double>(color[1]), static_cast<double>(color[2])}; +} + +static void normalize(CGALKernel::Vector_3& vec) { + double squared_length = vec.squared_length(); + if (squared_length > EPSILON) { + vec /= sqrt(squared_length); + } +} + +static double get_angle_between_vectors(const CGALKernel::Vector_3& v1, const CGALKernel::Vector_3& v2) { + CGALKernel::Vector_3 dir1{v1}, dir2{v2}; + normalize(dir1); + normalize(dir2); + double product_dot = dir1.x() * dir2.x() + dir1.y() * dir2.y() + dir1.z() * dir2.z(); + if (product_dot > 1.0 - EPSILON) { + return 0.0; + } else if (product_dot < -1.0 + EPSILON) { + return 180.0; + } + return std::acos(product_dot) / M_PI * 180.0; +} + +static void calc_face_normals(const CGALMesh& mesh, std::vector<Eigen::Vector3d>& face_normals) { + std::size_t fcnt = mesh.number_of_faces(); + face_normals.resize(fcnt); + for (auto face : mesh.faces()) { + std::vector<CGALKernel::Point_3> points; + for (auto vtx : mesh.vertices_around_face(mesh.halfedge(face))) { + points.push_back(mesh.point(vtx)); + } + Eigen::Vector3d pos1(points[0].x(), points[0].y(), points[0].z()); + Eigen::Vector3d pos2(points[1].x(), points[1].y(), points[1].z()); + Eigen::Vector3d pos3(points[2].x(), points[2].y(), points[2].z()); + face_normals[face] = (pos3 - pos2).cross(pos1 - pos2); + face_normals[face].normalize(); + } + return; +} + +static bool check_and_repair_self_intersect(CGALMesh& mesh, bool* is_self_intersect_status = nullptr) { + // true means the mesh is no self intersect now + // false means the mesh is still self intersect + auto is_self_intersect = PMP::does_self_intersect(mesh); + if (is_self_intersect_status != nullptr) { + *is_self_intersect_status = is_self_intersect; + } + if (is_self_intersect) { + bool repair = PMP::experimental::remove_self_intersections(mesh); + if (repair) { + return true; + } else { + return false; + } + } + return true; +} + +static bool save_polylines(const std::string& file_name, const std::vector<std::vector<CGALKernel::Point_3>>& polylines) { + std::vector<CGALKernel::Point_3> points; + std::vector<std::pair<std::size_t, std::size_t>> lines; + for (auto& polyline : polylines) { + std::size_t begin_pt_idx = points.size(); + for (auto& pt : polyline) { + points.push_back(pt); + } + for (std::size_t i = 1; i < polyline.size(); ++i) { + lines.emplace_back(begin_pt_idx + i, begin_pt_idx + i + 1); // obj is begin at 1 + } + } + std::ofstream output_file(file_name, std::ios::out); + for (auto& point : points) { + output_file << "v " << point[0] << " " << point[1] << " " << point[2] << "\n"; + } + for (auto& line : lines) { + output_file << "l " << line.first << " " << line.second << "\n"; + } + output_file.close(); + return true; +} + +static bool smooth_region_topo_boundary(CGALMesh& mesh, std::vector<std::size_t>& face_labels, std::size_t max_iters = 20) { + // Topological smoothing: reassign face labels + std::size_t iter = 0; + while (iter < max_iters) { + ++iter; + bool flip_flag = false; + for (auto face : mesh.faces()) { + std::size_t same_label_count = 0; + std::unordered_map<std::size_t, std::size_t> map_label_to_cnt; + std::size_t max_adj_cnt = 0; + std::size_t max_adj_label = face_labels[face]; + for (auto adj_face : mesh.faces_around_face(mesh.halfedge(face))) { + if (adj_face == CGALMesh::null_face() || !mesh.is_valid(adj_face) || mesh.is_removed(adj_face)) { + continue; + } + if (face_labels[adj_face] == face_labels[face]) { + ++same_label_count; + } else { + ++map_label_to_cnt[face_labels[adj_face]]; + if (map_label_to_cnt[face_labels[adj_face]] > max_adj_cnt) { + max_adj_cnt = map_label_to_cnt[face_labels[adj_face]]; + max_adj_label = face_labels[adj_face]; + } + } + } + if (max_adj_cnt > same_label_count) { + face_labels[face] = max_adj_label; + flip_flag = true; + } + } + + if (!flip_flag) { + break; + } + } + return true; +} + +static bool smooth_region_geom_boundary(CGALMesh& mesh, std::vector<std::size_t>& face_labels, const SmoothParameters& smooth_parameters) { + // 1. extract boundary vertices and make polines + std::unordered_map<CGAL::SM_Vertex_index, std::size_t> map_vtx_to_degree; + std::unordered_set<CGAL::SM_Edge_index> segment_boundary_edges; + std::unordered_set<CGAL::SM_Edge_index> feature_edges; + std::unordered_set<CGAL::SM_Vertex_index> feature_vertices; + constexpr double feature_angle = 45; + for (const auto& edge : mesh.edges()) { + if (!mesh.is_valid(edge) || mesh.is_border(edge)) { + continue; + } + auto source = mesh.source(mesh.halfedge(edge)); + auto target = mesh.target(mesh.halfedge(edge)); + auto face_1 = mesh.face(mesh.halfedge(edge)); + auto face_2 = mesh.face(mesh.opposite(mesh.halfedge(edge))); + auto normal_1 = PMP::compute_face_normal(face_1, mesh); + auto normal_2 = PMP::compute_face_normal(face_2, mesh); + double angle = get_angle_between_vectors(normal_1, normal_2); + if (angle > feature_angle) { + feature_edges.insert(edge); + feature_vertices.insert(source); + feature_vertices.insert(target); + } + + if (face_labels[face_1] == face_labels[face_2]) { + continue; + } + + segment_boundary_edges.insert(edge); + ++map_vtx_to_degree[source]; + ++map_vtx_to_degree[target]; + } + + // 2. smooth each polyline + std::vector<std::vector<CGAL::SM_Vertex_index>> polylines; + std::unordered_set<CGAL::SM_Edge_index> visited_edges; + + std::function<void(std::vector<CGAL::SM_Vertex_index>&)> trace_polyline = [&](std::vector<CGAL::SM_Vertex_index>& polyline) -> void { + if (polyline.empty()) { + return; + } + CGAL::SM_Vertex_index curr_vtx = polyline.back(); + if (map_vtx_to_degree[curr_vtx] != 2) { + return; + } + for (const auto& halfedge : mesh.halfedges_around_target(mesh.halfedge(curr_vtx))) { + CGAL::SM_Edge_index edge = mesh.edge(halfedge); + if (visited_edges.count(edge) || !segment_boundary_edges.count(edge)) { + continue; + } + visited_edges.insert(edge); + CGAL::SM_Vertex_index adj_vtx = mesh.source(halfedge); + if (!map_vtx_to_degree.count(adj_vtx)) { + continue; + } + polyline.push_back(adj_vtx); + return trace_polyline(polyline); + } + }; + + // 2.1. open polyline: from T nodes search other 2-degree nodes + for (auto& [src_vtx, degree] : map_vtx_to_degree) { + if (degree == 2) { + continue; + } + for (auto& src_halfedge : mesh.halfedges_around_target(mesh.halfedge(src_vtx))) { + CGAL::SM_Edge_index src_edge = mesh.edge(src_halfedge); + if (visited_edges.count(src_edge) || !segment_boundary_edges.count(src_edge)) { + continue; + } + visited_edges.insert(src_edge); + CGAL::SM_Vertex_index adj_vtx = mesh.source(src_halfedge); + if (!map_vtx_to_degree.count(adj_vtx)) { + continue; + } + std::vector<CGAL::SM_Vertex_index> polyline{src_vtx, adj_vtx}; + trace_polyline(polyline); + polylines.push_back(std::move(polyline)); + } + } + + // 2.2. closed polylines + for (auto edge : segment_boundary_edges) { + if (visited_edges.count(edge)) { + continue; + } + visited_edges.insert(edge); + CGAL::SM_Halfedge_index halfedge = mesh.halfedge(edge); + std::vector<CGAL::SM_Vertex_index> polyline{mesh.source(halfedge), mesh.target(halfedge)}; + trace_polyline(polyline); + if (polyline.front() != polyline.back()) { + std::cerr << "[Error]: loop polyline but not closed!!!\n"; + } + polylines.push_back(std::move(polyline)); + } + + // 3. smooth boundary + Tree boundary_tree(mesh.faces().begin(), mesh.faces().end(), mesh); + boundary_tree.accelerate_distance_queries(); + + constexpr std::size_t max_iters = 5; + const double smooth_weight = smooth_parameters.smooth_weight; // Controls smoothing intensity; larger values produce smoother results. Range: 0.1~1.0. + double origin_weight = std::max(1.0 - smooth_weight, 0.0); + for (std::size_t iter = 0; iter < max_iters; ++iter) { + for (const auto& polyline : polylines) { + std::size_t pt_cnt = polyline.size(); + std::vector<CGALKernel::Point_3> points(pt_cnt); + for (std::size_t pt_idx = 1; pt_idx + 1 < pt_cnt; ++pt_idx) { + std::vector<CGALKernel::Point_3> pts{mesh.point(polyline[pt_idx - 1]), mesh.point(polyline[pt_idx + 1])}; + CGALKernel::Point_3 smooth_pt = CGAL::ORIGIN + ((CGAL::centroid(pts.begin(), pts.end()) - CGAL::ORIGIN) * smooth_weight + + (mesh.point(polyline[pt_idx]) - CGAL::ORIGIN) * origin_weight); + points[pt_idx] = boundary_tree.closest_point(smooth_pt); + } + + if (polyline.front() == polyline.back()) { + if (feature_vertices.count(polyline.front())) { + continue; + } + std::vector<CGALKernel::Point_3> pts{mesh.point(polyline[1]), mesh.point(polyline[pt_cnt - 2])}; + CGALKernel::Point_3 smooth_pt = CGAL::ORIGIN + ((CGAL::centroid(pts.begin(), pts.end()) - CGAL::ORIGIN) * smooth_weight + + (mesh.point(polyline.front()) - CGAL::ORIGIN) * origin_weight); + mesh.point(polyline.front()) = boundary_tree.closest_point(smooth_pt); + } + + for (std::size_t pt_idx = 1; pt_idx + 1 < pt_cnt; ++pt_idx) { + if (feature_vertices.count(polyline[pt_idx])) { + continue; + } + mesh.point(polyline[pt_idx]) = points[pt_idx]; + } + } + } + + for (auto& polyline : polylines) { + for (auto& vtx : polyline) { + mesh.point(vtx) = boundary_tree.closest_point(mesh.point(vtx)); + } + } + + return true; +} + +// HSV, XYZ, and LAB are only used internally for computing color differences, so they are declared in this cpp file only. +typedef std::array<double, 3> HSV; +typedef std::array<double, 3> XYZ; // Intermediate space for converting between LAB and RGB +typedef std::array<double, 3> LAB; // CIELAB was designed to match human visual perception; the standard method for perceptual color difference +// Common white points +const XYZ D65_WHITE = {0.95047, 1.0, 1.08883}; + +/** + * @brief Convert an RGB color to the HSV color space. + * + * @param rgb Input RGB color [R, G, B], range 0~255. + * @return HSV output [H, S, V], H in 0~360, S and V in 0~1. + */ +static HSV convert_rgb_to_hsv(const RGB& rgb) { + // Normalize to [0, 1] + double r = rgb[0] / 255.0; + double g = rgb[1] / 255.0; + double b = rgb[2] / 255.0; + + double max = std::max({r, g, b}); + double min = std::min({r, g, b}); + double delta = max - min; + + // Compute hue H + double h = 0; + if (delta == 0) { + h = 0; // Gray; hue is undefined + } else { + if (max == r) { + h = 60.0 * fmod((g - b) / delta, 6.0); + } else if (max == g) { + h = 60.0 * ((b - r) / delta + 2.0); + } else { // max == b + h = 60.0 * ((r - g) / delta + 4.0); + } + if (h < 0) { + h += 360.0; + } + } + + // Compute saturation S + double s = (max == 0) ? 0 : (delta / max); + + // Compute value V + double v = max; + + return {h, s, v}; +} + +/** + * @brief Convert an HSV color to the RGB color space. + * + * @param hsv Input HSV color [H, S, V], H in 0~360, S and V in 0~1. + * @return RGB output [R, G, B], range 0~255. + */ +static RGB convert_hsv_to_rgb(const HSV& hsv) { + double h = hsv[0]; + double s = hsv[1]; + double v = hsv[2]; + + double c = v * s; + double x = c * (1 - std::abs(fmod(h / 60.0, 2.0) - 1)); + double m = v - c; + + double r, g, b; + + if (h < 60) { + r = c; + g = x; + b = 0; + } else if (h < 120) { + r = x; + g = c; + b = 0; + } else if (h < 180) { + r = 0; + g = c; + b = x; + } else if (h < 240) { + r = 0; + g = x; + b = c; + } else if (h < 300) { + r = x; + g = 0; + b = c; + } else { + r = c; + g = 0; + b = x; + } + + return {static_cast<std::size_t>((r + m) * 255 + 0.5), static_cast<std::size_t>((g + m) * 255 + 0.5), static_cast<std::size_t>((b + m) * 255 + 0.5)}; +} + +static XYZ convert_rgb_to_xyz(const RGB& color_rgb) { + ColorDouble rgb{static_cast<double>(color_rgb[0]), static_cast<double>(color_rgb[1]), static_cast<double>(color_rgb[2])}; + auto gammaCorrect = [](double v) -> double { + v = v / 255.0; + if (v > 0.04045) { + return std::pow((v + 0.055) / 1.055, 2.4); + } else { + return v / 12.92; + } + }; + + double r = gammaCorrect(rgb[0]); + double g = gammaCorrect(rgb[1]); + double b = gammaCorrect(rgb[2]); + + // sRGB to XYZ matrix + return {r * 0.4124564 + g * 0.3575761 + b * 0.1804375, r * 0.2126729 + g * 0.7151522 + b * 0.0721750, r * 0.0193339 + g * 0.1191920 + b * 0.9503041}; +} + +// sRGB non-linear channel values [0,1] (double) -> linear light -> XYZ; equivalent to convert_rgb_to_xyz when v=n/255 +static XYZ convert_srgb01_to_xyz(double rs, double gs, double bs) { + auto gamma_correct = [](double v) -> double { + v = std::clamp(v, 0.0, 1.0); + return (v > 0.04045) ? std::pow((v + 0.055) / 1.055, 2.4) : (v / 12.92); + }; + const double r = gamma_correct(rs); + const double g = gamma_correct(gs); + const double b = gamma_correct(bs); + return {r * 0.4124564 + g * 0.3575761 + b * 0.1804375, r * 0.2126729 + g * 0.7151522 + b * 0.0721750, r * 0.0193339 + g * 0.1191920 + b * 0.9503041}; +} + +static LAB convert_xyz_to_lab(const XYZ& xyz) { + auto f = [](double t) -> double { + const double delta = 6.0 / 29.0; + if (t > delta * delta * delta) { + return std::cbrt(t); + } else { + return t / (3.0 * delta * delta) + 4.0 / 29.0; + } + }; + + // D65 white point + double xn = D65_WHITE[0], yn = D65_WHITE[1], zn = D65_WHITE[2]; + + double fx = f(xyz[0] / xn); + double fy = f(xyz[1] / yn); + double fz = f(xyz[2] / zn); + + return {116.0 * fy - 16.0, 500.0 * (fx - fy), 200.0 * (fy - fz)}; +} + +// ColorDouble here represents sRGB [0,1]; see calc_rgb_color_difference_by_ciede2000_srgb01 header comment +static LAB convert_srgb01_to_lab(const ColorDouble& srgb01) { + return convert_xyz_to_lab(convert_srgb01_to_xyz(srgb01[0], srgb01[1], srgb01[2])); +} + +static LAB convert_rgb_to_lab(const RGB& rgb) { + return convert_xyz_to_lab(convert_rgb_to_xyz(rgb)); +} + +static Color convert_lab_to_rgb(const LAB& lab) { + // Lab → XYZ + const double delta = 6.0 / 29.0; + const double delta2x3 = 3.0 * delta * delta; + + double fy = (lab[0] + 16.0) / 116.0; + double fx = lab[1] / 500.0 + fy; + double fz = fy - lab[2] / 200.0; + + double x = D65_WHITE[0] * (fx > delta ? fx * fx * fx : delta2x3 * (fx - 4.0 / 29.0)); + double y = D65_WHITE[1] * (fy > delta ? fy * fy * fy : delta2x3 * (fy - 4.0 / 29.0)); + double z = D65_WHITE[2] * (fz > delta ? fz * fz * fz : delta2x3 * (fz - 4.0 / 29.0)); + + // XYZ -> linear RGB (sRGB inverse matrix) + double r_lin = 3.2404542 * x - 1.5371385 * y - 0.4985314 * z; + double g_lin = -0.9692660 * x + 1.8760108 * y + 0.0415560 * z; + double b_lin = 0.0556434 * x - 0.2040259 * y + 1.0572252 * z; + + // linear RGB -> sRGB (inverse gamma correction) + auto inverse_gamma = [](double v) -> double { + v = std::max(v, 0.0); + return v <= 0.0031308 ? 12.92 * v : 1.055 * std::pow(v, 1.0 / 2.4) - 0.055; + }; + + auto to_uint8 = [](double v) -> std::size_t { return static_cast<std::size_t>(std::clamp(std::round(v * 255.0), 0.0, 255.0)); }; + + return {to_uint8(inverse_gamma(r_lin)), to_uint8(inverse_gamma(g_lin)), to_uint8(inverse_gamma(b_lin))}; +} + +/** + * @brief CIEDE2000 color-difference computation. + * + * Currently the most accurate color-difference formula, recommended by CIE as the industry standard. + * + * @param lab1 LAB values of the first color. + * @param lab2 LAB values of the second color. + * @return Color difference (typically < 1.0 is imperceptible to the human eye). + */ +static double ciede2000(const std::array<double, 3>& lab1, const std::array<double, 3>& lab2) { + // Parameters in the CIE L*C*h* formula + double L1 = lab1[0], a1 = lab1[1], b1 = lab1[2]; + double L2 = lab2[0], a2 = lab2[1], b2 = lab2[2]; + + // Compute C1 and C2 + double C1 = std::sqrt(a1 * a1 + b1 * b1); + double C2 = std::sqrt(a2 * a2 + b2 * b2); + double C_avg = (C1 + C2) / 2.0; + + // G factor (compensates for non-linearity in the mid-low chroma region) + double C7 = C_avg * C_avg * C_avg * C_avg * C_avg * C_avg * C_avg; + double G = 0.5 * (1.0 - std::sqrt(C7 / (C7 + 6103515625.0))); + + // a1' and a2' + double a1_prime = (1.0 + G) * a1; + double a2_prime = (1.0 + G) * a2; + + // C'1 and C'2 + double C1_prime = std::sqrt(a1_prime * a1_prime + b1 * b1); + double C2_prime = std::sqrt(a2_prime * a2_prime + b2 * b2); + double C_prime_avg = (C1_prime + C2_prime) / 2.0; + + // h'1 and h'2 + double h1_prime = std::atan2(b1, a1_prime); + double h2_prime = std::atan2(b2, a2_prime); + if (h1_prime < 0) { + h1_prime += 2 * M_PI; + } + if (h2_prime < 0) { + h2_prime += 2 * M_PI; + } + + // Compute dh' + double dh_prime; + if (std::abs(h1_prime - h2_prime) <= M_PI) { + dh_prime = h2_prime - h1_prime; + } else if (h2_prime <= h1_prime) { + dh_prime = h2_prime - h1_prime + 2 * M_PI; + } else { + dh_prime = h2_prime - h1_prime - 2 * M_PI; + } + + // Compute dH' + double dH_prime = 2.0 * std::sqrt(C1_prime * C2_prime) * std::sin(dh_prime / 2.0); + + // Compute dL' + double dL_prime = L2 - L1; + + // Compute dC' + double dC_prime = C2_prime - C1_prime; + + // Compute h_prime_avg + double h_prime_avg; + if (std::abs(h1_prime - h2_prime) > M_PI) { + h_prime_avg = (h1_prime + h2_prime + 2 * M_PI) / 2.0; + } else { + h_prime_avg = (h1_prime + h2_prime) / 2.0; + } + + // Compute T + double T = 1.0 - 0.17 * std::cos(h_prime_avg - M_PI / 6.0) + 0.24 * std::cos(2.0 * h_prime_avg) + 0.32 * std::cos(3.0 * h_prime_avg + M_PI / 30.0) - + 0.20 * std::cos(4.0 * h_prime_avg - 3.0 * M_PI / 6.0); + + // Compute rotation term R_T = -R_C * sin(2*delta_theta), where delta_theta = 30 * exp(-((h_bar'-275)/25)^2) + // h_prime_avg is in radians; convert to degrees for delta_theta; 2*delta_theta = 60 * exp(...), convert back to radians for sin + double h_prime_avg_deg = h_prime_avg * 180.0 / M_PI; + double C_prime_avg_7 = std::pow(C_prime_avg, 7); + double R = -2.0 * std::sqrt(C_prime_avg_7 / (C_prime_avg_7 + 6103515625.0)) * + std::sin((60.0 * M_PI / 180.0) * std::exp(-std::pow((h_prime_avg_deg - 275.0) / 25.0, 2))); + + // Compute SL, SC, SH + double L_prime_avg = (L1 + L2) / 2.0; + double SL = 1.0 + 0.015 * std::pow(L_prime_avg - 50.0, 2) / std::sqrt(20 + std::pow(L_prime_avg - 50.0, 2)); + double SC = 1.0 + 0.045 * C_prime_avg; + double SH = 1.0 + 0.015 * C_prime_avg * T; + + // Final color difference + double deltaE = std::sqrt(std::pow(dL_prime / SL, 2) + std::pow(dC_prime / SC, 2) + std::pow(dH_prime / SH, 2) + R * (dC_prime / SC) * (dH_prime / SH)); + + return deltaE; +} + +double calc_rgb_color_difference_by_ciede2000(const RGB& rgb1, const RGB& rgb2) { + auto lab1 = convert_rgb_to_lab(rgb1); + auto lab2 = convert_rgb_to_lab(rgb2); + return ciede2000(lab1, lab2); +} + +double calc_rgb_color_difference_by_ciede2000_srgb01(const ColorDouble& rgb1, const ColorDouble& rgb2) { + const LAB lab1 = convert_srgb01_to_lab(rgb1); + const LAB lab2 = convert_srgb01_to_lab(rgb2); + return ciede2000(lab1, lab2); +} + +// Working-space distance function type: inputs are two colors in the same space (RGB-double or Lab) +using WorkingDistFunc = double (*)(const ColorDouble&, const ColorDouble&); + +// Farthest Point Sampling (FPS) initialization algorithm. +// The first center is the point nearest to the global centroid; subsequent centers are the points farthest from the existing set. +static std::vector<ColorDouble> farthest_point_sampling_init(const std::vector<ColorDouble>& working_colors, std::size_t k, WorkingDistFunc dist_func) { + std::vector<ColorDouble> centers(k); + + // 1. Compute the global centroid and pick the nearest point as the first center + ColorDouble centroid = {0.0, 0.0, 0.0}; + for (const auto& c : working_colors) { + centroid[0] += c[0]; + centroid[1] += c[1]; + centroid[2] += c[2]; + } + const auto n = static_cast<double>(working_colors.size()); + centroid[0] /= n; + centroid[1] /= n; + centroid[2] /= n; + + double best_dist = std::numeric_limits<double>::max(); + std::size_t first_idx = 0; + for (std::size_t i = 0; i < working_colors.size(); ++i) { + double d = dist_func(working_colors[i], centroid); + if (d < best_dist) { + best_dist = d; + first_idx = i; + } + } + centers[0] = working_colors[first_idx]; + + // Minimum distance from each point to the already-selected center set + std::vector<double> min_distances(working_colors.size(), std::numeric_limits<double>::max()); + + // 2. Greedily select the remaining K-1 centers: pick the point with the largest min_distance each time + for (std::size_t i = 1; i < k; ++i) { + const ColorDouble& last_center = centers[i - 1]; + + // Update each point's minimum distance with the newly added center + double farthest_dist = -1.0; + std::size_t farthest_idx = 0; + for (std::size_t c_idx = 0; c_idx < working_colors.size(); ++c_idx) { + double d = dist_func(working_colors[c_idx], last_center); + if (d < min_distances[c_idx]) { + min_distances[c_idx] = d; + } + if (min_distances[c_idx] > farthest_dist) { + farthest_dist = min_distances[c_idx]; + farthest_idx = c_idx; + } + } + + centers[i] = working_colors[farthest_idx]; + } + + return centers; +} + +bool remesh_mesh(TriMesh& bbs_mesh, std::vector<std::size_t>& face_labels, double target_edge_length_ratio) { + if (face_labels.size() != bbs_mesh.indices.size()) { + BOOST_LOG_TRIVIAL(warning) << "Input mesh face count does not match label count"; + return false; + } + + // Back up face labels for recovery after remeshing. + std::vector<std::size_t> face_labels_of_original_mesh(face_labels); + + CGALMesh cgal_mesh; + cgalutils::convert_trimesh_to_cgal(bbs_mesh, cgal_mesh); + + // AABBTreeIndirect references vertices/faces externally, so snapshot the + // pre-remesh geometry by moving it out of bbs_mesh (which is overwritten + // below with the post-remesh mesh). std::move on std::vector is O(1). + TriVertices old_vertices = std::move(bbs_mesh.vertices); + TriFaces old_indices = std::move(bbs_mesh.indices); + auto original_mesh_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(old_vertices, old_indices); + + std::unordered_set<CGAL::SM_Edge_index> feature_edges; + std::unordered_set<CGAL::SM_Vertex_index> feature_vertices; + CGALMesh::Property_map<CGALMesh::Edge_index, bool> constrained_edges = + cgal_mesh.add_property_map<CGALMesh::Edge_index, bool>("constrained_edges", false).first; + CGALMesh::Property_map<CGALMesh::Vertex_index, bool> constrained_vertices = + cgal_mesh.add_property_map<CGALMesh::Vertex_index, bool>("constrained_vertices", false).first; + + // An edge is considered a geometric feature edge if its dihedral angle is less than 135 degrees (loose threshold) + constexpr double feature_angle = 135; + + auto is_feature_edge = [&](CGAL::SM_Edge_index edge) -> bool { + if (cgal_mesh.is_border(edge)) { + return true; + } + auto halfedge_1 = cgal_mesh.halfedge(edge); + auto halfedge_2 = cgal_mesh.opposite(halfedge_1); + auto face_1 = cgal_mesh.face(halfedge_1); + auto face_2 = cgal_mesh.face(halfedge_2); + if (face_labels[face_1] != face_labels[face_2]) { + // Boundary between different color regions; treated as a feature edge + return true; + } + // TODO: CGAL remeshing tends to crash when too many constrained edges are added; needs handling + //auto normal_1 = PMP::compute_face_normal(face_1, cgal_mesh); + //auto normal_2 = PMP::compute_face_normal(face_2, cgal_mesh); + //double angle = 180 - get_angle_between_vectors(normal_1, normal_2); + //BOOST_LOG_TRIVIAL(debug) << "end.\n"; + //return angle > feature_angle; + return false; + }; + + for (auto edge : cgal_mesh.edges()) { + if (is_feature_edge(edge)) { + feature_edges.insert(edge); + feature_vertices.insert(cgal_mesh.source(cgal_mesh.halfedge(edge))); + feature_vertices.insert(cgal_mesh.target(cgal_mesh.halfedge(edge))); + constrained_edges[edge] = true; + constrained_vertices[cgal_mesh.source(cgal_mesh.halfedge(edge))] = true; + constrained_vertices[cgal_mesh.target(cgal_mesh.halfedge(edge))] = true; + } + } + +#ifdef DEBUG_FLAG + BOOST_LOG_TRIVIAL(debug) << "remesh_mesh: feature_edges.size() = " << feature_edges.size() << ".\n"; + + std::vector<std::vector<CGALKernel::Point_3>> polylines; + for (auto edge : feature_edges) { + auto src_vtx = cgal_mesh.source(cgal_mesh.halfedge(edge)); + auto trg_vtx = cgal_mesh.target(cgal_mesh.halfedge(edge)); + polylines.push_back({cgal_mesh.point(src_vtx), cgal_mesh.point(trg_vtx)}); + } + save_polylines("ColorUtils_remesh_feature_lines.obj", polylines); +#endif // DEBUG_FLAG + + std::size_t iters = 5; +#ifdef DEBUG_FLAG + BOOST_LOG_TRIVIAL(debug) << "PMP::isotropic_remeshing start...\n"; +#endif // DEBUG_FLAG + // TODO: CGAL remeshing preserves geometric boundaries but does not maintain face labels well; colors need to be recomputed + PMP::isotropic_remeshing(cgal_mesh.faces(), target_edge_length_ratio * detail::average_edge_length_impl(cgal_mesh), cgal_mesh, + CGAL::parameters::number_of_iterations(iters) + .protect_constraints(true) + .edge_is_constrained_map(constrained_edges) + .vertex_is_constrained_map(constrained_vertices) + .collapse_constraints(true)); +#ifdef DEBUG_FLAG + BOOST_LOG_TRIVIAL(debug) << "PMP::isotropic_remeshing finshed...\n"; +#endif // DEBUG_FLAG + if (PMP::does_self_intersect(cgal_mesh)) { + PMP::experimental::remove_self_intersections(cgal_mesh); + } + + bbs_mesh.clear(); + + std::unordered_map<CGAL::SM_Vertex_index, std::size_t> map_cgal_vtx_to_bbs_vtx; + TriVertices bbs_vertices; + TriFaces bbs_faces; + face_labels.clear(); + face_labels.reserve(cgal_mesh.number_of_faces()); + + for (const auto& cgal_vtx : cgal_mesh.vertices()) { + if (!cgal_mesh.is_valid(cgal_vtx) || cgal_mesh.is_removed(cgal_vtx) || cgal_mesh.is_isolated(cgal_vtx)) { + continue; + } + if (!map_cgal_vtx_to_bbs_vtx.count(cgal_vtx)) { + map_cgal_vtx_to_bbs_vtx[cgal_vtx] = bbs_vertices.size(); + const auto& cgal_point = cgal_mesh.point(cgal_vtx); + bbs_vertices.emplace_back(TriVertex(cgal_point.x(), cgal_point.y(), cgal_point.z())); + } + } + + for (const auto& cgal_face : cgal_mesh.faces()) { + if (!cgal_mesh.is_valid(cgal_face) || cgal_mesh.is_removed(cgal_face)) { + continue; + } + TriFace bbs_face; + std::size_t face_vid = 0; + for (auto cgal_vtx : cgal_mesh.vertices_around_face(cgal_mesh.halfedge(cgal_face))) { + do { if (!(map_cgal_vtx_to_bbs_vtx.count(cgal_vtx))) { BOOST_LOG_TRIVIAL(warning) << "CGAL mesh contains a face with an invalid vertex"; return false; } } while(0); + bbs_face[face_vid] = map_cgal_vtx_to_bbs_vtx[cgal_vtx]; + ++face_vid; + } + bbs_faces.push_back(std::move(bbs_face)); + } + + bbs_mesh = TriMesh(bbs_faces, bbs_vertices); + + face_labels.resize(bbs_mesh.indices.size()); + tbb::parallel_for(tbb::blocked_range<std::size_t>(0, bbs_mesh.indices.size()), [&](const tbb::blocked_range<size_t>& range) { + for (std::size_t fid = range.begin(); fid < range.end(); ++fid) { + auto& face = bbs_mesh.indices[fid]; + Vec3f face_centroid = (bbs_mesh.vertices[face[0]] + bbs_mesh.vertices[face[1]] + bbs_mesh.vertices[face[2]]) / 3.0; + size_t hit_idx = 0; + Vec3f closest; + AABBTreeIndirect::squared_distance_to_indexed_triangle_set( + old_vertices, old_indices, original_mesh_tree, face_centroid, hit_idx, closest); + face_labels[fid] = face_labels_of_original_mesh[hit_idx]; + } + }); + + return true; +} + +bool is_closed(const TriMesh& bbs_mesh) { + CGALMesh cgal_mesh; + cgalutils::convert_trimesh_to_cgal(bbs_mesh, cgal_mesh); + +#ifdef DEBUG_FLAG + std::size_t border_edges_count = 0; + std::size_t edges_count = 0; + for (auto edge : cgal_mesh.edges()) { + if (cgal_mesh.is_border(edge)) { + ++border_edges_count; + } + ++edges_count; + } + + BOOST_LOG_TRIVIAL(debug) << "border edges count = " << border_edges_count << "\n"; + BOOST_LOG_TRIVIAL(debug) << "edges count = " << edges_count << "\n"; + + std::size_t border_faces_count = 0; + std::size_t faces_count = 0; + for (auto face : cgal_mesh.faces()) { + for (auto halfedge : cgal_mesh.halfedges_around_face(cgal_mesh.halfedge(face))) { + auto edge = cgal_mesh.edge(halfedge); + if (cgal_mesh.is_border(edge)) { + ++border_faces_count; + break; + } + } + ++faces_count; + } + + BOOST_LOG_TRIVIAL(debug) << "border faces count = " << border_faces_count << "\n"; + BOOST_LOG_TRIVIAL(debug) << "faces count = " << faces_count << "\n"; + + BOOST_LOG_TRIVIAL(debug) << "vertices count = " << cgal_mesh.number_of_vertices() << "\n"; + std::size_t num_of_components = 0; + std::unordered_set<CGAL::SM_Face_index> visited_faces; + for (auto src_face : cgal_mesh.faces()) { + if (visited_faces.count(src_face)) { + continue; + } + ++num_of_components; + std::queue<CGAL::SM_Face_index> que; + que.push(src_face); + visited_faces.insert(src_face); + while (!que.empty()) { + auto curr_face = que.front(); + que.pop(); + for (auto adj_face : cgal_mesh.faces_around_face(cgal_mesh.halfedge(curr_face))) { + if (!cgal_mesh.is_valid(adj_face) || cgal_mesh.is_removed(adj_face) || visited_faces.count(adj_face)) { + continue; + } + que.push(adj_face); + visited_faces.insert(adj_face); + } + } + } + BOOST_LOG_TRIVIAL(debug) << "components count = " << num_of_components << "\n"; +#endif // DEBUG_FLAG + + return CGAL::is_closed(cgal_mesh); +} + +static bool smooth_region_labels(TriMesh& tri_mesh, std::vector<std::size_t>& face_labels, const SmoothParameters& smooth_parameters) { + CGALMesh mesh; + cgalutils::convert_trimesh_to_cgal(tri_mesh, mesh); + + if (mesh.number_of_faces() != face_labels.size()) { + BOOST_LOG_TRIVIAL(warning) << "Face count does not match label count"; + return false; + } + + if (smooth_parameters.smooth_weight >= TOPO_SMOOTH_WEIGHT_THRESHOLD) { + // Topological smoothing: reassign face labels. + smooth_region_topo_boundary(mesh, face_labels); + } + + if (smooth_parameters.smooth_weight > EPSILON) { + // Geometric smoothing: smooth polylines and project back onto the original mesh. + smooth_region_geom_boundary(mesh, face_labels, smooth_parameters); + } + + tri_mesh = cgalutils::cgal_to_trimesh(mesh); + + return true; +} + +bool smooth_region(TriMesh& tri_mesh, std::vector<std::array<std::size_t, 3>>& face_colors, const SmoothParameters& smooth_parameters) { + // Convert colors to labels + std::size_t label_next = 0; + std::vector<std::size_t> face_labels; + face_labels.reserve(face_colors.size()); + std::map<std::array<std::size_t, 3>, std::size_t> map_color_to_label; + std::unordered_map<std::size_t, std::array<std::size_t, 3>> map_label_to_color; + for (auto& color : face_colors) { + if (!map_color_to_label.count(color)) { + map_color_to_label[color] = label_next; + map_label_to_color[label_next] = color; + ++label_next; + } + face_labels.push_back(map_color_to_label[color]); + } + + if (!smooth_region_labels(tri_mesh, face_labels, smooth_parameters)) + return false; + + // Convert labels back to colors + for (std::size_t fid = 0; fid < face_labels.size(); ++fid) { + face_colors[fid] = map_label_to_color[face_labels[fid]]; + } + + return true; +} + +bool smooth_region(TriMesh& tri_mesh, std::vector<std::size_t>& face_labels, const SmoothParameters& smooth_parameters) { + return smooth_region_labels(tri_mesh, face_labels, smooth_parameters); +} + +// Compute the squared Euclidean distance between two colors (RGB vectors) +double calc_rgb_color_difference_by_squared_rgb_double(const ColorDouble& c1, const ColorDouble& c2) { + double dr = c1[0] - c2[0]; + double dg = c1[1] - c2[1]; + double db = c1[2] - c2[2]; + return dr * dr + dg * dg + db * db; +} + +// Compute the squared Euclidean distance between two colors (RGB vectors) +double calc_rgb_color_difference_by_squared_rgb(const RGB& c1, const RGB& c2) { + auto c1d = convert_rgb_uint_to_rgb_double(c1); + auto c2d = convert_rgb_uint_to_rgb_double(c2); + return calc_rgb_color_difference_by_squared_rgb_double(c1d, c2d); +} + +// K-Means core: run FPS initialization + assign/update iterations in working space, return cluster centers +static std::vector<ColorDouble> kmeans_core(const std::vector<ColorDouble>& working_colors, std::size_t k, std::size_t max_iter, WorkingDistFunc dist_func, + const std::function<bool()>& cancel_cb = nullptr) { + std::vector<ColorDouble> centers = farthest_point_sampling_init(working_colors, k, dist_func); + std::vector<std::size_t> assignments(working_colors.size()); + + for (std::size_t iter = 0; iter < max_iter; ++iter) { + if (cancel_cb && cancel_cb()) return centers; + std::atomic<bool> changed(false); + + tbb::parallel_for(tbb::blocked_range<std::size_t>(0, working_colors.size()), [&](const tbb::blocked_range<size_t>& range) { + for (std::size_t i = range.begin(); i < range.end(); ++i) { + double min_dist = std::numeric_limits<double>::max(); + std::size_t best_cluster = 0; + + for (std::size_t j = 0; j < k; ++j) { + double d = dist_func(working_colors[i], centers[j]); + if (d < min_dist) { + min_dist = d; + best_cluster = j; + } + } + + if (assignments[i] != best_cluster) { + changed.store(true, std::memory_order_relaxed); + assignments[i] = best_cluster; + } + } + }); + + if (!changed.load()) { + break; + } + + std::vector<ColorDouble> new_centers(k, {0.0, 0.0, 0.0}); + std::vector<std::size_t> counts(k, 0); + + for (std::size_t i = 0; i < working_colors.size(); ++i) { + std::size_t cluster_id = assignments[i]; + ++counts[cluster_id]; + new_centers[cluster_id][0] += working_colors[i][0]; + new_centers[cluster_id][1] += working_colors[i][1]; + new_centers[cluster_id][2] += working_colors[i][2]; + } + + for (std::size_t i = 0; i < k; ++i) { + if (counts[i] == 0) { + double max_min_dist = -1.0; + std::size_t best_idx = 0; + for (std::size_t p = 0; p < working_colors.size(); ++p) { + double nearest = std::numeric_limits<double>::max(); + for (std::size_t c = 0; c < k; ++c) { + if (c == i || counts[c] == 0) { + continue; + } + double d = dist_func(working_colors[p], centers[c]); + if (d < nearest) { + nearest = d; + } + } + if (nearest > max_min_dist) { + max_min_dist = nearest; + best_idx = p; + } + } + centers[i] = working_colors[best_idx]; + } else { + centers[i][0] = new_centers[i][0] / counts[i]; + centers[i][1] = new_centers[i][1] / counts[i]; + centers[i][2] = new_centers[i][2] / counts[i]; + } + } + } + + return centers; +} + +// K-Means clustering algorithm +std::vector<Color> cluster_k_means(const std::vector<Color>& colors, const ClusterParameters& cluster_parameters) { + std::size_t k = cluster_parameters.cluster_k; + std::size_t max_iter = cluster_parameters.max_iter; + + if (k == 0 || colors.empty()) { + return {}; + } + + const bool use_lab = (cluster_parameters.color_difference_method != ColorDifferenceMethod::RGB); + WorkingDistFunc working_dist_func = use_lab ? ciede2000 : calc_rgb_color_difference_by_squared_rgb_double; + + // ========================================== + // 1. Preprocessing: deduplicate + pre-convert to working space + // ========================================== + std::vector<Color> unique_colors = colors; + std::sort(unique_colors.begin(), unique_colors.end()); + auto last = std::unique(unique_colors.begin(), unique_colors.end()); + unique_colors.erase(last, unique_colors.end()); + +#ifdef DEBUG_FLAG + BOOST_LOG_TRIVIAL(debug) << "Input colors: " << colors.size() << ", Unique colors: " << unique_colors.size(); +#endif // DEBUG_FLAG + + if (unique_colors.size() < k) { + BOOST_LOG_TRIVIAL(warning) << "Unique color count (" << unique_colors.size() << ") is less than target K (" << k << "). Adjusting K."; + k = unique_colors.size(); + if (k == 0) { + return {}; + } + return unique_colors; + } + + std::vector<ColorDouble> working_colors(unique_colors.size()); + for (std::size_t i = 0; i < unique_colors.size(); ++i) { + if (use_lab) { + working_colors[i] = convert_rgb_to_lab(unique_colors[i]); + } else { + working_colors[i] = {static_cast<double>(unique_colors[i][0]), static_cast<double>(unique_colors[i][1]), static_cast<double>(unique_colors[i][2])}; + } + } + + // ========================================== + // 2. K-Means clustering + // ========================================== + auto centers = kmeans_core(working_colors, k, max_iter, working_dist_func, cluster_parameters.cancel_callback); + + // ========================================== + // 3. Output: convert from working space back to RGB + // ========================================== + std::vector<Color> result(k); + for (std::size_t i = 0; i < k; ++i) { + if (use_lab) { + result[i] = convert_lab_to_rgb(centers[i]); + } else { + result[i] = {static_cast<std::size_t>(std::round(centers[i][0])), static_cast<std::size_t>(std::round(centers[i][1])), + static_cast<std::size_t>(std::round(centers[i][2]))}; + } + } + + return result; +} + +std::vector<Color> cluster_to_specified_colors(const std::vector<Color>& colors, const std::vector<Color>& specified_colors) { + std::vector<Color> cluster_colors = colors; + std::vector<ColorDouble> specified_double_colors; + specified_double_colors.reserve(specified_colors.size()); + for (auto& color : specified_colors) { + specified_double_colors.push_back(ColorDouble{static_cast<double>(color[0]), static_cast<double>(color[1]), static_cast<double>(color[2])}); + } + + tbb::parallel_for(tbb::blocked_range<std::size_t>(0, cluster_colors.size()), [&](const tbb::blocked_range<size_t>& range) { + for (std::size_t i = range.begin(); i < range.end(); ++i) { + ColorDouble p_color{static_cast<double>(cluster_colors[i][0]), static_cast<double>(cluster_colors[i][1]), + static_cast<double>(cluster_colors[i][2])}; + + double min_dist = std::numeric_limits<double>::max(); + std::size_t best_cluster = 0; + + for (std::size_t j = 0; j < specified_double_colors.size(); ++j) { + double d = calc_rgb_color_difference_by_squared_rgb_double(p_color, specified_double_colors[j]); + if (d < min_dist) { + min_dist = d; + best_cluster = j; + } + } + + cluster_colors[i] = specified_colors[best_cluster]; + } + }); + + return cluster_colors; +} + +// Color PCA struct +struct ColorPCA { + std::size_t color_idx; // Index of the original color in ColorList + double pca_value; // Projection value onto the first principal component + + ColorPCA(std::size_t c_idx, double pca_val) + : color_idx(c_idx), + pca_value(pca_val) {} + + // Comparison operator (ascending order) + bool operator<(const ColorPCA& other) const { return pca_value < other.pca_value; } + + // Equality check + bool operator==(const ColorPCA& other) const { return color_idx == other.color_idx; } +}; + +[[maybe_unused]] static std::vector<ColorPCA> sort_colors_by_pca(const std::vector<Color>& colors) { + const std::size_t n = colors.size(); + + if (n == 0) { + return {}; + } + + if (n == 1) { + return {{0, 0.0}}; + } + + // Step 1: Data preprocessing - normalize to [0, 1] + Eigen::MatrixXd data(n, 3); + + for (std::size_t i = 0; i < n; ++i) { + data(i, 0) = static_cast<double>(colors[i][0]) / 255.0; // R + data(i, 1) = static_cast<double>(colors[i][1]) / 255.0; // G + data(i, 2) = static_cast<double>(colors[i][2]) / 255.0; // B + } + + // Step 2: Compute mean and center the data + Eigen::RowVector3d mean = data.colwise().mean(); + Eigen::MatrixXd centered = data.rowwise() - mean; + + // Step 3: Compute covariance matrix (3x3) + Eigen::Matrix3d cov = (centered.adjoint() * centered) / static_cast<double>(n - 1); + + // Step 4: Eigenvalue decomposition + Eigen::SelfAdjointEigenSolver<Eigen::Matrix3d> solver(cov); + + if (solver.info() != Eigen::Success) { +#ifdef DEBUG_FLAG + std::cerr << "PCA: Eigenvalue decomposition failed" << std::endl; +#endif // DEBUG_FLAG + // Fallback: return an approximate result sorted by luminance. + // Luminance is a key perceptual feature; convert RGB to grayscale (L = 0.299R + 0.587G + 0.114B) and sort in ascending order. + std::vector<ColorPCA> result; + result.reserve(n); + for (std::size_t i = 0; i < n; ++i) { + double luminance = 0.299 * colors[i][0] + 0.587 * colors[i][1] + 0.114 * colors[i][2]; + result.push_back({i, luminance}); + } + std::sort(result.begin(), result.end()); + return result; + } + + // Get eigenvalues and eigenvectors (sorted by eigenvalue in descending order) + Eigen::Vector3d eigenvalues = solver.eigenvalues(); + Eigen::Matrix3d eigenvectors = solver.eigenvectors(); + + // Step 5: Find the eigenvector corresponding to the largest eigenvalue (first principal component) + Eigen::MatrixXd::Index max_eigenvalue_idx; + eigenvalues.maxCoeff(&max_eigenvalue_idx); + + Eigen::Vector3d first_principal_component = eigenvectors.col(max_eigenvalue_idx); + + // Step 6: Project centered data onto the first principal component + Eigen::VectorXd projections = centered * first_principal_component; + + // Step 7: Build result and sort + std::vector<ColorPCA> result; + result.reserve(n); + + for (std::size_t i = 0; i < n; ++i) { + result.push_back({i, projections(i)}); + } + + std::sort(result.begin(), result.end()); + + return result; +} + +std::vector<Color> cluster_adaptive(const std::vector<Color>& colors, const ClusterParameters& cluster_parameters) { + if (colors.empty()) { + return {}; + } + + const double max_color_distance = cluster_parameters.max_color_distance; + const std::size_t max_iter = cluster_parameters.max_iter; + const bool use_lab = (cluster_parameters.color_difference_method != ColorDifferenceMethod::RGB); + WorkingDistFunc working_dist_func = use_lab ? ciede2000 : calc_rgb_color_difference_by_squared_rgb_double; + + // ========================================== + // 1. Deduplicate + convert to working space + // ========================================== + std::vector<Color> unique_colors = colors; + std::sort(unique_colors.begin(), unique_colors.end()); + auto last = std::unique(unique_colors.begin(), unique_colors.end()); + unique_colors.erase(last, unique_colors.end()); + + BOOST_LOG_TRIVIAL(debug) << "cluster_adaptive: colors=" << colors.size() + << " unique=" << unique_colors.size() + << " max_color_distance=" << max_color_distance; + + if (unique_colors.size() <= 1) { + return unique_colors; + } + + std::vector<ColorDouble> working_colors(unique_colors.size()); + for (std::size_t i = 0; i < unique_colors.size(); ++i) { + if (use_lab) { + working_colors[i] = convert_rgb_to_lab(unique_colors[i]); + } else { + working_colors[i] = {static_cast<double>(unique_colors[i][0]), static_cast<double>(unique_colors[i][1]), static_cast<double>(unique_colors[i][2])}; + } + } + + // ========================================== + // 2. Binary search k: find the smallest k where P99 radius <= max_color_distance + // ========================================== + const std::size_t max_k = cluster_parameters.max_cluster_k; + std::size_t lo = 1; + std::size_t hi = std::min<std::size_t>(max_k, unique_colors.size()); + std::size_t best_k = 0; + std::vector<ColorDouble> best_centers; + + constexpr double kRadiusPercentile = 0.99; + + auto calc_max_radius = [&](const std::vector<ColorDouble>& centers) -> double { + std::vector<double> distances(working_colors.size()); + tbb::parallel_for(tbb::blocked_range<std::size_t>(0, working_colors.size()), [&](const tbb::blocked_range<size_t>& range) { + for (std::size_t i = range.begin(); i < range.end(); ++i) { + double min_dist = std::numeric_limits<double>::max(); + for (const auto& center : centers) { + double d = working_dist_func(working_colors[i], center); + if (d < min_dist) { + min_dist = d; + } + } + distances[i] = min_dist; + } + }); + if (distances.empty()) { + return 0.0; + } + std::size_t idx = std::min<std::size_t>(static_cast<std::size_t>(distances.size() * kRadiusPercentile), distances.size() - 1); + std::nth_element(distances.begin(), distances.begin() + idx, distances.end()); + return distances[idx]; + }; + + const auto& cancel_cb = cluster_parameters.cancel_callback; + + while (lo <= hi) { + if (cancel_cb && cancel_cb()) return {}; + std::size_t mid = lo + (hi - lo) / 2; + auto centers = kmeans_core(working_colors, mid, max_iter, working_dist_func, cancel_cb); + if (cancel_cb && cancel_cb()) return {}; + double max_radius = calc_max_radius(centers); + + BOOST_LOG_TRIVIAL(debug) << "cluster_adaptive binary search: k=" << mid << " max_radius=" << max_radius; + + if (max_radius <= max_color_distance) { + best_k = mid; + best_centers = std::move(centers); + hi = mid - 1; + } else { + lo = mid + 1; + } + } + + if (best_k == 0) { + best_k = std::min<std::size_t>(max_k, unique_colors.size()); + best_centers = kmeans_core(working_colors, best_k, max_iter, working_dist_func, cancel_cb); + BOOST_LOG_TRIVIAL(warning) << "cluster_adaptive: binary search found no k satisfying max_radius<=" + << max_color_distance << ", fallback to k=" << best_k; + } + + BOOST_LOG_TRIVIAL(debug) << "cluster_adaptive: best_k=" << best_k; + + // ========================================== + // 3. Convert centers back to RGB + // ========================================== + std::vector<Color> result(best_k); + for (std::size_t i = 0; i < best_k; ++i) { + if (use_lab) { + result[i] = convert_lab_to_rgb(best_centers[i]); + } else { + result[i] = {static_cast<std::size_t>(std::round(best_centers[i][0])), static_cast<std::size_t>(std::round(best_centers[i][1])), + static_cast<std::size_t>(std::round(best_centers[i][2]))}; + } + } + + return result; +} + +static std::vector<std::vector<CGAL::SM_Face_index>> get_connected_face_groups(const CGALMesh& mesh) { + std::vector<std::vector<CGAL::SM_Face_index>> face_groups; + std::unordered_set<CGAL::SM_Face_index> visited_faces; + for (auto src_face : mesh.faces()) { + if (visited_faces.count(src_face)) { + continue; + } + std::vector<CGAL::SM_Face_index> face_group; + std::queue<CGAL::SM_Face_index> que; + que.push(src_face); + visited_faces.insert(src_face); + while (!que.empty()) { + auto curr_face = que.front(); + que.pop(); + face_group.push_back(curr_face); + for (auto adj_face : mesh.faces_around_face(mesh.halfedge(curr_face))) { + if (!mesh.is_valid(adj_face) || mesh.is_removed(adj_face) || visited_faces.count(adj_face)) { + continue; + } + que.push(adj_face); + visited_faces.insert(adj_face); + } + } + face_groups.push_back(std::move(face_group)); + } + return face_groups; +} + +bool get_components(const TriMesh& bbs_mesh, const std::vector<Vec2f>& bbs_vertex_uvs, std::vector<TriMesh>& component_meshes, + std::vector<std::vector<Vec2f>>& component_vertex_uvs) { + component_meshes.clear(); + component_vertex_uvs.clear(); + + if (bbs_mesh.vertices.size() != bbs_vertex_uvs.size()) { + BOOST_LOG_TRIVIAL(warning) << "Input mesh vertex count does not match texture coordinate count"; + return false; + } + + CGALMesh cgal_mesh; + std::vector<Vec2f> cgal_vertex_uvs; + if (!cgalutils::convert_trimesh_to_cgal(bbs_mesh, bbs_vertex_uvs, cgal_mesh, cgal_vertex_uvs)) { + BOOST_LOG_TRIVIAL(warning) << "Mesh conversion failed"; + return false; + } + + auto face_groups = get_connected_face_groups(cgal_mesh); + + for (const auto& faces : face_groups) { + std::unordered_map<CGAL::SM_Vertex_index, std::size_t> map_cgal_vtx_to_bbs_vtx; + TriVertices bbs_vertices; + TriFaces bbs_faces; + std::vector<Vec2f> bbs_vertex_uvs; + + for (auto cgal_face : faces) { + TriFace bbs_face; + std::size_t face_vid = 0; + for (auto cgal_vtx : cgal_mesh.vertices_around_face(cgal_mesh.halfedge(cgal_face))) { + if (!map_cgal_vtx_to_bbs_vtx.count(cgal_vtx)) { + map_cgal_vtx_to_bbs_vtx[cgal_vtx] = bbs_vertices.size(); + const auto& cgal_point = cgal_mesh.point(cgal_vtx); + bbs_vertices.push_back(TriVertex(cgal_point.x(), cgal_point.y(), cgal_point.z())); + bbs_vertex_uvs.push_back(cgal_vertex_uvs[cgal_vtx]); + } + bbs_face[face_vid] = map_cgal_vtx_to_bbs_vtx[cgal_vtx]; + ++face_vid; + } + bbs_faces.push_back(std::move(bbs_face)); + } + + component_meshes.push_back(TriMesh(bbs_faces, bbs_vertices)); + component_vertex_uvs.push_back(std::move(bbs_vertex_uvs)); + } + + return true; +} + +bool calc_nearest_color_id(const std::vector<RGB>& colors, const RGB& color, std::size_t& nearest_color_id) { + if (colors.empty()) { + BOOST_LOG_TRIVIAL(warning) << "No color list provided"; + return false; + } + double min_dist = std::numeric_limits<double>::max(); + nearest_color_id = 0; + for (std::size_t i = 0; i < colors.size(); ++i) { + double dist = calc_rgb_color_difference_by_ciede2000(colors[i], color); + if (dist < min_dist) { + min_dist = dist; + nearest_color_id = i; + } + } + return true; +} + +bool mesh_cluster(const TriMesh& bbs_mesh, const std::vector<RGB>& cluster_centers, std::vector<RGB>& map_face_to_rgb, + std::vector<std::size_t>& map_face_to_cluster_id) { + if (cluster_centers.empty()) { + BOOST_LOG_TRIVIAL(warning) << "No cluster centers provided"; + return false; + } + + CGALMesh mesh; + cgalutils::convert_trimesh_to_cgal(bbs_mesh, mesh); + if (mesh.number_of_faces() != bbs_mesh.indices.size()) { + BOOST_LOG_TRIVIAL(warning) << "CGAL mesh face count does not match BBS mesh face count"; + return false; + } + if (mesh.number_of_faces() != map_face_to_rgb.size()) { + BOOST_LOG_TRIVIAL(warning) << "CGAL mesh face count does not match RGB count"; + return false; + } + + if (cluster_centers.size() == 1) { + std::fill(map_face_to_rgb.begin(), map_face_to_rgb.end(), cluster_centers[0]); +#ifdef DEBUG_FLAG + BOOST_LOG_TRIVIAL(debug) << "input cluster centers' size is 1, so we set all face RGB as same with it and return.\n"; +#endif + return true; + } + + std::vector<double> map_face_to_area(mesh.number_of_faces()); + tbb::parallel_for(tbb::blocked_range<std::size_t>(0, mesh.number_of_faces()), [&](const tbb::blocked_range<size_t>& range) { + for (std::size_t fid = range.begin(); fid < range.end(); ++fid) { + CGAL::SM_Face_index face(fid); + map_face_to_area[fid] = std::max(PMP::face_area(face, mesh), EPSILON); + } + }); + + // Step 1: Identify faces that definitely belong to a cluster center. + // A face is definitively assigned when dist1 * absolute_difference_times < dist2 (nearest vs. second-nearest center). + constexpr double absolute_difference_times = 1.5; + // dE <= 1.0: imperceptible to the human eye, high-precision color matching + // dE <= 2.0: slight difference, noticeable by experts; printing / image processing standard + // dE <= 3.0: noticeable by ordinary observers; general quality control + constexpr double difference_epsilon = 3.0; + constexpr std::size_t invalid_cluster_id = std::numeric_limits<std::size_t>::max(); + map_face_to_cluster_id.resize(mesh.number_of_faces(), invalid_cluster_id); + std::vector<std::vector<std::pair<double, std::size_t>>> map_face_to_dists(mesh.number_of_faces(), + std::vector<std::pair<double, std::size_t>>(cluster_centers.size())); + tbb::parallel_for(tbb::blocked_range<std::size_t>(0, mesh.number_of_faces()), [&](const tbb::blocked_range<size_t>& range) { + for (std::size_t fid = range.begin(); fid < range.end(); ++fid) { + CGAL::SM_Face_index face(fid); + std::vector<std::pair<double, std::size_t>>& dist_and_cid_vec = map_face_to_dists[fid]; + for (std::size_t cluster_id = 0; cluster_id < cluster_centers.size(); ++cluster_id) { + double dist = calc_rgb_color_difference_by_ciede2000(map_face_to_rgb[fid], cluster_centers[cluster_id]); + //dist_and_cid_vec.emplace_back(dist, cluster_id); + dist_and_cid_vec[cluster_id] = std::pair<double, std::size_t>{dist, cluster_id}; + } + std::sort(dist_and_cid_vec.begin(), dist_and_cid_vec.end()); + if (dist_and_cid_vec[0].first < difference_epsilon || dist_and_cid_vec[0].first * absolute_difference_times < dist_and_cid_vec[1].first) { + map_face_to_cluster_id[fid] = dist_and_cid_vec[0].second; + } + } + }); + + std::unordered_set<std::size_t> unclusted_fids; + for (std::size_t fid = 0; fid < mesh.number_of_faces(); ++fid) { + if (map_face_to_cluster_id[fid] == invalid_cluster_id) { + unclusted_fids.insert(fid); + } + } + + auto convert_unclustered_to_clustered = [&](const std::unordered_set<std::size_t>& iter_clustered_fids) -> bool { + if (iter_clustered_fids.empty()) { + return false; + } + for (auto& fid : iter_clustered_fids) { + unclusted_fids.erase(fid); + } + return true; + }; + + // Step 2: Flood. Use faces computed in the previous step as seeds and propagate outward. + while (!unclusted_fids.empty()) { + bool changed = false; + std::unordered_set<std::size_t> iter_clustered_fids; + // If an uncolored face has an adjacent color whose count exceeds the sum of all other colors, assign that color + for (auto fid : unclusted_fids) { + CGAL::SM_Face_index face(fid); + std::size_t count = 0; + std::unordered_map<std::size_t, std::size_t> map_cluster_id_to_count; + for (auto adj_face : mesh.faces_around_face(mesh.halfedge(face))) { + if (!mesh.is_valid(adj_face) || mesh.is_removed(adj_face)) { + continue; + } + ++count; + ++map_cluster_id_to_count[map_face_to_cluster_id[adj_face]]; + } + for (auto& [cluster_id, cnt] : map_cluster_id_to_count) { + if (cluster_id != invalid_cluster_id && cnt * 2 > count) { + map_face_to_cluster_id[fid] = cluster_id; + iter_clustered_fids.insert(fid); + break; + } + } + } + changed = convert_unclustered_to_clustered(iter_clustered_fids) || changed; + iter_clustered_fids.clear(); + + // If a face's nearest cluster center (by color distance) happens to have an adjacent face, assign that color too + for (auto fid : unclusted_fids) { + CGAL::SM_Face_index face(fid); + std::unordered_set<std::size_t> adj_cluster_ids; + for (auto adj_face : mesh.faces_around_face(mesh.halfedge(face))) { + if (!mesh.is_valid(adj_face) || mesh.is_removed(adj_face)) { + continue; + } + if (map_face_to_cluster_id[adj_face] == invalid_cluster_id) { + continue; + } + adj_cluster_ids.insert(map_face_to_cluster_id[adj_face]); + } + if (adj_cluster_ids.count(map_face_to_dists[fid].front().second)) { + map_face_to_cluster_id[fid] = map_face_to_dists[fid].front().second; + iter_clustered_fids.insert(fid); + } + } + changed = convert_unclustered_to_clustered(iter_clustered_fids) || changed; + iter_clustered_fids.clear(); + + // If no face colors were modified in this iteration, stop + if (!changed) { + break; + } + } + + // Step 3: Handle remaining unclustered faces (run after the above operations complete) + constexpr bool use_average_color = true; + for (auto src_fid : std::vector<std::size_t>(unclusted_fids.begin(), unclusted_fids.end())) { + if (!unclusted_fids.count(src_fid)) { + continue; + } + // Compute connected unclustered faces + std::queue<std::size_t> que; + std::unordered_set<std::size_t> connected_unclustered_faces; + que.push(src_fid); + connected_unclustered_faces.insert(src_fid); + double sum_r = 0, sum_g = 0, sum_b = 0; + double sum_area = 0.0; + std::unordered_map<std::size_t, double> map_cluster_id_to_adj_area; + while (!que.empty()) { + auto curr_fid = que.front(); + que.pop(); + sum_r += map_face_to_area[curr_fid] * map_face_to_rgb[curr_fid][0]; + sum_g += map_face_to_area[curr_fid] * map_face_to_rgb[curr_fid][1]; + sum_b += map_face_to_area[curr_fid] * map_face_to_rgb[curr_fid][2]; + sum_area += map_face_to_area[curr_fid]; + CGAL::SM_Face_index curr_face(curr_fid); + for (auto adj_face : mesh.faces_around_face(mesh.halfedge(curr_face))) { + if (!mesh.is_valid(adj_face) || mesh.is_removed(adj_face)) { // Invalid face + continue; + } + if (map_face_to_cluster_id[adj_face] != invalid_cluster_id) { + map_cluster_id_to_adj_area[map_face_to_cluster_id[adj_face]] += map_face_to_area[adj_face]; + } else { + if (!connected_unclustered_faces.count(adj_face)) { // Already clustered or already recorded + que.push(adj_face); + connected_unclustered_faces.insert(adj_face); + } + } + } + } + std::size_t matched_cluster_id = invalid_cluster_id; + if (use_average_color) { + // Use average color + RGB average_color{static_cast<std::size_t>(sum_r / sum_area), static_cast<std::size_t>(sum_g / sum_area), + static_cast<std::size_t>(sum_b / sum_area)}; + double min_dist = std::numeric_limits<double>::max(); + for (auto& [cluster_id, area] : map_cluster_id_to_adj_area) { + double dist = calc_rgb_color_difference_by_ciede2000(average_color, cluster_centers[cluster_id]); + if (dist < min_dist) { + min_dist = dist; + matched_cluster_id = cluster_id; + } + } + } else { + // Use adjacent area + double adj_max_area = 0.0; + for (auto& [cluster_id, area] : map_cluster_id_to_adj_area) { + if (area > adj_max_area) { + adj_max_area = area; + matched_cluster_id = cluster_id; + } + } + } + for (auto fid : connected_unclustered_faces) { + map_face_to_cluster_id[fid] = matched_cluster_id; + unclusted_fids.erase(fid); + } + } + + // Convert cluster center IDs to colors + for (std::size_t fid = 0; fid < mesh.number_of_faces(); ++fid) { + if (map_face_to_cluster_id[fid] == invalid_cluster_id) { + map_face_to_rgb[fid] = cluster_centers[0]; + map_face_to_cluster_id[fid] = 0; // Prevent out-of-bounds errors when using cluster_id later + } else { + map_face_to_rgb[fid] = cluster_centers[map_face_to_cluster_id[fid]]; + } + } + + return true; +} + +} // namespace color_utils + +} // namespace tex2color +} // namespace Slic3r diff --git a/src/libslic3r/TextureToColor/ColorUtils.hpp b/src/libslic3r/TextureToColor/ColorUtils.hpp new file mode 100644 index 0000000000..05849109f4 --- /dev/null +++ b/src/libslic3r/TextureToColor/ColorUtils.hpp @@ -0,0 +1,207 @@ +#pragma once + +#include "Callbacks.hpp" +#include "TriMesh.hpp" + +namespace Slic3r { namespace tex2color { + +namespace color_utils { +struct ClusterParameters; + +typedef std::array<std::size_t, 3> Color; // RGB: [R, G, B] 0~255 +typedef std::vector<Color> ColorList; +typedef std::array<double, 3> ColorDouble; +typedef std::array<std::size_t, 3> RGB; + +// Function pointer type that points to a specific color-difference function based on the chosen method. +using DistanceFunction = double (*)(const Color&, const Color&); + +// Color space used for computing color differences. +enum struct ColorDifferenceMethod : std::size_t { + RGB = 0, // Simplest and fastest + Lab = 1 // Most perceptually accurate +}; + +struct ClusterParameters { + ColorDifferenceMethod color_difference_method = ColorDifferenceMethod::Lab; // Method for measuring color difference; Lab is the most accurate + + double max_color_distance = 25; // Max intra-cluster radius (CIEDE2000 dE) for adaptive clustering; ignored by the fixed-K algorithm + + std::size_t cluster_k = 10; // Target number of cluster centers; ignored by the adaptive algorithm + + std::size_t max_cluster_k = 32; // Max cluster count upper bound for adaptive algorithm + + std::size_t max_iter = 50; // Maximum number of iterations + + std::function<bool()> cancel_callback; // Optional cancellation check; returns true when the caller requests abort +}; + +struct SmoothParameters { + double smooth_weight = 0.5; // Controls smoothing intensity; larger values produce smoother results. Range: [0.0, 1.0] +}; + +/** + * @brief Compute the squared Euclidean distance between two RGB colors. + * + * @param[in] rgb1 First RGB color [R, G, B], range 0~255. + * @param[in] rgb2 Second RGB color [R, G, B], range 0~255. + * @return Squared Euclidean distance: (R1-R2)^2 + (G1-G2)^2 + (B1-B2)^2. + */ +double calc_rgb_color_difference_by_squared_rgb(const RGB& rgb1, const RGB& rgb2); + +/** + * @brief Compute the squared Euclidean distance between two RGB colors (double precision). + * + * @param[in] c1 First RGB color [R, G, B], as double. + * @param[in] c2 Second RGB color [R, G, B], as double. + * @return Squared Euclidean distance: (R1-R2)^2 + (G1-G2)^2 + (B1-B2)^2. + */ +double calc_rgb_color_difference_by_squared_rgb_double(const ColorDouble& c1, const ColorDouble& c2); + +/** + * @brief Compute the CIEDE2000 color difference between two RGB colors. + * + * Currently the most accurate color-difference formula, recommended by CIE as the industry standard. + * - dE <= 1.0: imperceptible to the human eye, high-precision color matching. + * - dE <= 2.0: slight difference, noticeable by experts; printing / image processing standard. + * - dE <= 3.0: noticeable by ordinary observers; general quality control. + * + * @param[in] rgb1 First RGB color [R, G, B], range 0~255. + * @param[in] rgb2 Second RGB color [R, G, B], range 0~255. + * @return CIEDE2000 color difference; smaller values indicate more similar colors. + */ +double calc_rgb_color_difference_by_ciede2000(const RGB& rgb1, const RGB& rgb2); + +/** + * @brief Compute the CIEDE2000 color difference between two sRGB colors (double precision, non-linear channels in [0,1]). + * + * Uses the same XYZ/Lab/dE00 pipeline as calc_rgb_color_difference_by_ciede2000 but without uint8 + * quantization or the intermediate x255 conversion; suitable for bisection, color blending, and other + * iterative scenarios. Note: ColorDouble here represents [R,G,B] in [0,1], which differs from the + * 0~255 scale used by other interfaces in this file. Callers should follow the naming convention. + * + * @param[in] rgb1 rgb2 sRGB non-linear channel values, recommended range [0,1]. + */ +double calc_rgb_color_difference_by_ciede2000_srgb01(const ColorDouble& rgb1, const ColorDouble& rgb2); + +/** + * @brief K-Means clustering algorithm that minimizes the sum of squared errors. + * + * Uses K-Means++ initialization to iteratively find the optimal cluster centers. + * + * @param[in] colors Input color list. + * @param[in] cluster_parameters Clustering parameters including cluster count, max iterations, color-difference method, etc. + * @return List of cluster-center colors whose size equals cluster_parameters.cluster_k. + */ +std::vector<Color> cluster_k_means(const std::vector<Color>& colors, const ClusterParameters& cluster_parameters); + +/** + * @brief Adaptive K-Means clustering that determines an appropriate number of clusters under a max color-distance constraint. + * + * Automatically finds the optimal cluster count via binary search so that max_color_distance is satisfied. + * + * @param[in] colors Input color list. + * @param[in] cluster_parameters Clustering parameters; cluster_k is ignored and determined automatically. + * @return List of cluster-center colors whose count is determined by the algorithm based on max_color_distance. + */ +std::vector<Color> cluster_adaptive(const std::vector<Color>& colors, const ClusterParameters& cluster_parameters); + +/** + * @brief Cluster a color list to a set of specified cluster centers. + * + * For each input color, find the nearest specified cluster center and replace it. + * + * @param[in] colors Input color list. + * @param[in] specified_colors Specified cluster-center colors. + * @return Clustered color list where each color is replaced by its nearest center. + */ +std::vector<Color> cluster_to_specified_colors(const std::vector<Color>& colors, const std::vector<Color>& specified_colors); + +/** + * @brief Remesh the mesh while preserving color boundaries. + * + * Performs isotropic remeshing while protecting color boundaries. Edges whose two adjacent + * faces have different colors are marked as feature edges and will not be modified. + * + * @param[in,out] mesh Input mesh; modified in-place after remeshing. + * @param[in,out] face_labels Face color labels; updated to match the new mesh. + * @param[in] target_edge_length_ratio Ratio of target average edge length to input average edge length; >1 simplifies, <1 refines. + * @return true on success, false on failure. + */ +bool remesh_mesh(TriMesh& mesh, std::vector<std::size_t>& face_labels, double target_edge_length_ratio); + +/** + * @brief Check whether the mesh is closed (watertight). + * + * A mesh is closed if it has no boundary edges, i.e. every edge is shared by exactly two faces. + * + * @param[in] tri_mesh Input mesh. + * @return true if the mesh is closed, false if it has boundary edges. + */ +bool is_closed(const TriMesh& tri_mesh); + +/** + * @brief Smooth region boundaries (RGB color labels). + * + * Applies topological smoothing (label reassignment) and geometric smoothing (boundary vertex relocation). + * + * @param[in,out] tri_mesh Input mesh; modified in-place after smoothing. + * @param[in,out] face_labels Face color labels (RGB format); updated after smoothing. + * @param[in] smooth_parameters Smoothing control parameters. + * @return true on success, false on failure. + */ +bool smooth_region(TriMesh& tri_mesh, std::vector<std::array<std::size_t, 3>>& face_labels, const SmoothParameters& smooth_parameters = SmoothParameters()); + +/** + * @brief Smooth region boundaries (integer labels). + * + * Applies topological smoothing (label reassignment) and geometric smoothing (boundary vertex relocation). + * + * @param[in,out] tri_mesh Input mesh; modified in-place after smoothing. + * @param[in,out] face_labels Integer face labels; updated after smoothing. + * @param[in] smooth_parameters Smoothing control parameters. + * @return true on success, false on failure. + */ +bool smooth_region(TriMesh& tri_mesh, std::vector<std::size_t>& face_labels, const SmoothParameters& smooth_parameters = SmoothParameters()); + +/** + * @brief Split the mesh into connected components. + * + * Based on face connectivity, the mesh is split into independent components, each forming a + * standalone mesh. Texture coordinates for each component are preserved. + * + * @param[in] mesh Input mesh. + * @param[in] vertex_uvs Vertex texture coordinates. + * @param[out] component_meshes Output list of component meshes. + * @param[out] component_vertex_uvs Output list of texture coordinates per component. + * @return true on success, false on failure. + */ +bool get_components(const TriMesh& mesh, const std::vector<Vec2f>& vertex_uvs, std::vector<TriMesh>& component_meshes, + std::vector<std::vector<Vec2f>>& component_vertex_uvs); + +/** + * @brief Find the ID of the nearest color in a color list to a given color. + * + * @param[in] colors Color list. + * @param[in] color Target color. + * @param[out] nearest_color_id ID of the nearest color found. + * @return true on success, false on failure. + */ +bool calc_nearest_color_id(const std::vector<RGB>& colors, const RGB& color, std::size_t& nearest_color_id); + +/** + * @brief Cluster mesh face colors based on given cluster centers. + * + * @param[in] mesh Input mesh. + * @param[in] cluster_centers Cluster-center RGB colors. + * @param[in, out] map_face_to_rgb RGB color per face; updated to the nearest cluster center after clustering. + * @param[out] map_face_to_cluster_id Cluster-center ID per face; updated to the nearest cluster center ID. + * @return true on success, false on failure. + */ +bool mesh_cluster(const TriMesh& mesh, const std::vector<RGB>& cluster_centers, std::vector<RGB>& map_face_to_rgb, + std::vector<std::size_t>& map_face_to_cluster_id); + +} // namespace color_utils + +} // namespace tex2color +} // namespace Slic3r diff --git a/src/libslic3r/TextureToColor/Repair.hpp b/src/libslic3r/TextureToColor/Repair.hpp new file mode 100644 index 0000000000..44dc30c144 --- /dev/null +++ b/src/libslic3r/TextureToColor/Repair.hpp @@ -0,0 +1,252 @@ +#pragma once +#include "TriMesh.hpp" +#include "CgalUtils.hpp" +#include "Callbacks.hpp" +#include <CGAL/Polygon_mesh_processing/border.h> +#include <CGAL/Polygon_mesh_processing/manifoldness.h> +#include <CGAL/Polygon_mesh_processing/repair_polygon_soup.h> +#include <CGAL/Polygon_mesh_processing/repair.h> +#include <CGAL/Polygon_mesh_processing/orient_polygon_soup.h> +#include <CGAL/Polygon_mesh_processing/polygon_soup_to_polygon_mesh.h> +#include <CGAL/Polygon_mesh_processing/stitch_borders.h> +#include <CGAL/Polygon_mesh_processing/triangulate_hole.h> +#include <boost/log/trivial.hpp> +#include <algorithm> +#include <chrono> +#include <cstddef> +#include <memory> +#include <utility> + +namespace Slic3r { namespace tex2color { + +namespace PMP = CGAL::Polygon_mesh_processing; + +// Default upper bound on the number of half-edges in any single boundary cycle +// that CloseBoundariesAndRepairManifoldness will attempt to triangulate. The +// cost of triangulate_hole grows non-linearly with cycle length, so this caps +// the worst-case per-hole work rather than the aggregate boundary size: a mesh +// with many small holes is still fully repaired, while a mesh containing one +// pathologically large hole skips triangulation entirely. +inline constexpr std::size_t MAX_REPAIRABLE_MESH_HOLE_EDGES = 500; + +// Default upper bound on the aggregate number of boundary half-edges in the +// mesh (summed across every boundary cycle). When the total boundary length is +// excessive, even if each individual cycle is short, triangulating all of them +// usually indicates a severely fragmented input (e.g. heavily damaged scans) +// and rarely yields a usable result, so we skip hole closing entirely. +inline constexpr std::size_t MAX_REPAIRABLE_MESH_BOUNDARY_EDGES = 5000; + +struct RepairSetting +{ + // Skip triangulating a boundary cycle whose half-edge count exceeds this. + std::size_t max_hole_edges = MAX_REPAIRABLE_MESH_HOLE_EDGES; + // Skip hole closing entirely when the total boundary half-edge count + // (summed across all cycles) exceeds this. + std::size_t max_boundary_edges = MAX_REPAIRABLE_MESH_BOUNDARY_EDGES; +}; + +struct BoundaryEdgeStats +{ + std::size_t total_boundary_edges = 0; + std::size_t max_cycle_edges = 0; + std::size_t cycle_count = 0; +}; + +// Read-only inspection of the mesh's boundary cycles. Caller is responsible for +// any pre-processing (e.g. stitch_borders) needed for the count to be meaningful. +inline BoundaryEdgeStats ComputeBoundaryEdgeStats(const cgalutils::CGALMesh& cgal_mesh) +{ + using CGALMesh = cgalutils::CGALMesh; + using HalfedgeDescriptor = boost::graph_traits<CGALMesh>::halfedge_descriptor; + + std::vector<HalfedgeDescriptor> border_cycles; + PMP::extract_boundary_cycles(cgal_mesh, std::back_inserter(border_cycles)); + + BoundaryEdgeStats stats; + stats.cycle_count = border_cycles.size(); + for (const HalfedgeDescriptor h0 : border_cycles) { + std::size_t len = 0; + HalfedgeDescriptor h = h0; + do { + ++len; + h = next(h, cgal_mesh); + } while (h != h0); + stats.max_cycle_edges = std::max(stats.max_cycle_edges, len); + stats.total_boundary_edges += len; + } + return stats; +} + +// Unconditionally close every boundary cycle of the mesh and repair non-manifold +// vertices. The caller (e.g. RepairMesh) is expected to gate this call based on +// boundary statistics; entering this function always triggers triangulation. +inline void CloseBoundariesAndRepairManifoldness(cgalutils::CGALMesh& cgal_mesh) +{ + using CGALMesh = cgalutils::CGALMesh; + using HalfedgeDescriptor = boost::graph_traits<CGALMesh>::halfedge_descriptor; + using FaceDescriptor = boost::graph_traits<CGALMesh>::face_descriptor; + + PMP::stitch_borders(cgal_mesh); + PMP::duplicate_non_manifold_vertices(cgal_mesh); + + std::vector<HalfedgeDescriptor> border_cycles; + PMP::extract_boundary_cycles(cgal_mesh, std::back_inserter(border_cycles)); + + for (const HalfedgeDescriptor h : border_cycles) { + std::vector<FaceDescriptor> patch_faces; + PMP::triangulate_hole(cgal_mesh, h, std::back_inserter(patch_faces)); + } + + PMP::remove_degenerate_faces(cgal_mesh); + PMP::duplicate_non_manifold_vertices(cgal_mesh); +} + +inline bool RepairMesh(const TriMesh& mesh, + std::shared_ptr<TriMesh>& out_mesh, + AlgoProgressCallback progress_callback = nullptr, + AlgoCancelCallback cancel_callback = nullptr, + const RepairSetting& setting = RepairSetting{}) +{ + using Clock = std::chrono::steady_clock; + auto elapsed_ms = [](Clock::time_point t0) { + return std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - t0).count(); + }; + + const Clock::time_point t_total = Clock::now(); + + // Convert TriMesh to polygon soup (point container + triangle index container) + std::vector<cgalutils::Kernel::Point_3> soup_points; + std::vector<std::vector<std::size_t>> soup_triangles; + + soup_points.reserve(mesh.vertices.size()); + for (const TriVertex& v : mesh.vertices) { + soup_points.emplace_back(v.x(), v.y(), v.z()); + } + + soup_triangles.reserve(mesh.indices.size()); + for (const TriFace& f : mesh.indices) { + soup_triangles.push_back({static_cast<std::size_t>(f[0]), + static_cast<std::size_t>(f[1]), + static_cast<std::size_t>(f[2])}); + } + + if (progress_callback) { + progress_callback({30, "Repairing polygon soup"}); + } + if (cancel_callback && cancel_callback()) { + return false; + } + + { + const auto t0 = Clock::now(); + PMP::repair_polygon_soup(soup_points, soup_triangles); + BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=repair_polygon_soup took=" + << elapsed_ms(t0) << " ms"; + } + + if (progress_callback) { + progress_callback({50, "Orienting polygon soup"}); + } + if (cancel_callback && cancel_callback()) { + return false; + } + + { + const auto t0 = Clock::now(); + PMP::orient_polygon_soup(soup_points, soup_triangles); + BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=orient_polygon_soup took=" + << elapsed_ms(t0) << " ms"; + } + + if (progress_callback) { + progress_callback({70, "Converting to CGAL mesh"}); + } + if (cancel_callback && cancel_callback()) { + return false; + } + + cgalutils::CGALMesh cgal_mesh; + { + const auto t0 = Clock::now(); + PMP::polygon_soup_to_polygon_mesh(soup_points, soup_triangles, cgal_mesh); + BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=polygon_soup_to_polygon_mesh took=" + << elapsed_ms(t0) << " ms"; + } + + { + const auto t0 = Clock::now(); + PMP::remove_degenerate_faces(cgal_mesh); + BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=remove_degenerate_faces took=" + << elapsed_ms(t0) << " ms"; + } + + if (progress_callback) { + progress_callback({80, "Closing mesh boundaries"}); + } + if (cancel_callback && cancel_callback()) { + return false; + } + + // Stitch borders and duplicate non-manifold vertices first so that the + // boundary statistics below reflect the post-stitch topology; otherwise + // boundaries that would close on stitching inflate the counts and may + // cause the gate to skip hole filling unnecessarily. + BoundaryEdgeStats stats; + { + const auto t0 = Clock::now(); + PMP::stitch_borders(cgal_mesh); + PMP::duplicate_non_manifold_vertices(cgal_mesh); + stats = ComputeBoundaryEdgeStats(cgal_mesh); + BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=boundary_stats took=" + << elapsed_ms(t0) << " ms" + << " total_boundary_edges=" << stats.total_boundary_edges + << " max_cycle_edges=" << stats.max_cycle_edges + << " cycle_count=" << stats.cycle_count; + } + + const bool can_repair_holes = + stats.total_boundary_edges <= setting.max_boundary_edges && + stats.max_cycle_edges <= setting.max_hole_edges; + + if (can_repair_holes) { + const auto t0 = Clock::now(); + CloseBoundariesAndRepairManifoldness(cgal_mesh); + BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=close_boundaries took=" + << elapsed_ms(t0) << " ms"; + } else { + BOOST_LOG_TRIVIAL(info) + << "TextureToColor: RepairMesh skip hole closing" + << ", total_boundary_edges=" << stats.total_boundary_edges + << " (limit=" << setting.max_boundary_edges << ")" + << ", max_cycle_edges=" << stats.max_cycle_edges + << " (limit=" << setting.max_hole_edges << ")" + << ", cycle_count=" << stats.cycle_count; + } + + if (progress_callback) { + progress_callback({85, "Converting from CGAL mesh"}); + } + if (cancel_callback && cancel_callback()) { + return false; + } + + std::shared_ptr<TriMesh> out; + { + const auto t0 = Clock::now(); + out = std::make_shared<TriMesh>(cgalutils::cgal_to_trimesh(cgal_mesh)); + BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=cgal_to_trimesh took=" + << elapsed_ms(t0) << " ms"; + } + + out_mesh = std::move(out); + if (progress_callback) { + progress_callback({100, "Done"}); + } + + BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh total=" << elapsed_ms(t_total) << " ms"; + + return true; +} + +} // namespace tex2color +} // namespace Slic3r diff --git a/src/libslic3r/TextureToColor/TextureToColor.cpp b/src/libslic3r/TextureToColor/TextureToColor.cpp new file mode 100644 index 0000000000..e5dde36714 --- /dev/null +++ b/src/libslic3r/TextureToColor/TextureToColor.cpp @@ -0,0 +1,1043 @@ +#include "TextureToColor.hpp" + +#include <tbb/parallel_for.h> +#include <array> +#include <atomic> +#include <chrono> +#include <cmath> +#include <cstdint> +#include <functional> + +#include <boost/next_prior.hpp> +#include "CgalUtils.hpp" +#include "ColorUtils.hpp" +#include "libslic3r/TriangleMesh.hpp" +#include <filesystem> +#include <fstream> +#include "Repair.hpp" +#include "libslic3r/AABBTreeIndirect.hpp" +#include <boost/log/trivial.hpp> + +namespace Slic3r { namespace tex2color { + +using namespace color_utils; + +// #define OUTPUT_TEST_RESULT + +static void SaveToOFF(const std::string& path, const TriMesh& mesh, const std::vector<RGB>& face_colors) +{ + std::filesystem::create_directories(std::filesystem::path(path).parent_path()); + std::ofstream ofs(path); + if (!ofs.is_open()) { + BOOST_LOG_TRIVIAL(warning) << "SaveToOFF: failed to open " << path; + return; + } + + const auto& vertices = mesh.vertices; + const auto& faces = mesh.indices; + + ofs << "OFF\n"; + ofs << vertices.size() << " " << faces.size() << " 0\n"; + + for (const auto& v : vertices) { + ofs << v.x() << " " << v.y() << " " << v.z() << "\n"; + } + + for (std::size_t i = 0; i < faces.size(); ++i) { + const auto& f = faces[i]; + ofs << "3 " << f[0] << " " << f[1] << " " << f[2]; + if (i < face_colors.size()) { + ofs << " " << face_colors[i][0] / 255.0 + << " " << face_colors[i][1] / 255.0 + << " " << face_colors[i][2] / 255.0 + << " 1.0"; + } + ofs << "\n"; + } +} + +static std::vector<std::size_t> count_cluster_label_usage(const std::vector<std::size_t>& face_labels, std::size_t cluster_count) +{ + std::vector<std::size_t> usage(cluster_count, 0); + for (std::size_t label : face_labels) { + if (label < cluster_count) { + ++usage[label]; + } + } + return usage; +} + +static bool discard_unused_cluster_centers(std::vector<RGB>& cluster_centers, std::vector<std::size_t>& face_labels, const char* stage_name) +{ + if (cluster_centers.empty()) { + BOOST_LOG_TRIVIAL(warning) << "TextureToColor: cannot discard unused cluster centers at " << stage_name + << ", no cluster center is available."; + return false; + } + + const std::vector<std::size_t> usage = count_cluster_label_usage(face_labels, cluster_centers.size()); + std::vector<std::size_t> label_remap(cluster_centers.size(), std::numeric_limits<std::size_t>::max()); + std::vector<RGB> used_cluster_centers; + used_cluster_centers.reserve(cluster_centers.size()); + + for (std::size_t cluster_id = 0; cluster_id < cluster_centers.size(); ++cluster_id) { + if (usage[cluster_id] == 0) { + continue; + } + label_remap[cluster_id] = used_cluster_centers.size(); + used_cluster_centers.push_back(cluster_centers[cluster_id]); + } + + if (used_cluster_centers.size() == cluster_centers.size()) { + return true; + } + if (used_cluster_centers.empty()) { + BOOST_LOG_TRIVIAL(warning) << "TextureToColor: cannot discard unused cluster centers at " << stage_name + << ", no face uses any valid cluster center."; + return false; + } + + for (std::size_t& label : face_labels) { + if (label >= label_remap.size() || label_remap[label] == std::numeric_limits<std::size_t>::max()) { + BOOST_LOG_TRIVIAL(warning) << "TextureToColor: cannot remap cluster label " << label + << " at " << stage_name << "."; + return false; + } + label = label_remap[label]; + } + + BOOST_LOG_TRIVIAL(debug) << "TextureToColor: discarded " << (cluster_centers.size() - used_cluster_centers.size()) + << " unused adaptive cluster centers at " << stage_name << "."; + cluster_centers = std::move(used_cluster_centers); + return true; +} + +static bool ensure_all_cluster_centers_used(const std::vector<RGB>& source_face_colors, const std::vector<RGB>& cluster_centers, + std::vector<std::size_t>& face_labels, const char* stage_name) +{ + if (source_face_colors.size() != face_labels.size()) { + BOOST_LOG_TRIVIAL(warning) << "TextureToColor: cannot preserve cluster colors at " << stage_name + << ", face color count does not match label count."; + return false; + } + if (cluster_centers.empty()) { + BOOST_LOG_TRIVIAL(warning) << "TextureToColor: cannot preserve cluster colors at " << stage_name + << ", no cluster center is available."; + return false; + } + if (cluster_centers.size() > face_labels.size()) { + BOOST_LOG_TRIVIAL(warning) << "TextureToColor: cannot use all cluster centers at " << stage_name + << ", centers=" << cluster_centers.size() << " faces=" << face_labels.size() << "."; + return false; + } + + std::vector<std::size_t> usage = count_cluster_label_usage(face_labels, cluster_centers.size()); + std::size_t missing_count = 0; + for (std::size_t cluster_id = 0; cluster_id < usage.size(); ++cluster_id) { + if (usage[cluster_id] != 0) { + continue; + } + ++missing_count; + + double best_cost = std::numeric_limits<double>::max(); + std::size_t best_face_id = std::numeric_limits<std::size_t>::max(); + std::size_t best_old_cluster_id = std::numeric_limits<std::size_t>::max(); + + for (std::size_t fid = 0; fid < face_labels.size(); ++fid) { + const std::size_t old_cluster_id = face_labels[fid]; + if (old_cluster_id >= cluster_centers.size() || usage[old_cluster_id] <= 1) { + continue; + } + + const double old_dist = calc_rgb_color_difference_by_ciede2000(source_face_colors[fid], cluster_centers[old_cluster_id]); + const double new_dist = calc_rgb_color_difference_by_ciede2000(source_face_colors[fid], cluster_centers[cluster_id]); + const double cost = new_dist - old_dist; + if (cost < best_cost) { + best_cost = cost; + best_face_id = fid; + best_old_cluster_id = old_cluster_id; + } + } + + if (best_face_id == std::numeric_limits<std::size_t>::max()) { + BOOST_LOG_TRIVIAL(warning) << "TextureToColor: failed to assign a seed face for unused cluster " << cluster_id + << " at " << stage_name << "."; + continue; + } + + face_labels[best_face_id] = cluster_id; + --usage[best_old_cluster_id]; + ++usage[cluster_id]; + } + + if (missing_count > 0) { + BOOST_LOG_TRIVIAL(debug) << "TextureToColor: reassigned seed faces for " << missing_count + << " unused cluster centers at " << stage_name << "."; + } + + for (std::size_t count : usage) { + if (count == 0) { + return false; + } + } + return true; +} + +// Bilinear interpolation texture sampling; sub-pixel precision avoids nearest-neighbor aliasing +static RGB get_pixel_color(float u, float v, const cv::Mat& texture) { + u = u - std::floor(u); + v = v - std::floor(v); + + // glTF UV convention: (0,0) = top-left, v increases downward + float fx = u * (texture.cols - 1); + float fy = v * (texture.rows - 1); + + int x0 = std::clamp(static_cast<int>(fx), 0, texture.cols - 1); + int y0 = std::clamp(static_cast<int>(fy), 0, texture.rows - 1); + int x1 = std::min(x0 + 1, texture.cols - 1); + int y1 = std::min(y0 + 1, texture.rows - 1); + + float wx = fx - x0; + float wy = fy - y0; + + const int ch = texture.channels(); + auto sample = [&](int row, int col) -> std::array<float, 3> { + const uchar* ptr = texture.data + row * texture.step[0] + col * ch; + return {static_cast<float>(ptr[2]), static_cast<float>(ptr[1]), static_cast<float>(ptr[0])}; + }; + + auto c00 = sample(y0, x0); + auto c10 = sample(y0, x1); + auto c01 = sample(y1, x0); + auto c11 = sample(y1, x1); + + // Bilinear blend: lerp(lerp(c00,c10,wx), lerp(c01,c11,wx), wy) + RGB color; + for (int i = 0; i < 3; ++i) { + float top = c00[i] * (1.0f - wx) + c10[i] * wx; + float bot = c01[i] * (1.0f - wx) + c11[i] * wx; + color[i] = static_cast<std::size_t>(std::clamp(top * (1.0f - wy) + bot * wy, 0.0f, 255.0f)); + } + return color; +} + +// 7-point triangular Gaussian quadrature barycentric coordinates and weights (precision sufficient for capturing texture detail within faces) +static constexpr std::array<std::array<float, 3>, 7> GAUSS_TRI_BARY = {{ + {1.0f / 3.0f, 1.0f / 3.0f, 1.0f / 3.0f}, + {0.059715871f, 0.470142064f, 0.470142064f}, + {0.470142064f, 0.059715871f, 0.470142064f}, + {0.470142064f, 0.470142064f, 0.059715871f}, + {0.797426985f, 0.101286507f, 0.101286507f}, + {0.101286507f, 0.797426985f, 0.101286507f}, + {0.101286507f, 0.101286507f, 0.797426985f}, +}}; +static constexpr std::array<float, 7> GAUSS_TRI_WEIGHT = {0.225f, 0.132394152f, 0.132394152f, 0.132394152f, 0.125939181f, 0.125939181f, 0.125939181f}; +static_assert( + []() constexpr { + float sum = 0.0f; + for (auto w : GAUSS_TRI_WEIGHT) { + sum += w; + } + return sum > 0.999f && sum < 1.001f; + }(), + "Sum of Gaussian quadrature weights must be 1.0"); + +// Multi-point Gaussian quadrature sampling on a single face; returns weighted average color. +// GAUSS_TRI_WEIGHT sums to 1.0 (Hammer quadrature formula), no normalization needed. +static RGB sample_face_color(const std::array<Vec2f, 3>& uvs, const cv::Mat& texture) { + float r = 0.0f, g = 0.0f, b = 0.0f; + for (int k = 0; k < 7; ++k) { + float u = GAUSS_TRI_BARY[k][0] * uvs[0].x() + GAUSS_TRI_BARY[k][1] * uvs[1].x() + GAUSS_TRI_BARY[k][2] * uvs[2].x(); + float v = GAUSS_TRI_BARY[k][0] * uvs[0].y() + GAUSS_TRI_BARY[k][1] * uvs[1].y() + GAUSS_TRI_BARY[k][2] * uvs[2].y(); + RGB c = get_pixel_color(u, v, texture); + float w = GAUSS_TRI_WEIGHT[k]; + r += w * c[0]; + g += w * c[1]; + b += w * c[2]; + } + return RGB{static_cast<std::size_t>(std::clamp(r, 0.0f, 255.0f)), static_cast<std::size_t>(std::clamp(g, 0.0f, 255.0f)), + static_cast<std::size_t>(std::clamp(b, 0.0f, 255.0f))}; +} + +// Use array<Vec2f,3> instead of vector<Vec2f> for UV storage to avoid per-face heap allocations at million-face scale +using FaceUVArray = std::array<Vec2f, 3>; + +static bool linear_subdivision(TriMesh& mesh, std::vector<FaceUVArray>& uv_coords, const std::function<void(int)>& sub_progress = nullptr) { + const auto& original_vertices = mesh.vertices; + const auto& original_faces = mesh.indices; + TriVertices sub_vertices = mesh.vertices; + sub_vertices.reserve(original_vertices.size() + original_faces.size() * 3); + TriFaces sub_faces; + std::vector<FaceUVArray> sub_uv_coords; + + // Single-level flat map with edge key encoding replaces nested unordered_map; + // merges two vertex indices into a single uint64_t to reduce hash lookups and indirection. + if (original_vertices.size() >= (1ULL << 32)) [[unlikely]] { + BOOST_LOG_TRIVIAL(warning) << "[boundary] " << __FUNCTION__ << " vertex_count=" << original_vertices.size() << " exceeds 32-bit edge_key encoding range, skipping subdivision"; + return false; + } + auto edge_key = [](std::size_t a, std::size_t b) -> uint64_t { + return a < b ? ((static_cast<uint64_t>(a) << 32) | b) : ((static_cast<uint64_t>(b) << 32) | a); + }; + std::unordered_map<uint64_t, std::size_t> map_edge_to_sub_vtx; + map_edge_to_sub_vtx.reserve(original_faces.size() * 3 / 2); + + for (const auto& face : original_faces) { + for (std::size_t i = 0; i < 3; ++i) { + std::size_t vtx_1 = face[i]; + std::size_t vtx_2 = face[(i + 1) % 3]; + uint64_t key = edge_key(vtx_1, vtx_2); + if (map_edge_to_sub_vtx.count(key) > 0) { + continue; + } + TriVertex edge_vtx = (original_vertices[vtx_1] + original_vertices[vtx_2]) * 0.5; + map_edge_to_sub_vtx[key] = sub_vertices.size(); + sub_vertices.push_back(edge_vtx); + } + } + if (sub_progress) { + sub_progress(50); + } + + // Subdivide faces and their UVs: each original face splits into 4 sub-faces (parallel writes, no contention) + const std::size_t N = original_faces.size(); + sub_faces.resize(N * 4); + sub_uv_coords.resize(N * 4); + std::atomic<bool> has_missing_edge{false}; + + tbb::parallel_for(tbb::blocked_range<std::size_t>(0, N), [&](const tbb::blocked_range<size_t>& range) { + for (std::size_t fid = range.begin(); fid < range.end(); ++fid) { + const std::size_t base = fid * 4; + const auto& face = original_faces[fid]; + std::size_t vtx_0 = face[0]; + std::size_t vtx_1 = face[1]; + std::size_t vtx_2 = face[2]; + + auto it01 = map_edge_to_sub_vtx.find(edge_key(vtx_0, vtx_1)); + auto it12 = map_edge_to_sub_vtx.find(edge_key(vtx_1, vtx_2)); + auto it20 = map_edge_to_sub_vtx.find(edge_key(vtx_2, vtx_0)); + if (it01 == map_edge_to_sub_vtx.end() || it12 == map_edge_to_sub_vtx.end() || it20 == map_edge_to_sub_vtx.end()) [[unlikely]] { + has_missing_edge.store(true, std::memory_order_relaxed); + Vec3i32 degen(vtx_0, vtx_0, vtx_0); + FaceUVArray degen_uv = {uv_coords[fid][0], uv_coords[fid][0], uv_coords[fid][0]}; + for (int k = 0; k < 4; ++k) { + sub_faces[base + k] = degen; + sub_uv_coords[base + k] = degen_uv; + } + continue; + } + std::size_t e01 = it01->second; + std::size_t e12 = it12->second; + std::size_t e20 = it20->second; + + const Vec2f& uv0 = uv_coords[fid][0]; + const Vec2f& uv1 = uv_coords[fid][1]; + const Vec2f& uv2 = uv_coords[fid][2]; + Vec2f uv_e01 = (uv0 + uv1) * 0.5f; + Vec2f uv_e12 = (uv1 + uv2) * 0.5f; + Vec2f uv_e20 = (uv2 + uv0) * 0.5f; + + sub_faces[base + 0] = Vec3i32(vtx_0, e01, e20); + sub_uv_coords[base + 0] = {uv0, uv_e01, uv_e20}; + + sub_faces[base + 1] = Vec3i32(e01, vtx_1, e12); + sub_uv_coords[base + 1] = {uv_e01, uv1, uv_e12}; + + sub_faces[base + 2] = Vec3i32(e01, e12, e20); + sub_uv_coords[base + 2] = {uv_e01, uv_e12, uv_e20}; + + sub_faces[base + 3] = Vec3i32(e20, e12, vtx_2); + sub_uv_coords[base + 3] = {uv_e20, uv_e12, uv2}; + } + }); + // Remove degenerate triangles (three identical vertices) to avoid impacting downstream SDF / Remesh steps + if (has_missing_edge.load(std::memory_order_relaxed)) { + std::size_t write_idx = 0; + for (std::size_t i = 0; i < sub_faces.size(); ++i) { + if (sub_faces[i][0] == sub_faces[i][1] && sub_faces[i][1] == sub_faces[i][2]) { + continue; + } + if (write_idx != i) { + sub_faces[write_idx] = sub_faces[i]; + sub_uv_coords[write_idx] = sub_uv_coords[i]; + } + ++write_idx; + } + BOOST_LOG_TRIVIAL(warning) << "[warning] linear_subdivision has missing edge vertex, removed " << (sub_faces.size() - write_idx) << " degenerate triangles"; + sub_faces.resize(write_idx); + sub_uv_coords.resize(write_idx); + } + + if (sub_progress) { + sub_progress(100); + } + BOOST_LOG_TRIVIAL(debug) << "TextureToColor: input faces count = " << mesh.indices.size() << "."; + mesh = TriMesh(sub_faces, sub_vertices); + uv_coords = std::move(sub_uv_coords); + BOOST_LOG_TRIVIAL(debug) << "TextureToColor: output faces count = " << mesh.indices.size() << "."; + return true; +} + +using VertexColor = std::array<float, 4>; + +// Quantize continuous per-vertex colors into a small palette of cluster centers. +// The legacy OBJ vertex-color import consumed discrete filament ids, so split +// decisions could be made by comparing integers. Quantizing up front restores +// that property for the adaptive splitter below. +static bool quantize_vertex_colors( + const std::vector<VertexColor>& vertex_colors, + const TextureToColorSettings& settings, + AlgoCancelCallback cancel_callback, + std::vector<RGB>& out_centers, + std::vector<std::size_t>& out_vertex_cluster_ids) +{ + out_centers.clear(); + out_vertex_cluster_ids.clear(); + if (vertex_colors.empty()) + return false; + + std::vector<RGB> vertex_rgb(vertex_colors.size()); + for (std::size_t i = 0; i < vertex_colors.size(); ++i) { + for (int c = 0; c < 3; ++c) { + float v = std::clamp(vertex_colors[i][c] * 255.0f, 0.0f, 255.0f); + vertex_rgb[i][c] = static_cast<std::size_t>(v); + } + } + + ClusterParameters para; + para.cancel_callback = cancel_callback ? [&]() { return cancel_callback(); } : std::function<bool()>{}; + if (settings.target_colors_num == 0) { + para.max_color_distance = settings.max_color_distance; + para.max_cluster_k = settings.max_cluster_k; + out_centers = cluster_adaptive(vertex_rgb, para); + } else { + para.cluster_k = settings.target_colors_num; + out_centers = cluster_k_means(vertex_rgb, para); + } + if (out_centers.empty()) { + BOOST_LOG_TRIVIAL(debug) << "quantize_vertex_colors: no cluster center generated."; + return false; + } + + out_vertex_cluster_ids.resize(vertex_rgb.size()); + for (std::size_t i = 0; i < vertex_rgb.size(); ++i) { + std::size_t nearest_id = 0; + if (!calc_nearest_color_id(out_centers, vertex_rgb[i], nearest_id)) + nearest_id = 0; + out_vertex_cluster_ids[i] = nearest_id; + } + BOOST_LOG_TRIVIAL(debug) << "quantize_vertex_colors: quantized " << vertex_rgb.size() + << " vertex colors into " << out_centers.size() << " clusters."; + return true; +} + +// Single-level adaptive subdivision driven by per-vertex cluster ids. +// +// Reproduces the split topology that the legacy OBJ vertex-color import encoded +// into mmu_segmentation_facets (TriangleSelector::perform_split cases 1/2/3), but +// materializes it as real geometry. An edge is split at its midpoint if and only +// if its two endpoints belong to different clusters. Because that predicate reads +// only the shared endpoints, adjacent faces always reach the same conclusion and +// no T-junctions can appear. +static bool adaptive_split_by_vertex_clusters( + TriMesh& mesh, + const std::vector<std::size_t>& vertex_cluster_ids, + const std::vector<RGB>& cluster_centers, + std::vector<RGB>& out_face_colors) +{ + const TriVertices original_vertices = mesh.vertices; + const TriFaces original_faces = mesh.indices; + if (original_vertices.empty() || original_faces.empty() || cluster_centers.empty()) + return false; + if (vertex_cluster_ids.size() != original_vertices.size()) { + BOOST_LOG_TRIVIAL(warning) << "adaptive_split_by_vertex_clusters: cluster id count (" + << vertex_cluster_ids.size() << ") != vertex count (" + << original_vertices.size() << ")."; + return false; + } + if (original_vertices.size() >= (1ULL << 32)) [[unlikely]] { + BOOST_LOG_TRIVIAL(warning) << "adaptive_split_by_vertex_clusters: vertex_count=" + << original_vertices.size() << " exceeds 32-bit edge_key range."; + return false; + } + + TriVertices out_vertices = original_vertices; + TriFaces out_faces; + out_faces.reserve(original_faces.size() * 5); + out_face_colors.clear(); + out_face_colors.reserve(original_faces.size() * 5); + + auto edge_key = [](std::size_t a, std::size_t b) -> uint64_t { + return a < b ? ((static_cast<uint64_t>(a) << 32) | b) + : ((static_cast<uint64_t>(b) << 32) | a); + }; + std::unordered_map<uint64_t, std::size_t> edge_to_mid; + edge_to_mid.reserve(original_faces.size() * 3 / 2); + + // Midpoints on shared edges must be deduplicated so that neighbouring faces + // reference the same vertex instead of coincident duplicates. + auto midpoint_of_edge = [&](std::size_t a, std::size_t b) -> std::size_t { + const uint64_t key = edge_key(a, b); + auto it = edge_to_mid.find(key); + if (it != edge_to_mid.end()) + return it->second; + const std::size_t idx = out_vertices.size(); + out_vertices.push_back((original_vertices[a] + original_vertices[b]) * 0.5f); + edge_to_mid.emplace(key, idx); + return idx; + }; + // Points strictly inside an original face are never shared, so they skip the map. + // The midpoint is computed before push_back so a reallocation cannot dangle it. + auto append_interior_midpoint = [&](std::size_t a, std::size_t b) -> std::size_t { + const TriVertex mid = (out_vertices[a] + out_vertices[b]) * 0.5f; + const std::size_t idx = out_vertices.size(); + out_vertices.push_back(mid); + return idx; + }; + auto emit = [&](std::size_t a, std::size_t b, std::size_t c, std::size_t cluster_id) { + out_faces.push_back(Vec3i32(static_cast<int>(a), static_cast<int>(b), static_cast<int>(c))); + out_face_colors.push_back(cluster_centers[cluster_id]); + }; + + for (const auto& f : original_faces) { + const std::size_t v[3] = {static_cast<std::size_t>(f[0]), static_cast<std::size_t>(f[1]), static_cast<std::size_t>(f[2])}; + const std::size_t c[3] = {vertex_cluster_ids[v[0]], vertex_cluster_ids[v[1]], vertex_cluster_ids[v[2]]}; + + // Case A: uniform cluster, keep the face untouched. + if (c[0] == c[1] && c[1] == c[2]) { + emit(v[0], v[1], v[2], c[0]); + continue; + } + + // Case B: two vertices share a cluster and the third is isolated. Split the + // two edges incident to the isolated vertex, which are exactly the + // cross-cluster ones; the opposite edge stays intact. + int iso = -1; + if (c[1] == c[2]) iso = 0; + else if (c[2] == c[0]) iso = 1; + else if (c[0] == c[1]) iso = 2; + if (iso >= 0) { + const int i = iso, j = (iso + 1) % 3, k = (iso + 2) % 3; + const std::size_t m_ij = midpoint_of_edge(v[i], v[j]); + const std::size_t m_ki = midpoint_of_edge(v[k], v[i]); + emit(v[i], m_ij, m_ki, c[i]); + emit(m_ij, v[j], m_ki, c[j]); + emit(v[j], v[k], m_ki, c[j]); + continue; + } + + // Case C: all three clusters differ. Split every edge, then cut the centre + // triangle once more. The centre is equidistant from all three clusters, so + // the legacy heuristic selects the cut by widest interior angle, which is + // the vertex opposite the longest edge. + const std::size_t m01 = midpoint_of_edge(v[0], v[1]); + const std::size_t m12 = midpoint_of_edge(v[1], v[2]); + const std::size_t m20 = midpoint_of_edge(v[2], v[0]); + emit(v[0], m01, m20, c[0]); + emit(m01, v[1], m12, c[1]); + emit(m12, v[2], m20, c[2]); + + const TriVertex& p0 = original_vertices[v[0]]; + const TriVertex& p1 = original_vertices[v[1]]; + const TriVertex& p2 = original_vertices[v[2]]; + const float sq_opposite_v0 = (p2 - p1).squaredNorm(); + const float sq_opposite_v1 = (p0 - p2).squaredNorm(); + const float sq_opposite_v2 = (p1 - p0).squaredNorm(); + int widest = 0; + float widest_len = sq_opposite_v0; + if (sq_opposite_v1 > widest_len) { widest = 1; widest_len = sq_opposite_v1; } + if (sq_opposite_v2 > widest_len) { widest = 2; } + + if (widest == 0) { + const std::size_t mc = append_interior_midpoint(m20, m01); + emit(m12, m20, mc, c[1]); + emit(mc, m01, m12, c[2]); + } else if (widest == 1) { + const std::size_t mc = append_interior_midpoint(m01, m12); + emit(m20, m01, mc, c[0]); + emit(mc, m12, m20, c[2]); + } else { + const std::size_t mc = append_interior_midpoint(m12, m20); + emit(m01, m12, mc, c[1]); + emit(mc, m20, m01, c[0]); + } + } + + BOOST_LOG_TRIVIAL(info) << "adaptive_split_by_vertex_clusters: faces " << original_faces.size() + << " -> " << out_faces.size() << ", vertices " << original_vertices.size() + << " -> " << out_vertices.size(); + mesh = TriMesh(out_faces, out_vertices); + return true; +} + +// Shared pipeline: mesh repair -> color clustering -> label assignment -> smoothing. +// Called by both TextureToColor (after UV sampling) and ClusterAndSmooth (after vertex-color oversample). +// progress_callback reports 0~100 within this function; the caller maps it to its own global range. +static bool repair_cluster_smooth( + TriMesh& mesh, + std::vector<RGB>& face_colors, + std::vector<RGB>& out_clustered_face_colors, + const TextureToColorSettings& settings, + AlgoProgressCallback progress_callback, + AlgoCancelCallback cancel_callback, + const char* log_prefix) +{ + auto report = [&](int pct, const char* msg) { + if (progress_callback) + progress_callback({pct, msg}); + }; + auto cancelled = [&]() -> bool { + if (cancel_callback && cancel_callback()) { + BOOST_LOG_TRIVIAL(debug) << log_prefix << " cancelled"; + return true; + } + return false; + }; + + report(0, "Repairing mesh"); + if (cancelled()) return false; + + // Resample face colors onto a repaired mesh via centroid nearest-neighbor. + auto resample_face_colors = [&](TriMesh&& repaired_mesh) -> bool { + TriVertices old_vertices = std::move(mesh.vertices); + TriFaces old_indices = std::move(mesh.indices); + auto aabb_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(old_vertices, old_indices); + mesh = std::move(repaired_mesh); + + if (is_closed(mesh)) { + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": repaired mesh is closed."; + } else { + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": repaired mesh is open."; + } + + std::vector<RGB> new_face_colors(mesh.facets_count()); + tbb::parallel_for(tbb::blocked_range<std::size_t>(0, mesh.facets_count()), [&](const tbb::blocked_range<size_t>& range) { + for (std::size_t fid = range.begin(); fid < range.end(); ++fid) { + const auto& face = mesh.indices[fid]; + Vec3f center = (mesh.vertices[face[0]] + mesh.vertices[face[1]] + mesh.vertices[face[2]]) / 3.0f; + size_t hit_idx = 0; + Vec3f closest; + AABBTreeIndirect::squared_distance_to_indexed_triangle_set( + old_vertices, old_indices, aabb_tree, center, hit_idx, closest); + new_face_colors[fid] = face_colors[hit_idx]; + } + }); + face_colors = std::move(new_face_colors); + return true; + }; + + auto repair_and_resample = [&]() -> bool { + std::shared_ptr<TriMesh> repaired_mesh; + if (!RepairMesh(mesh, repaired_mesh)) { + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": RepairMesh failed."; + return false; + } + if (cancelled()) return false; + return resample_face_colors(std::move(*repaired_mesh)); + }; + + { + TriangleMesh stats_mesh(static_cast<const indexed_triangle_set&>(mesh)); + const auto& stats = stats_mesh.stats(); + // Orca's TriangleMeshStats only counts open edges: manifold() is open_edges == 0, and + // there are no separate non-manifold edge/vertex counters to test or log here. + if (!stats.manifold()) { + BOOST_LOG_TRIVIAL(info) << log_prefix << ": mesh has non-manifold geometry or open boundaries, open_edges=" + << stats.open_edges; + if (settings.mesh_repair_decision == MeshRepairDecision::Ask) { + if (settings.mesh_repair_decision_required) + *settings.mesh_repair_decision_required = true; + return false; + } + if (settings.mesh_repair_decision == MeshRepairDecision::RepairAndImport) { + indexed_triangle_set repaired_its; + std::string repair_error; + bool repaired = settings.mesh_repair_callback && settings.mesh_repair_callback( + static_cast<const indexed_triangle_set&>(mesh), repaired_its, + [&](const char* message, unsigned /*percent*/) { + report(5, message ? message : "Repairing mesh"); + }, + [&]() { return cancelled(); }, &repair_error); + if (repaired) { + if (cancelled()) return false; + BOOST_LOG_TRIVIAL(info) << log_prefix << ": Windows 3D mesh repair finished."; + if (!resample_face_colors(TriMesh(std::move(repaired_its)))) + return false; + } else { + BOOST_LOG_TRIVIAL(warning) << log_prefix << ": Windows 3D mesh repair failed: " << repair_error; + } + } else { + BOOST_LOG_TRIVIAL(info) << log_prefix << ": importing mesh without Windows 3D repair."; + } + } + } + + if (!cgalutils::is_mesh_halfedge_compatible(mesh)) { + BOOST_LOG_TRIVIAL(info) << log_prefix << ": mesh not halfedge-compatible, attempting RepairMesh."; + if (!repair_and_resample()) + return false; + } + +#ifdef OUTPUT_TEST_RESULT + SaveToOFF(std::string(log_prefix) + "_1_repair.off", mesh, face_colors); +#endif + + report(20, "Color clustering"); + if (cancelled()) return false; + + // Clustering + std::vector<RGB> cluster_centers; + out_clustered_face_colors = face_colors; + std::vector<std::size_t> clustered_face_labels(face_colors.size()); + const bool adaptive_cluster = settings.target_colors_num == 0; + + if (adaptive_cluster) { + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": use cluster adaptive method."; + ClusterParameters para; + para.max_color_distance = settings.max_color_distance; + para.max_cluster_k = settings.max_cluster_k; + para.cancel_callback = cancel_callback ? [&]() { return cancel_callback(); } : std::function<bool()>{}; + cluster_centers = cluster_adaptive(face_colors, para); + if (cancelled()) return false; + } else { + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": use cluster k-means method."; + ClusterParameters para; + para.cluster_k = settings.target_colors_num; + para.cancel_callback = cancel_callback ? [&]() { return cancel_callback(); } : std::function<bool()>{}; + cluster_centers = cluster_k_means(face_colors, para); + if (cancelled()) return false; + } + + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": k = " << cluster_centers.size() << "."; + if (cluster_centers.empty()) { + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": no cluster center generated."; + return false; + } + + report(40, "Assigning cluster labels"); + if (cancelled()) return false; + + // Assign each face to nearest cluster center + { + std::atomic<size_t> done{0}; + std::atomic<bool> cancel_requested{false}; + const size_t total = mesh.indices.size(); + const size_t interval = std::max<size_t>(total / 20, 1); + tbb::parallel_for(tbb::blocked_range<std::size_t>(0, total), [&](const tbb::blocked_range<size_t>& range) { + for (std::size_t fid = range.begin(); fid < range.end(); ++fid) { + if (cancel_requested.load(std::memory_order_relaxed)) return; + std::size_t nearest_id = 0; + calc_nearest_color_id(cluster_centers, face_colors[fid], nearest_id); + clustered_face_labels[fid] = nearest_id; + out_clustered_face_colors[fid] = cluster_centers[nearest_id]; + size_t cnt = done.fetch_add(1, std::memory_order_relaxed) + 1; + if (cnt % interval == 0) { + if (cancelled()) { cancel_requested.store(true, std::memory_order_relaxed); return; } + } + } + }); + if (cancel_requested.load() || cancelled()) return false; + } + if (adaptive_cluster) { + if (!discard_unused_cluster_centers(cluster_centers, clustered_face_labels, "cluster assignment")) + return false; + } else { + ensure_all_cluster_centers_used(face_colors, cluster_centers, clustered_face_labels, "cluster assignment"); + } + +#ifdef OUTPUT_TEST_RESULT + { + std::vector<RGB> tmp = out_clustered_face_colors; + for (std::size_t i = 0; i < tmp.size(); ++i) + tmp[i] = cluster_centers[clustered_face_labels[i]]; + SaveToOFF(std::string(log_prefix) + "_3_cluster.off", mesh, tmp); + } +#endif + + report(65, "Smoothing colors"); + if (cancelled()) return false; + + SmoothParameters smooth_parameters; + smooth_parameters.smooth_weight = settings.smooth_weight; + if (!smooth_region(mesh, clustered_face_labels, smooth_parameters)) { + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": smooth region failed."; + return false; + } + if (adaptive_cluster) { + if (!discard_unused_cluster_centers(cluster_centers, clustered_face_labels, "color smoothing")) + return false; + } else { + ensure_all_cluster_centers_used(face_colors, cluster_centers, clustered_face_labels, "color smoothing"); + } + + report(90, "Updating face colors"); + if (cancelled()) return false; + + for (std::size_t i = 0; i < out_clustered_face_colors.size(); ++i) + out_clustered_face_colors[i] = cluster_centers[clustered_face_labels[i]]; + +#ifdef OUTPUT_TEST_RESULT + SaveToOFF(std::string(log_prefix) + "_4_smooth.off", mesh, out_clustered_face_colors); +#endif + + report(100, "Completed"); + return true; +} + +bool TextureToColor(const TriMesh& texture_mesh, const std::vector<std::vector<Vec2f>>& texture_mesh_uv_coords, const cv::Mat& texture, TriMesh& color_mesh, + std::vector<std::array<std::size_t, 3>>& face_colors, const TextureToColorSettings& settings, AlgoProgressCallback progress_callback, + AlgoCancelCallback cancel_callback) { + auto report = [&](int pct, const char* msg) { + if (progress_callback) { + progress_callback({pct, msg}); + } + }; + auto sub_report = [&](int sub_pct, int range_start, int range_end, const char* msg) { + int pct = range_start + sub_pct * (range_end - range_start) / 100; + report(pct, msg); + }; + auto cancelled = [&]() -> bool { + if (cancel_callback && cancel_callback()) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " cancelled"; + return true; + } + return false; + }; + + color_mesh.clear(); + face_colors.clear(); + + report(0, "Initializing"); + if (cancelled()) { + return false; + } + + if (texture_mesh.indices.size() == 0) { + BOOST_LOG_TRIVIAL(debug) << "TextureToColor: texture mesh has no faces."; + return false; + } + if (texture.empty()) { + BOOST_LOG_TRIVIAL(debug) << "TextureToColor: texture is empty."; + return false; + } + if (texture.channels() < 3) { + BOOST_LOG_TRIVIAL(debug) << "TextureToColor: texture must have at least 3 channels, got " << texture.channels(); + return false; + } + if (texture_mesh_uv_coords.size() != texture_mesh.indices.size()) { + BOOST_LOG_TRIVIAL(debug) << "TextureToColor: uv_coords size is not equal to texture mesh faces size."; + return false; + } + for (std::size_t fid = 0; fid < texture_mesh.indices.size(); ++fid) { + if (texture_mesh_uv_coords[fid].size() != 3) { + BOOST_LOG_TRIVIAL(debug) << "TextureToColor: uv_coords of single face size is not equal to 3."; + return false; + } + } + color_mesh = texture_mesh; + + using Clock = std::chrono::high_resolution_clock; + const auto t_total_start = Clock::now(); + auto t_step = t_total_start; + auto lap = [&](const char* step_name) { + auto now = Clock::now(); + double ms = std::chrono::duration<double, std::milli>(now - t_step).count(); + BOOST_LOG_TRIVIAL(debug) << "[timing] " << step_name << ": " << ms << "ms" + << " faces=" << color_mesh.facets_count(); + t_step = now; + }; + + report(5, "Oversampling"); + if (cancelled()) { + return false; + } + + // Step 1: Oversampling (subdivision while propagating UVs) + // Convert external vector<vector<Vec2f>> to internal vector<array<Vec2f,3>> to eliminate inner-level heap allocations + std::vector<FaceUVArray> color_mesh_uv_coords(texture_mesh_uv_coords.size()); + for (std::size_t i = 0; i < texture_mesh_uv_coords.size(); ++i) { + color_mesh_uv_coords[i] = {texture_mesh_uv_coords[i][0], texture_mesh_uv_coords[i][1], texture_mesh_uv_coords[i][2]}; + } + { + // Estimate total iterations and map each iteration's sub-progress to the [5, 25] range + size_t estimated_iters = 0; + if (settings.oversampling_iters > 0) { + estimated_iters = settings.oversampling_iters; + } else { + size_t fc = color_mesh.facets_count(); + while (fc < settings.oversampling_min_face_count) { + fc *= 4; + ++estimated_iters; + } + if (estimated_iters == 0) { + estimated_iters = 1; + } + } + + auto make_iter_progress = [&](size_t iter) { + return [&, iter, estimated_iters](int pct) { + int iter_start = static_cast<int>(iter * 100 / estimated_iters); + int iter_end = static_cast<int>((iter + 1) * 100 / estimated_iters); + int sub_pct = iter_start + pct * (iter_end - iter_start) / 100; + sub_report(sub_pct, 5, 25, "Oversampling"); + }; + }; + + if (settings.oversampling_iters > 0) { + for (size_t i = 0; i < settings.oversampling_iters && color_mesh.facets_count() * 4.0 < settings.oversampling_max_face_count; ++i) { + if (cancelled()) return false; + linear_subdivision(color_mesh, color_mesh_uv_coords, make_iter_progress(i)); + } + } else { + size_t iter = 0; + while (color_mesh.facets_count() < settings.oversampling_min_face_count) { + if (cancelled()) return false; + linear_subdivision(color_mesh, color_mesh_uv_coords, make_iter_progress(iter++)); + } + } + } + + lap("Oversampling"); + + face_colors.resize(color_mesh.indices.size()); + + report(25, "Computing face colors"); + if (cancelled()) { + return false; + } + + // Step 2: Compute each face's color (7-point Gaussian quadrature + bilinear interpolation sampling) + { + std::atomic<size_t> done_faces{0}; + std::atomic<bool> cancel_requested{false}; + const size_t total_faces = color_mesh.indices.size(); + const size_t report_interval = std::max<size_t>(total_faces / 20, 1); + tbb::parallel_for(tbb::blocked_range<std::size_t>(0, total_faces), [&](const tbb::blocked_range<size_t>& range) { + for (std::size_t fid = range.begin(); fid < range.end(); ++fid) { + if (cancel_requested.load(std::memory_order_relaxed)) return; + face_colors[fid] = sample_face_color(color_mesh_uv_coords[fid], texture); + size_t cnt = done_faces.fetch_add(1, std::memory_order_relaxed) + 1; + if (cnt % report_interval == 0) { + if (cancelled()) { cancel_requested.store(true, std::memory_order_relaxed); return; } + sub_report(static_cast<int>(cnt * 100 / total_faces), 25, 40, "Computing face colors"); + } + } + }); + if (cancel_requested.load() || cancelled()) return false; + } + lap("Computing face colors"); +#ifdef OUTPUT_TEST_RESULT + SaveToOFF("texture_to_color_0_initialize.off", color_mesh, face_colors); +#endif + + // Map progress from repair_cluster_smooth's [0,100] to TextureToColor's [40,100] + AlgoProgressCallback rcs_progress = nullptr; + if (progress_callback) { + rcs_progress = [&](AlgoProgress p) { + int mapped_pct = 40 + p.percent * 60 / 100; + progress_callback({mapped_pct, p.message}); + }; + } + + std::vector<RGB> clustered_face_colors; + if (!repair_cluster_smooth(color_mesh, face_colors, clustered_face_colors, + settings, rcs_progress, cancel_callback, "TextureToColor")) + return false; + + face_colors = std::move(clustered_face_colors); + lap("Repair + Clustering + Smoothing"); + double total_ms = std::chrono::duration<double, std::milli>(Clock::now() - t_total_start).count(); + BOOST_LOG_TRIVIAL(debug) << "[timing] TextureToColor total: " << total_ms << "ms" + << " faces=" << color_mesh.facets_count(); + report(100, "Completed"); + return true; +} + +bool ClusterAndSmooth(const TriMesh& mesh, + const std::vector<std::array<std::size_t, 3>>& input_face_colors, + TriMesh& out_mesh, + std::vector<std::array<std::size_t, 3>>& out_face_colors, + const TextureToColorSettings& settings, + AlgoProgressCallback progress_callback, + AlgoCancelCallback cancel_callback, + const std::vector<std::array<float, 4>>& vertex_colors) +{ + auto report = [&](int pct, const char* msg) { + if (progress_callback) + progress_callback({pct, msg}); + }; + auto cancelled = [&]() -> bool { + if (cancel_callback && cancel_callback()) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " cancelled"; + return true; + } + return false; + }; + + out_mesh = mesh; + out_face_colors.clear(); + + if (mesh.indices.empty() || input_face_colors.empty()) { + BOOST_LOG_TRIVIAL(debug) << "ClusterAndSmooth: empty mesh or face colors."; + return false; + } + if (input_face_colors.size() != mesh.indices.size()) { + BOOST_LOG_TRIVIAL(warning) << "ClusterAndSmooth: face_colors size (" + << input_face_colors.size() << ") != indices size (" + << mesh.indices.size() << "), clamping."; + } + + report(0, "Initializing"); + if (cancelled()) return false; + + // Prepare face colors aligned to mesh size + std::vector<RGB> face_colors(out_mesh.indices.size()); + for (size_t i = 0; i < out_mesh.indices.size(); ++i) { + if (i < input_face_colors.size()) + face_colors[i] = input_face_colors[i]; + else + face_colors[i] = {128, 128, 128}; + } + + // Low-poly vertex-color meshes take the legacy OBJ import route: quantize the + // vertex colors, then split only across cluster boundaries. Colors are exact + // cluster centers afterwards, so repair / re-clustering / smoothing are skipped + // to match the legacy behaviour, which never touched the mesh either. + // A vertex color count that disagrees with the mesh falls through to the generic + // pipeline below rather than failing the import outright. + if (!vertex_colors.empty() && + vertex_colors.size() == out_mesh.vertices.size() && + out_mesh.facets_count() < settings.oversampling_min_face_count) { + report(10, "Quantizing vertex colors"); + std::vector<RGB> cluster_centers; + std::vector<std::size_t> vertex_cluster_ids; + if (!quantize_vertex_colors(vertex_colors, settings, cancel_callback, cluster_centers, vertex_cluster_ids)) { + BOOST_LOG_TRIVIAL(debug) << "ClusterAndSmooth: vertex color quantization failed."; + return false; + } + if (cancelled()) return false; + + report(50, "Splitting color boundaries"); + if (!adaptive_split_by_vertex_clusters(out_mesh, vertex_cluster_ids, cluster_centers, face_colors)) { + BOOST_LOG_TRIVIAL(debug) << "ClusterAndSmooth: adaptive vertex-color split failed."; + return false; + } + if (cancelled()) return false; + + out_face_colors = std::move(face_colors); + report(100, "Completed"); + return true; + } + + std::vector<RGB> clustered_face_colors; + if (!repair_cluster_smooth(out_mesh, face_colors, clustered_face_colors, + settings, progress_callback, cancel_callback, + "ClusterAndSmooth")) + return false; + + out_face_colors = std::move(clustered_face_colors); + return true; +} + +} // namespace tex2color +} // namespace Slic3r diff --git a/src/libslic3r/TextureToColor/TextureToColor.hpp b/src/libslic3r/TextureToColor/TextureToColor.hpp new file mode 100644 index 0000000000..019a113fc4 --- /dev/null +++ b/src/libslic3r/TextureToColor/TextureToColor.hpp @@ -0,0 +1,103 @@ +#pragma once + +#include "Callbacks.hpp" +#include "TriMesh.hpp" +#include "opencv2/core.hpp" +#include <functional> +#include <string> + +namespace Slic3r { namespace tex2color { + +enum class MeshRepairDecision { + Ask, + ImportWithoutRepair, + RepairAndImport +}; + +using MeshRepairCallback = std::function<bool(const indexed_triangle_set& mesh, + indexed_triangle_set& repaired_mesh, + std::function<void(const char* message, unsigned progress)> progress_callback, + std::function<bool()> cancel_callback, + std::string* error_message)>; + +struct TextureToColorSettings { + std::size_t target_colors_num = 4; // 目标颜色数量, 为0时, 自适应计算; 否则计算指定数目的颜色聚类 + + double smooth_weight = 0.5; // 光顺权重, 范围[0, 1], 0表示不进行光顺, 1表示完全光顺 + + // 当超采样迭代次数大于0时, 进行指定迭代次数的超采样; 否则, 自适应超采样 + std::size_t oversampling_iters = 0; // 超采样迭代次数 + std::size_t oversampling_min_face_count = 10000; // 自适应采样: 当face_count小于oversampling_min_face_count时, 进行超采样 + std::size_t oversampling_max_face_count = 1000000; // 无论输入参数如何, 超采样后的面片数不能超过oversampling_max_face_count + + double max_color_distance = 25.0; // 自适应聚类允许的最大簇内半径(CIEDE2000 ΔE) + std::size_t max_cluster_k = 32; // 自适应聚类的最大颜色数量上限 + + MeshRepairDecision mesh_repair_decision = MeshRepairDecision::ImportWithoutRepair; + + // Set by TextureToColor when Ask is selected and mesh repair needs user confirmation. + bool* mesh_repair_decision_required = nullptr; + + MeshRepairCallback mesh_repair_callback; +}; + +/** + * @brief 将纹理贴图转换为网格面片颜色, 并通过聚类和光顺生成可用于多色打印的着色网格 + * + * 基于纹理网格的UV坐标对纹理图像进行采样, 计算每个面片的颜色, + * 然后对颜色进行聚类(K-Means或自适应)和区域光顺, 最终输出带颜色信息的网格 + * + * @param[in] texture_mesh 带有UV坐标的输入三角网格 + * @param[in] uv_coords 每个面片的UV坐标, 大小等于面片数, 每个面片有三个UV坐标 + * @param[in] texture 纹理图像 + * @param[out] color_mesh 输出的着色网格 + * @param[out] face_colors 输出的着色网格的面片颜色, 大小等于面片数, 颜色值为[R, G, B], 范围0~255 + * @param[in] settings 算法参数, 包括目标颜色数量、光顺权重等 + * @param[in] progress_callback 进度回调函数 + * @param[in] cancel_callback 取消回调函数 + * @return 成功返回true, 输入数据无效(空网格、无UV、空纹理等)返回false + */ +bool TextureToColor(const TriMesh& texture_mesh, const std::vector<std::vector<Vec2f>>& uv_coords, const cv::Mat& texture, TriMesh& color_mesh, + std::vector<std::array<std::size_t, 3>>& face_colors, const TextureToColorSettings& settings = TextureToColorSettings(), + AlgoProgressCallback progress_callback = nullptr, AlgoCancelCallback cancel_callback = nullptr); + +/** + * @brief Turn pre-computed per-face colors into a clustered color mesh (no texture/UV). + * + * Used for OBJ vertex colors and MTL face colors, which bypass texture sampling. + * Two routes are possible: + * - Low-poly meshes carrying per-vertex colors: the vertex colors are quantized + * into a small palette and the mesh is geometrically split along cluster + * boundaries, reproducing the split topology of the legacy OBJ vertex-color + * import. Output colors are then exact cluster centers, so mesh repair, + * re-clustering and smoothing are skipped. + * - Everything else: mesh repair, color clustering (K-Means or adaptive) and + * region smoothing, sharing the same pipeline as TextureToColor. + * + * @param[in] mesh Input triangle mesh + * @param[in] input_face_colors Pre-computed per-face RGB colors [0..255] + * @param[out] out_mesh Output mesh. Geometry is subdivided on the + * vertex-color route, and may still be replaced + * by mesh repair on the generic route. + * @param[out] out_face_colors Output per-face colors, one entry per out_mesh face + * @param[in] settings Algorithm parameters (target_colors_num, smooth_weight; + * oversampling_min_face_count doubles as the low-poly + * threshold for the vertex-color route) + * @param[in] progress_callback Progress callback + * @param[in] cancel_callback Cancel callback + * @param[in] vertex_colors Optional per-vertex RGBA [0..1]. Must match + * mesh.vertices in size to enable the vertex-color + * route; otherwise it is ignored. + * @return true on success, false on failure or cancellation + */ +bool ClusterAndSmooth(const TriMesh& mesh, + const std::vector<std::array<std::size_t, 3>>& input_face_colors, + TriMesh& out_mesh, + std::vector<std::array<std::size_t, 3>>& out_face_colors, + const TextureToColorSettings& settings = TextureToColorSettings(), + AlgoProgressCallback progress_callback = nullptr, + AlgoCancelCallback cancel_callback = nullptr, + const std::vector<std::array<float, 4>>& vertex_colors = {}); + +} // namespace tex2color +} // namespace Slic3r diff --git a/src/libslic3r/TextureToColor/TriMesh.hpp b/src/libslic3r/TextureToColor/TriMesh.hpp new file mode 100644 index 0000000000..d557ae1d2e --- /dev/null +++ b/src/libslic3r/TextureToColor/TriMesh.hpp @@ -0,0 +1,28 @@ +#pragma once +#include <admesh/stl.h> +#include "Point.hpp" + +namespace Slic3r { namespace tex2color { + +using TriVertex = stl_vertex; +using TriVertices = std::vector<stl_vertex>; +using TriFace = stl_triangle_vertex_indices; +using TriFaces = std::vector<stl_triangle_vertex_indices>; + +struct TriMesh : ::indexed_triangle_set { + TriMesh() = default; + TriMesh(const TriMesh&) = default; + TriMesh& operator=(const TriMesh&) = default; + TriMesh(TriMesh&&) = default; + TriMesh& operator=(TriMesh&&) = default; + TriMesh(const ::indexed_triangle_set& d) : ::indexed_triangle_set(d) {} + TriMesh(::indexed_triangle_set&& d) : ::indexed_triangle_set(std::move(d)) {} + TriMesh(std::vector<stl_triangle_vertex_indices> indices_, + std::vector<stl_vertex> vertices_) + : ::indexed_triangle_set(std::move(indices_), std::move(vertices_)) {} + + std::size_t facets_count() const { return indices.size(); } +}; + +} // namespace tex2color +} // namespace Slic3r diff --git a/src/libslic3r/TriangleMeshSlicer.cpp b/src/libslic3r/TriangleMeshSlicer.cpp index 2c1c0da23f..417ca354d3 100644 --- a/src/libslic3r/TriangleMeshSlicer.cpp +++ b/src/libslic3r/TriangleMeshSlicer.cpp @@ -146,6 +146,85 @@ public: using IntersectionLines = std::vector<IntersectionLine>; +// Orca: A planar face is commonly represented by multiple triangles. A slicing plane then crosses +// their shared edges and creates intermediate 2D points which are not part of the model contour. +// Track only edges whose two incident triangles lie in the same geometric plane within the slicing +// coordinate precision, so those artificial junctions can be omitted without simplifying genuine, +// nearly-collinear geometry. +using CoplanarEdges = std::vector<bool>; + +static CoplanarEdges coplanar_edges(const indexed_triangle_set &mesh, const std::vector<Vec3i32> &face_edge_ids, + const Transform3d &trafo) +{ + struct FacePlane { + Vec3d origin { Vec3d::Zero() }; + Vec3d normal { Vec3d::Zero() }; + bool valid { false }; + }; + + // Orca: Edge IDs are dense but may include boundary edges referenced by just one face. + int num_edges = 0; + for (const Vec3i32 &edge_ids : face_edge_ids) + num_edges = std::max(num_edges, edge_ids.maxCoeff() + 1); + + CoplanarEdges coplanar(num_edges, false); + std::vector<int> first_face(num_edges, -1); + std::vector<int> first_face_edge(num_edges, -1); + std::vector<FacePlane> face_planes(face_edge_ids.size()); + std::vector<bool> face_plane_computed(face_edge_ids.size(), false); + auto transformed_vertex = [&mesh, &trafo](int vertex_idx) { + return trafo * mesh.vertices[vertex_idx].cast<double>(); + }; + // Orca: Compute planes lazily. The single-plane slicer masks most faces, so eagerly calculating + // every plane would defeat part of that optimization. + auto face_plane = [&mesh, &face_planes, &face_plane_computed, &transformed_vertex](int face_idx) -> const FacePlane& { + if (! face_plane_computed[face_idx]) { + const Vec3i32 &face = mesh.indices[face_idx]; + const Vec3d a = transformed_vertex(face(0)); + const Vec3d b = transformed_vertex(face(1)); + const Vec3d c = transformed_vertex(face(2)); + FacePlane &plane = face_planes[face_idx]; + plane.origin = a; + plane.normal = (b - a).cross(c - a); + const double normal_length = plane.normal.norm(); + if (normal_length > 0.) { + plane.normal /= normal_length; + plane.valid = true; + } + face_plane_computed[face_idx] = true; + } + return face_planes[face_idx]; + }; + const double plane_distance_tolerance = SCALING_FACTOR; + for (int face_idx = 0; face_idx < int(face_edge_ids.size()); ++ face_idx) { + for (int edge_idx = 0; edge_idx < 3; ++ edge_idx) { + const int edge_id = face_edge_ids[face_idx](edge_idx); + if (edge_id < 0) + continue; + if (first_face[edge_id] == -1) { + first_face[edge_id] = face_idx; + first_face_edge[edge_id] = edge_idx; + } else { + const int first_face_idx = first_face[edge_id]; + const FacePlane &first_plane = face_plane(first_face_idx); + const FacePlane &second_plane = face_plane(face_idx); + const int first_opposite_idx = mesh.indices[first_face_idx]((first_face_edge[edge_id] + 2) % 3); + const int second_opposite_idx = mesh.indices[face_idx]((edge_idx + 2) % 3); + const Vec3d first_opposite = transformed_vertex(first_opposite_idx); + const Vec3d second_opposite = transformed_vertex(second_opposite_idx); + // Orca: A shared edge guarantees that the planes intersect, but not that they coincide. + // Check both opposite vertices against the neighboring plane using one coord_t as the + // distance tolerance. The normal dot product only preserves face orientation; it does + // not classify a shallow angle as coplanar (see #15364). + coplanar[edge_id] = first_plane.valid && second_plane.valid && first_plane.normal.dot(second_plane.normal) > 0. && + std::abs(first_plane.normal.dot(second_opposite - first_plane.origin)) <= plane_distance_tolerance && + std::abs(second_plane.normal.dot(first_opposite - second_plane.origin)) <= plane_distance_tolerance; + } + } + } + return coplanar; +} + enum class FacetSliceType { NoSlice = 0, Slicing = 1, @@ -1057,7 +1136,8 @@ struct OpenPolyline { // called by make_loops() to connect sliced triangles into closed loops and open polylines by the triangle connectivity. // Only connects segments crossing triangles of the same orientation. -static void chain_lines_by_triangle_connectivity(IntersectionLines &lines, Polygons &loops, std::vector<OpenPolyline> &open_polylines) +static void chain_lines_by_triangle_connectivity(IntersectionLines &lines, const CoplanarEdges &coplanar_edges, + Polygons &loops, std::vector<OpenPolyline> &open_polylines) { // Build a map of lines by edge_a_id and a_id. std::vector<IntersectionLine*> by_edge_a_id; @@ -1134,6 +1214,11 @@ static void chain_lines_by_triangle_connectivity(IntersectionLines &lines, Polyg (first_line->a_id != -1 && first_line->a_id == last_line->b_id)) { // The current loop is complete. Add it to the output. assert(first_line->a == last_line->b); + // Orca: The seed point is also a triangle junction. Handle it explicitly because it + // is never visited through the next_line branch below when the loop closes. + if (first_line->edge_a_id >= 0 && first_line->edge_a_id < int(coplanar_edges.size()) && + coplanar_edges[first_line->edge_a_id]) + loop_pts.erase(loop_pts.begin()); loops.emplace_back(std::move(loop_pts)); #ifdef SLIC3R_TRIANGLEMESH_DEBUG printf(" Discovered %s polygon of %d points\n", (p.is_counter_clockwise() ? "ccw" : "cw"), (int)p.points.size()); @@ -1153,7 +1238,12 @@ static void chain_lines_by_triangle_connectivity(IntersectionLines &lines, Polyg next_line->a.x, next_line->a.y, next_line->b.x, next_line->b.y); */ assert(last_line->b == next_line->a); - loop_pts.emplace_back(next_line->a); + // Orca: Skip only junctions introduced by triangulating one planar face. Unlike a generic + // collinearity cleanup, this preserves intentional shallow corners used when comparing + // adjacent layers for bridges and overhang perimeters (see #15364). + if (next_line->edge_a_id < 0 || next_line->edge_a_id >= int(coplanar_edges.size()) || + ! coplanar_edges[next_line->edge_a_id]) + loop_pts.emplace_back(next_line->a); last_line = next_line; next_line->set_skip(); } @@ -1382,7 +1472,8 @@ static void chain_open_polylines_close_gaps(std::vector<OpenPolyline> &open_poly static Polygons make_loops( // Lines will have their flags modified. - IntersectionLines &lines) + IntersectionLines &lines, + const CoplanarEdges &coplanar_edges) { Polygons loops; #if 0 @@ -1412,7 +1503,7 @@ static Polygons make_loops( #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ std::vector<OpenPolyline> open_polylines; - chain_lines_by_triangle_connectivity(lines, loops, open_polylines); + chain_lines_by_triangle_connectivity(lines, coplanar_edges, loops, open_polylines); #ifdef SLIC3R_DEBUG_SLICE_PROCESSING { @@ -1484,6 +1575,7 @@ template<typename ThrowOnCancel> static std::vector<Polygons> make_loops( // Lines will have their flags modified. std::vector<IntersectionLines> &lines, + const CoplanarEdges &coplanar_edges, const MeshSlicingParams ¶ms, ThrowOnCancel throw_on_cancel) { @@ -1491,13 +1583,13 @@ static std::vector<Polygons> make_loops( layers.resize(lines.size()); tbb::parallel_for( tbb::blocked_range<size_t>(0, lines.size()), - [&lines, &layers, ¶ms, throw_on_cancel](const tbb::blocked_range<size_t> &range) { + [&lines, &layers, &coplanar_edges, ¶ms, throw_on_cancel](const tbb::blocked_range<size_t> &range) { for (size_t line_idx = range.begin(); line_idx < range.end(); ++ line_idx) { if ((line_idx & 0x0ffff) == 0) throw_on_cancel(); Polygons &polygons = layers[line_idx]; - polygons = make_loops(lines[line_idx]); + polygons = make_loops(lines[line_idx], coplanar_edges); auto this_mode = line_idx < params.slicing_mode_normal_below_layer ? params.mode_below : params.mode; if (! polygons.empty()) { @@ -1626,7 +1718,7 @@ static std::vector<Polygons> make_slab_loops( #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ Polygons &loops = layers[line_idx]; std::vector<OpenPolyline> open_polylines; - chain_lines_by_triangle_connectivity(in, loops, open_polylines); + chain_lines_by_triangle_connectivity(in, {}, loops, open_polylines); #ifdef SLIC3R_DEBUG_SLICE_PROCESSING { SVG svg(debug_out_path("make_slab_loops-out-%d-%d-%s.svg", iRun, line_idx, ProjectionFromTop ? "top" : "bottom").c_str(), bbox_svg); @@ -1666,7 +1758,7 @@ static ExPolygons make_expolygons_simple(std::vector<IntersectionLine> &lines) ExPolygons slices; Polygons holes; - for (Polygon &loop : make_loops(lines)) + for (Polygon &loop : make_loops(lines, {})) if (loop.area() >= 0.) slices.emplace_back(std::move(loop)); else @@ -1871,6 +1963,7 @@ std::vector<Polygons> slice_mesh( BOOST_LOG_TRIVIAL(debug) << "slice_mesh to polygons"; std::vector<IntersectionLines> lines; + CoplanarEdges coplanar; { //FIXME facets_edges is likely not needed and quite costly to calculate. @@ -1878,6 +1971,8 @@ std::vector<Polygons> slice_mesh( // However facets_edges assigns a single edge ID to two triangles only, thus when factoring facets_edges out, one will have // to make sure that no code relies on it. std::vector<Vec3i32> face_edge_ids = its_face_edge_ids(mesh); + // Orca: Keep the coplanarity classification aligned with the edge IDs used to chain this slice. + coplanar = coplanar_edges(mesh, face_edge_ids, params.trafo); if (zs.size() <= 1) { // It likely is not worthwile to copy the vertices. Apply the transformation in place. if (is_identity(params.trafo)) { @@ -1899,7 +1994,7 @@ std::vector<Polygons> slice_mesh( throw_on_cancel(); - std::vector<Polygons> layers = make_loops(lines, params, throw_on_cancel); + std::vector<Polygons> layers = make_loops(lines, coplanar, params, throw_on_cancel); #ifdef SLIC3R_DEBUG { @@ -1945,6 +2040,7 @@ Polygons slice_mesh( const MeshSlicingParams ¶ms) { std::vector<IntersectionLines> lines; + CoplanarEdges coplanar; { bool trafo_identity = is_identity(params.trafo); @@ -1980,6 +2076,8 @@ Polygons slice_mesh( // 3) Calculate face neighbors for just the faces in face_mask. std::vector<Vec3i32> face_edge_ids = its_face_edge_ids(mesh, face_mask); + // Orca: The single-plane path has its own masked edge-ID space, so classify that space separately. + coplanar = coplanar_edges(mesh, face_edge_ids, params.trafo); // 4) Slice "face_mask" triangles, collect line segments. // It likely is not worthwile to copy the vertices. Apply the transformation in place. @@ -1995,7 +2093,7 @@ Polygons slice_mesh( } // 5) Chain the line segments. - std::vector<Polygons> layers = make_loops(lines, params, [](){}); + std::vector<Polygons> layers = make_loops(lines, coplanar, params, [](){}); assert(layers.size() == 1); return layers.front(); } diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index 3b032cc57e..b47004fca5 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -1736,13 +1736,22 @@ TriangleSelector::TriangleSplittingData TriangleSelector::serialize() const { data.used_states[n] = true; if (n >= 3) { - assert(n <= 16); - if (n <= 16) { - // Store "11" plus 4 bits of (n-3). - data.bitstream.insert(data.bitstream.end(), { true, true }); - n -= 3; + assert(n <= int(EnforcerBlockerType::ExtruderMax)); + // Store "11" plus 4 bits of (n-3), which covers states 3..17. State 18 and + // above set that nibble to 0b1111 and store (n-18) in a second nibble. This is + // the encoding the CONST_FILAMENTS table in Model.cpp already writes for + // colored mesh imports. + data.bitstream.insert(data.bitstream.end(), { true, true }); + auto &bitstream = data.bitstream; + auto push_nibble = [&bitstream](int value) { for (size_t bit_idx = 0; bit_idx < 4; ++bit_idx) - data.bitstream.push_back(n & (uint64_t(0b0001) << bit_idx)); + bitstream.push_back(value & (uint64_t(0b0001) << bit_idx)); + }; + if (n <= 17) { + push_nibble(n - 3); + } else { + push_nibble(0b1111); + push_nibble(n - 18); } } else { // Simple case, compatible with PrusaSlicer 2.3.1 and older for storing paint on supports and seams. @@ -1810,6 +1819,12 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data, n |= data.bitstream[ibit ++] << i; return n; }; + // Decode a leaf state stored behind the "11" prefix: one nibble of (state-3) for states + // 3..17, or 0b1111 followed by a nibble of (state-18) above that. + auto decode_leaf_state = [&next_nibble]() { + const int nibble = next_nibble(); + return EnforcerBlockerType(nibble == 0b1111 ? next_nibble() + 18 : nibble + 3); + }; parents.clear(); while (true) { @@ -1818,8 +1833,8 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data, int num_of_split_sides = code & 0b11; int num_of_children = num_of_split_sides == 0 ? 0 : num_of_split_sides + 1; bool is_split = num_of_children != 0; - // Only valid if not is_split. Value of the second nibble was subtracted by 3, so it is added back. - auto state = is_split ? EnforcerBlockerType::NONE : EnforcerBlockerType((code & 0b1100) == 0b1100 ? next_nibble() + 3 : code >> 2); + // Only valid if not is_split. + auto state = is_split ? EnforcerBlockerType::NONE : ((code & 0b1100) == 0b1100 ? decode_leaf_state() : EnforcerBlockerType(code >> 2)); // BBS if (state == to_delete_filament) @@ -1916,7 +1931,14 @@ void TriangleSelector::TriangleSplittingData::update_used_states(const size_t bi if (const bool is_split = (code & 0b11) != 0; is_split) continue; - const uint8_t facet_state = (code & 0b1100) == 0b1100 ? read_next_nibble() + 3 : code >> 2; + uint8_t facet_state; + if ((code & 0b1100) == 0b1100) { + // Leaf behind the "11" prefix: one nibble of (state-3), or 0b1111 + (state-18). + const uint8_t nibble = read_next_nibble(); + facet_state = nibble == 0b1111 ? uint8_t(read_next_nibble() + 18) : uint8_t(nibble + 3); + } else { + facet_state = code >> 2; + } assert(facet_state < this->used_states.size()); if (facet_state >= this->used_states.size()) continue; @@ -1946,9 +1968,13 @@ bool TriangleSelector::has_facets(const TriangleSplittingData &data, const Enfor auto num_children_or_state = [&next_nibble]() -> int { int code = next_nibble(); int num_of_split_sides = code & 0b11; - return num_of_split_sides == 0 ? - ((code & 0b1100) == 0b1100 ? next_nibble() + 3 : code >> 2) : - - num_of_split_sides - 1; + if (num_of_split_sides != 0) + return - num_of_split_sides - 1; + if ((code & 0b1100) != 0b1100) + return code >> 2; + // Leaf behind the "11" prefix: one nibble of (state-3), or 0b1111 + (state-18). + const int nibble = next_nibble(); + return nibble == 0b1111 ? next_nibble() + 18 : nibble + 3; }; int state = num_children_or_state(); @@ -1983,6 +2009,20 @@ void TriangleSelector::seed_fill_unselect_all_triangles() triangle.unselect_by_seed_fill(); } +void TriangleSelector::shift_states_above(EnforcerBlockerType threshold, int delta) +{ + for (Triangle &triangle : m_triangles) { + if (triangle.is_split() || !triangle.valid()) + continue; + EnforcerBlockerType s = triangle.get_state(); + if (s >= threshold && s != EnforcerBlockerType::NONE) { + int new_val = (int)s + delta; + if (new_val >= 0) + triangle.set_state(EnforcerBlockerType(new_val)); + } + } +} + void TriangleSelector::seed_fill_apply_on_triangles(EnforcerBlockerType new_state) { for (Triangle &triangle : m_triangles) diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 11517f5c6c..594f710e45 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -17,7 +17,9 @@ enum class EnforcerBlockerType : int8_t { BLOCKER = 2, // For the fuzzy skin, we use just two values (NONE and FUZZY_SKIN). FUZZY_SKIN = ENFORCER, - // Maximum is 15. The value is serialized in TriangleSelector into 6 bits using a 2 bit prefix code. + // States 3..17 are serialized into 6 bits using a 2 bit prefix code; states 18 and above use + // one additional nibble (see TriangleSelector::serialize). ExtruderMax matches the last entry + // of CONST_FILAMENTS in Model.cpp, which encodes the same range for colored mesh imports. Extruder1 = ENFORCER, Extruder2 = BLOCKER, Extruder3, @@ -34,7 +36,23 @@ enum class EnforcerBlockerType : int8_t { Extruder14, Extruder15, Extruder16, - ExtruderMax = Extruder16 + Extruder17, + Extruder18, + Extruder19, + Extruder20, + Extruder21, + Extruder22, + Extruder23, + Extruder24, + Extruder25, + Extruder26, + Extruder27, + Extruder28, + Extruder29, + Extruder30, + Extruder31, + Extruder32, + ExtruderMax = Extruder32 }; // Type alias for the state mapping array to improve code readability @@ -369,6 +387,9 @@ public: // For all triangles, remove the flag indicating that the triangle was selected by seed fill. void seed_fill_unselect_all_triangles(); + // Shift all triangle states >= threshold by delta (used when inserting filaments) + void shift_states_above(EnforcerBlockerType threshold, int delta); + // For all triangles selected by seed fill, set new EnforcerBlockerType and remove flag indicating that triangle was selected by seed fill. // The operation may merge split triangles if they are being assigned the same color. void seed_fill_apply_on_triangles(EnforcerBlockerType new_state); diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index 62b2eeb78e..55d9b716cf 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -3,6 +3,7 @@ #include <iomanip> #include <locale> +#include <set> #include <utility> #include <functional> #include <type_traits> @@ -18,6 +19,7 @@ #include <openssl/md5.h> #include "libslic3r.h" +#include "Semver.hpp" //define CLI errors @@ -722,11 +724,42 @@ void copy_directory_recursively(const boost::filesystem::path& source, std::function<bool(const std::string)> filter = nullptr, bool merge_mode = false); -// Install vendor bundles from resources directory to data directory -// bundle_names: vector of vendor bundle names (without .json extension) -// resource_subdir: subdirectory under resources_dir() (default: "profiles") -// data_subdir: subdirectory under data_dir() (default: "system") -// Returns: true if all bundles installed successfully, false otherwise +// ---- Vendor installation on disk ------------------------------------------ +// How a vendor bundle is installed from resources into data_dir()/system: as +// its profile and preset JSONs or, in a build that ships preset caches, as its +// .opc preset cache alone. Loading what is installed is PresetBundle's business; +// the cache file format itself is VendorCacheFile's (PresetCacheFormat.hpp). + +// True if `vendor` is installed in data_dir()/system. A build that ships preset +// caches installs the cache alone, so it — not the profile — marks a vendor +// installed; a cache this build cannot read marks nothing. +bool is_vendor_installed(const std::string& vendor); + +// The version the installed vendor would be loaded at: its cache's stamp while +// that covers the profile beside it, the profile's own version once it does not. +// Invalid Semver if neither form is installed. +Semver installed_vendor_version(const std::string& vendor); + +// Remove every form `vendor` can be installed as from data_dir()/system: its +// profile, its preset cache, and its preset directory. +void remove_installed_vendor(const std::string& vendor); + +// The vendors `dir` holds, sorted: one is named by its profile or, in a build that +// ships preset caches instead of the raw profile JSONs, by its cache alone. +std::set<std::string> vendor_names_in(const boost::filesystem::path& dir); + +// The version a build ships `vendor` at: whichever of its preset cache and its +// profile is newer, that being the one installing lays down. Invalid Semver if the +// build ships neither. +Semver resource_vendor_version(const std::string& vendor); + +// Install vendors from the resources directory into the data directory, each as +// its preset cache or as its profile and preset JSONs — whichever of the two the +// build ships at the newer version. Anything the previous install of that vendor +// left behind goes, so only the form just installed is there to be loaded. +// bundle_names: vendor names, without extension. +// Every bundle that can be installed is, whatever the others do. Returns false +// if any named bundle could not be installed. bool install_vendor_bundles_from_resources(const std::vector<std::string>& bundle_names, const std::string& resource_subdir = "profiles", const std::string& data_subdir = "system"); diff --git a/src/libslic3r/calib.cpp b/src/libslic3r/calib.cpp index 10fa9653eb..3c2981d822 100644 --- a/src/libslic3r/calib.cpp +++ b/src/libslic3r/calib.cpp @@ -28,9 +28,9 @@ std::string CalibPressureAdvance::move_to(Vec2d pt, GCodeWriter &writer, std::st gcode << writer.retract(); // retract before z move or move if(z > EPSILON && layer_height >= 0){ - gcode << writer.travel_to_z(z, "z-hop"); // Perform z hop + gcode << writer.travel_to_z(z, "Z-hop"); // Perform z hop gcode << writer.travel_to_xy(pt, comment); // Travel with z move - gcode << writer.travel_to_z(layer_height, "undo z-hop"); // Undo z hop + gcode << writer.travel_to_z(layer_height, "undo Z-hop"); // Undo z hop }else { gcode << writer.travel_to_xy(pt, comment); } diff --git a/src/libslic3r/calib.hpp b/src/libslic3r/calib.hpp index ed0f76ee86..abca5e79dc 100644 --- a/src/libslic3r/calib.hpp +++ b/src/libslic3r/calib.hpp @@ -91,29 +91,15 @@ class CaliPresetInfo { public: int tray_id; - int extruder_id; - NozzleVolumeType nozzle_volume_type; - BedType bed_type; + int extruder_id = 0; + NozzleVolumeType nozzle_volume_type{nvtStandard}; + BedType bed_type{btDefault}; float nozzle_diameter; int nozzle_pos_id{-1}; std::string nozzle_sn; std::string filament_id; std::string setting_id; std::string name; - - CaliPresetInfo &operator=(const CaliPresetInfo &other) - { - this->tray_id = other.tray_id; - this->extruder_id = other.extruder_id; - this->nozzle_volume_type = other.nozzle_volume_type; - this->nozzle_diameter = other.nozzle_diameter; - this->nozzle_pos_id = other.nozzle_pos_id; - this->nozzle_sn = other.nozzle_sn; - this->filament_id = other.filament_id; - this->setting_id = other.setting_id; - this->name = other.name; - return *this; - } }; struct PrinterCaliInfo diff --git a/src/libslic3r/libslic3r.h b/src/libslic3r/libslic3r.h index f4291d36df..6584566f40 100644 --- a/src/libslic3r/libslic3r.h +++ b/src/libslic3r/libslic3r.h @@ -64,6 +64,11 @@ static constexpr double LARGE_BED_THRESHOLD = 2147; // Orca: maximum number of extruders is 64. For SEMM printers, it defines maximum filament number. static constexpr size_t MAXIMUM_EXTRUDER_NUMBER = 64; +// Orca: how many filament slots syncing an AMS setup may create. This was derived from +// EnforcerBlockerType::ExtruderMax, but that cap now covers 32 paintable filaments, so the AMS +// limit is pinned here to keep sync behaving as it does for projects without mixed-color filaments. +static constexpr size_t MAXIMUM_AMS_SYNC_FILAMENT_NUMBER = 16; + // Orca: maximum line width is 5 times the nozzle diameter static constexpr float MAX_LINE_WIDTH_MULTIPLIER = 5; diff --git a/src/libslic3r/libslic3r_version.h.in b/src/libslic3r/libslic3r_version.h.in index 750e092d28..df83e813c9 100644 --- a/src/libslic3r/libslic3r_version.h.in +++ b/src/libslic3r/libslic3r_version.h.in @@ -5,9 +5,6 @@ #define SLIC3R_APP_KEY "@SLIC3R_APP_KEY@" #define SLIC3R_VERSION "@SLIC3R_VERSION@" #define SoftFever_VERSION "@SoftFever_VERSION@" -#ifndef GIT_COMMIT_HASH - #define GIT_COMMIT_HASH "0000000" // 0000000 means uninitialized -#endif #define SLIC3R_BUILD_ID "@SLIC3R_BUILD_ID@" //#define SLIC3R_RC_VERSION "@SLIC3R_VERSION@" #define BBL_INTERNAL_TESTING @BBL_INTERNAL_TESTING@ diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index 5f429f076a..5f4baac951 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -17,6 +17,10 @@ #include "Platform.hpp" #include "Time.hpp" #include "libslic3r.h" +// For the vendor-installation helpers: the vendor profile version +// (get_version_from_json) and the preset cache stamp (VendorCacheFile). +#include "Preset.hpp" +#include "PresetCacheFormat.hpp" #ifdef __APPLE__ #include "MacUtils.hpp" @@ -306,7 +310,11 @@ void set_data_dir(const std::string &dir) { g_data_dir = dir; if (!g_data_dir.empty() && !boost::filesystem::exists(g_data_dir)) { - boost::filesystem::create_directory(g_data_dir); + try { + boost::filesystem::create_directories(g_data_dir); + } catch (const boost::filesystem::filesystem_error &ex) { + BOOST_LOG_TRIVIAL(error) << "set_data_dir: failed to create data directory " << g_data_dir << ": " << ex.what(); + } } } @@ -1724,6 +1732,85 @@ void copy_directory_recursively(const boost::filesystem::path& source, return; } +// ---- Vendor installation on disk ------------------------------------------ + +// Whether a cache stamped `cache_ver` still speaks for a vendor whose profile on +// disk claims `profile_ver`: it does unless the profile has moved ahead of it. A +// profile that is missing or carries no judgeable version cannot be ahead of +// anything. The one rule behind both "which form gets installed" and "which form +// is installed"; they must not drift apart. Deliberately NOT the serve rule +// (VendorCacheFile::load), which refuses an unjudgeable profile instead. +static bool cache_covers(const Semver& cache_ver, const Semver& profile_ver) +{ + return cache_ver.valid() && (! profile_ver.valid() || cache_ver >= profile_ver); +} + +bool is_vendor_installed(const std::string& vendor) +{ + const boost::filesystem::path dir = boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR; + // A cache is the whole of a cache-only installation, so a file this build + // cannot serve the vendor from is not an installation. Left counted as one, + // the updater would never lay a working copy down. + return boost::filesystem::exists(dir / (vendor + ".json")) + || VendorCacheFile::usable_version((dir / (vendor + ".opc")).string(), vendor).valid(); +} + +Semver installed_vendor_version(const std::string& vendor) +{ + const boost::filesystem::path dir = boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR; + const boost::filesystem::path json = dir / (vendor + ".json"); + // Guarded: get_version_from_json logs an error and throws-and-catches its way + // to an invalid version on a file that is not there, and a cache-only vendor + // never has one. + const Semver from_json = boost::filesystem::exists(json) ? get_version_from_json(json.string()) : Semver(); + const Semver from_cache = VendorCacheFile::usable_version((dir / (vendor + ".opc")).string(), vendor); + // Whichever form a load would serve. + return cache_covers(from_cache, from_json) ? from_cache : from_json; +} + +void remove_installed_vendor(const std::string& vendor) +{ + const boost::filesystem::path dir = boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR; + boost::filesystem::remove(dir / (vendor + ".json")); + boost::filesystem::remove(dir / (vendor + ".opc")); + if (boost::filesystem::exists(dir / vendor)) + boost::filesystem::remove_all(dir / vendor); +} + +std::set<std::string> vendor_names_in(const boost::filesystem::path& dir) +{ + std::set<std::string> names; + for (auto& dir_entry : boost::filesystem::directory_iterator(dir)) { + const auto& path = dir_entry.path(); + if (Slic3r::is_json_file(path.string()) || path.extension() == ".opc") + names.insert(path.stem().string()); + } + return names; +} + +// A vendor's preset cache is the whole of its installation: it carries the presets, +// the vendor profile and the version they were built at, so where one ships nothing +// else needs copying. Unless the profile beside it claims a newer version — a cache +// generated before that profile was bumped is out of date, and a cache that cannot +// be read is no installation at all — and the vendor is installed the way it was +// before caches existed, as its profile and the preset JSONs it points at. Returns +// the version the cache is stamped with, invalid when it is not the form to install. +static Semver installable_cache_version(const boost::filesystem::path& dir, const std::string& vendor) +{ + const auto cache_ver = Semver::parse(VendorCacheFile::peek_version((dir / (vendor + ".opc")).string(), vendor)); + if (! cache_ver) + return Semver::invalid(); + const Semver profile_ver = get_version_from_json((dir / (vendor + ".json")).string()); + return cache_covers(*cache_ver, profile_ver) ? *cache_ver : Semver::invalid(); +} + +Semver resource_vendor_version(const std::string& vendor) +{ + const boost::filesystem::path dir = boost::filesystem::path(resources_dir()) / "profiles"; + const Semver ver = installable_cache_version(dir, vendor); + return ver.valid() ? ver : get_version_from_json((dir / (vendor + ".json")).string()); +} + bool install_vendor_bundles_from_resources( const std::vector<std::string>& bundle_names, const std::string& resource_subdir, @@ -1736,37 +1823,82 @@ bool install_vendor_bundles_from_resources( BOOST_LOG_TRIVIAL(info) << "Installing " << bundle_names.size() << " bundles from resources..."; + // One vendor that cannot be installed is one vendor missing, not a reason to + // leave the rest uninstalled. The caller is told, and every bundle that can + // be laid down is. + bool all_installed = true; + for (const auto &bundle : bundle_names) { try { + if (bundle.empty()) { + BOOST_LOG_TRIVIAL(warning) << "Refusing to install a bundle with no name"; + all_installed = false; + continue; + } + // Install the JSON file auto path_in_rsrc = (rsrc_path / bundle).replace_extension(".json"); auto path_in_vendors = (vendor_path / bundle).replace_extension(".json"); + auto cache_in_rsrc = (rsrc_path / bundle).replace_extension(".opc"); + auto cache_in_vendors = (vendor_path / bundle).replace_extension(".opc"); - if (!fs::exists(path_in_rsrc)) { + // Either form of the vendor will do: a build may ship it as a cache alone. + if (!fs::exists(path_in_rsrc) && !fs::exists(cache_in_rsrc)) { BOOST_LOG_TRIVIAL(warning) << "Bundle not found in resources: " << bundle; - return false; + all_installed = false; + continue; } // Create target directory if needed if (!fs::exists(vendor_path)) fs::create_directories(vendor_path); - // Copy JSON file std::string error_message; - CopyFileResult cfr = copy_file(path_in_rsrc.string(), path_in_vendors.string(), error_message, false); - if (cfr != CopyFileResult::SUCCESS) { - BOOST_LOG_TRIVIAL(error) << "Failed to copy " << bundle << ".json: " << error_message; - return false; + bool installed_cache = false; + if (installable_cache_version(rsrc_path, bundle).valid()) { + installed_cache = copy_file(cache_in_rsrc.string(), cache_in_vendors.string(), error_message, false) == CopyFileResult::SUCCESS; + if (! installed_cache) { + BOOST_LOG_TRIVIAL(warning) << "Failed to copy " << bundle << ".opc: " << error_message; + } else if (! VendorCacheFile::usable_version(cache_in_vendors.string(), bundle).valid()) { + // The copy is what will be loaded, so it — not the kilobyte + // peek that chose this form — decides whether the profile + // beside it can go. + BOOST_LOG_TRIVIAL(warning) << "Installed cache for " << bundle << " cannot be read; installing its profile instead"; + boost::system::error_code ec; + fs::remove(cache_in_vendors, ec); + installed_cache = false; + } + } + + if (! installed_cache) { + CopyFileResult cfr = copy_file(path_in_rsrc.string(), path_in_vendors.string(), error_message, false); + if (cfr != CopyFileResult::SUCCESS) { + BOOST_LOG_TRIVIAL(error) << "Failed to copy " << bundle << ".json: " << error_message; + all_installed = false; + continue; + } + // Only now: an earlier install's cache would shadow this profile, + // but removing it before the profile lands would leave neither. + boost::system::error_code ec; + fs::remove(cache_in_vendors, ec); + } else { + // Left in place, an earlier install's profile would shadow the cache. + boost::system::error_code ec; + fs::remove(path_in_vendors, ec); + if (ec) + BOOST_LOG_TRIVIAL(warning) << "Could not remove the superseded profile " << path_in_vendors.string() << ": " << ec.message(); } // Copy the vendor directory (if it exists) auto dir_in_rsrc = rsrc_path / bundle; auto dir_in_vendors = vendor_path / bundle; - if (fs::exists(dir_in_rsrc) && fs::is_directory(dir_in_rsrc)) { - // Remove existing directory - if (fs::exists(dir_in_vendors)) - fs::remove_all(dir_in_vendors); + // Whatever is installed came from an earlier version of this vendor and + // would be parsed in place of the one being installed now. + if (fs::exists(dir_in_vendors)) + fs::remove_all(dir_in_vendors); + + if (! installed_cache && fs::exists(dir_in_rsrc) && fs::is_directory(dir_in_rsrc)) { fs::create_directories(dir_in_vendors); // Copy with file filter (same as PresetUpdater::install_bundles_rsrc) @@ -1787,11 +1919,11 @@ bool install_vendor_bundles_from_resources( } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "Exception installing bundle " << bundle << ": " << e.what(); - return false; + all_installed = false; } } - return true; + return all_installed; } void save_string_file(const boost::filesystem::path& p, const std::string& str) diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index b98396f943..1e39e30b0d 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -38,6 +38,8 @@ set(SLIC3R_GUI_SOURCES GUI/AuxiliaryDataViewModel.hpp GUI/AuxiliaryDialog.cpp GUI/AuxiliaryDialog.hpp + GUI/AVVideoDecoder.cpp + GUI/AVVideoDecoder.hpp GUI/Auxiliary.hpp GUI/BackgroundSlicingProcess.cpp GUI/BackgroundSlicingProcess.hpp @@ -61,6 +63,8 @@ set(SLIC3R_GUI_SOURCES GUI/BitmapComboBox.hpp GUI/BonjourDialog.cpp GUI/BonjourDialog.hpp + GUI/BuildCommit.cpp + GUI/BuildCommit.hpp GUI/CrealityDiscoveryDialog.cpp GUI/CrealityDiscoveryDialog.hpp GUI/calib_dlg.cpp @@ -353,6 +357,16 @@ set(SLIC3R_GUI_SOURCES GUI/Monitor.hpp GUI/MonitorPage.cpp GUI/MonitorPage.hpp + GUI/MixedFilamentDialog.cpp + GUI/MixedFilamentDialog.hpp + GUI/GradientCurveEditor.cpp + GUI/GradientCurveEditor.hpp + GUI/ColorDecomposeDialog.cpp + GUI/ColorDecomposeDialog.hpp + GUI/ColorDecomposeSupport.cpp + GUI/ColorDecomposeSupport.hpp + GUI/TextureImportDialog.cpp + GUI/TextureImportDialog.hpp GUI/Mouse3DController.cpp GUI/Mouse3DController.hpp GUI/MsgDialog.cpp @@ -601,6 +615,8 @@ set(SLIC3R_GUI_SOURCES GUI/WipeTowerDialog.cpp GUI/wxExtensions.cpp GUI/wxExtensions.hpp + GUI/wxMediaCtrl3.cpp + GUI/wxMediaCtrl3.h plugin/PythonInterpreter.cpp plugin/PythonInterpreter.hpp plugin/PythonPluginBridge.cpp @@ -620,6 +636,8 @@ set(SLIC3R_GUI_SOURCES plugin/host/PluginHostSlicing.cpp plugin/host/PluginHostUi.cpp plugin/host/PluginHostUi.hpp + plugin/host/PluginPages.cpp + plugin/host/PluginPages.hpp plugin/CloudPluginService.cpp plugin/CloudPluginService.hpp plugin/PluginFsUtils.cpp @@ -640,6 +658,9 @@ set(SLIC3R_GUI_SOURCES plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp + plugin/pluginTypes/pages/PagesPluginCapability.hpp + plugin/pluginTypes/pages/PagesPluginCapability.cpp + plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp plugin/pluginTypes/script/ScriptPluginCapability.hpp plugin/pluginTypes/script/ScriptPluginCapability.cpp plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp @@ -784,21 +805,8 @@ if (APPLE) GUI/DeepLinkHandlerMac.mm GUI/DeepLinkHandlerMac.h GUI/GUI_UtilsMac.mm - GUI/wxMediaCtrl2.mm - GUI/wxMediaCtrl2.h ) FIND_LIBRARY(DISKARBITRATION_LIBRARY DiskArbitration) -else () - list(APPEND SLIC3R_GUI_SOURCES - GUI/wxMediaCtrl2.cpp - GUI/wxMediaCtrl2.h - ) -endif () - -if (UNIX AND NOT APPLE) - list(APPEND SLIC3R_GUI_SOURCES - GUI/Printer/gstbambusrc.c - ) endif () set(ORCA_UPDATER_SIG_KEY_B64 "${ORCA_UPDATER_SIG_KEY}") @@ -839,6 +847,18 @@ source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SLIC3R_GUI_SOURCES}) encoding_check(libslic3r_gui) +# Only BuildCommit.cpp includes the generated header, plus BaseException.cpp on +# Windows. Both build into libslic3r_gui, so the header only has to exist before +# that target builds. +set(_git_commit_hash_header "${CMAKE_CURRENT_BINARY_DIR}/git_commit_hash.h") +add_custom_target(git_commit_hash_header + BYPRODUCTS "${_git_commit_hash_header}" + COMMAND ${CMAKE_COMMAND} + "-DSOURCE_DIR=${CMAKE_SOURCE_DIR}" + "-DOUT_FILE=${_git_commit_hash_header}" + -P "${CMAKE_CURRENT_LIST_DIR}/GitCommitHash.cmake" + COMMENT "Resolving the git commit hash") +add_dependencies(libslic3r_gui git_commit_hash_header) if(APPLE AND CMAKE_VERSION VERSION_GREATER_EQUAL "4.0") set(_opengl_link_lib "") @@ -898,6 +918,38 @@ if (SLIC3R_PCH AND NOT SLIC3R_SYNTAXONLY) add_precompiled_header(libslic3r_gui pchheader.hpp FORCEINCLUDE) endif () +if (APPLE) + # Static FFmpeg from the deps install: nothing to bundle into the .app, + # no rpath/install_name handling. Order matters: avcodec -> swscale -> avutil. + find_library(LIBAVCODEC_LIBRARY NAMES libavcodec.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) + find_library(LIBSWSCALE_LIBRARY NAMES libswscale.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) + find_library(LIBAVUTIL_LIBRARY NAMES libavutil.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) + if (NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY) + message(FATAL_ERROR "Static FFmpeg (libavcodec.a/libswscale.a/libavutil.a) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps — FFMPEG builds static-only on macOS.") + endif () + target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY}) + target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include) +elseif (WIN32) + # Prebuilt shared FFmpeg from the deps install. Windows has no pkg-config, + # so resolve the import libraries out of the deps prefix directly; the DLLs + # are copied next to the executable by the top level CMakeLists. + find_library(LIBAVCODEC_LIBRARY NAMES avcodec PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) + find_library(LIBSWSCALE_LIBRARY NAMES swscale PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) + find_library(LIBAVUTIL_LIBRARY NAMES avutil PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) + if (NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY) + message(FATAL_ERROR "FFmpeg (avcodec/swscale/avutil) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps.") + endif () + target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY}) + target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include) +else () + pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET + libavcodec + libswscale + libavutil + ) + target_link_libraries(libslic3r_gui PkgConfig::LIBAV) +endif() + # We need to implement some hacks for wxWidgets and touch the underlying GTK # layer and sub-libraries. This forces us to use the include locations and # link these libraries. @@ -924,11 +976,6 @@ if (UNIX AND NOT APPLE) target_compile_definitions(libslic3r_gui PRIVATE wxHAVE_GDK_WAYLAND) endif () - # We add GStreamer for bambu:/// support. - pkg_check_modules(GSTREAMER REQUIRED gstreamer-1.0) - pkg_check_modules(GST_BASE REQUIRED gstreamer-base-1.0) - target_link_libraries(libslic3r_gui ${GSTREAMER_LIBRARIES} ${GST_BASE_LIBRARIES}) - target_include_directories(libslic3r_gui SYSTEM PRIVATE ${GSTREAMER_INCLUDE_DIRS} ${GST_BASE_INCLUDE_DIRS}) endif () # Add a definition so that we can tell we are compiling slic3r. diff --git a/src/slic3r/Config/Snapshot.cpp b/src/slic3r/Config/Snapshot.cpp index 4b071994fc..a7135eac6f 100644 --- a/src/slic3r/Config/Snapshot.cpp +++ b/src/slic3r/Config/Snapshot.cpp @@ -432,14 +432,9 @@ const Snapshot& SnapshotDB::take_snapshot(const AppConfig &app_config, Snapshot: cfg.models_variants_installed.erase(it ++); else ++ it; - // Read the active config bundle, parse the config version. - PresetBundle bundle; - //BBS: change directoties by design - //bundle.load_configbundle((data_dir / PRESET_SYSTEM_DIR / (cfg.name + ".ini")).string(), PresetBundle::LoadConfigBundleAttribute::LoadVendorOnly, ForwardCompatibilitySubstitutionRule::EnableSilent); - bundle.load_vendor_configs_from_json((data_dir/PRESET_SYSTEM_DIR).string(), cfg.name, PresetBundle::LoadConfigBundleAttribute::LoadVendorOnly, ForwardCompatibilitySubstitutionRule::EnableSilent); - for (const auto &vp : bundle.vendors) - if (vp.second.id == cfg.name) - cfg.version.config_version = vp.second.config_version; + // Orca: the version the vendor is installed at, read from its profile or — + // where the cache is the whole installation — from the cache's own stamp. + cfg.version.config_version = installed_vendor_version(cfg.name); snapshot.vendor_configs.emplace_back(std::move(cfg)); } diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index bf5d1f2421..f65c0e3532 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -682,13 +682,19 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj if (shader) { if (idx == 0) { int extruder_id = model_volume->extruder_id(); - //to make black not too hard too see - ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[extruder_id - 1]); - if (ban_light) { - new_color[3] = (255 - (extruder_id - 1))/255.0f; + // ORCA: extruder_id may be 0 (unset) or point past the colour list after a + // filament is deleted/remapped, so clamp the index instead of reading out of + // bounds. + if (!extruder_colors.empty()) { + int color_idx = std::clamp(extruder_id - 1, 0, int(extruder_colors.size()) - 1); + //to make black not too hard too see + ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[color_idx]); + if (ban_light) { + new_color[3] = (255 - color_idx)/255.0f; + } + m.set_color(new_color); + // shader->set_uniform("uniform_color", new_color); } - m.set_color(new_color); - // shader->set_uniform("uniform_color", new_color); } else { if (idx <= extruder_colors.size()) { diff --git a/src/slic3r/GUI/AMSDryControl.cpp b/src/slic3r/GUI/AMSDryControl.cpp index ddd3380975..f83eed30db 100644 --- a/src/slic3r/GUI/AMSDryControl.cpp +++ b/src/slic3r/GUI/AMSDryControl.cpp @@ -437,7 +437,7 @@ wxBoxSizer* AMSDryCtrWin::create_normal_state_panel(wxPanel* parent) m_temperature_input = new wxTextCtrl(parent, wxID_ANY, "", wxDefaultPosition, wxSize(FromDIP(80), -1)); m_temperature_input->SetMaxLength(3); // Limit to 3 digits - m_temperature_input->Bind(wxEVT_CHAR, [this](wxKeyEvent& event) { + m_temperature_input->Bind(wxEVT_CHAR, [](wxKeyEvent& event) { int keycode = event.GetKeyCode(); if (keycode >= '0' && keycode <= '9') { event.Skip(); @@ -464,7 +464,7 @@ wxBoxSizer* AMSDryCtrWin::create_normal_state_panel(wxPanel* parent) m_time_input = new wxTextCtrl(parent, wxID_ANY, "", wxDefaultPosition, wxSize(FromDIP(100), -1)); m_time_input->SetMaxLength(3); // Limit to 3 digits - m_time_input->Bind(wxEVT_CHAR, [this](wxKeyEvent& event) { + m_time_input->Bind(wxEVT_CHAR, [](wxKeyEvent& event) { int keycode = event.GetKeyCode(); if (keycode >= '0' && keycode <= '9') { event.Skip(); @@ -698,7 +698,7 @@ wxBoxSizer* AMSDryCtrWin::create_guide_info_section(wxPanel* parent) m_rotate_spool_toggle = new wxCheckBox(parent, wxID_ANY, ""); m_rotate_spool_toggle->SetValue(false); - m_rotate_spool_toggle->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) { + m_rotate_spool_toggle->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) { bool is_checked = event.IsChecked(); // Add toggle behavior logic here }); diff --git a/src/slic3r/GUI/AMSDryControl.hpp b/src/slic3r/GUI/AMSDryControl.hpp index a7500b5b03..46699650d6 100644 --- a/src/slic3r/GUI/AMSDryControl.hpp +++ b/src/slic3r/GUI/AMSDryControl.hpp @@ -55,7 +55,6 @@ private: Label* m_text_label; wxStaticBitmap* m_icon_bitmap; - int m_target_size; std::string m_icon_name; ScalableBitmap m_icon; }; diff --git a/src/slic3r/GUI/AMSMaterialsSetting.cpp b/src/slic3r/GUI/AMSMaterialsSetting.cpp index 74165c20fe..2e436e8462 100644 --- a/src/slic3r/GUI/AMSMaterialsSetting.cpp +++ b/src/slic3r/GUI/AMSMaterialsSetting.cpp @@ -4,6 +4,7 @@ #include "GUI_App.hpp" #include "libslic3r/Preset.hpp" #include "I18N.hpp" +#include <algorithm> #include <boost/log/trivial.hpp> #include <wx/colordlg.h> #include <wx/dcgraph.h> @@ -1075,54 +1076,105 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi // Sort the filaments { - static std::unordered_map<wxString, int> sorted_names - { {"Bambu PLA Basic", 0}, - {"Bambu PLA Matte", 1}, - {"Bambu PETG HF", 2}, - {"Bambu ABS", 3}, - {"Bambu PLA Silk", 4}, - {"Bambu PLA-CF" , 5}, - {"Bambu PLA Galaxy", 6}, - {"Bambu PLA Metal", 7}, - {"Bambu PLA Marble", 8}, - {"Bambu PETG-CF", 9}, - {"Bambu PETG Translucent", 10}, - {"Bambu ABS-GF", 11} + std::unordered_map<wxString, int> selected_filament_ranks; + + // Helper lambda to find a filament Preset by name. We can call this multiple times to walk the inheritance chain and find the base filament. + auto find_filament_by_name = [](const std::string& wanted, const PresetCollection& filaments) -> const Preset* { + for (auto it = filaments.begin(); it != filaments.end(); ++it) { + if (it->name == wanted) { + return &(*it); + } + } + return nullptr; }; - static std::vector<wxString> sorted_vendors { "Bambu Lab", "Generic" }; - static std::vector<wxString> sorted_types { "PLA", "PETG", "ABS", "TPU" }; - auto _filament_sorter = [&query_filament_vendors, &query_filament_types](const wxString& left, const wxString& right) -> bool - { - { // Compare name order - const auto& iter1 = sorted_names.find(left); - int name_order1 = (iter1 != sorted_names.end()) ? iter1->second : INT_MAX; + // For each active filament preset, find its base filament alias and promote it in extruder order. + auto bundle = wxGetApp().preset_bundle; + const auto& preset_names = bundle->filament_presets; + for (size_t i = preset_names.size(); i-- > 0; ) { + std::string wanted = preset_names[i]; + const int sort_rank = -static_cast<int>(preset_names.size() - i); + + const Preset* match = nullptr; - const auto& iter2 = sorted_names.find(right); - int name_order2 = (iter2 != sorted_names.end()) ? iter2->second : INT_MAX; - if (name_order1 != name_order2) + do { + auto find_result = find_filament_by_name(wanted, bundle->filaments); + if (!find_result) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " No available filament name matches " << wanted; + break; + } + + match = find_result; + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Found available filament matching current preset name " << wanted + << " - Name: " << match->name << " - Alias: " << match->alias + << " - Inherits: " << match->inherits(); + + if (match->inherits().length() == 0) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " No more inherits so we reached the base filament"; + break; + } + + wanted = match->inherits(); + } while (1); // Or loop while (match->alias.length() == 0) because existence of alias and inherits on a Preset seem to be exclusive + + if (!match) { + continue; + } + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Update filament rank to " + std::to_string(sort_rank) + " for preset Name: " + << match->name << " - Alias: " << match->alias; + selected_filament_ranks.insert_or_assign(match->alias, sort_rank); + } + + static const std::vector<wxString> sorted_vendors { "Generic" }; + static const std::vector<wxString> sorted_types { "PLA", "PETG", "ABS", "TPU" }; + auto priority_rank = [](const std::vector<wxString>& priorities, const wxString& value) { + const auto iter = std::find_if(priorities.begin(), priorities.end(), [&value](const wxString& priority) { + return priority.CmpNoCase(value) == 0; + }); + return iter - priorities.begin(); + }; + auto _filament_sorter = [&query_filament_vendors, &query_filament_types, &selected_filament_ranks, &priority_rank](const wxString& left, const wxString& right) -> bool + { + { // Compare selected filament order + const auto& iter1 = selected_filament_ranks.find(left); + int selected_order1 = (iter1 != selected_filament_ranks.end()) ? iter1->second : INT_MAX; + + const auto& iter2 = selected_filament_ranks.find(right); + int selected_order2 = (iter2 != selected_filament_ranks.end()) ? iter2->second : INT_MAX; + if (selected_order1 != selected_order2) { - return name_order1 < name_order2; + return selected_order1 < selected_order2; } } { // Compare vendor - auto iter1 = std::find(sorted_vendors.begin(), sorted_vendors.end(), query_filament_vendors[left]); - auto iter2 = std::find(sorted_vendors.begin(), sorted_vendors.end(), query_filament_vendors[right]); - if (iter1 != iter2) - { - return iter1 < iter2; - }; + const wxString& vendor1 = query_filament_vendors.at(left); + const wxString& vendor2 = query_filament_vendors.at(right); + const auto rank1 = priority_rank(sorted_vendors, vendor1); + const auto rank2 = priority_rank(sorted_vendors, vendor2); + if (rank1 != rank2) + return rank1 < rank2; + + const int vendor_compare = vendor1.CmpNoCase(vendor2); + if (vendor_compare != 0) + return vendor_compare < 0; } { // Compare type - auto iter1 = std::find(sorted_types.begin(), sorted_types.end(), query_filament_types[left]); - auto iter2 = std::find(sorted_types.begin(), sorted_types.end(), query_filament_types[right]); - if (iter1 != iter2) - { - return iter1 < iter2; - } + const wxString& type1 = query_filament_types.at(left); + const wxString& type2 = query_filament_types.at(right); + const auto rank1 = priority_rank(sorted_types, type1); + const auto rank2 = priority_rank(sorted_types, type2); + if (rank1 != rank2) + return rank1 < rank2; + + const int type_compare = type1.CmpNoCase(type2); + if (type_compare != 0) + return type_compare < 0; } - return left < right; + const int name_compare = left.CmpNoCase(right); + return name_compare != 0 ? name_compare < 0 : left < right; }; std::sort(filament_items.begin(), filament_items.end(), _filament_sorter); diff --git a/src/slic3r/GUI/AVVideoDecoder.cpp b/src/slic3r/GUI/AVVideoDecoder.cpp new file mode 100644 index 0000000000..d7b8432bd3 --- /dev/null +++ b/src/slic3r/GUI/AVVideoDecoder.cpp @@ -0,0 +1,170 @@ +#include "AVVideoDecoder.hpp" + +#include <assert.h> + +extern "C" +{ + #include <libavutil/avutil.h> + #include <libavutil/imgutils.h> +} + +AVVideoDecoder::AVVideoDecoder() +{ + codec_ctx_ = avcodec_alloc_context3(nullptr); +} + +AVVideoDecoder::~AVVideoDecoder() +{ + if (sws_ctx_) + sws_freeContext(sws_ctx_); + if (frame_) + av_frame_free(&frame_); + if (codec_ctx_) + avcodec_free_context(&codec_ctx_); +} + +int AVVideoDecoder::open(Bambu_StreamInfo const &info) +{ + auto codec_id = info.sub_type == AVC1 ? AV_CODEC_ID_H264 : AV_CODEC_ID_MJPEG; + auto codec = avcodec_find_decoder(codec_id); + if (codec == nullptr) { + fprintf(stderr, "AVVideoDecoder: unsupported codec!\n"); + return -1; // Codec not found + } + /* open the coderc */ + if (avcodec_open2(codec_ctx_, codec, nullptr) < 0) { + fprintf(stderr, "AVVideoDecoder: could not open codec\n"); + return -1; + } + + // Allocate an AVFrame structure + frame_ = av_frame_alloc(); + if (frame_ == nullptr) + return -1; + + return 0; +} + +int AVVideoDecoder::decode(const Bambu_Sample &sample) +{ + int ret = -1; + AVPacket *pkt = av_packet_alloc(); + if (!pkt) { + return ret; + } + + ret = av_new_packet(pkt, sample.size); + if (ret != 0) { + av_packet_free(&pkt); + return ret; + } + + memcpy(pkt->data, sample.buffer, size_t(sample.size)); + + ret = avcodec_send_packet(codec_ctx_, pkt); + if (ret == 0) { + got_frame_ = avcodec_receive_frame(codec_ctx_, frame_) == 0; + } + + av_packet_unref(pkt); + av_packet_free(&pkt); + return ret; +} + +int AVVideoDecoder::flush() +{ + int ret = avcodec_send_packet(codec_ctx_, nullptr); + got_frame_ = avcodec_receive_frame(codec_ctx_, frame_) == 0; + return ret; +} + +void AVVideoDecoder::close() +{ +} + +bool AVVideoDecoder::toWxImage(wxImage &image, wxSize const &size2) +{ + if (!got_frame_) + return false; + + auto size1 = size2; + if (!size1.IsFullySpecified()) + size1 = {frame_->width, frame_->height }; + auto size = size1; + if (size.GetWidth() & 0x0f) { + size.SetWidth((size.GetWidth() & ~0x0f) + 0x10); + if (size.GetWidth() != width_) { + std::fill(bits_.begin(), bits_.end(), 0); + width_ = size.GetWidth(); + } + } + AVPixelFormat wxFmt = AV_PIX_FMT_RGB24; + sws_ctx_ = sws_getCachedContext(sws_ctx_, + frame_->width, frame_->height, AVPixelFormat(frame_->format), + size1.GetWidth(), size1.GetHeight(), wxFmt, + SWS_GAUSS, + nullptr, nullptr, nullptr); + if (sws_ctx_ == nullptr) + return false; + int length = size.GetWidth() * size.GetHeight() * 3; + if (bits_.size() < length) + bits_.resize(length); + uint8_t * datas[] = { bits_.data() }; + int strides[] = { size.GetWidth() * 3 }; + int result_h = sws_scale(sws_ctx_, frame_->data, frame_->linesize, 0, frame_->height, datas, strides); + if (result_h != size.GetHeight()) { + return false; + } + // Copy: the frame outlives this decoder and is painted by the GUI thread while the + // next sws_scale is already overwriting bits_, so it must own its pixels. The Windows + // path below needs no equivalent, wxBitmap copies the bits into GDI. + image = wxImage(size.GetWidth(), size.GetHeight(), bits_.data(), true).Copy(); + if (!image.IsOk()) { + fprintf(stderr, "AVVideoDecoder: image not ok %dx%d\n", size.GetWidth(), size.GetHeight()); + return false; + } + return true; +} + +bool AVVideoDecoder::toWxBitmap(wxBitmap &bitmap, wxSize const &size2) +{ + if (!got_frame_) + return false; + + auto size1 = size2; + if (!size1.IsFullySpecified()) + size1 = {frame_->width, frame_->height }; + auto size = size1; + if (size.GetWidth() & 0x0f) { + size.SetWidth((size.GetWidth() & ~0x0f) + 0x10); + if (size.GetWidth() != width_) { + std::fill(bits_.begin(), bits_.end(), 0); + width_ = size.GetWidth(); + } + } + AVPixelFormat wxFmt = AV_PIX_FMT_RGB32; + sws_ctx_ = sws_getCachedContext(sws_ctx_, + frame_->width, frame_->height, AVPixelFormat(frame_->format), + size1.GetWidth(), size1.GetHeight(), wxFmt, + SWS_GAUSS, + nullptr, nullptr, nullptr); + if (sws_ctx_ == nullptr) + return false; + int length = size.GetWidth() * size.GetHeight() * 4; + if (bits_.size() < length) + bits_.resize(length); + uint8_t *datas[] = { bits_.data() }; + int strides[] = { size.GetWidth() * 4 }; + int result_h = sws_scale(sws_ctx_, frame_->data, frame_->linesize, 0, frame_->height, datas, strides); + if (result_h != size.GetHeight()) { + fprintf(stderr, "AVVideoDecoder: result_h %d %d\n", result_h, size.GetHeight()); + return false; + } + bitmap = wxBitmap((char const *) bits_.data(), size.GetWidth(), size.GetHeight(), 32); + assert(bitmap.IsOk()); + if (!bitmap.IsOk()) { + fprintf(stderr, "AVVideoDecoder: bitmap not ok %dx%d\n", size.GetWidth(), size.GetHeight()); + return false; + } + return true; +} diff --git a/src/slic3r/GUI/AVVideoDecoder.hpp b/src/slic3r/GUI/AVVideoDecoder.hpp new file mode 100644 index 0000000000..4111e860a2 --- /dev/null +++ b/src/slic3r/GUI/AVVideoDecoder.hpp @@ -0,0 +1,46 @@ +#ifndef AVVIDEODECODER_HPP +#define AVVIDEODECODER_HPP + +#include "Printer/BambuTunnel.h" + +extern "C" { + #include <libavcodec/avcodec.h> + #include <libswscale/swscale.h> +} +#include <vector> +#include <wx/bitmap.h> +#include <wx/gdicmn.h> +#include <wx/image.h> + +class wxBitmap; + +class AVVideoDecoder +{ +public: + AVVideoDecoder(); + + ~AVVideoDecoder(); + +public: + int open(Bambu_StreamInfo const &info); + + int decode(Bambu_Sample const &sample); + + int flush(); + + void close(); + + bool toWxImage(wxImage &image, wxSize const &size); + + bool toWxBitmap(wxBitmap &bitmap, wxSize const & size); + +private: + AVCodecContext *codec_ctx_ = nullptr; + AVFrame * frame_ = nullptr; + SwsContext * sws_ctx_ = nullptr; + bool got_frame_ = false; + int width_ { 0 }; // scale result width + std::vector<uint8_t> bits_; +}; + +#endif // AVVIDEODECODER_HPP diff --git a/src/slic3r/GUI/AboutDialog.cpp b/src/slic3r/GUI/AboutDialog.cpp index a4f0f06d65..9e1cefbc84 100644 --- a/src/slic3r/GUI/AboutDialog.cpp +++ b/src/slic3r/GUI/AboutDialog.cpp @@ -3,6 +3,7 @@ #include "libslic3r/Utils.hpp" #include "libslic3r/Color.hpp" +#include "BuildCommit.hpp" #include "GUI.hpp" #include "GUI_App.hpp" #include "MainFrame.hpp" @@ -245,7 +246,7 @@ AboutDialog::AboutDialog() vesizer->Add(0, 0, 1, wxEXPAND, FromDIP(5)); auto version_string = std::string(SoftFever_VERSION); // _L("Orca Slicer ") + " " + std::string(SoftFever_VERSION); wxStaticText* version = new wxStaticText(this, wxID_ANY, version_string.c_str(), wxDefaultPosition, wxDefaultSize); - wxStaticText* credits_string = new wxStaticText(this, wxID_ANY, wxString::Format("Build %s", std::string(GIT_COMMIT_HASH)), wxDefaultPosition, wxDefaultSize); + wxStaticText* credits_string = new wxStaticText(this, wxID_ANY, wxString::Format("Build %s", build_commit_label), wxDefaultPosition, wxDefaultSize); credits_string->SetFont(_build_string_font); wxFont version_font = GetFont(); version_font = version_font.Scaled(1.85f); // SetPointSize(20) not works on macOS because it uses a 72 PPI reference diff --git a/src/slic3r/GUI/AboutDialog.hpp b/src/slic3r/GUI/AboutDialog.hpp index 8dc72d15a3..e96c74ba08 100644 --- a/src/slic3r/GUI/AboutDialog.hpp +++ b/src/slic3r/GUI/AboutDialog.hpp @@ -60,7 +60,6 @@ class AboutDialog : public DPIDialog wxHtmlWindow* m_html; wxStaticBitmap* m_logo; int m_copy_rights_btn_id { wxID_ANY }; - int m_copy_version_btn_id { wxID_ANY }; public: AboutDialog(); diff --git a/src/slic3r/GUI/Auxiliary.cpp b/src/slic3r/GUI/Auxiliary.cpp index 95244436a3..13ca173eb6 100644 --- a/src/slic3r/GUI/Auxiliary.cpp +++ b/src/slic3r/GUI/Auxiliary.cpp @@ -869,11 +869,11 @@ void AuxiliaryPanel::init_tabpanel() m_assembly_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::ASSEMBLY_GUIDE); m_others_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::OTHERS); - m_tabpanel->AddPage(m_designer_panel, _L("Basic Info"), "", true); - m_tabpanel->AddPage(m_pictures_panel, _L("Pictures"), "", false); - m_tabpanel->AddPage(m_bill_of_materials_panel, _L("Bill of Materials"), "", false); - m_tabpanel->AddPage(m_assembly_panel, _L("Assembly Guide"), "", false); - m_tabpanel->AddPage(m_others_panel, _L("Others"), "", false); + m_tabpanel->AddPage(m_designer_panel, _L("Basic Info"), true); + m_tabpanel->AddPage(m_pictures_panel, _L("Pictures"), false); + m_tabpanel->AddPage(m_bill_of_materials_panel, _L("Bill of Materials"), false); + m_tabpanel->AddPage(m_assembly_panel, _L("Assembly Guide"), false); + m_tabpanel->AddPage(m_others_panel, _L("Others"), false); } wxWindow *AuxiliaryPanel::create_side_tools() diff --git a/src/slic3r/GUI/BBLTopbar.hpp b/src/slic3r/GUI/BBLTopbar.hpp index 465126944b..e60a95f64f 100644 --- a/src/slic3r/GUI/BBLTopbar.hpp +++ b/src/slic3r/GUI/BBLTopbar.hpp @@ -93,7 +93,6 @@ private: CenteredTitle* m_title_ctrl { nullptr }; wxString m_titleText; - wxAuiToolBarItem* m_account_item; wxAuiToolBarItem* m_model_store_item; //wxAuiToolBarItem *m_publish_item; diff --git a/src/slic3r/GUI/BambuPlayer/BambuPlayer.h b/src/slic3r/GUI/BambuPlayer/BambuPlayer.h deleted file mode 100644 index fe5f0d049a..0000000000 --- a/src/slic3r/GUI/BambuPlayer/BambuPlayer.h +++ /dev/null @@ -1,30 +0,0 @@ -// -// BambuPlayer.h -// BambuPlayer -// -// Created by cmguo on 2021/12/6. -// - -#import <Foundation/Foundation.h> -#import <AVFoundation/AVSampleBufferDisplayLayer.h> -#import <Cocoa/Cocoa.h> - -NS_ASSUME_NONNULL_BEGIN - -@interface BambuPlayer : NSObject - -+ (void) initialize; - -- (instancetype) initWithDisplayLayer: (AVSampleBufferDisplayLayer*) layer; -- (instancetype) initWithImageView: (NSView*) view; -- (int) open: (char const *) url; -- (NSSize) videoSize; -- (int) play; -- (void) stop; -- (void) close; - -- (void) setLogger: (void (*)(void const * context, int level, char const * msg)) logger withContext: (void const *) context; - -@end - -NS_ASSUME_NONNULL_END diff --git a/src/slic3r/GUI/BindDialog.cpp b/src/slic3r/GUI/BindDialog.cpp index a4b568e82d..4c6d3c249d 100644 --- a/src/slic3r/GUI/BindDialog.cpp +++ b/src/slic3r/GUI/BindDialog.cpp @@ -468,7 +468,7 @@ PingCodeBindDialog::~PingCodeBindDialog() { m_link_privacy_title->SetFont(Label::Head_13); m_link_privacy_title->SetMaxSize(wxSize(FromDIP(450), -1)); m_link_privacy_title->Wrap(FromDIP(450)); - m_link_privacy_title->Bind(wxEVT_LEFT_DOWN, [this](auto& e) { + m_link_privacy_title->Bind(wxEVT_LEFT_DOWN, [](auto& e) { std::string url; std::string country_code = Slic3r::GUI::wxGetApp().app_config->get_country_code(); @@ -893,7 +893,7 @@ void BindMachineDialog::on_show(wxShowEvent &event) } } }) - .on_error([this](std::string body, std::string error, unsigned status) { + .on_error([](std::string body, std::string error, unsigned status) { //BOOST_LOG_TRIVIAL(info) << "load oss picture failed, oss path: " << oss_path << " status:" << status << " error:" << error; }).perform(); } @@ -1099,7 +1099,7 @@ void UnBindMachineDialog::on_show(wxShowEvent &event) } } }) - .on_error([this](std::string body, std::string error, unsigned status) { + .on_error([](std::string body, std::string error, unsigned status) { //BOOST_LOG_TRIVIAL(info) << "load oss picture failed, oss path: " << oss_path << " status:" << status << " error:" << error; }).perform(); diff --git a/src/slic3r/GUI/BuildCommit.cpp b/src/slic3r/GUI/BuildCommit.cpp new file mode 100644 index 0000000000..adfe66af68 --- /dev/null +++ b/src/slic3r/GUI/BuildCommit.cpp @@ -0,0 +1,9 @@ +#include "BuildCommit.hpp" +#include "git_commit_hash.h" + +namespace Slic3r { namespace GUI { + +const char *const build_commit_hash = GIT_COMMIT_HASH; +const char *const build_commit_label = GIT_COMMIT_HASH GIT_COMMIT_SUFFIX; + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/BuildCommit.hpp b/src/slic3r/GUI/BuildCommit.hpp new file mode 100644 index 0000000000..0450e710c9 --- /dev/null +++ b/src/slic3r/GUI/BuildCommit.hpp @@ -0,0 +1,15 @@ +#pragma once + +// Read these rather than including git_commit_hash.h, which changes with every +// commit and rebuilds everything that includes it. + +namespace Slic3r { namespace GUI { + +// The commit alone, safe to use in a commit URL. +extern const char *const build_commit_hash; + +// The same, with "-dirty" when the build had uncommitted changes. Use this +// wherever the build is shown to a person. +extern const char *const build_commit_label; + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/CaliHistoryDialog.cpp b/src/slic3r/GUI/CaliHistoryDialog.cpp index 4de90d8b8f..6c1e612864 100644 --- a/src/slic3r/GUI/CaliHistoryDialog.cpp +++ b/src/slic3r/GUI/CaliHistoryDialog.cpp @@ -443,7 +443,7 @@ void HistoryWindow::sync_history_data() { auto edit_button = new Button(m_history_data_panel, _L("Edit")); edit_button->SetStyle(ButtonStyle::Confirm, ButtonType::Window); - edit_button->Bind(wxEVT_BUTTON, [this, result, k_value, name_value, edit_button](auto& e) { + edit_button->Bind(wxEVT_BUTTON, [this, result, k_value, name_value](auto& e) { if (m_ui_op_lock) return; PACalibResult result_buffer = result; diff --git a/src/slic3r/GUI/Calibration.cpp b/src/slic3r/GUI/Calibration.cpp index 0c2b4f1659..f3351fdb6b 100644 --- a/src/slic3r/GUI/Calibration.cpp +++ b/src/slic3r/GUI/Calibration.cpp @@ -201,7 +201,7 @@ wxWindow* CalibrationDialog::create_check_option(wxString title, wxWindow* paren checkbox->SetToolTip(tooltip); text->SetToolTip(tooltip); - text->Bind(wxEVT_LEFT_DOWN, [this, check](wxMouseEvent&) { check->SetValue(check->GetValue() ? false : true); }); + text->Bind(wxEVT_LEFT_DOWN, [check](wxMouseEvent&) { check->SetValue(check->GetValue() ? false : true); }); m_checkbox_list[param] = check; m_checkbox_list[param]->SetValue(true); return checkbox; diff --git a/src/slic3r/GUI/CalibrationPanel.cpp b/src/slic3r/GUI/CalibrationPanel.cpp index b006509adf..bdc79c1c8e 100644 --- a/src/slic3r/GUI/CalibrationPanel.cpp +++ b/src/slic3r/GUI/CalibrationPanel.cpp @@ -488,7 +488,6 @@ void CalibrationPanel::init_tabpanel() { selected = true; m_tabpanel->AddPage(m_cali_panels[i], get_calibration_type_name(m_cali_panels[i]->get_calibration_mode()), - "", selected); } diff --git a/src/slic3r/GUI/CalibrationWizard.cpp b/src/slic3r/GUI/CalibrationWizard.cpp index b1f5c206c6..7496d59a51 100644 --- a/src/slic3r/GUI/CalibrationWizard.cpp +++ b/src/slic3r/GUI/CalibrationWizard.cpp @@ -477,7 +477,7 @@ void CalibrationWizard::on_cali_go_home() if (go_home_dialog == nullptr) go_home_dialog = new SecondaryCheckDialog(this, wxID_ANY, _L("Confirm")); - go_home_dialog->Bind(EVT_SECONDARY_CHECK_CONFIRM, [this, method](wxCommandEvent &e) { + go_home_dialog->Bind(EVT_SECONDARY_CHECK_CONFIRM, [this](wxCommandEvent &e) { if (curr_obj) { curr_obj->command_task_abort(); } else { diff --git a/src/slic3r/GUI/CalibrationWizardCaliPage.cpp b/src/slic3r/GUI/CalibrationWizardCaliPage.cpp index d9604da84a..152f46f37c 100644 --- a/src/slic3r/GUI/CalibrationWizardCaliPage.cpp +++ b/src/slic3r/GUI/CalibrationWizardCaliPage.cpp @@ -90,7 +90,7 @@ void CalibrationCaliPage::on_subtask_abort(wxCommandEvent& event) if (abort_dlg == nullptr) { abort_dlg = new SecondaryCheckDialog(this->GetParent(), wxID_ANY, _L("Cancel print")); - abort_dlg->Bind(EVT_SECONDARY_CHECK_CONFIRM, [this, obj](wxCommandEvent& e) { + abort_dlg->Bind(EVT_SECONDARY_CHECK_CONFIRM, [obj](wxCommandEvent& e) { if (obj) obj->command_task_abort(); }); } diff --git a/src/slic3r/GUI/CalibrationWizardPage.cpp b/src/slic3r/GUI/CalibrationWizardPage.cpp index ef17da9081..c117ec8857 100644 --- a/src/slic3r/GUI/CalibrationWizardPage.cpp +++ b/src/slic3r/GUI/CalibrationWizardPage.cpp @@ -762,7 +762,7 @@ void CaliPageActionPanel::bind_button(CaliPageActionType action_type, bool is_bl if (is_block) { m_action_btns[i]->Bind(wxEVT_BUTTON, - [this](wxCommandEvent& evt) { + [](wxCommandEvent& evt) { MessageDialog msg(nullptr, _L("The current firmware version of the printer does not support calibration.\nPlease upgrade the printer firmware."), _L("Calibration not supported"), wxOK | wxICON_WARNING); msg.ShowModal(); }); diff --git a/src/slic3r/GUI/CalibrationWizardPresetPage.cpp b/src/slic3r/GUI/CalibrationWizardPresetPage.cpp index db97354ad1..5267715439 100644 --- a/src/slic3r/GUI/CalibrationWizardPresetPage.cpp +++ b/src/slic3r/GUI/CalibrationWizardPresetPage.cpp @@ -1015,7 +1015,7 @@ wxBoxSizer* CalibrationPresetPage::create_ams_items_sizer(MachineObject* obj, wx auto ams_items_sizer = new wxBoxSizer(wxHORIZONTAL); for (auto &info : ams_info) { auto preview_ams_item = new AMSPreview(ams_preview_panel, wxID_ANY, info, info.ams_type); - preview_ams_item->Update(info); + preview_ams_item->UpdateInfo(info); preview_ams_item->Open(); ams_preview_list.push_back(preview_ams_item); std::string ams_id = preview_ams_item->get_ams_id(); diff --git a/src/slic3r/GUI/CalibrationWizardSavePage.cpp b/src/slic3r/GUI/CalibrationWizardSavePage.cpp index 78bc4bb77f..f7699cfab0 100644 --- a/src/slic3r/GUI/CalibrationWizardSavePage.cpp +++ b/src/slic3r/GUI/CalibrationWizardSavePage.cpp @@ -272,7 +272,7 @@ void CaliPASaveAutoPanel::sync_cali_result(const std::vector<PACalibResult>& cal preset_names = default_naming(preset_names); std::vector<PACalibResult> sorted_cali_result = cali_result; - std::sort(sorted_cali_result.begin(), sorted_cali_result.end(), [this](const PACalibResult &left, const PACalibResult& right) { + std::sort(sorted_cali_result.begin(), sorted_cali_result.end(), [](const PACalibResult &left, const PACalibResult& right) { return left.tray_id < right.tray_id; }); @@ -366,7 +366,7 @@ void CaliPASaveAutoPanel::sync_cali_result(const std::vector<PACalibResult>& cal } } - comboBox_tray_name->Bind(wxEVT_COMBOBOX, [this, comboBox_tray_name, k_value, n_value](auto& e) { + comboBox_tray_name->Bind(wxEVT_COMBOBOX, [comboBox_tray_name](auto& e) { int selection = comboBox_tray_name->GetSelection(); auto history = filtered_results[selection]; }); @@ -744,7 +744,7 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector< } } - comboBox_tray_name->Bind(wxEVT_COMBOBOX, [this, comboBox_tray_name, k_value, n_value](auto &e) { + comboBox_tray_name->Bind(wxEVT_COMBOBOX, [comboBox_tray_name](auto &e) { int selection = comboBox_tray_name->GetSelection(); auto history = filtered_results[selection]; }); diff --git a/src/slic3r/GUI/CalibrationWizardSavePage.hpp b/src/slic3r/GUI/CalibrationWizardSavePage.hpp index 4726cb1230..eb15720e96 100644 --- a/src/slic3r/GUI/CalibrationWizardSavePage.hpp +++ b/src/slic3r/GUI/CalibrationWizardSavePage.hpp @@ -193,7 +193,7 @@ public: void show_panels(CalibrationMethod method, const PrinterSeries printer_ser); - void on_device_connected(MachineObject* obj); + void on_device_connected(MachineObject* obj) override; void update(MachineObject* obj) override; diff --git a/src/slic3r/GUI/CalibrationWizardStartPage.hpp b/src/slic3r/GUI/CalibrationWizardStartPage.hpp index 0e893bce10..026ce187ac 100644 --- a/src/slic3r/GUI/CalibrationWizardStartPage.hpp +++ b/src/slic3r/GUI/CalibrationWizardStartPage.hpp @@ -48,8 +48,8 @@ public: void create_page(wxWindow* parent); - void on_reset_page(); - void on_device_connected(MachineObject* obj); + void on_reset_page() override; + void on_device_connected(MachineObject* obj) override; void msw_rescale() override; }; @@ -63,8 +63,8 @@ public: long style = wxTAB_TRAVERSAL); void create_page(wxWindow* parent); - void on_reset_page(); - void on_device_connected(MachineObject* obj); + void on_reset_page() override; + void on_device_connected(MachineObject* obj) override; void msw_rescale() override; }; diff --git a/src/slic3r/GUI/CameraPopup.cpp b/src/slic3r/GUI/CameraPopup.cpp index 0ee5694a34..cc7e00eec6 100644 --- a/src/slic3r/GUI/CameraPopup.cpp +++ b/src/slic3r/GUI/CameraPopup.cpp @@ -140,7 +140,7 @@ CameraPopup::CameraPopup(wxWindow *parent) vcamera_guide_link->Wrap(-1); vcamera_guide_link->SetForegroundColour(wxColour(0x1F, 0x8E, 0xEA)); auto text_size = vcamera_guide_link->GetTextExtent(text); - vcamera_guide_link->Bind(wxEVT_LEFT_DOWN, [this, url](wxMouseEvent& e) {wxLaunchDefaultBrowser(url); }); + vcamera_guide_link->Bind(wxEVT_LEFT_DOWN, [url](wxMouseEvent& e) {wxLaunchDefaultBrowser(url); }); link_underline = new wxPanel(m_panel, wxID_ANY, wxDefaultPosition, wxSize(-1, 1), wxTAB_TRAVERSAL); link_underline->SetBackgroundColour(wxColour(0x1F, 0x8E, 0xEA)); diff --git a/src/slic3r/GUI/ColorDecomposeDialog.cpp b/src/slic3r/GUI/ColorDecomposeDialog.cpp new file mode 100644 index 0000000000..2176de110e --- /dev/null +++ b/src/slic3r/GUI/ColorDecomposeDialog.cpp @@ -0,0 +1,951 @@ +#include "ColorDecomposeDialog.hpp" + +#include <algorithm> +#include <cmath> +#include <functional> +#include <memory> +#include <set> +#include <wx/sizer.h> +#include <wx/dcclient.h> +#include <wx/dcbuffer.h> +#include "wx/graphics.h" + +#include "I18N.hpp" +#include "GUI.hpp" +#include "GUI_App.hpp" +#include "format.hpp" +#include "Widgets/ComboBox.hpp" +#include "Widgets/DropDown.hpp" +#include "Widgets/Button.hpp" +#include "Widgets/CheckBox.hpp" +#include "Widgets/Label.hpp" +#include "wxExtensions.hpp" +#include "ColorDecomposeSupport.hpp" +#include "libslic3r/ColorDecomposeRecipe.hpp" + +namespace Slic3r { +namespace GUI { + +static const wxColour COLOR_BRAND("#009688"); +static const wxColour COLOR_BORDER_NORMAL("#EEEEEE"); +static const wxColour COLOR_BG_CARD("#F8F8F8"); +static const wxColour COLOR_LABEL_GREY("#ACACAC"); +static const wxColour COLOR_TEXT_DARK("#262E30"); +static const wxColour COLOR_DIVIDER("#EEEEEE"); + +// Standard CMYW base colors +static const wxColour CMYW_CYAN(0, 255, 255); +static const wxColour CMYW_MAGENTA(255, 0, 255); +static const wxColour CMYW_YELLOW(255, 255, 0); +static const wxColour CMYW_WHITE(255, 255, 255); + +// Standard RYBW base colors +static const wxColour RYBW_RED(255, 0, 0); +static const wxColour RYBW_YELLOW(255, 255, 0); +static const wxColour RYBW_BLUE(0, 0, 255); +static const wxColour RYBW_WHITE(255, 255, 255); + +static size_t mode_index(DecomposeMode mode) +{ + return static_cast<size_t>(mode); +} + +static ColorDecomposeRgb wx_colour_to_recipe_rgb(const wxColour& color) +{ + return { + static_cast<unsigned char>(color.Red()), + static_cast<unsigned char>(color.Green()), + static_cast<unsigned char>(color.Blue()) + }; +} + +static wxColour hex_to_wx_colour(const std::string& hex, const wxColour& fallback) +{ + wxColour color(hex); + return color.IsOk() ? color : fallback; +} + +static bool same_rgb(const wxColour& lhs, const wxColour& rhs) +{ + return lhs.Red() == rhs.Red() && lhs.Green() == rhs.Green() && lhs.Blue() == rhs.Blue(); +} + +static DecomposeBaseColor standard_base_color_from_key(const std::string& key) +{ + if (key == "Cyan") return DecomposeBaseColor::Cyan; + if (key == "Magenta") return DecomposeBaseColor::Magenta; + if (key == "Yellow") return DecomposeBaseColor::Yellow; + if (key == "White") return DecomposeBaseColor::White; + if (key == "Red") return DecomposeBaseColor::Red; + if (key == "Green") return DecomposeBaseColor::Green; + if (key == "Blue") return DecomposeBaseColor::Blue; + return DecomposeBaseColor::None; +} + +static wxColour pure_color_for_base(DecomposeBaseColor base) +{ + switch (base) { + case DecomposeBaseColor::Cyan: return CMYW_CYAN; + case DecomposeBaseColor::Magenta: return CMYW_MAGENTA; + case DecomposeBaseColor::Yellow: return CMYW_YELLOW; + case DecomposeBaseColor::White: return CMYW_WHITE; + case DecomposeBaseColor::Red: return RYBW_RED; + case DecomposeBaseColor::Blue: return RYBW_BLUE; + default: return *wxBLACK; + } +} + +static DecomposeBaseColor standard_base_color_for(DecomposeMode mode, const wxColour& color) +{ + if (mode == DecomposeMode::CMYW) { + if (same_rgb(color, CMYW_CYAN)) return DecomposeBaseColor::Cyan; + if (same_rgb(color, CMYW_MAGENTA)) return DecomposeBaseColor::Magenta; + if (same_rgb(color, CMYW_YELLOW)) return DecomposeBaseColor::Yellow; + if (same_rgb(color, CMYW_WHITE)) return DecomposeBaseColor::White; + } else if (mode == DecomposeMode::RYBW) { + if (same_rgb(color, RYBW_RED)) return DecomposeBaseColor::Red; + if (same_rgb(color, RYBW_YELLOW)) return DecomposeBaseColor::Yellow; + if (same_rgb(color, RYBW_BLUE)) return DecomposeBaseColor::Blue; + if (same_rgb(color, RYBW_WHITE)) return DecomposeBaseColor::White; + } + return DecomposeBaseColor::None; +} + +static ColorDecomposeResult to_dialog_result(const ColorDecomposeRecipeResult& recipe, + const wxColour& fallback) +{ + ColorDecomposeResult result; + result.mode = recipe.mode; + result.matched_color = hex_to_wx_colour(recipe.matched_color_hex, fallback); + for (const auto& comp_recipe : recipe.components) { + DecomposeComponent comp; + comp.colour = hex_to_wx_colour(comp_recipe.color_hex, fallback); + comp.ratio = comp_recipe.ratio; + comp.filament_index = static_cast<int>(comp_recipe.filament_index); + comp.base_color = standard_base_color_from_key(comp_recipe.base_color); + if (comp.base_color == DecomposeBaseColor::None) + comp.base_color = standard_base_color_for(recipe.mode, comp.colour); + result.components.push_back(comp); + } + return result; +} + +static wxPanel* create_h_divider(wxWindow* parent, int fixed_width = -1) +{ + const int h = parent->FromDIP(1); + int w = fixed_width > 0 ? fixed_width : -1; + auto* panel = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(w, h)); + panel->SetMinSize(wxSize(w, h)); + if (fixed_width > 0) + panel->SetMaxSize(wxSize(fixed_width, h)); + panel->SetBackgroundColour(StateColor::darkModeColorFor(COLOR_DIVIDER)); + return panel; +} + +static wxStaticText* create_mode_group_label(wxWindow* parent, const wxString& text) +{ + auto* label = new wxStaticText(parent, wxID_ANY, text); + label->SetFont(Label::Body_11); + label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_LABEL_GREY)); + return label; +} + +static void match_parent_bg(wxWindow* w, const wxColour& bg) +{ + w->SetBackgroundColour(bg); +} + +static bool material_type_matches(const std::string& a, const std::string& b) +{ + if (a.empty() || b.empty()) + return false; + return a == b || a == b + " Basic" || b == a + " Basic"; +} + + +ColorDecomposeDialog::ColorDecomposeDialog(wxWindow* parent, + int filament_idx, + const wxColour& target_color, + const std::vector<std::string>& physical_colors, + const std::vector<std::string>& filament_names, + const std::vector<std::string>& filament_types, + size_t current_filament_count, + size_t max_filament_count, + std::vector<size_t> physical_config_indices) + : DPIDialog(parent, wxID_ANY, _L("Decompose Color"), wxDefaultPosition, + wxDefaultSize, wxCAPTION | wxCLOSE_BOX) + , m_filament_idx(filament_idx) + , m_target_color(target_color) + , m_physical_colors(physical_colors) + , m_filament_names(filament_names) + , m_filament_types(filament_types) + , m_current_filament_count(current_filament_count) + , m_max_filament_count(max_filament_count) + , m_physical_config_indices(std::move(physical_config_indices)) +{ + for (const auto& t : m_filament_types) { + if (std::find(m_project_types.begin(), m_project_types.end(), t) == m_project_types.end()) + m_project_types.push_back(t); + } + + if (m_filament_idx >= 0 && static_cast<size_t>(m_filament_idx) < m_filament_types.size()) + m_preferred_type = m_filament_types[m_filament_idx]; + else if (!m_project_types.empty()) + m_preferred_type = m_project_types.front(); + + build_ui(); + wxGetApp().UpdateDlgDarkUI(this); + // Restore target swatch after dark mode color remapping + if (m_target_swatch) { + m_target_swatch->SetBackgroundColour(m_target_color); + m_target_swatch->Refresh(); + } + + update_card_visibility(); + Fit(); + compute_decomposition(); + update_matched_color_display(); + update_ok_button_state(); +} + +void ColorDecomposeDialog::on_dpi_changed(const wxRect& suggested_rect) +{ + (void)suggested_rect; + Fit(); + Refresh(); +} + +void ColorDecomposeDialog::build_ui() +{ + SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + + auto* main_sizer = new wxBoxSizer(wxVERTICAL); + + const int selector_side_margin = FromDIP(26); + const int selector_top_gap = FromDIP(22); + const int content_side_margin = FromDIP(30); + const int target_section_top_gap = FromDIP(18); + + main_sizer->AddSpacer(selector_top_gap); + main_sizer->Add(create_filament_selector(), 0, wxEXPAND | wxLEFT | wxRIGHT, selector_side_margin); + main_sizer->AddSpacer(target_section_top_gap); + main_sizer->Add(create_target_color_section(), 0, wxEXPAND | wxLEFT | wxRIGHT, content_side_margin); + main_sizer->AddSpacer(FromDIP(16)); + main_sizer->Add(create_h_divider(this), 0, wxEXPAND | wxLEFT | wxRIGHT, content_side_margin); + main_sizer->AddSpacer(FromDIP(16)); + main_sizer->Add(create_mode_selection_section(), 0, wxEXPAND | wxLEFT | wxRIGHT, content_side_margin); + main_sizer->AddSpacer(FromDIP(16)); + main_sizer->Add(create_button_panel(), 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, content_side_margin); + + SetSizer(main_sizer); + SetMinSize(wxSize(FromDIP(477), FromDIP(380))); + Fit(); + CenterOnParent(); +} + +wxBoxSizer* ColorDecomposeDialog::create_filament_selector() +{ + auto* sizer = new wxBoxSizer(wxHORIZONTAL); + + m_type_combo = new ComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, + wxSize(-1, FromDIP(36)), 0, nullptr, wxCB_READONLY); + m_type_combo->SetFont(Label::Body_13); + + m_combo_item_types.clear(); + int default_sel = -1; + + // --- Group 1: Project filament list (deduplicated by type) --- + m_type_combo->Append(_L("Project Filament List"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED); + m_combo_item_types.push_back(std::string()); + + std::set<std::string> seen_types; + for (size_t i = 0; i < m_filament_names.size(); ++i) { + const std::string& type = (i < m_filament_types.size()) ? m_filament_types[i] : "PLA"; + if (!seen_types.insert(type).second) + continue; + int idx = m_type_combo->Append(wxString::FromUTF8(m_filament_names[i])); + m_combo_item_types.push_back(type); + if (type == m_preferred_type && default_sel < 0) + default_sel = idx; + } + + // --- Group 2: Standard mode material recommendations --- + static const char* kStandardTypes[] = { + kDecomposePlaBasicType + }; + + m_type_combo->Append(_L("Standard Mode Recommendations"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED); + m_combo_item_types.push_back(std::string()); + + for (size_t s = 0; s < sizeof(kStandardTypes) / sizeof(kStandardTypes[0]); ++s) { + // Always show standard recommendations, even if the same type already + // appears in the project filament list above. + const std::string label = std::string(kDecomposeBambuPresetPrefix) + kStandardTypes[s]; + int idx = m_type_combo->Append(wxString::FromUTF8(label)); + m_combo_item_types.push_back(kStandardTypes[s]); + if (kStandardTypes[s] == m_preferred_type && default_sel < 0) + default_sel = idx; + } + + if (default_sel < 0) { + for (int i = 0; i < static_cast<int>(m_combo_item_types.size()); ++i) { + if (!m_combo_item_types[i].empty()) { + default_sel = i; + break; + } + } + } + + if (default_sel >= 0) { + m_type_combo->SetSelection(default_sel); + if (!m_combo_item_types[default_sel].empty()) + m_preferred_type = m_combo_item_types[default_sel]; + } + + m_type_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& evt) { + evt.StopPropagation(); + int sel = m_type_combo->GetSelection(); + if (sel >= 0 && static_cast<size_t>(sel) < m_combo_item_types.size() + && !m_combo_item_types[sel].empty()) { + m_preferred_type = m_combo_item_types[sel]; + } + update_card_visibility(); + compute_decomposition(); + update_matched_color_display(); + update_ok_button_state(); + }); + + sizer->Add(m_type_combo, 1, wxEXPAND); + return sizer; +} + +static wxPanel* create_color_swatch(wxWindow* parent, const wxColour& color, int size) +{ + auto* panel = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(size, size)); + panel->SetBackgroundColour(color); + panel->SetMinSize(wxSize(size, size)); + panel->SetBackgroundStyle(wxBG_STYLE_PAINT); + panel->Bind(wxEVT_PAINT, [panel](wxPaintEvent&) { + wxAutoBufferedPaintDC dc(panel); + wxSize sz = panel->GetClientSize(); + wxColour c = panel->GetBackgroundColour(); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(c)); + dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + // Mirror sidebar (FilamentBitmapUtils::create_single_filament_bitmap): + // gray border for near-white in light mode so white swatches stay + // visible on a white background; light border for near-black in dark mode. + const bool light_mode = !wxGetApp().dark_mode(); + if ((light_mode && c.Red() > 224 && c.Green() > 224 && c.Blue() > 224) || + (!light_mode && c.Red() < 45 && c.Green() < 45 && c.Blue() < 45)) { + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.SetPen(wxPen(light_mode ? wxColour(130, 130, 128) : wxColour(207, 207, 207), + 1, wxPENSTYLE_SOLID)); + dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + } + }); + return panel; +} + +wxBoxSizer* ColorDecomposeDialog::create_target_color_section() +{ + auto* sizer = new wxBoxSizer(wxHORIZONTAL); + + auto* label = new wxStaticText(this, wxID_ANY, _L("Target Color")); + label->SetFont(Label::Head_14); + label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK)); + sizer->Add(label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(19)); + + m_target_swatch = create_color_swatch(this, m_target_color, FromDIP(28)); + sizer->Add(m_target_swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12)); + + m_target_rgb_text = new wxStaticText(this, wxID_ANY, + wxString::Format("RGB: %d, %d, %d", m_target_color.Red(), m_target_color.Green(), m_target_color.Blue())); + m_target_rgb_text->SetFont(Label::Body_13); + m_target_rgb_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK)); + sizer->Add(m_target_rgb_text, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12)); + + auto* arrow_text = new wxStaticText(this, wxID_ANY, wxString::FromUTF8("\xe2\x86\x92")); + arrow_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK)); + sizer->Add(arrow_text, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12)); + + m_matched_swatch = create_color_swatch(this, m_target_color, FromDIP(28)); + sizer->Add(m_matched_swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12)); + + m_matched_rgb_text = new wxStaticText(this, wxID_ANY, + wxString::Format("RGB: %d, %d, %d", m_target_color.Red(), m_target_color.Green(), m_target_color.Blue())); + m_matched_rgb_text->SetFont(Label::Head_13); + m_matched_rgb_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK)); + sizer->Add(m_matched_rgb_text, 0, wxALIGN_CENTER_VERTICAL); + + return sizer; +} + +wxPanel* ColorDecomposeDialog::create_mode_card(wxWindow* parent, DecomposeMode mode, + const wxString& title) +{ + const int pad = FromDIP(12); + + auto* card = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); + card->SetBackgroundStyle(wxBG_STYLE_PAINT); + + auto* card_sizer = new wxBoxSizer(wxVERTICAL); + + auto* title_sizer = new wxBoxSizer(wxHORIZONTAL); + auto* title_label = new wxStaticText(card, wxID_ANY, title); + title_label->SetFont(Label::Body_14); + title_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6A6A"))); + match_parent_bg(title_label, StateColor::darkModeColorFor(COLOR_BG_CARD)); + title_sizer->Add(title_label, 1, wxALIGN_CENTER_VERTICAL); + + auto* chk = new ::CheckBox(card); + chk->SetValue(mode == m_selected_mode); + match_parent_bg(chk, StateColor::darkModeColorFor(COLOR_BG_CARD)); + switch (mode) { + case DecomposeMode::MaterialList: m_chk_material_list = chk; break; + case DecomposeMode::CMYW: m_chk_cmyw = chk; break; + case DecomposeMode::RYBW: m_chk_rybw = chk; break; + } + chk->Bind(wxEVT_TOGGLEBUTTON, [this, mode](wxCommandEvent& e) { + select_mode(mode); + e.Skip(); // let CheckBox::update() re-sync its bitmap to GetValue() + }); + title_sizer->Add(chk, 0, wxALIGN_CENTER_VERTICAL); + + card_sizer->Add(title_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, pad); + + card_sizer->Add(create_h_divider(card), 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(8)); + + auto* colors_sizer = new wxBoxSizer(wxHORIZONTAL); + card_sizer->Add(colors_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, pad); + + auto& controls = m_mode_cards[mode_index(mode)]; + controls.card = card; + controls.components_sizer = colors_sizer; + + card->SetSizer(card_sizer); + card->SetMinSize(wxSize(FromDIP(128), FromDIP(111))); + card->SetMaxSize(wxSize(FromDIP(128), FromDIP(111))); + + card->Bind(wxEVT_PAINT, [this, card, mode](wxPaintEvent&) { + wxBufferedPaintDC dc(card); + wxSize sz = card->GetClientSize(); + dc.SetBackground(wxBrush(StateColor::darkModeColorFor(*wxWHITE))); + dc.Clear(); + + bool selected = (m_selected_mode == mode); + wxColour border_col = selected + ? StateColor::darkModeColorFor(COLOR_BRAND) + : StateColor::darkModeColorFor(COLOR_BORDER_NORMAL); + const int border_width = FromDIP(selected ? 2 : 1); + const double inset = border_width / 2.0; + std::unique_ptr<wxGraphicsContext> gc(wxGraphicsContext::Create(dc)); + if (gc) { + gc->SetPen(wxPen(border_col, border_width)); + gc->SetBrush(wxBrush(StateColor::darkModeColorFor(COLOR_BG_CARD))); + gc->DrawRoundedRectangle(inset, inset, sz.x - 2 * inset, sz.y - 2 * inset, FromDIP(8)); + } else { + const int fallback_inset = (border_width + 1) / 2; + dc.SetPen(wxPen(border_col, border_width)); + dc.SetBrush(wxBrush(StateColor::darkModeColorFor(COLOR_BG_CARD))); + dc.DrawRoundedRectangle(fallback_inset, fallback_inset, sz.x - 2 * fallback_inset, sz.y - 2 * fallback_inset, FromDIP(8)); + } + }); + + std::function<void(wxWindow*)> bind_click; + bind_click = [this, mode, chk, &bind_click](wxWindow* w) { + if (w == chk || dynamic_cast<::CheckBox*>(w)) + return; + w->Bind(wxEVT_LEFT_UP, [this, mode](wxMouseEvent&) { + select_mode(mode); + }); + w->SetCursor(wxCursor(wxCURSOR_HAND)); + for (auto* child : w->GetChildren()) + bind_click(child); + }; + bind_click(card); + + return card; +} + +wxBoxSizer* ColorDecomposeDialog::create_mode_selection_section() +{ + auto* sizer = new wxBoxSizer(wxVERTICAL); + + auto* section_label = new wxStaticText(this, wxID_ANY, _L("Select Color Decomposition")); + section_label->SetFont(Label::Head_14); + section_label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK)); + sizer->Add(section_label, 0, wxBOTTOM, FromDIP(4)); + + auto* modes_sizer = new wxBoxSizer(wxHORIZONTAL); + + // --- Arbitrary mode column (wrapped in a panel so the whole column hides together) --- + m_arb_column_panel = new wxPanel(this, wxID_ANY); + m_arb_column_panel->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + auto* arb_col = new wxBoxSizer(wxVERTICAL); + { + auto* arb_header_sizer = new wxBoxSizer(wxHORIZONTAL); + arb_header_sizer->Add(create_mode_group_label(m_arb_column_panel, _L("Arbitrary Mode")), + 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5)); + arb_header_sizer->Add(create_h_divider(m_arb_column_panel, FromDIP(88)), 0, wxALIGN_CENTER_VERTICAL); + arb_col->Add(arb_header_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(8)); + + m_card_material_list = create_mode_card(m_arb_column_panel, DecomposeMode::MaterialList, + _L("Material List")); + arb_col->Add(m_card_material_list, 0, wxEXPAND); + } + m_arb_column_panel->SetSizer(arb_col); + modes_sizer->Add(m_arb_column_panel, 0, wxEXPAND | wxRIGHT, FromDIP(16)); + + // --- Standard mode column --- + auto* std_col = new wxBoxSizer(wxVERTICAL); + { + auto* std_header_sizer = new wxBoxSizer(wxHORIZONTAL); + std_header_sizer->Add(create_mode_group_label(this, _L("Standard Mode")), + 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5)); + std_header_sizer->Add(create_h_divider(this), 1, wxALIGN_CENTER_VERTICAL); + std_col->Add(std_header_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(8)); + + auto* cards_sizer = new wxBoxSizer(wxHORIZONTAL); + + m_card_cmyw = create_mode_card(this, DecomposeMode::CMYW, "CMYW"); + cards_sizer->Add(m_card_cmyw, 0, wxRIGHT, FromDIP(12)); + + m_card_rybw = create_mode_card(this, DecomposeMode::RYBW, "RYBW"); + cards_sizer->Add(m_card_rybw, 0); + + std_col->Add(cards_sizer, 0, wxEXPAND); + } + modes_sizer->Add(std_col, 0, wxEXPAND); + + sizer->Add(modes_sizer, 0, wxEXPAND); + + m_no_card_hint = new wxStaticText(this, wxID_ANY, + _L("At least two filaments of the same material type are required for decomposition")); + m_no_card_hint->SetFont(Label::Body_13); + m_no_card_hint->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6A6A"))); + m_no_card_hint->Wrap(FromDIP(400)); + m_no_card_hint->Hide(); + sizer->Add(m_no_card_hint, 0, wxTOP, FromDIP(8)); + + m_limit_warning_panel = new wxPanel(this, wxID_ANY); + m_limit_warning_panel->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + auto* warning_sizer = new wxBoxSizer(wxHORIZONTAL); + auto* warn_bmp = new wxStaticBitmap(m_limit_warning_panel, wxID_ANY, + create_scaled_bitmap("obj_warning", m_limit_warning_panel, 16), + wxDefaultPosition, wxSize(FromDIP(16), FromDIP(16))); + m_limit_warning_text = new wxStaticText(m_limit_warning_panel, wxID_ANY, wxEmptyString); + m_limit_warning_text->SetFont(Label::Body_13); + m_limit_warning_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#D01B1B"))); + m_limit_warning_text->Wrap(FromDIP(400)); + warning_sizer->Add(warn_bmp, 0, wxALIGN_TOP | wxRIGHT, FromDIP(6)); + warning_sizer->Add(m_limit_warning_text, 1, wxEXPAND); + m_limit_warning_panel->SetSizer(warning_sizer); + m_limit_warning_panel->Hide(); + sizer->Add(m_limit_warning_panel, 0, wxEXPAND | wxTOP, FromDIP(8)); + + return sizer; +} + +wxBoxSizer* ColorDecomposeDialog::create_button_panel() +{ + auto* sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->AddStretchSpacer(); + + m_btn_cancel = new Button(this, _L("Cancel")); + m_btn_cancel->SetStyle(ButtonStyle::Regular, ButtonType::Choice); + m_btn_cancel->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); }); + + m_btn_ok = new Button(this, _L("OK")); + m_btn_ok->SetStyle(ButtonStyle::Confirm, ButtonType::Choice); + m_btn_ok->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + EndModal(wxID_OK); + }); + + sizer->Add(m_btn_cancel, 0, wxRIGHT, FromDIP(12)); + sizer->Add(m_btn_ok, 0); + + return sizer; +} + +void ColorDecomposeDialog::select_mode(DecomposeMode mode) +{ + m_selected_mode = mode; + m_result = m_mode_results[mode_index(mode)]; + update_card_styles(); + update_matched_color_display(); + update_ok_button_state(); +} + +void ColorDecomposeDialog::update_card_styles() +{ + if (m_card_material_list) m_card_material_list->Refresh(); + if (m_card_cmyw) m_card_cmyw->Refresh(); + if (m_card_rybw) m_card_rybw->Refresh(); + + if (m_chk_material_list) + m_chk_material_list->SetValue(m_selected_mode == DecomposeMode::MaterialList); + if (m_chk_cmyw) + m_chk_cmyw->SetValue(m_selected_mode == DecomposeMode::CMYW); + if (m_chk_rybw) + m_chk_rybw->SetValue(m_selected_mode == DecomposeMode::RYBW); +} + +void ColorDecomposeDialog::update_card_visibility() +{ + // Count physical filaments of the same type (excluding the source filament) + int same_type_count = 0; + for (size_t i = 0; i < m_filament_types.size(); ++i) { + if (static_cast<int>(i) == m_filament_idx) + continue; + if (material_type_matches(m_filament_types[i], m_preferred_type)) + ++same_type_count; + } + + bool show_arb = (same_type_count >= 2); + bool show_cmyw = (m_preferred_type == kDecomposePlaBasicType); + bool show_rybw = (m_preferred_type == kDecomposePlaBasicType); + + if (m_arb_column_panel) m_arb_column_panel->Show(show_arb); + if (m_card_material_list) m_card_material_list->Show(show_arb); + if (m_card_cmyw) m_card_cmyw->Show(show_cmyw); + if (m_card_rybw) m_card_rybw->Show(show_rybw); + + bool any_visible = show_arb || show_cmyw || show_rybw; + if (m_no_card_hint) + m_no_card_hint->Show(!any_visible); + + // Auto-select a visible mode when current selection becomes hidden + if (any_visible) { + bool cur_visible = false; + if (m_selected_mode == DecomposeMode::MaterialList && show_arb) cur_visible = true; + if (m_selected_mode == DecomposeMode::CMYW && show_cmyw) cur_visible = true; + if (m_selected_mode == DecomposeMode::RYBW && show_rybw) cur_visible = true; + if (!cur_visible) { + if (show_arb) select_mode(DecomposeMode::MaterialList); + else if (show_cmyw) select_mode(DecomposeMode::CMYW); + else select_mode(DecomposeMode::RYBW); + } + } + + Layout(); + update_ok_button_state(); +} + +void ColorDecomposeDialog::update_filament_limit_warning() +{ + if (!m_limit_warning_panel || !m_limit_warning_text) + return; + + size_t missing_new = 0; + if (m_missing_calculator) { + missing_new = m_missing_calculator(m_result); + } else { + const size_t source_physical_idx = m_filament_idx >= 0 ? static_cast<size_t>(m_filament_idx) : size_t(-1); + const std::vector<size_t>* indices = + m_physical_config_indices.empty() ? nullptr : &m_physical_config_indices; + missing_new = count_decompose_new_physical_filaments( + m_result, m_physical_colors, m_filament_types, source_physical_idx, indices); + } + // A result with fewer than 2 components (e.g. target color is already a + // standard base color shown as "100%") creates no mixed filament and no new + // physical filament, so it can never exceed the limit. + const bool creates_mixed = m_result.components.size() >= 2; + // +1 for the mixed filament slot that will be created after decomposition. + const size_t needed = m_current_filament_count + missing_new + 1; + const bool blocked = creates_mixed && needed > m_max_filament_count; + + const bool was_shown = m_limit_warning_panel->IsShown(); + + if (!blocked) { + if (was_shown) { + m_limit_warning_panel->Hide(); + Layout(); + Fit(); + } + return; + } + + wxString mode_name; + switch (m_selected_mode) { + case DecomposeMode::CMYW: mode_name = "CMYW"; break; + case DecomposeMode::RYBW: mode_name = "RYBW"; break; + case DecomposeMode::MaterialList: mode_name = _L("Material List"); break; + } + + const wxString warning_text = format_wxstr( + _L("The material list supports at most %1% colors. After %2% decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing."), + m_max_filament_count, mode_name); + + // Show first so the panel is laid out and the text control gets its real + // width, then wrap to that width so the paragraph fills the content area. + m_limit_warning_panel->Show(); + Layout(); + const int avail = m_limit_warning_text->GetClientSize().x; + m_limit_warning_text->SetLabel(warning_text); + if (avail > FromDIP(50)) + m_limit_warning_text->Wrap(avail); + + Layout(); + // Only resize when the warning panel actually toggled from hidden to shown. + // While already visible, switching modes must not re-Fit the dialog, which + // would make it jump on every card switch. Fit keeps the user-moved position. + if (!was_shown) { + Fit(); + } +} + +void ColorDecomposeDialog::set_missing_physical_calculator(std::function<size_t(const ColorDecomposeResult&)> fn) +{ + m_missing_calculator = std::move(fn); + update_ok_button_state(); +} + +void ColorDecomposeDialog::update_ok_button_state() +{ + if (!m_btn_ok) return; + update_filament_limit_warning(); + bool any_card_visible = (m_card_material_list && m_card_material_list->IsShown()) + || (m_card_cmyw && m_card_cmyw->IsShown()) + || (m_card_rybw && m_card_rybw->IsShown()); + const bool blocked = m_limit_warning_panel && m_limit_warning_panel->IsShown(); + m_btn_ok->Enable(any_card_visible && !blocked); + Layout(); +} + +void ColorDecomposeDialog::update_mode_card_content(DecomposeMode mode) +{ + auto& controls = m_mode_cards[mode_index(mode)]; + auto* sizer = controls.components_sizer; + auto* card = controls.card; + if (!sizer || !card) + return; + + sizer->Clear(true); + const auto& components = m_mode_results[mode_index(mode)].components; + const size_t count = components.size(); + if (count == 0) { + card->Layout(); + card->Refresh(); + return; + } + + const int swatch_sz = FromDIP(24); + const int plus_gap = FromDIP(24); + const wxFont& ratio_font = Label::Body_13; + auto bind_select = [this, mode](wxWindow* w) { + w->Bind(wxEVT_LEFT_UP, [this, mode](wxMouseEvent&) { + select_mode(mode); + }); + w->SetCursor(wxCursor(wxCURSOR_HAND)); + }; + + for (size_t i = 0; i < count; ++i) { + auto* col = new wxBoxSizer(wxVERTICAL); + auto* swatch = create_color_swatch(card, components[i].colour, swatch_sz); + bind_select(swatch); + col->Add(swatch, 0, wxALIGN_CENTER_HORIZONTAL); + auto* ratio_text = new wxStaticText(card, wxID_ANY, wxString::Format("%d%%", components[i].ratio)); + ratio_text->SetFont(ratio_font); + ratio_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK)); + match_parent_bg(ratio_text, StateColor::darkModeColorFor(COLOR_BG_CARD)); + bind_select(ratio_text); + col->Add(ratio_text, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, FromDIP(4)); + sizer->Add(col, 0, wxALIGN_TOP); + + if (i + 1 < count) { + sizer->AddStretchSpacer(); + auto* plus_panel = new wxPanel(card, wxID_ANY, wxDefaultPosition, wxSize(plus_gap, swatch_sz)); + plus_panel->SetMinSize(wxSize(plus_gap, swatch_sz)); + plus_panel->SetMaxSize(wxSize(plus_gap, swatch_sz)); + plus_panel->SetBackgroundColour(StateColor::darkModeColorFor(COLOR_BG_CARD)); + auto* plus_sizer = new wxBoxSizer(wxVERTICAL); + auto* plus_label = new wxStaticText(plus_panel, wxID_ANY, "+"); + plus_label->SetFont(Label::Body_13); + plus_label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK)); + match_parent_bg(plus_label, StateColor::darkModeColorFor(COLOR_BG_CARD)); + bind_select(plus_panel); + bind_select(plus_label); + plus_sizer->AddStretchSpacer(); + plus_sizer->Add(plus_label, 0, wxALIGN_CENTER_HORIZONTAL); + plus_sizer->AddStretchSpacer(); + plus_panel->SetSizer(plus_sizer); + sizer->Add(plus_panel, 0, wxALIGN_TOP); + sizer->AddStretchSpacer(); + } + } + + const int card_width = FromDIP(128 + (count > 2 ? static_cast<int>(count - 2) * 31 : 0)); + card->SetMinSize(wxSize(card_width, FromDIP(111))); + card->SetMaxSize(wxSize(card_width, FromDIP(111))); + + card->Layout(); + card->Refresh(); +} + +void ColorDecomposeDialog::update_mode_card_contents() +{ + update_mode_card_content(DecomposeMode::MaterialList); + update_mode_card_content(DecomposeMode::CMYW); + update_mode_card_content(DecomposeMode::RYBW); + Layout(); + Fit(); +} + +void ColorDecomposeDialog::update_matched_color_display() +{ + if (!m_result.matched_color.IsOk()) + m_result.matched_color = m_target_color; + + if (m_matched_swatch) { + m_matched_swatch->SetBackgroundColour(m_result.matched_color); + m_matched_swatch->Refresh(); + } + if (m_matched_rgb_text) { + m_matched_rgb_text->SetLabel(wxString::Format("RGB: %d, %d, %d", + m_result.matched_color.Red(), m_result.matched_color.Green(), m_result.matched_color.Blue())); + } +} + +bool ColorDecomposeDialog::try_build_single_base_result(DecomposeMode mode, ColorDecomposeResult& out) const +{ + // Gate by preferred type, matching card visibility: CMYW and RYBW only for PLA Basic. + if (mode == DecomposeMode::CMYW || mode == DecomposeMode::RYBW) { + if (m_preferred_type != kDecomposePlaBasicType) + return false; + } else { + return false; + } + + static const DecomposeBaseColor cmyw_bases[] = { + DecomposeBaseColor::Cyan, DecomposeBaseColor::Magenta, + DecomposeBaseColor::Yellow, DecomposeBaseColor::White + }; + static const DecomposeBaseColor rybw_bases[] = { + DecomposeBaseColor::Red, DecomposeBaseColor::Yellow, + DecomposeBaseColor::Blue, DecomposeBaseColor::White + }; + const DecomposeBaseColor* bases = (mode == DecomposeMode::CMYW) ? cmyw_bases : rybw_bases; + const size_t base_count = (mode == DecomposeMode::CMYW) + ? sizeof(cmyw_bases) / sizeof(cmyw_bases[0]) + : sizeof(rybw_bases) / sizeof(rybw_bases[0]); + + const std::string target_hex = decompose_normalize_color_hex( + m_target_color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString()); + + for (size_t i = 0; i < base_count; ++i) { + const DecomposeBaseColor base = bases[i]; + DecomposeOfficialComponent official = + lookup_decompose_official_component(m_preferred_type, base, pure_color_for_base(base)); + if (decompose_normalize_color_hex(official.color_hex) != target_hex) + continue; + + out = ColorDecomposeResult{}; + out.mode = mode; + out.matched_color = hex_to_wx_colour(official.color_hex, m_target_color); + DecomposeComponent comp; + comp.colour = out.matched_color; + comp.ratio = 100; + comp.filament_index = -1; + comp.base_color = base; + out.components.push_back(comp); + return true; + } + return false; +} + +void ColorDecomposeDialog::compute_decomposition() +{ + auto fallback_result = [this](DecomposeMode mode, const std::vector<DecomposeComponent>& components) { + ColorDecomposeResult result; + result.mode = mode; + result.components = components; + int total = 0; + double r = 0.0, g = 0.0, b = 0.0; + for (const auto& comp : result.components) + total += comp.ratio; + if (total <= 0) + total = 100; + for (const auto& comp : result.components) { + const double w = static_cast<double>(comp.ratio) / total; + r += comp.colour.Red() * w; + g += comp.colour.Green() * w; + b += comp.colour.Blue() * w; + } + result.matched_color = result.components.empty() + ? m_target_color + : wxColour(static_cast<unsigned char>(std::clamp(r, 0.0, 255.0)), + static_cast<unsigned char>(std::clamp(g, 0.0, 255.0)), + static_cast<unsigned char>(std::clamp(b, 0.0, 255.0))); + return result; + }; + + std::vector<ColorDecomposePhysicalFilament> physical_filaments; + physical_filaments.reserve(m_physical_colors.size()); + for (size_t i = 0; i < m_physical_colors.size(); ++i) { + if (m_filament_idx >= 0 && i == static_cast<size_t>(m_filament_idx)) + continue; + ColorDecomposePhysicalFilament filament; + filament.color_hex = m_physical_colors[i]; + filament.name = i < m_filament_names.size() ? m_filament_names[i] : ""; + filament.type = i < m_filament_types.size() ? m_filament_types[i] : ""; + filament.filament_index = static_cast<unsigned int>(i + 1); + physical_filaments.push_back(std::move(filament)); + } + + const ColorDecomposeRgb target_rgb = wx_colour_to_recipe_rgb(m_target_color); + + auto material_recipe = recommend_from_physical_filaments(target_rgb, physical_filaments, m_preferred_type); + if (material_recipe.valid) { + m_mode_results[mode_index(DecomposeMode::MaterialList)] = + to_dialog_result(material_recipe, m_target_color); + } else { + std::vector<DecomposeComponent> components; + for (size_t i = 0; i < std::min<size_t>(2, physical_filaments.size()); ++i) { + DecomposeComponent comp; + comp.colour = wxColour(physical_filaments[i].color_hex); + comp.ratio = 50; + comp.filament_index = static_cast<int>(physical_filaments[i].filament_index); + components.push_back(comp); + } + if (components.empty()) { + components.push_back({m_target_color, 100, -1}); + } else if (components.size() == 1) { + components.front().ratio = 100; + } + m_mode_results[mode_index(DecomposeMode::MaterialList)] = + fallback_result(DecomposeMode::MaterialList, components); + } + + ColorDecomposeResult single_base; + if (try_build_single_base_result(DecomposeMode::CMYW, single_base)) { + m_mode_results[mode_index(DecomposeMode::CMYW)] = single_base; + } else { + auto cmyw_recipe = lookup_standard_recipe(target_rgb, ColorDecomposeRecipeMode::CMYW, m_preferred_type); + m_mode_results[mode_index(DecomposeMode::CMYW)] = cmyw_recipe.valid + ? to_dialog_result(cmyw_recipe, m_target_color) + : fallback_result(DecomposeMode::CMYW, { + {CMYW_YELLOW, 50, -1, DecomposeBaseColor::Yellow}, + {CMYW_CYAN, 50, -1, DecomposeBaseColor::Cyan} + }); + } + + if (try_build_single_base_result(DecomposeMode::RYBW, single_base)) { + m_mode_results[mode_index(DecomposeMode::RYBW)] = single_base; + } else { + auto rybw_recipe = lookup_standard_recipe(target_rgb, ColorDecomposeRecipeMode::RYBW, m_preferred_type); + m_mode_results[mode_index(DecomposeMode::RYBW)] = rybw_recipe.valid + ? to_dialog_result(rybw_recipe, m_target_color) + : fallback_result(DecomposeMode::RYBW, { + {RYBW_YELLOW, 50, -1, DecomposeBaseColor::Yellow}, + {RYBW_BLUE, 50, -1, DecomposeBaseColor::Blue} + }); + } + + m_result = m_mode_results[mode_index(m_selected_mode)]; + update_mode_card_contents(); + update_ok_button_state(); +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/ColorDecomposeDialog.hpp b/src/slic3r/GUI/ColorDecomposeDialog.hpp new file mode 100644 index 0000000000..419dfe8cea --- /dev/null +++ b/src/slic3r/GUI/ColorDecomposeDialog.hpp @@ -0,0 +1,152 @@ +#ifndef slic3r_ColorDecomposeDialog_hpp_ +#define slic3r_ColorDecomposeDialog_hpp_ + +#include <array> +#include <functional> +#include <string> +#include <vector> +#include <utility> +#include <wx/colour.h> +#include <wx/panel.h> +#include <wx/statbmp.h> +#include <wx/stattext.h> + +#include "GUI_Utils.hpp" +#include "libslic3r/ColorDecomposeRecipe.hpp" + +class Button; +class CheckBox; +class ComboBox; + +namespace Slic3r { +namespace GUI { + +using DecomposeMode = ColorDecomposeRecipeMode; + +enum class DecomposeBaseColor { + None, + Cyan, + Magenta, + Yellow, + White, + Red, + Green, + Blue +}; + +struct DecomposeComponent { + wxColour colour; + int ratio{50}; // percentage + int filament_index{-1}; // 1-based physical filament index, -1 if standard base color + DecomposeBaseColor base_color{DecomposeBaseColor::None}; +}; + +struct ColorDecomposeResult { + DecomposeMode mode{DecomposeMode::MaterialList}; + wxColour matched_color; + std::vector<DecomposeComponent> components; +}; + +class ColorDecomposeDialog : public DPIDialog +{ +public: + ColorDecomposeDialog(wxWindow* parent, + int filament_idx, + const wxColour& target_color, + const std::vector<std::string>& physical_colors, + const std::vector<std::string>& filament_names, + const std::vector<std::string>& filament_types, + size_t current_filament_count = 0, + size_t max_filament_count = 32, + std::vector<size_t> physical_config_indices = {}); + + ColorDecomposeResult get_result() const { return m_result; } + + // Override the "new physical filaments" count used by the filament-limit + // warning. The Texture import path supplies its own calculator so the + // pre-check shares the exact reuse rule as its write-back (existing + + // virtual physical filaments), instead of the project-config based default + // that cannot see not-yet-committed virtual base colors. + void set_missing_physical_calculator(std::function<size_t(const ColorDecomposeResult&)> fn); + +protected: + void on_dpi_changed(const wxRect& suggested_rect) override; + +private: + void build_ui(); + wxBoxSizer* create_filament_selector(); + wxBoxSizer* create_target_color_section(); + wxBoxSizer* create_mode_selection_section(); + wxPanel* create_mode_card(wxWindow* parent, DecomposeMode mode, const wxString& title); + wxBoxSizer* create_button_panel(); + + void select_mode(DecomposeMode mode); + void update_card_styles(); + void update_card_visibility(); + void update_mode_card_content(DecomposeMode mode); + void update_mode_card_contents(); + void update_matched_color_display(); + void update_ok_button_state(); + void update_filament_limit_warning(); + + void compute_decomposition(); + + // When the target color is exactly one of the standard base colors for the + // preferred type, the standard card should show that base at 100% instead of + // a mix. PLA Basic covers CMYW and RYBW. + bool try_build_single_base_result(DecomposeMode mode, ColorDecomposeResult& out) const; + + struct ModeCardControls { + wxPanel* card{nullptr}; + wxBoxSizer* components_sizer{nullptr}; + }; + + ColorDecomposeResult m_result; + std::array<ColorDecomposeResult, 3> m_mode_results; + std::array<ModeCardControls, 3> m_mode_cards; + int m_filament_idx{-1}; + wxColour m_target_color; + std::vector<std::string> m_physical_colors; + std::vector<std::string> m_filament_names; + std::vector<std::string> m_filament_types; + std::vector<std::string> m_project_types; + std::string m_preferred_type; + // Dropdown selectable item index -> material type string + std::vector<std::string> m_combo_item_types; + size_t m_current_filament_count{0}; + size_t m_max_filament_count{32}; + std::vector<size_t> m_physical_config_indices; + std::function<size_t(const ColorDecomposeResult&)> m_missing_calculator; + + // UI controls + ComboBox* m_type_combo{nullptr}; + wxPanel* m_target_swatch{nullptr}; + wxStaticText* m_target_rgb_text{nullptr}; + wxPanel* m_matched_swatch{nullptr}; + wxStaticText* m_matched_rgb_text{nullptr}; + + // Mode cards + wxPanel* m_card_material_list{nullptr}; + wxPanel* m_card_cmyw{nullptr}; + wxPanel* m_card_rybw{nullptr}; + wxPanel* m_arb_column_panel{nullptr}; + CheckBox* m_chk_material_list{nullptr}; + CheckBox* m_chk_cmyw{nullptr}; + CheckBox* m_chk_rybw{nullptr}; + DecomposeMode m_selected_mode{DecomposeMode::MaterialList}; + + // Hint shown when no mode card is visible + wxStaticText* m_no_card_hint{nullptr}; + + // Warning shown when decomposition would exceed filament limit + wxPanel* m_limit_warning_panel{nullptr}; + wxStaticText* m_limit_warning_text{nullptr}; + + Button* m_btn_ok{nullptr}; + Button* m_btn_cancel{nullptr}; +}; + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_ColorDecomposeDialog_hpp_ diff --git a/src/slic3r/GUI/ColorDecomposeSupport.cpp b/src/slic3r/GUI/ColorDecomposeSupport.cpp new file mode 100644 index 0000000000..e8fb4c9082 --- /dev/null +++ b/src/slic3r/GUI/ColorDecomposeSupport.cpp @@ -0,0 +1,386 @@ +#include "ColorDecomposeSupport.hpp" +#include "MixedFilamentDialog.hpp" +#include "GUI_App.hpp" +#include "MsgDialog.hpp" +#include "I18N.hpp" +#include "libslic3r/Preset.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Utils.hpp" + +#include "nlohmann/json.hpp" + +#include <fstream> +#include <algorithm> +#include <cctype> + +using json = nlohmann::json; + +namespace Slic3r { namespace GUI { + +std::string decompose_normalize_color_hex(std::string color) +{ + if (color.size() >= 7) + color = color.substr(0, 7); + std::transform(color.begin(), color.end(), color.begin(), [](unsigned char c) { + return static_cast<char>(std::toupper(c)); + }); + return color; +} + +const char* decompose_base_color_en(DecomposeBaseColor color) +{ + switch (color) { + case DecomposeBaseColor::Cyan: return "Cyan"; + case DecomposeBaseColor::Magenta: return "Magenta"; + case DecomposeBaseColor::Yellow: return "Yellow"; + case DecomposeBaseColor::White: return "White"; + case DecomposeBaseColor::Red: return "Red"; + case DecomposeBaseColor::Green: return "Green"; + case DecomposeBaseColor::Blue: return "Blue"; + default: return ""; + } +} + +wxString decompose_base_color_display(DecomposeBaseColor color) +{ + switch (color) { + case DecomposeBaseColor::Cyan: return _L("Cyan"); + case DecomposeBaseColor::Magenta: return _L("Magenta"); + case DecomposeBaseColor::Yellow: return _L("Yellow"); + case DecomposeBaseColor::White: return _L("White"); + case DecomposeBaseColor::Red: return _L("Red"); + case DecomposeBaseColor::Green: return _L("Green"); + case DecomposeBaseColor::Blue: return _L("Blue"); + default: return wxString(); + } +} + +std::string decompose_basic_type_from_source(size_t source_config_idx, + size_t source_physical_idx, + const std::vector<std::string>& physical_types) +{ + auto& project_config = wxGetApp().preset_bundle->project_config; + if (auto* filament_id_opt = project_config.option<ConfigOptionStrings>("filament_id")) { + if (source_config_idx < filament_id_opt->values.size()) { + const std::string& filament_id = filament_id_opt->values[source_config_idx]; + if (filament_id == kDecomposePetgFilamentId) + return kDecomposePetgBasicType; + if (filament_id == kDecomposePlaFilamentId) + return kDecomposePlaBasicType; + } + } + + if (source_physical_idx < physical_types.size()) { + const std::string& type = physical_types[source_physical_idx]; + if (type == kDecomposePetgShortType || type == kDecomposePetgBasicType) + return kDecomposePetgBasicType; + if (type == kDecomposePlaShortType || type == kDecomposePlaBasicType) + return kDecomposePlaBasicType; + } + return kDecomposePlaBasicType; +} + +std::string decompose_basic_filament_id(const std::string& basic_type) +{ + if (basic_type == kDecomposePetgBasicType) + return kDecomposePetgFilamentId; + return kDecomposePlaFilamentId; +} + +void set_created_standard_component_metadata(size_t config_idx, const DecomposeOfficialComponent& component) +{ + auto& project_config = wxGetApp().preset_bundle->project_config; + if (!component.filament_id.empty()) { + if (auto* filament_id_opt = project_config.option<ConfigOptionStrings>("filament_id")) { + while (filament_id_opt->values.size() <= config_idx) + filament_id_opt->values.push_back(""); + filament_id_opt->values[config_idx] = component.filament_id; + } + } + + const std::string type = component.filament_id == kDecomposePetgFilamentId ? kDecomposePetgShortType : + component.filament_id == kDecomposePlaFilamentId ? kDecomposePlaShortType : ""; + if (!type.empty()) { + if (auto* type_opt = project_config.option<ConfigOptionStrings>("filament_type")) { + while (type_opt->values.size() <= config_idx) + type_opt->values.push_back(""); + type_opt->values[config_idx] = type; + } + } +} + +DecomposeOfficialComponent lookup_decompose_official_component( + const std::string& basic_type, + DecomposeBaseColor base_color, + const wxColour& fallback) +{ + DecomposeOfficialComponent result; + result.base_color = base_color; + result.color_hex = decompose_normalize_color_hex(fallback.GetAsString(wxC2S_HTML_SYNTAX).ToStdString()); + result.filament_id = decompose_basic_filament_id(basic_type); + + const char* color_name = decompose_base_color_en(base_color); + if (color_name[0] == '\0') + return result; + + // Some materials name a standard base color differently in the color-code + // table. PETG Basic's RYBW blue base is "Reflex Blue" (deep blue, B00, + // #001489), not "Blue". Match by an ordered list of exact English names so + // "Navy Blue" (B01, #0086D6) is never picked up by mistake. + std::vector<std::string> candidate_names; + candidate_names.emplace_back(color_name); + if (base_color == DecomposeBaseColor::Blue && basic_type == kDecomposePetgBasicType) + candidate_names.emplace_back("Reflex Blue"); + + std::ifstream ifs(resources_dir() + "/profiles/BBL/filament/filaments_color_codes.json"); + if (!ifs) + return result; + + json root = json::parse(ifs, nullptr, false); + if (root.is_discarded() || !root.contains("data") || !root["data"].is_array()) + return result; + + for (const std::string& candidate : candidate_names) { + for (const auto& item : root["data"]) { + if (!item.is_object() || item.value("fila_type", "") != basic_type) + continue; + if (!item.contains("fila_color_name")) + continue; + const auto& names = item["fila_color_name"]; + if (!names.is_object() || names.value("en", "") != candidate) + continue; + if (item.contains("fila_color") && item["fila_color"].is_array() && !item["fila_color"].empty()) + result.color_hex = decompose_normalize_color_hex(item["fila_color"][0].get<std::string>()); + result.filament_id = item.value("fila_id", result.filament_id); + return result; + } + } + return result; +} + +std::string find_decompose_standard_preset_name(size_t source_config_idx, const std::string& basic_type) +{ + const PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + if (source_config_idx < preset_bundle.filament_presets.size()) { + const std::string& source_name = preset_bundle.filament_presets[source_config_idx]; + if (source_name.find(std::string(kDecomposeBambuPresetPrefix) + basic_type) != std::string::npos) + return source_name; + } + + const std::string prefix = std::string(kDecomposeBambuPresetPrefix) + basic_type + " @BBL "; + for (const std::string& preset_name : preset_bundle.filament_presets) { + if (preset_name.find(prefix) == 0) + return preset_name; + } + + return {}; +} + +std::string official_basic_type_from_preset_name(const std::string& preset_name) +{ + if (preset_name.find(std::string(kDecomposeBambuPresetPrefix) + kDecomposePlaBasicType) != std::string::npos) + return kDecomposePlaBasicType; + if (preset_name.find(std::string(kDecomposeBambuPresetPrefix) + kDecomposePetgBasicType) != std::string::npos) + return kDecomposePetgBasicType; + return {}; +} + +std::string filament_type_for_color_decompose(Preset* preset) +{ + if (!preset) + return kDecomposePlaShortType; + + std::string display_type; + std::string ft = preset->config.get_filament_type(display_type); + const std::string basic = official_basic_type_from_preset_name(preset->name); + if (!basic.empty()) + ft = basic; + if (ft.empty()) + ft = kDecomposePlaShortType; + return ft; +} + +int find_existing_decompose_component( + const DecomposeOfficialComponent& component, + const std::vector<std::string>& physical_colors, + const std::vector<size_t>& physical_config_indices, + size_t source_config_idx) +{ + auto& project_config = wxGetApp().preset_bundle->project_config; + auto* filament_id_opt = project_config.option<ConfigOptionStrings>("filament_id"); + auto* type_opt = project_config.option<ConfigOptionStrings>("filament_type"); + const PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + const size_t num_physical = physical_colors.size(); + const std::string expected_basic_type = component.filament_id == kDecomposePetgFilamentId ? kDecomposePetgBasicType : + component.filament_id == kDecomposePlaFilamentId ? kDecomposePlaBasicType : ""; + const std::string expected_short_type = expected_basic_type == kDecomposePetgBasicType ? kDecomposePetgShortType : + expected_basic_type == kDecomposePlaBasicType ? kDecomposePlaShortType : ""; + const std::string expected_preset_part = expected_basic_type.empty() ? "" : std::string(kDecomposeBambuPresetPrefix) + expected_basic_type; + for (size_t i = 0; i < num_physical && i < physical_config_indices.size(); ++i) { + const size_t config_idx = physical_config_indices[i]; + const std::string slot_color = decompose_normalize_color_hex(physical_colors[i]); + const std::string slot_filament_id = (filament_id_opt && config_idx < filament_id_opt->values.size()) ? filament_id_opt->values[config_idx] : ""; + const std::string slot_type = (type_opt && config_idx < type_opt->values.size()) ? type_opt->values[config_idx] : ""; + const std::string preset_name = config_idx < preset_bundle.filament_presets.size() ? preset_bundle.filament_presets[config_idx] : ""; + if (config_idx == source_config_idx) { + continue; + } + if (slot_color != component.color_hex) { + continue; + } + + if (!component.filament_id.empty() && slot_filament_id == component.filament_id) { + return static_cast<int>(config_idx + 1); + } + + if (!expected_basic_type.empty() && (slot_type == expected_basic_type || slot_type == expected_short_type)) { + return static_cast<int>(config_idx + 1); + } + + if (!expected_preset_part.empty() && preset_name.find(expected_preset_part) != std::string::npos) { + return static_cast<int>(config_idx + 1); + } + + const bool has_material_hint = !slot_filament_id.empty() || !slot_type.empty() || !preset_name.empty(); + if (!expected_basic_type.empty() && has_material_hint) + continue; + + return static_cast<int>(config_idx + 1); + } + return -1; +} + +bool prepare_decompose_mixed_result( + const ColorDecomposeResult& result, + size_t source_config_idx, + size_t source_physical_idx, + const std::vector<std::string>& physical_colors, + const std::vector<std::string>& physical_types, + const std::vector<size_t>& physical_config_indices, + MixedFilamentResult& out_result, + std::vector<DecomposeMissingComponent>& missing) +{ + out_result = {}; + missing.clear(); + if (result.components.size() < 2) { + return false; + } + + const bool standard_mode = result.mode == DecomposeMode::CMYW || result.mode == DecomposeMode::RYBW; + std::string basic_type; + std::string preset_name; + if (standard_mode) { + basic_type = decompose_basic_type_from_source(source_config_idx, source_physical_idx, physical_types); + preset_name = find_decompose_standard_preset_name(source_config_idx, basic_type); + } + + for (size_t i = 0; i < result.components.size(); ++i) { + const DecomposeComponent& comp = result.components[i]; + out_result.ratios.push_back(comp.ratio); + if (!standard_mode) { + if (comp.filament_index <= 0) { + return false; + } + const size_t physical_idx = static_cast<size_t>(comp.filament_index - 1); + if (physical_idx >= physical_config_indices.size()) { + return false; + } + out_result.components.push_back(static_cast<unsigned int>(physical_config_indices[physical_idx] + 1)); + continue; + } + + if (comp.base_color == DecomposeBaseColor::None) { + return false; + } + DecomposeOfficialComponent official_component = + lookup_decompose_official_component(basic_type, comp.base_color, comp.colour); + int existing_idx = find_existing_decompose_component(official_component, physical_colors, + physical_config_indices, source_config_idx); + if (existing_idx > 0) { + out_result.components.push_back(static_cast<unsigned int>(existing_idx)); + continue; + } + + DecomposeMissingComponent missing_comp; + missing_comp.component_idx = out_result.components.size(); + missing_comp.official_component = official_component; + missing_comp.preset_name = preset_name; + missing_comp.display_name = decompose_base_color_display(comp.base_color) + + wxString::FromUTF8(" ") + wxString::FromUTF8(basic_type); + missing.push_back(std::move(missing_comp)); + out_result.components.push_back(0); + } + + const bool ok = out_result.components.size() == out_result.ratios.size() && out_result.components.size() >= 2; + return ok; +} + +size_t count_decompose_new_physical_filaments( + const ColorDecomposeResult& result, + const std::vector<std::string>& physical_colors, + const std::vector<std::string>& physical_types, + size_t source_physical_idx, + const std::vector<size_t>* physical_config_indices) +{ + if (result.mode != DecomposeMode::CMYW && result.mode != DecomposeMode::RYBW) + return 0; + + std::vector<size_t> fallback_indices; + const std::vector<size_t>* indices = physical_config_indices; + if (!indices) { + fallback_indices.resize(physical_colors.size()); + for (size_t i = 0; i < fallback_indices.size(); ++i) + fallback_indices[i] = i; + indices = &fallback_indices; + } + + size_t source_config_idx = size_t(-1); + if (source_physical_idx < indices->size()) + source_config_idx = (*indices)[source_physical_idx]; + + const std::string basic_type = + decompose_basic_type_from_source(source_config_idx, source_physical_idx, physical_types); + + size_t missing_count = 0; + for (const DecomposeComponent& comp : result.components) { + if (comp.base_color == DecomposeBaseColor::None) + continue; + DecomposeOfficialComponent official_component = + lookup_decompose_official_component(basic_type, comp.base_color, comp.colour); + int existing_idx = find_existing_decompose_component(official_component, physical_colors, + *indices, source_config_idx); + if (existing_idx <= 0) + ++missing_count; + } + return missing_count; +} + +bool confirm_create_decompose_missing_components(wxWindow* parent, const std::vector<DecomposeMissingComponent>& missing) +{ + if (missing.empty()) + return true; + + static const char* config_key = "not_show_color_decompose_missing_component_tip"; + if (wxGetApp().app_config->get(config_key) == "1") { + return true; + } + + wxString missing_text; + for (size_t i = 0; i < missing.size(); ++i) { + if (i > 0) + missing_text += _L(", "); + missing_text += missing[i].display_name; + } + + wxString message = _L("The current filament list does not contain ") + missing_text + + _L(". A project filament required by the mixed filament will be created automatically after decomposition."); + + MessageDialog dlg(parent, message, _L("Tip"), wxOK | wxCANCEL | wxICON_INFORMATION); + dlg.show_dsa_button(); + int res = dlg.ShowModal(); + if (res == wxID_OK && dlg.get_checkbox_state()) + wxGetApp().app_config->set(config_key, "1"); + return res == wxID_OK; +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/ColorDecomposeSupport.hpp b/src/slic3r/GUI/ColorDecomposeSupport.hpp new file mode 100644 index 0000000000..a982c51303 --- /dev/null +++ b/src/slic3r/GUI/ColorDecomposeSupport.hpp @@ -0,0 +1,104 @@ +#ifndef slic3r_GUI_ColorDecomposeSupport_hpp_ +#define slic3r_GUI_ColorDecomposeSupport_hpp_ + +#include <string> +#include <vector> +#include <wx/string.h> +#include <wx/colour.h> +#include "ColorDecomposeDialog.hpp" + +class wxWindow; + +namespace Slic3r { +class Preset; +namespace GUI { + +// ---- Constants ---- + +inline constexpr const char* kDecomposePlaBasicType = "PLA Basic"; +inline constexpr const char* kDecomposePetgBasicType = "PETG Basic"; +inline constexpr const char* kDecomposePlaShortType = "PLA"; +inline constexpr const char* kDecomposePetgShortType = "PETG"; +inline constexpr const char* kDecomposePlaFilamentId = "GFA00"; +inline constexpr const char* kDecomposePetgFilamentId = "GFG00"; +inline constexpr const char* kDecomposeBambuPresetPrefix = "Bambu "; + +// ---- Types ---- + +struct DecomposeOfficialComponent { + DecomposeBaseColor base_color{DecomposeBaseColor::None}; + std::string color_hex; + std::string filament_id; +}; + +struct DecomposeMissingComponent { + size_t component_idx{0}; + DecomposeOfficialComponent official_component; + std::string preset_name; + wxString display_name; +}; + +struct MixedFilamentResult; + +// ---- Functions ---- + +std::string decompose_normalize_color_hex(std::string color); + +const char* decompose_base_color_en(DecomposeBaseColor color); + +wxString decompose_base_color_display(DecomposeBaseColor color); + +std::string decompose_basic_type_from_source(size_t source_config_idx, + size_t source_physical_idx, + const std::vector<std::string>& physical_types); + +std::string decompose_basic_filament_id(const std::string& basic_type); + +void set_created_standard_component_metadata(size_t config_idx, const DecomposeOfficialComponent& component); + +DecomposeOfficialComponent lookup_decompose_official_component( + const std::string& basic_type, + DecomposeBaseColor base_color, + const wxColour& fallback); + +std::string find_decompose_standard_preset_name(size_t source_config_idx, const std::string& basic_type); + +// Returns "PLA Basic" / "PETG Basic" when preset_name names an official Bambu +// basic filament, else an empty string. +std::string official_basic_type_from_preset_name(const std::string& preset_name); + +// Resolve display type for color-decompose: official Bambu Basic overrides +// get_filament_type when preset name matches; empty/missing -> "PLA". +std::string filament_type_for_color_decompose(Preset* preset); + +int find_existing_decompose_component( + const DecomposeOfficialComponent& component, + const std::vector<std::string>& physical_colors, + const std::vector<size_t>& physical_config_indices, + size_t source_config_idx); + +bool prepare_decompose_mixed_result( + const ColorDecomposeResult& result, + size_t source_config_idx, + size_t source_physical_idx, + const std::vector<std::string>& physical_colors, + const std::vector<std::string>& physical_types, + const std::vector<size_t>& physical_config_indices, + MixedFilamentResult& out_result, + std::vector<DecomposeMissingComponent>& missing); + +// For standard modes: how many base colors are not reusable from physical list. +// MaterialList returns 0. When physical_config_indices is null, indices are 0..n-1. +size_t count_decompose_new_physical_filaments( + const ColorDecomposeResult& result, + const std::vector<std::string>& physical_colors, + const std::vector<std::string>& physical_types, + size_t source_physical_idx, + const std::vector<size_t>* physical_config_indices); + +bool confirm_create_decompose_missing_components(wxWindow* parent, + const std::vector<DecomposeMissingComponent>& missing); + +}} // namespace Slic3r::GUI + +#endif // slic3r_GUI_ColorDecomposeSupport_hpp_ diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index e46faa803a..09b7fcfad4 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -12,6 +12,7 @@ #include "libslic3r/GCode/AdaptivePAProcessor.hpp" #include "Plater.hpp" +#include <algorithm> #include <sstream> #include <wx/msgdlg.h> @@ -250,6 +251,59 @@ void ConfigManipulation::check_chamber_minimal_temperature(DynamicPrintConfig* c } } +void ConfigManipulation::layer_height_limits(double& min_layer_height, double& max_layer_height) const +{ + const DynamicPrintConfig& printer_config = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config; + const std::vector<double>& min_limits = printer_config.option<ConfigOptionFloats>("min_layer_height")->values; + const std::vector<double>& max_limits = printer_config.option<ConfigOptionFloats>("max_layer_height")->values; + min_layer_height = *std::min_element(min_limits.begin(), min_limits.end()); + max_layer_height = *std::max_element(max_limits.begin(), max_limits.end()); +} + +bool ConfigManipulation::check_layer_height(DynamicPrintConfig* config) +{ + double min_layer_height = 0., max_layer_height = 0.; + layer_height_limits(min_layer_height, max_layer_height); + const double layer_height = config->opt_float("layer_height"); + + if (min_layer_height > EPSILON && layer_height < EPSILON) { + const wxString msg_text = wxString::Format(_L("Layer height is too small. It will be set to the minimum (%g mm)."), min_layer_height); + MessageDialog dialog(wxGetApp().plater(), msg_text, "", wxICON_WARNING | wxOK); + dialog.SetButtonLabel(wxID_OK, _L("OK")); + is_msg_dlg_already_exist = true; + dialog.ShowModal(); + is_msg_dlg_already_exist = false; + DynamicPrintConfig new_conf = *config; + new_conf.set_key_value("layer_height", new ConfigOptionFloat(min_layer_height)); + apply(config, &new_conf); + return true; + } + if (max_layer_height > EPSILON && layer_height > max_layer_height + EPSILON) + return layer_height_out_of_range_dialog(config, max_layer_height); + if (min_layer_height > EPSILON && layer_height < min_layer_height - EPSILON) + return layer_height_out_of_range_dialog(config, min_layer_height); + return false; +} + +bool ConfigManipulation::layer_height_out_of_range_dialog(DynamicPrintConfig* config, double clamp_to) +{ + wxString msg_text = _(L("Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, " + "this may cause printing quality issues.")); + msg_text += "\n\n" + wxString::Format(_L("Adjust it to the limit (%g mm) automatically?"), clamp_to); + MessageDialog dialog(wxGetApp().plater(), msg_text, "", wxICON_WARNING | wxYES | wxNO); + dialog.SetButtonLabel(wxID_YES, _L("Adjust")); + dialog.SetButtonLabel(wxID_NO, _L("Ignore")); + is_msg_dlg_already_exist = true; + const bool adjust = dialog.ShowModal() == wxID_YES; + if (adjust) { + DynamicPrintConfig new_conf = *config; + new_conf.set_key_value("layer_height", new ConfigOptionFloat(clamp_to)); + apply(config, &new_conf); + } + is_msg_dlg_already_exist = false; + return adjust; +} + void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, const bool is_global_config, const bool is_plate_config) { // #ys_FIXME_to_delete @@ -264,7 +318,6 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con // layer_height shouldn't be equal to zero auto layer_height = config->opt_float("layer_height"); - auto gpreset = GUI::wxGetApp().preset_bundle->printers.get_edited_preset(); if (layer_height < EPSILON) { const wxString msg_text = _(L("Layer height too small\nIt has been reset to 0.2")); @@ -277,20 +330,6 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con is_msg_dlg_already_exist = false; } - //BBS: limite the max layer_herght - auto max_lh = gpreset.config.opt_float("max_layer_height",0); - if (max_lh > 0.2 && layer_height > max_lh+ EPSILON) - { - const wxString msg_text = wxString::Format(L"Too large layer height.\nReset to %0.3f.", max_lh); - MessageDialog dialog(nullptr, msg_text, "", wxICON_WARNING | wxOK); - DynamicPrintConfig new_conf = *config; - is_msg_dlg_already_exist = true; - dialog.ShowModal(); - new_conf.set_key_value("layer_height", new ConfigOptionFloat(max_lh)); - apply(config, &new_conf); - is_msg_dlg_already_exist = false; - } - //BBS: ironing_spacing shouldn't be too small or equal to zero if (config->opt_float("ironing_spacing") < 0.05) { @@ -414,7 +453,7 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con if (config->opt_bool("alternate_extra_wall") && (config->opt_enum<EnsureVerticalShellThickness>("ensure_vertical_shell_thickness") == evstAll)) { - wxString msg_text = _(L("Alternate extra wall does't work well when ensure vertical shell thickness is set to All.")); + wxString msg_text = _(L("Alternate extra wall doesn't work well when ensure vertical shell thickness is set to All.")); if (is_global_config) msg_text += "\n\n" + _(L("Change these settings automatically?\n" @@ -538,27 +577,72 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con } // BBS - static const char* keys[] = { "support_filament", "support_interface_filament"}; - for (int i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) { - std::string key = std::string(keys[i]); + // Reset filament overrides pointing at a slot that no longer exists. Support and the wipe + // tower additionally reject mixed slots: the engine consumes those keys directly, so a virtual + // slot would reach the G-code unresolved, while the per-feature keys are resolved per layer. + static const char* physical_only_keys[] = { "support_filament", "support_interface_filament", "wipe_tower_filament" }; + static const char* feature_keys[] = { "outer_wall_filament_id", "inner_wall_filament_id", + "sparse_infill_filament_id", "internal_solid_filament_id", + "top_surface_filament_id", "bottom_surface_filament_id" }; + auto reset_invalid_filament = [this, config, filament_cnt](const char* key, bool allow_mixed) { auto* opt = dynamic_cast<ConfigOptionInt*>(config->option(key, false)); - if (opt != nullptr) { - if (opt->getInt() > filament_cnt) { - DynamicPrintConfig new_conf = *config; - const DynamicPrintConfig *conf_temp = wxGetApp().plater()->config(); - int new_value = 0; - if (conf_temp != nullptr && conf_temp->has(key)) { - new_value = conf_temp->opt_int(key); + if (opt == nullptr) + return; + const int val = opt->getInt(); + const bool out_of_range = val > filament_cnt; + const bool is_mixed = !allow_mixed && val > 0 && val <= filament_cnt && + wxGetApp().preset_bundle->is_mixed_filament(val - 1); + if (!out_of_range && !is_mixed) + return; + DynamicPrintConfig new_conf = *config; + int new_value = 0; + if (out_of_range) { + const DynamicPrintConfig *conf_temp = wxGetApp().plater()->config(); + if (conf_temp != nullptr && conf_temp->has(key)) + new_value = conf_temp->opt_int(key); + } + new_conf.set_key_value(key, new ConfigOptionInt(new_value)); + apply(config, &new_conf); + }; + for (const char* key : physical_only_keys) + reset_invalid_filament(key, false); + for (const char* key : feature_keys) + reset_invalid_filament(key, true); + + // Sub-layer splitting divides each layer by the mix ratio; an adaptive layer profile makes + // those sub-layer heights vary per layer, which degrades the blend. Warn once per enable. + { + static bool s_mixed_sublayer_warned = false; + bool sublayer_on = config->opt_bool("enable_mixed_color_sublayer"); + if (sublayer_on && !s_mixed_sublayer_warned && + wxGetApp().app_config->get("no_warn_mixed_sublayer_variable_layer") != "1") { + bool has_variable_layer = false; + for (const auto* obj : wxGetApp().model().objects) { + if (obj->layer_height_profile.get().size() > 4) { + has_variable_layer = true; + break; } - new_conf.set_key_value(key, new ConfigOptionInt(new_value)); - apply(config, &new_conf); + } + if (has_variable_layer) { + MessageDialog dialog(m_msg_dlg_parent, + _L("Using variable layer height together with mixed color sublayer may result in poor color mixing quality."), + "", wxICON_WARNING | wxOK); + dialog.show_dsa_button(); + is_msg_dlg_already_exist = true; + dialog.ShowModal(); + is_msg_dlg_already_exist = false; + if (dialog.get_checkbox_state()) + wxGetApp().app_config->set("no_warn_mixed_sublayer_variable_layer", "1"); + s_mixed_sublayer_warned = true; } } + if (!sublayer_on) + s_mixed_sublayer_warned = false; } if (config->opt_enum<SeamScarfType>("seam_slope_type") != SeamScarfType::None && config->get_abs_value("seam_slope_start_height") >= layer_height) { - const wxString msg_text = _(L("seam_slope_start_height need to be smaller than layer_height.\nReset to 0.")); + const wxString msg_text = _(L("seam_slope_start_height needs to be smaller than layer_height.\nReset to 0.")); MessageDialog dialog(m_msg_dlg_parent, msg_text, "", wxICON_WARNING | wxOK); DynamicPrintConfig new_conf = *config; is_msg_dlg_already_exist = true; @@ -572,7 +656,7 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con float skin_depth = config->opt_float("skin_infill_depth"); if (config->opt_float("infill_lock_depth") > skin_depth) { // xgettext:no-c-format, no-boost-format - const wxString msg_text = _(L("Lock depth should smaller than skin depth.\nReset to 50% of skin depth.")); + const wxString msg_text = _(L("Lock depth should be smaller than skin depth.\nReset to 50% of skin depth.")); MessageDialog dialog(m_msg_dlg_parent, msg_text, "", wxICON_WARNING | wxOK); DynamicPrintConfig new_conf = *config; is_msg_dlg_already_exist = true; @@ -713,7 +797,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in bool has_top_shell = has_top_shell_layers && config->option<ConfigOptionPercent>("top_surface_density")->value > 0; bool has_bottom_shell = config->opt_int("bottom_shell_layers") > 0; bool has_solid_infill = has_top_shell_layers || has_bottom_shell; - toggle_line("sparse_infill_smooth_factor", pattern == ipHilbertCurve); + toggle_line("sparse_infill_smooth_factor", is_smoothable_infill_pattern(pattern, config->opt_int("fill_multiline"))); toggle_field("top_surface_pattern", has_top_shell); toggle_field("bottom_surface_pattern", has_bottom_shell); toggle_field("top_surface_density", has_top_shell_layers); diff --git a/src/slic3r/GUI/ConfigManipulation.hpp b/src/slic3r/GUI/ConfigManipulation.hpp index d191ef2c4f..ac53ffb4bb 100644 --- a/src/slic3r/GUI/ConfigManipulation.hpp +++ b/src/slic3r/GUI/ConfigManipulation.hpp @@ -86,6 +86,9 @@ public: void check_filament_max_volumetric_speed(DynamicPrintConfig *config); void check_chamber_temperature(DynamicPrintConfig* config); void check_chamber_minimal_temperature(DynamicPrintConfig* config); + bool check_layer_height(DynamicPrintConfig* config); + bool layer_height_out_of_range_dialog(DynamicPrintConfig* config, double clamp_to); + void layer_height_limits(double& min_layer_height, double& max_layer_height) const; void set_is_BBL_Printer(bool is_bbl_printer) { is_BBL_Printer = is_bbl_printer; }; bool get_is_BBL_Printer() { return is_BBL_Printer; }; // SLA print diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index 0bbbc15f87..412df19091 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -66,41 +66,41 @@ using Config::SnapshotDB; // Configuration data structures extensions needed for the wizard //BBS: set BBL as default -bool Bundle::load(fs::path source_path, bool ais_in_resources, bool ais_bbl_bundle) +bool Bundle::load(fs::path dir, const std::string &vendor_name, bool ais_in_resources, bool ais_bbl_bundle) { this->preset_bundle = std::make_unique<PresetBundle>(); this->is_in_resources = ais_in_resources; this->is_bbl_bundle = ais_bbl_bundle; - std::string path_string = source_path.string(); - std::string parent_path = source_path.parent_path().string(); //BBS: add json logic for vendor bundles - std::string vendor_name = source_path.filename().string(); - if (Slic3r::is_json_file(path_string)) { - // Remove the .json suffix. - vendor_name.erase(vendor_name.size() - 5); - } - else + // Orca: served from the vendor's preset cache where one covers it — which is + // how a shipped build carries its vendors — and parsed from the JSONs otherwise. + // A vendor that can be neither read nor parsed — a cache the build cannot use + // with the preset JSONs behind it pruned, say — is one the wizard cannot offer. + // Every other vendor still can be, so it is left out rather than thrown over. + size_t presets_loaded = 0; + try { + auto [config_substitutions, loaded] = preset_bundle->load_vendor_configs_from_json( + dir.string(), vendor_name, 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()); + presets_loaded = loaded; + } catch (const std::exception &e) { + BOOST_LOG_TRIVIAL(fatal) << boost::format("Vendor bundle: `%1%`: cannot be loaded, leaving it out: %2%") % vendor_name % e.what(); return false; - - // Throw when parsing invalid configuration. Only valid configuration is supposed to be provided over the air. - //BBS: add json logic for vendor bundles - auto [config_substitutions, presets_loaded] = preset_bundle->load_vendor_configs_from_json( - parent_path, vendor_name, 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; + BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % vendor_name; return false; } if (presets_loaded == 0) { - BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No profile loaded.") % path_string; + BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No profile loaded.") % vendor_name; return false; - } + } - BOOST_LOG_TRIVIAL(trace) << boost::format("Vendor bundle: `%1%`: %2% profiles loaded.") % path_string % presets_loaded; + BOOST_LOG_TRIVIAL(trace) << boost::format("Vendor bundle: `%1%`: %2% profiles loaded.") % vendor_name % presets_loaded; this->vendor_profile = &first_vendor->second; return true; } @@ -125,15 +125,10 @@ BundleMap BundleMap::load() //Orca: add custom as default //Orca: add json logic for vendor bundle - auto orca_bundle_path = (vendor_dir / PresetBundle::ORCA_DEFAULT_BUNDLE).replace_extension(".json"); - auto orca_bundle_rsrc = false; - if (!boost::filesystem::exists(orca_bundle_path)) { - orca_bundle_path = (rsrc_vendor_dir / PresetBundle::ORCA_DEFAULT_BUNDLE).replace_extension(".json"); - orca_bundle_rsrc = true; - } { + const bool from_rsrc = ! is_vendor_installed(PresetBundle::ORCA_DEFAULT_BUNDLE); Bundle bbl_bundle; - if (bbl_bundle.load(std::move(orca_bundle_path), orca_bundle_rsrc, true)) + if (bbl_bundle.load(from_rsrc ? rsrc_vendor_dir : vendor_dir, PresetBundle::ORCA_DEFAULT_BUNDLE, from_rsrc, true)) res.emplace(PresetBundle::ORCA_DEFAULT_BUNDLE, std::move(bbl_bundle)); } @@ -141,18 +136,13 @@ BundleMap BundleMap::load() // 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)) { - //BBS: add json logic for vendor bundle - if (Slic3r::is_json_file(dir_entry.path().string())) { - std::string id = dir_entry.path().stem().string(); // stem() = filename() without the trailing ".json" part + for (const std::string &id : vendor_names_in(*dir)) { + // Don't load this bundle if we've already loaded it. + if (res.find(id) != res.end()) { continue; } - // 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)); - } + Bundle bundle; + if (bundle.load(*dir, id, is_in_resources)) + res.emplace(id, std::move(bundle)); } is_in_resources = true; @@ -2762,7 +2752,7 @@ ConfigWizard::ConfigWizard(wxWindow *parent) }); if (wxLinux_gtk3) - this->Bind(wxEVT_SHOW, [this, vsizer](const wxShowEvent& e) { + this->Bind(wxEVT_SHOW, [](const wxShowEvent& e) { ; }); diff --git a/src/slic3r/GUI/ConfigWizard_private.hpp b/src/slic3r/GUI/ConfigWizard_private.hpp index 364d378b42..7b9674b216 100644 --- a/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/src/slic3r/GUI/ConfigWizard_private.hpp @@ -71,9 +71,11 @@ struct Bundle Bundle() = default; Bundle(Bundle&& other); + // Load the vendor `vendor_name` as it is installed in `dir`, from its preset + // cache or its profile JSONs, whichever is usable. // Returns false if not loaded. Reason for that is logged as boost::log error. //BBS: set BBL as default - bool load(fs::path source_path, bool is_in_resources, bool is_bbl_bundle = false); + bool load(fs::path dir, const std::string &vendor_name, bool is_in_resources, bool is_bbl_bundle = false); const std::string& vendor_id() const { return vendor_profile->id; } }; diff --git a/src/slic3r/GUI/ConnectPrinter.cpp b/src/slic3r/GUI/ConnectPrinter.cpp index b4cd7f4f2f..3e78e7fe5c 100644 --- a/src/slic3r/GUI/ConnectPrinter.cpp +++ b/src/slic3r/GUI/ConnectPrinter.cpp @@ -156,6 +156,8 @@ void ConnectPrinterDialog::on_input_enter(wxCommandEvent& evt) void ConnectPrinterDialog::on_button_confirm(wxCommandEvent &event) { wxString code = m_textCtrl_code->GetTextCtrl()->GetValue(); + if (code.empty()) + code = "88888888"; for (char c : code) { if (!(('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'))) { show_error(this, _L("Invalid input")); @@ -163,7 +165,7 @@ void ConnectPrinterDialog::on_button_confirm(wxCommandEvent &event) } } if (m_obj) { - m_obj->set_user_access_code(code.ToStdString()); + m_obj->set_access_code(code.ToStdString()); } EndModal(wxID_OK); } diff --git a/src/slic3r/GUI/CreatePresetsDialog.cpp b/src/slic3r/GUI/CreatePresetsDialog.cpp index 1bd80d5f00..e6fb6e0782 100644 --- a/src/slic3r/GUI/CreatePresetsDialog.cpp +++ b/src/slic3r/GUI/CreatePresetsDialog.cpp @@ -599,7 +599,9 @@ static char* read_json_file(const std::string &preset_path) return NULL; } - fread(json_contents, 1, file_size, json_file); + const size_t read_bytes = fread(json_contents, 1, file_size, json_file); + if (read_bytes != static_cast<size_t>(file_size)) + BOOST_LOG_TRIVIAL(error) << "Read " << read_bytes << " of " << file_size << " bytes from the JSON file"; fclose(json_file); return json_contents; @@ -1885,7 +1887,7 @@ wxBoxSizer *CreatePrinterPresetDialog::create_nozzle_diameter_item(wxWindow *par m_custom_nozzle_diameter_ctrl = new wxTextCtrl(parent, wxID_ANY, "", wxDefaultPosition, NAME_OPTION_COMBOBOX_SIZE); m_custom_nozzle_diameter_ctrl->SetHint(_L("Input Custom Nozzle Diameter")); - m_custom_nozzle_diameter_ctrl->Bind(wxEVT_CHAR, [this](wxKeyEvent &event) { + m_custom_nozzle_diameter_ctrl->Bind(wxEVT_CHAR, [](wxKeyEvent &event) { int key = event.GetKeyCode(); if (key != 44 && key != 46 && cannot_input_key.find(key) != cannot_input_key.end()) { // "@" can not be inputed event.Skip(false); @@ -2201,25 +2203,14 @@ bool CreatePrinterPresetDialog::load_system_and_user_presets_with_curr_model(Pre } else { selected_vendor_id = m_printer_preset_vendor_selected.id; - if (boost::filesystem::exists(boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR / selected_vendor_id)) { - preset_path = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).string(); - } else if (boost::filesystem::exists(boost::filesystem::path(Slic3r::resources_dir()) / "profiles" / selected_vendor_id)) { - preset_path = (boost::filesystem::path(Slic3r::resources_dir()) / "profiles").string(); - } - - if (preset_path.empty()) { - BOOST_LOG_TRIVIAL(info) << "Preset path was not found"; - MessageDialog dlg(this, _L("Preset path was not found; please reselect vendor."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), - wxYES_NO | wxYES_DEFAULT | wxCENTRE); - dlg.ShowModal(); - return false; - } - try { // Pass the app's preset bundle (which already holds OrcaFilamentLibrary) as the base // bundle so vendor filaments that inherit OFL bases resolve via the existing // cross-vendor inheritance path. - temp_preset_bundle.load_vendor_configs_from_json(preset_path, selected_vendor_id, + // Orca: served from the vendor's preset cache where one covers it — a shipped + // build carries that instead of the raw preset JSONs — and parsed otherwise. + temp_preset_bundle.load_vendor_configs_from_json((boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).string(), + selected_vendor_id, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent, wxGetApp().preset_bundle); @@ -3878,14 +3869,14 @@ void ExportConfigsDialog::select_curr_radiobox(std::vector<std::pair<RadioBox *, m_preset_sizer->Add(create_checkbox(m_presets_window, preset.second, printer_name, m_preset), 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(5)); } - m_serial_text->SetLabel(_L("Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a zip.")); + m_serial_text->SetLabel(_L("Only printer names with user printer presets will be displayed, and each preset you choose will be exported as a ZIP archive.")); } else if (export_type == m_exprot_type.filament_preset) { for (std::pair<std::string, std::vector<std::pair<std::string, Preset *>>> filament_name_to_preset : m_filament_name_to_presets) { if (filament_name_to_preset.second.empty()) continue; wxString filament_name = wxString::FromUTF8(filament_name_to_preset.first); m_preset_sizer->Add(create_checkbox(m_presets_window, filament_name, m_printer_name), 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(5)); } - m_serial_text->SetLabel(_L("Only the filament names with user filament presets will be displayed, \nand all user filament presets in each filament name you select will be exported as a zip.")); + m_serial_text->SetLabel(_L("Only the filament names with user filament presets will be displayed, \nand all user filament presets in each filament name you select will be exported as a ZIP archive.")); } else if (export_type == m_exprot_type.process_preset) { for (std::pair<std::string, std::vector<Preset *>> presets : m_process_presets) { Preset * printer_preset = preset_bundle->printers.find_preset(presets.first, false); @@ -3901,7 +3892,7 @@ void ExportConfigsDialog::select_curr_radiobox(std::vector<std::pair<RadioBox *, } } - m_serial_text->SetLabel(_L("Only printer names with changed process presets will be displayed, \nand all user process presets in each printer name you select will be exported as a zip.")); + m_serial_text->SetLabel(_L("Only printer names with changed process presets will be displayed, \nand all user process presets in each printer name you select will be exported as a ZIP archive.")); } //m_presets_window->SetSizerAndFit(m_preset_sizer); m_presets_window->Layout(); diff --git a/src/slic3r/GUI/CreatePresetsDialog.hpp b/src/slic3r/GUI/CreatePresetsDialog.hpp index 678f4d40fd..701779b1bf 100644 --- a/src/slic3r/GUI/CreatePresetsDialog.hpp +++ b/src/slic3r/GUI/CreatePresetsDialog.hpp @@ -74,12 +74,10 @@ private: std::unordered_set<std::string> m_system_filament_types_set; std::set<std::string> m_visible_printers; CreateType m_create_type; - Button * m_button_create = nullptr; Button * m_button_cancel = nullptr; ComboBox * m_filament_vendor_combobox = nullptr; ::CheckBox * m_can_not_find_vendor_checkbox = nullptr; ComboBox * m_filament_type_combobox = nullptr; - ComboBox * m_exist_vendor_combobox = nullptr; ComboBox * m_filament_preset_combobox = nullptr; TextInput * m_filament_custom_vendor_input = nullptr; wxGridSizer * m_filament_presets_sizer = nullptr; diff --git a/src/slic3r/GUI/DeviceCore/DevConfigUtil.cpp b/src/slic3r/GUI/DeviceCore/DevConfigUtil.cpp index b86d3cde98..25008cad29 100644 --- a/src/slic3r/GUI/DeviceCore/DevConfigUtil.cpp +++ b/src/slic3r/GUI/DeviceCore/DevConfigUtil.cpp @@ -16,24 +16,27 @@ namespace Slic3r // This block is never executed at runtime. static void _toolhead_translation_markers() { - // Dynamic toolhead display names from JSON config — xgettext cannot scan these - L("Main Extruder"); L("Main extruder"); L("main extruder"); - L("Auxiliary Extruder"); L("Auxiliary extruder"); L("auxiliary extruder"); - L("Left Extruder"); L("Left extruder"); L("left extruder"); - L("Right Extruder"); L("Right extruder"); L("right extruder"); - L("Main Nozzle"); L("Main nozzle"); L("main nozzle"); - L("Auxiliary Nozzle"); L("Auxiliary nozzle"); L("auxiliary nozzle"); - L("Left Nozzle"); L("Left nozzle"); L("left nozzle"); - L("Right Nozzle"); L("Right nozzle"); L("right nozzle"); - L("Main Hotend"); L("Main hotend"); L("main hotend"); - L("Auxiliary Hotend"); L("Auxiliary hotend"); L("auxiliary hotend"); - L("Left Hotend"); L("Left hotend"); L("left hotend"); - L("Right Hotend"); L("Right hotend"); L("right hotend"); - // standalone position words (short_name=true runtime results) - L("main"); L("auxiliary"); - L("Main"); L("Auxiliary"); - L("left"); L("right"); - L("Left"); L("Right"); + // Possible runtime values of tool_head_display_names, marked for extraction. + static const char *const markers[] = { + L("Main Extruder"), L("Main extruder"), L("main extruder"), + L("Auxiliary Extruder"), L("Auxiliary extruder"), L("auxiliary extruder"), + L("Left Extruder"), L("Left extruder"), L("left extruder"), + L("Right Extruder"), L("Right extruder"), L("right extruder"), + L("Main Nozzle"), L("Main nozzle"), L("main nozzle"), + L("Auxiliary Nozzle"), L("Auxiliary nozzle"), L("auxiliary nozzle"), + L("Left Nozzle"), L("Left nozzle"), L("left nozzle"), + L("Right Nozzle"), L("Right nozzle"), L("right nozzle"), + L("Main Hotend"), L("Main hotend"), L("main hotend"), + L("Auxiliary Hotend"), L("Auxiliary hotend"), L("auxiliary hotend"), + L("Left Hotend"), L("Left hotend"), L("left hotend"), + L("Right Hotend"), L("Right hotend"), L("right hotend"), + // standalone position words (short_name=true runtime results) + L("main"), L("auxiliary"), + L("Main"), L("Auxiliary"), + L("left"), L("right"), + L("Left"), L("Right"), + }; + (void) markers; } std::string DevPrinterConfigUtil::m_resource_file_path = ""; diff --git a/src/slic3r/GUI/DeviceCore/DevFirmware.h b/src/slic3r/GUI/DeviceCore/DevFirmware.h index 9dae603702..5b0ea986a2 100644 --- a/src/slic3r/GUI/DeviceCore/DevFirmware.h +++ b/src/slic3r/GUI/DeviceCore/DevFirmware.h @@ -64,7 +64,7 @@ public: DevFirmware(MachineObject* obj) : m_owner(obj) {} private: - MachineObject* m_owner = nullptr; + [[maybe_unused]] MachineObject* m_owner = nullptr; }; } // namespace Slic3r \ No newline at end of file diff --git a/src/slic3r/GUI/DeviceCore/DevManager.cpp b/src/slic3r/GUI/DeviceCore/DevManager.cpp index 2d54b5c85f..8844303793 100644 --- a/src/slic3r/GUI/DeviceCore/DevManager.cpp +++ b/src/slic3r/GUI/DeviceCore/DevManager.cpp @@ -10,11 +10,39 @@ #include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" +#include "slic3r/Utils/NetworkAgentFactory.hpp" #include "libslic3r/Time.hpp" using namespace nlohmann; +namespace { + // Orca: access_code lives on BBLocalMachine::access_code (keyed by dev_id via + // get_local_machines(), scoped by the record's own printer_agent_id field) - so binding a + // printer under one agent doesn't silently appear as already-bound under a different, + // independent agent. This only covers LAN devices (BBLocalMachine's own scope); access_code + // and user_access_code used to be the only, flat dev_id-only AppConfig keys before + // BBLocalMachine::access_code existed, and codes saved back then are still stored flat (no + // agent association at all). Since BBL was the only agent that existed at the time, honor + // those flat legacy keys as implicitly BBL's - but only for the BBL agent, so they aren't + // leaked to other agents that never bound the device themselves. + std::string get_access_code_with_legacy_fallback(Slic3r::AppConfig* config, const std::string& dev_id, const std::string& agent_id) + { + const auto& machines = config->get_local_machines(); + auto it = machines.find(dev_id); + if (it != machines.end() && it->second.printer_agent_id == agent_id && !it->second.access_code.empty()) + return it->second.access_code; + + if (agent_id == Slic3r::BBL_PRINTER_AGENT_ID || agent_id.empty()) { + std::string code = config->get("access_code", dev_id); + if (code.empty()) + code = config->get("user_access_code", dev_id); + return code; + } + return ""; + } +} + namespace Slic3r { DeviceManager::DeviceManager(NetworkAgent* agent) @@ -43,13 +71,13 @@ namespace Slic3r continue; MachineObject* obj = new MachineObject(this, m_agent, m.dev_name, m.dev_id, m.dev_ip); obj->printer_type = m.printer_type; + obj->printer_agent_id = m.printer_agent_id; obj->dev_connection_type = "lan"; obj->bind_state = "free"; obj->bind_sec_link = "secure"; obj->m_is_online = true; obj->last_alive = Slic3r::Utils::get_current_time_utc(); - obj->set_access_code(config->get("access_code", m.dev_id), false); - obj->set_user_access_code(config->get("user_access_code", m.dev_id), false); + obj->set_access_code(get_access_code_with_legacy_fallback(config, m.dev_id, obj->printer_agent_id), false); if (obj->has_access_right()) { localMachineList.insert(std::make_pair(m.dev_id, obj)); } else { @@ -66,10 +94,12 @@ namespace Slic3r if (m.is_lan_mode_printer()) { if (m.has_access_right()) { BBLocalMachine local_machine; - local_machine.dev_id = m.get_dev_id(); - local_machine.dev_name = m.get_dev_name(); - local_machine.dev_ip = m.get_dev_ip(); - local_machine.printer_type = m.printer_type; + local_machine.dev_id = m.get_dev_id(); + local_machine.dev_name = m.get_dev_name(); + local_machine.dev_ip = m.get_dev_ip(); + local_machine.printer_type = m.printer_type; + local_machine.printer_agent_id = m.printer_agent_id; + local_machine.access_code = m.get_access_code(); config->update_local_machine(local_machine); } } else { @@ -132,6 +162,14 @@ namespace Slic3r } } + std::string DeviceManager::get_current_printer_agent_id() const + { + if (!m_agent) + return ""; + auto printer_agent = m_agent->get_printer_agent(); + return printer_agent ? printer_agent->get_agent_info().id : ""; + } + void DeviceManager::EnableMultiMachine(bool enable) { m_agent->enable_multi_machine(enable); @@ -328,6 +366,7 @@ namespace Slic3r /* insert a new machine */ obj = new MachineObject(this, m_agent, dev_name, dev_id, dev_ip); obj->printer_type = _parse_printer_type(printer_type_str); + obj->printer_agent_id = get_current_printer_agent_id(); obj->wifi_signal = printer_signal; obj->dev_connection_type = connect_type; obj->bind_state = bind_state; @@ -339,8 +378,7 @@ namespace Slic3r //load access code AppConfig* config = Slic3r::GUI::wxGetApp().app_config; if (config) { - obj->set_access_code(Slic3r::GUI::wxGetApp().app_config->get("access_code", dev_id), false); - obj->set_user_access_code(Slic3r::GUI::wxGetApp().app_config->get("user_access_code", dev_id), false); + obj->set_access_code(get_access_code_with_legacy_fallback(config, dev_id, obj->printer_agent_id), false); } localMachineList.insert(std::make_pair(dev_id, obj)); @@ -369,6 +407,7 @@ namespace Slic3r obj = it->second; } else { obj = new MachineObject(this, m_agent, machine.dev_name, machine.dev_id, machine.dev_ip); + obj->printer_agent_id = get_current_printer_agent_id(); localMachineList.insert(std::make_pair(machine.dev_id, obj)); } if (machine.printer_type.empty()) @@ -382,7 +421,6 @@ namespace Slic3r obj->m_is_online = true; obj->last_alive = Slic3r::Utils::get_current_time_utc(); obj->set_access_code(access_code, false); - obj->set_user_access_code(access_code, false); update_local_machine(*obj); @@ -496,6 +534,36 @@ namespace Slic3r OnSelectedMachineChanged(previous_selected_machine, selected_machine); } + void DeviceManager::clear_other_devices(const std::string& target_agent_id) + { + // why: on agent swap, keep "My Devices" but drop the transient "Other Devices" + // Those belong to the previous agent's network scan; the new agent's start_discovery re-populates its own. + // + // Also drop "My Devices" stamped by a different agent than the one we're swapping to + // (target_agent_id, passed by the caller since the live agent hasn't been repointed yet + // at this point): otherwise a device first discovered under agent A survives every swap + // with a stale printer_agent_id, stays hidden from every agent's filtered list, and only + // gets re-tagged if something happens to delete and re-create it (e.g. account logout). + // Dropping it here instead lets the new agent's start_discovery re-insert and re-stamp it + // like any other fresh device. + const auto my = get_my_machine_list(); + for (auto it = localMachineList.begin(); it != localMachineList.end();) + { + const bool is_my_device = my.find(it->first) != my.end(); + const bool agent_mismatch = !target_agent_id.empty() && it->second && + it->second->printer_agent_id != target_agent_id; + if (!is_my_device || agent_mismatch) + { + delete it->second; + it = localMachineList.erase(it); + } + else + { + ++it; + } + } + } + bool DeviceManager::set_selected_machine(std::string dev_id) { BOOST_LOG_TRIVIAL(info) << "set_selected_machine=" << dev_id @@ -558,7 +626,6 @@ namespace Slic3r } else { - Slic3r::GUI::wxGetApp().reset_unsigned_plugin_warning(); if (m_agent) { if (it->second->connection_type() != "lan" || it->second->connection_type().empty()) @@ -669,13 +736,16 @@ namespace Slic3r m_agent->add_subscribe(subscribe_list_cache); } - std::map<std::string, MachineObject*> DeviceManager::get_my_machine_list() + std::map<std::string, MachineObject*> DeviceManager::get_my_machine_list(const std::string& agent_id) { std::map<std::string, MachineObject*> result; for (auto it = userMachineList.begin(); it != userMachineList.end(); it++) { - if (it->second && !it->second->is_lan_mode_printer()) + if (!it->second || (!agent_id.empty() && it->second->printer_agent_id != agent_id)) + continue; + + if (!it->second->is_lan_mode_printer()) { result.insert(std::make_pair(it->first, it->second)); } @@ -683,7 +753,10 @@ namespace Slic3r for (auto it = localMachineList.begin(); it != localMachineList.end(); it++) { - if (it->second && it->second->has_access_right() && it->second->is_avaliable() && it->second->is_lan_mode_printer()) + if (!it->second || (!agent_id.empty() && it->second->printer_agent_id != agent_id)) + continue; + + if (it->second->has_access_right() && it->second->is_avaliable() && it->second->is_lan_mode_printer()) { // remove redundant in userMachineList if (result.find(it->first) == result.end()) @@ -695,12 +768,15 @@ namespace Slic3r return result; } - std::map<std::string, MachineObject*> DeviceManager::get_my_cloud_machine_list() + std::map<std::string, MachineObject*> DeviceManager::get_my_cloud_machine_list(const std::string& agent_id) { std::map<std::string, MachineObject*> result; for (auto it = userMachineList.begin(); it != userMachineList.end(); it++) { - if (it->second && !it->second->is_lan_mode_printer()) { result.emplace(*it); } + if (!it->second || (!agent_id.empty() && it->second->printer_agent_id != agent_id)) + continue; + + if (!it->second->is_lan_mode_printer()) { result.emplace(*it); } } return result; } @@ -773,6 +849,7 @@ namespace Slic3r else { obj = new MachineObject(this, m_agent, "", "", ""); + obj->printer_agent_id = get_current_printer_agent_id(); if (m_agent) { obj->set_bind_status(m_agent->get_user_name(provider)); @@ -851,7 +928,9 @@ namespace Slic3r int result = m_agent->get_user_print_info(&http_code, &body, provider); if (result == 0) { - parse_user_print_info(body); + // parse_user_print_info and on_machine_alive (SSDP for discovery) both mutate the same userMachineList map. + // on_machine_alive mutates the map on the UI thread, do the same for parse_user_print_info. + Slic3r::GUI::wxGetApp().CallAfter([this, body]() { parse_user_print_info(body); }); } } @@ -878,17 +957,15 @@ namespace Slic3r void DeviceManager::load_last_machine() { - if (userMachineList.empty()) return; - else if (userMachineList.size() == 1) { - this->set_selected_machine(userMachineList.begin()->second->get_dev_id()); - } else { - const auto& last_monitor_machine = get_user_last_machine(); - if (userMachineList.find(last_monitor_machine) != userMachineList.end()) { - set_selected_machine(last_monitor_machine); - } else { - this->set_selected_machine(userMachineList.begin()->second->get_dev_id()); - } - } + // Only reconnect the remembered cloud machine. Do not select an arbitrary + // first machine: agent swaps intentionally leave the selection empty until + // the new agent explicitly selects its configured printer. + if (userMachineList.empty()) + return; + + const auto& last_monitor_machine = get_user_last_machine(); + if (userMachineList.find(last_monitor_machine) != userMachineList.end()) + set_selected_machine(last_monitor_machine); } void DeviceManager::OnMachineBindStateChanged(MachineObject* obj, const std::string& new_state) diff --git a/src/slic3r/GUI/DeviceCore/DevManager.h b/src/slic3r/GUI/DeviceCore/DevManager.h index 1f7f87b7fb..1f48baba98 100644 --- a/src/slic3r/GUI/DeviceCore/DevManager.h +++ b/src/slic3r/GUI/DeviceCore/DevManager.h @@ -48,6 +48,10 @@ public: MachineObject* get_selected_machine(); bool set_selected_machine(std::string dev_id); + // why: clears stale sidebar sync-status / AMS visuals. Public so the printer-agent + // swap path can reuse it instead of duplicating the two sidebar calls. + void OnSelectedMachineLost(); + void record_user_last_machine(const std::string& dev_id); std::string get_user_last_machine() const; @@ -70,6 +74,11 @@ public: void erase_user_machine(std::string dev_id) { userMachineList.erase(dev_id); } void clean_user_info(bool keep_local_selection = false); + // target_agent_id: id of the agent being swapped to (empty = no agent-mismatch check, + // just the original "drop Other Devices" behavior). Pass the incoming agent's id, not the + // live one - this runs before the live agent is repointed. + void clear_other_devices(const std::string& target_agent_id = ""); + void load_last_machine(); void update_user_machine_list_info(const std::string& provider); void parse_user_print_info(std::string body); @@ -84,10 +93,15 @@ public: /* my machine*/ MachineObject* get_my_machine(std::string dev_id); - std::map<std::string, MachineObject*> get_my_machine_list(); - std::map<std::string, MachineObject*> get_my_cloud_machine_list(); + std::map<std::string, MachineObject*> get_my_machine_list(const std::string& agent_id = ""); + std::map<std::string, MachineObject*> get_my_cloud_machine_list(const std::string& agent_id = ""); void modify_device_name(std::string dev_id, std::string dev_name, const std::string& provider); + // id of the currently live IPrinterAgent (IPrinterAgent::get_agent_info().id), or empty if + // m_agent has no printer agent set yet. Pass to get_my_machine_list()/get_my_cloud_machine_list() + // to scope results to the active agent. + std::string get_current_printer_agent_id() const; + /* create machine or update machine properties */ void on_machine_alive(std::string json_str); int query_bind_status(std::string& msg, const std::string& provider); @@ -110,7 +124,6 @@ private: void check_pushing(); void OnMachineBindStateChanged(MachineObject* obj, const std::string& new_state); - void OnSelectedMachineLost(); void OnSelectedMachineChanged(const std::string& pre_dev_id, const std::string& new_dev_id); diff --git a/src/slic3r/GUI/DeviceCore/DevStatus.cpp b/src/slic3r/GUI/DeviceCore/DevStatus.cpp index 26d2bc4ceb..e37a0f9abc 100644 --- a/src/slic3r/GUI/DeviceCore/DevStatus.cpp +++ b/src/slic3r/GUI/DeviceCore/DevStatus.cpp @@ -27,6 +27,7 @@ void DevStatus::ParseStatus(const nlohmann::json& print_jj) #else BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": get exception=" << e.what(); #endif + (void)e; // suppress C4101 when BBL_RELEASE_TO_PUBLIC } } diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index 5499694686..d8487f4660 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -3,6 +3,7 @@ #include "libslic3r/Time.hpp" #include "libslic3r/Thread.hpp" #include "slic3r/Utils/NetworkAgent.hpp" +#include "slic3r/Utils/NetworkAgentFactory.hpp" #include "GuiColor.hpp" #include "GUI_App.hpp" @@ -449,9 +450,7 @@ bool MachineObject::HasRecentLanMessage() std::string MachineObject::get_access_code() const { - if (get_user_access_code().empty()) - return access_code; - return get_user_access_code(); + return access_code; } void MachineObject::set_access_code(std::string code, bool only_refresh) @@ -460,47 +459,46 @@ void MachineObject::set_access_code(std::string code, bool only_refresh) if (only_refresh) { AppConfig* config = GUI::wxGetApp().app_config; if (config) { - if (!code.empty()) { - GUI::wxGetApp().app_config->set_str("access_code", get_dev_id(), code); - DeviceManager::update_local_machine(*this); + if (is_lan_mode_printer()) { + // why: LAN codes are scoped via BBLocalMachine::access_code, keyed by dev_id and + // scoped by that record's own printer_agent_id field - see the matching comment + // on get_access_code_with_legacy_fallback() in DevManager.cpp - so binding this + // device under one printer agent doesn't silently read as already-bound under a + // different, independent one. Cloud devices (the else branch below) aren't + // scoped this way: they're never recalled from a stale local cache across a + // session boundary, since parse_user_print_info() always overwrites their code + // fresh from the cloud API's current response, so there's no cross-agent leakage + // risk to guard against there. + if (!code.empty()) { + DeviceManager::update_local_machine(*this); + } else { + // Only patch an existing record's code - don't persist a brand-new + // never-bound entry just because set_access_code("") was called on it. + const auto& machines = config->get_local_machines(); + auto it = machines.find(get_dev_id()); + if (it != machines.end()) { + BBLocalMachine local_machine = it->second; + local_machine.access_code = ""; + config->update_local_machine(local_machine); + } + // Also clear the pre-scoping flat legacy key when unbinding under BBL, so an + // old BBL-era code can't silently "re-bind" this device again via + // get_access_code_with_legacy_fallback()'s legacy fallback. + if (printer_agent_id == BBL_PRINTER_AGENT_ID || printer_agent_id.empty()) { + config->erase("access_code", get_dev_id()); + config->erase("user_access_code", get_dev_id()); + } + } } else { - GUI::wxGetApp().app_config->erase("access_code", get_dev_id()); + if (!code.empty()) + config->set_str("access_code", get_dev_id(), code); + else + config->erase("access_code", get_dev_id()); } } } } -void MachineObject::erase_user_access_code() -{ - this->user_access_code = ""; - AppConfig* config = GUI::wxGetApp().app_config; - if (config) { - GUI::wxGetApp().app_config->erase("user_access_code", get_dev_id()); - //GUI::wxGetApp().app_config->save(); - } -} - -void MachineObject::set_user_access_code(std::string code, bool only_refresh) -{ - this->user_access_code = code; - if (only_refresh && !code.empty()) { - AppConfig* config = GUI::wxGetApp().app_config; - if (config && !code.empty()) { - GUI::wxGetApp().app_config->set_str("user_access_code", get_dev_id(), code); - DeviceManager::update_local_machine(*this); - } - } -} - -std::string MachineObject::get_user_access_code() const -{ - AppConfig* config = GUI::wxGetApp().app_config; - if (config) { - return GUI::wxGetApp().app_config->get("user_access_code", get_dev_id()); - } - return ""; -} - std::string MachineObject::get_show_printer_type() const { std::string printer_type = this->printer_type; @@ -2907,7 +2905,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ std::string access_code = j_pre["system"]["access_code"].get<std::string>(); if (!access_code.empty()) { set_access_code(access_code); - set_user_access_code(access_code); } } } @@ -4647,6 +4644,40 @@ void MachineObject::set_ctt_dlg( wxString text){ } } +void MachineObject::show_unsupported_dlg(int code) +{ + // why: a dead control invites repeat clicks, and the frame is modeless - without the guard + // every click stacks another one. Same shape as set_ctt_dlg above, including the reset on + // both hide and close so a dismissed dialog can reappear on the next attempt. + if (m_unsupported_dlg_shown) { + return; + } + m_unsupported_dlg_shown = true; + + // why: two codes so the user learns which kind of dead end this is - the slicer having no + // translation for the command, or the printer's own config lacking the hardware to run it. + const wxString text = (code == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE) ? + _L("This printer is not configured with the hardware this control needs.") : + _L("This control is not supported on this printer."); + + // note: constructed directly rather than through CallAfter because every publish_json caller + // is on the UI thread - clicks come from wx handlers, and the agent marshals its own push + // callbacks back to main before parse_json runs. set_ctt_dlg relies on the same property. + auto unsupported_dlg = new GUI::SecondaryCheckDialog(nullptr, wxID_ANY, _L("Warning"), + GUI::SecondaryCheckDialog::VisibleButtons::ONLY_CONFIRM); + unsupported_dlg->update_text(text); + unsupported_dlg->Bind(wxEVT_SHOW, [this](auto& e) { + if (!e.IsShown()) { + m_unsupported_dlg_shown = false; + } + }); + unsupported_dlg->Bind(wxEVT_CLOSE_WINDOW, [this](auto& e) { + e.Skip(); + m_unsupported_dlg_shown = false; + }); + unsupported_dlg->on_show(); +} + int MachineObject::publish_gcode(std::string gcode_str) { json j; diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index 456901cf84..914c8f7868 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -113,7 +113,6 @@ private: std::string dev_name; std::string dev_ip; std::string access_code; - std::string user_access_code; // type, time stamp, delay std::vector<std::tuple<std::string, uint64_t, uint64_t>> message_delay; @@ -228,13 +227,18 @@ public: std::string get_access_code() const; void set_access_code(std::string code, bool only_refresh = true); - /*user access code*/ - void set_user_access_code(std::string code, bool only_refresh = true); - void erase_user_access_code(); - std::string get_user_access_code() const; - //PRINTER_TYPE printer_type = PRINTER_3DPrinter_UKNOWN; std::string printer_type; /* model_id */ + + // id of the IPrinterAgent that was used to discover or bind this device (IPrinterAgent::get_agent_info().id, + // e.g. "bbl"), stamped at creation time — not derived from get_agent(), since m_agent is a single + // process-wide NetworkAgent shared by every MachineObject and gets repointed on agent swap + // (see DeviceManager::set_agent()), so it can't tell which agent originally found this device. + // We persist this as well so that when the printer agent is swapped, we don't show unrelated devices, + // e.g. if the current printer agent is elegoo, we shouldn't show printers connected by BBL printer agent + // under local machines. + std::string printer_agent_id; + std::string get_show_printer_type() const; PrinterSeries get_printer_series() const; PrinterArch get_printer_arch() const; @@ -272,9 +276,11 @@ public: bool m_is_online; bool m_lan_mode_connection_state{false}; bool m_set_ctt_dlg{ false }; + bool m_unsupported_dlg_shown{ false }; void set_lan_mode_connection_state(bool state) {m_lan_mode_connection_state = state;}; bool get_lan_mode_connection_state() {return m_lan_mode_connection_state;}; void set_ctt_dlg( wxString text); + void show_unsupported_dlg(int code); int parse_msg_count = 0; int keep_alive_count = 0; std::chrono::system_clock::time_point last_update_time; /* last received print data from machine */ diff --git a/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.cpp b/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.cpp index 103584602f..a258a2178a 100644 --- a/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.cpp +++ b/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.cpp @@ -2,7 +2,7 @@ /* File: uiAMSBestPositionPopup.hpp * Description: The popup with suggest best ams position * -//**********************************************************/ +************************************************************/ #include "uiAMSBestPositionPopup.hpp" diff --git a/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.hpp b/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.hpp index 427a5c6a73..be8f957c94 100644 --- a/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.hpp +++ b/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.hpp @@ -2,7 +2,7 @@ /* File: uiAMSBestPositionPopup.hpp * Description: The popup with suggest best ams position * -//**********************************************************/ +************************************************************/ #pragma once #include "slic3r/GUI/Widgets/AMSItem.hpp" @@ -199,7 +199,6 @@ private: void OnRefreshButton(wxCommandEvent& event); private: - int saveTimes{0}; wxBoxSizer* mainSizer{nullptr}; wxPanel* textPanel{nullptr}; wxBoxSizer* textSizer{nullptr}; diff --git a/src/slic3r/GUI/DeviceTab/uiAmsHumidityPopup.cpp b/src/slic3r/GUI/DeviceTab/uiAmsHumidityPopup.cpp index 33e9cdf518..259506612d 100644 --- a/src/slic3r/GUI/DeviceTab/uiAmsHumidityPopup.cpp +++ b/src/slic3r/GUI/DeviceTab/uiAmsHumidityPopup.cpp @@ -1,9 +1,9 @@ -//**********************************************************/ -/* File: uiAmsHumidityPopup.cpp +/********************************************************** +* File: uiAmsHumidityPopup.cpp * Description: The popup with DevAms Humidity * * \n class uiAmsHumidityPopup -//**********************************************************/ +**********************************************************/ #include "uiAmsHumidityPopup.h" @@ -191,4 +191,4 @@ void uiAmsPercentHumidityDryPopup::msw_rescale() } // namespace GUI -} // namespace Slic3r \ No newline at end of file +} // namespace Slic3r diff --git a/src/slic3r/GUI/DeviceTab/uiAmsHumidityPopup.h b/src/slic3r/GUI/DeviceTab/uiAmsHumidityPopup.h index 0f22c6b662..a109381c86 100644 --- a/src/slic3r/GUI/DeviceTab/uiAmsHumidityPopup.h +++ b/src/slic3r/GUI/DeviceTab/uiAmsHumidityPopup.h @@ -1,9 +1,9 @@ -//**********************************************************/ -/* File: uiAmsHumidityPopup.h +/********************************************************** +* File: uiAmsHumidityPopup.h * Description: The popup with DevAms Humidity * * \n class uiAmsHumidityPopup -//**********************************************************/ +**********************************************************/ #pragma once #include "slic3r/GUI/Widgets/AMSItem.hpp" @@ -68,7 +68,7 @@ private: wxStaticBitmap* m_dry_state_img; Label* m_dry_state; - + Label* m_humidity_header; Label* m_humidity_label; @@ -81,4 +81,4 @@ private: wxSizer* m_sizer; }; -}} // namespace Slic3r::GUI \ No newline at end of file +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/DeviceTab/uiDeviceUpdateVersion.cpp b/src/slic3r/GUI/DeviceTab/uiDeviceUpdateVersion.cpp index 5167f482b8..59fc8809ce 100644 --- a/src/slic3r/GUI/DeviceTab/uiDeviceUpdateVersion.cpp +++ b/src/slic3r/GUI/DeviceTab/uiDeviceUpdateVersion.cpp @@ -1,9 +1,9 @@ -//**********************************************************/ -/* File: uiDeviceUpdateVersion.cpp +/********************************************************** +* File: uiDeviceUpdateVersion.cpp * Description: The panel with firmware info * * \n class uiDeviceUpdateVersion -//**********************************************************/ +**********************************************************/ #include "uiDeviceUpdateVersion.h" @@ -114,4 +114,4 @@ void uiDeviceUpdateVersion::CreateWidgets() Layout(); wxGetApp().UpdateDarkUIWin(this); -} \ No newline at end of file +} diff --git a/src/slic3r/GUI/DeviceTab/uiDeviceUpdateVersion.h b/src/slic3r/GUI/DeviceTab/uiDeviceUpdateVersion.h index 100280db62..342067e374 100644 --- a/src/slic3r/GUI/DeviceTab/uiDeviceUpdateVersion.h +++ b/src/slic3r/GUI/DeviceTab/uiDeviceUpdateVersion.h @@ -1,9 +1,9 @@ -//**********************************************************/ -/* File: uiDeviceUpdateVersion.h +/********************************************************** +* File: uiDeviceUpdateVersion.h * Description: The panel with firmware info * * \n class uiDeviceUpdateVersion -//**********************************************************/ +**********************************************************/ #pragma once #include <wx/panel.h> @@ -44,4 +44,4 @@ private: wxStaticText* m_dev_version; wxStaticBitmap* m_dev_upgrade_indicator; }; -};// end of namespace Slic3r::GUI \ No newline at end of file +};// end of namespace Slic3r::GUI diff --git a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.cpp b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.cpp index a62277a858..1e40a71150 100644 --- a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.cpp +++ b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.cpp @@ -6,7 +6,7 @@ * \n class wgtDeviceNozzleRackNozzleItem; * \n class wgtDeviceNozzleRackToolHead; * \n class wgtDeviceNozzleRackPos; -//**********************************************************/ +************************************************************/ #include "wgtDeviceNozzleRack.h" #include "wgtDeviceNozzleRackUpdate.h" diff --git a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.h b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.h index fe12b8bc50..385fa6be48 100644 --- a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.h +++ b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.h @@ -6,7 +6,7 @@ * \n class wgtDeviceNozzleRackNozzleItem; * \n class wgtDeviceNozzleRackToolHead; * \n class wgtDeviceNozzleRackPos; -//**********************************************************/ +************************************************************/ #pragma once #include "slic3r/GUI/DeviceCore/DevNozzleRack.h" diff --git a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackUpdate.cpp b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackUpdate.cpp index 2750ad6323..fac14e31d9 100644 --- a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackUpdate.cpp +++ b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackUpdate.cpp @@ -3,7 +3,7 @@ * Description: The panel with rack updating * * \n class wgtDeviceNozzleRackUpdate -//**********************************************************/ +************************************************************/ #include "wgtDeviceNozzleRackUpdate.h" diff --git a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackUpdate.h b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackUpdate.h index 0fa07fd63a..06b950cd7e 100644 --- a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackUpdate.h +++ b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRackUpdate.h @@ -3,7 +3,7 @@ * Description: The panel for updating hotends * * \n class wgtDeviceNozzleRackUpdate -//**********************************************************/ +************************************************************/ #pragma once #include "slic3r/GUI/DeviceCore/DevNozzleRack.h" @@ -122,7 +122,6 @@ private: private: int m_ext_nozzle_id = -1; int m_rack_nozzle_id = -1; - bool m_isRefreshFinish = false; bool findNozzleImage = false; NozzleStatus m_nozzle_status = NOZZLE_STATUS_DC; @@ -154,7 +153,6 @@ private: Label* m_diameter_label; Label* m_flowtype_label; Label* m_type_label; - ScalableButton* m_error_button{ nullptr }; Label* m_sn_label; Label* m_version_label; diff --git a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleSelect.cpp b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleSelect.cpp index 0d61cfd144..4dc072d721 100644 --- a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleSelect.cpp +++ b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleSelect.cpp @@ -3,7 +3,7 @@ * Description: The panel to select nozzle * * \n class wgtDeviceNozzleSelect; -//**********************************************************/ +************************************************************/ #include "wgtDeviceNozzleSelect.h" #include "wgtDeviceNozzleRack.h" @@ -266,7 +266,7 @@ void wgtDeviceNozzleRackSelect::OnNozzleItemSelected(wxCommandEvent &evt) } auto *item = dynamic_cast<wgtDeviceNozzleRackNozzleItem *>(evt.GetEventObject()); - if (item; auto ptr = m_nozzle_rack.lock()) { + if (auto ptr = m_nozzle_rack.lock(); item && ptr) { int to_select_pos_id = sGetNozzlePosId(item, m_toolhead_nozzle_l, m_toolhead_nozzle_r); if (to_select_pos_id > -1 && to_select_pos_id != GetSelectedNozzlePosID()) { SetSelectedNozzle(ptr->GetNozzleSystem()->GetNozzleByPosId(to_select_pos_id)); diff --git a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleSelect.h b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleSelect.h index 3ff866f3a1..729d24a03d 100644 --- a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleSelect.h +++ b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleSelect.h @@ -3,7 +3,7 @@ * Description: The panel to select nozzle * * \n class wgtDeviceNozzleSelect; -//**********************************************************/ +************************************************************/ #pragma once diff --git a/src/slic3r/GUI/DownloadProgressDialog.cpp b/src/slic3r/GUI/DownloadProgressDialog.cpp index 9bc0d90e5e..1c5ff4c3d7 100644 --- a/src/slic3r/GUI/DownloadProgressDialog.cpp +++ b/src/slic3r/GUI/DownloadProgressDialog.cpp @@ -26,8 +26,6 @@ #include "Widgets/HyperLink.hpp" // ORCA -#define DESIGN_INPUT_SIZE wxSize(FromDIP(100), -1) - namespace Slic3r { namespace GUI { diff --git a/src/slic3r/GUI/Downloader.cpp b/src/slic3r/GUI/Downloader.cpp index c61b2716fc..0d37a0eca6 100644 --- a/src/slic3r/GUI/Downloader.cpp +++ b/src/slic3r/GUI/Downloader.cpp @@ -134,7 +134,7 @@ void Downloader::start_download(const std::string& full_url) Plater* plater = wxGetApp().plater(); mainframe->Freeze(); - mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor); + mainframe->select_tab(TAB_ID_PREPARE); plater->select_view_3D("3D"); plater->select_view("plate"); plater->get_current_canvas3D()->zoom_to_bed(); diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp index 1fcaef1b52..142cf70522 100644 --- a/src/slic3r/GUI/Field.cpp +++ b/src/slic3r/GUI/Field.cpp @@ -35,6 +35,7 @@ #include "Widgets/TextCtrl.h" #include "../Utils/ColorSpaceConvert.hpp" +#include "../Utils/NetworkAgentFactory.hpp" #ifdef __WXOSX__ #define wxOSX true #else @@ -330,8 +331,10 @@ void Field::PostInitialize() } default: break; } - if (tab_id >= 0) - wxGetApp().mainframe->select_tab(tab_id); + if (tab_id >= 0) { + static constexpr const char* kShortcutTabIds[] = {TAB_ID_HOME, TAB_ID_PREPARE, TAB_ID_PREVIEW, TAB_ID_MONITOR}; + wxGetApp().mainframe->select_tab(kShortcutTabIds[tab_id]); + } if (tab_id > 0) // tab panel should be focused for correct navigation between tabs wxGetApp().tab_panel()->SetFocus(); @@ -938,7 +941,11 @@ void TextCtrl::BUILD() { temp->SetToolTip(get_tooltip_text(text_value)); if (!m_opt.multiline) { - text_ctrl->Bind(wxEVT_TEXT_ENTER, ([this, temp](wxEvent &e) + text_ctrl->Bind(wxEVT_TEXT_ENTER, ([ +#if !defined(__WXGTK__) + temp, +#endif // __WXGTK__ + this](wxEvent &e) { #if !defined(__WXGTK__) e.Skip(); @@ -970,7 +977,11 @@ void TextCtrl::BUILD() { temp->GetToolTip()->Enable(flag); }), text_ctrl->GetId()); - temp->Bind(wxEVT_KILL_FOCUS, ([this, temp](wxEvent &e) + temp->Bind(wxEVT_KILL_FOCUS, ([ +#if !defined(__WXGTK__) + temp, +#endif // __WXGTK__ + this](wxEvent &e) { e.Skip(); #if !defined(__WXGTK__) @@ -1321,7 +1332,7 @@ void SpinCtrl::BUILD() { bEnterPressed = true; }), temp->GetId()); - temp->GetTextCtrl()->Bind(wxEVT_TEXT, ([this, temp](wxCommandEvent e) + temp->GetTextCtrl()->Bind(wxEVT_TEXT, ([this](wxCommandEvent e) { // # On OSX / Cocoa, SpinInput::GetValue() doesn't return the new value // # when it was changed from the text control, so the on_change callback @@ -1403,39 +1414,6 @@ using choice_ctrl = ::ComboBox; // BBS static std::map<std::string, DynamicList*> dynamic_lists; -static bool is_plugin_printer_agent_key(const std::string& value) -{ - return value.rfind("plugin:", 0) == 0; -} - -static int printer_agent_item_for_enum_index(const choice_ctrl* field, int enum_index) -{ - if (!field) - return -1; - - const unsigned int count = field->GetCount(); - for (unsigned int idx = 0; idx < count; ++idx) { - if (void* data = field->GetClientData(idx)) { - const int stored = static_cast<int>(reinterpret_cast<uintptr_t>(data)) - 1; - if (stored == enum_index) - return static_cast<int>(idx); - } - } - - return -1; -} - -static int printer_agent_enum_index_for_item(const choice_ctrl* field, int item_index, int fallback) -{ - if (!field || item_index < 0) - return fallback; - - if (void* data = field->GetClientData(item_index)) - return static_cast<int>(reinterpret_cast<uintptr_t>(data)) - 1; - - return fallback; -} - void Choice::register_dynamic_list(std::string const &optname, DynamicList *list) { dynamic_lists.emplace(optname, list); } void DynamicList::update() @@ -1518,33 +1496,7 @@ void Choice::BUILD() window = dynamic_cast<wxWindow*>(temp); if (! m_opt.enum_labels.empty() || ! m_opt.enum_values.empty()) { - if (m_opt_id == "printer_agent") { - const bool has_builtin_agents = std::any_of(m_opt.enum_values.begin(), m_opt.enum_values.end(), - [](const std::string& value) { return !is_plugin_printer_agent_key(value); }); - const bool has_plugin_agents = std::any_of(m_opt.enum_values.begin(), m_opt.enum_values.end(), - [](const std::string& value) { return is_plugin_printer_agent_key(value); }); - - auto append_agent_rows = [this, temp](bool plugins) { - for (size_t i = 0; i < m_opt.enum_values.size(); ++i) { - const bool is_plugin = is_plugin_printer_agent_key(m_opt.enum_values[i]); - if (is_plugin != plugins) - continue; - - const wxString label = i < m_opt.enum_labels.size() ? _(m_opt.enum_labels[i]) : wxString(m_opt.enum_values[i]); - const int item = temp->Append(label); - temp->SetClientData(item, reinterpret_cast<void*>(static_cast<uintptr_t>(i + 1))); - } - }; - - if (has_builtin_agents) { - temp->Append(_L("System agents"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED); - append_agent_rows(false); - } - if (has_plugin_agents) { - temp->Append(_L("Plugins"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED); - append_agent_rows(true); - } - } else if (m_opt.enum_labels.empty()) { + if (m_opt.enum_labels.empty()) { // Append non-localized enum_values for (auto el : m_opt.enum_values) temp->Append(el); @@ -1651,7 +1603,7 @@ void Choice::set_selection() switch (m_opt.type) { case coEnum:{ const int val = m_opt.default_value->getInt(); - field->SetSelection(m_opt_id == "printer_agent" ? printer_agent_item_for_enum_index(field, val) : val); + field->SetSelection(val); break; } case coFloat: @@ -1701,12 +1653,7 @@ void Choice::set_value(const std::string& value, bool change_event) //! Redunda } choice_ctrl* field = dynamic_cast<choice_ctrl*>(window); - if (m_opt_id == "printer_agent") { - const int enum_index = idx == m_opt.enum_values.size() ? - (m_opt.default_value ? m_opt.default_value->getInt() : 0) : - static_cast<int>(idx); - field->SetSelection(printer_agent_item_for_enum_index(field, enum_index)); - } else if (idx == m_opt.enum_values.size()) + if (idx == m_opt.enum_values.size()) field->SetValue(value); else field->SetSelection(idx); @@ -1772,33 +1719,11 @@ void Choice::set_value(const boost::any& value, bool change_event) case coEnum: // BBS case coEnums: { - auto printer_agent_index_from_key = [this](const std::string& key) { - auto it = std::find(m_opt.enum_values.begin(), m_opt.enum_values.end(), key); - if (it != m_opt.enum_values.end()) - return static_cast<int>(it - m_opt.enum_values.begin()); - return m_opt.default_value ? m_opt.default_value->getInt() : 0; - }; - - int val = 0; - if (m_opt_id == "printer_agent") { - if (const int* int_value = boost::any_cast<int>(&value)) - val = *int_value; - else if (const wxString* wx_value = boost::any_cast<wxString>(&value)) - val = printer_agent_index_from_key(into_u8(*wx_value)); - else if (const std::string* string_value = boost::any_cast<std::string>(&value)) - val = printer_agent_index_from_key(*string_value); - else { - m_disable_change_event = false; - return; - } - } else - val = boost::any_cast<int>(value); + int val = boost::any_cast<int>(value); int selection = val; - if (m_opt_id == "printer_agent") { - selection = printer_agent_item_for_enum_index(field, val); - } else if (m_opt_id == "input_shaping_type") { + if (m_opt_id == "input_shaping_type") { if (field != nullptr) { const unsigned int count = field->GetCount(); int match_index = -1; @@ -1920,12 +1845,6 @@ boost::any& Choice::get_value() { if (m_opt.nullable && field->GetSelection() == -1) m_value = ConfigOptionEnumsGenericNullable::nil_value(); - else if (m_opt_id == "printer_agent") - { - const int selection = field->GetSelection(); - const int fallback = m_opt.default_value ? m_opt.default_value->getInt() : 0; - m_value = printer_agent_enum_index_for_item(field, selection, fallback); - } else if (m_opt_id == "input_shaping_type") { int selection = field->GetSelection(); @@ -2067,6 +1986,171 @@ void Choice::msw_rescale() } +// PrinterAgentChoice + +void PrinterAgentChoice::reload_rows() +{ + auto* combo = dynamic_cast<choice_ctrl*>(window); // wxWidgets ComboBox + if (!combo) + return; + + // clear ComboBox + combo->Clear(); + + // helpers + const auto agents = NetworkAgentFactory::get_registered_printer_agents(); + const bool has_builtin_agents = std::any_of(agents.begin(), agents.end(), + [](const PrinterAgentInfo& a) { return !a.is_plugin(); }); + const bool has_plugin_agents = std::any_of(agents.begin(), agents.end(), + [](const PrinterAgentInfo& a) { return a.is_plugin(); }); + + auto append_agent_rows = [combo](bool is_plugin) + { + const auto agents = NetworkAgentFactory::get_registered_printer_agents(); + for (size_t i = 0; i < agents.size(); ++i) + { + if (agents[i].is_plugin() != is_plugin) + continue; + const int item = combo->Append(_(agents[i].display_name)); + // why: carry the agent-id string on the row. alias is an owned wxString (auto-freed, never rendered) + combo->SetItemAlias(item, from_u8(agents[i].id)); + } + }; + + // append rows + if (has_builtin_agents) + { + combo->Append(_L("System agents"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED); + append_agent_rows(false); // append rows for agents that are not plugins + } + if (has_plugin_agents) + { + combo->Append(_L("Plugins"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED); + append_agent_rows(true); // append rows for agents that are plugins + } +} + +void PrinterAgentChoice::BUILD() +{ + wxSize size(def_width_wider() * m_em_unit, wxDefaultCoord); + if (m_opt.height >= 0) size.SetHeight(m_opt.height * m_em_unit); + if (m_opt.width >= 0) size.SetWidth(m_opt.width * m_em_unit); + + static Builder<choice_ctrl> builder; + choice_ctrl* temp = builder.build(m_parent, wxID_ANY, wxString(""), wxDefaultPosition, size, 0, nullptr, + wxCB_READONLY); + temp->Clear(); + temp->GetDropDown().SetUseContentWidth(true); + if (parent_is_custom_ctrl && m_opt.height < 0) + opt_height = (double)temp->GetTextCtrl()->GetSize().GetHeight() / m_em_unit; + temp->SetTextLabel(_L(m_opt.sidetext)); + m_combine_side_text = true; +#ifdef __WXGTK3__ + wxSize best_sz = temp->GetBestSize(); + if (best_sz.x > size.x) temp->SetSize(best_sz); +#endif + if (!wxOSX) temp->SetBackgroundStyle(wxBG_STYLE_PAINT); + + window = dynamic_cast<wxWindow*>(temp); + + reload_rows(); + + temp->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { on_change_field(); }, temp->GetId()); + temp->SetToolTip(get_tooltip_text(temp->GetValue())); +} + +// Resolve CONFIG id string to a matching row in the live REGISTRY. "" uses the vendor default. +// An unregistered id clears selection and shows "<id> (missing)" as free text. +void PrinterAgentChoice::set_value(const std::string& value, bool change_event) +{ + m_disable_change_event = !change_event; + + auto* field = dynamic_cast<choice_ctrl*>(window); + + // check if any row's corresponding id matches the agent id we are attempting to set + const std::string effective_agent_id = wxGetApp().resolve_printer_agent_id(value); + const unsigned int count = field->GetCount(); + int match = wxNOT_FOUND; + for (unsigned int i = 0; i < count; ++i) + { + if (into_u8(field->GetItemAlias(i)) == effective_agent_id) // if alias == id + { + match = static_cast<int>(i); + break; + } + } + + // based on match or not, set selection and value + // - SetSelection and SetValue are UI to manipulate the display of the ComboBox + // - SetSelection automatically calls SetValue for the same value + // - we can also SetValue separately from SetSelection + if (match == wxNOT_FOUND) + { + field->SetSelection(wxNOT_FOUND); // nothing shows as selected in the dropdown + field->SetValue(from_u8(value + " (missing)")); // set a value not in the selection (upper display field) + } + else + { + // display name of agent shows both in upper display field and appears selected in dropdown + field->SetSelection(match); + } + + m_disable_change_event = false; +} + +// Accept boost::any values from callers (usually to OptionsGroup/Field parent classes) and normalize them to an agent id. +// Then use PrinterAgentChoice::set_value(std::string& value, ...) +void PrinterAgentChoice::set_value(const boost::any& value, bool change_event) +{ + m_disable_change_event = !change_event; + + auto* field = dynamic_cast<choice_ctrl*>(window); + if (value.empty()) + { + field->SetValue(""); + m_value = value; + m_disable_change_event = false; + return; + } + + std::string id; + if (const std::string* s = boost::any_cast<std::string>(&value)) + id = *s; + else if (const wxString* w = boost::any_cast<wxString>(&value)) + id = into_u8(*w); + set_value(id, change_event); +} + +// A real row returns its alias, which is the agent id. Header rows, missing rows, +// and no selection return empty boost::any so the custom writer leaves config unchanged. +boost::any& PrinterAgentChoice::get_value() +{ + auto* field = dynamic_cast<choice_ctrl*>(window); + const int sel = field->GetSelection(); + const std::string id = sel < 0 ? std::string{} : into_u8(field->GetItemAlias(sel)); + if (id.empty()) + m_value = boost::any{}; + else + m_value = id; + return m_value; +} + +void PrinterAgentChoice::enable() { dynamic_cast<choice_ctrl*>(window)->Enable(); } +void PrinterAgentChoice::disable() { dynamic_cast<choice_ctrl*>(window)->Disable(); } + +void PrinterAgentChoice::msw_rescale() +{ + Field::msw_rescale(); + + auto* field = dynamic_cast<choice_ctrl*>(window)->GetTextCtrl(); + wxSize size(wxDefaultSize); + size.SetWidth((m_opt.width > 0 ? m_opt.width : def_width_wider()) * m_em_unit); + field->SetMinSize(wxSize(-1, int(1.5f * field->GetFont().GetPixelSize().y + 0.5f))); + field->SetSize(size); + + dynamic_cast<choice_ctrl*>(window)->Rescale(); +} + void PluginField::BUILD() { auto* panel = new wxPanel(m_parent, wxID_ANY); @@ -2521,7 +2605,11 @@ void ColourPicker::BUILD() // // recast as a wxWindow to fit the calling convention window = dynamic_cast<wxWindow*>(temp); - temp->Bind(wxEVT_COLOURPICKER_CHANGED, ([this,temp](wxCommandEvent e) { + temp->Bind(wxEVT_COLOURPICKER_CHANGED, ([ + #ifdef __WXMSW__ + temp, + #endif + this](wxCommandEvent e) { #ifdef __WXMSW__ draw_bmp_btn(temp, temp->GetColour()); #endif diff --git a/src/slic3r/GUI/Field.hpp b/src/slic3r/GUI/Field.hpp index 4e9c65da5d..5d5d549427 100644 --- a/src/slic3r/GUI/Field.hpp +++ b/src/slic3r/GUI/Field.hpp @@ -385,7 +385,7 @@ public: wxWindow* window{ nullptr }; void BUILD() override; /// Propagate value from field to the OptionGroupe and Config after kill_focus/ENTER - void propagate_value() ; + void propagate_value() override; void set_value(const std::string& value, bool change_event = false) { m_disable_change_event = !change_event; @@ -440,7 +440,7 @@ public: wxWindow* window{ nullptr }; void BUILD() override; // Propagate value from field to the OptionGroupe and Config after kill_focus/ENTER - void propagate_value(); + void propagate_value() override; /* Under OSX: wxBitmapComboBox->GetWindowStyle() returns some weard value, * so let use a flag, which has TRUE value for a control without wxCB_READONLY style @@ -469,6 +469,44 @@ public: void suppress_scroll(); }; +// printer_agent is a coString whose choices come from the live agent registry. +// PrinterAgentChoice uses a ComboBox directly because Choice expects static config enums. +// Real rows carry the stored agent id in the row alias (SetItemAlias/GetItemAlias). +class PrinterAgentChoice : public Field +{ + using Field::Field; + +public: + PrinterAgentChoice(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) + { + } + + PrinterAgentChoice(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field( + parent, opt, id) + { + } + + ~PrinterAgentChoice() + { + } + + wxWindow* window{nullptr}; + + void BUILD() override; + // Clear and repopulate rows from the live registry (grouped System agents / Plugins). + // Does not change selection; the caller follows with set_value(stored id). + void reload_rows(); + + void set_value(const std::string& value, bool change_event = false); + void set_value(const boost::any& value, bool change_event = false) override; + boost::any& get_value() override; + + void enable() override; + void disable() override; + void msw_rescale() override; + wxWindow* getWindow() override { return window; } +}; + class PluginField : public Field { using Field::Field; public: diff --git a/src/slic3r/GUI/FilamentBitmapUtils.cpp b/src/slic3r/GUI/FilamentBitmapUtils.cpp index 9b43647ac0..1f51fc79b3 100644 --- a/src/slic3r/GUI/FilamentBitmapUtils.cpp +++ b/src/slic3r/GUI/FilamentBitmapUtils.cpp @@ -4,7 +4,10 @@ #include <cmath> #include "EncodedFilament.hpp" +#include "FilamentBitmapUtils.hpp" #include "GUI_App.hpp" +#include "libslic3r/FilamentMixer.hpp" +#include "libslic3r/PrintConfig.hpp" namespace Slic3r { namespace GUI { @@ -28,6 +31,113 @@ void fill_gradient_rect_east(wxDC& dc, const wxRect& rect, const wxColour& from, } } +static std::string to_hex(const wxColour& c) +{ + return wxString::Format("#%02X%02X%02X", c.Red(), c.Green(), c.Blue()).ToStdString(); +} + +wxColour blend_n_colors(const std::vector<wxColour>& cols, const std::vector<double>& weights) +{ + const size_t n = std::min(cols.size(), weights.size()); + std::vector<std::string> hex_colors; + std::vector<int> int_weights; + hex_colors.reserve(n); + int_weights.reserve(n); + for (size_t i = 0; i < n; ++i) { + hex_colors.push_back(to_hex(cols[i])); + // Scale double weights (e.g. 0.5) to int (5000) for blend_color_multi; + // only relative magnitude matters. + int_weights.push_back(static_cast<int>(std::lround(weights[i] * 10000.0))); + } + wxColour blended(Slic3r::blend_color_multi(hex_colors, int_weights)); + return blended.IsOk() ? blended : wxColour(128, 128, 128); +} + +std::vector<wxColour> sample_gradient_ramp(const wxColour& first, + const wxColour& second, + const Slic3r::GradientCurve& curve, + int steps) +{ + std::vector<wxColour> ramp; + if (steps <= 0 || curve.points.size() < 2) return ramp; + + ramp.reserve(steps); + for (int i = 0; i < steps; ++i) { + const double t = (steps > 1) ? (i + 0.5) / steps : 0.5; + const double r1 = Slic3r::sample_gradient_curve(curve, t); + ramp.push_back(blend_n_colors({first, second}, {r1, 1.0 - r1})); + } + return ramp; +} + +// Resolve the curve a gradient slot is sampled with, mirroring the slicer's fallback in +// ToolOrdering: a custom curve wins, otherwise a straight line between gradient_range's +// endpoints, otherwise the 0.10 -> 0.90 default. +static Slic3r::GradientCurve mixed_gradient_curve(const Slic3r::DynamicPrintConfig& cfg, size_t slot) +{ + const auto* curve_opt = cfg.option<ConfigOptionStrings>("filament_mixed_gradient_curve"); + if (curve_opt && slot < curve_opt->values.size() && !curve_opt->values[slot].empty()) { + Slic3r::GradientCurve custom = Slic3r::parse_gradient_curve(curve_opt->values[slot]); + if (custom.points.size() >= 2) return custom; + } + + double start = kGradientMinRatio, end = kGradientMaxRatio; + const auto* range_opt = cfg.option<ConfigOptionStrings>("filament_mixed_gradient_range"); + if (range_opt && slot < range_opt->values.size() && !range_opt->values[slot].empty()) { + CNumericLocalesSetter c_locale_setter; + float v0 = 0, v1 = 0; + if (std::sscanf(range_opt->values[slot].c_str(), "%f,%f", &v0, &v1) == 2 && + v0 > 0 && v0 < 1.0 && v1 > 0 && v1 < 1.0) { + start = v0; + end = v1; + } + } + + Slic3r::GradientCurve curve; + curve.points = {{0.0, start, NAN, NAN}, {1.0, end, NAN, NAN}}; + return curve; +} + +std::vector<wxColour> mixed_gradient_ramp(const Slic3r::DynamicPrintConfig& cfg, size_t slot, int steps) +{ + const auto* is_mixed_opt = cfg.option<ConfigOptionBools>("filament_is_mixed"); + const auto* grad_opt = cfg.option<ConfigOptionBools>("filament_mixed_gradient"); + const auto* comp_opt = cfg.option<ConfigOptionStrings>("filament_mixed_components"); + const auto* colour_opt = cfg.option<ConfigOptionStrings>("filament_colour"); + if (!is_mixed_opt || !grad_opt || !comp_opt || !colour_opt) return {}; + if (slot >= is_mixed_opt->values.size() || !is_mixed_opt->values[slot]) return {}; + if (slot >= grad_opt->values.size() || !grad_opt->values[slot]) return {}; + if (slot >= comp_opt->values.size()) return {}; + + // Only two-component slots fade; anything else stays on the plain blended swatch. + const auto comp_ids = Slic3r::parse_mixed_components(comp_opt->values[slot]); + if (comp_ids.size() != 2) return {}; + + auto component_colour = [&](unsigned int id) { + wxColour c = (id >= 1 && id <= colour_opt->values.size()) ? wxColour(colour_opt->values[id - 1]) : wxColour(); + return c.IsOk() ? c : wxColour("#D9D9D9"); + }; + + // Both gradient_range and the curve express the *first* component's ratio over Z, so + // the components stay in config order and the curve alone decides which end is which. + return sample_gradient_ramp(component_colour(comp_ids[0]), component_colour(comp_ids[1]), + mixed_gradient_curve(cfg, slot), steps); +} + +void fill_gradient_ramp_rect(wxDC& dc, const wxRect& rect, const std::vector<wxColour>& ramp) +{ + if (rect.width <= 0 || rect.height <= 0 || ramp.empty()) return; + + dc.SetPen(*wxTRANSPARENT_PEN); + for (int y = 0; y < rect.height; ++y) { + // Row 0 is the top of the rect and so takes the ramp's last entry, the model's top. + // Mapping over height - 1 puts both ends of the ramp on screen even in a short swatch. + const double t = (rect.height > 1) ? (double) (rect.height - 1 - y) / (rect.height - 1) : 0.5; + dc.SetBrush(wxBrush(ramp[static_cast<size_t>(t * (ramp.size() - 1) + 0.5)])); + dc.DrawRectangle(rect.x, rect.y + y, rect.width, 1); + } +} + // Helper struct to hold bitmap and DC struct BitmapDC { wxBitmap bitmap; @@ -47,6 +157,19 @@ static BitmapDC init_bitmap_dc(const wxSize& size) { return BitmapDC(size); } +wxBitmap create_gradient_ramp_bitmap(const std::vector<wxColour>& ramp, const wxSize& size) +{ + if (ramp.empty()) return wxNullBitmap; + + BitmapDC bdc = init_bitmap_dc(size); + if (!bdc.dc.IsOk()) return wxNullBitmap; + + fill_gradient_ramp_rect(bdc.dc, wxRect(0, 0, size.GetWidth(), size.GetHeight()), ramp); + + bdc.dc.SelectObject(wxNullBitmap); + return bdc.bitmap; +} + // Check if a color is transparent (alpha == 0) static bool is_transparent_color(const wxColour& color) { return color.Alpha() == 0; @@ -265,4 +388,65 @@ wxBitmap create_filament_bitmap(const std::vector<wxColour>& colors, const wxSiz } } +void recompute_mixed_slot_colors(std::vector<wxColour>& colors, + const Slic3r::DynamicPrintConfig& cfg) +{ + const auto* is_mixed_opt = cfg.option<ConfigOptionBools>("filament_is_mixed"); + const auto* comp_opt = cfg.option<ConfigOptionStrings>("filament_mixed_components"); + const auto* ratio_opt = cfg.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios"); + const auto* grad_opt = cfg.option<ConfigOptionBools>("filament_mixed_gradient"); + if (!is_mixed_opt || !comp_opt) return; + + const size_t n = is_mixed_opt->values.size(); + if (colors.size() < n) colors.resize(n); + + const auto* colour_opt = cfg.option<ConfigOptionStrings>("filament_colour"); + const auto kFallback = wxColour(128, 128, 128, 255); + + for (size_t i = 0; i < n; ++i) { + if (!is_mixed_opt->values[i]) continue; + + if (i >= comp_opt->values.size()) { colors[i] = kFallback; continue; } + auto comp_ids = Slic3r::parse_mixed_components(comp_opt->values[i]); + if (comp_ids.empty()) { colors[i] = kFallback; continue; } + + bool is_gradient = grad_opt && i < grad_opt->values.size() && grad_opt->values[i]; + std::vector<unsigned int> use_ids = comp_ids; + std::vector<int> weights; + + if (is_gradient && comp_ids.size() >= 2) { + use_ids = { comp_ids.front(), comp_ids.back() }; + weights = { 5000, 5000 }; + } else { + auto ratios_d = Slic3r::parse_mixed_ratios( + (ratio_opt && i < ratio_opt->values.size()) ? ratio_opt->values[i] : std::string{}, + comp_ids.size()); + weights.reserve(comp_ids.size()); + for (double r : ratios_d) + weights.push_back(static_cast<int>(std::lround(r * 10000.0))); + } + + std::vector<std::string> hex_colors; + hex_colors.reserve(use_ids.size()); + bool any_invalid = false; + for (unsigned int id : use_ids) { + if (id == 0 || id > colors.size()) { any_invalid = true; break; } + wxColour c = colors[id - 1]; + if (c.IsOk() && (c.Red() > 0 || c.Green() > 0 || c.Blue() > 0)) { + hex_colors.push_back(to_hex(c)); + } else if (colour_opt && (id - 1) < colour_opt->values.size()) { + hex_colors.push_back(colour_opt->values[id - 1]); + } else { + any_invalid = true; break; + } + } + if (any_invalid) { colors[i] = kFallback; continue; } + + std::string hex = Slic3r::blend_color_multi(hex_colors, weights); + wxColour blended(hex); + if (!blended.IsOk()) blended = kFallback; + colors[i] = wxColour(blended.Red(), blended.Green(), blended.Blue(), 255); + } +} + }} // namespace Slic3r::GUI \ No newline at end of file diff --git a/src/slic3r/GUI/FilamentBitmapUtils.hpp b/src/slic3r/GUI/FilamentBitmapUtils.hpp index 87d5b275cc..11696f3401 100644 --- a/src/slic3r/GUI/FilamentBitmapUtils.hpp +++ b/src/slic3r/GUI/FilamentBitmapUtils.hpp @@ -7,6 +7,10 @@ #include <wx/gdicmn.h> #include <vector> +// Orca: forward-declare so the header is self-contained outside libslic3r_gui's +// force-included pch (the GUI test suite includes it directly). +namespace Slic3r { class DynamicPrintConfig; struct GradientCurve; } + namespace Slic3r { namespace GUI { // Fills a rect with a west->east linear gradient by drawing solid 1px columns. @@ -28,6 +32,37 @@ wxBitmap create_filament_bitmap(const std::vector<wxColour>& colors, const wxSize& size, bool force_gradient = false); +// Blend colours at the given relative weights through blend_color_multi, so a measured +// real-world mix is used where one exists instead of a plain channel lerp. +wxColour blend_n_colors(const std::vector<wxColour>& cols, const std::vector<double>& weights); + +// Sample a gradient mixed filament the way the slicer builds it: t runs 0..1 over the +// model's height, the curve gives the first component's ratio at t, and the two +// components are blended at that ratio through blend_n_colors. Entry 0 is the bottom +// of the model, the last entry its top. +std::vector<wxColour> sample_gradient_ramp(const wxColour& first, + const wxColour& second, + const Slic3r::GradientCurve& curve, + int steps); + +// Same ramp for a project config slot, resolving components, colours and curve (or the +// linear gradient_range fallback) from cfg. Returns empty for any slot that is not a +// two-component gradient mixed filament. steps is the ramp's resolution; pass the +// destination's height in pixels. +std::vector<wxColour> mixed_gradient_ramp(const Slic3r::DynamicPrintConfig& cfg, size_t slot, int steps); + +// Fill rect with a ramp, ramp.front() along the bottom edge. +void fill_gradient_ramp_rect(wxDC& dc, const wxRect& rect, const std::vector<wxColour>& ramp); + +// Swatch bitmap for a gradient mixed filament, drawn bottom to top from the ramp. +wxBitmap create_gradient_ramp_bitmap(const std::vector<wxColour>& ramp, const wxSize& size); + +// Recompute blended representative colors for mixed (virtual) filament slots. +// Reads mixed-filament config keys from cfg and writes back into colors[i] +// for every slot where filament_is_mixed[i] is true. +void recompute_mixed_slot_colors(std::vector<wxColour>& colors, + const Slic3r::DynamicPrintConfig& cfg); + }} // namespace Slic3r::GUI #endif // slic3r_GUI_FilamentBitmapUtils_hpp_ \ No newline at end of file diff --git a/src/slic3r/GUI/FilamentMapDialog.cpp b/src/slic3r/GUI/FilamentMapDialog.cpp index 4d93002210..28757a22cf 100644 --- a/src/slic3r/GUI/FilamentMapDialog.cpp +++ b/src/slic3r/GUI/FilamentMapDialog.cpp @@ -241,7 +241,7 @@ FilamentMapDialog::FilamentMapDialog(wxWindow *parent, wxBoxSizer *mode_sizer = new wxBoxSizer(wxHORIZONTAL); - m_auto_btn = new CapsuleButton(this, PageType::ptAuto, only_saving_mode ? _L("Fila Saving") : _L("Auto"), false); + m_auto_btn = new CapsuleButton(this, PageType::ptAuto, only_saving_mode ? _L("File Saving") : _L("Auto"), false); m_manual_btn = new CapsuleButton(this, PageType::ptManual, _L("Custom"), false); if (show_default) m_default_btn = new CapsuleButton(this, PageType::ptDefault, _L("Same as Global"), true); diff --git a/src/slic3r/GUI/FilamentPickerDialog.cpp b/src/slic3r/GUI/FilamentPickerDialog.cpp index 98c955fb76..5b5af59501 100644 --- a/src/slic3r/GUI/FilamentPickerDialog.cpp +++ b/src/slic3r/GUI/FilamentPickerDialog.cpp @@ -426,11 +426,11 @@ wxScrolledWindow* FilamentPickerDialog::CreateColorGrid() }); // Hover highlight - btn->Bind(wxEVT_ENTER_WINDOW, [btn](wxMouseEvent& evt) { + btn->Bind(wxEVT_ENTER_WINDOW, [](wxMouseEvent& evt) { evt.Skip(); }); - btn->Bind(wxEVT_LEAVE_WINDOW, [btn](wxMouseEvent& evt) { + btn->Bind(wxEVT_LEAVE_WINDOW, [](wxMouseEvent& evt) { evt.Skip(); }); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index b882117aae..e5a88e4ea4 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -420,7 +420,7 @@ void GCodeViewer::SequentialView::Marker::render_position_window(const libvgcode if (properties_shown) { float label_w = 0.0f; float value_w = 0.0f; - properties_rows.reserve(13); + properties_rows.reserve(14); auto add_row = [&properties_rows, &label_w, &value_w](std::string label, std::string value) { label_w = std::max(label_w, ImGui::CalcTextSize(label.c_str()).x); value_w = std::max(value_w, ImGui::CalcTextSize(value.c_str()).x); @@ -433,6 +433,27 @@ void GCodeViewer::SequentialView::Marker::render_position_window(const libvgcode add_row(_u8L("Width"), buff); if (is_extrusion) sprintf(buff, ("%.3f " + _u8L("mm")).c_str(), vertex.height); else strcpy(buff, NA_CSTR); add_row(_u8L("Height"), buff); + // ORCA: Length of the move ending at the current vertex. Arc moves (G2/G3) are discretized + // into several vertices sharing the same gcode line id, so accumulate the whole run to report + // the arc length instead of the length of a single chord. + if (vertex_id > 0 && (is_extrusion || vertex.is_travel() || vertex.is_wipe())) { + const size_t vertices_count = viewer->get_vertices_count(); + size_t first_id = vertex_id; + while (first_id > 0 && viewer->get_vertex_at(first_id - 1).gcode_id == vertex.gcode_id) + --first_id; + size_t last_id = vertex_id; + while (last_id + 1 < vertices_count && viewer->get_vertex_at(last_id + 1).gcode_id == vertex.gcode_id) + ++last_id; + float length = 0.0f; + for (size_t i = std::max<size_t>(first_id, 1); i <= last_id; ++i) { + length += (libvgcode::convert(viewer->get_vertex_at(i).position) - + libvgcode::convert(viewer->get_vertex_at(i - 1).position)).norm(); + } + sprintf(buff, ("%.3f " + _u8L("mm")).c_str(), length); + } + else + strcpy(buff, NA_CSTR); + add_row(_u8L("Length"), buff); sprintf(buff, "%d", vertex.layer_id + 1); add_row(_u8L("Layer"), buff); sprintf(buff, ("%.1f " + _u8L("mm/s")).c_str(), vertex.feedrate); @@ -2592,7 +2613,7 @@ void GCodeViewer::render_all_plates_stats(const std::vector<const GCodeProcessor return ret; }; - auto append_item = [icon_size, &imgui, imperial_units, &window_padding, &draw_list, this](const ColorRGBA& color, const std::vector<std::pair<std::string, float>>& columns_offsets) + auto append_item = [icon_size, &imgui, &window_padding, &draw_list, this](const ColorRGBA& color, const std::vector<std::pair<std::string, float>>& columns_offsets) { // render icon ImVec2 pos = ImVec2(ImGui::GetCursorScreenPos().x + window_padding * 3, ImGui::GetCursorScreenPos().y); @@ -2627,7 +2648,7 @@ void GCodeViewer::render_all_plates_stats(const std::vector<const GCodeProcessor } ImGui::Separator(); }; - auto get_used_filament_from_volume = [this, imperial_units, &filament_diameters, &filament_densities](double volume, int extruder_id) { + auto get_used_filament_from_volume = [imperial_units, &filament_diameters, &filament_densities](double volume, int extruder_id) { double koef = imperial_units ? 1.0 / GizmoObjectManipulation::in_to_mm : 0.001; std::pair<double, double> ret = { koef * volume / (PI * sqr(0.5 * filament_diameters[extruder_id])), volume * filament_densities[extruder_id] * 0.001 }; @@ -3212,7 +3233,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv //ImVec2(pos_rect.x + ImGui::GetWindowWidth() + ImGui::GetFrameHeight(),pos_rect.y + ImGui::GetFrameHeight() + window_padding * 2.5), //ImGui::GetColorU32(ImVec4(0,0,0,0.3))); - auto append_item = [icon_size, &imgui, imperial_units, &window_padding, &draw_list, this]( + auto append_item = [icon_size, &imgui, &window_padding, &draw_list, this]( EItemType type, const ColorRGBA& color, const std::vector<std::pair<std::string, float>>& columns_offsets, @@ -3347,7 +3368,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv return ret; }; - auto calculate_offsets = [&imgui, max_width, window_padding, this](const std::vector<std::pair<std::string, std::vector<::string>>>& title_columns, float extra_size = 0.0f) { + auto calculate_offsets = [max_width, this](const std::vector<std::pair<std::string, std::vector<::string>>>& title_columns, float extra_size = 0.0f) { const ImGuiStyle& style = ImGui::GetStyle(); std::vector<float> offsets; // ORCA increase spacing for more readable format. Using direct number requires much less code change in here. GetTextLineHeight for additional spacing for icon_size @@ -3835,7 +3856,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv columns_offsets.push_back({ distance_text, offsets[3] }); if (full_layout && !count_text.empty()) columns_offsets.push_back({ count_text, distance_text.empty() ? offsets[3] : offsets[4] }); - append_item(EItemType::Rect, color, columns_offsets, true, offsets.back()/*ORCA checkbox_pos*/, visible, [this, type, visible]() { + append_item(EItemType::Rect, color, columns_offsets, true, offsets.back()/*ORCA checkbox_pos*/, visible, [this, type]() { m_viewer.toggle_option_visibility(type); update_moves_slider(); }); @@ -3892,7 +3913,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv columns_offsets.push_back({used_filaments_length[i], offsets[3]}); columns_offsets.push_back({used_filaments_weight[i], offsets[4]}); append_item(EItemType::Rect, libvgcode::convert(m_viewer.get_extrusion_role_color(role)), columns_offsets, - true, offsets.back(), visible, [this, role, visible]() { + true, offsets.back(), visible, [this, role]() { m_viewer.toggle_extrusion_role_visibility(role); update_moves_slider(); }); @@ -3911,7 +3932,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv columns_offsets.push_back({ travel_percent, offsets[2] }); columns_offsets.push_back({ travel_distance, offsets[3] }); // Usage column columns_offsets.push_back({ travel_moves, offsets[4] }); // Usage column - append_item(EItemType::Rect, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Travels)), columns_offsets, true, offsets.back()/*ORCA checkbox_pos*/, visible, [this, item, visible]() { + append_item(EItemType::Rect, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Travels)), columns_offsets, true, offsets.back()/*ORCA checkbox_pos*/, visible, [this, item]() { m_viewer.toggle_option_visibility(item); update_moves_slider(); }); @@ -3930,7 +3951,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv append_headers({ {_u8L("Options"), offsets[0] }, { _u8L("Display"), offsets[1]} }); const bool travel_visible = m_viewer.is_option_visible(libvgcode::EOptionType::Travels); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 3.0f)); - append_item(EItemType::None, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Travels)), { {_u8L("Travel"), offsets[0] }}, true, predictable_icon_pos/*ORCA checkbox_pos*/, travel_visible, [this, travel_visible]() { + append_item(EItemType::None, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Travels)), { {_u8L("Travel"), offsets[0] }}, true, predictable_icon_pos/*ORCA checkbox_pos*/, travel_visible, [this]() { m_viewer.toggle_option_visibility(libvgcode::EOptionType::Travels); // refresh(*m_gcode_result, wxGetApp().plater()->get_extruder_colors_from_plater_config(m_gcode_result)); update_moves_slider(); @@ -3947,7 +3968,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv append_headers({ {_u8L("Options"), offsets[0] }, { _u8L("Display"), offsets[1]} }); const bool travel_visible = m_viewer.is_option_visible(libvgcode::EOptionType::Travels); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 3.0f)); - append_item(EItemType::None, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Travels)), { {_u8L("Travel"), offsets[0] }}, true, predictable_icon_pos/*ORCA checkbox_pos*/, travel_visible, [this, travel_visible]() { + append_item(EItemType::None, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Travels)), { {_u8L("Travel"), offsets[0] }}, true, predictable_icon_pos/*ORCA checkbox_pos*/, travel_visible, [this]() { m_viewer.toggle_option_visibility(libvgcode::EOptionType::Travels); // refresh(*m_gcode_result, wxGetApp().plater()->get_extruder_colors_from_plater_config(m_gcode_result)); update_moves_slider(); @@ -3964,7 +3985,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv append_headers({ {_u8L("Options"), offsets[0] }, { _u8L("Display"), offsets[1]} }); const bool travel_visible = m_viewer.is_option_visible(libvgcode::EOptionType::Travels); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 3.0f)); - append_item(EItemType::None, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Travels)), { {_u8L("Travel"), offsets[0] }}, true, predictable_icon_pos/*ORCA checkbox_pos*/, travel_visible, [this, travel_visible]() { + append_item(EItemType::None, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Travels)), { {_u8L("Travel"), offsets[0] }}, true, predictable_icon_pos/*ORCA checkbox_pos*/, travel_visible, [this]() { m_viewer.toggle_option_visibility(libvgcode::EOptionType::Travels); update_moves_slider(); }); @@ -3980,7 +4001,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv append_headers({ {_u8L("Options"), offsets[0] }, { _u8L("Display"), offsets[1]} }); const bool travel_visible = m_viewer.is_option_visible(libvgcode::EOptionType::Travels); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 3.0f)); - append_item(EItemType::None, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Travels)), { {_u8L("Travel"), offsets[0] }}, true, predictable_icon_pos/*ORCA checkbox_pos*/, travel_visible, [this, travel_visible]() { + append_item(EItemType::None, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Travels)), { {_u8L("Travel"), offsets[0] }}, true, predictable_icon_pos/*ORCA checkbox_pos*/, travel_visible, [this]() { m_viewer.toggle_option_visibility(libvgcode::EOptionType::Travels); update_moves_slider(); }); @@ -4100,7 +4121,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv } float checkbox_pos = std::max(predictable_icon_pos, color_print_offsets[_u8L("Display")]); // ORCA prefer predictable_icon_pos when header not reacing end - append_item(EItemType::Rect, libvgcode::convert(tool_colors[extruder_idx]), columns_offsets, false, checkbox_pos/*ORCA*/, true, [this, extruder_idx]() {}); + append_item(EItemType::Rect, libvgcode::convert(tool_colors[extruder_idx]), columns_offsets, false, checkbox_pos/*ORCA*/, true, []() {}); } i++; } diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index a19f7bb9ae..5ba748775c 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -72,7 +72,6 @@ public: float m_model_z_offset{ 0.5f }; bool m_visible{ true }; bool m_is_dark = false; - bool m_fixed_screen_size{ false }; float m_scale_factor{ 1.0f }; #if ENABLE_ACTUAL_SPEED_DEBUG ActualSpeedImguiWidget m_actual_speed_imgui_widget; diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 13f32f2347..ace44a2d7c 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -155,6 +155,11 @@ std::string& get_filament_mixture_warning_text(){ return filament_mixture_warning_text; } +std::string& get_single_extruder_mixed_filament_warning_text(){ + static std::string single_extruder_mixed_filament_warning_text; + return single_extruder_mixed_filament_warning_text; +} + static std::string format_number(float value) { @@ -2871,6 +2876,9 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re } if (wt && (need_wipe_tower || filaments_count > 1) && !wxGetApp().plater()->only_gcode_mode() && !wxGetApp().plater()->is_gcode_3mf()) { + // The tower size estimate reads printer- and filament-scope keys, which the print preset + // does not carry; built once here rather than per plate. + const DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config(); for (int plate_id = 0; plate_id < n_plates; plate_id++) { // If print ByObject and there is only one object in the plate, the wipe tower is allowed to be generated. PartPlate* part_plate = ppl.get_plate(plate_id); @@ -2884,7 +2892,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re float x = dynamic_cast<const ConfigOptionFloats*>(proj_cfg.option("wipe_tower_x"))->get_at(plate_id); float y = dynamic_cast<const ConfigOptionFloats*>(proj_cfg.option("wipe_tower_y"))->get_at(plate_id); float w = dynamic_cast<const ConfigOptionFloat*>(m_config->option("prime_tower_width"))->value; - float a = dynamic_cast<const ConfigOptionFloat*>(proj_cfg.option("wipe_tower_rotation_angle"))->value; + float a = dynamic_cast<const ConfigOptionFloat*>(m_config->option("wipe_tower_rotation_angle"))->value; // BBS float v = dynamic_cast<const ConfigOptionFloat*>(m_config->option("prime_volume"))->value; Vec3d plate_origin = ppl.get_plate(plate_id)->get_origin(); @@ -2895,51 +2903,26 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re if (part_plate->get_objects_on_this_plate().empty()) continue; float brim_width = print->wipe_tower_data(filaments_count).brim_width; - const DynamicPrintConfig &print_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; int nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); - Vec3d wipe_tower_size = ppl.get_plate(plate_id)->estimate_wipe_tower_size(print_cfg, w, v, nozzle_nums, 0, false, dynamic_cast<const ConfigOptionBool*>(dconfig.option("enable_wrapping_detection"))->value); + Vec3d wipe_tower_size = ppl.get_plate(plate_id)->estimate_wipe_tower_size(full_config, w, v, nozzle_nums, 0, false, dynamic_cast<const ConfigOptionBool*>(dconfig.option("enable_wrapping_detection"))->value); - { - const float margin = WIPE_TOWER_MARGIN + brim_width; - BoundingBoxf3 plate_bbox = part_plate->get_bounding_box(); - BoundingBoxf plate_bbox_2d(Vec2d(plate_bbox.min(0), plate_bbox.min(1)), Vec2d(plate_bbox.max(0), plate_bbox.max(1))); - const std::vector<Pointfs> &extruder_areas = part_plate->get_extruder_areas(); - for (Pointfs points : extruder_areas) { - BoundingBoxf bboxf(points); - plate_bbox_2d.min = plate_bbox_2d.min(0) >= bboxf.min(0) ? plate_bbox_2d.min : bboxf.min; - plate_bbox_2d.max = plate_bbox_2d.max(0) <= bboxf.max(0) ? plate_bbox_2d.max : bboxf.max; - } - - coordf_t plate_bbox_x_min_local_coord = plate_bbox_2d.min(0) - plate_origin(0); - coordf_t plate_bbox_x_max_local_coord = plate_bbox_2d.max(0) - plate_origin(0); - coordf_t plate_bbox_y_max_local_coord = plate_bbox_2d.max(1) - plate_origin(1); - - if (!current_print->is_step_done(psWipeTower) || !current_print->wipe_tower_data().wipe_tower_mesh_data) { - // update for wipe tower position - { - int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview(1000 + plate_id, x + plate_origin(0), y + plate_origin(1), - (float) wipe_tower_size(0), (float) wipe_tower_size(1), (float) wipe_tower_size(2), - a, - /*!print->is_step_done(psWipeTower)*/ true, brim_width); - int volume_idx_wipe_tower_old = volume_idxs_wipe_tower_old[plate_id]; - if (volume_idx_wipe_tower_old != -1) map_glvolume_old_to_new[volume_idx_wipe_tower_old] = volume_idx_wipe_tower_new; - } - } else { - const float margin = 2.f; - auto tower_bottom = current_print->wipe_tower_data().wipe_tower_mesh_data->bottom; - tower_bottom.translate(scaled(Vec2d{x, y})); - tower_bottom.translate(scaled(Vec2d{plate_origin[0], plate_origin[1]})); - auto tower_bottom_bbox = get_extents(tower_bottom); - BoundingBoxf3 plate_bbox = wxGetApp().plater()->get_partplate_list().get_plate(plate_id)->get_build_volume(true); - BoundingBox plate_bbox2d = BoundingBox(scaled(Vec2f(plate_bbox.min[0], plate_bbox.min[1])), scaled(Vec2f(plate_bbox.max[0], plate_bbox.max[1]))); - Vec2f offset = WipeTower::move_box_inside_box(tower_bottom_bbox, plate_bbox2d, scaled(margin)); - int volume_idx_wipe_tower_new = m_volumes.load_real_wipe_tower_preview(1000 + plate_id, x + plate_origin(0), y + plate_origin(1), - current_print->wipe_tower_data().wipe_tower_mesh_data->real_wipe_tower_mesh, - current_print->wipe_tower_data().wipe_tower_mesh_data->real_brim_mesh, - true,a,/*!print->is_step_done(psWipeTower)*/ true, m_initialized); - int volume_idx_wipe_tower_old = volume_idxs_wipe_tower_old[plate_id]; - if (volume_idx_wipe_tower_old != -1) map_glvolume_old_to_new[volume_idx_wipe_tower_old] = volume_idx_wipe_tower_new; - } + // The stored position is already clamped onto the bed, by + // set_default_wipe_tower_pos_for_plate and again on every drag. + if (!current_print->is_step_done(psWipeTower) || !current_print->wipe_tower_data().wipe_tower_mesh_data) { + // update for wipe tower position + int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview(1000 + plate_id, x + plate_origin(0), y + plate_origin(1), + (float) wipe_tower_size(0), (float) wipe_tower_size(1), (float) wipe_tower_size(2), + a, + /*!print->is_step_done(psWipeTower)*/ true, brim_width); + int volume_idx_wipe_tower_old = volume_idxs_wipe_tower_old[plate_id]; + if (volume_idx_wipe_tower_old != -1) map_glvolume_old_to_new[volume_idx_wipe_tower_old] = volume_idx_wipe_tower_new; + } else { + int volume_idx_wipe_tower_new = m_volumes.load_real_wipe_tower_preview(1000 + plate_id, x + plate_origin(0), y + plate_origin(1), + current_print->wipe_tower_data().wipe_tower_mesh_data->real_wipe_tower_mesh, + current_print->wipe_tower_data().wipe_tower_mesh_data->real_brim_mesh, + true,a,/*!print->is_step_done(psWipeTower)*/ true, m_initialized); + int volume_idx_wipe_tower_old = volume_idxs_wipe_tower_old[plate_id]; + if (volume_idx_wipe_tower_old != -1) map_glvolume_old_to_new[volume_idx_wipe_tower_old] = volume_idx_wipe_tower_new; } } } @@ -3006,6 +2989,9 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re bool mix_pla_and_petg = cur_plate->check_mixture_of_pla_and_petg(full_config_temp); _set_warning_notification(EWarning::MixUsePLAAndPETG, !mix_pla_and_petg); + bool single_extruder_mixed_risk = cur_plate->check_single_extruder_mixed_filament_risk(full_config_temp, get_single_extruder_mixed_filament_warning_text()); + _set_warning_notification(EWarning::SingleExtruderMixedFilament, single_extruder_mixed_risk); + bool filament_nozzle_compatible = cur_plate->check_compatible_of_nozzle_and_filament(full_config_temp, wxGetApp().preset_bundle->filament_presets, get_nozzle_filament_incompatible_text()); _set_warning_notification(EWarning::NozzleFilamentIncompatible, !filament_nozzle_compatible); @@ -3032,6 +3018,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re _set_warning_notification(EWarning::TPUPrintableError, false); _set_warning_notification(EWarning::FilamentPrintableError, false); _set_warning_notification(EWarning::MixUsePLAAndPETG, false); + _set_warning_notification(EWarning::SingleExtruderMixedFilament, false); _set_warning_notification(EWarning::PrimeTowerOutside, false); _set_warning_notification(EWarning::MultiExtruderPrintableError,false); _set_warning_notification(EWarning::MultiExtruderHeightOutside,false); @@ -4756,7 +4743,9 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) deselect_all(); } //BBS Select plate in this 3D canvas. - else if (evt.LeftUp() && !m_mouse.dragging && m_picking_enabled && !m_hover_plate_idxs.empty() && (m_canvas_type == CanvasView3D) && !is_layers_editing_enabled()) + // The left up may come from an ImGui window (e.g. a drag started on the gizmo floating window and released over the bed), + // in which case it must not be treated as a click on the plate, otherwise the gizmo would be closed (see deselect_all below). + else if (evt.LeftUp() && !m_mouse.ignore_left_up && !m_mouse.dragging && m_picking_enabled && !m_hover_plate_idxs.empty() && (m_canvas_type == CanvasView3D) && !is_layers_editing_enabled()) { int hover_idx = m_hover_plate_idxs.front(); wxGetApp().plater()->select_plate_by_hover_id(hover_idx); @@ -6997,7 +6986,7 @@ void GLCanvas3D::_update_select_plate_toolbar_stats_item(bool force_selected) { else m_sel_plate_toolbar.show_stats_item = false; - if (force_selected && m_sel_plate_toolbar.show_stats_item) + if (force_selected && m_sel_plate_toolbar.show_stats_item && m_sel_plate_toolbar.m_all_plates_stats_item) m_sel_plate_toolbar.m_all_plates_stats_item->selected = true; } @@ -8922,7 +8911,10 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar() m_sel_plate_toolbar.m_items[i]->slice_state = IMToolbarItem::SliceState::SLICE_FAILED; } else { - if ((!is_empty && !can_slice) || (plate_list.get_plate(i)->has_printable_instances() && !plate_list.get_plate(i)->can_slice())) + // A plate using a mixed filament whose components are broken cannot be sliced, + // so surface that on the plate toolbar the same way an unsliceable plate is. + if ((!is_empty && !can_slice) || (plate_list.get_plate(i)->has_printable_instances() && !plate_list.get_plate(i)->can_slice()) + || wxGetApp().plater()->sidebar().has_broken_mixed_filament(plate_list.get_plate(i))) m_sel_plate_toolbar.m_items[i]->slice_state = IMToolbarItem::SliceState::SLICE_FAILED; else { if (plate_list.get_plate(i)->get_slicing_percent() < 0.0f) @@ -9216,7 +9208,7 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar() view3d_canvas->get_gizmos_manager().reset_all_states(); // close all gizmos view3d_canvas->reload_scene(true); } - app.mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor); + app.mainframe->select_tab(TAB_ID_PREPARE); } } }); @@ -9244,7 +9236,7 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar() ImVec2 size = ImVec2(button_width, button_height); ImVec2 end_pos = ImVec2(start_pos.x + size.x, start_pos.y + size.y); // ORCA show additional information depends on state - auto draw_info_btn = [end_pos, f_scale, margin, window_bg](std::string str, ImVec4 bg_color, ImVec4 fg_color){ + auto draw_info_btn = [end_pos, f_scale, margin](std::string str, ImVec4 bg_color, ImVec4 fg_color){ GImGui->FontSize = 15.0f * f_scale; ImVec2 txt_slice_sz = ImGui::CalcTextSize(str.c_str()); ImVec2 btn_pad = ImVec2(8.f, 1.f) * f_scale; @@ -9500,7 +9492,7 @@ void GLCanvas3D::_render_canvas_toolbar() Plater* p = wxGetApp().plater(); AppConfig* cfg = wxGetApp().app_config; - auto create_menu_item = [this, sc]( + auto create_menu_item = [sc]( const std::string& name, bool enable, bool condition, @@ -9517,7 +9509,7 @@ void GLCanvas3D::_render_canvas_toolbar() create_menu_item( _utf8(L("3D Navigator")), m_canvas_type != ECanvasType::CanvasAssembleView, // not work on assembly wxGetApp().show_3d_navigator(), - [this]{ + []{ wxGetApp().toggle_show_3d_navigator(); ImGui::CloseCurrentPopup(); // Close popup to show changes on UI } @@ -9526,7 +9518,7 @@ void GLCanvas3D::_render_canvas_toolbar() create_menu_item( _utf8(L("Zoom button")), true, // work on all wxGetApp().show_canvas_zoom_button(), - [this]{ + []{ wxGetApp().toggle_canvas_zoom_button(); ImGui::CloseCurrentPopup(); // Close popup to show changes on UI } @@ -9537,13 +9529,13 @@ void GLCanvas3D::_render_canvas_toolbar() create_menu_item( _utf8(L("Overhangs")), m_canvas_type == ECanvasType::CanvasView3D, // work only on prepare p->is_view3D_overhang_shown(), - [this, p]{p->show_view3D_overhang(!p->is_view3D_overhang_shown());} + [p]{p->show_view3D_overhang(!p->is_view3D_overhang_shown());} ); create_menu_item( _utf8(L("Outline")), m_canvas_type != ECanvasType::CanvasPreview, // not work on preview wxGetApp().show_outline(), - [this]{wxGetApp().toggle_show_outline();} + []{wxGetApp().toggle_show_outline();} ); create_menu_item( _utf8(L("Wireframe")), @@ -9555,7 +9547,7 @@ void GLCanvas3D::_render_canvas_toolbar() create_menu_item( _utf8(L("Realistic View")), m_canvas_type != ECanvasType::CanvasPreview, // not work on preview cfg->get_bool(SETTING_OPENGL_REALISTIC_MODE), - [this, &cfg]{ + [&cfg]{ cfg->set_bool(SETTING_OPENGL_REALISTIC_MODE, !cfg->get_bool(SETTING_OPENGL_REALISTIC_MODE)); cfg->save(); } @@ -9566,7 +9558,7 @@ void GLCanvas3D::_render_canvas_toolbar() create_menu_item( _utf8(L("Perspective")), true, // work on all cfg->get_bool("use_perspective_camera"), - [this, &cfg]{ + [&cfg]{ cfg->set_bool("use_perspective_camera", !(cfg->get_bool("use_perspective_camera"))); wxGetApp().update_ui_from_settings(); } @@ -9583,7 +9575,7 @@ void GLCanvas3D::_render_canvas_toolbar() create_menu_item( _utf8(L("Gridlines")), m_canvas_type != ECanvasType::CanvasAssembleView, // not work on assembly wxGetApp().show_plate_gridlines(), - [this]{wxGetApp().toggle_show_plate_gridlines();} + []{wxGetApp().toggle_show_plate_gridlines();} ); ImGui::Separator(); @@ -9591,7 +9583,7 @@ void GLCanvas3D::_render_canvas_toolbar() create_menu_item( _utf8(L("Labels")), m_canvas_type == ECanvasType::CanvasView3D, // work only on prepare p->are_view3D_labels_shown(), - [this, p]{p->show_view3D_labels(!p->are_view3D_labels_shown());} + [p]{p->show_view3D_labels(!p->are_view3D_labels_shown());} ); ImGui::PopItemFlag(); @@ -9689,6 +9681,13 @@ void GLCanvas3D::_render_paint_toolbar() const } } } + // ORCA: the loop above only labels a slot whose preset was found in the preset collection, + // while the render loop below iterates extruder_num. Pad the label arrays so a slot without a + // matching preset cannot index past them; a garbage std::string crashes ImGui::CalcTextSize. + while (int(filament_text_first_line.size()) < extruder_num) { + filament_text_first_line.emplace_back(); + filament_text_second_line.emplace_back(); + } ImGuiWrapper& imgui = *wxGetApp().imgui(); const float canvas_w = float(get_canvas_size().get_width()); @@ -9718,6 +9717,10 @@ void GLCanvas3D::_render_paint_toolbar() const bool disabled = !wxGetApp().plater()->can_fillcolor(); ColorRGBA rgba; + // Gradient mixed filaments fade over Z, so their swatch is drawn as that fade rather than + // the single blended colour in `colors`. Every other slot's ramp is empty. + const auto& gradient_ramps = wxGetApp().plater()->get_filament_gradient_ramps(); + for (int i = 0; i < extruder_num; i++) { if (i > 0) ImGui::SameLine(); @@ -9731,6 +9734,8 @@ void GLCanvas3D::_render_paint_toolbar() const if (!ImGui::IsMouseHoveringRect(left_arrow_button.Min, left_arrow_button.Max) && !ImGui::IsMouseHoveringRect(right_arrow_button.Min, right_arrow_button.Max)) wxPostEvent(m_canvas, IntEvent(EVT_GLTOOLBAR_FILLCOLOR, i + 1)); } + if (i < (int) gradient_ramps.size() && !gradient_ramps[i].empty()) + ImGuiWrapper::draw_gradient_ramp(draw_list, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), gradient_ramps[i]); if (ImGui::IsItemHovered() && i < 9) { if (!ImGui::IsMouseHoveringRect(left_arrow_button.Min, left_arrow_button.Max) && !ImGui::IsMouseHoveringRect(right_arrow_button.Min, right_arrow_button.Max)) { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 20.0f * f_scale, 10.0f * f_scale }); @@ -9746,7 +9751,13 @@ void GLCanvas3D::_render_paint_toolbar() const const float text_offset_y = 4.0f * em_unit * f_scale; for (int i = 0; i < extruder_num; i++) { - decode_color(colors[i], rgba); + // A gradient slot's swatch shows its fade instead of the blended colour in `colors`, so the + // labels take their contrast from the colour printed at the middle of the fade they sit on. + if (i < (int) gradient_ramps.size() && !gradient_ramps[i].empty()) { + const wxColour& c = gradient_ramps[i][gradient_ramps[i].size() / 2]; + rgba = ColorRGBA(c.Red(), c.Green(), c.Blue(), c.Alpha()); + } else + decode_color(colors[i], rgba); float gray = 0.299 * rgba.r_uchar() + 0.587 * rgba.g_uchar() + 0.114 * rgba.b_uchar(); ImVec4 text_color = gray < 80 ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : ImVec4(0, 0, 0, 1.0f); @@ -10590,6 +10601,9 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state) case EWarning::MixUsePLAAndPETG: text = _u8L("PLA and PETG filaments detected in the mixture. Adjust parameters according to the Wiki to ensure print quality."); break; + case EWarning::SingleExtruderMixedFilament: + text = get_single_extruder_mixed_filament_warning_text(); + break; case EWarning::PrimeTowerOutside: text = _u8L("The prime tower extends beyond the plate boundary."); break; @@ -10622,9 +10636,8 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state) wxString region = L"en"; if (language.find("zh") == 0) region = L"zh"; - // Use the generic dual-nozzle PLA+PETG guide rather than the H2D-specific page - // so the link is relevant for all dual-extrusion printers, not just Bambu H2D. (#12073) - wxGetApp().open_browser_with_warning_dialog(wxString::Format(L"https://wiki.bambulab.com/%s/filament-acc/filament/pla-and-petg-dual-extrusion", region)); + // Although this link looks like it's only for the H2D, its guidance is generic. + wxGetApp().open_browser_with_warning_dialog(wxString::Format(L"https://wiki.bambulab.com/%s/filament-acc/filament/h2d-pla-and-petg-mutual-support", region)); return false; }); } @@ -10639,6 +10652,14 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state) notification_manager.close_slicing_customize_error_notification(NotificationType::BBLNozzleFilamentIncompatible, NotificationLevel::WarningNotificationLevel); } } + else if (warning == EWarning::SingleExtruderMixedFilament) { + // Close by type: check_single_extruder_mixed_filament_risk() clears the shared text + // buffer on every call, so a close-by-text would miss once the risk is gone. + if (state) + notification_manager.push_slicing_customize_error_notification(NotificationType::BBLSingleExtruderMixedFilamentRisk, NotificationLevel::WarningNotificationLevel, text); + else + notification_manager.close_slicing_customize_error_notification(NotificationType::BBLSingleExtruderMixedFilamentRisk, NotificationLevel::WarningNotificationLevel); + } else { if (state) notification_manager.push_plater_warning_notification(text); @@ -10758,24 +10779,14 @@ bool GLCanvas3D::is_flushing_matrix_error() { if (!Sidebar::should_show_SEMM_buttons()) return false; + std::vector<int> plate_extruders = wxGetApp().plater()->get_partplate_list().get_curr_plate()->get_extruders(true); + if (plate_extruders.size() < 2) + return false; + const auto &project_config = wxGetApp().preset_bundle->project_config; const std::vector<double> &config_matrix = (project_config.option<ConfigOptionFloats>("flush_volumes_matrix"))->values; const std::vector<double> &config_multiplier = (project_config.option<ConfigOptionFloats>("flush_multiplier"))->values; - - for (auto multiplier : config_multiplier) { - if (multiplier == 0) return true; - } - - int matrix_len = config_matrix.size() / config_multiplier.size(); - int row_len = std::sqrt(matrix_len); - for (int i = 0; i < config_matrix.size(); i++) - { - int relative_id = i % matrix_len; - int row_id = relative_id / row_len; - int col_id = relative_id % row_len; - if (row_id != col_id && config_matrix[i] == 0) return true; - } - return false; + return has_zero_flush_volume_for_used_filaments(config_matrix, config_multiplier, plate_extruders); } bool GLCanvas3D::_is_any_volume_outside() const diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 7bae744098..84dbd5d652 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -391,6 +391,7 @@ class GLCanvas3D PrimeTowerOutside, NozzleFilamentIncompatible, MixtureFilamentIncompatible, + SingleExtruderMixedFilament, FlushingVolumeZero }; @@ -1119,6 +1120,10 @@ public: void set_mouse_as_dragging() { m_mouse.dragging = true; } bool is_mouse_dragging() const { return m_mouse.dragging; } + // True when the current left up event comes from an ImGui window and was not processed by it + // (e.g. a drag that started on a gizmo floating window and was released over the 3D scene). + // Such a release is the end of an ImGui interaction, not a click on the scene. + bool is_mouse_left_up_ignored() const { return m_mouse.ignore_left_up; } double get_size_proportional_to_max_bed_size(double factor) const; diff --git a/src/slic3r/GUI/GLToolbar.hpp b/src/slic3r/GUI/GLToolbar.hpp index 85413edc66..c4bc88045e 100644 --- a/src/slic3r/GUI/GLToolbar.hpp +++ b/src/slic3r/GUI/GLToolbar.hpp @@ -327,7 +327,6 @@ private: GLTexture m_icons_texture; bool m_icons_texture_dirty; mutable GLTexture m_images_texture; - mutable bool m_images_texture_dirty; BackgroundTexture m_background_texture; GLTexture m_arrow_texture; Layout m_layout; diff --git a/src/slic3r/GUI/GUI.cpp b/src/slic3r/GUI/GUI.cpp index 554ecd4a4e..78a511c90c 100644 --- a/src/slic3r/GUI/GUI.cpp +++ b/src/slic3r/GUI/GUI.cpp @@ -18,7 +18,9 @@ #import <IOKit/pwr_mgt/IOPMLib.h> #elif _WIN32 #define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX #define NOMINMAX +#endif #include <Windows.h> #include "boost/nowide/convert.hpp" #endif @@ -256,18 +258,18 @@ void change_opt_value(DynamicPrintConfig& config, const t_config_option_key& opt } } -void show_error(wxWindow* parent, const wxString& message, bool monospaced_font) +void show_error(wxWindow* parent, const wxString& message, bool has_code_excerpts) { wxGetApp().CallAfter([=] { - ErrorDialog msg(parent, message, monospaced_font); + ErrorDialog msg(parent, message, has_code_excerpts); msg.ShowModal(); }); } -void show_error(wxWindow* parent, const char* message, bool monospaced_font) +void show_error(wxWindow* parent, const char* message, bool has_code_excerpts) { assert(message); - show_error(parent, wxString::FromUTF8(message), monospaced_font); + show_error(parent, wxString::FromUTF8(message), has_code_excerpts); } void show_error_id(int id, const std::string& message) diff --git a/src/slic3r/GUI/GUI.hpp b/src/slic3r/GUI/GUI.hpp index 357fd20a97..db882b79cf 100644 --- a/src/slic3r/GUI/GUI.hpp +++ b/src/slic3r/GUI/GUI.hpp @@ -40,11 +40,11 @@ extern void add_menus(wxMenuBar *menu, int event_preferences_changed, int event_ // Change option value in config void change_opt_value(DynamicPrintConfig& config, const t_config_option_key& opt_key, const boost::any& value, int opt_index = 0); -// If monospaced_font is true, the error message is displayed using html <code><pre></pre></code> tags, -// so that the code formatting will be preserved. This is useful for reporting errors from the placeholder parser. -void show_error(wxWindow* parent, const wxString& message, bool monospaced_font = false); -void show_error(wxWindow* parent, const char* message, bool monospaced_font = false); -inline void show_error(wxWindow* parent, const std::string& message, bool monospaced_font = false) { show_error(parent, message.c_str(), monospaced_font); } +// If has_code_excerpts is true, code excerpts (a source line and the caret line below it) render +// monospaced so the caret aligns. Used for placeholder-parser errors. +void show_error(wxWindow* parent, const wxString& message, bool has_code_excerpts = false); +void show_error(wxWindow* parent, const char* message, bool has_code_excerpts = false); +inline void show_error(wxWindow* parent, const std::string& message, bool has_code_excerpts = false) { show_error(parent, message.c_str(), has_code_excerpts); } void show_error_id(int id, const std::string& message); // For Perl void show_info(wxWindow* parent, const wxString& message, const wxString& title = wxString()); void show_info(wxWindow* parent, const char* message, const char* title = nullptr); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 4c4bdfd62c..5b80a866ca 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -9,6 +9,7 @@ #include "slic3r/GUI/TaskManager.hpp" #include "format.hpp" #include "libslic3r_version.h" +#include "BuildCommit.hpp" #include "Downloader.hpp" #include <boost/chrono/duration.hpp> #include <boost/log/detail/native_typeof.hpp> @@ -521,10 +522,10 @@ static const FileWildcards file_wildcards_by_type[FT_SIZE] = { /* FT_GCODE */ { L("G-code files"), { ".gcode"sv} }, #ifdef __APPLE__ /* FT_MODEL */ - {L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".usd"sv, ".usda"sv, ".usdc"sv, ".usdz"sv, ".abc"sv, ".ply"sv, ".drc"sv}}, + {L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".gltf"sv, ".glb"sv, ".fbx"sv, ".usd"sv, ".usda"sv, ".usdc"sv, ".usdz"sv, ".abc"sv, ".ply"sv, ".drc"sv}}, #else /* FT_MODEL */ - {L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".drc"sv}}, + {L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".gltf"sv, ".glb"sv, ".fbx"sv, ".drc"sv}}, #endif /* FT_ZIP */ { L("ZIP files"), { ".zip"sv } }, /* FT_PROJECT */ { L("Project files"), { ".3mf"sv} }, @@ -813,12 +814,12 @@ void GUI_App::post_init() m_open_method = "url"; } else { if (this->init_params->input_gcode) { - mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + mainframe->select_tab(TAB_ID_PREPARE); plater_->select_view_3D("3D"); this->plater()->load_gcode(from_u8(this->init_params->input_files.front())); m_open_method = "gcode"; } else { - mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + mainframe->select_tab(TAB_ID_PREPARE); plater_->select_view_3D("3D"); wxArrayString input_files; for (auto& file : this->init_params->input_files) { @@ -852,7 +853,7 @@ void GUI_App::post_init() mainframe->Freeze(); #endif plater_->canvas3D()->enable_render(false); - mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + mainframe->select_tab(TAB_ID_PREPARE); plater_->select_view_3D("3D"); //BBS init the opengl resource here if (!plater_->canvas3D()->get_wxglcanvas()->IsShownOnScreen() || @@ -890,9 +891,9 @@ void GUI_App::post_init() } } if (is_editor()) - mainframe->select_tab(size_t(0)); + mainframe->select_tab(TAB_ID_HOME); if (app_config->get("default_page") == "1") - mainframe->select_tab(size_t(1)); + mainframe->select_tab(TAB_ID_PREPARE); #ifndef __linux__ mainframe->Thaw(); #endif @@ -1829,10 +1830,10 @@ bool GUI_App::hot_reload_network_plugin() wxWindowDisabler disabler; if (mainframe) { - int current_tab = mainframe->m_tabpanel->GetSelection(); - if (current_tab == MainFrame::TabPosition::tpMonitor) { + wxString current_tab = mainframe->m_tabpanel->GetSelectedPageName(); + if (current_tab == TAB_ID_MONITOR) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": navigating away from Monitor tab before unload"; - mainframe->m_tabpanel->SetSelection(MainFrame::TabPosition::tp3DEditor); + mainframe->m_tabpanel->SelectPageByName(TAB_ID_PREPARE); } } @@ -2166,7 +2167,6 @@ void GUI_App::init_networking_callbacks() obj->is_tunnel_mqtt = tunnel; obj->command_request_push_all(true); obj->command_get_version(); - obj->erase_user_access_code(); obj->command_get_access_code(); if (m_agent) m_agent->install_device_cert(obj->get_dev_id(), obj->is_lan_mode_printer()); @@ -2216,7 +2216,6 @@ void GUI_App::init_networking_callbacks() wxString text; if (msg == "5") { obj->set_access_code(""); - obj->erase_user_access_code(); text = wxString::Format(_L("Incorrect password")); wxGetApp().show_dialog(text); } else { @@ -2582,7 +2581,7 @@ void GUI_App::init_app_config() set_log_path_and_level(log_filename, 3); #endif - BOOST_LOG_TRIVIAL(info) << boost::format("gui mode, Current OrcaSlicer Version %1% build %2%") % SoftFever_VERSION % GIT_COMMIT_HASH; + BOOST_LOG_TRIVIAL(info) << boost::format("gui mode, Current OrcaSlicer Version %1% build %2%") % SoftFever_VERSION % build_commit_label; //BBS: remove GCodeViewer as seperate APP logic if (!app_config) @@ -2643,7 +2642,7 @@ std::string GUI_App::get_bbl_client_version() void GUI_App::on_start_subscribe_again(std::string dev_id) { auto start_subscribe_timer = new wxTimer(this, wxID_ANY); - Bind(wxEVT_TIMER, [this, start_subscribe_timer, dev_id](auto& e) { + Bind(wxEVT_TIMER, [start_subscribe_timer, dev_id](auto& e) { start_subscribe_timer->Stop(); Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); if (!dev) return; @@ -2809,16 +2808,68 @@ void GUI_App::init_plugin_gui_wiring() }); }; + // why: a newly loaded plugin only adds a selectable agent + // refresh the dropdown and leave the live agent alone + auto refresh_printer_agent_dropdown_after_load = [](const std::string&) + { + if (!wxTheApp) + return; + + GUI_App* app = &GUI::wxGetApp(); + if (app->is_closing()) + return; + + app->CallAfter([app] + { + if (!app->is_closing()) + app->refresh_printer_agent_dropdown(); + }); + }; + + // why: the unloaded plugin may have been the provider of the live agent + // re-run selection, where a now-missing agent will be cleared + // refresh dropdown after + auto switch_printer_agent_after_unload = [](const std::string&) + { + if (!wxTheApp) + return; + + GUI_App* app = &GUI::wxGetApp(); + if (app->is_closing()) + return; + + app->CallAfter([app] { + if (app->is_closing()) + return; + + app->switch_printer_agent(); + app->refresh_printer_agent_dropdown(); + }); + }; + plugin_mgr.subscribe_on_unload_callback(PluginHostUi::close_windows_for_plugin); plugin_mgr.subscribe_on_load_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); }); plugin_mgr.subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); }); plugin_mgr.subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin); plugin_mgr.subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin); + plugin_mgr.subscribe_on_load_callback([](const std::string& plugin_key) { + if (wxTheApp == nullptr || wxGetApp().is_closing() || wxGetApp().mainframe == nullptr) + return; + wxGetApp().mainframe->plugin_pages().on_plugin_register(plugin_key); + }); + plugin_mgr.subscribe_on_unload_callback([](const std::string& plugin_key) { + if (wxTheApp == nullptr || wxGetApp().is_closing() || wxGetApp().mainframe == nullptr) + return; + wxGetApp().mainframe->plugin_pages().on_plugin_deregister(plugin_key); + }); + plugin_mgr.subscribe_on_load_callback(refresh_printer_agent_dropdown_after_load); + plugin_mgr.subscribe_on_unload_callback(switch_printer_agent_after_unload); plugin_mgr.subscribe_on_capability_load_callback( - [refresh_plugins_dialog](const PluginCapabilityId& capability) { + [refresh_plugins_dialog, refresh_printer_agent_dropdown_after_load](const PluginCapabilityId& capability) { if (capability.type == PluginCapabilityType::PrinterConnection) NetworkAgentFactory::register_python_printer_agent(capability.plugin_key, capability.name); refresh_plugins_dialog(); + refresh_printer_agent_dropdown_after_load(capability.plugin_key); // A newly loaded capability may satisfy a missing-plugin notification; re-validate the // current plate (on the UI thread) so the notification clears once its plugin is available. if (wxTheApp && !wxGetApp().is_closing()) @@ -2826,12 +2877,17 @@ void GUI_App::init_plugin_gui_wiring() if (Plater* plater = wxGetApp().plater()) plater->revalidate_current_plate_if_plugins_missing(); }); + if (capability.type == PluginCapabilityType::Pages && wxTheApp && !wxGetApp().is_closing() && wxGetApp().mainframe) + wxGetApp().mainframe->plugin_pages().on_cap_register(capability); }); plugin_mgr.subscribe_on_capability_unload_callback( - [refresh_plugins_dialog](const PluginCapabilityId& capability) { + [refresh_plugins_dialog, switch_printer_agent_after_unload](const PluginCapabilityId& capability) { if (capability.type == PluginCapabilityType::PrinterConnection) NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name); + if (capability.type == PluginCapabilityType::Pages && wxTheApp && !wxGetApp().is_closing() && wxGetApp().mainframe) + wxGetApp().mainframe->plugin_pages().on_cap_deregister(capability); refresh_plugins_dialog(); + switch_printer_agent_after_unload(capability.plugin_key); }); } @@ -3176,7 +3232,7 @@ bool GUI_App::on_init_inner() } }); - Bind(EVT_SHOW_NO_NEW_VERSION, [this](const wxCommandEvent& evt) { + Bind(EVT_SHOW_NO_NEW_VERSION, [](const wxCommandEvent& evt) { wxString msg = _L("This is the newest version."); InfoDialog dlg(nullptr, _L("Info"), msg); dlg.ShowModal(); @@ -3232,15 +3288,12 @@ bool GUI_App::on_init_inner() } } */ - copy_network_if_available(); if (scrn) { scrn->SetText(_L("Loading Plugins") + dots, 20); wxYield(); } - on_init_network(); - // Initialize plugins after network then register on_load callbacks so once the plugin loads finish, it gets registered automatically. // initialize() also installs the libslic3r hooks (capability resolver, // slicing-pipeline dispatcher) via plugin_hooks::install() -- no @@ -3269,6 +3322,9 @@ bool GUI_App::on_init_inner() } } + copy_network_if_available(); + on_init_network(); + if (m_agent) plugin_mgr.set_cloud_agent(std::dynamic_pointer_cast<OrcaCloudServiceAgent>(m_agent->get_cloud_agent())); @@ -3341,7 +3397,7 @@ bool GUI_App::on_init_inner() mainframe = new MainFrame(); // hide settings tabs after first Layout if (is_editor()) { - mainframe->select_tab(size_t(0)); + mainframe->select_tab(TAB_ID_HOME); } sidebar().obj_list()->init(); @@ -3873,6 +3929,54 @@ unsigned GUI_App::get_colour_approx_luma(const wxColour &colour) )); } +void GUI_App::refresh_printer_agent_dropdown() +{ + if (Tab* tab = get_tab(Preset::TYPE_PRINTER)) + { + if (auto* printer_tab = dynamic_cast<TabPrinter*>(tab)) + printer_tab->refresh_printer_agent_dropdown(); + } +} + +void GUI_App::set_live_printer_agent(std::shared_ptr<IPrinterAgent> agent) +{ + if (!m_agent) + return; + + // why: tearing down the old machine selection is only ever the prefix of setting the live + // agent (to a new one, or to null when the selection is missing) - so it lives here, not as + // a standalone helper. Pass nullptr to clear the selection. + if (DeviceManager* dev = getDeviceManager()) + { + dev->set_selected_machine(""); // why: empty id disconnects and deselects the current machine + m_agent->set_user_selected_machine(""); + // note: belt-and-suspenders (precedent: DeviceManagerRefresher::on_timer) + dev->OnSelectedMachineLost(); // why: clear stale sidebar sync-status / AMS + // why: drop stale LAN discoveries; keep My Devices, but only those belonging to the + // agent we're about to swap to, so a device stamped by the outgoing agent doesn't + // linger hidden - the new agent's start_discovery re-inserts and re-stamps it fresh. + // agent is null when clearing the live agent entirely (e.g. plugin unload); there's no + // target to filter against then, so fall back to the original "keep all My Devices" + // behavior rather than guessing. + dev->clear_other_devices(agent ? agent->get_agent_info().id : std::string()); + } + + m_agent->set_printer_agent(agent); + sidebar().update_all_preset_comboboxes(); +} + +std::string GUI_App::resolve_printer_agent_id(const std::string& stored_id) +{ + if (!stored_id.empty()) + return stored_id; + return (preset_bundle && preset_bundle->is_bbl_vendor()) ? BBL_PRINTER_AGENT_ID : ORCA_PRINTER_AGENT_ID; +} + +std::string GUI_App::canonical_printer_agent_id(const std::string& picked_id) +{ + return picked_id == resolve_printer_agent_id("") ? std::string() : picked_id; +} + void GUI_App::switch_printer_agent() { if (!m_agent) { @@ -3880,24 +3984,17 @@ void GUI_App::switch_printer_agent() return; } - // Read printer_agent from config, falling back to default - std::string effective_agent_id = ORCA_PRINTER_AGENT_ID; - if (preset_bundle->is_bbl_vendor()) - effective_agent_id = BBL_PRINTER_AGENT_ID; - const DynamicPrintConfig& config = preset_bundle->printers.get_edited_preset().config; - if (config.has("printer_agent")) { - const std::string& value = config.option<ConfigOptionString>("printer_agent")->value; - if (!value.empty()) - effective_agent_id = value; - } + const std::string effective_agent_id = resolve_printer_agent_id(config.opt_string("printer_agent")); // Check if agent is registered const PrinterAgentInfo* agent_info_ptr = NetworkAgentFactory::get_printer_agent_info(effective_agent_id); if (!agent_info_ptr) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": unregistered agent ID '" << effective_agent_id - << "', keeping current agent"; - // Keep current agent, don't switch + // why: the selected agent's provider is gone (e.g. plugin unloaded); leaving the old + // live agent up would keep talking to a machine the user can no longer select. + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": agent ID '" << effective_agent_id + << "' is unregistered; clearing live printer agent"; + set_live_printer_agent(nullptr); return; } const PrinterAgentInfo agent_info = *agent_info_ptr; @@ -3911,7 +4008,9 @@ void GUI_App::switch_printer_agent() NetworkAgentFactory::create_printer_agent_by_id(effective_agent_id, cloud_agent, log_dir); if (!new_printer_agent) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to create agent '" << effective_agent_id << "', keeping current agent"; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to create agent '" << effective_agent_id + << "'; clearing live printer agent"; + set_live_printer_agent(nullptr); return; } @@ -3934,9 +4033,9 @@ void GUI_App::switch_printer_agent() return; } - // Swap the agent - m_agent->set_printer_agent(new_printer_agent); - sidebar().update_all_preset_comboboxes(); + // Swap the agent; set_live_printer_agent resets the device selection so the new + // agent starts clean (#124). + set_live_printer_agent(new_printer_agent); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": printer agent switched to " << effective_agent_id; @@ -4512,7 +4611,7 @@ void GUI_App::recreate_GUI(const wxString &msg_name) mainframe = new MainFrame(); if (is_editor()) // hide settings tabs after first Layout - mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + mainframe->select_tab(TAB_ID_PREPARE); // Propagate model objects to object list. sidebar().obj_list()->init(); //sidebar().aux_list()->init_auxiliary(); @@ -5158,7 +5257,7 @@ std::string GUI_App::handle_web_request(std::string cmd) } } else if (command_str.compare("begin_network_plugin_download") == 0) { - CallAfter([this] { wxGetApp().ShowDownNetPluginDlg(); }); + CallAfter([] { wxGetApp().ShowDownNetPluginDlg(); }); } else if (command_str.compare("get_web_shortcut") == 0) { if (root.get_child_optional("key_event") != boost::none) { @@ -6719,6 +6818,12 @@ void GUI_App::add_pending_vendor_preset(const std::pair<std::string, std::map<st // Add the corresponding vendor std::string vendor_name = PresetBundle::find_preset_vendor(inherits_name, type); + if (vendor_name.empty()) { + // No vendor ships this preset's parent. An unnamed entry here becomes an + // unnamed bundle at install time, which nothing can install. + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": no vendor carries " << inherits_name << ", skipping"; + return; + } if (need_add_vendors.find(vendor_name) == need_add_vendors.end()) need_add_vendors[vendor_name] = std::map<std::string, std::set<std::string>>(); @@ -8193,7 +8298,7 @@ bool GUI_App::show_modal_ip_address_enter_dialog(bool input_sn, wxString title) dlg.set_machine_obj(obj); if (!title.empty()) dlg.update_title(title); - dlg.Bind(EVT_ENTER_IP_ADDRESS, [this, obj](wxCommandEvent& e) { + dlg.Bind(EVT_ENTER_IP_ADDRESS, [obj](wxCommandEvent& e) { auto selection_data_arr = wxSplit(e.GetString().ToStdString(), '|'); if (selection_data_arr.size() == 2) { @@ -8206,7 +8311,7 @@ bool GUI_App::show_modal_ip_address_enter_dialog(bool input_sn, wxString title) wxGetApp().app_config->save(); obj->set_dev_ip(ip_address.ToStdString()); - obj->set_user_access_code(access_code.ToStdString()); + obj->set_access_code(access_code.ToStdString()); } } }); @@ -8673,7 +8778,7 @@ bool GUI_App::check_and_keep_current_preset_changes(const wxString& caption, con if (!no_need_change && dlg.ShowModal() == wxID_CANCEL) return false; - auto reset_modifications = [this, is_called_from_configwizard]() { + auto reset_modifications = [this]() { //if (is_called_from_configwizard) // return; // no need to discared changes. It will be done fromConfigWizard closing @@ -8801,7 +8906,17 @@ void GUI_App::load_current_presets(bool active_preset_combox/*= false*/, bool ch if (printer_technology == ptFFF && !edited_printer_preset.config.opt_bool("single_extruder_multi_material")) { auto* nozzle_diameter = edited_printer_preset.config.option<ConfigOptionFloats>("nozzle_diameter"); if (nozzle_diameter) { - preset_bundle->set_num_filaments(nozzle_diameter->values.size()); + // Mixed-color slots are virtual filaments kept at the tail of the list, so they have no + // nozzle of their own and the count has to allow for them. Only ever grow: this sizes + // the list so the combo boxes have something to bind to, and set_num_filaments() trims + // at the raw tail, so shrinking here would eat the mixes rather than the surplus + // physical slots. A list longer than the nozzle count is a state the app reaches + // legitimately - raising the extruder count and not saving the printer preset leaves + // exactly that on the next start - and losing the project's mixes to it is worse than + // carrying a filament the printer has no nozzle for until the count is next changed. + const size_t target = nozzle_diameter->values.size() + preset_bundle->num_mixed_filaments(); + if (target > preset_bundle->filament_presets.size()) + preset_bundle->set_num_filaments(target); } } this->plater()->set_printer_technology(printer_technology); @@ -9771,7 +9886,7 @@ bool GUI_App::check_url_association(std::wstring url_prefix, std::wstring& reg_b { reg_bin = L""; #ifdef WIN32 - wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix + "\\shell\\open\\command"); + wxRegKey key_full(wxRegKey::HKCU, L"Software\\Classes\\" + url_prefix + L"\\shell\\open\\command"); if (!key_full.Exists()) { return false; } @@ -9797,8 +9912,8 @@ void GUI_App::associate_url(std::wstring url_prefix) wxString key_string = "\"" + wbinary + "\" \"%1\""; - wxRegKey key_first(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix); - wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix + "\\shell\\open\\command"); + wxRegKey key_first(wxRegKey::HKCU, L"Software\\Classes\\" + url_prefix); + wxRegKey key_full(wxRegKey::HKCU, L"Software\\Classes\\" + url_prefix + L"\\shell\\open\\command"); if (!key_first.Exists()) { key_first.Create(false); } @@ -9818,7 +9933,7 @@ void GUI_App::disassociate_url(std::wstring url_prefix) #ifdef WIN32 if (is_running_in_msix()) return; - wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix + "\\shell\\open\\command"); + wxRegKey key_full(wxRegKey::HKCU, L"Software\\Classes\\" + url_prefix + L"\\shell\\open\\command"); if (!key_full.Exists()) { return; } diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index bda27d40ec..8bf32df64c 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -365,9 +365,14 @@ public: HMSQuery* get_hms_query() { return hms_query; } NetworkAgent* getAgent() { return m_agent; } - // Dynamic printer agent switching + // Reconcile the live printer agent with the stored preset selection. void switch_printer_agent(); + std::string resolve_printer_agent_id(const std::string& stored_id); + // ORCA TODO: in the future, bbl presets should specify "bbl" printer agent id + // then, all resolve and canonical would just be ORCA<->"" + std::string canonical_printer_agent_id(const std::string& picked_id); + FilamentColorCodeQuery* get_filament_color_code_query(); bool is_editor() const { return m_app_mode == EAppMode::Editor; } bool is_gcode_viewer() const { return m_app_mode == EAppMode::GCodeViewer; } @@ -798,6 +803,11 @@ private: void window_pos_center(wxTopLevelWindow *window); bool select_language(); + // Dynamic printer agent selection - internal helpers for switch_printer_agent + // and the plugin load/unload callbacks (init_plugin_gui_wiring). + void refresh_printer_agent_dropdown(); + void set_live_printer_agent(std::shared_ptr<IPrinterAgent> agent); // null clears the selection + bool config_wizard_startup(); void check_updates(const bool verbose); diff --git a/src/slic3r/GUI/GUI_AuxiliaryList.cpp b/src/slic3r/GUI/GUI_AuxiliaryList.cpp index 1fa5b194ec..be9f99a76a 100644 --- a/src/slic3r/GUI/GUI_AuxiliaryList.cpp +++ b/src/slic3r/GUI/GUI_AuxiliaryList.cpp @@ -217,7 +217,7 @@ void AuxiliaryList::on_context_menu(wxDataViewEvent& evt) } else { append_menu_item(menu, wxID_ANY, _L("Open"), wxEmptyString, - [this, node](wxCommandEvent&) + [node](wxCommandEvent&) { wxLaunchDefaultApplication(node->path, 0); }); diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 5254492e5d..d8e54978fa 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -1414,7 +1414,7 @@ void MenuFactory::create_default_menu() append_menu_check_item(&m_default_menu, wxID_ANY, _L("Show Labels"), "", [](wxCommandEvent&) { plater()->show_view3D_labels(!plater()->are_view3D_labels_shown()); plater()->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, &m_default_menu, - []() { return plater()->is_view3D_shown(); }, [this]() { return plater()->are_view3D_labels_shown(); }, m_parent); + []() { return plater()->is_view3D_shown(); }, []() { return plater()->are_view3D_labels_shown(); }, m_parent); } void MenuFactory::create_common_object_menu(wxMenu* menu) @@ -1656,16 +1656,16 @@ void MenuFactory::create_filament_action_menu(bool init, int active_filament_men { wxMenu *menu = &m_filament_action_menu; - if (init) { + // ORCA rebuild menu everytime instead checking existing of every item then deleting + while (menu->GetMenuItemCount() > 0) + menu->Destroy(menu->FindItemByPosition(0)); + + //if (init) { // append_menu_item( menu, wxID_ANY, _L("Edit"), "", [](wxCommandEvent&) { plater()->sidebar().edit_filament(); }, "", nullptr, []() { return true; }, m_parent); - } - - const int item_id = menu->FindItem(_L("Merge with")); - if (item_id != wxNOT_FOUND) - menu->Destroy(item_id); + //} wxMenu* sub_menu = new wxMenu(); std::vector<wxBitmap*> icons = get_extruder_color_icons(true); @@ -1684,11 +1684,15 @@ void MenuFactory::create_filament_action_menu(bool init, int active_filament_men append_submenu(menu, sub_menu, wxID_ANY, _L("Merge with"), "", "", [filaments_cnt]() { return filaments_cnt > 1; }, m_parent); + // Decompose a target colour into a printable mix of the loaded filaments. Placed before the + append_menu_item( + menu, wxID_ANY, _L("Decompose Color"), "", [](wxCommandEvent&) { + plater()->sidebar().decompose_filament_color(kSidebarContextMenuFilamentId); }, "", nullptr, + []() { return plater()->sidebar().combos_filament().size() >= 2; }, m_parent); + + menu->AppendSeparator(); // ORCA use seperator for reducing accidental clicks to delete + // ORCA use delete item on end of menu to prevent accidental clicks. clicking to submenus(merge) already not allowed by OS - const int delete_id = menu->FindItem(_L("Delete")); - if (delete_id != wxNOT_FOUND) - menu->Destroy(delete_id); - append_menu_item( menu, wxID_ANY, _L("Delete"), _L("Delete this filament"), [](wxCommandEvent&) { plater()->sidebar().delete_filament(-2); }, "", nullptr, @@ -2086,7 +2090,7 @@ void MenuFactory::append_menu_item_clone(wxMenu* menu) static const wxString ctrl = _L("Ctrl+"); #endif append_menu_item(menu, wxID_ANY, _L("Clone") + "\t" + ctrl + "K", "", - [this](wxCommandEvent&) { + [](wxCommandEvent&) { plater()->clone_selection(); }, "", nullptr, []() { @@ -2111,7 +2115,7 @@ void MenuFactory::append_menu_item_smooth_mesh(wxMenu *menu) void MenuFactory::append_menu_item_center(wxMenu* menu) { append_menu_item(menu, wxID_ANY, _L("Center") , "", - [this](wxCommandEvent&) { + [](wxCommandEvent&) { plater()->center_selection(); }, "", nullptr, []() { @@ -2130,7 +2134,7 @@ void MenuFactory::append_menu_item_center(wxMenu* menu) void MenuFactory::append_menu_item_drop(wxMenu* menu) { append_menu_item(menu, wxID_ANY, _L("Drop") , "", - [this](wxCommandEvent&) { + [](wxCommandEvent&) { plater()->drop_selection(); }, "", nullptr, []() { @@ -2305,7 +2309,7 @@ void MenuFactory::append_menu_item_set_printable(wxMenu* menu) } } - wxMenuItem* menu_item_set_printable = append_menu_check_item(menu, wxID_ANY, _L("Printable") + "\t" + "V", "", [this, all_printable](wxCommandEvent&) { + wxMenuItem* menu_item_set_printable = append_menu_check_item(menu, wxID_ANY, _L("Printable") + "\t" + "V", "", [all_printable](wxCommandEvent&) { Selection& selection = plater()->canvas3D()->get_selection(); selection.set_printable(!all_printable); }, menu); @@ -2325,7 +2329,7 @@ void MenuFactory::append_menu_item_set_auto_drop(wxMenu* menu) wxString menu_tooltip = _L("Automatically snaps the selected object to the build plate."); wxMenuItem* menu_item_set_auto_drop = append_menu_check_item( menu, wxID_ANY, menu_text, menu_tooltip, - [this, current_auto_drop](wxCommandEvent&) { + [current_auto_drop](wxCommandEvent&) { Selection& selection = plater()->canvas3D()->get_selection(); selection.set_auto_drop(!current_auto_drop); }, @@ -2380,7 +2384,7 @@ void MenuFactory::append_menu_item_plate_name(wxMenu *menu) auto item = append_menu_item( menu, wxID_ANY, name, "", - [plate](wxCommandEvent &e) { + [](wxCommandEvent &e) { int hover_idx =plater()->canvas3D()->GetHoverId(); if (hover_idx == -1) { int plate_idx=plater()->GetPlateIndexByRightMenuInLeftUI(); diff --git a/src/slic3r/GUI/GUI_ObjectLayers.cpp b/src/slic3r/GUI/GUI_ObjectLayers.cpp index 3b36158bd1..93bdeec9ee 100644 --- a/src/slic3r/GUI/GUI_ObjectLayers.cpp +++ b/src/slic3r/GUI/GUI_ObjectLayers.cpp @@ -48,7 +48,7 @@ void ObjectLayers::select_editor(LayerRangeEditor* editor, const bool is_last_ed * And as a result we couldn't edit this control. * */ #ifdef __WXOSX__ - wxTheApp->CallAfter([editor]() { + wxTheApp->CallAfter([]() { #endif //editor->SetFocus(); //editor->SelectAll(); @@ -223,7 +223,7 @@ void ObjectLayers::update_layers_list() // only call sizer->Clear(true) via CallAfter, otherwise crash happens in Linux when press enter in Height Range // because an element cannot be destroyed while there are pending events for this element.(https://github.com/wxWidgets/Phoenix/issues/1854) - wxGetApp().CallAfter([this, type, objects_ctrl, range]() { + wxGetApp().CallAfter([this, type, range]() { m_og->ctrl_parent()->Freeze(); // Delete all controls from options group diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index b23b554a74..8067afd8df 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -2324,7 +2324,7 @@ void ObjectList::load_modifier(const wxArrayString& input_files, ModelObject& mo bool split_compound = wxGetApp().app_config->get_bool("is_split_compound"); model = Model::read_from_step( input_file, LoadStrategy::LoadModel, nullptr, nullptr, - [this, &is_user_cancel, &linear, &angle, &split_compound](Slic3r::Step& file, double& linear_value, + [&is_user_cancel, &linear, &angle, &split_compound](Slic3r::Step& file, double& linear_value, double& angle_value, bool& is_split) -> int { if (wxGetApp().app_config->get_bool("enable_step_mesh_setting")) { StepMeshDialog mesh_dlg(nullptr, file, linear, angle); @@ -3213,7 +3213,7 @@ void ObjectList::merge(bool to_multipart_object) //changed_object(obj_idx); //remove(); } - /* wxGetApp().plater()->load_model_objects(objects); + // wxGetApp().plater()->load_model_objects(objects); Selection& selection = p->view3D->get_canvas3d()->get_selection(); size_t last_obj_idx = p->model.objects.size() - 1; @@ -3233,6 +3233,24 @@ void ObjectList::merge(bool to_multipart_object) void ObjectList::layers_editing() { + // Height ranges give each range its own layer height, varying the mixed sub-layer heights just + // like an adaptive profile, so this raises the same warning as variable layer height and shares + // its do-not-show-again flag. + const auto& print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + if (print_config.opt_bool("enable_mixed_color_sublayer")) { + if (wxGetApp().app_config->get("no_warn_mixed_sublayer_variable_layer") != "1") { + // Orca: parent to the plater like the sibling site in Plater::priv::on_action_layersediting + // (BBS passes nullptr, which MsgDialog remaps to the main frame). + MessageDialog dlg(wxGetApp().plater(), + _L("Using variable layer height together with mixed color sublayer may result in poor color mixing quality."), + _L("Warning"), wxICON_WARNING | wxOK); + dlg.show_dsa_button(); + dlg.ShowModal(); + if (dlg.get_checkbox_state()) + wxGetApp().app_config->set("no_warn_mixed_sublayer_variable_layer", "1"); + } + } + const Selection& selection = scene_selection(); const int obj_idx = selection.get_object_idx(); wxDataViewItem item = obj_idx >= 0 && GetSelectedItemsCount() > 1 && selection.is_single_full_object() ? @@ -3299,7 +3317,7 @@ void ObjectList::boolean() } } - TriangleMesh mesh = Plater::combine_mesh_fff(*object, -1, [this](const std::string& msg) {return wxGetApp().notification_manager()->push_plater_error_notification(msg); }); + TriangleMesh mesh = Plater::combine_mesh_fff(*object, -1, [](const std::string& msg) {return wxGetApp().notification_manager()->push_plater_error_notification(msg); }); // add mesh to model as a new object, keep the original object's name and config Model* model = object->get_model(); @@ -6249,7 +6267,7 @@ void GUI::ObjectList::smooth_mesh() get_selection_indexes(obj_idxs, vol_idxs); auto object_idx = obj_idxs.front(); ModelObject *obj{nullptr}; - auto show_warning_dlg = [this](int cur_face_count,std::string name,bool is_part) { + auto show_warning_dlg = [](int cur_face_count,std::string name,bool is_part) { int limit_face_count = 1000000; if (cur_face_count > limit_face_count) { auto name_str = wxString::FromUTF8(name); @@ -6262,7 +6280,7 @@ void GUI::ObjectList::smooth_mesh() } return false; }; - auto show_smooth_mesh_error_dlg = [this](std::string name) { + auto show_smooth_mesh_error_dlg = [](std::string name) { auto name_str = wxString::FromUTF8(name); auto content = wxString::Format(_L("\"%s\" part's mesh contains errors. Please repair it first."), name_str); WarningDialog dlg(static_cast<wxWindow *>(wxGetApp().mainframe), content, wxEmptyString, wxOK); diff --git a/src/slic3r/GUI/GUI_ObjectSettings.cpp b/src/slic3r/GUI/GUI_ObjectSettings.cpp index 65cd99a2fa..25e6204a40 100644 --- a/src/slic3r/GUI/GUI_ObjectSettings.cpp +++ b/src/slic3r/GUI/GUI_ObjectSettings.cpp @@ -139,7 +139,7 @@ bool ObjectSettings::update_settings_list() optgroup->sidetext_width = 5; optgroup->m_on_change = [this, config](const t_config_option_key& opt_id, const boost::any& value) { - this->update_config_values(config); + this->update_config_values(config, opt_id); wxGetApp().obj_list()->changed_object(); }; // call back for rescaling of the extracolumn control @@ -325,7 +325,7 @@ bool ObjectSettings::add_missed_options(ModelConfig* config_to, const DynamicPri return is_added; } -void ObjectSettings::update_config_values(ModelConfig* config) +void ObjectSettings::update_config_values(ModelConfig* config, const std::string& changed_opt_key) { const auto objects_model = wxGetApp().obj_list()->GetModel(); const auto item = wxGetApp().obj_list()->GetSelection(); @@ -403,6 +403,10 @@ void ObjectSettings::update_config_values(ModelConfig* config) } main_config.apply(config->get(), true); + + if (printer_technology == ptFFF && changed_opt_key == "layer_height") + config_manipulation.check_layer_height(&main_config); + printer_technology == ptFFF ? config_manipulation.update_print_fff_config(&main_config) : config_manipulation.update_print_sla_config(&main_config) ; diff --git a/src/slic3r/GUI/GUI_ObjectSettings.hpp b/src/slic3r/GUI/GUI_ObjectSettings.hpp index 8903f8748b..21146425cc 100644 --- a/src/slic3r/GUI/GUI_ObjectSettings.hpp +++ b/src/slic3r/GUI/GUI_ObjectSettings.hpp @@ -66,7 +66,7 @@ public: * we should add sparse_infill_pattern to avoid endless loop in update */ bool add_missed_options(ModelConfig *config_to, const DynamicPrintConfig &config_from); - void update_config_values(ModelConfig *config); + void update_config_values(ModelConfig *config, const std::string& changed_opt_key = ""); void UpdateAndShow(const bool show); void msw_rescale(); void sys_color_changed(); diff --git a/src/slic3r/GUI/GUI_ObjectTable.cpp b/src/slic3r/GUI/GUI_ObjectTable.cpp index 1fc07bf5a2..35508c6113 100644 --- a/src/slic3r/GUI/GUI_ObjectTable.cpp +++ b/src/slic3r/GUI/GUI_ObjectTable.cpp @@ -3520,13 +3520,13 @@ void GridCellTextEditor::BeginEdit(int row, int col, wxGrid *grid) Text()->GetTextCtrl()->SetInsertionPointEnd(); - m_control->Bind(wxEVT_TEXT_ENTER, [this, row, col, grid](wxCommandEvent &e) { + m_control->Bind(wxEVT_TEXT_ENTER, [grid](wxCommandEvent &e) { grid->HideCellEditControl(); grid->SaveEditControlValue(); e.Skip(); }); - m_control->Bind(wxEVT_CHAR_HOOK, [this, row, col, grid](wxKeyEvent &e) { + m_control->Bind(wxEVT_CHAR_HOOK, [grid](wxKeyEvent &e) { if (e.GetKeyCode() == WXK_ESCAPE) { grid->HideCellEditControl(); grid->SaveEditControlValue(); diff --git a/src/slic3r/GUI/GUI_ObjectTableSettings.cpp b/src/slic3r/GUI/GUI_ObjectTableSettings.cpp index 37ccfb8e41..2290018419 100644 --- a/src/slic3r/GUI/GUI_ObjectTableSettings.cpp +++ b/src/slic3r/GUI/GUI_ObjectTableSettings.cpp @@ -184,7 +184,7 @@ bool ObjectTableSettings::update_settings_list(bool is_object, bool is_multiple_ btn->Bind(EVT_LOCK_ENABLE, [this, btn](auto &e) { btn->SetBitmap(m_bmp_reset_focus.bmp()); }); #endif - btn->Bind(wxEVT_BUTTON, [btn, opt_key, this, is_object, object, config, group_category](wxEvent &event) { + btn->Bind(wxEVT_BUTTON, [opt_key, this, is_object, object, config, group_category](wxEvent &event) { //wxGetApp().plater()->take_snapshot(from_u8((boost::format(_utf8(L("Reset Option %s"))) % opt_key).str())); config->erase(opt_key); //btn->Hide(); @@ -204,10 +204,13 @@ bool ObjectTableSettings::update_settings_list(bool is_object, bool is_multiple_ this->m_parent->Thaw(); #ifdef __WXOSX_MAC__ - if (!btn->IsEnabled()) { - btn->SetBitmap(m_bmp_reset_disable.bmp()); - } else { - btn->SetBitmap(m_bmp_reset_focus.bmp()); + // The handler is bound on the button, so the event source is that button. + if (auto *btn = dynamic_cast<ScalableButton *>(event.GetEventObject())) { + if (!btn->IsEnabled()) { + btn->SetBitmap(m_bmp_reset_disable.bmp()); + } else { + btn->SetBitmap(m_bmp_reset_focus.bmp()); + } } #endif }); @@ -223,7 +226,7 @@ bool ObjectTableSettings::update_settings_list(bool is_object, bool is_multiple_ std::weak_ptr<ConfigOptionsGroup> weak_optgroup(optgroup); optgroup->m_on_change = [this, is_object, object, config, group_category](const t_config_option_key &opt_id, const boost::any &value) { this->m_parent->Freeze(); - this->update_config_values(is_object, object, config, group_category); + this->update_config_values(is_object, object, config, group_category, opt_id); wxGetApp().obj_list()->changed_object(); this->m_parent->Thaw(); //update_extra_column_visible_status(optgroup.get(), cat.second, config); @@ -284,13 +287,13 @@ bool ObjectTableSettings::update_settings_list(bool is_object, bool is_multiple_ m_settings_list_sizer->Add(optgroup->sizer, 0, wxEXPAND | wxALL, 0); m_og_settings.push_back(optgroup); - auto toggle_field = [this, optgroup](const t_config_option_key & opt_key, bool toggle, int opt_index) + auto toggle_field = [optgroup](const t_config_option_key & opt_key, bool toggle, int opt_index) { Field* field = optgroup->get_fieldc(opt_key, opt_index);; if (field) field->toggle(toggle); }; - auto toggle_line = [this, optgroup](const t_config_option_key &opt_key, bool toggle, int opt_index) + auto toggle_line = [optgroup](const t_config_option_key &opt_key, bool toggle, int opt_index) { Line* line = optgroup->get_line(opt_key); if (line) line->toggle_visible = toggle; @@ -369,7 +372,7 @@ int ObjectTableSettings::update_extra_column_visible_status(ConfigOptionsGroup* return count; } -void ObjectTableSettings::update_config_values(bool is_object, ModelObject* object, ModelConfig* config, const std::string& category) +void ObjectTableSettings::update_config_values(bool is_object, ModelObject* object, ModelConfig* config, const std::string& category, const std::string& changed_opt_key) { int different_count = 0; const auto printer_technology = wxGetApp().plater()->printer_technology(); @@ -403,6 +406,9 @@ void ObjectTableSettings::update_config_values(bool is_object, ModelObject* obje config_manipulation.set_is_BBL_Printer(wxGetApp().preset_bundle->is_bbl_vendor()); + if (printer_technology == ptFFF && changed_opt_key == "layer_height") + config_manipulation.check_layer_height(&main_config); + printer_technology == ptFFF ? config_manipulation.update_print_fff_config(&main_config) : config_manipulation.update_print_sla_config(&main_config) ; diff --git a/src/slic3r/GUI/GUI_ObjectTableSettings.hpp b/src/slic3r/GUI/GUI_ObjectTableSettings.hpp index 534426f554..39e7e514e2 100644 --- a/src/slic3r/GUI/GUI_ObjectTableSettings.hpp +++ b/src/slic3r/GUI/GUI_ObjectTableSettings.hpp @@ -70,7 +70,7 @@ public: bool add_missed_options(ModelConfig *config_to, const DynamicPrintConfig &config_from); //return visible count int update_extra_column_visible_status(ConfigOptionsGroup* option_group, const std::vector<SimpleSettingData>& option_keys, ModelConfig* config); - void update_config_values(bool is_object, ModelObject* object, ModelConfig* config, const std::string& category); + void update_config_values(bool is_object, ModelObject* object, ModelConfig* config, const std::string& category, const std::string& changed_opt_key = ""); void UpdateAndShow(int row, const bool show, bool is_object, bool is_multiple_selection, ModelObject* object, ModelConfig* config, const std::string& category); void ValueChanged(int row, bool is_object, ModelObject* object, ModelConfig* config, const std::string& category, const std::string& key); void resetAllValues(int row, bool is_object, ModelObject* object, ModelConfig* config, const std::string& category); diff --git a/src/slic3r/GUI/GUI_Utils.hpp b/src/slic3r/GUI/GUI_Utils.hpp index 890e8b9e1c..85790516ee 100644 --- a/src/slic3r/GUI/GUI_Utils.hpp +++ b/src/slic3r/GUI/GUI_Utils.hpp @@ -113,14 +113,7 @@ public: update_dark_ui(this); #endif - // Linux specific issue : get_dpi_for_window(this) still doesn't responce to the Display's scale in new wxWidgets(3.1.3). - // So, calculate the m_em_unit value from the font size, as before -#if !defined(__WXGTK__) - m_em_unit = std::max<size_t>(10, 10.0f * m_scale_factor); -#else - // initialize default width_unit according to the width of the one symbol ("m") of the currently active font of this window. - m_em_unit = std::max<size_t>(10, this->GetTextExtent("m").x - 1); -#endif // __WXGTK__ + update_em_unit(); // recalc_font(); @@ -162,6 +155,9 @@ public: update_dark_config(); on_sys_color_changed(); event.Skip(); +#else + // Not calling Skip() is what stops the event propagating on Windows. + (void) this; #endif // __WINDOWS__ }); @@ -235,6 +231,19 @@ private: // m_em_unit = metrics.averageWidth; // } + // update em_unit value for new window font + void update_em_unit() + { + // Linux specific issue : get_dpi_for_window(this) still doesn't responce to the Display's scale in new wxWidgets(3.1.3). + // So, calculate the m_em_unit value from the font size, as before +#if !defined(__WXGTK__) + m_em_unit = std::max<size_t>(10, 10.0f * m_scale_factor); +#else + // initialize default width_unit according to the width of the one symbol ("m") of the currently active font of this window. + m_em_unit = std::max<size_t>(10, this->GetTextExtent("m").x - 1); +#endif // __WXGTK__ + } + // check if new scale is differ from previous bool is_new_scale_factor() const { return fabs(m_scale_factor - m_prev_scale_factor) > 0.001; } @@ -247,8 +256,7 @@ private: // set normal application font as a current window font m_normal_font = this->GetFont(); - // update em_unit value for new window font - m_em_unit = std::max<int>(10, 10.0f * m_scale_factor); + update_em_unit(); // rescale missed controls sizes and images on_dpi_changed(suggested_rect); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.hpp b/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.hpp index 4e531e6acc..cd3bc53cbd 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.hpp @@ -85,7 +85,7 @@ public: void update_model_object(); //ClippingPlane get_sla_clipping_plane() const; - bool is_selection_rectangle_dragging() const { return m_selection_rectangle.is_dragging(); } + bool is_selection_rectangle_dragging() const override { return m_selection_rectangle.is_dragging(); } bool wants_enter_leave_snapshots() const override { return true; } std::string get_gizmo_entering_text() const override { return _u8L("Entering Brim Ears"); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index b5273e3024..273bb94b90 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -3596,7 +3596,7 @@ void GLGizmoCut3D::perform_cut(const Selection& selection) // model_name failing reason std::vector<std::pair<std::string, std::string>> failed_models; auto plater = wxGetApp().plater(); - auto fix_and_update_progress = [this, plater, keep_painting](ModelObject *model_object, const int vol_idx, const string &model_name, ProgressDialog &progress_dlg, + auto fix_and_update_progress = [keep_painting](ModelObject *model_object, const int vol_idx, const string &model_name, ProgressDialog &progress_dlg, std::vector<std::string> &succes_models, std::vector<std::pair<std::string, std::string>> &failed_models) { wxString msg = _L("Repairing model object"); msg += ": " + from_u8(model_name) + "\n"; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp index feba37133a..ecf465afe7 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp @@ -566,8 +566,11 @@ bool GLGizmoEmboss::on_mouse_for_translate(const wxMouseEvent &mouse_event) void GLGizmoEmboss::on_mouse_change_selection(const wxMouseEvent &mouse_event) { - static bool was_dragging = true; - if ((mouse_event.LeftUp() || mouse_event.RightUp()) && !was_dragging) { + static bool was_dragging = true; + // The left up may be the end of a drag that started on the gizmo floating window (e.g. selecting + // text in the input field). Such a release is not a click on the scene and must not close the gizmo. + // (The flag is only set for left up events, so right up behavior is unchanged.) + if ((mouse_event.LeftUp() || mouse_event.RightUp()) && !was_dragging && !m_parent.is_mouse_left_up_ignored()) { // is hovered volume closest hovered? int hovered_idx = m_parent.get_first_hover_volume_idx(); if (hovered_idx < 0) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp index f42d327a0e..57a6cc1981 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp @@ -86,7 +86,6 @@ private: boost::thread m_thread; // Mutex and condition variable to synchronize m_thread with the UI thread. std::mutex m_mutex; - int m_generate_count; // 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. diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp index 0a3936a81b..e21498163a 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp @@ -597,7 +597,6 @@ void GLGizmoMeasure::on_render() } } Vec3d position_on_model; - Vec3d direction_on_model; size_t model_facet_idx = -1; double closest_hit_distance = std::numeric_limits<double>::max(); { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.cpp index c7186e5d80..8b12da14f9 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.cpp @@ -223,7 +223,7 @@ void GLGizmoMeshBoolean::on_render_input_window(float x, float y, float bottom_l const int select_btn_length = 2 * ImGui::GetStyle().FramePadding.x + std::max(ImGui::CalcTextSize(("1 " + _u8L("selected")).c_str()).x, ImGui::CalcTextSize(_u8L("Select").c_str()).x); - auto selectable = [this](const std::string& label, bool selected, const ImVec2& size_arg) { + auto selectable = [](const std::string& label, bool selected, const ImVec2& size_arg) { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 0,0 }); ImGuiWindow* window = ImGui::GetCurrentWindow(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp index 9c36be5cd9..3ba613295d 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp @@ -75,7 +75,7 @@ protected: virtual void on_render() override; virtual void on_set_state() override; virtual CommonGizmosDataID on_get_requirements() const override; - virtual void on_render_input_window(float x, float y, float bottom_limit); + virtual void on_render_input_window(float x, float y, float bottom_limit) override; void on_load(cereal::BinaryInputArchive &ar) override; void on_save(cereal::BinaryOutputArchive &ar) const override; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index 761228550f..3d4af75cde 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -78,6 +78,9 @@ void GLGizmoMmuSegmentation::init_extruders_data() m_extruders_colors = wxGetApp().plater()->get_extruders_colors(); m_selected_extruder_idx = 0; + m_gradient_ramps = wxGetApp().plater()->get_filament_gradient_ramps(); + m_gradient_ramps.resize(m_extruders_colors.size()); + // keep remap table consistent with current extruder count m_extruder_remap.resize(m_extruders_colors.size()); for (size_t i = 0; i < m_extruder_remap.size(); ++i) @@ -305,15 +308,32 @@ void GLGizmoMmuSegmentation::render_tooltip_button(float x, float y) } // ORCA -bool GLGizmoMmuSegmentation::draw_color_button(int idx, std::string id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale) +bool GLGizmoMmuSegmentation::draw_color_button(int idx, const char* id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale) { + // Inset of the frame stroked below, which is what trims the swatch down to its visible shape. + const float frame_inset = 1.5f; + ImDrawList* draw_list = ImGui::GetWindowDrawList(); std::string label_id = std::to_string(idx) + id_str + std::to_string(idx); ImVec2 pos = ImGui::GetCursorScreenPos(); ImVec2 size = ImVec2(27.f * scale, 27.f * scale); ImVec4 color_vec = ImGuiWrapper::to_ImVec4(color); ImU32 br_color = ImGui::ColorConvertFloat4ToU32(active ? ImGuiWrapper::COL_ORCA : m_is_dark_mode ? ImVec4(.35f, .35f, .35f, 1) : ImVec4(.85f, .85f, .85f, 1)); - bool dark_tone = (0.299f * color.r() + 0.587f * color.g() + 0.114f * color.b()) < 0.51f; // matching values used by wxWidgets with clr.GetLuminance() < 0.51 + // Every caller labels the button with the 1 based slot number, so idx - 1 picks out the slot's fade. + const std::vector<wxColour>* gradient = gradient_of(idx - 1); + // The centered slot number sits at the swatch's mid height, so take its contrast from the colour + // printed there rather than from the slot's blended color. + bool dark_tone = gradient ? (*gradient)[gradient->size() / 2].GetLuminance() < 0.51 : + (0.299f * color.r() + 0.587f * color.g() + 0.114f * color.b()) < 0.51f; // matching values used by wxWidgets with clr.GetLuminance() < 0.51 + + // Paint a gradient mixed filament's fade before the button and keep the button transparent, so + // the slot number and the frame below stay on top of it. The bands cannot round their corners, + // so the fade is inset to the frame, which masks it into the shape a plain color slot gets. + if (gradient) { + ImGuiWrapper::draw_gradient_ramp(draw_list, {pos.x + frame_inset * scale, pos.y + frame_inset * scale}, + {pos.x + size.x - frame_inset * scale, pos.y + size.y - frame_inset * scale}, *gradient); + color_vec.w = 0.f; // let the fade show through + } ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding , 7.f * scale); @@ -329,7 +349,7 @@ bool GLGizmoMmuSegmentation::draw_color_button(int idx, std::string id_str, cons auto drawBorder = [&](float d, float r, float t, ImU32 col) { draw_list->AddRect({pos.x + d * scale, pos.y + d * scale}, {pos.x + size.x - d * scale , pos.y + size.y - d * scale}, col, r * scale, 0, t * scale); }; - drawBorder(1.5f, 3.f, 4.f, ImGui::ColorConvertFloat4ToU32(ImGui::GetStyleColorVec4(ImGuiCol_WindowBg))); + drawBorder(frame_inset, 3.f, 4.f, ImGui::ColorConvertFloat4ToU32(ImGui::GetStyleColorVec4(ImGuiCol_WindowBg))); if(active) drawBorder(.5f, 4.f , 2.f, br_color); else @@ -433,7 +453,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott m_selected_extruder_idx = extruder_idx; } - if (extruder_idx < 16 && ImGui::IsItemHovered()) m_imgui->tooltip(_L("Shortcut Key ") + std::to_string(extruder_idx + 1), max_tooltip_width); + if (extruder_idx < int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT) && ImGui::IsItemHovered()) m_imgui->tooltip(_L("Shortcut Key ") + std::to_string(extruder_idx + 1), max_tooltip_width); } // ORCA: Remap filaments section (Border only, Title in border). // Styled as a panel for visual grouping. @@ -731,6 +751,10 @@ void GLGizmoMmuSegmentation::init_model_triangle_selectors() continue; int extruder_idx = (mv->extruder_id() > 0) ? mv->extruder_id() - 1 : 0; + // A volume may be assigned to a mixed-color slot, whose index can sit past the + // physical colour list; fall back to the first colour rather than reading OOB. + if (extruder_idx >= (int)m_extruders_colors.size()) + extruder_idx = 0; std::vector<ColorRGBA> ebt_colors; ebt_colors.push_back(m_extruders_colors[size_t(extruder_idx)]); ebt_colors.insert(ebt_colors.end(), m_extruders_colors.begin(), m_extruders_colors.end()); @@ -753,6 +777,9 @@ void GLGizmoMmuSegmentation::update_triangle_selectors_colors() TriangleSelectorPatch* selector = dynamic_cast<TriangleSelectorPatch*>(m_triangle_selectors[i].get()); int extruder_idx = m_volumes_extruder_idxs[i]; int extruder_color_idx = std::max(0, extruder_idx - 1); + // A mixed-color slot can index past the physical colour list; fall back to the first colour. + if (extruder_color_idx >= (int)m_extruders_colors.size()) + extruder_color_idx = 0; std::vector<ColorRGBA> ebt_colors; ebt_colors.push_back(m_extruders_colors[extruder_color_idx]); ebt_colors.insert(ebt_colors.end(), m_extruders_colors.begin(), m_extruders_colors.end()); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp index 55308bffa9..70cfde5aed 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp @@ -73,11 +73,10 @@ public: void data_changed(bool is_serializing) override; - // TriangleSelector::serialization/deserialization has a limit to store 19 different states. - // EXTRUDER_LIMIT + 1 states are used to storing the painting because also uncolored triangles are stored. - // When increasing EXTRUDER_LIMIT, it needs to ensure that TriangleSelector::serialization/deserialization - // will be also extended to support additional states, requiring at least one state to remain free out of 19 states. - static const constexpr size_t EXTRUDERS_LIMIT = 16; + // The paint material limit follows EnforcerBlockerType::ExtruderMax: TriangleSelector + // serialization covers the extended (17..32) range through an escape nibble. Mixed-color + // filaments occupy ordinary slots, so they draw from the same budget as physical ones. + static const constexpr size_t EXTRUDERS_LIMIT = static_cast<size_t>(EnforcerBlockerType::ExtruderMax); const float get_cursor_radius_min() const override { return CursorRadiusMin; } @@ -116,6 +115,10 @@ protected: // Filament remap feature std::vector<size_t> m_extruder_remap; // index → target extruder index + // Colours each gradient mixed filament actually prints, bottom of the model first, mirrored + // from Plater so the extruder swatches draw the same fade the editor previews. Plain + // filament slots keep an empty ramp. + std::vector<std::vector<wxColour>> m_gradient_ramps; // ORCA: Cache used filaments to filter UI std::set<size_t> m_used_filaments; // Set of used filament indices (cached) @@ -137,7 +140,13 @@ private: void init_model_triangle_selectors(); // ORCA - bool draw_color_button(int idx, std::string id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale); + bool draw_color_button(int idx, const char* id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale); + // Gradient ramp of a filament slot, or nullptr when the slot is a plain single color + // filament. A non-null result is never empty. + const std::vector<wxColour>* gradient_of(int idx) const + { + return idx >= 0 && idx < (int) m_gradient_ramps.size() && !m_gradient_ramps[idx].empty() ? &m_gradient_ramps[idx] : nullptr; + } // BBS void update_triangle_selectors_colors(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp index df3abdddc7..fecc8abf1c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp @@ -67,7 +67,7 @@ protected: void on_register_raycasters_for_picking() override; void on_unregister_raycasters_for_picking() override; //BBS: GUI refactor: add object manipulation - virtual void on_render_input_window(float x, float y, float bottom_limit); + virtual void on_render_input_window(float x, float y, float bottom_limit) override; private: double calc_projection(const UpdateData& data) const; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 8f0cd9a43d..3dcf51cbb5 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -1456,7 +1456,7 @@ void TriangleSelectorPatch::update_triangles_per_patch() return touching_triangles; }; - auto calc_fragment_area = [this](const TrianglePatch& patch, float max_limit_area, int stride) { + auto calc_fragment_area = [](const TrianglePatch& patch, float max_limit_area, int stride) { double total_area = 0.f; const std::vector<int>& ti = patch.triangle_indices; /*for (int i = 0; i < ti.size() / 3; i++) { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp b/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp index 6b46a596ba..3bfb63ff7a 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp @@ -89,7 +89,7 @@ protected: virtual void on_register_raycasters_for_picking() override; virtual void on_unregister_raycasters_for_picking() override; //BBS: GUI refactor: add object manipulation - virtual void on_render_input_window(float x, float y, float bottom_limit); + virtual void on_render_input_window(float x, float y, float bottom_limit) override; private: void render_grabbers_connection(unsigned int id_1, unsigned int id_2, const ColorRGBA& color); diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 94c76d896b..9cff834346 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -998,16 +998,40 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt) keyCode = keyCode- WXK_NUMPAD0+'0'; } if (keyCode >= '0' && keyCode <= '9') { - if (keyCode == '1' && !m_timer_set_color.IsRunning()) { + // The paint palette reaches EXTRUDERS_LIMIT slots (mixed-color filaments take + // ordinary slots too), so any leading digit that can start a valid two-digit + // number waits briefly for a second one. + const int digit = keyCode - '0'; + const int shortcut_max = int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT); + auto can_start_two_digit = [](int d) { return d > 0 && d * 10 <= shortcut_max; }; + auto select = [mmu_seg](int number) { return number > 0 && mmu_seg->on_number_key_down(number); }; + + if (m_timer_set_color.IsRunning() && m_pending_color_shortcut_tens > 0) { + const int two_digit = m_pending_color_shortcut_tens * 10 + digit; + const int pending = m_pending_color_shortcut_tens; + m_pending_color_shortcut_tens = 0; + m_timer_set_color.Stop(); + if (two_digit <= shortcut_max) { + processed = select(two_digit); + } else { + // Out of range: commit the pending digit, then treat this one as new input. + processed = select(pending); + if (can_start_two_digit(digit)) { + m_pending_color_shortcut_tens = digit; + m_timer_set_color.StartOnce(500); + processed = true; + } else { + processed = select(digit) || processed; + } + } + } + else if (can_start_two_digit(digit)) { + m_pending_color_shortcut_tens = digit; m_timer_set_color.StartOnce(500); processed = true; } - else if (keyCode < '7' && m_timer_set_color.IsRunning()) { - processed = mmu_seg->on_number_key_down(keyCode - '0'+10); - m_timer_set_color.Stop(); - } else { - processed = mmu_seg->on_number_key_down(keyCode - '0'); + processed = select(digit); } } else if (keyCode == 'F' || keyCode == 'T' || keyCode == 'S' || keyCode == 'C' || keyCode == 'H' || keyCode == 'G') { @@ -1054,11 +1078,15 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt) void GLGizmosManager::on_set_color_timer(wxTimerEvent& evt) { - if (m_current == MmSegmentation) { + // No second digit arrived in time: commit the pending leading digit on its own. + if (m_current == MmSegmentation && m_pending_color_shortcut_tens > 0) { GLGizmoMmuSegmentation* mmu_seg = dynamic_cast<GLGizmoMmuSegmentation*>(get_current()); - mmu_seg->on_number_key_down(1); - m_parent.set_as_dirty(); + if (mmu_seg != nullptr) { + mmu_seg->on_number_key_down(m_pending_color_shortcut_tens); + m_parent.set_as_dirty(); + } } + m_pending_color_shortcut_tens = 0; } void GLGizmosManager::update_after_undo_redo(const UndoRedo::Snapshot& snapshot) diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 01814521aa..157eb43dc7 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -144,6 +144,8 @@ private: //When there are more than 9 colors, shortcut key coloring wxTimer m_timer_set_color; + // Leading digit of a two-digit color shortcut still waiting for its second digit. + int m_pending_color_shortcut_tens = 0; void on_set_color_timer(wxTimerEvent& evt); // key MENU_ICON_NAME, value = ImtextureID diff --git a/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp b/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp index d7b082426c..7c4a2afd38 100644 --- a/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp +++ b/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp @@ -239,7 +239,7 @@ void GizmoObjectManipulation::update_if_dirty() }; for (int i = 0; i < 3; ++ i) { - auto update = [this, i](Vec3d &cached, Vec3d &cached_rounded, const Vec3d &new_value) { + auto update = [i](Vec3d &cached, Vec3d &cached_rounded, const Vec3d &new_value) { //wxString new_text = double_to_string(new_value(i), 2); double new_rounded = round(new_value(i)*100)/100.0; //new_text.ToDouble(&new_rounded); diff --git a/src/slic3r/GUI/GradientCurveEditor.cpp b/src/slic3r/GUI/GradientCurveEditor.cpp new file mode 100644 index 0000000000..5b1073d231 --- /dev/null +++ b/src/slic3r/GUI/GradientCurveEditor.cpp @@ -0,0 +1,656 @@ +#include "GradientCurveEditor.hpp" +#include "GUI_App.hpp" +#include "GuiColor.hpp" +#include "I18N.hpp" +#include "Widgets/StateColor.hpp" + +#include <algorithm> +#include <cmath> +#include <limits> + +#include <wx/dcbuffer.h> +#include <wx/dcclient.h> +#include <wx/dcgraph.h> +#include <wx/settings.h> + +namespace Slic3r { +namespace GUI { + +wxDEFINE_EVENT(wxEVT_GRADIENT_CURVE_CHANGED, wxCommandEvent); + +namespace { +// Layout ratios of the plot rect within the widget, taken from a 214 x 180 px reference drawing. +// Plot rect occupies the upper-left region; right + bottom margins host axis arrows / labels. +constexpr double kPlotLeftRatio = 0.0316; +constexpr double kPlotRightRatio = 0.6766; +constexpr double kPlotTopRatio = 0.1529; +constexpr double kPlotBottomRatio = 0.8474; +constexpr int kGridDivisions = 9; // 10 grid lines including the outer borders. + +// Hit / stroke (DIP). +constexpr int kHitRadius = 6; +constexpr int kCurveHitRadius = 5; +constexpr int kPointRadius = 4; // anchor outer radius (DIP) +constexpr int kStrokeUnselected = 2; +constexpr int kStrokeSelected = 4; +constexpr int kStrokeAxis = 2; // axis line width (px, no DPI scaling - matches kGridColor pen and 2DBed convention) +constexpr int kAxisArrowHalf = 5; // half-base of the axis arrow triangle (DIP) +constexpr int kAxisArrowLen = 10; // length of the axis arrow triangle (DIP) + +// Light-mode design tokens. Resolved through StateColor::darkModeColorFor() +// at paint time so the editor follows the app theme (#EEEEEE -> #4C4C55, #6B6B6B -> +// #818183, #262E30 -> #EFEFF0, #ACACAC -> #65656A, *wxWHITE -> #2D2D31). Don't read these +// directly in paint; always go through the resolved locals declared at the top of on_paint(). +const wxColour kGridColor (238, 238, 238); // #EEEEEE grey 300 +const wxColour kAxisColor (107, 107, 107); // #6B6B6B grey 700 +const wxColour kLabelMuted (107, 107, 107); // #6B6B6B grey 700 +const wxColour kLabelStrong ( 38, 46, 48); // #262E30 grey 900 +const wxColour kOutlineColor(172, 172, 172); // #ACACAC dimmed elements + +// LAB (DeltaE76) threshold for "curve color is too close to the background": below it the curve +// gets a subtle outline so it does not visually vanish, otherwise it is drawn plain. Looser than +// the 5.0 of FlushPredict::is_similar_color, so a pastel pink on white still gets an outline. +constexpr float kBgSimilarThreshold = 15.0f; +constexpr int kOutlineExtraDip = 2; +} // namespace + +GradientCurveEditor::GradientCurveEditor(wxWindow* parent, + const wxColour& color_low, + const wxColour& color_high) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) + , m_color_low(color_low) + , m_color_high(color_high) +{ + SetBackgroundStyle(wxBG_STYLE_PAINT); + SetBackgroundColour(wxGetApp().get_window_default_clr()); + // Wide enough so the X-axis "Material Ratio" label fits past the arrow tip without overlap. + SetMinSize(FromDIP(wxSize(260, 200))); + + reset_to_linear(0.10, 0.90); + + Bind(wxEVT_PAINT, &GradientCurveEditor::on_paint, this); + Bind(wxEVT_LEFT_DOWN, &GradientCurveEditor::on_left_down, this); + Bind(wxEVT_LEFT_UP, &GradientCurveEditor::on_left_up, this); + Bind(wxEVT_RIGHT_DOWN, &GradientCurveEditor::on_right_down, this); + Bind(wxEVT_MOTION, &GradientCurveEditor::on_motion, this); + Bind(wxEVT_LEAVE_WINDOW,&GradientCurveEditor::on_leave, this); + Bind(wxEVT_SIZE, &GradientCurveEditor::on_size, this); + Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) { + m_drag_mode = DragMode::None; + m_drag_idx = -1; + m_dragged_moved = false; + }); +} + +GradientCurveEditor::~GradientCurveEditor() +{ + // See MixedFilamentDialog::~MixedFilamentDialog: a widget destroyed while it + // still holds the capture wedges mouse input for the whole application. + if (HasCapture()) + ReleaseMouse(); +} + +void GradientCurveEditor::set_points(const PointList& pts) +{ + m_points = pts; + normalize_points(); + Refresh(); +} + +void GradientCurveEditor::set_colors(const wxColour& color_low, const wxColour& color_high) +{ + m_color_low = color_low; + m_color_high = color_high; + Refresh(); +} + +void GradientCurveEditor::set_selected_curve(int curve_idx) +{ + const int new_sel = (curve_idx == 0) ? 0 : 1; + if (m_selected_curve == new_sel) return; + m_selected_curve = new_sel; + Refresh(); +} + +void GradientCurveEditor::reset_to_linear(double y0, double y1) +{ + auto clamp_y = [](double v) { + return std::max(kGradientMinRatio, std::min(kGradientMaxRatio, v)); + }; + m_points.clear(); + GradientAnchor a0; a0.x = 0.0; a0.y = clamp_y(y0); + GradientAnchor a1; a1.x = 1.0; a1.y = clamp_y(y1); + m_points.push_back(a0); + m_points.push_back(a1); + m_selected_curve = 0; + Refresh(); + emit_changed(); +} + +void GradientCurveEditor::reverse() +{ + // Mirror y around 0.5. Tangents are slopes dy/dx so they flip sign to keep the + // local shape consistent across the mirror; NaN tangents remain "use PCHIP default". + for (auto& p : m_points) { + p.y = 1.0 - p.y; + if (std::isfinite(p.m_in)) p.m_in = -p.m_in; + if (std::isfinite(p.m_out)) p.m_out = -p.m_out; + } + Refresh(); + emit_changed(); +} + +void GradientCurveEditor::normalize_points() +{ + if (m_points.empty()) { + GradientAnchor a0; a0.x = 0.0; a0.y = kGradientMinRatio; + GradientAnchor a1; a1.x = 1.0; a1.y = kGradientMaxRatio; + m_points.push_back(a0); + m_points.push_back(a1); + return; + } + + for (auto& p : m_points) { + p.x = std::max(0.0, std::min(1.0, p.x)); + p.y = std::max(kGradientMinRatio, std::min(kGradientMaxRatio, p.y)); + } + std::sort(m_points.begin(), m_points.end(), + [](const GradientAnchor& a, const GradientAnchor& b) { + return a.x < b.x; + }); + + if (m_points.size() < 2) { + GradientAnchor tail; tail.x = 1.0; tail.y = m_points.front().y; + m_points.push_back(tail); + } + + m_points.front().x = 0.0; + m_points.back().x = 1.0; +} + +void GradientCurveEditor::emit_changed() +{ + wxCommandEvent evt(wxEVT_GRADIENT_CURVE_CHANGED, GetId()); + evt.SetEventObject(this); + ProcessWindowEvent(evt); +} + +wxRect GradientCurveEditor::plot_rect() const +{ + const wxSize sz = GetClientSize(); + const int x = static_cast<int>(std::lround(sz.x * kPlotLeftRatio)); + const int y = static_cast<int>(std::lround(sz.y * kPlotTopRatio)); + const int x2 = static_cast<int>(std::lround(sz.x * kPlotRightRatio)); + const int y2 = static_cast<int>(std::lround(sz.y * kPlotBottomRatio)); + // Force square 1:1 so X/Y axes share the same scale and grid cells stay square. Anchor at + // the top-left so the "100%" labels on the bottom/right still align with the plot edges. + const int side = std::max(1, std::min(x2 - x, y2 - y)); + return wxRect(x, y, side, side); +} + +wxPoint2DDouble GradientCurveEditor::data_to_px_f(double x, double y) const +{ + const wxRect r = plot_rect(); + // y axis is inverted: y=1 should sit at the top. + return wxPoint2DDouble(r.x + x * r.width, r.y + (1.0 - y) * r.height); +} + +wxPoint GradientCurveEditor::data_to_px(double x, double y) const +{ + const wxPoint2DDouble p = data_to_px_f(x, y); + return wxPoint(static_cast<int>(std::lround(p.m_x)), static_cast<int>(std::lround(p.m_y))); +} + +void GradientCurveEditor::px_to_data(int px, int py, double& x, double& y) const +{ + const wxRect r = plot_rect(); + const double w = std::max(1, r.width); + const double h = std::max(1, r.height); + x = std::max(0.0, std::min(1.0, (px - r.x) / w)); + y = std::max(0.0, std::min(1.0, 1.0 - (py - r.y) / h)); +} + +double GradientCurveEditor::sample_curve_y(double x) const +{ + GradientCurve gc; + gc.points = m_points; + return sample_gradient_curve(gc, x); +} + +int GradientCurveEditor::hit_test(int px, int py) const +{ + const int tol = FromDIP(kHitRadius); + int best_idx = -1; + int best_d2 = tol * tol; + for (size_t i = 0; i < m_points.size(); ++i) { + // Anchor visual y is curve-specific: component 1's anchor sits at (x, 1 - stored_y). + const double vy = to_visual_y(m_selected_curve, m_points[i].y); + const wxPoint p = data_to_px(m_points[i].x, vy); + const int dx = px - p.x; + const int dy = py - p.y; + const int d2 = dx * dx + dy * dy; + if (d2 <= best_d2) { + best_idx = static_cast<int>(i); + best_d2 = d2; + } + } + return best_idx; +} + +int GradientCurveEditor::hit_test_curve(int px, int py, int* seg_out) const +{ + if (seg_out) *seg_out = -1; + if (m_points.size() < 2) return -1; + const int tol = FromDIP(kCurveHitRadius); + const int tol2 = tol * tol; + + auto dist2_to_seg = [&](int ax, int ay, int bx, int by) -> int { + const double dx = bx - ax; + const double dy = by - ay; + const double l2 = dx * dx + dy * dy; + if (l2 == 0.0) { + const double ddx = px - ax; + const double ddy = py - ay; + return static_cast<int>(ddx * ddx + ddy * ddy); + } + double t = ((px - ax) * dx + (py - ay) * dy) / l2; + t = std::max(0.0, std::min(1.0, t)); + const double ex = ax + t * dx; + const double ey = ay + t * dy; + const double ddx = px - ex; + const double ddy = py - ey; + return static_cast<int>(ddx * ddx + ddy * ddy); + }; + + // Hit-test against the same dense Hermite polyline that on_paint draws, so the + // clickable line follows the visual curve exactly (no offset on the bent parts). + // When a hit is found, also report the index of the left anchor of the data-space + // segment that covers cursor x; needed by the segment-bend interaction. + const wxRect rc = plot_rect(); + const int samples = std::max(128, rc.width * 2); + auto seg_for_x = [&](double cursor_x) -> int { + for (size_t i = 1; i < m_points.size(); ++i) { + if (cursor_x <= m_points[i].x) + return static_cast<int>(i - 1); + } + return static_cast<int>(m_points.size() - 2); + }; + + auto curve_hit = [&](int curve_idx) -> bool { + wxPoint prev; + for (int s = 0; s <= samples; ++s) { + const double x = double(s) / samples; + const double y0 = sample_curve_y(x); + const double vy = to_visual_y(curve_idx, y0); + const wxPoint cur = data_to_px(x, vy); + if (s > 0 && dist2_to_seg(prev.x, prev.y, cur.x, cur.y) <= tol2) + return true; + prev = cur; + } + return false; + }; + + // Prefer the selected curve so overlapping segments don't unintentionally steal focus. + if (curve_hit(m_selected_curve)) { + if (seg_out) { + double nx = 0, dummy = 0; + px_to_data(px, py, nx, dummy); + *seg_out = seg_for_x(nx); + } + return m_selected_curve; + } + const int other = 1 - m_selected_curve; + if (curve_hit(other)) { + if (seg_out) { + double nx = 0, dummy = 0; + px_to_data(px, py, nx, dummy); + *seg_out = seg_for_x(nx); + } + return other; + } + return -1; +} + +void GradientCurveEditor::on_paint(wxPaintEvent& /*evt*/) +{ + // Resolve theme colors every paint so dark-mode toggles (no re-construction) take + // effect without an explicit listener. Window bg is read from GUI_App, not + // GetBackgroundColour(), since the latter is snapshotted at construction time. + const wxColour bg = wxGetApp().get_window_default_clr(); + const wxColour grid_color = StateColor::darkModeColorFor(kGridColor); + const wxColour axis_color = StateColor::darkModeColorFor(kAxisColor); + const wxColour label_muted = StateColor::darkModeColorFor(kLabelMuted); + const wxColour label_strong = StateColor::darkModeColorFor(kLabelStrong); + const wxColour point_fill = StateColor::darkModeColorFor(*wxWHITE); + // Softer than axis_color: the curve outline only has to lift the curve off the + // background, it must not compete with the structural axis / grid. + const wxColour outline_color = StateColor::darkModeColorFor(kOutlineColor); + + wxAutoBufferedPaintDC raw_dc(this); + raw_dc.SetBackground(wxBrush(bg)); + raw_dc.Clear(); + + // Render through wxGCDC so curves, arrows and anchor circles get anti-aliased; the buffered + // DC is the actual back buffer that gets blitted to the window. + wxGCDC dc(raw_dc); + // The curve and its anchors are drawn straight on the graphics context so their + // coordinates stay sub-pixel accurate (see data_to_px_f). + wxGraphicsContext* gc = dc.GetGraphicsContext(); + + const wxRect rc = plot_rect(); + if (rc.width <= 0 || rc.height <= 0) + return; + + // 10x10 light grid (10 lines including outer borders, 9 equal divisions). + dc.SetPen(wxPen(grid_color, 1)); + for (int i = 0; i <= kGridDivisions; ++i) { + const int x = rc.x + rc.width * i / kGridDivisions; + const int y = rc.y + rc.height * i / kGridDivisions; + dc.DrawLine(x, rc.y, x, rc.y + rc.height); + dc.DrawLine(rc.x, y, rc.x + rc.width, y); + } + + // Set the label font first so text width measurements drive arrow / label placement. + wxFont label_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + label_font.SetPointSize(std::max(7, label_font.GetPointSize() - 1)); + dc.SetFont(label_font); + + const wxString axis_y_title = _L("Material Ratio"); + const wxString axis_x_title = _L("Model Height"); + const wxString pct_text = wxT("100%"); + const wxSize x_title_sz = dc.GetTextExtent(axis_x_title); + const wxSize y_title_sz = dc.GetTextExtent(axis_y_title); + + wxFont strong_font = label_font; + strong_font.SetWeight(wxFONTWEIGHT_SEMIBOLD); + dc.SetFont(strong_font); + const wxSize pct_text_sz = dc.GetTextExtent(pct_text); + dc.SetFont(label_font); + + // Axes (grey 700) with filled triangle arrows. Y-axis extends above the plot top to the + // canvas top edge; X-axis extends past the plot right toward the canvas right edge. + const int arrow_half = FromDIP(kAxisArrowHalf); + const int arrow_len = FromDIP(kAxisArrowLen); + const wxSize sz = GetClientSize(); + dc.SetPen(wxPen(axis_color, kStrokeAxis)); + dc.SetBrush(wxBrush(axis_color)); + + // Y-axis: vertical line at plot_left, from arrow tip near canvas top down to plot bottom. + const int y_axis_x = rc.x; + const int y_title_pct_gap = FromDIP(1); + const int y_title_bottom_pad = FromDIP(2); + const int y_title_y = std::max(0, rc.y - y_title_sz.y - y_title_pct_gap - pct_text_sz.y - y_title_bottom_pad); + const int y_arrow_tip_y = y_title_y; + const int y_arrow_ty = y_arrow_tip_y + arrow_len; + dc.DrawLine(y_axis_x, y_arrow_ty, y_axis_x, rc.y + rc.height); + { + wxPoint tri[3] = { + wxPoint(y_axis_x, y_arrow_tip_y), + wxPoint(y_axis_x - arrow_half, y_arrow_ty), + wxPoint(y_axis_x + arrow_half, y_arrow_ty), + }; + dc.DrawPolygon(3, tri); + } + + // X-axis arrow tip: stays just past the plot ideally, but is clamped so the trailing + // "Material Ratio" label still fits inside the canvas without overlapping the arrow. + const int x_axis_y = rc.y + rc.height; + const int x_label_gap = FromDIP(4); + const int x_edge_pad = FromDIP(6); + const int x_arrow_ideal = rc.x + rc.width + FromDIP(10); + const int x_arrow_max = sz.x - x_title_sz.x - x_label_gap - x_edge_pad - arrow_len; + const int x_arrow_tx = std::max(rc.x + rc.width + arrow_len, + std::min(x_arrow_ideal, x_arrow_max)); + const int x_arrow_tip_x = x_arrow_tx + arrow_len; + const int x_title_x = x_arrow_tip_x + x_label_gap; + dc.DrawLine(rc.x, x_axis_y, x_arrow_tx, x_axis_y); + { + wxPoint tri[3] = { + wxPoint(x_arrow_tip_x, x_axis_y), + wxPoint(x_arrow_tx, x_axis_y - arrow_half), + wxPoint(x_arrow_tx, x_axis_y + arrow_half), + }; + dc.DrawPolygon(3, tri); + } + + // Labels. + // "Model Height" and "100%" share the same left x; the gap is larger than the + // axis-arrow half-base so the text never visually touches the Y-axis arrow. + const int label_left_x = y_axis_x + FromDIP(10); + dc.SetTextForeground(label_muted); + dc.DrawText(axis_y_title, label_left_x, y_title_y); + + dc.SetFont(strong_font); + dc.SetTextForeground(label_strong); + dc.DrawText(pct_text, label_left_x, y_title_y + y_title_sz.y + y_title_pct_gap); + + // Bottom-right "100%" sits under the right end of the plot; "Material Ratio" follows the + // X-axis arrow tip (placement was already clamped above to leave room). + dc.DrawText(pct_text, rc.x + rc.width - pct_text_sz.x, x_axis_y); + dc.SetFont(label_font); + dc.SetTextForeground(label_muted); + dc.DrawText(axis_x_title, x_title_x, x_axis_y - x_title_sz.y / 2); + + if (m_points.size() < 2 || !gc) + return; + + auto color_for_curve = [&](int curve_idx) -> wxColour { + wxColour c = (curve_idx == 0) ? m_color_low : m_color_high; + // Transparent filaments (alpha == 0, e.g. #FFFFFF00) would be invisible. + // Lift alpha so the curve stays visible while still hinting at transparency. + if (c.Alpha() == 0) + c.Set(c.Red(), c.Green(), c.Blue(), 150); + return c; + }; + + auto build_polyline = [&](int curve_idx) -> std::vector<wxPoint2DDouble> { + const int samples = std::max(128, rc.width * 2); + std::vector<wxPoint2DDouble> poly; + poly.reserve(samples + 1); + for (int s = 0; s <= samples; ++s) { + const double x = double(s) / samples; + const double y0 = sample_curve_y(x); + const double vy = to_visual_y(curve_idx, y0); + poly.push_back(data_to_px_f(x, vy)); + } + return poly; + }; + + // Only the geometry goes through the graphics context: dc.DrawLines() takes integer wxPoint + // and would quantize the curve back to whole pixels. The pen is still set on the dc, which + // forwards it here while keeping its own cached state in sync for later dc drawing. + auto draw_polyline = [&](const std::vector<wxPoint2DDouble>& poly, const wxColour& col, int stroke_dip) { + dc.SetPen(wxPen(col, FromDIP(stroke_dip))); + gc->StrokeLines(poly.size(), poly.data()); + }; + + // Outline only when the curve color is perceptually close to the background; otherwise + // the plain filament color reads fine and the extra stroke would look heavy. + auto needs_outline = [&](const wxColour& c) { + return calc_color_distance(c, bg) < kBgSimilarThreshold; + }; + + auto draw_one = [&](int curve_idx, int stroke_dip) { + const auto poly = build_polyline(curve_idx); + const wxColour col = color_for_curve(curve_idx); + if (needs_outline(col)) + draw_polyline(poly, outline_color, stroke_dip + kOutlineExtraDip); + draw_polyline(poly, col, stroke_dip); + }; + + // Draw unselected first so the selected curve sits on top. + const int other = 1 - m_selected_curve; + draw_one(other, kStrokeUnselected); + draw_one(m_selected_curve, kStrokeSelected); + + // Control points (selected curve only): hollow circle with axis-color border, theme-aware fill. + // Drawn on the graphics context with a sub-pixel center so the ring stays centered on the + // curve instead of drifting up to half a pixel off it; pen and brush go through the dc for + // the same reason as in draw_polyline above. + const double r = FromDIP(kPointRadius); + dc.SetPen(wxPen(axis_color, 1)); + dc.SetBrush(wxBrush(point_fill)); + for (size_t i = 0; i < m_points.size(); ++i) { + const double vy = to_visual_y(m_selected_curve, m_points[i].y); + const wxPoint2DDouble p = data_to_px_f(m_points[i].x, vy); + gc->DrawEllipse(p.m_x - r, p.m_y - r, r * 2, r * 2); + } +} + +void GradientCurveEditor::on_left_down(wxMouseEvent& evt) +{ + const wxPoint pos = evt.GetPosition(); + m_dragged_moved = false; + + // 1) Anchor on the selected curve takes precedence over everything else. + // Dragging an anchor resets its tangent overrides so the surrounding curve + // returns to PCHIP-default shape (matches user expectation that pulling an + // anchor "straightens out" the local mess). + const int idx = hit_test(pos.x, pos.y); + if (idx >= 0) { + m_drag_mode = DragMode::Anchor; + m_drag_idx = idx; + // Only emit a change event when clearing the tangents actually mutates + // the curve. A plain click on an already-default anchor must not trigger + // re-slicing through the changed-event listener. + const bool had_tangent = std::isfinite(m_points[idx].m_in) + || std::isfinite(m_points[idx].m_out); + m_points[idx].m_in = std::numeric_limits<double>::quiet_NaN(); + m_points[idx].m_out = std::numeric_limits<double>::quiet_NaN(); + if (!HasCapture()) + CaptureMouse(); + Refresh(); + if (had_tangent) + emit_changed(); + return; + } + + // 2) Line-body hit. Determine which curve and which segment. + int seg = -1; + const int curve_hit = hit_test_curve(pos.x, pos.y, &seg); + if (curve_hit < 0) { + m_drag_mode = DragMode::None; + evt.Skip(); + return; + } + + // 3) Non-selected curve hit -> switch selection only, no drag arming. + if (curve_hit != m_selected_curve) { + m_selected_curve = curve_hit; + m_drag_mode = DragMode::None; + Refresh(); + evt.Skip(); + return; + } + + // 4) Selected curve line body hit -> insert a new anchor at cursor x (snapped + // to the current smooth curve so the initial click is visually invisible) + // and immediately enter Anchor drag mode. Bending the segment without + // inserting an anchor is not an option: a single cubic between two existing + // anchors cannot put its peak under an off-center cursor. + double nx = 0, dummy = 0; + px_to_data(pos.x, pos.y, nx, dummy); + if (nx <= 0.0 || nx >= 1.0 || seg < 0) { + m_drag_mode = DragMode::None; + evt.Skip(); + return; + } + GradientAnchor a; + a.x = nx; + a.y = sample_curve_y(nx); + const size_t insert_idx = static_cast<size_t>(seg) + 1; + m_points.insert(m_points.begin() + insert_idx, a); + + m_drag_mode = DragMode::Anchor; + m_drag_idx = static_cast<int>(insert_idx); + if (!HasCapture()) + CaptureMouse(); + Refresh(); + emit_changed(); +} + +void GradientCurveEditor::on_left_up(wxMouseEvent& evt) +{ + if (HasCapture()) + ReleaseMouse(); + + // Anchor mode (either an existing anchor or one freshly inserted by on_left_down) + // already fired emit_changed on mouse_down; only fire again here if the user + // actually dragged so the slicer doesn't re-run on a pure click. + if (m_drag_mode == DragMode::Anchor && m_dragged_moved) + emit_changed(); + + m_drag_mode = DragMode::None; + m_drag_idx = -1; + m_dragged_moved = false; + (void)evt; +} + +void GradientCurveEditor::on_right_down(wxMouseEvent& evt) +{ + const wxPoint pos = evt.GetPosition(); + const int idx = hit_test(pos.x, pos.y); + if (idx > 0 && static_cast<size_t>(idx) + 1 < m_points.size()) { + // Interior anchor on the selected curve -> delete it. Endpoints stay locked. + m_points.erase(m_points.begin() + idx); + Refresh(); + emit_changed(); + return; + } + // Right-click on the non-selected curve switches selection (never deletes). + const int curve_hit = hit_test_curve(pos.x, pos.y); + if (curve_hit >= 0 && curve_hit != m_selected_curve) { + m_selected_curve = curve_hit; + Refresh(); + return; + } + evt.Skip(); +} + +void GradientCurveEditor::on_motion(wxMouseEvent& evt) +{ + if (!evt.LeftIsDown() || m_drag_mode != DragMode::Anchor) { + evt.Skip(); + return; + } + if (static_cast<size_t>(m_drag_idx) >= m_points.size()) + return; + + const wxPoint pos = evt.GetPosition(); + double nx = 0, vy = 0; + px_to_data(pos.x, pos.y, nx, vy); + + auto& p = m_points[m_drag_idx]; + const bool is_first = (m_drag_idx == 0); + const bool is_last = (static_cast<size_t>(m_drag_idx) + 1 == m_points.size()); + + // Endpoints stay locked at x=0 / x=1; interior anchors clamp into + // (left_neighbor.x, right_neighbor.x) so they can't cross or coincide. + if (!is_first && !is_last) { + const double xl = m_points[m_drag_idx - 1].x; + const double xr = m_points[m_drag_idx + 1].x; + const double eps = 1e-4; + nx = std::max(xl + eps, std::min(xr - eps, nx)); + p.x = nx; + } + // y is constrained to the reserved blend band so neither component ever + // reaches 0% / 100%, matching the sampler's clamp. + p.y = std::max(kGradientMinRatio, + std::min(kGradientMaxRatio, to_stored_y(m_selected_curve, vy))); + m_dragged_moved = true; + Refresh(); +} + +void GradientCurveEditor::on_leave(wxMouseEvent& evt) +{ + evt.Skip(); +} + +void GradientCurveEditor::on_size(wxSizeEvent& evt) +{ + Refresh(); + evt.Skip(); +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/GradientCurveEditor.hpp b/src/slic3r/GUI/GradientCurveEditor.hpp new file mode 100644 index 0000000000..f2e082aff5 --- /dev/null +++ b/src/slic3r/GUI/GradientCurveEditor.hpp @@ -0,0 +1,122 @@ +#ifndef slic3r_GradientCurveEditor_hpp_ +#define slic3r_GradientCurveEditor_hpp_ + +#include <vector> +#include <wx/colour.h> +#include <wx/event.h> +#include <wx/gdicmn.h> +#include <wx/geometry.h> +#include <wx/panel.h> + +#include "libslic3r/FilamentMixer.hpp" + +namespace Slic3r { +namespace GUI { + +// Photoshop-style curve editor for "Z progress -> first-component ratio" mapping. +// Curve evaluation uses cubic Hermite with PCHIP defaults plus optional per-anchor +// tangent overrides (m_in / m_out, NaN = use PCHIP default). The same evaluator +// (FilamentMixer::sample_gradient_curve) is shared with the slicing backend so what +// the editor renders matches the G-code output 1:1. +// +// Interaction model (PS Curves style): +// - Click or press-and-drag on the line body inserts a new anchor at the cursor x +// (snapped to the current smooth curve, NaN tangents) and starts dragging it. +// A pure click leaves an anchor sitting exactly on the previous curve shape; a +// drag moves the new anchor freely so the bump follows the cursor 1:1. +// - Dragging an existing anchor moves (x, y) and clears its m_in / m_out so the +// local curve returns to the PCHIP default shape around it. +// - Right-click on an interior anchor deletes it; endpoints stay locked. +class GradientCurveEditor : public wxPanel +{ +public: + using PointList = std::vector<GradientAnchor>; + + GradientCurveEditor(wxWindow* parent, + const wxColour& color_low = wxColour(217, 217, 217), + const wxColour& color_high = wxColour(217, 217, 217)); + + ~GradientCurveEditor() override; + + // Replace the entire point list. The widget enforces x in [0,1], y in [0,1], + // sorts by x, and clamps the first / last x to 0 / 1. Tangent overrides are + // preserved as-is (NaN entries continue to use PCHIP defaults). + void set_points(const PointList& pts); + const PointList& get_points() const { return m_points; } + + void set_colors(const wxColour& color_low, const wxColour& color_high); + + // Which curve currently responds to drag / add / delete and is drawn with the thick stroke. + // 0 = first component (color_low), 1 = second component (color_high). Storage layer is + // unaffected: m_points always represents component 0's ratio. + void set_selected_curve(int curve_idx); + int get_selected_curve() const { return m_selected_curve; } + + // Reset to a two-point linear curve from y0 at t=0 to y1 at t=1. + // Clears all tangent overrides. + void reset_to_linear(double y0, double y1); + // Flip the curve top to bottom (all y -> 1 - y; tangents negated to mirror shape). + void reverse(); + +private: + enum class DragMode { + None, // nothing armed + Anchor, // dragging an anchor (either existing or just inserted from a line hit) + }; + + void normalize_points(); + void emit_changed(); + + void on_paint(wxPaintEvent& evt); + void on_left_down(wxMouseEvent& evt); + void on_left_up(wxMouseEvent& evt); + void on_right_down(wxMouseEvent& evt); + void on_motion(wxMouseEvent& evt); + void on_leave(wxMouseEvent& evt); + void on_size(wxSizeEvent& evt); + + // Coordinate mapping between data (x, y in [0,1]) and pixels in plot area. + wxRect plot_rect() const; + // Sub-pixel accurate mapping, used for drawing: rounding the curve vertices to whole + // pixels leaves a staircase that anti-aliasing cannot smooth out, and the step is + // twice as coarse on 2x (Retina) displays. + wxPoint2DDouble data_to_px_f(double x, double y) const; + wxPoint data_to_px(double x, double y) const; + void px_to_data(int px, int py, double& x, double& y) const; + // Anchor hit test for the currently-selected curve (uses translated visual y). + int hit_test(int px, int py) const; // returns point index or -1 + // Line-body hit test across both curves. Returns 0/1 for which curve was hit, -1 if none. + // Prefers the selected curve when both are within threshold. seg_out (when non-null) + // receives the left-anchor index of the segment that was hit on the returned curve; + // on_left_down uses it to know where in m_points to insert a freshly-added anchor. + int hit_test_curve(int px, int py, int* seg_out = nullptr) const; + + // Sample the curve in stored space (component 0) at x. + double sample_curve_y(double x) const; + + // Symmetric translation between visual y (what the user sees / clicks) and stored y + // (component 0's ratio in m_points). + static double to_stored_y(int curve_idx, double visual_y) { + return (curve_idx == 0) ? visual_y : (1.0 - visual_y); + } + static double to_visual_y(int curve_idx, double stored_y) { + return (curve_idx == 0) ? stored_y : (1.0 - stored_y); + } + + PointList m_points; + wxColour m_color_low; + wxColour m_color_high; + + int m_selected_curve = 0; + DragMode m_drag_mode = DragMode::None; + int m_drag_idx = -1; // valid when m_drag_mode == Anchor + bool m_dragged_moved = false; +}; + +// Custom event raised when the curve is edited (drag / add / remove / reset / reverse). +wxDECLARE_EVENT(wxEVT_GRADIENT_CURVE_CHANGED, wxCommandEvent); + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_GradientCurveEditor_hpp_ diff --git a/src/slic3r/GUI/HMS.cpp b/src/slic3r/GUI/HMS.cpp index 4d67a398f7..f00e661df2 100644 --- a/src/slic3r/GUI/HMS.cpp +++ b/src/slic3r/GUI/HMS.cpp @@ -1,6 +1,7 @@ #include "HMS.hpp" #include "GUI.hpp" +#include "GUI_App.hpp" #include "DeviceManager.hpp" #include "DeviceCore/DevManager.h" #include "DeviceCore/DevUtil.h" @@ -45,7 +46,7 @@ int get_hms_info_version(std::string& version) std::string url = (boost::format("https://%1%/GetVersion.php?%2%") % hms_host % query_params).str(); Slic3r::Http http = Slic3r::Http::get(url); http.timeout_max(10) - .on_complete([&result, &version](std::string body, unsigned status){ + .on_complete([&version](std::string body, unsigned status){ try { json j = json::parse(body); if (j.contains("ver")) { @@ -94,7 +95,7 @@ int HMSQuery::download_hms_related(const std::string& hms_type, const std::strin BOOST_LOG_TRIVIAL(info) << "hms: download url = " << url; Slic3r::Http http = Slic3r::Http::get(url); - http.on_complete([this, receive_json, hms_type, &to_save_local, &j, & local_version](std::string body, unsigned status) { + http.on_complete([receive_json, hms_type, &to_save_local, &j, & local_version](std::string body, unsigned status) { try { j = json::parse(body); if (j.contains("result")) { diff --git a/src/slic3r/GUI/HMS.hpp b/src/slic3r/GUI/HMS.hpp index d2a87ebf42..c494539b36 100644 --- a/src/slic3r/GUI/HMS.hpp +++ b/src/slic3r/GUI/HMS.hpp @@ -1,7 +1,6 @@ #ifndef slic3r_HMS_hpp_ #define slic3r_HMS_hpp_ -#include "GUI_App.hpp" #include "GUI.hpp" #include "I18N.hpp" #include "Widgets/Label.hpp" @@ -11,7 +10,11 @@ #include "slic3r/Utils/Http.hpp" #include "libslic3r/Thread.hpp" #include "nlohmann/json.hpp" +#include <ctime> #include <mutex> +#include <string> +#include <unordered_map> +#include <vector> namespace Slic3r { @@ -26,12 +29,12 @@ namespace GUI { class HMSQuery { protected: - std::unordered_map<string, json> m_hms_info_jsons; // key-> device id type, the first three digits of SN number - std::unordered_map<string, json> m_hms_action_jsons;// key-> device id type + std::unordered_map<std::string, nlohmann::json> m_hms_info_jsons; // key-> device id type, the first three digits of SN number + std::unordered_map<std::string, nlohmann::json> m_hms_action_jsons;// key-> device id type std::unordered_map<wxString, wxImage> m_hms_local_images; // key-> image name mutable std::mutex m_hms_mutex; - std::unordered_map<string, time_t> m_cloud_hms_last_update_time; + std::unordered_map<std::string, std::time_t> m_cloud_hms_last_update_time; public: HMSQuery() { } @@ -61,18 +64,18 @@ private: // load hms void init_hms_info(const std::string& dev_type_id); void copy_from_data_dir_to_local(); - int download_hms_related(const std::string& hms_type, const std::string& dev_id_type, json* receive_json); - int load_from_local(const std::string& hms_type, const std::string& dev_id_type, json* receive_json, std::string& version_info); - int save_to_local(std::string lang, std::string hms_type, std::string dev_id_type, json save_json); + int download_hms_related(const std::string& hms_type, const std::string& dev_id_type, nlohmann::json* receive_json); + int load_from_local(const std::string& hms_type, const std::string& dev_id_type, nlohmann::json* receive_json, std::string& version_info); + int save_to_local(std::string lang, std::string hms_type, std::string dev_id_type, nlohmann::json save_json); std::string get_hms_file(std::string hms_type, std::string lang = std::string("en"), std::string dev_id_type = ""); // internal query - string get_dev_id_type(const MachineObject* obj) const; - wxString _query_hms_msg(const string& dev_id_type, const string& long_error_code, const string& lang_code = std::string("en")); + std::string get_dev_id_type(const MachineObject* obj) const; + wxString _query_hms_msg(const std::string& dev_id_type, const std::string& long_error_code, const std::string& lang_code = std::string("en")); - bool _is_internal_error(const string &dev_id_type, const string &long_error_code, const string &lang_code = std::string("en")); - wxString _query_error_msg(const string& dev_id_type, const std::string& long_error_code, const std::string& lang_code = std::string("en")); - wxString _query_error_image_action(const string& dev_id_type, const std::string& long_error_code, std::vector<int>& button_action); + bool _is_internal_error(const std::string &dev_id_type, const std::string &long_error_code, const std::string &lang_code = std::string("en")); + wxString _query_error_msg(const std::string& dev_id_type, const std::string& long_error_code, const std::string& lang_code = std::string("en")); + wxString _query_error_image_action(const std::string& dev_id_type, const std::string& long_error_code, std::vector<int>& button_action); }; int get_hms_info_version(std::string &version); @@ -85,4 +88,4 @@ std::string get_error_message(int error_code); } -#endif \ No newline at end of file +#endif diff --git a/src/slic3r/GUI/IMSlider.cpp b/src/slic3r/GUI/IMSlider.cpp index 35f4761257..c008963646 100644 --- a/src/slic3r/GUI/IMSlider.cpp +++ b/src/slic3r/GUI/IMSlider.cpp @@ -1733,7 +1733,7 @@ std::string IMSlider::get_label(int tick, LabelType label_type) ::sprintf(layer_height, "%.2f", m_values.empty() ? m_label_koef * value : m_values[value]); if (label_type == ltHeight) return std::string(layer_height); if (label_type == ltHeightWithLayer) { - char buffer[64]; + char buffer[90]; size_t layer_number; layer_number = m_draw_mode == dmSequentialFffPrint ? (m_values.empty() ? value : value + 1) : m_is_wipe_tower ? get_layer_number(value, label_type) + 1 : (m_values.empty() ? value : value + 1); ::sprintf(buffer, "%5s\n%5s", std::to_string(layer_number).c_str(), layer_height); diff --git a/src/slic3r/GUI/IMSlider.hpp b/src/slic3r/GUI/IMSlider.hpp index bd2399e990..45e4d6ccb8 100644 --- a/src/slic3r/GUI/IMSlider.hpp +++ b/src/slic3r/GUI/IMSlider.hpp @@ -176,7 +176,6 @@ private: // Use those values to disable selection of active extruders bool m_is_dark = false; - bool is_osx{false}; int m_min_value; int m_max_value; int m_lower_value; @@ -201,10 +200,6 @@ private: void *m_one_layer_on_hover_id; void *m_one_layer_off_id; void *m_one_layer_off_hover_id; - void* m_one_layer_on_light_id; - void* m_one_layer_on_hover_light_id; - void* m_one_layer_off_light_id; - void* m_one_layer_off_hover_light_id; void* m_one_layer_on_dark_id; void* m_one_layer_on_hover_dark_id; void* m_one_layer_off_dark_id; diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index 8974a169d1..eeaadd0cbd 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -2404,6 +2404,25 @@ void ImGuiWrapper::draw( } } +void ImGuiWrapper::draw_gradient_ramp(ImDrawList *draw_list, const ImVec2 &top_left, const ImVec2 &bottom_right, const std::vector<wxColour> &ramp) +{ + if (draw_list == nullptr || ramp.empty() || bottom_right.x <= top_left.x || bottom_right.y <= top_left.y) + return; + + const int rows = std::max(1, (int) std::lround(bottom_right.y - top_left.y)); + const float row_h = (bottom_right.y - top_left.y) / rows; + const size_t last = ramp.size() - 1; + for (int r = 0; r < rows; ++r) { + // Row 0 is the top of the rect and so takes the ramp's last entry, the model's top. + const double t = (rows > 1) ? (double) (rows - 1 - r) / (rows - 1) : 0.5; + const wxColour &c = ramp[(size_t) (t * last + 0.5)]; + // The bottom row snaps to the rect's edge so rounding never leaves a sliver uncovered. + const float y0 = top_left.y + r * row_h; + const float y1 = (r + 1 == rows) ? bottom_right.y : top_left.y + (r + 1) * row_h; + draw_list->AddRectFilled({top_left.x, y0}, {bottom_right.x, y1}, IM_COL32(c.Red(), c.Green(), c.Blue(), c.Alpha())); + } +} + void ImGuiWrapper::draw_cross_hair(const ImVec2 &position, float radius, ImU32 color, int num_segments, float thickness) { auto draw_list = ImGui::GetOverlayDrawList(); draw_list->AddCircle(position, radius, color, num_segments, thickness); @@ -2463,6 +2482,19 @@ static const ImWchar ranges_keyboard_shortcuts[] = }; #endif // __APPLE__ +// Names drawn through the atlas come from file names and CAD data, not from the UI language. +// GetGlyphRangesDefault() already gives every language the CJK ideographs, which is why a +// Chinese file name renders under an English UI; these are the alphabetic scripts it omits. +// Codepoints the font lacks are skipped at build time, so only existing glyphs cost anything. +static const ImWchar ranges_language_independent[] = +{ + 0x0100, 0x024F, // Latin Extended-A and Extended-B + 0x0370, 0x03FF, // Greek and Coptic + 0x0400, 0x04FF, // Cyrillic + 0x1E00, 0x1EFF, // Latin Extended Additional (Vietnamese) + 0, +}; + std::vector<unsigned char> ImGuiWrapper::load_svg(const std::string& bitmap_name, unsigned target_width, unsigned target_height, unsigned *outwidth, unsigned *outheight) { @@ -2773,6 +2805,7 @@ void ImGuiWrapper::init_font(bool compress) ImFontAtlas::GlyphRangesBuilder builder; builder.AddRanges(m_glyph_ranges); builder.AddRanges(ImGui::GetIO().Fonts->GetGlyphRangesDefault()); + builder.AddRanges(ranges_language_independent); #ifdef __APPLE__ if (m_font_cjk) // Apple keyboard shortcuts are only contained in the CJK fonts. @@ -2794,12 +2827,17 @@ void ImGuiWrapper::init_font(bool compress) // Orca: temp fix for Korean font auto font_name_regular = "HarmonyOS_Sans_SC_Regular.ttf"; auto font_name_bold = "HarmonyOS_Sans_SC_Bold.ttf"; + // The Korean and Thai fonts cover their own script and little else, so they need the + // default font merged in behind them to reach the full range. + bool needs_glyph_fallback = false; if(m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesKorean()) { font_name_regular = "NanumGothic-Regular.ttf"; font_name_bold = "NanumGothic-Bold.ttf"; + needs_glyph_fallback = true; } else if (m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesThai()) { font_name_regular = "Sarabun-Medium.ttf"; font_name_bold = "Sarabun-SemiBold.ttf"; + needs_glyph_fallback = true; } default_font = io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/" + font_name_regular).c_str(), m_font_size, &cfg, ranges.Data); if (default_font == nullptr) { @@ -2809,12 +2847,26 @@ void ImGuiWrapper::init_font(bool compress) } } + // A merged font only supplies glyphs the font ahead of it lacks, so this fills the gaps + // without restyling anything the script font already covers. + if (needs_glyph_fallback) { + ImFontConfig fallback_cfg = cfg; + fallback_cfg.MergeMode = true; + io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/HarmonyOS_Sans_SC_Regular.ttf").c_str(), m_font_size, &fallback_cfg, ranges.Data); + } + bold_font = io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/" + font_name_bold).c_str(), m_font_size, &cfg, ranges.Data); if (bold_font == nullptr) { bold_font = io.Fonts->AddFontDefault(); if (bold_font == nullptr) { throw Slic3r::RuntimeError("ImGui: Could not load deafult font"); } } + if (needs_glyph_fallback) { + ImFontConfig fallback_cfg = cfg; + fallback_cfg.MergeMode = true; + io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/HarmonyOS_Sans_SC_Bold.ttf").c_str(), m_font_size, &fallback_cfg, ranges.Data); + } + if (m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesThai()) { default_font->Scale *= 1.25f; bold_font->Scale *= 1.25f; @@ -2864,13 +2916,18 @@ void ImGuiWrapper::init_font(bool compress) glsafe(::glGetIntegerv(GL_MAX_TEXTURE_SIZE, &gl_max_tex_size)); constexpr int max_retries = 6; for (int attempt = 0; attempt < max_retries && io.Fonts->TexHeight > gl_max_tex_size; ++attempt) { - io.Fonts->TexDesiredWidth = (io.Fonts->TexDesiredWidth > 0 ? io.Fonts->TexDesiredWidth : io.Fonts->TexWidth) * 2; + const int width = io.Fonts->TexDesiredWidth > 0 ? io.Fonts->TexDesiredWidth : io.Fonts->TexWidth; + // Both dimensions share the same limit, so widening past it would only trade an + // illegal height for an illegal width. + if (width * 2 > gl_max_tex_size) + break; + io.Fonts->TexDesiredWidth = width * 2; io.Fonts->Build(); } if (io.Fonts->TexHeight > gl_max_tex_size) { - // Shouldn't really happen - BOOST_LOG_TRIVIAL(error) << "Font atlas height " << io.Fonts->TexHeight - << " still exceeds GL_MAX_TEXTURE_SIZE (" << gl_max_tex_size << ")" + // Needs both a very large glyph set and a small GL_MAX_TEXTURE_SIZE. + BOOST_LOG_TRIVIAL(error) << "Font atlas " << io.Fonts->TexWidth << "x" << io.Fonts->TexHeight + << " does not fit GL_MAX_TEXTURE_SIZE (" << gl_max_tex_size << ")" << " after " << max_retries << " attempts; rendering may be incomplete"; } @@ -3318,8 +3375,9 @@ const char* ImGuiWrapper::clipboard_get(void* user_data) wxTextDataObject data; wxTheClipboard->GetData(data); - if (data.GetTextLength() > 0) { - self->m_clipboard_text = into_u8(data.GetText()); + const wxString text = data.GetText(); + if (text.Length() > 0) { + self->m_clipboard_text = into_u8(text); res = self->m_clipboard_text.c_str(); } } diff --git a/src/slic3r/GUI/ImGuiWrapper.hpp b/src/slic3r/GUI/ImGuiWrapper.hpp index b586094ab3..db94b3edcb 100644 --- a/src/slic3r/GUI/ImGuiWrapper.hpp +++ b/src/slic3r/GUI/ImGuiWrapper.hpp @@ -3,10 +3,12 @@ #include <string> #include <map> +#include <vector> #include <cstdlib> #include <imgui/imgui.h> +#include <wx/colour.h> #include <wx/string.h> #include "libslic3r/Point.hpp" @@ -299,6 +301,20 @@ public: int num_segments = 0, float thickness = 4.f); + /// <summary> + /// Fill a rect with a filament gradient ramp, one band per pixel row, ramp.front() along + /// the bottom edge. Bands rather than one interpolated rect, because the ramp follows the + /// slot's gradient curve and ImGui's corner interpolation could only draw a straight fade. + /// </summary> + /// <param name="draw_list">Define where to draw it</param> + /// <param name="top_left">Upper left corner of the rect</param> + /// <param name="bottom_right">Lower right corner of the rect</param> + /// <param name="ramp">Colours printed, bottom of the model first</param> + static void draw_gradient_ramp(ImDrawList * draw_list, + const ImVec2 & top_left, + const ImVec2 & bottom_right, + const std::vector<wxColour> &ramp); + /// <summary> /// Check that font ranges contain all chars in string /// (rendered Unicodes are stored in GlyphRanges) diff --git a/src/slic3r/GUI/ImageDPIFrame.cpp b/src/slic3r/GUI/ImageDPIFrame.cpp index 2133f18784..8dad9c44da 100644 --- a/src/slic3r/GUI/ImageDPIFrame.cpp +++ b/src/slic3r/GUI/ImageDPIFrame.cpp @@ -74,7 +74,7 @@ bool ImageDPIFrame::Show(bool show) } void ImageDPIFrame::set_bitmap(const wxBitmap &bit_map) { - if (&bit_map && bit_map.IsOk()) { + if (bit_map.IsOk()) { m_bitmap->SetBitmap(bit_map); } } diff --git a/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp b/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp index f988f622ce..31bd6728b1 100644 --- a/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp +++ b/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp @@ -45,24 +45,26 @@ void CreateFontStyleImagesJob::process(Ctl &ctl) for (const ExPolygon &shape : shapes) bounding_box.merge(BoundingBox(shape.contour.points)); for (ExPolygon &shape : shapes) shape.translate(-bounding_box.min); - - // calculate conversion from FontPoint to screen pixels by size of font - double scale = get_text_shape_scale(item.prop, *item.font.font_file) * m_input.ppm; - scales[index] = scale; - //double scale = font_prop.size_in_mm * SCALING_FACTOR; - BoundingBoxf bb2(bounding_box.min.cast<double>(), - bounding_box.max.cast<double>()); + if (bounding_box.size().x() < 1 || bounding_box.size().y() < 1) + continue; // or however the font job's degenerate-box case is handled + + // Normalize to fit max_size, exactly like CreateFontImageJob does against m_input.size. + // Fit by height (matches row height), then clamp width if needed. + constexpr float preview_padding_px = 2.f; // margin for AA sampling, tune to your AA kernel radius + + double scale = m_input.max_size.y() / (double) bounding_box.size().y(); + BoundingBoxf bb2(bounding_box.min.cast<double>(), bounding_box.max.cast<double>()); bb2.scale(scale); - image.tex_size.x = std::ceil(bb2.max.x() - bb2.min.x()); - image.tex_size.y = std::ceil(bb2.max.y() - bb2.min.y()); - // crop image width - if (image.tex_size.x > m_input.max_size.x()) + // crop width only if the (now height-normalized) text is too wide + image.tex_size.x = std::ceil(bb2.max.x() - bb2.min.x()) + 2 * preview_padding_px; + image.tex_size.y = std::ceil(bb2.max.y() - bb2.min.y()) + 2 * preview_padding_px; + + if (image.tex_size.x > m_input.max_size.x()) image.tex_size.x = m_input.max_size.x(); - // crop image height - if (image.tex_size.y > m_input.max_size.y()) - image.tex_size.y = m_input.max_size.y(); + + scales[index] = scale; } // arrange bounding boxes diff --git a/src/slic3r/GUI/Jobs/FillBedJob.cpp b/src/slic3r/GUI/Jobs/FillBedJob.cpp index a9a66cb96f..5364cb5648 100644 --- a/src/slic3r/GUI/Jobs/FillBedJob.cpp +++ b/src/slic3r/GUI/Jobs/FillBedJob.cpp @@ -184,7 +184,7 @@ void FillBedJob::prepare() ap.poly = m_selected.front().poly; ap.bed_idx = PartPlateList::MAX_PLATES_COUNT; ap.itemid = -1; - ap.setter = [this, mi, offset](const ArrangePolygon &p) { + ap.setter = [this, offset](const ArrangePolygon &p) { ModelObject *mo = m_plater->model().objects[m_object_idx]; ModelObject *obj; if (m_instances) { diff --git a/src/slic3r/GUI/Jobs/OrientJob.cpp b/src/slic3r/GUI/Jobs/OrientJob.cpp index 7347bad6a2..ee8ea875c0 100644 --- a/src/slic3r/GUI/Jobs/OrientJob.cpp +++ b/src/slic3r/GUI/Jobs/OrientJob.cpp @@ -149,6 +149,46 @@ void OrientJob::prepare() } } +/// parameters to minimize support area +static void setMinimalSupportAreaPrams(Slic3r::orientation::OrientParams &out) +{ + out.TAR_A = 0.015f; + out.TAR_B = 0.177f; + out.RELATIVE_F = 20; + out.CONTOUR_F = 0.5f; + out.BOTTOM_F = 2.5f; + out.BOTTOM_HULL_F = 0.1f; + out.TAR_C = 0.1f; + out.TAR_D = 1; + out.TAR_E = 0.0115f; + out.FIRST_LAY_H = 0.2f; // 0.0475; + out.VECTOR_TOL = -0.00083f; + out.NEGL_FACE_SIZE = 0.01f; + out.ASCENT = -0.5f; + out.PLAFOND_ADV = 0.0599f; + out.CONTOUR_AMOUNT = 0.0182427f; + out.OV_H = 2.574f; + out.height_offset = 2.3728f; + out.height_log = 0.041375f; + out.height_log_k = 1.9325457f; + out.LAF_MAX = 0.999f; // cos(1.4\degree) for low angle face 0.9997f + out.LAF_MIN = 0.97f; // cos(14\degree) 0.9703f + out.TAR_LAF = 0.001f; // 0.01f + out.TAR_PROJ_AREA = 0.1f; + out.BOTTOM_MIN = 0.1f; // min bottom area. If lower than it the object may be unstable + out.BOTTOM_MAX = 2000; // max bottom area. If get to it the object is stable enough (further increase bottom area won't do more help) + out.height_to_bottom_hull_ratio_MIN = 1, + out.BOTTOM_HULL_MAX = 2000; // max bottom hull area + out.APPERANCE_FACE_SUPP = 3; // penalty of generating supports on appearance face + out.overhang_angle = 60.f; + out.use_low_angle_face = true; + out.min_volume = false; + out.fun_dir = {}; + out.parallel = true; + out.progressind = {}; + out.stopcondition = {}; +} + void OrientJob::process(Ctl &ctl) { static const auto arrangestr = _u8L("Orienting..."); @@ -161,9 +201,8 @@ void OrientJob::process(Ctl &ctl) const GLCanvas3D::OrientSettings& settings = m_plater->canvas3D()->get_orient_settings(); orientation::OrientParams params; - orientation::OrientParamsArea params_area; if (settings.min_area) { - memcpy(¶ms, ¶ms_area, sizeof(params)); + setMinimalSupportAreaPrams(params); params.min_volume = false; } else { diff --git a/src/slic3r/GUI/Jobs/PlaterWorker.hpp b/src/slic3r/GUI/Jobs/PlaterWorker.hpp index d38a57b6a6..f22bd6d511 100644 --- a/src/slic3r/GUI/Jobs/PlaterWorker.hpp +++ b/src/slic3r/GUI/Jobs/PlaterWorker.hpp @@ -79,9 +79,12 @@ class PlaterWorker: public Worker { steady_clock::time_point finalize_end = steady_clock::now(); long long finalize_duration = duration_cast<milliseconds>(finalize_end - finalize_start).count(); + // Bound first so typeid's operand is not a call. typeid evaluates it for a + // polymorphic type, which clang reports as -Wpotentially-evaluated-expression. + const Job &job = *m_job; BOOST_LOG_TRIVIAL(info) << std::fixed // do not use scientific notations - << "Job '" << typeid(*m_job).name() << "' " + << "Job '" << typeid(job).name() << "' " << "spend " << m_process_duration + finalize_duration << "ms " << "(process " << m_process_duration << "ms + finalize " << finalize_duration << "ms)"; diff --git a/src/slic3r/GUI/Jobs/PrintJob.cpp b/src/slic3r/GUI/Jobs/PrintJob.cpp index d79deb955b..dae05494e3 100644 --- a/src/slic3r/GUI/Jobs/PrintJob.cpp +++ b/src/slic3r/GUI/Jobs/PrintJob.cpp @@ -135,7 +135,6 @@ void PrintJob::process(Ctl &ctl) { /* display info */ std::string msg; - wxString error_str; int curr_percent = 10; NetworkAgent* m_agent = wxGetApp().getAgent(); AppConfig* config = wxGetApp().app_config; @@ -402,9 +401,7 @@ void PrintJob::process(Ctl &ctl) &is_try_lan_mode, &is_try_lan_mode_failed, &msg, - &error_str, &curr_percent, - &error_text, StagePercentPoint ](int stage, int code, std::string info) { @@ -491,7 +488,7 @@ void PrintJob::process(Ctl &ctl) DeviceManager* dev = wxGetApp().getDeviceManager(); MachineObject* obj = dev->get_selected_machine(); - auto wait_fn = [this, &ctl, curr_percent, &obj](int state, std::string job_info) { + auto wait_fn = [&ctl, &obj](int state, std::string job_info) { BOOST_LOG_TRIVIAL(info) << "print_job: get_job_info = " << job_info; if (!obj->is_support_wait_sending_finish) { diff --git a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp index f97b03bf62..7f8219008f 100644 --- a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp +++ b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp @@ -55,7 +55,7 @@ void RotoptimizeJob::process(Ctl &ctl) sla::RotOptimizeParams{} .accuracy(m_accuracy) .print_config(&m_default_print_cfg) - .statucb([this, &prev_status, &ctl/*, &statustxt*/](int s) + .statucb([&ctl/*, &statustxt*/](int s) { return !ctl.was_canceled(); }); diff --git a/src/slic3r/GUI/Jobs/SendJob.cpp b/src/slic3r/GUI/Jobs/SendJob.cpp index bb52367188..67ce02b476 100644 --- a/src/slic3r/GUI/Jobs/SendJob.cpp +++ b/src/slic3r/GUI/Jobs/SendJob.cpp @@ -213,7 +213,6 @@ void SendJob::process(Ctl &ctl) params.password = m_access_code; params.use_ssl_for_ftp = m_local_use_ssl_for_ftp; params.use_ssl_for_mqtt = m_local_use_ssl; - wxString error_text; std::string msg_text; const int StagePercentPoint[(int)PrintingStageFinished + 1] = { @@ -227,7 +226,7 @@ void SendJob::process(Ctl &ctl) }; auto update_fn = [this, &ctl, - &msg, &curr_percent, &error_text, StagePercentPoint](int stage, int code, std::string info) { + &msg, &curr_percent, StagePercentPoint](int stage, int code, std::string info) { if (stage == SendingPrintJobStage::PrintingStageCreate) { if (this->connection_type == "lan") { msg = _u8L("Sending G-code file over LAN"); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 7b638e3316..675bc8da27 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -493,9 +493,8 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ }); //BBS - Bind(EVT_SELECT_TAB, [this](wxCommandEvent&evt) { - TabPosition pos = (TabPosition)evt.GetInt(); - m_tabpanel->SetSelection(pos); + Bind(EVT_SELECT_TAB, [this](wxCommandEvent& evt) { + m_tabpanel->SelectPageByName(evt.GetString()); }); Bind(EVT_SYNC_CLOUD_PRESET, &MainFrame::on_select_default_preset, this); @@ -702,13 +701,13 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ } return;} #endif - if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SetSelection(tpPreview); } return; } + if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW); } return; } if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'G') { m_plater->apply_background_progress(); m_print_enable = get_enable_print_status(); m_print_btn->Enable(m_print_enable); if (m_print_enable) { - if (wxGetApp().preset_bundle->use_bbl_network()) + if (wxGetApp().preset_bundle->use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents")) wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_PRINT_PLATE)); else wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SEND_GCODE)); @@ -723,7 +722,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'S') { if (can_save_as()) m_plater->save_project(true); return;} else if (evt.CmdDown() && evt.GetKeyCode() == 'S') { if (can_save()) m_plater->save_project(); return;} if (evt.CmdDown() && evt.GetKeyCode() == 'F') { - if (m_plater && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview)) { + if (m_plater && is_prepare_or_preview_tab()) { m_plater->sidebar().can_search(); } } @@ -747,7 +746,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ evt.Skip(); }); - Bind(wxEVT_SHOW, [this](wxShowEvent &evt) { + Bind(wxEVT_SHOW, [](wxShowEvent &evt) { DeviceManager *manger = wxGetApp().getDeviceManager(); if (manger) { evt.IsShown() ? manger->start_refresher() : manger->stop_refresher(); @@ -1007,8 +1006,8 @@ void MainFrame::update_layout() m_layout = layout; // From the very beginning the Print settings should be selected - //m_last_selected_tab = m_layout == ESettingsLayout::Dlg ? 0 : 1; - m_last_selected_tab = 1; + //m_last_selected_tab = m_layout == ESettingsLayout::Dlg ? TAB_ID_HOME : TAB_ID_PREPARE; + m_last_selected_tab = TAB_ID_PREPARE; // Set new settings switch (m_layout) @@ -1016,14 +1015,18 @@ void MainFrame::update_layout() case ESettingsLayout::Old: { m_plater->Reparent(m_tabpanel); - m_tabpanel->InsertPage(tp3DEditor, m_plater, _L("Prepare"), std::string("tab_3d_active"), std::string("tab_3d_active"), false); - m_tabpanel->InsertPage(tpPreview, m_plater, _L("Preview"), std::string("tab_preview_active"), std::string("tab_preview_active"), false); + // Right after Home — or first, when there is no Home tab (PositionAfter() would + // append instead, and by now the other built-in tabs are already in place). + const int home_idx = m_tabpanel->FindPageByName(TAB_ID_HOME); + const size_t prepare_pos = (home_idx == wxNOT_FOUND) ? 0 : static_cast<size_t>(home_idx) + 1; + m_tabpanel->InsertPage(prepare_pos, TAB_ID_PREPARE, m_plater, _L("Prepare"), "tab_3d_active"); + m_tabpanel->InsertPage(prepare_pos + 1, TAB_ID_PREVIEW, m_plater, _L("Preview"), "tab_preview_active"); m_main_sizer->Add(m_tabpanel, 1, wxEXPAND | wxTOP, 0); m_tabpanel->Bind(wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED, [this](wxCommandEvent& evt) { // jump to 3deditor under preview_only mode - if (evt.GetId() == tp3DEditor){ + if (evt.GetId() == m_tabpanel->FindPageByName(TAB_ID_PREPARE)) { Sidebar& sidebar = GUI::wxGetApp().sidebar(); if (sidebar.need_auto_sync_after_connect_printer()) { sidebar.set_need_auto_sync_after_connect_printer(false); @@ -1107,6 +1110,9 @@ void MainFrame::update_edge_panels() void MainFrame::shutdown() { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "MainFrame::shutdown enter"; + if (m_project != nullptr) + m_project->shutdown(); + m_plugin_pages.shutdown(); #ifdef __WXGTK__ // Edge panels are child windows — wxWidgets destroys them automatically. m_edge_bottom = nullptr; @@ -1252,15 +1258,14 @@ void MainFrame::init_tabpanel() { #endif //BBS wxWindow* panel = m_tabpanel->GetCurrentPage(); - int sel = m_tabpanel->GetSelection(); //wxString page_text = m_tabpanel->GetPageText(sel); - m_last_selected_tab = m_tabpanel->GetSelection(); + m_last_selected_tab = m_tabpanel->GetSelectedPageName(); if (panel == m_plater) { - if (sel == tp3DEditor) { + if (m_last_selected_tab == TAB_ID_PREPARE) { wxPostEvent(m_plater, SimpleEvent(EVT_GLVIEWTOOLBAR_3D)); m_param_panel->OnActivate(); } - else if (sel == tpPreview) { + else if (m_last_selected_tab == TAB_ID_PREVIEW) { m_plater->reset_check_status(); if (!m_plater->check_ams_status(m_slice_select == eSliceAll)) return; @@ -1275,7 +1280,7 @@ void MainFrame::init_tabpanel() { //monitor } #ifndef __APPLE__ - if (sel == tp3DEditor) { + if (m_last_selected_tab == TAB_ID_PREPARE) { m_topbar->EnableUndoRedoItems(); } else { @@ -1285,34 +1290,16 @@ void MainFrame::init_tabpanel() { if (panel) panel->SetFocus(); - - /*switch (sel) { - case TabPosition::tpHome: - show_option(false); - break; - case TabPosition::tp3DEditor: - show_option(true); - break; - case TabPosition::tpPreview: - show_option(true); - break; - case TabPosition::tpMonitor: - show_option(false); - break; - default: - show_option(false); - break; - }*/ }); if (wxGetApp().is_editor()) { m_webview = new WebViewPanel(m_tabpanel); Bind(EVT_LOAD_URL, [this](wxCommandEvent &evt) { wxString url = evt.GetString(); - select_tab(MainFrame::tpHome); + select_tab(TAB_ID_HOME); m_webview->load_url(url); }); - m_tabpanel->AddPage(m_webview, "", "tab_home_active", "tab_home_active", false); + m_tabpanel->AddPage(TAB_ID_HOME, m_webview, "", "tab_home_active"); m_param_panel = new ParamsPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL); } @@ -1327,7 +1314,7 @@ void MainFrame::init_tabpanel() { //BBS add pages m_monitor = new MonitorPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_monitor->SetBackgroundColour(*wxWHITE); - m_tabpanel->AddPage(m_monitor, _L("Device"), std::string("tab_monitor_active"), std::string("tab_monitor_active"), false); + m_tabpanel->AddPage(TAB_ID_MONITOR, m_monitor, _L("Device"), "tab_monitor_active"); m_printer_view = new PrinterWebView(m_tabpanel); Bind(EVT_LOAD_PRINTER_URL, [this](LoadPrinterViewEvent &evt) { @@ -1342,16 +1329,20 @@ void MainFrame::init_tabpanel() { m_multi_machine = new MultiMachinePage(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_multi_machine->SetBackgroundColour(*wxWHITE); // TODO: change the bitmap - m_tabpanel->AddPage(m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"), std::string("tab_multi_active"), false); + m_tabpanel->AddPage(TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), "tab_multi_active"); } m_project = new ProjectPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_project->SetBackgroundColour(*wxWHITE); - m_tabpanel->AddPage(m_project, _L("Project"), std::string("tab_auxiliary_active"), std::string("tab_auxiliary_active"), false); + m_tabpanel->AddPage(TAB_ID_PROJECT, m_project, _L("Project"), "tab_auxiliary_active"); m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_calibration->SetBackgroundColour(*wxWHITE); - m_tabpanel->AddPage(m_calibration, _L("Calibration"), std::string("tab_calibration_active"), std::string("tab_calibration_active"), false); + m_tabpanel->AddPage(TAB_ID_CALIBRATION, m_calibration, _L("Calibration"), "tab_calibration_active"); + + // Plugin pages are appended after the built-in tabs; their ids are namespaced + // (plugin.<plugin_key>.<name>) so they can't collide with the built-in TAB_ID_* constants. + m_plugin_pages.initialize(m_tabpanel); if (m_plater) { // load initial config @@ -1368,9 +1359,97 @@ void MainFrame::init_tabpanel() { } // SoftFever -void MainFrame::show_device(bool bBBLPrinter) { +void MainFrame::show_device(bool should_use_native) { auto idx = -1; - if (bBBLPrinter) { + + const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); + + // The web Device page is the extra tab printer-agents mode shows alongside the native one. + // Printers that drive the native Bambu device tab have nothing to put in it, so they don't + // get it — otherwise a Bambu user sees two Device tabs, one of them permanently empty. + const bool want_web_device_tab = use_printer_agents && wxGetApp().preset_bundle != nullptr && + !wxGetApp().preset_bundle->use_bbl_device_tab(); + + // Remove the extra page before switching to any layout that shouldn't have it. + if (!want_web_device_tab) { + if ((idx = m_tabpanel->FindPageByName(TAB_ID_MONITOR_WEB)) != wxNOT_FOUND) { + m_printer_view->Show(false); + m_tabpanel->RemovePage(idx); + } + } + + if (use_printer_agents) { + if (!m_monitor) { + m_monitor = new MonitorPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); + m_monitor->SetBackgroundColour(*wxWHITE); + } + + if (m_tabpanel->FindPage(m_monitor) == wxNOT_FOUND) { + if ((idx = m_tabpanel->FindPage(m_printer_view)) != wxNOT_FOUND) { + m_printer_view->Show(false); + m_tabpanel->RemovePage(idx); + } + m_monitor->Show(false); + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_monitor, + _L("Device"), "tab_monitor_active"); + } + + if (m_printer_view == nullptr) { + m_printer_view = new PrinterWebView(m_tabpanel); + Bind(EVT_LOAD_PRINTER_URL, [this](LoadPrinterViewEvent& evt) { + wxString url = evt.GetString(); + wxString key = evt.GetAPIkey(); + // select_tab(MainFrame::tpMonitor); + m_printer_view->load_url(url, key); + }); + } + + if (wxGetApp().is_enable_multi_machine()) { + if (!m_multi_machine) { + m_multi_machine = new MultiMachinePage(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); + m_multi_machine->SetBackgroundColour(*wxWHITE); + } + // TODO: change the bitmap + if (m_tabpanel->FindPage(m_multi_machine) == wxNOT_FOUND) { + m_multi_machine->Show(false); + // Past the web Device tab when it is already there, so enabling multi-machine + // later can't wedge this page between the two Device tabs. + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR_WEB, TAB_ID_MONITOR}), + TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), "tab_multi_active"); + } + } + if (!m_calibration) { + m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); + m_calibration->SetBackgroundColour(*wxWHITE); + } + if (m_tabpanel->FindPage(m_calibration) == wxNOT_FOUND) { + m_calibration->Show(false); + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PROJECT}), TAB_ID_CALIBRATION, m_calibration, + _L("Calibration"), "tab_calibration_active"); + } + + if (want_web_device_tab) { + if ((idx = m_tabpanel->FindPage(m_printer_view)) == wxNOT_FOUND) { + m_printer_view->Show(false); + // Immediately right of the native Device tab, not at the end of the tab bar. + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR}), TAB_ID_MONITOR_WEB, + m_printer_view, _L("Device (Web)"), "tab_monitor_active"); + } else { + m_tabpanel->SetPageText(idx, _L("Device (Web)")); + } + } + +#ifdef _MSW_DARK_MODE + wxGetApp().UpdateDarkUIWin(this); +#endif // _MSW_DARK_MODE + + fit_tab_labels(); // ORCA on printer change + m_plugin_pages.relayout(); // re-sync plugin tabs against the native tabs just mutated above + + return; + } + + if (should_use_native) { if (m_tabpanel->FindPage(m_monitor) != wxNOT_FOUND) { fit_tab_labels(); // ORCA on printer change - same button layout return; @@ -1387,7 +1466,8 @@ void MainFrame::show_device(bool bBBLPrinter) { m_monitor->SetBackgroundColour(*wxWHITE); } m_monitor->Show(false); - m_tabpanel->InsertPage(tpMonitor, m_monitor, _L("Device"), std::string("tab_monitor_active"), std::string("tab_monitor_active")); + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_monitor, + _L("Device"), "tab_monitor_active"); if (wxGetApp().is_enable_multi_machine()) { if (!m_multi_machine) { @@ -1396,18 +1476,18 @@ void MainFrame::show_device(bool bBBLPrinter) { } // TODO: change the bitmap m_multi_machine->Show(false); - m_tabpanel->InsertPage(tpMultiDevice, m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"), - std::string("tab_multi_active"), false); + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR}), TAB_ID_MULTI_DEVICE, m_multi_machine, + _L("Multi-device"), "tab_multi_active"); } if (!m_calibration) { m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_calibration->SetBackgroundColour(*wxWHITE); } m_calibration->Show(false); - // Calibration is always the last page, so don't use InsertPage here. Otherwise, if multi_machine page is not enabled, - // the calibration tab won't be properly added as well, due to the TabPosition::tpCalibration no longer matches the real tab position. - m_tabpanel->AddPage(m_calibration, _L("Calibration"), std::string("tab_calibration_active"), - std::string("tab_calibration_active"), false); + // Last of the built-in tabs, but plugin tabs already sit past it — anchor rather than + // append, so its position doesn't depend on the relayout() below running afterwards. + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PROJECT}), TAB_ID_CALIBRATION, m_calibration, + _L("Calibration"), "tab_calibration_active"); #ifdef _MSW_DARK_MODE wxGetApp().UpdateDarkUIWin(this); @@ -1440,10 +1520,17 @@ void MainFrame::show_device(bool bBBLPrinter) { }); } m_printer_view->Show(false); - m_tabpanel->InsertPage(tpMonitor, m_printer_view, _L("Device"), std::string("tab_monitor_active"), - std::string("tab_monitor_active")); + m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_printer_view, + _L("Device"), "tab_monitor_active"); } fit_tab_labels(); // ORCA on printer change + m_plugin_pages.relayout(); // re-sync plugin tabs against the native tabs just mutated above +} + +bool MainFrame::is_prepare_or_preview_tab() const +{ + const wxString tab = m_tabpanel->GetSelectedPageName(); + return tab == TAB_ID_PREPARE || tab == TAB_ID_PREVIEW; } void MainFrame::fit_tab_labels() @@ -1475,7 +1562,7 @@ void MainFrame::fit_tab_labels() bool MainFrame::preview_only_hint() { if (m_plater && (m_plater->only_gcode_mode() || (m_plater->using_exported_file()))) { - BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2% in preview mode")%m_tabpanel->GetSelection() %tp3DEditor; + BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2% in preview mode")%m_tabpanel->GetSelectedPageName() %wxString(TAB_ID_PREPARE); ConfirmBeforeSendDialog confirm_dlg(this, wxID_ANY, _L("Warning")); confirm_dlg.Bind(EVT_SECONDARY_CHECK_CONFIRM, [this](wxCommandEvent& e) { @@ -1793,22 +1880,22 @@ bool MainFrame::can_clone() const { bool MainFrame::can_select() const { - return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->model().objects.empty(); + return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->model().objects.empty(); } bool MainFrame::can_deselect() const { - return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->is_selection_empty(); + return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->is_selection_empty(); } bool MainFrame::can_delete() const { - return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->is_selection_empty(); + return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->is_selection_empty(); } bool MainFrame::can_delete_all() const { - return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->model().objects.empty(); + return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->model().objects.empty(); } bool MainFrame::can_reslice() const @@ -1917,7 +2004,7 @@ wxBoxSizer* MainFrame::create_side_tools() wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_ALL)); else wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); - this->m_tabpanel->SetSelection(tpPreview); + this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW); } }); @@ -1999,7 +2086,8 @@ wxBoxSizer* MainFrame::create_side_tools() SidePopup* p = new SidePopup(this); if (wxGetApp().preset_bundle - && !wxGetApp().preset_bundle->is_bbl_vendor()) { + && !wxGetApp().preset_bundle->is_bbl_vendor() + && !wxGetApp().app_config->get_bool("use_printer_agents")) { // ThirdParty Buttons SideButton* export_gcode_btn = new SideButton(p, _L("Export G-code file"), ""); export_gcode_btn->SetCornerRadius(0); @@ -2132,7 +2220,7 @@ wxBoxSizer* MainFrame::create_side_tools() const auto preset_bundle = wxGetApp().preset_bundle; if (preset_bundle) { - if (preset_bundle->use_bbl_network()) { + if (preset_bundle->use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents")) { // BBL network support everything } else { support_send = false; // All 3rd print hosts do not have the send options @@ -2242,6 +2330,11 @@ bool MainFrame::get_enable_slice_status() } } + // A mixed filament whose components were deleted, or whose components disagree in type, + // cannot be resolved at slicing time. Block the slice until the user fixes it. + if (enable && m_plater->sidebar().has_broken_mixed_filament()) + enable = false; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": m_slice_select %1%, enable= %2% ")%m_slice_select %enable; return enable; } @@ -2757,12 +2850,12 @@ void MainFrame::init_menubar_as_editor() [this](wxCommandEvent&) { if (m_plater) { m_plater->add_model(); } }, "", nullptr, [this](){return can_add_models(); }, this); #endif - append_menu_item(import_menu, wxID_ANY, _L("Import Zip Archive") + dots, _L("Load models contained within a zip archive"), + append_menu_item(import_menu, wxID_ANY, _L("Import ZIP Archive") + dots, _L("Load models contained within a ZIP archive"), [this](wxCommandEvent&) { if (m_plater) m_plater->import_zip_archive(); }, "menu_import", nullptr, [this]() { return can_add_models(); }); append_menu_item(import_menu, wxID_ANY, _L("Import Configs") + dots /*+ "\t" + ctrl + "I"*/, _L("Load configs"), [this](wxCommandEvent&) { load_config_file(); }, "menu_import", nullptr, - [this](){return true; }, this); + [](){return true; }, this); append_submenu(fileMenu, import_menu, wxID_ANY, _L("Import"), ""); @@ -2875,7 +2968,7 @@ void MainFrame::init_menubar_as_editor() _L("Duplicate the current plate"),[this](wxCommandEvent&) { m_plater->duplicate_plate(); }, - "menu_remove", nullptr, [this](){return true;}, this); + "menu_remove", nullptr, [](){return true;}, this); editMenu->AppendSeparator(); #else // BBS undo @@ -2975,10 +3068,10 @@ void MainFrame::init_menubar_as_editor() "", nullptr, [this](){return can_clone(); }, this); editMenu->AppendSeparator(); append_menu_item(editMenu, wxID_ANY, _L("Duplicate Current Plate"), - _L("Duplicate the current plate"),[this, handle_key_event](wxCommandEvent&) { + _L("Duplicate the current plate"),[this](wxCommandEvent&) { m_plater->duplicate_plate(); }, - "", nullptr, [this](){return true;}, this); + "", nullptr, [](){return true;}, this); editMenu->AppendSeparator(); #endif @@ -3042,13 +3135,13 @@ void MainFrame::init_menubar_as_editor() //BBS perspective view wxWindowID camera_id_base = wxWindow::NewControlId(int(wxID_CAMERA_COUNT)); auto perspective_item = append_menu_radio_item(viewMenu, wxID_CAMERA_PERSPECTIVE + camera_id_base, _L("Use Perspective View"), _L("Use Perspective View"), - [this](wxCommandEvent&) { + [](wxCommandEvent&) { wxGetApp().app_config->set_bool("use_perspective_camera", true); wxGetApp().update_ui_from_settings(); }, nullptr); //BBS orthogonal view auto orthogonal_item = append_menu_radio_item(viewMenu, wxID_CAMERA_ORTHOGONAL + camera_id_base, _L("Use Orthogonal View"), _L("Use Orthogonal View"), - [this](wxCommandEvent&) { + [](wxCommandEvent&) { wxGetApp().app_config->set_bool("use_perspective_camera", false); wxGetApp().update_ui_from_settings(); }, nullptr); @@ -3063,8 +3156,8 @@ void MainFrame::init_menubar_as_editor() wxGetApp().app_config->set_bool("auto_perspective", !wxGetApp().app_config->get_bool("auto_perspective")); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, - this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; }, - [this]() { return wxGetApp().app_config->get_bool("auto_perspective"); }, this); + this, [this]() { return is_prepare_or_preview_tab(); }, + []() { return wxGetApp().app_config->get_bool("auto_perspective"); }, this); viewMenu->AppendSeparator(); append_menu_check_item(viewMenu, wxID_ANY, _L("Show &G-code Window") + sep + "C", _L("Show G-code window in Preview scene."), @@ -3072,8 +3165,8 @@ void MainFrame::init_menubar_as_editor() wxGetApp().toggle_show_gcode_window(); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, - this, [this]() { return m_tabpanel->GetSelection() == tpPreview; }, - [this]() { return wxGetApp().show_gcode_window(); }, this); + this, [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW; }, + []() { return wxGetApp().show_gcode_window(); }, this); append_menu_check_item( viewMenu, wxID_ANY, _L("Show 3D Navigator"), _L("Show 3D navigator in Prepare and Preview scene."), @@ -3081,23 +3174,22 @@ void MainFrame::init_menubar_as_editor() wxGetApp().toggle_show_3d_navigator(); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, - this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; }, - [this]() { return wxGetApp().show_3d_navigator(); }, this); + this, [this]() { return is_prepare_or_preview_tab(); }, + []() { return wxGetApp().show_3d_navigator(); }, this); append_menu_check_item(viewMenu, wxID_ANY, _L("Show Gridlines"), _L("Show Gridlines on plate"), [this](wxCommandEvent&) { wxGetApp().toggle_show_plate_gridlines(); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, this, - [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; }, - [this]() { return wxGetApp().show_plate_gridlines(); }, this); + [this]() { return is_prepare_or_preview_tab(); }, + []() { return wxGetApp().show_plate_gridlines(); }, this); append_menu_item( viewMenu, wxID_ANY, _L("Reset Window Layout"), _L("Reset to default window layout"), [this](wxCommandEvent&) { m_plater->reset_window_layout(); }, "", this, [this]() { - return (m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview) && - m_plater->is_sidebar_enabled(); + return is_prepare_or_preview_tab() && m_plater->is_sidebar_enabled(); }, this); @@ -3119,8 +3211,8 @@ void MainFrame::init_menubar_as_editor() wxGetApp().toggle_show_outline(); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); }, - this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor; }, - [this]() { return wxGetApp().show_outline(); }, this); + this, [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE; }, + []() { return wxGetApp().show_outline(); }, this); /*viewMenu->AppendSeparator(); append_menu_check_item(viewMenu, wxID_ANY, _L("Show &Wireframe") + "\t" + ctrl + shift + _L("Enter"), _L("Show wireframes in 3D scene."), @@ -3246,11 +3338,11 @@ void MainFrame::init_menubar_as_editor() //parent_menu->Insert(0, about_item); append_menu_item( parent_menu, wxID_ANY, _L(about_title), "", - [this](wxCommandEvent &) { Slic3r::GUI::about();}, + [](wxCommandEvent &) { Slic3r::GUI::about();}, "", nullptr, []() { return true; }, this, 0); append_menu_item( parent_menu, wxID_ANY, _L("Preferences") + "\t" + ctrl + ",", "", - [this](wxCommandEvent &) { + [](wxCommandEvent &) { wxGetApp().open_preferences(); }, "", nullptr, []() { return true; }, this, 1); @@ -3269,7 +3361,7 @@ void MainFrame::init_menubar_as_editor() append_menu_item( m_topbar->GetTopMenu(), wxID_ANY, _L("Preferences") + "\t" + ctrl + "P", "", - [this](wxCommandEvent &) { + [](wxCommandEvent &) { // Orca: Use GUI_App::open_preferences instead of direct call so windows associations are updated on exit wxGetApp().open_preferences(); }, @@ -3301,14 +3393,14 @@ void MainFrame::init_menubar_as_editor() into_u8(_L("Syncing presets from cloud\u2026"))); wxGetApp().restart_sync_user_preset(); }, "", nullptr, - [this]() { + []() { return wxGetApp().is_user_login() && !wxGetApp().app_config->get_stealth_mode(); }, this); top_menu->AppendSeparator(); append_menu_item( top_menu, wxID_ANY, _L("Plugins") + "\t", "", - [this](wxCommandEvent &) { + [](wxCommandEvent &) { wxGetApp().open_plugins_dialog(); }, "", nullptr, []() { return true; }, this); @@ -3409,7 +3501,7 @@ void MainFrame::init_menubar_as_editor() [this]() {return m_plater->is_view3D_shown();; }, this); // help - append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Calibration Guide"), _L("Calibration Guide"), [this](wxCommandEvent &) + append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Calibration Guide"), _L("Calibration Guide"), [](wxCommandEvent &) { wxLaunchDefaultBrowser("https://www.orcaslicer.com/wiki/calibration_guide", wxBROWSER_NEW_WINDOW); }, "", nullptr, [this]() {return m_plater->is_view3D_shown();; }, this); @@ -3438,13 +3530,13 @@ void MainFrame::init_menubar_as_editor() into_u8(_L("Syncing presets from cloud\u2026"))); wxGetApp().restart_sync_user_preset(); }, "", nullptr, - [this]() { + []() { return wxGetApp().is_user_login() && !wxGetApp().app_config->get_stealth_mode(); }, this); fileMenu->AppendSeparator(); append_menu_item( - fileMenu, wxID_ANY, _L("Plugins"), "", [this](wxCommandEvent&) { wxGetApp().open_plugins_dialog(); }, "", nullptr, + fileMenu, wxID_ANY, _L("Plugins"), "", [](wxCommandEvent&) { wxGetApp().open_plugins_dialog(); }, "", nullptr, []() { return true; }, this); fileMenu->AppendSeparator(); @@ -3548,7 +3640,7 @@ void MainFrame::init_menubar_as_editor() [this]() {return m_plater->is_view3D_shown();; }, this); // help append_menu_item(calib_menu, wxID_ANY, _L("Calibration Guide"), _L("Calibration Guide"), - [this](wxCommandEvent&) { wxLaunchDefaultBrowser("https://www.orcaslicer.com/wiki/calibration_guide", wxBROWSER_NEW_WINDOW); }, "", nullptr, + [](wxCommandEvent&) { wxLaunchDefaultBrowser("https://www.orcaslicer.com/wiki/calibration_guide", wxBROWSER_NEW_WINDOW); }, "", nullptr, [this]() {return m_plater->is_view3D_shown();; }, this); m_menubar->Append(calib_menu,wxString::Format("&%s", _L("Calibration"))); @@ -3920,13 +4012,16 @@ void MainFrame::select_tab(wxPanel* panel) wxGetApp().params_dialog()->Popup(); return; } + // Not panel->GetName(): Prepare and Preview share the single m_plater window, so the + // window has no one correct name. The slot -> id lookup is the only correct resolution. int page_idx = m_tabpanel->FindPage(panel); - if (page_idx == tp3DEditor && m_tabpanel->GetSelection() == tpPreview) + wxString page_name = (page_idx == wxNOT_FOUND) ? wxString() : m_tabpanel->GetPageName(static_cast<size_t>(page_idx)); + if (page_name == TAB_ID_PREPARE && m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW) return; //BBS GUI refactor: remove unused layout new/dlg /*if (page_idx != wxNOT_FOUND && m_layout == ESettingsLayout::Dlg) page_idx++;*/ - select_tab(size_t(page_idx)); + select_tab(page_name); } //BBS @@ -3934,7 +4029,7 @@ void MainFrame::jump_to_monitor(std::string dev_id) { if(!m_monitor) return; - m_tabpanel->SetSelection(tpMonitor); + m_tabpanel->SelectPageByName(TAB_ID_MONITOR); if (!dev_id.empty()) { ((MonitorPanel*)m_monitor)->select_machine(dev_id); } @@ -3944,26 +4039,26 @@ void MainFrame::jump_to_multipage() { if(!m_multi_machine) return; - m_tabpanel->SetSelection(tpMultiDevice); + m_tabpanel->SelectPageByName(TAB_ID_MULTI_DEVICE); ((MultiMachinePage*)m_multi_machine)->jump_to_send_page(); } //BBS GUI refactor: remove unused layout new/dlg -void MainFrame::select_tab(size_t tab/* = size_t(-1)*/) +void MainFrame::select_tab(const wxString& id/* = wxString()*/) { //bool tabpanel_was_hidden = false; // Controls on page are created on active page of active tab now. // We should select/activate tab before its showing to avoid an UI-flickering - auto select = [this, tab](bool was_hidden) { - // when tab == -1, it means we should show the last selected tab + auto select = [this, id](bool was_hidden) { + // when id is empty, it means we should show the last selected tab //BBS GUI refactor: remove unused layout new/dlg //size_t new_selection = tab == (size_t)(-1) ? m_last_selected_tab : (m_layout == ESettingsLayout::Dlg && tab != 0) ? tab - 1 : tab; - size_t new_selection = tab == (size_t)(-1) ? m_last_selected_tab : tab; + wxString new_selection = id.empty() ? m_last_selected_tab : id; - if (m_tabpanel->GetSelection() != (int)new_selection) - m_tabpanel->SetSelection(new_selection); + if (m_tabpanel->GetSelectedPageName() != new_selection) + m_tabpanel->SelectPageByName(new_selection); #ifdef _MSW_DARK_MODE /*if (wxGetApp().tabs_as_menu()) { if (Tab* cur_tab = dynamic_cast<Tab*>(m_tabpanel->GetPage(new_selection))) @@ -3972,10 +4067,12 @@ void MainFrame::select_tab(size_t tab/* = size_t(-1)*/) m_plater->get_current_canvas3D()->render(); }*/ #endif - if (tab == MainFrame::tp3DEditor && m_layout == ESettingsLayout::Old) + // Intentionally `id`, not `new_selection`: the fallback-to-last-tab path must not + // trigger this render even when the last selected tab was Prepare. + if (id == TAB_ID_PREPARE && m_layout == ESettingsLayout::Old) m_plater->canvas3D()->render(); else if (was_hidden) { - Tab* cur_tab = dynamic_cast<Tab*>(m_tabpanel->GetPage(new_selection)); + Tab* cur_tab = dynamic_cast<Tab*>(m_tabpanel->GetPageByName(new_selection)); if (cur_tab) cur_tab->OnActivate(); } @@ -3984,10 +4081,10 @@ void MainFrame::select_tab(size_t tab/* = size_t(-1)*/) select(false); } -void MainFrame::request_select_tab(TabPosition pos) +void MainFrame::request_select_tab(const wxString& id) { wxCommandEvent* evt = new wxCommandEvent(EVT_SELECT_TAB); - evt->SetInt(pos); + evt->SetString(id); wxQueueEvent(this, evt); } @@ -4253,21 +4350,33 @@ void MainFrame::load_printer_url(wxString url, wxString apikey) void MainFrame::load_printer_url() { PresetBundle &preset_bundle = *wxGetApp().preset_bundle; - if (preset_bundle.use_bbl_device_tab() || NetworkAgentFactory::is_current_printer_agent_plugin()) + if (preset_bundle.use_bbl_device_tab() && !wxGetApp().app_config->get_bool("use_printer_agents")) return; auto cfg = preset_bundle.printers.get_edited_preset().config; + if (cfg.opt_string("print_host").empty()) { + if (auto *device_manager = wxGetApp().getDeviceManager()) { + auto *machine = device_manager->get_selected_machine(); + if (!machine) { + auto machines = device_manager->get_my_machine_list(); + if (machines.size() == 1) + machine = machines.begin()->second; + } + if (machine && !machine->get_dev_ip().empty()) + cfg.opt_string("print_host") = machine->get_dev_ip(); + } + } wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); wxString apikey; const auto host_type = cfg.option<ConfigOptionEnum<PrintHostType>>("host_type")->value; - if (cfg.has("printhost_apikey") && (host_type == htPrusaLink || host_type == htPrusaConnect)) + if (cfg.has("printhost_apikey") && host_type != htSimplyPrint) apikey = cfg.opt_string("printhost_apikey"); if (!url.empty()) { load_printer_url(url, apikey); } } -bool MainFrame::is_printer_view() const { return m_tabpanel->GetSelection() == TabPosition::tpMonitor; } +bool MainFrame::is_printer_view() const { return m_tabpanel->GetSelectedPageName() == TAB_ID_MONITOR; } void MainFrame::refresh_plugin_tips() diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 20229a611e..2052860d80 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -35,6 +35,21 @@ #include "PrinterWebView.hpp" #include "calib_dlg.hpp" #include "MultiMachinePage.hpp" +#include "slic3r/plugin/host/PluginPages.hpp" + +// Stable identifiers for MainFrame::m_tabpanel's built-in pages. These are +// names rather than positional indices so optional pages cannot shift them. +#define TAB_ID_HOME "home" +#define TAB_ID_PREPARE "prepare" +#define TAB_ID_PREVIEW "preview" +#define TAB_ID_MONITOR "monitor" +// Printer-agents mode shows the legacy web page alongside the native Device tab, so it needs an +// id of its own: sharing TAB_ID_MONITOR makes every name lookup resolve to whichever of the two +// comes first, which silently defeats PluginPages' selection round-trip across a tab relayout. +#define TAB_ID_MONITOR_WEB "monitor_web" +#define TAB_ID_MULTI_DEVICE "multi_device" +#define TAB_ID_PROJECT "project" +#define TAB_ID_CALIBRATION "calibration" #define ENABEL_PRINT_ALL 0 @@ -115,7 +130,7 @@ class MainFrame : public DPIFrame wxMenuItem* m_menu_item_reslice_now { nullptr }; wxSizer* m_main_sizer{ nullptr }; - size_t m_last_selected_tab; + wxString m_last_selected_tab; std::string get_base_name(const wxString &full_name, const char *extension = nullptr) const; std::string get_dir_name(const wxString &full_name) const; @@ -214,19 +229,6 @@ public: #ifdef __APPLE__ bool get_mac_full_screen() { return m_mac_fullscreen; } #endif - //BBS GUI refactor - enum TabPosition - { - tpHome = 0, - tp3DEditor = 1, - tpPreview = 2, - tpMonitor = 3, - tpMultiDevice = 4, - tpProject = 5, - tpCalibration = 6, - tpAuxiliary = 7, - toDebugTool = 8, - }; //BBS: add slice&&print status update logic enum SlicePrintEventType @@ -326,8 +328,8 @@ public: // When tab == -1, will be selected last selected tab //BBS: GUI refactor void select_tab(wxPanel* panel); - void select_tab(size_t tab = size_t(-1)); - void request_select_tab(TabPosition pos); + void select_tab(const wxString& id = wxString()); + void request_select_tab(const wxString& id); int get_calibration_curr_tab(); void select_view(const std::string& direction); // Propagate changed configuration from the Tab to the Plater and save changes to the AppConfig @@ -358,8 +360,11 @@ public: void RunScript(wxString js); //SoftFever - void show_device(bool bBBLPrinter); + void show_device(bool should_use_native); void fit_tab_labels(); // ORCA + // True while either of the two tabs backed by m_plater is selected. + bool is_prepare_or_preview_tab() const; + PluginPages& plugin_pages() { return m_plugin_pages; } PA_Calibration_Dlg* m_pa_calib_dlg{ nullptr }; FlowRateCalibrationDialog* m_flow_rate_calib_dlg{ nullptr }; @@ -385,6 +390,7 @@ public: CalibrationPanel* m_calibration{ nullptr }; WebViewPanel* m_webview { nullptr }; PrinterWebView* m_printer_view{nullptr}; + PluginPages m_plugin_pages; wxLogWindow* m_log_window { nullptr }; // BBS //wxBookCtrlBase* m_tabpanel { nullptr }; diff --git a/src/slic3r/GUI/MediaFilePanel.h b/src/slic3r/GUI/MediaFilePanel.h index 72fbc96a13..36696d2225 100644 --- a/src/slic3r/GUI/MediaFilePanel.h +++ b/src/slic3r/GUI/MediaFilePanel.h @@ -61,7 +61,6 @@ private: ::Button *m_button_year = nullptr; ::Button *m_button_month = nullptr; ::Button *m_button_all = nullptr; - ::Label *m_switch_label = nullptr; ::StaticBox * m_type_panel = nullptr; ::Button * m_button_video = nullptr; diff --git a/src/slic3r/GUI/MediaPlayCtrl.cpp b/src/slic3r/GUI/MediaPlayCtrl.cpp index 90e956be69..71fbcb054b 100644 --- a/src/slic3r/GUI/MediaPlayCtrl.cpp +++ b/src/slic3r/GUI/MediaPlayCtrl.cpp @@ -39,13 +39,14 @@ static std::map<int, std::string> error_messages = { namespace Slic3r { namespace GUI { -MediaPlayCtrl::MediaPlayCtrl(wxWindow *parent, wxMediaCtrl2 *media_ctrl, const wxPoint &pos, const wxSize &size) +MediaPlayCtrl::MediaPlayCtrl(wxWindow *parent, wxMediaCtrl3 *media_ctrl, const wxPoint &pos, const wxSize &size) : wxPanel(parent, wxID_ANY, pos, size) , m_media_ctrl(media_ctrl) { SetLabel("MediaPlayCtrl"); SetBackgroundColour(*wxWHITE); m_media_ctrl->Bind(wxEVT_MEDIA_STATECHANGED, &MediaPlayCtrl::onStateChanged, this); + m_media_ctrl->SetIdleImage(from_u8(resources_dir() + "/images/live_stream_default.png")); m_button_play = new Button(this, "", "media_play", wxBORDER_NONE); m_button_play->SetCanFocus(false); @@ -177,13 +178,6 @@ void MediaPlayCtrl::SetMachineObject(MachineObject* obj) if (machine == m_machine) { if (m_last_state == MEDIASTATE_IDLE && IsEnabled()) Play(); - else if (m_last_state == MEDIASTATE_LOADING && m_tutk_state == "disable" - && m_last_user_play + wxTimeSpan::Seconds(3) < wxDateTime::Now()) { - // resend ttcode to printer - if (auto agent = wxGetApp().getAgent()) - agent->get_camera_url(machine, [](auto) {}, wxGetApp().get_printer_cloud_provider()); - m_last_user_play = wxDateTime::Now(); - } return; } m_machine = machine; @@ -313,7 +307,7 @@ void MediaPlayCtrl::Play() // !m_lan_mode && !m_remote_proto && m_lan_proto == LVL_None (x) if (m_lan_proto <= MachineObject::LVL_Disable && (m_lan_mode || !m_remote_proto)) { - Stop(m_lan_proto == MachineObject::LVL_None + Stop(m_lan_proto == MachineObject::LVL_None ? _L("A problem occurred. Please update the printer firmware and try again.") : _L("LAN Only Liveview is off. Please turn on the liveview on printer screen.")); return; @@ -351,7 +345,7 @@ void MediaPlayCtrl::Play() url += "&cli_id=" + wxGetApp().app_config->get("slicer_uuid"); url += "&cli_ver=" + std::string(SLIC3R_VERSION); } - BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: " << hide_passwd(url, + BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: " << hide_passwd(url, {"?uid=", "authkey=", "passwd=", "license=", "token="}); CallAfter([this, m, url] { if (m != m_machine) { @@ -426,7 +420,7 @@ void MediaPlayCtrl::Stop(wxString const &msg, wxString const &msg2) auto tunnel = m_url.empty() ? "" : into_u8(wxURI(m_url).GetPath()).substr(1); if (auto n = tunnel.find_first_of("/_"); n != std::string::npos) tunnel = tunnel.substr(0, n); - if (last_state != wxMEDIASTATE_PLAYING && m_failed_code != 0 + if (last_state != wxMEDIASTATE_PLAYING && m_failed_code != 0 && m_last_failed_codes.find(m_failed_code) == m_last_failed_codes.end() && (m_user_triggered || m_failed_retry > 3)) { m_last_failed_codes.insert(m_failed_code); @@ -560,7 +554,7 @@ void MediaPlayCtrl::ToggleStream() url += "&cli_id=" + wxGetApp().app_config->get("slicer_uuid"); url += "&cli_ver=" + std::string(SLIC3R_VERSION); } - BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::ToggleStream: " << hide_passwd(url, + BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::ToggleStream: " << hide_passwd(url, {"?uid=", "authkey=", "passwd=", "license=", "token="}); CallAfter([this, m, url] { if (m != m_machine) return; @@ -580,8 +574,8 @@ void MediaPlayCtrl::ToggleStream() }, wxGetApp().get_printer_cloud_provider()); } -void MediaPlayCtrl::msw_rescale() { - m_button_play->Rescale(); +void MediaPlayCtrl::msw_rescale() { + m_button_play->Rescale(); } void MediaPlayCtrl::jump_to_play() @@ -715,7 +709,9 @@ void MediaPlayCtrl::media_proc() break; } else if (url == "<play>") { + BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: start play"; m_media_ctrl->Play(); + BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: end play"; } else { BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: start load"; @@ -771,15 +767,15 @@ bool MediaPlayCtrl::start_stream_service(bool *need_install) if (!boost::filesystem::exists(file_dll) || boost::filesystem::last_write_time(file_dll) != boost::filesystem::last_write_time(file_dll2)) boost::filesystem::copy_file(file_dll2, file_dll, boost::filesystem::copy_options::overwrite_existing); } - boost::process::child process_source(file_source, file_url2.ToStdWstring(), boost::process::start_dir(tools_dir), - boost::process::windows::create_no_window, + boost::process::child process_source(file_source, file_url2.ToStdWstring(), boost::process::start_dir(tools_dir), + boost::process::windows::create_no_window, boost::process::std_out > intermediate, boost::process::limit_handles); - boost::process::child process_ffmpeg(file_ffmpeg, configss, boost::process::windows::create_no_window, + boost::process::child process_ffmpeg(file_ffmpeg, configss, boost::process::windows::create_no_window, boost::process::std_in < intermediate, boost::process::limit_handles); #else boost::filesystem::permissions(file_source, boost::filesystem::owner_exe | boost::filesystem::add_perms); boost::filesystem::permissions(file_ffmpeg, boost::filesystem::owner_exe | boost::filesystem::add_perms); - boost::process::child process_source(file_source, file_url2.data().AsInternal(), boost::process::start_dir(start_dir), + boost::process::child process_source(file_source, file_url2.data().AsInternal(), boost::process::start_dir(start_dir), boost::process::std_out > intermediate, boost::process::limit_handles); boost::process::child process_ffmpeg(file_ffmpeg, configss, boost::process::std_in < intermediate, boost::process::limit_handles); #endif @@ -830,27 +826,16 @@ bool MediaPlayCtrl::get_stream_url(std::string *url) }} -void wxMediaCtrl2::DoSetSize(int x, int y, int width, int height, int sizeFlags) +void wxMediaCtrl_OnSize(wxWindow * ctrl, wxSize const & videoSize, int width, int height) { -#ifdef __WXMAC__ - wxWindow::DoSetSize(x, y, width, height, sizeFlags); -#else - wxMediaCtrl::DoSetSize(x, y, width, height, sizeFlags); -#endif -#if defined(__LINUX__) && defined(__WXGTK__) - if (m_gtk_video_window) { - const wxSize client_size = GetClientSize(); - m_gtk_video_window->SetSize(0, 0, client_size.GetWidth(), client_size.GetHeight()); - } -#endif - if (sizeFlags & wxSIZE_USE_EXISTING) return; - wxSize size = m_video_size; + wxSize size = videoSize; + if (!size.IsFullySpecified()) size = {16, 9}; int maxHeight = (width * size.GetHeight() + size.GetHeight() - 1) / size.GetWidth(); - if (maxHeight != GetMaxHeight()) { - // BOOST_LOG_TRIVIAL(info) << "wxMediaCtrl2::DoSetSize: width: " << width << ", height: " << height << ", maxHeight: " << maxHeight; - SetMaxSize({-1, maxHeight}); - CallAfter([this] { - if (auto p = GetParent()) { + if (maxHeight != ctrl->GetMaxHeight()) { + // BOOST_LOG_TRIVIAL(info) << "wxMediaCtrl_OnSize: width: " << width << ", height: " << height << ", maxHeight: " << maxHeight; + ctrl->SetMaxSize({-1, maxHeight}); + ctrl->CallAfter([ctrl] { + if (auto p = ctrl->GetParent()) { p->Layout(); p->Refresh(); } diff --git a/src/slic3r/GUI/MediaPlayCtrl.h b/src/slic3r/GUI/MediaPlayCtrl.h index f5e5dcddfc..0a01daefc9 100644 --- a/src/slic3r/GUI/MediaPlayCtrl.h +++ b/src/slic3r/GUI/MediaPlayCtrl.h @@ -8,7 +8,7 @@ #ifndef MediaPlayCtrl_h #define MediaPlayCtrl_h -#include "wxMediaCtrl2.h" +#include "wxMediaCtrl3.h" #include <wx/panel.h> @@ -30,7 +30,7 @@ namespace GUI { class MediaPlayCtrl : public wxPanel { public: - MediaPlayCtrl(wxWindow *parent, wxMediaCtrl2 *media_ctrl, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize); + MediaPlayCtrl(wxWindow *parent, wxMediaCtrl3 *media_ctrl, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize); ~MediaPlayCtrl(); @@ -75,7 +75,7 @@ private: // token std::shared_ptr<int> m_token = std::make_shared<int>(0); - wxMediaCtrl2 * m_media_ctrl; + wxMediaCtrl3 * m_media_ctrl; wxMediaState m_last_state = MEDIASTATE_IDLE; std::string m_machine; int m_lan_proto = 0; @@ -90,7 +90,7 @@ private: bool m_device_busy = false; bool m_disable_lan = false; wxString m_url; - + std::deque<wxString> m_tasks; boost::mutex m_mutex; boost::condition_variable m_cond; diff --git a/src/slic3r/GUI/MixedFilamentDialog.cpp b/src/slic3r/GUI/MixedFilamentDialog.cpp new file mode 100644 index 0000000000..902c12d27b --- /dev/null +++ b/src/slic3r/GUI/MixedFilamentDialog.cpp @@ -0,0 +1,1948 @@ +#include "MixedFilamentDialog.hpp" + +#include <algorithm> +#include <cmath> +#include <functional> +#include <map> +#include <set> +#include <wx/image.h> +#include <wx/sizer.h> +#include <wx/dcclient.h> +#include <wx/dcbuffer.h> +#include <wx/dcgraph.h> +#include <wx/scrolwin.h> +#include <wx/textctrl.h> +#include <wx/wrapsizer.h> +#include <wx/tokenzr.h> + +#include "libslic3r/Utils.hpp" +#include "libslic3r/FilamentMixer.hpp" +#include "I18N.hpp" +#include "GUI.hpp" +#include "GUI_App.hpp" +#include "GradientCurveEditor.hpp" +#include "FilamentBitmapUtils.hpp" +#include "wxExtensions.hpp" +#include "Widgets/Button.hpp" +#include "Widgets/CheckBox.hpp" +#include "Widgets/ComboBox.hpp" +#include "Widgets/DropDown.hpp" +#include "Widgets/Label.hpp" + +namespace Slic3r { +namespace GUI { + +static constexpr int MAX_COMPONENTS = 3; +static constexpr int MIN_COMPONENT_RATIO = 10; + +// Section headings and the placeholder text share one muted tone; light key, resolved at each use. +static const wxColour COLOR_LABEL_MUTED("#6B6A6A"); + +// Lightweight self-painting label used for both dual-color and triple-color +// ratio percentage display. Hover shows a rounded-rect background; click +// fires wxEVT_LEFT_DOWN which the owning dialog binds to start_ratio_editor. +class RatioLabelPanel : public wxPanel +{ +public: + RatioLabelPanel(wxWindow* parent) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) + { + SetBackgroundStyle(wxBG_STYLE_PAINT); + SetCursor(wxCursor(wxCURSOR_HAND)); + SetToolTip(_L("Click to edit ratio")); + SetFont(::Label::Body_10); + + Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& e) { m_hovered = true; Refresh(); e.Skip(); }); + Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& e) { m_hovered = false; Refresh(); e.Skip(); }); + Bind(wxEVT_PAINT, &RatioLabelPanel::on_paint, this); + } + + void SetLabel(const wxString& text) override + { + if (m_text == text) return; + m_text = text; + update_best_size(); + Refresh(); + } + wxString GetLabel() const override { return m_text; } + +private: + void update_best_size() + { + wxClientDC dc(this); + dc.SetFont(GetFont()); + wxSize ts = dc.GetTextExtent(m_text); + int pad_x = FromDIP(4), pad_y = FromDIP(3); + SetMinSize(wxSize(ts.GetWidth() + pad_x * 2, ts.GetHeight() + pad_y * 2)); + InvalidateBestSize(); + } + + void on_paint(wxPaintEvent&) + { + wxBufferedPaintDC dc(this); + wxSize sz = GetClientSize(); + + wxColour parent_bg = GetParent() ? GetParent()->GetBackgroundColour() + : StateColor::darkModeColorFor(*wxWHITE); + dc.SetBrush(wxBrush(parent_bg)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + + if (m_hovered) { + dc.SetBrush(wxBrush(StateColor::darkModeColorFor(wxColour("#F8F8F8")))); + dc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour("#CECECE")), 1)); + dc.DrawRoundedRectangle(0, 0, sz.GetWidth(), sz.GetHeight(), FromDIP(3)); + } + + dc.SetFont(GetFont()); + dc.SetTextForeground(m_hovered ? StateColor::darkModeColorFor(wxColour("#009688")) + : StateColor::darkModeColorFor(wxColour("#262E30"))); + wxSize ts = dc.GetTextExtent(m_text); + int x = (sz.GetWidth() - ts.GetWidth()) / 2; + int y = (sz.GetHeight() - ts.GetHeight()) / 2; + dc.DrawText(m_text, x, y); + } + + wxString m_text; + bool m_hovered{false}; +}; + +static wxColour blend_colors(const wxColour& a, const wxColour& b, double ratio_a) +{ + unsigned char r, g, bl; + Slic3r::filament_mixer_lerp(a.Red(), a.Green(), a.Blue(), + b.Red(), b.Green(), b.Blue(), + static_cast<float>(1.0 - ratio_a), + &r, &g, &bl); + return wxColour(r, g, bl); +} + + +// ---- Constructors ---- + +MixedFilamentDialog::MixedFilamentDialog(wxWindow* parent, + const std::vector<std::string>& physical_colors, + const std::vector<std::string>& physical_names, + const std::vector<std::string>& physical_types) + : DPIDialog(parent, wxID_ANY, _L("Add Mixed Filament"), wxDefaultPosition, + wxDefaultSize, wxCAPTION | wxCLOSE_BOX) + , m_edit_mode(false) + , m_physical_colors(physical_colors) + , m_physical_names(physical_names) + , m_physical_types(physical_types) +{ + m_result.components = {1, (physical_colors.size() >= 2) ? 2u : 1u}; + m_result.ratios = {50, 50}; + build_ui(); + wxGetApp().UpdateDlgDarkUI(this); +} + +MixedFilamentDialog::~MixedFilamentDialog() +{ + // Backstop: a child must never be destroyed while it still holds the mouse + // capture. wxWidgets only asserts about this (compiled out in release), and + // the macOS port never unwinds its capture stack, so the stale entry would + // make wxNSWindow::sendEvent swallow every mouse event in the application. + if (m_ratio_bar && m_ratio_bar->HasCapture()) + m_ratio_bar->ReleaseMouse(); + if (m_triangle_panel && m_triangle_panel->HasCapture()) + m_triangle_panel->ReleaseMouse(); +} + +MixedFilamentDialog::MixedFilamentDialog(wxWindow* parent, + const MixedFilamentResult& existing, + const std::vector<std::string>& physical_colors, + const std::vector<std::string>& physical_names, + const std::vector<std::string>& physical_types) + : DPIDialog(parent, wxID_ANY, _L("Edit Mixed Filament"), wxDefaultPosition, + wxDefaultSize, wxCAPTION | wxCLOSE_BOX) + , m_result(existing) + , m_edit_mode(true) + , m_physical_colors(physical_colors) + , m_physical_names(physical_names) + , m_physical_types(physical_types) +{ + if (m_result.components.size() < 2) { + m_result.components = {1, (physical_colors.size() >= 2) ? 2u : 1u}; + m_result.ratios = {50, 50}; + } + if (m_result.ratios.size() >= 3) { + int sum = 0; + for (int r : m_result.ratios) sum += r; + if (sum > 0) { + m_tri_wx = (double)m_result.ratios[0] / sum; + m_tri_wy = (double)m_result.ratios[1] / sum; + m_tri_wz = (double)m_result.ratios[2] / sum; + } + } + build_ui(); + wxGetApp().UpdateDlgDarkUI(this); +} + +void MixedFilamentDialog::on_dpi_changed(const wxRect&) +{ + int h = (num_components() >= 3) ? FromDIP(680) : FromDIP(580); + SetSize(FromDIP(439), h); + Refresh(); +} + +wxColour MixedFilamentDialog::comp_colour(size_t i) const +{ + unsigned int c = comp(i); + if (c >= 1 && c <= m_physical_colors.size()) + return wxColour(m_physical_colors[c - 1]); + return wxColour("#D9D9D9"); +} + +static wxBitmap make_alpha_bitmap(int w, int h, + const std::function<void(wxDC& dc)>& draw_fn) +{ + wxBitmap bmp(w, h); + wxMemoryDC memdc; +#ifdef __WXOSX__ + bmp.UseAlpha(); + memdc.SelectObject(bmp); +#else + { + wxImage img(w, h); + img.InitAlpha(); + memset(img.GetAlpha(), 0, w * h); + bmp = wxBitmap(std::move(img)); + } + memdc.SelectObject(bmp); +#endif + { +#ifdef __WXMSW__ + wxGCDC dc(memdc); +#else + wxDC& dc = memdc; +#endif + draw_fn(dc); + } + memdc.SelectObject(wxNullBitmap); + return bmp; +} + +wxBitmap MixedFilamentDialog::make_swatch_bitmap(size_t idx) +{ + int swatch_sz = FromDIP(20); + int pad_left = FromDIP(2); + int pad_right = FromDIP(6); + int bmp_w = pad_left + swatch_sz + pad_right; + int bmp_h = swatch_sz; + + // Reuse the sidebar clr_picker swatch (get_extruder_color_icon) so the + // checkerboard (transparent.svg tiling), border and label style match the + // sidebar exactly, instead of a self-drawn rounded rect / programmatic grid. + std::string color_hex = "#D9D9D9"; + if (idx < m_physical_colors.size()) + color_hex = m_physical_colors[idx]; + std::string label = std::to_string(idx + 1); + + wxBitmap* icon = get_extruder_color_icon(color_hex, label, swatch_sz, swatch_sz); + + return make_alpha_bitmap(bmp_w, bmp_h, [&](wxDC& dc) { + if (icon && icon->IsOk()) + dc.DrawBitmap(*icon, pad_left, 0); + }); +} + +void MixedFilamentDialog::apply_uniform_label_width(wxStaticText* lbl) +{ + // A material row places the combo right after the label, so the combo x follows the label + // width and the rows drift apart with fonts that render digits at different advances (which + // is what macOS does). Reserve the width of the widest row label on every row instead. + // The label itself is used as the measuring device on purpose: SetMinSize overrides the + // control's own best size rather than being merged with it, and on macOS the native cell is + // wider than the plain text extent, so a wxDC-measured width would clip the text. + const wxString text = lbl->GetLabel(); + int w = 0; + for (int i = 1; i <= MAX_COMPONENTS; ++i) { + lbl->SetLabel(wxString::Format(_L("Filament %d"), i)); + lbl->InvalidateBestSize(); + w = std::max(w, lbl->GetBestSize().x); + } + lbl->SetLabel(text); + lbl->InvalidateBestSize(); + lbl->SetMinSize(wxSize(w, -1)); +} + +void MixedFilamentDialog::append_material_row() +{ + auto* row = new wxBoxSizer(wxHORIZONTAL); + auto* lbl = new wxStaticText(this, wxID_ANY, + wxString::Format(_L("Filament %d"), (int)(m_combo_filaments.size() + 1))); + lbl->SetFont(::Label::Body_12); + apply_uniform_label_width(lbl); + row->Add(lbl, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + + auto* combo = new ComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, + wxSize(FromDIP(166), FromDIP(24)), 0, nullptr, wxCB_READONLY); + combo->SetKeepDropArrow(true); + combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { on_filament_changed(); }); + row->Add(combo, 1, wxALIGN_CENTER_VERTICAL); + + m_combo_filaments.push_back(combo); + m_combo_to_physical.push_back({}); + m_material_rows_sizer->Add(row, 0, wxEXPAND | wxTOP, FromDIP(9)); +} + +void MixedFilamentDialog::reset_manual_ratio_state() +{ + m_ratio_manual_order.clear(); + if (m_ratio_editor_panel) + m_ratio_editor_panel->Hide(); + // Restore any label hidden by an in-flight editor so it can never be left + // permanently invisible if the editor is dismissed without a commit. + if (m_ratio_editor_anchor) { + m_ratio_editor_anchor->Show(); + m_ratio_editor_anchor = nullptr; + } +} + +void MixedFilamentDialog::refresh_ratio_labels() +{ + if (m_label_ratio_a) + m_label_ratio_a->SetLabel(wxString::Format(wxT("%d%%"), ratio(0))); + if (m_label_ratio_b) + m_label_ratio_b->SetLabel(wxString::Format(wxT("%d%%"), ratio(1))); + if (m_ratio_sizer) + m_ratio_sizer->Layout(); + if (m_triangle_panel) + m_triangle_panel->Refresh(); +} + +void MixedFilamentDialog::sync_triangle_weights_from_ratios() +{ + if (m_result.ratios.size() < 3) + return; + + int sum = 0; + for (int r : m_result.ratios) + sum += r; + if (sum <= 0) + return; + + m_tri_wx = (double)m_result.ratios[0] / sum; + m_tri_wy = (double)m_result.ratios[1] / sum; + m_tri_wz = (double)m_result.ratios[2] / sum; +} + +void MixedFilamentDialog::apply_manual_ratio(size_t idx, int value) +{ + const size_t n = num_components(); + if (idx >= n) + return; + if (m_result.ratios.size() != n) + m_result.ratios.assign(n, n > 0 ? 100 / (int)n : 0); + bool manual_stale = false; + for (size_t o : m_ratio_manual_order) { + if (o >= n) { manual_stale = true; break; } + } + if (manual_stale) + reset_manual_ratio_state(); + + int max_value = (int)(100 - (n - 1) * MIN_COMPONENT_RATIO); + value = std::clamp(value, MIN_COMPONENT_RATIO, std::max(MIN_COMPONENT_RATIO, max_value)); + + if (n == 2) { + if (idx == 0) { + m_result.ratios[0] = value; + m_result.ratios[1] = 100 - value; + } else { + m_result.ratios[1] = value; + m_result.ratios[0] = 100 - value; + } + m_result.ratios[0] = std::clamp(m_result.ratios[0], MIN_COMPONENT_RATIO, 100 - MIN_COMPONENT_RATIO); + m_result.ratios[1] = 100 - m_result.ratios[0]; + } else if (n >= 3) { + m_result.ratios[idx] = value; + int remaining = 100 - value; + + std::vector<size_t> others; + int others_sum = 0; + for (size_t i = 0; i < n; ++i) { + if (i == idx) continue; + others.push_back(i); + others_sum += m_result.ratios[i]; + } + + if (!others.empty()) { + if (others_sum > 0) { + int assigned = 0; + for (size_t k = 0; k < others.size(); ++k) { + int nv = (int)((double)remaining * m_result.ratios[others[k]] / others_sum + 0.5); + nv = std::max(nv, MIN_COMPONENT_RATIO); + m_result.ratios[others[k]] = nv; + assigned += nv; + } + while (assigned != remaining) { + if (assigned > remaining) { + int pick = -1; + for (size_t k = 0; k < others.size(); ++k) + if (m_result.ratios[others[k]] > MIN_COMPONENT_RATIO + && (pick < 0 || m_result.ratios[others[k]] > m_result.ratios[others[pick]])) + pick = (int)k; + if (pick < 0) break; + --m_result.ratios[others[pick]]; --assigned; + } else { + int pick = 0; + for (size_t k = 1; k < others.size(); ++k) + if (m_result.ratios[others[k]] > m_result.ratios[others[pick]]) + pick = (int)k; + ++m_result.ratios[others[pick]]; ++assigned; + } + } + } else { + int base = remaining / (int)others.size(); + for (size_t k = 0; k < others.size(); ++k) + m_result.ratios[others[k]] = base; + m_result.ratios[others.back()] += remaining - base * (int)others.size(); + } + } + } + + refresh_ratio_labels(); + sync_triangle_weights_from_ratios(); + update_preview(); +} + +void MixedFilamentDialog::apply_dragged_triangle_ratio(int r0, int r1, int r2) +{ + if (m_result.ratios.size() < 3) + return; + + int ratios[3] = { + std::clamp(r0, MIN_COMPONENT_RATIO, 100), + std::clamp(r1, MIN_COMPONENT_RATIO, 100), + std::clamp(r2, MIN_COMPONENT_RATIO, 100) + }; + + int sum = ratios[0] + ratios[1] + ratios[2]; + while (sum > 100) { + int idx = 0; + for (int i = 1; i < 3; ++i) { + if (ratios[i] > ratios[idx]) + idx = i; + } + if (ratios[idx] <= MIN_COMPONENT_RATIO) + break; + --ratios[idx]; + --sum; + } + while (sum < 100) { + int idx = 0; + for (int i = 1; i < 3; ++i) { + if (ratios[i] < ratios[idx]) + idx = i; + } + ++ratios[idx]; + ++sum; + } + + m_result.ratios[0] = ratios[0]; + m_result.ratios[1] = ratios[1]; + m_result.ratios[2] = ratios[2]; + sync_triangle_weights_from_ratios(); + reset_manual_ratio_state(); + update_preview(); +} + +void MixedFilamentDialog::start_ratio_editor(size_t idx, wxWindow* anchor, const wxRect& anchor_rect) +{ + if (!anchor || idx >= m_result.ratios.size()) + return; + if (m_ratio_editor_panel && m_ratio_editor_panel->IsShown()) + commit_ratio_editor(true); + + if (!m_ratio_editor_panel) { + wxColour bg = StateColor::darkModeColorFor(wxColour("#F8F8F8")); + wxColour fg = StateColor::darkModeColorFor(wxColour("#262E30")); + + m_ratio_editor_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, + wxDefaultSize, wxBORDER_SIMPLE); + m_ratio_editor_panel->SetBackgroundColour(bg); + + auto* hsizer = new wxBoxSizer(wxHORIZONTAL); + + m_ratio_editor = new wxTextCtrl(m_ratio_editor_panel, wxID_ANY, wxEmptyString, + wxDefaultPosition, wxDefaultSize, + wxTE_PROCESS_ENTER | wxTE_RIGHT | wxBORDER_NONE); + m_ratio_editor->SetFont(::Label::Body_10); + m_ratio_editor->SetMaxLength(3); + m_ratio_editor->SetBackgroundColour(bg); + m_ratio_editor->SetForegroundColour(fg); + // Default wxTextCtrl best width (~140px) is too wide for the sizer to + // shrink, which would push the "%" suffix out of the panel. Size the + // editor for the *widest* three digits rather than the largest accepted + // value: SetMaxLength above lets anything up to "888" be typed, and the + // macOS system font renders digits at different advances, so "100" is + // narrower than what the user can actually enter. GetSizeFromTextSize() + // then adds the platform's own text field margins; on macOS those margins + // are what clipped the digits. + { + wxClientDC mdc(m_ratio_editor); + mdc.SetFont(::Label::Body_10); + int digits_w = mdc.GetTextExtent(wxT("888")).GetWidth(); + m_ratio_editor->SetMinSize(m_ratio_editor->GetSizeFromTextSize(digits_w)); + } + + auto* pct_label = new wxStaticText(m_ratio_editor_panel, wxID_ANY, wxT("%")); + pct_label->SetFont(::Label::Body_10); + pct_label->SetForegroundColour(fg); + pct_label->SetBackgroundColour(bg); + pct_label->SetMinSize(pct_label->GetBestSize()); + + hsizer->Add(m_ratio_editor, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(2)); + hsizer->Add(pct_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(2)); + m_ratio_editor_panel->SetSizer(hsizer); + m_ratio_editor_panel->Hide(); + + m_ratio_editor->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { commit_ratio_editor(true); }); + m_ratio_editor->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { + commit_ratio_editor(true); + e.Skip(); + }); + m_ratio_editor->Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& e) { + if (e.GetKeyCode() == WXK_ESCAPE) + commit_ratio_editor(false); + else + e.Skip(); + }); + } + + m_ratio_editor_idx = idx; + + // Keep the editor in the same window hierarchy as the clicked label so the + // z-order is reliable and the editor fully covers the anchor (dual-color + // labels live on the dialog, triple-color labels live on the triangle + // panel). + wxWindow* target_parent = anchor->GetParent(); + if (target_parent && m_ratio_editor_panel->GetParent() != target_parent) + m_ratio_editor_panel->Reparent(target_parent); + + // Hide the label being edited to avoid its (hover-state) text leaking out + // next to the editor; restored on commit. + m_ratio_editor_anchor = anchor; + anchor->Hide(); + + wxPoint pos = anchor->GetPosition() + anchor_rect.GetTopLeft(); + // Match the editor to the label (hover box) size so the inline editor and + // the hover state look identical, but never go below what the digits and + // the "%" suffix need: the sizer takes any missing width out of the + // stretchable editor, which would clip the value. + wxSize needed = m_ratio_editor_panel->ClientToWindowSize( + m_ratio_editor_panel->GetSizer()->CalcMin()); + wxSize size = anchor->GetSize(); + size.SetWidth(std::max(size.GetWidth(), needed.GetWidth())); + size.SetHeight(std::max(size.GetHeight(), needed.GetHeight())); + // An editor wider than the label must still stay inside its parent, or the + // corner labels of the triangle picker would have it clipped at the edge. + if (wxWindow* editor_parent = m_ratio_editor_panel->GetParent()) { + wxSize avail = editor_parent->GetClientSize(); + pos.x = std::clamp(pos.x, 0, std::max(0, avail.GetWidth() - size.GetWidth())); + pos.y = std::clamp(pos.y, 0, std::max(0, avail.GetHeight() - size.GetHeight())); + } + m_ratio_editor_panel->SetSize(wxRect(pos, size)); + m_ratio_editor_panel->Layout(); + m_ratio_editor->SetValue(wxString::Format(wxT("%d"), ratio(idx))); + m_ratio_editor_panel->Show(); + m_ratio_editor_panel->Raise(); + m_ratio_editor->SetFocus(); + m_ratio_editor->SelectAll(); + m_ratio_editor_panel->Refresh(); + Update(); +} + +void MixedFilamentDialog::commit_ratio_editor(bool apply) +{ + if (!m_ratio_editor_panel || !m_ratio_editor_panel->IsShown() || m_ratio_editor_committing) + return; + + m_ratio_editor_committing = true; + + // Restore the hidden anchor before applying the ratio, so any sizer layout + // triggered by refresh_ratio_labels() accounts for the visible label. + m_ratio_editor_panel->Hide(); + if (m_ratio_editor_anchor) { + m_ratio_editor_anchor->Show(); + m_ratio_editor_anchor = nullptr; + } + + if (apply) { + wxString value = m_ratio_editor->GetValue(); + value.Trim(true); + value.Trim(false); + if (value.EndsWith(wxT("%"))) + value.RemoveLast(); + + long parsed = 0; + if (value.ToLong(&parsed)) + apply_manual_ratio(m_ratio_editor_idx, (int)parsed); + else + refresh_ratio_labels(); + } + + m_ratio_editor_committing = false; +} + +void MixedFilamentDialog::commit_ratio_editor_from_background(wxMouseEvent& e) +{ + if (m_ratio_editor_panel && m_ratio_editor_panel->IsShown()) { + wxPoint mouse_in_panel = m_ratio_editor_panel->ScreenToClient(wxGetMousePosition()); + if (!m_ratio_editor_panel->GetClientRect().Contains(mouse_in_panel)) + commit_ratio_editor(true); + } + e.Skip(); +} + +// ---- UI Construction ---- + +void MixedFilamentDialog::build_ui() +{ + SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + Bind(wxEVT_LEFT_DOWN, &MixedFilamentDialog::commit_ratio_editor_from_background, this); + SetSize(FromDIP(439), FromDIP(580)); + + auto* main_sizer = new wxBoxSizer(wxVERTICAL); + + auto* top_sizer = new wxBoxSizer(wxHORIZONTAL); + top_sizer->Add(create_preview_panel(), 0, wxALL, FromDIP(20)); + + m_right_sizer = new wxBoxSizer(wxVERTICAL); + m_right_sizer->Add(create_material_selection(), 0, wxEXPAND); + m_right_sizer->Add(create_gradient_section(), 0, wxEXPAND | wxTOP, FromDIP(7)); + + m_ratio_sizer = create_ratio_slider(); + m_right_sizer->Add(m_ratio_sizer, 0, wxEXPAND | wxTOP, FromDIP(7)); + + m_triangle_sizer = create_triangle_picker(); + m_right_sizer->Add(m_triangle_sizer, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, FromDIP(7)); + + top_sizer->Add(m_right_sizer, 1, wxTOP | wxRIGHT | wxBOTTOM, FromDIP(20)); + main_sizer->Add(top_sizer, 0, wxEXPAND); + + main_sizer->Add(create_recommendation_grid(), 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(25)); + + // Warning panel: red bordered box with exclamation icon + text + m_warning_sizer = new wxBoxSizer(wxVERTICAL); + m_warning_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(48))); + m_warning_panel->SetMinSize(wxSize(-1, FromDIP(48))); + m_warning_panel->SetBackgroundStyle(wxBG_STYLE_PAINT); + m_warning_panel->Bind(wxEVT_PAINT, &MixedFilamentDialog::paint_warning_panel, this); + m_warning_sizer->Add(m_warning_panel, 0, wxEXPAND); + main_sizer->Add(m_warning_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(25)); + m_warning_panel->Hide(); + + main_sizer->Add(create_button_panel(), 0, wxALIGN_RIGHT | wxALL, FromDIP(20)); + + SetSizer(main_sizer); + + rebuild_all_combos(); + update_component_count_ui(); + update_preview(); + update_ok_button_state(); + + Layout(); + CentreOnParent(); +} + +wxBoxSizer* MixedFilamentDialog::create_preview_panel() +{ + auto* sizer = new wxBoxSizer(wxVERTICAL); + + m_preview_canvas = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(129), FromDIP(129))); + m_preview_canvas->SetBackgroundStyle(wxBG_STYLE_PAINT); + + m_preview_canvas->Bind(wxEVT_PAINT, [this](wxPaintEvent&) { + wxBufferedPaintDC dc(m_preview_canvas); + wxSize sz = m_preview_canvas->GetClientSize(); + size_t n = num_components(); + + dc.SetBrush(wxBrush(StateColor::darkModeColorFor(*wxWHITE))); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + + if (n == 0) return; + + int swatch_sz = FromDIP(80); + int x0 = (sz.GetWidth() - swatch_sz) / 2; + int y0 = (sz.GetHeight() - swatch_sz) / 2; + double radius = FromDIP(6); + + if (m_result.gradient_enabled && n == 2) { + Slic3r::GradientCurve curve; + if (!m_result.gradient_curve.empty()) { + curve.points = m_result.gradient_curve; + } else { + double yStart = (m_result.gradient_direction == 0) ? kGradientMaxRatio : kGradientMinRatio; + double yEnd = (m_result.gradient_direction == 0) ? kGradientMinRatio : kGradientMaxRatio; + curve.points = {{0.0, yStart, NAN, NAN}, {1.0, yEnd, NAN, NAN}}; + } + + // Same sampler the sidebar, extruder icons and paint gizmo swatches use, so this + // preview and every swatch drawn for the filament agree on what it looks like. + auto ramp = sample_gradient_ramp(comp_colour(0), comp_colour(1), curve, std::max(80, swatch_sz)); + fill_gradient_ramp_rect(dc, wxRect(x0, y0, swatch_sz, swatch_sz), ramp); + + // Mask corners: overdraw a thick background-colored rounded rect frame + // so the inner edge forms the desired rounded corners. + // Known limitation: this assumes the panel background equals + // darkModeColorFor(white). wxGraphicsContext::Clip(path) is not + // available in our wxWidgets build (only Clip(wxRegion) exists). + int r = static_cast<int>(radius); + wxColour bg = StateColor::darkModeColorFor(*wxWHITE); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.SetPen(wxPen(bg, r * 2)); + dc.DrawRoundedRectangle(x0 - r, y0 - r, swatch_sz + r * 2, swatch_sz + r * 2, radius * 2); + } else { + std::vector<wxColour> cols; + std::vector<double> weights; + for (size_t i = 0; i < n; ++i) { + cols.push_back(comp_colour(i)); + weights.push_back(ratio(i) / 100.0); + } + wxColour mixed = blend_n_colors(cols, weights); + dc.SetBrush(wxBrush(mixed)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRoundedRectangle(x0, y0, swatch_sz, swatch_sz, radius); + } + }); + + sizer->Add(m_preview_canvas, 0, wxALIGN_CENTER); + + auto* label = new wxStaticText(this, wxID_ANY, _L("Effect Preview")); + label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_LABEL_MUTED)); + label->SetFont(::Label::Body_13); + sizer->Add(label, 0, wxALIGN_CENTER | wxTOP, FromDIP(4)); + + return sizer; +} + +wxBoxSizer* MixedFilamentDialog::create_material_selection() +{ + auto* sizer = new wxBoxSizer(wxVERTICAL); + + // Summary panel — draws N components dynamically + m_summary_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(234), FromDIP(40))); + m_summary_panel->SetBackgroundStyle(wxBG_STYLE_PAINT); + m_summary_panel->Bind(wxEVT_PAINT, [this](wxPaintEvent&) { + wxBufferedPaintDC dc(m_summary_panel); + wxSize sz = m_summary_panel->GetClientSize(); + + wxColour sum_bg = StateColor::darkModeColorFor(wxColour("#F8F8F8")); + wxColour sum_text = StateColor::darkModeColorFor(wxColour("#262E30")); + dc.SetBrush(wxBrush(sum_bg)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + + int swatch_sz = FromDIP(20); + int y_center = (sz.GetHeight() - swatch_sz) / 2; + int x = FromDIP(13); + + dc.SetFont(::Label::Body_13); + + auto draw_summary_swatch = [&](size_t comp_idx) { + unsigned int c = comp(comp_idx); + std::string color_hex = "#D9D9D9"; + if (c >= 1 && c <= m_physical_colors.size()) + color_hex = m_physical_colors[c - 1]; + std::string label = std::to_string(c); + wxBitmap* icon = get_extruder_color_icon(color_hex, label, swatch_sz, swatch_sz); + if (icon && icon->IsOk()) + dc.DrawBitmap(*icon, x, y_center); + x += swatch_sz + FromDIP(4); + }; + + if (m_result.gradient_enabled && num_components() == 2) { + size_t idx_a = (m_result.gradient_direction == 0) ? 0 : 1; + size_t idx_b = 1 - idx_a; + draw_summary_swatch(idx_a); + + dc.SetTextForeground(sum_text); + wxString arrow = wxT("\u2192"); + wxSize arrow_sz = dc.GetTextExtent(arrow); + dc.DrawText(arrow, x, y_center + (swatch_sz - arrow_sz.GetHeight()) / 2); + x += arrow_sz.GetWidth() + FromDIP(4); + + draw_summary_swatch(idx_b); + } else { + for (size_t i = 0; i < num_components(); ++i) { + if (i > 0) { + dc.SetTextForeground(sum_text); + wxString plus = wxT("+"); + wxSize plus_sz = dc.GetTextExtent(plus); + dc.DrawText(plus, x, y_center + (swatch_sz - plus_sz.GetHeight()) / 2); + x += plus_sz.GetWidth() + FromDIP(4); + } + draw_summary_swatch(i); + + dc.SetTextForeground(sum_text); + wxString pct = wxString::Format(wxT("%d%%"), ratio(i)); + wxSize pct_sz = dc.GetTextExtent(pct); + dc.DrawText(pct, x, y_center + (swatch_sz - pct_sz.GetHeight()) / 2); + x += pct_sz.GetWidth() + FromDIP(4); + } + } + }); + sizer->Add(m_summary_panel, 0, wxEXPAND); + + auto* sel_label = new wxStaticText(this, wxID_ANY, _L("Select Mixed Materials")); + sel_label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_LABEL_MUTED)); + sel_label->SetFont(::Label::Body_12); + sizer->Add(sel_label, 0, wxTOP, FromDIP(6)); + + m_material_rows_sizer = new wxBoxSizer(wxVERTICAL); + + m_combo_filaments.clear(); + m_combo_to_physical.clear(); + for (size_t i = 0; i < m_result.components.size(); ++i) + append_material_row(); + + sizer->Add(m_material_rows_sizer, 0, wxEXPAND); + + auto* btn_sizer = new wxBoxSizer(wxHORIZONTAL); + + m_btn_add_material = new Button(this, _L("+ Add Material")); + m_btn_add_material->SetBackgroundColor(wxColour("#F8F8F8")); + m_btn_add_material->SetBorderColor(wxColour("#EEEEEE")); + // The disabled tone rides on the StateColor so Enable() alone repaints it, the way m_btn_ok does. + m_btn_add_material->SetTextColor(StateColor( + std::make_pair(wxColour("#ACACAC"), (int) StateColor::Disabled), + std::make_pair(wxColour("#262E30"), (int) StateColor::Normal))); + m_btn_add_material->SetMinSize(wxSize(-1, FromDIP(24))); + m_btn_add_material->SetCursor(wxCursor(wxCURSOR_HAND)); + m_btn_add_material->EnableTooltipEvenDisabled(); + m_btn_add_material->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_add_material(); }); + btn_sizer->Add(m_btn_add_material, 1, wxRIGHT, FromDIP(6)); + + m_btn_remove_material = new Button(this, _L("- Delete Material")); + m_btn_remove_material->SetBackgroundColor(wxColour("#F8F8F8")); + m_btn_remove_material->SetBorderColor(wxColour("#EEEEEE")); + m_btn_remove_material->SetTextColor(wxColour("#262E30")); + m_btn_remove_material->SetMinSize(wxSize(-1, FromDIP(24))); + m_btn_remove_material->SetCursor(wxCursor(wxCURSOR_HAND)); + m_btn_remove_material->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_remove_material(); }); + m_btn_remove_material->Hide(); + btn_sizer->Add(m_btn_remove_material, 1, 0, 0); + + sizer->Add(btn_sizer, 0, wxEXPAND | wxTOP, FromDIP(9)); + + return sizer; +} + +wxBoxSizer* MixedFilamentDialog::create_ratio_slider() +{ + auto* sizer = new wxBoxSizer(wxVERTICAL); + + auto* ratio_label = new wxStaticText(this, wxID_ANY, _L("Ratio")); + ratio_label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_LABEL_MUTED)); + ratio_label->SetFont(::Label::Body_12); + sizer->Add(ratio_label, 0, wxBOTTOM, FromDIP(4)); + + m_ratio_bar = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(27))); + m_ratio_bar->SetMinSize(wxSize(-1, FromDIP(27))); + m_ratio_bar->SetBackgroundStyle(wxBG_STYLE_PAINT); + + m_ratio_bar->Bind(wxEVT_PAINT, [this](wxPaintEvent&) { + wxBufferedPaintDC dc(m_ratio_bar); + wxSize sz = m_ratio_bar->GetClientSize(); + + wxColour col_a = comp_colour(0), col_b = comp_colour(1); + + for (int x = 0; x < sz.GetWidth(); ++x) { + double t = (double)x / sz.GetWidth(); + wxColour c = blend_colors(col_a, col_b, 1.0 - t); + dc.SetPen(wxPen(c)); + dc.DrawLine(x, 0, x, sz.GetHeight()); + } + + int div_x = (int)(ratio(1) / 100.0 * sz.GetWidth()); + // Fixed in both themes, like the triangle picker's drag handle: the divider is drawn over + // blended filament colour, so it has to keep its contrast against data rather than chrome. + dc.SetPen(wxPen(wxColour(80, 80, 80), FromDIP(4))); + dc.DrawLine(div_x, 0, div_x, sz.GetHeight()); + dc.SetPen(wxPen(*wxWHITE, FromDIP(2))); + dc.DrawLine(div_x, 0, div_x, sz.GetHeight()); + }); + + m_ratio_bar->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& e) { + if (m_ratio_editor_panel && m_ratio_editor_panel->IsShown()) + commit_ratio_editor(true); + m_ratio_dragging = true; + if (!m_ratio_bar->HasCapture()) + m_ratio_bar->CaptureMouse(); + int new_ratio = 100 - (int)(e.GetX() * 100.0 / m_ratio_bar->GetClientSize().GetWidth() + 0.5); + on_ratio_changed(std::max(MIN_COMPONENT_RATIO, std::min(100 - MIN_COMPONENT_RATIO, new_ratio))); + }); + + m_ratio_bar->Bind(wxEVT_MOTION, [this](wxMouseEvent& e) { + if (!m_ratio_dragging) return; + int new_ratio = 100 - (int)(e.GetX() * 100.0 / m_ratio_bar->GetClientSize().GetWidth() + 0.5); + on_ratio_changed(std::max(MIN_COMPONENT_RATIO, std::min(100 - MIN_COMPONENT_RATIO, new_ratio))); + }); + + // Key the release off the capture itself, not off the drag flag: the two can fall out of + // sync (a lost capture clears the flag on its own), and a capture that outlives the widget + // wedges mouse input for the whole application. + m_ratio_bar->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent&) { + m_ratio_dragging = false; + if (m_ratio_bar->HasCapture()) + m_ratio_bar->ReleaseMouse(); + }); + + m_ratio_bar->Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) { + m_ratio_dragging = false; + }); + + sizer->Add(m_ratio_bar, 0, wxEXPAND); + + auto* pct_sizer = new wxBoxSizer(wxHORIZONTAL); + m_label_ratio_a = new RatioLabelPanel(this); + m_label_ratio_a->SetLabel(wxString::Format(wxT("%d%%"), ratio(0))); + m_label_ratio_b = new RatioLabelPanel(this); + m_label_ratio_b->SetLabel(wxString::Format(wxT("%d%%"), ratio(1))); + auto bind_ratio_click = [this](RatioLabelPanel* label, size_t idx) { + label->Bind(wxEVT_LEFT_DOWN, [this, label, idx](wxMouseEvent&) { + wxRect rect(wxPoint(0, 0), label->GetClientSize()); + start_ratio_editor(idx, label, rect); + }); + }; + bind_ratio_click(m_label_ratio_a, 0); + bind_ratio_click(m_label_ratio_b, 1); + pct_sizer->Add(m_label_ratio_a, 0); + pct_sizer->AddStretchSpacer(1); + pct_sizer->Add(m_label_ratio_b, 0); + sizer->Add(pct_sizer, 0, wxEXPAND | wxTOP, FromDIP(2)); + + return sizer; +} + +// ---- Triangle (ternary) ratio picker ---- + +// Barycentric coordinate utilities +struct TriPoint { double x, y; }; + +static double tri_signed_area2(TriPoint a, TriPoint b, TriPoint c) +{ + return (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y); +} + +static bool tri_contains(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2) +{ + double total = tri_signed_area2(v0, v1, v2); + if (std::abs(total) < 1e-9) return false; + double s0 = tri_signed_area2(p, v1, v2) / total; + double s1 = tri_signed_area2(v0, p, v2) / total; + double s2 = 1.0 - s0 - s1; + return s0 >= -0.001 && s1 >= -0.001 && s2 >= -0.001; +} + +static void tri_barycentric(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2, + double& w0, double& w1, double& w2) +{ + double total = std::abs(tri_signed_area2(v0, v1, v2)); + if (total < 1e-9) { w0 = w1 = w2 = 1.0 / 3.0; return; } + w0 = std::abs(tri_signed_area2(p, v1, v2)) / total; + w1 = std::abs(tri_signed_area2(v0, p, v2)) / total; + w2 = 1.0 - w0 - w1; + w0 = std::clamp(w0, 0.0, 1.0); + w1 = std::clamp(w1, 0.0, 1.0); + w2 = std::clamp(w2, 0.0, 1.0); + double s = w0 + w1 + w2; + if (s > 0) { w0 /= s; w1 /= s; w2 /= s; } +} + +static TriPoint tri_clamp(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2) +{ + double w0, w1, w2; + tri_barycentric(p, v0, v1, v2, w0, w1, w2); + w0 = std::clamp(w0, 0.0, 1.0); + w1 = std::clamp(w1, 0.0, 1.0); + w2 = std::clamp(w2, 0.0, 1.0); + double s = w0 + w1 + w2; + if (s > 0) { w0 /= s; w1 /= s; w2 /= s; } + return {w0 * v0.x + w1 * v1.x + w2 * v2.x, + w0 * v0.y + w1 * v1.y + w2 * v2.y}; +} + +wxBoxSizer* MixedFilamentDialog::create_triangle_picker() +{ + auto* sizer = new wxBoxSizer(wxVERTICAL); + + int panel_w = FromDIP(160); + int panel_h = FromDIP(160); + m_triangle_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(panel_w, panel_h)); + m_triangle_panel->SetMinSize(wxSize(panel_w, panel_h)); + m_triangle_panel->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + m_triangle_panel->SetBackgroundStyle(wxBG_STYLE_PAINT); + + auto get_vertices = [this]() -> std::tuple<TriPoint, TriPoint, TriPoint> { + wxSize sz = m_triangle_panel->GetClientSize(); + double pw = sz.GetWidth(), ph = sz.GetHeight(); + double margin = FromDIP(20); + double avail = std::min(pw, ph) - 2 * margin; + double side = avail; + double tri_h = side * std::sqrt(3.0) / 2.0; + double cx = pw / 2.0; + double top_y = (ph - tri_h) / 2.0; + double bot_y = top_y + tri_h; + TriPoint v0 = {cx, top_y}; // top + TriPoint v1 = {cx - side / 2.0, bot_y}; // bottom-left + TriPoint v2 = {cx + side / 2.0, bot_y}; // bottom-right + return {v0, v1, v2}; + }; + + m_triangle_panel->Bind(wxEVT_PAINT, [this, get_vertices](wxPaintEvent&) { + wxBufferedPaintDC dc(m_triangle_panel); + wxSize sz = m_triangle_panel->GetClientSize(); + auto [v0, v1, v2] = get_vertices(); + + wxColour tri_bg = StateColor::darkModeColorFor(*wxWHITE); + dc.SetBrush(wxBrush(tri_bg)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + + wxColour c0 = comp_colour(0), c1 = comp_colour(1), c2 = comp_colour(2); + + const bool cache_valid = m_tri_cache_bmp.IsOk() && + m_tri_cache_size == sz && + m_tri_cache_c0 == c0 && m_tri_cache_c1 == c1 && m_tri_cache_c2 == c2; + + if (!cache_valid) { + int min_y = (int)std::min({v0.y, v1.y, v2.y}); + int max_y = (int)std::max({v0.y, v1.y, v2.y}); + int min_x = (int)std::min({v0.x, v1.x, v2.x}); + int max_x = (int)std::max({v0.x, v1.x, v2.x}); + + m_tri_cache_bmp = wxBitmap(sz.GetWidth(), sz.GetHeight(), 24); + wxMemoryDC mdc(m_tri_cache_bmp); + mdc.SetBrush(wxBrush(tri_bg)); + mdc.SetPen(*wxTRANSPARENT_PEN); + mdc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + + for (int py = min_y; py <= max_y; ++py) { + for (int px = min_x; px <= max_x; ++px) { + TriPoint p = {(double)px, (double)py}; + if (!tri_contains(p, v0, v1, v2)) continue; + double w0, w1, w2; + tri_barycentric(p, v0, v1, v2, w0, w1, w2); + unsigned char mr, mg, mb; + if (w0 + w1 > 1e-6) { + float t01 = static_cast<float>(w1 / (w0 + w1)); + Slic3r::filament_mixer_lerp(c0.Red(), c0.Green(), c0.Blue(), + c1.Red(), c1.Green(), c1.Blue(), + t01, &mr, &mg, &mb); + float t2 = static_cast<float>(w2); + Slic3r::filament_mixer_lerp(mr, mg, mb, + c2.Red(), c2.Green(), c2.Blue(), + t2, &mr, &mg, &mb); + } else { + mr = c2.Red(); mg = c2.Green(); mb = c2.Blue(); + } + mdc.SetPen(wxPen(wxColour(mr, mg, mb))); + mdc.DrawPoint(px, py); + } + } + + mdc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour("#CECECE")), 1)); + mdc.SetBrush(*wxTRANSPARENT_BRUSH); + wxPoint pts[3] = {{(int)v0.x, (int)v0.y}, {(int)v1.x, (int)v1.y}, {(int)v2.x, (int)v2.y}}; + mdc.DrawPolygon(3, pts); + + mdc.SelectObject(wxNullBitmap); + m_tri_cache_c0 = c0; m_tri_cache_c1 = c1; m_tri_cache_c2 = c2; + m_tri_cache_size = sz; + } + + dc.DrawBitmap(m_tri_cache_bmp, 0, 0); + + // Drag handle (always redrawn on top of cached bitmap) + double hx = m_tri_wx * v0.x + m_tri_wy * v1.x + m_tri_wz * v2.x; + double hy = m_tri_wx * v0.y + m_tri_wy * v1.y + m_tri_wz * v2.y; + int handle_r = FromDIP(5); + dc.SetBrush(*wxWHITE_BRUSH); + dc.SetPen(wxPen(wxColour("#262E30"), FromDIP(2))); + dc.DrawCircle((int)hx, (int)hy, handle_r); + + if (m_result.ratios.size() >= 3) { + dc.SetFont(::Label::Body_10); + wxSize ts0 = dc.GetTextExtent(wxString::Format(wxT("%d%%"), m_result.ratios[0])); + int top_label_y = std::max(0, (int)(v0.y - ts0.GetHeight() - FromDIP(4))); + + dc.SetFont(::Label::Body_12); + dc.SetTextForeground(StateColor::darkModeColorFor(COLOR_LABEL_MUTED)); + dc.DrawText(_L("Ratio"), FromDIP(2), top_label_y); + + // Position the real RatioLabelPanel children + for (int i = 0; i < 3 && i < (int)m_triangle_ratio_labels.size(); ++i) { + if (!m_triangle_ratio_labels[i]) continue; + m_triangle_ratio_labels[i]->SetLabel( + wxString::Format(wxT("%d%%"), m_result.ratios[i])); + wxSize lsz = m_triangle_ratio_labels[i]->GetMinSize(); + int lx = 0, ly = 0; + if (i == 0) { + lx = (int)(v0.x - lsz.GetWidth() / 2); + ly = top_label_y; + } else if (i == 1) { + lx = (int)(v1.x - lsz.GetWidth() / 2); + ly = (int)(v1.y + FromDIP(3)); + } else { + lx = (int)(v2.x - lsz.GetWidth() / 2); + ly = (int)(v2.y + FromDIP(3)); + } + m_triangle_ratio_labels[i]->SetSize(lx, ly, lsz.GetWidth(), lsz.GetHeight()); + } + } + }); + + auto handle_mouse = [this, get_vertices](wxMouseEvent& e, bool is_down) { + auto [v0, v1, v2] = get_vertices(); + TriPoint p = {(double)e.GetX(), (double)e.GetY()}; + + if (is_down) { + // Only start dragging when the press lands inside the triangle; + // clicks outside the triangle must not change the mix ratio. + if (!tri_contains(p, v0, v1, v2)) + return; + m_tri_dragging = true; + if (!m_triangle_panel->HasCapture()) + m_triangle_panel->CaptureMouse(); + } + + if (!m_tri_dragging) return; + + TriPoint clamped = tri_clamp(p, v0, v1, v2); + tri_barycentric(clamped, v0, v1, v2, m_tri_wx, m_tri_wy, m_tri_wz); + + int r0 = (int)(m_tri_wx * 100 + 0.5); + int r1 = (int)(m_tri_wy * 100 + 0.5); + int r2 = 100 - r0 - r1; + r0 = std::clamp(r0, 0, 100); + r1 = std::clamp(r1, 0, 100); + r2 = std::clamp(r2, 0, 100); + + apply_dragged_triangle_ratio(r0, r1, r2); + }; + + // Create 3 RatioLabelPanel children on the triangle panel + m_triangle_ratio_labels.fill(nullptr); + for (int i = 0; i < 3; ++i) { + auto* lbl = new RatioLabelPanel(m_triangle_panel); + lbl->SetLabel(wxString::Format(wxT("%d%%"), + (i < (int)m_result.ratios.size()) ? m_result.ratios[i] : 33)); + size_t idx = (size_t)i; + lbl->Bind(wxEVT_LEFT_DOWN, [this, lbl, idx](wxMouseEvent&) { + wxRect rect(wxPoint(0, 0), lbl->GetClientSize()); + start_ratio_editor(idx, lbl, rect); + }); + m_triangle_ratio_labels[i] = lbl; + } + + m_triangle_panel->Bind(wxEVT_LEFT_DOWN, [this, handle_mouse](wxMouseEvent& e) { + if (m_ratio_editor_panel && m_ratio_editor_panel->IsShown()) + commit_ratio_editor(true); + handle_mouse(e, true); + }); + m_triangle_panel->Bind(wxEVT_MOTION, [this, handle_mouse](wxMouseEvent& e) { + if (m_tri_dragging) + handle_mouse(e, false); + }); + m_triangle_panel->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent&) { + m_tri_dragging = false; + if (m_triangle_panel->HasCapture()) + m_triangle_panel->ReleaseMouse(); + }); + m_triangle_panel->Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) { + m_tri_dragging = false; + }); + + sizer->Add(m_triangle_panel, 0); + + return sizer; +} + +wxBoxSizer* MixedFilamentDialog::create_gradient_section() +{ + m_gradient_sizer = new wxBoxSizer(wxHORIZONTAL); + + m_chk_gradient = new ::CheckBox(this); + m_chk_gradient->SetValue(m_result.gradient_enabled); + m_chk_gradient->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) { e.Skip(); on_gradient_toggled(); }); + m_gradient_sizer->Add(m_chk_gradient, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, FromDIP(4)); + + m_label_gradient = new wxStaticText(this, wxID_ANY, _L("Gradient Effect")); + m_label_gradient->SetFont(::Label::Body_13); + m_gradient_sizer->Add(m_label_gradient, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8)); + + m_combo_gradient_dir = new ComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, + wxSize(FromDIP(152), FromDIP(24)), 0, nullptr, wxCB_READONLY); + m_combo_gradient_dir->SetKeepDropArrow(true); + update_gradient_direction_items(); + m_combo_gradient_dir->SetSelection(m_result.gradient_direction); + m_combo_gradient_dir->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { on_gradient_direction_changed(); }); + m_combo_gradient_dir->Show(m_result.gradient_enabled); + + m_gradient_sizer->Add(m_combo_gradient_dir, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8)); + + auto* outer = new wxBoxSizer(wxVERTICAL); + outer->Add(m_gradient_sizer, 0, wxEXPAND); + + // Custom curve editor: visible only when gradient is on and exactly 2 components are mixed. + m_curve_sizer = new wxBoxSizer(wxVERTICAL); + m_curve_editor = new GradientCurveEditor(this, comp_colour(0), comp_colour(1)); + if (!m_result.gradient_curve.empty()) + m_curve_editor->set_points(m_result.gradient_curve); + else + m_curve_editor->reset_to_linear((m_result.gradient_direction == 0) ? 0.9 : 0.1, + (m_result.gradient_direction == 0) ? 0.1 : 0.9); + m_curve_editor->Bind(wxEVT_GRADIENT_CURVE_CHANGED, + [this](wxCommandEvent&) { on_gradient_curve_changed(); }); + m_curve_sizer->Add(m_curve_editor, 0, wxEXPAND | wxTOP, FromDIP(4)); + + outer->Add(m_curve_sizer, 0, wxEXPAND | wxTOP, FromDIP(6)); + const bool curve_visible = m_result.gradient_enabled && num_components() == 2; + m_curve_sizer->ShowItems(curve_visible); + + // Per-part gradient toggle sits BELOW the curve editor. + m_per_part_gradient_sizer = new wxBoxSizer(wxHORIZONTAL); + + m_chk_per_part_gradient = new ::CheckBox(this); + m_chk_per_part_gradient->SetValue(m_result.per_part_gradient); + m_chk_per_part_gradient->Bind(wxEVT_TOGGLEBUTTON, + [this](wxCommandEvent& e) { e.Skip(); on_per_part_gradient_toggled(); }); + m_per_part_gradient_sizer->Add(m_chk_per_part_gradient, 0, + wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, FromDIP(4)); + + m_label_per_part_gradient = new wxStaticText(this, wxID_ANY, _L("Enable per-part gradient effect")); + m_label_per_part_gradient->SetFont(::Label::Body_13); + m_per_part_gradient_sizer->Add(m_label_per_part_gradient, 0, + wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8)); + + outer->Add(m_per_part_gradient_sizer, 0, wxEXPAND | wxTOP, FromDIP(2)); + m_per_part_gradient_sizer->ShowItems(m_result.gradient_enabled); + + return outer; +} + +wxBoxSizer* MixedFilamentDialog::create_recommendation_grid() +{ + auto* outer = new wxBoxSizer(wxVERTICAL); + + auto* title_sizer = new wxBoxSizer(wxHORIZONTAL); + auto* rec_label = new wxStaticText(this, wxID_ANY, _L("Mixing Recommendations")); + rec_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#ACACAC"))); + rec_label->SetFont(::Label::Body_10); + title_sizer->Add(rec_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + + auto* rec_line = new wxPanel(this, wxID_ANY); + rec_line->SetMinSize(wxSize(-1, 1)); + rec_line->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#EEEEEE"))); + title_sizer->Add(rec_line, 1, wxALIGN_CENTER_VERTICAL); + + outer->Add(title_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(4)); + + m_recommendation_scroll = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(116))); + m_recommendation_scroll->SetScrollRate(0, 5); + m_recommendation_scroll->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F8F8F8"))); + + m_recommendation_grid = new wxWrapSizer(wxHORIZONTAL, wxREMOVE_LEADING_SPACES); + auto* scroll_inner_sizer = new wxBoxSizer(wxVERTICAL); + scroll_inner_sizer->Add(m_recommendation_grid, 1, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); + m_recommendation_scroll->SetSizer(scroll_inner_sizer); + + rebuild_recommendation_items(); + + outer->Add(m_recommendation_scroll, 1, wxEXPAND | wxTOP, FromDIP(4)); + return outer; +} + +void MixedFilamentDialog::rebuild_recommendation_items() +{ + if (!m_recommendation_scroll || !m_recommendation_grid) + return; + + static constexpr int MAX_RECOMMENDATIONS = 100; + + m_recommendation_scroll->Freeze(); + m_recommendation_grid->Clear(true); + + size_t n = m_physical_colors.size(); + int count = 0; + + // Group physical filaments by type (only same-type combos are recommended) + std::map<std::string, std::vector<size_t>> type_groups; + for (size_t i = 0; i < n; ++i) { + std::string t = (i < m_physical_types.size()) ? m_physical_types[i] : "PLA"; + // Skip support filaments (type ends with "-S") + if (t.size() >= 2 && t.compare(t.size() - 2, 2, "-S") == 0) + continue; + type_groups[t].push_back(i); + } + + if (num_components() >= 3) { + // Three-color: C(g,3) x 3 variants per same-type group + for (auto& [type, indices] : type_groups) { + if (count >= MAX_RECOMMENDATIONS) break; + size_t g = indices.size(); + for (size_t ai = 0; ai < g && count < MAX_RECOMMENDATIONS; ++ai) { + for (size_t bi = ai + 1; bi < g && count < MAX_RECOMMENDATIONS; ++bi) { + for (size_t ci = bi + 1; ci < g && count < MAX_RECOMMENDATIONS; ++ci) { + size_t idx[3] = {indices[ai], indices[bi], indices[ci]}; + // 3 variants: each filament takes the 50% role in turn + for (int dominant = 0; dominant < 3 && count < MAX_RECOMMENDATIONS; ++dominant) { + size_t i0 = idx[(dominant + 1) % 3]; // 25% + size_t i1 = idx[(dominant + 2) % 3]; // 25% + size_t i2 = idx[dominant]; // 50% + + wxColour ca(m_physical_colors[i0]); + wxColour cb(m_physical_colors[i1]); + wxColour cc(m_physical_colors[i2]); + wxColour mixed = blend_n_colors({ca, cb, cc}, {0.25, 0.25, 0.50}); + + auto* item = new wxPanel(m_recommendation_scroll, wxID_ANY, + wxDefaultPosition, wxSize(FromDIP(20), FromDIP(20))); + item->SetBackgroundColour(mixed); + item->SetCursor(wxCursor(wxCURSOR_HAND)); + + unsigned int ca_1 = (unsigned int)(i0 + 1); + unsigned int cb_1 = (unsigned int)(i1 + 1); + unsigned int cc_1 = (unsigned int)(i2 + 1); + item->Bind(wxEVT_LEFT_UP, [this, ca_1, cb_1, cc_1](wxMouseEvent&) { + on_recommendation_clicked_triple(ca_1, cb_1, cc_1); + }); + item->SetToolTip(wxString::Format(wxT("%s + %s + %s"), + wxString::FromUTF8(m_physical_names[i0]), + wxString::FromUTF8(m_physical_names[i1]), + wxString::FromUTF8(m_physical_names[i2]))); + + m_recommendation_grid->Add(item, 0, wxRIGHT | wxBOTTOM, FromDIP(6)); + ++count; + } + } + } + } + } + } else { + // Two-color: C(g,2) per same-type group + for (auto& [type, indices] : type_groups) { + if (count >= MAX_RECOMMENDATIONS) break; + size_t g = indices.size(); + for (size_t ai = 0; ai < g && count < MAX_RECOMMENDATIONS; ++ai) { + for (size_t bi = ai + 1; bi < g && count < MAX_RECOMMENDATIONS; ++bi) { + size_t i = indices[ai]; + size_t j = indices[bi]; + + wxColour ca(m_physical_colors[i]); + wxColour cb(m_physical_colors[j]); + wxColour mixed = blend_colors(ca, cb, 0.5); + + auto* item = new wxPanel(m_recommendation_scroll, wxID_ANY, + wxDefaultPosition, wxSize(FromDIP(20), FromDIP(20))); + item->SetBackgroundColour(mixed); + item->SetCursor(wxCursor(wxCURSOR_HAND)); + + unsigned int comp_a = (unsigned int)(i + 1); + unsigned int comp_b = (unsigned int)(j + 1); + item->Bind(wxEVT_LEFT_UP, [this, comp_a, comp_b](wxMouseEvent&) { + on_recommendation_clicked(comp_a, comp_b); + }); + item->SetToolTip(wxString::Format(wxT("%s + %s"), + wxString::FromUTF8(m_physical_names[i]), + wxString::FromUTF8(m_physical_names[j]))); + + m_recommendation_grid->Add(item, 0, wxRIGHT | wxBOTTOM, FromDIP(6)); + ++count; + } + } + } + } + + m_recommendation_scroll->SetScrollbars(0, FromDIP(20), 0, 1); + m_recommendation_scroll->FitInside(); + m_recommendation_scroll->Layout(); + m_recommendation_scroll->Thaw(); +} + +wxBoxSizer* MixedFilamentDialog::create_button_panel() +{ + auto* sizer = new wxBoxSizer(wxHORIZONTAL); + + m_btn_cancel = new Button(this, _L("Cancel")); + m_btn_cancel->SetStyle(ButtonStyle::Regular, ButtonType::Choice); + m_btn_cancel->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); }); + + m_btn_ok = new Button(this, _L("OK")); + m_btn_ok->SetStyle(ButtonStyle::Confirm, ButtonType::Choice); + m_btn_ok->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_OK); }); + + sizer->Add(m_btn_cancel, 0, wxRIGHT, FromDIP(12)); + sizer->Add(m_btn_ok, 0); + + return sizer; +} + +void MixedFilamentDialog::rebuild_all_combos() +{ + m_combo_to_physical.resize(m_combo_filaments.size()); + + for (size_t i = 0; i < m_combo_filaments.size(); ++i) { + std::set<unsigned int> others_selected; + std::set<std::string> others_types; + for (size_t k = 0; k < m_result.components.size(); ++k) { + if (k == i) continue; + unsigned int phys = m_result.components[k]; + others_selected.insert(phys); + if (phys >= 1 && phys <= m_physical_types.size()) + others_types.insert(m_physical_types[phys - 1]); + } + + auto* combo = m_combo_filaments[i]; + combo->Clear(); + m_combo_to_physical[i].clear(); + + int restore_sel = -1; + unsigned int cur_phys = (i < m_result.components.size()) ? m_result.components[i] : 0; + + if (cur_phys == 0) { + combo->Append(_L("-- Select --")); + m_combo_to_physical[i].push_back(0); + restore_sel = 0; + } + + for (size_t j = 0; j < m_physical_names.size(); ++j) { + unsigned int phys_1based = (unsigned int)(j + 1); + + if (others_selected.count(phys_1based)) + continue; + + int style = 0; + if (!others_types.empty() && !m_physical_types.empty()) { + std::string this_type = (j < m_physical_types.size()) ? m_physical_types[j] : "PLA"; + if (others_types.find(this_type) == others_types.end()) + style = DD_ITEM_STYLE_DIMMED; + } + + int idx = combo->Append(wxString::FromUTF8(m_physical_names[j]), make_swatch_bitmap(j), style); + m_combo_to_physical[i].push_back(phys_1based); + + if (phys_1based == cur_phys) + restore_sel = idx; + } + + if (restore_sel >= 0) + combo->SetSelection(restore_sel); + else if (combo->GetCount() > 0) + combo->SetSelection(0); + } +} + +void MixedFilamentDialog::refresh_curve_editor_colors() +{ + if (m_curve_editor) + m_curve_editor->set_colors(comp_colour(0), comp_colour(1)); +} + +// ---- Event Handlers ---- + +void MixedFilamentDialog::on_filament_changed() +{ + for (size_t i = 0; i < m_combo_filaments.size() && i < m_result.components.size(); ++i) { + int sel = m_combo_filaments[i]->GetSelection(); + if (sel >= 0 && i < m_combo_to_physical.size() && sel < (int)m_combo_to_physical[i].size()) + m_result.components[i] = m_combo_to_physical[i][sel]; + } + + refresh_curve_editor_colors(); + rebuild_all_combos(); + update_gradient_direction_items(); + update_preview(); + update_ok_button_state(); +} + +void MixedFilamentDialog::on_ratio_changed(int new_ratio_a) +{ + if (m_result.ratios.size() < 2) return; + m_result.ratios[0] = new_ratio_a; + m_result.ratios[1] = 100 - new_ratio_a; + + reset_manual_ratio_state(); + refresh_ratio_labels(); + + update_preview(); +} + +void MixedFilamentDialog::on_gradient_toggled() +{ + + m_result.gradient_enabled = m_chk_gradient->GetValue(); + + if (m_ratio_sizer) + m_ratio_sizer->ShowItems(!m_result.gradient_enabled && num_components() == 2); + if (m_combo_gradient_dir) + m_combo_gradient_dir->Show(m_result.gradient_enabled); + if (m_per_part_gradient_sizer) + m_per_part_gradient_sizer->ShowItems(m_result.gradient_enabled); + if (m_curve_sizer) + m_curve_sizer->ShowItems(m_result.gradient_enabled && num_components() == 2); + if (!m_result.gradient_enabled) { + m_result.per_part_gradient = false; + if (m_chk_per_part_gradient) m_chk_per_part_gradient->SetValue(false); + } + + // Toggling the curve editor changes the right column height (and width when + // turning gradient on), so the dialog must follow or the recommendation list + // gets squeezed off-screen. Same trick as 2-color -> 3-color switching. + const wxSize new_size = compute_dialog_size(); + if (GetSize() != new_size) { + const wxRect old_rect = GetRect(); + const wxPoint center(old_rect.x + old_rect.width / 2, + old_rect.y + old_rect.height / 2); + SetSize(new_size); + SetPosition(wxPoint(center.x - new_size.x / 2, + center.y - new_size.y / 2)); + } + + Layout(); + Refresh(); +} + +void MixedFilamentDialog::on_gradient_direction_changed() +{ + if (!m_combo_gradient_dir) return; + m_result.gradient_direction = m_combo_gradient_dir->GetSelection(); + + // Mirror the user's custom curve around y=0.5 instead of resetting it, so + // shape work (added anchors, bent segments) survives a direction toggle. + // reverse() flips y and tangent signs consistently; default two-point + // linear curves end up matching the new direction exactly (0.9->0.1 <-> 0.1->0.9). + if (m_curve_editor) { + m_curve_editor->reverse(); + m_result.gradient_curve = m_curve_editor->get_points(); + } + update_preview(); +} + +void MixedFilamentDialog::on_gradient_curve_changed() +{ + if (m_curve_editor) + m_result.gradient_curve = m_curve_editor->get_points(); + update_preview(); +} + +void MixedFilamentDialog::on_per_part_gradient_toggled() +{ + if (m_chk_per_part_gradient) + m_result.per_part_gradient = m_chk_per_part_gradient->GetValue(); +} + +void MixedFilamentDialog::on_add_material() +{ + size_t n = num_components(); + if (n >= (size_t)MAX_COMPONENTS) return; + + unsigned int new_comp = 0; + for (size_t j = 0; j < m_physical_names.size(); ++j) { + unsigned int candidate = (unsigned int)(j + 1); + bool used = false; + for (auto c : m_result.components) + if (c == candidate) { used = true; break; } + if (!used) { new_comp = candidate; break; } + } + if (new_comp == 0) return; + m_result.components.push_back(new_comp); + + int each = 100 / (int)(n + 1); + m_result.ratios.clear(); + int assigned = 0; + for (size_t i = 0; i < n; ++i) { + m_result.ratios.push_back(each); + assigned += each; + } + m_result.ratios.push_back(100 - assigned); + + if (m_result.ratios.size() >= 3) { + int sum = 0; + for (int r : m_result.ratios) sum += r; + if (sum > 0) { + m_tri_wx = (double)m_result.ratios[0] / sum; + m_tri_wy = (double)m_result.ratios[1] / sum; + m_tri_wz = (double)m_result.ratios[2] / sum; + } + } + reset_manual_ratio_state(); + + append_material_row(); + + rebuild_all_combos(); + refresh_curve_editor_colors(); + update_component_count_ui(); + update_preview(); + update_ok_button_state(); + rebuild_recommendation_items(); + + Layout(); + Refresh(); +} + +void MixedFilamentDialog::on_remove_material() +{ + if (num_components() <= 2) + return; + + m_result.components.resize(2); + m_result.ratios = {50, 50}; + m_tri_wx = 0.5; + m_tri_wy = 0.5; + m_tri_wz = 0.0; + + reset_manual_ratio_state(); + refresh_ratio_labels(); + + if (m_material_rows_sizer && m_material_rows_sizer->GetItemCount() > 2) { + auto* sizer_item = m_material_rows_sizer->GetItem(m_material_rows_sizer->GetItemCount() - 1); + if (sizer_item && sizer_item->GetSizer()) + sizer_item->GetSizer()->Clear(true); + m_material_rows_sizer->Remove(m_material_rows_sizer->GetItemCount() - 1); + } + + if (m_combo_filaments.size() > 2) + m_combo_filaments.pop_back(); + if (m_combo_to_physical.size() > 2) + m_combo_to_physical.pop_back(); + + rebuild_all_combos(); + refresh_curve_editor_colors(); + update_component_count_ui(); + update_preview(); + update_ok_button_state(); + rebuild_recommendation_items(); + Layout(); + Refresh(); +} + +void MixedFilamentDialog::on_recommendation_clicked(unsigned int comp_a, unsigned int comp_b) +{ + while (m_material_rows_sizer->GetItemCount() > 2) { + auto* sizer_item = m_material_rows_sizer->GetItem(m_material_rows_sizer->GetItemCount() - 1); + if (sizer_item && sizer_item->GetSizer()) + sizer_item->GetSizer()->Clear(true); + m_material_rows_sizer->Remove(m_material_rows_sizer->GetItemCount() - 1); + } + + while (m_combo_filaments.size() > 2) + m_combo_filaments.pop_back(); + while (m_combo_to_physical.size() > 2) + m_combo_to_physical.pop_back(); + + m_result.components = {comp_a, comp_b}; + m_result.ratios = {50, 50}; + + reset_manual_ratio_state(); + refresh_ratio_labels(); + + rebuild_all_combos(); + refresh_curve_editor_colors(); + update_gradient_direction_items(); + update_component_count_ui(); + update_preview(); + update_ok_button_state(); + Layout(); + Refresh(); +} + +void MixedFilamentDialog::on_recommendation_clicked_triple(unsigned int a, unsigned int b, unsigned int c) +{ + // Ensure we have exactly 3 combo rows + if (num_components() < 3) { + // Need to add a 3rd combo row + while (m_combo_filaments.size() < 3) + append_material_row(); + } else if (num_components() > 3) { + while (m_material_rows_sizer->GetItemCount() > 3) { + auto* sizer_item = m_material_rows_sizer->GetItem(m_material_rows_sizer->GetItemCount() - 1); + if (sizer_item && sizer_item->GetSizer()) + sizer_item->GetSizer()->Clear(true); + m_material_rows_sizer->Remove(m_material_rows_sizer->GetItemCount() - 1); + } + while (m_combo_filaments.size() > 3) + m_combo_filaments.pop_back(); + while (m_combo_to_physical.size() > 3) + m_combo_to_physical.pop_back(); + } + + m_result.components = {a, b, c}; + m_result.ratios = {25, 25, 50}; + m_tri_wx = 0.25; + m_tri_wy = 0.25; + m_tri_wz = 0.50; + reset_manual_ratio_state(); + + rebuild_all_combos(); + refresh_curve_editor_colors(); + update_gradient_direction_items(); + update_component_count_ui(); + update_preview(); + update_ok_button_state(); + Layout(); + Refresh(); +} + +void MixedFilamentDialog::update_preview() +{ + if (m_preview_canvas) m_preview_canvas->Refresh(); + if (m_summary_panel) m_summary_panel->Refresh(); + if (m_ratio_bar) m_ratio_bar->Refresh(); + if (m_triangle_panel) m_triangle_panel->Refresh(); +} + +void MixedFilamentDialog::paint_warning_panel(wxPaintEvent&) +{ + wxBufferedPaintDC dc(m_warning_panel); + wxSize sz = m_warning_panel->GetClientSize(); + + dc.SetBrush(wxBrush(StateColor::darkModeColorFor(*wxWHITE))); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + + dc.SetBrush(wxBrush(StateColor::darkModeColorFor(wxColour("#F8F8F8")))); + dc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour("#D01B1B")), 1)); + dc.DrawRoundedRectangle(0, 0, sz.GetWidth(), sz.GetHeight(), FromDIP(4)); + + int x = FromDIP(10); + int cy = sz.GetHeight() / 2; + + int icon_r = FromDIP(7); + dc.SetBrush(wxBrush(StateColor::darkModeColorFor(wxColour("#D01B1B")))); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawCircle(x + icon_r, cy, icon_r); + dc.SetFont(::Label::Body_10); + dc.SetTextForeground(*wxWHITE); + wxSize ex = dc.GetTextExtent(wxT("!")); + dc.DrawText(wxT("!"), x + icon_r - ex.GetWidth() / 2, cy - ex.GetHeight() / 2); + x += icon_r * 2 + FromDIP(6); + + if (m_type_mismatch_msg.empty()) return; + + dc.SetFont(::Label::Body_12); + dc.SetTextForeground(StateColor::darkModeColorFor(wxColour("#D01B1B"))); + wxString msg = m_type_mismatch_msg; + int avail_w = sz.GetWidth() - x - FromDIP(10); + wxSize ts = dc.GetTextExtent(msg); + if (ts.GetWidth() <= avail_w) { + dc.DrawText(msg, x, cy - ts.GetHeight() / 2); + } else { + wxArrayString lines; + wxString cur_line; + wxArrayString words; + wxStringTokenizer tkz(msg, wxT(" "), wxTOKEN_RET_EMPTY_ALL); + while (tkz.HasMoreTokens()) words.Add(tkz.GetNextToken()); + if (words.empty()) words.Add(msg); + for (size_t w = 0; w < words.size(); ++w) { + wxString test = cur_line.empty() ? words[w] : cur_line + wxT(" ") + words[w]; + if (dc.GetTextExtent(test).GetWidth() > avail_w && !cur_line.empty()) { + lines.Add(cur_line); + cur_line = words[w]; + } else { + cur_line = test; + } + } + if (!cur_line.empty()) lines.Add(cur_line); + if (lines.empty()) lines.Add(msg); + int line_h = dc.GetTextExtent(wxT("Mg")).GetHeight(); + int total_h = (int)lines.size() * line_h; + int y0 = (sz.GetHeight() - total_h) / 2; + for (size_t l = 0; l < lines.size(); ++l) + dc.DrawText(lines[l], x, y0 + (int)l * line_h); + } +} + +void MixedFilamentDialog::update_ok_button_state() +{ + if (!m_btn_ok) return; + + bool has_type_mismatch = false; + if (!m_physical_types.empty() && m_result.components.size() >= 2) { + std::map<std::string, std::vector<unsigned int>> type_groups; + for (size_t i = 0; i < m_result.components.size(); ++i) { + unsigned int phys = m_result.components[i]; + if (phys < 1 || phys > m_physical_types.size()) continue; + type_groups[m_physical_types[phys - 1]].push_back(phys); + } + has_type_mismatch = type_groups.size() > 1; + if (has_type_mismatch) { + wxString parts; + for (auto it = type_groups.begin(); it != type_groups.end(); ++it) { + if (!parts.empty()) + parts += _L(" and "); + wxString slots; + for (size_t j = 0; j < it->second.size(); ++j) { + if (!slots.empty()) slots += ", "; + slots += std::to_string(it->second[j]); + } + parts += wxString::Format(_L("Slot %s (%s)"), slots, wxString::FromUTF8(it->first)); + } + m_type_mismatch_msg = parts + " " + _L("cannot be mixed. Please select the same filament type."); + } else { + m_type_mismatch_msg.clear(); + } + } else { + m_type_mismatch_msg.clear(); + } + + bool has_unselected = false; + for (unsigned int c : m_result.components) { + if (c == 0) { has_unselected = true; break; } + } + + bool can_confirm = !has_type_mismatch && !has_unselected; + // Enable() alone repaints the button: its StateColor carries the disabled grey. + m_btn_ok->Enable(can_confirm); + if (has_unselected) + m_btn_ok->SetToolTip(_L("Please select a filament for all components")); + else if (has_type_mismatch) + m_btn_ok->SetToolTip(_L("Cannot mix different filament types")); + else + m_btn_ok->SetToolTip(wxEmptyString); + + if (m_warning_panel) { + m_warning_panel->Show(has_type_mismatch); + // Force a repaint: when the panel is already visible and only the + // mismatch text changes (e.g. PETG -> ABS), Show()/Layout() do not + // generate a paint event, so paint_warning_panel keeps the stale text. + m_warning_panel->Refresh(); + Layout(); + } +} + +void MixedFilamentDialog::update_gradient_direction_items() +{ + if (!m_combo_gradient_dir) return; + + int prev_sel = m_combo_gradient_dir->GetSelection(); + m_combo_gradient_dir->Clear(); + + if (num_components() < 2) return; + + auto make_direction_bitmap = [this](size_t idx_from, size_t idx_to) -> wxBitmap { + int swatch_sz = FromDIP(20); + int arrow_w = FromDIP(16); + int gap = FromDIP(4); + int bmp_w = swatch_sz + gap + arrow_w + gap + swatch_sz; + int bmp_h = swatch_sz; + + wxColour dir_text = StateColor::darkModeColorFor(wxColour("#262E30")); + + return make_alpha_bitmap(bmp_w, bmp_h, [&](wxDC& dc) { + dc.SetFont(::Label::Body_13); + + auto draw_swatch = [&](int x, size_t idx) { + std::string color_hex = "#D9D9D9"; + if (idx < m_physical_colors.size()) + color_hex = m_physical_colors[idx]; + std::string label = std::to_string(idx + 1); + wxBitmap* icon = get_extruder_color_icon(color_hex, label, swatch_sz, swatch_sz); + if (icon && icon->IsOk()) + dc.DrawBitmap(*icon, x, 0); + }; + + int x = 0; + draw_swatch(x, idx_from); + x += swatch_sz + gap; + + dc.SetTextForeground(dir_text); + wxString arrow = wxT("\u2192"); + wxSize arrow_sz = dc.GetTextExtent(arrow); + dc.DrawText(arrow, x + (arrow_w - arrow_sz.GetWidth()) / 2, + (bmp_h - arrow_sz.GetHeight()) / 2); + x += arrow_w + gap; + + draw_swatch(x, idx_to); + }); + }; + + size_t idx_a = (comp(0) >= 1) ? comp(0) - 1 : 0; + size_t idx_b = (comp(1) >= 1) ? comp(1) - 1 : 1; + + m_combo_gradient_dir->Append(wxT(" "), make_direction_bitmap(idx_a, idx_b)); + m_combo_gradient_dir->Append(wxT(" "), make_direction_bitmap(idx_b, idx_a)); + + if (prev_sel >= 0 && prev_sel < (int)m_combo_gradient_dir->GetCount()) + m_combo_gradient_dir->SetSelection(prev_sel); + else + m_combo_gradient_dir->SetSelection(0); +} + +wxSize MixedFilamentDialog::compute_dialog_size() const +{ + const bool is_three = (num_components() >= 3); + const bool curve_visible = !is_three && m_result.gradient_enabled; + + int w = FromDIP(439); + int h = FromDIP(580); + if (is_three) { + h = FromDIP(680); + } else if (curve_visible) { + // Wider so the gradient editor can show "Material Ratio" intact; + // +40 over the 3-color height to fit the curve editor while keeping the + // recommendation list visible (it can still scroll if needed). + w = FromDIP(470); + h = FromDIP(720); + } + return wxSize(w, h); +} + +void MixedFilamentDialog::update_component_count_ui() +{ + bool is_two = (num_components() == 2); + bool is_three = (num_components() >= 3); + + // Toggle ratio slider vs triangle picker + if (m_ratio_sizer) + m_ratio_sizer->ShowItems(is_two && !m_result.gradient_enabled); + if (m_triangle_sizer) + m_triangle_sizer->ShowItems(is_three); + + // 3-color: hide gradient entirely, force off + if (m_gradient_sizer) { + bool show_gradient = is_two; + m_chk_gradient->Show(show_gradient); + if (m_label_gradient) m_label_gradient->Show(show_gradient); + m_combo_gradient_dir->Show(show_gradient && m_result.gradient_enabled); + if (m_per_part_gradient_sizer) + m_per_part_gradient_sizer->ShowItems(show_gradient && m_result.gradient_enabled); + if (m_curve_sizer) + m_curve_sizer->ShowItems(show_gradient && m_result.gradient_enabled); + } + if (is_three) { + m_result.gradient_enabled = false; + if (m_chk_gradient) m_chk_gradient->SetValue(false); + m_result.per_part_gradient = false; + if (m_chk_per_part_gradient) m_chk_per_part_gradient->SetValue(false); + } + + if (m_btn_add_material) { + bool can_add = (num_components() < (size_t)MAX_COMPONENTS && m_physical_colors.size() > num_components()); + m_btn_add_material->Enable(can_add); + m_btn_add_material->SetToolTip(can_add ? wxString() + : (is_three ? _L("Maximum 3 materials for mixing") : _L("Maximum number of components reached"))); + } + + if (m_btn_remove_material) { + m_btn_remove_material->Show(is_three); + m_btn_remove_material->Enable(is_three); + m_btn_remove_material->SetToolTip(is_three ? _L("Remove the third material") : wxString()); + } + + const wxSize new_size = compute_dialog_size(); + const wxRect old_rect = GetRect(); + const wxPoint center(old_rect.x + old_rect.width / 2, + old_rect.y + old_rect.height / 2); + SetSize(new_size); + SetPosition(wxPoint(center.x - new_size.x / 2, + center.y - new_size.y / 2)); + Layout(); +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/MixedFilamentDialog.hpp b/src/slic3r/GUI/MixedFilamentDialog.hpp new file mode 100644 index 0000000000..ea8ac5ad16 --- /dev/null +++ b/src/slic3r/GUI/MixedFilamentDialog.hpp @@ -0,0 +1,183 @@ +#ifndef slic3r_MixedFilamentDialog_hpp_ +#define slic3r_MixedFilamentDialog_hpp_ + +#include <array> +#include <string> +#include <utility> +#include <vector> +#include <wx/bitmap.h> +#include <wx/panel.h> +#include <wx/tglbtn.h> +#include <wx/stattext.h> + +#include "GUI_Utils.hpp" +#include "libslic3r/FilamentMixer.hpp" + +class Button; +class CheckBox; +class ComboBox; +class wxMouseEvent; +class wxScrolledWindow; +class wxTextCtrl; +class wxWrapSizer; + +namespace Slic3r { +namespace GUI { + +class GradientCurveEditor; +class RatioLabelPanel; + +struct MixedFilamentResult { + std::vector<unsigned int> components; // 1-based physical filament indices + std::vector<int> ratios; // percentages, sum = 100 + bool gradient_enabled = false; + int gradient_direction = 0; // 0 = A→B, 1 = B→A (only for 2-color) + bool per_part_gradient = false; // valid only when gradient_enabled == true + // Optional Photoshop-style custom curve overriding the linear A→B gradient. + // Empty -> use linear (gradient_direction). Non-empty -> cubic Hermite over [0,1]^2 + // with optional per-anchor tangent overrides (see GradientAnchor). + std::vector<GradientAnchor> gradient_curve; +}; + +class MixedFilamentDialog : public DPIDialog +{ +public: + MixedFilamentDialog(wxWindow* parent, + const std::vector<std::string>& physical_colors, + const std::vector<std::string>& physical_names, + const std::vector<std::string>& physical_types = {}); + + MixedFilamentDialog(wxWindow* parent, + const MixedFilamentResult& existing, + const std::vector<std::string>& physical_colors, + const std::vector<std::string>& physical_names, + const std::vector<std::string>& physical_types = {}); + + ~MixedFilamentDialog(); + + MixedFilamentResult get_result() const { return m_result; } + +protected: + void on_dpi_changed(const wxRect& suggested_rect) override; + +private: + void build_ui(); + wxBoxSizer* create_preview_panel(); + wxBoxSizer* create_material_selection(); + wxBoxSizer* create_ratio_slider(); + wxBoxSizer* create_triangle_picker(); + wxBoxSizer* create_gradient_section(); + wxBoxSizer* create_recommendation_grid(); + wxBoxSizer* create_button_panel(); + + void on_filament_changed(); + void on_ratio_changed(int new_ratio_a); + void on_gradient_toggled(); + void on_gradient_direction_changed(); + void on_gradient_curve_changed(); + void on_per_part_gradient_toggled(); + void on_add_material(); + void on_remove_material(); + void on_recommendation_clicked(unsigned int comp_a, unsigned int comp_b); + void on_recommendation_clicked_triple(unsigned int a, unsigned int b, unsigned int c); + void apply_manual_ratio(size_t idx, int value); + void apply_dragged_triangle_ratio(int r0, int r1, int r2); + void reset_manual_ratio_state(); + void refresh_ratio_labels(); + void sync_triangle_weights_from_ratios(); + void start_ratio_editor(size_t idx, wxWindow* anchor, const wxRect& anchor_rect); + void commit_ratio_editor(bool apply); + void commit_ratio_editor_from_background(wxMouseEvent& e); + void update_preview(); + void update_ok_button_state(); + void update_gradient_direction_items(); + void update_component_count_ui(); + // Picks dialog (width, height) based on current state so the gradient curve + // editor and the recommendation list stay visible at the same time. + wxSize compute_dialog_size() const; + void rebuild_all_combos(); + void rebuild_recommendation_items(); + void refresh_curve_editor_colors(); + void paint_warning_panel(wxPaintEvent& evt); + + wxBitmap make_swatch_bitmap(size_t idx); + + // Reserves the same width on every material row label so the combo boxes line up. + static void apply_uniform_label_width(wxStaticText* lbl); + // Appends one "Filament N" label + combo row to m_material_rows_sizer. N follows the + // number of rows already there, so callers must not renumber anything themselves. + void append_material_row(); + + // Helpers for component/ratio access + size_t num_components() const { return m_result.components.size(); } + unsigned int comp(size_t i) const { return (i < m_result.components.size()) ? m_result.components[i] : 1; } + int ratio(size_t i) const { return (i < m_result.ratios.size()) ? m_result.ratios[i] : 0; } + wxColour comp_colour(size_t i) const; + + MixedFilamentResult m_result; + bool m_edit_mode{false}; + std::vector<std::string> m_physical_colors; + std::vector<std::string> m_physical_names; + std::vector<std::string> m_physical_types; + wxString m_type_mismatch_msg; + + // Combo item index -> 1-based physical filament index (per combo) + std::vector<std::vector<unsigned int>> m_combo_to_physical; + + // UI controls + wxPanel* m_preview_canvas{nullptr}; + wxPanel* m_summary_panel{nullptr}; + std::vector<ComboBox*> m_combo_filaments; + wxBoxSizer* m_material_rows_sizer{nullptr}; + wxPanel* m_ratio_bar{nullptr}; + wxPanel* m_triangle_panel{nullptr}; + RatioLabelPanel* m_label_ratio_a{nullptr}; + RatioLabelPanel* m_label_ratio_b{nullptr}; + wxPanel* m_ratio_editor_panel{nullptr}; + wxTextCtrl* m_ratio_editor{nullptr}; + CheckBox* m_chk_gradient{nullptr}; + wxStaticText* m_label_gradient{nullptr}; + ComboBox* m_combo_gradient_dir{nullptr}; + wxBoxSizer* m_gradient_sizer{nullptr}; + GradientCurveEditor* m_curve_editor{nullptr}; + wxBoxSizer* m_curve_sizer{nullptr}; + CheckBox* m_chk_per_part_gradient{nullptr}; + wxStaticText* m_label_per_part_gradient{nullptr}; + wxBoxSizer* m_per_part_gradient_sizer{nullptr}; + Button* m_btn_add_material{nullptr}; + Button* m_btn_remove_material{nullptr}; + Button* m_btn_ok{nullptr}; + Button* m_btn_cancel{nullptr}; + wxBoxSizer* m_warning_sizer{nullptr}; + wxPanel* m_warning_panel{nullptr}; + + wxBoxSizer* m_ratio_sizer{nullptr}; + wxBoxSizer* m_triangle_sizer{nullptr}; + wxBoxSizer* m_right_sizer{nullptr}; + + wxScrolledWindow* m_recommendation_scroll{nullptr}; + wxWrapSizer* m_recommendation_grid{nullptr}; + + // Drag state. The ratio bar and the triangle picker capture the mouse + // independently, so they must not share a flag: a mouse-up on one would + // otherwise clear the other's flag and skip its ReleaseMouse(). + bool m_ratio_dragging{false}; + bool m_tri_dragging{false}; + std::vector<size_t> m_ratio_manual_order; + size_t m_ratio_editor_idx{0}; + bool m_ratio_editor_committing{false}; + wxWindow* m_ratio_editor_anchor{nullptr}; + // Triangle picker drag point (barycentric weights) + double m_tri_wx{0.333}, m_tri_wy{0.333}, m_tri_wz{0.334}; + + // Cached triangle color bitmap (invalidated when colors or size change) + wxBitmap m_tri_cache_bmp; + wxColour m_tri_cache_c0, m_tri_cache_c1, m_tri_cache_c2; + wxSize m_tri_cache_size; + std::array<RatioLabelPanel*, 3> m_triangle_ratio_labels{nullptr, nullptr, nullptr}; +}; + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_MixedFilamentDialog_hpp_ diff --git a/src/slic3r/GUI/Monitor.cpp b/src/slic3r/GUI/Monitor.cpp index 9f074d3f93..1a6d969988 100644 --- a/src/slic3r/GUI/Monitor.cpp +++ b/src/slic3r/GUI/Monitor.cpp @@ -186,17 +186,17 @@ void MonitorPanel::init_tabpanel() //m_status_add_machine_panel = new AddMachinePanel(m_tabpanel); m_status_info_panel = new StatusPanel(m_tabpanel); - m_tabpanel->AddPage(m_status_info_panel, _L("Status"), "", true); + m_tabpanel->AddPage(m_status_info_panel, _L("Status"), true); m_media_file_panel = new MediaFilePanel(m_tabpanel); - m_tabpanel->AddPage(m_media_file_panel, _L("Storage"), "", false); - //m_tabpanel->AddPage(m_media_file_panel, _L("Internal Storage"), "", false); + m_tabpanel->AddPage(m_media_file_panel, _L("Storage"), false); + //m_tabpanel->AddPage(m_media_file_panel, _L("Internal Storage"), false); m_upgrade_panel = new UpgradePanel(m_tabpanel); - m_tabpanel->AddPage(m_upgrade_panel, _L_CONTEXT(L_CONTEXT("Update", "Firmware"), "Firmware"), "", false); + m_tabpanel->AddPage(m_upgrade_panel, _L_CONTEXT(L_CONTEXT("Update", "Firmware"), "Firmware"), false); m_hms_panel = new HMSPanel(m_tabpanel); - m_tabpanel->AddPage(m_hms_panel, _L("Assistant(HMS)"), "", false); + m_tabpanel->AddPage(m_hms_panel, _L("Assistant(HMS)"), false); std::string network_ver = Slic3r::NetworkAgent::get_version(); if (!network_ver.empty()) { @@ -413,7 +413,10 @@ void MonitorPanel::update_hms_tag() bool MonitorPanel::Show(bool show) { #ifdef __APPLE__ - wxGetApp().mainframe->SetMinSize(wxGetApp().plater()->GetMinSize()); + // Notebook::InsertPage() hides every page it appends, so this also runs while MainFrame is + // still constructing, before GUI_App::mainframe is assigned. Same guard as Plater::Show(). + if (wxGetApp().mainframe) + wxGetApp().mainframe->SetMinSize(wxGetApp().plater()->GetMinSize()); #endif NetworkAgent* m_agent = wxGetApp().getAgent(); diff --git a/src/slic3r/GUI/Monitor.hpp b/src/slic3r/GUI/Monitor.hpp index fe115a351a..13688d6526 100644 --- a/src/slic3r/GUI/Monitor.hpp +++ b/src/slic3r/GUI/Monitor.hpp @@ -86,11 +86,8 @@ private: /* side tools */ SideTools* m_side_tools{nullptr}; - wxStaticBitmap* m_bitmap_printer_type; wxStaticBitmap* m_bitmap_arrow; - wxStaticText* m_staticText_printer_name; wxStaticBitmap* m_bitmap_wifi_signal; - wxBoxSizer * m_side_tools_sizer; SelectMachinePopup m_select_machine; /* images */ @@ -101,7 +98,6 @@ private: wxBitmap m_printer_img; wxBitmap m_arrow_img; - int last_wifi_signal = -1; int last_status; bool m_initialized { false }; bool update_flag{false}; diff --git a/src/slic3r/GUI/MonitorBasePanel.h b/src/slic3r/GUI/MonitorBasePanel.h index c5c76acec3..42424a7187 100644 --- a/src/slic3r/GUI/MonitorBasePanel.h +++ b/src/slic3r/GUI/MonitorBasePanel.h @@ -34,7 +34,6 @@ #include "Widgets/AxisCtrlButton.hpp" #include "Widgets/TextInput.hpp" #include "Widgets/StaticLine.hpp" -#include "wxMediaCtrl2.h" #include "MediaPlayCtrl.h" /////////////////////////////////////////////////////////////////////////// diff --git a/src/slic3r/GUI/MsgDialog.cpp b/src/slic3r/GUI/MsgDialog.cpp index ebf4db6846..7f1effd162 100644 --- a/src/slic3r/GUI/MsgDialog.cpp +++ b/src/slic3r/GUI/MsgDialog.cpp @@ -9,8 +9,13 @@ #include <wx/clipbrd.h> #include <wx/checkbox.h> #include <wx/html/htmlwin.h> +#include <wx/html/winpars.h> + +#include <algorithm> #include <boost/algorithm/string/replace.hpp> +#include <boost/algorithm/string/split.hpp> +#include <boost/algorithm/string/classification.hpp> #include "libslic3r/libslic3r.h" #include "libslic3r/Utils.hpp" @@ -229,12 +234,82 @@ void MsgDialog::finalize() } +// A placeholder-parser caret line, pointing at the column where parsing failed. +static bool is_caret_line(const std::string &line) +{ + return std::count(line.begin(), line.end(), '^') == 1 && + std::all_of(line.begin(), line.end(), [](char c) { return c == ' ' || c == '^'; }); +} + +// Tag each line as a code excerpt (a caret line or the source line above one) that must stay +// monospaced for the '^' to align. +static std::vector<std::pair<std::string, bool>> classify_code_lines(const std::string &msg) +{ + std::vector<std::string> lines; + boost::split(lines, msg, boost::is_any_of("\n")); + for (std::string &line : lines) + if (!line.empty() && line.back() == '\r') + line.pop_back(); + + std::vector<std::pair<std::string, bool>> tagged; + tagged.reserve(lines.size()); + for (size_t i = 0; i < lines.size(); ++i) { + bool is_code = is_caret_line(lines[i]) || (i + 1 < lines.size() && is_caret_line(lines[i + 1])); + tagged.emplace_back(std::move(lines[i]), is_code); + } + return tagged; +} + +// Keeps whitespace literal so the caret's leading spaces survive. +// Used inside <code>, which supplies the fixed face. <pre> does both but adds a blank line above it. +class CodeExcerptTagHandler : public wxHtmlWinTagHandler +{ +public: + wxString GetSupportedTags() override { return wxT("EXCERPT"); } + bool HandleTag(const wxHtmlTag &tag) override + { + const wxHtmlWinParser::WhitespaceMode ws = m_WParser->GetWhitespaceMode(); + m_WParser->SetWhitespaceMode(wxHtmlWinParser::Whitespace_Pre); + ParseInner(tag); + m_WParser->SetWhitespaceMode(ws); + return true; + } +}; + +// Render the message as HTML, monospacing only the code excerpts. +static std::string format_parser_error_html(const std::string &msg) +{ + std::string out; + for (const auto &[text, is_code] : classify_code_lines(msg)) { + if (!out.empty()) out += "<br>"; // join, not trail; a trailing <br> forces a scrollbar + std::string escaped = xml_escape(text); + if (is_code) + out += "<code><excerpt>" + escaped + "</excerpt></code>"; + else + out += escaped; + } + return out; +} + +// Measure each line in the font it will render in, so the dialog fits the longest line without slack. +static wxSize measure_mixed_text(wxWindow *parent, const std::string &msg, const wxFont &prose_font, const wxFont &code_font) +{ + wxClientDC dc(parent); + int width = 0, height = 0; + for (const auto &[text, is_code] : classify_code_lines(msg)) { + dc.SetFont(is_code ? code_font : prose_font); + width = std::max(width, dc.GetTextExtent(wxString::FromUTF8(text.c_str())).GetWidth()); + height += dc.GetCharHeight(); + } + return wxSize(width, height); +} + // Text shown as HTML, so that mouse selection and Ctrl-V to copy will work. static void add_msg_content(wxWindow *parent, wxBoxSizer *content_sizer, wxString msg, - bool monospaced_font = false, - bool is_marked_msg = false, + bool has_code_excerpts = false, + bool is_marked_msg = false, const wxString &link_text = "", std::function<void(const wxString &)> link_callback = nullptr) { @@ -243,7 +318,7 @@ static void add_msg_content(wxWindow *parent, // count lines in the message int msg_lines = 0; - if (!monospaced_font) { + if (!has_code_excerpts) { int line_len = 55;// count of symbols in one line int start_line = 0; for (auto i = msg.begin(); i != msg.end(); ++i) { @@ -300,13 +375,23 @@ static void add_msg_content(wxWindow *parent, page_size = wxSize(info_width, page_height); } else { - wxClientDC dc(parent); - dc.SetFont(font); // ORCA without this it calculates bigger size - wxSize msg_sz = dc.GetMultiLineTextExtent(msg) + parent->FromDIP(wxSize(10,5)); // added extra spacing to prevent wrapping + wxSize msg_sz; + if (has_code_excerpts) { + msg_sz = measure_mixed_text(parent, msg.ToUTF8().data(), font, monospace); + } else { + wxClientDC dc(parent); + dc.SetFont(font); // ORCA without this it calculates bigger size + msg_sz = dc.GetMultiLineTextExtent(msg); + } + msg_sz += parent->FromDIP(wxSize(10,5)); // added extra spacing to prevent wrapping - page_size = wxSize(std::min(msg_sz.GetX(), info_width), std::min(msg_sz.GetY(), info_width)); + int page_height = msg_sz.GetY(); + // Reserve the horizontal scrollbar's height, or it clips the last line. + if (msg_sz.GetX() > info_width) + page_height += wxSystemSettings::GetMetric(wxSYS_HSCROLL_Y, parent); + page_size = wxSize(std::min(msg_sz.GetX(), info_width), std::min(page_height, info_width)); // Extra line breaks in message dialog - if (link_text.IsEmpty() && !link_callback && is_marked_msg == false) {//for common text + if (link_text.IsEmpty() && !link_callback && is_marked_msg == false && !has_code_excerpts) {//for common text html->Destroy(); if (msg_sz.GetX() < info_width) {//No need for line breaks info_width = msg_sz.GetX(); @@ -337,12 +422,15 @@ static void add_msg_content(wxWindow *parent, } html->SetMinSize(page_size); - std::string msg_escaped = xml_escape(msg.ToUTF8().data(), is_marked_msg); - boost::replace_all(msg_escaped, "\r\n", "<br>"); - boost::replace_all(msg_escaped, "\n", "<br>"); - if (monospaced_font) - // Code formatting will be preserved. This is useful for reporting errors from the placeholder parser. - msg_escaped = std::string("<pre><code>") + msg_escaped + "</code></pre>"; + std::string msg_escaped; + if (has_code_excerpts) { + html->GetParser()->AddTagHandler(new CodeExcerptTagHandler()); + msg_escaped = format_parser_error_html(msg.ToUTF8().data()); + } else { + msg_escaped = xml_escape(msg.ToUTF8().data(), is_marked_msg); + boost::replace_all(msg_escaped, "\r\n", "<br>"); + boost::replace_all(msg_escaped, "\n", "<br>"); + } if (!link_text.IsEmpty() && link_callback) { msg_escaped += "<span><a href=\"#\" style=\"color:rgb(0, 150, 136); text-decoration:underline;\">" + std::string(link_text.ToUTF8().data()) + "</a></span>"; @@ -360,15 +448,15 @@ static void add_msg_content(wxWindow *parent, // ErrorDialog -ErrorDialog::ErrorDialog(wxWindow *parent, const wxString &temp_msg, bool monospaced_font) +ErrorDialog::ErrorDialog(wxWindow *parent, const wxString &temp_msg, bool has_code_excerpts) : MsgDialog(parent, wxString::Format(_(L("%s error")), SLIC3R_APP_FULL_NAME), wxString::Format(_(L("%s has encountered an error")), SLIC3R_APP_FULL_NAME), wxOK) , msg(temp_msg) { - add_msg_content(this, content_sizer, msg, monospaced_font); + add_msg_content(this, content_sizer, msg, has_code_excerpts); - // Use a small bitmap with monospaced font, as the error text will not be wrapped. - logo->SetBitmap(create_scaled_bitmap("OrcaSlicer_192px_grayscale.png", this, monospaced_font ? 48 : /*1*/64)); + // Use a small bitmap for code excerpts, which cannot wrap and so need the width. + logo->SetBitmap(create_scaled_bitmap("OrcaSlicer_192px_grayscale.png", this, has_code_excerpts ? 48 : /*1*/64)); SetMaxSize(MSG_DLG_MAX_SIZE); diff --git a/src/slic3r/GUI/MsgDialog.hpp b/src/slic3r/GUI/MsgDialog.hpp index 174d734336..90fd160310 100644 --- a/src/slic3r/GUI/MsgDialog.hpp +++ b/src/slic3r/GUI/MsgDialog.hpp @@ -106,9 +106,9 @@ protected: class ErrorDialog : public MsgDialog { public: - // If monospaced_font is true, the error message is displayed using html <code><pre></pre></code> tags, - // so that the code formatting will be preserved. This is useful for reporting errors from the placeholder parser. - ErrorDialog(wxWindow *parent, const wxString &temp_msg, bool courier_font); + // If has_code_excerpts is true, code excerpts (a source line and the caret line below it) render + // monospaced so the caret aligns. Used for placeholder-parser errors. + ErrorDialog(wxWindow *parent, const wxString &temp_msg, bool has_code_excerpts); ErrorDialog(ErrorDialog &&) = delete; ErrorDialog(const ErrorDialog &) = delete; ErrorDialog &operator=(ErrorDialog &&) = delete; diff --git a/src/slic3r/GUI/MultiMachine.cpp b/src/slic3r/GUI/MultiMachine.cpp index f5ffd1319f..c0e84f40e9 100644 --- a/src/slic3r/GUI/MultiMachine.cpp +++ b/src/slic3r/GUI/MultiMachine.cpp @@ -217,7 +217,7 @@ std::vector<DeviceItem*> selected_machines(const std::vector<DeviceItem*>& dev_i SortItem::SortItem() { - sort_map.emplace(std::make_pair(SortRule::SR_None, [this](const DeviceItem* d1, const DeviceItem* d2) { + sort_map.emplace(std::make_pair(SortRule::SR_None, [](const DeviceItem* d1, const DeviceItem* d2) { return d1->state_dev_name > d2->state_dev_name; })); sort_map.emplace(std::make_pair(SortRule::SR_DEV_NAME, [this](const DeviceItem* d1, const DeviceItem* d2) { diff --git a/src/slic3r/GUI/MultiMachineManagerPage.cpp b/src/slic3r/GUI/MultiMachineManagerPage.cpp index d6531cc8d4..792c23fb13 100644 --- a/src/slic3r/GUI/MultiMachineManagerPage.cpp +++ b/src/slic3r/GUI/MultiMachineManagerPage.cpp @@ -19,7 +19,7 @@ MultiMachineItem::MultiMachineItem(wxWindow* parent, MachineObject* obj) Bind(wxEVT_LEAVE_WINDOW, &MultiMachineItem::OnLeaveWindow, this); Bind(wxEVT_LEFT_DOWN, &MultiMachineItem::OnLeftDown, this); Bind(wxEVT_MOTION, &MultiMachineItem::OnMove, this); - Bind(EVT_MULTI_DEVICE_VIEW, [this, obj](auto& e) { + Bind(EVT_MULTI_DEVICE_VIEW, [obj](auto& e) { wxGetApp().mainframe->jump_to_monitor(obj->get_dev_id()); if (wxGetApp().mainframe->m_monitor->get_status_panel()->get_media_play_ctrl()) { wxGetApp().mainframe->m_monitor->get_status_panel()->get_media_play_ctrl()->jump_to_play(); diff --git a/src/slic3r/GUI/MultiMachineManagerPage.hpp b/src/slic3r/GUI/MultiMachineManagerPage.hpp index 7bb12ffc75..2576e36f12 100644 --- a/src/slic3r/GUI/MultiMachineManagerPage.hpp +++ b/src/slic3r/GUI/MultiMachineManagerPage.hpp @@ -88,7 +88,6 @@ private: Button* m_task_name{ nullptr }; Button* m_status{ nullptr }; Button* m_action{ nullptr }; - Button* m_stop_all_botton{nullptr}; // tip when no device wxStaticText* m_tip_text{ nullptr }; diff --git a/src/slic3r/GUI/MultiMachinePage.cpp b/src/slic3r/GUI/MultiMachinePage.cpp index b9b71ad670..dd67e099a0 100644 --- a/src/slic3r/GUI/MultiMachinePage.cpp +++ b/src/slic3r/GUI/MultiMachinePage.cpp @@ -80,15 +80,15 @@ void MultiMachinePage::init_tabpanel() sizer_side_tools->Add(m_side_tools, 1, wxEXPAND, 0); m_tabpanel = new Tabbook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, sizer_side_tools, wxNB_LEFT | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME); m_tabpanel->SetBackgroundColour(wxColour("#FEFFFF")); - m_tabpanel->Bind(wxEVT_BOOKCTRL_PAGE_CHANGED, [this](wxBookCtrlEvent& e) {; }); + m_tabpanel->Bind(wxEVT_BOOKCTRL_PAGE_CHANGED, [](wxBookCtrlEvent& e) {; }); m_local_task_manager = new LocalTaskManagerPage(m_tabpanel); m_cloud_task_manager = new CloudTaskManagerPage(m_tabpanel); m_machine_manager = new MultiMachineManagerPage(m_tabpanel); - m_tabpanel->AddPage(m_machine_manager, _L("Device"), "", true); - m_tabpanel->AddPage(m_local_task_manager, _L("Task Sending"), "", false); - m_tabpanel->AddPage(m_cloud_task_manager, _L("Task Sent"), "", false); + m_tabpanel->AddPage(m_machine_manager, _L("Device"), true); + m_tabpanel->AddPage(m_local_task_manager, _L("Task Sending"), false); + m_tabpanel->AddPage(m_cloud_task_manager, _L("Task Sent"), false); } void MultiMachinePage::init_timer() diff --git a/src/slic3r/GUI/MultiTaskManagerPage.cpp b/src/slic3r/GUI/MultiTaskManagerPage.cpp index bca2de8761..48a7fbe43e 100644 --- a/src/slic3r/GUI/MultiTaskManagerPage.cpp +++ b/src/slic3r/GUI/MultiTaskManagerPage.cpp @@ -783,7 +783,7 @@ void LocalTaskManagerPage::refresh_user_device(bool clear) mtitem->m_send_time = task_state_info->get_sent_time(); mtitem->state_local_task = task_state_info->state(); - task_state_info->set_state_changed_fn([this, mtitem](TaskState state, int percent) { + task_state_info->set_state_changed_fn([mtitem](TaskState state, int percent) { mtitem->state_local_task = state; if (state == TaskState::TS_SEND_COMPLETED) { diff --git a/src/slic3r/GUI/NetworkPluginDialog.cpp b/src/slic3r/GUI/NetworkPluginDialog.cpp index bc898d0d24..42e1fc5fc5 100644 --- a/src/slic3r/GUI/NetworkPluginDialog.cpp +++ b/src/slic3r/GUI/NetworkPluginDialog.cpp @@ -156,14 +156,14 @@ void NetworkPluginDownloadDialog::create_update_available_ui(const std::string& auto daa_chk = new CheckBox(this); daa_chk->SetValue(cfg->is_network_update_prompt_disabled()); - daa_chk->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e){ + daa_chk->Bind(wxEVT_TOGGLEBUTTON, [](wxCommandEvent& e){ auto cfg = wxGetApp().app_config; cfg->set_network_update_prompt_disabled(e.IsChecked()); cfg->save(); }); auto daa_str = new Label(this, _L("Don't Ask Again")); - auto on_toggle = [this, daa_chk]() { + auto on_toggle = [daa_chk]() { daa_chk->SetValue(!daa_chk->GetValue()); wxCommandEvent evt(wxEVT_TOGGLEBUTTON, daa_chk->GetId()); evt.SetEventObject(daa_chk); diff --git a/src/slic3r/GUI/NetworkTestDialog.cpp b/src/slic3r/GUI/NetworkTestDialog.cpp index 78f395febc..53876d9f97 100644 --- a/src/slic3r/GUI/NetworkTestDialog.cpp +++ b/src/slic3r/GUI/NetworkTestDialog.cpp @@ -268,7 +268,7 @@ void NetworkTestDialog::start_test_url(TestJob job, wxString name, wxString url) int result = -1; http.timeout_max(10) - .on_complete([this, &result](std::string body, unsigned status) { + .on_complete([&result](std::string body, unsigned status) { try { if (status == 200) { result = 0; diff --git a/src/slic3r/GUI/Notebook.cpp b/src/slic3r/GUI/Notebook.cpp index ceda3fc0d6..67c1831e4d 100644 --- a/src/slic3r/GUI/Notebook.cpp +++ b/src/slic3r/GUI/Notebook.cpp @@ -56,7 +56,7 @@ ButtonsListCtrl::ButtonsListCtrl(wxWindow *parent, wxBoxSizer* side_tools) : // BBS: disable custom paint //this->Bind(wxEVT_PAINT, &ButtonsListCtrl::OnPaint, this); - Bind(wxEVT_SYS_COLOUR_CHANGED, [this](auto& e){ + Bind(wxEVT_SYS_COLOUR_CHANGED, [](auto& e){ }); } @@ -120,11 +120,11 @@ void ButtonsListCtrl::Rescale() void ButtonsListCtrl::SetSelection(int sel) { - if (m_selection == sel) + if (m_selection == sel && sel >= 0 && sel < static_cast<int>(m_pageButtons.size())) return; // BBS: change button color wxColour selected_btn_bg("#009688"); // Gradient #009688 - if (m_selection >= 0) { + if (m_selection >= 0 && m_selection < static_cast<int>(m_pageButtons.size())) { StateColor bg_color = StateColor( std::pair{wxColour(107, 107, 107), (int) StateColor::Hovered}, std::pair{wxColour(59, 68, 70), (int) StateColor::Normal}); @@ -132,9 +132,15 @@ void ButtonsListCtrl::SetSelection(int sel) StateColor text_color = StateColor( std::pair{wxColour(254,254, 254), (int) StateColor::Normal} ); - m_pageButtons[m_selection]->SetSelected(false); m_pageButtons[m_selection]->SetTextColor(text_color); } + + if (sel < 0 || sel >= static_cast<int>(m_pageButtons.size())) { + m_selection = -1; + Refresh(); + return; + } + m_selection = sel; StateColor bg_color = StateColor( @@ -145,17 +151,19 @@ void ButtonsListCtrl::SetSelection(int sel) StateColor text_color = StateColor( std::pair{wxColour(254, 254, 254), (int) StateColor::Normal} ); - m_pageButtons[m_selection]->SetSelected(true); m_pageButtons[m_selection]->SetTextColor(text_color); Refresh(); } -bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, const std::string &inactive_bmp_name) +bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, const wxBitmap &bmp /* = wxNullBitmap */) { Button * btn = new Button(this, text.empty() ? text : " " + text, bmp_name, wxNO_BORDER); btn->SetCornerRadius(0); + if (bmp_name.empty() && bmp.IsOk()) + btn->SetIcon(bmp); + int em = em_unit(this); //BBS set size for button btn->SetMinSize({(text.empty() ? 40 : 136) * em / 10, 36 * em / 10}); @@ -168,8 +176,6 @@ bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* StateColor text_color = StateColor( std::pair{wxColour(254,254, 254), (int) StateColor::Normal}); btn->SetTextColor(text_color); - btn->SetInactiveIcon(inactive_bmp_name); - btn->SetSelected(false); btn->Bind(wxEVT_BUTTON, [this, btn](wxCommandEvent& event) { if (auto it = std::find(m_pageButtons.begin(), m_pageButtons.end(), btn); it != m_pageButtons.end()) { auto sel = it - m_pageButtons.begin(); @@ -192,6 +198,14 @@ bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* void ButtonsListCtrl::RemovePage(size_t n) { + if (n >= m_pageButtons.size()) + return; + + if (m_selection == static_cast<int>(n)) + m_selection = -1; + else if (m_selection > static_cast<int>(n)) + --m_selection; + Button* btn = m_pageButtons[n]; m_pageButtons.erase(m_pageButtons.begin() + n); m_pageLabels.erase(m_pageLabels.begin() + n); // ORCA @@ -240,6 +254,24 @@ wxString ButtonsListCtrl::GetPageText(size_t n) const return btn->GetLabel(); } +// ORCA +void ButtonsListCtrl::SetOverflowButton(wxWindow* button) +{ + if (m_overflow_button == button) + return; + + if (m_overflow_button != nullptr) + m_sizer->Detach(m_overflow_button); + + m_overflow_button = button; + + if (m_overflow_button != nullptr) + // Right after the tab buttons (index 0), ahead of any stretch spacer / side_tools. + m_sizer->Insert(1, m_overflow_button, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxBOTTOM, m_btn_margin); + + m_sizer->Layout(); +} + //#endif // _WIN32 void Notebook::Init() @@ -253,6 +285,8 @@ void Notebook::Init() m_showTimeout = m_hideTimeout = 0; + m_pageNames.clear(); + /* On Linux, Gstreamer wxMediaCtrl does not seem to get along well with * 32-bit X11 visuals (the overlay does not work). Is this a wxWindows * bug? Is this a Gstreamer bug? No idea, but it is our problem ... diff --git a/src/slic3r/GUI/Notebook.hpp b/src/slic3r/GUI/Notebook.hpp index d333956561..4734122b18 100644 --- a/src/slic3r/GUI/Notebook.hpp +++ b/src/slic3r/GUI/Notebook.hpp @@ -3,7 +3,11 @@ //#ifdef _WIN32 +#include <initializer_list> +#include <string> +#include <vector> #include <wx/bookctrl.h> +#include <wx/bitmap.h> #include <wx/sizer.h> class ScalableButton; @@ -23,13 +27,16 @@ public: void SetSelection(int sel); void UpdateMode(); void Rescale(); - bool InsertPage(size_t n, const wxString &text, bool bSelect = false, const std::string &bmp_name = "", const std::string &inactive_bmp_name = ""); + bool InsertPage(size_t n, const wxString &text, bool bSelect = false, const std::string &bmp_name = "", const wxBitmap &bmp = wxNullBitmap); void RemovePage(size_t n); bool SetPageImage(size_t n, const std::string& bmp_name) const; void SetPageText(size_t n, const wxString& strText); void SetCompact(size_t n, bool compact); // ORCA wxString GetPageText(size_t n) const; wxFlexGridSizer* GetBtnsSizer(){return m_buttons_sizer;}; // ORCA + // ORCA: a companion widget shown right after the tab buttons (before any side_tools), e.g. + // an overflow indicator. Pass nullptr to remove it; ownership stays with the caller. + void SetOverflowButton(wxWindow* button); private: wxFlexGridSizer* m_buttons_sizer; @@ -40,9 +47,10 @@ private: int m_btn_margin; int m_line_margin; std::vector<wxString> m_pageLabels; // ORCA + wxWindow* m_overflow_button{nullptr}; // ORCA }; -class Notebook: public wxBookCtrlBase +class Notebook : public wxBookCtrlBase { public: Notebook(wxWindow * parent, @@ -103,7 +111,7 @@ public: // by this control) and show it immediately. bool ShowNewPage(wxWindow * page) { - return AddPage(page, wxString(), "", ""); + return AddPage(page, wxString(), false, NO_IMAGE); } @@ -135,51 +143,56 @@ public: // Implement base class pure virtual methods. - // adds a new page to the control - bool AddPage(wxWindow* page, + // Page management. Every insertion funnels through the InsertPage() below; `id` is the + // stable page name FindPageByName() resolves. Built-in tabs name a resource bitmap, + // plugin pages hand over a ready wxBitmap; wx's own imageId overloads carry neither. + bool AddPage(const wxString& id, + wxWindow* page, const wxString& text, - const std::string& bmp_name, - const std::string& inactive_bmp_name, + const std::string& bmp_name = "", bool bSelect = false) { DoInvalidateBestSize(); - return InsertPage(GetPageCount(), page, text, bmp_name, inactive_bmp_name, bSelect); + return InsertPage(GetPageCount(), id, page, text, bmp_name, bSelect); } - // Page management - virtual bool InsertPage(size_t n, - wxWindow * page, - const wxString & text, - bool bSelect = false, - int imageId = NO_IMAGE) override + bool AddPage(wxWindow* page, const wxString& text, bool bSelect = false, int imageId = NO_IMAGE) override { - if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect, imageId)) + DoInvalidateBestSize(); + return InsertPage(GetPageCount(), page, text, bSelect, imageId); + } + + bool InsertPage(size_t n, + const wxString& id, + wxWindow * page, + const wxString & text, + const std::string& bmp_name = "", + bool bSelect = false, + const wxBitmap& bmp = wxNullBitmap) + { + if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect)) return false; - GetBtnsListCtrl()->InsertPage(n, text, bSelect); + m_pageNames.insert(m_pageNames.begin() + n, id); + GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name, bmp); + // wxBookCtrlBase::InsertPage() only inserts into the page list and sizes the new + // page to the current page's rect — it never touches visibility, and a freshly + // constructed page defaults to shown. Without this it renders on top of whatever + // page is currently selected until the next SetSelection() call hides it. if (!DoSetSelectionAfterInsertion(n, bSelect)) page->Hide(); return true; } - bool InsertPage(size_t n, - wxWindow * page, - const wxString & text, - const std::string& bmp_name = "", - const std::string& inactive_bmp_name = "", - bool bSelect = false) + virtual bool InsertPage(size_t n, + wxWindow * page, + const wxString & text, + bool bSelect = false, + int WXUNUSED(imageId) = NO_IMAGE) override { - if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect)) - return false; - - GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name, inactive_bmp_name); - - if (bSelect) - SetSelection(n); - - return true; + return InsertPage(n, wxString(), page, text, "", bSelect); } virtual int SetSelection(size_t n) override @@ -211,8 +224,8 @@ public: return DoSetSelection(n); } - // Neither labels nor images are supported but we still store the labels - // just in case the user code attaches some importance to them. + // Labels are stored by the custom button list; wx's image-list API is unused — tab icons + // are set directly on the buttons, either from a resource name or a ready wxBitmap. virtual bool SetPageText(size_t n, const wxString & strText) override { wxCHECK_MSG(n < GetPageCount(), false, wxS("Invalid page")); @@ -251,7 +264,64 @@ public: page->SetFocus(); } + // The base clears its page list directly instead of calling DoRemovePage() per page, + // which would leave m_pageNames behind. No caller today; kept in sync regardless. + virtual bool DeleteAllPages() override + { + m_pageNames.clear(); + return wxBookCtrlBase::DeleteAllPages(); + } + ButtonsListCtrl* GetBtnsListCtrl() const { return static_cast<ButtonsListCtrl*>(m_bookctrl); } + void SetOverflowButton(wxWindow* button) { GetBtnsListCtrl()->SetOverflowButton(button); } + + // Insertion index just past the first of `ids` that is present, or the end of the bar + // if none is — lets call sites state tab order as "after X" instead of re-deriving it. + size_t PositionAfter(std::initializer_list<const char*> ids) const + { + for (const char* id : ids) + if (const int idx = FindPageByName(id); idx != wxNOT_FOUND) + return static_cast<size_t>(idx) + 1; + return GetPageCount(); + } + + int FindPageByName(const wxString& id) const + { + if (id.empty()) + return wxNOT_FOUND; + for (size_t i = 0; i < m_pageNames.size(); ++i) + if (m_pageNames[i] == id) + return static_cast<int>(i); + return wxNOT_FOUND; + } + + wxWindow* GetPageByName(const wxString& id) const + { + const int idx = FindPageByName(id); + return idx == wxNOT_FOUND ? nullptr : GetPage(static_cast<size_t>(idx)); + } + + bool SelectPageByName(const wxString& id) + { + const int idx = FindPageByName(id); + if (idx == wxNOT_FOUND) + return false; + SetSelection(static_cast<size_t>(idx)); + return true; + } + + // Inverse of FindPageByName: index -> id. Empty string for an out-of-range + // index or a page that was never given an id (e.g. settings Tab pages). + wxString GetPageName(size_t n) const + { + return n < m_pageNames.size() ? m_pageNames[n] : wxString(); + } + + wxString GetSelectedPageName() const + { + const int sel = GetSelection(); + return sel < 0 ? wxString() : GetPageName(static_cast<size_t>(sel)); + } void UpdateMode() { @@ -369,6 +439,7 @@ protected: wxWindow* const win = wxBookCtrlBase::DoRemovePage(page); if (win) { + m_pageNames.erase(m_pageNames.begin() + page); GetBtnsListCtrl()->RemovePage(page); DoSetSelectionAfterRemoval(page); } @@ -394,6 +465,8 @@ protected: private: void Init(); + std::vector<wxString> m_pageNames; // index-parallel to wxBookCtrlBase::m_pages + wxShowEffect m_showEffect, m_hideEffect; diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 8e81f0654c..5e83ad845f 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -1918,7 +1918,7 @@ void NotificationManager::push_validate_error_notification(StringObjectException wxGetApp().sidebar().jump_to_option(opt, Preset::TYPE_PRINT, L""); } else { - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); } return false; }; @@ -1985,7 +1985,7 @@ void NotificationManager::push_validate_error_notification(StringObjectException wxGetApp().sidebar().jump_to_option(opt, opt_type, L""); } else { - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); } return false; }; @@ -2015,7 +2015,7 @@ void NotificationManager::push_slicing_error_notification(const std::string &tex if (iter != objects.end()) { ovs.push_back({ *iter, nullptr }); } } if (!ovs.empty()) { - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); wxGetApp().obj_list()->select_items(ovs); } return false; @@ -2046,7 +2046,7 @@ void NotificationManager::push_slicing_warning_notification(const std::string& t auto& objects = wxGetApp().model().objects; auto iter = std::find_if(objects.begin(), objects.end(), [id](auto o) { return o->id() == id; }); if (iter != objects.end()) { - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); wxGetApp().obj_list()->select_items({ {*iter, nullptr} }); } return false; @@ -2693,7 +2693,7 @@ void NotificationManager::push_slicing_serious_warning_notification(const std::s if (iter != objects.end()) { ovs.push_back({ *iter, nullptr }); } } if (!ovs.empty()) { - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); wxGetApp().obj_list()->select_items(ovs); wxGetApp().obj_list()->update_selections_on_canvas(); } @@ -2777,7 +2777,7 @@ void NotificationManager::push_slicing_serious_warning_notification(const std::s } } - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (!sel_items.empty()) { obj_list->select_items(sel_items); diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 22720bf33e..bf0a5cfe1a 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -162,6 +162,8 @@ enum class NotificationType //BBL: plugin install hint BBLPluginInstallHint, BBLFlushingVolumeZero, + // A mixed-color filament references a deleted component, or its components disagree in type. + BBLMixedFilamentBroken, BBLPluginUpdateAvailable, BBLPreviewOnlyMode, BBLPrinterConfigUpdateAvailable, @@ -172,6 +174,8 @@ enum class NotificationType BBLBedFilamentIncompatible, BBLMixUsePLAAndPETG, BBLNozzleFilamentIncompatible, + // A mixed-color filament is printed on a single-nozzle printer (frequent changes and purging). + BBLSingleExtruderMixedFilamentRisk, OrcaSharedProfilesAvailable, OrcaCloudAPIError, OrcaSyncConflict, diff --git a/src/slic3r/GUI/OG_CustomCtrl.cpp b/src/slic3r/GUI/OG_CustomCtrl.cpp index 2ee94dbeeb..4fce046162 100644 --- a/src/slic3r/GUI/OG_CustomCtrl.cpp +++ b/src/slic3r/GUI/OG_CustomCtrl.cpp @@ -158,7 +158,7 @@ wxPoint OG_CustomCtrl::get_pos(const Line& line, Field* field_in/* = nullptr*/) ctrl_line.height = size.y; }; - auto add_buttons_width = [&h_pos, this] (int blinking_button_width) { + auto add_buttons_width = [&h_pos] (int blinking_button_width) { #ifndef DISABLE_BLINKING # ifndef DISABLE_UNDO_SYS h_pos += 3 * blinking_button_width; diff --git a/src/slic3r/GUI/ObjColorDialog.cpp b/src/slic3r/GUI/ObjColorDialog.cpp index 47f62aa168..d5a37fc9a7 100644 --- a/src/slic3r/GUI/ObjColorDialog.cpp +++ b/src/slic3r/GUI/ObjColorDialog.cpp @@ -254,7 +254,7 @@ ObjColorPanel::ObjColorPanel(wxWindow *parent, Slic3r::ObjDialogInOut &in_out, c m_color_cluster_num_by_user_ebox->Bind(wxEVT_TEXT_ENTER, on_apply_color_cluster_text_modify); m_color_cluster_num_by_user_ebox->Bind(wxEVT_SPINCTRL, on_apply_color_cluster_text_modify); - m_color_cluster_num_by_user_ebox->Bind(wxEVT_CHAR, [this](wxKeyEvent &e) { + m_color_cluster_num_by_user_ebox->Bind(wxEVT_CHAR, [](wxKeyEvent &e) { int keycode = e.GetKeyCode(); wxString input_char = wxString::Format("%c", keycode); long value; diff --git a/src/slic3r/GUI/ObjColorDialog.hpp b/src/slic3r/GUI/ObjColorDialog.hpp index 1ad74cdccd..19ed1dcb24 100644 --- a/src/slic3r/GUI/ObjColorDialog.hpp +++ b/src/slic3r/GUI/ObjColorDialog.hpp @@ -79,7 +79,6 @@ private: std::vector<wxBoxSizer *> m_row_col_boxsizer_list; std::vector<ButtonState*> m_result_icon_list; int m_last_cluster_num{-1}; - const int m_combox_width{50}; int m_combox_icon_width; int m_combox_icon_height; wxButton * m_image_button = nullptr; diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index 9fb4483883..25c13c4b8d 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -54,6 +54,9 @@ const t_field& OptionsGroup::build_field(const t_config_option_key& id, const Co case ConfigOptionDef::GUIType::one_string: m_fields.emplace(id, TextCtrl::Create<TextCtrl>(this->ctrl_parent(), opt, id)); break; case ConfigOptionDef::GUIType::plugin_picker: m_fields.emplace(id, PluginField::Create<PluginField>(this->ctrl_parent(), opt, id)); break; case ConfigOptionDef::GUIType::plugin_config: m_fields.emplace(id, PluginConfigField::Create<PluginConfigField>(this->ctrl_parent(), opt, id)); break; + case ConfigOptionDef::GUIType::printer_agent_select: m_fields.emplace( + id, PrinterAgentChoice::Create<PrinterAgentChoice>(this->ctrl_parent(), opt, id)); + break; default: switch (opt.type) { case coFloatOrPercent: @@ -654,6 +657,16 @@ Option ConfigOptionsGroup::get_option(const std::string& opt_key, int opt_index void ConfigOptionsGroup::on_change_OG(const t_config_option_key& opt_id, const boost::any& value) { + if (opt_id == "printer_agent") { + // TODO: Replace this option-specific branch with a generic value adapter if + // more fields need custom field-value to config-value conversion. + if (const std::string* id = boost::any_cast<std::string>(&value)) + this->change_opt_value("printer_agent", wxGetApp().canonical_printer_agent_id(*id)); + + OptionsGroup::on_change_OG(opt_id, value); + return; + } + if (!m_opt_map.empty()) { auto it = m_opt_map.find(opt_id); if (it == m_opt_map.end()) { @@ -772,6 +785,19 @@ void ConfigOptionsGroup::back_to_config_value(const DynamicPrintConfig& config, } } #endif + else if (opt_key == "printer_agent") + { + // why: printer_agent is a coString kept out of m_opt_map. The generic non-opt_map revert + // below restores the edited config from get_value(), but a deregistered/"(missing)" saved + // id has no selectable row, so the field yields no value and the edited config keeps the + // user's interim pick -> stuck dirty. Restore the SAVED id straight into the edited config + // (displayable or not; config is the saved or system baseline), then repaint and notify. + const std::string saved_id = config.opt_string("printer_agent"); + set_value(opt_key, saved_id); + this->change_opt_value(opt_key, saved_id); + OptionsGroup::on_change_OG(opt_key, saved_id); + return; + } else if (m_opt_map.find(opt_key) == m_opt_map.end() || // This option don't have corresponded field opt_key == "printable_area" || opt_key == "compatible_printers" || opt_key == "compatible_prints" || opt_key == "thumbnails" || diff --git a/src/slic3r/GUI/ParamsPanel.cpp b/src/slic3r/GUI/ParamsPanel.cpp index 7d054d8a1b..a0834666ad 100644 --- a/src/slic3r/GUI/ParamsPanel.cpp +++ b/src/slic3r/GUI/ParamsPanel.cpp @@ -128,7 +128,7 @@ wxBoxSizer *TipsDialog::create_item_checkbox(wxString title, wxWindow *parent, w m_show_again = wxGetApp().app_config->has(param); checkbox->SetValue(m_show_again); - checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox, param](wxCommandEvent &e) { + checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, param](wxCommandEvent &e) { m_show_again = m_show_again ? false : true; e.Skip(); }); @@ -290,11 +290,11 @@ ParamsPanel::ParamsPanel( wxWindow* parent, wxWindowID id, const wxPoint& pos, c m_compare_btn = new ScalableButton(m_top_panel, wxID_ANY, "compare", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true); m_compare_btn->SetToolTip(_L("Compare presets")); - m_compare_btn->Bind(wxEVT_BUTTON, ([this](wxCommandEvent e) { wxGetApp().mainframe->diff_dialog.show(); })); + m_compare_btn->Bind(wxEVT_BUTTON, ([](wxCommandEvent e) { wxGetApp().mainframe->diff_dialog.show(); })); m_setting_btn = new ScalableButton(m_top_panel, wxID_ANY, "table", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true); m_setting_btn->SetToolTip(_L("View all object's settings")); - m_setting_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { wxGetApp().plater()->PopupObjectTable(-1, -1, {0, 0}); }); + m_setting_btn->Bind(wxEVT_BUTTON, [](wxCommandEvent &) { wxGetApp().plater()->PopupObjectTable(-1, -1, {0, 0}); }); m_highlighter.set_timer_owner(this, 0); this->Bind(wxEVT_TIMER, [this](wxTimerEvent &) diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 4f57c2577d..849aa4e31a 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -6,6 +6,7 @@ #include <sstream> #include <regex> #include "libslic3r/MultiNozzleUtils.hpp" +#include "libslic3r/FilamentMixer.hpp" #include <future> #include <glad/gl.h> #include <boost/algorithm/string.hpp> @@ -156,7 +157,7 @@ PartPlate::PartPlate() init(); } -PartPlate::PartPlate(PartPlateList *partplate_list, Vec3d origin, int width, int depth, int height, Plater* platerObj, Model* modelObj, bool printable, PrinterTechnology tech) +PartPlate::PartPlate(PartPlateList *partplate_list, Vec3d origin, int width, int depth, double height, Plater* platerObj, Model* modelObj, bool printable, PrinterTechnology tech) :m_partplate_list(partplate_list), m_plater(platerObj), m_model(modelObj), printer_technology(tech), m_origin(origin), m_width(width), m_depth(depth), m_height(height), m_printable(printable) { init(); @@ -1675,6 +1676,25 @@ std::vector<int> PartPlate::get_extruders(bool conside_custom_gcode) const std::sort(plate_extruders.begin(), plate_extruders.end()); auto it_end = std::unique(plate_extruders.begin(), plate_extruders.end()); plate_extruders.resize(std::distance(plate_extruders.begin(), it_end)); + + // Expand mixed filament slots to their physical components. A mixed slot is virtual and + // is never loaded into a tray, so callers (AMS mapping, filament checks) must see the + // physical filaments it resolves to instead. + { + auto& project_config = wxGetApp().preset_bundle->project_config; + auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed"); + auto* comp_strs_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components"); + if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) { + std::vector<unsigned int> ext_0based; + for (int e : plate_extruders) + if (e >= 1) ext_0based.push_back((unsigned int)(e - 1)); + auto expanded = expand_mixed_filaments(ext_0based, is_mixed_opt->values, comp_strs_opt->values); + plate_extruders.clear(); + for (unsigned int e : expanded) + plate_extruders.push_back((int)(e + 1)); + } + } + return plate_extruders; } @@ -1737,26 +1757,25 @@ std::vector<int> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, D else obj_support = glb_support; - if (!obj_support) - continue; + if (obj_support) { + int obj_support_intf_extr = 0; + const ConfigOption* support_intf_extr_opt = object->config.option("support_interface_filament"); + if (support_intf_extr_opt != nullptr) + obj_support_intf_extr = support_intf_extr_opt->getInt(); + if (obj_support_intf_extr != 0) + plate_extruders.push_back(obj_support_intf_extr); + else if (glb_support_intf_extr != 0) + plate_extruders.push_back(glb_support_intf_extr); - int obj_support_intf_extr = 0; - const ConfigOption* support_intf_extr_opt = object->config.option("support_interface_filament"); - if (support_intf_extr_opt != nullptr) - obj_support_intf_extr = support_intf_extr_opt->getInt(); - if (obj_support_intf_extr != 0) - plate_extruders.push_back(obj_support_intf_extr); - else if (glb_support_intf_extr != 0) - plate_extruders.push_back(glb_support_intf_extr); - - int obj_support_extr = 0; - const ConfigOption* support_extr_opt = object->config.option("support_filament"); - if (support_extr_opt != nullptr) - obj_support_extr = support_extr_opt->getInt(); - if (obj_support_extr != 0) - plate_extruders.push_back(obj_support_extr); - else if (glb_support_extr != 0) - plate_extruders.push_back(glb_support_extr); + int obj_support_extr = 0; + const ConfigOption* support_extr_opt = object->config.option("support_filament"); + if (support_extr_opt != nullptr) + obj_support_extr = support_extr_opt->getInt(); + if (obj_support_extr != 0) + plate_extruders.push_back(obj_support_extr); + else if (glb_support_extr != 0) + plate_extruders.push_back(glb_support_extr); + } int obj_outer_wall_extr = 0; if (const ConfigOption* wall_opt = object->config.option("outer_wall_filament_id"); wall_opt != nullptr) @@ -1836,6 +1855,24 @@ std::vector<int> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, D std::sort(plate_extruders.begin(), plate_extruders.end()); auto it_end = std::unique(plate_extruders.begin(), plate_extruders.end()); plate_extruders.resize(std::distance(plate_extruders.begin(), it_end)); + + // Expand mixed filament slots to their physical components. A mixed slot is virtual and + // is never loaded into a tray, so callers (AMS mapping, filament checks) must see the + // physical filaments it resolves to instead. + { + auto* is_mixed_opt = full_config.option<ConfigOptionBools>("filament_is_mixed"); + auto* comp_strs_opt = full_config.option<ConfigOptionStrings>("filament_mixed_components"); + if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) { + std::vector<unsigned int> ext_0based; + for (int e : plate_extruders) + if (e >= 1) ext_0based.push_back((unsigned int)(e - 1)); + auto expanded = expand_mixed_filaments(ext_0based, is_mixed_opt->values, comp_strs_opt->values); + plate_extruders.clear(); + for (unsigned int e : expanded) + plate_extruders.push_back((int)(e + 1)); + } + } + return plate_extruders; } @@ -1889,6 +1926,25 @@ std::vector<int> PartPlate::get_extruders_without_support(bool conside_custom_gc std::sort(plate_extruders.begin(), plate_extruders.end()); auto it_end = std::unique(plate_extruders.begin(), plate_extruders.end()); plate_extruders.resize(std::distance(plate_extruders.begin(), it_end)); + + // Expand mixed filament slots to their physical components. A mixed slot is virtual and + // is never loaded into a tray, so callers (AMS mapping, filament checks) must see the + // physical filaments it resolves to instead. + { + auto& project_config = wxGetApp().preset_bundle->project_config; + auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed"); + auto* comp_strs_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components"); + if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) { + std::vector<unsigned int> ext_0based; + for (int e : plate_extruders) + if (e >= 1) ext_0based.push_back((unsigned int)(e - 1)); + auto expanded = expand_mixed_filaments(ext_0based, is_mixed_opt->values, comp_strs_opt->values); + plate_extruders.clear(); + for (unsigned int e : expanded) + plate_extruders.push_back((int)(e + 1)); + } + } + return plate_extruders; } @@ -1990,6 +2046,50 @@ bool PartPlate::check_tpu_printable_status(const DynamicPrintConfig & config, co return true; } +// A mixed-color filament alternates between its components constantly. On a single-nozzle +// printer every one of those switches is a full filament change plus a purge, so warn before +// slicing. Multi-nozzle printers keep the components loaded at once and are not affected. +bool PartPlate::check_single_extruder_mixed_filament_risk(const DynamicPrintConfig &config, std::string &warning_text) const +{ + warning_text.clear(); + + auto *nozzle_diameter_opt = config.option<ConfigOptionFloatsNullable>("nozzle_diameter"); + if (!nozzle_diameter_opt || nozzle_diameter_opt->values.size() > 1) + return false; + + auto *is_mixed_opt = wxGetApp().preset_bundle->project_config.option<ConfigOptionBools>("filament_is_mixed"); + if (!is_mixed_opt || !has_any_mixed_filament(is_mixed_opt->values)) + return false; + + auto is_mixed_slot = [&](int extruder_1based) { + size_t idx = (size_t)(extruder_1based - 1); + return idx < is_mixed_opt->values.size() && is_mixed_opt->values[idx]; + }; + + const std::string mixed_warn_msg = _u8L("Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, " + "which may significantly increase waste and the risk of nozzle / waste-chute clogging."); + + for (int obj_idx = 0; obj_idx < (int)m_model->objects.size(); ++obj_idx) { + if (!contain_instance_totally(obj_idx, 0)) + continue; + ModelObject *mo = m_model->objects[obj_idx]; + int obj_ext = mo->config.has("extruder") ? mo->config.extruder() : 1; + if (is_mixed_slot(obj_ext)) { + warning_text = mixed_warn_msg; + return true; + } + for (ModelVolume *mv : mo->volumes) { + int vol_ext = mv->config.has("extruder") ? mv->config.extruder() : obj_ext; + if (is_mixed_slot(vol_ext)) { + warning_text = mixed_warn_msg; + return true; + } + } + } + + return false; +} + bool PartPlate::check_mixture_of_pla_and_petg(const DynamicPrintConfig &config) { bool has_pla = false; @@ -2262,6 +2362,12 @@ Vec3d PartPlate::estimate_wipe_tower_size(const DynamicPrintConfig & config, con } double volume = wipe_volume * (extruder_count == 2 ? plate_extruder_size : (plate_extruder_size - 1)); if (extruder_count == 2) volume += filament_change_volume * (int) (plate_extruder_size / 2); + // Read from the passed plate config — m_print may not have been applied yet + // (fresh plates, CLI), in which case its PrintConfig still holds defaults. + const auto *purge_opt = config.option<ConfigOptionBool>("purge_in_prime_tower"); + const auto *semm_opt = config.option<ConfigOptionBool>("single_extruder_multi_material"); + const bool semm_flush = purge_opt && purge_opt->value && semm_opt && semm_opt->value; + if (semm_flush) volume = WipeTower2::estimate_semm_flush_volume(config, plate_extruder_size); if (use_rib_wall) { depth = std::sqrt(volume / layer_height * extra_spacing); if (need_wipe_tower || plate_extruder_size > 1) { @@ -2274,7 +2380,9 @@ Vec3d PartPlate::estimate_wipe_tower_size(const DynamicPrintConfig & config, con } } else { - depth = volume/ (layer_height * w) *extra_spacing; + depth = volume / (layer_height * w); + // The flush volumes already hold the spacing between wipes. + if (!semm_flush) depth *= extra_spacing; if (need_wipe_tower || depth > EPSILON) { float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height); depth = std::max((double)min_wipe_tower_depth, depth); @@ -2364,7 +2472,7 @@ void PartPlate::clear(bool clear_sliced_result) /* size and position related functions*/ //set position and size -void PartPlate::set_pos_and_size(Vec3d& origin, int width, int depth, int height, bool with_instance_move, bool do_clear) +void PartPlate::set_pos_and_size(Vec3d& origin, int width, int depth, double height, bool with_instance_move, bool do_clear) { bool size_changed = false; //size changed means the machine changed bool pos_changed = false; @@ -2663,10 +2771,10 @@ bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* boundi if (instance_box.min.z() < SINKING_Z_THRESHOLD) { // Orca: For sinking object, we use a more expensive algorithm so part below build plate won't be considered - // m_plater is null in CLI mode. - if (m_plater && plate_box.intersects(instance_box)) { + // m_height mirrors the printer's printable height and is set in CLI mode too, unlike m_plater. + if (plate_box.intersects(instance_box)) { // TODO: FIXME: this does not take exclusion area into account - const BuildVolume build_volume(get_shape(), m_plater->build_volume().printable_height(), m_extruder_areas, m_extruder_heights); + const BuildVolume build_volume(get_shape(), m_height, m_extruder_areas, m_extruder_heights); const auto state = instance->calc_print_volume_state(build_volume); outside = state == ModelInstancePVS_Partly_Outside; } @@ -3337,6 +3445,11 @@ BoundingBoxf3 PartPlate::get_build_volume(bool use_share) return plate_box; } +Polygon PartPlate::get_shared_printable_polygon() const +{ + return m_extruder_areas.empty() ? Polygon::new_scale(m_shape) : get_shared_poly(m_extruder_areas); +} + bool PartPlate::contains(const Vec3d& point) const { return m_bounding_box.contains(point); @@ -3985,7 +4098,7 @@ void PartPlate::on_filament_deleted(int filament_count, int filament_id) /* PartPlate List related functions*/ -PartPlateList::PartPlateList(int width, int depth, int height, Plater* platerObj, Model* modelObj, PrinterTechnology tech) +PartPlateList::PartPlateList(int width, int depth, double height, Plater* platerObj, Model* modelObj, PrinterTechnology tech) :m_plate_width(width), m_plate_depth(depth), m_plate_height(height), m_plater(platerObj), m_model(modelObj), printer_technology(tech), unprintable_plate(this, Vec3d(0.0 + width * (1. + LOGICAL_PART_PLATE_GAP), 0.0, 0.0), width, depth, height, platerObj, modelObj, false, tech) { @@ -4375,22 +4488,21 @@ void PartPlateList::set_default_wipe_tower_pos_for_plate(int plate_idx, bool ini f_volume_maps = wxGetApp().preset_bundle->get_default_nozzle_volume_types_for_filaments(filament_maps); } DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config(false, filament_maps, f_volume_maps); - const DynamicPrintConfig &print_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; - float w = dynamic_cast<const ConfigOptionFloat *>(print_cfg.option("prime_tower_width"))->value; + float w = dynamic_cast<const ConfigOptionFloat *>(full_config.option("prime_tower_width"))->value; float v = dynamic_cast<const ConfigOptionFloat *>(full_config.option("prime_volume"))->value; bool enable_wrapping = false; const ConfigOptionBool *wrapping_opt = dynamic_cast<const ConfigOptionBool *>(full_config.option("enable_wrapping_detection")); if (wrapping_opt) enable_wrapping = wrapping_opt->value; int nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); - Vec3d wipe_tower_size = part_plate->estimate_wipe_tower_size(print_cfg, w, v, nozzle_nums, init_pos ? 2 : 0, false, enable_wrapping); + Vec3d wipe_tower_size = part_plate->estimate_wipe_tower_size(full_config, w, v, nozzle_nums, init_pos ? 2 : 0, false, enable_wrapping); if (!init_pos && (is_approx(wipe_tower_size(0), 0.0) || is_approx(wipe_tower_size(1), 0.0))) { - wipe_tower_size = part_plate->estimate_wipe_tower_size(print_cfg, w, v, nozzle_nums, 2, false, enable_wrapping); + wipe_tower_size = part_plate->estimate_wipe_tower_size(full_config, w, v, nozzle_nums, 2, false, enable_wrapping); } // Compute brim-aware margin: brim extends outward from tower position float brim_width = 0.f; - const ConfigOptionFloat *brim_opt = print_cfg.option<ConfigOptionFloat>("prime_tower_brim_width"); + const ConfigOptionFloat *brim_opt = full_config.option<ConfigOptionFloat>("prime_tower_brim_width"); if (brim_opt) { brim_width = brim_opt->value; if (brim_width < 0) brim_width = WipeTower::get_auto_brim_by_height((float) wipe_tower_size.z()); @@ -4412,6 +4524,18 @@ void PartPlateList::set_default_wipe_tower_pos_for_plate(int plate_idx, bool ini } } + // The bounding box above still allows a corner a delta or hexagonal bed does not have, and the + // prime tower is validated against the real outline — pull it onto the bed before storing. + { + Polygons bed{part_plate->get_shared_printable_polygon()}; + bed.front().translate(Point(-scaled(plate_origin.x()), -scaled(plate_origin.y()))); // into the frame x/y live in + const BoundingBox tower(Point::new_scale(x, y), + Point::new_scale(x + wipe_tower_size(0), y + wipe_tower_size(1))); + const Vec2f move = WipeTower::move_box_inside_polygon(tower, bed, scaled<coord_t>(margin)); + x += move.x(); + y += move.y(); + } + ConfigOptionFloat wt_x_opt(x); ConfigOptionFloat wt_y_opt(y); dynamic_cast<ConfigOptionFloats *>(proj_cfg.option("wipe_tower_x"))->set_at(&wt_x_opt, plate_idx, 0); @@ -4419,10 +4543,8 @@ void PartPlateList::set_default_wipe_tower_pos_for_plate(int plate_idx, bool ini } //this may be happened after machine changed -void PartPlateList::reset_size(int width, int depth, int height, bool reload_objects, bool update_shapes) +void PartPlateList::reset_size(int width, int depth, double height, bool reload_objects, bool update_shapes) { - Vec3d origin1, origin2; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":before size: plate_width %1%, plate_depth %2%, plate_height %3%") % m_plate_width % m_plate_depth % m_plate_height; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":after size: plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height; if ((m_plate_width != width) || (m_plate_depth != depth) || (m_plate_height != height)) @@ -6347,7 +6469,7 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w plate_data_item->filament_change_sequence = m_plate_list[i]->m_gcode_result->filament_change_sequence; plate_data_item->nozzle_change_sequence = m_plate_list[i]->m_gcode_result->nozzle_change_sequence; plate_data_item->optimal_assignment = m_plate_list[i]->m_gcode_result->optimal_assignment; - plate_data_item->first_layer_time = std::to_string(m_plate_list[i]->cali_bboxes_data.first_layer_time); + plate_data_item->first_layer_time = std::to_string(m_plate_list[i]->get_slice_result()->initial_layer_time); Print *print = nullptr; m_plate_list[i]->get_print((PrintBase **) &print, nullptr, nullptr); if (print) { @@ -6362,6 +6484,31 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w } //parse filament info plate_data_item->parse_filament_info(m_plate_list[i]->get_slice_result()); + + // Record mixed (virtual) filaments actually used on this plate. + // Source is ToolOrdering::used_mixed_filaments (slots that appeared in + // layer tools before resolve), persisted on GCodeProcessorResult / Print — + // not print->extruders() which only reflects assignment. + { + std::vector<unsigned int> used_mixed; + if (auto *slice_result = m_plate_list[i]->get_slice_result()) + used_mixed = slice_result->used_mixed_filaments; + if (used_mixed.empty() && print) + used_mixed = print->get_slice_used_mixed_filaments(); + if (!used_mixed.empty() && print) { + const auto &fila_types = print->config().filament_type.values; + const auto &fila_colors = print->config().filament_colour.values; + const auto &fila_comps = print->config().filament_mixed_components.values; + for (unsigned int fid : used_mixed) { + PlateMixedFilamentInfo mixed_info; + mixed_info.id = (int) fid + 1; + if (fid < fila_types.size()) mixed_info.type = fila_types[fid]; + if (fid < fila_colors.size()) mixed_info.color = fila_colors[fid]; + if (fid < fila_comps.size()) mixed_info.components = fila_comps[fid]; + plate_data_item->mixed_filaments_info.push_back(mixed_info); + } + } + } } else { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "slice result = " << m_plate_list[i]->get_slice_result() << ", result valid = " << m_plate_list[i]->is_slice_result_valid(); @@ -6428,6 +6575,13 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f m_plate_list[index]->slice_filaments_info = plate_data_list[i]->slice_filaments_info; gcode_result->warnings = plate_data_list[i]->warnings; gcode_result->filament_maps = plate_data_list[i]->filament_maps; + gcode_result->used_mixed_filaments.clear(); + for (const auto &mixed_info : plate_data_list[i]->mixed_filaments_info) { + if (mixed_info.id > 0) + gcode_result->used_mixed_filaments.push_back(static_cast<unsigned int>(mixed_info.id - 1)); + } + if (Print *print = dynamic_cast<Print*>(fff_print)) + print->set_slice_used_mixed_filaments(gcode_result->used_mixed_filaments); // Reconstruct the device-side nozzle grouping from the loaded 3mf so // the monitor/preview can map filaments to physical nozzles. @@ -6544,7 +6698,7 @@ int PartPlateList::load_gcode_files() //BoundingBoxf3 print_volume = m_plate_list[i]->get_bounding_box(false); //print_volume.max(2) = this->m_plate_height; //print_volume.min(2) = -1e10; - m_model->update_print_volume_state({m_plate_list[i]->get_shape(), (double)this->m_plate_height, m_plate_list[i]->get_extruder_areas(), m_plate_list[i]->get_extruder_heights() }); + m_model->update_print_volume_state({m_plate_list[i]->get_shape(), this->m_plate_height, m_plate_list[i]->get_extruder_areas(), m_plate_list[i]->get_extruder_heights() }); if (!m_plate_list[i]->load_gcode_from_file(m_plate_list[i]->m_gcode_path_from_3mf)) ret ++; diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 8f4055706f..8ad2f4a7d1 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -96,7 +96,7 @@ private: Vec3d m_origin; int m_width; int m_depth; - int m_height; + double m_height; float m_height_to_lid; float m_height_to_rod; bool m_printable; @@ -227,7 +227,7 @@ public: static void load_render_colors(); PartPlate(); - PartPlate(PartPlateList *partplate_list, Vec3d origin, int width, int depth, int height, Plater* platerObj, Model* modelObj, bool printable=true, PrinterTechnology tech = ptFFF); + PartPlate(PartPlateList *partplate_list, Vec3d origin, int width, int depth, double height, Plater* platerObj, Model* modelObj, bool printable=true, PrinterTechnology tech = ptFFF); ~PartPlate(); bool operator<(PartPlate&) const; @@ -328,7 +328,7 @@ public: Vec3d get_center_origin(); /* size and position related functions*/ //set position and size - void set_pos_and_size(Vec3d& origin, int width, int depth, int height, bool with_instance_move, bool do_clear = true); + void set_pos_and_size(Vec3d& origin, int width, int depth, double height, bool with_instance_move, bool do_clear = true); // BBS Vec2d get_size() const { return Vec2d(m_width, m_depth); } @@ -354,6 +354,9 @@ public: bool check_filament_printable(const DynamicPrintConfig & config, wxString& error_message); bool check_tpu_printable_status(const DynamicPrintConfig & config, const std::vector<int> &tpu_filaments); bool check_mixture_of_pla_and_petg(const DynamicPrintConfig & config); + // Warns when a mixed-color filament is used on a single-nozzle printer, where every + // component switch costs a full filament change and purge. + bool check_single_extruder_mixed_filament_risk(const DynamicPrintConfig &config, std::string &warning_text) const; bool check_mixture_filament_compatible(const DynamicPrintConfig& config, std::string &error_msg); bool check_compatible_of_nozzle_and_filament(const DynamicPrintConfig & config, const std::vector<std::string>& filament_presets, std::string& error_msg); @@ -425,6 +428,9 @@ public: const BoundingBox get_bounding_box_crd(); BoundingBoxf3 get_plate_box() {return get_build_volume();} BoundingBoxf3 get_build_volume(bool use_share = false); + // Polygon counterpart of get_build_volume(true), in scaled world coordinates. The bounding box + // that one returns hides the corners a non-rectangular bed does not have. + Polygon get_shared_printable_polygon() const; const std::vector<BoundingBoxf3>& get_exclude_areas() { return m_exclude_bounding_box; } @@ -584,7 +590,7 @@ class PartPlateList : public ObjectBase int m_plate_width; int m_plate_depth; - int m_plate_height; + double m_plate_height; float m_height_to_lid; float m_height_to_rod; @@ -669,16 +675,6 @@ public: offset = Vec2d(0, 0); } - TexturePart(const TexturePart& part) { - this->x = part.x; - this->y = part.y; - this->w = part.w; - this->h = part.h; - this->offset = part.offset; - this->buffer = part.buffer; - this->filename = part.filename; - this->texture = part.texture; - } void update_pos(float xx, float yy, float ww, float hh) { x = xx; y = yy; @@ -702,12 +698,12 @@ public: static bool is_load_cali_texture; static bool is_load_extruder_only_area_textures; - PartPlateList(int width, int depth, int height, Plater* platerObj, Model* modelObj, PrinterTechnology tech = ptFFF); + PartPlateList(int width, int depth, double height, Plater* platerObj, Model* modelObj, PrinterTechnology tech = ptFFF); PartPlateList(Plater* platerObj, Model* modelObj, PrinterTechnology tech = ptFFF); ~PartPlateList(); //this may be happened after machine changed - void reset_size(int width, int depth, int height, bool reload_objects = true, bool update_shapes = false); + void reset_size(int width, int depth, double height, bool reload_objects = true, bool update_shapes = false); //clear all the instances in the plate, but keep the plates void clear(bool delete_plates = false, bool release_print_list = false, bool except_locked = false, int plate_index = -1); //clear all the instances in the plate, and delete the plates, only keep the first default plate @@ -721,7 +717,7 @@ public: //get the plate stride double plate_stride_x(); double plate_stride_y(); - void get_plate_size(int& width, int& depth, int& height) { + void get_plate_size(int& width, int& depth, double& height) { width = m_plate_width; depth = m_plate_depth; height = m_plate_height; diff --git a/src/slic3r/GUI/PartSkipDialog.hpp b/src/slic3r/GUI/PartSkipDialog.hpp index 95eba9f24d..b95681dcde 100644 --- a/src/slic3r/GUI/PartSkipDialog.hpp +++ b/src/slic3r/GUI/PartSkipDialog.hpp @@ -128,7 +128,6 @@ private: bool is_drag_mode(); boost::shared_ptr<PrinterFileSystem> m_file_sys; - bool m_file_sys_result{false}; std::string m_timestamp; std::string m_tmp_path; std::vector<string> m_local_paths; diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index b40cd22697..989cf204e1 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -25,7 +25,6 @@ #include "GUI.hpp" #include "GUI_App.hpp" #include "MainFrame.hpp" -#include "slic3r/Utils/NetworkAgentFactory.hpp" #include "format.hpp" #include "Tab.hpp" #include "wxExtensions.hpp" @@ -128,22 +127,8 @@ PhysicalPrinterDialog::~PhysicalPrinterDialog() void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgroup) { m_optgroup->m_on_change = [this](t_config_option_key opt_key, boost::any value) { - // Special handling for printer_agent: convert fake enum index to string agent ID - if (opt_key == "printer_agent") { - try { - int selected_idx = boost::any_cast<int>(value); - auto agents = NetworkAgentFactory::get_registered_printer_agents(); - if (selected_idx >= 0 && selected_idx < static_cast<int>(agents.size())) { - m_config->set_key_value("printer_agent", - new ConfigOptionString(agents[selected_idx].id)); - } - } catch (const boost::bad_any_cast&) { - // If value is not an int, ignore - } + if (opt_key == "host_type" || opt_key == "printhost_authorization_type") this->update(); - } else if (opt_key == "host_type" || opt_key == "printhost_authorization_type") { - this->update(); - } if (opt_key == "print_host") this->update_printhost_buttons(); if (opt_key == "printhost_port") @@ -154,47 +139,6 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr m_optgroup->append_single_option_line("host_type"); - // Build printer agent dropdown from registry (only if network agent is available) - if (wxGetApp().getAgent() != nullptr) { - auto agents = NetworkAgentFactory::get_registered_printer_agents(); - - if (!agents.empty()) { - // Create a fake enum option to force a Choice widget instead of TextCtrl - // (printer_agent is coString in config, but we need a dropdown) - ConfigOptionDef def; - def.type = coEnum; - def.width = Field::def_width_wider(); - def.label = L("Printer Agent"); - def.tooltip = L("Select the network agent implementation for printer communication. " - "Available agents are registered at startup."); - def.mode = comAdvanced; - - // Populate enum values and labels from registered agents - for (const auto& agent : agents) { - def.enum_values.push_back(agent.id); - def.enum_labels.push_back(agent.display_name); - } - - // Resolve selected agent: use config value if valid, otherwise fall back to default - std::string selected_agent = m_config->opt_string("printer_agent"); - auto it = std::find_if(agents.begin(), agents.end(), [&selected_agent](const auto& a) { return a.id == selected_agent; }); - if (it == agents.end()) { - selected_agent = ORCA_PRINTER_AGENT_ID; - it = std::find_if(agents.begin(), agents.end(), [&selected_agent](const auto& a) { return a.id == selected_agent; }); - } - - if (it != agents.end()) { - size_t default_idx = std::distance(agents.begin(), it); - def.set_default_value(new ConfigOptionInt(static_cast<int>(default_idx))); - } - - // Create and append the option line - auto agent_option = Option(def, "printer_agent"); - Line agent_line = m_optgroup->create_single_option_line(agent_option); - m_optgroup->append_line(agent_line); - } - } - auto create_sizer_with_btn = [](wxWindow* parent, Button** btn, const std::string& icon_name, const wxString& label) { *btn = new Button(parent, label); (*btn)->SetStyle(ButtonStyle::Regular, ButtonType::Parameter); @@ -725,7 +669,7 @@ void PhysicalPrinterDialog::update(bool printer_change) } // For bbl printers, show option to control the device tab - if (wxGetApp().preset_bundle->is_bbl_vendor()) { + if (wxGetApp().preset_bundle->is_bbl_vendor() || wxGetApp().app_config->get_bool("use_printer_agents")) { m_optgroup->show_field("bbl_use_print_host_webui"); const bool use_print_host_webui = !current_webui.empty(); if (Field* printhost_webui_field = m_optgroup->get_field("bbl_use_print_host_webui"); printhost_webui_field) { @@ -816,31 +760,6 @@ void PhysicalPrinterDialog::update_host_type(bool printer_change) } } -void PhysicalPrinterDialog::update_printer_agent_type() -{ - if (m_config == nullptr) - return; - - Field* agent_field = m_optgroup->get_field("printer_agent"); - if (!agent_field) - return; - - Choice* agent_choice = dynamic_cast<Choice*>(agent_field); - if (!agent_choice) - return; - - // Sync selection with current config value - const std::string current_agent = m_config->opt_string("printer_agent"); - - auto agents = NetworkAgentFactory::get_registered_printer_agents(); - for (size_t i = 0; i < agents.size(); ++i) { - if (agents[i].id == current_agent) { - agent_choice->set_value(i); - return; - } - } -} - void PhysicalPrinterDialog::update_printers() { wxBusyCursor wait; @@ -894,11 +813,6 @@ void PhysicalPrinterDialog::OnOK(wxEvent& event) { wxGetApp().get_tab(Preset::TYPE_PRINTER)->save_preset("", false, false, true, m_preset_name); event.Skip(); - - // Defer printer agent switch to ensure preset save completes first - wxGetApp().CallAfter([] { - wxGetApp().switch_printer_agent(); - }); } }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.hpp b/src/slic3r/GUI/PhysicalPrinterDialog.hpp index 694e7aaf90..c32da334be 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.hpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.hpp @@ -33,7 +33,6 @@ class PhysicalPrinterDialog : public DPIDialog Button* m_printhost_test_btn {nullptr}; Button* m_printhost_logout_btn {nullptr}; Button* m_printhost_cafile_browse_btn {nullptr}; - Button* m_printhost_client_cert_browse_btn {nullptr}; Button* m_printhost_port_browse_btn {nullptr}; RoundedRectangle* m_input_area {nullptr}; @@ -60,7 +59,6 @@ public: void update(bool printer_change = false); void update_host_type(bool printer_change); - void update_printer_agent_type(); void update_preset_input(); void update_printhost_buttons(); void update_printers(); diff --git a/src/slic3r/GUI/PlateSettingsDialog.cpp b/src/slic3r/GUI/PlateSettingsDialog.cpp index ea24d646c3..07c955ef01 100644 --- a/src/slic3r/GUI/PlateSettingsDialog.cpp +++ b/src/slic3r/GUI/PlateSettingsDialog.cpp @@ -472,6 +472,31 @@ PlateSettingsDialog::PlateSettingsDialog(wxWindow* parent, const wxString& title m_sizer_main->AddSpacer(FromDIP(5)); m_sizer_main->Add(m_other_layers_seq_panel, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(30)); + // A mixed-color slot resolves to a different physical filament per layer, so a user-defined + // filament order cannot be honoured; grey out the choice and explain that in the dialog. + { + auto &proj_cfg = wxGetApp().preset_bundle->project_config; + auto *is_mixed_opt = proj_cfg.option<ConfigOptionBools>("filament_is_mixed"); + if (is_mixed_opt && Slic3r::has_any_mixed_filament(is_mixed_opt->values)) { + m_first_layer_print_seq_choice->Enable(false); + m_other_layers_seq_panel->enable_seq_choice(false); + + auto *warn_sizer = new wxBoxSizer(wxHORIZONTAL); + auto *warn_icon = new wxStaticBitmap(this, wxID_ANY, create_scaled_bitmap("warning", this, 16), + wxDefaultPosition, wxSize(FromDIP(16), FromDIP(16))); + auto *warn_text = new wxStaticText(this, wxID_ANY, + _L("The filament list contains mixed filaments. Custom filament sequence will not take effect.")); + warn_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#FF6F00"))); + warn_text->SetFont(Label::Body_12); + warn_text->Wrap(FromDIP(300)); + + warn_sizer->Add(warn_icon, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5)); + warn_sizer->Add(warn_text, 1, wxALIGN_CENTER_VERTICAL, 0); + m_sizer_main->AddSpacer(FromDIP(5)); + m_sizer_main->Add(warn_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(30)); + } + } + auto dlg_btns = new DialogButtons(this, {"OK", "Cancel"}); dlg_btns->GetOK()->Bind(wxEVT_BUTTON, [this](auto& e) { diff --git a/src/slic3r/GUI/PlateSettingsDialog.hpp b/src/slic3r/GUI/PlateSettingsDialog.hpp index 1e61b0a708..b94739348f 100644 --- a/src/slic3r/GUI/PlateSettingsDialog.hpp +++ b/src/slic3r/GUI/PlateSettingsDialog.hpp @@ -62,6 +62,9 @@ public: int get_layers_print_seq_choice() { return m_other_layer_print_seq_choice->GetSelection(); }; std::vector<LayerSeqInfo> get_layers_print_seq_infos() { return m_layer_seq_infos; } + // Lets callers grey out the sequence choice (e.g. when a mixed filament makes a + // user-defined filament order impossible). + void enable_seq_choice(bool enable) { m_other_layer_print_seq_choice->Enable(enable); } protected: void append_layer(const LayerSeqInfo* layer_info = nullptr); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 106c142fea..7f1e6ab400 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -166,6 +166,12 @@ #include <wx/glcanvas.h> // Needs to be last because reasons :-/ #include <libslic3r/miniz_extension.hpp> #include "WipeTowerDialog.hpp" +#include "MixedFilamentDialog.hpp" +#include "TextureImportDialog.hpp" +#include "libslic3r/TexturePainting.hpp" +#include "ColorDecomposeSupport.hpp" +#include "FilamentBitmapUtils.hpp" +#include "libslic3r/FilamentMixer.hpp" #include "ObjColorDialog.hpp" #include "libslic3r/CustomGCode.hpp" @@ -202,6 +208,20 @@ static const std::pair<unsigned int, unsigned int> THUMBNAIL_SIZE_3MF = { 512, 5 namespace Slic3r { namespace GUI { +// A textured mesh is only worth routing through the import dialog when it actually carries +// decoded image data; UV-only meshes have nothing to sample. +static bool has_importable_texture(const Slic3r::TexturedMesh& textured_mesh) +{ + if (textured_mesh.vertices.empty() || textured_mesh.indices.empty()) + return false; + + if (!textured_mesh.precomputed_face_colors.empty()) + return true; + + return std::any_of(textured_mesh.textures.begin(), textured_mesh.textures.end(), + [](const Slic3r::TextureImage& texture) { return !texture.data.empty(); }); +} + wxDEFINE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); wxDEFINE_EVENT(EVT_SLICING_UPDATE, SlicingStatusEvent); wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent); @@ -717,7 +737,26 @@ struct Sidebar::priv ScalableButton * m_bpButton_ams_filament; ScalableButton * m_bpButton_set_filament; int m_menu_filament_id = -1; + + wxPanel* m_filament_area_wrapper; + wxScrolledWindow* m_panel_filament_content; + + // Mixed-color filament section. Sits directly under the physical filament list in + // scrolled_sizer. BBS hosts the equivalent widgets inside an m_filament_area_wrapper + // that Orca's sidebar has no counterpart for, so these are parented to p->scrolled. + wxPanel* m_btn_add_mixed_filament{nullptr}; // "+ Add Mixed Filament" full-width button + wxPanel* m_panel_mixed_title{nullptr}; // title row: "Mixed Filament" + add/del buttons + wxStaticText* m_text_mixed_title{nullptr}; + ScalableButton* m_btn_mixed_add{nullptr}; + ScalableButton* m_btn_mixed_del{nullptr}; + wxScrolledWindow* m_mixed_scroll_area{nullptr}; // independent scrollbar for mixed rows + wxPanel* m_panel_mixed_content{nullptr}; + wxBoxSizer* m_sizer_mixed_filaments{nullptr}; // two-column, mirrors sizer_filaments + wxPanel* m_panel_mixed_warning{nullptr}; // red bar for broken/mismatched mixes + wxStaticText* m_text_mixed_warning{nullptr}; + bool m_mixed_filament_broken{false}; + wxScrolledWindow* m_scrolledWindow_filament_content; wxStaticLine* m_staticline2; wxPanel* m_panel_project_title; @@ -1060,23 +1099,38 @@ std::vector<int> get_min_flush_volumes(const DynamicPrintConfig &full_config, si struct DynamicFilamentList : DynamicList { + // Orca: support and wipe-tower keys are consumed by the engine without per-layer mixed + // resolution (see ConfigManipulation::update_print_fff_config), so their dropdowns list + // physical slots only; the per-feature *_filament_id keys keep every slot. BBS uses one + // physical-only list for all of its keys. + explicit DynamicFilamentList(bool physical_only = false) : physical_only(physical_only) {} + bool physical_only; std::vector<std::pair<wxString, wxBitmap *>> items; + std::vector<int> slot_map{0}; // combo index -> 1-based filament slot; slot_map[0] = 0 is "Default" void apply_on(Choice *c) override { + if (!c) + return; if (items.empty()) update(true); auto cb = dynamic_cast<ComboBox *>(c->window); + if (!cb) + return; wxString old_selection = cb->GetStringSelection(); int old_index = cb->GetSelection(); + // slot_map is already rebuilt here: restoring through it keeps the index of every slot + // still listed and sends a vanished slot to the fallback below. + int old_slot = old_index >= 0 && old_index < int(slot_map.size()) ? slot_map[old_index] : -1; cb->Clear(); cb->Append(_L("Default")); for (auto i : items) { cb->Append(i.first, i.second ? *i.second : wxNullBitmap); } - if (old_index >= 0 && (unsigned int) old_index < cb->GetCount()) { - cb->SetSelection(old_index); + int restored = index_of(wxString::Format("%d", old_slot)); + if (restored > 0 || old_slot == 0) { + cb->SetSelection(restored); return; } @@ -1092,27 +1146,36 @@ struct DynamicFilamentList : DynamicList wxString get_value(int index) override { wxString str; - str << index; + str << (index >= 0 && index < int(slot_map.size()) ? slot_map[index] : 0); return str; } int index_of(wxString value) override { long n = 0; - return (value.ToLong(&n) && n <= items.size()) ? int(n) : -1; + if (!value.ToLong(&n)) + return -1; + for (int i = 0; i < int(slot_map.size()); ++i) + if (slot_map[i] == int(n)) + return i; + return 0; } void update(bool force = false) { items.clear(); + slot_map.assign(1, 0); if (!force && m_choices.empty()) return; auto icons = get_extruder_color_icons(true); auto presets = wxGetApp().preset_bundle->filament_presets; for (int i = 0; i < presets.size(); ++i) { + if (physical_only && wxGetApp().preset_bundle->is_mixed_filament(i)) + continue; wxString str; std::string type; wxGetApp().preset_bundle->filaments.find_preset(presets[i])->get_filament_type(type); str << type; items.push_back({str, i < icons.size() ? icons[i] : nullptr}); + slot_map.push_back(i + 1); } DynamicList::update(); } @@ -1133,7 +1196,8 @@ static bool has_junction_deviation(const DynamicPrintConfig* printer_config) junction_dev->values.front() > 0.0; } -static DynamicFilamentList dynamic_filament_list; +static DynamicFilamentList dynamic_filament_list; // every slot, mixed included (per-feature *_filament_id keys) +static DynamicFilamentList dynamic_physical_filament_list(true); // physical slots only (support_*, wipe_tower_filament) class AMSCountPopupWindow : public PopupWindow { @@ -1276,7 +1340,7 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title if (index >= 0) label_flow->SetMinSize({FromDIP(80), -1}); auto combo_flow = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); combo_flow->GetDropDown().SetUseContentWidth(true); - combo_flow->Bind(wxEVT_COMBOBOX, [this, index, combo_flow](wxCommandEvent &evt) { + combo_flow->Bind(wxEVT_COMBOBOX, [index, combo_flow](wxCommandEvent &evt) { auto printer_tab = dynamic_cast<TabPrinter *>(wxGetApp().get_tab(Preset::TYPE_PRINTER)); NozzleVolumeType volume_type = NozzleVolumeType(intptr_t(combo_flow->GetClientData(evt.GetInt()))); printer_tab->set_extruder_volume_type(index, volume_type); @@ -1336,7 +1400,7 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title btn_up = new ScalableButton(this, wxID_ANY, "page_up", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 14); btn_up->SetBackgroundColour(*wxWHITE); - btn_up->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this, index](auto &evt) { + btn_up->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) { if (page_cur > 0) --page_cur; update_ams(); @@ -1344,7 +1408,7 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title btn_up->Hide(); btn_down = new ScalableButton(this, wxID_ANY, "page_down", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 14); btn_down->SetBackgroundColour(*wxWHITE); - btn_down->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this, index](auto &evt) { + btn_down->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) { if (page_cur + 1 < page_num) ++page_cur; update_ams(); @@ -1403,12 +1467,12 @@ void ExtruderGroup::update_ams() size_t left = 4; size_t index = 0; for (size_t i = i4; i < ams_n4 && left > 0; ++i, ++index, left -= 2) { - ams[index]->Update(i < ams_4.size() ? ams_4[i] : info4); + ams[index]->UpdateInfo(i < ams_4.size() ? ams_4[i] : info4); ams[index]->Refresh(); ams[index]->Open(); } for (size_t i = i1; i < ams_n1 && left > 0; ++i, ++index, --left) { - ams[index]->Update(i < ams_1.size() ? ams_1[i] : info1); + ams[index]->UpdateInfo(i < ams_1.size() ? ams_1[i] : info1); ams[index]->Refresh(); ams[index]->Open(); } @@ -1961,7 +2025,7 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man if (!this->plater) return false; - this->plater->update_objects_position_when_select_preset([&obj, machine_preset]() { + this->plater->update_objects_position_when_select_preset([machine_preset]() { Tab *printer_tab = GUI::wxGetApp().get_tab(Preset::Type::TYPE_PRINTER); printer_tab->select_preset(machine_preset->name); }); @@ -2106,7 +2170,7 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man void Sidebar::priv::update_sync_status(const MachineObject *obj) { StateColor not_synced_colour(std::pair<wxColour, int>(wxColour("#009688"), StateColor::Normal)); - auto clear_all_sync_status = [this, ¬_synced_colour]() { + auto clear_all_sync_status = [this]() { panel_printer_preset->ShowBadge(false); panel_printer_bed->ShowBadge(false); panel_nozzle_dia->ShowBadge(false); // ORCA add support for nozzle sync @@ -2355,15 +2419,15 @@ void Sidebar::update_sync_ams_btn_enable(wxUpdateUIEvent &e) Sidebar::Sidebar(Plater *parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(39 * wxGetApp().em_unit(), -1)), p(new priv(parent)) { - Choice::register_dynamic_list("support_filament", &dynamic_filament_list); - Choice::register_dynamic_list("support_interface_filament", &dynamic_filament_list); + Choice::register_dynamic_list("support_filament", &dynamic_physical_filament_list); + Choice::register_dynamic_list("support_interface_filament", &dynamic_physical_filament_list); Choice::register_dynamic_list("outer_wall_filament_id", &dynamic_filament_list); Choice::register_dynamic_list("inner_wall_filament_id", &dynamic_filament_list); Choice::register_dynamic_list("sparse_infill_filament_id", &dynamic_filament_list); Choice::register_dynamic_list("internal_solid_filament_id", &dynamic_filament_list); Choice::register_dynamic_list("top_surface_filament_id", &dynamic_filament_list); Choice::register_dynamic_list("bottom_surface_filament_id", &dynamic_filament_list); - Choice::register_dynamic_list("wipe_tower_filament", &dynamic_filament_list); + Choice::register_dynamic_list("wipe_tower_filament", &dynamic_physical_filament_list); p->scrolled = new wxPanel(this); // p->scrolled->SetScrollbars(0, 100, 1, 2); // ys_DELETE_after_testing. pixelsPerUnitY = 100 @@ -2410,7 +2474,7 @@ Sidebar::Sidebar(Plater *parent) p->m_printer_icon = new ScalableButton(p->m_panel_printer_title, wxID_ANY, "printer"); p->m_text_printer_settings = new Label(p->m_panel_printer_title, _L("Printer"), LB_PROPAGATE_MOUSE_EVENT | wxST_ELLIPSIZE_END); - p->m_printer_icon->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { + p->m_printer_icon->Bind(wxEVT_BUTTON, [](wxCommandEvent& e) { //auto wizard_t = new ConfigWizard(wxGetApp().mainframe); //wizard_t->run(ConfigWizard::RR_USER, ConfigWizard::SP_CUSTOM); }); @@ -2431,7 +2495,7 @@ Sidebar::Sidebar(Plater *parent) }); p->m_printer_setting = new ScalableButton(p->m_panel_printer_title, wxID_ANY, "settings"); - p->m_printer_setting->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { + p->m_printer_setting->Bind(wxEVT_BUTTON, [](wxCommandEvent &e) { // p->editing_filament = -1; // wxGetApp().params_dialog()->Popup(); // wxGetApp().get_tab(Preset::TYPE_FILAMENT)->restore_last_select_item(); @@ -2546,8 +2610,8 @@ Sidebar::Sidebar(Plater *parent) p->image_printer->SetBackgroundColour(bg_color); p->combo_printer->SetBackgroundColour(bg_color); // paints margins instead combo background }; - p->combo_printer->Bind(wxEVT_SET_FOCUS, [this, printer_focus_bg](auto& e) {printer_focus_bg(true ); e.Skip();}); - p->combo_printer->Bind(wxEVT_KILL_FOCUS, [this, printer_focus_bg](auto& e) {printer_focus_bg(false); e.Skip();}); + p->combo_printer->Bind(wxEVT_SET_FOCUS, [printer_focus_bg](auto& e) {printer_focus_bg(true ); e.Skip();}); + p->combo_printer->Bind(wxEVT_KILL_FOCUS, [printer_focus_bg](auto& e) {printer_focus_bg(false); e.Skip();}); /* ORCA This part moved to titlebar p->btn_connect_printer = new ScalableButton(p->panel_printer_preset, wxID_ANY, "monitor_signal_strong"); @@ -2624,8 +2688,8 @@ Sidebar::Sidebar(Plater *parent) p->label_nozzle_type->SetBackgroundColour(bg_color); p->combo_nozzle_dia->SetBackgroundColour(bg_color); // paints margins instead combo background }; - p->combo_nozzle_dia->Bind(wxEVT_SET_FOCUS, [this, nozzle_focus_bg](auto& e) {nozzle_focus_bg(true ); e.Skip();}); - p->combo_nozzle_dia->Bind(wxEVT_KILL_FOCUS, [this, nozzle_focus_bg](auto& e) {nozzle_focus_bg(false); e.Skip();}); + p->combo_nozzle_dia->Bind(wxEVT_SET_FOCUS, [nozzle_focus_bg](auto& e) {nozzle_focus_bg(true ); e.Skip();}); + p->combo_nozzle_dia->Bind(wxEVT_KILL_FOCUS, [nozzle_focus_bg](auto& e) {nozzle_focus_bg(false); e.Skip();}); p->label_nozzle_type = new Label(p->panel_nozzle_dia, "Brass", LB_PROPAGATE_MOUSE_EVENT | wxST_ELLIPSIZE_END | wxALIGN_CENTRE_HORIZONTAL); p->label_nozzle_type->SetFont(Label::Body_10); @@ -2699,8 +2763,8 @@ Sidebar::Sidebar(Plater *parent) p->image_printer_bed->SetBackgroundColour(bg_color); p->combo_printer_bed->SetBackgroundColour(bg_color); // paints margins instead combo background }; - p->combo_printer_bed->Bind(wxEVT_SET_FOCUS, [this, bed_focus_bg](auto& e) {bed_focus_bg(true ); e.Skip();}); - p->combo_printer_bed->Bind(wxEVT_KILL_FOCUS, [this, bed_focus_bg](auto& e) {bed_focus_bg(false); e.Skip();}); + p->combo_printer_bed->Bind(wxEVT_SET_FOCUS, [bed_focus_bg](auto& e) {bed_focus_bg(true ); e.Skip();}); + p->combo_printer_bed->Bind(wxEVT_KILL_FOCUS, [bed_focus_bg](auto& e) {bed_focus_bg(false); e.Skip();}); // highlight border on hover auto printer_bed_hovered = std::make_shared<std::unordered_set<wxWindow*>>(); @@ -2835,7 +2899,7 @@ Sidebar::Sidebar(Plater *parent) p->m_panel_filament_title->SetBackgroundColor(title_bg); p->m_panel_filament_title->SetBackgroundColor2(0xF1F1F1); p->m_panel_filament_title->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent &e) { - if (!p || !p->m_panel_filament_content || !m_scrolled_sizer || !p->m_bpButton_set_filament || !p->m_purge_mode_btn || !p->m_flushing_volume_btn || !p->m_bpButton_add_filament || !ams_btn) + if (!p || !p->m_filament_area_wrapper || !m_scrolled_sizer || !p->m_bpButton_set_filament || !p->m_purge_mode_btn || !p->m_flushing_volume_btn || !p->m_bpButton_add_filament || !ams_btn) return; // ORCA exclude area of del button from titlebar collapse/expand feature to fix undesired collapse when user spams del filament button // also block fold/unfold feature when user clicks to spacing between icons @@ -2846,8 +2910,8 @@ Sidebar::Sidebar(Plater *parent) else if (ams_btn->IsShown()) exclude_pt = ams_btn->GetPosition().x; if (e.GetPosition().x > exclude_pt) return; - bool isShown = p->m_panel_filament_content->IsShown(); - p->m_panel_filament_content->Show(!isShown); + bool isShown = p->m_filament_area_wrapper->IsShown(); + p->m_filament_area_wrapper->Show(!isShown); p->m_panel_filament_separator->Show(isShown); m_scrolled_sizer->Layout(); @@ -2914,7 +2978,7 @@ Sidebar::Sidebar(Plater *parent) ScalableButton* add_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "add_filament"); add_btn->SetToolTip(_L("Add one filament")); - add_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent& e){ + add_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e){ add_filament(); update_filaments_counter(); }); @@ -2924,7 +2988,7 @@ Sidebar::Sidebar(Plater *parent) ScalableButton* del_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "delete_filament"); del_btn->SetToolTip(_L("Remove last filament")); - del_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent &e) { + del_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { delete_filament(); update_filaments_counter(); }); @@ -2938,7 +3002,7 @@ Sidebar::Sidebar(Plater *parent) ams_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "ams_fila_sync", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 16); // ORCA match icon size with other icons as 16x16 ams_btn->SetToolTip(_L("Synchronize filament list from AMS")); - ams_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent &e) { + ams_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { sync_ams_list(); }); @@ -2961,8 +3025,13 @@ Sidebar::Sidebar(Plater *parent) bSizer39->Add(set_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::WideSpacing())); bSizer39->AddSpacer(FromDIP(SidebarProps::TitlebarMargin())); + // ---- Wrapper panel for collapse/expand of all filament content ---- + p->m_filament_area_wrapper = new wxPanel(p->scrolled, wxID_ANY); + p->m_filament_area_wrapper->SetBackgroundColour(*wxWHITE); + auto* wrapper_sizer = new wxBoxSizer(wxVERTICAL); + // add filament content - p->m_panel_filament_content = new wxScrolledWindow( p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + p->m_panel_filament_content = new wxScrolledWindow(p->m_filament_area_wrapper, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); p->m_panel_filament_content->SetScrollbars(0, 100, 1, 2); p->m_panel_filament_content->SetScrollRate(0, 5); //p->m_panel_filament_content->SetMaxSize(wxSize{-1, FromDIP(174)}); @@ -2990,7 +3059,129 @@ Sidebar::Sidebar(Plater *parent) update_filaments_area_height(); // ORCA - scrolled_sizer->Add(p->m_panel_filament_content, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(SidebarProps::ContentMarginV())); // ORCA use vertical margin on parent otherwise it shows scrollbar even on 1 filament + wrapper_sizer->Add(p->m_panel_filament_content, 0, wxEXPAND); + + // ---- Mixed-color filament section ---- + // A mixed filament is a virtual slot realized from 2-3 physical filaments at slicing time. + // Everything here stays hidden until at least two physical filaments exist, so a single + // filament setup looks exactly as before. + { + // 1) "+ Add Mixed Filament" button, shown only while no mixed filament exists yet. + p->m_btn_add_mixed_filament = new wxPanel(p->m_filament_area_wrapper, wxID_ANY); + p->m_btn_add_mixed_filament->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F8F8F8"))); + p->m_btn_add_mixed_filament->SetMinSize(wxSize(-1, FromDIP(23))); + { + auto* btn_sizer = new wxBoxSizer(wxHORIZONTAL); + auto* icon_add = new ScalableButton(p->m_btn_add_mixed_filament, wxID_ANY, "add_filament", wxEmptyString, + wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 16); + auto* add_label = new wxStaticText(p->m_btn_add_mixed_filament, wxID_ANY, _L("Add Mixed Filament"), + wxDefaultPosition, wxDefaultSize, 0); + add_label->SetFont(::Label::Body_13); + btn_sizer->AddStretchSpacer(); + btn_sizer->Add(icon_add, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + btn_sizer->Add(add_label, 0, wxALIGN_CENTER_VERTICAL); + btn_sizer->AddStretchSpacer(); + p->m_btn_add_mixed_filament->SetSizer(btn_sizer); + p->m_btn_add_mixed_filament->SetCursor(wxCursor(wxCURSOR_HAND)); + // Whole panel is the hit target, so forward clicks from the children too. + auto on_click = [this](wxMouseEvent&) { add_mixed_filament(); }; + p->m_btn_add_mixed_filament->Bind(wxEVT_LEFT_UP, on_click); + add_label->Bind(wxEVT_LEFT_UP, on_click); + icon_add->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { add_mixed_filament(); }); + } + wrapper_sizer->Add(p->m_btn_add_mixed_filament, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, FromDIP(8)); + + // 2) Title row with add / remove buttons, shown once a mixed filament exists. + p->m_panel_mixed_title = new wxPanel(p->m_filament_area_wrapper, wxID_ANY); + p->m_panel_mixed_title->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + { + auto* title_sizer = new wxBoxSizer(wxHORIZONTAL); + p->m_text_mixed_title = new wxStaticText(p->m_panel_mixed_title, wxID_ANY, _L("Mixed Filament")); + p->m_text_mixed_title->SetFont(::Label::Head_14); + title_sizer->Add(p->m_text_mixed_title, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); + title_sizer->AddStretchSpacer(); + + p->m_btn_mixed_del = new ScalableButton(p->m_panel_mixed_title, wxID_ANY, "delete_filament"); + p->m_btn_mixed_del->SetToolTip(_L("Remove last mixed filament")); + p->m_btn_mixed_del->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + auto* plater_ptr = dynamic_cast<Plater*>(GetParent()); + if (!plater_ptr) return; + auto mixed_indices = plater_ptr->mixed_filament_config_indices(); + if (!mixed_indices.empty()) + delete_mixed_filament_at(mixed_indices.size() - 1); + }); + title_sizer->Add(p->m_btn_mixed_del, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::IconSpacing())); + + p->m_btn_mixed_add = new ScalableButton(p->m_panel_mixed_title, wxID_ANY, "add_filament"); + p->m_btn_mixed_add->SetToolTip(_L("Add mixed filament")); + p->m_btn_mixed_add->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { add_mixed_filament(); }); + title_sizer->Add(p->m_btn_mixed_add, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::IconSpacing())); + title_sizer->AddSpacer(FromDIP(SidebarProps::TitlebarMargin())); + + p->m_panel_mixed_title->SetSizer(title_sizer); + } + wrapper_sizer->Add(p->m_panel_mixed_title, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(8)); + + // 3) Mixed filament rows, in their own scroll area so a long mixed list does not + // push the physical filament list off screen. + p->m_mixed_scroll_area = new wxScrolledWindow(p->m_filament_area_wrapper, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); + p->m_mixed_scroll_area->SetScrollbars(0, 100, 1, 2); + p->m_mixed_scroll_area->SetScrollRate(0, 5); + p->m_mixed_scroll_area->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + { + auto* mix_scroll_sizer = new wxBoxSizer(wxVERTICAL); + p->m_panel_mixed_content = new wxPanel(p->m_mixed_scroll_area, wxID_ANY); + p->m_panel_mixed_content->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + + // Two columns, same idiom as sizer_filaments. + p->m_sizer_mixed_filaments = new wxBoxSizer(wxHORIZONTAL); + p->m_sizer_mixed_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); + p->m_sizer_mixed_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); + + auto* sizer_mixed2 = new wxBoxSizer(wxVERTICAL); + sizer_mixed2->Add(p->m_sizer_mixed_filaments, 0, wxEXPAND, 0); + p->m_panel_mixed_content->SetSizer(sizer_mixed2); + mix_scroll_sizer->Add(p->m_panel_mixed_content, 0, wxEXPAND | wxBOTTOM, FromDIP(8)); + p->m_mixed_scroll_area->SetSizer(mix_scroll_sizer); + } + p->m_mixed_scroll_area->EnableScrolling(false, true); + p->m_mixed_scroll_area->ShowScrollbars(wxSHOW_SB_NEVER, wxSHOW_SB_DEFAULT); + p->m_mixed_scroll_area->Bind(wxEVT_SIZE, [this](wxSizeEvent& e) { + int w = p->m_mixed_scroll_area->GetClientSize().GetWidth(); + if (w > 0) + p->m_mixed_scroll_area->SetVirtualSize(w, p->m_mixed_scroll_area->GetVirtualSize().GetHeight()); + e.Skip(); + }); + wrapper_sizer->Add(p->m_mixed_scroll_area, 0, wxEXPAND, 0); + + // 4) Warning bar for mixes whose components were deleted or whose types disagree. + p->m_panel_mixed_warning = new wxPanel(p->m_filament_area_wrapper, wxID_ANY); + p->m_panel_mixed_warning->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + { + auto* warn_sizer = new wxBoxSizer(wxHORIZONTAL); + p->m_text_mixed_warning = new wxStaticText(p->m_panel_mixed_warning, wxID_ANY, + _L("Mixed filament has invalid or mismatched components. Please re-edit affected entries.")); + p->m_text_mixed_warning->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#D01B1B"))); + p->m_text_mixed_warning->SetFont(::Label::Body_12); + p->m_text_mixed_warning->Wrap(FromDIP(360)); + warn_sizer->Add(p->m_text_mixed_warning, 1, wxALL, FromDIP(6)); + p->m_panel_mixed_warning->SetSizer(warn_sizer); + } + wrapper_sizer->Add(p->m_panel_mixed_warning, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(8)); + + // Hidden until update_mixed_filament_list() decides otherwise. + p->m_btn_add_mixed_filament->Hide(); + p->m_panel_mixed_title->Hide(); + p->m_mixed_scroll_area->Hide(); + p->m_panel_mixed_content->Hide(); + p->m_panel_mixed_warning->Hide(); + } + // ---- End mixed-color filament section ---- + + p->m_filament_area_wrapper->SetSizer(wrapper_sizer); + p->m_filament_area_wrapper->Layout(); + scrolled_sizer->Add(p->m_filament_area_wrapper, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(SidebarProps::ContentMarginV())); // ORCA use vertical margin on parent otherwise it shows scrollbar even on 1 filament + // ---- End filament area ---- } { @@ -3246,7 +3437,8 @@ void Sidebar::update_all_preset_comboboxes() auto p_mainframe = wxGetApp().mainframe; auto cfg = preset_bundle.printers.get_edited_preset().config; - const bool use_native_device_tab = preset_bundle.use_bbl_device_tab() || NetworkAgentFactory::is_current_printer_agent_plugin(); + const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); + const bool use_native_device_tab = preset_bundle.use_bbl_device_tab() || use_printer_agents; if (preset_bundle.use_bbl_network()) { //only show connection button for not-BBL printer @@ -3258,7 +3450,8 @@ void Sidebar::update_all_preset_comboboxes() p_mainframe->set_print_button_to_default(MainFrame::PrintSelectType::ePrintPlate); } else { //p->btn_connect_printer->Show(); - p->m_printer_connect->Show(); + // ORCA: hide the physical-printer connection button when printer agents are enabled + p->m_printer_connect->Show(!use_printer_agents); // ORCA: show/hide sync-ams button based on filament sync mode auto agent = wxGetApp().getAgent(); @@ -3280,10 +3473,14 @@ void Sidebar::update_all_preset_comboboxes() const auto host_type = cfg.option<ConfigOptionEnum<PrintHostType>>("host_type")->value; if (cfg.has("printhost_apikey") && (host_type != htSimplyPrint)) apikey = cfg.opt_string("printhost_apikey"); - print_btn_type = preset_bundle.is_bbl_vendor() ? MainFrame::PrintSelectType::ePrintPlate : MainFrame::PrintSelectType::eSendGcode; + print_btn_type = (preset_bundle.is_bbl_vendor() || wxGetApp().app_config->get_bool("use_printer_agents")) + ? MainFrame::PrintSelectType::ePrintPlate + : MainFrame::PrintSelectType::eSendGcode; } - if (!use_native_device_tab) + if (use_printer_agents) + p_mainframe->load_printer_url(); + else if (!use_native_device_tab) p_mainframe->load_printer_url(url, apikey); @@ -3439,7 +3636,10 @@ void Sidebar::update_presets(Preset::Type preset_type) bool isBBL = preset_bundle.is_bbl_vendor(); bool is_dual_extruder = extruder_variants->size() == 2; - p->layout_printer(preset_bundle.use_bbl_network(), isBBL && is_dual_extruder); + // why: agent mode drives the native device tab, so the sidebar lays out like BBL + // (no physical-printer connect button). + p->layout_printer(preset_bundle.use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents"), + isBBL && is_dual_extruder); // Update nozzle titles from printer config (e.g. "Main Nozzle" / "Auxiliary Nozzle" for N6) // UI left = DEPUTY_EXTRUDER_ID(1), UI right = MAIN_EXTRUDER_ID(0) @@ -3482,7 +3682,7 @@ void Sidebar::update_presets(Preset::Type preset_type) extruder.combo_flow->SetSelection(select); }; - auto update_extruder_diameter = [&diameters, &diameter, &nozzle_diameter](int extruder_index,ExtruderGroup & extruder) { + auto update_extruder_diameter = [&diameters, &nozzle_diameter](int extruder_index,ExtruderGroup & extruder) { extruder.combo_diameter->Clear(); int select = -1; // ORCA get the actual nozzle diameter from printer config @@ -3670,7 +3870,7 @@ bool Sidebar::reset_bed_type_combox_choices(bool is_sidebar_init) } } m_last_combo_bedtype_count = p->combo_printer_bed->GetCount(); - if (!is_sidebar_init && &p->plater->get_partplate_list()) { + if (!is_sidebar_init) { p->plater->get_partplate_list().check_all_plate_local_bed_type(m_cur_combox_bed_types); } return true; @@ -3687,6 +3887,1131 @@ void Sidebar::change_top_border_for_mode_sizer(bool increase_border) #endif } + +// ---- Mixed-color filament sidebar support ---- +// The mixed rows get their own scroll area, capped by Orca's filaments_area_preferred_count +// row budget rather than BBS's fixed 3-row / 12-filament limit. +void Sidebar::recalc_filament_scroll_sizes() +{ + if (!p->m_mixed_scroll_area || !p->m_mixed_scroll_area->GetSizer()) + return; + + // Same preferred-row budget the physical list uses, so both lists cap consistently. + auto left_sizer = p->sizer_filaments->GetItem((size_t) 0)->GetSizer(); + auto combo_sizer = left_sizer->GetItem((size_t) 0)->GetSizer(); + const int row_h = combo_sizer ? combo_sizer->GetSize().GetHeight() : 0; + int preferred_rows = std::ceil(0.5 * std::stoi(wxGetApp().app_config->get("filaments_area_preferred_count"))); + const int max_h = (row_h > 0) ? preferred_rows * row_h : -1; + + auto content_size = p->m_mixed_scroll_area->GetSizer()->GetMinSize(); + if (max_h > 0 && content_size.y > max_h) { + p->m_mixed_scroll_area->SetMaxSize({-1, max_h}); + content_size.y = max_h; + } else { + p->m_mixed_scroll_area->SetMaxSize({-1, -1}); + } + p->m_mixed_scroll_area->SetMinSize({0, content_size.y}); +} +static std::string blend_mixed_color(const std::vector<unsigned int> &comp_ids, + const std::vector<int> &ratios, + const std::vector<std::string> &color_strs) +{ + std::vector<std::string> hex_colors; + hex_colors.reserve(comp_ids.size()); + for (unsigned int id : comp_ids) + hex_colors.push_back((id >= 1 && id <= color_strs.size()) ? color_strs[id - 1] : "#808080"); + return Slic3r::blend_color_multi(hex_colors, ratios); +} + +void Sidebar::update_mixed_filament_list() +{ + auto* plater = dynamic_cast<Plater*>(GetParent()); + if (!plater) return; + + wxWindowUpdateLocker noUpdates(this); + + const wxColour mc_bg = StateColor::darkModeColorFor(*wxWHITE); + const wxColour mc_border = StateColor::darkModeColorFor(wxColour("#CECECE")); + const wxColour mc_text = StateColor::darkModeColorFor(wxColour("#262E30")); + const wxColour mc_dim = StateColor::darkModeColorFor(wxColour("#ACACAC")); + + auto& project_config = wxGetApp().preset_bundle->project_config; + auto mixed_indices = plater->mixed_filament_config_indices(); + size_t num_physical = p->combos_filament.size(); + + auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed"); + auto* components_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components"); + auto* ratios_opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios"); + auto* colours_opt = project_config.option<ConfigOptionStrings>("filament_colour"); + auto* grad_opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient"); + auto* grad_range_opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_range"); + + bool can_mix = (num_physical >= 2); + bool has_mixed = can_mix && !mixed_indices.empty(); + + // Check integrity of mixed filament component references + std::vector<size_t> broken_slots; + if (is_mixed_opt && components_opt) + broken_slots = check_mixed_filament_integrity(is_mixed_opt->values, components_opt->values, num_physical); + std::set<size_t> broken_set(broken_slots.begin(), broken_slots.end()); + + // Type consistency check + if (is_mixed_opt && components_opt) { + std::vector<std::string> physical_types; + auto& preset_bundle = *wxGetApp().preset_bundle; + for (size_t i = 0; i < num_physical; ++i) { + std::string ft; + if (i < preset_bundle.filament_presets.size()) { + auto* preset = preset_bundle.filaments.find_preset(preset_bundle.filament_presets[i]); + if (preset) { + std::string display_type; + ft = preset->config.get_filament_type(display_type); + } + } + if (ft.empty()) ft = "PLA"; + physical_types.push_back(ft); + } + auto type_mismatch_slots = check_mixed_filament_type_consistency( + is_mixed_opt->values, components_opt->values, physical_types); + for (size_t s : type_mismatch_slots) + broken_set.insert(s); + broken_slots.insert(broken_slots.end(), type_mismatch_slots.begin(), type_mismatch_slots.end()); + } + + bool at_limit = (wxGetApp().preset_bundle->filament_presets.size() >= size_t(EnforcerBlockerType::ExtruderMax)); + p->m_btn_add_mixed_filament->Show(can_mix && !has_mixed && !at_limit); + p->m_panel_mixed_title->Show(has_mixed); + p->m_mixed_scroll_area->Show(has_mixed); + p->m_panel_mixed_content->Show(has_mixed); + if (p->m_btn_mixed_add) + p->m_btn_mixed_add->Enable(!at_limit); + p->m_panel_mixed_warning->Show(false); + + // Show/dismiss 3D canvas notification for broken mixed filaments + if (has_mixed && !broken_set.empty()) { + auto* notify = wxGetApp().plater()->get_notification_manager(); + if (notify) + notify->push_notification(NotificationType::BBLMixedFilamentBroken, + NotificationManager::NotificationLevel::ErrorNotificationLevel, + _u8L("Mixed filament has invalid or mismatched components. Please re-edit affected entries.")); + } else { + auto* notify = wxGetApp().plater()->get_notification_manager(); + if (notify) + notify->close_notification_of_type(NotificationType::BBLMixedFilamentBroken); + } + + if (has_mixed) { + auto* left_col = p->m_sizer_mixed_filaments->GetItem(size_t(0))->GetSizer(); + auto* right_col = p->m_sizer_mixed_filaments->GetItem(size_t(1))->GetSizer(); + left_col->Clear(true); + right_col->Clear(true); + + std::vector<std::string> physical_colors; + if (colours_opt) { + for (size_t i = 0; i < num_physical && i < colours_opt->values.size(); ++i) + physical_colors.push_back(colours_opt->values[i]); + } + + auto make_swatch_panel = [this](wxWindow* parent, const wxColour& col, unsigned int num) -> wxPanel* { + int swatch_sz = FromDIP(20); + auto* panel = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(swatch_sz, swatch_sz)); + panel->SetMinSize(wxSize(swatch_sz, swatch_sz)); + bool is_dark = wxGetApp().dark_mode(); + panel->Bind(wxEVT_PAINT, [panel, col, num, is_dark](wxPaintEvent&) { + wxPaintDC dc(panel); + wxSize sz = panel->GetClientSize(); + dc.SetBackground(wxBrush(col)); + dc.Clear(); + if (!is_dark && col.Red() > 224 && col.Green() > 224 && col.Blue() > 224) { + dc.SetPen(wxPen(wxColour(130, 130, 128), 1)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + } + if (is_dark && col.Red() < 45 && col.Green() < 45 && col.Blue() < 45) { + dc.SetPen(wxPen(wxColour(207, 207, 207), 1)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + } + wxString txt = wxString::Format("%u", num); + dc.SetFont(::Label::Body_14); + wxSize txt_sz = dc.GetTextExtent(txt); + dc.SetTextForeground(col.GetLuminance() > 0.5 ? wxColour(50, 58, 61) : *wxWHITE); + dc.DrawText(txt, (sz.GetWidth() - txt_sz.GetWidth()) / 2, + (sz.GetHeight() - txt_sz.GetHeight()) / 2); + }); + return panel; + }; + + for (size_t i = 0; i < mixed_indices.size(); ++i) { + size_t cfg_idx = mixed_indices[i]; + auto* combo_and_btn_sizer = new wxBoxSizer(wxHORIZONTAL); + + combo_and_btn_sizer->Add(FromDIP(10), 0, 0, 0, 0); + + // Parse components and ratios from config strings (supports 2-N components) + std::vector<unsigned int> comp_ids; + std::vector<int> comp_ratios; + if (components_opt && cfg_idx < components_opt->values.size()) { + std::istringstream iss(components_opt->values[cfg_idx]); + std::string tok; + while (std::getline(iss, tok, ',')) { + unsigned int v = 0; + if (std::sscanf(tok.c_str(), "%u", &v) == 1) + comp_ids.push_back(v); + } + } + if (ratios_opt && cfg_idx < ratios_opt->values.size()) { + CNumericLocalesSetter c_locale_setter; + std::istringstream iss(ratios_opt->values[cfg_idx]); + std::string tok; + while (std::getline(iss, tok, ',')) { + float v = 0; + if (std::sscanf(tok.c_str(), "%f", &v) == 1) + comp_ratios.push_back((int)(v * 100 + 0.5f)); + } + } + if (!comp_ids.empty() && comp_ratios.size() != comp_ids.size()) { + BOOST_LOG_TRIVIAL(warning) << "Mixed filament slot " << cfg_idx + << ": ratio count (" << comp_ratios.size() + << ") != component count (" << comp_ids.size() + << "), resetting to even distribution"; + int n = (int)comp_ids.size(); + comp_ratios.assign(n, 100 / n); + comp_ratios[0] += 100 - (100 / n) * n; + } + + bool is_broken = broken_set.count(cfg_idx) > 0; + + // Recalculate mixed color based on current physical colors + if (!is_broken && !comp_ids.empty() && comp_ids.size() == comp_ratios.size()) { + std::string new_mixed_color = blend_mixed_color(comp_ids, comp_ratios, physical_colors); + + if (colours_opt && cfg_idx < colours_opt->values.size() && colours_opt->values[cfg_idx] != new_mixed_color) { + colours_opt->values[cfg_idx] = new_mixed_color; + + auto* multi_colour_opt = project_config.option<ConfigOptionStrings>("filament_multi_colour"); + if (multi_colour_opt && cfg_idx < multi_colour_opt->values.size()) { + multi_colour_opt->values[cfg_idx] = new_mixed_color; + } + } + } + + bool is_gradient = false; + int gradient_direction = 0; + if (grad_opt && cfg_idx < grad_opt->values.size()) + is_gradient = grad_opt->values[cfg_idx]; + if (is_gradient && grad_range_opt && cfg_idx < grad_range_opt->values.size()) { + CNumericLocalesSetter c_locale_setter; + float v0 = 0, v1 = 0; + if (std::sscanf(grad_range_opt->values[cfg_idx].c_str(), "%f,%f", &v0, &v1) == 2) + gradient_direction = (v0 > v1) ? 0 : 1; + } + + std::string mix_color_str = (colours_opt && cfg_idx < colours_opt->values.size()) + ? colours_opt->values[cfg_idx] : "#888888"; + wxColour mix_col(mix_color_str); + unsigned int mix_num = (unsigned int)(cfg_idx + 1); + + // The swatch fades bottom to top over the model's height, sampled the same way + // the slicer builds the sublayers, so it matches the editor's Effect Preview. The + // ramp comes back empty for every slot that is not a two component gradient mix. + const int swatch_sz = FromDIP(20); + const std::vector<wxColour> gradient_ramp = mixed_gradient_ramp(project_config, cfg_idx, swatch_sz); + + if (!gradient_ramp.empty()) { + auto* grad_panel = new wxPanel(p->m_panel_mixed_content, wxID_ANY, + wxDefaultPosition, wxSize(swatch_sz, swatch_sz)); + grad_panel->SetMinSize(wxSize(swatch_sz, swatch_sz)); + grad_panel->SetBackgroundStyle(wxBG_STYLE_PAINT); + grad_panel->Bind(wxEVT_PAINT, [grad_panel, gradient_ramp, mix_num](wxPaintEvent&) { + wxBufferedPaintDC dc(grad_panel); + wxSize sz = grad_panel->GetClientSize(); + fill_gradient_ramp_rect(dc, wxRect(0, 0, sz.GetWidth(), sz.GetHeight()), gradient_ramp); + wxString txt = wxString::Format("%u", mix_num); + dc.SetFont(::Label::Body_14); + wxSize txt_sz = dc.GetTextExtent(txt); + // The number sits at the swatch's middle, so take its contrast from the + // colour printed at mid height rather than from either endpoint. + dc.SetTextForeground(gradient_ramp[gradient_ramp.size() / 2].GetLuminance() > 0.5 ? wxColour(50, 58, 61) : *wxWHITE); + dc.DrawText(txt, (sz.GetWidth() - txt_sz.GetWidth()) / 2, + (sz.GetHeight() - txt_sz.GetHeight()) / 2); + }); + combo_and_btn_sizer->Add(grad_panel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + } else { + combo_and_btn_sizer->Add(make_swatch_panel(p->m_panel_mixed_content, mix_col, mix_num), + 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + } + + auto* content_panel = new wxPanel(p->m_panel_mixed_content, wxID_ANY); + content_panel->SetBackgroundColour(mc_bg); + content_panel->SetBackgroundStyle(wxBG_STYLE_PAINT); + + // Pre-compute all values the paint lambda needs (avoid capturing `this` for FromDIP) + int cp_pad = FromDIP(4); + int cp_swatch_sz = FromDIP(20); + int cp_sep_margin = FromDIP(3); + int cp_pct_left = FromDIP(2); + int cp_gap = FromDIP(2); + int cp_pct_gap = FromDIP(4); + bool cp_is_dark = wxGetApp().dark_mode(); + + // Build per-component colour list for the lambda + std::vector<wxColour> cp_colours; + std::vector<bool> cp_valid; + std::vector<unsigned int> cp_ids = comp_ids; + std::vector<int> cp_ratios = comp_ratios; + bool cp_is_gradient = is_gradient; + int cp_gradient_dir = gradient_direction; + for (size_t ci = 0; ci < comp_ids.size(); ++ci) { + bool valid = (comp_ids[ci] >= 1 && comp_ids[ci] <= physical_colors.size()); + cp_valid.push_back(valid); + cp_colours.push_back(valid ? wxColour(physical_colors[comp_ids[ci] - 1]) : wxColour("#D9D9D9")); + } + + // Reorder for gradient display: from -> to + std::vector<unsigned int> draw_ids; + std::vector<int> draw_ratios; + std::vector<wxColour> draw_colours; + std::vector<bool> draw_valid; + if (cp_is_gradient && cp_ids.size() == 2) { + int fi = (cp_gradient_dir == 0) ? 0 : 1; + int ti = 1 - fi; + draw_ids = { cp_ids[fi], cp_ids[ti] }; + draw_ratios = { cp_ratios.size() > (size_t)fi ? cp_ratios[fi] : 0, + cp_ratios.size() > (size_t)ti ? cp_ratios[ti] : 0 }; + draw_colours = { cp_colours[fi], cp_colours[ti] }; + draw_valid = { cp_valid[fi], cp_valid[ti] }; + } else { + draw_ids = cp_ids; + draw_ratios = cp_ratios; + draw_colours = cp_colours; + draw_valid = cp_valid; + } + + content_panel->Bind(wxEVT_PAINT, [content_panel, mc_bg, mc_border, mc_text, mc_dim, + cp_pad, cp_swatch_sz, cp_sep_margin, cp_pct_left, + cp_gap, cp_pct_gap, cp_is_dark, + cp_is_gradient, + draw_ids, draw_ratios, draw_colours, draw_valid](wxPaintEvent&) { + wxBufferedPaintDC dc(content_panel); + wxSize sz = content_panel->GetClientSize(); + + dc.SetBrush(wxBrush(mc_bg)); + dc.SetPen(wxPen(mc_border, 1)); + dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + + dc.SetFont(::Label::Body_13); + int x = cp_pad; + int y_swatch = (sz.GetHeight() - cp_swatch_sz) / 2; + int text_h = dc.GetTextExtent(wxT("A")).GetHeight(); + int y_text = y_swatch + (cp_swatch_sz - text_h) / 2; + int avail = sz.GetWidth() - cp_pad; + wxString ellipsis = wxT("..."); + int ellipsis_w = dc.GetTextExtent(ellipsis).GetWidth(); + + auto fits = [&](int needed) -> bool { + return (x + needed) <= (avail - ellipsis_w); + }; + + size_t n = draw_ids.size(); + for (size_t ci = 0; ci < n; ++ci) { + // Separator: "+" or arrow + if (ci > 0) { + wxString sep = cp_is_gradient ? wxT("\u2192") : wxT("+"); + int sep_w = dc.GetTextExtent(sep).GetWidth() + cp_sep_margin * 2; + if (!fits(sep_w + cp_swatch_sz)) { + dc.SetTextForeground(mc_text); + dc.DrawText(ellipsis, x, y_text); + break; + } + dc.SetTextForeground(mc_text); + dc.DrawText(sep, x + cp_sep_margin, y_text); + x += sep_w; + } + + // Swatch + if (!fits(cp_swatch_sz)) { + dc.SetTextForeground(mc_text); + dc.DrawText(ellipsis, x, y_text); + break; + } + + if (draw_valid[ci]) { + wxColour col = draw_colours[ci]; + dc.SetBrush(wxBrush(col)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(x, y_swatch, cp_swatch_sz, cp_swatch_sz); + if (!cp_is_dark && col.Red() > 224 && col.Green() > 224 && col.Blue() > 224) { + dc.SetPen(wxPen(wxColour(130, 130, 128), 1)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRectangle(x, y_swatch, cp_swatch_sz, cp_swatch_sz); + } + if (cp_is_dark && col.Red() < 45 && col.Green() < 45 && col.Blue() < 45) { + dc.SetPen(wxPen(wxColour(207, 207, 207), 1)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRectangle(x, y_swatch, cp_swatch_sz, cp_swatch_sz); + } + dc.SetFont(::Label::Body_14); + wxString num = wxString::Format("%u", draw_ids[ci]); + wxSize num_sz = dc.GetTextExtent(num); + dc.SetTextForeground(col.GetLuminance() > 0.5 ? wxColour(50, 58, 61) : *wxWHITE); + dc.DrawText(num, x + (cp_swatch_sz - num_sz.GetWidth()) / 2, + y_swatch + (cp_swatch_sz - num_sz.GetHeight()) / 2); + dc.SetFont(::Label::Body_13); + } else { + dc.SetBrush(wxBrush(mc_bg)); + dc.SetPen(wxPen(mc_dim, 1)); + dc.DrawRectangle(x, y_swatch, cp_swatch_sz, cp_swatch_sz); + wxString dash = wxT("\u2014"); + wxSize dash_sz = dc.GetTextExtent(dash); + dc.SetTextForeground(mc_dim); + dc.DrawText(dash, x + (cp_swatch_sz - dash_sz.GetWidth()) / 2, + y_swatch + (cp_swatch_sz - dash_sz.GetHeight()) / 2); + } + x += cp_swatch_sz + cp_gap; + + // Ratio text (skip for gradient) + if (!cp_is_gradient) { + int r = (ci < draw_ratios.size()) ? draw_ratios[ci] : 0; + wxString pct = wxString::Format("%d%%", r); + int pct_w = dc.GetTextExtent(pct).GetWidth(); + if (!fits(pct_w)) { + dc.SetTextForeground(mc_text); + dc.DrawText(ellipsis, x, y_text); + break; + } + dc.SetTextForeground(mc_text); + dc.DrawText(pct, x + cp_pct_left, y_text); + x += pct_w + cp_pct_gap; + } + } + }); + + // Tooltip: always show full info + { + wxString tip; + for (size_t ci = 0; ci < draw_ids.size(); ++ci) { + if (ci > 0) tip += cp_is_gradient ? wxT(" \u2192 ") : wxT(" + "); + int r = (ci < draw_ratios.size()) ? draw_ratios[ci] : 0; + tip += wxString::Format("%u (%d%%)", draw_ids[ci], r); + } + content_panel->SetToolTip(tip); + } + + // Repaint on resize so truncation updates + content_panel->Bind(wxEVT_SIZE, [content_panel](wxSizeEvent& e) { + content_panel->Refresh(); + e.Skip(); + }); + + content_panel->SetCursor(wxCursor(wxCURSOR_HAND)); + size_t panel_idx = i; + content_panel->Bind(wxEVT_LEFT_UP, [this, panel_idx](wxMouseEvent&) { edit_mixed_filament(panel_idx); }); + + combo_and_btn_sizer->Add(content_panel, 1, wxALL | wxEXPAND, FromDIP(2))->SetMinSize({-1, FromDIP(30)}); + + auto* menu_btn = new ScalableButton(p->m_panel_mixed_content, wxID_ANY, + is_broken ? "error" : "menu_filament"); + menu_btn->SetToolTip(is_broken ? _L("Mixed filament has broken component references") : _L("Edit / Delete / Merge")); + menu_btn->Bind(wxEVT_BUTTON, [this, panel_idx, cfg_idx](wxCommandEvent&) { + wxMenu menu; + + auto* edit_item = menu.Append(wxID_ANY, _L("Edit")); + menu.Bind(wxEVT_MENU, [this, panel_idx](wxCommandEvent&) { + edit_mixed_filament(panel_idx); + }, edit_item->GetId()); + + wxMenu* sub_menu = new wxMenu(); + std::vector<wxBitmap*> icons = get_extruder_color_icons(true); + int filaments_cnt = icons.size(); + for (int j = 0; j < filaments_cnt; ++j) { + if ((size_t)j == cfg_idx) + continue; + + wxString item_name; + bool is_target_mixed = wxGetApp().preset_bundle->is_mixed_filament(j); + if (is_target_mixed) { + item_name = wxString::Format(_L("Filament %d"), j + 1); + } else { + auto preset = wxGetApp().preset_bundle->filaments.find_preset( + wxGetApp().preset_bundle->filament_presets[j]); + item_name = preset ? from_u8(preset->label(false)) + : wxString::Format(_L("Filament %d"), j + 1); + } + + auto* mi = new wxMenuItem(sub_menu, wxID_ANY, item_name); +#ifndef __linux__ + mi->SetBitmap(*icons[j]); +#endif + sub_menu->Append(mi); + sub_menu->Bind(wxEVT_MENU, [this, cfg_idx, j](wxCommandEvent&) { + change_filament(cfg_idx, j); + }, mi->GetId()); + } + if (filaments_cnt > 1) + menu.AppendSubMenu(sub_menu, _L("Merge with")); + else + delete sub_menu; + + menu.AppendSeparator(); // ORCA use seperator for reducing accidental clicks to delete + + // ORCA use delete item on end of menu to prevent accidental clicks. clicking to submenus(merge) already not allowed by OS + auto* del_item = menu.Append(wxID_ANY, _L("Delete")); + menu.Bind(wxEVT_MENU, [this, panel_idx](wxCommandEvent&) { + delete_mixed_filament_at(panel_idx); + }, del_item->GetId()); + + PopupMenu(&menu); + }); + combo_and_btn_sizer->Add(menu_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4)); + + combo_and_btn_sizer->Add(FromDIP(16), 0, 0, 0, 0); + + int side = i % 2; + auto* col = (side == 0) ? left_col : right_col; + if (side == 1 && i > 1) col->Remove(i / 2); + col->Add(combo_and_btn_sizer, 1, wxEXPAND); + if (side == 0 && i > 0) { + right_col->AddStretchSpacer(1); + } + } + } + + recalc_filament_scroll_sizes(); + + p->m_panel_filament_content->FitInside(); + p->m_mixed_scroll_area->FitInside(); + p->scrolled->Layout(); + m_scrolled_sizer->Layout(); + p->scrolled->Layout(); + + size_t total = wxGetApp().preset_bundle->filament_presets.size(); + obj_list()->update_objects_list_filament_column(total); + + // Sync mixed filament colors into the config used by 3D view rendering. + plater->update_filament_colors_in_full_config(); + obj_list()->update_filament_colors(); + + // Check if any broken mixed filament is used by objects on current plate. + // Scan raw extruder assignments (object / volume / height-range / painting) + // instead of get_extruders() which expands mixed slots and loses their IDs. + p->m_mixed_filament_broken = false; + if (!broken_slots.empty()) { + std::set<size_t> broken_1based; + for (size_t s : broken_slots) broken_1based.insert(s + 1); + + auto* curr_plate = plater->get_partplate_list().get_curr_plate(); + if (curr_plate) { + for (auto& obj : plater->model().objects) { + if (!curr_plate->contain_instance_totally(obj, 0)) + continue; + // Check object-level extruder + int obj_ext = obj->config.has("extruder") ? obj->config.extruder() : 1; + if (broken_1based.count((size_t)obj_ext)) { + p->m_mixed_filament_broken = true; + break; + } + bool found = false; + for (auto* vol : obj->volumes) { + // Check volume-level extruder + int vol_ext = vol->config.has("extruder") ? vol->config.extruder() : obj_ext; + if (broken_1based.count((size_t)vol_ext)) { found = true; break; } + // Check color painting data (mmu segmentation facets) + if (vol->is_model_part() && !vol->mmu_segmentation_facets.empty()) { + for (size_t broken_slot : broken_1based) { + if (vol->mmu_segmentation_facets.has_facets(*vol, EnforcerBlockerType(broken_slot))) + { found = true; break; } + } + if (found) break; + } + } + if (found) { p->m_mixed_filament_broken = true; break; } + // Check height range modifier extruder overrides + for (auto& [range, cfg] : obj->layer_config_ranges) { + if (cfg.has("extruder")) { + int layer_ext = cfg.option("extruder")->getInt(); + if (layer_ext > 0 && broken_1based.count((size_t)layer_ext)) + { found = true; break; } + } + } + if (found) { p->m_mixed_filament_broken = true; break; } + } + } + } + + if (plater->canvas3D()) { + plater->canvas3D()->set_as_dirty(); + plater->get_view3D_canvas3D()->reload_scene(false); + } + + if (p->m_mixed_filament_broken) { + auto* mf = wxGetApp().mainframe; + if (mf) + mf->update_slice_print_status(MainFrame::eEventObjectUpdate, false); + } + + if (auto *tab = dynamic_cast<TabPrintPlate *>(wxGetApp().plate_tab)) + tab->update_mixed_filament_seq_state(); + +} + +bool Sidebar::has_broken_mixed_filament() const +{ + auto* plater = dynamic_cast<Plater*>(GetParent()); + if (!plater) return false; + return has_broken_mixed_filament(plater->get_partplate_list().get_curr_plate()); +} + +bool Sidebar::has_broken_mixed_filament(const PartPlate* plate) const +{ + if (!plate) return false; + auto* plater = dynamic_cast<Plater*>(GetParent()); + if (!plater) return false; + + auto& project_config = wxGetApp().preset_bundle->project_config; + auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed"); + auto* comp_strs_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components"); + if (!is_mixed_opt || !comp_strs_opt) return false; + + size_t num_physical = p->combos_filament.size(); + auto broken_slots = check_mixed_filament_integrity(is_mixed_opt->values, comp_strs_opt->values, num_physical); + + // Type consistency check + { + std::vector<std::string> physical_types; + auto& preset_bundle = *wxGetApp().preset_bundle; + for (size_t i = 0; i < num_physical; ++i) { + std::string ft; + if (i < preset_bundle.filament_presets.size()) { + auto* preset = preset_bundle.filaments.find_preset(preset_bundle.filament_presets[i]); + if (preset) { + std::string display_type; + ft = preset->config.get_filament_type(display_type); + } + } + if (ft.empty()) ft = "PLA"; + physical_types.push_back(ft); + } + auto type_mismatch_slots = check_mixed_filament_type_consistency( + is_mixed_opt->values, comp_strs_opt->values, physical_types); + broken_slots.insert(broken_slots.end(), type_mismatch_slots.begin(), type_mismatch_slots.end()); + } + + if (broken_slots.empty()) return false; + + std::set<size_t> broken_1based; + for (size_t s : broken_slots) broken_1based.insert(s + 1); + + // Scan model objects on the given plate for raw extruder assignments + // (don't use get_extruders() which expands mixed slots) + for (auto& entry : plater->model().objects) { + if (!plate->contain_instance_totally(entry, 0)) + continue; + // Check object-level extruder + int obj_ext = entry->config.has("extruder") ? entry->config.extruder() : 1; + if (broken_1based.count((size_t)obj_ext)) + return true; + for (auto* vol : entry->volumes) { + // Check volume-level extruder + int vol_ext = vol->config.has("extruder") ? vol->config.extruder() : obj_ext; + if (broken_1based.count((size_t)vol_ext)) + return true; + // Check color painting data (mmu segmentation facets) + if (vol->is_model_part() && !vol->mmu_segmentation_facets.empty()) { + for (size_t broken_slot : broken_1based) { + if (vol->mmu_segmentation_facets.has_facets(*vol, EnforcerBlockerType(broken_slot))) + return true; + } + } + } + // Check height range modifier extruder overrides + for (auto& [range, cfg] : entry->layer_config_ranges) { + if (cfg.has("extruder")) { + int layer_ext = cfg.option("extruder")->getInt(); + if (layer_ext > 0 && broken_1based.count((size_t)layer_ext)) + return true; + } + } + } + + return false; +} + +void Sidebar::collect_physical_filament_info(std::vector<std::string>& color_strs, + std::vector<std::string>& names, + std::vector<std::string>& types, + std::vector<size_t>* config_indices) +{ + color_strs.clear(); + names.clear(); + types.clear(); + if (config_indices) + config_indices->clear(); + + size_t num_physical = p->combos_filament.size(); + auto& project_config = wxGetApp().preset_bundle->project_config; + auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed"); + std::vector<size_t> physical_indices; + const size_t total = wxGetApp().preset_bundle->filament_presets.size(); + physical_indices.reserve(num_physical); + for (size_t i = 0; i < total && physical_indices.size() < num_physical; ++i) { + if (!is_mixed_opt || i >= is_mixed_opt->values.size() || !is_mixed_opt->values[i]) + physical_indices.push_back(i); + } + while (physical_indices.size() < num_physical) + physical_indices.push_back(physical_indices.size()); + if (config_indices) + *config_indices = physical_indices; + + auto* colours_opt = project_config.option<ConfigOptionStrings>("filament_colour"); + if (colours_opt) { + for (size_t i = 0; i < num_physical; ++i) { + const size_t cfg_idx = physical_indices[i]; + if (cfg_idx < colours_opt->values.size()) + color_strs.push_back(colours_opt->values[cfg_idx]); + } + } + + for (size_t i = 0; i < num_physical; ++i) { + auto* combo = p->combos_filament[i]; + names.push_back(combo ? into_u8(combo->GetValue()) : "Filament " + std::to_string(i + 1)); + } + + auto& preset_bundle = *wxGetApp().preset_bundle; + for (size_t i = 0; i < num_physical; ++i) { + const size_t cfg_idx = physical_indices[i]; + Preset* preset = nullptr; + if (cfg_idx < preset_bundle.filament_presets.size()) + preset = preset_bundle.filaments.find_preset(preset_bundle.filament_presets[cfg_idx]); + std::string ft; + if (preset) { + std::string display_type; + ft = preset->config.get_filament_type(display_type); + } + if (ft.empty()) ft = "PLA"; + types.push_back(ft); + } +} + +// Serialize the dialog's custom gradient curve only when it deviates from the +// direction-implied two-point linear default. Returning an empty string keeps +// projects with the default shape bit-identical with the legacy 2-field format +// (curve string stays "" so the slicer falls back to gradient_range linear). +// Shared by add_mixed_filament / edit_mixed_filament so the "is default" rule +// stays consistent between both entry points. +static std::string serialize_mixed_gradient_curve_if_custom(const MixedFilamentResult& result) +{ + if (!(result.components.size() == 2 && !result.gradient_curve.empty())) + return {}; + + const double y0 = (result.gradient_direction == 0) ? kGradientMaxRatio : kGradientMinRatio; + const double y1 = (result.gradient_direction == 0) ? kGradientMinRatio : kGradientMaxRatio; + const double eps = 1e-4; + if (result.gradient_curve.size() == 2) { + const auto& a0 = result.gradient_curve[0]; + const auto& a1 = result.gradient_curve[1]; + // Default curve also requires no tangent overrides; any finite tangent + // means the user bent the segment, so we must serialize it. + const bool is_default = + std::abs(a0.x - 0.0) < eps + && std::abs(a1.x - 1.0) < eps + && std::abs(a0.y - y0) < eps + && std::abs(a1.y - y1) < eps + && !std::isfinite(a0.m_in) && !std::isfinite(a0.m_out) + && !std::isfinite(a1.m_in) && !std::isfinite(a1.m_out); + if (is_default) return {}; + } + + Slic3r::GradientCurve gc; + gc.points = result.gradient_curve; + return Slic3r::serialize_gradient_curve(gc); +} + +static bool create_mixed_filament_from_result( + Sidebar* sidebar, + const MixedFilamentResult& result, + const std::vector<std::string>& color_strs) +{ + if (!sidebar || result.components.size() < 2 || result.ratios.size() < 2) + return false; + if (!dynamic_cast<Plater*>(sidebar->GetParent())) + return false; + + size_t num_physical = sidebar->combos_filament().size(); + if (num_physical < 2) + return false; + if (wxGetApp().preset_bundle->filament_presets.size() >= size_t(EnforcerBlockerType::ExtruderMax)) + return false; + + auto& project_config = wxGetApp().preset_bundle->project_config; + size_t total = wxGetApp().preset_bundle->filament_presets.size(); + size_t new_idx = total; + + std::string mixed_color = blend_mixed_color(result.components, result.ratios, color_strs); + wxGetApp().preset_bundle->set_num_filaments(total + 1, mixed_color); + + auto* multi_colour_opt = project_config.option<ConfigOptionStrings>("filament_multi_colour"); + if (multi_colour_opt) { + while (multi_colour_opt->values.size() <= new_idx) multi_colour_opt->values.push_back(""); + multi_colour_opt->values[new_idx] = mixed_color; + } + + // set_num_filaments() above already grows these parallel arrays; the writes are still + // size-guarded so a sizing bug degrades into a no-op rather than a heap overwrite. + { + auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed"); + while (is_mixed_opt->values.size() <= new_idx) is_mixed_opt->values.push_back(false); + is_mixed_opt->values[new_idx] = true; + } + + std::string comp_str; + for (size_t i = 0; i < result.components.size(); ++i) { + if (i > 0) comp_str += ","; + comp_str += std::to_string(result.components[i]); + } + { + auto* comp_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components"); + while (comp_opt->values.size() <= new_idx) comp_opt->values.push_back(std::string{}); + comp_opt->values[new_idx] = comp_str; + } + + int ratio_sum = 0; + for (int r : result.ratios) ratio_sum += r; + if (ratio_sum <= 0) ratio_sum = 100; + + std::string ratio_str; + { + CNumericLocalesSetter c_locale_setter; + for (size_t i = 0; i < result.ratios.size(); ++i) { + if (i > 0) ratio_str += ","; + char buf[32]; + std::snprintf(buf, sizeof(buf), "%.4f", (float)result.ratios[i] / ratio_sum); + ratio_str += buf; + } + } + { + auto* ratios_opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios"); + while (ratios_opt->values.size() <= new_idx) ratios_opt->values.push_back(std::string{}); + ratios_opt->values[new_idx] = ratio_str; + } + + if (!project_config.option("filament_mixed_gradient")) + project_config.set_key_value("filament_mixed_gradient", new ConfigOptionBools({false})); + if (!project_config.option("filament_mixed_gradient_range")) + project_config.set_key_value("filament_mixed_gradient_range", new ConfigOptionStrings({""}) ); + if (!project_config.option("filament_mixed_gradient_curve")) + project_config.set_key_value("filament_mixed_gradient_curve", new ConfigOptionStrings({""}) ); + if (!project_config.option("filament_mixed_gradient_per_part")) + project_config.set_key_value("filament_mixed_gradient_per_part", new ConfigOptionBools({false})); + + { + auto* grad_opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient"); + while (grad_opt->values.size() <= new_idx) grad_opt->values.push_back(false); + grad_opt->values[new_idx] = result.gradient_enabled; + } + { + auto* grad_range_opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_range"); + while (grad_range_opt->values.size() <= new_idx) grad_range_opt->values.push_back(""); + if (result.gradient_enabled && result.components.size() == 2) { + const char* fmt = (result.gradient_direction == 0) ? "0.9000,0.1000" : "0.1000,0.9000"; + grad_range_opt->values[new_idx] = fmt; + } else { + grad_range_opt->values[new_idx] = ""; + } + } + { + auto* grad_curve_opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_curve"); + while (grad_curve_opt->values.size() <= new_idx) grad_curve_opt->values.push_back(""); + grad_curve_opt->values[new_idx] = serialize_mixed_gradient_curve_if_custom(result); + } + { + auto* per_part_opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient_per_part"); + while (per_part_opt->values.size() <= new_idx) per_part_opt->values.push_back(false); + per_part_opt->values[new_idx] = result.gradient_enabled && result.per_part_gradient; + } + + auto& presets = wxGetApp().preset_bundle->filament_presets; + if (result.components[0] >= 1 && result.components[0] <= num_physical && presets.size() > new_idx) + presets[new_idx] = presets[result.components[0] - 1]; + + size_t filament_count = wxGetApp().preset_bundle->filament_presets.size(); + wxGetApp().plater()->get_partplate_list().on_filament_added(filament_count); + wxGetApp().plater()->on_filament_count_change(filament_count); + wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + + sidebar->update_mixed_filament_list(); + wxGetApp().plater()->update_project_dirty_from_presets(); + wxPostEvent(sidebar, SimpleEvent(EVT_SCHEDULE_BACKGROUND_PROCESS, sidebar)); + return true; +} + +void Sidebar::add_mixed_filament() +{ + auto* plater = dynamic_cast<Plater*>(GetParent()); + if (!plater) return; + + size_t num_physical = p->combos_filament.size(); + if (num_physical < 2) return; + if (wxGetApp().preset_bundle->filament_presets.size() >= size_t(EnforcerBlockerType::ExtruderMax)) return; + + std::vector<std::string> color_strs, names, types; + collect_physical_filament_info(color_strs, names, types); + + MixedFilamentDialog dlg(this, color_strs, names, types); + if (dlg.ShowModal() == wxID_OK) { + auto result = dlg.get_result(); + create_mixed_filament_from_result(this, result, color_strs); + } +} + +void Sidebar::edit_mixed_filament(size_t panel_idx) +{ + auto* plater = dynamic_cast<Plater*>(GetParent()); + if (!plater) return; + + auto mixed_indices = plater->mixed_filament_config_indices(); + if (panel_idx >= mixed_indices.size()) return; + size_t cfg_idx = mixed_indices[panel_idx]; + + std::vector<std::string> color_strs, names, types; + collect_physical_filament_info(color_strs, names, types); + + auto& project_config = wxGetApp().preset_bundle->project_config; + MixedFilamentResult existing; + auto* components_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components"); + auto* ratios_opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios"); + + // Parse existing components + if (components_opt && cfg_idx < components_opt->values.size()) { + const std::string& cs = components_opt->values[cfg_idx]; + std::istringstream iss(cs); + std::string tok; + while (std::getline(iss, tok, ',')) { + unsigned int v = 0; + if (std::sscanf(tok.c_str(), "%u", &v) == 1) + existing.components.push_back(v); + } + } + // Parse existing ratios + if (ratios_opt && cfg_idx < ratios_opt->values.size()) { + CNumericLocalesSetter c_locale_setter; + const std::string& rs = ratios_opt->values[cfg_idx]; + std::istringstream iss(rs); + std::string tok; + while (std::getline(iss, tok, ',')) { + float v = 0; + if (std::sscanf(tok.c_str(), "%f", &v) == 1) + existing.ratios.push_back((int)(v * 100 + 0.5f)); + } + } + if (existing.components.size() < 2) { + existing.components = {1, 2}; + existing.ratios = {50, 50}; + } else if (existing.ratios.size() != existing.components.size()) { + BOOST_LOG_TRIVIAL(warning) << "Mixed filament edit: ratio count (" + << existing.ratios.size() << ") != component count (" + << existing.components.size() + << "), resetting to even distribution"; + int n = (int)existing.components.size(); + existing.ratios.assign(n, 100 / n); + existing.ratios[0] += 100 - (100 / n) * n; + } + + // Read gradient settings + auto* grad_opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient"); + if (grad_opt && cfg_idx < grad_opt->values.size()) + existing.gradient_enabled = grad_opt->values[cfg_idx]; + auto* grad_range_opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_range"); + if (existing.gradient_enabled && grad_range_opt && cfg_idx < grad_range_opt->values.size()) { + CNumericLocalesSetter c_locale_setter; + float v0 = 0, v1 = 0; + if (std::sscanf(grad_range_opt->values[cfg_idx].c_str(), "%f,%f", &v0, &v1) == 2) + existing.gradient_direction = (v0 > v1) ? 0 : 1; + } + auto* grad_curve_opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_curve"); + if (existing.gradient_enabled && grad_curve_opt && cfg_idx < grad_curve_opt->values.size()) { + auto curve = Slic3r::parse_gradient_curve(grad_curve_opt->values[cfg_idx]); + existing.gradient_curve = curve.points; + } + auto* per_part_opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient_per_part"); + if (existing.gradient_enabled && per_part_opt && cfg_idx < per_part_opt->values.size()) + existing.per_part_gradient = per_part_opt->values[cfg_idx]; + + MixedFilamentDialog dlg(this, existing, color_strs, names, types); + if (dlg.ShowModal() == wxID_OK) { + auto result = dlg.get_result(); + if (result.components.size() < 2 || result.ratios.size() < 2) return; + + // Serialize components + std::string comp_str; + for (size_t i = 0; i < result.components.size(); ++i) { + if (i > 0) comp_str += ","; + comp_str += std::to_string(result.components[i]); + } + components_opt->values[cfg_idx] = comp_str; + + // Serialize ratios + int ratio_sum = 0; + for (int r : result.ratios) ratio_sum += r; + if (ratio_sum <= 0) ratio_sum = 100; + + std::string ratio_str; + { + CNumericLocalesSetter c_locale_setter; + for (size_t i = 0; i < result.ratios.size(); ++i) { + if (i > 0) ratio_str += ","; + char buf[32]; + std::snprintf(buf, sizeof(buf), "%.4f", (float)result.ratios[i] / ratio_sum); + ratio_str += buf; + } + } + ratios_opt->values[cfg_idx] = ratio_str; + + // Gradient settings — ensure keys exist in dynamic config + if (!project_config.option("filament_mixed_gradient")) + project_config.set_key_value("filament_mixed_gradient", new ConfigOptionBools({false})); + if (!project_config.option("filament_mixed_gradient_range")) + project_config.set_key_value("filament_mixed_gradient_range", new ConfigOptionStrings({""}) ); + if (!project_config.option("filament_mixed_gradient_curve")) + project_config.set_key_value("filament_mixed_gradient_curve", new ConfigOptionStrings({""}) ); + if (!project_config.option("filament_mixed_gradient_per_part")) + project_config.set_key_value("filament_mixed_gradient_per_part", new ConfigOptionBools({false})); + + { + auto* grad_opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient"); + while (grad_opt->values.size() <= cfg_idx) grad_opt->values.push_back(false); + grad_opt->values[cfg_idx] = result.gradient_enabled; + } + { + auto* grad_range_opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_range"); + while (grad_range_opt->values.size() <= cfg_idx) grad_range_opt->values.push_back(""); + if (result.gradient_enabled && result.components.size() == 2) { + const char* fmt = (result.gradient_direction == 0) ? "0.9000,0.1000" : "0.1000,0.9000"; + grad_range_opt->values[cfg_idx] = fmt; + } else { + grad_range_opt->values[cfg_idx] = ""; + } + } + { + auto* grad_curve_opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_curve"); + while (grad_curve_opt->values.size() <= cfg_idx) grad_curve_opt->values.push_back(""); + grad_curve_opt->values[cfg_idx] = serialize_mixed_gradient_curve_if_custom(result); + } + { + auto* per_part_opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient_per_part"); + while (per_part_opt->values.size() <= cfg_idx) per_part_opt->values.push_back(false); + per_part_opt->values[cfg_idx] = result.gradient_enabled && result.per_part_gradient; + } + + // Compute blended color + std::string blended = blend_mixed_color(result.components, result.ratios, color_strs); + auto* colours_opt = project_config.option<ConfigOptionStrings>("filament_colour"); + if (colours_opt && cfg_idx < colours_opt->values.size()) + colours_opt->values[cfg_idx] = blended; + + auto* multi_colour_opt = project_config.option<ConfigOptionStrings>("filament_multi_colour"); + if (multi_colour_opt && cfg_idx < multi_colour_opt->values.size()) + multi_colour_opt->values[cfg_idx] = blended; + + // The edited slot keeps its index, so nothing else refreshes the per-feature filament + // lists - and its blended colour and type are what they show for it. + update_mixed_filament_list(); + update_dynamic_filament_list(); + wxGetApp().plater()->update_project_dirty_from_presets(); + wxPostEvent(this, SimpleEvent(EVT_SCHEDULE_BACKGROUND_PROCESS, this)); + } +} + +void Sidebar::delete_mixed_filament_at(size_t panel_idx) +{ + auto* plater = dynamic_cast<Plater*>(GetParent()); + if (!plater) return; + + auto mixed_indices = plater->mixed_filament_config_indices(); + if (panel_idx >= mixed_indices.size()) return; + size_t cfg_idx = mixed_indices[panel_idx]; + + delete_filament(cfg_idx, -1); +} + +void Sidebar::decompose_filament_color(int filament_idx) +{ + if (filament_idx == kSidebarContextMenuFilamentId) + filament_idx = p->m_menu_filament_id; + if (filament_idx < 0) + return; + + auto& project_config = wxGetApp().preset_bundle->project_config; + auto* colours_opt = project_config.option<ConfigOptionStrings>("filament_colour"); + if (!colours_opt || static_cast<size_t>(filament_idx) >= colours_opt->values.size()) + return; + + wxColour target_color(colours_opt->values[filament_idx]); + + std::vector<std::string> color_strs, names, types; + std::vector<size_t> physical_config_indices; + collect_physical_filament_info(color_strs, names, types, &physical_config_indices); + + // Build decompose-specific types: ColorDecomposeDialog needs "PLA Basic" + // distinction (for CMYW/RYBW card visibility), while collect_physical_filament_info + // now returns coarse filament_type (e.g. "PLA" for all PLA variants). + std::vector<std::string> decompose_types; + { + auto& pb = *wxGetApp().preset_bundle; + for (size_t i = 0; i < physical_config_indices.size(); ++i) { + const size_t ci = physical_config_indices[i]; + Preset* pr = (ci < pb.filament_presets.size()) + ? pb.filaments.find_preset(pb.filament_presets[ci]) : nullptr; + decompose_types.push_back(filament_type_for_color_decompose(pr)); + } + } + + size_t source_physical_idx = size_t(-1); + for (size_t i = 0; i < physical_config_indices.size(); ++i) { + if (physical_config_indices[i] == static_cast<size_t>(filament_idx)) { + source_physical_idx = i; + break; + } + } + + ColorDecomposeDialog dlg(this, + source_physical_idx == size_t(-1) ? -1 : static_cast<int>(source_physical_idx), + target_color, color_strs, names, decompose_types, + wxGetApp().preset_bundle->filament_presets.size(), + static_cast<size_t>(EnforcerBlockerType::ExtruderMax), + physical_config_indices); + int modal_res = dlg.ShowModal(); + if (modal_res == wxID_OK) { + ColorDecomposeResult dialog_result = dlg.get_result(); + MixedFilamentResult mixed_result; + std::vector<DecomposeMissingComponent> missing_components; + if (!prepare_decompose_mixed_result(dialog_result, static_cast<size_t>(filament_idx), source_physical_idx, + color_strs, decompose_types, physical_config_indices, mixed_result, missing_components)) + return; + + if (!confirm_create_decompose_missing_components(this, missing_components)) + return; + + for (const DecomposeMissingComponent& missing : missing_components) { + size_t before_count = p->combos_filament.size(); + add_custom_filament(wxColour(missing.official_component.color_hex), missing.preset_name, true); + size_t after_count = p->combos_filament.size(); + if (after_count <= before_count) + return; + set_created_standard_component_metadata(before_count, missing.official_component); + if (missing.component_idx < mixed_result.components.size()) + mixed_result.components[missing.component_idx] = static_cast<unsigned int>(before_count + 1); + } + + if (!missing_components.empty()) { + collect_physical_filament_info(color_strs, names, types, &physical_config_indices); + } + + create_mixed_filament_from_result(this, mixed_result, color_strs); + } +} + void Sidebar::update_filaments_area_height() // ORCA { @@ -3909,6 +5234,9 @@ void Sidebar::sys_color_changed() p->scrolled->Layout(); + // Mixed rows are custom-drawn, so they need rebuilding for the new theme colours. + update_mixed_filament_list(); + p->searcher.dlg_sys_color_changed(); } @@ -3943,21 +5271,42 @@ void Sidebar::jump_to_option(size_t selected) // BBS. Move logic from Plater::on_extruders_change() to Sidebar::on_filament_count_change(). void Sidebar::on_filament_count_change(size_t num_filaments) { + // num_filaments counts every slot; mixed-color slots are virtual and get no combo of + // their own (they are rendered by update_mixed_filament_list instead), so the physical + // subset drives the combo list. + auto& project_config = wxGetApp().preset_bundle->project_config; + auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed"); + + std::vector<size_t> physical_indices; + for (size_t i = 0; i < num_filaments; ++i) { + if (!is_mixed_opt || i >= is_mixed_opt->values.size() || !is_mixed_opt->values[i]) + physical_indices.push_back(i); + } + const size_t num_physical = physical_indices.size(); + auto& choices = combos_filament(); - if (num_filaments == choices.size()) + if (num_physical == choices.size()) { + // The ctor pre-creates one combo, so a single-filament project hits this guard before + // any layout pass has sized the scroll areas; refresh them here as well. + // Adding a mixed slot also lands here, since only the virtual count changed, so the + // per-feature filament lists - which do list mixed slots - have to be refreshed too. + recalc_filament_scroll_sizes(); + update_mixed_filament_list(); + update_dynamic_filament_list(); return; + } - if (choices.size() == 1 || num_filaments == 1) + if (choices.size() == 1 || num_physical == 1) choices[0]->GetDropDown().Invalidate(); wxWindowUpdateLocker noUpdates_scrolled_panel(this); size_t i = choices.size(); - while (i < num_filaments) + while (i < num_physical) { PlaterPresetComboBox* choice/*{ nullptr }*/; - init_filament_combo(&choice, i); + init_filament_combo(&choice, physical_indices[i]); int last_selection = choices.back()->GetSelection(); choices.push_back(choice); @@ -3968,11 +5317,13 @@ void Sidebar::on_filament_count_change(size_t num_filaments) } // remove unused choices if any - remove_unused_filament_combos(num_filaments); + remove_unused_filament_combos(num_physical); show_SEMM_buttons(); // ORCA update_filaments_area_height(); // ORCA + recalc_filament_scroll_sizes(); + update_mixed_filament_list(); Layout(); p->m_panel_filament_title->Refresh(); @@ -3984,51 +5335,54 @@ void Sidebar::on_filaments_delete(size_t filament_id) { auto &choices = combos_filament(); - if (filament_id >= choices.size()) - return; + // A mixed (virtual) slot has no combo of its own, so there is no combo UI to remove — + // but the shared refresh below must still run so the mixed filament panel drops its row. + if (filament_id < choices.size()) { + if (choices.size() == 1) + choices[0]->GetDropDown().Invalidate(); - if (choices.size() == 1) - choices[0]->GetDropDown().Invalidate(); + wxWindowUpdateLocker noUpdates_scrolled_panel(this); - wxWindowUpdateLocker noUpdates_scrolled_panel(this); + // delete UI item + { + const int last = p->combos_filament.size() - 1; + auto sizer_filaments = this->p->sizer_filaments->GetItem(last % 2)->GetSizer(); + sizer_filaments->Remove(last / 2); - // delete UI item - if (filament_id < p->combos_filament.size()) { - const int last = p->combos_filament.size() - 1; - auto sizer_filaments = this->p->sizer_filaments->GetItem(last % 2)->GetSizer(); - sizer_filaments->Remove(last / 2); + PlaterPresetComboBox* to_delete_combox = p->combos_filament[filament_id]; + (*p->combos_filament[last]).Destroy(); + p->combos_filament.pop_back(); - PlaterPresetComboBox* to_delete_combox = p->combos_filament[filament_id]; - (*p->combos_filament[last]).Destroy(); - p->combos_filament.pop_back(); + // BBS: filament double columns + auto sizer_filaments0 = this->p->sizer_filaments->GetItem((size_t) 0)->GetSizer(); + auto sizer_filaments1 = this->p->sizer_filaments->GetItem(1)->GetSizer(); + if (p->combos_filament.size() < 2) { + sizer_filaments1->Clear(); + } else { + size_t c0 = sizer_filaments0->GetChildren().GetCount(); + size_t c1 = sizer_filaments1->GetChildren().GetCount(); + if (c0 < c1) + sizer_filaments1->Remove(c1 - 1); + else if (c0 > c1) + sizer_filaments1->AddStretchSpacer(1); + } + } - // BBS: filament double columns - auto sizer_filaments0 = this->p->sizer_filaments->GetItem((size_t) 0)->GetSizer(); - auto sizer_filaments1 = this->p->sizer_filaments->GetItem(1)->GetSizer(); - if (p->combos_filament.size() < 2) { - sizer_filaments1->Clear(); - } else { - size_t c0 = sizer_filaments0->GetChildren().GetCount(); - size_t c1 = sizer_filaments1->GetChildren().GetCount(); - if (c0 < c1) - sizer_filaments1->Remove(c1 - 1); - else if (c0 > c1) - sizer_filaments1->AddStretchSpacer(1); + show_SEMM_buttons(); // ORCA + + for (size_t idx = filament_id ; idx < p->combos_filament.size(); ++idx) { + p->combos_filament[idx]->update(); } } - show_SEMM_buttons(); // ORCA - - for (size_t idx = filament_id ; idx < p->combos_filament.size(); ++idx) { - p->combos_filament[idx]->update(); - } - update_filaments_area_height(); // ORCA + recalc_filament_scroll_sizes(); + update_mixed_filament_list(); Layout(); p->m_panel_filament_title->Refresh(); update_ui_from_settings(); - dynamic_filament_list.update(); + update_dynamic_filament_list(); } void Sidebar::add_filament() { @@ -4056,20 +5410,38 @@ void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) { filament_id = filament_count; } - if (filament_id > filament_count) + // Mixed (virtual) slots have no combo of their own, so their config index lies past + // filament_count; bound explicit ids by the total slot count instead. + size_t total_filaments = wxGetApp().preset_bundle->filament_presets.size(); + if (filament_id > filament_count && filament_id >= total_filaments) return; - if (wxGetApp().preset_bundle->is_the_only_edited_filament(filament_id) || (filament_id == 0)) { - wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0], false, "", true); + bool is_mixed = (filament_id >= p->combos_filament.size()); + + if (!is_mixed) { + if (wxGetApp().preset_bundle->is_the_only_edited_filament(filament_id) || (filament_id == 0)) { + wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0], false, "", true); + } + + if (p->editing_filament == filament_id || p->editing_filament >= filament_count) { + p->editing_filament = -1; + } } - if (p->editing_filament == filament_id || p->editing_filament >= filament_count) { - p->editing_filament = -1; - } + // update_num_filaments() shrinks filament_is_mixed along with the other per-filament arrays, + // so snapshot it first — the paint cleanup below needs to know which slots were mixed + // *before* the delete to avoid discarding assignments to still-valid mixed slots. + std::vector<unsigned char> is_mixed_snapshot; + if (auto* opt = wxGetApp().preset_bundle->project_config.option<ConfigOptionBools>("filament_is_mixed")) + is_mixed_snapshot = opt->values; wxGetApp().preset_bundle->update_num_filaments(filament_id); - wxGetApp().plater()->get_partplate_list().on_filament_deleted(filament_count, filament_id); - wxGetApp().plater()->on_filaments_delete(filament_count, filament_id, replace_filament_id > (int)filament_id ? (replace_filament_id - 1) : replace_filament_id); + + // filament_count only counts physical combos, so with mixed slots present it is not the + // new number of slots; recompute from the shrunk preset list for the downstream updates. + size_t total_after_delete = wxGetApp().preset_bundle->filament_presets.size(); + wxGetApp().plater()->get_partplate_list().on_filament_deleted(total_after_delete, filament_id); + wxGetApp().plater()->on_filaments_delete(total_after_delete, filament_id, replace_filament_id > (int)filament_id ? (replace_filament_id - 1) : replace_filament_id, is_mixed_snapshot); wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); @@ -4086,6 +5458,36 @@ void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) { void Sidebar::change_filament(size_t from_id, size_t to_id) { + // Merging a physical filament into a mixed one that lists it as a component would delete + // the very filament the mix depends on, leaving it broken. Warn before doing so. + auto& pb = *wxGetApp().preset_bundle; + bool from_is_physical = !pb.is_mixed_filament(from_id); + bool to_is_mixed = pb.is_mixed_filament(to_id); + + if (from_is_physical && to_is_mixed) { + auto* comp_opt = pb.project_config.option<ConfigOptionStrings>("filament_mixed_components"); + if (comp_opt && to_id < comp_opt->values.size()) { + auto comps = Slic3r::parse_mixed_components(comp_opt->values[to_id]); + unsigned int from_1based = (unsigned int)from_id + 1; + bool target_uses_source = false; + for (unsigned int c : comps) { + if (c == from_1based) { + target_uses_source = true; + break; + } + } + if (target_uses_source) { + int ret = wxMessageBox( + _L("The target mixed filament uses this physical filament as a component. " + "Merging will remove this physical filament and may invalidate the mixed filament. Continue?"), + _L("Warning"), + wxOK | wxCANCEL | wxICON_WARNING); + if (ret != wxOK) + return; + } + } + } + delete_filament(from_id, int(to_id)); } @@ -4097,18 +5499,96 @@ void Sidebar::edit_filament() p->editing_filament = p->m_menu_filament_id; // sync with TabPresetComboxBox's m_filament_idx } -void Sidebar::add_custom_filament(wxColour new_col) { +void Sidebar::add_custom_filament(wxColour new_col, const std::string& preset_name, bool /*skip_preset_validation*/) { if (is_new_project_in_gcode3mf()) { return; } if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) return; + if (wxGetApp().preset_bundle->filament_presets.size() >= MAXIMUM_EXTRUDER_NUMBER) return; - int filament_count = p->combos_filament.size() + 1; + // Mixed-color slots are kept at the tail of the filament arrays, so a new physical + // filament has to be inserted just after the last physical one rather than appended. + // Count off filament_is_mixed, not filament_presets or the combos: the extruder-count spinner + // reaches this before the sidebar has rebuilt, and update_multi_material_filament_presets() + // can have grown filament_presets alone. + auto *bundle = wxGetApp().preset_bundle; + size_t insert_pos = bundle->num_physical_filaments(); + size_t total = insert_pos + bundle->num_mixed_filaments(); + int filament_count = (int)(total + 1); std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); - wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color); + bundle->set_num_filaments(filament_count, new_color); + + // Maintain physical-first ordering: rotate the new slot from end to insert_pos. + // No mixed slots -> insert_pos == total -> every rotate below is a no-op. + if (insert_pos < total) { + auto& presets = wxGetApp().preset_bundle->filament_presets; + std::rotate(presets.begin() + insert_pos, presets.begin() + total, presets.end()); + + auto& project_config = wxGetApp().preset_bundle->project_config; + auto& ams_mc = wxGetApp().preset_bundle->ams_multi_color_filment; + + auto rotate_strings = [&](const char* key) { + if (auto* opt = project_config.option<ConfigOptionStrings>(key)) + if (opt->values.size() > total) + std::rotate(opt->values.begin() + insert_pos, opt->values.begin() + total, opt->values.end()); + }; + auto rotate_ints = [&](const char* key) { + if (auto* opt = project_config.option<ConfigOptionInts>(key)) + if (opt->values.size() > total) + std::rotate(opt->values.begin() + insert_pos, opt->values.begin() + total, opt->values.end()); + }; + auto rotate_bools = [&](const char* key) { + if (auto* opt = project_config.option<ConfigOptionBools>(key)) + if (opt->values.size() > total) + std::rotate(opt->values.begin() + insert_pos, opt->values.begin() + total, opt->values.end()); + }; + + rotate_strings("filament_colour"); + rotate_strings("filament_multi_colour"); + rotate_strings("filament_colour_type"); + rotate_ints("filament_map"); + rotate_ints("filament_nozzle_map"); + rotate_ints("filament_volume_map"); + rotate_bools("filament_is_mixed"); + rotate_strings("filament_mixed_components"); + rotate_strings("filament_mixed_sublayer_ratios"); + rotate_bools("filament_mixed_gradient"); + rotate_strings("filament_mixed_gradient_range"); + rotate_strings("filament_mixed_gradient_curve"); + rotate_bools("filament_mixed_gradient_per_part"); + + if (ams_mc.size() > total) + std::rotate(ams_mc.begin() + insert_pos, ams_mc.begin() + total, ams_mc.end()); + + // Remap object/volume extruder IDs and paint data: anything >= insert_pos+1 (1-based) shifts up by 1 + int threshold_1based = (int)(insert_pos + 1); + auto ebt_threshold = EnforcerBlockerType(threshold_1based); + for (auto* obj : wxGetApp().plater()->model().objects) { + if (obj->config.has("extruder")) { + int ext = obj->config.extruder(); + if (ext >= threshold_1based) + obj->config.set("extruder", ext + 1); + } + for (auto* vol : obj->volumes) { + if (vol->config.has("extruder")) { + int ext = vol->config.extruder(); + if (ext >= threshold_1based) + vol->config.set("extruder", ext + 1); + } + vol->mmu_segmentation_facets.shift_states_above(*vol, ebt_threshold, +1); + } + } + } + + if (!preset_name.empty() && + wxGetApp().preset_bundle->filaments.find_preset(preset_name, false) && + insert_pos < wxGetApp().preset_bundle->filament_presets.size()) { + wxGetApp().preset_bundle->filament_presets[insert_pos] = preset_name; + } + wxGetApp().plater()->get_partplate_list().on_filament_added(filament_count); wxGetApp().plater()->on_filament_count_change(filament_count); wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); - auto_calc_flushing_volumes(filament_count - 1); + auto_calc_flushing_volumes(insert_pos); } bool Sidebar::is_new_project_in_gcode3mf() @@ -4406,7 +5886,7 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) if (m_sync_dlg->is_dirty_filament()) { wxGetApp().get_tab(Preset::TYPE_FILAMENT)->select_preset(wxGetApp().preset_bundle->filament_presets[0], false, "", false, true); wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); - dynamic_filament_list.update(); + update_dynamic_filament_list(); } m_sync_dlg->set_check_dirty_fialment(false); dlg_res = m_sync_dlg->ShowModal(); @@ -4662,6 +6142,7 @@ void Sidebar::enable_nozzle_count_edit(bool enable) void Sidebar::update_dynamic_filament_list() { dynamic_filament_list.update(); + dynamic_physical_filament_list.update(); } PlaterPresetComboBox* Sidebar::printer_combox() @@ -5041,6 +6522,10 @@ void Sidebar::auto_calc_flushing_volumes(const int filament_idx, const int extru void Sidebar::auto_calc_flushing_volumes_internal(const int modify_id, const int extruder_id) { auto& preset_bundle = wxGetApp().preset_bundle; + // A mixed-colour slot is virtual and is never flushed to or from: leave its row and column + // alone (the flushing dialog hides them and only compares physical slots). + if (modify_id >= 0 && preset_bundle->is_mixed_filament((size_t)modify_id)) + return; auto& project_config = preset_bundle->project_config; const auto& full_config = wxGetApp().preset_bundle->full_config(); auto& ams_multi_color_filament = preset_bundle->ams_multi_color_filment; @@ -5079,6 +6564,8 @@ void Sidebar::auto_calc_flushing_volumes_internal(const int modify_id, const int if (modify_id >= 0 && modify_id < multi_colours.size()) { for (int i = 0; i < multi_colours.size(); ++i) { + if (preset_bundle->is_mixed_filament((size_t)i)) + continue; // from to modify int from_idx = i; if (from_idx != modify_id) { @@ -5448,9 +6935,34 @@ struct Plater::priv BoundingBox scaled_bed_shape_bb() const; // BBS: backup & restore + using LoadProgressCallback = std::function<bool(int, const wxString&)>; std::vector<size_t> load_files(const std::vector<fs::path>& input_files, LoadStrategy strategy, bool ask_multi = false); std::vector<size_t> load_model_objects(const ModelObjectPtrs& model_objects, bool allow_negative_z = false, bool split_object = false, bool auto_drop = true); + // Texture-to-color import: a mesh loaded with UVs + a texture map gets its faces clustered + // into printable colours, which are then matched against (or added to) the filament list. + struct TextureImportResult { + Slic3r::PaintedMesh painted; + std::vector<Slic3r::FilamentMatch> matches; + std::vector<std::array<float, 4>> new_filament_colors; + std::vector<std::string> new_filament_preset_names; + std::vector<TextureNewMixedFilament> new_mixed_filaments; + std::vector<TextureFilamentEntry> filament_entries; + size_t existing_filament_count = 0; + bool skipped = false; + bool fallback_to_geometry_only = false; + wxString fallback_warning; + }; + + bool run_textured_mesh_import_dialog(Slic3r::Model& loaded_model, TextureImportResult& result, + std::function<bool()> cancel_callback = {}, + std::function<bool(int)> progress_callback = {}); + void apply_textured_mesh_import_result(Slic3r::Model& loaded_model, const std::vector<size_t>& obj_idxs, + const TextureImportResult& result, + LoadProgressCallback progress_callback = {}, bool update_scene = true); + void handle_textured_mesh_import(Slic3r::Model& model, const std::vector<size_t>& obj_idxs, + std::function<bool()> cancel_callback = {}); + fs::path get_export_file_path(GUI::FileType file_type); wxString get_export_file(GUI::FileType file_type); @@ -5625,6 +7137,7 @@ struct Plater::priv void on_action_slice_all(SimpleEvent&); void on_action_publish(wxCommandEvent &evt); void on_action_print_plate(SimpleEvent&); + void open_machine_select_dialog(int plate_idx, PrintFromType print_type = PrintFromType::FROM_NORMAL); void on_action_print_all(SimpleEvent&); void on_action_export_gcode(SimpleEvent&); void on_action_send_gcode(SimpleEvent&); @@ -5773,6 +7286,8 @@ private: bool show_warning_dialog { false }; }; +Plater::~Plater() = default; + const std::regex Plater::priv::pattern_bundle(".*[.](amf|amf[.]xml|zip[.]amf|3mf)", std::regex::icase); const std::regex Plater::priv::pattern_3mf(".*3mf", std::regex::icase); const std::regex Plater::priv::pattern_zip_amf(".*[.]zip[.]amf", std::regex::icase); @@ -5787,7 +7302,7 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &fi #endif // WIN32 m_mainframe.Raise(); - m_mainframe.select_tab(size_t(MainFrame::tp3DEditor)); + m_mainframe.select_tab(TAB_ID_PREPARE); if (wxGetApp().is_editor()) m_plater.select_view_3D("3D"); @@ -5822,7 +7337,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) "brim_width", "brim_object_gap", "brim_flow_ratio", "brim_use_efc_outline", "combine_brims", "brim_type", "nozzle_diameter", "single_extruder_multi_material", "preferred_orientation", "enable_prime_tower", "wipe_tower_x", "wipe_tower_y", "prime_tower_width", "prime_tower_brim_width", "prime_tower_skip_points", "prime_tower_enable_framework", "prime_tower_infill_gap", "prime_volume", - "extruder_colour", "filament_colour", "filament_type", "material_colour", "printable_height", "extruder_printable_height", "printer_model", "printer_technology", + "extruder_colour", "filament_colour", "filament_type", "filament_is_support", "material_colour", "printable_height", "extruder_printable_height", "printer_model", "printer_technology", // These values are necessary to construct SlicingParameters by the Canvas3D variable layer height editor. "layer_height", "initial_layer_print_height", "min_layer_height", "max_layer_height", "wall_loops", "outer_wall_filament_id", "inner_wall_filament_id", "sparse_infill_density", "sparse_infill_filament_id", "top_shell_layers", @@ -5982,7 +7497,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) // Keep tracking the current sidebar size, by storing it using `best_size`, which will be stored // in the config and re-applied when the app is opened again. - this->sidebar->Bind(wxEVT_IDLE, [&sidebar, this](wxIdleEvent& e) { + this->sidebar->Bind(wxEVT_IDLE, [&sidebar](wxIdleEvent& e) { if (sidebar.IsShown() && sidebar.IsDocked() && sidebar.rect.GetWidth() > 0) { sidebar.BestSize(sidebar.rect.GetWidth(), sidebar.best_size.GetHeight()); } @@ -6281,8 +7796,8 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) wxGetApp().removable_drive_manager()->init(this->q); #ifdef _WIN32 //Trigger enumeration of removable media on Win32 notification. - this->q->Bind(EVT_VOLUME_ATTACHED, [this](VolumeAttachedEvent &evt) { wxGetApp().removable_drive_manager()->volumes_changed(); }); - this->q->Bind(EVT_VOLUME_DETACHED, [this](VolumeDetachedEvent &evt) { wxGetApp().removable_drive_manager()->volumes_changed(); }); + this->q->Bind(EVT_VOLUME_ATTACHED, [](VolumeAttachedEvent &evt) { wxGetApp().removable_drive_manager()->volumes_changed(); }); + this->q->Bind(EVT_VOLUME_DETACHED, [](VolumeDetachedEvent &evt) { wxGetApp().removable_drive_manager()->volumes_changed(); }); #endif /* _WIN32 */ } @@ -6569,9 +8084,9 @@ void Plater::priv::select_next_view_3D() { if (current_panel == view3D) - wxGetApp().mainframe->select_tab(size_t(MainFrame::tpPreview)); + wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW); else if (current_panel == preview) - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); // else if (current_panel == assemble_view) // set_current_panel(view3D); } @@ -6763,7 +8278,8 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_ bool dlg_cont = true; bool is_user_cancel = false; bool translate_old = false; - int current_width = 0, current_depth = 0, current_height = 0, project_filament_count = 1; + int current_width = 0, current_depth = 0, project_filament_count = 1; + double current_height = 0; if (input_files.empty()) return std::vector<size_t>(); @@ -6839,7 +8355,6 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_ const float LOAD_MODEL_RATIO = 0.9; for (size_t i = 0; i < input_files.size(); ++i) { - int file_percent = 0; #ifdef _WIN32 auto path = input_files[i]; @@ -6888,7 +8403,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_ q->skip_thumbnail_invalid = true; model = Slic3r::Model::read_from_archive(path.string(), &config_loaded, &config_substitutions, en_3mf_file_type, strategy, &plate_data, &project_presets, &file_version, - [this, &dlg, real_filename, &progress_percent, &file_percent, stage_percent, INPUT_FILES_RATIO, total_files, i, + [&dlg, real_filename, &progress_percent, stage_percent, INPUT_FILES_RATIO, total_files, i, &is_user_cancel](int import_stage, int current, int total, bool &cancel) { bool cont = true; float percent_float = (100.0f * (float)i / (float)total_files) + INPUT_FILES_RATIO * ((float)stage_percent[import_stage] + (float)current * (float)(stage_percent[import_stage + 1] - stage_percent[import_stage]) /(float) total) / (float)total_files; @@ -7471,7 +8986,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_ Semver file_version; //ObjImportColorFn obj_color_fun=nullptr; - auto obj_color_fun = [this, &path](ObjDialogInOut &in_out) { + auto obj_color_fun = [&path](ObjDialogInOut &in_out) { if (!boost::iends_with(path.string(), ".obj")) { return; } const std::vector<std::string> extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); @@ -7488,7 +9003,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_ if (angle <= 0) angle = 0.5; bool split_compound = wxGetApp().app_config->get_bool("is_split_compound"); model = Slic3r::Model:: read_from_step(path.string(), strategy, - [this, &dlg, real_filename, &progress_percent, &file_percent, step_percent, INPUT_FILES_RATIO, total_files, i](int load_stage, int current, int total, bool &cancel) + [&dlg, real_filename, &progress_percent, step_percent, INPUT_FILES_RATIO, total_files, i](int load_stage, int current, int total, bool &cancel) { bool cont = true; float percent_float = (100.0f * (float)i / (float)total_files) + INPUT_FILES_RATIO * ((float)step_percent[load_stage] + (float)current * (float)(step_percent[load_stage + 1] - step_percent[load_stage]) / (float)total) / (float)total_files; @@ -7502,7 +9017,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_ if (!isUtf8StepFile) { const auto no_warn = wxGetApp().app_config->get_bool("step_not_utf8_no_warn"); if (!no_warn) { - MessageDialog dlg(nullptr, _L("Component name(s) inside step file not in UTF8 format!") + "\n\n" + _L("Because of unsupported text encoding, garbage characters may appear!"), + MessageDialog dlg(nullptr, _L("Component name(s) inside step file not in UTF-8 format!") + "\n\n" + _L("Because of unsupported text encoding, garbage characters may appear!"), wxString(SLIC3R_APP_FULL_NAME " - ") + _L("Attention!"), wxOK | wxICON_INFORMATION); dlg.show_dsa_button(_L("Remember my choice.")); dlg.ShowModal(); @@ -7512,7 +9027,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_ } } }, - [this, &path, &is_user_cancel, &linear, &angle, &split_compound](Slic3r::Step& file, double& linear_value, double& angle_value, bool& is_split)-> int { + [&is_user_cancel, &linear, &angle, &split_compound](Slic3r::Step& file, double& linear_value, double& angle_value, bool& is_split)-> int { if (wxGetApp().app_config->get_bool("enable_step_mesh_setting")) { StepMeshDialog mesh_dlg(nullptr, file, linear, angle); if (mesh_dlg.ShowModal() == wxID_OK) { @@ -7533,7 +9048,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_ }else { model = Slic3r::Model:: read_from_file( path.string(), nullptr, nullptr, strategy, &plate_data, &project_presets, &is_xxx, &file_version, nullptr, - [this, &dlg, real_filename, &progress_percent, &file_percent, INPUT_FILES_RATIO, total_files, i, &designer_model_id, &designer_country_code](int current, int total, bool &cancel, std::string &mode_id, std::string &code) + [&dlg, real_filename, &progress_percent, INPUT_FILES_RATIO, total_files, i, &designer_model_id, &designer_country_code](int current, int total, bool &cancel, std::string &mode_id, std::string &code) { designer_model_id = mode_id; designer_country_code = code; @@ -7615,7 +9130,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_ else if (model.looks_like_saved_in_meters()) { // BBS do not handle look like in meters MessageDialog dlg(q, - format_wxstr(_L("The object from file %s is too small, and may be in meters or inches.\n Do you want to scale to millimeters\?"), + format_wxstr(_L("The object from file %s is too small, and may be in meters or inches.\nDo you want to scale to millimeters\?"), from_path(filename)), _L("Object too small"), wxICON_QUESTION | wxYES_NO); int answer = dlg.ShowModal(); @@ -7623,7 +9138,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_ } else if (model.looks_like_imperial_units()) { // BBS do not handle look like in meters MessageDialog dlg(q, - format_wxstr(_L("The object from file %s is too small, and may be in meters or inches.\n Do you want to scale to millimeters\?"), + format_wxstr(_L("The object from file %s is too small, and may be in meters or inches.\nDo you want to scale to millimeters\?"), from_path(filename)), _L("Object too small"), wxICON_QUESTION | wxYES_NO); int answer = dlg.ShowModal(); @@ -7718,6 +9233,38 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_ q->model().load_from(model); load_auxiliary_files(); } + // Texture-to-color: a mesh that arrived with UVs and a decoded texture gets its + // faces clustered into printable colours and matched against the filament list, + // before the objects are handed to the plater. Inert for every other model. + if (model.texture_mesh && has_importable_texture(*model.texture_mesh)) { + TextureImportResult texture_import_result; + auto cancel_cb = [&dlg, &dlg_cont]() { return !dlg_cont || dlg.WasCancelled(); }; + auto progress_cb = [&dlg, &dlg_cont, &progress_percent](int percent) { + progress_percent = std::clamp(percent, 0, 100); + dlg_cont = dlg.Update(progress_percent, _L("Matching textures to filaments")); + return dlg_cont; + }; + if (!run_textured_mesh_import_dialog(model, texture_import_result, cancel_cb, progress_cb)) { + q->skip_thumbnail_invalid = false; + return empty_result; + } + if (texture_import_result.fallback_to_geometry_only && !texture_import_result.fallback_warning.empty()) { + MessageDialog(q, texture_import_result.fallback_warning, + _L("Texture Import Warning"), + wxOK | wxICON_WARNING).ShowModal(); + } + if (!texture_import_result.painted.face_colors.empty()) { + std::vector<size_t> texture_object_idxs(model.objects.size()); + std::iota(texture_object_idxs.begin(), texture_object_idxs.end(), 0); + auto apply_progress_cb = [&dlg](int percent, const wxString& msg) { + dlg.Update(std::clamp(percent, 0, 100), msg); + return true; + }; + apply_textured_mesh_import_result(model, texture_object_idxs, texture_import_result, + apply_progress_cb, false); + } + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", before load_model_objects, count %1%")%model.objects.size(); auto loaded_idxs = load_model_objects(model.objects, is_project_file); obj_idxs.insert(obj_idxs.end(), loaded_idxs.begin(), loaded_idxs.end()); @@ -7870,7 +9417,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_ q->select_plate(first_plate_index); //set to 3d tab q->select_view_3D("Preview"); - wxGetApp().mainframe->select_tab(MainFrame::tpPreview); + wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW); } else { //set to 3d tab @@ -7889,7 +9436,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_ else { //always set to 3D after loading files q->select_view_3D("3D"); - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); } if (load_model) { @@ -8321,7 +9868,11 @@ void Plater::priv::object_list_changed() // BBS //sidebar->enable_buttons(!model.objects.empty() && !export_in_progress && model_fits && part_plate->has_printable_instances()); - bool can_slice = !model.objects.empty() && !export_in_progress && model_fits && part_plate->has_printable_instances(); + // A mixed filament with deleted or type-mismatched components cannot be resolved at slicing + // time, so block the slice buttons the same way MainFrame::get_enable_slice_status() does. + bool mixed_broken = sidebar->has_broken_mixed_filament(); + bool can_slice = !model.objects.empty() && !export_in_progress && model_fits && part_plate->has_printable_instances() + && !mixed_broken; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": can_slice %1%, model_fits= %2%, export_in_progress %3%, has_printable_instances %4% ")%can_slice %model_fits %export_in_progress %part_plate->has_printable_instances(); main_frame->update_slice_print_status(MainFrame::eEventObjectUpdate, can_slice); @@ -8797,7 +10348,7 @@ void Plater::priv::process_validation_warning(StringObjectException const &warni } } - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (inst_idx != -1) { auto* model = wxGetApp().obj_list()->GetModel(); @@ -8826,7 +10377,7 @@ void Plater::priv::process_validation_warning(StringObjectException const &warni } else { auto iter = id.id ? std::find_if(objects.begin(), objects.end(), [id](auto o) { return o->id() == id; }) : objects.end(); if (iter != objects.end()) { - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); wxGetApp().obj_list()->select_items({{*iter, nullptr}}); wxGetApp().obj_list()->update_selections_on_canvas(); } @@ -9493,7 +11044,7 @@ void Plater::priv::replace_all_with_stl() return; } - std::string status = _L("Replaced with 3D files from directory:\n").ToStdString() + out_path.string() + "\n\n"; + wxString status = _L("Replaced with 3D files from directory:\n") + from_u8(out_path.string()) + "\n\n"; for (unsigned int idx : volume_idxs) { const GLVolume* v = selection.get_volume(idx); @@ -9513,13 +11064,13 @@ void Plater::priv::replace_all_with_stl() std::string volume_name = volume->name; if (new_path == input_path) { - status += boost::str(boost::format(_L("✖ Skipped %1%: same file.\n").ToStdString()) % volume_name); + status += wxString::Format(_L("✖ Skipped %s: same file.\n"), from_u8(volume_name)); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " skipping replace volume : same filename " << new_path; continue; } if (!fs::exists(new_path)) { - status += boost::str(boost::format(_L("✖ Skipped %1%: file does not exist.\n").ToStdString()) % volume_name); + status += wxString::Format(_L("✖ Skipped %s: file does not exist.\n"), from_u8(volume_name)); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " cannot replace volume : filen does not exist " << new_path; continue; } @@ -9527,12 +11078,12 @@ void Plater::priv::replace_all_with_stl() BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " replacing volume : " << input_path << " with " << new_path; if (!replace_volume_with_stl(object_idx, volume_idx, new_path, _u8L("Replace with 3D file"))) { - status += boost::str(boost::format(_L("✖ Skipped %1%: failed to replace.\n").ToStdString()) % volume_name); + status += wxString::Format(_L("✖ Skipped %s: failed to replace.\n"), from_u8(volume_name)); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " cannot replace volume : failed to replace with " << new_path; continue; } - status += boost::str(boost::format(_L("✔ Replaced %1%.\n").ToStdString()) % volume_name); + status += wxString::Format(_L("✔ Replaced %s.\n"), from_u8(volume_name)); } // update 3D scene @@ -9743,7 +11294,7 @@ void Plater::priv::reload_from_disk() // load one file at a time for (size_t i = 0; i < input_paths.size(); ++i) { const auto& path = input_paths[i].string(); - auto obj_color_fun = [this, &path](ObjDialogInOut &in_out) { + auto obj_color_fun = [&path](ObjDialogInOut &in_out) { if (!boost::iends_with(path, ".obj")) { return; } const std::vector<std::string> extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); ObjColorDialog color_dlg(nullptr, in_out, extruder_colours, Sidebar::should_show_SEMM_buttons()); @@ -10069,9 +11620,11 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) if (current_plate->is_slice_result_valid() && this->model.objects.empty() && !current_has_print_instances) only_has_gcode_need_preview = true; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": from set_current_panel, no_slice %1%, export_in_progress %2%, model_fits %3%, m_is_slicing %4%")%no_slice%export_in_progress%model_fits%m_is_slicing; + bool mixed_broken = sidebar->has_broken_mixed_filament(); - if (!no_slice && !this->model.objects.empty() && !export_in_progress && model_fits && current_has_print_instances) + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": from set_current_panel, no_slice %1%, export_in_progress %2%, model_fits %3%, m_is_slicing %4%, mixed_broken %5%")%no_slice%export_in_progress%model_fits%m_is_slicing%mixed_broken; + + if (!no_slice && !this->model.objects.empty() && !export_in_progress && model_fits && current_has_print_instances && !mixed_broken) { //if already running in background, not relice here //BBS: add more judge for slicing @@ -11166,18 +12719,23 @@ void Plater::priv::on_action_print_plate(SimpleEvent&) } PresetBundle& preset_bundle = *wxGetApp().preset_bundle; - if (preset_bundle.use_bbl_network()) { - // BBS - if (!m_select_machine_dlg) - m_select_machine_dlg = new SelectMachineDialog(q); - m_select_machine_dlg->set_print_type(PrintFromType::FROM_NORMAL); - m_select_machine_dlg->prepare(partplate_list.get_curr_plate_index()); - m_select_machine_dlg->ShowModal(); + if (preset_bundle.use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents")) { + open_machine_select_dialog(partplate_list.get_curr_plate_index()); } else { q->send_gcode_legacy(PLATE_CURRENT_IDX, nullptr); } } +void Plater::priv::open_machine_select_dialog(int plate_idx, PrintFromType print_type) +{ + // BBS + if (!m_select_machine_dlg) + m_select_machine_dlg = new SelectMachineDialog(q); + m_select_machine_dlg->set_print_type(print_type); + m_select_machine_dlg->prepare(plate_idx); + m_select_machine_dlg->ShowModal(); +} + void Plater::priv::on_action_send_to_multi_machine(SimpleEvent&) { if (!m_send_multi_dlg) @@ -11193,10 +12751,7 @@ void Plater::priv::on_action_print_plate_from_sdcard(SimpleEvent&) } //BBS - if (!m_select_machine_dlg) m_select_machine_dlg = new SelectMachineDialog(q); - m_select_machine_dlg->set_print_type(PrintFromType::FROM_SDCARD_VIEW); - m_select_machine_dlg->prepare(0); - m_select_machine_dlg->ShowModal(); + open_machine_select_dialog(0, PrintFromType::FROM_SDCARD_VIEW); } void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) @@ -11208,16 +12763,22 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) } const int new_sel = e.GetSelection(); - sidebar_layout.show = new_sel == MainFrame::tp3DEditor || new_sel == MainFrame::tpPreview; + if (new_sel == wxNOT_FOUND) { + // GetPage(new_sel) below needs a valid index. + e.Skip(); + return; + } + const wxString new_name = main_frame->m_tabpanel->GetPageName(new_sel); + sidebar_layout.show = new_name == TAB_ID_PREPARE || new_name == TAB_ID_PREVIEW; update_sidebar(); int old_sel = e.GetOldSelection(); - const bool is_printer_agent_plugin = NetworkAgentFactory::is_current_printer_agent_plugin(); + const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); const bool use_native_device_tab = wxGetApp().preset_bundle && - (wxGetApp().preset_bundle->use_bbl_device_tab() || is_printer_agent_plugin); - if (use_native_device_tab && new_sel == MainFrame::tpMonitor) { + (wxGetApp().preset_bundle->use_bbl_device_tab() || use_printer_agents); + if (use_native_device_tab && new_name == TAB_ID_MONITOR) { // BBL network module is only required for BBL-vendor printers. // Non-BBL Python plugins (e.g. moonraker) drive the Device tab without it. - if (!is_printer_agent_plugin && wxGetApp().preset_bundle->is_bbl_vendor() && !Slic3r::NetworkAgent::is_network_module_loaded()) { + if (!use_printer_agents && wxGetApp().preset_bundle->is_bbl_vendor() && !Slic3r::NetworkAgent::is_network_module_loaded()) { e.Veto(); BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2%, lack of network plugins") % old_sel % new_sel; if (q) { @@ -11226,9 +12787,17 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) } } } else { - if (new_sel == MainFrame::tpMonitor && wxGetApp().preset_bundle != nullptr) { + // Pointer test, not a name lookup: in printer-agents mode this page is TAB_ID_MONITOR_WEB + // while the native Device tab holds TAB_ID_MONITOR, and in legacy-web mode it holds + // TAB_ID_MONITOR itself. + const bool selecting_web_device_tab = main_frame->m_printer_view && + main_frame->m_tabpanel->GetPage(new_sel) == main_frame->m_printer_view; + if (selecting_web_device_tab) { + // Use the selected discovered machine when the preset has no host. + main_frame->load_printer_url(); + } else if (new_name == TAB_ID_MONITOR && wxGetApp().preset_bundle != nullptr) { auto cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; - wxString url = cfg.opt_string("print_host_webui").empty() ? cfg.opt_string("print_host") : cfg.opt_string("print_host_webui"); + wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); if (main_frame->m_printer_view && url.empty()) { // It's missing_connection page, reload so that we can replay the gif image main_frame->m_printer_view->reload(); @@ -11273,13 +12842,8 @@ void Plater::priv::on_action_print_all(SimpleEvent&) } PresetBundle& preset_bundle = *wxGetApp().preset_bundle; - if (preset_bundle.use_bbl_network()) { - // BBS - if (!m_select_machine_dlg) - m_select_machine_dlg = new SelectMachineDialog(q); - m_select_machine_dlg->set_print_type(PrintFromType::FROM_NORMAL); - m_select_machine_dlg->prepare(PLATE_ALL_IDX); - m_select_machine_dlg->ShowModal(); + if (preset_bundle.use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents")) { + open_machine_select_dialog(PLATE_ALL_IDX); } else { q->send_gcode_legacy(PLATE_ALL_IDX, nullptr); } @@ -11402,6 +12966,9 @@ void Plater::priv::on_filament_color_changed(wxCommandEvent &event) if (wxGetApp().app_config->get("auto_calculate_flush") != "disabled") { sidebar->auto_calc_flushing_volumes(modify_id); } + + // A mixed slot's colour is derived from its components, so recompute the swatches. + sidebar->update_mixed_filament_list(); } void Plater::priv::install_network_plugin(wxCommandEvent &event) @@ -12094,7 +13661,7 @@ bool Plater::priv::check_ams_status_impl(bool is_slice_all) wxPostEvent(q, SimpleEvent(EVT_GLTOOLBAR_SLICE_ALL)); else wxPostEvent(q, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); - wxGetApp().mainframe->m_tabpanel->SetSelection(MainFrame::TabPosition::tpPreview); + wxGetApp().mainframe->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW); } return false; } @@ -12526,6 +14093,23 @@ bool Plater::priv::can_layers_editing() const void Plater::priv::on_action_layersediting(SimpleEvent&) { + // Sub-layer splitting divides each layer by the mix ratio, so a variable layer height profile + // makes those sub-layer heights uneven and degrades the blend. ConfigManipulation warns for the + // opposite order, when the option is switched on while a variable profile already exists. + if (!view3D->is_layers_editing_enabled()) { + const auto& print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + if (print_config.opt_bool("enable_mixed_color_sublayer")) { + if (wxGetApp().app_config->get("no_warn_mixed_sublayer_variable_layer") != "1") { + MessageDialog dlg(q, + _L("Using variable layer height together with mixed color sublayer may result in poor color mixing quality."), + _L("Warning"), wxICON_WARNING | wxOK); + dlg.show_dsa_button(); + dlg.ShowModal(); + if (dlg.get_checkbox_state()) + wxGetApp().app_config->set("no_warn_mixed_sublayer_variable_layer", "1"); + } + } + } view3D->enable_layers_editing(!view3D->is_layers_editing_enabled()); notification_manager->set_move_from_overlay(view3D->is_layers_editing_enabled()); } @@ -12662,7 +14246,7 @@ void Plater::priv::take_snapshot(const std::string& snapshot_name, const UndoRed ModelWipeTower& tower = model.wipe_tower; tower.positions[plate_idx] = Vec2d(tower_x_opt->get_at(plate_idx), tower_y_opt->get_at(plate_idx)); - tower.rotation = proj_cfg.opt_float("wipe_tower_rotation_angle"); + tower.rotation = config.opt_float("wipe_tower_rotation_angle"); } } const GLGizmosManager& gizmos = get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_gizmos_manager() : view3D->get_canvas3d()->get_gizmos_manager(); @@ -12772,7 +14356,7 @@ void Plater::priv::undo_redo_to(std::vector<UndoRedo::Snapshot>::const_iterator ModelWipeTower& tower = model.wipe_tower; tower.positions[plate_idx] = Vec2d(tower_x_opt->get_at(plate_idx), tower_y_opt->get_at(plate_idx)); - tower.rotation = proj_cfg.opt_float("wipe_tower_rotation_angle"); + tower.rotation = config.opt_float("wipe_tower_rotation_angle"); } } const int layer_range_idx = it_snapshot->snapshot_data.layer_range_idx; @@ -13011,6 +14595,348 @@ void Plater::reset_project_dirty_initial_presets() { p->reset_project_dirty_init void Plater::render_project_state_debug_window() const { p->render_project_state_debug_window(); } #endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW +std::vector<size_t> Plater::mixed_filament_config_indices() const +{ + std::vector<size_t> indices; + auto& config = wxGetApp().preset_bundle->project_config; + auto* opt = config.option<ConfigOptionBools>("filament_is_mixed"); + if (!opt) return indices; + for (size_t i = 0; i < opt->values.size(); ++i) + if (opt->values[i]) indices.push_back(i); + return indices; +} + +std::vector<size_t> Plater::physical_filament_config_indices() const +{ + std::vector<size_t> indices; + auto& config = wxGetApp().preset_bundle->project_config; + auto* opt = config.option<ConfigOptionBools>("filament_is_mixed"); + size_t total = wxGetApp().preset_bundle->filament_presets.size(); + for (size_t i = 0; i < total; ++i) { + if (!opt || i >= opt->values.size() || !opt->values[i]) + indices.push_back(i); + } + return indices; +} + +bool Plater::priv::run_textured_mesh_import_dialog(Slic3r::Model& loaded_model, TextureImportResult& result, + std::function<bool()> cancel_callback, + std::function<bool(int)> progress_callback) +{ + if (!loaded_model.texture_mesh || !has_importable_texture(*loaded_model.texture_mesh)) return false; + + // Defense in depth: if all geometry got dropped earlier (e.g. by a future + // regression of the zero-volume cleanup) but the textured mesh is still + // alive, there is nothing for the dialog to paint onto. Skip the dialog + // gracefully so load_files() can fall through to its "no geometry" + // message instead of making the user round-trip a meaningless matcher. + if (loaded_model.objects.empty()) { + BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: skipping dialog because the loaded model has no geometry objects"; + loaded_model.texture_mesh.reset(); + result.skipped = true; + return true; + } + + const wxString fallback_warning = _L("Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only."); + + BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: opening texture import dialog"; + + std::vector<TextureFilamentEntry> filament_entries; + { + auto& preset_bundle = *wxGetApp().preset_bundle; + auto& project_config = preset_bundle.project_config; + auto* colours_opt = project_config.option<ConfigOptionStrings>("filament_colour"); + auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed"); + auto* type_opt = project_config.option<ConfigOptionStrings>("filament_type"); + auto* components_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components"); + auto* ratios_opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios"); + const size_t total = preset_bundle.filament_presets.size(); + filament_entries.reserve(total); + for (size_t i = 0; i < total; ++i) { + TextureFilamentEntry entry; + entry.kind = (is_mixed_opt && i < is_mixed_opt->values.size() && is_mixed_opt->values[i]) ? + TextureFilamentKind::ExistingMixed : TextureFilamentKind::ExistingPhysical; + entry.dialog_index = (int)filament_entries.size(); + entry.project_config_index = i; + entry.color_hex = (colours_opt && i < colours_opt->values.size()) ? colours_opt->values[i] : "#808080"; + entry.type = (type_opt && i < type_opt->values.size()) ? type_opt->values[i] : ""; + + std::string name; + if (i < preset_bundle.filament_presets.size()) { + auto* preset = preset_bundle.filaments.find_preset(preset_bundle.filament_presets[i]); + if (preset) + name = preset->label(false); + } + if (name.empty()) + name = "Filament " + std::to_string(i + 1); + entry.name = name; + + if (entry.kind == TextureFilamentKind::ExistingMixed) { + if (components_opt && i < components_opt->values.size()) + entry.mixed_components = Slic3r::parse_mixed_components(components_opt->values[i]); + std::vector<double> ratios = Slic3r::parse_mixed_ratios( + ratios_opt && i < ratios_opt->values.size() ? ratios_opt->values[i] : "", + entry.mixed_components.size()); + entry.mixed_ratios.reserve(ratios.size()); + for (double ratio : ratios) + entry.mixed_ratios.push_back((int)std::lround(ratio * 100.0)); + } + filament_entries.push_back(std::move(entry)); + } + } + + TextureImportDialog dlg(q, *loaded_model.texture_mesh, filament_entries, + std::move(cancel_callback), std::move(progress_callback)); + if (dlg.ShowModal() != wxID_OK) { + if (dlg.was_skipped()) { + BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: user skipped texture matching"; + result.skipped = true; + loaded_model.texture_mesh.reset(); + return true; + } + if (dlg.fallback_to_geometry_only()) { + BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: texture import failed, falling back to geometry-only import"; + result.fallback_to_geometry_only = true; + result.fallback_warning = fallback_warning; + loaded_model.texture_mesh.reset(); + return true; + } + BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: user cancelled"; + loaded_model.texture_mesh.reset(); + return false; + } + + auto painted = dlg.get_painted_mesh(); + auto final_matches = dlg.get_matches(); + + if (painted.face_colors.empty() || final_matches.empty()) { + BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: no painting result"; + result.fallback_to_geometry_only = true; + result.fallback_warning = fallback_warning; + loaded_model.texture_mesh.reset(); + return true; + } + + BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: got " << painted.cluster_colors.size() + << " clusters, skipped=" << dlg.was_skipped(); + + result.painted = std::move(painted); + result.matches = std::move(final_matches); + result.new_filament_colors = dlg.get_new_filament_colors(); + result.new_filament_preset_names = dlg.get_new_filament_preset_names(); + result.new_mixed_filaments = dlg.get_new_mixed_filaments(); + result.filament_entries = dlg.get_filament_entries(); + result.existing_filament_count = dlg.get_existing_filament_count(); + result.skipped = dlg.was_skipped(); + return true; +} + +void Plater::priv::apply_textured_mesh_import_result(Slic3r::Model& loaded_model, const std::vector<size_t>& obj_idxs, + const TextureImportResult& result, + LoadProgressCallback progress_callback, bool update_scene) +{ + auto update_apply_progress = [&progress_callback](int percent, const wxString& message) { + return !progress_callback || progress_callback(std::clamp(percent, 0, 100), message); + }; + + const auto& painted = result.painted; + const auto& final_matches = result.matches; + + if (painted.face_colors.empty() || final_matches.empty()) { + BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: no painting result"; + loaded_model.texture_mesh.reset(); + return; + } + + BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: got " << painted.cluster_colors.size() + << " clusters, skipped=" << result.skipped; + if (!update_apply_progress(0, _L("Applying texture colors..."))) + return; + + auto collect_physical_color_strs = []() { + std::vector<std::string> colors; + auto& project_config = wxGetApp().preset_bundle->project_config; + auto* colours_opt = project_config.option<ConfigOptionStrings>("filament_colour"); + auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed"); + const size_t total = wxGetApp().preset_bundle->filament_presets.size(); + for (size_t i = 0; i < total; ++i) { + const bool is_mixed = is_mixed_opt && i < is_mixed_opt->values.size() && is_mixed_opt->values[i]; + if (!is_mixed) + colors.push_back(colours_opt && i < colours_opt->values.size() ? colours_opt->values[i] : "#808080"); + } + return colors; + }; + + const auto& entries = result.filament_entries; + std::vector<int> filament_index_remap(entries.size(), -1); + size_t existing_physical_count = 0; + size_t new_physical_count = 0; + for (const auto& entry : entries) { + if (entry.kind == TextureFilamentKind::ExistingPhysical) + ++existing_physical_count; + else if (entry.kind == TextureFilamentKind::NewPhysical) + ++new_physical_count; + } + + for (const auto& entry : entries) { + if (entry.dialog_index < 0 || entry.dialog_index >= (int)filament_index_remap.size()) + continue; + if (entry.kind == TextureFilamentKind::ExistingPhysical) { + filament_index_remap[entry.dialog_index] = (int)entry.project_config_index; + } else if (entry.kind == TextureFilamentKind::ExistingMixed) { + filament_index_remap[entry.dialog_index] = (int)(entry.project_config_index + new_physical_count); + } + } + + size_t new_physical_order = 0; + for (const auto& entry : entries) { + if (entry.kind != TextureFilamentKind::NewPhysical) + continue; + wxColour new_col(entry.color_hex); + const size_t final_idx = existing_physical_count + new_physical_order; + sidebar->add_custom_filament(new_col, entry.preset_name); + if (entry.dialog_index >= 0 && entry.dialog_index < (int)filament_index_remap.size()) + filament_index_remap[entry.dialog_index] = (int)final_idx; + BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: created pending physical filament dialog=" + << entry.dialog_index << " final=" << final_idx + << " color=" << entry.color_hex + << " preset=" << entry.preset_name; + ++new_physical_order; + } + + std::vector<std::string> physical_colors_for_mixing = collect_physical_color_strs(); + for (const auto& mixed : result.new_mixed_filaments) { + MixedFilamentResult mixed_result; + mixed_result.ratios = mixed.ratios; + mixed_result.components.reserve(mixed.component_dialog_indices.size()); + bool valid_components = true; + for (int component_dialog_idx : mixed.component_dialog_indices) { + if (component_dialog_idx < 0 || component_dialog_idx >= (int)filament_index_remap.size() || + filament_index_remap[component_dialog_idx] < 0) { + valid_components = false; + break; + } + mixed_result.components.push_back((unsigned int)(filament_index_remap[component_dialog_idx] + 1)); + } + if (!valid_components || mixed_result.components.size() < 2 || + mixed_result.components.size() != mixed_result.ratios.size()) { + BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: invalid pending mixed filament dialog=" + << mixed.dialog_index; + continue; + } + + const int final_idx = (int)wxGetApp().preset_bundle->filament_presets.size(); + if (create_mixed_filament_from_result(sidebar, mixed_result, physical_colors_for_mixing)) { + if (mixed.dialog_index >= 0 && mixed.dialog_index < (int)filament_index_remap.size()) + filament_index_remap[mixed.dialog_index] = final_idx; + physical_colors_for_mixing = collect_physical_color_strs(); + BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: created pending mixed filament dialog=" + << mixed.dialog_index << " final=" << final_idx; + } + } + + std::vector<Slic3r::FilamentMatch> remapped_matches = final_matches; + for (auto& m : remapped_matches) { + if (m.filament_index < 0) + continue; + if (m.filament_index < (int)filament_index_remap.size() && filament_index_remap[m.filament_index] >= 0) { + m.filament_index = filament_index_remap[m.filament_index]; + } else { + BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: invalid filament index " + << m.filament_index << " in texture mapping"; + m.filament_index = -1; + } + } + + int min_used_filament_1based = -1; + { + std::map<std::array<std::size_t, 3>, int> color_to_filament; + for (const auto& m : remapped_matches) { + if (m.cluster_index >= 0 && m.cluster_index < (int)painted.cluster_colors.size() && m.filament_index >= 0) + color_to_filament[painted.cluster_colors[m.cluster_index]] = m.filament_index + 1; + } + for (const auto& face_color : painted.face_colors) { + auto it = color_to_filament.find(face_color); + if (it == color_to_filament.end()) + continue; + if (min_used_filament_1based < 0 || it->second < min_used_filament_1based) + min_used_filament_1based = it->second; + } + } + if (min_used_filament_1based < 0) + BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: cannot determine base filament from painted faces"; + + if (!update_apply_progress(25, _L("Applying texture colors..."))) + return; + + for (size_t obj_order = 0; obj_order < obj_idxs.size(); ++obj_order) { + size_t idx = obj_idxs[obj_order]; + if (idx >= loaded_model.objects.size()) continue; + ModelObject* obj = loaded_model.objects[idx]; + if (!obj) continue; + + // painted is derived from the whole textured mesh and is meaningful + // only against a single MODEL_PART volume. Applying it to every + // volume of a multi-part / modifier object would overwrite each + // volume with the same painted geometry. Restrict to the first + // model_part and warn when the object holds more than one. + ModelVolume* target = nullptr; + int part_count = 0; + for (ModelVolume* vol : obj->volumes) { + if (vol && vol->is_model_part()) { + ++part_count; + if (!target) target = vol; + } + } + if (!target) continue; + if (part_count > 1) { + BOOST_LOG_TRIVIAL(warning) + << "handle_textured_mesh_import: object has " << part_count + << " model parts; painting only applied to the first part."; + } + if (Slic3r::apply_painted_mesh_to_volume(painted, remapped_matches, *target) + && min_used_filament_1based > 0) { + target->config.set("extruder", min_used_filament_1based); + obj->config.set("extruder", min_used_filament_1based); + if (update_scene) { + if (auto* obj_list = wxGetApp().obj_list()) { + obj_list->update_objects_list_filament_column(std::max<size_t>( + wxGetApp().filaments_cnt(), (size_t)min_used_filament_1based)); + obj_list->update_info_items(idx); + } + } + BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: set base filament to " + << min_used_filament_1based << " for object index " << idx + << ", object extruder=" << obj->config.extruder() + << ", volume extruder=" << target->config.extruder(); + } + // bbox invalidation is performed inside apply_painted_mesh_to_volume. + obj->ensure_on_bed(); + const int object_percent = 25 + (int)(60 * (obj_order + 1) / std::max<size_t>(obj_idxs.size(), 1)); + if (!update_apply_progress(object_percent, _L("Applying texture colors..."))) + return; + } + + BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: painting applied to model volumes"; + loaded_model.texture_mesh.reset(); + if (update_scene) { + if (!update_apply_progress(90, _L("Updating 3D view..."))) + return; + update(); + } + update_apply_progress(100, _L("Texture colors applied.")); +} + +void Plater::priv::handle_textured_mesh_import(Slic3r::Model& loaded_model, const std::vector<size_t>& obj_idxs, + std::function<bool()> cancel_callback) +{ + TextureImportResult result; + if (!run_textured_mesh_import_dialog(loaded_model, result, std::move(cancel_callback))) + return; + if (!result.painted.face_colors.empty()) + apply_textured_mesh_import_result(loaded_model, obj_idxs, result); +} + Sidebar& Plater::sidebar() { return *p->sidebar; } const Model& Plater::model() const { return p->model; } Model& Plater::model() { return p->model; } @@ -13051,7 +14977,7 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_ get_notification_manager()->clear_all(); if (!silent) - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); //get_partplate_list().reinit(); //get_partplate_list().update_slice_context_to_current_plate(p->background_process); @@ -13200,7 +15126,7 @@ void Plater::load_project(wxString const& filename2, if (!m_exported_file) { p->select_view("topfront"); p->camera.requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); } else { p->partplate_list.select_plate_view(); @@ -13314,7 +15240,7 @@ void Plater::import_model_id(wxString download_info) const int max_retries = 3; /* jump to 3D eidtor */ - wxGetApp().mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); /* prepare progress dialog */ bool cont = true; @@ -13339,7 +15265,7 @@ void Plater::import_model_id(wxString download_info) p->project.reset(); /* prepare project and profile */ - boost::thread import_thread = Slic3r::create_thread([&percent, &cont, &cancel, &retry_count, max_retries, &msg, &target_path, &download_ok, download_url, &filename] { + boost::thread import_thread = Slic3r::create_thread([&percent, &cont, &retry_count, &msg, &target_path, &download_ok, download_url, &filename] { // Orca: NetworkAgent is not needed and only prevents this from running // NetworkAgent* m_agent = Slic3r::GUI::wxGetApp().getAgent(); @@ -13446,7 +15372,7 @@ void Plater::import_model_id(wxString download_info) msg = wxString::Format(_L("Project downloaded %d%%"), percent); } }) - .on_error([&msg, &cont, &retry_count, max_retries](std::string body, std::string error, unsigned http_status) { + .on_error([&msg, &cont, &retry_count](std::string body, std::string error, unsigned http_status) { (void)body; BOOST_LOG_TRIVIAL(error) << format("Error getting: `%1%`: HTTP %2%, %3%", body, @@ -13623,7 +15549,7 @@ void Plater::calib_pa(const Calib_Params& params) { const auto calib_pa_name = wxString::Format(L"Pressure Advance Test"); new_project(false, false, calib_pa_name); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; print_config->set_key_value("overhang_reverse", new ConfigOptionBool(false)); @@ -14104,7 +16030,7 @@ void Plater::calib_flowrate(bool is_linear, int pass, InfillPattern pattern) { if (new_project(false, false, calib_name) == wxID_CANCEL) return; - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (is_linear) { if (pass == 1) @@ -14141,7 +16067,7 @@ void Plater::calib_temp(const Calib_Params& params) { const auto calib_temp_name = wxString::Format(L"Nozzle temperature test"); new_project(false, false, calib_temp_name); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (params.mode != CalibMode::Calib_Temp_Tower) return; if (!add_model(false, Slic3r::resources_dir() + "/calib/temperature_tower/temperature_tower.drc")) @@ -14221,7 +16147,7 @@ void Plater::calib_max_vol_speed(const Calib_Params& params) { const auto calib_vol_speed_name = wxString::Format(L"Max volumetric speed test"); new_project(false, false, calib_vol_speed_name); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (params.mode != CalibMode::Calib_Vol_speed_Tower) return; if (!add_model(false, Slic3r::resources_dir() + "/calib/volumetric_speed/SpeedTestStructure.drc")) @@ -14300,7 +16226,7 @@ void Plater::calib_retraction(const Calib_Params& params) { const auto calib_retraction_name = wxString::Format(L"Retraction"); new_project(false, false, calib_retraction_name); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (params.mode != CalibMode::Calib_Retraction_tower) return; @@ -14342,6 +16268,7 @@ void Plater::calib_retraction(const Calib_Params& params) obj->config.set_key_value("wall_sequence", new ConfigOptionEnum<WallSequence>(WallSequence::InnerOuter)); obj->config.set_key_value("overhang_reverse", new ConfigOptionBool(false)); obj->config.set_key_value("precise_z_height", new ConfigOptionBool(false)); + obj->config.set_key_value("seam_slope_type", new ConfigOptionEnum<SeamScarfType>(SeamScarfType::None)); changed_objects({ 0 }); @@ -14360,7 +16287,7 @@ void Plater::calib_VFA(const Calib_Params& params) { const auto calib_vfa_name = wxString::Format(L"VFA test"); new_project(false, false, calib_vfa_name); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (params.mode != CalibMode::Calib_VFA_Tower) return; @@ -14443,7 +16370,7 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params) { const auto calib_input_shaping_name = wxString::Format(L"Input shaping Frequency test"); new_project(false, false, calib_input_shaping_name); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (params.mode != CalibMode::Calib_Input_shaping_freq) return; @@ -14509,7 +16436,7 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params) { const auto calib_input_shaping_name = wxString::Format(L"Input shaping Damping test"); new_project(false, false, calib_input_shaping_name); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (params.mode != CalibMode::Calib_Input_shaping_damp) return; @@ -14574,7 +16501,7 @@ void Plater::Calib_Cornering(const Calib_Params& params) { const auto Calib_Cornering = wxString::Format(L"Cornering test"); new_project(false, false, Calib_Cornering); - wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); if (params.mode != CalibMode::Calib_Cornering) return; @@ -14707,7 +16634,7 @@ void Plater::load_gcode(const wxString& filename) //p->gcode_result.reset(); //reset_gcode_toolpaths(); p->preview->reload_print(m_only_gcode); - wxGetApp().mainframe->select_tab(MainFrame::tpPreview); + wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW); p->set_current_panel(p->preview, true); p->get_current_canvas3D()->render(); //p->notification_manager->bbl_show_plateinfo_notification(into_u8(_L("Preview only mode for gcode file."))); @@ -15380,7 +17307,7 @@ LoadType determine_load_type(std::string filename, std::string override_setting) wxGetApp().app_config->set("import_project_action", std::to_string(choice)); // BBS: jump to plater panel - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); return load_type; } @@ -15609,7 +17536,7 @@ void Plater::reset_with_confirm() .ShowModal() == wxID_YES) { reset(); // BBS: jump to plater panel - wxGetApp().mainframe->select_tab(size_t(0)); + wxGetApp().mainframe->select_tab(TAB_ID_HOME); } } @@ -16911,6 +18838,15 @@ void Plater::reslice() return; } + // A mixed filament with deleted or type-mismatched components cannot be resolved at slicing + // time. MainFrame::get_enable_slice_status() already disables the Slice button for it, but the + // Preview-tab switch, auto-slice and queued slice events reach reslice() directly, so refuse + // here too instead of letting the engine slice the broken slot as a plain filament. + if (sidebar().has_broken_mixed_filament()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": broken mixed filament detected, refuse to slice"; + return; + } + // In case SLA gizmo is in editing mode, refuse to continue // and notify user that he should leave it first. if (get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode(true)) @@ -17423,7 +19359,7 @@ int Plater::export_config_3mf(int plate_idx, Export3mfProgressFn proFn) //BBS void Plater::send_calibration_job_finished(wxCommandEvent & evt) { - p->main_frame->request_select_tab(MainFrame::TabPosition::tpCalibration); + p->main_frame->request_select_tab(TAB_ID_CALIBRATION); auto calibration_panel = p->main_frame->m_calibration; if (calibration_panel) { auto curr_wizard = static_cast<CalibrationWizard*>(calibration_panel->get_tabpanel()->GetPage(evt.GetInt())); @@ -17455,7 +19391,7 @@ void Plater::print_job_finished(wxCommandEvent &evt) if (!dev) return; dev->set_selected_machine(evt.GetString().ToStdString()); - p->main_frame->request_select_tab(MainFrame::TabPosition::tpMonitor); + p->main_frame->request_select_tab(TAB_ID_MONITOR); //jump to monitor and select device status panel MonitorPanel* curr_monitor = p->main_frame->m_monitor; if(curr_monitor) @@ -17470,7 +19406,7 @@ void Plater::send_job_finished(wxCommandEvent& evt) send_gcode_finish(evt.GetString()); p->hide_send_to_printer_dlg(); - //p->main_frame->request_select_tab(MainFrame::TabPosition::tpMonitor); + //p->main_frame->request_select_tab(TAB_ID_MONITOR); ////jump to monitor and select device status panel //MonitorPanel* curr_monitor = p->main_frame->m_monitor; //if (curr_monitor) @@ -17606,7 +19542,7 @@ void Plater::on_filament_count_change(size_t num_filaments) } } -void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int replace_filament_id) +void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int replace_filament_id, const std::vector<unsigned char>& is_mixed_before_delete) { // only update elements in plater update_filament_colors_in_full_config(); @@ -17620,14 +19556,22 @@ void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int r }*/ // update mmu info + // A volume assigned to a mixed slot legitimately sits past the physical filament count, so + // the paint cleanup must know which slots were mixed. Callers that already shrank the arrays + // pass the pre-delete flags; otherwise read the current ones. + const auto &is_mixed = is_mixed_before_delete.empty() + ? wxGetApp().preset_bundle->project_config.option<ConfigOptionBools>("filament_is_mixed")->values + : is_mixed_before_delete; for (ModelObject *mo : wxGetApp().model().objects) { for (ModelVolume *mv : mo->volumes) { - mv->update_extruder_count_when_delete_filament(num_filaments, filament_id + 1, replace_filament_id + 1); // this function is 1 base + mv->update_extruder_count_when_delete_filament(num_filaments, filament_id + 1, replace_filament_id + 1, is_mixed); // this function is 1 base } } - // update UI - sidebar().on_filaments_delete(filament_id); + // update object/volume/support(object and volume) filament id + // Must run before UI update which triggers update_mixed_filament_list() → + // update_objects_list_filament_column() that clips extruders above total count. + sidebar().obj_list()->update_objects_list_filament_column_when_delete_filament(filament_id, num_filaments, replace_filament_id); // update global support filament static const char *keys[] = {"support_filament", "support_interface_filament"}; @@ -17641,8 +19585,8 @@ void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int r } } - // update object/volume/support(object and volume) filament id - sidebar().obj_list()->update_objects_list_filament_column_when_delete_filament(filament_id, num_filaments, replace_filament_id); + // update UI — runs after remap so update_mixed_filament_list() won't clip remapped extruder IDs + sidebar().on_filaments_delete(filament_id); // update customize gcode for (auto item = p->model.plates_custom_gcodes.begin(); item != p->model.plates_custom_gcodes.end(); ++item) { @@ -17735,6 +19679,7 @@ void Plater::on_config_change(const DynamicPrintConfig &config) update_scheduled = true; // update should be scheduled (for update 3DScene) #2738 if (update_filament_colors_in_full_config()) { + p->sidebar->update_mixed_filament_list(); p->sidebar->obj_list()->update_filament_colors(); p->sidebar->update_dynamic_filament_list(); continue; @@ -17742,6 +19687,15 @@ void Plater::on_config_change(const DynamicPrintConfig &config) } if (opt_key == "filament_type") { update_filament_colors_in_full_config(); + p->sidebar->update_mixed_filament_list(); + continue; + } + // The mixed-filament type check folds filament_is_support into the component type + // (DynamicPrintConfig::get_filament_type -> "PLA-S"), so a support-preset switch must + // refresh the list even though filament_type itself did not change. + if (opt_key == "filament_is_support") { + p->config->set_key_value(opt_key, config.option(opt_key)->clone()); + p->sidebar->update_mixed_filament_list(); continue; } if (opt_key == "material_colour") { @@ -17961,6 +19915,63 @@ std::vector<std::string> Plater::get_extruder_colors_from_plater_config(const GC } } +namespace { + +// A gradient mixed filament fades between its two components over Z, so the UI shows it as a +// two-tone swatch rather than one blended colour. Resolve each slot to its from/to endpoint +// colours; non-gradient slots are left untouched. +struct MixedGradientSlot { + bool is_gradient = false; + std::string color_from; + std::string color_to; +}; + +std::vector<MixedGradientSlot> parse_mixed_gradient_slots(const Slic3r::DynamicPrintConfig& config, size_t slot_count) +{ + std::vector<MixedGradientSlot> result(slot_count); + const auto* is_mixed = config.option<ConfigOptionBools>("filament_is_mixed"); + const auto* mixed_grad = config.option<ConfigOptionBools>("filament_mixed_gradient"); + const auto* mixed_comp = config.option<ConfigOptionStrings>("filament_mixed_components"); + const auto* grad_range = config.option<ConfigOptionStrings>("filament_mixed_gradient_range"); + const auto* fil_colour = config.option<ConfigOptionStrings>("filament_colour"); + if (!is_mixed || !mixed_grad || !mixed_comp || !fil_colour) return result; + + for (size_t i = 0; i < slot_count && i < is_mixed->values.size(); ++i) { + if (!is_mixed->values[i]) continue; + if (i >= mixed_grad->values.size() || !mixed_grad->values[i]) continue; + if (i >= mixed_comp->values.size()) continue; + + std::vector<unsigned int> comp_ids; + std::istringstream iss(mixed_comp->values[i]); + std::string tok; + while (std::getline(iss, tok, ',')) { + unsigned int v = 0; + if (std::sscanf(tok.c_str(), "%u", &v) == 1) + comp_ids.push_back(v); + } + if (comp_ids.size() != 2) continue; + + int direction = 0; + if (grad_range && i < grad_range->values.size()) { + CNumericLocalesSetter c_locale_setter; + float v0 = 0, v1 = 0; + if (std::sscanf(grad_range->values[i].c_str(), "%f,%f", &v0, &v1) == 2) + direction = (v0 > v1) ? 0 : 1; + } + + unsigned int from_id = (direction == 0) ? comp_ids[0] : comp_ids[1]; + unsigned int to_id = (direction == 0) ? comp_ids[1] : comp_ids[0]; + result[i].is_gradient = true; + result[i].color_from = (from_id >= 1 && from_id <= fil_colour->values.size()) + ? fil_colour->values[from_id - 1] : "#D9D9D9"; + result[i].color_to = (to_id >= 1 && to_id <= fil_colour->values.size()) + ? fil_colour->values[to_id - 1] : "#D9D9D9"; + } + return result; +} + +} // anonymous namespace + std::vector<std::string> Plater::get_filament_colors_render_info() const { const Slic3r::DynamicPrintConfig* config = &wxGetApp().preset_bundle->project_config; @@ -17968,6 +19979,13 @@ std::vector<std::string> Plater::get_filament_colors_render_info() const if (!config->has("filament_multi_colour")) return color_packs; color_packs = (config->option<ConfigOptionStrings>("filament_multi_colour"))->values; + + auto slots = parse_mixed_gradient_slots(*config, color_packs.size()); + for (size_t i = 0; i < color_packs.size(); ++i) { + if (slots[i].is_gradient) + color_packs[i] = slots[i].color_from + " " + slots[i].color_to; + } + return color_packs; } @@ -17978,9 +19996,51 @@ std::vector<std::string> Plater::get_filament_color_render_type() const if (!config->has("filament_colour_type")) return ctype; ctype = (config->option<ConfigOptionStrings>("filament_colour_type"))->values; + + auto slots = parse_mixed_gradient_slots(*config, ctype.size()); + while (ctype.size() < slots.size()) ctype.push_back("1"); + for (size_t i = 0; i < ctype.size() && i < slots.size(); ++i) { + if (slots[i].is_gradient) + ctype[i] = "0"; + } + return ctype; } +const std::vector<std::vector<wxColour>>& Plater::get_filament_gradient_ramps() const +{ + // Sampling a ramp walks the measured-blend recipe table once per step and the paint toolbar + // asks for the ramps every rendered frame, so they are cached against the config values they + // are built from. The cache is static rather than a Plater member because the extruder icons + // ask for the ramps from MenuFactory::init(), which runs while this Plater is still inside its + // own constructor, so wxGetApp().plater_ is not assigned yet. + static std::string s_ramps_key; + static std::vector<std::vector<wxColour>> s_ramps; + + static const char* ramp_keys[] = {"filament_is_mixed", "filament_mixed_gradient", + "filament_mixed_components", "filament_colour", + "filament_mixed_gradient_range", "filament_mixed_gradient_curve"}; + + const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->project_config; + std::string key; + for (const char* opt_key : ramp_keys) + if (const ConfigOption* opt = config.option(opt_key)) + key += opt->serialize() + '\n'; + if (key == s_ramps_key) + return s_ramps; + + // 64 bands outresolve every swatch drawn from this, all of which resample it down to their + // own height, so one cached resolution serves the icons and both ImGui filament bars. + const auto* colour_opt = config.option<ConfigOptionStrings>("filament_colour"); + const size_t n = colour_opt ? colour_opt->values.size() : 0; + s_ramps.assign(n, {}); + for (size_t i = 0; i < n; ++i) + s_ramps[i] = mixed_gradient_ramp(config, i, 64); + s_ramps_key = std::move(key); + + return s_ramps; +} + /* Get vector of colors used for rendering of a Preview scene in "Color print" mode * It consists of extruder colors and colors, saved in model.custom_gcode_per_print_z */ @@ -18364,7 +20424,7 @@ void Plater::pop_warning_and_go_to_device_page(wxString printer_name, PrinterWar MessageDialog dlg(this, content, title, wxOK | wxFORWARD | wxICON_WARNING, _L("Device Page")); auto result = dlg.ShowModal(); if (result == wxFORWARD) { - wxGetApp().mainframe->select_tab(size_t(MainFrame::tpMonitor)); + wxGetApp().mainframe->select_tab(TAB_ID_MONITOR); } } @@ -19576,7 +21636,7 @@ void Plater::show_object_info() auto mesh_errors = p->sidebar->obj_list()->get_mesh_errors_info(&info_manifold, &non_manifold_edges); if (non_manifold_edges > 0) { - info_manifold += into_u8("\n" + _L("Tips:") + "\n" + _L("Use \"Fix Model\" to repair the mesh.")); + info_manifold += "\n" + _L("Tips:") + "\n" + _L("Use \"Fix Model\" to repair the mesh."); } info_manifold = "<Error>" + info_manifold + "</Error>"; diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index b60c0eb242..5308deec61 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -5,6 +5,7 @@ #include <vector> #include <boost/filesystem/path.hpp> +#include <wx/colour.h> #include <wx/panel.h> // BBS #include <wx/notebook.h> @@ -86,6 +87,10 @@ using t_optgroups = std::vector <std::shared_ptr<ConfigOptionsGroup>>; class Plater; enum class ActionButtonType : int; +// Sentinel filament id meaning "use the slot the sidebar context menu was opened on" +// (Sidebar::priv::m_menu_filament_id) rather than an explicit index. +inline constexpr int kSidebarContextMenuFilamentId = -2; + #define EVT_PUBLISHING_START 1 #define EVT_PUBLISHING_STOP 2 @@ -188,7 +193,7 @@ public: void delete_filament(size_t filament_id = size_t(-1), int replace_filament_id = -1); // 0 base, -1 means default void change_filament(size_t from_id, size_t to_id); // 0 base void edit_filament(); - void add_custom_filament(wxColour new_col); + void add_custom_filament(wxColour new_col, const std::string& preset_name = std::string(), bool skip_preset_validation = false); bool is_new_project_in_gcode3mf(); // BBS void on_bed_type_change(BedType bed_type); @@ -262,6 +267,20 @@ public: std::vector<PlaterPresetComboBox*>& combos_filament(); void clear_combos_filament_badge(); void udpate_combos_filament_badge(); + + // Mixed-color filament sidebar section + void add_mixed_filament(); + void edit_mixed_filament(size_t idx); + void delete_mixed_filament_at(size_t idx); + void decompose_filament_color(int filament_idx); + void recalc_filament_scroll_sizes(); + void update_mixed_filament_list(); + bool has_broken_mixed_filament() const; + bool has_broken_mixed_filament(const PartPlate* plate) const; + void collect_physical_filament_info(std::vector<std::string>& color_strs, + std::vector<std::string>& names, + std::vector<std::string>& types, + std::vector<size_t>* config_indices = nullptr); Search::OptionsSearcher& get_searcher(); std::string& get_search_line(); void update_printer_thumbnail(); @@ -290,7 +309,7 @@ public: Plater(const Plater &) = delete; Plater &operator=(Plater &&) = delete; Plater &operator=(const Plater &) = delete; - ~Plater() = default; + ~Plater(); bool Show(bool show = true); @@ -313,6 +332,11 @@ public: const SLAPrint& sla_print() const; SLAPrint& sla_print(); + // Helper: returns config indices where filament_is_mixed == true + std::vector<size_t> mixed_filament_config_indices() const; + // Helper: returns config indices where filament_is_mixed == false + std::vector<size_t> physical_filament_config_indices() const; + int new_project(bool skip_confirm = false, bool silent = false, const wxString& project_name = wxString()); // BBS: save & backup void load_project(wxString const & filename = "", wxString const & originfile = "-"); @@ -568,7 +592,7 @@ public: void on_filament_change(size_t filament_idx); void on_filament_count_change(size_t extruders_count); - void on_filaments_delete(size_t extruders_count, size_t filament_id, int replace_filament_id = -1); + void on_filaments_delete(size_t extruders_count, size_t filament_id, int replace_filament_id = -1, const std::vector<unsigned char>& is_mixed_before_delete = {}); std::vector<Slic3r::ColorRGBA> get_extruders_colors(); // BBS void on_bed_type_change(BedType bed_type); @@ -583,6 +607,12 @@ public: std::vector<std::string> get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result = nullptr) const; std::vector<std::string> get_filament_colors_render_info() const; std::vector<std::string> get_filament_color_render_type() const; + + // Per slot, the colours a gradient mixed filament actually prints, sampled bottom (index 0) + // to top, so the sidebar, the paint gizmo and the extruder icons draw the same fade the + // editor previews rather than a straight blend of two endpoints. A slot that is not a + // gradient mixed filament gets an empty ramp. Cached; recomputed when the config changes. + const std::vector<std::vector<wxColour>>& get_filament_gradient_ramps() const; std::vector<std::string> get_colors_for_color_print(const GCodeProcessorResult* const result = nullptr) const; void set_global_filament_map_mode(FilamentMapMode mode); @@ -1020,4 +1050,4 @@ wxArrayString get_all_camera_view_type(); } // namespace GUI } // namespace Slic3r -#endif \ No newline at end of file +#endif diff --git a/src/slic3r/GUI/PluginWebDialog.cpp b/src/slic3r/GUI/PluginWebDialog.cpp index 1808f21ce9..d89aac7270 100644 --- a/src/slic3r/GUI/PluginWebDialog.cpp +++ b/src/slic3r/GUI/PluginWebDialog.cpp @@ -15,39 +15,6 @@ namespace Slic3r { namespace GUI { namespace { -// Low-specificity element defaults (no !important) for UNSTYLED plugin HTML, so a bare -// plugin page looks native while any CSS the plugin ships still wins. Built on the -// --orca-* variables the host injects (see WebViewHostDialog); document-start injected -// AFTER the host contract so the variables are defined (shares the base injector's -// WebView2 timing guard). -std::string plugin_defaults_user_script() -{ - std::string css; - css += "<style id=\"orca-plugin-defaults\">"; - css += "html,body{background:var(--orca-bg);color:var(--orca-fg);" - "font-family:var(--orca-font);font-size:13px;}"; - css += "body{margin:0;}"; - css += "h1,h2,h3,h4,h5,h6{color:var(--orca-fg);font-weight:600;}"; - css += "a{color:var(--orca-accent);}"; - css += "hr{border:0;border-top:1px solid var(--orca-border);}"; - css += "button{font:inherit;color:var(--orca-accent-fg);background:var(--orca-accent);" - "border:1px solid var(--orca-accent);border-radius:4px;padding:5px 14px;cursor:pointer;}"; - css += "button:hover{filter:brightness(1.1);}"; - css += "button:disabled{opacity:.5;cursor:default;}"; - css += "input,select,textarea{font:inherit;color:var(--orca-fg);" - "background:var(--orca-bg);border:1px solid var(--orca-border);" - "border-radius:4px;padding:4px 8px;}"; - css += "input:focus,select:focus,textarea:focus{outline:none;border-color:var(--orca-accent);}"; - css += "table{border-collapse:collapse;}"; - css += "th,td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--orca-border);}"; - css += "th{color:var(--orca-muted);font-weight:600;}"; - css += "::-webkit-scrollbar{width:12px;height:12px;}"; - css += "::-webkit-scrollbar-thumb{background:var(--orca-border);border-radius:6px;}"; - css += "::-webkit-scrollbar-track{background:transparent;}"; - css += "</style>"; - return WebViewHostDialog::document_start_injector(css, "orca-plugin-defaults", "beforeend"); -} - // Injected into the top-level page at document start (before the plugin's own // scripts). Defines window.orca as the only host surface the page may use. It // references window.wx lazily (at call time) so it never races the backend's @@ -129,7 +96,7 @@ PluginWebDialog::PluginWebDialog(wxWindow* parent, void PluginWebDialog::add_user_scripts() { if (wxWebView* wv = browser()) { - wv->AddUserScript(wxString::FromUTF8(plugin_defaults_user_script())); + wv->AddUserScript(wxString::FromUTF8(WebViewHostDialog::plugin_defaults_user_script())); wv->AddUserScript(ORCA_BRIDGE_JS); } } diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 6bcc00848b..355c4a26a8 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -79,7 +79,7 @@ public: Bind(wxEVT_LEFT_DOWN, &WikiLabel::OnLeftDown, this); } - void SetLabel(const wxString& label) + void SetLabel(const wxString& label) override { m_label = label; m_last_wrap_width = -1; // force re-wrap @@ -519,7 +519,7 @@ wxBoxSizer *PreferencesDialog::create_item_language_combobox(wxString title, wxS } } - auto check = [this](bool yes_or_no) { + auto check = [](bool yes_or_no) { // if (yes_or_no) // return true; int act_btns = ActionButtons::SAVE; @@ -1135,6 +1135,14 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too wxGetApp().plater()->sidebar().update_presets(Preset::TYPE_FILAMENT); } + if (param == "use_printer_agents") + { + // Rebuild the Device tab so the native/web-UI choice reflects the new flag + // immediately, instead of only on the next printer-preset change or restart. + if (wxGetApp().plater()) + wxGetApp().plater()->sidebar().update_all_preset_comboboxes(); + } + if (param == "enable_high_low_temp_mixed_printing") { if (checkbox->GetValue()) { const wxString warning_title = _L("Bed Temperature Difference Warning"); @@ -1192,7 +1200,7 @@ wxBoxSizer* PreferencesDialog::create_item_button(wxString title, wxString title m_button_download->SetStyle(title2 == _L("Clear") ? ButtonStyle::Alert : ButtonStyle::Regular, ButtonType::Parameter); m_button_download->SetToolTip(tooltip2.IsEmpty() ? tooltip : tooltip2); // use label tooltip if button tooltip empty - m_button_download->Bind(wxEVT_BUTTON, [this, onclick](auto &e) { onclick(); }); + m_button_download->Bind(wxEVT_BUTTON, [onclick](auto &e) { onclick(); }); m_sizer->Add(m_button_download, 0, wxALIGN_CENTER_VERTICAL); @@ -1482,7 +1490,7 @@ void PreferencesDialog::create() m_sizer_body = new wxBoxSizer(wxVERTICAL); m_pref_tabs = new TabCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTR_NO_BUTTONS | wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES | wxBORDER_NONE | wxWANTS_CHARS | wxTR_FULL_ROW_HIGHLIGHT); - m_pref_tabs->Bind(wxEVT_RIGHT_DOWN, [this](auto &e) {}); // disable right select + m_pref_tabs->Bind(wxEVT_RIGHT_DOWN, [](auto &e) {}); // disable right select m_pref_tabs->SetFont(Label::Body_14); create_items(); @@ -1740,11 +1748,26 @@ void PreferencesDialog::create_items() g_sizer->Add(item_pop_up_filament_map_dialog); #endif + //// GENERAL > Plugins + g_sizer->Add(create_item_title(_L("Plugins")), 1, wxEXPAND); + + auto item_plugin_pages_visible_count = create_item_spinctrl( + _L("Visible plugin pages"), + "", + _L("pages"), + _L("Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab."), + SETTING_PLUGIN_PAGES_VISIBLE_COUNT, + PLUGIN_PAGES_VISIBLE_COUNT_MIN, + PLUGIN_PAGES_VISIBLE_COUNT_MAX, + [](int value) { wxGetApp().mainframe->plugin_pages().set_visible_page_count(value); } + ); + g_sizer->Add(item_plugin_pages_visible_count); + g_sizer->AddSpacer(FromDIP(10)); sizer_page->Add(g_sizer, 0, wxEXPAND); ////////////////////////// - //// CONTROL TAB + //// CONTROL TAB ///////////////////////////////////// m_pref_tabs->AppendItem(_L("Control")); f_sizers.push_back(new wxFlexGridSizer(1, 1, v_gap, 0)); @@ -2101,6 +2124,12 @@ void PreferencesDialog::create_items() auto item_show_unsupported = create_item_checkbox(_L("Show unsupported presets"), _L("Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."), "show_unsupported_presets"); g_sizer->Add(item_show_unsupported); + auto item_plugin_printer_agents = create_item_checkbox( + _L("(Experimental) Use printer agents instead of print hosts"), _L( + "Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\nWhen disabled, OrcaSlicer uses the legacy print-host behavior."), + "use_printer_agents"); + g_sizer->Add(item_plugin_printer_agents); + //// DEVELOPER > Experimental Features g_sizer->Add(create_item_title(_L("Experimental Features")), 1, wxEXPAND); diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index c979fc3212..837f4ebe0c 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -1042,7 +1042,7 @@ bool PlaterPresetComboBox::switch_to_tab() //BBS Select NoteBook Tab params if (tab->GetParent() == wxGetApp().params_panel()) - wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); else { wxGetApp().params_dialog()->Popup(); tab->OnActivate(); @@ -1653,7 +1653,7 @@ void TabPresetComboBox::OnSelect(wxCommandEvent &evt) default: break; } if (sp != ConfigWizard::SP_WELCOME) { - wxTheApp->CallAfter([this, sp]() { + wxTheApp->CallAfter([sp]() { run_wizard(sp); }); } @@ -1949,7 +1949,7 @@ GUI::CalibrateFilamentComboBox::CalibrateFilamentComboBox(wxWindow *parent) { clr_picker->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); clr_picker->SetToolTip(""); - clr_picker->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) {}); + clr_picker->Bind(wxEVT_BUTTON, [](wxCommandEvent& e) {}); } GUI::CalibrateFilamentComboBox::~CalibrateFilamentComboBox() diff --git a/src/slic3r/GUI/PresetComboBoxes.hpp b/src/slic3r/GUI/PresetComboBoxes.hpp index 53644cecf5..20f75f4616 100644 --- a/src/slic3r/GUI/PresetComboBoxes.hpp +++ b/src/slic3r/GUI/PresetComboBoxes.hpp @@ -39,7 +39,7 @@ public: PresetComboBox(wxWindow* parent, Preset::Type preset_type, const wxSize& size = wxDefaultSize, PresetBundle* preset_bundle = nullptr); ~PresetComboBox(); - enum LabelItemType { + enum LabelItemType : std::size_t { LABEL_ITEM_PHYSICAL_PRINTER = 0xffffff01, LABEL_ITEM_PRINTER_MODELS, LABEL_ITEM_DISABLED, diff --git a/src/slic3r/GUI/PrintHostDialogs.cpp b/src/slic3r/GUI/PrintHostDialogs.cpp index 669468d852..2b4d212d0f 100644 --- a/src/slic3r/GUI/PrintHostDialogs.cpp +++ b/src/slic3r/GUI/PrintHostDialogs.cpp @@ -2062,7 +2062,7 @@ void CrealityPrintHostSendDialog::init() } for (auto* c : m_slot_combos) { - c->Bind(wxEVT_COMBOBOX, [this, ext_slot_idx](wxCommandEvent& e) { + c->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& e) { int sel = e.GetSelection(); if (sel >= 0 && sel < (int)m_printer_slots.size() && m_printer_slots[sel].box_id == 0) { diff --git a/src/slic3r/GUI/PrintHostDialogs.hpp b/src/slic3r/GUI/PrintHostDialogs.hpp index 988d4c8171..6f55c0d953 100644 --- a/src/slic3r/GUI/PrintHostDialogs.hpp +++ b/src/slic3r/GUI/PrintHostDialogs.hpp @@ -163,7 +163,7 @@ public: BedType bedType() const { return m_BedType; } virtual void init() override; - virtual std::map<std::string, std::string> extendedInfo() const + virtual std::map<std::string, std::string> extendedInfo() const override { return {{"bedType", std::to_string(static_cast<int>(m_BedType))}, {"timeLapse", std::to_string(m_timeLapse)}, @@ -200,7 +200,7 @@ public: PrintHost* printhost); virtual void init() override; - virtual std::map<std::string, std::string> extendedInfo() const; + virtual std::map<std::string, std::string> extendedInfo() const override; private: static constexpr const char* CONFIG_KEY_ENABLESELFTEST = "crealityprint_enable_self_test"; diff --git a/src/slic3r/GUI/Printer/PrinterFileSystem.cpp b/src/slic3r/GUI/Printer/PrinterFileSystem.cpp index 8ecbe87391..8ec6909c8c 100644 --- a/src/slic3r/GUI/Printer/PrinterFileSystem.cpp +++ b/src/slic3r/GUI/Printer/PrinterFileSystem.cpp @@ -164,7 +164,7 @@ void PrinterFileSystem::ListAllFiles() req["storage"] = m_file_storage; req["api_version"] = 2; req["notify"] = "DETAIL"; - SendRequest<FileList>(LIST_INFO, req, [this, type = m_file_type](json const& resp, FileList & list, auto) -> int { + SendRequest<FileList>(LIST_INFO, req, [type = m_file_type](json const& resp, FileList & list, auto) -> int { json files = resp["file_lists"]; for (auto& f : files) { std::string name = f["name"]; @@ -662,10 +662,10 @@ PrinterFileSystem::File const &PrinterFileSystem::GetFile(size_t index, bool &se void PrinterFileSystem::Attached() { boost::unique_lock lock(m_mutex); - m_recv_thread = std::move(boost::thread([w = weak_from_this()] { + m_recv_thread = boost::thread([w = weak_from_this()] { boost::shared_ptr<PrinterFileSystem> s = w.lock(); if (s) s->RecvMessageThread(); - })); + }); } void PrinterFileSystem::Start() @@ -1230,7 +1230,7 @@ boost::uint32_t PrinterFileSystem::RequestMediaAbility(int api_version) req["api_version"] = api_version; return SendRequest<MediaAbilityList>( - REQUEST_MEDIA_ABILITY, req, [this](const json &resp, MediaAbilityList &list, auto) -> int { + REQUEST_MEDIA_ABILITY, req, [](const json &resp, MediaAbilityList &list, auto) -> int { json abliity_list = resp["storage"]; list = abliity_list.get<MediaAbilityList>(); return 0; diff --git a/src/slic3r/GUI/Printer/gstbambusrc.c b/src/slic3r/GUI/Printer/gstbambusrc.c deleted file mode 100644 index d3f4ab112a..0000000000 --- a/src/slic3r/GUI/Printer/gstbambusrc.c +++ /dev/null @@ -1,657 +0,0 @@ -/* bambusrc for gstreamer - * integration with proprietary Bambu Lab blob for getting raw h.264 video - * - * Copyright (C) 2023 Joshua Wise <joshua@accelerated.tech> - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - * - * Alternatively, the contents of this file may be used under the - * GNU Lesser General Public License Version 2.1 (the "LGPL"), in - * which case the following provisions apply instead of the ones - * mentioned above: - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place - Suite 330, - * Boston, MA 02111-1307, USA. - */ - -#ifdef HAVE_CONFIG_H -# include <config.h> -#endif - -#include <gst/gst.h> - -#include "gstbambusrc.h" - -#include <stdio.h> -#include <unistd.h> -#ifndef EXTERNAL_GST_PLUGIN -#define BAMBU_DYNAMIC -#endif -#include "BambuTunnel.h" - -#ifdef BAMBU_DYNAMIC -// From PrinterFileSystem. -#ifdef __cplusplus -extern "C" -#else -extern -#endif -BambuLib *bambulib_get(); -BambuLib *_lib = NULL; -#define BAMBULIB(x) (_lib->x) - -#else -#define BAMBULIB(x) (x) -#endif - -GST_DEBUG_CATEGORY_STATIC (gst_bambusrc_debug); -#define GST_CAT_DEFAULT gst_bambusrc_debug - -static GstStaticPadTemplate srctemplate = GST_STATIC_PAD_TEMPLATE ("src", - GST_PAD_SRC, - GST_PAD_ALWAYS, - GST_STATIC_CAPS_ANY); - //GST_STATIC_CAPS("video/x-h264,framerate=0/1,parsed=(boolean)false,stream-format=(string)byte-stream")); - -enum -{ - PROP_0, - PROP_LOCATION, -}; - -static void gst_bambusrc_uri_handler_init (gpointer g_iface, - gpointer iface_data); -static void gst_bambusrc_finalize (GObject * gobject); -static void gst_bambusrc_dispose (GObject * gobject); - -static void gst_bambusrc_set_property (GObject * object, guint prop_id, - const GValue * value, GParamSpec * pspec); -static void gst_bambusrc_get_property (GObject * object, guint prop_id, - GValue * value, GParamSpec * pspec); - -static GstStateChangeReturn gst_bambusrc_change_state (GstElement * - element, GstStateChange transition); -static GstFlowReturn gst_bambusrc_create (GstPushSrc * psrc, - GstBuffer ** outbuf); -static gboolean gst_bambusrc_start (GstBaseSrc * bsrc); -static gboolean gst_bambusrc_stop (GstBaseSrc * bsrc); -static gboolean gst_bambusrc_is_seekable (GstBaseSrc * bsrc); -static gboolean gst_bambusrc_query (GstBaseSrc * bsrc, GstQuery * query); -static gboolean gst_bambusrc_unlock (GstBaseSrc * bsrc); -static gboolean gst_bambusrc_unlock_stop (GstBaseSrc * bsrc); -static gboolean gst_bambusrc_set_location (GstBambuSrc * src, - const gchar * uri, GError ** error); - -#define gst_bambusrc_parent_class parent_class -G_DEFINE_TYPE_WITH_CODE (GstBambuSrc, gst_bambusrc, GST_TYPE_PUSH_SRC, - G_IMPLEMENT_INTERFACE (GST_TYPE_URI_HANDLER, - gst_bambusrc_uri_handler_init)); - -static void -gst_bambusrc_class_init (GstBambuSrcClass * klass) -{ - GObjectClass *gobject_class; - GstElementClass *gstelement_class; - GstBaseSrcClass *gstbasesrc_class; - GstPushSrcClass *gstpushsrc_class; - - gobject_class = (GObjectClass *) klass; - gstelement_class = (GstElementClass *) klass; - gstbasesrc_class = (GstBaseSrcClass *) klass; - gstpushsrc_class = (GstPushSrcClass *) klass; - - gobject_class->set_property = gst_bambusrc_set_property; - gobject_class->get_property = gst_bambusrc_get_property; - gobject_class->finalize = gst_bambusrc_finalize; - gobject_class->dispose = gst_bambusrc_dispose; - - g_object_class_install_property (gobject_class, - PROP_LOCATION, - g_param_spec_string ("location", "Location", - "URI to pass to Bambu Lab blobs", "", - (GParamFlags)(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); - - gst_element_class_add_static_pad_template (gstelement_class, &srctemplate); - - gst_element_class_set_static_metadata (gstelement_class, "Bambu Lab source", - "Source/Network", - "Receive data as a client over the network using the proprietary Bambu Lab blobs", - "Joshua Wise <joshua@accelerated.tech>"); - gstelement_class->change_state = - GST_DEBUG_FUNCPTR (gst_bambusrc_change_state); - - gstbasesrc_class->start = GST_DEBUG_FUNCPTR (gst_bambusrc_start); - gstbasesrc_class->stop = GST_DEBUG_FUNCPTR (gst_bambusrc_stop); - gstbasesrc_class->unlock = GST_DEBUG_FUNCPTR (gst_bambusrc_unlock); - gstbasesrc_class->unlock_stop = - GST_DEBUG_FUNCPTR (gst_bambusrc_unlock_stop); - gstbasesrc_class->is_seekable = - GST_DEBUG_FUNCPTR (gst_bambusrc_is_seekable); - gstbasesrc_class->query = GST_DEBUG_FUNCPTR (gst_bambusrc_query); - - gstpushsrc_class->create = GST_DEBUG_FUNCPTR (gst_bambusrc_create); - - GST_DEBUG_CATEGORY_INIT (gst_bambusrc_debug, "bambusrc", 0, - "Bambu Lab src"); -} - -static void -gst_bambusrc_reset (GstBambuSrc * src) -{ - gst_caps_replace (&src->src_caps, NULL); - if (src->tnl) { - BAMBULIB(Bambu_Close)(src->tnl); - BAMBULIB(Bambu_Destroy)(src->tnl); - src->tnl = NULL; - } -} - -static void -gst_bambusrc_init (GstBambuSrc * src) -{ - src->location = NULL; - src->tnl = NULL; - - gst_base_src_set_automatic_eos (GST_BASE_SRC (src), FALSE); - gst_base_src_set_live(GST_BASE_SRC(src), TRUE); - - gst_bambusrc_reset (src); -} - -static void -gst_bambusrc_dispose (GObject * gobject) -{ - GstBambuSrc *src = GST_BAMBUSRC (gobject); - - GST_DEBUG_OBJECT (src, "dispose"); - - G_OBJECT_CLASS (parent_class)->dispose (gobject); -} - -static void -gst_bambusrc_finalize (GObject * gobject) -{ - GstBambuSrc *src = GST_BAMBUSRC (gobject); - - GST_DEBUG_OBJECT (src, "finalize"); - - g_free (src->location); - if (src->tnl) { - BAMBULIB(Bambu_Close)(src->tnl); - BAMBULIB(Bambu_Destroy)(src->tnl); - } - - G_OBJECT_CLASS (parent_class)->finalize (gobject); -} - -static void -gst_bambusrc_set_property (GObject * object, guint prop_id, - const GValue * value, GParamSpec * pspec) -{ - GstBambuSrc *src = GST_BAMBUSRC (object); - - switch (prop_id) { - case PROP_LOCATION: - { - const gchar *location; - - location = g_value_get_string (value); - - if (location == NULL) { - GST_WARNING ("location property cannot be NULL"); - goto done; - } - if (!gst_bambusrc_set_location (src, location, NULL)) { - GST_WARNING ("badly formatted location"); - goto done; - } - break; - } - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } -done: - return; -} - -static void -gst_bambusrc_get_property (GObject * object, guint prop_id, - GValue * value, GParamSpec * pspec) -{ - GstBambuSrc *src = GST_BAMBUSRC (object); - - switch (prop_id) { - case PROP_LOCATION: - g_value_set_string (value, src->location); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } -} - -int gst_bambu_last_error = 0; - -static GstFlowReturn -gst_bambusrc_create (GstPushSrc * psrc, GstBuffer ** outbuf) -{ - GstBambuSrc *src; - - src = GST_BAMBUSRC (psrc); - - (void) src; - GST_DEBUG_OBJECT (src, "create()"); - - int rv; - Bambu_Sample sample; - - if (!src->tnl) { - return GST_FLOW_ERROR; - } - - while ((rv = BAMBULIB(Bambu_ReadSample)(src->tnl, &sample)) == Bambu_would_block) { - GST_DEBUG_OBJECT(src, "create would block"); - usleep(33333); /* 30Hz */ - } - - if (rv == Bambu_stream_end) { - return GST_FLOW_EOS; - } - - if (rv != Bambu_success) { - gst_bambu_last_error = rv; - return GST_FLOW_ERROR; - } - -#if GLIB_CHECK_VERSION(2,68,0) - gpointer sbuf = g_memdup2(sample.buffer, sample.size); -#else - gpointer sbuf = g_memdup(sample.buffer, sample.size); -#endif - *outbuf = gst_buffer_new_wrapped_full(0, sbuf, sample.size, 0, sample.size, sbuf, g_free); - - /* Synthesize monotonic timestamps at the announced frame rate, anchored - * to the first frame's arrival time. The X1C's RTSPS server emits - * unreliable decode timestamps (wildly non-monotonic jumps, or sometimes - * none at all); forwarding them directly froze the pipeline after a few - * seconds. Pacing on a synthesized clock — the same trick mpv uses when - * it reports "No video PTS! Making something up." — gives smooth - * playback regardless of network jitter, and only drops late frames if - * the printer can't keep up. A snap-back resets the anchor if real - * arrival drifts more than two frame periods from the synthesized - * timeline (e.g. announced framerate was wrong). - */ - GstClock *clock = GST_ELEMENT_CLOCK(psrc); - GstClockTime base_time = gst_element_get_base_time((GstElement *)psrc); - GstClockTime running_now = GST_CLOCK_TIME_NONE; - if (clock) { - GstClockTime now = gst_clock_get_time(clock); - if (now != GST_CLOCK_TIME_NONE && now >= base_time) - running_now = now - base_time; - } - - /* Adapt the period to actual inter-arrival time via EWMA. The announced - * frame_rate is unreliable on Bambu printers (X1C announces 30 but - * delivers ~28), so trusting it causes the synthesized timeline to drift - * relative to real time, which makes the sink consider frames late and - * skip pacing entirely. Measuring the real rate keeps PTS in step with - * arrival on average, so the sink can pace inside bursts while still - * tracking the printer's actual frame cadence. - */ - if (src->avg_period == 0) { - int fps = src->frame_rate > 0 ? src->frame_rate : 30; - src->avg_period = GST_SECOND / fps; - } - if (running_now != GST_CLOCK_TIME_NONE && src->last_arrival != 0) { - GstClockTimeDiff delta = GST_CLOCK_DIFF(src->last_arrival, running_now); - /* clamp to plausible video frame periods (5..200 ms) so a one-off - * burst-of-zero or long stall doesn't poison the average */ - if (delta > 5 * GST_MSECOND && delta < 200 * GST_MSECOND) { - src->avg_period = (src->avg_period * 15 + (GstClockTime)delta) / 16; - } - } - src->last_arrival = (running_now != GST_CLOCK_TIME_NONE) ? running_now : src->last_arrival; - GstClockTime period = src->avg_period; - - /* Lead time: schedule frames a few periods in the future of their - * arrival, so the sink has a small jitter buffer. Without this, frames - * arriving slightly later than expected land behind the running clock - * and the sink renders them immediately, producing visible stutter. - * 100ms is invisible for a live print-monitor view. - */ - const GstClockTime LEAD = 100 * GST_MSECOND; - - if (!src->sttime) { - src->sttime = (running_now != GST_CLOCK_TIME_NONE) ? running_now + LEAD : LEAD; - src->frame_count = 0; - } - - GstClockTime pts = src->sttime + src->frame_count * period; - - /* Safety net: with the lead applied, expected drift is roughly -LEAD - * (pts sits LEAD ns ahead of running_now). Re-anchor only if the - * synthesized timeline diverges from that expectation by several frame - * periods, which indicates a real disturbance (printer paused, stream - * resumed, large fps change) rather than ordinary jitter. - */ - if (running_now != GST_CLOCK_TIME_NONE) { - GstClockTimeDiff drift = GST_CLOCK_DIFF(pts, running_now); - GstClockTimeDiff expected = -(GstClockTimeDiff)LEAD; - GstClockTimeDiff slack = (GstClockTimeDiff)(4 * period); - if (drift > expected + slack || drift < expected - slack) { - GST_DEBUG_OBJECT(src, "ts drift %" G_GINT64_FORMAT " ns; re-anchoring", drift); - src->sttime = running_now + LEAD; - src->frame_count = 0; - pts = src->sttime; - } - } - - GST_BUFFER_PTS(*outbuf) = pts; - GST_BUFFER_DTS(*outbuf) = pts; - GST_BUFFER_DURATION(*outbuf) = period; - src->frame_count++; - GST_DEBUG_OBJECT(src, - "sttime:%lu, DTS:%lu, PTS: %lu~", - src->sttime, GST_BUFFER_DTS(*outbuf), GST_BUFFER_PTS(*outbuf)); - - return GST_FLOW_OK; -} - -static void _log(void *ctx, int lvl, const char *msg) { - GstBambuSrc *src = (GstBambuSrc *) ctx; - GST_DEBUG_OBJECT(src, "bambu: %s", msg); - BAMBULIB(Bambu_FreeLogMsg)(msg); -} - -static gboolean -gst_bambusrc_start (GstBaseSrc * bsrc) -{ - GstBambuSrc *src = GST_BAMBUSRC (bsrc); - - GST_DEBUG_OBJECT (src, "start(\"%s\")", src->location); - - if (src->tnl) { - BAMBULIB(Bambu_Close)(src->tnl); - BAMBULIB(Bambu_Destroy)(src->tnl); - src->tnl = NULL; - } - -#ifdef BAMBU_DYNAMIC - if (!_lib) { - _lib = bambulib_get(); - if (!_lib->Bambu_Open) { - return FALSE; - } - } -#endif - if (BAMBULIB(Bambu_Create)(&src->tnl, src->location) != Bambu_success) { - return FALSE; - } - - int rv = 0; - BAMBULIB(Bambu_SetLogger)(src->tnl, _log, (void *)src); - if ((rv = BAMBULIB(Bambu_Open)(src->tnl)) != Bambu_success) { - BAMBULIB(Bambu_Destroy)(src->tnl); - src->tnl = NULL; - gst_bambu_last_error = rv; - return FALSE; - } - - int n = 0; - while ((rv = BAMBULIB(Bambu_StartStream)(src->tnl, 1 /* video */)) == Bambu_would_block) { - usleep(100000); - } - if (rv != Bambu_success) { - BAMBULIB(Bambu_Close)(src->tnl); - BAMBULIB(Bambu_Destroy)(src->tnl); - src->tnl = NULL; - gst_bambu_last_error = rv; - return FALSE; - } - - src->video_type = AVC1; - n = BAMBULIB(Bambu_GetStreamCount)(src->tnl); - GST_INFO_OBJECT (src, "Bambu_GetStreamCount returned stream count=%d",n); - for (int i = 0; i < n; ++i) { - Bambu_StreamInfo info; - BAMBULIB(Bambu_GetStreamInfo)(src->tnl, i, &info); - - GST_INFO_OBJECT (src, "stream %d type=%d, sub_type=%d", i, info.type, info.sub_type); - if (info.type == VIDE) { - src->video_type = info.sub_type; - src->frame_rate = info.format.video.frame_rate; - GST_INFO_OBJECT (src, " width %d height=%d, frame_rate=%d", - info.format.video.width, info.format.video.height, info.format.video.frame_rate); - } - } - - src->sttime = 0; - src->frame_count = 0; - src->last_arrival = 0; - src->avg_period = 0; - return TRUE; -} - -static gboolean -gst_bambusrc_stop (GstBaseSrc * bsrc) -{ - GstBambuSrc *src; - - src = GST_BAMBUSRC (bsrc); - GST_DEBUG_OBJECT (src, "stop()"); - if (src->tnl) { - BAMBULIB(Bambu_Close)(src->tnl); - BAMBULIB(Bambu_Destroy)(src->tnl); - src->tnl = NULL; - } - - return TRUE; -} - -static GstStateChangeReturn -gst_bambusrc_change_state (GstElement * element, GstStateChange transition) -{ - GstStateChangeReturn ret; - GstBambuSrc *src; - - src = GST_BAMBUSRC (element); - - (void) src; - - switch (transition) { - case GST_STATE_CHANGE_READY_TO_NULL: - //gst_bambusrc_session_close (src); - break; - default: - break; - } - - ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition); - - return ret; -} - -/* Interrupt a blocking request. */ -static gboolean -gst_bambusrc_unlock (GstBaseSrc * bsrc) -{ - GstBambuSrc *src; - - src = GST_BAMBUSRC (bsrc); - GST_DEBUG_OBJECT (src, "unlock()"); - - return TRUE; -} - -/* Interrupt interrupt. */ -static gboolean -gst_bambusrc_unlock_stop (GstBaseSrc * bsrc) -{ - GstBambuSrc *src; - - src = GST_BAMBUSRC (bsrc); - GST_DEBUG_OBJECT (src, "unlock_stop()"); - - return TRUE; -} - -static gboolean -gst_bambusrc_is_seekable (GstBaseSrc * bsrc) -{ - GstBambuSrc *src = GST_BAMBUSRC (bsrc); - - (void) src; - - return FALSE; -} - -static gboolean -gst_bambusrc_query (GstBaseSrc * bsrc, GstQuery * query) -{ - GstBambuSrc *src = GST_BAMBUSRC (bsrc); - gboolean ret; - GstSchedulingFlags flags; - gint minsize, maxsize, align; - - switch (GST_QUERY_TYPE (query)) { - case GST_QUERY_URI: - gst_query_set_uri (query, src->location); - ret = TRUE; - break; - default: - ret = FALSE; - break; - } - - if (!ret) - ret = GST_BASE_SRC_CLASS (parent_class)->query (bsrc, query); - - switch (GST_QUERY_TYPE (query)) { - case GST_QUERY_SCHEDULING: - gst_query_parse_scheduling (query, &flags, &minsize, &maxsize, &align); - flags = (GstSchedulingFlags)((int)flags | (int)GST_SCHEDULING_FLAG_SEQUENTIAL); - gst_query_set_scheduling (query, flags, minsize, maxsize, align); - break; - default: - break; - } - - return ret; -} - -static gboolean -gst_bambusrc_set_location (GstBambuSrc * src, const gchar * uri, - GError ** error) -{ - if (src->location) { - g_free (src->location); - src->location = NULL; - } - - if (uri == NULL) - return FALSE; - - src->location = g_strdup (uri); - - return TRUE; -} - -static GstURIType -gst_bambusrc_uri_get_type (GType type) -{ - return GST_URI_SRC; -} - -static const gchar *const * -gst_bambusrc_uri_get_protocols (GType type) -{ - static const gchar *protocols[] = { "bambu", NULL }; - - return protocols; -} - -static gchar * -gst_bambusrc_uri_get_uri (GstURIHandler * handler) -{ - GstBambuSrc *src = GST_BAMBUSRC (handler); - - /* FIXME: make thread-safe */ - return g_strdup (src->location); -} - -static gboolean -gst_bambusrc_uri_set_uri (GstURIHandler * handler, const gchar * uri, - GError ** error) -{ - GstBambuSrc *src = GST_BAMBUSRC (handler); - - return gst_bambusrc_set_location (src, uri, error); -} - -static void -gst_bambusrc_uri_handler_init (gpointer g_iface, gpointer iface_data) -{ - GstURIHandlerInterface *iface = (GstURIHandlerInterface *) g_iface; - - iface->get_type = gst_bambusrc_uri_get_type; - iface->get_protocols = gst_bambusrc_uri_get_protocols; - iface->get_uri = gst_bambusrc_uri_get_uri; - iface->set_uri = gst_bambusrc_uri_set_uri; -} - -static gboolean gstbambusrc_init(GstPlugin *plugin) -{ - return gst_element_register(plugin, "bambusrc", GST_RANK_PRIMARY, GST_TYPE_BAMBUSRC); -} - -#ifndef EXTERNAL_GST_PLUGIN - -// for use inside of Bambu Slicer -void gstbambusrc_register() -{ - static int did_register = 0; - if (did_register) - return; - did_register = 1; - - gst_plugin_register_static(GST_VERSION_MAJOR, GST_VERSION_MINOR, "bambusrc", "Bambu Lab source", gstbambusrc_init, "0.0.1", "GPL", "BambuStudio", "BambuStudio", "https://github.com/bambulab/BambuStudio"); -} - -#else - -#ifndef PACKAGE -#define PACKAGE "bambusrc" -#endif - -GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, GST_VERSION_MINOR, bambusrc, "Bambu Lab source", gstbambusrc_init, "0.0.1", "GPL", "BambuStudio", "https://github.com/bambulab/BambuStudio") - -#endif diff --git a/src/slic3r/GUI/Printer/gstbambusrc.h b/src/slic3r/GUI/Printer/gstbambusrc.h deleted file mode 100644 index d5c022a40e..0000000000 --- a/src/slic3r/GUI/Printer/gstbambusrc.h +++ /dev/null @@ -1,78 +0,0 @@ -/* bambusrc for gstreamer - * integration with proprietary Bambu Lab blob for getting raw h.264 video - * - * Copyright (C) 2023 Joshua Wise <joshua@accelerated.tech> - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - * - * Alternatively, the contents of this file may be used under the - * GNU Lesser General Public License Version 2.1 (the "LGPL"), in - * which case the following provisions apply instead of the ones - * mentioned above: - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place - Suite 330, - * Boston, MA 02111-1307, USA. - */ - -#ifndef __GST_BAMBUSRC_H__ -#define __GST_BAMBUSRC_H__ - -#include <gst/gst.h> -#include <gst/base/gstpushsrc.h> -#include <glib.h> - -G_BEGIN_DECLS - -#define GST_TYPE_BAMBUSRC (gst_bambusrc_get_type()) -G_DECLARE_FINAL_TYPE (GstBambuSrc, gst_bambusrc, - GST, BAMBUSRC, GstPushSrc) - -typedef void *Bambu_Tunnel; - -struct _GstBambuSrc -{ - GstPushSrc element; - GstCaps *src_caps; - gchar *location; - Bambu_Tunnel tnl; - GstClockTime sttime; - int video_type; - int frame_rate; - guint64 frame_count; - GstClockTime last_arrival; - GstClockTime avg_period; -}; - -extern void gstbambusrc_register(); - -G_END_DECLS - -#endif /* __GST_BAMBUSRC_H__ */ diff --git a/src/slic3r/GUI/PrivacyUpdateDialog.cpp b/src/slic3r/GUI/PrivacyUpdateDialog.cpp index ef4dac9b13..92d6d6c8c7 100644 --- a/src/slic3r/GUI/PrivacyUpdateDialog.cpp +++ b/src/slic3r/GUI/PrivacyUpdateDialog.cpp @@ -97,7 +97,7 @@ PrivacyUpdateDialog::PrivacyUpdateDialog(wxWindow* parent, wxWindowID id, const this->on_hide(); }); - Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& e) {e.Veto(); }); + Bind(wxEVT_CLOSE_WINDOW, [](wxCloseEvent& e) {e.Veto(); }); if (btn_style != CONFIRM_AND_CANCEL) m_button_cancel->Hide(); diff --git a/src/slic3r/GUI/Project.cpp b/src/slic3r/GUI/Project.cpp index 57410b1202..6d1eb0e180 100644 --- a/src/slic3r/GUI/Project.cpp +++ b/src/slic3r/GUI/Project.cpp @@ -74,7 +74,18 @@ ProjectPanel::ProjectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, Fit(); } -ProjectPanel::~ProjectPanel() {} +ProjectPanel::~ProjectPanel() +{ + shutdown(); +} + +void ProjectPanel::shutdown() +{ + m_reload_cancel_token->store(true, std::memory_order_release); + if (m_reload_task && m_reload_task->joinable()) + m_reload_task->join(); + m_reload_task.reset(); +} // Helper to convert newlines to <br> static std::string convert_newlines_to_br(const std::string& text) { @@ -101,7 +112,17 @@ void ProjectPanel::onWebNavigating(wxWebViewEvent& evt) void ProjectPanel::on_reload(wxCommandEvent& evt) { - boost::thread reload = boost::thread([this] { + if (wxTheApp == nullptr || wxGetApp().is_closing() || + m_reload_cancel_token->load(std::memory_order_acquire)) + return; + + if (m_reload_task && m_reload_task->joinable()) + m_reload_task->join(); + + const auto cancel_token = m_reload_cancel_token; + m_reload_task = std::make_unique<boost::thread>([this, cancel_token] { + if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing()) + return; std::string update_type; std::string license; std::string model_name; @@ -115,6 +136,9 @@ void ProjectPanel::on_reload(wxCommandEvent& evt) std::map<std::string, std::vector<json>> files; + if (wxGetApp().plater() == nullptr) + return; + Model model = wxGetApp().plater()->model(); auto model_info = model.model_info; @@ -156,7 +180,14 @@ void ProjectPanel::on_reload(wxCommandEvent& evt) std::string file_path = encode_path(wxGetApp().plater()->model().get_auxiliary_file_temp_path().c_str()); if (!file_path.empty()) { files = Reload(file_path); - wxGetApp().CallAfter([this, file_path, files] { m_auxiliary->Reload(file_path, files); }); + if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing()) + return; + + wxGetApp().CallAfter([this, cancel_token, file_path, files] { + if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing()) + return; + m_auxiliary->Reload(file_path, files); + }); } else { clear_model_info(); return; @@ -215,15 +246,18 @@ void ProjectPanel::on_reload(wxCommandEvent& evt) json m_Res = json::object(); m_Res["command"] = "show_3mf_info"; - m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id++); + m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id.fetch_add(1, std::memory_order_relaxed)); m_Res["model"] = j; wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore)); - if (m_web_init_completed) { - wxGetApp().CallAfter([this, strJS] { + if (m_web_init_completed.load(std::memory_order_acquire) && + !cancel_token->load(std::memory_order_acquire) && wxTheApp != nullptr && !wxGetApp().is_closing()) { + wxGetApp().CallAfter([this, cancel_token, strJS] { + if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing()) + return; RunScript(strJS.ToStdString()); - }); + }); } }); } @@ -264,7 +298,7 @@ void ProjectPanel::OnScriptMessage(wxWebViewEvent& evt) } } else if (strCmd == "request_3mf_info") { - m_web_init_completed = true; + m_web_init_completed.store(true, std::memory_order_release); } else if (strCmd == "edit_project_info") { show_info_editor(true); @@ -307,13 +341,20 @@ void ProjectPanel::update_model_data() void ProjectPanel::clear_model_info() { + if (wxTheApp == nullptr || wxGetApp().is_closing() || + m_reload_cancel_token->load(std::memory_order_acquire)) + return; + json m_Res = json::object(); m_Res["command"] = "clear_3mf_info"; - m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id++); + m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id.fetch_add(1, std::memory_order_relaxed)); wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore)); - wxGetApp().CallAfter([this, strJS] { + const auto cancel_token = m_reload_cancel_token; + wxGetApp().CallAfter([this, cancel_token, strJS] { + if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing()) + return; RunScript(strJS.ToStdString()); }); } diff --git a/src/slic3r/GUI/Project.hpp b/src/slic3r/GUI/Project.hpp index 0071685e7d..a590212cb7 100644 --- a/src/slic3r/GUI/Project.hpp +++ b/src/slic3r/GUI/Project.hpp @@ -26,9 +26,11 @@ #include "nlohmann/json.hpp" #include "slic3r/Utils/json_diff.hpp" +#include <atomic> #include <map> #include <vector> #include <memory> +#include <boost/thread.hpp> #include "Event.hpp" #include "libslic3r/ProjectTask.hpp" #include "wxExtensions.hpp" @@ -60,14 +62,16 @@ struct project_file{ class ProjectPanel : public wxPanel { private: - bool m_web_init_completed = {false}; - bool m_reload_already = {false}; + std::atomic<bool> m_web_init_completed{false}; + + std::shared_ptr<std::atomic<bool>> m_reload_cancel_token{std::make_shared<std::atomic<bool>>(false)}; + std::unique_ptr<boost::thread> m_reload_task; wxWebView* m_browser = {nullptr}; AuxiliaryPanel* m_auxiliary{nullptr}; wxString m_project_home_url; wxString m_root_dir; - static inline int m_sequence_id = 8000; + static inline std::atomic<int> m_sequence_id{8000}; void show_info_editor(bool show); @@ -75,6 +79,7 @@ private: public: ProjectPanel(wxWindow *parent, wxWindowID id = wxID_ANY, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize, long style = wxTAB_TRAVERSAL); ~ProjectPanel(); + void shutdown(); void onWebNavigating(wxWebViewEvent& evt); diff --git a/src/slic3r/GUI/RecenterDialog.hpp b/src/slic3r/GUI/RecenterDialog.hpp index 1e48e7990b..cd7511d3f5 100644 --- a/src/slic3r/GUI/RecenterDialog.hpp +++ b/src/slic3r/GUI/RecenterDialog.hpp @@ -10,8 +10,6 @@ namespace Slic3r { namespace GUI { class RecenterDialog : public DPIDialog { private: - wxStaticText* m_staticText_hint; - wxStaticBitmap* m_bitmap_home; ScalableBitmap m_home_bmp; wxString hint1; wxString hint2; diff --git a/src/slic3r/GUI/ReleaseNote.cpp b/src/slic3r/GUI/ReleaseNote.cpp index 22f65f4a60..029310faf9 100644 --- a/src/slic3r/GUI/ReleaseNote.cpp +++ b/src/slic3r/GUI/ReleaseNote.cpp @@ -1250,7 +1250,7 @@ void ConfirmBeforeSendDialog::update_text(std::vector<ConfirmBeforeSendInfo> tex else { label_item = new Label(m_vebview_release_note, text.text + " " + _L("Please refer to Wiki before use->"), LB_AUTO_WRAP); - label_item->Bind(wxEVT_LEFT_DOWN, [this, text](wxMouseEvent& e) { wxLaunchDefaultBrowser(text.wiki_url);}); + label_item->Bind(wxEVT_LEFT_DOWN, [text](wxMouseEvent& e) { wxLaunchDefaultBrowser(text.wiki_url);}); label_item->Bind(wxEVT_ENTER_WINDOW, [this](auto& e) { SetCursor(wxCURSOR_HAND); }); label_item->Bind(wxEVT_LEAVE_WINDOW, [this](auto& e) { SetCursor(wxCURSOR_ARROW); }); } @@ -1991,7 +1991,7 @@ void InputIpAddressDialog::workerThreadFunc(std::string str_ip, std::string str_ if (w.expired()) return; if (m_obj) { - m_obj->set_user_access_code(str_access_code); + m_obj->set_access_code(str_access_code); wxGetApp().getDeviceManager()->set_selected_machine(m_obj->get_dev_id()); } @@ -2055,6 +2055,11 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt) { auto str_ip = m_input_ip->GetTextCtrl()->GetValue(); auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue(); + + if (str_access_code.empty()) { + str_access_code = "88888888"; + } + auto str_name = m_input_printer_name->GetTextCtrl()->GetValue().Strip(wxString::both); auto str_sn = m_input_sn->GetTextCtrl()->GetValue().Strip(wxString::both); bool invalid_access_code = true; @@ -2062,7 +2067,7 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt) for (char c : str_access_code) { if (!(('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'))) { invalid_access_code = false; - return; + break; } } diff --git a/src/slic3r/GUI/SavePresetDialog.cpp b/src/slic3r/GUI/SavePresetDialog.cpp index 52bcbed4de..65b75c3d92 100644 --- a/src/slic3r/GUI/SavePresetDialog.cpp +++ b/src/slic3r/GUI/SavePresetDialog.cpp @@ -112,12 +112,59 @@ SavePresetDialog::Item::Item(Preset::Type type, const std::string &suffix, wxBox sizer->Add(m_radio_group, 0, wxEXPAND | wxTOP | wxLEFT, BORDER_W); if (parent->m_mode == comDevelop) { - m_detach_checkbox = new wxCheckBox(parent, wxID_ANY, _L("Detach from parent")); - sizer->Add(m_detach_checkbox, 0, wxALIGN_LEFT | wxALL, BORDER_W); - // Set initial state (unchecked by default) - m_detach_checkbox->SetValue(m_detach); - // Bind the checkbox event to update the detach state for this item - m_detach_checkbox->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { m_detach = m_detach_checkbox->GetValue(); }); + // A new user copy of a system preset inherits from the selected system preset. + const std::string parent_name = sel_preset.is_system ? sel_preset.name : sel_preset.inherits(); + const bool can_detach = !parent_name.empty(); + + wxBoxSizer *detach_sizer = new wxBoxSizer(wxHORIZONTAL); + + auto detach_tooltip = _L("Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported."); + + auto detach_checkbox = new ::CheckBox(parent); + detach_checkbox->SetToolTip(detach_tooltip); + + auto detach_label = new wxStaticText(parent, wxID_ANY, _L("Detach from parent")); + detach_label->SetFont(::Label::Body_14); + detach_label->SetToolTip(detach_tooltip); + + detach_sizer->Add(detach_checkbox, 0, wxALIGN_LEFT | wxLEFT, BORDER_W); + detach_sizer->Add(detach_label , 0, wxALIGN_CENTRE_VERTICAL | wxLEFT, FromDIP(5)); + sizer->Add(detach_sizer, 0, wxEXPAND | wxTOP, BORDER_W); + sizer->AddSpacer(FromDIP(5)); + + const wxString parent_text = can_detach ? from_u8(parent_name) : _L("Unique preset"); + auto parent_label = new wxStaticText(parent, wxID_ANY, parent_text); + parent_label->SetFont(::Label::Body_12); + parent_label->SetForegroundColour(wxColour("#6B6B6B")); + parent_label->SetToolTip(can_detach ? _L("Parent preset") : _L("This preset does not inherit from another preset.")); + sizer->Add(parent_label, 0, wxEXPAND | wxLEFT, BORDER_W + FromDIP(24)); + + sizer->AddSpacer(FromDIP(5)); + + if (!can_detach) { + detach_checkbox->Disable(); + detach_label->SetForegroundColour(wxColour("#6B6B6B")); + } + else { + // Set initial state (unchecked by default) + detach_checkbox->SetValue(m_detach); + // Bind the checkbox event to update the detach state for this item + detach_checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, detach_checkbox](wxCommandEvent& event) { + m_detach = detach_checkbox->GetValue(); + event.Skip(); // Let CheckBox update its bitmap for the new state. + }); + + detach_label->SetForegroundColour(wxColour("#363636")); + + auto on_toggle = [detach_checkbox]() { + detach_checkbox->SetValue(!detach_checkbox->GetValue()); + wxCommandEvent ev(wxEVT_TOGGLEBUTTON, detach_checkbox->GetId()); + ev.SetEventObject(detach_checkbox); + detach_checkbox->GetEventHandler()->ProcessEvent(ev); + }; + detach_label->Bind(wxEVT_LEFT_DOWN, [on_toggle](wxMouseEvent& e) {if(!e.LeftDClick()) on_toggle();}); + detach_label->Bind(wxEVT_LEFT_DCLICK, [on_toggle](wxMouseEvent& e) {on_toggle();}); + } } m_radio_group->Bind(wxEVT_COMMAND_RADIOBOX_SELECTED, [this](wxCommandEvent &e) { diff --git a/src/slic3r/GUI/SavePresetDialog.hpp b/src/slic3r/GUI/SavePresetDialog.hpp index 0b71325927..05aa1b2d39 100644 --- a/src/slic3r/GUI/SavePresetDialog.hpp +++ b/src/slic3r/GUI/SavePresetDialog.hpp @@ -75,7 +75,6 @@ class SavePresetDialog : public DPIDialog bool m_save_to_project {false}; RadioGroup* m_radio_group; // ORCA bool m_detach{false}; - wxCheckBox* m_detach_checkbox{nullptr}; void update(); }; diff --git a/src/slic3r/GUI/Search.cpp b/src/slic3r/GUI/Search.cpp index 1d5cf0b9e5..f8fc51ed02 100644 --- a/src/slic3r/GUI/Search.cpp +++ b/src/slic3r/GUI/Search.cpp @@ -681,7 +681,7 @@ SearchDialog::SearchDialog(OptionsSearcher *searcher, Preset::Type type, wxWindo SearchDialog::~SearchDialog() {} -void SearchDialog::Popup(wxPoint position /*= wxDefaultPosition*/) +void SearchDialog::Popup(wxWindow *focus /*= nullptr*/) { /* const std::string& line = searcher->search_string(); search_line->SetValue(line.empty() ? default_string : from_u8(line)); @@ -696,17 +696,19 @@ void SearchDialog::Popup(wxPoint position /*= wxDefaultPosition*/) search_line2->SetValue(wxString("")); //const std::string &line = searcher->search_string(); //searcher->search(into_u8(line), true); - PopupWindow::Popup(); + PopupWindow::Popup(focus); search_line2->SetFocus(); update_list(); } +#ifdef __WXMSW__ void SearchDialog::MSWDismissUnfocusedPopup() { Dismiss(); OnDismiss(); } +#endif // __WXMSW__ void SearchDialog::OnDismiss() { } @@ -926,7 +928,7 @@ SearchObjectDialog::SearchObjectDialog(GUI::ObjectList* object_list, wxWindow* p SearchObjectDialog::~SearchObjectDialog() {} -void SearchObjectDialog::Popup(wxPoint position /*= wxDefaultPosition*/) +void SearchObjectDialog::Popup(wxWindow *focus /*= nullptr*/) { if (m_is_dismissing || this->IsShown()) { return; @@ -937,7 +939,7 @@ void SearchObjectDialog::Popup(wxPoint position /*= wxDefaultPosition*/) // dropdown list, otherwise the text input won't be usable m_object_list->SetFocus(); #endif - PopupWindow::Popup(); + PopupWindow::Popup(focus); search_line2->SetFocus(); m_object_list->assembly_plate_object_name(); @@ -945,11 +947,13 @@ void SearchObjectDialog::Popup(wxPoint position /*= wxDefaultPosition*/) update_list(); } +#ifdef __WXMSW__ void SearchObjectDialog::MSWDismissUnfocusedPopup() { Dismiss(); OnDismiss(); } +#endif // __WXMSW__ void SearchObjectDialog::OnDismiss() {} diff --git a/src/slic3r/GUI/Search.hpp b/src/slic3r/GUI/Search.hpp index bdb4da83c4..4ae43dbca0 100644 --- a/src/slic3r/GUI/Search.hpp +++ b/src/slic3r/GUI/Search.hpp @@ -216,10 +216,12 @@ public: SearchDialog(OptionsSearcher *searcher, Preset::Type type, wxWindow *parent, TextInput *input, wxWindow *search_btn); ~SearchDialog(); - void MSWDismissUnfocusedPopup(); - void Popup(wxPoint position = wxDefaultPosition); - void OnDismiss(); - void Dismiss(); +#ifdef __WXMSW__ + void MSWDismissUnfocusedPopup() override; +#endif // __WXMSW__ + void Popup(wxWindow *focus = nullptr) override; + void OnDismiss() override; + void Dismiss() override; void Die(); void msw_rescale(); @@ -260,10 +262,12 @@ public: SearchObjectDialog(GUI::ObjectList* object_list, wxWindow* parent, TextInput* input); ~SearchObjectDialog(); - void MSWDismissUnfocusedPopup(); - void Popup(wxPoint position = wxDefaultPosition); - void OnDismiss(); - void Dismiss(); +#ifdef __WXMSW__ + void MSWDismissUnfocusedPopup() override; +#endif // __WXMSW__ + void Popup(wxWindow *focus = nullptr) override; + void OnDismiss() override; + void Dismiss() override; void Die(); void OnInputText(wxCommandEvent& event); diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index 1ab78fcc11..199de9236e 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -4,6 +4,7 @@ #include "libslic3r/Utils.hpp" #include "libslic3r/Thread.hpp" #include "libslic3r/Color.hpp" +#include "FilamentBitmapUtils.hpp" #include "GUI.hpp" #include "GUI_App.hpp" #include "GUI_Preview.hpp" @@ -482,7 +483,7 @@ SelectMachineDialog::SelectMachineDialog(Plater *plater) m_link_edit_nozzle->Bind(wxEVT_LEFT_DOWN, [this](auto &e) { - if (this && this->m_is_in_sending_mode) { + if (m_is_in_sending_mode) { return; } @@ -1088,8 +1089,8 @@ void SelectMachineDialog::sync_ams_mapping_result(std::vector<FilamentInfo> &res } } relayout_nozzle_cards(); - auto tab_index = (MainFrame::TabPosition) dynamic_cast<Notebook *>(wxGetApp().tab_panel())->GetSelection(); - if (tab_index == MainFrame::TabPosition::tp3DEditor || tab_index == MainFrame::TabPosition::tpPreview) { + wxString tab_name = wxGetApp().tab_panel()->GetSelectedPageName(); + if (tab_name == TAB_ID_PREPARE || tab_name == TAB_ID_PREVIEW) { updata_thumbnail_data_after_connected_printer(); } } @@ -2846,7 +2847,7 @@ void SelectMachineDialog::on_ok_btn(wxCommandEvent &event) }); // STUDIO-9580 - /* use warning color if there are warning and normal messages* / + /* use warning color if there are warning and normal messages*/ /* use indexes if there are several messages*/ /* add header and ending if there are several messages or has none block warnings*/ if (confirm_text.size() > 1 || !is_printing_block) @@ -3427,7 +3428,7 @@ void SelectMachineDialog::show_timelapse_storage_dialog(MachineObject* obj) if (show_cleanup_btn) { auto* btn_cleanup = new Button(&dlg, _L("Clean Up")); - btn_cleanup->Bind(wxEVT_BUTTON, [&dlg, ID_CLEANUP](wxCommandEvent&) { dlg.EndModal(ID_CLEANUP); }); + btn_cleanup->Bind(wxEVT_BUTTON, [&dlg](wxCommandEvent&) { dlg.EndModal(ID_CLEANUP); }); btn_sizer->Add(btn_cleanup, 0, wxEXPAND); } @@ -3629,7 +3630,7 @@ void SelectMachineDialog::on_send_print() BOOST_LOG_TRIVIAL(error) << "build_nozzle_info errors"; } - m_print_job->sdcard_state = obj_->GetStorage()->get_sdcard_state(); + m_print_job->sdcard_state = obj_->GetStorage()->get_sdcard_state(); m_print_job->has_sdcard = wxGetApp().app_config->get("allow_abnormal_storage") == "true" ? (m_print_job->sdcard_state == DevStorage::SdcardState::HAS_SDCARD_NORMAL || m_print_job->sdcard_state == DevStorage::SdcardState::HAS_SDCARD_ABNORMAL) @@ -3868,12 +3869,11 @@ _compare_obj_names(MachineObject* obj1, MachineObject* obj2) } /******************************************************************* -*@note _collect_machine_list -*@param dev_manager -- the device manager -*@param sorted_machine_objs -- return the sorted machine objects -*@param best_one -- return the best one -*/ -/*******************************************************************/ +* @note _collect_machine_list +* @param dev_manager -- the device manager +* @param sorted_machine_objs -- return the sorted machine objects +* @param best_one -- return the best one +*******************************************************************/ static void _collect_sorted_machines(Slic3r::DeviceManager* dev_manager, std::vector<MachineObject*>& sorted_machine_objs) @@ -3913,7 +3913,7 @@ _collect_sorted_machines(Slic3r::DeviceManager* dev_manager, }; // collect from user machine list - const auto& user_machine_list = dev_manager->get_my_machine_list();// user machine list + const auto& user_machine_list = dev_manager->get_my_machine_list(dev_manager->get_current_printer_agent_id());// user machine list for (const auto& elem : user_machine_list) { MachineObject* mobj = elem.second; @@ -5497,7 +5497,7 @@ void SelectMachineDialog::reset_and_sync_ams_list() first_enabled_id = extruder; } - item->Bind(wxEVT_LEFT_UP, [this, item, materials, extruder](wxMouseEvent &e) {}); + item->Bind(wxEVT_LEFT_UP, [materials](wxMouseEvent &e) {}); item->Bind(wxEVT_LEFT_DOWN, [this, item, materials, extruder](wxMouseEvent &e) { if (!item->m_enable) {return;} if (!m_check_flag || m_print_status == PrintDialogStatus::PrintStatusUnsupportedPrinter) { return; } /*STUDIO-11301*/ @@ -5694,10 +5694,15 @@ void SelectMachineDialog::clone_thumbnail_data() { m_preview_colors_in_thumbnail.resize(m_materialList.size()); } while (iter != m_materialList.end()) { - int id = iter->first; Material * item = iter->second; MaterialItem *m = item->item; - m_preview_colors_in_thumbnail[id] = m->m_material_coloul; + // Orca: key the preview colours by filament slot, as m_cur_colors_in_thumbnail and + // SyncAmsInfoDialog already do, so recompute_mixed_slot_colors() below can look a mixed + // slot's component colours up by id (BBS keys this array by list position). + if (item->id >= m_preview_colors_in_thumbnail.size()) { + m_preview_colors_in_thumbnail.resize(item->id + 1); + } + m_preview_colors_in_thumbnail[item->id] = m->m_material_coloul; if (item->id < m_cur_colors_in_thumbnail.size()) { m_cur_colors_in_thumbnail[item->id] = m->m_ams_coloul; } @@ -5707,6 +5712,20 @@ void SelectMachineDialog::clone_thumbnail_data() { } iter++; } + + // Expand color arrays to cover mixed (virtual) slots and compute their blended colors + const auto& cfg = wxGetApp().preset_bundle->project_config; + size_t total = 0; + if (auto* opt = cfg.option<ConfigOptionBools>("filament_is_mixed")) + total = opt->values.size(); + size_t target = std::max(total, m_cur_colors_in_thumbnail.size()); + if (m_cur_colors_in_thumbnail.size() < target) + m_cur_colors_in_thumbnail.resize(target); + if (m_preview_colors_in_thumbnail.size() < target) + m_preview_colors_in_thumbnail.resize(target); + recompute_mixed_slot_colors(m_preview_colors_in_thumbnail, cfg); + recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg); + //copy data auto &data = m_cur_input_thumbnail_data; m_preview_thumbnail_data.reset(); @@ -5881,6 +5900,10 @@ void SelectMachineDialog::change_default_normal(int old_filament_id, wxColour te return; } } + // Recompute mixed slot colors after physical slot color change + const auto& cfg = wxGetApp().preset_bundle->project_config; + recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg); + ThumbnailData& data = m_cur_input_thumbnail_data; ThumbnailData& no_light_data = m_cur_no_light_thumbnail_data; if (data.width > 0 && data.height > 0 && data.width == no_light_data.width && data.height == no_light_data.height) { @@ -6105,8 +6128,8 @@ void SelectMachineDialog::set_default_from_sdcard() first_enabled_id = fo.id; } - item->Bind(wxEVT_LEFT_UP, [this, item, materials](wxMouseEvent& e) {}); - item->Bind(wxEVT_LEFT_DOWN, [this, obj_, item, materials, diameters_count, fo](wxMouseEvent& e) { + item->Bind(wxEVT_LEFT_UP, [materials](wxMouseEvent& e) {}); + item->Bind(wxEVT_LEFT_DOWN, [this, obj_, item, materials, fo](wxMouseEvent& e) { if (!item->m_enable) {return;} if (!m_check_flag || m_print_status == PrintDialogStatus::PrintStatusUnsupportedPrinter) { return; } /*STUDIO-11301*/ diff --git a/src/slic3r/GUI/SelectMachine.hpp b/src/slic3r/GUI/SelectMachine.hpp index fd326f7c85..46d6adf4f4 100644 --- a/src/slic3r/GUI/SelectMachine.hpp +++ b/src/slic3r/GUI/SelectMachine.hpp @@ -522,7 +522,7 @@ public: bool is_timeout(); int update_print_required_data(Slic3r::DynamicPrintConfig config, Slic3r::Model model, Slic3r::PlateDataPtrs plate_data_list, std::string file_name, std::string file_path); void set_print_type(PrintFromType type) {m_print_type = type;}; - bool Show(bool show); + bool Show(bool show) override; void show_init(); bool do_ams_mapping(MachineObject *obj_,bool use_ams); bool get_ams_mapping_result(std::string& mapping_array_str, std::string& mapping_array_str2, std::string& ams_mapping_info) const; diff --git a/src/slic3r/GUI/SelectMachinePop.cpp b/src/slic3r/GUI/SelectMachinePop.cpp index 492199569e..9b34e74f23 100644 --- a/src/slic3r/GUI/SelectMachinePop.cpp +++ b/src/slic3r/GUI/SelectMachinePop.cpp @@ -501,6 +501,7 @@ void SelectMachinePopup::update_other_devices() DeviceManager* dev = wxGetApp().getDeviceManager(); if (!dev) return; m_free_machine_list = dev->get_local_machinelist(); + const std::string current_agent_id = dev->get_current_printer_agent_id(); BOOST_LOG_TRIVIAL(trace) << "SelectMachinePopup update_other_devices start"; this->Freeze(); @@ -512,6 +513,10 @@ void SelectMachinePopup::update_other_devices() /* do not show printer bind state is empty */ if (!mobj->is_avaliable()) continue; + /* do not show devices discovered/bound by a different printer agent */ + if (mobj->printer_agent_id != current_agent_id) + continue; + if (!wxGetApp().is_user_login(wxGetApp().get_printer_cloud_provider()) && !mobj->is_lan_mode_printer()) continue; @@ -562,7 +567,7 @@ void SelectMachinePopup::update_other_devices() } } - op->Bind(EVT_CONNECT_LAN_PRINT, [this, mobj](wxCommandEvent &e) { + op->Bind(EVT_CONNECT_LAN_PRINT, [mobj](wxCommandEvent &e) { if (mobj) { if (mobj->is_lan_mode_printer()) { ConnectPrinterDialog dlg(wxGetApp().mainframe, wxID_ANY, _L("Input access code")); @@ -574,7 +579,7 @@ void SelectMachinePopup::update_other_devices() } }); - op->Bind(EVT_BIND_MACHINE, [this, mobj](wxCommandEvent &e) { + op->Bind(EVT_BIND_MACHINE, [mobj](wxCommandEvent &e) { BindMachineDialog dlg; dlg.update_machine_info(mobj); int dlg_result = wxID_CANCEL; @@ -634,7 +639,7 @@ void SelectMachinePopup::update_user_devices() } m_bind_machine_list.clear(); - m_bind_machine_list = dev->get_my_machine_list(); + m_bind_machine_list = dev->get_my_machine_list(dev->get_current_printer_agent_id()); //sort list std::vector<std::pair<std::string, MachineObject*>> user_machine_list; @@ -695,7 +700,7 @@ void SelectMachinePopup::update_user_devices() op->set_printer_state(PrinterState::LOCK); } } - op->Bind(EVT_UNBIND_MACHINE, [this, dev, mobj](wxCommandEvent& e) { + op->Bind(EVT_UNBIND_MACHINE, [dev, mobj](wxCommandEvent& e) { dev->set_selected_machine(""); if (mobj) { AppConfig* config = wxGetApp().app_config; @@ -704,7 +709,6 @@ void SelectMachinePopup::update_user_devices() } mobj->set_access_code(""); - mobj->erase_user_access_code(); } if (GUI::wxGetApp().plater()) @@ -716,7 +720,7 @@ void SelectMachinePopup::update_user_devices() } else { op->show_printer_bind(true, PrinterBindState::ALLOW_UNBIND); - op->Bind(EVT_UNBIND_MACHINE, [this, mobj, dev](wxCommandEvent& e) { + op->Bind(EVT_UNBIND_MACHINE, [mobj, dev](wxCommandEvent& e) { // show_unbind_dialog UnBindMachineDialog dlg; dlg.update_machine_info(mobj); @@ -743,7 +747,7 @@ void SelectMachinePopup::update_user_devices() } } - op->Bind(EVT_CONNECT_LAN_PRINT, [this, mobj](wxCommandEvent &e) { + op->Bind(EVT_CONNECT_LAN_PRINT, [mobj](wxCommandEvent &e) { if (mobj) { if (mobj->is_lan_mode_printer()) { ConnectPrinterDialog dlg(wxGetApp().mainframe, wxID_ANY, _L("Input access code")); @@ -755,7 +759,7 @@ void SelectMachinePopup::update_user_devices() } }); - op->Bind(EVT_EDIT_PRINT_NAME, [this, mobj](wxCommandEvent &e) { + op->Bind(EVT_EDIT_PRINT_NAME, [mobj](wxCommandEvent &e) { EditDevNameDialog dlg; dlg.set_machine_obj(mobj); dlg.ShowModal(); diff --git a/src/slic3r/GUI/SelectMachinePop.hpp b/src/slic3r/GUI/SelectMachinePop.hpp index 757cb08ed5..764ffd6af5 100644 --- a/src/slic3r/GUI/SelectMachinePop.hpp +++ b/src/slic3r/GUI/SelectMachinePop.hpp @@ -181,8 +181,6 @@ private: PinCodePanel* m_panel_direct_connection{nullptr}; wxWindow* m_placeholder_panel{nullptr}; HyperLink* m_hyperlink{nullptr}; // ORCA - Label* m_ping_code_text{nullptr}; - wxStaticBitmap* m_img_ping_code{nullptr}; wxBoxSizer * m_sizer_body{nullptr}; wxBoxSizer * m_sizer_my_devices{nullptr}; wxBoxSizer * m_sizer_other_devices{nullptr}; diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index 5d6a545873..b6d7abde21 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -1270,9 +1270,7 @@ void Selection::translate(const Vec3d &displacement, TransformationType transfor } else { if (v.is_wipe_tower) {//in world cs int plate_idx = v.object_idx() - 1000; - BoundingBoxf3 plate_bbox = wxGetApp().plater()->get_partplate_list().get_plate(plate_idx)->get_build_volume(true); - BoundingBox plate_bbox2d = BoundingBox(scaled(Vec2f(plate_bbox.min[0], plate_bbox.min[1])), scaled(Vec2f(plate_bbox.max[0], plate_bbox.max[1]))); - Vec3d tower_size = v.bounding_box().size(); + const Polygons bed_polys{wxGetApp().plater()->get_partplate_list().get_plate(plate_idx)->get_shared_printable_polygon()}; Vec3d tower_origin = m_cache.volumes_data[i].get_volume_position(); Vec3d actual_displacement = displacement; bool show_read_wipe_tower = wxGetApp().plater()->get_partplate_list().get_plate(plate_idx)->fff_print()->is_step_done(psWipeTower); @@ -1287,18 +1285,7 @@ void Selection::translate(const Vec3d &displacement, TransformationType transfor BoundingBoxf3 tower_bbox = v.bounding_box(); tower_bbox.translate(actual_displacement + tower_origin); BoundingBox tower_bbox2d = BoundingBox(scaled(Vec2f(tower_bbox.min[0], tower_bbox.min[1])), scaled(Vec2f(tower_bbox.max[0], tower_bbox.max[1]))); - Vec2f offset = WipeTower::move_box_inside_box(tower_bbox2d, plate_bbox2d,scaled(margin)); - //if (tower_origin(0) + actual_displacement(0) - margin < plate_bbox.min(0)) { - // actual_displacement(0) = plate_bbox.min(0) - tower_origin(0) + margin; - //} else if (tower_origin(0) + actual_displacement(0) + tower_size(0) + margin > plate_bbox.max(0)) { - // actual_displacement(0) = plate_bbox.max(0) - tower_origin(0) - tower_size(0) - margin; - //} - - //if (tower_origin(1) + actual_displacement(1) - margin < plate_bbox.min(1)) { - // actual_displacement(1) = plate_bbox.min(1) - tower_origin(1) + margin; - //} else if (tower_origin(1) + actual_displacement(1) + tower_size(1) + margin > plate_bbox.max(1)) { - // actual_displacement(1) = plate_bbox.max(1) - tower_origin(1) - tower_size(1) - margin; - //} + const Vec2f offset = WipeTower::move_box_inside_polygon(tower_bbox2d, bed_polys, scaled(margin)); actual_displacement += Vec3d(offset[0], offset[1],0); v.set_volume_offset(m_cache.volumes_data[i].get_volume_position() + actual_displacement); } diff --git a/src/slic3r/GUI/SendMultiMachinePage.cpp b/src/slic3r/GUI/SendMultiMachinePage.cpp index 7b7c42922f..3a52caec3f 100644 --- a/src/slic3r/GUI/SendMultiMachinePage.cpp +++ b/src/slic3r/GUI/SendMultiMachinePage.cpp @@ -1468,7 +1468,7 @@ void SendMultiMachinePage::sync_ams_list() item->set_ams_info(wxColour("#CECECE"), "Ext", 0, std::vector<wxColour>()); m_ams_list_sizer->Add(item, 0, wxALL, FromDIP(4)); - item->Bind(wxEVT_LEFT_UP, [this, item, materials, extruder](wxMouseEvent& e) {}); + item->Bind(wxEVT_LEFT_UP, [materials](wxMouseEvent& e) {}); item->Bind(wxEVT_LEFT_DOWN, [this, item, materials, extruder](wxMouseEvent& e) { MaterialHash::iterator iter = m_material_list.begin(); while (iter != m_material_list.end()) { diff --git a/src/slic3r/GUI/SendMultiMachinePage.hpp b/src/slic3r/GUI/SendMultiMachinePage.hpp index a531e01262..7d77849bf3 100644 --- a/src/slic3r/GUI/SendMultiMachinePage.hpp +++ b/src/slic3r/GUI/SendMultiMachinePage.hpp @@ -149,7 +149,6 @@ private: ScalableBitmap* print_time{ nullptr }; wxStaticBitmap* weightimg{ nullptr }; ScalableBitmap* print_weight{ nullptr }; - wxBoxSizer* m_thumbnail_sizer{ nullptr }; ThumbnailPanel* m_thumbnail_panel{nullptr}; wxPanel* m_panel_image{ nullptr }; wxBoxSizer* m_image_sizer{ nullptr }; diff --git a/src/slic3r/GUI/SendToPrinter.cpp b/src/slic3r/GUI/SendToPrinter.cpp index 17da4b66a0..a63d96ffce 100644 --- a/src/slic3r/GUI/SendToPrinter.cpp +++ b/src/slic3r/GUI/SendToPrinter.cpp @@ -978,18 +978,16 @@ void SendToPrinterDialog::on_ok(wxCommandEvent &event) m_send_job->on_check_ip_address_fail([this, token = std::weak_ptr(m_token)](int result) { CallAfter([token, this] { if (token.expired()) { return; } - if (this) { - SendFailedConfirm sfcDlg; - auto res = sfcDlg.ShowModal(); - m_status_bar->cancel(); + SendFailedConfirm sfcDlg; + auto res = sfcDlg.ShowModal(); + m_status_bar->cancel(); - if (res == wxYES) { - wxQueueEvent(m_button_ensure, new wxCommandEvent(wxEVT_BUTTON)); - } else if (res == wxAPPLY) { - wxCommandEvent *evt = new wxCommandEvent(EVT_CLEAR_IPADDRESS); - wxQueueEvent(this, evt); - wxGetApp().show_ip_address_enter_dialog(); - } + if (res == wxYES) { + wxQueueEvent(m_button_ensure, new wxCommandEvent(wxEVT_BUTTON)); + } else if (res == wxAPPLY) { + wxCommandEvent *evt = new wxCommandEvent(EVT_CLEAR_IPADDRESS); + wxQueueEvent(this, evt); + wxGetApp().show_ip_address_enter_dialog(); } }); }); diff --git a/src/slic3r/GUI/SendToPrinter.hpp b/src/slic3r/GUI/SendToPrinter.hpp index 14493a1f20..93215c4e06 100644 --- a/src/slic3r/GUI/SendToPrinter.hpp +++ b/src/slic3r/GUI/SendToPrinter.hpp @@ -62,7 +62,6 @@ private: bool m_is_in_sending_mode{ false }; bool m_is_rename_mode{ false }; bool enable_prepare_mode{ true }; - bool m_need_adaptation_screen{ false }; bool m_export_3mf_cancel{ false }; bool m_is_canceled{ false }; bool m_tcp_try_connect{true}; @@ -81,7 +80,6 @@ private: wxStaticBitmap* m_staticbitmap{ nullptr }; ThumbnailPanel* m_thumbnailPanel{ nullptr }; ComboBox* m_comboBox_printer{ nullptr }; - ComboBox* m_comboBox_bed{ nullptr }; Button* m_rename_button{ nullptr }; Button* m_button_refresh{ nullptr }; Button* m_button_ensure{ nullptr }; @@ -180,7 +178,7 @@ public: SendToPrinterDialog(Plater *plater = nullptr); ~SendToPrinterDialog(); - bool Show(bool show); + bool Show(bool show) override; bool is_timeout(); void on_rename_click(wxCommandEvent& event); void on_rename_enter(); diff --git a/src/slic3r/GUI/SkipPartCanvas.cpp b/src/slic3r/GUI/SkipPartCanvas.cpp index 5e01b8140f..7b7db213b1 100644 --- a/src/slic3r/GUI/SkipPartCanvas.cpp +++ b/src/slic3r/GUI/SkipPartCanvas.cpp @@ -287,7 +287,7 @@ void SkipPartCanvas::Render() glDisable(GL_CULL_FACE); glEnable(GL_STENCIL_TEST); - auto draw_shape = [this, border_w](const int stencil, const PartState part_type, const ColorRGB& rgb) { + auto draw_shape = [this](const int stencil, const PartState part_type, const ColorRGB& rgb) { glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); glStencilFunc(GL_ALWAYS, stencil, 0xFF); glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE); @@ -335,7 +335,7 @@ void SkipPartCanvas::Render() // stencil3 => skipped draw_shape(skipped_stencil, psSkipped, ColorRGB{95 / 255.f, 95 / 255.f, 95 / 255.f}); - auto draw_mask = [this, view_rect, border_w, w, h](const int stencil, const PartState part_type, + auto draw_mask = [this, view_rect](const int stencil, const PartState part_type, const ColorRGB& background, const ColorRGB& line, const ColorRGB& bound) { glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); glStencilFunc(GL_EQUAL, stencil, 0xFF); diff --git a/src/slic3r/GUI/SkipPartCanvas.hpp b/src/slic3r/GUI/SkipPartCanvas.hpp index a9c8f1fec7..d70ad3718f 100644 --- a/src/slic3r/GUI/SkipPartCanvas.hpp +++ b/src/slic3r/GUI/SkipPartCanvas.hpp @@ -87,7 +87,6 @@ private: std::unordered_map < uint32_t, std::vector<std::vector<FloatPoint>>> parts_triangles_; std::unordered_map < uint32_t, std::vector<std::vector<cv::Point>>> pick_parts_; std::unordered_map<uint32_t, PartState> parts_state_; - bool gl_inited_{false}; int zoom_percent_{100}; wxPoint offset_{0,0}; wxPoint drag_start_offset_{0,0}; diff --git a/src/slic3r/GUI/SliceInfoPanel.cpp b/src/slic3r/GUI/SliceInfoPanel.cpp index 37a3ba698d..a7e436c0ff 100644 --- a/src/slic3r/GUI/SliceInfoPanel.cpp +++ b/src/slic3r/GUI/SliceInfoPanel.cpp @@ -115,7 +115,7 @@ SliceInfoPopup::SliceInfoPopup(wxWindow *parent, wxBitmap bmp, BBLSliceInfo *inf f_sizer->Add(f_type, 0, wxEXPAND | wxALL, FromDIP(5)); f_sizer->Add(f_used_g, 0, wxEXPAND | wxALL, FromDIP(5)); grid_sizer->Add(f_sizer, 0, wxEXPAND, 0); - f_type->Bind(wxEVT_LEFT_DOWN, [this](auto &e) {}); + f_type->Bind(wxEVT_LEFT_DOWN, [](auto &e) {}); } } topSizer->Add(grid_sizer, 0, wxALL, FromDIP(5)); diff --git a/src/slic3r/GUI/StatusPanel.cpp b/src/slic3r/GUI/StatusPanel.cpp index 43522f352b..f7bd41fe84 100644 --- a/src/slic3r/GUI/StatusPanel.cpp +++ b/src/slic3r/GUI/StatusPanel.cpp @@ -983,7 +983,7 @@ void PrintingTaskPanel::paint(wxPaintEvent&) dc.DrawBitmap(m_thumbnail_bmp_display, wxPoint(0, 0)); } dc.SetFont(Label::Body_12); - + if (m_plate_index >= 0) { wxString plate_id_str = wxString::Format("%d", m_plate_index); dc.DrawText(plate_id_str, wxPoint(4, 4)); @@ -1271,7 +1271,7 @@ void PrintingTaskPanel::set_plate_index(int plate_idx) } void PrintingTaskPanel::market_scoring_show() -{ +{ m_score_staticline->Show(); m_score_subtask_info->Show(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " show market scoring page"; @@ -1402,7 +1402,7 @@ void StatusBasePanel::init_bitmaps() m_bitmap_fan_off = ScalableBitmap(this, "monitor_fan_off", 22); m_bitmap_speed = ScalableBitmap(this, "monitor_speed", 24); m_bitmap_speed_active = ScalableBitmap(this, "monitor_speed_active", 24); - + m_thumbnail_brokenimg = ScalableBitmap(this, "monitor_brokenimg", 120); m_thumbnail_sdcard = ScalableBitmap(this, "monitor_sdcard_thumbnail", 120); //m_bitmap_camera = create_scaled_bitmap("monitor_camera", nullptr, 18); @@ -1530,7 +1530,7 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page() // media_ctrl_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); // media_ctrl_panel->SetBackgroundColour(*wxBLACK); // wxBoxSizer *bSizer_monitoring = new wxBoxSizer(wxVERTICAL); - m_media_ctrl = new wxMediaCtrl2(this); + m_media_ctrl = new wxMediaCtrl3(this); m_media_ctrl->SetMinSize(wxSize(PAGE_MIN_WIDTH, FromDIP(288))); m_custom_camera_view = WebView::CreateWebView(this, wxEmptyString); @@ -2458,7 +2458,7 @@ StatusPanel::StatusPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, co m_project_task_panel->get_pause_resume_button()->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_subtask_pause_resume), NULL, this); m_project_task_panel->get_abort_button()->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_subtask_abort), NULL, this); m_project_task_panel->get_market_scoring_button()->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_market_scoring), NULL, this); - m_project_task_panel->get_market_retry_buttom()->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_market_retry), NULL, this); + m_project_task_panel->get_market_retry_buttom()->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_market_retry), NULL, this); m_project_task_panel->get_clean_button()->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_print_error_clean), NULL, this); m_setting_button->Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(StatusPanel::on_camera_enter), NULL, this); @@ -2520,7 +2520,7 @@ StatusPanel::~StatusPanel() m_project_task_panel->get_pause_resume_button()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_subtask_pause_resume), NULL, this); m_project_task_panel->get_abort_button()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_subtask_abort), NULL, this); m_project_task_panel->get_market_scoring_button()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_market_scoring), NULL, this); - m_project_task_panel->get_market_retry_buttom()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_market_retry), NULL, this); + m_project_task_panel->get_market_retry_buttom()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_market_retry), NULL, this); m_project_task_panel->get_clean_button()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_print_error_clean), NULL, this); m_setting_button->Disconnect(wxEVT_LEFT_DOWN, wxMouseEventHandler(StatusPanel::on_camera_enter), NULL, this); @@ -2566,7 +2566,7 @@ StatusPanel::~StatusPanel() if (sdcard_hint_dlg != nullptr) delete sdcard_hint_dlg; - if (m_score_data != nullptr) { + if (m_score_data != nullptr) { delete m_score_data; } } @@ -2590,7 +2590,7 @@ void StatusPanel::init_scaled_buttons() m_bpButton_e_down_10->SetCornerRadius(FromDIP(12)); } -void StatusPanel::on_market_scoring(wxCommandEvent &event) { +void StatusPanel::on_market_scoring(wxCommandEvent &event) { if (obj && obj->is_makeworld_subtask() && obj->rating_info && obj->rating_info->request_successful) { // model is mall model and has rating_id BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_market_scoring" ; if (m_score_data && m_score_data->rating_id == obj->rating_info->rating_id) { // current score data for model is same as mall model @@ -2599,7 +2599,7 @@ void StatusPanel::on_market_scoring(wxCommandEvent &event) { int ret = m_score_dlg.ShowModal(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": old data"; - if (ret == wxID_OK) { + if (ret == wxID_OK) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": old data is upload"; m_score_data->rating_id = -1; m_project_task_panel->set_star_count_dirty(false); @@ -2621,11 +2621,11 @@ void StatusPanel::on_market_scoring(wxCommandEvent &event) { std::string comment = obj->rating_info->content; if (!comment.empty()) { m_score_dlg.set_comment(comment); } - + std::vector<std::string> images_json_array; images_json_array = obj->rating_info->image_url_paths; if (!images_json_array.empty()) m_score_dlg.set_cloud_bitmap(images_json_array); - + int ret = m_score_dlg.ShowModal(); if (ret == wxID_OK) { @@ -3651,14 +3651,14 @@ void StatusPanel::update_basic_print_data(bool def) void StatusPanel::update_model_info() { auto get_subtask_fn = [this](BBLModelTask* subtask) { - CallAfter([this, subtask]() { + CallAfter([this, subtask]() { if (obj && obj->subtask_id_ == subtask->task_id) { obj->set_modeltask(subtask); } }); }; - + if (wxGetApp().getAgent() && obj) { BBLSubTask* curr_task = obj->get_subtask(); if (curr_task) { @@ -3982,7 +3982,7 @@ void StatusPanel::reset_printing_values() m_project_task_panel->update_left_time(NA_STR); m_project_task_panel->update_layers_num(true, wxString::Format(_L("Layer: %s"), NA_STR)); update_calib_bitmap(); - + task_thumbnail_state = ThumbnailState::PLACE_HOLDER; m_start_loading_thumbnail = false; m_load_sdcard_thumbnail = false; @@ -4037,7 +4037,7 @@ bool StatusPanel::check_axis_z_at_home(MachineObject* obj) } void StatusPanel::on_axis_ctrl_z_up_10(wxCommandEvent &event) -{ +{ if (obj) { obj->command_axis_control("Z", 1.0, -10.0f, 900); if (!check_axis_z_at_home(obj)) @@ -5396,7 +5396,7 @@ void StatusPanel::msw_rescale() m_calibration_btn->Rescale(); m_options_btn->SetMinSize(wxSize(-1, FromDIP(26))); - m_options_btn->Rescale(); + m_options_btn->Rescale(); m_safety_btn->SetMinSize(wxSize(-1, FromDIP(26))); m_safety_btn->Rescale(); @@ -5578,11 +5578,11 @@ ScoreDialog::ScoreDialog(wxWindow *parent, ScoreData *score_data) , m_upload_status_code(StatusCode::CODE_NUMBER) { m_tocken.reset(new int(0)); - + wxBoxSizer *m_main_sizer = get_main_sizer(score_data->local_to_url_image, score_data->comment_text); m_image_url_paths = score_data->image_url_paths; - + this->SetSizer(m_main_sizer); Fit(); @@ -5598,16 +5598,16 @@ void ScoreDialog::on_dpi_changed(const wxRect &suggested_rect) {} void ScoreDialog::OnBitmapClicked(wxMouseEvent &event) { wxStaticBitmap *clickedBitmap = dynamic_cast<wxStaticBitmap *>(event.GetEventObject()); - if (m_image.find(clickedBitmap) != m_image.end()) { + if (m_image.find(clickedBitmap) != m_image.end()) { if (!m_image[clickedBitmap].is_selected) { - for (auto panel : m_image[clickedBitmap].image_broad) { + for (auto panel : m_image[clickedBitmap].image_broad) { panel->Show(); } m_image[clickedBitmap].is_selected = true; m_selected_image_list.insert(clickedBitmap); } else { - for (auto panel : m_image[clickedBitmap].image_broad) { - panel->Hide(); + for (auto panel : m_image[clickedBitmap].image_broad) { + panel->Hide(); } m_image[clickedBitmap].is_selected = false; m_selected_image_list.erase(clickedBitmap); @@ -5624,9 +5624,9 @@ void ScoreDialog::OnBitmapClicked(wxMouseEvent &event) } std::set <std::pair<wxStaticBitmap * ,wxString>> ScoreDialog::add_need_upload_imgs() -{ +{ std::set<std::pair<wxStaticBitmap *, wxString>> need_upload_images; - for (auto bitmap : m_image) { + for (auto bitmap : m_image) { if (!bitmap.second.is_uploaded) { wxString &local_image_path = bitmap.second.local_image_url; if (!local_image_path.empty()) { need_upload_images.insert(std::make_pair(bitmap.first, local_image_path)); } @@ -5646,7 +5646,7 @@ std::pair<wxStaticBitmap *, ScoreDialog::ImageMsg> ScoreDialog::create_local_thu cur_image_msg.local_image_url = local_path; cur_image_msg.img_url_paths = ""; cur_image_msg.is_uploaded = false; - + wxStaticBitmap *imageCtrl = new wxStaticBitmap(this, wxID_ANY, wxBitmap(wxImage(local_path, wxBITMAP_TYPE_ANY).Rescale(FromDIP(80), FromDIP(60))), wxDefaultPosition, wxDefaultSize, 0); imageCtrl->Bind(wxEVT_LEFT_DOWN, &ScoreDialog::OnBitmapClicked, this); @@ -5711,7 +5711,7 @@ void ScoreDialog::update_static_bitmap(wxStaticBitmap* static_bitmap, wxImage im } wxBoxSizer *ScoreDialog::create_broad_sizer(wxStaticBitmap *bitmap, ImageMsg& cur_image_msg) -{ +{ // tb: top and bottom lr: left and right auto m_image_tb_broad = new wxBoxSizer(wxVERTICAL); auto line_top = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(-1, 1), wxTAB_TRAVERSAL); @@ -5755,7 +5755,7 @@ void ScoreDialog::init() { fail_image = wxImage(Slic3r::resources_dir() + "/images/oss_picture_load_failed.png", wxBITMAP_TYPE_ANY); } -wxBoxSizer *ScoreDialog::get_score_sizer() { +wxBoxSizer *ScoreDialog::get_score_sizer() { wxBoxSizer *score_sizer = new wxBoxSizer(wxHORIZONTAL); wxStaticText *static_score_text = new wxStaticText(this, wxID_ANY, _L("Rate"), wxDefaultPosition, wxDefaultSize, 0); static_score_text->Wrap(-1); @@ -5878,18 +5878,18 @@ wxBoxSizer *ScoreDialog::get_photo_btn_sizer() { for (int i = 0; i < filePaths.GetCount(); i++) { //It's ugly, but useful bool is_repeat = false; for (auto image : m_image) { - if (filePaths[i] == image.second.local_image_url) { + if (filePaths[i] == image.second.local_image_url) { is_repeat = true; continue; } } if (!is_repeat) { local_path.push_back(std::make_pair(filePaths[i], "")); - if (local_path.size() + m_image.size() > m_photo_nums) { - break; + if (local_path.size() + m_image.size() > m_photo_nums) { + break; } } - + } load_photo(local_path); @@ -6007,7 +6007,7 @@ wxBoxSizer *ScoreDialog::get_button_sizer() } } progress_dialog->Hide(); - if (progress_dialog) { + if (progress_dialog) { delete progress_dialog; progress_dialog = nullptr; } @@ -6141,7 +6141,7 @@ wxBoxSizer *ScoreDialog::get_main_sizer(const std::vector<std::pair<wxString, st m_main_sizer->Add(m_photo_sizer, 0, wxEXPAND | wxTOP, FromDIP(8)); m_image_sizer = new wxGridSizer(5, FromDIP(5), FromDIP(5)); - if (!images.empty()) { + if (!images.empty()) { load_photo(images); } m_main_sizer->Add(m_image_sizer, 0, wxEXPAND | wxLEFT, FromDIP(24)); @@ -6153,7 +6153,7 @@ wxBoxSizer *ScoreDialog::get_main_sizer(const std::vector<std::pair<wxString, st return m_main_sizer; } -ScoreData ScoreDialog::get_score_data() { +ScoreData ScoreDialog::get_score_data() { ScoreData score_data; score_data.rating_id = m_rating_id; score_data.design_id = m_design_id; @@ -6164,20 +6164,20 @@ ScoreData ScoreDialog::get_score_data() { score_data.comment_text = m_comment_text->GetValue(); score_data.image_url_paths = m_image_url_paths; for (auto img : m_image) { score_data.local_to_url_image.push_back(std::make_pair(img.second.local_image_url, img.second.img_url_paths)); } - + return score_data; } void ScoreDialog::set_comment(std::string comment) { - if (m_comment_text) { + if (m_comment_text) { m_comment_text->SetValue(wxString::FromUTF8(comment)); } } void ScoreDialog::set_cloud_bitmap(std::vector<std::string> cloud_bitmaps) -{ +{ m_image_url_paths = cloud_bitmaps; for (std::string &url : cloud_bitmaps) { if (std::string::npos == url.find(m_model_id)) continue; diff --git a/src/slic3r/GUI/StatusPanel.hpp b/src/slic3r/GUI/StatusPanel.hpp index d0c8c99f49..341bb9ab69 100644 --- a/src/slic3r/GUI/StatusPanel.hpp +++ b/src/slic3r/GUI/StatusPanel.hpp @@ -14,7 +14,6 @@ #include <wx/sizer.h> #include <wx/gbsizer.h> #include <wx/webrequest.h> -#include "wxMediaCtrl2.h" #include "MediaPlayCtrl.h" #include "AMSSetting.hpp" #include "Calibration.hpp" @@ -195,11 +194,11 @@ public: void set_cloud_bitmap(std::vector<std::string> cloud_bitmaps); protected: - enum StatusCode { - UPLOAD_PROGRESS = 0, - UPLOAD_EXIST_ISSUE, + enum StatusCode { + UPLOAD_PROGRESS = 0, + UPLOAD_EXIST_ISSUE, UPLOAD_IMG_FAILED, - CODE_NUMBER + CODE_NUMBER }; std::shared_ptr<int> m_tocken; @@ -217,7 +216,7 @@ protected: { wxString local_image_url; //local image path std::string img_url_paths; // oss url path - vector<wxPanel *> image_broad; + vector<wxPanel *> image_broad; bool is_selected; bool is_uploaded; // load wxBoxSizer * image_tb_broad = nullptr; @@ -252,7 +251,7 @@ protected: std::set<std::pair<wxStaticBitmap *, wxString>> add_need_upload_imgs(); std::pair<wxStaticBitmap *, ImageMsg> create_local_thumbnail(wxString &local_path); std::pair<wxStaticBitmap *, ImageMsg> create_oss_thumbnail(std::string &oss_path); - + }; class PrintingTaskPanel : public wxPanel @@ -261,7 +260,7 @@ public: PrintingTaskPanel(wxWindow* parent, PrintingTaskType type); ~PrintingTaskPanel(); void create_panel(wxWindow* parent); - + private: MachineObject* m_obj{nullptr}; @@ -353,7 +352,7 @@ public: void set_plate_index(int plate_idx = -1); void market_scoring_show(); void market_scoring_hide(); - + public: ScalableButton* get_abort_button() {return m_button_abort;}; ScalableButton* get_pause_resume_button() {return m_button_pause_resume;}; @@ -443,7 +442,7 @@ protected: wxStaticBitmap* m_camera_switch_button; - wxMediaCtrl2 * m_media_ctrl; + wxMediaCtrl3 * m_media_ctrl; MediaPlayCtrl * m_media_play_ctrl; Label * m_staticText_printing; @@ -567,7 +566,7 @@ protected: virtual void on_bed_temp_kill_focus(wxFocusEvent &event) { event.Skip(); } virtual void on_bed_temp_set_focus(wxFocusEvent &event) { event.Skip(); } virtual void on_nozzle_temp_kill_focus(wxFocusEvent &event) { event.Skip(); } - virtual void on_nozzle_temp_set_focus(wxFocusEvent &event) { event.Skip(); } + virtual void on_nozzle_temp_set_focus(wxFocusEvent &event) { event.Skip(); } virtual void on_nozzle_fan_switch(wxCommandEvent &event) { event.Skip(); } virtual void on_printing_fan_switch(wxCommandEvent &event) { event.Skip(); } virtual void on_axis_ctrl_z_up_10(wxCommandEvent &event) { event.Skip(); } diff --git a/src/slic3r/GUI/StepMeshDialog.cpp b/src/slic3r/GUI/StepMeshDialog.cpp index b1a8e2319a..c6131b7bfa 100644 --- a/src/slic3r/GUI/StepMeshDialog.cpp +++ b/src/slic3r/GUI/StepMeshDialog.cpp @@ -167,7 +167,7 @@ StepMeshDialog::StepMeshDialog(wxWindow* parent, Slic3r::Step& file, double line e.Skip(); })); // textctrl bind slider - linear_input->Bind(wxEVT_TEXT, ([this, linear_slider, linear_input](wxCommandEvent& e) { + linear_input->Bind(wxEVT_TEXT, ([linear_slider, linear_input](wxCommandEvent& e) { double slider_value_long; int slider_value; wxString value = linear_input->GetTextCtrl()->GetValue(); @@ -222,7 +222,7 @@ StepMeshDialog::StepMeshDialog(wxWindow* parent, Slic3r::Step& file, double line e.Skip(); })); // textctrl bind slider - angle_input->Bind(wxEVT_TEXT, ([this, angle_slider, angle_input](wxCommandEvent& e) { + angle_input->Bind(wxEVT_TEXT, ([angle_slider, angle_input](wxCommandEvent& e) { double slider_value_long; int slider_value; wxString value = angle_input->GetTextCtrl()->GetValue(); diff --git a/src/slic3r/GUI/SyncAmsInfoDialog.cpp b/src/slic3r/GUI/SyncAmsInfoDialog.cpp index 3c3ea8b609..7ae48008c8 100644 --- a/src/slic3r/GUI/SyncAmsInfoDialog.cpp +++ b/src/slic3r/GUI/SyncAmsInfoDialog.cpp @@ -30,6 +30,7 @@ #include "DeviceCore/DevManager.h" #include "DeviceCore/DevMapping.h" #include "DeviceCore/DevStorage.h" +#include "FilamentBitmapUtils.hpp" using namespace Slic3r; using namespace Slic3r::GUI; @@ -1112,7 +1113,7 @@ void SyncAmsInfoDialog::init_bind() e.Skip(); }); - Bind(EVT_CONNECT_LAN_MODE_PRINT, [this](wxCommandEvent &e) { + Bind(EVT_CONNECT_LAN_MODE_PRINT, [](wxCommandEvent &e) { if (e.GetInt() == 0) { DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager(); if (!dev) return; @@ -1218,8 +1219,8 @@ void SyncAmsInfoDialog::sync_ams_mapping_result(std::vector<FilamentInfo> &resul iter++; } } - auto tab_index = (MainFrame::TabPosition) dynamic_cast<Notebook *>(wxGetApp().tab_panel())->GetSelection(); - if (tab_index == MainFrame::TabPosition::tp3DEditor || tab_index == MainFrame::TabPosition::tpPreview) { + wxString tab_name = wxGetApp().tab_panel()->GetSelectedPageName(); + if (tab_name == TAB_ID_PREPARE || tab_name == TAB_ID_PREVIEW) { updata_thumbnail_data_after_connected_printer(); } } @@ -2575,6 +2576,10 @@ void SyncAmsInfoDialog::reset_and_sync_ams_list() m_materialList.clear(); m_filaments.clear(); + // Mixed-color slots are virtual: they never occupy a tray, so they must not appear as + // AMS sync targets. + auto* is_mixed_opt = preset_bundle->project_config.option<ConfigOptionBools>("filament_is_mixed"); + bool use_double_extruder = get_is_double_extruder(); if (use_double_extruder) { const auto &project_config = preset_bundle->project_config; @@ -2592,6 +2597,8 @@ void SyncAmsInfoDialog::reset_and_sync_ams_list() auto colour_rgb = wxColour((int) rgb[0], (int) rgb[1], (int) rgb[2], (int) rgb[3]); if (extruder >= materials.size() || extruder < 0 || extruder >= display_materials.size()) continue; + if (is_mixed_opt && extruder < (int) is_mixed_opt->values.size() && is_mixed_opt->values[extruder]) + continue; if (contronal_index % SYNC_FLEX_GRID_COL == 0) { wxBoxSizer *ams_tip_sizer = new wxBoxSizer(wxVERTICAL); @@ -2637,7 +2644,7 @@ void SyncAmsInfoDialog::reset_and_sync_ams_list() item_index++; contronal_index++; - item->Bind(wxEVT_LEFT_UP, [this, item, materials, extruder](wxMouseEvent &e) {}); + item->Bind(wxEVT_LEFT_UP, [materials](wxMouseEvent &e) {}); item->Bind(wxEVT_LEFT_DOWN, [this, item, materials, extruder, item_index_str](wxMouseEvent &e) { MaterialHash::iterator iter = m_materialList.begin(); while (iter != m_materialList.end()) { @@ -2793,6 +2800,10 @@ void SyncAmsInfoDialog::generate_override_fix_ams_list() m_fix_materialList.clear(); m_fix_filaments.clear(); + // Mixed-color slots are virtual: they never occupy a tray, so they must not appear as + // AMS sync targets. + auto* is_mixed_opt = preset_bundle->project_config.option<ConfigOptionBools>("filament_is_mixed"); + bool use_double_extruder = get_is_double_extruder(); if (use_double_extruder) { const auto &project_config = preset_bundle->project_config; @@ -2810,6 +2821,8 @@ void SyncAmsInfoDialog::generate_override_fix_ams_list() auto colour_rgb = wxColour((int) rgb[0], (int) rgb[1], (int) rgb[2], (int) rgb[3]); if (extruder >= extruders.size() || extruder < 0 || extruder >= m_ams_combo_info.ams_filament_colors.size()) continue; + if (is_mixed_opt && extruder < (int) is_mixed_opt->values.size() && is_mixed_opt->values[extruder]) + continue; if (contronal_index % SYNC_FLEX_GRID_COL == 0) { wxBoxSizer *ams_tip_sizer = new wxBoxSizer(wxVERTICAL); @@ -2931,6 +2944,20 @@ void SyncAmsInfoDialog::clone_thumbnail_data() iter++; } } + + // Expand color arrays to cover mixed (virtual) slots and compute their blended colors + const auto& cfg = wxGetApp().preset_bundle->project_config; + size_t total = 0; + if (auto* opt = cfg.option<ConfigOptionBools>("filament_is_mixed")) + total = opt->values.size(); + size_t target = std::max(total, m_cur_colors_in_thumbnail.size()); + if (m_cur_colors_in_thumbnail.size() < target) + m_cur_colors_in_thumbnail.resize(target); + if (m_preview_colors_in_thumbnail.size() < target) + m_preview_colors_in_thumbnail.resize(target); + recompute_mixed_slot_colors(m_preview_colors_in_thumbnail, cfg); + recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg); + // copy data auto &data = m_cur_input_thumbnail_data; m_preview_thumbnail_data.reset(); @@ -3119,6 +3146,10 @@ void SyncAmsInfoDialog::change_default_normal(int old_filament_id, wxColour temp return; } } + // Recompute mixed slot colors after physical slot color change + const auto& cfg = wxGetApp().preset_bundle->project_config; + recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg); + ThumbnailData &data = m_cur_input_thumbnail_data; ThumbnailData &no_light_data = m_cur_no_light_thumbnail_data; if (data.width > 0 && data.height > 0 && data.width == no_light_data.width && data.height == no_light_data.height) { diff --git a/src/slic3r/GUI/SyncAmsInfoDialog.hpp b/src/slic3r/GUI/SyncAmsInfoDialog.hpp index 8ff8f18aff..b0868f8c58 100644 --- a/src/slic3r/GUI/SyncAmsInfoDialog.hpp +++ b/src/slic3r/GUI/SyncAmsInfoDialog.hpp @@ -307,8 +307,6 @@ private: // wxBoxSizer * m_plate_combox_sizer = nullptr; wxBoxSizer * m_mode_combox_sizer = nullptr; //wxStaticText * m_printer_title = nullptr; - wxStaticText * m_printer_device_name = nullptr; - wxStaticText * m_printer_is_map_title = nullptr; CapsuleButton * m_colormap_btn = nullptr; CapsuleButton * m_override_btn = nullptr; @@ -326,7 +324,6 @@ private: bool m_check_dirty_fialment = true; bool m_expand_more_settings = true; - bool m_image_is_top = false; const int LEFT_THUMBNAIL_SIZE_WIDTH = 100; const int RIGHT_THUMBNAIL_SIZE_WIDTH = 300; @@ -371,7 +368,7 @@ public: }; FinishSyncAmsDialog(InputInfo &input_info); ~FinishSyncAmsDialog() override; - void deal_ok(); + void deal_ok() override; void update_info(InputInfo& info); bool Layout() override; diff --git a/src/slic3r/GUI/SysInfoDialog.cpp b/src/slic3r/GUI/SysInfoDialog.cpp index 933cfb4d7c..585767d318 100644 --- a/src/slic3r/GUI/SysInfoDialog.cpp +++ b/src/slic3r/GUI/SysInfoDialog.cpp @@ -21,7 +21,9 @@ #ifdef _WIN32 // The standard Windows includes. #define WIN32_LEAN_AND_MEAN + #ifndef NOMINMAX #define NOMINMAX + #endif #include <Windows.h> #include <psapi.h> #endif /* _WIN32 */ diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index c8fa524ca5..e650c45126 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4,6 +4,7 @@ #include "PresetHints.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/FilamentMixer.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp" @@ -33,6 +34,7 @@ #include "GUI_App.hpp" #include "GUI_ObjectList.hpp" +#include "slic3r/Utils/NetworkAgentFactory.hpp" #include "slic3r/Utils/PresetUpdater.hpp" #include "slic3r/plugin/PluginConfig.hpp" #include "Plater.hpp" @@ -457,7 +459,7 @@ void Tab::create_preset_tab() // tree m_tabctrl = new TabCtrl(panel, wxID_ANY, wxDefaultPosition, wxSize(20 * m_em_unit, -1), wxTR_NO_BUTTONS | wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES | wxBORDER_NONE | wxWANTS_CHARS | wxTR_FULL_ROW_HIGHLIGHT); - m_tabctrl->Bind(wxEVT_RIGHT_DOWN, [this](auto &e) {}); // disable right select + m_tabctrl->Bind(wxEVT_RIGHT_DOWN, [](auto &e) {}); // disable right select m_tabctrl->SetFont(Label::Body_14); //m_left_sizer->Add(m_tabctrl, 1, wxEXPAND); const int img_sz = int(32 * scale_factor + 0.5f); @@ -2135,43 +2137,9 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) m_last_sparse_infill_rotate_template_value = m_config->opt_string("sparse_infill_rotate_template"); } - if(opt_key=="layer_height"){ - auto min_layer_height_from_nozzle=m_preset_bundle->full_config().option<ConfigOptionFloats>("min_layer_height")->values; - auto max_layer_height_from_nozzle=m_preset_bundle->full_config().option<ConfigOptionFloats>("max_layer_height")->values; - auto layer_height_floor = *std::min_element(min_layer_height_from_nozzle.begin(), min_layer_height_from_nozzle.end()); - auto layer_height_ceil = *std::max_element(max_layer_height_from_nozzle.begin(), max_layer_height_from_nozzle.end()); - const auto lh = m_config->opt_float("layer_height"); - bool exceed_minimum_flag = lh < layer_height_floor; - bool exceed_maximum_flag = lh > layer_height_ceil; - - if (exceed_maximum_flag || exceed_minimum_flag) { - if (lh < EPSILON) { - auto msg_text = _(L("Layer height is too small.\nIt will set to min_layer_height\n")); - MessageDialog dialog(wxGetApp().plater(), msg_text, "", wxICON_WARNING | wxOK); - dialog.SetButtonLabel(wxID_OK, _L("OK")); - dialog.ShowModal(); - auto new_conf = *m_config; - new_conf.set_key_value("layer_height", new ConfigOptionFloat(layer_height_floor)); - m_config_manipulation.apply(m_config, &new_conf); - } else { - wxString msg_text = _(L("Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, " - "this may cause printing quality issues.")); - msg_text += "\n\n" + _(L("Adjust to the set range automatically?\n")); - MessageDialog dialog(wxGetApp().plater(), msg_text, "", wxICON_WARNING | wxYES | wxNO); - dialog.SetButtonLabel(wxID_YES, _L("Adjust")); - dialog.SetButtonLabel(wxID_NO, _L("Ignore")); - auto answer = dialog.ShowModal(); - auto new_conf = *m_config; - if (answer == wxID_YES) { - if (exceed_maximum_flag) - new_conf.set_key_value("layer_height", new ConfigOptionFloat(layer_height_ceil)); - if (exceed_minimum_flag) - new_conf.set_key_value("layer_height", new ConfigOptionFloat(layer_height_floor)); - m_config_manipulation.apply(m_config, &new_conf); - } - } + if (opt_key == "layer_height") { + if (m_config_manipulation.check_layer_height(m_config)) wxGetApp().plater()->update(); - } } string opt_key_without_idx = opt_key.substr(0, opt_key.find('#')); @@ -2206,18 +2174,25 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) //Orca: sync filament num if it's a multi tool printer if (opt_key == "extruders_count" && !m_config->opt_bool("single_extruder_multi_material")){ - auto num_extruder = boost::any_cast<size_t>(value); - int old_filament_size = wxGetApp().preset_bundle->filament_presets.size(); - std::vector<std::string> new_colors; - for (int i = old_filament_size; i < num_extruder; ++i) { - wxColour new_col = Plater::get_next_color_for_filament(); - std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); - new_colors.push_back(new_color); + const size_t num_extruder = boost::any_cast<size_t>(value); + auto *bundle = wxGetApp().preset_bundle; + Sidebar &sidebar = wxGetApp().plater()->sidebar(); + // A tool changer feeds filament N from nozzle N, so the extruder count sizes the physical + // run only; mixed slots are virtual and keep the tail. Go one slot at a time through the + // sidebar's own +/- calls: they insert ahead of the mixed tail and renumber filament ids, + // painted facets, custom g-code and mixed components, which a bulk resize clamps away. + // Both also refresh the print tab and export the selections, so nothing to do afterwards. + size_t physical = bundle->num_physical_filaments(); + while (physical != num_extruder) { + if (physical < num_extruder) + sidebar.add_custom_filament(Plater::get_next_color_for_filament()); + else + sidebar.delete_filament(physical - 1); // physical > num_extruder >= 1 + const size_t updated = bundle->num_physical_filaments(); + if (updated == physical) + break; // the call declined, e.g. the total slot limit - do not spin + physical = updated; } - wxGetApp().preset_bundle->set_num_filaments(num_extruder, new_colors); - wxGetApp().plater()->on_filament_count_change(num_extruder); - wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); - wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); } //Orca: disable purge_in_prime_tower if single_extruder_multi_material is disabled @@ -2662,6 +2637,7 @@ void TabPrint::build() auto optgroup = page->new_optgroup(L("Layer height"), L"param_layer_height"); optgroup->append_single_option_line("layer_height","quality_settings_layer_height"); optgroup->append_single_option_line("initial_layer_print_height","quality_settings_layer_height"); + optgroup->append_single_option_line("enable_mixed_color_sublayer"); optgroup = page->new_optgroup(L("Line width"), L"param_line_width"); optgroup->append_single_option_line("line_width","quality_settings_line_width"); @@ -2823,7 +2799,7 @@ void TabPrint::build() optgroup->append_single_option_line("fill_multiline", "strength_settings_infill#fill-multiline"); optgroup->append_single_option_line("sparse_infill_pattern", "strength_settings_infill#sparse-infill-pattern"); optgroup->append_single_option_line("gyroid_optimized", "strength_settings_patterns#gyroid-optimized"); - optgroup->append_single_option_line("sparse_infill_smooth_factor", "strength_settings_patterns#sparse-infill-smooth-factor"); + optgroup->append_single_option_line("sparse_infill_smooth_factor", "strength_settings_infill#sparse-infill-smooth-factor"); optgroup->append_single_option_line("infill_direction", "strength_settings_infill#direction"); optgroup->append_single_option_line("sparse_infill_rotate_template", "strength_settings_infill_rotation_template_metalanguage"); optgroup->append_single_option_line("skin_infill_density", "strength_settings_patterns#locked-zag"); @@ -3077,7 +3053,7 @@ void TabPrint::build() optgroup->append_single_option_line("combine_brims", "others_settings_brim#combine-brims"); optgroup->append_single_option_line("brim_ears_max_angle", "others_settings_brim#ear-max-angle"); optgroup->append_single_option_line("brim_ears_detection_length", "others_settings_brim#ear-detection-radius"); - optgroup->append_single_option_line("brim_ears_outer_only"); + optgroup->append_single_option_line("brim_ears_outer_only", "others_settings_brim#brim-ears-outer-only"); optgroup = page->new_optgroup(L("Special mode"), L"param_special"); optgroup->append_single_option_line("slicing_mode", "others_settings_special_mode#slicing-mode"); @@ -3530,6 +3506,21 @@ void TabPrintModel::activate_selected_page(std::function<void()> throw_if_cancel f->set_value(boost::any(), false); } } + if (m_type == Preset::TYPE_PLATE) + static_cast<TabPrintPlate *>(this)->update_mixed_filament_seq_state(); +} + +// A mixed-color slot resolves to a different physical filament per layer, so a +// user-defined filament print order cannot be honoured while one exists. +void TabPrintPlate::update_mixed_filament_seq_state() +{ + if (!m_active_page) return; + auto &proj_cfg = m_preset_bundle->project_config; + auto *opt = proj_cfg.option<ConfigOptionBools>("filament_is_mixed"); + bool has_mixed = opt && has_any_mixed_filament(opt->values); + + toggle_option("first_layer_sequence_choice", !has_mixed); + toggle_option("other_layers_sequence_choice", !has_mixed); } void TabPrintModel::on_value_change(const std::string& opt_id, const boost::any& value) @@ -4006,13 +3997,12 @@ void TabFilament::add_filament_overrides_page() const int extruder_idx = 0; // #ys_FIXME - ConfigOptionsGroupShp retraction_optgroup = page->new_optgroup(L("Retraction"), L"param_retraction"); - auto append_retraction_option = [this, retraction_optgroup](const std::string& opt_key, int opt_index) + auto append_retraction_option = [this](ConfigOptionsGroupShp optgroup, const std::string& opt_key, int opt_index) { Line line {"",""}; - line = retraction_optgroup->create_single_option_line(retraction_optgroup->get_option(opt_key, opt_index)); + line = optgroup->create_single_option_line(optgroup->get_option(opt_key, opt_index)); - line.near_label_widget = [this, optgroup_wk = ConfigOptionsGroupWkp(retraction_optgroup), opt_key, opt_index](wxWindow* parent) { + line.near_label_widget = [this, optgroup_wk = ConfigOptionsGroupWkp(optgroup), opt_key, opt_index](wxWindow* parent) { auto check_box = new ::CheckBox(parent); // ORCA modernize checkboxes check_box->Bind(wxEVT_TOGGLEBUTTON, [this, optgroup_wk, opt_key, opt_index](wxCommandEvent& evt) { const bool is_checked = evt.IsChecked(); @@ -4039,9 +4029,10 @@ void TabFilament::add_filament_overrides_page() return check_box; }; - retraction_optgroup->append_line(line); + optgroup->append_line(line); }; + ConfigOptionsGroupShp retraction_optgroup = page->new_optgroup(L("Retraction"), L"param_retraction"); for (const std::string opt_key : { "filament_retraction_length", "filament_z_hop", "filament_z_hop_types", @@ -4065,7 +4056,13 @@ void TabFilament::add_filament_overrides_page() //SoftFever // "filament_seam_gap" }) - append_retraction_option(opt_key, extruder_idx); + append_retraction_option(retraction_optgroup, opt_key, extruder_idx); + + ConfigOptionsGroupShp toolchange_optgroup = page->new_optgroup(L("Retraction when switching material"), L"param_retraction_material_change"); + for (const std::string opt_key : { "filament_retract_length_toolchange", + "filament_retract_restart_extra_toolchange" + }) + append_retraction_option(toolchange_optgroup, opt_key, extruder_idx); ConfigOptionsGroupShp ironing_optgroup = page->new_optgroup(L("Ironing"), L"param_ironing"); auto append_ironing_option = [this, ironing_optgroup](const std::string& opt_key, int opt_index) @@ -4180,6 +4177,8 @@ void TabFilament::update_filament_overrides_page(const DynamicPrintConfig* print "filament_retraction_speed", "filament_deretraction_speed", "filament_retract_restart_extra", + "filament_retract_length_toolchange", + "filament_retract_restart_extra_toolchange", "filament_retraction_minimum_travel", "filament_retract_when_changing_layer", "filament_wipe", @@ -4210,7 +4209,8 @@ void TabFilament::update_filament_overrides_page(const DynamicPrintConfig* print is_checked &= !dynamic_cast<ConfigOptionVectorBase*>(m_config->option(opt_key))->is_nil(extruder_idx); m_overrides_options[opt_key]->SetValue(is_checked); - Field* field = optgroup->get_fieldc(opt_key, 0); + // the toolchange overrides live in their own optgroup, so search the whole page + Field* field = page->get_field(opt_key, 0); if (field == nullptr) continue; if (opt_key == "filament_long_retractions_when_cut") { @@ -5016,8 +5016,43 @@ void TabPrinter::build_fff() optgroup->append_single_option_line("printer_structure", "printer_basic_information_advanced#printer-structure"); optgroup->append_single_option_line("gcode_flavor", "printer_basic_information_advanced#g-code-flavor"); + optgroup->append_single_option_line("gcode_skip_config_block", "printer_basic_information_advanced#skip-g-code-config-block"); optgroup->append_single_option_line("pellet_modded_printer", "printer_basic_information_advanced#pellet-modded-printer"); optgroup->append_single_option_line("bbl_use_printhost", "printer_basic_information_advanced#use-3rd-party-print-host"); + + // "Printer Agent" dropdown - printer_agent is a coString; gui_type routes it to + // PrinterAgentChoice instead of a TextCtrl. Rows and values come from the live agent + // registry, and the value is stored as the agent-id string. + if (wxGetApp().getAgent() != nullptr) + { + auto registered_printer_agents = NetworkAgentFactory::get_registered_printer_agents(); + if (!registered_printer_agents.empty()) + { + ConfigOptionDef def; + def.type = coString; + def.gui_type = ConfigOptionDef::GUIType::printer_agent_select; + def.width = 3 * Field::def_width_wider() / 2; + def.label = L("Printer Agent"); + def.tooltip = L("Select the network agent implementation for printer communication. " + "Available agents are registered at startup."); + def.mode = comAdvanced; + + // Create the field without get_option() so it is not registered in m_opt_map. + // ConfigOptionsGroup handles printer_agent before the generic mapped write path. + Line agent_line = optgroup->create_single_option_line(Option(def, "printer_agent")); + optgroup->append_line(agent_line); + if (Field* agent_field = get_field("printer_agent")) + { + if (auto* choice = dynamic_cast<PrinterAgentChoice*>(agent_field); choice && choice->getWindow()) + choice->set_value(m_config->opt_string("printer_agent"), false); + } + + // Register by hand so the UnsavedChanges dialog can render a row for it. + wxGetApp().sidebar().get_searcher().add_key("printer_agent", m_type, optgroup->title, + optgroup->config_category()); + } + } + optgroup->append_single_option_line("use_3mf"); optgroup->append_single_option_line("scan_first_layer" , "printer_basic_information_advanced#scan-first-layer"); optgroup->append_single_option_line("enable_power_loss_recovery", "printer_basic_information_advanced#power-loss-recovery"); @@ -5095,7 +5130,7 @@ void TabPrinter::build_fff() optgroup->append_single_option_line("adaptive_bed_mesh_margin", "printer_basic_information_adaptive_bed_mesh#mesh-margin"); optgroup = page->new_optgroup(L("Accessory"), "param_accessory"); - optgroup->append_single_option_line("nozzle_type", "printer_basic_information_accessory#nozzle-type"); + optgroup->append_single_option_line("nozzle_type", "printer_basic_information_accessory#nozzle-type", 0); optgroup->append_single_option_line("nozzle_hrc", "printer_basic_information_accessory#nozzle-hrc"); optgroup->append_single_option_line("auxiliary_fan", "printer_basic_information_accessory#auxiliary-part-cooling-fan"); optgroup->append_single_option_line("fan_direction"); @@ -5582,6 +5617,7 @@ if (is_marlin_flavor) optgroup->append_single_option_line("purge_in_prime_tower", "printer_multimaterial_wipe_tower#purge-in-prime-tower"); optgroup->append_single_option_line("enable_filament_ramming", "printer_multimaterial_wipe_tower#enable-filament-ramming"); optgroup->append_single_option_line("tool_change_on_wipe_tower", "printer_multimaterial_wipe_tower#tool-change-on-wipe-tower"); + optgroup->append_single_option_line("wait_for_temp_on_wipe_tower", "printer_multimaterial_wipe_tower#wait-for-temperature-on-wipe-tower"); optgroup = page->new_optgroup(L("Single extruder multi-material parameters"), "param_settings"); @@ -5884,6 +5920,16 @@ void TabPrinter::reload_config() // so update it implicitly if (m_active_page && m_active_page->title() == "Multimaterial") m_active_page->set_value("extruders_count", int(m_extruders_count)); + + // m_opt_map-driven reload does not cover printer_agent, so sync this custom field explicitly. + if (Field* agent_field = get_field("printer_agent")) + { + if (auto* choice = dynamic_cast<PrinterAgentChoice*>(agent_field); choice && choice->getWindow()) + { + const std::string selected_agent = m_config->opt_string("printer_agent"); + choice->set_value(selected_agent, false); + } + } } void TabPrinter::activate_selected_page(std::function<void()> throw_if_canceled) @@ -5894,6 +5940,16 @@ void TabPrinter::activate_selected_page(std::function<void()> throw_if_canceled) // so update it implicitly if (m_active_page && m_active_page->title() == "Multimaterial") m_active_page->set_value("extruders_count", int(m_extruders_count)); + + // m_opt_map-driven reload does not cover printer_agent, so sync this custom field explicitly. + if (Field* agent_field = get_field("printer_agent")) + { + if (auto* choice = dynamic_cast<PrinterAgentChoice*>(agent_field); choice && choice->getWindow()) + { + const std::string selected_agent = m_config->opt_string("printer_agent"); + choice->set_value(selected_agent, false); + } + } } void TabPrinter::clear_pages() @@ -6019,7 +6075,7 @@ void TabPrinter::toggle_options() auto nozzle_volumes = m_preset_bundle->project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type"); auto extruders = m_config->option<ConfigOptionEnumsGeneric>("extruder_type"); auto get_index_for_extruder = - [this, &extruders, &nozzle_volumes](int extruder_id, int stride = 1) { + [this, &extruders](int extruder_id, int stride = 1) { return m_config->get_index_for_extruder(extruder_id + 1, "printer_extruder_id", ExtruderType(extruders->values[extruder_id]), get_actual_nozzle_volume_type(extruder_id), "printer_extruder_variant", stride); }; @@ -6095,6 +6151,7 @@ void TabPrinter::toggle_options() // so the option is irrelevant there. const size_t extruders_count = m_config->option<ConfigOptionFloats>("nozzle_diameter")->size(); toggle_option("tool_change_on_wipe_tower", !bSEMM && supports_wipe_tower_2 && extruders_count > 1); + toggle_option("wait_for_temp_on_wipe_tower", !bSEMM && supports_wipe_tower_2 && extruders_count > 1); } wxString extruder_number; long val = 1; @@ -6425,7 +6482,7 @@ void Tab::load_current_preset() std::string bmp_name = tab->type() == Slic3r::Preset::TYPE_FILAMENT ? "spool" : tab->type() == Slic3r::Preset::TYPE_SLA_MATERIAL ? "" : "cog"; tab->Hide(); // #ys_WORKAROUND : Hide tab before inserting to avoid unwanted rendering of the tab - dynamic_cast<Notebook*>(wxGetApp().tab_panel())->InsertPage(wxGetApp().tab_panel()->FindPage(this), tab, tab->title(), bmp_name); + dynamic_cast<Notebook*>(wxGetApp().tab_panel())->InsertPage(wxGetApp().tab_panel()->FindPage(this), wxString(), tab, tab->title(), bmp_name); } else #endif @@ -7473,7 +7530,7 @@ void Tab::delete_preset() for (auto &preset2 : *m_presets) if (preset2.inherits() == current_preset.name) { ++count; - presets += "\n - " + preset2.name; + presets += "\n - " + from_u8(preset2.name); } if (count > 0) { msg = _L("Presets inherited by other presets cannot be deleted!"); @@ -7656,18 +7713,18 @@ wxSizer* Tab::compatible_widget_create(wxWindow* parent, PresetDependencies &dep this->update_changed_ui(); }; - deps.checkbox_title->Bind(wxEVT_LEFT_DOWN,([this, &deps, on_toggle](wxMouseEvent& e) { + deps.checkbox_title->Bind(wxEVT_LEFT_DOWN,([&deps, on_toggle](wxMouseEvent& e) { if (e.GetEventType() == wxEVT_LEFT_DCLICK) return; on_toggle(!deps.checkbox->GetValue()); e.Skip(); })); - deps.checkbox_title->Bind(wxEVT_LEFT_DCLICK,([this, &deps, on_toggle](wxMouseEvent& e) { + deps.checkbox_title->Bind(wxEVT_LEFT_DCLICK,([&deps, on_toggle](wxMouseEvent& e) { on_toggle(!deps.checkbox->GetValue()); e.Skip(); })); - deps.checkbox->Bind(wxEVT_TOGGLEBUTTON, ([this, on_toggle](wxCommandEvent& e) { + deps.checkbox->Bind(wxEVT_TOGGLEBUTTON, ([on_toggle](wxCommandEvent& e) { on_toggle(e.IsChecked()); e.Skip(); }), deps.checkbox->GetId()); @@ -7852,6 +7909,24 @@ bool TabPrinter::apply_extruder_cnt_from_cache() return false; } +void TabPrinter::refresh_printer_agent_dropdown() const +{ + auto* choice = dynamic_cast<PrinterAgentChoice*>(get_field("printer_agent")); + if (!choice || !choice->getWindow()) + return; + + const auto agents = NetworkAgentFactory::get_registered_printer_agents(); + if (agents.empty()) + return; + + // why: rows live on PrinterAgentChoice now; rebuild them from the live registry and re-select the stored id. + const std::string selected_agent = wxGetApp().preset_bundle->printers.get_edited_preset() + .config.opt_string("printer_agent"); + choice->reload_rows(); + choice->set_value(selected_agent, false); + this->GetParent()->Layout(); +} + bool Tab::validate_custom_gcodes() { if (m_type != Preset::TYPE_FILAMENT && @@ -8231,7 +8306,7 @@ void Tab::switch_excluder(int extruder_id, bool reload) return; } auto get_index_for_extruder = - [this, &extruders, &nozzle_volumes, variant_keys = extruder_variant_keys[m_type >= Preset::TYPE_COUNT ? Preset::TYPE_PRINT : m_type]](int extruder_id, int stride = 1) { + [this, &extruders, variant_keys = extruder_variant_keys[m_type >= Preset::TYPE_COUNT ? Preset::TYPE_PRINT : m_type]](int extruder_id, int stride = 1) { return m_config->get_index_for_extruder(extruder_id + 1, variant_keys.first, ExtruderType(extruders->values[extruder_id]), get_actual_nozzle_volume_type(extruder_id), variant_keys.second, stride); }; @@ -8288,7 +8363,7 @@ void Tab::sync_excluder() auto nozzle_volumes = m_preset_bundle->project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type"); auto extruders = printer_preset.config.option<ConfigOptionEnumsGeneric>("extruder_type"); auto get_index_for_extruder = - [this, &extruders, &nozzle_volumes, variant_keys = extruder_variant_keys[m_type >= Preset::TYPE_COUNT ? Preset::TYPE_PRINT : m_type]](int extruder_id, NozzleVolumeType nozzle_type) { + [this, &extruders, variant_keys = extruder_variant_keys[m_type >= Preset::TYPE_COUNT ? Preset::TYPE_PRINT : m_type]](int extruder_id, NozzleVolumeType nozzle_type) { return m_config->get_index_for_extruder(extruder_id + 1, variant_keys.first, ExtruderType(extruders->values[extruder_id]), nozzle_type, variant_keys.second); }; @@ -8329,7 +8404,7 @@ void Tab::sync_excluder() } } if (config_to_apply.empty()) { - MessageDialog md(wxGetApp().plater(), _L("No modifications need to be copied."), _L("Copy paramters"), wxICON_INFORMATION | wxOK); + MessageDialog md(wxGetApp().plater(), _L("No modifications need to be copied."), _L("Copy parameters"), wxICON_INFORMATION | wxOK); md.ShowModal(); return; } @@ -8337,7 +8412,7 @@ void Tab::sync_excluder() std::string pt = m_preset_bundle->printers.get_edited_preset().get_printer_type(m_preset_bundle); std::string active_nozzle_name = DevPrinterConfigUtil::get_toolhead_display_name(pt, active_index, ToolHeadComponent::Nozzle, ToolHeadNameCase::LowerCase); std::string other_nozzle_name = DevPrinterConfigUtil::get_toolhead_display_name(pt, 1 - active_index, ToolHeadComponent::Nozzle, ToolHeadNameCase::LowerCase); - wxString title = wxString::Format(_L("Modify paramters of %s"), _L(active_nozzle_name)); + wxString title = wxString::Format(_L("Modify parameters of %s"), _L(active_nozzle_name)); wxString header = wxString::Format(_L("Do you want to modify the following parameters of the %s to that of the %s?"), _L(active_nozzle_name), _L(other_nozzle_name)); UnsavedChangesDialog dlg(title, header, &config_origin, from_index, dest_index, active_index == 0, active_nozzle); @@ -8486,8 +8561,12 @@ void Page::activate(ConfigOptionMode mode, std::function<void()> throw_if_cancel #ifdef __WXMSW__ // BBS: fix field control position - wxTheApp->CallAfter([this]() { - for (auto group : m_optgroups) { + wxTheApp->CallAfter([wp = std::weak_ptr<Page>(shared_from_this())]() { + auto page = wp.lock(); + if (!page) + return; + + for (auto group : page->m_optgroups) { if (group->custom_ctrl) group->custom_ctrl->fixup_items_positions(); } diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 7187aff467..5069b76417 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -515,13 +515,13 @@ public: bool has_key(std::string const &key); protected: - virtual void activate_selected_page(std::function<void()> throw_if_canceled); + virtual void activate_selected_page(std::function<void()> throw_if_canceled) override; virtual void on_value_change(const std::string& opt_key, const boost::any& value) override; virtual void notify_changed(ObjectBase * object) = 0; - virtual void reload_config(); + virtual void reload_config() override; virtual void update_custom_dirty(std::vector<std::string> &dirty_options, std::vector<std::string> &nonsys_options) override; @@ -545,6 +545,8 @@ public: void build() override; void reset_model_config() override; int show_spiral_mode_settings_dialog(bool is_object_config) { return m_config_manipulation.show_spiral_mode_settings_dialog(is_object_config); } + // Disables the user-defined filament print order while a mixed-color filament exists. + void update_mixed_filament_seq_state(); protected: virtual void on_value_change(const std::string& opt_key, const boost::any& value) override; @@ -624,8 +626,6 @@ private: bool m_rebuild_kinematics_page = false; void update_input_shaper_menu(GCodeFlavor flavor); - ogStaticText* m_fff_print_host_upload_description_line {nullptr}; - ogStaticText* m_sla_print_host_upload_description_line {nullptr}; std::vector<PageShp> m_pages_fff; std::vector<PageShp> m_pages_sla; @@ -675,6 +675,7 @@ public: wxSizer* create_bed_shape_widget(wxWindow* parent); void cache_extruder_cnt(const DynamicPrintConfig* config = nullptr); bool apply_extruder_cnt_from_cache(); + void refresh_printer_agent_dropdown() const; }; class TabSLAMaterial : public Tab diff --git a/src/slic3r/GUI/TabButton.hpp b/src/slic3r/GUI/TabButton.hpp index 7accf248c4..05ce1c6bd3 100644 --- a/src/slic3r/GUI/TabButton.hpp +++ b/src/slic3r/GUI/TabButton.hpp @@ -40,7 +40,7 @@ public: void SetBitmap(ScalableBitmap &bitmap); - bool Enable(bool enable = true); + bool Enable(bool enable = true) override; void Rescale(); diff --git a/src/slic3r/GUI/Tabbook.hpp b/src/slic3r/GUI/Tabbook.hpp index 0cea1b8326..b1301a5c23 100644 --- a/src/slic3r/GUI/Tabbook.hpp +++ b/src/slic3r/GUI/Tabbook.hpp @@ -36,7 +36,6 @@ public: TabButton* pageButton; private: - wxWindow* m_parent; wxFlexGridSizer* m_buttons_sizer; wxBoxSizer* m_sizer; ScalableBitmap m_arrow_img; @@ -108,7 +107,7 @@ public: // by this control) and show it immediately. bool ShowNewPage(wxWindow * page) { - return AddPage(page, wxString(), ""/*true *//* select it */); + return AddPage(page, wxString()); } // Set effect to use for showing/hiding pages. @@ -139,14 +138,13 @@ public: // Implement base class pure virtual methods. - // adds a new page to the control bool AddPage(wxWindow* page, const wxString& text, - const std::string& bmp_name, - bool bSelect = false) + bool bSelect = false, + int imageId = NO_IMAGE) override { DoInvalidateBestSize(); - return InsertNewPage(GetPageCount(), page, text, bmp_name, bSelect); + return InsertPage(GetPageCount(), page, text, bSelect, imageId); } //// Page management @@ -167,24 +165,7 @@ public: return true; } - bool InsertNewPage(size_t n, - wxWindow * page, - const wxString & text, - const std::string& bmp_name = "", - bool bSelect = false) - { - if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect)) - return false; - - GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name); - - if (bSelect) - SetSelection(n); - - return true; - } - - bool RemovePage(size_t n) + bool RemovePage(size_t n) override { if (!wxBookCtrlBase::RemovePage(n)) return false; @@ -418,8 +399,6 @@ private: unsigned m_showTimeout, m_hideTimeout; - TabButtonsListCtrl *m_ctrl{nullptr}; - }; //#endif // _WIN32 #endif // slic3r_Tabbook_hpp_ diff --git a/src/slic3r/GUI/TextureImportDialog.cpp b/src/slic3r/GUI/TextureImportDialog.cpp new file mode 100644 index 0000000000..1a24799a15 --- /dev/null +++ b/src/slic3r/GUI/TextureImportDialog.cpp @@ -0,0 +1,4361 @@ +#include <glad/gl.h> +#include "OpenGLManager.hpp" + +#include "TextureImportDialog.hpp" +#include "I18N.hpp" +#include "GUI_App.hpp" +#include "MsgDialog.hpp" +#include "ColorDecomposeDialog.hpp" +#include "ColorDecomposeSupport.hpp" +#include "Widgets/StateColor.hpp" +#include "Widgets/StaticLine.hpp" +#include "libslic3r/ColorDecomposeRecipe.hpp" +#include "libslic3r/MeshBoolean.hpp" +#include "libslic3r/TriangleSelector.hpp" + +#include <wx/button.h> +#include <wx/colour.h> +#include <wx/colordlg.h> +#include <wx/dcclient.h> +#include <wx/dcbuffer.h> +#include <wx/display.h> +#include <wx/evtloop.h> +#include <wx/statline.h> +#include <wx/scrolwin.h> +#include <wx/msgdlg.h> +#include <wx/utils.h> +#include <wx/valtext.h> + +#include <algorithm> +#include <cctype> +#include <cstring> +#include <cmath> +#include <limits> +#include <map> +#include <set> +#include <sstream> + +#include <boost/log/trivial.hpp> + +static constexpr const char* DEFAULT_VIRTUAL_FILAMENT_BASIC_TYPE = "PLA Basic"; +static constexpr const char* DEFAULT_VIRTUAL_FILAMENT_SHORT_TYPE = "PLA"; +static constexpr const char* DEFAULT_VIRTUAL_FILAMENT_NAME = "Bambu PLA Basic"; + +static bool is_dark() { return Slic3r::GUI::wxGetApp().dark_mode(); } + +static wxColour texture_import_gray9000() +{ + return wxColour(38, 46, 48); +} + +static wxColour texture_import_text_colour() +{ + return StateColor::darkModeColorFor(texture_import_gray9000()); +} + +// StaticLine::SetLineColour stores the raw key and resolves it itself when it paints, so those +// sinks take SEPARATOR_COLOUR_KEY directly; only raw wx sinks need the resolved form below. +static constexpr const char* SEPARATOR_COLOUR_KEY = "#CECECE"; + +static wxColour texture_import_separator_colour() +{ + return StateColor::darkModeColorFor(wxColour(SEPARATOR_COLOUR_KEY)); +} + +// Orca's confirm palette, applied here rather than through Button::SetStyle because these buttons +// keep custom pill geometry that SetStyle resets. The Disabled entries are load-bearing: without +// one, StateColor::colorForStates falls through to the Normal entry and a disabled button paints +// as a live accent button. +static void apply_accent_button_colours(Button* btn) +{ + btn->SetBackgroundColor(StateColor( + std::pair<wxColour, int>(wxColour("#CECECE"), StateColor::Disabled), + std::pair<wxColour, int>(wxColour(0, 137, 123), StateColor::Pressed), + std::pair<wxColour, int>(wxColour(38, 166, 154), StateColor::Hovered), + std::pair<wxColour, int>(wxColour(0, 150, 136), StateColor::Normal))); + btn->SetBorderColor(StateColor( + std::pair<wxColour, int>(wxColour("#CECECE"), StateColor::Disabled), + std::pair<wxColour, int>(wxColour(0, 150, 136), StateColor::Normal))); + btn->SetTextColor(StateColor( + std::pair<wxColour, int>(wxColour("#6B6B6A"), StateColor::Disabled), + std::pair<wxColour, int>(wxColour("#FFFFFE"), StateColor::Normal))); +} + +// The same button while the parameters behind it are dirty: still clickable, but reading as +// "what you see is not what this button would apply". +static void apply_muted_button_colours(Button* btn) +{ + btn->SetBackgroundColor(wxColour("#CECECE")); + btn->SetBorderColor(wxColour("#CECECE")); + btn->SetTextColor(wxColour("#6B6B6A")); +} + +static wxFont texture_import_section_title_font(wxWindow* win) +{ + wxFont font = win ? win->GetFont() : wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + font.MakeBold(); + return font; +} + +static wxSize gl_viewport_size(wxWindow* win, const wxSize& logical_size) +{ + wxSize viewport_size = logical_size; +#ifdef __APPLE__ + const double scale = win ? win->GetContentScaleFactor() : 1.0; + if (scale > 0.0) { + viewport_size.x = std::max(1, (int)std::round(viewport_size.x * scale)); + viewport_size.y = std::max(1, (int)std::round(viewport_size.y * scale)); + } +#else + (void)win; +#endif + return viewport_size; +} + +class ScopedInteractiveBusyCursorSuspender +{ +public: + ScopedInteractiveBusyCursorSuspender() + { +#if defined(__WXMSW__) || defined(__APPLE__) + while (wxIsBusy()) { + wxEndBusyCursor(); + ++m_suspended_count; + } +#endif + } + + ~ScopedInteractiveBusyCursorSuspender() + { +#if defined(__WXMSW__) || defined(__APPLE__) + for (int i = 0; i < m_suspended_count; ++i) + wxBeginBusyCursor(); +#endif + } + +private: + int m_suspended_count = 0; +}; + +static bool needs_filament_swatch_border(const wxColour& colour) +{ + if (is_dark()) + return colour.Red() < 45 && colour.Green() < 45 && colour.Blue() < 45; + return colour.Red() > 224 && colour.Green() > 224 && colour.Blue() > 224; +} + +static wxColour filament_swatch_border_colour() +{ + return is_dark() ? wxColour(207, 207, 207) : wxColour(130, 130, 128); +} + +static void draw_filament_swatch_border(wxDC& dc, const wxColour& colour, + int x, int y, int w, int h, int radius = 0) +{ + if (!needs_filament_swatch_border(colour)) + return; + + dc.SetPen(wxPen(filament_swatch_border_colour(), 1)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + if (radius > 0) + dc.DrawRoundedRectangle(x, y, w, h, radius); + else + dc.DrawRectangle(x, y, w, h); +} + +static void draw_filament_swatch_ellipse_border(wxDC& dc, const wxColour& colour, + int x, int y, int w, int h) +{ + if (!needs_filament_swatch_border(colour)) + return; + + dc.SetPen(wxPen(filament_swatch_border_colour(), 1)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawEllipse(x, y, w, h); +} + +static wxString ellipsize_text(wxDC& dc, wxString text, int max_width) +{ + if (max_width <= 0) + return wxEmptyString; + if (dc.GetTextExtent(text).x <= max_width) + return text; + + const wxString ellipsis = "..."; + while (!text.empty() && dc.GetTextExtent(text + ellipsis).x > max_width) + text.RemoveLast(); + if (text.empty() && dc.GetTextExtent(ellipsis).x > max_width) + return wxString(); + return text + ellipsis; +} + +// ============================================================ +// AccentSlider — thin track + accent-coloured triangle thumb +// ============================================================ + +class AccentSlider : public wxPanel { +public: + AccentSlider(wxWindow* parent, int value, int minVal, int maxVal, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize); + ~AccentSlider() override; + int GetValue() const; + void SetValue(int val); + bool Enable(bool enable = true) override; +private: + void OnPaint(wxPaintEvent&); + void OnMouse(wxMouseEvent&); + int xFromValue() const; + int valueFromX(int x) const; + int m_value, m_min, m_max; + bool m_dragging = false; +}; + +AccentSlider::AccentSlider(wxWindow* parent, int value, int minVal, int maxVal, + const wxPoint& pos, const wxSize& size) + : wxPanel(parent, wxID_ANY, pos, size.IsFullySpecified() ? size : wxSize(-1, parent->FromDIP(24)), + wxTAB_TRAVERSAL | wxFULL_REPAINT_ON_RESIZE) + , m_value(std::clamp(value, minVal, maxVal)), m_min(minVal), m_max(maxVal) +{ + SetBackgroundStyle(wxBG_STYLE_PAINT); + SetMinSize(wxSize(-1, FromDIP(24))); + + Bind(wxEVT_PAINT, &AccentSlider::OnPaint, this); + Bind(wxEVT_SIZE, [this](wxSizeEvent& evt) { + evt.Skip(); + Refresh(); + }); + Bind(wxEVT_LEFT_DOWN, &AccentSlider::OnMouse, this); + Bind(wxEVT_LEFT_UP, &AccentSlider::OnMouse, this); + Bind(wxEVT_MOTION, &AccentSlider::OnMouse, this); + Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) { m_dragging = false; }); +} + +AccentSlider::~AccentSlider() +{ + // See MixedFilamentDialog::~MixedFilamentDialog: a widget destroyed while it + // still holds the capture wedges mouse input for the whole application. + if (HasCapture()) + ReleaseMouse(); +} + +int AccentSlider::GetValue() const { return m_value; } + +void AccentSlider::SetValue(int val) +{ + val = std::clamp(val, m_min, m_max); + if (val != m_value) { m_value = val; Refresh(); } +} + +bool AccentSlider::Enable(bool enable) +{ + bool ok = wxPanel::Enable(enable); + Refresh(); + return ok; +} + +int AccentSlider::xFromValue() const +{ + wxSize sz = GetClientSize(); + int margin = FromDIP(6); + int track_w = sz.x - 2 * margin; + if (m_max <= m_min || track_w <= 0) return margin; + return margin + (m_value - m_min) * track_w / (m_max - m_min); +} + +int AccentSlider::valueFromX(int x) const +{ + wxSize sz = GetClientSize(); + int margin = FromDIP(6); + int track_w = sz.x - 2 * margin; + if (track_w <= 0 || m_max <= m_min) return m_min; + int val = m_min + (x - margin) * (m_max - m_min) / track_w; + return std::clamp(val, m_min, m_max); +} + +void AccentSlider::OnPaint(wxPaintEvent&) +{ + wxAutoBufferedPaintDC dc(this); + wxSize sz = GetClientSize(); + + dc.SetBackground(wxBrush(GetParent()->GetBackgroundColour())); + dc.Clear(); + + int margin = FromDIP(6); + int track_y = sz.y / 2; + int ts = FromDIP(8); + int pen_w = FromDIP(2); + + wxColour accent_clr = StateColor::darkModeColorFor(IsEnabled() ? wxColour("#009688") : wxColour("#ACACAC")); + wxColour track_clr = StateColor::darkModeColorFor(IsEnabled() ? wxColour("#CECECE") : wxColour("#DFDFDF")); + + int tx = xFromValue(); + + dc.SetPen(wxPen(accent_clr, pen_w)); + dc.DrawLine(margin, track_y, tx, track_y); + + dc.SetPen(wxPen(track_clr, pen_w)); + dc.DrawLine(tx, track_y, sz.x - margin, track_y); + + wxPoint tri[3] = { + {tx, track_y + FromDIP(1)}, + {tx - ts / 2, track_y + FromDIP(1) + ts}, + {tx + ts / 2, track_y + FromDIP(1) + ts} + }; + dc.SetBrush(wxBrush(accent_clr)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawPolygon(3, tri); +} + +void AccentSlider::OnMouse(wxMouseEvent& evt) +{ + if (!IsEnabled()) return; + + auto update = [&](int x) { + int nv = valueFromX(x); + if (nv != m_value) { + m_value = nv; + Refresh(); + wxCommandEvent e(wxEVT_SLIDER, GetId()); + e.SetEventObject(this); + ProcessWindowEvent(e); + } + }; + + if (evt.LeftDown()) { + m_dragging = true; + if (!HasCapture()) CaptureMouse(); + update(evt.GetX()); + } else if (evt.LeftUp()) { + m_dragging = false; + if (HasCapture()) ReleaseMouse(); + } else if (evt.Dragging() && m_dragging) { + update(evt.GetX()); + } +} + +namespace Slic3r { namespace GUI { + +wxDEFINE_EVENT(EVT_TEXTURE_COMPUTE_DONE, wxCommandEvent); +wxDEFINE_EVENT(EVT_TEXTURE_COMPUTE_PROGRESS, wxCommandEvent); +wxDEFINE_EVENT(EVT_TEXTURE_COMPUTE_ERROR, wxCommandEvent); +wxDEFINE_EVENT(EVT_TEXTURE_MESH_REPAIR_DECISION, wxCommandEvent); + +static std::array<float, 4> parse_color_string(const std::string& hex) +{ + std::array<float, 4> c = {1.f, 1.f, 1.f, 1.f}; + if (hex.size() >= 7 && hex[0] == '#') { + unsigned long val = std::strtoul(hex.c_str() + 1, nullptr, 16); + c[0] = ((val >> 16) & 0xFF) / 255.f; + c[1] = ((val >> 8) & 0xFF) / 255.f; + c[2] = ((val ) & 0xFF) / 255.f; + } + return c; +} + +static wxString rgb_to_hex(const std::array<std::size_t, 3>& c) +{ + return wxString::Format("#%02X%02X%02X", + (unsigned)c[0], (unsigned)c[1], (unsigned)c[2]); +} + +static wxString filament_name_to_wx_string(const std::string& name) +{ + wxString utf8_name = wxString::FromUTF8(name.c_str()); + if (!utf8_name.empty() || name.empty()) + return utf8_name; + return wxString(name); +} + +static std::string texture_normalize_color_hex(std::string hex) +{ + if (hex.empty()) + return "#808080"; + if (hex.front() != '#') + hex = "#" + hex; + return decompose_normalize_color_hex(std::move(hex)); +} + +static std::string texture_rgba_to_hex(const std::array<float, 4>& rgba) +{ + return wxString::Format("#%02X%02X%02X", + (unsigned char)std::clamp(rgba[0] * 255.f, 0.f, 255.f), + (unsigned char)std::clamp(rgba[1] * 255.f, 0.f, 255.f), + (unsigned char)std::clamp(rgba[2] * 255.f, 0.f, 255.f)).ToStdString(); +} + +static bool texture_entry_is_physical(TextureFilamentKind kind) +{ + return kind == TextureFilamentKind::ExistingPhysical || kind == TextureFilamentKind::NewPhysical; +} + +static bool texture_entry_is_mixed(TextureFilamentKind kind) +{ + return kind == TextureFilamentKind::ExistingMixed || kind == TextureFilamentKind::NewMixed; +} + +static bool texture_entry_is_pla_basic(const TextureFilamentEntry& entry) +{ + return entry.type == DEFAULT_VIRTUAL_FILAMENT_SHORT_TYPE || entry.type == DEFAULT_VIRTUAL_FILAMENT_BASIC_TYPE || + entry.name.find(DEFAULT_VIRTUAL_FILAMENT_BASIC_TYPE) != std::string::npos || + entry.preset_name.find(DEFAULT_VIRTUAL_FILAMENT_BASIC_TYPE) != std::string::npos; +} + +static bool texture_entry_official_basic(const TextureFilamentEntry& entry) +{ + if (!texture_entry_is_physical(entry.kind)) + return false; + // NewPhysical entries are created by add_virtual_filament with a fixed Bambu Basic name. + if (entry.kind == TextureFilamentKind::NewPhysical) + return !official_basic_type_from_preset_name(entry.name).empty(); + // ExistingPhysical: resolve the filament preset name from project_config_index. + auto& pb = *wxGetApp().preset_bundle; + const size_t cfg = entry.project_config_index; + if (cfg < pb.filament_presets.size()) + return !official_basic_type_from_preset_name(pb.filament_presets[cfg]).empty(); + return false; +} + +static Slic3r::ColorDecomposeRecipeMode texture_recipe_mode(TextureAutoMixMode mode) +{ + return mode == TextureAutoMixMode::CMYW ? Slic3r::ColorDecomposeRecipeMode::CMYW : + Slic3r::ColorDecomposeRecipeMode::RYBW; +} + +static bool starts_with_preset_name(const std::string& name, const char* prefix) +{ + const size_t prefix_len = std::strlen(prefix); + return name.size() >= prefix_len && name.compare(0, prefix_len, prefix) == 0; +} + +static std::string resolve_default_virtual_filament_preset_name() +{ + auto* preset_bundle = wxGetApp().preset_bundle; + if (!preset_bundle) + return {}; + + auto valid_preset_name = [preset_bundle](const std::string& name) -> bool { + return !name.empty() && preset_bundle->filaments.find_preset(name, false) != nullptr; + }; + + const auto* default_profiles = preset_bundle->printers.get_selected_preset() + .config.option<ConfigOptionStrings>("default_filament_profile"); + if (default_profiles) { + for (const std::string& name : default_profiles->values) { + if (starts_with_preset_name(name, DEFAULT_VIRTUAL_FILAMENT_NAME) && valid_preset_name(name)) + return name; + } + } + + for (const Preset& preset : preset_bundle->filaments.get_presets()) { + if (preset.is_system && preset.is_visible && preset.is_compatible && + starts_with_preset_name(preset.name, DEFAULT_VIRTUAL_FILAMENT_NAME)) { + return preset.name; + } + } + + for (const Preset& preset : preset_bundle->filaments.get_presets()) { + if (preset.is_visible && preset.is_compatible && + starts_with_preset_name(preset.name, DEFAULT_VIRTUAL_FILAMENT_NAME)) { + return preset.name; + } + } + + std::string selected = preset_bundle->filaments.get_selected_preset_name(); + return valid_preset_name(selected) ? selected : std::string(); +} + +static wxString auto_mix_mode_label(TextureAutoMixMode mode) +{ + return mode == TextureAutoMixMode::CMYW ? _L("One-click CMYW auto-mix") : + _L("One-click RYBW auto-mix"); +} + +static wxPoint constrained_dialog_position(wxWindow* anchor, const wxSize& dialog_size) +{ + if (!anchor) + return wxDefaultPosition; + + wxSize size = dialog_size; + if (size.x <= 0 || size.y <= 0) + size = wxSize(anchor->FromDIP(450), anchor->FromDIP(350)); + + wxPoint pos = anchor->ClientToScreen(wxPoint(0, anchor->GetSize().y)); + wxRect display_rect; + int display_idx = wxDisplay::GetFromPoint(pos); + if (display_idx != wxNOT_FOUND) + display_rect = wxDisplay(display_idx).GetClientArea(); + else + display_rect = wxDisplay().GetClientArea(); + + pos.x = std::clamp(pos.x, display_rect.GetLeft(), + std::max(display_rect.GetLeft(), display_rect.GetRight() - size.x)); + pos.y = std::clamp(pos.y, display_rect.GetTop(), + std::max(display_rect.GetTop(), display_rect.GetBottom() - size.y)); + return pos; +} + +// ============================================================ +// FilamentSelectPopup +// ============================================================ + +class FilamentSelectPopup : public PopupWindow +{ +public: + FilamentSelectPopup(wxWindow* parent, + const std::vector<TextureFilamentEntry>& entries, + const std::vector<std::array<float, 4>>& colors_rgba, + const std::vector<std::string>& names, + size_t existing_count, + int popup_width, + wxWindow* dialog_anchor, + std::function<void(int)> on_select, + std::function<void(wxColour)> on_add_filament, + std::function<void()> on_decompose_color, + std::function<bool()> can_add_filament, + std::function<void(bool)> on_close, + std::vector<int> display_numbers) + : PopupWindow(parent, wxBORDER_NONE | wxPU_CONTAINS_CONTROLS) + , m_entries(entries) + , m_colors_rgba(colors_rgba) + , m_names(names) + , m_existing_count(existing_count) + , m_dialog_anchor(dialog_anchor) + , m_on_select(std::move(on_select)) + , m_on_add_filament(std::move(on_add_filament)) + , m_on_decompose_color(std::move(on_decompose_color)) + , m_can_add_filament(std::move(can_add_filament)) + , m_on_close(std::move(on_close)) + , m_display_numbers(std::move(display_numbers)) + { + wxColour pop_bg = StateColor::darkModeColorFor(*wxWHITE); + SetBackgroundColour(pop_bg); + + m_content = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); + m_content->SetBackgroundColour(pop_bg); + m_content->SetScrollRate(0, FromDIP(5)); + auto* outer = new wxBoxSizer(wxVERTICAL); + + const int pop_w = std::max(FromDIP(213), popup_width); + const int row_h = FromDIP(32); + const int pad = FromDIP(8); + const int max_visible_rows = 10; + const wxColour header_clr = StateColor::darkModeColorFor(wxColour("#ACACAC")); + + auto add_section_header = [&](const wxString& label) { + auto* hdr = new wxStaticText(m_content, wxID_ANY, label); + wxFont hf = hdr->GetFont(); + hf.SetPointSize(9); + hdr->SetFont(hf); + hdr->SetForegroundColour(header_clr); + outer->Add(hdr, 0, wxLEFT | wxRIGHT | wxTOP, pad); + auto* line = new StaticLine(m_content); + line->SetLineColour(wxColour(SEPARATOR_COLOUR_KEY)); + outer->Add(line, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, pad); + }; + + auto add_section = [&](const wxString& label, TextureFilamentKind kind) { + bool has_any = false; + for (const auto& entry : m_entries) { + if (entry.kind == kind) { + has_any = true; + break; + } + } + if (!has_any) + return; + add_section_header(label); + for (const auto& entry : m_entries) { + if (entry.kind != kind) + continue; + wxPanel* row = texture_entry_is_mixed(entry.kind) ? create_mixed_item_row(entry, row_h) + : create_item_row((size_t)entry.dialog_index, row_h); + outer->Add(row, 0, wxEXPAND | wxLEFT | wxRIGHT, pad); + } + }; + + // Section order matches compute_display_numbers() so the visible IDs + // ascend monotonically (ExistingPhysical -> NewPhysical -> ExistingMixed + // -> NewMixed) instead of jumping (e.g. 1,2 -> 7 -> 3,4,5,6 -> 8,9,10). + add_section(_L("Project Physical Filaments"), TextureFilamentKind::ExistingPhysical); + add_section(_L("New Physical Filaments"), TextureFilamentKind::NewPhysical); + add_section(_L("Project Mixed Filaments"), TextureFilamentKind::ExistingMixed); + add_section(_L("New Mixed Filaments"), TextureFilamentKind::NewMixed); + + auto* decompose_label = new wxStaticText(this, wxID_ANY, _L("Decompose Color")); + auto* add_label = new wxStaticText(this, wxID_ANY, _L("+ Add Material")); + wxFont af = add_label->GetFont(); + af.SetPointSize(10); + add_label->SetFont(af); + decompose_label->SetFont(af); + const bool add_enabled = !m_can_add_filament || m_can_add_filament(); + const wxColour action_clr = StateColor::darkModeColorFor(wxColour("#009688")); + add_label->SetForegroundColour(add_enabled ? action_clr : header_clr); + decompose_label->SetForegroundColour(add_enabled ? action_clr : header_clr); + add_label->SetCursor(wxCursor(add_enabled ? wxCURSOR_HAND : wxCURSOR_ARROW)); + decompose_label->SetCursor(wxCursor(add_enabled ? wxCURSOR_HAND : wxCURSOR_ARROW)); + if (!add_enabled) + add_label->SetToolTip(wxString::Format( + _L("The project supports up to %d filaments. Extra filaments will be discarded."), + (int)EnforcerBlockerType::ExtruderMax)); + decompose_label->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent&) { + if (m_can_add_filament && !m_can_add_filament()) + return; + auto on_decompose_color = m_on_decompose_color; + m_closing_from_action = true; + Dismiss(); + if (on_decompose_color) + on_decompose_color(); + }); + add_label->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent&) { + if (m_can_add_filament && !m_can_add_filament()) { + return; + } + auto on_add_filament = m_on_add_filament; + wxWindow* popup_parent = GetParent(); + wxWindow* color_anchor = m_dialog_anchor ? m_dialog_anchor : popup_parent; + m_closing_from_action = true; + Dismiss(); + wxColourData cd; + cd.SetChooseFull(true); + wxColourDialog dlg(popup_parent, &cd); + auto move_color_dialog = [&dlg, color_anchor]() { + dlg.Move(constrained_dialog_position(color_anchor, dlg.GetBestSize())); + }; + dlg.Bind(wxEVT_SHOW, [move_color_dialog](wxShowEvent& e) mutable { + e.Skip(); + if (e.IsShown()) + move_color_dialog(); + }); + move_color_dialog(); + if (dlg.ShowModal() == wxID_OK) { + wxColour clr = dlg.GetColourData().GetColour(); + if (on_add_filament) on_add_filament(clr); + } + }); + + m_content->SetSizer(outer); + m_content->FitInside(); + + auto* top_sizer = new wxBoxSizer(wxVERTICAL); + int list_h = outer->GetMinSize().y; + if (m_colors_rgba.size() > max_visible_rows) + list_h -= ((int)m_colors_rgba.size() - max_visible_rows) * row_h; + m_content->SetMinSize(wxSize(pop_w, list_h)); + m_content->SetMaxSize(wxSize(pop_w, list_h)); + top_sizer->Add(m_content, 0, wxEXPAND); + + top_sizer->AddSpacer(FromDIP(4)); + auto* sep_line = new StaticLine(this); + sep_line->SetLineColour(wxColour(SEPARATOR_COLOUR_KEY)); + top_sizer->Add(sep_line, 0, wxEXPAND | wxLEFT | wxRIGHT, pad); + top_sizer->Add(decompose_label, 0, wxALIGN_CENTER_HORIZONTAL | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, pad); + auto* sep_line2 = new StaticLine(this); + sep_line2->SetLineColour(wxColour(SEPARATOR_COLOUR_KEY)); + top_sizer->Add(sep_line2, 0, wxEXPAND | wxLEFT | wxRIGHT, pad); + top_sizer->Add(add_label, 0, wxALIGN_CENTER_HORIZONTAL | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, pad); + SetSizerAndFit(top_sizer); + + SetSize(pop_w, top_sizer->GetMinSize().y); + } + +private: + void OnDismiss() override + { + restore_cursor_state(); + if (m_on_close) m_on_close(m_closing_from_action); + m_closing_from_action = false; + wxPopupTransientWindow::OnDismiss(); + schedule_destroy(); + } + + void restore_cursor_state() + { + SetCursor(wxNullCursor); + if (m_content) + m_content->SetCursor(wxNullCursor); + if (m_dialog_anchor) + m_dialog_anchor->SetCursor(wxCursor(wxCURSOR_HAND)); + wxSetCursor(wxNullCursor); + } + + void schedule_destroy() + { + if (m_destroy_scheduled) + return; + m_destroy_scheduled = true; + CallAfter([this]() { Destroy(); }); + } + + wxPanel* create_item_row(size_t idx, int row_h) + { + wxColour row_bg = StateColor::darkModeColorFor(*wxWHITE); + wxColour hover_bg = StateColor::darkModeColorFor(wxColour("#F4F4F4")); + wxColour name_fg = texture_import_text_colour(); + + wxPanel* row = new wxPanel(m_content, wxID_ANY, wxDefaultPosition, wxSize(-1, row_h)); + row->SetBackgroundColour(row_bg); + row->SetBackgroundStyle(wxBG_STYLE_PAINT); + row->SetCursor(wxCursor(wxCURSOR_HAND)); + + const int sq = row->FromDIP(24); + const int sq_r = row->FromDIP(2); + const int sq_x = row->FromDIP(4); + const int gap1 = row->FromDIP(8); + + wxColour fil_clr = idx < m_colors_rgba.size() + ? wxColour((unsigned char)(m_colors_rgba[idx][0] * 255.f), + (unsigned char)(m_colors_rgba[idx][1] * 255.f), + (unsigned char)(m_colors_rgba[idx][2] * 255.f)) + : wxColour(128, 128, 128); + + wxString name_str = (idx < m_names.size()) ? filament_name_to_wx_string(m_names[idx]) + : wxString::Format("Filament %d", display_number((int)idx)); + row->SetToolTip(name_str); + + row->Bind(wxEVT_PAINT, [this, idx, sq, sq_r, sq_x, gap1, fil_clr, name_str, row_bg, hover_bg, name_fg](wxPaintEvent& e) { + auto* p = static_cast<wxPanel*>(e.GetEventObject()); + wxAutoBufferedPaintDC dc(p); + wxSize sz = p->GetClientSize(); + + bool hovered = (m_hover_idx == (int)idx); + dc.SetBrush(wxBrush(hovered ? hover_bg : row_bg)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.x, sz.y); + + int sq_y = (sz.y - sq) / 2; + wxColour paint_clr = fil_clr; + if (idx < m_colors_rgba.size()) { + paint_clr = wxColour((unsigned char)(m_colors_rgba[idx][0] * 255.f), + (unsigned char)(m_colors_rgba[idx][1] * 255.f), + (unsigned char)(m_colors_rgba[idx][2] * 255.f)); + } + dc.SetBrush(wxBrush(paint_clr)); + dc.DrawRoundedRectangle(sq_x, sq_y, sq, sq, sq_r); + draw_filament_swatch_border(dc, paint_clr, sq_x, sq_y, sq, sq, sq_r); + + { + wxFont nf = p->GetFont(); + nf.SetPointSize(9); + dc.SetFont(nf); + dc.SetTextForeground(paint_clr.GetLuminance() < 0.6 ? *wxWHITE : texture_import_gray9000()); + wxString ns = wxString::Format("%d", display_number((int)idx)); + wxSize tsz = dc.GetTextExtent(ns); + dc.DrawText(ns, sq_x + (sq - tsz.x) / 2, sq_y + (sq - tsz.y) / 2); + } + + // Material name + { + wxFont mf = p->GetFont(); + mf.SetPointSize(10); + dc.SetFont(mf); + dc.SetTextForeground(name_fg); + int tx = sq_x + sq + gap1; + wxString display = ellipsize_text(dc, name_str, sz.x - tx - p->FromDIP(4)); + wxSize tsz = dc.GetTextExtent(display); + if (!display.empty()) + dc.DrawText(display, tx, (sz.y - tsz.y) / 2); + } + }); + + row->Bind(wxEVT_MOTION, [this, idx](wxMouseEvent& evt) { + if (m_hover_idx != (int)idx) { + m_hover_idx = (int)idx; + m_content->Refresh(); + } + evt.Skip(); + }); + row->Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& evt) { + if (m_hover_idx != -1) { + m_hover_idx = -1; + m_content->Refresh(); + } + evt.Skip(); + }); + + row->Bind(wxEVT_LEFT_DOWN, [this, idx](wxMouseEvent&) { + if (m_on_select) m_on_select((int)idx); + m_closing_from_action = true; + Dismiss(); + }); + + return row; + } + + wxPanel* create_mixed_item_row(const TextureFilamentEntry& entry, int row_h) + { + wxColour row_bg = StateColor::darkModeColorFor(*wxWHITE); + wxColour hover_bg = StateColor::darkModeColorFor(wxColour("#F4F4F4")); + wxColour name_fg = texture_import_text_colour(); + const int idx = entry.dialog_index; + + wxPanel* row = new wxPanel(m_content, wxID_ANY, wxDefaultPosition, wxSize(-1, row_h)); + row->SetBackgroundColour(row_bg); + row->SetBackgroundStyle(wxBG_STYLE_PAINT); + row->SetCursor(wxCursor(wxCURSOR_HAND)); + row->SetToolTip(entry.name.empty() ? wxString::Format("Filament %d", display_number(idx)) : filament_name_to_wx_string(entry.name)); + + row->Bind(wxEVT_PAINT, [this, entry, idx, row_bg, hover_bg, name_fg](wxPaintEvent& e) { + auto* p = static_cast<wxPanel*>(e.GetEventObject()); + wxAutoBufferedPaintDC dc(p); + wxSize sz = p->GetClientSize(); + const bool hovered = (m_hover_idx == idx); + dc.SetBrush(wxBrush(hovered ? hover_bg : row_bg)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.x, sz.y); + + wxFont font = p->GetFont(); + font.SetPointSize(9); + dc.SetFont(font); + int x = p->FromDIP(2); + const int sw = p->FromDIP(22); + const int sw_r = p->FromDIP(2); + const int y = (sz.y - sw) / 2; + + for (size_t ci = 0; ci < entry.mixed_components.size() && ci < entry.mixed_ratios.size(); ++ci) { + if (ci > 0) { + dc.SetTextForeground(name_fg); + wxString plus = "+"; + wxSize psz = dc.GetTextExtent(plus); + dc.DrawText(plus, x, (sz.y - psz.y) / 2); + x += psz.x + p->FromDIP(4); + } + + const unsigned int comp_id = entry.mixed_components[ci]; + const int comp_dialog_idx = comp_id >= 1 ? (int)comp_id - 1 : -1; + wxColour comp_clr("#D9D9D9"); + if (comp_dialog_idx >= 0 && comp_dialog_idx < (int)m_colors_rgba.size()) { + const auto& c = m_colors_rgba[comp_dialog_idx]; + comp_clr = wxColour((unsigned char)(c[0] * 255.f), + (unsigned char)(c[1] * 255.f), + (unsigned char)(c[2] * 255.f)); + } + + dc.SetBrush(wxBrush(comp_clr)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRoundedRectangle(x, y, sw, sw, sw_r); + draw_filament_swatch_border(dc, comp_clr, x, y, sw, sw, sw_r); + + wxString num = wxString::Format("%d", display_number(comp_dialog_idx)); + wxSize nsz = dc.GetTextExtent(num); + dc.SetTextForeground(comp_clr.GetLuminance() < 0.6 ? *wxWHITE : texture_import_gray9000()); + dc.DrawText(num, x + (sw - nsz.x) / 2, y + (sw - nsz.y) / 2); + x += sw + p->FromDIP(4); + + dc.SetTextForeground(name_fg); + wxString pct = wxString::Format("%d%%", entry.mixed_ratios[ci]); + wxSize psz = dc.GetTextExtent(pct); + dc.DrawText(pct, x, (sz.y - psz.y) / 2); + x += psz.x + p->FromDIP(4); + } + }); + + row->Bind(wxEVT_MOTION, [this, idx](wxMouseEvent& evt) { + if (m_hover_idx != idx) { + m_hover_idx = idx; + m_content->Refresh(); + } + evt.Skip(); + }); + row->Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& evt) { + if (m_hover_idx != -1) { + m_hover_idx = -1; + m_content->Refresh(); + } + evt.Skip(); + }); + row->Bind(wxEVT_LEFT_DOWN, [this, idx](wxMouseEvent&) { + if (m_on_select) m_on_select(idx); + m_closing_from_action = true; + Dismiss(); + }); + + return row; + } + + wxScrolledWindow* m_content = nullptr; + std::vector<TextureFilamentEntry> m_entries; + std::vector<std::array<float, 4>> m_colors_rgba; + std::vector<std::string> m_names; + size_t m_existing_count = 0; + wxWindow* m_dialog_anchor = nullptr; + std::function<void(int)> m_on_select; + std::function<void(wxColour)> m_on_add_filament; + std::function<void()> m_on_decompose_color; + std::function<bool()> m_can_add_filament; + std::function<void(bool)> m_on_close; + // 1-based display number per dialog_index, mirroring the post-apply + // sidebar ordering (ExistingPhysical, NewPhysical, ExistingMixed, NewMixed). + std::vector<int> m_display_numbers; + int m_hover_idx = -1; + bool m_closing_from_action = false; + bool m_destroy_scheduled = false; + + // Returns the display number for a dialog_index, falling back to idx + 1 + // when no mapping is available (e.g. index out of range). + int display_number(int idx) const { + return (idx >= 0 && idx < (int)m_display_numbers.size() && m_display_numbers[idx] > 0) + ? m_display_numbers[idx] : idx + 1; + } +}; + +// ============================================================ +// AutoMixSelectPopup +// ============================================================ + +class AutoMixSelectPopup : public PopupWindow +{ +public: + AutoMixSelectPopup(wxWindow* parent, + TextureAutoMixMode current_mode, + int popup_width, + int font_point_size, + std::function<void(TextureAutoMixMode)> on_select, + std::function<void()> on_close) + : PopupWindow(parent, wxBORDER_NONE | wxPU_CONTAINS_CONTROLS) + , m_current_mode(current_mode) + , m_font_point_size(font_point_size) + , m_on_select(std::move(on_select)) + , m_on_close(std::move(on_close)) + { + wxColour pop_bg = StateColor::darkModeColorFor(*wxWHITE); + SetBackgroundColour(pop_bg); + + auto* content = new wxPanel(this, wxID_ANY); + content->SetBackgroundColour(pop_bg); + content->SetBackgroundStyle(wxBG_STYLE_PAINT); + + auto* sizer = new wxBoxSizer(wxVERTICAL); + const int row_h = FromDIP(36); + const int pop_w = std::max(FromDIP(216), popup_width); + sizer->Add(create_item_row(content, TextureAutoMixMode::CMYW, row_h), 0, wxEXPAND); + sizer->Add(create_item_row(content, TextureAutoMixMode::RYBW, row_h), 0, wxEXPAND); + content->SetSizer(sizer); + content->SetMinSize(wxSize(pop_w, row_h * 2)); + + auto* top_sizer = new wxBoxSizer(wxVERTICAL); + top_sizer->Add(content, 0, wxEXPAND | wxALL, FromDIP(4)); + SetSizerAndFit(top_sizer); + SetSize(pop_w, top_sizer->GetMinSize().y); + } + +private: + void OnDismiss() override + { + if (m_on_close) + m_on_close(); + wxPopupTransientWindow::OnDismiss(); + CallAfter([this]() { Destroy(); }); + } + + wxPanel* create_item_row(wxWindow* parent, TextureAutoMixMode mode, int row_h) + { + wxColour row_bg = StateColor::darkModeColorFor(*wxWHITE); + wxColour hover_bg = StateColor::darkModeColorFor(wxColour("#F4F4F4")); + wxColour text_fg = texture_import_text_colour(); + wxColour accent = StateColor::darkModeColorFor(wxColour("#009688")); + + wxPanel* row = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(-1, row_h), + wxTAB_TRAVERSAL | wxFULL_REPAINT_ON_RESIZE); + row->SetBackgroundColour(row_bg); + row->SetBackgroundStyle(wxBG_STYLE_PAINT); + row->SetCursor(wxCursor(wxCURSOR_HAND)); + + const int row_idx = mode == TextureAutoMixMode::CMYW ? 0 : 1; + row->Bind(wxEVT_PAINT, [this, row_bg, hover_bg, text_fg, accent, mode, row_idx](wxPaintEvent& e) { + auto* p = static_cast<wxPanel*>(e.GetEventObject()); + wxAutoBufferedPaintDC dc(p); + wxSize sz = p->GetClientSize(); + const bool hovered = (m_hover_idx == row_idx); + const bool selected = (m_current_mode == mode); + + dc.SetBrush(wxBrush(hovered ? hover_bg : row_bg)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.x, sz.y); + + wxFont font = p->GetFont(); + font.SetPointSize(m_font_point_size); + dc.SetFont(font); + dc.SetTextForeground(text_fg); + wxString label = auto_mix_mode_label(mode); + wxSize tsz = dc.GetTextExtent(label); + dc.DrawText(label, p->FromDIP(12), (sz.y - tsz.y) / 2); + + if (selected) { + wxFont check_font = p->GetFont(); + check_font.SetPointSize(12); + check_font.MakeBold(); + dc.SetFont(check_font); + dc.SetTextForeground(accent); + wxString check = wxString::FromUTF8("✓"); + wxSize csz = dc.GetTextExtent(check); + dc.DrawText(check, sz.x - p->FromDIP(16) - csz.x, (sz.y - csz.y) / 2); + } + }); + + row->Bind(wxEVT_MOTION, [this, row, row_idx](wxMouseEvent& evt) { + if (m_hover_idx != row_idx) { + m_hover_idx = row_idx; + row->Refresh(); + } + evt.Skip(); + }); + row->Bind(wxEVT_LEAVE_WINDOW, [this, row](wxMouseEvent& evt) { + m_hover_idx = -1; + row->Refresh(); + evt.Skip(); + }); + row->Bind(wxEVT_LEFT_DOWN, [this, mode](wxMouseEvent&) { + if (m_on_select) + m_on_select(mode); + Dismiss(); + }); + + return row; + } + + TextureAutoMixMode m_current_mode; + int m_font_point_size = 10; + int m_hover_idx = -1; + std::function<void(TextureAutoMixMode)> m_on_select; + std::function<void()> m_on_close; +}; + +// ============================================================ +// TexturePreviewCanvas +// ============================================================ + +TexturePreviewCanvas::TexturePreviewCanvas(wxWindow* parent, const wxGLAttributes& attrs) + : wxGLCanvas(parent, attrs, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE) +{ + m_context = new wxGLContext(this); + + Bind(wxEVT_PAINT, &TexturePreviewCanvas::on_paint, this); + Bind(wxEVT_SIZE, &TexturePreviewCanvas::on_size, this); + Bind(wxEVT_MOUSEWHEEL, &TexturePreviewCanvas::on_mouse, this); + Bind(wxEVT_LEFT_DOWN, &TexturePreviewCanvas::on_mouse, this); + Bind(wxEVT_LEFT_UP, &TexturePreviewCanvas::on_mouse, this); + Bind(wxEVT_RIGHT_DOWN, &TexturePreviewCanvas::on_mouse, this); + Bind(wxEVT_RIGHT_UP, &TexturePreviewCanvas::on_mouse, this); + Bind(wxEVT_MIDDLE_DOWN, &TexturePreviewCanvas::on_mouse, this); + Bind(wxEVT_MIDDLE_UP, &TexturePreviewCanvas::on_mouse, this); + Bind(wxEVT_MOTION, &TexturePreviewCanvas::on_mouse, this); + Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) { + m_drag_mode = DragMode::None; + m_reset_overlay_pressed = false; + }); + Bind(wxEVT_LEAVE_WINDOW, &TexturePreviewCanvas::on_mouse, this); +} + +TexturePreviewCanvas::~TexturePreviewCanvas() +{ + // See MixedFilamentDialog::~MixedFilamentDialog: a widget destroyed while it + // still holds the capture wedges mouse input for the whole application. + if (HasCapture()) + ReleaseMouse(); + + if (m_context) { + SetCurrent(*m_context); + if (m_tex_id) + glDeleteTextures(1, &m_tex_id); + for (unsigned int id : m_gl_tex_ids) + if (id) glDeleteTextures(1, &id); + for (unsigned int id : {m_reset_icon_tex, m_reset_icon_hover_tex, + m_reset_icon_dark_tex, m_reset_icon_dark_hover_tex}) + if (id) glDeleteTextures(1, &id); + delete m_context; + } +} + +void TexturePreviewCanvas::set_mesh_data( + const std::vector<std::array<float, 3>>& vertices, + const std::vector<std::array<int, 3>>& indices) +{ + m_vertices = vertices; + m_indices = indices; + update_bounding_box(); + compute_smooth_normals(); + Refresh(); +} + +void TexturePreviewCanvas::compute_smooth_normals() +{ + m_vertex_normals.clear(); + if (m_vertices.empty() || m_indices.empty()) return; + + m_vertex_normals.resize(m_vertices.size(), {0.f, 0.f, 0.f}); + + for (const auto& face : m_indices) { + int i0 = face[0], i1 = face[1], i2 = face[2]; + if (i0 < 0 || i0 >= (int)m_vertices.size() || + i1 < 0 || i1 >= (int)m_vertices.size() || + i2 < 0 || i2 >= (int)m_vertices.size()) + continue; + + const auto& v0 = m_vertices[i0]; + const auto& v1 = m_vertices[i1]; + const auto& v2 = m_vertices[i2]; + + float nx = (v1[1]-v0[1])*(v2[2]-v0[2]) - (v1[2]-v0[2])*(v2[1]-v0[1]); + float ny = (v1[2]-v0[2])*(v2[0]-v0[0]) - (v1[0]-v0[0])*(v2[2]-v0[2]); + float nz = (v1[0]-v0[0])*(v2[1]-v0[1]) - (v1[1]-v0[1])*(v2[0]-v0[0]); + + m_vertex_normals[i0][0] += nx; m_vertex_normals[i0][1] += ny; m_vertex_normals[i0][2] += nz; + m_vertex_normals[i1][0] += nx; m_vertex_normals[i1][1] += ny; m_vertex_normals[i1][2] += nz; + m_vertex_normals[i2][0] += nx; m_vertex_normals[i2][1] += ny; m_vertex_normals[i2][2] += nz; + } + + for (auto& n : m_vertex_normals) { + float len = std::sqrt(n[0]*n[0] + n[1]*n[1] + n[2]*n[2]); + if (len > 1e-8f) { n[0] /= len; n[1] /= len; n[2] /= len; } + } +} + +void TexturePreviewCanvas::set_texture_data( + const std::vector<std::array<float, 2>>& uvs, + const unsigned char* tex_data, int tex_w, int tex_h, int tex_channels) +{ + m_uvs = uvs; + m_tex_w = tex_w; + m_tex_h = tex_h; + m_tex_channels = tex_channels; + m_tex_dirty = true; + + size_t sz = (size_t)tex_w * tex_h * tex_channels; + m_tex_data.assign(tex_data, tex_data + sz); + Refresh(); +} + +void TexturePreviewCanvas::set_texture_render_data( + const std::vector<std::vector<unsigned char>>& tex_pixels_rgb, + const std::vector<int>& tex_widths, + const std::vector<int>& tex_heights, + const std::vector<std::array<std::array<float,2>, 3>>& face_uvs, + const std::vector<int>& face_tex_ids) +{ + m_tex_pixels_rgb = tex_pixels_rgb; + m_tex_widths = tex_widths; + m_tex_heights = tex_heights; + m_face_uvs = face_uvs; + m_face_tex_ids = face_tex_ids; + m_multi_tex_dirty = true; + Refresh(); +} + +void TexturePreviewCanvas::upload_textures() +{ + if (!m_multi_tex_dirty) return; + m_multi_tex_dirty = false; + + for (unsigned int id : m_gl_tex_ids) + if (id) glDeleteTextures(1, &id); + m_gl_tex_ids.clear(); + + m_gl_tex_ids.resize(m_tex_pixels_rgb.size(), 0); + for (size_t i = 0; i < m_tex_pixels_rgb.size(); ++i) { + if (m_tex_pixels_rgb[i].empty() || m_tex_widths[i] <= 0 || m_tex_heights[i] <= 0) + continue; + GLuint tex_id = 0; + glGenTextures(1, &tex_id); + glBindTexture(GL_TEXTURE_2D, tex_id); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, m_tex_widths[i], m_tex_heights[i], + 0, GL_RGB, GL_UNSIGNED_BYTE, m_tex_pixels_rgb[i].data()); + m_gl_tex_ids[i] = tex_id; + } + glBindTexture(GL_TEXTURE_2D, 0); +} + +void TexturePreviewCanvas::set_painted_mesh_data( + const std::vector<std::array<float, 3>>& vertices, + const std::vector<std::array<int, 3>>& indices) +{ + m_painted_vertices = vertices; + m_painted_indices = indices; + Refresh(); +} + +static void convert_face_colors(const std::vector<std::array<std::size_t, 3>>& src, + std::vector<std::array<float, 3>>& dst) +{ + dst.resize(src.size()); + for (size_t i = 0; i < src.size(); ++i) + dst[i] = { src[i][0] / 255.f, src[i][1] / 255.f, src[i][2] / 255.f }; +} + +void TexturePreviewCanvas::set_face_colors(const std::vector<std::array<std::size_t, 3>>& face_colors) +{ + convert_face_colors(face_colors, m_face_colors_rgb); + Refresh(); +} + +void TexturePreviewCanvas::set_original_face_colors(const std::vector<std::array<std::size_t, 3>>& face_colors) +{ + convert_face_colors(face_colors, m_original_face_colors_rgb); + Refresh(); +} + +void TexturePreviewCanvas::set_filament_color_map( + const std::map<std::array<std::size_t, 3>, std::array<float, 3>>& color_map) +{ + m_color_map = color_map; + m_filament_colors_rgb.resize(m_face_colors_rgb.size()); + for (size_t i = 0; i < m_face_colors_rgb.size(); ++i) { + std::array<std::size_t, 3> key = { + (std::size_t)(m_face_colors_rgb[i][0] * 255.f + 0.5f), + (std::size_t)(m_face_colors_rgb[i][1] * 255.f + 0.5f), + (std::size_t)(m_face_colors_rgb[i][2] * 255.f + 0.5f) + }; + auto it = color_map.find(key); + if (it != color_map.end()) + m_filament_colors_rgb[i] = it->second; + else + m_filament_colors_rgb[i] = m_face_colors_rgb[i]; + } + Refresh(); +} + +void TexturePreviewCanvas::set_render_mode(RenderMode mode) +{ + if (m_mode != mode) { + m_mode = mode; + Refresh(); + } +} + +void TexturePreviewCanvas::set_computing_overlay(bool /*show*/) +{ + Refresh(); +} + +void TexturePreviewCanvas::reset_view() +{ + m_zoom = 1.0f; + m_rot_x = -30.0f; + m_rot_y = 30.0f; + m_pan_x = 0.0f; + m_pan_y = 0.0f; + Refresh(); +} + +wxRect TexturePreviewCanvas::reset_overlay_rect() const +{ + wxSize sz = GetClientSize(); + const int button_size = FromDIP(40); + const int margin = FromDIP(20); + return wxRect( + std::max(margin, sz.x - button_size - margin), + std::max(margin, sz.y - button_size - margin), + button_size, + button_size); +} + +unsigned int TexturePreviewCanvas::upload_reset_icon_texture(const std::string& icon_name) +{ + wxBitmap bmp = create_scaled_bitmap(icon_name, this, 40); + if (!bmp.IsOk()) + return 0; + + wxImage image = bmp.ConvertToImage(); + if (!image.IsOk()) + return 0; + + const int w = image.GetWidth(); + const int h = image.GetHeight(); + const unsigned char* rgb = image.GetData(); + const unsigned char* alpha = image.HasAlpha() ? image.GetAlpha() : nullptr; + if (!rgb || w <= 0 || h <= 0) + return 0; + + std::vector<unsigned char> rgba((size_t)w * h * 4); + for (int i = 0; i < w * h; ++i) { + rgba[(size_t)i * 4 + 0] = rgb[i * 3 + 0]; + rgba[(size_t)i * 4 + 1] = rgb[i * 3 + 1]; + rgba[(size_t)i * 4 + 2] = rgb[i * 3 + 2]; + rgba[(size_t)i * 4 + 3] = alpha ? alpha[i] : 255; + } + + GLuint tex_id = 0; + glGenTextures(1, &tex_id); + glBindTexture(GL_TEXTURE_2D, tex_id); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data()); + glBindTexture(GL_TEXTURE_2D, 0); + return tex_id; +} + +void TexturePreviewCanvas::upload_reset_icon_textures() +{ + if (m_reset_icon_tex && m_reset_icon_hover_tex && m_reset_icon_dark_tex && m_reset_icon_dark_hover_tex) + return; + + if (!m_reset_icon_tex) + m_reset_icon_tex = upload_reset_icon_texture("canvas_zoom"); + if (!m_reset_icon_hover_tex) + m_reset_icon_hover_tex = upload_reset_icon_texture("canvas_zoom_hover"); + if (!m_reset_icon_dark_tex) + m_reset_icon_dark_tex = upload_reset_icon_texture("canvas_zoom_dark"); + if (!m_reset_icon_dark_hover_tex) + m_reset_icon_dark_hover_tex = upload_reset_icon_texture("canvas_zoom_dark_hover"); +} + +bool TexturePreviewCanvas::handle_reset_overlay_mouse(wxMouseEvent& evt) +{ + if (evt.Leaving()) { + if (m_reset_overlay_pressed) { + m_reset_overlay_hovered = false; + m_reset_overlay_pressed = false; + SetCursor(wxCursor(wxCURSOR_ARROW)); + if (HasCapture()) + ReleaseMouse(); + Refresh(); + return true; + } + if (m_reset_overlay_hovered) { + m_reset_overlay_hovered = false; + SetCursor(wxCursor(wxCURSOR_ARROW)); + Refresh(); + } + return false; + } + + const bool over = reset_overlay_rect().Contains(evt.GetPosition()); + if (over != m_reset_overlay_hovered) { + m_reset_overlay_hovered = over; + SetCursor(wxCursor(over ? wxCURSOR_HAND : wxCURSOR_ARROW)); + Refresh(); + } + + if (m_drag_mode != DragMode::None && !m_reset_overlay_pressed) + return false; + + if (evt.LeftDown() && over) { + m_reset_overlay_pressed = true; + if (!HasCapture()) + CaptureMouse(); + Refresh(); + return true; + } + + if (evt.LeftUp() && m_reset_overlay_pressed) { + const bool activate = over; + m_reset_overlay_pressed = false; + if (HasCapture()) + ReleaseMouse(); + if (activate) + reset_view(); + else + Refresh(); + return true; + } + + return over; +} + +void TexturePreviewCanvas::update_bounding_box() +{ + if (m_vertices.empty()) return; + std::array<float, 3> mn = m_vertices[0], mx = m_vertices[0]; + for (const auto& v : m_vertices) { + for (int i = 0; i < 3; ++i) { + mn[i] = std::min(mn[i], v[i]); + mx[i] = std::max(mx[i], v[i]); + } + } + m_center = { (mn[0]+mx[0])/2, (mn[1]+mx[1])/2, (mn[2]+mx[2])/2 }; + float dx = mx[0]-mn[0], dy = mx[1]-mn[1], dz = mx[2]-mn[2]; + m_radius = std::sqrt(dx*dx + dy*dy + dz*dz) / 2.0f; + if (m_radius < 1e-6f) m_radius = 1.0f; +} + +void TexturePreviewCanvas::ensure_gl_ready() +{ + if (m_gl_initialized) return; + + // BBS loads the GL entry points here with GLEW; Orca loads them centrally in + // OpenGLManager, so only check that this has already happened (glad leaves unresolved + // entry points null) and drain any stale error state. + if (glGetString == nullptr) { + BOOST_LOG_TRIVIAL(error) << "TexturePreviewCanvas: OpenGL functions are not loaded yet"; + return; + } + while (glGetError() != GL_NO_ERROR) {} + + m_gl_initialized = true; + + glEnable(GL_DEPTH_TEST); + glEnable(GL_LIGHTING); + glEnable(GL_LIGHT0); + glEnable(GL_COLOR_MATERIAL); + glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE); + + GLfloat light_pos[] = { 0.5f, 1.0f, 1.0f, 0.0f }; + GLfloat light_ambient[] = { 0.3f, 0.3f, 0.3f, 1.0f }; + GLfloat light_diffuse[] = { 0.8f, 0.8f, 0.8f, 1.0f }; + glLightfv(GL_LIGHT0, GL_POSITION, light_pos); + glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient); + glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse); +} + +void TexturePreviewCanvas::on_paint(wxPaintEvent&) +{ + wxPaintDC dc(this); + if (!m_context) return; + SetCurrent(*m_context); + ensure_gl_ready(); + render(); + SwapBuffers(); +} + +void TexturePreviewCanvas::on_size(wxSizeEvent&) +{ + Refresh(); +} + +void TexturePreviewCanvas::on_mouse(wxMouseEvent& evt) +{ + if (handle_reset_overlay_mouse(evt)) + return; + + if (evt.LeftDown()) { + m_drag_mode = DragMode::Rotate; + m_last_mouse_pos = evt.GetPosition(); + if (!HasCapture()) CaptureMouse(); + } + else if (evt.LeftUp()) { + if (m_drag_mode == DragMode::Rotate) { + m_drag_mode = DragMode::None; + if (HasCapture()) ReleaseMouse(); + } + } + else if (evt.RightDown()) { + m_drag_mode = DragMode::Pan; + m_last_mouse_pos = evt.GetPosition(); + if (!HasCapture()) CaptureMouse(); + } + else if (evt.MiddleDown()) { + m_drag_mode = DragMode::Pan; + m_last_mouse_pos = evt.GetPosition(); + if (!HasCapture()) CaptureMouse(); + } + else if (evt.RightUp() || evt.MiddleUp()) { + if (m_drag_mode == DragMode::Pan) { + m_drag_mode = DragMode::None; + if (HasCapture()) ReleaseMouse(); + } + } + else if (evt.Dragging() && m_drag_mode != DragMode::None) { + wxPoint pos = evt.GetPosition(); + float dx = (float)(pos.x - m_last_mouse_pos.x); + float dy = (float)(pos.y - m_last_mouse_pos.y); + + if (m_drag_mode == DragMode::Rotate) { + m_rot_y += dx * 0.5f; + m_rot_x += dy * 0.5f; + m_rot_x = std::max(-89.0f, std::min(89.0f, m_rot_x)); + } else if (m_drag_mode == DragMode::Pan) { + wxSize sz = GetClientSize(); + if (sz.x > 0) + m_pan_x += dx / (float)sz.x * m_radius * 2.0f / m_zoom; + if (sz.y > 0) + m_pan_y -= dy / (float)sz.y * m_radius * 2.0f / m_zoom; + } + + m_last_mouse_pos = pos; + Refresh(); + } + else if (evt.GetWheelRotation() != 0) { + float delta = evt.GetWheelRotation() > 0 ? 1.1f : 0.9f; + m_zoom *= delta; + m_zoom = std::max(0.1f, std::min(20.0f, m_zoom)); + Refresh(); + } +} + +void TexturePreviewCanvas::render() +{ + wxSize sz = GetClientSize(); + if (sz.x <= 0 || sz.y <= 0) return; + + wxSize viewport_sz = gl_viewport_size(this, sz); + glViewport(0, 0, viewport_sz.x, viewport_sz.y); + // Same palette key as the preview container, so canvas and frame cannot drift apart. + const wxColour clear_clr = StateColor::darkModeColorFor(wxColour("#EEEEEE")); + glClearColor(clear_clr.Red() / 255.f, clear_clr.Green() / 255.f, clear_clr.Blue() / 255.f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + float aspect = (float)viewport_sz.x / (float)viewport_sz.y; + float dist = m_radius * 3.0f / m_zoom; + float near_plane = dist * 0.01f; + float far_plane = dist * 10.0f; + float fov_rad = 45.0f * static_cast<float>(M_PI) / 180.0f; + float f = 1.0f / std::tan(fov_rad / 2.0f); + float proj[16] = {}; + proj[0] = f / aspect; + proj[5] = f; + proj[10] = (far_plane + near_plane) / (near_plane - far_plane); + proj[11] = -1.0f; + proj[14] = (2.0f * far_plane * near_plane) / (near_plane - far_plane); + glMultMatrixf(proj); + + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + glTranslatef(0.0f, 0.0f, -dist); + glTranslatef(m_pan_x, m_pan_y, 0.0f); + glRotatef(m_rot_x, 1.0f, 0.0f, 0.0f); + glRotatef(m_rot_y, 0.0f, 1.0f, 0.0f); + glTranslatef(-m_center[0], -m_center[1], -m_center[2]); + + render_mesh(); + render_reset_overlay(sz, viewport_sz); +} + +void TexturePreviewCanvas::render_reset_overlay(const wxSize& logical_size, const wxSize& viewport_size) +{ + if (logical_size.x <= 0 || logical_size.y <= 0 || viewport_size.x <= 0 || viewport_size.y <= 0) + return; + + upload_reset_icon_textures(); + + const unsigned int tex_id = is_dark() + ? (m_reset_overlay_hovered ? m_reset_icon_dark_hover_tex : m_reset_icon_dark_tex) + : (m_reset_overlay_hovered ? m_reset_icon_hover_tex : m_reset_icon_tex); + if (!tex_id) + return; + + wxRect rc = reset_overlay_rect(); + const float sx = (float)viewport_size.x / (float)logical_size.x; + const float sy = (float)viewport_size.y / (float)logical_size.y; + const float x0 = rc.GetLeft() * sx; + const float y0 = rc.GetTop() * sy; + const float x1 = (rc.GetLeft() + rc.GetWidth()) * sx; + const float y1 = (rc.GetTop() + rc.GetHeight()) * sy; + const float alpha = m_reset_overlay_hovered ? 1.0f : 0.78f; + + glPushAttrib(GL_ENABLE_BIT | GL_COLOR_BUFFER_BIT | GL_TEXTURE_BIT | GL_DEPTH_BUFFER_BIT); + glDisable(GL_DEPTH_TEST); + glDisable(GL_LIGHTING); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, tex_id); + glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); + + glMatrixMode(GL_PROJECTION); + glPushMatrix(); + glLoadIdentity(); + glOrtho(0.0, viewport_size.x, viewport_size.y, 0.0, -1.0, 1.0); + + glMatrixMode(GL_MODELVIEW); + glPushMatrix(); + glLoadIdentity(); + + glColor4f(1.0f, 1.0f, 1.0f, alpha); + glBegin(GL_QUADS); + glTexCoord2f(0.0f, 0.0f); glVertex2f(x0, y0); + glTexCoord2f(1.0f, 0.0f); glVertex2f(x1, y0); + glTexCoord2f(1.0f, 1.0f); glVertex2f(x1, y1); + glTexCoord2f(0.0f, 1.0f); glVertex2f(x0, y1); + glEnd(); + + glPopMatrix(); + glMatrixMode(GL_PROJECTION); + glPopMatrix(); + glMatrixMode(GL_MODELVIEW); + + glBindTexture(GL_TEXTURE_2D, 0); + glPopAttrib(); +} + +void TexturePreviewCanvas::render_textured_original() +{ + if (m_vertices.empty() || m_indices.empty()) return; + if (m_face_uvs.empty() || m_face_tex_ids.empty()) return; + if (m_face_uvs.size() != m_indices.size()) return; + + upload_textures(); + + const bool has_smooth = (m_vertex_normals.size() == m_vertices.size()); + + // Group faces by texture id for batch rendering + std::map<int, std::vector<size_t>> tex_groups; + for (size_t fi = 0; fi < m_indices.size(); ++fi) { + int tid = (fi < m_face_tex_ids.size()) ? m_face_tex_ids[fi] : -1; + tex_groups[tid].push_back(fi); + } + + glEnable(GL_LIGHTING); + glColor3f(1.0f, 1.0f, 1.0f); + + for (const auto& [tid, face_list] : tex_groups) { + bool tex_bound = false; + if (tid >= 0 && tid < (int)m_gl_tex_ids.size() && m_gl_tex_ids[tid] != 0) { + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, m_gl_tex_ids[tid]); + tex_bound = true; + } else { + glDisable(GL_TEXTURE_2D); + } + + glBegin(GL_TRIANGLES); + for (size_t fi : face_list) { + const auto& face = m_indices[fi]; + const auto& uvs = m_face_uvs[fi]; + + if (!tex_bound) { + if (fi < m_original_face_colors_rgb.size()) + glColor3fv(m_original_face_colors_rgb[fi].data()); + else + glColor3f(0.7f, 0.7f, 0.7f); + } + + for (int vi = 0; vi < 3; ++vi) { + int idx = face[vi]; + if (idx < 0 || idx >= (int)m_vertices.size()) continue; + + if (has_smooth) { + glNormal3fv(m_vertex_normals[idx].data()); + } else if (vi == 0) { + const auto& v0 = m_vertices[face[0]]; + const auto& v1 = m_vertices[face[1]]; + const auto& v2 = m_vertices[face[2]]; + float nx = (v1[1]-v0[1])*(v2[2]-v0[2]) - (v1[2]-v0[2])*(v2[1]-v0[1]); + float ny = (v1[2]-v0[2])*(v2[0]-v0[0]) - (v1[0]-v0[0])*(v2[2]-v0[2]); + float nz = (v1[0]-v0[0])*(v2[1]-v0[1]) - (v1[1]-v0[1])*(v2[0]-v0[0]); + float len = std::sqrt(nx*nx + ny*ny + nz*nz); + if (len > 1e-8f) { nx /= len; ny /= len; nz /= len; } + glNormal3f(nx, ny, nz); + } + + if (tex_bound) + glTexCoord2fv(uvs[vi].data()); + glVertex3fv(m_vertices[idx].data()); + } + } + glEnd(); + } + + glDisable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, 0); +} + +void TexturePreviewCanvas::render_mesh() +{ + if (m_vertices.empty() || m_indices.empty()) return; + + // Original mode with texture data: use proper texture mapping + if (m_mode == RenderMode::Original && !m_face_uvs.empty()) { + render_textured_original(); + return; + } + + // For Multi-Color / FilamentMap, use the painted (remeshed) geometry if available; + // the face color arrays match the painted mesh, not the original mesh. + const bool use_painted = (m_mode != RenderMode::Original) + && !m_painted_vertices.empty() + && !m_painted_indices.empty(); + + const auto& verts = use_painted ? m_painted_vertices : m_vertices; + const auto& faces = use_painted ? m_painted_indices : m_indices; + + const std::vector<std::array<float, 3>>* colors_ptr = nullptr; + if (m_mode == RenderMode::Original && !m_original_face_colors_rgb.empty() + && m_original_face_colors_rgb.size() == m_indices.size()) { + colors_ptr = &m_original_face_colors_rgb; + } else if (m_mode == RenderMode::FilamentMap && !m_filament_colors_rgb.empty() + && m_filament_colors_rgb.size() == faces.size()) { + colors_ptr = &m_filament_colors_rgb; + } else if (!m_face_colors_rgb.empty() && m_face_colors_rgb.size() == faces.size()) { + colors_ptr = &m_face_colors_rgb; + } + + // Use smooth normals for the original mesh when available + const bool has_smooth = !use_painted + && (m_vertex_normals.size() == m_vertices.size()); + + glDisable(GL_TEXTURE_2D); + glEnable(GL_LIGHTING); + + glBegin(GL_TRIANGLES); + for (size_t fi = 0; fi < faces.size(); ++fi) { + if (colors_ptr) + glColor3fv((*colors_ptr)[fi].data()); + else + glColor3f(0.7f, 0.7f, 0.7f); + + const auto& face = faces[fi]; + for (int vi = 0; vi < 3; ++vi) { + int idx = face[vi]; + if (idx < 0 || idx >= (int)verts.size()) continue; + + if (has_smooth && idx < (int)m_vertex_normals.size()) { + glNormal3fv(m_vertex_normals[idx].data()); + } else if (vi == 0) { + const auto& v0 = verts[face[0]]; + const auto& v1 = verts[face[1]]; + const auto& v2 = verts[face[2]]; + float nx = (v1[1]-v0[1])*(v2[2]-v0[2]) - (v1[2]-v0[2])*(v2[1]-v0[1]); + float ny = (v1[2]-v0[2])*(v2[0]-v0[0]) - (v1[0]-v0[0])*(v2[2]-v0[2]); + float nz = (v1[0]-v0[0])*(v2[1]-v0[1]) - (v1[1]-v0[1])*(v2[0]-v0[0]); + float len = std::sqrt(nx*nx + ny*ny + nz*nz); + if (len > 1e-8f) { nx /= len; ny /= len; nz /= len; } + glNormal3f(nx, ny, nz); + } + + glVertex3fv(verts[idx].data()); + } + } + glEnd(); +} + + +// ============================================================ +// TextureImportDialog +// ============================================================ + +wxBEGIN_EVENT_TABLE(TextureImportDialog, DPIDialog) + EVT_BUTTON(TextureImportDialog::ID_COLOR_4, TextureImportDialog::on_color_preset_clicked) + EVT_BUTTON(TextureImportDialog::ID_COLOR_8, TextureImportDialog::on_color_preset_clicked) + EVT_BUTTON(TextureImportDialog::ID_COLOR_16, TextureImportDialog::on_color_preset_clicked) + EVT_BUTTON(TextureImportDialog::ID_COLOR_AUTO, TextureImportDialog::on_color_preset_clicked) + EVT_BUTTON(TextureImportDialog::ID_BTN_APPLY, TextureImportDialog::on_apply_clicked) + EVT_BUTTON(TextureImportDialog::ID_BTN_SKIP, TextureImportDialog::on_skip_clicked) + EVT_BUTTON(wxID_OK, TextureImportDialog::on_ok_clicked) +wxEND_EVENT_TABLE() + +TextureImportDialog::TextureImportDialog( + wxWindow* parent, + const Slic3r::TexturedMesh& textured_mesh, + const std::vector<TextureFilamentEntry>& filament_entries, + std::function<bool()> initial_cancel_callback, + std::function<bool(int)> initial_progress_callback) + : DPIDialog(parent, wxID_ANY, _L("Import Model"), + wxDefaultPosition, wxDefaultSize, + (wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) & ~(wxMINIMIZE_BOX | wxMAXIMIZE_BOX)) + , m_textured_mesh(textured_mesh) + , m_filament_entries(filament_entries) + , m_initial_cancel_callback(std::move(initial_cancel_callback)) + , m_initial_progress_callback(std::move(initial_progress_callback)) +{ + SetSize(wxSize(FromDIP(960), FromDIP(640))); + + m_filament_colors_rgba.reserve(m_filament_entries.size()); + m_filament_color_strs.reserve(m_filament_entries.size()); + m_filament_names.reserve(m_filament_entries.size()); + for (size_t i = 0; i < m_filament_entries.size(); ++i) { + auto& entry = m_filament_entries[i]; + entry.dialog_index = (int)i; + entry.color_hex = texture_normalize_color_hex(entry.color_hex); + if (entry.name.empty()) + entry.name = "Filament " + std::to_string(i + 1); + m_filament_color_strs.push_back(entry.color_hex); + m_filament_names.push_back(entry.name); + m_filament_colors_rgba.push_back(parse_color_string(entry.color_hex)); + } + + m_existing_filament_count = m_filament_colors_rgba.size(); + m_default_virtual_filament_preset_name = resolve_default_virtual_filament_preset_name(); + + Bind(EVT_TEXTURE_COMPUTE_DONE, &TextureImportDialog::on_computation_complete, this); + Bind(EVT_TEXTURE_COMPUTE_PROGRESS, &TextureImportDialog::on_computation_progress, this); + Bind(EVT_TEXTURE_COMPUTE_ERROR, &TextureImportDialog::on_computation_error, this); + Bind(EVT_TEXTURE_MESH_REPAIR_DECISION, &TextureImportDialog::on_mesh_repair_decision_required, this); + + build_ui(); + SetMinSize(wxSize(FromDIP(800), FromDIP(500))); + CenterOnParent(); + wxGetApp().UpdateDlgDarkUI(this); + + m_preview_canvas->set_mesh_data(m_textured_mesh.vertices, m_textured_mesh.indices); + + // Pre-computed face colors (OBJ vertex colors / MTL face colors): + // use them directly as the Original preview, skip texture decode. + if (!m_textured_mesh.precomputed_face_colors.empty()) { + m_preview_canvas->set_original_face_colors(m_textured_mesh.precomputed_face_colors); + } else if (!m_textured_mesh.textures.empty()) { + std::vector<std::vector<unsigned char>> tex_pixels_rgb; + std::vector<int> tex_widths, tex_heights; + tex_pixels_rgb.reserve(m_textured_mesh.textures.size()); + tex_widths.reserve(m_textured_mesh.textures.size()); + tex_heights.reserve(m_textured_mesh.textures.size()); + + for (const auto& ti : m_textured_mesh.textures) { + std::vector<unsigned char> bgr_pixels; + int w = 0, h = 0; + if (Slic3r::decode_texture_to_pixels(ti, bgr_pixels, w, h) && !bgr_pixels.empty()) { + // Convert BGR to RGB for OpenGL + for (size_t p = 0; p < bgr_pixels.size(); p += 3) + std::swap(bgr_pixels[p], bgr_pixels[p + 2]); + tex_pixels_rgb.push_back(std::move(bgr_pixels)); + } else { + tex_pixels_rgb.push_back({}); + } + tex_widths.push_back(w); + tex_heights.push_back(h); + } + + const size_t nf = m_textured_mesh.indices.size(); + const bool has_mapping = !m_textured_mesh.material_texture_map.empty(); + + // Build per-face UV array + std::vector<std::array<std::array<float,2>, 3>> face_uvs(nf); + for (size_t fi = 0; fi < nf; ++fi) { + if (m_textured_mesh.has_face_uvs()) { + const auto& ui = m_textured_mesh.uv_indices[fi]; + for (int vi = 0; vi < 3; ++vi) { + int idx = ui[vi]; + if (idx >= 0 && static_cast<size_t>(idx) < m_textured_mesh.uv_coords.size()) + face_uvs[fi][vi] = m_textured_mesh.uv_coords[idx]; + else + face_uvs[fi][vi] = {0.f, 0.f}; + } + } else if (!m_textured_mesh.uvs.empty()) { + const auto& face = m_textured_mesh.indices[fi]; + for (int vi = 0; vi < 3; ++vi) { + int idx = face[vi]; + if (idx >= 0 && static_cast<size_t>(idx) < m_textured_mesh.uvs.size()) + face_uvs[fi][vi] = m_textured_mesh.uvs[idx]; + else + face_uvs[fi][vi] = {0.f, 0.f}; + } + } + } + + // Build per-face texture index + std::vector<int> face_tex_ids(nf, 0); + for (size_t fi = 0; fi < nf; ++fi) { + int mat_idx = (fi < m_textured_mesh.material_ids.size()) + ? m_textured_mesh.material_ids[fi] : -1; + if (has_mapping && mat_idx >= 0 + && static_cast<size_t>(mat_idx) < m_textured_mesh.material_texture_map.size()) + face_tex_ids[fi] = m_textured_mesh.material_texture_map[mat_idx]; + else if (!tex_pixels_rgb.empty()) + face_tex_ids[fi] = 0; + else + face_tex_ids[fi] = -1; + } + + m_preview_canvas->set_texture_render_data( + tex_pixels_rgb, tex_widths, tex_heights, face_uvs, face_tex_ids); + + // Still sample per-face colors as fallback + std::vector<std::array<std::size_t, 3>> orig_colors; + if (Slic3r::sample_original_face_colors(m_textured_mesh, orig_colors)) + m_preview_canvas->set_original_face_colors(orig_colors); + } + + set_state(TextureImportState::Idle); +} + +TextureImportDialog::~TextureImportDialog() +{ + dismiss_auto_mix_popup(); + dismiss_filament_popup(); + m_cancel_flag = true; + if (m_worker && m_worker->joinable()) + m_worker->join(); +} + +int TextureImportDialog::ShowModal() +{ + if (m_state == TextureImportState::Idle && m_painted.face_colors.empty()) { + start_computation(true, true); + + while (m_initial_computation_pending) { + if (auto* event_loop = wxEventLoopBase::GetActive()) + event_loop->Yield(); + else + wxYield(); + if (m_progress_dlg && m_progress_dlg->WasCancelled()) + m_cancel_flag = true; + if (m_initial_cancel_callback && m_initial_cancel_callback()) + m_cancel_flag = true; + wxMilliSleep(10); + } + + if (m_worker && m_worker->joinable()) + m_worker->join(); + m_worker.reset(); + + if (m_initial_computation_cancelled || m_initial_computation_failed) + return wxID_CANCEL; + } + + ScopedInteractiveBusyCursorSuspender busy_cursor_suspender; + return DPIDialog::ShowModal(); +} + +void TextureImportDialog::build_ui() +{ + const wxColour dialog_bg = StateColor::darkModeColorFor(*wxWHITE); + SetBackgroundColour(dialog_bg); + SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D"))); + + wxBoxSizer* root_sizer = new wxBoxSizer(wxVERTICAL); + + auto line_top = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(-1, 1)); + line_top->SetBackgroundColour(texture_import_separator_colour()); + root_sizer->Add(line_top, 0, wxEXPAND); + + wxBoxSizer* main_sizer = new wxBoxSizer(wxHORIZONTAL); + + wxBoxSizer* left_sizer = new wxBoxSizer(wxVERTICAL); + build_preview_panel(this, left_sizer); + main_sizer->Add(left_sizer, 3, wxEXPAND | wxALL, FromDIP(8)); + + wxBoxSizer* right_sizer = new wxBoxSizer(wxVERTICAL); + build_params_panel(this, right_sizer); + build_mapping_panel(this, right_sizer); + build_bottom_buttons(right_sizer); + main_sizer->Add(right_sizer, 2, wxEXPAND | wxALL, FromDIP(8)); + + root_sizer->Add(main_sizer, 1, wxEXPAND); + + SetSizer(root_sizer); + Layout(); + Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this); + +#ifdef __WXMSW__ + wxPanel* size_grip_cover = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); + size_grip_cover->SetBackgroundColour(dialog_bg); + size_grip_cover->SetBackgroundStyle(wxBG_STYLE_COLOUR); + + auto update_size_grip_cover = [this, size_grip_cover]() { + const int cover_size = FromDIP(20); + wxSize client_size = GetClientSize(); + size_grip_cover->SetSize(client_size.x - cover_size, client_size.y - cover_size, cover_size, cover_size); + size_grip_cover->Raise(); + }; + update_size_grip_cover(); + + Bind(wxEVT_SIZE, [update_size_grip_cover](wxSizeEvent& e) { + e.Skip(); + update_size_grip_cover(); + }); +#endif +} + +void TextureImportDialog::build_preview_panel(wxWindow* parent, wxSizer* sizer) +{ + wxColour preview_bg = StateColor::darkModeColorFor(wxColour("#EEEEEE")); + wxColour preview_bd = texture_import_separator_colour(); + + wxPanel* preview_container = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); + preview_container->SetBackgroundColour(preview_bg); + preview_container->SetBackgroundStyle(wxBG_STYLE_PAINT); + preview_container->Bind(wxEVT_PAINT, [preview_bg, preview_bd](wxPaintEvent& e) { + auto* p = static_cast<wxPanel*>(e.GetEventObject()); + wxAutoBufferedPaintDC dc(p); + wxSize sz = p->GetClientSize(); + dc.SetBrush(wxBrush(p->GetParent()->GetBackgroundColour())); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.x, sz.y); + dc.SetBrush(wxBrush(preview_bg)); + dc.SetPen(wxPen(preview_bd, 1)); + dc.DrawRoundedRectangle(0, 0, sz.x, sz.y, 4); + }); + + wxBoxSizer* container_sizer = new wxBoxSizer(wxVERTICAL); + + wxGLAttributes canvas_attrs; + canvas_attrs.PlatformDefaults().RGBA().DoubleBuffer().Depth(24).EndList(); + m_preview_canvas = new TexturePreviewCanvas(preview_container, canvas_attrs); + container_sizer->Add(m_preview_canvas, 1, wxEXPAND | wxALL, FromDIP(1)); + + preview_container->SetSizer(container_sizer); + sizer->Add(preview_container, 1, wxEXPAND); + + m_tab_panel = new wxPanel(preview_container, wxID_ANY); + m_tab_panel->SetBackgroundColour(preview_bg); + + m_btn_view_original = new Button(m_tab_panel, _L("Original")); + m_btn_view_original->SetId(ID_VIEW_ORIGINAL); + m_btn_view_multicolor = new Button(m_tab_panel, _L("Multi-Color")); + m_btn_view_multicolor->SetId(ID_VIEW_MULTICOLOR); + + const int view_button_height = FromDIP(27); + m_btn_view_original->SetCornerRadius(view_button_height / 2); + m_btn_view_original->SetMinSize(wxSize(FromDIP(57), view_button_height)); + m_btn_view_original->SetFont(m_btn_view_original->GetFont().Bold()); + m_btn_view_original->SetToolTip(_L("Your input texture model")); + m_btn_view_multicolor->SetCornerRadius(view_button_height / 2); + m_btn_view_multicolor->SetMinSize(wxSize(FromDIP(57), view_button_height)); + m_btn_view_multicolor->SetFont(m_btn_view_multicolor->GetFont().Bold()); + m_btn_view_multicolor->SetToolTip(_L("Processed multi-color model")); + + wxBoxSizer* tab_sizer = new wxBoxSizer(wxHORIZONTAL); + tab_sizer->Add(m_btn_view_original, 0, wxRIGHT, FromDIP(2)); + tab_sizer->Add(m_btn_view_multicolor, 0); + m_tab_panel->SetSizer(tab_sizer); + m_tab_panel->Fit(); + + m_btn_view_multicolor->Hide(); + + auto preview_original = [this](wxMouseEvent& e) { + if (m_preview_canvas) { + m_preview_canvas->set_render_mode(TexturePreviewCanvas::RenderMode::Original); + highlight_view_button(0); + } + e.Skip(); + }; + auto preview_multicolor = [this](wxMouseEvent& e) { + if (m_preview_canvas) { + m_preview_canvas->set_render_mode(TexturePreviewCanvas::RenderMode::MultiColor); + highlight_view_button(1); + } + e.Skip(); + }; + auto restore_filament_if_outside = [this](wxMouseEvent& e) { + if (m_preview_canvas && m_tab_panel) { + wxWindow* event_window = wxDynamicCast(e.GetEventObject(), wxWindow); + wxPoint screen_pos = event_window ? event_window->ClientToScreen(e.GetPosition()) : wxGetMousePosition(); + wxPoint panel_pos = m_tab_panel->ScreenToClient(screen_pos); + if (!m_tab_panel->GetClientRect().Contains(panel_pos)) { + const bool mapping_ready = (m_state == TextureImportState::Ready); + m_preview_canvas->set_render_mode(mapping_ready ? TexturePreviewCanvas::RenderMode::FilamentMap : + TexturePreviewCanvas::RenderMode::Original); + highlight_view_button(-1); + } + } + e.Skip(); + }; + + m_btn_view_original->Bind(wxEVT_ENTER_WINDOW, preview_original); + m_btn_view_multicolor->Bind(wxEVT_ENTER_WINDOW, preview_multicolor); + m_btn_view_original->Bind(wxEVT_LEAVE_WINDOW, restore_filament_if_outside); + m_btn_view_multicolor->Bind(wxEVT_LEAVE_WINDOW, restore_filament_if_outside); + m_tab_panel->Bind(wxEVT_LEAVE_WINDOW, restore_filament_if_outside); + + auto update_preview_overlay_buttons = [this]() { + if (m_tab_panel) { + m_tab_panel->Fit(); + m_tab_panel->SetPosition(wxPoint(FromDIP(8), FromDIP(8))); + m_tab_panel->Raise(); + } + }; + + preview_container->Bind(wxEVT_SIZE, [update_preview_overlay_buttons](wxSizeEvent& e) { + e.Skip(); + update_preview_overlay_buttons(); + }); + update_preview_overlay_buttons(); + + highlight_view_button(-1); +} + +void TextureImportDialog::build_params_panel(wxWindow* parent, wxSizer* sizer) +{ + wxColour label_fg = StateColor::darkModeColorFor(wxColour("#323A3D")); + + wxBoxSizer* color_header_sizer = new wxBoxSizer(wxHORIZONTAL); + wxStaticText* lbl_colors = new wxStaticText(parent, wxID_ANY, _L("Color Count")); + lbl_colors->SetForegroundColour(label_fg); + lbl_colors->SetFont(lbl_colors->GetFont().Bold()); + color_header_sizer->Add(lbl_colors, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + + m_btn_color_4 = new Button(parent, "4"); + m_btn_color_4->SetId(ID_COLOR_4); + m_btn_color_8 = new Button(parent, "8"); + m_btn_color_8->SetId(ID_COLOR_8); + m_btn_color_16 = new Button(parent, "16"); + m_btn_color_16->SetId(ID_COLOR_16); + m_btn_color_auto = new Button(parent, _L("Auto")); + m_btn_color_auto->SetId(ID_COLOR_AUTO); + + { + StateColor preset_bg( + std::pair<wxColour, int>(wxColour(0, 137, 123), StateColor::Pressed | StateColor::Checked), + std::pair<wxColour, int>(wxColour(38, 166, 154), StateColor::Hovered | StateColor::Checked), + std::pair<wxColour, int>(wxColour(0, 150, 136), StateColor::Checked), + std::pair<wxColour, int>(wxColour("#CECECE"), StateColor::Pressed), + std::pair<wxColour, int>(wxColour("#EEEEEE"), StateColor::Hovered), + std::pair<wxColour, int>(*wxWHITE, StateColor::Normal)); + StateColor preset_bd( + std::pair<wxColour, int>(wxColour(0, 150, 136), StateColor::Checked), + std::pair<wxColour, int>(wxColour("#CECECE"), StateColor::Normal)); + StateColor preset_text( + std::pair<wxColour, int>(wxColour("#FFFFFE"), StateColor::Checked), + std::pair<wxColour, int>(wxColour("#323A3D"), StateColor::Normal)); + + for (auto* btn : {m_btn_color_4, m_btn_color_8, m_btn_color_16}) { + btn->SetCornerRadius(FromDIP(12)); + btn->SetMinSize(wxSize(FromDIP(28), FromDIP(28))); + btn->SetBackgroundColor(preset_bg); + btn->SetBorderColor(preset_bd); + btn->SetTextColor(preset_text); + } + } + + update_color_count_preset_buttons(); + + color_header_sizer->Add(m_btn_color_4, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(2)); + color_header_sizer->Add(m_btn_color_8, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(2)); + color_header_sizer->Add(m_btn_color_16, 0, wxALIGN_CENTER_VERTICAL); + sizer->Add(color_header_sizer, 0, wxBOTTOM, FromDIP(4)); + + wxBoxSizer* color_slider_sizer = new wxBoxSizer(wxHORIZONTAL); + m_color_slider = new AccentSlider(parent, m_param_color_count, 1, (int)max_filament_count()); + m_color_spin = new SpinInput(parent, wxString::Format("%d", m_param_color_count), + wxEmptyString, wxDefaultPosition, + wxSize(FromDIP(60), FromDIP(28)), + wxTE_PROCESS_ENTER, 1, (int)max_filament_count(), m_param_color_count); + + m_color_slider->Bind(wxEVT_SLIDER, &TextureImportDialog::on_color_slider_changed, this); + m_color_spin->Bind(wxEVT_SPINCTRL, &TextureImportDialog::on_color_spin_changed, this); + m_color_spin->Bind(EVT_SPINCTRL_TEXT, &TextureImportDialog::on_color_spin_text_changed, this); + + color_slider_sizer->Add(m_color_slider, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + color_slider_sizer->Add(m_color_spin, 0, wxALIGN_CENTER_VERTICAL); + sizer->Add(color_slider_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(8)); + + wxStaticText* lbl_smooth = new wxStaticText(parent, wxID_ANY, _L("Smooth Level")); + lbl_smooth->SetForegroundColour(label_fg); + lbl_smooth->SetFont(lbl_smooth->GetFont().Bold()); + sizer->Add(lbl_smooth, 0, wxBOTTOM, FromDIP(4)); + + wxBoxSizer* smooth_sizer = new wxBoxSizer(wxHORIZONTAL); + m_smooth_slider = new AccentSlider(parent, m_param_smooth, 0, 10); + m_smooth_spin = new SpinInput(parent, wxString::Format("%d", m_param_smooth), + wxEmptyString, wxDefaultPosition, + wxSize(FromDIP(60), FromDIP(28)), + wxTE_PROCESS_ENTER, 0, 10, m_param_smooth); + + m_smooth_slider->Bind(wxEVT_SLIDER, &TextureImportDialog::on_smooth_slider_changed, this); + m_smooth_spin->Bind(wxEVT_SPINCTRL, &TextureImportDialog::on_smooth_spin_changed, this); + m_smooth_spin->Bind(EVT_SPINCTRL_TEXT, &TextureImportDialog::on_smooth_spin_text_changed, this); + + smooth_sizer->Add(m_smooth_slider, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + smooth_sizer->Add(m_smooth_spin, 0, wxALIGN_CENTER_VERTICAL); + sizer->Add(smooth_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(4)); + + m_btn_apply = new Button(parent, _L("Apply")); + m_btn_apply->SetId(ID_BTN_APPLY); + + { + StateColor btn_bg_white( + std::pair<wxColour, int>(wxColour("#CECECE"), StateColor::Pressed), + std::pair<wxColour, int>(wxColour("#EEEEEE"), StateColor::Hovered), + std::pair<wxColour, int>(*wxWHITE, StateColor::Normal)); + const wxColour btn_bd_accent = wxColour(0, 150, 136); + const wxColour btn_text_accent = wxColour(0, 150, 136); + + m_btn_color_auto->SetCornerRadius(FromDIP(12)); + m_btn_color_auto->SetMinSize(wxSize(FromDIP(60), FromDIP(28))); + m_btn_color_auto->SetBackgroundColor(btn_bg_white); + m_btn_color_auto->SetBorderColor(btn_bd_accent); + m_btn_color_auto->SetTextColor(btn_text_accent); + + m_btn_apply->SetCornerRadius(FromDIP(12)); + m_btn_apply->SetMinSize(wxSize(FromDIP(60), FromDIP(28))); + m_btn_apply->SetBackgroundColor(btn_bg_white); + m_btn_apply->SetBorderColor(btn_bd_accent); + m_btn_apply->SetTextColor(btn_text_accent); + } + + // Defer attaching the Auto/Apply tooltips until the dialog has actually + // been shown. On macOS, AppKit creates NSTrackingArea and dispatches a + // synthetic mouseEntered: as soon as the window first becomes visible, + // which would otherwise pop the native tooltip without the user actually + // hovering when the cursor happens to land on these buttons as the dialog + // appears. + Bind(wxEVT_SHOW, [this](wxShowEvent& e) { + e.Skip(); + if (!e.IsShown() || m_initial_tooltips_set) + return; + m_initial_tooltips_set = true; + CallAfter([this]() { + if (m_btn_color_auto) + m_btn_color_auto->SetToolTip(_L("Automatically determine the optimal color count only and recompute filament mapping")); + if (m_btn_apply) + m_btn_apply->SetToolTip(_L("Convert texture to painting using the specified color count and smooth level")); + }); + }); + + wxBoxSizer* apply_sizer = new wxBoxSizer(wxHORIZONTAL); + apply_sizer->Add(m_btn_color_auto, 0, wxRIGHT, FromDIP(4)); + apply_sizer->Add(m_btn_apply, 0); + sizer->Add(apply_sizer, 0, wxALIGN_RIGHT | wxBOTTOM, FromDIP(8)); + + m_hint_label = new wxStaticText(parent, wxID_ANY, + _L("Reminder: parameters changed, click Apply to take effect")); + m_hint_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#FF6F00"))); + m_hint_label->SetFont(texture_import_section_title_font(parent)); + m_hint_label->Hide(); + sizer->Add(m_hint_label, 0, wxBOTTOM, FromDIP(4)); + + auto* mapping_separator = new StaticLine(parent); + mapping_separator->SetLineColour(wxColour(SEPARATOR_COLOUR_KEY)); + sizer->Add(mapping_separator, 0, wxEXPAND | wxBOTTOM, FromDIP(8)); +} + +void TextureImportDialog::build_mapping_panel(wxWindow* parent, wxSizer* sizer) +{ + wxColour secondary_fg = StateColor::darkModeColorFor(wxColour("#6B6B6B")); + + wxBoxSizer* header_sizer = new wxBoxSizer(wxHORIZONTAL); + + wxStaticText* lbl_mapping = new wxStaticText(parent, wxID_ANY, _L("Filament Mapping")); + lbl_mapping->SetForegroundColour(secondary_fg); + lbl_mapping->SetFont(texture_import_section_title_font(parent)); + m_auto_mix_font_point_size = lbl_mapping->GetFont().GetPointSize(); + header_sizer->Add(lbl_mapping, 0, wxALIGN_CENTER_VERTICAL); + + m_btn_mix_reset = new Button(parent, "", "revert_btn", wxBORDER_NONE, 16); + m_btn_mix_reset->SetCanFocus(false); + m_btn_mix_reset->SetPaddingSize(wxSize(FromDIP(2), FromDIP(2))); + { + StateColor reset_bg( + std::pair<wxColour, int>(wxColour("#F4F4F4"), StateColor::Pressed), + std::pair<wxColour, int>(wxColour("#F8F8F8"), StateColor::Hovered), + std::pair<wxColour, int>(*wxWHITE, StateColor::Normal)); + m_btn_mix_reset->SetBackgroundColor(reset_bg); + m_btn_mix_reset->SetBorderColor(StateColor()); + } + m_btn_mix_reset->SetToolTip(_L("Reset filament mapping to the state before one-click mixing")); + m_btn_mix_reset->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) { + reset_auto_mix(); + evt.Skip(); + }); + m_btn_mix_reset->Hide(); + header_sizer->Add(m_btn_mix_reset, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(6)); + + header_sizer->AddStretchSpacer(); + + m_btn_auto_mix = new Button(parent, auto_mix_mode_label(m_auto_mix_mode)); + { + wxFont btn_font = m_btn_auto_mix->GetFont(); + btn_font.SetPointSize(m_auto_mix_font_point_size); + m_btn_auto_mix->SetFont(btn_font); + } + m_btn_auto_mix->SetCornerRadius(FromDIP(14)); + m_btn_auto_mix->SetMinSize(wxSize(FromDIP(178), FromDIP(28))); + { + StateColor btn_bg( + std::pair<wxColour, int>(wxColour("#F4F4F4"), StateColor::Pressed), + std::pair<wxColour, int>(wxColour("#F8F8F8"), StateColor::Hovered), + std::pair<wxColour, int>(*wxWHITE, StateColor::Normal)); + const wxColour btn_bd = wxColour("#CECECE"); + const wxColour btn_text = texture_import_gray9000(); + m_btn_auto_mix->SetBackgroundColor(btn_bg); + m_btn_auto_mix->SetBorderColor(btn_bd); + m_btn_auto_mix->SetTextColor(btn_text); + } + m_btn_auto_mix->SetToolTip(_L("Choose the one-click auto-mix mode for texture color import")); + m_btn_auto_mix->Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& evt) { + show_auto_mix_popup(); + evt.Skip(); + }); + m_btn_auto_mix->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& evt) { + show_auto_mix_popup(); + evt.Skip(); + }); + header_sizer->Add(m_btn_auto_mix, 0, wxALIGN_CENTER_VERTICAL); + + sizer->Add(header_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(4)); + + wxBoxSizer* merge_sizer = new wxBoxSizer(wxHORIZONTAL); + m_auto_merge_cb = new wxCheckBox(parent, wxID_ANY, _L("Auto-merge same filament")); + m_auto_merge_cb->SetToolTip(_L("Automatically merge identical filaments into existing filaments in the project")); + m_auto_merge_cb->SetForegroundColour(secondary_fg); + m_auto_merge_cb->SetValue(true); + m_auto_merge_cb->Bind(wxEVT_CHECKBOX, &TextureImportDialog::on_auto_merge_toggled, this); + merge_sizer->Add(m_auto_merge_cb, 0, wxALIGN_CENTER_VERTICAL); + + sizer->Add(merge_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(8)); + + m_mapping_scroll = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, + wxSize(-1, FromDIP(300))); + m_mapping_scroll->SetScrollRate(0, FromDIP(10)); + m_mapping_scroll->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + m_mapping_scroll->Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this); + + m_mapping_sizer = new wxBoxSizer(wxVERTICAL); + m_mapping_scroll->SetSizer(m_mapping_sizer); + + sizer->Add(m_mapping_scroll, 1, wxEXPAND | wxBOTTOM, FromDIP(8)); +} + +void TextureImportDialog::build_bottom_buttons(wxSizer* sizer) +{ + m_drop_warning_label = new wxStaticText(this, wxID_ANY, + wxString::Format( + _L("The project supports up to %d filaments. Extra filaments will be discarded."), + (int)max_filament_count())); + m_drop_warning_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#FF6F00"))); + m_drop_warning_label->SetFont(texture_import_section_title_font(this)); + m_drop_warning_label->Hide(); + sizer->Add(m_drop_warning_label, 0, wxALIGN_LEFT | wxBOTTOM, FromDIP(4)); + + wxBoxSizer* btn_sizer = new wxBoxSizer(wxHORIZONTAL); + m_btn_skip = new Button(this, _L("Skip Matching")); + m_btn_skip->SetId(ID_BTN_SKIP); + m_btn_skip->SetToolTip(_L("Skip filament mapping and import as a single-color model")); + m_btn_skip->SetCornerRadius(FromDIP(20)); + m_btn_skip->SetMinSize(wxSize(FromDIP(136), FromDIP(40))); + { + StateColor skip_bg( + std::pair<wxColour, int>(wxColour("#CECECE"), StateColor::Pressed), + std::pair<wxColour, int>(wxColour("#EEEEEE"), StateColor::Hovered), + std::pair<wxColour, int>(*wxWHITE, StateColor::Normal)); + const wxColour skip_bd = wxColour("#CECECE"); + const wxColour skip_text = wxColour("#6B6B6A"); + m_btn_skip->SetBackgroundColor(skip_bg); + m_btn_skip->SetBorderColor(skip_bd); + m_btn_skip->SetTextColor(skip_text); + } + + m_btn_ok = new Button(this, _L("Confirm")); + m_btn_ok->SetId(wxID_OK); + m_btn_ok->SetCornerRadius(FromDIP(20)); + m_btn_ok->SetMinSize(wxSize(FromDIP(156), FromDIP(40))); + apply_accent_button_colours(m_btn_ok); + + btn_sizer->AddStretchSpacer(); + btn_sizer->Add(m_btn_skip, 0, wxRIGHT, FromDIP(16)); + btn_sizer->Add(m_btn_ok, 0); + + sizer->Add(btn_sizer, 0, wxEXPAND | wxTOP, FromDIP(8)); +} + +// ---- State machine ---- + +void TextureImportDialog::set_state(TextureImportState new_state) +{ + m_state = new_state; + update_ui_for_state(); +} + +void TextureImportDialog::update_ui_for_state() +{ + bool computing = (m_state == TextureImportState::Computing); + bool ready = (m_state == TextureImportState::Ready); + bool idle = (m_state == TextureImportState::Idle); + bool valid = has_valid_result(); + + m_color_slider->Enable(!computing); + m_color_spin->Enable(!computing); + m_smooth_slider->Enable(!computing); + m_smooth_spin->Enable(!computing); + m_btn_apply->Enable(!computing); + m_btn_color_4->Enable(!computing); + m_btn_color_8->Enable(!computing); + m_btn_color_16->Enable(!computing); + m_btn_color_auto->Enable(!computing); + if (m_btn_auto_mix) + m_btn_auto_mix->Enable(!computing); + if (m_btn_mix_reset) + m_btn_mix_reset->Enable(!computing); + if (computing) + dismiss_auto_mix_popup(); + + m_btn_ok->Enable(ready && valid); + m_btn_skip->Enable(ready || idle); + + m_auto_merge_cb->Enable(!computing); + + m_preview_canvas->set_computing_overlay(computing); + + if (ready && valid) + style_confirm_button(is_params_dirty()); + else if (m_hint_label) + m_hint_label->Hide(); + + m_btn_ok->Refresh(); + Layout(); +} + +// ---- Async computation ---- + +void TextureImportDialog::start_computation(bool auto_color, bool initial) +{ + cancel_computation(); + + m_cancel_flag = false; + m_current_computation_initial = initial; + m_current_computation_auto_color = auto_color; + if (initial) { + m_initial_computation_pending = true; + m_initial_computation_cancelled = false; + m_initial_computation_failed = false; + } + set_state(TextureImportState::Computing); + + bool silent_initial = initial && static_cast<bool>(m_initial_cancel_callback); + if (!silent_initial) { + m_progress_dlg = new ProgressDialog( + _L("Processing"), _L("Computing texture colors..."), + 100, initial ? GetParent() : this, wxPD_APP_MODAL | wxPD_CAN_ABORT | wxPD_AUTO_HIDE); + } + + Slic3r::TexturePaintingSettings settings; + settings.target_colors_num = auto_color ? 0 : (size_t)m_param_color_count; + settings.smooth_weight = m_param_smooth / 10.0; + settings.mesh_repair_decision = m_mesh_repair_decision; + // BBS repairs the mesh through the Windows 3D SDK, which is only available on Windows + // builds that ship the SDK. Orca's CGAL-based repair (MeshBoolean::cgal::repair) works + // on all three platforms, so use that instead. + settings.mesh_repair_callback = [](const indexed_triangle_set& mesh, + indexed_triangle_set& repaired_mesh, + std::function<void(const char*, unsigned)> progress_callback, + std::function<bool()> cancel_callback, + std::string* error_message) -> bool { + if (cancel_callback && cancel_callback()) + return false; + if (progress_callback) + progress_callback(_u8L("Repairing mesh").c_str(), 0); + + TriangleMesh tm(mesh); + if (!MeshBoolean::cgal::repair(tm, nullptr, error_message)) + return false; + + if (cancel_callback && cancel_callback()) + return false; + repaired_mesh = tm.its; + if (progress_callback) + progress_callback(_u8L("Repairing mesh").c_str(), 100); + return true; + }; + + Slic3r::TexturedMesh mesh_copy = m_textured_mesh; + wxEvtHandler* handler = this; + + m_worker = std::make_unique<std::thread>([this, settings, mesh_copy, handler]() { + Slic3r::PaintedMesh result; + + auto progress_cb = [handler](int percent, const char*) { + auto* evt = new wxCommandEvent(EVT_TEXTURE_COMPUTE_PROGRESS); + evt->SetInt(percent); + wxQueueEvent(handler, evt); + }; + + auto cancel_cb = [this]() -> bool { + return m_cancel_flag.load(); + }; + + auto worker_settings = settings; + bool mesh_repair_decision_required = false; + worker_settings.mesh_repair_decision_required = &mesh_repair_decision_required; + bool ok; + if (!mesh_copy.precomputed_face_colors.empty()) { + ok = Slic3r::face_colors_to_painting( + mesh_copy, result, worker_settings, progress_cb, cancel_cb); + } else { + ok = Slic3r::texture_to_painting(mesh_copy, result, worker_settings, progress_cb, cancel_cb); + } + + if (m_cancel_flag.load()) { + wxQueueEvent(handler, new wxCommandEvent(EVT_TEXTURE_COMPUTE_ERROR)); + return; + } + + if (!ok && mesh_repair_decision_required) { + wxQueueEvent(handler, new wxCommandEvent(EVT_TEXTURE_MESH_REPAIR_DECISION)); + return; + } + + { + std::lock_guard<std::mutex> lock(m_result_mutex); + m_pending_result = std::move(result); + } + + if (ok) { + wxQueueEvent(handler, new wxCommandEvent(EVT_TEXTURE_COMPUTE_DONE)); + } else { + wxQueueEvent(handler, new wxCommandEvent(EVT_TEXTURE_COMPUTE_ERROR)); + } + }); +} + +void TextureImportDialog::cancel_computation() +{ + m_cancel_flag = true; + if (m_worker && m_worker->joinable()) + m_worker->join(); + m_worker.reset(); + + if (m_progress_dlg) { + m_progress_dlg->Destroy(); + m_progress_dlg = nullptr; + } + + if (m_current_computation_initial) { + m_initial_computation_cancelled = true; + m_initial_computation_pending = false; + m_current_computation_initial = false; + } +} + +void TextureImportDialog::on_computation_progress(wxCommandEvent& evt) +{ + if (m_progress_dlg) { + if (!m_progress_dlg->Update(evt.GetInt())) + m_cancel_flag = true; + } else if (m_current_computation_initial && m_initial_progress_callback) { + if (!m_initial_progress_callback(evt.GetInt())) + m_cancel_flag = true; + } +} + +void TextureImportDialog::on_computation_complete(wxCommandEvent&) +{ + bool initial = m_current_computation_initial; + + if (m_progress_dlg) { + m_progress_dlg->Destroy(); + m_progress_dlg = nullptr; + } + + { + std::lock_guard<std::mutex> lock(m_result_mutex); + m_painted = std::move(m_pending_result); + } + + int actual_colors = (int)m_painted.cluster_colors.size(); + if (actual_colors >= 2 && actual_colors <= (int)max_filament_count()) { + set_color_count_value(actual_colors, true); + } + + m_preview_canvas->set_painted_mesh_data(m_painted.vertices, m_painted.indices); + m_preview_canvas->set_face_colors(m_painted.face_colors); + + // A fresh texture computation replaces m_painted, so virtual filaments from + // the previous computation must not consume capacity when deciding whether + // this run drops extra colors. Rebuild virtual filaments from this result. + m_current_matches.clear(); + if (m_filament_colors_rgba.size() > m_existing_filament_count) + m_filament_colors_rgba.resize(m_existing_filament_count); + if (m_filament_color_strs.size() > m_existing_filament_count) + m_filament_color_strs.resize(m_existing_filament_count); + if (m_filament_names.size() > m_existing_filament_count) + m_filament_names.resize(m_existing_filament_count); + if (m_filament_entries.size() > m_existing_filament_count) + m_filament_entries.resize(m_existing_filament_count); + while (m_filament_entries.size() < m_existing_filament_count) { + TextureFilamentEntry entry; + entry.kind = TextureFilamentKind::ExistingPhysical; + entry.dialog_index = (int)m_filament_entries.size(); + entry.project_config_index = m_filament_entries.size(); + m_filament_entries.push_back(entry); + } + for (size_t i = 0; i < m_filament_entries.size(); ++i) { + m_filament_entries[i].dialog_index = (int)i; + m_filament_entries[i].color_hex = i < m_filament_color_strs.size() ? + texture_normalize_color_hex(m_filament_color_strs[i]) : "#808080"; + m_filament_entries[i].name = i < m_filament_names.size() ? + m_filament_names[i] : "Filament " + std::to_string(i + 1); + } + m_new_filament_colors.clear(); + m_new_filament_preset_names.clear(); + m_new_mixed_filaments.clear(); + + do_auto_match(); + compact_used_virtual_filaments(); + sort_current_matches_by_filament_index(); + update_filament_color_map(); + rebuild_mapping_rows(); + + m_applied_color_count = m_param_color_count; + m_applied_smooth = m_param_smooth; + + set_state(TextureImportState::Ready); + update_drop_warning_visibility(); + update_auto_mix_reset_visibility(); + + m_btn_view_multicolor->Show(); + if (m_tab_panel) { + m_tab_panel->GetSizer()->Layout(); + m_tab_panel->Fit(); + m_tab_panel->SetPosition(wxPoint(FromDIP(8), FromDIP(8))); + } + GetSizer()->Layout(); + + m_preview_canvas->set_render_mode(TexturePreviewCanvas::RenderMode::FilamentMap); + highlight_view_button(-1); + + if (initial) { + m_initial_computation_pending = false; + m_current_computation_initial = false; + } +} + +void TextureImportDialog::on_computation_error(wxCommandEvent&) +{ + bool initial = m_current_computation_initial; + + if (m_progress_dlg) { + m_progress_dlg->Destroy(); + m_progress_dlg = nullptr; + } + + if (m_cancel_flag.load()) { + if (initial) { + m_initial_computation_cancelled = true; + m_initial_computation_pending = false; + m_current_computation_initial = false; + return; + } + if (has_valid_result()) { + if (m_applied_color_count >= 0) { + m_param_color_count = m_applied_color_count; + m_color_slider->SetValue(m_param_color_count); + m_color_spin->SetValue(m_param_color_count); + update_color_count_preset_buttons(); + } + if (m_applied_smooth >= 0) { + m_param_smooth = m_applied_smooth; + m_smooth_slider->SetValue(m_param_smooth); + m_smooth_spin->SetValue(m_param_smooth); + } + set_state(TextureImportState::Ready); + return; + } + set_state(TextureImportState::Idle); + return; + } + + if (initial) { + m_initial_computation_failed = true; + m_initial_computation_pending = false; + m_current_computation_initial = false; + m_fallback_to_geometry_only = true; + return; + } + + set_state(TextureImportState::Error); + Slic3r::GUI::MessageDialog dlg(initial ? GetParent() : this, + _L("Computation failed. Please adjust parameters and retry."), + _L("Error"), wxOK | wxICON_ERROR); + dlg.ShowModal(); +} + +void TextureImportDialog::on_mesh_repair_decision_required(wxCommandEvent&) +{ + bool initial = m_current_computation_initial; + bool auto_color = m_current_computation_auto_color; + + if (m_progress_dlg) { + m_progress_dlg->Destroy(); + m_progress_dlg = nullptr; + } + +#ifdef HAS_WIN10SDK + Slic3r::GUI::MessageDialog dlg(initial ? GetParent() : this, + _L("The mesh has non-manifold geometry or open boundaries. You can import it as-is or repair it with Windows 3D repair service before importing."), + _L("Mesh repair"), wxYES_NO | wxICON_WARNING | wxYES_DEFAULT); + dlg.SetButtonLabel(wxID_YES, _L("Import without repair")); + dlg.SetButtonLabel(wxID_NO, _L("Repair and import"), true); + // "Repair and import" is the recommended action here, so the accent moves off the default YES + // button onto NO. MsgDialog::add_button already styled both as ButtonType::Choice, so restyling + // with the same type swaps only the palette and leaves the geometry alone. + if (auto* yes_btn = dynamic_cast<Button*>(dlg.FindWindow(wxID_YES))) { + yes_btn->SetStyle(ButtonStyle::Regular, ButtonType::Choice); + yes_btn->SetMinSize(wxSize(FromDIP(180), FromDIP(24))); + } + if (auto* no_btn = dynamic_cast<Button*>(dlg.FindWindow(wxID_NO))) { + no_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Choice); + no_btn->SetMinSize(wxSize(FromDIP(160), FromDIP(24))); + } + dlg.Layout(); + dlg.Fit(); + dlg.CenterOnParent(); + int ret = dlg.ShowModal(); + m_mesh_repair_decision = (ret == wxID_NO) + ? Slic3r::TexturePaintingSettings::MeshRepairDecision::RepairAndImport + : Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair; +#else + Slic3r::GUI::MessageDialog dlg(initial ? GetParent() : this, + _L("Please note that the mesh has non-manifold geometry or open boundaries."), + _L("Mesh issue"), wxOK | wxCANCEL | wxICON_WARNING | wxOK_DEFAULT); + dlg.SetButtonLabel(wxID_OK, _L("Continue"), true); + dlg.SetButtonLabel(wxID_CANCEL, _L("Cancel")); + int ret = dlg.ShowModal(); + if (ret != wxID_OK) { + m_cancel_flag = true; + if (initial) { + m_initial_computation_cancelled = true; + m_initial_computation_pending = false; + m_current_computation_initial = false; + } else if (has_valid_result()) { + set_state(TextureImportState::Ready); + } else { + set_state(TextureImportState::Idle); + } + return; + } + m_mesh_repair_decision = Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair; +#endif + + start_computation(auto_color, initial); +} + +// ---- Mapping ---- + +void TextureImportDialog::update_filament_color_map() +{ + std::map<std::array<std::size_t, 3>, std::array<float, 3>> color_map; + for (const auto& m : m_current_matches) { + if (m.filament_index >= 0 && m.filament_index < (int)m_filament_colors_rgba.size()) { + color_map[m.cluster_color] = { + m_filament_colors_rgba[m.filament_index][0], + m_filament_colors_rgba[m.filament_index][1], + m_filament_colors_rgba[m.filament_index][2] + }; + } + } + m_preview_canvas->set_filament_color_map(color_map); +} + +// Canonical ordering used on the very first display after a computation: +// sort ascending by filament_index, and push unmapped (filament_index < 0) +// entries to the end. This gives the user a stable, predictable mapping +// layout regardless of the cluster discovery order. +void TextureImportDialog::sort_current_matches_by_filament_index() +{ + std::stable_sort(m_current_matches.begin(), m_current_matches.end(), + [](const auto& lhs, const auto& rhs) { + const bool lhs_valid = lhs.filament_index >= 0; + const bool rhs_valid = rhs.filament_index >= 0; + + if (lhs_valid != rhs_valid) + return lhs_valid; + if (!lhs_valid) + return false; + + return lhs.filament_index < rhs.filament_index; + }); +} + +// Preserve the row order the user is currently looking at across a +// re-computation (e.g. when auto-merge is toggled). We key on cluster_index +// because it survives compact_used_virtual_filaments() and filament-index +// renumbering, whereas filament_index does not. +// +// Behaviour: +// * Entries whose cluster_index appeared in `previous_matches` keep their +// previous relative order. +// * Entries whose cluster_index is new (not in `previous_matches`) are +// appended at the end, in their current relative order. +// +// Assumption: each cluster_index appears at most once in both vectors. This +// is currently guaranteed by do_auto_match(), which emits exactly one match +// per cluster. If that invariant ever changes, the std::map::emplace below +// silently keeps only the first occurrence and the order will be wrong. +void TextureImportDialog::restore_current_match_order(const std::vector<Slic3r::FilamentMatch>& previous_matches) +{ + if (previous_matches.empty() || m_current_matches.size() < 2) + return; + + std::map<int, size_t> previous_order_by_cluster; + for (size_t i = 0; i < previous_matches.size(); ++i) { + if (previous_matches[i].cluster_index >= 0) + previous_order_by_cluster.emplace(previous_matches[i].cluster_index, i); + } + + std::stable_sort(m_current_matches.begin(), m_current_matches.end(), + [&previous_order_by_cluster](const auto& lhs, const auto& rhs) { + const auto lhs_it = previous_order_by_cluster.find(lhs.cluster_index); + const auto rhs_it = previous_order_by_cluster.find(rhs.cluster_index); + const bool lhs_known = lhs_it != previous_order_by_cluster.end(); + const bool rhs_known = rhs_it != previous_order_by_cluster.end(); + + if (lhs_known != rhs_known) + return lhs_known; + if (!lhs_known) + return false; + + return lhs_it->second < rhs_it->second; + }); +} + +size_t TextureImportDialog::max_filament_count() const +{ + return static_cast<size_t>(EnforcerBlockerType::ExtruderMax); +} + +bool TextureImportDialog::can_add_virtual_filament() const +{ + return m_filament_colors_rgba.size() < max_filament_count(); +} + +int TextureImportDialog::find_closest_filament_index(const std::array<std::size_t, 3>& color) const +{ + int best_idx = -1; + double best_delta = std::numeric_limits<double>::max(); + const size_t filament_count = std::min(m_filament_colors_rgba.size(), max_filament_count()); + for (size_t i = 0; i < filament_count; ++i) { + const double delta = Slic3r::compute_delta_e(color, m_filament_colors_rgba[i]); + if (delta < best_delta) { + best_delta = delta; + best_idx = (int)i; + } + } + return best_idx; +} + +int TextureImportDialog::add_virtual_filament(const std::array<float, 4>& rgba, const std::string& hex, + const std::string& preset_name) +{ + if (m_filament_color_strs.size() != m_filament_colors_rgba.size() || + m_filament_names.size() != m_filament_colors_rgba.size() || + m_filament_entries.size() != m_filament_colors_rgba.size()) { + return -1; + } + if (!can_add_virtual_filament()) { + // Mark that this do_auto_match() run hit the filament cap and had to + // drop at least one cluster. The mapping itself still falls back via + // find_closest_filament_index() below; this flag only drives the + // inline orange warning above the bottom buttons. + // Note: only the false -> true transition happens here; the flag is + // cleared exclusively at the entry of do_auto_match() so it always + // reflects the most recent match, never an accumulated history. + m_filaments_dropped = true; + return -1; + } + + const int new_idx = (int)m_filament_colors_rgba.size(); + m_filament_colors_rgba.push_back(rgba); + m_filament_color_strs.push_back(hex); + m_filament_names.push_back(DEFAULT_VIRTUAL_FILAMENT_NAME); + TextureFilamentEntry entry; + entry.kind = TextureFilamentKind::NewPhysical; + entry.dialog_index = new_idx; + entry.project_config_index = size_t(-1); + entry.color_hex = texture_normalize_color_hex(hex); + entry.name = DEFAULT_VIRTUAL_FILAMENT_NAME; + entry.preset_name = preset_name.empty() ? m_default_virtual_filament_preset_name : preset_name; + m_filament_entries.push_back(entry); + m_new_filament_colors.push_back(rgba); + m_new_filament_preset_names.push_back(preset_name.empty() ? m_default_virtual_filament_preset_name : preset_name); + return new_idx; +} + +int TextureImportDialog::add_virtual_mixed_filament(const std::string& color_hex, + const std::vector<int>& component_dialog_indices, + const std::vector<int>& ratios) +{ + if (m_filament_color_strs.size() != m_filament_colors_rgba.size() || + m_filament_names.size() != m_filament_colors_rgba.size() || + m_filament_entries.size() != m_filament_colors_rgba.size()) { + return -1; + } + if (component_dialog_indices.size() < 2 || component_dialog_indices.size() != ratios.size()) + return -1; + for (int idx : component_dialog_indices) { + if (idx < 0 || idx >= (int)m_filament_entries.size() || + !texture_entry_is_physical(m_filament_entries[idx].kind)) { + return -1; + } + } + if (!can_add_virtual_filament()) { + m_filaments_dropped = true; + return -1; + } + + const int new_idx = (int)m_filament_colors_rgba.size(); + TextureFilamentEntry entry; + entry.kind = TextureFilamentKind::NewMixed; + entry.dialog_index = new_idx; + entry.project_config_index = size_t(-1); + entry.color_hex = texture_normalize_color_hex(color_hex); + entry.name = DEFAULT_VIRTUAL_FILAMENT_NAME; + entry.mixed_ratios = ratios; + for (int idx : component_dialog_indices) + entry.mixed_components.push_back((unsigned int)(idx + 1)); + + TextureNewMixedFilament mixed; + mixed.dialog_index = entry.dialog_index; + mixed.color_hex = entry.color_hex; + mixed.component_dialog_indices = component_dialog_indices; + mixed.ratios = ratios; + + m_filament_entries.push_back(entry); + m_filament_color_strs.push_back(entry.color_hex); + m_filament_names.push_back(entry.name); + m_filament_colors_rgba.push_back(parse_color_string(entry.color_hex)); + m_new_mixed_filaments.push_back(mixed); + return entry.dialog_index; +} + +void TextureImportDialog::compact_used_virtual_filaments() +{ + if (m_current_matches.empty()) + return; + + const std::vector<std::array<float, 4>> old_colors = m_filament_colors_rgba; + const std::vector<std::string> old_color_strs = m_filament_color_strs; + const std::vector<std::string> old_names = m_filament_names; + const std::vector<TextureFilamentEntry> old_entries = m_filament_entries; + + auto old_new_mixed_has_valid_components = [&old_entries, &old_colors](const TextureFilamentEntry& entry) { + if (entry.kind != TextureFilamentKind::NewMixed) + return true; + if (entry.mixed_components.size() < 2 || entry.mixed_components.size() != entry.mixed_ratios.size()) + return false; + for (unsigned int comp : entry.mixed_components) { + int comp_idx = comp >= 1 ? (int)comp - 1 : -1; + if (comp_idx < 0 || comp_idx >= (int)old_entries.size() || comp_idx >= (int)old_colors.size() || + !texture_entry_is_physical(old_entries[comp_idx].kind)) { + return false; + } + } + return true; + }; + + std::set<int> used_virtual_indices; + for (const auto& m : m_current_matches) { + if (m.filament_index >= (int)m_existing_filament_count && + m.filament_index < (int)old_colors.size()) { + if (m.filament_index < (int)old_entries.size() && + old_entries[m.filament_index].kind == TextureFilamentKind::NewMixed && + !old_new_mixed_has_valid_components(old_entries[m.filament_index])) { + continue; + } + used_virtual_indices.insert(m.filament_index); + } + } + bool added_dependency = true; + while (added_dependency) { + added_dependency = false; + std::vector<int> current_used(used_virtual_indices.begin(), used_virtual_indices.end()); + for (int used_idx : current_used) { + if (used_idx < 0 || used_idx >= (int)old_entries.size() || + old_entries[used_idx].kind != TextureFilamentKind::NewMixed || + !old_new_mixed_has_valid_components(old_entries[used_idx])) + continue; + for (unsigned int comp : old_entries[used_idx].mixed_components) { + int comp_idx = comp >= 1 ? (int)comp - 1 : -1; + if (comp_idx >= (int)m_existing_filament_count && comp_idx < (int)old_entries.size() && + used_virtual_indices.insert(comp_idx).second) { + added_dependency = true; + } + } + } + } + + std::vector<std::array<float, 4>> compact_colors; + std::vector<std::string> compact_color_strs; + std::vector<std::string> compact_names; + std::vector<TextureFilamentEntry> compact_entries; + compact_colors.reserve(m_existing_filament_count + used_virtual_indices.size()); + compact_color_strs.reserve(m_existing_filament_count + used_virtual_indices.size()); + compact_names.reserve(m_existing_filament_count + used_virtual_indices.size()); + compact_entries.reserve(m_existing_filament_count + used_virtual_indices.size()); + + const size_t existing_count = std::min(m_existing_filament_count, old_colors.size()); + for (size_t i = 0; i < existing_count; ++i) { + compact_colors.push_back(old_colors[i]); + compact_color_strs.push_back(i < old_color_strs.size() ? old_color_strs[i] : ""); + compact_names.push_back(i < old_names.size() ? old_names[i] : "Filament " + std::to_string(i + 1)); + TextureFilamentEntry entry = i < old_entries.size() ? old_entries[i] : TextureFilamentEntry{}; + entry.dialog_index = (int)i; + entry.color_hex = texture_normalize_color_hex(compact_color_strs.back()); + entry.name = compact_names.back(); + compact_entries.push_back(entry); + } + + std::map<int, int> old_to_new; + std::vector<std::array<float, 4>> compact_new_colors; + std::vector<std::string> compact_new_preset_names; + compact_new_colors.reserve(used_virtual_indices.size()); + compact_new_preset_names.reserve(used_virtual_indices.size()); + + for (int old_idx : used_virtual_indices) { + old_to_new[old_idx] = (int)compact_colors.size(); + compact_colors.push_back(old_colors[old_idx]); + compact_color_strs.push_back(old_idx < (int)old_color_strs.size() ? old_color_strs[old_idx] : ""); + compact_names.push_back(old_idx < (int)old_names.size() ? old_names[old_idx] : DEFAULT_VIRTUAL_FILAMENT_NAME); + TextureFilamentEntry entry = old_idx < (int)old_entries.size() ? old_entries[old_idx] : TextureFilamentEntry{}; + entry.dialog_index = (int)compact_entries.size(); + entry.color_hex = texture_normalize_color_hex(compact_color_strs.back()); + entry.name = compact_names.back(); + if (entry.kind == TextureFilamentKind::NewPhysical) { + compact_new_colors.push_back(old_colors[old_idx]); + compact_new_preset_names.push_back(entry.preset_name.empty() ? m_default_virtual_filament_preset_name : entry.preset_name); + } + compact_entries.push_back(entry); + } + + m_filament_colors_rgba = std::move(compact_colors); + m_filament_color_strs = std::move(compact_color_strs); + m_filament_names = std::move(compact_names); + m_filament_entries = std::move(compact_entries); + m_new_filament_colors = std::move(compact_new_colors); + m_new_filament_preset_names = std::move(compact_new_preset_names); + m_new_mixed_filaments.clear(); + std::set<int> invalid_compacted_mixed_indices; + for (auto& entry : m_filament_entries) { + if (entry.kind != TextureFilamentKind::NewMixed) + continue; + TextureNewMixedFilament mixed; + mixed.dialog_index = entry.dialog_index; + mixed.color_hex = entry.color_hex; + mixed.ratios = entry.mixed_ratios; + mixed.component_dialog_indices.reserve(entry.mixed_components.size()); + bool valid_components = entry.mixed_components.size() >= 2 && + entry.mixed_components.size() == entry.mixed_ratios.size(); + for (unsigned int comp : entry.mixed_components) { + int old_comp_idx = comp >= 1 ? (int)comp - 1 : -1; + if (old_comp_idx < 0) { + valid_components = false; + break; + } + auto remap_it = old_to_new.find(old_comp_idx); + int new_comp_idx = remap_it != old_to_new.end() ? remap_it->second : old_comp_idx; + if (new_comp_idx < 0 || new_comp_idx >= (int)m_filament_entries.size() || + !texture_entry_is_physical(m_filament_entries[new_comp_idx].kind)) { + valid_components = false; + break; + } + mixed.component_dialog_indices.push_back(new_comp_idx); + } + if (!valid_components) { + invalid_compacted_mixed_indices.insert(entry.dialog_index); + continue; + } + entry.mixed_components.clear(); + for (int comp_idx : mixed.component_dialog_indices) + entry.mixed_components.push_back((unsigned int)(comp_idx + 1)); + m_new_mixed_filaments.push_back(mixed); + } + + auto find_closest_physical_filament_index = [this](const std::array<std::size_t, 3>& color) { + int best_idx = -1; + double best_delta = std::numeric_limits<double>::max(); + const size_t filament_count = std::min(m_filament_colors_rgba.size(), max_filament_count()); + for (size_t i = 0; i < filament_count && i < m_filament_entries.size(); ++i) { + if (!texture_entry_is_physical(m_filament_entries[i].kind)) + continue; + const double delta = Slic3r::compute_delta_e(color, m_filament_colors_rgba[i]); + if (delta < best_delta) { + best_delta = delta; + best_idx = (int)i; + } + } + return best_idx; + }; + + for (auto& m : m_current_matches) { + auto it = old_to_new.find(m.filament_index); + if (it != old_to_new.end()) { + m.filament_index = it->second; + } else if (m.filament_index >= (int)m_existing_filament_count) { + m.filament_index = find_closest_filament_index(m.cluster_color); + } + if (invalid_compacted_mixed_indices.count(m.filament_index) > 0) { + int fallback_idx = find_closest_physical_filament_index(m.cluster_color); + m.filament_index = fallback_idx >= 0 ? fallback_idx : find_closest_filament_index(m.cluster_color); + } + + if (m.filament_index >= 0 && m.filament_index < (int)m_filament_colors_rgba.size()) { + m.filament_color = m_filament_colors_rgba[m.filament_index]; + m.delta_e = Slic3r::compute_delta_e(m.cluster_color, m.filament_color); + if (m.filament_index >= (int)m_existing_filament_count) + m.delta_e = 0.0; + } + } +} + +std::vector<int> TextureImportDialog::compute_display_numbers() const +{ + // Assigns each entry a 1-based display number in the order the sidebar will + // show after apply: ExistingPhysical, NewPhysical, ExistingMixed, NewMixed. + // This keeps the dialog's visible IDs in sync with the post-apply sidebar, + // instead of the raw dialog_index (which interleaves physicals and mixeds + // by processing order and causes e.g. CMYW to show 4,5,6,8 instead of 3,4,5,6). + // MUST mirror ordering in apply_textured_mesh_import_result (Plater.cpp:9896): + // - ExistingPhysical keeps its project_config_index + // - NewPhysical is inserted at existing_physical_count + new_order + // - ExistingMixed shifts to project_config_index + new_physical_count + // - NewMixed is appended after all existing mixeds + std::vector<int> result(m_filament_entries.size(), 0); + int next = 1; + + auto assign_group = [&](TextureFilamentKind kind, bool by_project_config_index) { + if (by_project_config_index) { + std::vector<const TextureFilamentEntry*> group; + for (const auto& e : m_filament_entries) + if (e.kind == kind) + group.push_back(&e); + std::sort(group.begin(), group.end(), + [](const TextureFilamentEntry* a, const TextureFilamentEntry* b) { + return a->project_config_index < b->project_config_index; + }); + for (const auto* e : group) { + if (e->dialog_index >= 0 && e->dialog_index < (int)result.size()) + result[e->dialog_index] = next; + ++next; + } + } else { + for (const auto& e : m_filament_entries) { + if (e.kind != kind) + continue; + if (e.dialog_index >= 0 && e.dialog_index < (int)result.size()) + result[e.dialog_index] = next; + ++next; + } + } + }; + + assign_group(TextureFilamentKind::ExistingPhysical, true); + assign_group(TextureFilamentKind::NewPhysical, false); + assign_group(TextureFilamentKind::ExistingMixed, true); + assign_group(TextureFilamentKind::NewMixed, false); + return result; +} + +void TextureImportDialog::dismiss_filament_popup() +{ + if (!m_filament_popup) { + m_filament_popup_row = -1; + return; + } + + FilamentSelectPopup* popup = m_filament_popup; + m_filament_popup = nullptr; + m_filament_popup_row = -1; + if (popup->IsShown()) + popup->Dismiss(); + else + popup->Destroy(); +} + +void TextureImportDialog::show_auto_mix_popup() +{ + if (!m_btn_auto_mix || !m_btn_auto_mix->IsEnabled()) + return; + + if (m_auto_mix_popup && m_auto_mix_popup->IsShown()) + return; + dismiss_auto_mix_popup(); + + auto on_select = [this](TextureAutoMixMode mode) { + set_auto_mix_mode(mode); + }; + auto on_close = [this]() { + m_auto_mix_popup = nullptr; + }; + + auto* popup = new AutoMixSelectPopup(this, m_auto_mix_mode, m_btn_auto_mix->GetSize().x, + m_auto_mix_font_point_size, + on_select, on_close); + wxPoint pos = m_btn_auto_mix->ClientToScreen(wxPoint(0, m_btn_auto_mix->GetSize().y)); + wxRect display_rect; + int display_idx = wxDisplay::GetFromPoint(pos); + if (display_idx != wxNOT_FOUND) + display_rect = wxDisplay(display_idx).GetClientArea(); + else + display_rect = wxDisplay().GetClientArea(); + pos.x = std::clamp(pos.x, display_rect.GetLeft(), + std::max(display_rect.GetLeft(), display_rect.GetRight() - popup->GetSize().x)); + popup->Position(pos, wxSize(0, 0)); + popup->Bind(wxEVT_DESTROY, [this, popup](wxWindowDestroyEvent& e) { + e.Skip(); + if (m_auto_mix_popup == popup) + m_auto_mix_popup = nullptr; + }); + m_auto_mix_popup = popup; + popup->Popup(); +} + +void TextureImportDialog::dismiss_auto_mix_popup() +{ + if (!m_auto_mix_popup) + return; + + AutoMixSelectPopup* popup = m_auto_mix_popup; + m_auto_mix_popup = nullptr; + if (popup->IsShown()) + popup->Dismiss(); + else + popup->Destroy(); +} + +void TextureImportDialog::set_auto_mix_mode(TextureAutoMixMode mode) +{ + m_auto_mix_mode = mode; + if (m_btn_auto_mix) { + m_btn_auto_mix->SetLabel(auto_mix_mode_label(mode)); + m_btn_auto_mix->Refresh(); + } + apply_auto_standard_mix(mode); +} + +void TextureImportDialog::apply_auto_standard_mix(TextureAutoMixMode mode) +{ + if (m_mapping_rows.empty()) + return; + m_filaments_dropped = false; + + auto find_or_add_base_physical = [this](const std::string& color_hex) -> int { + const std::string normalized = texture_normalize_color_hex(color_hex); + for (const auto& entry : m_filament_entries) { + if (!texture_entry_is_physical(entry.kind)) + continue; + if (texture_normalize_color_hex(entry.color_hex) != normalized) + continue; + if (texture_entry_is_pla_basic(entry)) + return entry.dialog_index; + } + + std::array<float, 4> rgba = parse_color_string(normalized); + int idx = add_virtual_filament(rgba, normalized, m_default_virtual_filament_preset_name); + if (idx >= 0 && idx < (int)m_filament_entries.size()) { + m_filament_entries[idx].type = DEFAULT_VIRTUAL_FILAMENT_BASIC_TYPE; + m_filament_entries[idx].name = DEFAULT_VIRTUAL_FILAMENT_NAME; + } + return idx; + }; + + auto find_existing_mixed = [this](const std::vector<int>& component_indices, const std::vector<int>& ratios) -> int { + for (const auto& entry : m_filament_entries) { + if (!texture_entry_is_mixed(entry.kind) || entry.mixed_components.size() != component_indices.size() || + entry.mixed_ratios.size() != ratios.size()) + continue; + bool same = true; + for (size_t i = 0; i < component_indices.size(); ++i) { + if (entry.mixed_components[i] != (unsigned int)(component_indices[i] + 1) || + entry.mixed_ratios[i] != ratios[i]) { + same = false; + break; + } + } + if (same) + return entry.dialog_index; + } + return -1; + }; + + bool changed = false; + const auto recipe_mode = texture_recipe_mode(mode); + for (size_t row_index = 0; row_index < m_mapping_rows.size(); ++row_index) { + Slic3r::ColorDecomposeRgb target_rgb; + if (!Slic3r::color_decompose_hex_to_rgb(m_mapping_rows[row_index].source_hex, target_rgb)) + continue; + + auto recipe = Slic3r::lookup_standard_recipe(target_rgb, recipe_mode, DEFAULT_VIRTUAL_FILAMENT_BASIC_TYPE); + if (!recipe.valid || recipe.components.size() < 2) + continue; + + std::vector<int> component_dialog_indices; + std::vector<int> ratios; + for (const auto& comp : recipe.components) { + int component_idx = find_or_add_base_physical(comp.color_hex); + if (component_idx < 0) { + component_dialog_indices.clear(); + break; + } + component_dialog_indices.push_back(component_idx); + ratios.push_back(comp.ratio); + } + if (component_dialog_indices.size() < 2 || component_dialog_indices.size() != ratios.size()) + continue; + + int mixed_idx = find_existing_mixed(component_dialog_indices, ratios); + if (mixed_idx < 0) + mixed_idx = add_virtual_mixed_filament(recipe.matched_color_hex, component_dialog_indices, ratios); + if (mixed_idx < 0) + continue; + + m_mapping_rows[row_index].target_filament_idx = mixed_idx; + if (row_index < m_current_matches.size()) { + m_current_matches[row_index].filament_index = mixed_idx; + m_current_matches[row_index].filament_color = m_filament_colors_rgba[mixed_idx]; + m_current_matches[row_index].delta_e = Slic3r::compute_delta_e( + m_current_matches[row_index].cluster_color, m_current_matches[row_index].filament_color); + if (mixed_idx >= (int)m_existing_filament_count) + m_current_matches[row_index].delta_e = 0.0; + } + changed = true; + } + + if (!changed) + return; + + m_auto_mix_applied = true; + compact_used_virtual_filaments(); + update_filament_color_map(); + rebuild_mapping_rows(); + update_drop_warning_visibility(); + update_auto_mix_reset_visibility(); +} + +void TextureImportDialog::reset_auto_mix() +{ + if (m_state != TextureImportState::Ready || !m_auto_mix_applied) + return; + + dismiss_auto_mix_popup(); + + // Clear mixed filament references so the compact inside do_auto_match() + // removes them (and their exclusively-owned base physicals) from the + // filament arrays, giving the baseline matching a clean starting state. + for (auto& m : m_current_matches) { + if (m.filament_index >= 0 && m.filament_index < (int)m_filament_entries.size() && + texture_entry_is_mixed(m_filament_entries[m.filament_index].kind)) { + m.filament_index = -1; + } + } + + // Re-run the baseline auto-match (same flow as the auto-merge toggle) so the + // mapping reverts to the pre-mix state: every colour matches an existing + // physical filament or a virtual physical filament, with no mixed filaments. + const auto previous_matches = m_current_matches; + do_auto_match(); + restore_current_match_order(previous_matches); + compact_used_virtual_filaments(); + update_filament_color_map(); + rebuild_mapping_rows(); + update_drop_warning_visibility(); + update_auto_mix_reset_visibility(); +} + +void TextureImportDialog::update_auto_mix_reset_visibility() +{ + if (!m_btn_mix_reset) + return; + if (m_btn_mix_reset->Show(m_auto_mix_applied)) { + if (wxWindow* parent = m_btn_mix_reset->GetParent()) + parent->Layout(); + } +} + +bool TextureImportDialog::add_decomposed_mixed_filament(size_t row_index) +{ + if (row_index >= m_mapping_rows.size()) + return false; + + std::vector<std::string> physical_colors; + std::vector<std::string> physical_names; + std::vector<std::string> physical_types; + std::vector<int> physical_dialog_indices; + std::vector<size_t> physical_config_indices; + auto& preset_bundle = *wxGetApp().preset_bundle; + for (const auto& entry : m_filament_entries) { + if (entry.kind != TextureFilamentKind::ExistingPhysical) + continue; + physical_colors.push_back(entry.color_hex); + physical_names.push_back(entry.name); + const size_t cfg_idx = entry.project_config_index; + Preset* preset = nullptr; + if (cfg_idx < preset_bundle.filament_presets.size()) + preset = preset_bundle.filaments.find_preset(preset_bundle.filament_presets[cfg_idx]); + physical_types.push_back(filament_type_for_color_decompose(preset)); + physical_dialog_indices.push_back(entry.dialog_index); + physical_config_indices.push_back(cfg_idx); + } + if (physical_colors.empty()) + return false; + + wxColour target(m_mapping_rows[row_index].source_hex); + ColorDecomposeDialog dlg(this, -1, target, physical_colors, physical_names, physical_types, + m_filament_entries.size(), max_filament_count(), + std::move(physical_config_indices)); + // Count "new physical filaments" with the exact reuse rule of the write-back + // loop below: a base color is only new if no existing OR virtual official + // Bambu Basic filament already carries that color. This keeps the dialog's + // filament-limit pre-check consistent with what add_decomposed_mixed_filament + // will actually create, so already-present virtual base colors are not + // double counted (which previously could wrongly disable OK). + dlg.set_missing_physical_calculator([this](const ColorDecomposeResult& result) -> size_t { + size_t missing = 0; + for (const DecomposeComponent& comp : result.components) { + if (comp.filament_index > 0) + continue; // reuses a physical slot passed to the dialog, no new filament + const std::string comp_hex = texture_normalize_color_hex( + comp.colour.GetAsString(wxC2S_HTML_SYNTAX).ToStdString()); + bool found = false; + for (const auto& entry : m_filament_entries) { + if (!texture_entry_is_physical(entry.kind)) + continue; + if (texture_normalize_color_hex(entry.color_hex) != comp_hex) + continue; + if (texture_entry_official_basic(entry)) { + found = true; + break; + } + } + if (!found) + ++missing; + } + return missing; + }); + if (dlg.ShowModal() != wxID_OK) + return false; + + ColorDecomposeResult result = dlg.get_result(); + std::vector<int> component_dialog_indices; + std::vector<int> ratios; + for (const DecomposeComponent& comp : result.components) { + ratios.push_back(comp.ratio); + if (comp.filament_index > 0) { + const size_t physical_idx = (size_t)(comp.filament_index - 1); + if (physical_idx >= physical_dialog_indices.size()) + return false; + component_dialog_indices.push_back(physical_dialog_indices[physical_idx]); + continue; + } + + const std::string comp_hex = texture_normalize_color_hex(comp.colour.GetAsString(wxC2S_HTML_SYNTAX).ToStdString()); + int existing_idx = -1; + for (const auto& entry : m_filament_entries) { + if (!texture_entry_is_physical(entry.kind)) + continue; + if (texture_normalize_color_hex(entry.color_hex) != comp_hex) + continue; + if (texture_entry_official_basic(entry)) { + existing_idx = entry.dialog_index; + break; + } + } + if (existing_idx < 0) { + std::array<float, 4> rgba = parse_color_string(comp_hex); + existing_idx = add_virtual_filament(rgba, comp_hex); + if (existing_idx < 0) + return false; + } + component_dialog_indices.push_back(existing_idx); + } + + if (component_dialog_indices.size() < 2 || component_dialog_indices.size() != ratios.size()) + return false; + + const std::string mixed_hex = texture_normalize_color_hex( + result.matched_color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString()); + int mixed_idx = add_virtual_mixed_filament(mixed_hex, component_dialog_indices, ratios); + if (mixed_idx < 0) + return false; + + m_mapping_rows[row_index].target_filament_idx = mixed_idx; + if (row_index < m_current_matches.size()) + m_current_matches[row_index].filament_index = mixed_idx; + rebuild_mapping_rows(); + update_filament_color_map(); + return true; +} + +void TextureImportDialog::dismiss_filament_popup_on_wheel(wxMouseEvent& evt) +{ + dismiss_filament_popup(); + dismiss_auto_mix_popup(); + evt.Skip(); +} + +void TextureImportDialog::show_filament_popup(size_t row_index) +{ + if (row_index >= m_mapping_rows.size()) return; + + if (m_skip_next_filament_popup_row == (int)row_index) { + m_skip_next_filament_popup_row = -1; + return; + } + + if (m_filament_popup && m_filament_popup->IsShown()) { + if (m_filament_popup_row == (int)row_index) { + dismiss_filament_popup(); + return; + } + dismiss_filament_popup(); + } + + const auto display_numbers = compute_display_numbers(); + auto display_number = [display_numbers](int idx) -> int { + return (idx >= 0 && idx < (int)display_numbers.size() && display_numbers[idx] > 0) + ? display_numbers[idx] : idx + 1; + }; + + auto on_select = [this, row_index, display_number](int idx) { + if (row_index >= m_mapping_rows.size()) return; + m_mapping_rows[row_index].target_filament_idx = idx; + if (row_index < m_current_matches.size()) + m_current_matches[row_index].filament_index = idx; + if (m_mapping_rows[row_index].target_panel) { + wxString label = (idx >= 0 && idx < (int)m_filament_names.size()) + ? filament_name_to_wx_string(m_filament_names[idx]) + : wxString::Format("Filament %d", display_number(idx)); + m_mapping_rows[row_index].target_panel->SetToolTip(label); + m_mapping_rows[row_index].target_panel->Refresh(); + } + update_filament_color_map(); + }; + + auto on_add_filament = [this, row_index](wxColour clr) { + std::array<float, 4> rgba = {clr.Red() / 255.f, clr.Green() / 255.f, + clr.Blue() / 255.f, 1.0f}; + std::string hex = wxString::Format("#%02X%02X%02X", + clr.Red(), clr.Green(), clr.Blue()).ToStdString(); + int new_idx = add_virtual_filament(rgba, hex); + if (new_idx < 0) + return; + + if (row_index < m_mapping_rows.size()) { + m_mapping_rows[row_index].target_filament_idx = new_idx; + if (row_index < m_current_matches.size()) + m_current_matches[row_index].filament_index = new_idx; + } + rebuild_mapping_rows(); + update_filament_color_map(); + }; + + auto on_decompose_color = [this, row_index]() { + CallAfter([this, row_index]() { + add_decomposed_mixed_filament(row_index); + }); + }; + + wxPanel* tp = m_mapping_rows[row_index].target_panel; + if (!tp) return; + + auto on_close = [this, row_index](bool closed_by_action) { + if (m_filament_popup_row == (int)row_index) { + m_filament_popup = nullptr; + m_filament_popup_row = -1; + } + if (!closed_by_action) { + m_skip_next_filament_popup_row = (int)row_index; + CallAfter([this, row_index]() { + if (m_skip_next_filament_popup_row == (int)row_index) + m_skip_next_filament_popup_row = -1; + }); + } + }; + + auto* popup = new FilamentSelectPopup( + this, m_filament_entries, m_filament_colors_rgba, m_filament_names, + m_existing_filament_count, tp->GetSize().x, tp, on_select, on_add_filament, + on_decompose_color, + [this]() { return can_add_virtual_filament(); }, + on_close, + display_numbers); + + wxPoint pos = tp->ClientToScreen(wxPoint(0, tp->GetSize().y)); + wxRect display_rect; + int display_idx = wxDisplay::GetFromPoint(pos); + if (display_idx != wxNOT_FOUND) + display_rect = wxDisplay(display_idx).GetClientArea(); + else + display_rect = wxDisplay().GetClientArea(); + pos.x = std::clamp(pos.x, display_rect.GetLeft(), + std::max(display_rect.GetLeft(), display_rect.GetRight() - popup->GetSize().x)); + popup->Position(pos, wxSize(0, 0)); + popup->Bind(wxEVT_DESTROY, [this, popup](wxWindowDestroyEvent& e) { + e.Skip(); + if (m_filament_popup == popup) { + m_filament_popup = nullptr; + m_filament_popup_row = -1; + } + }); + m_filament_popup = popup; + m_filament_popup_row = (int)row_index; + popup->Popup(); +} + +void TextureImportDialog::do_auto_match() +{ + if (m_painted.cluster_colors.empty()) return; + + // do_auto_match() always rebuilds the baseline mapping without any mixed + // filaments, so it is the common entry for every "revert one-click mix" + // path. Clear the applied flag here; callers refresh the reset button. + m_auto_mix_applied = false; + + // Reset the "filaments were dropped" flag at the start of every run, so it + // strictly reflects what happens during *this* match (no historical + // accumulation). add_virtual_filament() will flip it back to true if and + // only if it hits the global filament cap below. + m_filaments_dropped = false; + + // Drop any virtual filaments left over from previous match runs that the + // current m_current_matches no longer references. Without this, the + // residual virtual filaments inflate m_filament_colors_rgba.size() at the + // entry of this match, which can make add_virtual_filament() fail (and + // wrongly flip m_filaments_dropped to true) even when the *real* count + // of needed virtual filaments for this run is well below the global cap. + // This is purely a state cleanup; it does not change any mapping rule. + compact_used_virtual_filaments(); + + const auto previous_matches = m_current_matches; + + std::map<std::array<std::size_t, 3>, int> previous_virtual_by_cluster; + for (const auto& match : previous_matches) { + if (match.filament_index >= (int)m_existing_filament_count && + match.filament_index < (int)m_filament_entries.size() && + texture_entry_is_physical(m_filament_entries[match.filament_index].kind)) { + previous_virtual_by_cluster[match.cluster_color] = match.filament_index; + } + } + + auto find_virtual_filament_by_color = [this](const std::array<std::size_t, 3>& color) -> int { + std::string hex = rgb_to_hex(color).ToStdString(); + for (size_t i = m_existing_filament_count; i < m_filament_color_strs.size(); ++i) { + if (m_filament_color_strs[i] == hex && + i < m_filament_entries.size() && texture_entry_is_physical(m_filament_entries[i].kind)) + return (int)i; + } + return -1; + }; + + auto get_or_add_virtual_filament = [this, &previous_virtual_by_cluster, &find_virtual_filament_by_color]( + const std::array<std::size_t, 3>& color) -> int { + auto previous_it = previous_virtual_by_cluster.find(color); + if (previous_it != previous_virtual_by_cluster.end() && + previous_it->second >= (int)m_existing_filament_count && + previous_it->second < (int)m_filament_colors_rgba.size()) { + return previous_it->second; + } + + int existing_idx = find_virtual_filament_by_color(color); + if (existing_idx >= 0) + return existing_idx; + + std::array<float, 4> rgba = { + color[0] / 255.f, + color[1] / 255.f, + color[2] / 255.f, + 1.f + }; + return add_virtual_filament(rgba, rgb_to_hex(color).ToStdString()); + }; + + if (m_auto_merge_cb && m_auto_merge_cb->GetValue()) { + // Match clusters to closest existing filaments + std::vector<std::string> names; + for (size_t i = 0; i < m_existing_filament_count; ++i) + names.push_back(m_filament_names.size() > i ? m_filament_names[i] : "Filament " + std::to_string(i + 1)); + + std::vector<std::array<float, 4>> existing_filament_colors( + m_filament_colors_rgba.begin(), + m_filament_colors_rgba.begin() + std::min(m_existing_filament_count, m_filament_colors_rgba.size())); + + m_current_matches = Slic3r::match_clusters_to_filaments( + m_painted.cluster_colors, existing_filament_colors, names); + + // For clusters with poor match (CIEDE2000 ΔE > 5), create virtual filaments. + constexpr double NEW_FILAMENT_THRESHOLD = 5.0; + std::map<std::array<std::size_t, 3>, int> virtual_color_index; + + for (auto& m : m_current_matches) { + if (m.delta_e <= NEW_FILAMENT_THRESHOLD) + continue; + + auto it = virtual_color_index.find(m.cluster_color); + if (it != virtual_color_index.end()) { + m.filament_index = it->second; + } else { + int new_idx = get_or_add_virtual_filament(m.cluster_color); + if (new_idx >= 0) + virtual_color_index[m.cluster_color] = new_idx; + m.filament_index = new_idx >= 0 ? new_idx : find_closest_filament_index(m.cluster_color); + } + if (m.filament_index >= 0 && m.filament_index < (int)m_filament_colors_rgba.size()) { + m.filament_color = m_filament_colors_rgba[m.filament_index]; + m.delta_e = Slic3r::compute_delta_e(m.cluster_color, m.filament_color); + if (m.filament_index >= (int)m_existing_filament_count) + m.delta_e = 0.0; + } + } + } else { + // Keep all virtual filaments in this dialog; unused ones are pruned only on OK. + m_current_matches.clear(); + std::map<std::array<std::size_t, 3>, int> virtual_map; + + for (size_t i = 0; i < m_painted.cluster_colors.size(); ++i) { + const auto& cc = m_painted.cluster_colors[i]; + Slic3r::FilamentMatch fm; + fm.cluster_index = (int)i; + fm.cluster_color = cc; + + auto it = virtual_map.find(cc); + if (it != virtual_map.end()) { + fm.filament_index = it->second; + } else { + int idx = get_or_add_virtual_filament(cc); + if (idx >= 0) + virtual_map[cc] = idx; + fm.filament_index = idx >= 0 ? idx : find_closest_filament_index(cc); + } + if (fm.filament_index >= 0 && fm.filament_index < (int)m_filament_colors_rgba.size()) { + fm.filament_color = m_filament_colors_rgba[fm.filament_index]; + fm.delta_e = Slic3r::compute_delta_e(fm.cluster_color, fm.filament_color); + if (fm.filament_index >= (int)m_existing_filament_count) + fm.delta_e = 0.0; + } + m_current_matches.push_back(fm); + } + } + + update_filament_color_map(); +} + +void TextureImportDialog::rebuild_mapping_rows() +{ + m_mapping_scroll->Freeze(); + m_mapping_sizer->Clear(true); + m_mapping_rows.clear(); + + if (m_current_matches.empty()) { + m_mapping_scroll->FitInside(); + m_mapping_scroll->Thaw(); + return; + } + + auto get_target_wxcolor = [this](int idx) -> wxColour { + if (idx >= 0 && idx < (int)m_filament_colors_rgba.size()) { + const auto& c = m_filament_colors_rgba[idx]; + return wxColour((unsigned char)(c[0] * 255.f), + (unsigned char)(c[1] * 255.f), + (unsigned char)(c[2] * 255.f)); + } + return wxColour(128, 128, 128); + }; + + const auto display_numbers = compute_display_numbers(); + auto display_number = [display_numbers](int idx) -> int { + return (idx >= 0 && idx < (int)display_numbers.size() && display_numbers[idx] > 0) + ? display_numbers[idx] : idx + 1; + }; + + auto get_filament_label = [this, display_number](int idx) -> wxString { + if (idx >= 0 && idx < (int)m_filament_names.size()) + return filament_name_to_wx_string(m_filament_names[idx]); + return wxString::Format("Filament %d", display_number(idx)); + }; + + const wxColour dash_clr = StateColor::darkModeColorFor(wxColour("#ACACAC")); + const wxColour hex_fg = texture_import_text_colour(); + const wxColour card_bg = StateColor::darkModeColorFor(wxColour("#E8E8E8")); + const wxColour card_bd = StateColor::darkModeColorFor(wxColour("#DBDBDB")); + const wxColour name_fg = texture_import_text_colour(); + const wxColour chev_clr = StateColor::darkModeColorFor(wxColour("#6B6B6A")); + + m_mapping_rows.resize(m_current_matches.size()); + for (size_t ci = 0; ci < m_current_matches.size(); ++ci) { + auto& row = m_mapping_rows[ci]; + row.cluster_id = m_current_matches[ci].cluster_index; + row.source_color = m_current_matches[ci].cluster_color; + row.source_hex = rgb_to_hex(row.source_color).ToStdString(); + row.target_filament_idx = m_current_matches[ci].filament_index; + + wxColour src_wx_color( + (unsigned char)row.source_color[0], + (unsigned char)row.source_color[1], + (unsigned char)row.source_color[2]); + + // --- Row container --- + wxPanel* row_panel = new wxPanel(m_mapping_scroll, wxID_ANY); + row_panel->SetBackgroundColour(m_mapping_scroll->GetBackgroundColour()); + row_panel->Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this); + wxBoxSizer* row_sizer = new wxBoxSizer(wxHORIZONTAL); + + // --- Source card (dashed border, circle + hex) --- + const int src_w = FromDIP(138); + const int target_min_w = FromDIP(239); + const int row_h = FromDIP(44); + row.source_panel = new wxPanel(row_panel, wxID_ANY, wxDefaultPosition, wxSize(src_w, row_h), + wxTAB_TRAVERSAL | wxFULL_REPAINT_ON_RESIZE); + row.source_panel->SetMinSize(wxSize(src_w, row_h)); + row.source_panel->SetMaxSize(wxSize(src_w, row_h)); + row.source_panel->SetBackgroundStyle(wxBG_STYLE_PAINT); + + row.source_panel->Bind(wxEVT_PAINT, [this, ci, src_wx_color, dash_clr, hex_fg](wxPaintEvent& e) { + auto* p = static_cast<wxPanel*>(e.GetEventObject()); + wxAutoBufferedPaintDC dc(p); + wxSize sz = p->GetClientSize(); + + dc.SetBrush(wxBrush(p->GetParent()->GetBackgroundColour())); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.x, sz.y); + + wxPen dash_pen(dash_clr, 1, wxPENSTYLE_SHORT_DASH); + dc.SetPen(dash_pen); + dc.SetBrush(wxBrush(p->GetParent()->GetBackgroundColour())); + int r = p->FromDIP(8); + dc.DrawRoundedRectangle(0, 0, sz.x, sz.y, r); + + // Color circle 24px + int cd = p->FromDIP(24); + int cx = p->FromDIP(10); + int cy = (sz.y - cd) / 2; + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(src_wx_color)); + dc.DrawEllipse(cx, cy, cd, cd); + draw_filament_swatch_ellipse_border(dc, src_wx_color, cx, cy, cd, cd); + + if (ci < m_mapping_rows.size()) { + wxFont hex_font = p->GetFont(); + hex_font.SetPointSize(9); + dc.SetFont(hex_font); + dc.SetTextForeground(hex_fg); + wxString hex_str = wxString::Format("# %s", m_mapping_rows[ci].source_hex.substr(1)); + wxSize tsz = dc.GetTextExtent(hex_str); + dc.DrawText(hex_str, cx + cd + p->FromDIP(6), (sz.y - tsz.y) / 2); + } + }); + row.source_panel->Bind(wxEVT_SIZE, [](wxSizeEvent& e) { + e.Skip(); + static_cast<wxWindow*>(e.GetEventObject())->Refresh(); + }); + row.source_panel->Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this); + + row_sizer->Add(row.source_panel, 0, wxEXPAND); + + // --- Arrow panel (dashed arrow) --- + const int arrow_w = FromDIP(24); + wxPanel* arrow_panel = new wxPanel(row_panel, wxID_ANY, wxDefaultPosition, wxSize(arrow_w, row_h)); + arrow_panel->SetMinSize(wxSize(arrow_w, row_h)); + arrow_panel->SetMaxSize(wxSize(arrow_w, row_h)); + arrow_panel->SetBackgroundStyle(wxBG_STYLE_PAINT); + arrow_panel->Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this); + arrow_panel->Bind(wxEVT_PAINT, [dash_clr](wxPaintEvent& e) { + auto* p = static_cast<wxPanel*>(e.GetEventObject()); + wxAutoBufferedPaintDC dc(p); + wxSize sz = p->GetClientSize(); + + dc.SetBrush(wxBrush(p->GetParent()->GetBackgroundColour())); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.x, sz.y); + + int mid_y = sz.y / 2; + int margin = p->FromDIP(2); + int arrow_tip = sz.x - margin; + int arrow_start = margin; + + wxPen dash_pen(dash_clr, p->FromDIP(1), wxPENSTYLE_SHORT_DASH); + dc.SetPen(dash_pen); + dc.DrawLine(arrow_start, mid_y, arrow_tip - p->FromDIP(4), mid_y); + + int ah = p->FromDIP(4); + wxPoint tri[3] = { + {arrow_tip, mid_y}, + {arrow_tip - ah, mid_y - ah / 2}, + {arrow_tip - ah, mid_y + ah / 2} + }; + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(dash_clr)); + dc.DrawPolygon(3, tri); + }); + + row_sizer->Add(arrow_panel, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, FromDIP(4)); + + // --- Target card (numbered square + material name + chevron) --- + row.target_panel = new wxPanel(row_panel, wxID_ANY, wxDefaultPosition, wxSize(-1, row_h), + wxTAB_TRAVERSAL | wxFULL_REPAINT_ON_RESIZE); + row.target_panel->SetMinSize(wxSize(target_min_w, row_h)); + row.target_panel->SetToolTip(get_filament_label(row.target_filament_idx)); + row.target_panel->SetBackgroundStyle(wxBG_STYLE_PAINT); + row.target_panel->SetCursor(wxCursor(wxCURSOR_HAND)); + + row.target_panel->Bind(wxEVT_PAINT, [this, ci, get_target_wxcolor, get_filament_label, + display_number, card_bg, card_bd, name_fg, chev_clr](wxPaintEvent& e) { + auto* p = static_cast<wxPanel*>(e.GetEventObject()); + wxAutoBufferedPaintDC dc(p); + wxSize sz = p->GetClientSize(); + + dc.SetBrush(wxBrush(p->GetParent()->GetBackgroundColour())); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, sz.x, sz.y); + + if (ci >= m_mapping_rows.size()) return; + int fil_idx = m_mapping_rows[ci].target_filament_idx; + + int r = p->FromDIP(8); + dc.SetBrush(wxBrush(card_bg)); + dc.SetPen(wxPen(card_bd, 1)); + dc.DrawRoundedRectangle(0, 0, sz.x, sz.y, r); + + if (fil_idx >= 0 && fil_idx < (int)m_filament_entries.size() && + texture_entry_is_mixed(m_filament_entries[fil_idx].kind)) { + const TextureFilamentEntry& entry = m_filament_entries[fil_idx]; + wxFont mixed_font = p->GetFont(); + mixed_font.SetPointSize(10); + dc.SetFont(mixed_font); + + int x = p->FromDIP(10); + const int sw = p->FromDIP(28); + const int sw_r = p->FromDIP(6); + const int sw_y = (sz.y - sw) / 2; + for (size_t mi = 0; mi < entry.mixed_components.size() && mi < entry.mixed_ratios.size(); ++mi) { + if (mi > 0) { + dc.SetTextForeground(name_fg); + wxString plus = "+"; + wxSize psz = dc.GetTextExtent(plus); + dc.DrawText(plus, x, (sz.y - psz.y) / 2); + x += psz.x + p->FromDIP(4); + } + + const unsigned int comp_id = entry.mixed_components[mi]; + const int comp_idx = comp_id >= 1 ? (int)comp_id - 1 : -1; + wxColour comp_clr("#D9D9D9"); + if (comp_idx >= 0 && comp_idx < (int)m_filament_colors_rgba.size()) { + const auto& c = m_filament_colors_rgba[comp_idx]; + comp_clr = wxColour((unsigned char)(c[0] * 255.f), + (unsigned char)(c[1] * 255.f), + (unsigned char)(c[2] * 255.f)); + } + + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(comp_clr)); + dc.DrawRoundedRectangle(x, sw_y, sw, sw, sw_r); + draw_filament_swatch_border(dc, comp_clr, x, sw_y, sw, sw, sw_r); + + wxString num_str = wxString::Format("%d", display_number(comp_idx)); + wxSize nsz = dc.GetTextExtent(num_str); + dc.SetTextForeground(comp_clr.GetLuminance() < 0.6 ? *wxWHITE : texture_import_gray9000()); + dc.DrawText(num_str, x + (sw - nsz.x) / 2, sw_y + (sw - nsz.y) / 2); + x += sw + p->FromDIP(5); + + dc.SetTextForeground(name_fg); + wxString pct = wxString::Format("%d%%", entry.mixed_ratios[mi]); + wxSize pct_sz = dc.GetTextExtent(pct); + dc.DrawText(pct, x, (sz.y - pct_sz.y) / 2); + x += pct_sz.x + p->FromDIP(5); + if (x > sz.x - p->FromDIP(34)) + break; + } + + int chev_cx = sz.x - p->FromDIP(14); + int chev_cy = sz.y / 2; + int hw = p->FromDIP(3); + int hh = p->FromDIP(2); + dc.SetPen(wxPen(chev_clr, p->FromDIP(1) > 0 ? p->FromDIP(1) : 1)); + dc.DrawLine(chev_cx - hw, chev_cy - hh, chev_cx, chev_cy + hh); + dc.DrawLine(chev_cx, chev_cy + hh, chev_cx + hw, chev_cy - hh); + return; + } + + // Numbered color square 32x32, rounded 6px + int sq = p->FromDIP(32); + int sq_x = p->FromDIP(6); + int sq_y = (sz.y - sq) / 2; + int sq_r = p->FromDIP(6); + wxColour fil_clr = get_target_wxcolor(fil_idx); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(fil_clr)); + dc.DrawRoundedRectangle(sq_x, sq_y, sq, sq, sq_r); + draw_filament_swatch_border(dc, fil_clr, sq_x, sq_y, sq, sq, sq_r); + + { + wxFont num_font = p->GetFont(); + num_font.SetPointSize(10); + dc.SetFont(num_font); + dc.SetTextForeground(fil_clr.GetLuminance() < 0.6 ? *wxWHITE : texture_import_gray9000()); + wxString num_str = wxString::Format("%d", display_number(fil_idx)); + wxSize nsz = dc.GetTextExtent(num_str); + dc.DrawText(num_str, sq_x + (sq - nsz.x) / 2, sq_y + (sq - nsz.y) / 2); + } + + // Material name + { + wxFont name_font = p->GetFont(); + name_font.SetPointSize(9); + dc.SetFont(name_font); + dc.SetTextForeground(name_fg); + wxString name_str = get_filament_label(fil_idx); + int text_x = sq_x + sq + p->FromDIP(8); + int max_text_w = sz.x - text_x - p->FromDIP(24); + if (max_text_w > 0) { + name_str = ellipsize_text(dc, name_str, max_text_w); + wxSize tsz = dc.GetTextExtent(name_str); + dc.DrawText(name_str, text_x, (sz.y - tsz.y) / 2); + } + } + + // Dropdown chevron at right edge + { + int chev_cx = sz.x - p->FromDIP(14); + int chev_cy = sz.y / 2; + int hw = p->FromDIP(3); + int hh = p->FromDIP(2); + dc.SetPen(wxPen(chev_clr, p->FromDIP(1) > 0 ? p->FromDIP(1) : 1)); + dc.DrawLine(chev_cx - hw, chev_cy - hh, chev_cx, chev_cy + hh); + dc.DrawLine(chev_cx, chev_cy + hh, chev_cx + hw, chev_cy - hh); + } + }); + + row.target_panel->Bind(wxEVT_LEFT_DOWN, [this, ci](wxMouseEvent&) { + show_filament_popup(ci); + }); + row.target_panel->Bind(wxEVT_SIZE, [](wxSizeEvent& e) { + e.Skip(); + static_cast<wxWindow*>(e.GetEventObject())->Refresh(); + }); + row.target_panel->Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this); + + row_sizer->Add(row.target_panel, 1, wxEXPAND); + + row_panel->SetSizer(row_sizer); + m_mapping_sizer->Add(row_panel, 0, wxEXPAND | wxBOTTOM, FromDIP(12)); + } + + m_mapping_scroll->FitInside(); + m_mapping_scroll->Layout(); + m_mapping_scroll->Thaw(); +} + +std::vector<Slic3r::FilamentMatch> TextureImportDialog::build_matches_from_rows() const +{ + std::vector<Slic3r::FilamentMatch> matches(m_mapping_rows.size()); + for (size_t i = 0; i < m_mapping_rows.size(); ++i) { + auto& m = matches[i]; + m.cluster_index = m_mapping_rows[i].cluster_id; + m.cluster_color = m_mapping_rows[i].source_color; + + int sel = m_mapping_rows[i].target_filament_idx; + if (sel >= 0 && sel < (int)m_filament_colors_rgba.size()) { + m.filament_index = sel; + m.filament_color = m_filament_colors_rgba[sel]; + m.delta_e = Slic3r::compute_delta_e(m.cluster_color, m.filament_color); + } + } + return matches; +} + +// ---- Event handlers ---- + +void TextureImportDialog::update_color_count_preset_buttons() +{ + if (m_btn_color_4) m_btn_color_4->SetValue(m_param_color_count == 4); + if (m_btn_color_8) m_btn_color_8->SetValue(m_param_color_count == 8); + if (m_btn_color_16) m_btn_color_16->SetValue(m_param_color_count == 16); +} + +void TextureImportDialog::set_color_count_value(int value, bool update_spin) +{ + m_param_color_count = std::clamp(value, 1, (int)max_filament_count()); + m_color_slider->SetValue(m_param_color_count); + if (update_spin) + m_color_spin->SetValue(m_param_color_count); + update_color_count_preset_buttons(); + update_confirm_button_state(); +} + +void TextureImportDialog::set_smooth_value(int value, bool update_spin) +{ + m_param_smooth = std::clamp(value, 0, 10); + m_smooth_slider->SetValue(m_param_smooth); + if (update_spin) + m_smooth_spin->SetValue(m_param_smooth); + update_confirm_button_state(); +} + +void TextureImportDialog::preview_spin_text_value(SpinInput* spin, AccentSlider* slider, int& param, + int min_value, int max_value, const wxString& text, + std::function<void()> on_value_changed) +{ + long value; + if (!text.ToLong(&value)) + return; + + wxTextCtrl* tc = spin->GetTextCtrl(); + long parsed = value; + value = std::clamp((int)parsed, min_value, max_value); + + wxString normalized = text; + if (parsed > max_value || (text.length() > 1 && text[0] == '0')) + normalized = wxString::Format("%ld", value); + + if (normalized != text) { + long pos = tc->GetInsertionPoint(); + tc->ChangeValue(normalized); + if (parsed > max_value) + tc->SetInsertionPointEnd(); + else + tc->SetInsertionPoint(std::min<long>(normalized.length(), std::max(0L, pos - 1))); + } + + param = (int)value; + slider->SetValue(param); + if (on_value_changed) + on_value_changed(); + update_confirm_button_state(); +} + +void TextureImportDialog::on_color_preset_clicked(wxCommandEvent& evt) +{ + int id = evt.GetId(); + int color_count = m_param_color_count; + if (id == ID_COLOR_4) { color_count = 4; } + if (id == ID_COLOR_8) { color_count = 8; } + if (id == ID_COLOR_16) { color_count = 16; } + + if (id == ID_COLOR_AUTO) { + start_computation(true); + return; + } + + set_color_count_value(color_count, true); +} + +void TextureImportDialog::on_color_slider_changed(wxCommandEvent&) +{ + set_color_count_value(m_color_slider->GetValue(), true); +} + +void TextureImportDialog::on_color_spin_changed(wxCommandEvent&) +{ + set_color_count_value(m_color_spin->GetValue(), true); +} + +void TextureImportDialog::on_color_spin_text_changed(wxCommandEvent& evt) +{ + preview_spin_text_value(m_color_spin, m_color_slider, m_param_color_count, + 1, (int)max_filament_count(), evt.GetString(), + [this]() { update_color_count_preset_buttons(); }); +} + +void TextureImportDialog::on_smooth_slider_changed(wxCommandEvent&) +{ + set_smooth_value(m_smooth_slider->GetValue(), true); +} + +void TextureImportDialog::on_smooth_spin_changed(wxCommandEvent&) +{ + set_smooth_value(m_smooth_spin->GetValue(), true); +} + +void TextureImportDialog::on_smooth_spin_text_changed(wxCommandEvent& evt) +{ + preview_spin_text_value(m_smooth_spin, m_smooth_slider, m_param_smooth, + 0, 10, evt.GetString()); +} + +void TextureImportDialog::on_apply_clicked(wxCommandEvent&) +{ + start_computation(); +} + +void TextureImportDialog::on_auto_merge_toggled(wxCommandEvent&) +{ + bool auto_merge_enabled = !m_auto_merge_cb || m_auto_merge_cb->GetValue(); + m_auto_merge_enabled = auto_merge_enabled; + + if (m_state == TextureImportState::Ready) { + const auto previous_matches = m_current_matches; + do_auto_match(); + restore_current_match_order(previous_matches); + compact_used_virtual_filaments(); + update_filament_color_map(); + rebuild_mapping_rows(); + update_drop_warning_visibility(); + update_auto_mix_reset_visibility(); + } +} + +void TextureImportDialog::highlight_view_button(int view_index) +{ + Button* btns[] = { m_btn_view_original, m_btn_view_multicolor }; + + // The inactive pill lies on m_tab_panel, which is preview_bg (#EEEEEE -> #4C4C55), and has to + // read as raised above that strip in both themes — so its fill steps away from the strip in + // opposite directions. gDarkColors pairs one light tone with one dark tone and cannot express + // an inversion, so the two are picked here the way filament_swatch_border_colour() does. + const bool dark_pill = is_dark(); + StateColor inactive_bg( + std::pair<wxColour, int>(dark_pill ? wxColour(0x5C, 0x5C, 0x64) : wxColour("#F4F4F4"), StateColor::Pressed), + std::pair<wxColour, int>(dark_pill ? wxColour(0x66, 0x66, 0x6E) : wxColour("#F8F8F8"), StateColor::Hovered), + std::pair<wxColour, int>(dark_pill ? wxColour(0x54, 0x54, 0x5B) : *wxWHITE, StateColor::Normal)); + const wxColour inactive_bd = dark_pill ? wxColour(0x54, 0x54, 0x5B) : *wxWHITE; + const wxColour inactive_text = wxColour("#6B6B6A"); + + for (int i = 0; i < 2; ++i) { + if (!btns[i]) continue; + if (i == view_index) { + apply_accent_button_colours(btns[i]); + } else { + btns[i]->SetBackgroundColor(inactive_bg); + btns[i]->SetBorderColor(inactive_bd); + btns[i]->SetTextColor(inactive_text); + } + btns[i]->Refresh(); + } +} + +void TextureImportDialog::on_skip_clicked(wxCommandEvent&) +{ + m_skipped = true; + m_new_filament_colors.clear(); + m_new_filament_preset_names.clear(); + m_new_mixed_filaments.clear(); + m_current_matches.clear(); + cancel_computation(); + EndModal(wxID_CANCEL); +} + +bool TextureImportDialog::has_valid_result() const +{ + if (m_painted.face_colors.empty() || m_current_matches.empty() || m_mapping_rows.empty()) + return false; + + if (m_mapping_rows.size() != m_current_matches.size()) + return false; + + const int filament_count = (int)std::min(m_filament_colors_rgba.size(), max_filament_count()); + for (const auto& row : m_mapping_rows) { + if (row.target_filament_idx < 0 || row.target_filament_idx >= filament_count) + return false; + } + return true; +} + +bool TextureImportDialog::is_params_dirty() const +{ + if (m_applied_color_count < 0) + return false; + return m_param_color_count != m_applied_color_count + || m_param_smooth != m_applied_smooth; +} + +void TextureImportDialog::update_drop_warning_visibility() +{ + if (!m_drop_warning_label) return; + // Show only when the most recent do_auto_match() ran into the filament + // cap AND we are in the Ready state. The flag is reset at every + // do_auto_match() entry, so any "clean" re-run automatically hides the + // warning even if a previous run had dropped clusters. + const bool show = (m_state == TextureImportState::Ready) && m_filaments_dropped; + if (m_drop_warning_label->IsShown() == show) return; + m_drop_warning_label->Show(show); + Layout(); +} + +void TextureImportDialog::update_confirm_button_state() +{ + if (m_state != TextureImportState::Ready) + return; + + if (!has_valid_result()) { + m_btn_ok->Enable(false); + if (m_hint_label) m_hint_label->Hide(); + m_btn_ok->Refresh(); + Layout(); + return; + } + + m_btn_ok->Enable(true); + style_confirm_button(is_params_dirty()); + + m_btn_ok->Refresh(); + Layout(); +} + +// Both state updaters land here: the Confirm button reads as accent only while it would apply +// exactly what the preview shows. +void TextureImportDialog::style_confirm_button(bool dirty) +{ + if (dirty) { + apply_muted_button_colours(m_btn_ok); + m_btn_ok->SetToolTip(_L("Reminder: parameters changed, click Apply to take effect")); + } else { + apply_accent_button_colours(m_btn_ok); + m_btn_ok->UnsetToolTip(); + } + if (m_hint_label) + m_hint_label->Show(dirty); +} + +void TextureImportDialog::on_ok_clicked(wxCommandEvent&) +{ + if (m_state != TextureImportState::Ready || !has_valid_result() || is_params_dirty()) + return; + + m_current_matches = build_matches_from_rows(); + if (m_current_matches.empty()) + return; + + compact_used_virtual_filaments(); + + EndModal(wxID_OK); +} + +// ---- Result accessors ---- + +Slic3r::PaintedMesh TextureImportDialog::get_painted_mesh() const +{ + return m_painted; +} + +std::vector<Slic3r::FilamentMatch> TextureImportDialog::get_matches() const +{ + if (!m_current_matches.empty()) + return m_current_matches; + return build_matches_from_rows(); +} + +void TextureImportDialog::on_dpi_changed(const wxRect&) +{ + // All control sizes below are baked into persistent properties (min size, + // corner radius, fixed wxSize) using FromDIP() at build time. The base + // DPIAware::rescale() only rescales fonts; it does not recompute these + // stored pixel values. Re-apply them here so the layout stays consistent + // when the dialog is dragged to a screen with a different DPI. + SetMinSize(wxSize(FromDIP(800), FromDIP(500))); + + const int view_button_height = FromDIP(27); + for (Button* btn : {m_btn_view_original, m_btn_view_multicolor}) { + if (btn) { + btn->SetCornerRadius(view_button_height / 2); + btn->SetMinSize(wxSize(FromDIP(57), view_button_height)); + } + } + + for (Button* btn : {m_btn_color_4, m_btn_color_8, m_btn_color_16}) { + if (btn) { + btn->SetCornerRadius(FromDIP(12)); + btn->SetMinSize(wxSize(FromDIP(28), FromDIP(28))); + } + } + + if (m_btn_color_auto) { + m_btn_color_auto->SetCornerRadius(FromDIP(12)); + m_btn_color_auto->SetMinSize(wxSize(FromDIP(60), FromDIP(28))); + } + if (m_btn_apply) { + m_btn_apply->SetCornerRadius(FromDIP(12)); + m_btn_apply->SetMinSize(wxSize(FromDIP(60), FromDIP(28))); + } + if (m_btn_auto_mix) { + m_btn_auto_mix->SetCornerRadius(FromDIP(14)); + m_btn_auto_mix->SetMinSize(wxSize(FromDIP(178), FromDIP(28))); + } + if (m_btn_mix_reset) + m_btn_mix_reset->SetPaddingSize(wxSize(FromDIP(2), FromDIP(2))); + + if (m_color_spin) + m_color_spin->SetMinSize(wxSize(FromDIP(60), FromDIP(28))); + if (m_smooth_spin) + m_smooth_spin->SetMinSize(wxSize(FromDIP(60), FromDIP(28))); + + if (m_mapping_scroll) { + m_mapping_scroll->SetMinSize(wxSize(-1, FromDIP(300))); + m_mapping_scroll->SetScrollRate(0, FromDIP(10)); + } + + if (m_btn_skip) { + m_btn_skip->SetCornerRadius(FromDIP(20)); + m_btn_skip->SetMinSize(wxSize(FromDIP(136), FromDIP(40))); + } + if (m_btn_ok) { + m_btn_ok->SetCornerRadius(FromDIP(20)); + m_btn_ok->SetMinSize(wxSize(FromDIP(156), FromDIP(40))); + } + + // Mapping rows store their panel sizes (source/target/arrow/row height) + // as fixed FromDIP min/max sizes, so rebuild them to pick up the new DPI. + rebuild_mapping_rows(); + + if (wxSizer* sizer = GetSizer()) + sizer->Layout(); + Layout(); + Refresh(); + wxGetApp().UpdateDlgDarkUI(this); +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/TextureImportDialog.hpp b/src/slic3r/GUI/TextureImportDialog.hpp new file mode 100644 index 0000000000..960bac6145 --- /dev/null +++ b/src/slic3r/GUI/TextureImportDialog.hpp @@ -0,0 +1,407 @@ +#pragma once + +#include "GUI_Utils.hpp" +#include "Widgets/ProgressDialog.hpp" +#include "libslic3r/TexturePainting.hpp" + +#include <wx/sizer.h> +#include <wx/stattext.h> +#include "Widgets/PopupWindow.hpp" +#include <wx/panel.h> +#include <wx/scrolwin.h> +#include <wx/textctrl.h> +#include "Widgets/SpinInput.hpp" +#include <wx/checkbox.h> +#include <wx/button.h> +#include "Widgets/Button.hpp" +#include <wx/glcanvas.h> +#include <wx/event.h> + +#include <array> +#include <atomic> +#include <functional> +#include <map> +#include <memory> +#include <mutex> +#include <thread> +#include <vector> +#include <string> + +class AccentSlider; + +namespace Slic3r { namespace GUI { + +wxDECLARE_EVENT(EVT_TEXTURE_COMPUTE_DONE, wxCommandEvent); +wxDECLARE_EVENT(EVT_TEXTURE_COMPUTE_PROGRESS, wxCommandEvent); +wxDECLARE_EVENT(EVT_TEXTURE_COMPUTE_ERROR, wxCommandEvent); +wxDECLARE_EVENT(EVT_TEXTURE_MESH_REPAIR_DECISION, wxCommandEvent); + +enum class TextureImportState { + Idle, + Computing, + Ready, + Error +}; + +enum class TextureAutoMixMode { + CMYW, + RYBW +}; + +enum class TextureFilamentKind { + ExistingPhysical, + ExistingMixed, + NewPhysical, + NewMixed +}; + +struct TextureFilamentEntry { + TextureFilamentKind kind{TextureFilamentKind::ExistingPhysical}; + int dialog_index{-1}; + size_t project_config_index{size_t(-1)}; + std::string color_hex; + std::string name; + std::string type; + std::string preset_name; + std::vector<unsigned int> mixed_components; + std::vector<int> mixed_ratios; +}; + +struct TextureNewMixedFilament { + int dialog_index{-1}; + std::string color_hex; + std::vector<int> component_dialog_indices; + std::vector<int> ratios; +}; + +struct FilamentMappingRow { + int cluster_id = -1; + std::array<std::size_t, 3> source_color = {0, 0, 0}; + std::string source_hex; + int target_filament_idx = 0; + wxPanel* source_panel = nullptr; + wxPanel* target_panel = nullptr; +}; + +class FilamentSelectPopup; +class AutoMixSelectPopup; +// Lightweight 3D preview panel using wxGLCanvas. +// Renders: original textured, multi-color, or filament-mapped. +class TexturePreviewCanvas : public wxGLCanvas +{ +public: + enum class RenderMode { Original, MultiColor, FilamentMap }; + + TexturePreviewCanvas(wxWindow* parent, const wxGLAttributes& attrs); + ~TexturePreviewCanvas(); + + void set_mesh_data( + const std::vector<std::array<float, 3>>& vertices, + const std::vector<std::array<int, 3>>& indices); + + void set_texture_data( + const std::vector<std::array<float, 2>>& uvs, + const unsigned char* tex_data, int tex_w, int tex_h, int tex_channels); + + void set_texture_render_data( + const std::vector<std::vector<unsigned char>>& tex_pixels_rgb, + const std::vector<int>& tex_widths, + const std::vector<int>& tex_heights, + const std::vector<std::array<std::array<float,2>, 3>>& face_uvs, + const std::vector<int>& face_tex_ids); + + void set_painted_mesh_data( + const std::vector<std::array<float, 3>>& vertices, + const std::vector<std::array<int, 3>>& indices); + void set_face_colors(const std::vector<std::array<std::size_t, 3>>& face_colors); + void set_original_face_colors(const std::vector<std::array<std::size_t, 3>>& face_colors); + void set_filament_color_map(const std::map<std::array<std::size_t, 3>, std::array<float, 3>>& color_map); + + void set_render_mode(RenderMode mode); + RenderMode get_render_mode() const { return m_mode; } + void set_computing_overlay(bool show); + void reset_view(); + +private: + void on_paint(wxPaintEvent& evt); + void on_size(wxSizeEvent& evt); + void on_mouse(wxMouseEvent& evt); + void ensure_gl_ready(); + void render(); + void render_mesh(); + void render_textured_original(); + void render_reset_overlay(const wxSize& logical_size, const wxSize& viewport_size); + void upload_reset_icon_textures(); + unsigned int upload_reset_icon_texture(const std::string& icon_name); + wxRect reset_overlay_rect() const; + bool handle_reset_overlay_mouse(wxMouseEvent& evt); + void upload_textures(); + void compute_smooth_normals(); + void update_bounding_box(); + + wxGLContext* m_context = nullptr; + bool m_gl_initialized = false; + RenderMode m_mode = RenderMode::Original; + + float m_zoom = 1.0f; + float m_rot_x = -30.0f; + float m_rot_y = 30.0f; + float m_pan_x = 0.0f; + float m_pan_y = 0.0f; + wxPoint m_last_mouse_pos; + enum class DragMode { None, Rotate, Pan }; + DragMode m_drag_mode = DragMode::None; + + std::vector<std::array<float, 3>> m_vertices; + std::vector<std::array<int, 3>> m_indices; + std::vector<std::array<float, 2>> m_uvs; + std::vector<std::array<float, 3>> m_painted_vertices; + std::vector<std::array<int, 3>> m_painted_indices; + std::vector<std::array<float, 3>> m_face_colors_rgb; + std::vector<std::array<float, 3>> m_original_face_colors_rgb; + std::vector<std::array<float, 3>> m_filament_colors_rgb; + std::map<std::array<std::size_t, 3>, std::array<float, 3>> m_color_map; + + unsigned int m_tex_id = 0; + int m_tex_w = 0; + int m_tex_h = 0; + int m_tex_channels = 3; + bool m_tex_dirty = false; + std::vector<unsigned char> m_tex_data; + + std::vector<unsigned int> m_gl_tex_ids; + std::vector<std::vector<unsigned char>> m_tex_pixels_rgb; + std::vector<int> m_tex_widths; + std::vector<int> m_tex_heights; + std::vector<std::array<std::array<float,2>, 3>> m_face_uvs; + std::vector<int> m_face_tex_ids; + bool m_multi_tex_dirty = false; + + std::vector<std::array<float, 3>> m_vertex_normals; + + std::array<float, 3> m_center = {0, 0, 0}; + float m_radius = 1.0f; + + unsigned int m_reset_icon_tex = 0; + unsigned int m_reset_icon_hover_tex = 0; + unsigned int m_reset_icon_dark_tex = 0; + unsigned int m_reset_icon_dark_hover_tex = 0; + bool m_reset_overlay_hovered = false; + bool m_reset_overlay_pressed = false; +}; + + +class TextureImportDialog : public DPIDialog +{ +public: + TextureImportDialog(wxWindow* parent, + const Slic3r::TexturedMesh& textured_mesh, + const std::vector<TextureFilamentEntry>& filament_entries, + std::function<bool()> initial_cancel_callback = {}, + std::function<bool(int)> initial_progress_callback = {}); + ~TextureImportDialog(); + + int ShowModal() override; + void on_dpi_changed(const wxRect& suggested_rect) override; + + Slic3r::PaintedMesh get_painted_mesh() const; + std::vector<Slic3r::FilamentMatch> get_matches() const; + bool was_skipped() const { return m_skipped; } + bool fallback_to_geometry_only() const { return m_fallback_to_geometry_only; } + // Colors of virtual filaments that need to be created after dialog confirmation. + // Index i corresponds to filament index (m_existing_filament_count + i). + const std::vector<std::array<float, 4>>& get_new_filament_colors() const { return m_new_filament_colors; } + const std::vector<std::string>& get_new_filament_preset_names() const { return m_new_filament_preset_names; } + const std::vector<TextureNewMixedFilament>& get_new_mixed_filaments() const { return m_new_mixed_filaments; } + const std::vector<TextureFilamentEntry>& get_filament_entries() const { return m_filament_entries; } + size_t get_existing_filament_count() const { return m_existing_filament_count; } + +private: + void build_ui(); + void build_preview_panel(wxWindow* parent, wxSizer* sizer); + void build_params_panel(wxWindow* parent, wxSizer* sizer); + void build_mapping_panel(wxWindow* parent, wxSizer* sizer); + void build_bottom_buttons(wxSizer* sizer); + + void set_state(TextureImportState new_state); + void update_ui_for_state(); + + void start_computation(bool auto_color = false, bool initial = false); + void cancel_computation(); + void on_computation_complete(wxCommandEvent& evt); + void on_computation_progress(wxCommandEvent& evt); + void on_computation_error(wxCommandEvent& evt); + void on_mesh_repair_decision_required(wxCommandEvent& evt); + + void rebuild_mapping_rows(); + void do_auto_match(); + // Reorder m_current_matches into a canonical, predictable order (ascending + // filament_index, with unmapped entries pushed to the end). Used right + // after the initial computation so the first view the user sees has a + // stable, intuitive layout. + void sort_current_matches_by_filament_index(); + // Reorder m_current_matches so they appear in the same order as + // `previous_matches` (keyed by cluster_index). Entries whose cluster_index + // was not present before are appended at the end, preserving their current + // relative order. Used when the user toggles auto-merge so the rows do not + // visually jump around. Assumes each cluster_index appears at most once in + // both vectors (this invariant is currently guaranteed by do_auto_match, + // which produces one match per cluster). + void restore_current_match_order(const std::vector<Slic3r::FilamentMatch>& previous_matches); + std::vector<Slic3r::FilamentMatch> build_matches_from_rows() const; + void update_filament_color_map(); + void show_filament_popup(size_t row_index); + void dismiss_filament_popup(); + void dismiss_filament_popup_on_wheel(wxMouseEvent& evt); + void show_auto_mix_popup(); + void dismiss_auto_mix_popup(); + void set_auto_mix_mode(TextureAutoMixMode mode); + void apply_auto_standard_mix(TextureAutoMixMode mode); + void reset_auto_mix(); + void update_auto_mix_reset_visibility(); + bool add_decomposed_mixed_filament(size_t row_index); + int add_virtual_filament(const std::array<float, 4>& rgba, const std::string& hex, + const std::string& preset_name = std::string()); + int add_virtual_mixed_filament(const std::string& color_hex, + const std::vector<int>& component_dialog_indices, + const std::vector<int>& ratios); + size_t max_filament_count() const; + bool can_add_virtual_filament() const; + // Recomputes m_drop_warning_label visibility from m_filaments_dropped and + // m_state. Safe to call whether or not the label has been created yet. + // Visibility reflects ONLY the result of the most recent do_auto_match(): + // if the latest match did not drop any cluster, the label is hidden even + // if a previous match had dropped (no historical accumulation). + void update_drop_warning_visibility(); + void compact_used_virtual_filaments(); + int find_closest_filament_index(const std::array<std::size_t, 3>& color) const; + // Returns a vector indexed by dialog_index whose value is the 1-based + // display number that mirrors the final sidebar ordering produced by + // apply_textured_mesh_import_result (Plater.cpp): ExistingPhysical, + // NewPhysical, ExistingMixed, NewMixed. Used so the dialog shows the + // same IDs the sidebar will show after OK, instead of the raw + // dialog_index + 1 (which interleaves physicals and mixeds). + // MUST mirror ordering in apply_textured_mesh_import_result (Plater.cpp:9896). + std::vector<int> compute_display_numbers() const; + + void on_color_preset_clicked(wxCommandEvent& evt); + void on_color_slider_changed(wxCommandEvent& evt); + void on_color_spin_changed(wxCommandEvent& evt); + void on_color_spin_text_changed(wxCommandEvent& evt); + void on_smooth_slider_changed(wxCommandEvent& evt); + void on_smooth_spin_changed(wxCommandEvent& evt); + void on_smooth_spin_text_changed(wxCommandEvent& evt); + void on_apply_clicked(wxCommandEvent& evt); + void on_auto_merge_toggled(wxCommandEvent& evt); + void highlight_view_button(int view_index); + void on_skip_clicked(wxCommandEvent& evt); + void on_ok_clicked(wxCommandEvent& evt); + + void set_color_count_value(int value, bool update_spin); + void set_smooth_value(int value, bool update_spin); + void preview_spin_text_value(SpinInput* spin, AccentSlider* slider, int& param, + int min_value, int max_value, const wxString& text, + std::function<void()> on_value_changed = {}); + void update_color_count_preset_buttons(); + + bool has_valid_result() const; + bool is_params_dirty() const; + void update_confirm_button_state(); + void style_confirm_button(bool dirty); + + Slic3r::TexturedMesh m_textured_mesh; + std::vector<std::string> m_filament_color_strs; // existing + virtual + std::vector<std::string> m_filament_names; // existing + virtual + std::vector<std::array<float, 4>> m_filament_colors_rgba; // existing + virtual + std::vector<TextureFilamentEntry> m_filament_entries; // aligned with m_filament_colors_rgba + size_t m_existing_filament_count = 0; + std::vector<std::array<float, 4>> m_new_filament_colors; // only virtual (to be created) + std::vector<std::string> m_new_filament_preset_names; // only virtual, aligned with m_new_filament_colors + std::vector<TextureNewMixedFilament> m_new_mixed_filaments; + std::string m_default_virtual_filament_preset_name; + + TextureImportState m_state = TextureImportState::Idle; + bool m_skipped = false; + bool m_fallback_to_geometry_only = false; + // True iff *the most recent* do_auto_match() ran into the global filament + // limit and had to drop one or more clusters. Reset to false on every + // do_auto_match() entry so it never accumulates across runs: a run that + // does not drop anything must observe false here, regardless of whether + // previous runs dropped. Drives the inline orange warning above the + // bottom buttons; never affects the mapping itself. + bool m_filaments_dropped = false; + bool m_auto_merge_enabled = true; + TextureAutoMixMode m_auto_mix_mode = TextureAutoMixMode::CMYW; + int m_auto_mix_font_point_size = 10; + + Slic3r::PaintedMesh m_painted; + std::vector<Slic3r::FilamentMatch> m_current_matches; + + std::unique_ptr<std::thread> m_worker; + std::atomic<bool> m_cancel_flag{false}; + std::mutex m_result_mutex; + Slic3r::PaintedMesh m_pending_result; + std::function<bool()> m_initial_cancel_callback; + std::function<bool(int)> m_initial_progress_callback; + bool m_current_computation_initial = false; + bool m_initial_computation_pending = false; + bool m_initial_computation_cancelled = false; + bool m_initial_computation_failed = false; + bool m_initial_tooltips_set = false; + bool m_current_computation_auto_color = false; + Slic3r::TexturePaintingSettings::MeshRepairDecision m_mesh_repair_decision = + Slic3r::TexturePaintingSettings::MeshRepairDecision::Ask; + + Button* m_btn_color_4 = nullptr; + Button* m_btn_color_8 = nullptr; + Button* m_btn_color_16 = nullptr; + Button* m_btn_color_auto = nullptr; + AccentSlider* m_color_slider = nullptr; + SpinInput* m_color_spin = nullptr; + AccentSlider* m_smooth_slider = nullptr; + SpinInput* m_smooth_spin = nullptr; + Button* m_btn_apply = nullptr; + + wxCheckBox* m_auto_merge_cb = nullptr; + Button* m_btn_auto_mix = nullptr; + Button* m_btn_mix_reset = nullptr; + bool m_auto_mix_applied = false; + AutoMixSelectPopup* m_auto_mix_popup = nullptr; + wxScrolledWindow* m_mapping_scroll = nullptr; + wxBoxSizer* m_mapping_sizer = nullptr; + std::vector<FilamentMappingRow> m_mapping_rows; + FilamentSelectPopup* m_filament_popup = nullptr; + int m_filament_popup_row = -1; + int m_skip_next_filament_popup_row = -1; + + TexturePreviewCanvas* m_preview_canvas = nullptr; + wxPanel* m_tab_panel = nullptr; + Button* m_btn_view_original = nullptr; + Button* m_btn_view_multicolor = nullptr; + + ProgressDialog* m_progress_dlg = nullptr; + + Button* m_btn_skip = nullptr; + Button* m_btn_ok = nullptr; + wxStaticText* m_drop_warning_label = nullptr; + + int m_param_color_count = 4; + int m_param_smooth = 5; + + int m_applied_color_count = -1; + int m_applied_smooth = -1; + wxStaticText* m_hint_label = nullptr; + + static const int ID_COLOR_4 = wxID_HIGHEST + 200; + static const int ID_COLOR_8 = wxID_HIGHEST + 201; + static const int ID_COLOR_16 = wxID_HIGHEST + 202; + static const int ID_COLOR_AUTO = wxID_HIGHEST + 203; + static const int ID_BTN_APPLY = wxID_HIGHEST + 204; + static const int ID_BTN_SKIP = wxID_HIGHEST + 205; + static const int ID_VIEW_ORIGINAL = wxID_HIGHEST + 206; + static const int ID_VIEW_MULTICOLOR = wxID_HIGHEST + 207; + + wxDECLARE_EVENT_TABLE(); +}; + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/ThermalPreconditioningDialog.hpp b/src/slic3r/GUI/ThermalPreconditioningDialog.hpp index d13a1d19f5..57cda90b18 100644 --- a/src/slic3r/GUI/ThermalPreconditioningDialog.hpp +++ b/src/slic3r/GUI/ThermalPreconditioningDialog.hpp @@ -31,7 +31,6 @@ private: wxStaticText *m_remaining_time_label; wxStaticText *m_explanation_label; wxButton *m_ok_button; - wxStaticBitmap *m_title_bitmap; DECLARE_EVENT_TABLE() }; diff --git a/src/slic3r/GUI/TroubleshootDialog.cpp b/src/slic3r/GUI/TroubleshootDialog.cpp index b0a8f71930..5acb75c6ea 100644 --- a/src/slic3r/GUI/TroubleshootDialog.cpp +++ b/src/slic3r/GUI/TroubleshootDialog.cpp @@ -1,6 +1,7 @@ #include "TroubleshootDialog.hpp" #include "I18N.hpp" +#include "BuildCommit.hpp" #include "GUI.hpp" #include "GUI_App.hpp" #include "MainFrame.hpp" @@ -71,7 +72,7 @@ wxFlexGridSizer* TroubleshootDialog::create_item_loaded_profiles() auto gen_stats = GetProfilesOverview(); gen_stats = ""; // clear mem. not needed after generating m_..._act, m_..._usr variables - auto add_sizer = [this, g_sizer, create_label](PresetCollection* col, wxString label, int in_use, int user) { + auto add_sizer = [g_sizer, create_label](PresetCollection* col, wxString label, int in_use, int user) { int sys = 0; for (auto it = col->begin(); it != col->end(); it++) { if (it->is_system) @@ -137,9 +138,9 @@ TroubleshootDialog::TroubleshootDialog() version->SetFont(version_font); version->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); - auto build = new Button(this, wxString(GIT_COMMIT_HASH)); + auto build = new Button(this, wxString(build_commit_label)); build->SetStyle(ButtonStyle::Regular, ButtonType::Window); - auto hash_url = "https://github.com/OrcaSlicer/OrcaSlicer/commit/" + wxString(GIT_COMMIT_HASH); + auto hash_url = "https://github.com/OrcaSlicer/OrcaSlicer/commit/" + wxString(build_commit_hash); build->SetToolTip(hash_url); build->Bind(wxEVT_BUTTON, [hash_url](wxCommandEvent &e) { wxLaunchDefaultBrowser(hash_url); @@ -178,7 +179,7 @@ TroubleshootDialog::TroubleshootDialog() return wxTheClipboard->SetData(new wxTextDataObject(GetSysInfoAll())); }); - sys_less_btn->Bind(wxEVT_BUTTON, [this, sys_panel, sys_less_btn, sys_info_lines, sys_copy_btn](wxCommandEvent &e) { + sys_less_btn->Bind(wxEVT_BUTTON, [this, sys_panel, sys_less_btn, sys_info_lines](wxCommandEvent &e) { m_sys_panel_mode = !m_sys_panel_mode; sys_panel->SetText(sys_info_lines(m_sys_panel_mode)); sys_less_btn->SetLabel(m_sys_panel_mode ? _L("Hide") : _L("Show")); @@ -226,7 +227,7 @@ TroubleshootDialog::TroubleshootDialog() }; auto info_desc_1 = create_info_line(_L("We need information for diagnosing source of the issue. Check wiki page for detailed guide.")); - auto info_desc_2 = create_info_line(_L("Pack button collects project file and logs of current session onto a zip file.")); + auto info_desc_2 = create_info_line(_L("Pack button collects project file and logs of current session onto a ZIP archive.")); auto info_desc_3 = create_info_line(_L("Any additional visual examples like images or screen recordings might be helpful while reporting the issue.")); wxBoxSizer *info_desc_sizer = new wxBoxSizer(wxVERTICAL); info_desc_sizer->Add(info_desc_1, 0, wxEXPAND | wxBOTTOM, FromDIP(8)); @@ -292,7 +293,7 @@ TroubleshootDialog::TroubleshootDialog() auto log_level_szr = create_label(_L("Log level"), ""); log_level_szr->Add(create_item_log_level_combo(), 0, wxALIGN_CENTER_VERTICAL); - auto log_pack_szr = create_label(_L("Stored logs"), _L("Packs all stored logs onto a zip file.")); + auto log_pack_szr = create_label(_L("Stored logs"), _L("Packs all stored logs onto a ZIP archive.")); auto log_pack_btn = create_btn(_L("Pack") + "...", ""); log_pack_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { auto data_dir = boost::filesystem::path(Slic3r::data_dir()); @@ -371,7 +372,7 @@ wxString TroubleshootDialog::GetSysInfoAll() { wxString info; info += "Version : " + wxString(SoftFever_VERSION) + "\n" - + "Build : " + wxString(GIT_COMMIT_HASH) + "\n" + + "Build : " + wxString(build_commit_label) + "\n" + "Package : " + GetPackageType() + "\n" + "Platform : " + GetOSinfo() + "\n" + "Processor : " + GetCPUinfo() + "\n" diff --git a/src/slic3r/GUI/UnsavedChangesDialog.hpp b/src/slic3r/GUI/UnsavedChangesDialog.hpp index b25e852c6b..fc6b8043f4 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.hpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.hpp @@ -343,7 +343,7 @@ public: UnsavedChangesDialog(const wxString &caption, const wxString &header, DynamicConfig *config, int from, int to, bool left_to_right, NozzleVolumeType nozzle); ~UnsavedChangesDialog() override = default; - int ShowModal(); + int ShowModal() override; void build(Preset::Type type, PresetCollection *dependent_presets, const std::string &new_selected_preset, const wxString &header = ""); void update(Preset::Type type, PresetCollection* dependent_presets, const std::string& new_selected_preset, const wxString& header); diff --git a/src/slic3r/GUI/UpgradePanel.cpp b/src/slic3r/GUI/UpgradePanel.cpp index 96b9c1b481..d31fde8613 100644 --- a/src/slic3r/GUI/UpgradePanel.cpp +++ b/src/slic3r/GUI/UpgradePanel.cpp @@ -862,7 +862,6 @@ void MachineInfoPanel::update_ams_ext(MachineObject *obj) if (new_extra_ams_ver != obj->new_ver_list.end()) has_new_version = true; - extra_ams_it->second.sw_new_ver; if (has_new_version) { m_extra_ams_panel->m_ams_new_version_img->Show(); ver_text = new_extra_ams_ver->second.sw_ver; diff --git a/src/slic3r/GUI/WebGuideDialog.cpp b/src/slic3r/GUI/WebGuideDialog.cpp index 0d2f6c724b..ae259f23d1 100644 --- a/src/slic3r/GUI/WebGuideDialog.cpp +++ b/src/slic3r/GUI/WebGuideDialog.cpp @@ -1,7 +1,9 @@ #include "WebGuideDialog.hpp" #include "ConfigWizard.hpp" +#include <boost/algorithm/string/join.hpp> #include <boost/filesystem/operations.hpp> +#include <boost/nowide/fstream.hpp> #include <boost/filesystem/path.hpp> #include <boost/iostreams/detail/select.hpp> #include <boost/log/trivial.hpp> @@ -9,7 +11,9 @@ #include "I18N.hpp" #include "libslic3r/AppConfig.hpp" #include "libslic3r/Config.hpp" +#include "libslic3r/Preset.hpp" #include "libslic3r/PresetBundle.hpp" +#include "libslic3r/PresetCacheFormat.hpp" #include "slic3r/GUI/wxExtensions.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "libslic3r_version.h" @@ -41,8 +45,6 @@ using namespace nlohmann; namespace Slic3r { namespace GUI { -json m_ProfileJson; - static wxString update_custom_filaments() { json m_Res = json::object(); @@ -190,12 +192,10 @@ GuideFrame::GuideFrame(GUI_App *pGUI, long style) GuideFrame::~GuideFrame() { - m_destroy = true; - if (m_load_task && m_load_task->joinable()) { + *m_cancel_token = true; // stop the loading thread and any queued CallAfter lambdas before join + if (m_load_task && m_load_task->joinable()) m_load_task->join(); - delete m_load_task; - m_load_task = nullptr; - } + m_load_task.reset(); if (m_browser) { delete m_browser; m_browser = nullptr; @@ -301,15 +301,71 @@ void GuideFrame::OnNavigationRequest(wxWebViewEvent &evt) /** * Callback invoked when a navigation request was accepted */ +// The empty shape every profile-loading path starts from or falls back to. +void GuideFrame::reset_profile_json() +{ + m_ProfileJson["model"] = json::array(); + m_ProfileJson["machine"] = json::object(); + m_ProfileJson["filament"] = json::object(); + m_ProfileJson["process"] = json::array(); +} + +void GuideFrame::init_guide_paths() +{ + m_ProfileJson = json::parse("{}"); + reset_profile_json(); + + vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred(); + rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); + orca_bundle_rsrc = true; + + if (boost::filesystem::exists(vendor_dir)) { + for (const auto& entry : boost::filesystem::directory_iterator(vendor_dir)) { + if (!boost::filesystem::is_directory(entry) && + boost::iequals(entry.path().extension().string(), ".json") && + !boost::iequals(entry.path().stem().string(), PresetBundle::ORCA_FILAMENT_LIBRARY)) { + orca_bundle_rsrc = false; + break; + } + } + } + + auto lib_json = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json"); + m_OrcaFilaLibPath = boost::filesystem::exists(vendor_dir / lib_json) + ? (vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string() + : (rsrc_vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string(); +} + +void GuideFrame::on_profile_loaded() +{ + // Must be called on the main thread. + SaveProfileData(); + const std::string strAll = m_ProfileJson.dump(-1, ' ', false, json::error_handler_t::ignore); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished, json contents:\n" << strAll; + json res; + res["command"] = "userguide_profile_load_finish"; + res["sequence_id"] = "10001"; + RunScript(wxString::Format("HandleStudio(%s)", res.dump(-1, ' ', true))); +} + void GuideFrame::OnNavigationComplete(wxWebViewEvent &evt) { //wxLogMessage("%s", "Navigation complete; url='" + evt.GetURL() + "'"); if (!bFirstComplete) { - m_load_task = new boost::thread(boost::bind(&GuideFrame::LoadProfileData, this)); - // boost::thread LoadProfileThread(boost::bind(&GuideFrame::LoadProfileData, this)); - //LoadProfileThread.detach(); - bFirstComplete = true; + try { + init_guide_paths(); + if (BuildProfileDataFromPresetBundle()) { + if (!*m_cancel_token) + on_profile_loaded(); + } else { + // Presets not yet in memory — delegate to background thread. + m_load_task = std::make_unique<boost::thread>(boost::bind(&GuideFrame::LoadProfileData, this)); + } + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", init error: " << e.what(); + m_load_task = std::make_unique<boost::thread>(boost::bind(&GuideFrame::LoadProfileData, this)); + } } m_browser->Show(); @@ -532,7 +588,7 @@ void GuideFrame::OnScriptMessage(wxWebViewEvent &evt) else if (strCmd == "user_guide_create_printer") { this->EndModal(wxID_CANCEL); this->Close(); - GUI::wxGetApp().CallAfter([this] {GUI::wxGetApp().sidebar().create_printer_preset();}); + GUI::wxGetApp().CallAfter([] {GUI::wxGetApp().sidebar().create_printer_preset();}); } else if (strCmd == "user_guide_cancel") { this->EndModal(wxID_CANCEL); @@ -762,11 +818,9 @@ bool GuideFrame::apply_config(AppConfig *app_config, PresetBundle *preset_bundle bool check_unsaved_preset_changes = false; std::vector<std::string> install_bundles; std::vector<std::string> remove_bundles; - const auto vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred(); for (const auto &it : enabled_vendors) { if (it.second.size() > 0) { - auto vendor_file = vendor_dir/(it.first + ".json"); - if (!fs::exists(vendor_file)) { + if (!is_vendor_installed(it.first)) { install_bundles.emplace_back(it.first); } } @@ -777,8 +831,7 @@ bool GuideFrame::apply_config(AppConfig *app_config, PresetBundle *preset_bundle if (it.second.size() > 0) { if (enabled_vendors.find(it.first) != enabled_vendors.end()) continue; - auto vendor_file = vendor_dir/(it.first + ".json"); - if (fs::exists(vendor_file)) { + if (is_vendor_installed(it.first)) { remove_bundles.emplace_back(it.first); } } @@ -818,7 +871,7 @@ bool GuideFrame::apply_config(AppConfig *app_config, PresetBundle *preset_bundle std::string preferred_model; std::string preferred_variant; PrinterTechnology preferred_pt = ptFFF; - auto get_preferred_printer_model = [preset_bundle, enabled_vendors, old_enabled_vendors, preferred_pt](const std::string& bundle_name, std::string& variant) { + auto get_preferred_printer_model = [preset_bundle, enabled_vendors, old_enabled_vendors](const std::string& bundle_name, std::string& variant) { const auto config = enabled_vendors.find(bundle_name); if (config == enabled_vendors.end()) return std::string(); @@ -1127,99 +1180,324 @@ int GuideFrame::GetFilamentInfo( std::string VendorDirectory, json & pFilaList, return status; } -int GuideFrame::LoadProfileData() +bool GuideFrame::BuildProfileJson(const PresetBundle& bundle, bool require_all_resource_vendors) { try { - m_ProfileJson = json::parse("{}"); - m_ProfileJson["model"] = json::array(); - m_ProfileJson["machine"] = json::object(); - m_ProfileJson["filament"] = json::object(); - m_ProfileJson["process"] = json::array(); + // Models from vendor profiles + for (const auto& [vendor_id, vp] : bundle.vendors) { + for (const auto& model : vp.models) { + std::string nozzle_str; + for (const auto& v : model.variants) { + if (!nozzle_str.empty()) nozzle_str += ";"; + nozzle_str += v.name; + } + const std::string materials_str = boost::algorithm::join(model.default_materials, ";"); + boost::filesystem::path cover_path = + (boost::filesystem::path(resources_dir()) / "profiles" / vp.id / (model.id + "_cover.png")) + .make_preferred(); + if (!boost::filesystem::exists(cover_path)) + cover_path = + (boost::filesystem::path(resources_dir()) / "web/image/printer" / (model.id + "_cover.png")) + .make_preferred(); - vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred(); - rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); - - // Orca: add custom as default - // Orca: add json logic for vendor bundle - orca_bundle_rsrc = true; - - // search if there exists a .json file in vendor_dir folder, if exists, set orca_bundle_rsrc to false - for (const auto& entry : boost::filesystem::directory_iterator(vendor_dir)) { - if (!boost::filesystem::is_directory(entry) && boost::iequals(entry.path().extension().string(), ".json") && !boost::iequals(entry.path().stem().string(), PresetBundle::ORCA_FILAMENT_LIBRARY)) { - orca_bundle_rsrc = false; - break; + json entry; + entry["model"] = model.id; + entry["name"] = model.name; + entry["vendor"] = vp.id; + entry["nozzle_diameter"] = nozzle_str; + entry["materials"] = materials_str; + entry["cover"] = cover_path.string(); + entry["nozzle_selected"] = ""; + entry["sub_path"] = ""; + m_ProfileJson["model"].push_back(entry); } } - // load the default filament library first - std::set<std::string> loaded_vendors; - auto filament_library_name = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json"); - if (boost::filesystem::exists(vendor_dir / filament_library_name)) { - m_OrcaFilaLibPath = (vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string(); - LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (vendor_dir / filament_library_name).string()); - } else { - m_OrcaFilaLibPath = (rsrc_vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string(); - LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (rsrc_vendor_dir / filament_library_name).string()); - } - loaded_vendors.insert(PresetBundle::ORCA_FILAMENT_LIBRARY); + // Machine map: preset name -> {model, nozzle variant} + for (const Preset& p : bundle.printers()) { + if (!p.is_system || !p.vendor) continue; + const auto* printer_model = p.config.option<ConfigOptionString>("printer_model"); + const auto* printer_variant = p.config.option<ConfigOptionString>("printer_variant"); + if (!printer_model || printer_model->value.empty() || !printer_variant) continue; - //load custom bundle from user data path - boost::filesystem::directory_iterator endIter; - for (boost::filesystem::directory_iterator iter(vendor_dir); iter != endIter; iter++) { - if (!boost::filesystem::is_directory(*iter)) { - wxString strVendor = from_u8(iter->path().string()).BeforeLast('.'); - strVendor = strVendor.AfterLast('\\'); - strVendor = strVendor.AfterLast('/'); - - wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower(); - if(strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end()) - continue; - - LoadProfileFamily(w2s(strVendor), iter->path().string()); - loaded_vendors.insert(w2s(strVendor)); - } - if (m_destroy) - return 0; + json mach; + mach["model"] = printer_model->value; + mach["nozzle"] = printer_variant->value; + m_ProfileJson["machine"][p.name] = mach; } - boost::filesystem::directory_iterator others_endIter; - for (boost::filesystem::directory_iterator iter(rsrc_vendor_dir); iter != others_endIter; iter++) { - if (!boost::filesystem::is_directory(*iter)) { - wxString strVendor = from_u8(iter->path().string()).BeforeLast('.'); - strVendor = strVendor.AfterLast('\\'); - strVendor = strVendor.AfterLast('/'); - wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower(); - if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end()) - continue; + // Filament map from system filament presets (vendor/type already resolved in config) + const json& machines = m_ProfileJson["machine"]; + for (const Preset& p : bundle.filaments()) { + if (!p.is_system || !p.vendor) continue; + const auto* fila_vendor = p.config.option<ConfigOptionStrings>("filament_vendor"); + const auto* fila_type = p.config.option<ConfigOptionStrings>("filament_type"); + const auto* compat_printers = p.config.option<ConfigOptionStrings>("compatible_printers"); - LoadProfileFamily(w2s(strVendor), iter->path().string()); - loaded_vendors.insert(w2s(strVendor)); + std::string vendor = (fila_vendor && !fila_vendor->values.empty()) ? fila_vendor->values[0] : ""; + std::string type = (fila_type && !fila_type->values.empty()) ? fila_type->values[0] : ""; + + std::string model_list; + if (compat_printers) { + for (const std::string& pname : compat_printers->values) { + auto it = machines.find(pname); + if (it != machines.end()) { + const std::string m = (*it)["model"]; + const std::string n = (*it)["nozzle"]; + model_list += "[" + m + "++" + n + "]"; + } + } } - if (m_destroy) - return 0; + + json ff; + ff["name"] = p.name; + ff["sub_path"] = p.file; + ff["vendor"] = vendor; + ff["type"] = type; + ff["models"] = model_list; + ff["selected"] = 0; + m_ProfileJson["filament"][p.name] = ff; } - wxGetApp().CallAfter([this] { - if (!m_destroy) { - //sync to appconfig first to populate current selections - SaveProfileData(); + // Process list from visible system print presets + for (const Preset& p : bundle.prints()) { + if (!p.is_system || !p.vendor || !p.is_visible) continue; + json entry; + entry["name"] = p.name; + entry["sub_path"] = p.file; + m_ProfileJson["process"].push_back(entry); + } - //sync to web after selections are populated - std::string strAll = m_ProfileJson.dump(-1, ' ', false, json::error_handler_t::ignore); + if (require_all_resource_vendors) { + // If rsrc_vendor_dir has vendors (profile JSONs, or the preset caches a + // packaged build ships instead) not covered by the current bundle, the + // bundle is incomplete (e.g. dev env where data_dir/system only has + // OrcaFilamentLibrary+Custom). Fall back so the slow path reads both dirs. + try { + for (const std::string& name : vendor_names_in(rsrc_vendor_dir)) { + if (bundle.vendors.find(name) == bundle.vendors.end()) { + BOOST_LOG_TRIVIAL(info) << "GuideFrame: vendor '" << name + << "' in resources but not in preset_bundle — falling back to JSON loading"; + reset_profile_json(); + return false; + } + } + } catch (const std::exception&) {} + } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished, json contents: " << std::endl << strAll; - json m_Res = json::object(); - m_Res["command"] = "userguide_profile_load_finish"; - m_Res["sequence_id"] = "10001"; - wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', true)); + BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data (" + << m_ProfileJson["model"].size() << " models, " + << m_ProfileJson["machine"].size() << " machines, " + << m_ProfileJson["filament"].size() << " filaments)"; + return !m_ProfileJson["machine"].empty(); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileJson failed: " << e.what() + << " — falling back to JSON loading"; + reset_profile_json(); + return false; + } +} - RunScript(strJS); +bool GuideFrame::BuildProfileDataFromPresetBundle() +{ + PresetBundle* pb = wxGetApp().preset_bundle; + if (!pb || pb->vendors.empty()) + return false; + return BuildProfileJson(*pb, /*require_all_resource_vendors=*/true); +} + +bool GuideFrame::BuildProfileDataFromVendors() +{ + try { + // Same vendor set and precedence as the JSON scan in LoadProfileData: a + // vendor in the user's system dir shadows the bundled one of that name. + // vendor_names_in names a vendor by its profile or, where a build ships + // preset caches instead, by its cache alone. + std::map<std::string, boost::filesystem::path> vendor_sources; + for (const boost::filesystem::path& dir : { vendor_dir, rsrc_vendor_dir }) { + boost::system::error_code ec; + if (boost::filesystem::exists(dir, ec)) + for (const std::string& name : vendor_names_in(dir)) + vendor_sources.emplace(name, dir); // first dir wins + } + + // The load order: the filament library first, because the others' + // filaments inherit from it, then every versioned vendor — each loaded + // from the directory it was found in, so a vendor that is not installed + // is served from the shipped profiles. Each is stamped by name and + // version alone: a profile change requires a version bump, so those two + // determine content wherever the vendor's copy sits. + struct VendorSource { std::string name; boost::filesystem::path dir; std::string version; }; + std::vector<VendorSource> ordered; + auto add_vendor = [&ordered](const std::string& name, const boost::filesystem::path& dir) { + // The version a load from `dir` would serve: the profile's where one + // exists (a cache is only served while it covers the profile beside + // it), the cache's own stamp where the cache is the whole vendor. + // A profile without a version (blacklist.json) carries no presets + // and is passed over. + const boost::filesystem::path profile = dir / (name + ".json"); + if (boost::filesystem::exists(profile)) { + const Semver v = get_version_from_json(profile.string()); + if (v.valid()) + ordered.push_back({name, dir, v.to_string()}); + } else { + ordered.push_back({name, dir, + VendorCacheFile::peek_version((dir / (name + ".opc")).string(), name)}); } + }; + const std::string filament_library(PresetBundle::ORCA_FILAMENT_LIBRARY); + if (auto it = vendor_sources.find(filament_library); it != vendor_sources.end()) + add_vendor(filament_library, it->second); + for (const auto& [name, dir] : vendor_sources) + if (name != filament_library) + add_vendor(name, dir); + if (ordered.empty()) + return false; + json stamps = json::array(); + for (const VendorSource& v : ordered) + stamps.push_back({v.name, v.version}); + + // What this function derives is a pure function of that stamped set, so + // the derived JSON is cached whole: a fresh cache makes an open one + // file read, with no bundle built and no preset installed. Stale or + // absent, the bundle is rebuilt below and the result written back. + const boost::filesystem::path cache_file = + boost::filesystem::path(Slic3r::data_dir()) / "cache" / "wizard_profile_data.json"; + try { + // Slurped whole and parsed from the buffer — nlohmann's fastest + // input path; a stream adapter costs real time on a multi-MB file. + boost::nowide::ifstream ifs(cache_file.string(), std::ios::binary); + if (ifs.is_open()) { + const std::string text{std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>()}; + json cached = json::parse(text); + if (cached.value("format", 0) == 1 && cached["vendors"] == stamps && + ! cached["profile"]["machine"].empty()) { + for (const char* key : { "model", "machine", "filament", "process" }) + m_ProfileJson[key] = std::move(cached["profile"][key]); + BOOST_LOG_TRIVIAL(info) << "GuideFrame: profile data served from " << cache_file; + return true; + } + } + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(info) << "GuideFrame: rejecting cached profile data: " << e.what(); + } + + // Each vendor comes from its preset cache where one covers it, which is + // what makes this worth doing instead of the scan below; loading into a + // bundle per vendor keeps the install order the startup path has. + PresetBundle bundle; + auto load_vendor = [](PresetBundle& into, const std::string& vendor, + const boost::filesystem::path& dir, const PresetBundle* base) { + into.load_vendor_configs_from_json(dir.string(), vendor, PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent, base); + }; + for (const VendorSource& v : ordered) { + if (*m_cancel_token) + return false; // as in the scan below: a vendor without a cache is parsed, and that takes time + if (v.name == filament_library) { + load_vendor(bundle, v.name, v.dir, nullptr); + } else { + PresetBundle tmp; + load_vendor(tmp, v.name, v.dir, &bundle); + bundle.merge_presets(std::move(tmp)); + } + } + if (bundle.vendors.empty()) + return false; + if (! BuildProfileJson(bundle, /*require_all_resource_vendors=*/false)) + return false; + + // Written through a temp file and moved into place, as the preset caches + // are: half a cache must never be readable, and the PID suffix keeps two + // instances from interleaving on one temp file. + const std::string tmp_path = cache_file.string() + "." + std::to_string(get_current_pid()) + ".tmp"; + try { + json out; + out["format"] = 1; + out["vendors"] = std::move(stamps); + json& profile = out["profile"]; + for (const char* key : { "model", "machine", "filament", "process" }) + profile[key] = m_ProfileJson[key]; + boost::filesystem::create_directories(cache_file.parent_path()); + { + boost::nowide::ofstream ofs(tmp_path, std::ios::binary | std::ios::trunc); + ofs << out.dump(-1, ' ', false, json::error_handler_t::ignore); + ofs.close(); + if (! ofs.good()) + throw std::runtime_error("write failed"); + } + if (const std::error_code ec = rename_file(tmp_path, cache_file.string())) + throw std::runtime_error(ec.message()); + } catch (const std::exception& e) { + boost::system::error_code rm; + boost::filesystem::remove(tmp_path, rm); + BOOST_LOG_TRIVIAL(warning) << "GuideFrame: could not write the profile data cache: " << e.what(); + } + return true; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed: " << e.what(); + reset_profile_json(); + return false; + } +} + +int GuideFrame::LoadProfileData() +{ + // Background thread: the fast path in OnNavigationComplete failed (presets not yet loaded). + // Loading order (fastest to slowest): + // 1. Load every vendor, from its preset cache wherever one covers it + // 2. Read all vendor JSONs by hand + try { + if (!BuildProfileDataFromVendors()) { + // Last resort — read all vendor JSONs + std::set<std::string> loaded_vendors; + auto filament_library_name = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json"); + if (boost::filesystem::exists(vendor_dir / filament_library_name)) + LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (vendor_dir / filament_library_name).string()); + else + LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (rsrc_vendor_dir / filament_library_name).string()); + loaded_vendors.insert(PresetBundle::ORCA_FILAMENT_LIBRARY); + + boost::filesystem::directory_iterator endIter; + for (boost::filesystem::directory_iterator iter(vendor_dir); iter != endIter; iter++) { + if (!boost::filesystem::is_directory(*iter)) { + wxString strVendor = from_u8(iter->path().string()).BeforeLast('.'); + strVendor = strVendor.AfterLast('\\'); + strVendor = strVendor.AfterLast('/'); + wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower(); + if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end()) + continue; + LoadProfileFamily(w2s(strVendor), iter->path().string()); + loaded_vendors.insert(w2s(strVendor)); + } + if (*m_cancel_token) return 0; + } + + boost::filesystem::directory_iterator others_endIter; + for (boost::filesystem::directory_iterator iter(rsrc_vendor_dir); iter != others_endIter; iter++) { + if (!boost::filesystem::is_directory(*iter)) { + wxString strVendor = from_u8(iter->path().string()).BeforeLast('.'); + strVendor = strVendor.AfterLast('\\'); + strVendor = strVendor.AfterLast('/'); + wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower(); + if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end()) + continue; + LoadProfileFamily(w2s(strVendor), iter->path().string()); + loaded_vendors.insert(w2s(strVendor)); + } + if (*m_cancel_token) return 0; + } + } + + // Capture the cancel token by value (shared_ptr) so the lambda doesn't + // touch `this` if GuideFrame is destroyed before the event fires. + auto tok = m_cancel_token; + wxGetApp().CallAfter([this, tok] { + if (!*tok) + on_profile_loaded(); }); - } catch (std::exception& e) { - // wxLogMessage("GUIDE: load_profile_error %s ", e.what()); - // wxMessageBox(e.what(), "", MB_OK); - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", error: " << e.what() << std::endl; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", error: " << e.what(); } filament_info_cache.clear(); diff --git a/src/slic3r/GUI/WebGuideDialog.hpp b/src/slic3r/GUI/WebGuideDialog.hpp index fcdb0841db..c4cfc8bf6d 100644 --- a/src/slic3r/GUI/WebGuideDialog.hpp +++ b/src/slic3r/GUI/WebGuideDialog.hpp @@ -30,10 +30,14 @@ #include "libslic3r/PresetBundle.hpp" #include "slic3r/Utils/PresetUpdater.hpp" +#include <atomic> +#include <memory> #include <unordered_map> #include <nlohmann/json.hpp> +#include <boost/thread.hpp> + namespace Slic3r { namespace GUI { class GuideFrame : public DPIDialog @@ -78,6 +82,12 @@ public: int LoadProfileData(); int SaveProfileData(); int LoadProfileFamily(std::string strVendor, std::string strFilePath); + void init_guide_paths(); + void on_profile_loaded(); + bool BuildProfileJson(const PresetBundle& bundle, bool require_all_resource_vendors); + bool BuildProfileDataFromPresetBundle(); + bool BuildProfileDataFromVendors(); + void reset_profile_json(); int SaveProfile(); int GetFilamentInfo( std::string VendorDirectory,json & pFilaList, std::string filepath, std::string &sVendor, std::string &sType); @@ -102,7 +112,6 @@ private: AppConfig m_appconfig_new; wxWebView *m_browser; - wxButton * m_TestBtn; wxString m_SectionName; @@ -112,8 +121,11 @@ private: //First Load bool bFirstComplete{false}; - bool m_destroy{false}; - boost::thread* m_load_task{ nullptr }; + // Set once in the destructor. Read through `this` by the loading thread + // (joined before `this` dies) and captured as the shared_ptr by CallAfter + // lambdas so they don't touch `this` after the object is freed. + std::shared_ptr<std::atomic<bool>> m_cancel_token{std::make_shared<std::atomic<bool>>(false)}; + std::unique_ptr<boost::thread> m_load_task; // User Config bool PrivacyUse; @@ -123,6 +135,7 @@ private: bool InstallNetplugin; bool network_plugin_ready {false}; + json m_ProfileJson; json m_OrcaFilaList; std::string m_OrcaFilaLibPath; diff --git a/src/slic3r/GUI/WebUserLoginDialog.cpp b/src/slic3r/GUI/WebUserLoginDialog.cpp index 4222ce745b..829e7ef106 100644 --- a/src/slic3r/GUI/WebUserLoginDialog.cpp +++ b/src/slic3r/GUI/WebUserLoginDialog.cpp @@ -489,7 +489,7 @@ void ZUserLogin::OnScriptMessage(wxWebViewEvent &evt) std::string jump_url = j["data"]["url"].get<std::string>(); int loopback_port = ensure_loopback_port(); jump_url = rewrite_loopback_url(jump_url, loopback_port); - CallAfter([this, jump_url] { + CallAfter([jump_url] { wxString url = wxString::FromUTF8(jump_url); wxLaunchDefaultBrowser(url); }); @@ -498,7 +498,7 @@ void ZUserLogin::OnScriptMessage(wxWebViewEvent &evt) else if (strCmd == "new_webpage") { if (j["data"].contains("url")) { std::string jump_url = j["data"]["url"].get<std::string>(); - CallAfter([this, jump_url] { + CallAfter([jump_url] { wxString url = wxString::FromUTF8(jump_url); wxLaunchDefaultBrowser(url); }); diff --git a/src/slic3r/GUI/WebViewDialog.hpp b/src/slic3r/GUI/WebViewDialog.hpp index 23b3c711a1..123025ef85 100644 --- a/src/slic3r/GUI/WebViewDialog.hpp +++ b/src/slic3r/GUI/WebViewDialog.hpp @@ -125,10 +125,6 @@ private: wxMenuItem* m_edit_undo; wxMenuItem* m_edit_redo; wxMenuItem* m_edit_mode; - wxMenuItem* m_scroll_line_up; - wxMenuItem* m_scroll_line_down; - wxMenuItem* m_scroll_page_up; - wxMenuItem* m_scroll_page_down; wxMenuItem* m_script_string; wxMenuItem* m_script_integer; wxMenuItem* m_script_double; diff --git a/src/slic3r/GUI/Widgets/AMSControl.cpp b/src/slic3r/GUI/Widgets/AMSControl.cpp index efcca12a05..41f14b6a11 100644 --- a/src/slic3r/GUI/Widgets/AMSControl.cpp +++ b/src/slic3r/GUI/Widgets/AMSControl.cpp @@ -984,7 +984,7 @@ void AMSControl::UpdateAms(const std::string &series_name, if (cans->get_ams_id() == std::to_string(VIRTUAL_TRAY_MAIN_ID) || cans->get_ams_id() == std::to_string(VIRTUAL_TRAY_DEPUTY_ID)) { for (auto ifo : m_ext_info) { if (ifo.ams_id == ams_id) { - cans->Update(ifo); + cans->UpdateInfo(ifo); cans->show_sn_value(m_ams_model == AMSModel::AMS_LITE ? false : true); } } @@ -992,7 +992,7 @@ void AMSControl::UpdateAms(const std::string &series_name, else{ for (auto ifo : m_ams_info) { if (ifo.ams_id == ams_id) { - cans->Update(ifo); + cans->UpdateInfo(ifo); cans->show_sn_value(m_ams_model == AMSModel::AMS_LITE ? false : true); } } @@ -1015,7 +1015,7 @@ void AMSControl::UpdateAms(const std::string &series_name, std::string id = ams_prv.second->get_ams_id(); auto item = m_ams_item_list.find(id); if (item != m_ams_item_list.end()) - { ams_prv.second->Update(item->second->get_ams_info()); + { ams_prv.second->UpdateInfo(item->second->get_ams_info()); } } } diff --git a/src/slic3r/GUI/Widgets/AMSItem.cpp b/src/slic3r/GUI/Widgets/AMSItem.cpp index b6f335b580..9d5ec9c1c1 100644 --- a/src/slic3r/GUI/Widgets/AMSItem.cpp +++ b/src/slic3r/GUI/Widgets/AMSItem.cpp @@ -114,7 +114,6 @@ bool AMSinfo::parse_ams_info(MachineObject *obj, DevAms *ams, bool remain_flag, info.ctype = 0; info.material_colour = AMS_TRAY_DEFAULT_COL; info.material_state = AMSCanType::AMS_CAN_TYPE_THIRDBRAND; - wxColour(255, 255, 255); } if (it->second->is_tray_info_ready() && obj->cali_version >= 0) { @@ -171,7 +170,6 @@ void AMSinfo::parse_ext_info(MachineObject* obj, DevAmsTray tray) { info.filament_id = ""; info.ctype = 0; info.material_colour = AMS_TRAY_DEFAULT_COL; - wxColour(255, 255, 255); } info.material_state = AMSCanType::AMS_CAN_TYPE_VIRTUAL; if (tray.is_tray_info_ready() && obj->cali_version >= 0) { @@ -325,7 +323,7 @@ AMSrefresh::AMSrefresh(wxWindow *parent, std::string ams_id, wxString can_id, Ca m_can_id = can_id.ToStdString(); create(parent, wxID_ANY, pos, size); - Update(ams_id, info); + UpdateInfo(ams_id, info); } AMSrefresh::AMSrefresh(wxWindow *parent, std::string ams_id, int can_id, Caninfo info, const wxPoint &pos, const wxSize &size) : AMSrefresh() @@ -333,7 +331,7 @@ AMSrefresh::AMSrefresh(wxWindow *parent, std::string ams_id, int can_id, Caninfo m_can_id = wxString::Format("%d", can_id).ToStdString(); create(parent, wxID_ANY, pos, size); - Update(ams_id, info); + UpdateInfo(ams_id, info); } AMSrefresh::~AMSrefresh() @@ -482,7 +480,7 @@ void AMSrefresh::paintEvent(wxPaintEvent &evt) dc.DrawText(m_refresh_id, pot); } -void AMSrefresh::Update(std::string ams_id, Caninfo info) +void AMSrefresh::UpdateInfo(std::string ams_id, Caninfo info) { if (m_ams_id == ams_id && m_info == info) { @@ -945,7 +943,7 @@ AMSLib::AMSLib(wxWindow *parent, std::string ams_idx, Caninfo info, AMSModelOrig Bind(wxEVT_LEAVE_WINDOW, &AMSLib::on_leave_window, this); Bind(wxEVT_LEFT_DOWN, &AMSLib::on_left_down, this); - Update(info, ams_idx, false); + UpdateInfo(info, ams_idx, false); } AMSLib::~AMSLib() @@ -1730,7 +1728,7 @@ void AMSLib::on_pass_road(bool pass) } } -void AMSLib::Update(Caninfo info, std::string ams_idx, bool refresh) +void AMSLib::UpdateInfo(Caninfo info, std::string ams_idx, bool refresh) { DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); if (!dev) return; @@ -1868,7 +1866,7 @@ AMSRoad::AMSRoad(wxWindow *parent, wxWindowID id, Caninfo info, int canindex, in void AMSRoad::create(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size) { wxWindow::Create(parent, id, pos, size); } -void AMSRoad::Update(AMSinfo amsinfo, Caninfo info, int canindex, int maxcan) +void AMSRoad::UpdateInfo(AMSinfo amsinfo, Caninfo info, int canindex, int maxcan) { m_amsinfo = amsinfo; m_info = info; @@ -2083,9 +2081,6 @@ void AMSRoad::OnPassRoad(std::vector<AMSPassRoadMode> prord_list) } } -/* - - /************************************************* Description:AMSRoadUpPart **************************************************/ @@ -2124,7 +2119,7 @@ void AMSRoadUpPart::create(wxWindow* parent, wxWindowID id, const wxPoint& pos, Refresh(); } -void AMSRoadUpPart::Update(AMSinfo amsinfo) +void AMSRoadUpPart::UpdateInfo(AMSinfo amsinfo) { if (m_amsinfo != amsinfo) { @@ -2616,7 +2611,7 @@ void AMSPreview::Close() Hide(); } -void AMSPreview::Update(AMSinfo amsinfo) +void AMSPreview::UpdateInfo(AMSinfo amsinfo) { if (m_amsinfo == amsinfo) { @@ -2954,7 +2949,7 @@ AMSHumidity::AMSHumidity(wxWindow* parent, wxWindowID id, AMSinfo info, const wx } }); - Update(info); + UpdateInfo(info); } void AMSHumidity::create(wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size) { @@ -2963,7 +2958,7 @@ void AMSHumidity::create(wxWindow* parent, wxWindowID id, const wxPoint& pos, co } -void AMSHumidity::Update(AMSinfo amsinfo) +void AMSHumidity::UpdateInfo(AMSinfo amsinfo) { if (m_amsinfo != amsinfo) { @@ -3380,7 +3375,7 @@ void AmsItem::AddLiteCan(Caninfo caninfo, int canindex, wxGridSizer* sizer) //m_can_road_list[caninfo.can_id] = m_panel_road; } -void AmsItem::Update(AMSinfo info) +void AmsItem::UpdateInfo(AMSinfo info) { if (m_info == info) { @@ -3392,7 +3387,7 @@ void AmsItem::Update(AMSinfo info) if (m_humidity) { - m_humidity->Update(m_info); + m_humidity->UpdateInfo(m_info); } for (int i = 0; i < m_can_count; i++) { @@ -3401,7 +3396,7 @@ void AmsItem::Update(AMSinfo info) auto refresh = it->second; if (refresh != nullptr){ - refresh->Update(info.ams_id, info.cans[i]); + refresh->UpdateInfo(info.ams_id, info.cans[i]); refresh->Show(); } } @@ -3410,7 +3405,7 @@ void AmsItem::Update(AMSinfo info) AMSLib* lib = m_can_lib_list[std::to_string(i)]; if (lib != nullptr){ if (i < m_can_count){ - lib->Update(info.cans[i], info.ams_id); + lib->UpdateInfo(info.cans[i], info.ams_id); lib->Show(); } else{ @@ -3419,12 +3414,7 @@ void AmsItem::Update(AMSinfo info) } } if (m_panel_road != nullptr){ - m_panel_road->Update(m_info); - } - - if (true || m_ams_model == AMSModel::GENERIC_AMS) { - /*m_panel_road->Update(m_info, info.cans[0]); - m_panel_road->Show();*/ + m_panel_road->UpdateInfo(m_info); } Layout(); diff --git a/src/slic3r/GUI/Widgets/AMSItem.hpp b/src/slic3r/GUI/Widgets/AMSItem.hpp index bed57e7d39..c43c71f580 100644 --- a/src/slic3r/GUI/Widgets/AMSItem.hpp +++ b/src/slic3r/GUI/Widgets/AMSItem.hpp @@ -312,7 +312,7 @@ public: ~AMSrefresh(); public: - void Update(std::string ams_id, Caninfo info); + void UpdateInfo(std::string ams_id, Caninfo info); std::string GetCanId() const { return m_info.can_id; }; @@ -492,7 +492,7 @@ public: AMSModel m_ams_model; AMSModelOriginType m_ext_type = { AMSModelOriginType::GENERIC_EXT }; - void Update(Caninfo info, std::string ams_idx, bool refresh = true); + void UpdateInfo(Caninfo info, std::string ams_idx, bool refresh = true); void UnableSelected() { m_unable_selected = true; }; void EableSelected() { m_unable_selected = false; }; void OnSelected(); @@ -581,7 +581,7 @@ public: double m_radius = {4}; wxColour m_road_def_color; wxColour m_road_color; - void Update(AMSinfo amsinfo, Caninfo info, int canindex, int maxcan); + void UpdateInfo(AMSinfo amsinfo, Caninfo info, int canindex, int maxcan); std::vector<ScalableBitmap> ams_humidity_img; @@ -614,7 +614,7 @@ public: void create(wxWindow* parent, wxWindowID id = wxID_ANY, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize); public: - void Update(AMSinfo amsinfo); + void UpdateInfo(AMSinfo amsinfo); void OnVamsLoading(bool load, wxColour col = AMS_CONTROL_GRAY500); void SetPassRoadColour(wxColour col); @@ -684,7 +684,6 @@ public: private: int m_nozzle_num = {1}; - AMSRoadShowMode m_single_ext_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR}; AMSRoadShowMode m_left_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR}; AMSRoadShowMode m_right_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR}; bool m_selected = {false}; @@ -693,7 +692,6 @@ private: int m_right_road_length = {-1}; int m_passroad_width = {6}; double m_radius = {4}; - AMSPassRoadType m_pass_road_type = {AMSPassRoadType::AMS_ROAD_TYPE_NONE}; AMSPassRoadSTEP m_pass_road_left_step = {AMSPassRoadSTEP::AMS_ROAD_STEP_NONE}; AMSPassRoadSTEP m_pass_road_right_step = {AMSPassRoadSTEP::AMS_ROAD_STEP_NONE}; @@ -715,7 +713,7 @@ public: void Open(); void Close(); - void Update(AMSinfo amsinfo); + void UpdateInfo(AMSinfo amsinfo); void create(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size); void OnEnterWindow(wxMouseEvent &evt); void OnLeaveWindow(wxMouseEvent &evt); @@ -768,7 +766,7 @@ public: int m_canindex = { 0 }; bool m_selected = { false }; double m_radius = { 12 }; - void Update(AMSinfo amsinfo); + void UpdateInfo(AMSinfo amsinfo); std::vector<ScalableBitmap> ams_humidity_imgs; std::vector<ScalableBitmap> ams_humidity_dark_imgs; @@ -801,7 +799,7 @@ public: AmsItem(wxWindow *parent, AMSinfo info, AMSModel model, AMSPanelPos pos); ~AmsItem(); - void Update(AMSinfo info); + void UpdateInfo(AMSinfo info); void create(wxWindow *parent); void AddCan(Caninfo caninfo, int canindex, int maxcan, wxBoxSizer* sizer); void AddLiteCan(Caninfo caninfo, int canindex, wxGridSizer* sizer); @@ -866,11 +864,7 @@ private: AMSinfo m_info; wxBoxSizer * sizer_can = {nullptr}; wxGridSizer* sizer_can_extra = { nullptr }; - wxBoxSizer * sizer_humidity = { nullptr }; wxBoxSizer * sizer_item = { nullptr }; - wxBoxSizer * sizer_can_middle = {nullptr}; - wxBoxSizer * sizer_can_left = {nullptr}; - wxBoxSizer * sizer_can_right = {nullptr}; AMSExtImage* m_ext_image = { nullptr }; //the ext image upon the ext ams AMSExtText* m_ext_text = { nullptr }; //the ext text upon the ext ams }; diff --git a/src/slic3r/GUI/Widgets/AxisCtrlButton.cpp b/src/slic3r/GUI/Widgets/AxisCtrlButton.cpp index ee0608ffbe..535abfcaf2 100644 --- a/src/slic3r/GUI/Widgets/AxisCtrlButton.cpp +++ b/src/slic3r/GUI/Widgets/AxisCtrlButton.cpp @@ -124,7 +124,7 @@ void AxisCtrlButton::SetInnerBackgroundColor(StateColor const& color) void AxisCtrlButton::SetBitmap(ScalableBitmap &bmp) { - if (&bmp && (& bmp.bmp()) && (bmp.bmp().IsOk())) { + if (bmp.bmp().IsOk()) { m_icon = bmp; } } diff --git a/src/slic3r/GUI/Widgets/Button.cpp b/src/slic3r/GUI/Widgets/Button.cpp index e236c84e67..5a5bd89403 100644 --- a/src/slic3r/GUI/Widgets/Button.cpp +++ b/src/slic3r/GUI/Widgets/Button.cpp @@ -95,14 +95,11 @@ void Button::SetIcon(const wxString& icon) } } -void Button::SetInactiveIcon(const wxString &icon) +void Button::SetIcon(const wxBitmap& icon) { - if (!icon.IsEmpty()) { - // BBS set button icon default size to 20 - this->inactive_icon = ScalableBitmap(this, icon.ToStdString(), this->active_icon.px_cnt()); - } else { - this->inactive_icon = ScalableBitmap(); - } + this->active_icon = ScalableBitmap(); + this->active_icon.bmp() = icon; + messureSize(); Refresh(); } @@ -257,12 +254,10 @@ void Button::SetStyle(const ButtonStyle style, const ButtonType type) void Button::Rescale() { - if (this->active_icon.bmp().IsOk()) + // Only a named icon can be re-rasterized; one set from a wxBitmap has no source file, + if (!this->active_icon.name().empty()) this->active_icon.msw_rescale(); - if (this->inactive_icon.bmp().IsOk()) - this->inactive_icon.msw_rescale(); - messureSize(); if(m_has_style) @@ -293,11 +288,7 @@ void Button::render(wxDC& dc) wxSize szIcon; wxSize textSize = this->textSize.GetSize(); - ScalableBitmap icon; - if (m_selected || ((states & (int)StateColor::State::Hovered) != 0)) - icon = active_icon; - else - icon = inactive_icon; + const ScalableBitmap& icon = active_icon; wxSize padding = this->paddingSize; int spacing = 5; // Wrap text @@ -512,8 +503,8 @@ void Button::OnParentMotion(wxMouseEvent& event) { if (!tipWindow) { - tipWindow = new wxTipWindow(this, tip); - tipWindow->Bind(wxEVT_DESTROY, [this](wxEvent& event) { this->tipWindow = nullptr;}); + tipWindow = wxTipWindow::New(this, tip); + if (!tipWindow) return event.Skip(); tipWindow->Enable(false); } @@ -531,7 +522,8 @@ void Button::OnParentMotion(wxMouseEvent& event) { if (tipWindow) { - delete tipWindow; + tipWindow->Dismiss(); + tipWindow->Destroy(); tipWindow = nullptr; } } @@ -552,7 +544,7 @@ void Button::OnParentLeave(wxMouseEvent& event) if (!screen_rect.Contains(pos)) { tipWindow->Dismiss(); - delete tipWindow; + tipWindow->Destroy(); tipWindow = nullptr; } } diff --git a/src/slic3r/GUI/Widgets/Button.hpp b/src/slic3r/GUI/Widgets/Button.hpp index 94b245a75b..2991edd425 100644 --- a/src/slic3r/GUI/Widgets/Button.hpp +++ b/src/slic3r/GUI/Widgets/Button.hpp @@ -3,6 +3,7 @@ #include "../wxExtensions.hpp" #include "StaticBox.hpp" +#include <wx/tipwin.h> class ButtonProps { @@ -27,14 +28,13 @@ enum class ButtonType{ Expanded , // Font14 Semi-Rounded For full length buttons. ex. buttons in static box }; -class wxTipWindow; class Button : public StaticBox { + wxTipWindow::Ref tipWindow; wxRect textSize; wxSize minSize; // set by outer wxSize paddingSize; ScalableBitmap active_icon; - ScalableBitmap inactive_icon; StateColor text_color; @@ -44,8 +44,6 @@ class Button : public StaticBox bool isCenter = true; bool vertical = false; - wxTipWindow* tipWindow = nullptr; - static const int buttonWidth = 200; static const int buttonHeight = 50; @@ -61,8 +59,7 @@ public: bool SetFont(const wxFont& font) override; void SetIcon(const wxString& icon); - - void SetInactiveIcon(const wxString& icon); + void SetIcon(const wxBitmap& icon); void SetMinSize(const wxSize& size) override; void SetMaxSize(const wxSize& size) override; diff --git a/src/slic3r/GUI/Widgets/ComboBox.cpp b/src/slic3r/GUI/Widgets/ComboBox.cpp index 783e1caadf..08687f3cf6 100644 --- a/src/slic3r/GUI/Widgets/ComboBox.cpp +++ b/src/slic3r/GUI/Widgets/ComboBox.cpp @@ -87,10 +87,18 @@ void ComboBox::SetSelection(int n) return; drop.SetSelection(n); SetLabel(drop.GetValue()); - if (drop.selection >= 0 && drop.iconSize.y > 0 && items[drop.selection].icon_textctrl.IsOk()) - SetIcon(items[drop.selection].icon_textctrl); - else + if (drop.selection >= 0 && drop.iconSize.y > 0 && items[drop.selection].icon_textctrl.IsOk()) { + if (m_keep_drop_arrow) { + SetIcon("drop_down"); + SetIcon_1(items[drop.selection].icon_textctrl); + } else { + SetIcon(items[drop.selection].icon_textctrl); + } + } else { SetIcon("drop_down"); + if (m_keep_drop_arrow) + SetIcon_1(wxNullBitmap); + } if (drop.selection >= 0) { SetStaticTips(items[drop.selection].text_static_tips, wxNullBitmap); @@ -120,10 +128,18 @@ void ComboBox::SetValue(const wxString &value) { drop.SetValue(value); SetLabel(value); - if (drop.selection >= 0 && drop.iconSize.y > 0 && items[drop.selection].icon_textctrl.IsOk()) - SetIcon(items[drop.selection].icon_textctrl); - else + if (drop.selection >= 0 && drop.iconSize.y > 0 && items[drop.selection].icon_textctrl.IsOk()) { + if (m_keep_drop_arrow) { + SetIcon("drop_down"); + SetIcon_1(items[drop.selection].icon_textctrl); + } else { + SetIcon(items[drop.selection].icon_textctrl); + } + } else { SetIcon("drop_down"); + if (m_keep_drop_arrow) + SetIcon_1(wxNullBitmap); + } if (drop.selection >= 0) { SetStaticTips(items[drop.selection].text_static_tips, wxNullBitmap); @@ -192,7 +208,7 @@ bool ComboBox::SetFont(wxFont const& font) int ComboBox::Append(const wxString &item, const wxBitmap &bitmap, int style) { - if (&bitmap && bitmap.IsOk()) { + if (bitmap.IsOk()) { return Append(item, bitmap, nullptr, style); } return Append(item, wxNullBitmap, nullptr, style); @@ -203,7 +219,7 @@ int ComboBox::Append(const wxString &text, void * clientData, int style) { - if (&bitmap && bitmap.IsOk()) { + if (bitmap.IsOk()) { return Append(text, bitmap, wxString{}, clientData, style); } return Append(text, wxNullBitmap, wxString{}, clientData, style); @@ -221,7 +237,7 @@ int ComboBox::Append(const wxString &text, void *clientData, int style) { - auto valid_bit_map = (&bitmap && bitmap.IsOk()) ? bitmap : wxNullBitmap; + auto valid_bit_map = bitmap.IsOk() ? bitmap : wxNullBitmap; Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, clientData, group_key, group_label}; item.style = style; items.push_back(item); @@ -317,7 +333,7 @@ wxBitmap ComboBox::GetItemBitmap(unsigned int n) { return items[n].icon; } void ComboBox::SetItemBitmap(unsigned int n, wxBitmap const &bitmap) { if (n >= items.size()) return; - items[n].icon = (&bitmap && bitmap.IsOk()) ? bitmap : wxNullBitmap; + items[n].icon = bitmap.IsOk() ? bitmap : wxNullBitmap; drop.Invalidate(); } diff --git a/src/slic3r/GUI/Widgets/ComboBox.hpp b/src/slic3r/GUI/Widgets/ComboBox.hpp index 552909b477..91c34d53aa 100644 --- a/src/slic3r/GUI/Widgets/ComboBox.hpp +++ b/src/slic3r/GUI/Widgets/ComboBox.hpp @@ -16,6 +16,7 @@ class ComboBox : public wxWindowWithItems<TextInput, wxItemContainer> bool drop_down = false; bool text_off = false; bool is_replace_text_to_image = false; + bool m_keep_drop_arrow = false; // When true, item icon goes to icon_1, keeping drop_down arrow wxString replace_text; wxString image_for_text; @@ -31,6 +32,11 @@ public: DropDown & GetDropDown() { return drop; } + // When true, item icon is shown as icon_1 (secondary), preserving drop_down arrow. + // Note: item bitmaps are set via raw wxBitmap (not ScalableBitmap), so they won't + // auto-rescale on DPI change. Caller should recreate items after DPI change. + void SetKeepDropArrow(bool keep) { m_keep_drop_arrow = keep; } + virtual bool SetFont(wxFont const & font) override; public: diff --git a/src/slic3r/GUI/Widgets/DropDown.cpp b/src/slic3r/GUI/Widgets/DropDown.cpp index 973113d0ac..cd4d5edff8 100644 --- a/src/slic3r/GUI/Widgets/DropDown.cpp +++ b/src/slic3r/GUI/Widgets/DropDown.cpp @@ -427,7 +427,10 @@ void DropDown::render(wxDC &dc) } pt.y += (rcContent.height - textSize.y) / 2; dc.SetFont(GetFont()); - dc.SetTextForeground(text_color.colorForStates(states2)); + // Dimmed items stay selectable, so they only borrow the disabled text tone rather + // than taking the disabled state itself. + const int text_states = (item.style & DD_ITEM_STYLE_DIMMED) ? (states2 & ~StateColor::Enabled) : states2; + dc.SetTextForeground(text_color.colorForStates(text_states)); dc.DrawText(text, pt); if (group.IsEmpty() && !item.group_key.IsEmpty()) { auto szBmp = arrow_bitmap.GetBmpSize(); diff --git a/src/slic3r/GUI/Widgets/DropDown.hpp b/src/slic3r/GUI/Widgets/DropDown.hpp index 09041e3dc0..bcd0a58c41 100644 --- a/src/slic3r/GUI/Widgets/DropDown.hpp +++ b/src/slic3r/GUI/Widgets/DropDown.hpp @@ -13,6 +13,7 @@ #define DD_ITEM_STYLE_SPLIT_ITEM 0x0001 // ----text----, text with horizontal line arounds #define DD_ITEM_STYLE_DISABLED 0x0002 // ----text----, text with horizontal line arounds +#define DD_ITEM_STYLE_DIMMED 0x0004 // gray text, but still selectable wxDECLARE_EVENT(EVT_DISMISS, wxCommandEvent); diff --git a/src/slic3r/GUI/Widgets/FanControl.cpp b/src/slic3r/GUI/Widgets/FanControl.cpp index 057baca430..f10553814e 100644 --- a/src/slic3r/GUI/Widgets/FanControl.cpp +++ b/src/slic3r/GUI/Widgets/FanControl.cpp @@ -1014,8 +1014,12 @@ void FanControlPopupNew::init_names(MachineObject* obj) { if (obj) { const std::string& special_cooling_text = DevPrinterConfigUtil::get_fan_text(obj->printer_type, "special_cooling_text"); if (!special_cooling_text.empty()) { - L("Cooling mode is suitable for printing PLA/PETG/TPU materials."); //some potential text, add i18n flags - L("Cooling mode is suitable for printing PLA/PETG/TPU materials and filters the chamber air."); + // Possible runtime values of special_cooling_text, marked for extraction. + static const char *const markers[] = { + L("Cooling mode is suitable for printing PLA/PETG/TPU materials."), + L("Cooling mode is suitable for printing PLA/PETG/TPU materials and filters the chamber air."), + }; + (void) markers; label_text[AIR_DUCT::AIR_DUCT_COOLING_FILT] = _L(special_cooling_text); } } @@ -1028,9 +1032,13 @@ wxString FanControlPopupNew::get_fan_func_name(int mode, int submode, AIR_FUN fu const std::string& func_text = DevPrinterConfigUtil::get_fan_text(m_obj->printer_type, mode, (int)func, submode); if (!func_text.empty()) { - L_CONTEXT("Right(Aux)", "air_duct"); - L_CONTEXT("Right(Filter)", "air_duct"); - L_CONTEXT("Left(Aux)", "air_duct"); + // Possible runtime values of func_text, marked for extraction. + static const char *const markers[] = { + L_CONTEXT("Right(Aux)", "air_duct"), + L_CONTEXT("Right(Filter)", "air_duct"), + L_CONTEXT("Left(Aux)", "air_duct"), + }; + (void) markers; return _L_CONTEXT(func_text, "air_duct"); } } diff --git a/src/slic3r/GUI/Widgets/FanControl.hpp b/src/slic3r/GUI/Widgets/FanControl.hpp index fc29d22000..b13ce19d41 100644 --- a/src/slic3r/GUI/Widgets/FanControl.hpp +++ b/src/slic3r/GUI/Widgets/FanControl.hpp @@ -109,7 +109,6 @@ public: void decrease_fan_speeds(); private: int m_current_speeds; - int m_target_speed; int m_min_speeds; int m_max_speeds; ScalableBitmap m_bitmap_add; diff --git a/src/slic3r/GUI/Widgets/LabeledStaticBox.cpp b/src/slic3r/GUI/Widgets/LabeledStaticBox.cpp index c8e054593f..a11839a8b8 100644 --- a/src/slic3r/GUI/Widgets/LabeledStaticBox.cpp +++ b/src/slic3r/GUI/Widgets/LabeledStaticBox.cpp @@ -98,7 +98,7 @@ void LabeledStaticBox::SetBorderColor(StateColor const &color) Refresh(); } -void LabeledStaticBox::SetFont(wxFont set_font) +bool LabeledStaticBox::SetFont(const wxFont &set_font) { m_font = set_font; @@ -109,6 +109,7 @@ void LabeledStaticBox::SetFont(wxFont set_font) m_label_width = tW; Refresh(); + return true; } bool LabeledStaticBox::Enable(bool enable) diff --git a/src/slic3r/GUI/Widgets/LabeledStaticBox.hpp b/src/slic3r/GUI/Widgets/LabeledStaticBox.hpp index f42175ae05..d3e7f2efce 100644 --- a/src/slic3r/GUI/Widgets/LabeledStaticBox.hpp +++ b/src/slic3r/GUI/Widgets/LabeledStaticBox.hpp @@ -42,7 +42,7 @@ public: void SetBorderColor(StateColor const &color); - void SetFont(wxFont set_font); + bool SetFont(const wxFont &set_font) override; bool Enable(bool enable) override; diff --git a/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp index 05857c6d0d..518bda1d19 100644 --- a/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp +++ b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp @@ -630,7 +630,7 @@ NozzleListTable::NozzleListTable(wxWindow* parent) : wxPanel(parent,wxID_ANY,wxD SetSizer(sizer); Layout(); - m_web_view->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, [this,sizer](wxWebViewEvent& evt) { + m_web_view->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, [this](wxWebViewEvent& evt) { std::string message = evt.GetString().ToStdString(); BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << "Received message: " << message; try { @@ -1168,8 +1168,8 @@ void MultiNozzleSyncDialog::UpdateButton(std::weak_ptr<DevNozzleRack> rack, bool m_cancel_btn->SetLabel(_L("Ignore")); m_confirm_btn->SetLabel(_L("Refresh")); - m_cancel_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, ignore_opt](auto& e) {ignore_opt(); }); - m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, refresh_cmd](auto& e) {refresh_cmd(); }); + m_cancel_btn->Bind(wxEVT_LEFT_DOWN, [rack, ignore_opt](auto& e) {ignore_opt(); }); + m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [rack, refresh_cmd](auto& e) {refresh_cmd(); }); } else if (has_unknown) { m_cancel_btn->Show(); @@ -1178,8 +1178,8 @@ void MultiNozzleSyncDialog::UpdateButton(std::weak_ptr<DevNozzleRack> rack, bool m_cancel_btn->SetLabel(_L("Ignore")); m_confirm_btn->SetLabel(_L("Refresh")); - m_cancel_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, ignore_opt](auto& e) {ignore_opt(); }); - m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, refresh_cmd](auto& e) {refresh_cmd(); }); + m_cancel_btn->Bind(wxEVT_LEFT_DOWN, [rack, ignore_opt](auto& e) {ignore_opt(); }); + m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [rack, refresh_cmd](auto& e) {refresh_cmd(); }); } else if (has_unreliable) { m_cancel_btn->Show(); @@ -1188,8 +1188,8 @@ void MultiNozzleSyncDialog::UpdateButton(std::weak_ptr<DevNozzleRack> rack, bool m_cancel_btn->SetLabel(_L("Refresh")); m_confirm_btn->SetLabel(_L("Confirm")); - m_cancel_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, refresh_cmd](auto& e) {refresh_cmd(); }); - m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, trust_cmd](auto& e) {trust_cmd(); }); + m_cancel_btn->Bind(wxEVT_LEFT_DOWN, [rack, refresh_cmd](auto& e) {refresh_cmd(); }); + m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [rack, trust_cmd](auto& e) {trust_cmd(); }); } else { diff --git a/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp b/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp index ab56663928..3baea061dc 100644 --- a/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp +++ b/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp @@ -167,7 +167,7 @@ class MultiNozzleSyncDialog : public DPIDialog { public: MultiNozzleSyncDialog(wxWindow* parent, std::weak_ptr<DevNozzleRack> rack); - virtual void on_dpi_changed(const wxRect& suggested_rect) {}; + virtual void on_dpi_changed(const wxRect& suggested_rect) override {}; std::vector<NozzleOption> GetNozzleOptions(const std::vector<MultiNozzleUtils::NozzleGroupInfo>& group_infos); std::optional<NozzleOption> GetSelectedOption() { @@ -204,7 +204,6 @@ private: Label* m_caution; wxTimer* m_refresh_timer {nullptr}; - size_t m_rack_event_token; Button* m_cancel_btn; Button* m_confirm_btn; }; diff --git a/src/slic3r/GUI/Widgets/ProgressBar.hpp b/src/slic3r/GUI/Widgets/ProgressBar.hpp index 38dda6c8d2..40ddb8e4be 100644 --- a/src/slic3r/GUI/Widgets/ProgressBar.hpp +++ b/src/slic3r/GUI/Widgets/ProgressBar.hpp @@ -56,7 +56,7 @@ protected: void paintEvent(wxPaintEvent &evt); void render(wxDC &dc); void doRender(wxDC &dc); - virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO); + virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO) override; diff --git a/src/slic3r/GUI/Widgets/ProgressDialog.hpp b/src/slic3r/GUI/Widgets/ProgressDialog.hpp index 597ec7f802..bb770298a9 100644 --- a/src/slic3r/GUI/Widgets/ProgressDialog.hpp +++ b/src/slic3r/GUI/Widgets/ProgressDialog.hpp @@ -33,7 +33,7 @@ public: void OnPaint(wxPaintEvent &evt); virtual ~ProgressDialog(); - virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO); + virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO) override; bool Create(const wxString &title, const wxString &message, int maximum = 100, wxWindow *parent = NULL, int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE); virtual bool Update(int value, const wxString &newmsg = wxEmptyString, bool *skip = NULL); diff --git a/src/slic3r/GUI/Widgets/ScrolledWindow.cpp b/src/slic3r/GUI/Widgets/ScrolledWindow.cpp index d570b9f764..90922f93f1 100644 --- a/src/slic3r/GUI/Widgets/ScrolledWindow.cpp +++ b/src/slic3r/GUI/Widgets/ScrolledWindow.cpp @@ -21,6 +21,8 @@ ScrolledWindow::ScrolledWindow(wxWindow *parent, wxWindowID id, wxPoint position m_bottomScrollbar = NULL; m_verticalSplitter = NULL; m_horizontalSplitter = NULL; + m_userPanel = NULL; + m_scroll_win = NULL; m_marginWidth = marginWidth; @@ -110,23 +112,13 @@ void ScrolledWindow::SetTipColor(wxColour color) if (m_bottomScrollbar) m_bottomScrollbar->SetTipColor(color); } -void ScrolledWindow::Refresh() +bool ScrolledWindow::SetBackgroundColour(const wxColour &color) { - // m_rightScrollbar->SetViewStart(0); - // m_rightScrollbar->Refresh(); - // m_rightScrollbar->Update(); - // m_userPanel->Refresh(); - // m_bottomScrollbar->SetViewStart(0); - // m_rightScrollbar->Refresh(); - // m_bottomScrollbar->Refresh(); -} - -void ScrolledWindow::SetBackgroundColour(wxColour color) -{ - wxWindow::SetBackgroundColour(color); + const bool result = wxWindow::SetBackgroundColour(color); m_verticalSplitter->SetBackgroundColour(color); m_userPanel->SetBackgroundColour(color); m_scroll_win->SetBackgroundColour(color); + return result; } void ScrolledWindow::SetMarginColor(wxColour color) diff --git a/src/slic3r/GUI/Widgets/ScrolledWindow.hpp b/src/slic3r/GUI/Widgets/ScrolledWindow.hpp index 56d54aade3..5c2bc2f9e5 100644 --- a/src/slic3r/GUI/Widgets/ScrolledWindow.hpp +++ b/src/slic3r/GUI/Widgets/ScrolledWindow.hpp @@ -15,8 +15,7 @@ public: ScrolledWindow(wxWindow *parent, wxWindowID id, wxPoint position, wxSize size, long style, int marginWidth = 0, int scrollbarWidth = 4, int tipLength = 0); void OnMouseWheel(wxMouseEvent &event); void SetTipColor(wxColour color); - void Refresh(); - void SetBackgroundColour(wxColour color); + bool SetBackgroundColour(const wxColour &color) override; void SetMarginColor(wxColour color); void SetScrollbarColor(wxColour color); @@ -27,7 +26,7 @@ public: // wxSplitterWindow* GetVerticalSplitter() { return m_verticalSplitter; } // wxSplitterWindow* GetHorizontalSplitter() { return m_horizontalSplitter; } bool IsBothDirections() { return m_bothDirections; } - virtual void SetScrollbars(int pixelsPerUnitX, int pixelsPerUnitY, int noUnitsX, int noUnitsY, int xPos = 0, int yPos = 0, bool noRefresh = false); + virtual void SetScrollbars(int pixelsPerUnitX, int pixelsPerUnitY, int noUnitsX, int noUnitsY, int xPos = 0, int yPos = 0, bool noRefresh = false) override; private: wxPanel * m_userPanel; // the panel targeted by the scrolled window diff --git a/src/slic3r/GUI/Widgets/SideButton.hpp b/src/slic3r/GUI/Widgets/SideButton.hpp index 4f8d893f93..894a7d8727 100644 --- a/src/slic3r/GUI/Widgets/SideButton.hpp +++ b/src/slic3r/GUI/Widgets/SideButton.hpp @@ -31,7 +31,7 @@ public: void SetLayoutStyle(int style); - void SetLabel(const wxString& label); + void SetLabel(const wxString& label) override; bool SetForegroundColour(wxColour const & colour) override; @@ -47,7 +47,7 @@ public: void SetBackgroundColor(StateColor const &color); - bool Enable(bool enable = true); + bool Enable(bool enable = true) override; void Rescale(); diff --git a/src/slic3r/GUI/Widgets/SpinInput.cpp b/src/slic3r/GUI/Widgets/SpinInput.cpp index fba5a45233..c91fd41543 100644 --- a/src/slic3r/GUI/Widgets/SpinInput.cpp +++ b/src/slic3r/GUI/Widgets/SpinInput.cpp @@ -9,6 +9,8 @@ #include "../GUI_Utils.hpp" #endif +wxDEFINE_EVENT(EVT_SPINCTRL_TEXT, wxCommandEvent); + BEGIN_EVENT_TABLE(SpinInput, StaticBox) EVT_KEY_DOWN(SpinInput::keyPressed) @@ -74,8 +76,9 @@ void SpinInput::Create(wxWindow *parent, state_handler.attach_child(text_ctrl); text_ctrl->Bind(wxEVT_KILL_FOCUS, &SpinInput::onTextLostFocus, this); text_ctrl->Bind(wxEVT_TEXT_ENTER, &SpinInput::onTextEnter, this); + text_ctrl->Bind(wxEVT_TEXT, &SpinInput::onTextChanged, this); text_ctrl->Bind(wxEVT_KEY_DOWN, &SpinInput::keyPressed, this); - text_ctrl->Bind(wxEVT_RIGHT_DOWN, [this](auto &e) {}); // disable context menu + text_ctrl->Bind(wxEVT_RIGHT_DOWN, [](auto &e) {}); // disable context menu button_inc = createButton(true); button_dec = createButton(false); delta = 0; @@ -300,6 +303,19 @@ void SpinInput::onTextEnter(wxCommandEvent &event) ProcessEventLocally(event); } +void SpinInput::onTextChanged(wxCommandEvent &event) +{ + long value; + if (text_ctrl->GetValue().ToLong(&value)) { + wxCommandEvent e(EVT_SPINCTRL_TEXT, GetId()); + e.SetEventObject(this); + e.SetInt((int) value); + e.SetString(text_ctrl->GetValue()); + GetEventHandler()->ProcessEvent(e); + } + event.Skip(); +} + void SpinInput::mouseWheelMoved(wxMouseEvent &event) { auto delta = event.GetWheelRotation() < 0 ? 1 : -1; diff --git a/src/slic3r/GUI/Widgets/SpinInput.hpp b/src/slic3r/GUI/Widgets/SpinInput.hpp index 275d42a95d..caf2bf3843 100644 --- a/src/slic3r/GUI/Widgets/SpinInput.hpp +++ b/src/slic3r/GUI/Widgets/SpinInput.hpp @@ -9,6 +9,10 @@ class Button; +// Fired on every keystroke that leaves a parseable integer in the field, so callers can +// react live rather than only on commit (wxEVT_SPINCTRL) or Enter. Ported from BambuStudio. +wxDECLARE_EVENT(EVT_SPINCTRL_TEXT, wxCommandEvent); + class SpinInput : public wxNavigationEnabled<StaticBox> { wxSize labelSize; @@ -98,6 +102,7 @@ private: void keyPressed(wxKeyEvent& event); void onTimer(wxTimerEvent &evnet); void onTextLostFocus(wxEvent &event); + void onTextChanged(wxCommandEvent &event); void onTextEnter(wxCommandEvent &event); void sendSpinEvent(); diff --git a/src/slic3r/GUI/Widgets/TabCtrl.cpp b/src/slic3r/GUI/Widgets/TabCtrl.cpp index 40e291d5ad..882d2f6897 100644 --- a/src/slic3r/GUI/Widgets/TabCtrl.cpp +++ b/src/slic3r/GUI/Widgets/TabCtrl.cpp @@ -111,7 +111,7 @@ int TabCtrl::AppendItem(const wxString &item, btns.push_back(btn); if (btns.size() > 1) sizer->GetItem(sizer->GetItemCount() - 1)->SetMinSize({0, 0}); - sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, TAB_BUTTON_SPACE * 2); + sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, TAB_BUTTON_SPACE); sizer->AddStretchSpacer(1); relayout(); return btns.size() - 1; @@ -225,8 +225,9 @@ bool TabCtrl::IsVisible(unsigned int item) const void TabCtrl::DoSetSize(int x, int y, int width, int height, int sizeFlags) { + auto size = GetSize(); wxWindow::DoSetSize(x, y, width, height, sizeFlags); - if (sizeFlags & wxSIZE_USE_EXISTING) return; + if (size == GetSize()) return; relayout(); } diff --git a/src/slic3r/GUI/Widgets/TabCtrl.hpp b/src/slic3r/GUI/Widgets/TabCtrl.hpp index a25f332fb3..04b5b8e24e 100644 --- a/src/slic3r/GUI/Widgets/TabCtrl.hpp +++ b/src/slic3r/GUI/Widgets/TabCtrl.hpp @@ -63,7 +63,7 @@ public: bool IsVisible(unsigned int item) const; private: - virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO); + virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO) override; #ifdef __WIN32__ WXLRESULT MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam) override; diff --git a/src/slic3r/GUI/Widgets/TempInput.cpp b/src/slic3r/GUI/Widgets/TempInput.cpp index 6a9809252a..6705378dac 100644 --- a/src/slic3r/GUI/Widgets/TempInput.cpp +++ b/src/slic3r/GUI/Widgets/TempInput.cpp @@ -134,7 +134,7 @@ void TempInput::Create(wxWindow *parent, wxString text, wxString label, wxString } } }); - text_ctrl->Bind(wxEVT_RIGHT_DOWN, [this](auto &e) {}); // disable context menu + text_ctrl->Bind(wxEVT_RIGHT_DOWN, [](auto &e) {}); // disable context menu text_ctrl->Bind(wxEVT_LEFT_DOWN, [this](auto &e) { if (m_read_only) { return; diff --git a/src/slic3r/GUI/Widgets/TempInput.hpp b/src/slic3r/GUI/Widgets/TempInput.hpp index c306ba59cc..f281a1ea6e 100644 --- a/src/slic3r/GUI/Widgets/TempInput.hpp +++ b/src/slic3r/GUI/Widgets/TempInput.hpp @@ -107,7 +107,7 @@ public: wxString GetTagTemp() { return text_ctrl->GetValue(); } wxString GetCurrTemp() { return GetLabel(); } int get_max_temp() { return max_temp; } - void SetLabel(const wxString &label); + void SetLabel(const wxString &label) override; void SetTextColor(StateColor const &color); @@ -128,7 +128,7 @@ public: void ReSetOnChanging() { m_on_changing = false; } protected: - virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO); + virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO) override; void DoSetToolTipText(wxString const &tip) override; diff --git a/src/slic3r/GUI/Widgets/TextInput.cpp b/src/slic3r/GUI/Widgets/TextInput.cpp index 49605e048d..23f55d155c 100644 --- a/src/slic3r/GUI/Widgets/TextInput.cpp +++ b/src/slic3r/GUI/Widgets/TextInput.cpp @@ -85,7 +85,7 @@ void TextInput::Create(wxWindow * parent, e.SetId(GetId()); ProcessEventLocally(e); }); - text_ctrl->Bind(wxEVT_RIGHT_DOWN, [this](auto &e) {}); // disable context menu + text_ctrl->Bind(wxEVT_RIGHT_DOWN, [](auto &e) {}); // disable context menu if (!icon.IsEmpty()) { this->icon = ScalableBitmap(this, icon.ToStdString(), 16); } @@ -139,6 +139,15 @@ void TextInput::SetIcon_1(const wxString &icon) { Rescale(); } +// Set icon_1 from a raw bitmap. Note: won't auto-rescale on DPI change +// since ScalableBitmap::name() will be empty. Caller should re-set after DPI change. +void TextInput::SetIcon_1(const wxBitmap &icon) { + this->icon_1 = ScalableBitmap(); + if (icon.IsOk()) + this->icon_1.bmp() = icon; + Rescale(); +} + void TextInput::SetLabelColor(StateColor const &color) { label_color = color; diff --git a/src/slic3r/GUI/Widgets/TextInput.hpp b/src/slic3r/GUI/Widgets/TextInput.hpp index 9aca7037c4..bbd38b0c00 100644 --- a/src/slic3r/GUI/Widgets/TextInput.hpp +++ b/src/slic3r/GUI/Widgets/TextInput.hpp @@ -46,7 +46,7 @@ public: // Only meant to be used by inspector, not public API int GetCornerRadius() const { return static_cast<int>(radius); } - void SetLabel(const wxString& label); + void SetLabel(const wxString& label) override; void SetStaticTips(const wxString& tips, const wxBitmap& bitmap); @@ -54,6 +54,7 @@ public: void SetIcon(const wxString & icon); void SetIcon_1(const wxString &icon); + void SetIcon_1(const wxBitmap &icon); void SetLabelColor(StateColor const &color); @@ -73,7 +74,7 @@ protected: virtual void OnEdit() {} virtual void DoSetSize( - int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO); + int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO) override; void DoSetToolTipText(wxString const &tip) override; diff --git a/src/slic3r/GUI/Widgets/WebView.cpp b/src/slic3r/GUI/Widgets/WebView.cpp index 36800dcf47..e281d97407 100644 --- a/src/slic3r/GUI/Widgets/WebView.cpp +++ b/src/slic3r/GUI/Widgets/WebView.cpp @@ -104,7 +104,7 @@ DWORD DownloadAndInstallWV2RT() { class WebViewEdge : public wxWebViewEdge { public: - bool SetUserAgent(const wxString &userAgent) + bool SetUserAgent(const wxString &userAgent) override { bool dark = userAgent.Contains("dark"); SetColorScheme(dark ? COREWEBVIEW2_PREFERRED_COLOR_SCHEME_DARK : COREWEBVIEW2_PREFERRED_COLOR_SCHEME_LIGHT); diff --git a/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp b/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp index 5e6026d1cf..044fe33cde 100644 --- a/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp +++ b/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp @@ -57,18 +57,6 @@ std::string host_theme_vars_css() return s; } -// Document-start user script: injects the contract <style>, stamps data-orca-theme before -// first paint, and raises a JS flag so the legacy globalapi.js dark.css poll stands down for -// host-themed pages. The WebView2 timing guard lives in document_start_injector(). -std::string host_theme_user_script() -{ - const std::string style = "<style id=\"orca-host-theme-vars\">" + host_theme_vars_css() + "</style>"; - return WebViewHostDialog::document_start_injector( - style, "orca-host-theme-vars", "afterbegin", - "window.__orcaHostThemed=true;var theme=\"" + host_theme_name() + "\";", - "if(document.documentElement)document.documentElement.setAttribute('data-orca-theme',theme);"); -} - // JS to re-theme an already-loaded document live (no reload): replace the injected // style's contents and update data-orca-theme. Everything downstream (theme.css // tokens, plugin element defaults, page layout) re-cascades from these values. @@ -87,6 +75,46 @@ if(document.documentElement) } // namespace +// Document-start user script: injects the contract <style>, stamps data-orca-theme before +// first paint, and raises a JS flag so the legacy globalapi.js dark.css poll stands down for +// host-themed pages. The WebView2 timing guard lives in document_start_injector(). +std::string WebViewHostDialog::theme_user_script() +{ + const std::string style = "<style id=\"orca-host-theme-vars\">" + host_theme_vars_css() + "</style>"; + return document_start_injector( + style, "orca-host-theme-vars", "afterbegin", + "window.__orcaHostThemed=true;var theme=\"" + host_theme_name() + "\";", + "if(document.documentElement)document.documentElement.setAttribute('data-orca-theme',theme);"); +} + +std::string WebViewHostDialog::plugin_defaults_user_script() +{ + std::string css; + css += "<style id=\"orca-plugin-defaults\">"; + css += "html,body{background:var(--orca-bg);color:var(--orca-fg);" + "font-family:var(--orca-font);font-size:13px;}"; + css += "body{margin:0;}"; + css += "h1,h2,h3,h4,h5,h6{color:var(--orca-fg);font-weight:600;}"; + css += "a{color:var(--orca-accent);}"; + css += "hr{border:0;border-top:1px solid var(--orca-border);}"; + css += "button{font:inherit;color:var(--orca-accent-fg);background:var(--orca-accent);" + "border:1px solid var(--orca-accent);border-radius:4px;padding:5px 14px;cursor:pointer;}"; + css += "button:hover{filter:brightness(1.1);}"; + css += "button:disabled{opacity:.5;cursor:default;}"; + css += "input,select,textarea{font:inherit;color:var(--orca-fg);" + "background:var(--orca-bg);border:1px solid var(--orca-border);" + "border-radius:4px;padding:4px 8px;}"; + css += "input:focus,select:focus,textarea:focus{outline:none;border-color:var(--orca-accent);}"; + css += "table{border-collapse:collapse;}"; + css += "th,td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--orca-border);}"; + css += "th{color:var(--orca-muted);font-weight:600;}"; + css += "::-webkit-scrollbar{width:12px;height:12px;}"; + css += "::-webkit-scrollbar-thumb{background:var(--orca-border);border-radius:6px;}"; + css += "::-webkit-scrollbar-track{background:transparent;}"; + css += "</style>"; + return document_start_injector(css, "orca-plugin-defaults", "beforeend"); +} + std::string WebViewHostDialog::document_start_injector(const std::string& markup, const char* dom_id, const char* position, @@ -244,7 +272,7 @@ void WebViewHostDialog::register_theme_user_scripts() // script message handler is registered separately (AddScriptMessageHandler), but on // some backends RemoveAllUserScripts() drops it too, which would break // window.wx.postMessage / HandleStudio. Live re-theme goes through apply_theme_live(). - m_browser->AddUserScript(wxString::FromUTF8(host_theme_user_script())); + m_browser->AddUserScript(wxString::FromUTF8(theme_user_script())); add_user_scripts(); } diff --git a/src/slic3r/GUI/Widgets/WebViewHostDialog.hpp b/src/slic3r/GUI/Widgets/WebViewHostDialog.hpp index ed21f15f94..119e3b5955 100644 --- a/src/slic3r/GUI/Widgets/WebViewHostDialog.hpp +++ b/src/slic3r/GUI/Widgets/WebViewHostDialog.hpp @@ -49,6 +49,10 @@ public: const std::string& prelude = {}, const std::string& on_inject = {}); + // Shared by modeless Pages tabs and PluginWebDialog. + static std::string theme_user_script(); + static std::string plugin_defaults_user_script(); + protected: wxWebView* browser() const { return m_browser; } diff --git a/src/slic3r/GUI/WipeTowerDialog.cpp b/src/slic3r/GUI/WipeTowerDialog.cpp index abf8baf086..d4fbcc6fe3 100644 --- a/src/slic3r/GUI/WipeTowerDialog.cpp +++ b/src/slic3r/GUI/WipeTowerDialog.cpp @@ -204,6 +204,10 @@ bool is_flush_config_modified() const auto &project_config = wxGetApp().preset_bundle->project_config; const std::vector<double> &config_matrix = (project_config.option<ConfigOptionFloats>("flush_volumes_matrix"))->values; const std::vector<double> &config_multiplier = (project_config.option<ConfigOptionFloats>("flush_multiplier"))->values; + // The config matrix is N x N per nozzle over every slot, while CalcFlushingVolumes is p x p + // over the physical slots (mixed slots never flush): map each default cell to its config index. + const auto physical_indices = wxGetApp().preset_bundle->physical_filament_config_indices(); + const size_t full_n = project_config.option<ConfigOptionStrings>("filament_colour")->values.size(); bool has_modify = false; for (int i = 0; i < config_multiplier.size(); i++) { @@ -212,11 +216,12 @@ bool is_flush_config_modified() break; } std::vector<std::vector<double>> default_matrix = WipingDialog::CalcFlushingVolumes(i); - int len = default_matrix.size(); - for (int m = 0; m < len; m++) { - for (int n = 0; n < len; n++) { - int idx = i * len * len + m * len + n; - if (config_matrix[idx] != default_matrix[m][n] * config_multiplier[i]) { + size_t p_len = default_matrix.size(); + size_t nozzle_offset = i * full_n * full_n; + for (size_t m = 0; m < p_len; m++) { + for (size_t n = 0; n < p_len; n++) { + size_t cfg_idx = nozzle_offset + physical_indices[m] * full_n + physical_indices[n]; + if (cfg_idx < config_matrix.size() && config_matrix[cfg_idx] != default_matrix[m][n] * config_multiplier[i]) { has_modify = true; break; } @@ -256,6 +261,40 @@ static std::vector<float> MatrixFlatten(const WipingDialog::VolumeMatrix& matrix return vec; } +// Mixed-color slots are virtual and have no flushing volumes, so the dialog shows only the +// physical filaments. That means converting between the full config matrix (indexed by config +// slot) and a dense physical sub-matrix (indexed by row/column in the table). +static std::vector<double> extract_physical_sub_matrix( + const std::vector<double>& full_matrix, size_t full_n, + const std::vector<size_t>& indices) +{ + size_t p = indices.size(); + std::vector<double> sub(p * p, 0.0); + if (full_matrix.size() < full_n * full_n) + return sub; + for (size_t pi = 0; pi < p; ++pi) + for (size_t pj = 0; pj < p; ++pj) + sub[pi * p + pj] = full_matrix[indices[pi] * full_n + indices[pj]]; + return sub; +} + +// Write the edited physical sub-matrix back into a copy of the full matrix, leaving the +// entries that belong to mixed slots untouched. +static std::vector<double> expand_physical_to_full_matrix( + const std::vector<double>& sub_matrix, + const std::vector<size_t>& indices, size_t full_n, + const std::vector<double>& original_matrix) +{ + std::vector<double> full = original_matrix; + if (full.size() < full_n * full_n) + return full; + size_t p = indices.size(); + for (size_t pi = 0; pi < p; ++pi) + for (size_t pj = 0; pj < p; ++pj) + full[indices[pi] * full_n + indices[pj]] = sub_matrix[pi * p + pj]; + return full; +} + wxString WipingDialog::BuildTableObjStr() { auto full_config = wxGetApp().preset_bundle->full_config(); @@ -265,9 +304,22 @@ wxString WipingDialog::BuildTableObjStr() auto raw_matrix_data = full_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values; auto nozzle_flush_dataset = full_config.option<ConfigOptionIntsNullable>("nozzle_flush_dataset")->values; + // Restrict the table to physical filaments; mixed slots have no flushing volumes. + m_physical_indices = wxGetApp().preset_bundle->physical_filament_config_indices(); + const size_t full_n = filament_colors.size(); + { + std::vector<std::string> physical_colors; + physical_colors.reserve(m_physical_indices.size()); + for (size_t i : m_physical_indices) + if (i < filament_colors.size()) + physical_colors.push_back(filament_colors[i]); + filament_colors = std::move(physical_colors); + } + std::vector<std::vector<double>> flush_matrixs; for (int idx = 0; idx < nozzle_num; ++idx) { - flush_matrixs.emplace_back(get_flush_volumes_matrix(raw_matrix_data, idx, nozzle_num)); + auto fm = get_flush_volumes_matrix(raw_matrix_data, idx, nozzle_num); + flush_matrixs.emplace_back(extract_physical_sub_matrix(fm, full_n, m_physical_indices)); } flush_multiplier.resize(nozzle_num, 1); @@ -372,7 +424,7 @@ WipingDialog::WipingDialog(wxWindow* parent, const int max_flush_volume) : wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); this->SetSizer(main_sizer); this->SetBackgroundColour(*wxWHITE); - auto filament_count = wxGetApp().preset_bundle->project_config.option<ConfigOptionStrings>("filament_colour")->values.size(); + auto filament_count = wxGetApp().preset_bundle->physical_filament_config_indices().size(); // Estimate table scroll area size based on filament count // Each table cell is ~60x25 DIP, plus headers and borders @@ -523,55 +575,51 @@ WipingDialog::VolumeMatrix WipingDialog::CalcFlushingVolumes(int extruder_id) auto& preset_bundle = wxGetApp().preset_bundle; auto full_config = preset_bundle->full_config(); auto& ams_multi_color_filament = preset_bundle->ams_multi_color_filment; + // Mixed-colour slots are virtual and never flushed: compute a p x p matrix over the physical + // slots only, laid out like the table; row/column k belongs to config slot physical_indices[k]. + auto physical_indices = preset_bundle->physical_filament_config_indices(); - std::vector<std::string> filament_color_strs = full_config.option<ConfigOptionStrings>("filament_colour")->values; - std::vector<std::vector<wxColour>> multi_colors; - std::vector<wxColour> filament_colors; - for (auto color_str : filament_color_strs) - filament_colors.emplace_back(color_str); - + std::vector<std::string> all_color_strs = full_config.option<ConfigOptionStrings>("filament_colour")->values; int flush_dataset_value = full_config.option<ConfigOptionIntsNullable>("nozzle_flush_dataset")->values[extruder_id]; + const std::vector<int> min_flush_volumes = get_min_flush_volumes(full_config, extruder_id); + // Support for multi-color filament - for (int i = 0; i < filament_colors.size(); ++i) { + std::vector<std::vector<wxColour>> multi_colors; + for (size_t cfg_idx : physical_indices) { std::vector<wxColour> single_filament; - if (i < ams_multi_color_filament.size()) { - if (!ams_multi_color_filament[i].empty()) { - std::vector<std::string> colors = ams_multi_color_filament[i]; - for (int j = 0; j < colors.size(); ++j) { - single_filament.push_back(wxColour(colors[j])); - } - multi_colors.push_back(single_filament); - continue; - } + if (cfg_idx < ams_multi_color_filament.size() && !ams_multi_color_filament[cfg_idx].empty()) { + for (const auto& c : ams_multi_color_filament[cfg_idx]) + single_filament.push_back(wxColour(c)); + } else if (cfg_idx < all_color_strs.size()) { + single_filament.push_back(wxColour(all_color_strs[cfg_idx])); } - single_filament.push_back(wxColour(filament_colors[i])); multi_colors.push_back(single_filament); } VolumeMatrix matrix; - const std::vector<int> min_flush_volumes = get_min_flush_volumes(full_config, extruder_id); - - for (int from_idx = 0; from_idx < multi_colors.size(); ++from_idx) { - bool is_from_support = is_support_filament(from_idx); + for (size_t pi = 0; pi < physical_indices.size(); ++pi) { + int from_cfg = (int)physical_indices[pi]; + bool is_from_support = is_support_filament(from_cfg); matrix.emplace_back(); - for (int to_idx = 0; to_idx < multi_colors.size(); ++to_idx) { - if (from_idx == to_idx) { + for (size_t pj = 0; pj < physical_indices.size(); ++pj) { + int to_cfg = (int)physical_indices[pj]; + if (from_cfg == to_cfg) { matrix.back().emplace_back(0); continue; } - bool is_to_support = is_support_filament(to_idx); - + bool is_to_support = is_support_filament(to_cfg); int flushing_volume = 0; if (is_to_support) { flushing_volume = Slic3r::g_flush_volume_to_support; } else { - for (int i = 0; i < multi_colors[from_idx].size(); ++i) { - const wxColour& from = multi_colors[from_idx][i]; - for (int j = 0; j < multi_colors[to_idx].size(); ++j) { - const wxColour& to = multi_colors[to_idx][j]; - int volume = CalcFlushingVolume(from, to, min_flush_volumes[from_idx], flush_dataset_value); + int min_flush_from = (from_cfg < (int)min_flush_volumes.size()) ? min_flush_volumes[from_cfg] : 0; + for (size_t i = 0; i < multi_colors[pi].size(); ++i) { + const wxColour& from = multi_colors[pi][i]; + for (size_t j = 0; j < multi_colors[pj].size(); ++j) { + const wxColour& to = multi_colors[pj][j]; + int volume = CalcFlushingVolume(from, to, min_flush_from, flush_dataset_value); flushing_volume = std::max(flushing_volume, volume); } } @@ -592,11 +640,29 @@ void WipingDialog::StoreFlushData(int extruder_num, const std::vector<std::vecto m_raw_matrixs = flush_volume_vecs; } +// The table edits a physical-only sub-matrix; GetFlattenMatrix has to hand back a full-size +// matrix so the config layout stays indexed by config slot. Mixed-slot entries keep whatever +// the config already held. +std::vector<double> WipingDialog::ExpandToFullMatrix(const std::vector<double>& sub_matrix, int nozzle_idx) const +{ + const auto& project_config = wxGetApp().preset_bundle->project_config; + const size_t full_n = project_config.option<ConfigOptionStrings>("filament_colour")->values.size(); + if (m_physical_indices.size() == full_n) + return sub_matrix; // no mixed slots: sub-matrix already is the full matrix + + auto raw = project_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values; + int nozzle_num = (int)wxGetApp().preset_bundle->project_config.option<ConfigOptionFloats>("flush_multiplier")->values.size(); + if (nozzle_num < 1) nozzle_num = 1; + auto original = get_flush_volumes_matrix(raw, nozzle_idx, nozzle_num); + return expand_physical_to_full_matrix(sub_matrix, m_physical_indices, full_n, original); +} + std::vector<double> WipingDialog::GetFlattenMatrix()const { std::vector<double> ret; - for (auto& matrix : m_raw_matrixs) { - ret.insert(ret.end(), matrix.begin(), matrix.end()); + for (size_t idx = 0; idx < m_raw_matrixs.size(); ++idx) { + auto full = ExpandToFullMatrix(m_raw_matrixs[idx], (int)idx); + ret.insert(ret.end(), full.begin(), full.end()); } return ret; } diff --git a/src/slic3r/GUI/WipeTowerDialog.hpp b/src/slic3r/GUI/WipeTowerDialog.hpp index 64e5758534..91944cfc78 100644 --- a/src/slic3r/GUI/WipeTowerDialog.hpp +++ b/src/slic3r/GUI/WipeTowerDialog.hpp @@ -58,12 +58,16 @@ private: wxString BuildTableObjStr(); wxString BuildTextObjStr(bool multi_language = true); void StoreFlushData(int extruder_num, const std::vector<std::vector<double>>& flush_volume_vecs, const std::vector<double>& flush_multipliers); + // Maps the physical-only matrix shown in the table back onto the full config-indexed matrix. + std::vector<double> ExpandToFullMatrix(const std::vector<double>& sub_matrix, int nozzle_idx) const; wxWebView* m_webview; int m_max_flush_volume; VolumeMatrix m_raw_matrixs; std::vector<double> m_flush_multipliers; + // Config indices of the physical (non-mixed) filaments, in table order. + std::vector<size_t> m_physical_indices; bool m_submit_flag{ false }; }; diff --git a/src/slic3r/GUI/wxExtensions.cpp b/src/slic3r/GUI/wxExtensions.cpp index 2ca8f3cfbd..88e2df31c0 100644 --- a/src/slic3r/GUI/wxExtensions.cpp +++ b/src/slic3r/GUI/wxExtensions.cpp @@ -555,14 +555,20 @@ std::vector<wxBitmap*> get_extruder_color_icons(bool thin_icon/* = false*/) const int icon_width = lround((thin_icon ? 2 : 4.4) * em); const int icon_height = lround(2 * em); + // A gradient mixed filament fades over the model's height, so it gets the same + // curve-sampled ramp the editor previews instead of a fade between two endpoints. + const auto& gradient_ramps = Slic3r::GUI::wxGetApp().plater()->get_filament_gradient_ramps(); + int index = 0; for (const auto &colors : readable_color_info) { auto label = std::to_string(++index); - bool is_gradient = ctype[index-1] == "0"; - if (colors.size() == 1) { + const size_t slot = index - 1; + bool is_gradient = ctype[slot] == "0"; + const std::vector<wxColour>* ramp = (slot < gradient_ramps.size() && !gradient_ramps[slot].empty()) ? &gradient_ramps[slot] : nullptr; + if (ramp == nullptr && colors.size() == 1) { bmps.push_back(get_extruder_color_icon(colors[0], label, icon_width, icon_height)); } else { - bmps.push_back(get_extruder_color_icon(colors, is_gradient, label, icon_width, icon_height)); + bmps.push_back(get_extruder_color_icon(colors, is_gradient, label, icon_width, icon_height, ramp)); } } } else { @@ -630,14 +636,27 @@ wxColourData show_sys_picker_dialog(wxWindow *parent, const wxColourData &clr_da return data; } -wxBitmap *get_extruder_color_icon(std::vector<std::string> colors, bool is_gradient, std::string label, int icon_width, int icon_height){ +wxBitmap *get_extruder_color_icon(std::vector<std::string> colors, bool is_gradient, std::string label, int icon_width, int icon_height, + const std::vector<wxColour> *ramp){ static Slic3r::GUI::BitmapCache bmp_cache; - // build cache key, include all color info + // build cache key, include all color info. A ramp already encodes its slot's components, + // colours and curve, so keying on it rebuilds the icon whenever any of them change. std::string bitmap_key = ""; - for (const auto& color : colors) { - bitmap_key += color + "_"; + if (ramp != nullptr) { + static const char hex_digits[] = "0123456789ABCDEF"; + bitmap_key = "grad_"; + for (const wxColour &c : *ramp) + for (unsigned char v : {c.Red(), c.Green(), c.Blue()}) { + bitmap_key += hex_digits[v >> 4]; + bitmap_key += hex_digits[v & 0x0F]; + } + bitmap_key += "_"; + } else { + for (const auto& color : colors) { + bitmap_key += color + "_"; + } } bitmap_key += "h" + std::to_string(icon_height) + "-w" + std::to_string(icon_width) + "-i" + label; @@ -647,16 +666,21 @@ wxBitmap *get_extruder_color_icon(std::vector<std::string> colors, bool is_gradi #endif if (bitmap == nullptr) { - std::vector<wxColour> wx_colors; - for (const auto& color_str : colors) { - wx_colors.push_back(wxColour(color_str)); - } - if (wx_colors.empty()) { - wx_colors.push_back(wxColour("#636363")); // default color if no colors provided - } + wxBitmap base_bitmap; + if (ramp != nullptr) { + base_bitmap = Slic3r::GUI::create_gradient_ramp_bitmap(*ramp, wxSize(icon_width, icon_height)); + } else { + std::vector<wxColour> wx_colors; + for (const auto& color_str : colors) { + wx_colors.push_back(wxColour(color_str)); + } + if (wx_colors.empty()) { + wx_colors.push_back(wxColour("#636363")); // default color if no colors provided + } - // create filament bitmap in multi color - wxBitmap base_bitmap = Slic3r::GUI::create_filament_bitmap(wx_colors, wxSize(icon_width, icon_height), is_gradient); + // create filament bitmap in multi color + base_bitmap = Slic3r::GUI::create_filament_bitmap(wx_colors, wxSize(icon_width, icon_height), is_gradient); + } if (!base_bitmap.IsOk()) { // if create failed, return nullptr diff --git a/src/slic3r/GUI/wxExtensions.hpp b/src/slic3r/GUI/wxExtensions.hpp index 502614eb92..2754b3e5ba 100644 --- a/src/slic3r/GUI/wxExtensions.hpp +++ b/src/slic3r/GUI/wxExtensions.hpp @@ -75,7 +75,10 @@ wxBitmap create_scaled_bitmap(const std::string& bmp_name, wxWindow *win = nullp wxBitmap* get_default_extruder_color_icon(bool thin_icon = false); std::vector<wxBitmap *> get_extruder_color_icons(bool thin_icon = false); wxBitmap * get_extruder_color_icon(std::string color, std::string label, int icon_width, int icon_height); -wxBitmap * get_extruder_color_icon(std::vector<std::string> colors, bool is_gradient, std::string label, int icon_width, int icon_height); +// A non-null ramp draws the slot as a gradient mixed filament instead: it holds the colours the +// slot actually prints, bottom entry first, and is drawn bottom to top rather than from colors. +wxBitmap * get_extruder_color_icon(std::vector<std::string> colors, bool is_gradient, std::string label, int icon_width, int icon_height, + const std::vector<wxColour> *ramp = nullptr); std::vector<std::vector<std::string>> read_color_pack(std::vector<std::string> color_pack); wxColourData show_sys_picker_dialog(wxWindow *parent, const wxColourData &clr_data); diff --git a/src/slic3r/GUI/wxMediaCtrl2.cpp b/src/slic3r/GUI/wxMediaCtrl2.cpp deleted file mode 100644 index f2d1d2701e..0000000000 --- a/src/slic3r/GUI/wxMediaCtrl2.cpp +++ /dev/null @@ -1,633 +0,0 @@ -#include "wxMediaCtrl2.h" -#include "libslic3r/Time.hpp" -#include "I18N.hpp" -#include "GUI_App.hpp" -#include "LinuxDisplayBackend.hpp" -#include <boost/filesystem/operations.hpp> -#include <string> -#ifdef __WIN32__ -#include <winuser.h> -#include <versionhelpers.h> -#include <wx/msw/registry.h> -#include <shellapi.h> -#endif - -#ifdef __LINUX__ -#include "Printer/gstbambusrc.h" -#include <gst/gst.h> // main gstreamer header -#endif - -#if defined(__LINUX__) && defined(__WXGTK__) -#include <gtk/gtk.h> -#include <wx/nativewin.h> - -namespace { -bool ensure_gstreamer_initialized_for_liveview() -{ - GError* error = nullptr; - if (!gst_init_check(nullptr, nullptr, &error)) { - BOOST_LOG_TRIVIAL(error) << "wxMediaCtrl2: gst_init_check failed before native Wayland liveview setup" - << (error ? std::string(": ") + error->message : std::string()); - if (error) - g_error_free(error); - return false; - } - - return true; -} - -bool is_gstreamer_feature_available(const char* feature) -{ - if (!ensure_gstreamer_initialized_for_liveview()) - return false; - - GstElementFactory* factory = gst_element_factory_find(feature); - if (!factory) - return false; - - gst_object_unref(factory); - return true; -} - -void set_gstreamer_feature_rank(const char* feature, guint rank) -{ - GstElementFactory* factory = gst_element_factory_find(feature); - if (!factory) - return; - - gst_plugin_feature_set_rank(GST_PLUGIN_FEATURE(factory), rank); - gst_object_unref(factory); -} - -void configure_wayland_gstreamer_liveview_path() -{ - static bool configured = false; - if (configured) - return; - configured = true; - - if (!ensure_gstreamer_initialized_for_liveview()) - return; - - // Prefer software decode for Bambu liveview on Wayland/NVIDIA, where - // zero-copy GL/DMABUF display paths can be fragile. Keep hardware - // decoders available as lower-ranked fallbacks for VAAPI/NVDEC/V4L2-only - // installations instead of passing preflight and then blocking autoplug. - set_gstreamer_feature_rank("avdec_h264", GST_RANK_PRIMARY + 300); - set_gstreamer_feature_rank("openh264dec", GST_RANK_PRIMARY + 100); - set_gstreamer_feature_rank("nvh264dec", GST_RANK_MARGINAL); - set_gstreamer_feature_rank("vaapih264dec", GST_RANK_MARGINAL); - set_gstreamer_feature_rank("vah264dec", GST_RANK_MARGINAL); - set_gstreamer_feature_rank("v4l2h264dec", GST_RANK_MARGINAL); -} -} - -#endif // defined(__LINUX__) && defined(__WXGTK__) - -#ifdef __LINUX__ -extern "C" int gst_bambu_last_error; - -class WXDLLIMPEXP_MEDIA - wxGStreamerMediaBackend : public wxMediaBackendCommonBase -{ -public: - GstElement *m_playbin; // GStreamer media element -}; -#endif - -wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent); - -wxMediaCtrl2::wxMediaCtrl2(wxWindow *parent) -{ -#if defined(__LINUX__) && defined(__WXGTK__) - m_native_wayland = Slic3r::GUI::is_running_on_wayland(); - if (m_native_wayland && is_gstreamer_feature_available("gtksink")) - configure_wayland_gstreamer_liveview_path(); - else if (m_native_wayland) { - m_gtk_sink_error = _L("Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer."); - BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: native Wayland liveview disabled because GStreamer gtksink is unavailable"; - } -#endif -#ifdef __WIN32__ - auto hModExe = GetModuleHandle(NULL); - // BOOST_LOG_TRIVIAL(info) << "wxMediaCtrl2: GetModuleHandle " << hModExe; - auto NvOptimusEnablement = (DWORD *) GetProcAddress(hModExe, "NvOptimusEnablement"); - auto AmdPowerXpressRequestHighPerformance = (int *) GetProcAddress(hModExe, "AmdPowerXpressRequestHighPerformance"); - if (NvOptimusEnablement) { - // BOOST_LOG_TRIVIAL(info) << "wxMediaCtrl2: NvOptimusEnablement " << *NvOptimusEnablement; - *NvOptimusEnablement = 0; - } - if (AmdPowerXpressRequestHighPerformance) { - // BOOST_LOG_TRIVIAL(info) << "wxMediaCtrl2: AmdPowerXpressRequestHighPerformance " << *AmdPowerXpressRequestHighPerformance; - *AmdPowerXpressRequestHighPerformance = 0; - } -#endif -#if defined(__LINUX__) && defined(__WXGTK__) - if (m_native_wayland) - wxControl::Create(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize); - else -#endif - wxMediaCtrl::Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxMEDIACTRLPLAYERCONTROLS_NONE); -#ifdef __LINUX__ - gstbambusrc_register(); -#ifdef __WXGTK__ - if (m_native_wayland && m_gtk_sink_error.empty()) - m_use_gtk_sink = CreateGtkSinkPlayer(); - if (m_native_wayland && !m_use_gtk_sink && m_gtk_sink_error.empty()) - m_gtk_sink_error = _L("Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation."); -#endif - if (!m_use_gtk_sink && m_imp) { - auto playbin = reinterpret_cast<wxGStreamerMediaBackend *>(m_imp)->m_playbin; - g_object_set(G_OBJECT(playbin), - "audio-sink", nullptr, - nullptr); - } else if (!m_use_gtk_sink) { - BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: wxMediaCtrl backend is unavailable"; - } - Bind(wxEVT_MEDIA_LOADED, [this](auto & e) { - m_loaded = true; - wxMediaEvent event(wxEVT_MEDIA_STATECHANGED); - event.SetId(0); - event.SetEventObject(this); - wxPostEvent(this, event); - }); -#endif -} - -wxMediaCtrl2::~wxMediaCtrl2() -{ -#if defined(__LINUX__) && defined(__WXGTK__) - DestroyGtkSinkPlayer(); -#endif -} - -#if defined(__LINUX__) && defined(__WXGTK__) -bool wxMediaCtrl2::CreateGtkSinkPlayer() -{ - GstElement *playbin = gst_element_factory_make("playbin", "orca-wayland-gtk-playbin"); - if (!playbin) - return false; - - GError *error = nullptr; - GstElement *video_sink = gst_parse_bin_from_description( - "videoconvert ! videoscale ! video/x-raw,format=BGRx ! gtksink name=orca_wayland_gtksink sync=false", - TRUE, - &error); - if (!video_sink) { - BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: failed to create gtksink video bin" - << (error ? std::string(": ") + error->message : std::string()); - if (error) - g_error_free(error); - gst_object_unref(playbin); - return false; - } - - GstElement *gtk_sink = gst_bin_get_by_name(GST_BIN(video_sink), "orca_wayland_gtksink"); - if (!gtk_sink) { - BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: failed to find gtksink in video bin"; - gst_object_unref(video_sink); - gst_object_unref(playbin); - return false; - } - - GtkWidget *gtk_widget = nullptr; - g_object_get(G_OBJECT(gtk_sink), "widget", >k_widget, nullptr); - if (!gtk_widget) { - BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: gtksink did not expose a GtkWidget"; - gst_object_unref(gtk_sink); - gst_object_unref(video_sink); - gst_object_unref(playbin); - return false; - } - - gtk_widget_show(gtk_widget); - m_gtk_video_window = new wxNativeWindow(this, wxID_ANY, gtk_widget); - m_gtk_video_window->Show(); - g_object_unref(gtk_widget); - - g_object_set(G_OBJECT(playbin), - "video-sink", video_sink, - "audio-sink", nullptr, - nullptr); - gst_object_unref(video_sink); - - m_gtk_playbin = playbin; - m_gtk_sink = gtk_sink; - - GstBus *bus = gst_element_get_bus(playbin); - m_gtk_bus_watch_id = gst_bus_add_watch(bus, [](GstBus *, GstMessage *message, gpointer data) -> gboolean { - auto *self = static_cast<wxMediaCtrl2 *>(data); - if (!self || !self->m_gtk_playbin) - return G_SOURCE_REMOVE; - - switch (GST_MESSAGE_TYPE(message)) { - case GST_MESSAGE_ERROR: - { - GError *error = nullptr; - gchar *debug = nullptr; - gst_message_parse_error(message, &error, &debug); - BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: gtksink pipeline error" - << (error ? std::string(": ") + error->message : std::string()) - << (debug ? std::string(" debug: ") + debug : std::string()); - if (error) - g_error_free(error); - if (debug) - g_free(debug); - - self->m_error = gst_bambu_last_error ? gst_bambu_last_error : 2; - self->m_loaded = false; - self->m_gtk_state = wxMEDIASTATE_STOPPED; - self->PostGtkSinkStateEvent(self->GetId()); - break; - } - case GST_MESSAGE_EOS: - self->m_loaded = false; - self->m_gtk_state = wxMEDIASTATE_STOPPED; - self->PostGtkSinkStateEvent(self->GetId()); - break; - case GST_MESSAGE_STATE_CHANGED: - if (GST_MESSAGE_SRC(message) == GST_OBJECT(self->m_gtk_playbin)) { - GstState old_state; - GstState new_state; - GstState pending_state; - gst_message_parse_state_changed(message, &old_state, &new_state, &pending_state); - - if (new_state == GST_STATE_PLAYING) { - self->m_loaded = true; - self->m_gtk_state = wxMEDIASTATE_PLAYING; - self->PostGtkSinkStateEvent(); - } else if (new_state == GST_STATE_PAUSED && old_state < GST_STATE_PAUSED) { - // Treat only upward READY/NULL -> PAUSED as load completion. - // PLAYING -> PAUSED is a normal teardown step before NULL. - self->m_loaded = true; - self->m_gtk_state = wxMEDIASTATE_PAUSED; - self->PostGtkSinkStateEvent(); - } else if (new_state <= GST_STATE_READY && old_state >= GST_STATE_PAUSED) { - self->m_loaded = false; - self->m_gtk_state = wxMEDIASTATE_STOPPED; - self->PostGtkSinkStateEvent(); - } - } - break; - default: - break; - } - - return G_SOURCE_CONTINUE; - }, this); - gst_object_unref(bus); - - BOOST_LOG_TRIVIAL(info) << "wxMediaCtrl2: using GTK native Wayland video sink"; - return true; -} - -void wxMediaCtrl2::DestroyGtkSinkPlayer() -{ - if (m_gtk_bus_watch_id) { - g_source_remove(m_gtk_bus_watch_id); - m_gtk_bus_watch_id = 0; - } - - if (m_gtk_playbin) { - gst_element_set_state(m_gtk_playbin, GST_STATE_NULL); - } - - if (m_gtk_video_window) { - m_gtk_video_window->Destroy(); - m_gtk_video_window = nullptr; - } - - if (m_gtk_playbin) { - gst_object_unref(m_gtk_playbin); - m_gtk_playbin = nullptr; - } - - if (m_gtk_sink) { - gst_object_unref(m_gtk_sink); - m_gtk_sink = nullptr; - } - -} - -void wxMediaCtrl2::PostGtkSinkStateEvent(int id) -{ - wxMediaEvent event(wxEVT_MEDIA_STATECHANGED); - event.SetId(id); - event.SetEventObject(this); - wxPostEvent(this, event); -} -#endif // defined(__LINUX__) && defined(__WXGTK__) - -#define CLSID_BAMBU_SOURCE L"{233E64FB-2041-4A6C-AFAB-FF9BCF83E7AA}" - -void wxMediaCtrl2::Load(wxURI url) -{ -#ifdef __WIN32__ - InvalidateBestSize(); - if (m_imp == nullptr) { - static bool notified = false; - if (!notified) CallAfter([] { - auto res = wxMessageBox(_L("Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?"), _L("Error"), wxOK | wxCANCEL); - if (res == wxOK) { - wxString url = IsWindows10OrGreater() - ? "ms-settings:optionalfeatures?activationSource=SMC-Article-14209" - : "https://support.microsoft.com/en-au/windows/get-windows-media-player-81718e0d-cfce-25b1-aee3-94596b658287"; - wxExecute("cmd /c start " + url, wxEXEC_HIDE_CONSOLE); - } - }); - m_error = 100; - wxMediaEvent event(wxEVT_MEDIA_STATECHANGED); - event.SetId(GetId()); - event.SetEventObject(this); - wxPostEvent(this, event); - return; - } - { - wxRegKey key11(wxRegKey::HKCU, L"SOFTWARE\\Classes\\CLSID\\" CLSID_BAMBU_SOURCE L"\\InProcServer32"); - wxRegKey key12(wxRegKey::HKCR, L"CLSID\\" CLSID_BAMBU_SOURCE L"\\InProcServer32"); - wxString path = key11.Exists() ? key11.QueryDefaultValue() - : key12.Exists() ? key12.QueryDefaultValue() : wxString{}; - wxRegKey key2(wxRegKey::HKCR, "bambu"); - wxString clsid; - if (key2.Exists()) - key2.QueryRawValue("Source Filter", clsid); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": clsid %1% path %2%") % clsid % path; - - std::string data_dir_str = Slic3r::data_dir(); - boost::filesystem::path data_dir_path(data_dir_str); - auto dll_path = data_dir_path / "plugins" / "BambuSource.dll"; - if (path.empty() || !wxFile::Exists(path) || clsid != CLSID_BAMBU_SOURCE) { - if (boost::filesystem::exists(dll_path)) { - CallAfter( - [dll_path] { - int res = wxMessageBox(_L("BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice"), _L("Error"), wxYES_NO); - if (res == wxYES) { - std::string regContent = R"(Windows Registry Editor Version 5.00 - [HKEY_CLASSES_ROOT\bambu] - "Source Filter"="{233E64FB-2041-4A6C-AFAB-FF9BCF83E7AA}" - )"; - - auto reg_path = (fs::temp_directory_path() / fs::unique_path()).replace_extension(".reg"); - std::ofstream temp_reg_file(reg_path.c_str()); - if (!temp_reg_file) { - return false; - } - temp_reg_file << regContent; - temp_reg_file.close(); - auto sei_params = L"/q /s " + reg_path.wstring(); - SHELLEXECUTEINFO sei{sizeof(sei), SEE_MASK_NOCLOSEPROCESS, NULL, L"open", - L"regedit", sei_params.c_str(),SW_HIDE,SW_HIDE}; - ::ShellExecuteEx(&sei); - - wstring quoted_dll_path = L"\"" + dll_path.wstring() + L"\""; - SHELLEXECUTEINFO info{sizeof(info), 0, NULL, L"runas", L"regsvr32", quoted_dll_path.c_str(), SW_HIDE }; - ::ShellExecuteEx(&info); - fs::remove(reg_path); - } - return true; - }); - } else { - CallAfter([] { - wxMessageBox(_L("Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help."), _L("Error"), wxOK); - }); - } - m_error = clsid != CLSID_BAMBU_SOURCE ? 101 : path.empty() ? 102 : 103; - wxMediaEvent event(wxEVT_MEDIA_STATECHANGED); - event.SetId(GetId()); - event.SetEventObject(this); - wxPostEvent(this, event); - return; - } - if (path != dll_path) { - static bool notified = false; - if (!notified) CallAfter([dll_path] { - int res = wxMessageBox(_L("Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it."), _L("Warning"), wxYES_NO | wxICON_WARNING); - if (res == wxYES) { - auto path = dll_path.wstring(); - if (path.find(L' ') != std::wstring::npos) - path = L"\"" + path + L"\""; - SHELLEXECUTEINFO info{sizeof(info), 0, NULL, L"open", L"regsvr32", path.c_str(), SW_HIDE}; - ::ShellExecuteEx(&info); - } - }); - notified = true; - } - wxRegKey keyWmp(wxRegKey::HKCU, "SOFTWARE\\Microsoft\\MediaPlayer\\Player\\Extensions\\."); - keyWmp.Create(); - long permissions = 0; - if (keyWmp.HasValue("Permissions")) - keyWmp.QueryValue("Permissions", &permissions); - if ((permissions & 32) == 0) { - permissions |= 32; - keyWmp.SetValue("Permissions", permissions); - } - } - url = wxURI(url.BuildURI().append("&hwnd=").append(boost::lexical_cast<std::string>(GetHandle())).append("&tid=").append( - boost::lexical_cast<std::string>(GetCurrentThreadId()))); -#endif -#ifdef __WXGTK3__ - GstElementFactory *factory; - int hasplugins = 1; - - factory = gst_element_factory_find("h264parse"); - if (!factory) { - hasplugins = 0; - } else { - gst_object_unref(factory); - } - - factory = gst_element_factory_find("openh264dec"); - if (!factory) { - factory = gst_element_factory_find("avdec_h264"); - } - if (!factory) { - factory = gst_element_factory_find("vaapih264dec"); - } - if (!factory) { - factory = gst_element_factory_find("vah264dec"); - } - if (!factory) { - factory = gst_element_factory_find("nvh264dec"); - } - if (!factory) { - factory = gst_element_factory_find("v4l2h264dec"); - } - if (!factory) { - hasplugins = 0; - } else { - gst_object_unref(factory); - } - - if (!hasplugins) { - CallAfter([] { - wxMessageBox(_L("Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)"), _L("Error"), wxOK); - }); - m_error = 101; - wxMediaEvent event(wxEVT_MEDIA_STATECHANGED); - event.SetId(GetId()); - event.SetEventObject(this); - wxPostEvent(this, event); - return; - } - wxLog::EnableLogging(false); -#endif - m_error = 0; - m_loaded = false; -#if defined(__LINUX__) && defined(__WXGTK__) - if (m_use_gtk_sink && m_gtk_playbin) { - const std::string uri = std::string(url.BuildURI().ToUTF8().data()); - gst_element_set_state(m_gtk_playbin, GST_STATE_NULL); - g_object_set(G_OBJECT(m_gtk_playbin), "uri", uri.c_str(), nullptr); - m_gtk_state = wxMEDIASTATE_STOPPED; - GstStateChangeReturn state = gst_element_set_state(m_gtk_playbin, GST_STATE_PAUSED); - if (state == GST_STATE_CHANGE_FAILURE) { - m_error = gst_bambu_last_error ? gst_bambu_last_error : 2; - PostGtkSinkStateEvent(GetId()); - } - return; - } - if (!m_imp) { - m_error = m_native_wayland && !m_gtk_sink_error.empty() ? 104 : 100; - m_loaded = false; - if (m_native_wayland && !m_gtk_sink_error.empty() && !m_gtk_sink_error_notified) { - m_gtk_sink_error_notified = true; - const wxString message = m_gtk_sink_error; - CallAfter([message] { - wxMessageBox(message, _L("Error"), wxOK); - }); - } - wxMediaEvent event(wxEVT_MEDIA_STATECHANGED); - event.SetId(GetId()); - event.SetEventObject(this); - wxPostEvent(this, event); - return; - } -#endif - wxMediaCtrl::Load(url); -} - -void wxMediaCtrl2::Play() -{ -#if defined(__LINUX__) && defined(__WXGTK__) - if (m_use_gtk_sink && m_gtk_playbin) { - GstStateChangeReturn state = gst_element_set_state(m_gtk_playbin, GST_STATE_PLAYING); - if (state == GST_STATE_CHANGE_FAILURE) { - m_error = gst_bambu_last_error ? gst_bambu_last_error : 2; - m_gtk_state = wxMEDIASTATE_STOPPED; - PostGtkSinkStateEvent(GetId()); - } - return; - } - if (!m_imp) { - m_error = m_native_wayland && !m_gtk_sink_error.empty() ? 104 : 100; - if (m_native_wayland && !m_gtk_sink_error.empty() && !m_gtk_sink_error_notified) { - m_gtk_sink_error_notified = true; - const wxString message = m_gtk_sink_error; - CallAfter([message] { - wxMessageBox(message, _L("Error"), wxOK); - }); - } - PostGtkSinkStateEvent(GetId()); - return; - } -#endif - wxMediaCtrl::Play(); -} - -void wxMediaCtrl2::Stop() -{ -#if defined(__LINUX__) && defined(__WXGTK__) - if (m_use_gtk_sink && m_gtk_playbin) { - gst_element_set_state(m_gtk_playbin, GST_STATE_NULL); - m_gtk_state = wxMEDIASTATE_STOPPED; - m_loaded = false; - PostGtkSinkStateEvent(0); - return; - } - if (!m_imp) - return; -#endif - wxMediaCtrl::Stop(); -} - -wxMediaState wxMediaCtrl2::GetState() -{ -#if defined(__LINUX__) && defined(__WXGTK__) - if (m_use_gtk_sink && m_gtk_playbin) - return m_gtk_state; - if (!m_imp) - return wxMEDIASTATE_STOPPED; -#endif - return wxMediaCtrl::GetState(); -} - -int wxMediaCtrl2::GetLastError() const -{ -#ifdef __LINUX__ -#ifdef __WXGTK__ - if (m_use_gtk_sink && m_error) - return m_error; -#endif - if (m_error) - return m_error; - return gst_bambu_last_error; -#else - return m_error; -#endif -} - -wxSize wxMediaCtrl2::GetVideoSize() const -{ -#ifdef __LINUX__ - // Gstreamer doesn't give us a VideoSize until we're playing, which - // confuses the MediaPlayCtrl into claiming that it is stuck - // "Loading...". Fake it out for now. - return m_loaded ? wxSize(1280, 720) : wxSize{}; -#else - wxSize size = m_imp ? m_imp->GetVideoSize() : wxSize(0, 0); - if (size.GetWidth() > 0) - const_cast<wxSize&>(m_video_size) = size; - return size; -#endif -} - -wxSize wxMediaCtrl2::DoGetBestSize() const -{ - return {-1, -1}; -} - -#ifdef __WIN32__ - -WXLRESULT wxMediaCtrl2::MSWWindowProc(WXUINT nMsg, - WXWPARAM wParam, - WXLPARAM lParam) -{ - // The stream source sends WM_USER+1000 with a synchronous SendMessage from its own threads, - // so this runs re-entrantly on the UI thread at whatever message-retrieval point the player - // happens to be in - often nested inside an Orca log statement. Never BOOST_LOG_TRIVIAL here: - // boost::log is not re-entrant on one thread, and doing so corrupted its per-thread record - // state, crashing later in unrelated places (the player, the log filter, a plug-in heap free). - // Post the string out (as the stat branch does) and log it on a clean stack instead. - if (nMsg == WM_USER + 1000) { - wxString msg((wchar_t const *) lParam); - if (wParam == 1) { - if (msg.EndsWith("]")) { - int n = msg.find_last_of('['); - if (n != wxString::npos) { - long val = 0; - if (msg.SubString(n + 1, msg.Length() - 2).ToLong(&val)) - m_error = (int) val; - } - } else if (msg.Contains("stat_log")) { - wxCommandEvent evt(EVT_MEDIA_CTRL_STAT); - evt.SetEventObject(this); - evt.SetString(msg.Mid(msg.Find(' ') + 1)); - wxPostEvent(this, evt); - } - } - return 0; - } - return wxMediaCtrl::MSWWindowProc(nMsg, wParam, lParam); -} - -#endif diff --git a/src/slic3r/GUI/wxMediaCtrl2.h b/src/slic3r/GUI/wxMediaCtrl2.h deleted file mode 100644 index 4f37b5cd1e..0000000000 --- a/src/slic3r/GUI/wxMediaCtrl2.h +++ /dev/null @@ -1,117 +0,0 @@ -// -// wxMediaCtrl2.h -// libslic3r_gui -// -// Created by cmguo on 2021/12/7. -// - -#ifndef wxMediaCtrl2_h -#define wxMediaCtrl2_h - -#include "wx/uri.h" -#include "wx/mediactrl.h" - -wxDECLARE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent); - -#if defined(__LINUX__) && defined(__WXGTK__) -typedef struct _GstElement GstElement; -#endif - -#ifdef __WXMAC__ - -class wxMediaCtrl2 : public wxWindow -{ -public: - wxMediaCtrl2(wxWindow * parent); - - ~wxMediaCtrl2(); - - void Load(wxURI url); - - void Play(); - - void Stop(); - - void SetIdleImage(wxString const & image); - - wxMediaState GetState() const; - - wxSize GetVideoSize() const; - - int GetLastError() const { return m_error; } - - static inline const wxMediaState MEDIASTATE_BUFFERING = static_cast<wxMediaState>(6); - -protected: - void DoSetSize(int x, int y, int width, int height, int sizeFlags) override; - - static void bambu_log(void const * ctx, int level, char const * msg); - - void NotifyStopped(); - -private: - void create_player(); - void * m_player = nullptr; - wxMediaState m_state = wxMEDIASTATE_STOPPED; - int m_error = 0; - wxSize m_video_size{16, 9}; -}; - -#else - -class wxMediaCtrl2 : public wxMediaCtrl -{ -public: - wxMediaCtrl2(wxWindow *parent); - ~wxMediaCtrl2(); - - void Load(wxURI url); - - void Play(); - - void Stop(); - - void SetIdleImage(wxString const & image); - - wxMediaState GetState(); - - int GetLastError() const; - - wxSize GetVideoSize() const; - -protected: - wxSize DoGetBestSize() const override; - - void DoSetSize(int x, int y, int width, int height, int sizeFlags) override; - -#ifdef __WIN32__ - WXLRESULT MSWWindowProc(WXUINT nMsg, - WXWPARAM wParam, - WXLPARAM lParam) override; -#endif - -private: -#if defined(__LINUX__) && defined(__WXGTK__) - bool CreateGtkSinkPlayer(); - void DestroyGtkSinkPlayer(); - void PostGtkSinkStateEvent(int id = 0); - - bool m_native_wayland = false; - bool m_use_gtk_sink = false; - wxString m_gtk_sink_error; - bool m_gtk_sink_error_notified = false; - GstElement *m_gtk_playbin = nullptr; - GstElement *m_gtk_sink = nullptr; - unsigned int m_gtk_bus_watch_id = 0; - wxWindow *m_gtk_video_window = nullptr; - wxMediaState m_gtk_state = wxMEDIASTATE_STOPPED; -#endif - wxString m_idle_image; - int m_error = 0; - bool m_loaded = false; - wxSize m_video_size{16, 9}; -}; - -#endif - -#endif /* wxMediaCtrl2_h */ diff --git a/src/slic3r/GUI/wxMediaCtrl2.mm b/src/slic3r/GUI/wxMediaCtrl2.mm deleted file mode 100644 index 063472eccc..0000000000 --- a/src/slic3r/GUI/wxMediaCtrl2.mm +++ /dev/null @@ -1,170 +0,0 @@ -// -// wxMediaCtrl2.m -// OrcaSlicer -// -// Created by cmguo on 2021/12/7. -// - -#import "wxMediaCtrl2.h" -#import "wx/mediactrl.h" -#include <boost/log/trivial.hpp> - -#import <Foundation/Foundation.h> -#import "BambuPlayer/BambuPlayer.h" -#import "../Utils/NetworkAgent.hpp" - -#include <stdlib.h> -#include <dlfcn.h> - -wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent); - -#define BAMBU_DYNAMIC - -void wxMediaCtrl2::bambu_log(void const * ctx, int level, char const * msg) -{ - if (level == 1) { - wxString msg2(msg); - if (msg2.EndsWith("]")) { - int n = msg2.find_last_of('['); - if (n != wxString::npos) { - long val = 0; - wxMediaCtrl2 * ctrl = (wxMediaCtrl2 *) ctx; - if (msg2.SubString(n + 1, msg2.Length() - 2).ToLong(&val)) - ctrl->m_error = (int) val; - } - } else if (strstr(msg, "stat_log")) { - wxMediaCtrl2 * ctrl = (wxMediaCtrl2 *) ctx; - wxCommandEvent evt(EVT_MEDIA_CTRL_STAT); - evt.SetEventObject(ctrl); - evt.SetString(strchr(msg, ' ') + 1); - wxPostEvent(ctrl, evt); - } - } else if (level < 0) { - wxMediaCtrl2 * ctrl = (wxMediaCtrl2 *) ctx; - ctrl->NotifyStopped(); - } - BOOST_LOG_TRIVIAL(info) << msg; -} - -wxMediaCtrl2::wxMediaCtrl2(wxWindow * parent) - : wxWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) -{ - NSView * imageView = (NSView *) GetHandle(); - imageView.layer = [[CALayer alloc] init]; - CGColorRef color = CGColorCreateGenericRGB(0, 0, 0, 1.0f); - imageView.layer.backgroundColor = color; - CGColorRelease(color); - imageView.wantsLayer = YES; - create_player(); -} - -wxMediaCtrl2::~wxMediaCtrl2() -{ - BambuPlayer * player = (BambuPlayer *) m_player; - [player dealloc]; -} - -void wxMediaCtrl2::create_player() -{ - auto module = Slic3r::NetworkAgent::get_bambu_source_entry(); - if (!module) { - //not ready yet - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "Network plugin not ready currently!"; - return; - } - Class cls = (__bridge Class) dlsym(module, "OBJC_CLASS_$_BambuPlayer"); - if (cls == nullptr) { - m_error = -2; - return; - } - NSView * imageView = (NSView *) GetHandle(); - BambuPlayer * player = [cls alloc]; - [player initWithImageView: imageView]; - [player setLogger: bambu_log withContext: this]; - m_player = player; -} - -void wxMediaCtrl2::Load(wxURI url) -{ - if (!m_player) { - create_player(); - if (!m_player) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": create_player failed currently!"; - return; - } - } - - BambuPlayer * player = (BambuPlayer *) m_player; - if (player) { - [player close]; - m_error = 0; - m_error = [player open: url.BuildURI().ToUTF8()]; - } - wxMediaEvent event(wxEVT_MEDIA_STATECHANGED); - event.SetId(GetId()); - event.SetEventObject(this); - wxPostEvent(this, event); -} - -void wxMediaCtrl2::Play() -{ - if (!m_player) { - create_player(); - if (!m_player) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": create_player failed currently!"; - return; - } - } - BambuPlayer * player2 = (BambuPlayer *) m_player; - [player2 play]; - if (m_state != wxMEDIASTATE_PLAYING) { - m_state = wxMEDIASTATE_PLAYING; - wxMediaEvent event(wxEVT_MEDIA_STATECHANGED); - event.SetId(GetId()); - event.SetEventObject(this); - wxPostEvent(this, event); - } -} - -void wxMediaCtrl2::Stop() -{ - if (!m_player) { - create_player(); - if (!m_player) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": create_player failed currently!"; - return; - } - } - BambuPlayer * player2 = (BambuPlayer *) m_player; - [player2 close]; - NotifyStopped(); -} - -void wxMediaCtrl2::NotifyStopped() -{ - if (m_state != wxMEDIASTATE_STOPPED) { - m_state = wxMEDIASTATE_STOPPED; - wxMediaEvent event(wxEVT_MEDIA_STATECHANGED); - event.SetId(GetId()); - event.SetEventObject(this); - wxPostEvent(this, event); - } -} - -wxMediaState wxMediaCtrl2::GetState() const -{ - return m_state; -} - -wxSize wxMediaCtrl2::GetVideoSize() const -{ - BambuPlayer * player2 = (BambuPlayer *) m_player; - if (player2) { - NSSize size = [player2 videoSize]; - if (size.width > 0) - const_cast<wxSize&>(m_video_size) = {(int) size.width, (int) size.height}; - return {(int) size.width, (int) size.height}; - } else { - return {0, 0}; - } -} diff --git a/src/slic3r/GUI/wxMediaCtrl3.cpp b/src/slic3r/GUI/wxMediaCtrl3.cpp new file mode 100644 index 0000000000..098db68808 --- /dev/null +++ b/src/slic3r/GUI/wxMediaCtrl3.cpp @@ -0,0 +1,313 @@ +#include "wxMediaCtrl3.h" +#include "AVVideoDecoder.hpp" +#include "I18N.hpp" +#include "libslic3r/Utils.hpp" +#include <boost/log/trivial.hpp> +#include <wx/dcclient.h> +#ifdef __WIN32__ +#include <versionhelpers.h> +#include <wx/msw/registry.h> +#include <shellapi.h> +#endif + +wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent); + +BEGIN_EVENT_TABLE(wxMediaCtrl3, wxWindow) + +// catch paint events +EVT_PAINT(wxMediaCtrl3::paintEvent) + +END_EVENT_TABLE() + +struct StaticBambuLib : BambuLib +{ + static StaticBambuLib &get(BambuLib *); +}; + +wxMediaCtrl3::wxMediaCtrl3(wxWindow *parent) + : wxWindow(parent, wxID_ANY) + , BambuLib(StaticBambuLib::get(this)) + , m_thread([this] { PlayThread(); }) +{ + SetBackgroundColour("#000001ff"); +} + +wxMediaCtrl3::~wxMediaCtrl3() +{ + { + std::unique_lock<std::mutex> lk(m_mutex); + m_url.reset(new wxURI); + m_frame = wxImage(m_idle_image); + m_cond.notify_all(); + } + m_thread.join(); +} + +void wxMediaCtrl3::Load(wxURI url) +{ + std::unique_lock<std::mutex> lk(m_mutex); + m_video_size = wxDefaultSize; + m_error = 0; + m_url.reset(new wxURI(url)); + m_cond.notify_all(); +} + +void wxMediaCtrl3::Play() +{ + std::unique_lock<std::mutex> lk(m_mutex); + if (m_state != wxMEDIASTATE_PLAYING) { + m_state = wxMEDIASTATE_PLAYING; + wxMediaEvent event(wxEVT_MEDIA_STATECHANGED); + event.SetId(GetId()); + event.SetEventObject(this); + wxPostEvent(this, event); + } +} + +void wxMediaCtrl3::Stop() +{ + std::unique_lock<std::mutex> lk(m_mutex); + m_url.reset(); + m_frame = wxImage(m_idle_image); + NotifyStopped(); + m_cond.notify_all(); + Refresh(); +} + +void wxMediaCtrl3::SetIdleImage(wxString const &image) +{ + if (m_idle_image == image) + return; + m_idle_image = image; + if (m_url == nullptr) { + std::unique_lock<std::mutex> lk(m_mutex); + m_frame = wxImage(m_idle_image); + assert(m_frame.IsOk()); + Refresh(); + } +} + +wxMediaState wxMediaCtrl3::GetState() +{ + std::unique_lock<std::mutex> lk(m_mutex); + return m_state; +} + +int wxMediaCtrl3::GetLastError() +{ + std::unique_lock<std::mutex> lk(m_mutex); + return m_error; +} + +wxSize wxMediaCtrl3::GetVideoSize() +{ + std::unique_lock<std::mutex> lk(m_mutex); + return m_video_size; +} + +wxSize wxMediaCtrl3::DoGetBestSize() const +{ + return {-1, -1}; +} + +static void adjust_frame_size(wxSize & frame, wxSize const & video, wxSize const & window) +{ + if (video.x * window.y < video.y * window.x) + frame = { video.x * window.y / video.y, window.y }; + else + frame = { window.x, video.y * window.x / video.x }; +} + +void wxMediaCtrl3::paintEvent(wxPaintEvent &evt) +{ + wxPaintDC dc(this); + auto size = GetSize(); + if (size.x <= 0 || size.y <= 0) + return; + std::unique_lock<std::mutex> lk(m_mutex); + if (!m_frame.IsOk()) + return; + auto size2 = m_frame.GetSize(); + if (size2.x != m_frame_size.x && size2.y == m_frame_size.y) + size2.x = m_frame_size.x; + auto size3 = (size - size2) / 2; + if (size2.x != size.x && size2.y != size.y) { + double scale = 1.; + if (size.x * size2.y > size.y * size2.x) { + size3 = {size.x * size2.y / size.y, size2.y}; + scale = double(size.y) / size2.y; + } else { + size3 = {size2.x, size.y * size2.x / size.x}; + scale = double(size.x) / size2.x; + } + dc.SetUserScale(scale, scale); + size3 = (size3 - size2) / 2; + } + dc.DrawBitmap(m_frame, size3.x, size3.y); +} + +void wxMediaCtrl3::DoSetSize(int x, int y, int width, int height, int sizeFlags) +{ + wxWindow::DoSetSize(x, y, width, height, sizeFlags); + if (sizeFlags == wxSIZE_USE_EXISTING) return; + wxMediaCtrl_OnSize(this, m_video_size, width, height); + std::unique_lock<std::mutex> lk(m_mutex); + adjust_frame_size(m_frame_size, m_video_size, GetSize()); + Refresh(); +} + +void wxMediaCtrl3::bambu_log(void *ctx, int level, tchar const *msg2) +{ +#ifdef _WIN32 + wxString msg(msg2); +#else + wxString msg = wxString::FromUTF8(msg2); +#endif + if (level == 1) { + if (msg.EndsWith("]")) { + int n = msg.find_last_of('['); + if (n != wxString::npos) { + long val = 0; + wxMediaCtrl3 *ctrl = (wxMediaCtrl3 *) ctx; + if (msg.SubString(n + 1, msg.Length() - 2).ToLong(&val)) { + std::unique_lock<std::mutex> lk(ctrl->m_mutex); + ctrl->m_error = (int) val; + } + } + } else if (msg.Contains("stat_log")) { + wxCommandEvent evt(EVT_MEDIA_CTRL_STAT); + wxMediaCtrl3 *ctrl = (wxMediaCtrl3 *) ctx; + evt.SetEventObject(ctrl); + evt.SetString(msg.Mid(msg.Find(' ') + 1)); + wxPostEvent(ctrl, evt); + } + } + BOOST_LOG_TRIVIAL(info) << msg.ToUTF8().data(); +} + +void wxMediaCtrl3::PlayThread() +{ + using namespace std::chrono_literals; + std::shared_ptr<wxURI> url; + std::unique_lock<std::mutex> lk(m_mutex); + while (true) { + m_cond.wait(lk, [this, &url] { return m_url != url; }); + url = m_url; + if (url == nullptr) + continue; + if (!url->HasScheme()) + break; + lk.unlock(); + Bambu_Tunnel tunnel = nullptr; + int error = Bambu_Create(&tunnel, m_url->BuildURI().ToUTF8()); + if (error == 0) { + Bambu_SetLogger(tunnel, &wxMediaCtrl3::bambu_log, this); + error = Bambu_Open(tunnel); + if (error == 0) + error = Bambu_would_block; + } + lk.lock(); + while (error == int(Bambu_would_block)) { + m_cond.wait_for(lk, 100ms); + if (m_url != url) { + error = 1; + break; + } + lk.unlock(); + error = Bambu_StartStream(tunnel, true); + lk.lock(); + } + Bambu_StreamInfo info; + if (error == 0) + error = Bambu_GetStreamInfo(tunnel, 0, &info); + AVVideoDecoder decoder; + int minFrameDuration = 0; + if (error == 0) { + decoder.open(info); + m_video_size = { info.format.video.width, info.format.video.height }; + adjust_frame_size(m_frame_size, m_video_size, GetSize()); + minFrameDuration = 800 / info.format.video.frame_rate; // 80% + NotifyStopped(); + } + Bambu_Sample sample; + while (error == 0) { + lk.unlock(); + error = Bambu_ReadSample(tunnel, &sample); + lk.lock(); + while (error == int(Bambu_would_block)) { + m_cond.wait_for(lk, 100ms); + if (m_url != url) { + error = 1; + break; + } + lk.unlock(); + error = Bambu_ReadSample(tunnel, &sample); + lk.lock(); + } + if (error == 0) { + auto frame_size = m_frame_size; + lk.unlock(); + decoder.decode(sample); +#ifdef _WIN32 + wxBitmap bm; + decoder.toWxBitmap(bm, frame_size); +#else + wxImage bm; + decoder.toWxImage(bm, frame_size); +#endif + lk.lock(); + if (m_url != url) { + error = 1; + break; + } + if (bm.IsOk()) { + auto now = std::chrono::system_clock::now(); + if (m_last_PTS && (sample.decode_time - m_last_PTS) < 30000000ULL) { // 3s + auto next_PTS_expected = m_last_PTS_expected + std::chrono::milliseconds((sample.decode_time - m_last_PTS) / 10000ULL); + // The frame is late, catch up a little + auto next_PTS_practical = m_last_PTS_practical + std::chrono::milliseconds(minFrameDuration); + auto next_PTS = std::max(next_PTS_expected, next_PTS_practical); + if(now < next_PTS) + std::this_thread::sleep_until(next_PTS); + else + next_PTS = now; + //auto text = wxString::Format(L"wxMediaCtrl3 pts diff %ld\n", std::chrono::duration_cast<std::chrono::milliseconds>(next_PTS - next_PTS_expected).count()); + //OutputDebugString(text); + m_last_PTS = sample.decode_time; + m_last_PTS_expected = next_PTS_expected; + m_last_PTS_practical = next_PTS; + } else { + // Resync + m_last_PTS = sample.decode_time; + m_last_PTS_expected = now; + m_last_PTS_practical = now; + } + m_frame = bm; + } + CallAfter([this] { Refresh(); }); + } + } + if (tunnel) { + lk.unlock(); + Bambu_Close(tunnel); + Bambu_Destroy(tunnel); + tunnel = nullptr; + lk.lock(); + } + if (m_url == url) + m_error = error; + m_frame_size = wxDefaultSize; + m_video_size = wxDefaultSize; + NotifyStopped(); + } + +} + +void wxMediaCtrl3::NotifyStopped() +{ + m_state = wxMEDIASTATE_STOPPED; + wxMediaEvent event(wxEVT_MEDIA_STATECHANGED); + event.SetId(GetId()); + event.SetEventObject(this); + wxPostEvent(this, event); +} diff --git a/src/slic3r/GUI/wxMediaCtrl3.h b/src/slic3r/GUI/wxMediaCtrl3.h new file mode 100644 index 0000000000..1d64955ffd --- /dev/null +++ b/src/slic3r/GUI/wxMediaCtrl3.h @@ -0,0 +1,85 @@ +// +// wxMediaCtrl3.h +// libslic3r_gui +// +// Created by cmguo on 2024/6/22. +// + +#ifndef wxMediaCtrl3_h +#define wxMediaCtrl3_h + +#include "wx/uri.h" +#include "wx/mediactrl.h" + +wxDECLARE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent); + +void wxMediaCtrl_OnSize(wxWindow * ctrl, wxSize const & videoSize, int width, int height); + +#define BAMBU_DYNAMIC +#include <condition_variable> +#include <thread> +#ifndef _WIN32 +#include <wx/image.h> +#endif +#include "Printer/BambuTunnel.h" + +class AVVideoDecoder; + +class wxMediaCtrl3 : public wxWindow, BambuLib +{ +public: + wxMediaCtrl3(wxWindow *parent); + + ~wxMediaCtrl3(); + + void Load(wxURI url); + + void Play(); + + void Stop(); + + void SetIdleImage(wxString const & image); + + wxMediaState GetState(); + + int GetLastError(); + + wxSize GetVideoSize(); + +protected: + DECLARE_EVENT_TABLE() + + void paintEvent(wxPaintEvent &evt); + + wxSize DoGetBestSize() const override; + + void DoSetSize(int x, int y, int width, int height, int sizeFlags) override; + + static void bambu_log(void *ctx, int level, tchar const *msg); + + void PlayThread(); + + void NotifyStopped(); + +private: + wxString m_idle_image; + wxMediaState m_state = wxMEDIASTATE_STOPPED; + int m_error = 0; + wxSize m_video_size = wxDefaultSize; + wxSize m_frame_size = wxDefaultSize; +#ifdef _WIN32 + wxBitmap m_frame; +#else + wxImage m_frame; +#endif + + std::shared_ptr<wxURI> m_url; + std::uint64_t m_last_PTS{0}; + std::chrono::system_clock::time_point m_last_PTS_expected; + std::chrono::system_clock::time_point m_last_PTS_practical; + std::mutex m_mutex; + std::condition_variable m_cond; + std::thread m_thread; +}; + +#endif /* wxMediaCtrl3_h */ diff --git a/src/slic3r/GitCommitHash.cmake b/src/slic3r/GitCommitHash.cmake new file mode 100644 index 0000000000..9302885e6a --- /dev/null +++ b/src/slic3r/GitCommitHash.cmake @@ -0,0 +1,51 @@ +# Writes GIT_COMMIT_HASH and GIT_COMMIT_SUFFIX into a generated header. +# GIT_COMMIT_SUFFIX is "-dirty" for a build with uncommitted changes, and empty +# otherwise. +# +# A custom target runs this at the start of every build, which picks up a new +# commit without a reconfigure. The header is rewritten only when the value +# changes. +# +# Inputs: SOURCE_DIR, OUT_FILE. + +find_package(Git QUIET) + +set(HASH "") +set(SUFFIX "") + +if (DEFINED ENV{git_commit_hash} AND NOT "$ENV{git_commit_hash}" STREQUAL "") + if (GIT_FOUND AND EXISTS "${SOURCE_DIR}/.git") + execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short "$ENV{git_commit_hash}" + WORKING_DIRECTORY ${SOURCE_DIR} OUTPUT_VARIABLE HASH OUTPUT_STRIP_TRAILING_WHITESPACE) + else () + # No .git directory (e.g. Flatpak sandbox) - truncate directly + string(SUBSTRING "$ENV{git_commit_hash}" 0 7 HASH) + endif () +elseif (GIT_FOUND AND EXISTS "${SOURCE_DIR}/.git") + execute_process(COMMAND ${GIT_EXECUTABLE} log -1 --format=%h + WORKING_DIRECTORY ${SOURCE_DIR} OUTPUT_VARIABLE HASH OUTPUT_STRIP_TRAILING_WHITESPACE) + execute_process(COMMAND ${GIT_EXECUTABLE} diff --quiet HEAD + WORKING_DIRECTORY ${SOURCE_DIR} RESULT_VARIABLE DIRTY ERROR_QUIET) + if (DIRTY EQUAL 1) + set(SUFFIX "-dirty") + endif () +endif () + +if (NOT HASH) + set(HASH "0000000") # uninitialized +endif () + +message(STATUS "Build commit: ${HASH}${SUFFIX}") + +string(CONCAT CONTENT + "#pragma once\n" + "#define GIT_COMMIT_HASH \"${HASH}\"\n" + "#define GIT_COMMIT_SUFFIX \"${SUFFIX}\"\n") + +set(OLD "") +if (EXISTS "${OUT_FILE}") + file(READ "${OUT_FILE}" OLD) +endif () +if (NOT OLD STREQUAL CONTENT) + file(WRITE "${OUT_FILE}" "${CONTENT}") +endif () diff --git a/src/slic3r/Utils/3DPrinterOS.cpp b/src/slic3r/Utils/3DPrinterOS.cpp index 79c8a62700..61fcc80d5b 100755 --- a/src/slic3r/Utils/3DPrinterOS.cpp +++ b/src/slic3r/Utils/3DPrinterOS.cpp @@ -323,7 +323,7 @@ bool C3DPrinterOS::login(wxString& msg) const msg.clear(); std::string token = get_api_auth_token(msg); if (token.empty()) { - msg = _L("Error. Can't get api token for authorization"); + msg = _L("Error. Can't get API token for authorization"); return false; } @@ -627,7 +627,7 @@ void C3DPrinterOS::send_form( responseTree.put("result", false); responseTree.put("message", error); }) - .on_complete([&, this](std::string body, unsigned) { + .on_complete([&](std::string body, unsigned) { std::stringstream ss(body); try { pt::read_json(ss, responseTree); diff --git a/src/slic3r/Utils/ASCIIFolding.cpp b/src/slic3r/Utils/ASCIIFolding.cpp index 0eb02a5f8c..016c30fcde 100644 --- a/src/slic3r/Utils/ASCIIFolding.cpp +++ b/src/slic3r/Utils/ASCIIFolding.cpp @@ -4,7 +4,6 @@ #include <string.h> #include <locale> #include <boost/locale/encoding_utf.hpp> -#include <codecvt> #include <regex> namespace Slic3r { @@ -1953,8 +1952,7 @@ std::string fold_utf8_to_ascii(const std::string &src, bool is_convert_for_filen for (wchar_t c : wstr) fold_to_ascii(c, out); if (is_convert_for_filename) { - std::wstring_convert<std::codecvt_utf8<wchar_t>> converter; - auto dstStr = converter.to_bytes(dst); + auto dstStr = boost::locale::conv::utf_to_utf<char>(dst.c_str(), dst.c_str() + dst.size()); std::size_t found = dstStr.find_last_of("/\\"); if (found != std::string::npos) { @@ -1964,7 +1962,7 @@ std::string fold_utf8_to_ascii(const std::string &src, bool is_convert_for_filen std::string newFileName = regex_replace(filename, reg, ""); dstStr = dir + "\\" + newFileName; } - dst = converter.from_bytes(dstStr); + dst = boost::locale::conv::utf_to_utf<wchar_t>(dstStr.c_str(), dstStr.c_str() + dstStr.size()); } return boost::locale::conv::utf_to_utf<char>(dst.c_str(), dst.c_str() + dst.size()); diff --git a/src/slic3r/Utils/CrealityPrint.hpp b/src/slic3r/Utils/CrealityPrint.hpp index ddb2054420..3b5287f382 100644 --- a/src/slic3r/Utils/CrealityPrint.hpp +++ b/src/slic3r/Utils/CrealityPrint.hpp @@ -21,14 +21,14 @@ public: ~CrealityPrint() override = default; const char* get_name() const override; - virtual bool can_test() const { return true; }; + virtual bool can_test() const override { return true; }; std::string get_host() const override; bool has_auto_discovery() const override { return true; } wxString get_test_ok_msg() const override; wxString get_test_failed_msg(wxString& msg) const override; virtual bool test(wxString& curl_msg) const override; - PrintHostPostUploadActions get_post_upload_actions() const; + PrintHostPostUploadActions get_post_upload_actions() const override; bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override; bool supports_multi_color_print() const; std::string query_boxes_info() const; diff --git a/src/slic3r/Utils/ElegooLink.hpp b/src/slic3r/Utils/ElegooLink.hpp index eb1ca7ba26..a60d2de1b3 100644 --- a/src/slic3r/Utils/ElegooLink.hpp +++ b/src/slic3r/Utils/ElegooLink.hpp @@ -32,10 +32,10 @@ public: PrintHostPostUploadActions get_post_upload_actions() const override; protected: #ifdef WIN32 - virtual bool upload_inner_with_resolved_ip(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn, const boost::asio::ip::address& resolved_addr) const; + virtual bool upload_inner_with_resolved_ip(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn, const boost::asio::ip::address& resolved_addr) const override; #endif - virtual bool validate_version_text(const boost::optional<std::string> &version_text) const; - virtual bool upload_inner_with_host(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const; + virtual bool validate_version_text(const boost::optional<std::string> &version_text) const override; + virtual bool upload_inner_with_host(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override; #ifdef WIN32 virtual bool test_with_resolved_ip(wxString& curl_msg) const override; diff --git a/src/slic3r/Utils/FixModelByCgal.cpp b/src/slic3r/Utils/FixModelByCgal.cpp index 8d196f7201..9d973844bb 100644 --- a/src/slic3r/Utils/FixModelByCgal.cpp +++ b/src/slic3r/Utils/FixModelByCgal.cpp @@ -12,6 +12,7 @@ #include "libslic3r/MeshBoolean.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/Format/bbs_3mf.hpp" #include "libslic3r/format.hpp" #include "libslic3r/Thread.hpp" #include "../GUI/I18N.hpp" @@ -69,6 +70,9 @@ public: // Returns false if fixing was canceled. fix_result contains error message if failed. bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dialog, const wxString &msg_header, std::string &fix_result, bool keep_painting) { + // Hold SaveObjectGaurd to prevent backup manager from racing concurrent mesh mutations (use-after-free). + SaveObjectGaurd backup_gaurd(model_object); + // Orca: Synchronization primitives for progress updates between worker thread and GUI. std::mutex mtx; std::condition_variable condition; diff --git a/src/slic3r/Utils/IPrinterAgent.hpp b/src/slic3r/Utils/IPrinterAgent.hpp index 91d271316e..0fa3616344 100644 --- a/src/slic3r/Utils/IPrinterAgent.hpp +++ b/src/slic3r/Utils/IPrinterAgent.hpp @@ -2,6 +2,13 @@ #define __I_PRINTER_AGENT_HPP__ #include "bambu_networking.hpp" +// why: these extend the BAMBU_NETWORK_* return space rather than opening a new one - the value +// flows through the same int domain callers already compare against BAMBU_NETWORK_SUCCESS. +// They live here and not in bambu_networking.hpp because that file is a vendor header replaced +// wholesale by header-sync commits (see c09252ce11), which would silently clobber them. +// -70xx is free: the vendor occupies -1..-25 and -10xx through -60xx. +#define ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED -7010 // no translation exists for this command +#define ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE -7020 // a translation exists; this printer lacks the capability #include <string> #include <memory> diff --git a/src/slic3r/Utils/Moonraker.cpp b/src/slic3r/Utils/Moonraker.cpp index f1ca432390..73fdb8486a 100644 --- a/src/slic3r/Utils/Moonraker.cpp +++ b/src/slic3r/Utils/Moonraker.cpp @@ -80,7 +80,7 @@ bool Moonraker::test(wxString &msg) const res = false; msg = format_error(body, error, status); }) - .on_complete([&, this](std::string body, unsigned) { + .on_complete([&](std::string body, unsigned) { BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: /server/info body: %2%") % name % body; try { std::stringstream ss(body); @@ -141,7 +141,7 @@ bool Moonraker::get_storage(wxArrayString &storage_path, wxArrayString &storage_ % name % error % status % body; } }) - .on_complete([&, this](std::string body, unsigned) { + .on_complete([&](std::string body, unsigned) { BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: /server/files/roots body: %2%") % name % body; try { std::stringstream ss(body); diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index d21dce5070..cd3ef82b62 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -1359,7 +1359,6 @@ void MoonrakerPrinterAgent::announce_printhost_device() if (auto* app_config = GUI::wxGetApp().app_config) { const std::string access_code = device_info.api_key.empty() ? "88888888" : device_info.api_key; app_config->set_str("access_code", device_info.dev_id, access_code); - app_config->set_str("user_access_code", device_info.dev_id, access_code); } nlohmann::json payload; diff --git a/src/slic3r/Utils/NetworkAgentFactory.cpp b/src/slic3r/Utils/NetworkAgentFactory.cpp index 3883d99f2e..ff950d0946 100644 --- a/src/slic3r/Utils/NetworkAgentFactory.cpp +++ b/src/slic3r/Utils/NetworkAgentFactory.cpp @@ -465,25 +465,5 @@ void NetworkAgentFactory::deregister_python_printer_agent(const std::string& plu << plugin_key << "' with agent ID '" << agent_id << "'"; } -bool NetworkAgentFactory::is_current_printer_agent_plugin() -{ - auto* preset_bundle = GUI::wxGetApp().preset_bundle; - if (!preset_bundle) - return false; - - std::string agent_key = ORCA_PRINTER_AGENT_ID; - if (preset_bundle->is_bbl_vendor()) - agent_key = BBL_PRINTER_AGENT_ID; - - const auto& cfg = preset_bundle->printers.get_edited_preset().config; - if (cfg.has("printer_agent")) { - const std::string& value = cfg.option<ConfigOptionString>("printer_agent")->value; - if (!value.empty()) - agent_key = value; - } - - const PrinterAgentInfo* info = get_printer_agent_info(agent_key); - return info && info->is_plugin(); -} } // namespace Slic3r diff --git a/src/slic3r/Utils/NetworkAgentFactory.hpp b/src/slic3r/Utils/NetworkAgentFactory.hpp index cfff6fb1c7..a055b19493 100644 --- a/src/slic3r/Utils/NetworkAgentFactory.hpp +++ b/src/slic3r/Utils/NetworkAgentFactory.hpp @@ -166,8 +166,6 @@ public: static void register_python_printer_agent(const std::string& plugin_key, const std::string& capability_name); static void deregister_python_printer_agent(const std::string& plugin_key, const std::string& capability_name); - static bool is_current_printer_agent_plugin(); - private: // Factory is not instantiable NetworkAgentFactory() = delete; diff --git a/src/slic3r/Utils/Obico.cpp b/src/slic3r/Utils/Obico.cpp index 5541d1c683..92d748b496 100644 --- a/src/slic3r/Utils/Obico.cpp +++ b/src/slic3r/Utils/Obico.cpp @@ -86,7 +86,7 @@ bool Obico::test(wxString& msg) const res = false; msg = format_error(body, error, status); }) - .on_complete([&, this](std::string body, unsigned) { + .on_complete([&](std::string body, unsigned) { BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got version: %2%") % name % body; }) #ifdef WIN32 diff --git a/src/slic3r/Utils/Obico.hpp b/src/slic3r/Utils/Obico.hpp index 9fd3d50f6b..f262d204bd 100644 --- a/src/slic3r/Utils/Obico.hpp +++ b/src/slic3r/Utils/Obico.hpp @@ -20,7 +20,7 @@ public: ~Obico() override = default; const char* get_name() const override; - virtual bool can_test() const { return true; }; + virtual bool can_test() const override { return true; }; bool has_auto_discovery() const override { return false; } bool is_cloud() const override { return true; } bool get_login_url(wxString& auth_url) const override; @@ -30,7 +30,7 @@ public: wxString get_test_failed_msg(wxString& msg) const override; virtual bool test(wxString& curl_msg) const override; bool get_printers(wxArrayString& printers) const override; - PrintHostPostUploadActions get_post_upload_actions() const; + PrintHostPostUploadActions get_post_upload_actions() const override; bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override; protected: diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp index a372ab5b7c..4419395b4d 100644 --- a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp +++ b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp @@ -572,7 +572,7 @@ int OrcaCloudServiceAgent::set_config_dir(std::string cfg_dir) { config_dir = cfg_dir; wxFileName fallback(wxString::FromUTF8(cfg_dir.c_str()), secret_constants::USER_SECRET_FILENAME); - fallback.Normalize(); + fallback.MakeAbsolute(); secret_fallback_path = fallback.GetFullPath().ToStdString(); return BAMBU_NETWORK_SUCCESS; } @@ -1564,7 +1564,7 @@ void OrcaCloudServiceAgent::persist_user_secret(const std::string& secret) return; } wxFileName path(wxString::FromUTF8(secret_fallback_path.c_str())); - path.Normalize(); + path.MakeAbsolute(); if (!wxFileName::DirExists(path.GetPath())) { wxFileName::Mkdir(path.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL); } @@ -2487,7 +2487,7 @@ void OrcaCloudServiceAgent::compute_fallback_path() if (wxTheApp == nullptr) return; wxFileName fallback(wxStandardPaths::Get().GetUserDataDir(), "orca_refresh_token.sec"); - fallback.Normalize(); + fallback.MakeAbsolute(); secret_fallback_path = fallback.GetFullPath().ToStdString(); } @@ -3581,7 +3581,7 @@ std::string OrcaCloudServiceAgent::token_lock_path() const if (config_dir.empty()) return {}; wxFileName lock(wxString::FromUTF8(config_dir.c_str()), "orca_refresh_token.lock"); - lock.Normalize(); + lock.MakeAbsolute(); return lock.GetFullPath().ToStdString(); } diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index 18a9db4e26..663a4ce8c3 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -513,7 +513,7 @@ void PresetUpdater::priv::sync_resources(std::string http_url, std::map<std::str cancel_http = true; } }) - .on_complete([this, &resource_list, resources](std::string body, unsigned) { + .on_complete([&resource_list, resources](std::string body, unsigned) { try { BOOST_LOG_TRIVIAL(info) << "[Orca Updater]: request_resources, body=" << body; @@ -1044,46 +1044,42 @@ void PresetUpdater::priv::check_installed_vendor_profiles() const std::set<std::string> bundles; // Orca: always install filament library bundles.insert(PresetBundle::ORCA_FILAMENT_LIBRARY); - for (auto &dir_entry : boost::filesystem::directory_iterator(rsrc_path)) { - const auto &path = dir_entry.path(); - std::string file_path = path.string(); - if (is_json_file(file_path)) { - const auto path_in_vendor = vendor_path / path.filename(); - std::string vendor_name = path.filename().string(); - // Remove the .json suffix. - vendor_name.erase(vendor_name.size() - 5); - if (bundles.find(vendor_name) != bundles.end())continue; + // A vendor is named by its profile or, where the build ships preset caches + // instead of the raw profile JSONs, by its cache alone. + for (const std::string &vendor_name : vendor_names_in(rsrc_path)) { + if (bundles.find(vendor_name) != bundles.end())continue; - const auto is_vendor_enabled = (vendor_name == PresetBundle::ORCA_DEFAULT_BUNDLE) // always update configs from resource to vendor for ORCA_DEFAULT_BUNDLE - || (enabled_vendors.find(vendor_name) != enabled_vendors.end()); - if (enabled_config_update) { - if ( fs::exists(path_in_vendor)) { - if (is_vendor_enabled) { - Semver resource_ver = get_version_from_json(file_path); - Semver vendor_ver = get_version_from_json(path_in_vendor.string()); + const auto is_vendor_enabled = (vendor_name == PresetBundle::ORCA_DEFAULT_BUNDLE) // always update configs from resource to vendor for ORCA_DEFAULT_BUNDLE + || (enabled_vendors.find(vendor_name) != enabled_vendors.end()); + if (enabled_config_update) { + if (is_vendor_installed(vendor_name)) { + if (is_vendor_enabled) { + // Orca: whichever form of the vendor resources ships at the newer + // version is the one installing lays down, and the one to judge + // what is installed against. + Semver resource_ver = resource_vendor_version(vendor_name); + // Orca: a vendor installed as a preset cache has no profile + // beside it; the version it was installed at is in the cache. + Semver vendor_ver = installed_vendor_version(vendor_name); - if (vendor_ver < resource_ver) { - BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:found vendor " << vendor_name << " newer version " - << resource_ver.to_string() << " from resource, old version " << vendor_ver.to_string(); - bundles.insert(vendor_name); - } - } - else { - //need to be removed because not installed - fs::remove(path_in_vendor); - const auto path_of_vendor = vendor_path / vendor_name; - if (fs::exists(path_of_vendor)) - fs::remove_all(path_of_vendor); + if (vendor_ver < resource_ver) { + BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:found vendor " << vendor_name << " newer version " + << resource_ver.to_string() << " from resource, old version " << vendor_ver.to_string(); + bundles.insert(vendor_name); } } - else if (is_vendor_enabled) { - bundles.insert(vendor_name); + else { + //need to be removed because not installed + remove_installed_vendor(vendor_name); } } else if (is_vendor_enabled) { bundles.insert(vendor_name); } } + else if (is_vendor_enabled) { + bundles.insert(vendor_name); + } } if (bundles.size() > 0) { @@ -1163,11 +1159,12 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version auto filament_in_cache = (cache_profile_path / vendor_name / PRESET_FILAMENT_NAME); auto machine_in_cache = (cache_profile_path / vendor_name / PRESET_PRINTER_NAME); - if (( fs::exists(path_in_vendor)) + if (is_vendor_installed(vendor_name) || fs::exists(print_in_cache) || fs::exists(filament_in_cache) || fs::exists(machine_in_cache)) { - Semver vendor_ver = get_version_from_json(path_in_vendor.string()); + // Orca: a vendor installed as a preset cache carries its version there. + Semver vendor_ver = installed_vendor_version(vendor_name); std::map<std::string, std::string> key_values; std::vector<std::string> keys(3); @@ -1201,7 +1198,7 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version version.config_version = cache_ver; version.comment = description; // Orca: update vendor.json - updates.updates.emplace_back(std::move(file_path), std::move(path_in_vendor.string()), std::move(version), vendor_name, changelog, "", force_update, false); + updates.updates.emplace_back(std::move(file_path), path_in_vendor.string(), std::move(version), vendor_name, changelog, "", force_update, false); //Orca: update vendor folder updates.updates.emplace_back(cache_profile_path / vendor_name, vendor_path / vendor_name, Version(), vendor_name, "", "", force_update, true); } else { diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp index 7f69d5e087..cc15805256 100644 --- a/src/slic3r/Utils/PrintHost.cpp +++ b/src/slic3r/Utils/PrintHost.cpp @@ -368,7 +368,7 @@ void PrintHostJobQueue::priv::perform_job(PrintHostJob the_job) emit_progress(100); if (the_job.switch_to_device_tab) { const auto mainframe = GUI::wxGetApp().mainframe; - mainframe->request_select_tab(MainFrame::TabPosition::tpMonitor); + mainframe->request_select_tab(TAB_ID_MONITOR); } } } diff --git a/src/slic3r/Utils/RaycastManager.cpp b/src/slic3r/Utils/RaycastManager.cpp index c51a19ebd9..62c7a922d7 100644 --- a/src/slic3r/Utils/RaycastManager.cpp +++ b/src/slic3r/Utils/RaycastManager.cpp @@ -107,7 +107,7 @@ std::optional<RaycastManager::Hit> RaycastManager::first_hit(const Vec3d& point, const AABBMesh *hit_mesh = nullptr; double hit_squared_distance = 0.; int hit_face = -1; - Vec3d hit_world; + Vec3d hit_world { Vec3d::Zero() }; const Transform3d *hit_tramsformation = nullptr; const TrKey *hit_key = nullptr; diff --git a/src/slic3r/Utils/Repetier.cpp b/src/slic3r/Utils/Repetier.cpp index 0d269e1cd5..a524f29b71 100644 --- a/src/slic3r/Utils/Repetier.cpp +++ b/src/slic3r/Utils/Repetier.cpp @@ -73,7 +73,7 @@ bool Repetier::test(wxString &msg) const res = false; msg = format_error(body, error, status); }) - .on_complete([&, this](std::string body, unsigned) { + .on_complete([&](std::string body, unsigned) { BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got version: %2%") % name % body; try { diff --git a/src/slic3r/Utils/SimplyPrint.cpp b/src/slic3r/Utils/SimplyPrint.cpp index c1e5235d98..a3edfd98ee 100644 --- a/src/slic3r/Utils/SimplyPrint.cpp +++ b/src/slic3r/Utils/SimplyPrint.cpp @@ -192,7 +192,7 @@ bool SimplyPrint::do_api_call(std::function<Http(bool)> bool res = true; - const auto create_request = [this, &build_request, &res, &on_complete](const std::string& access_token, bool is_retry) { + const auto create_request = [&build_request, &res, &on_complete](const std::string& access_token, bool is_retry) { auto http = build_request(is_retry); set_auth(http, access_token); http.header("User-Agent", "SimplyPrint Orca Plugin") @@ -300,7 +300,7 @@ bool SimplyPrint::do_temp_upload(const boost::filesystem::path& file_path, return http; }, - [&error_fn, &filename, this](std::string body, unsigned status) { + [&error_fn, &filename](std::string body, unsigned status) { BOOST_LOG_TRIVIAL(info) << boost::format("SimplyPrint: File uploaded: HTTP %1%: %2%") % status % body; // Get file UUID @@ -325,7 +325,7 @@ bool SimplyPrint::do_temp_upload(const boost::filesystem::path& file_path, wxLaunchDefaultBrowser(url); } else { const auto mainframe = GUI::wxGetApp().mainframe; - mainframe->request_select_tab(MainFrame::TabPosition::tpMonitor); + mainframe->request_select_tab(TAB_ID_MONITOR); mainframe->load_printer_url(url); } @@ -423,7 +423,7 @@ bool SimplyPrint::do_chunk_upload(const boost::filesystem::path& file_path, cons return http; }, - [&error_fn, i, chunk_amount, this, &chunk_id, &delete_token](std::string body, unsigned status) { + [&error_fn, i, chunk_amount, &chunk_id, &delete_token](std::string body, unsigned status) { BOOST_LOG_TRIVIAL(info) << boost::format("SimplyPrint: File chunk [%1%/%2%] uploaded: HTTP %3%: %4%") % (i + 1) % chunk_amount % status % body; if (i == 0) { // First chunk, parse chunk id diff --git a/src/slic3r/plugin/PluginAuditManager.cpp b/src/slic3r/plugin/PluginAuditManager.cpp index 29aa59b253..4d69a4f761 100644 --- a/src/slic3r/plugin/PluginAuditManager.cpp +++ b/src/slic3r/plugin/PluginAuditManager.cpp @@ -7,11 +7,127 @@ #include <boost/algorithm/string/predicate.hpp> #include <boost/log/trivial.hpp> +#include <algorithm> +#include <cctype> #include <cstdlib> +#include <future> +#include <slic3r/GUI/BindDialog.hpp> +#include <slic3r/GUI/GUI_App.hpp> +#include <slic3r/plugin/PluginFsUtils.hpp> +#include <slic3r/plugin/PluginManager.hpp> +#include <unordered_map> +#include <unordered_set> #include <utility> +#include <vector> +#include <wx/event.h> +#include <wx/msgdlg.h> namespace Slic3r { +// extensive list of audit events can be found at https://docs.python.org/3/library/audit_events.html +static const std::unordered_map<std::string, AuditEventCategory> audit_event_categories{ + // fsread + {"glob.glob", AuditEventCategory::FsRead}, + {"glob.glob/2", AuditEventCategory::FsRead}, + {"os.fwalk", AuditEventCategory::FsRead}, + {"os.getxattr", AuditEventCategory::FsRead}, + {"os.listdir", AuditEventCategory::FsRead}, + {"os.listdrives", AuditEventCategory::FsRead}, + {"os.listmounts", AuditEventCategory::FsRead}, + {"os.listvolumes", AuditEventCategory::FsRead}, + {"os.listxattr", AuditEventCategory::FsRead}, + {"os.scandir", AuditEventCategory::FsRead}, + {"os.walk", AuditEventCategory::FsRead}, + {"pathlib.Path.glob", AuditEventCategory::FsRead}, + {"pathlib.Path.rglob", AuditEventCategory::FsRead}, + + // fsreadwrite + {"os.chflags", AuditEventCategory::FsReadWrite}, + {"os.chmod", AuditEventCategory::FsReadWrite}, + {"os.chown", AuditEventCategory::FsReadWrite}, + {"os.removexattr", AuditEventCategory::FsReadWrite}, + {"os.rename", AuditEventCategory::FsReadWrite}, + {"os.setxattr", AuditEventCategory::FsReadWrite}, + {"os.truncate", AuditEventCategory::FsReadWrite}, + {"os.utime", AuditEventCategory::FsReadWrite}, + {"shutil.chown", AuditEventCategory::FsReadWrite}, + {"shutil.copymode", AuditEventCategory::FsReadWrite}, + {"shutil.copystat", AuditEventCategory::FsReadWrite}, + {"shutil.copyfile", AuditEventCategory::FsReadWrite}, + {"shutil.copytree", AuditEventCategory::FsReadWrite}, + {"shutil.make_archive", AuditEventCategory::FsReadWrite}, + {"shutil.move", AuditEventCategory::FsReadWrite}, + {"shutil.unpack_archive", AuditEventCategory::FsReadWrite}, + + // fscreate + {"os.link", AuditEventCategory::FsCreate}, + {"os.mkdir", AuditEventCategory::FsCreate}, + {"os.symlink", AuditEventCategory::FsCreate}, + {"tempfile.mkdtemp", AuditEventCategory::FsCreate}, + {"tempfile.mkstemp", AuditEventCategory::FsCreate}, + {"_winapi.CreateJunction", AuditEventCategory::FsCreate}, + + // fsdelete + {"os.remove", AuditEventCategory::FsDelete}, + {"os.rmdir", AuditEventCategory::FsDelete}, + {"shutil.rmtree", AuditEventCategory::FsDelete}, + + // http + {"http.client.connect", AuditEventCategory::Http}, + {"http.client.send", AuditEventCategory::Http}, + {"urllib.Request", AuditEventCategory::Http}, + + // socket + {"socket.__new__", AuditEventCategory::Socket}, + {"socket.bind", AuditEventCategory::Socket}, + {"socket.connect", AuditEventCategory::Socket}, + {"socket.getaddrinfo", AuditEventCategory::Socket}, + {"socket.gethostbyaddr", AuditEventCategory::Socket}, + {"socket.gethostbyname", AuditEventCategory::Socket}, + {"socket.gethostname", AuditEventCategory::Socket}, + {"socket.getnameinfo", AuditEventCategory::Socket}, + {"socket.getservbyname", AuditEventCategory::Socket}, + {"socket.getservbyport", AuditEventCategory::Socket}, + {"socket.sendmsg", AuditEventCategory::Socket}, + {"socket.sendto", AuditEventCategory::Socket}, + + // processcreate + {"os.fork", AuditEventCategory::ProcessCreate}, + {"os.forkpty", AuditEventCategory::ProcessCreate}, + {"os.posix_spawn", AuditEventCategory::ProcessCreate}, + {"os.spawn", AuditEventCategory::ProcessCreate}, + {"os.system", AuditEventCategory::ProcessCreate}, + {"os.startfile", AuditEventCategory::ProcessCreate}, + {"os.startfile/2", AuditEventCategory::ProcessCreate}, + {"pty.spawn", AuditEventCategory::ProcessCreate}, + {"subprocess.Popen", AuditEventCategory::ProcessCreate}, + {"_winapi.CreateProcess", AuditEventCategory::ProcessCreate}, + {"_posixsubprocess.fork_exec", AuditEventCategory::ProcessCreate}, +}; + +// Returns the category event_name belongs to, or AuditEventCategory::None when it isn't audited. +static AuditEventCategory event_category(const std::string& event_name) +{ + const auto it = audit_event_categories.find(event_name); + return it == audit_event_categories.end() ? AuditEventCategory::None : it->second; +} + +// True for the categories whose targets are filesystem paths, as opposed to a network +// address or a process command line -- the deny-path and allowed-root checks only make sense +// against a path. +static bool is_fs_category(AuditEventCategory category) +{ + switch (category) { + case AuditEventCategory::FsRead: + case AuditEventCategory::FsReadWrite: + case AuditEventCategory::FsCreate: + case AuditEventCategory::FsDelete: + return true; + default: + return false; + } +} + // --------------------------------------------------------------------------- // Path safety // --------------------------------------------------------------------------- @@ -78,24 +194,21 @@ bool is_inside_allowed_root(const boost::filesystem::path& candidate, const boos // ScopedPluginAuditContext // --------------------------------------------------------------------------- -thread_local std::string PluginAuditManager::m_current_plugin_key = ""; -thread_local std::string PluginAuditManager::m_current_capability_name = ""; -thread_local PluginAuditManager::AuditMode PluginAuditManager::m_audit_mode = PluginAuditManager::AuditMode::Loading; -thread_local std::vector<boost::filesystem::path> PluginAuditManager::m_scoped_allowed_roots; +thread_local std::string PluginAuditManager::m_current_plugin_key = ""; +thread_local std::string PluginAuditManager::m_current_capability_name = ""; +thread_local std::vector<AllowedRoot> PluginAuditManager::m_scoped_allowed_roots; +thread_local bool PluginAuditManager::m_audit_denial_pending = false; thread_local bool PluginAuditManager::m_has_last_violation = false; thread_local AuditViolation PluginAuditManager::m_last_violation; ScopedPluginAuditContext::ScopedPluginAuditContext(const std::string& plugin_key, - const std::string& capability_name, - PluginAuditManager::AuditMode mode) + const std::string& capability_name) : m_previous_id(PluginAuditManager::instance().current_plugin()) , m_previous_capability(PluginAuditManager::instance().current_capability()) - , m_previous_mode(PluginAuditManager::instance().audit_mode()) , m_previous_scoped_roots(PluginAuditManager::m_scoped_allowed_roots) { PluginAuditManager::instance().set_current_plugin(plugin_key); PluginAuditManager::instance().set_current_capability(capability_name); - PluginAuditManager::instance().set_audit_mode(mode); PluginAuditManager::m_scoped_allowed_roots.clear(); } @@ -103,7 +216,6 @@ ScopedPluginAuditContext::~ScopedPluginAuditContext() { PluginAuditManager::instance().set_current_plugin(m_previous_id); PluginAuditManager::instance().set_current_capability(m_previous_capability); - PluginAuditManager::instance().set_audit_mode(m_previous_mode); PluginAuditManager::m_scoped_allowed_roots = std::move(m_previous_scoped_roots); } @@ -129,23 +241,24 @@ std::string PluginAuditManager::current_capability() const { return m_current_ca void PluginAuditManager::clear_current_capability() { m_current_capability_name.clear(); } -void PluginAuditManager::add_global_allowed_root(const boost::filesystem::path& root) +void PluginAuditManager::add_global_allowed_root(const boost::filesystem::path& root, bool allow_write) { if (root.empty()) return; std::lock_guard<std::mutex> lock(m_mutex); - m_global_allowed_roots.push_back(root); - BOOST_LOG_TRIVIAL(info) << "[AUDIT] Global allowed root: " << root.string(); + m_global_allowed_roots.push_back({root, allow_write}); + BOOST_LOG_TRIVIAL(info) << "[AUDIT] Global allowed root: " << root.string() << " allow_write=" << allow_write; } -void PluginAuditManager::add_scoped_allowed_root(const boost::filesystem::path& root) +void PluginAuditManager::add_scoped_allowed_root(const boost::filesystem::path& root, bool allow_write) { if (root.empty()) return; - m_scoped_allowed_roots.push_back(root); - BOOST_LOG_TRIVIAL(info) << "[AUDIT] Scoped allowed root for plugin " << current_plugin() << ": " << root.string(); + m_scoped_allowed_roots.push_back({root, allow_write}); + BOOST_LOG_TRIVIAL(info) << "[AUDIT] Scoped allowed root for plugin " << current_plugin() << ": " << root.string() + << " allow_write=" << allow_write; } // --------------------------------------------------------------------------- @@ -199,12 +312,65 @@ bool PluginAuditManager::is_denied_filename(const boost::filesystem::path& candi } // --------------------------------------------------------------------------- -// Audit mode +// Denied path keywords // --------------------------------------------------------------------------- -void PluginAuditManager::set_audit_mode(AuditMode mode) { m_audit_mode = mode; } +void PluginAuditManager::add_denied_path_keyword(const std::string& keyword) +{ + if (keyword.empty()) + return; -PluginAuditManager::AuditMode PluginAuditManager::audit_mode() const { return m_audit_mode; } + std::string lower = keyword; + std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return std::tolower(c); }); + + std::lock_guard<std::mutex> lock(m_mutex); + m_denied_path_keywords.push_back(lower); + BOOST_LOG_TRIVIAL(info) << "[AUDIT] Denied path keyword: " << lower; +} + +std::vector<std::string> PluginAuditManager::default_denied_path_keywords() +{ + // Broad, categorical rules on top of the exact-name is_denied_filename registry: a plugin + // must never be able to reach a secret, a certificate, or a configuration file just because + // it happens to live inside an otherwise-allowed root (e.g. the bundled TLS client cert at + // resources_dir()/cert/..., which would become reachable the moment resources_dir() is + // granted as a read-only allowed root). + return {"secret", "cert", "conf"}; +} + +bool PluginAuditManager::is_denied_path_keyword(const boost::filesystem::path& candidate) const +{ + namespace fs = boost::filesystem; + + boost::system::error_code ec; + fs::path canon = fs::weakly_canonical(candidate, ec); + if (ec) { + canon = fs::absolute(candidate, ec).lexically_normal(); + if (ec) + canon = candidate; + } + + std::lock_guard<std::mutex> lock(m_mutex); + if (m_denied_path_keywords.empty()) + return false; + + for (const auto& component : canon) { + std::string name = component.string(); + if (name.empty()) + continue; + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return std::tolower(c); }); + for (const auto& keyword : m_denied_path_keywords) { + if (name.find(keyword) != std::string::npos) + return true; + } + } + return false; +} + +bool PluginAuditManager::is_denied_path(const boost::filesystem::path& candidate) const +{ + return is_denied_filename(candidate) || is_denied_path_keyword(candidate); +} // --------------------------------------------------------------------------- // Policy checks @@ -219,20 +385,20 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat if (plugin_key.empty()) return {true, ""}; // not running inside a plugin context - // Denied filenames are checked first, above both the Loading exemption below and the - // allowed roots. The app config and the cloud refresh token live directly inside - // data_dir(), which is a global allowed root, and no scope ever sets Enforcing — so a - // deny placed any lower would be unreachable for reads. + // Denied filenames/keywords are checked before the allowed roots. The app config and the + // cloud refresh token live directly inside data_dir(), which is a global allowed root, and + // the bundled TLS client cert lives inside resources_dir(), a read-only global allowed + // root, so a deny placed any lower would be unreachable. if (is_denied_filename(path)) { BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << path.string() << " is_write=" << is_write << " plugin=" << plugin_key << " reason=denied filename"; return {false, "denied filename"}; } - - // During import/loading, only block writes. Python must be able to read - // stdlib modules and the plugin file itself during import. - if (m_audit_mode == AuditMode::Loading && !is_write) - return {true, ""}; + if (is_denied_path_keyword(path)) { + BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << path.string() << " is_write=" << is_write + << " plugin=" << plugin_key << " reason=denied path keyword"; + return {false, "denied path keyword"}; + } namespace fs = boost::filesystem; fs::path candidate = path; @@ -245,8 +411,11 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat candidate = absolute_candidate; } + // A root that doesn't allow writes only matches a read-shaped request; a write/create/ + // delete-shaped one falls through to "outside allowed root" for that root even though the + // path is physically inside it. for (const auto& root : m_scoped_allowed_roots) { - if (is_inside_allowed_root(candidate, root)) { + if ((!is_write || root.allow_write) && is_inside_allowed_root(candidate, root.path)) { return {true, ""}; } } @@ -254,14 +423,13 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat { std::lock_guard<std::mutex> lock(m_mutex); for (const auto& root : m_global_allowed_roots) { - if (is_inside_allowed_root(candidate, root)) { + if ((!is_write || root.allow_write) && is_inside_allowed_root(candidate, root.path)) { return {true, ""}; } } } BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << candidate.string() << " is_write=" << is_write - << " audit_mode=" << (m_audit_mode == AuditMode::Loading ? "Loading" : "Enforcing") << " plugin=" << plugin_key; return {false, "outside allowed root"}; } @@ -269,19 +437,99 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat AuditDecision PluginAuditManager::check_open(const std::string& path_str, const std::string& mode) { const bool is_write = mode.find('w') != std::string::npos || mode.find('a') != std::string::npos || - mode.find('+') != std::string::npos; + mode.find('+') != std::string::npos || mode.find('x') != std::string::npos; return check_path_access(boost::filesystem::path(path_str), is_write); } +bool PluginAuditManager::request_filesystem_read_permissions(const std::string& plugin_key, + const std::vector<std::string>& paths) +{ + if (plugin_key.empty() || paths.empty()) + return true; + + PluginDescriptor descriptor; + if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty()) + return false; + + PluginInstallState state; + read_install_state(boost::filesystem::path(descriptor.plugin_root), state); + + std::vector<std::string> missing; + for (const std::string& path : paths) { + if (std::find(state.permissions.fs_read.begin(), state.permissions.fs_read.end(), path) == state.permissions.fs_read.end()) + missing.push_back(path); + } + if (missing.empty()) + return true; + + if (wxTheApp == nullptr || GUI::wxGetApp().is_closing()) + return false; + + auto show_dialog = [&descriptor, &missing]() { + wxString requested_paths; + for (const std::string& path : missing) + requested_paths += wxString::FromUTF8(path.c_str()) + "\n"; + + wxMessageDialog dialog( + nullptr, + wxString::Format("Plugin \"%s\" requests filesystem read access to:\n%s", + wxString::FromUTF8(descriptor.name.c_str()), requested_paths), + "Plugin permissions", + wxYES_NO | wxICON_WARNING); + return dialog.ShowModal() == wxID_YES; + }; + + bool granted = false; + if (wxIsMainThread()) { + granted = show_dialog(); + } else { + auto result = std::make_shared<std::promise<bool>>(); + auto future = result->get_future(); + GUI::wxGetApp().CallAfter([result, descriptor_name = descriptor.name, missing]() { + wxString requested_paths; + for (const std::string& path : missing) + requested_paths += wxString::FromUTF8(path.c_str()) + "\n"; + + wxMessageDialog dialog( + nullptr, + wxString::Format("Plugin \"%s\" requests filesystem read access to:\n%s", + wxString::FromUTF8(descriptor_name.c_str()), requested_paths), + "Plugin permissions", + wxYES_NO | wxICON_WARNING); + result->set_value(dialog.ShowModal() == wxID_YES); + }); + granted = future.get(); + } + + if (!granted) + return false; + + if (state.plugin_name.empty()) { + state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local"; + state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version; + state.plugin_name = descriptor.name; + state.cloud_uuid = descriptor.cloud_uuid(); + state.enabled = true; + } + + state.permissions.fs_read.insert(state.permissions.fs_read.end(), missing.begin(), missing.end()); + return write_install_state(boost::filesystem::path(descriptor.plugin_root), state); +} + void PluginAuditManager::report_violation(const AuditViolation& violation) { m_last_violation = violation; m_has_last_violation = true; + m_audit_denial_pending = true; BOOST_LOG_TRIVIAL(warning) << "[AUDIT BLOCKED] plugin=" << violation.plugin_key << " event=" << violation.event_name - << " path=" << violation.path.string() << " reason=" << violation.reason; + << " reason=" << violation.reason; } +bool PluginAuditManager::audit_denial_pending() const { return m_audit_denial_pending; } + +void PluginAuditManager::clear_audit_denial() { m_audit_denial_pending = false; } + void PluginAuditManager::clear_last_violation() { m_has_last_violation = false; @@ -297,31 +545,326 @@ bool PluginAuditManager::last_violation(AuditViolation& violation) const return true; } +bool PluginAuditManager::has_approved_ancestor(const std::string& plugin_key, + const std::vector<std::string>& call_site_ids) const +{ + if (plugin_key.empty() || call_site_ids.empty()) + return false; + + std::lock_guard<std::mutex> lock(m_mutex); + auto it = m_approved_call_sites.find(plugin_key); + if (it == m_approved_call_sites.end()) + return false; + + for (const auto& id : call_site_ids) + if (it->second.count(id)) + return true; + return false; +} + +void PluginAuditManager::record_approved_call_sites(const std::string& plugin_key, + const std::vector<std::string>& call_site_ids) +{ + if (plugin_key.empty() || call_site_ids.empty()) + return; + + std::lock_guard<std::mutex> lock(m_mutex); + auto& approved = m_approved_call_sites[plugin_key]; + approved.insert(call_site_ids.begin(), call_site_ids.end()); +} + // --------------------------------------------------------------------------- // The C-level audit hook // --------------------------------------------------------------------------- -namespace { +namespace PluginAuditDetail { + +PyObject* tuple_item(PyObject* args, Py_ssize_t index) +{ + if (!args || !PyTuple_Check(args) || index < 0 || index >= PyTuple_GET_SIZE(args)) + return nullptr; + return PyTuple_GET_ITEM(args, index); +} + +std::string python_path(PyObject* object) +{ + if (!object) + return {}; + + PyObject* path_object = PyOS_FSPath(object); + if (!path_object) { + PyErr_Clear(); + return {}; + } + + const char* path = PyUnicode_Check(path_object) ? PyUnicode_AsUTF8(path_object) : PyBytes_AsString(path_object); + std::string result = path ? path : ""; + Py_DECREF(path_object); + if (!path) + PyErr_Clear(); + return result; +} + +std::string python_unicode(PyObject* object) +{ + if (!object || !PyUnicode_Check(object)) + return {}; + const char* text = PyUnicode_AsUTF8(object); + if (!text) { + PyErr_Clear(); + return {}; + } + return text; +} + +std::string python_str(PyObject* object) +{ + if (!object) + return {}; + + PyObject* str_object = PyObject_Str(object); + if (!str_object) { + PyErr_Clear(); + return {}; + } + + const std::string result = python_unicode(str_object); + Py_DECREF(str_object); + return result; +} + +std::vector<std::string> call_site_identities(const std::string& plugin_root) +{ + std::vector<std::string> ids; + if (plugin_root.empty()) + return ids; + + PyFrameObject* frame = PyEval_GetFrame(); // borrowed reference + Py_XINCREF(frame); // normalize to an owned reference for the loop below + + while (frame) { + PyCodeObject* code = PyFrame_GetCode(frame); // new reference + const std::string filename = python_unicode(reinterpret_cast<PyObject*>(code->co_filename)); + const bool is_plugin_frame = !filename.empty() && boost::algorithm::starts_with(filename, plugin_root); + + std::string id; + if (!is_plugin_frame && !filename.empty()) { + const std::string funcname = python_unicode(reinterpret_cast<PyObject*>(code->co_name)); + id = filename + ":" + funcname + ":" + std::to_string(code->co_firstlineno); + } + Py_DECREF(code); + + PyFrameObject* back = PyFrame_GetBack(frame); // new reference, or nullptr at the top of the stack + Py_DECREF(frame); + frame = back; + + if (is_plugin_frame) + break; + if (!id.empty()) + ids.push_back(std::move(id)); + } + + Py_XDECREF(frame); // only holds a reference here if the loop exited via break + + return ids; +} + +static const std::unordered_set<std::string> two_path_fs_events{ + "os.rename", "os.link", "os.symlink", "shutil.copyfile", + "shutil.copytree", "shutil.copymode", "shutil.copystat", "shutil.move", + "shutil.unpack_archive", "_winapi.CreateJunction", +}; + +static const std::unordered_map<std::string, std::vector<Py_ssize_t>> audit_target_arg_indices{ + {"http.client.connect", {1}}, + {"urllib.Request", {0}}, + + {"socket.connect", {1}}, + {"socket.bind", {1}}, + {"socket.getaddrinfo", {0}}, + {"socket.gethostbyname", {0}}, + {"socket.gethostbyaddr", {0}}, + {"socket.getnameinfo", {0}}, + {"socket.getservbyname", {0}}, + {"socket.getservbyport", {0}}, + + {"os.system", {0}}, + {"subprocess.Popen", {1, 0}}, + {"os.posix_spawn", {1, 0}}, + {"os.spawn", {2, 1}}, + {"os.startfile", {0}}, + {"pty.spawn", {0}}, + {"_winapi.CreateProcess", {1, 0}}, + {"_posixsubprocess.fork_exec", {0}}, +}; + +AuditEventCategory open_category(PyObject* args) +{ + const std::string mode = python_unicode(tuple_item(args, 1)); + if (!mode.empty() && mode.find_first_of("wax+") == std::string::npos) + return AuditEventCategory::FsRead; + return AuditEventCategory::FsReadWrite; +} + +std::vector<std::string> audit_targets(const std::string& event_name, AuditEventCategory category, PyObject* args) +{ + std::vector<std::string> targets; + + switch (category) { + case AuditEventCategory::FsRead: + case AuditEventCategory::FsReadWrite: + case AuditEventCategory::FsCreate: + case AuditEventCategory::FsDelete: { + const Py_ssize_t path_count = two_path_fs_events.count(event_name) ? 2 : 1; + for (Py_ssize_t index = 0; index < path_count; ++index) { + const std::string path = python_path(tuple_item(args, index)); + if (!path.empty()) + targets.push_back(path); + } + return targets; + } + default: + break; + } + + const auto it = audit_target_arg_indices.find(event_name); + if (it != audit_target_arg_indices.end()) { + for (const Py_ssize_t index : it->second) { + std::string value = python_str(tuple_item(args, index)); + if (!value.empty()) { + targets.push_back(std::move(value)); + break; + } + } + } + + return targets; +} + +std::vector<std::string>* permission_list_for(AuditEventCategory category, PluginPermissions& permissions) +{ + switch (category) { + case AuditEventCategory::FsRead: return &permissions.fs_read; + case AuditEventCategory::FsReadWrite: return &permissions.fs_readwrite; + case AuditEventCategory::Http: return &permissions.network_http; + case AuditEventCategory::Socket: return &permissions.network_socket; + case AuditEventCategory::ProcessCreate: return &permissions.process; + default: return nullptr; + } +} + +bool has_permission(const std::vector<std::string>& granted, const std::string& target) +{ + return std::find(granted.begin(), granted.end(), target) != granted.end(); +} + +bool persist_permission(const std::string& plugin_key, + PluginInstallState& state, + std::vector<std::string>& permission_list, + const std::string& target) +{ + PluginDescriptor descriptor; + if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty()) + return false; + + // A sandbox plugin may not have a sidecar yet. Seed the small amount of install metadata + // needed by the writer so clicking Yes still creates the JSON file. + if (state.plugin_name.empty()) { + state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local"; + state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version; + state.plugin_name = descriptor.name; + state.cloud_uuid = descriptor.cloud_uuid(); + state.enabled = true; + } + + if (!has_permission(permission_list, target)) + permission_list.push_back(target); + return write_install_state(boost::filesystem::path(descriptor.plugin_root), state); +} -// Records a blocked event and raises PermissionError in the calling interpreter. Returns -1 -// so an event branch can `return report_denied(...)` directly. int report_denied(PluginAuditManager& mgr, const std::string& event_name, - const boost::filesystem::path& path, const AuditDecision& decision) { AuditViolation violation; violation.plugin_key = mgr.current_plugin(); violation.event_name = event_name; - violation.path = path; violation.reason = decision.reason; mgr.report_violation(violation); - PyErr_SetString(PyExc_PermissionError, "Plugin attempted to access a blocked file path"); + PyErr_SetString(PyExc_PermissionError, "Plugin attempted an audited operation without permission"); return -1; } -} // namespace +wxString audit_message(AuditEventCategory category, const wxString& plugin_name, const wxString& event_name, + const wxString& target_list) +{ + if (target_list.IsEmpty()) + return wxString::Format( + _L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\".\n\n" + "This operation does not expose a target the audit hook can display."), + plugin_name, event_name); + + switch (category) { + case AuditEventCategory::FsRead: + return wxString::Format(_L("Plugin \"%s\" is requesting to read the following file(s):\n%s"), plugin_name, target_list); + case AuditEventCategory::FsReadWrite: + return wxString::Format(_L("Plugin \"%s\" is requesting to read/write the following file(s):\n%s"), plugin_name, target_list); + case AuditEventCategory::FsCreate: + return wxString::Format(_L("Plugin \"%s\" is requesting to create the following file(s):\n%s"), plugin_name, target_list); + case AuditEventCategory::FsDelete: + return wxString::Format(_L("Plugin \"%s\" is requesting to delete the following file(s):\n%s"), plugin_name, target_list); + case AuditEventCategory::Http: + return wxString::Format(_L("Plugin \"%s\" is requesting to make an HTTP request to:\n%s"), plugin_name, target_list); + case AuditEventCategory::Socket: + return wxString::Format(_L("Plugin \"%s\" is requesting to open a network connection to:\n%s"), plugin_name, target_list); + case AuditEventCategory::ProcessCreate: + return wxString::Format(_L("Plugin \"%s\" is requesting to run the following command(s):\n%s"), plugin_name, target_list); + default: + return wxString::Format(_L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\"."), plugin_name, event_name); + } +} + +int decide_audited_event(PluginAuditManager& mgr, + PluginInstallState& state, + const std::string& plugin_key, + const std::string& plugin_name, + const std::string& event_name, + AuditEventCategory category, + const std::vector<std::string>& targets, + std::vector<std::string>* permission_list, + const std::vector<std::string>& call_site_ids) +{ + std::vector<std::string> unresolved = targets; + if (permission_list) { + unresolved.clear(); + for (const auto& target : targets) + if (!has_permission(*permission_list, target)) + unresolved.push_back(target); + if (!targets.empty() && unresolved.empty()) + return 0; + } + + wxString target_list; + for (const auto& target : unresolved) + target_list += wxString::FromUTF8(target.c_str()) + "\n"; + + wxMessageDialog dialog(nullptr, + audit_message(category, wxString::FromUTF8(plugin_name.c_str()), + wxString::FromUTF8(event_name.c_str()), target_list), + _L("Plugin permission request"), wxYES_NO | wxICON_WARNING); + if (dialog.ShowModal() != wxID_YES) + return report_denied(mgr, event_name, {false, "audit permission required"}); + + if (permission_list) + for (const auto& target : unresolved) + persist_permission(plugin_key, state, *permission_list, target); + + mgr.record_approved_call_sites(plugin_key, call_site_ids); + return 0; +} + +} // namespace PluginAuditDetail int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user_data) { @@ -331,94 +874,95 @@ int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user std::string event_name(event ? event : ""); + if (event_name.empty()) + return 0; + // Verbose logging of every audit event (can be noisy) if (mgr->verbose_events) { BOOST_LOG_TRIVIAL(debug) << "[AUDIT EVENT] " << event_name; } - // extensive list of audit events can be found at https://docs.python.org/3/library/audit_events.html - - // --- open event --- - if (event_name == "open") { - const char* path_cstr = nullptr; - const char* mode_cstr = nullptr; - int flags = 0; - - // open(path, mode, flags) — path may be str, bytes, or int fd - if (!PyArg_ParseTuple(args, "s|si", &path_cstr, &mode_cstr, &flags)) { - PyErr_Clear(); // couldn't parse; allow - return 0; - } - - std::string path_str(path_cstr ? path_cstr : ""); - std::string mode_str(mode_cstr ? mode_cstr : "r"); - - AuditDecision decision = mgr->check_open(path_str, mode_str); - if (!decision.allowed) - return report_denied(*mgr, event_name, path_str, decision); + if (mgr->current_plugin().empty()) { + BOOST_LOG_TRIVIAL(trace) << "[AUDIT] event=" << event_name << " bypassed (no plugin context)"; return 0; } - // --- os.rename event (raised by os.rename and os.replace) --- - if (event_name == "os.rename") { - const char* src_cstr = nullptr; - const char* dst_cstr = nullptr; - PyObject* src_dir_fd = nullptr; - PyObject* dst_dir_fd = nullptr; - - // os.rename(src, dst, src_dir_fd, dst_dir_fd) — paths may be str, bytes, or int fd. - // The dir_fd arguments are unused, but must be accepted for the tuple to parse. - if (!PyArg_ParseTuple(args, "ss|OO", &src_cstr, &dst_cstr, &src_dir_fd, &dst_dir_fd)) { - PyErr_Clear(); // couldn't parse; allow - return 0; - } - - // A rename writes at both ends, so either end being denied blocks the call. - for (const char* path_cstr : {src_cstr, dst_cstr}) { - std::string path_str(path_cstr ? path_cstr : ""); - AuditDecision decision = mgr->check_path_access(path_str, /* is_write */ true); - if (!decision.allowed) - return report_denied(*mgr, event_name, path_str, decision); - } + // the open function can take in different flags that determine if it is a read or readwrite. + const AuditEventCategory event_type = + event_name == "open" ? PluginAuditDetail::open_category(args) : event_category(event_name); + if (event_type == AuditEventCategory::None) return 0; + + const bool fs_category = is_fs_category(event_type); + const std::vector<std::string> targets = PluginAuditDetail::audit_targets(event_name, event_type, args); + + // A denied path (secrets, certificates, config files -- see is_denied_path) is an + // unconditional block: checked before the ancestor-cascade check below, so a cascade + // approval recorded for an unrelated action can never launder access to one, and before + // the allowed-root shortcut further down, so an allowed root (e.g. the read-only resources + // folder) cannot make a denied path underneath it reachable. + if (fs_category) { + for (const auto& target : targets) { + if (mgr->is_denied_path(boost::filesystem::path(target))) + return PluginAuditDetail::report_denied(*mgr, event_name, {false, "denied path"}); + } } - // --- os.remove event (raised by os.remove and os.unlink) --- - if (event_name == "os.remove") { - const char* path_cstr = nullptr; - PyObject* dir_fd = nullptr; + PluginDescriptor plugin_descriptor; + PluginManager::instance().try_get_plugin_descriptor(mgr->current_plugin(), plugin_descriptor); - // os.remove(path, dir_fd) — path may be str, bytes, or int fd - if (!PyArg_ParseTuple(args, "s|O", &path_cstr, &dir_fd)) { - PyErr_Clear(); // couldn't parse; allow - return 0; - } - - std::string path_str(path_cstr ? path_cstr : ""); - AuditDecision decision = mgr->check_path_access(path_str, /* is_write */ true); - if (!decision.allowed) - return report_denied(*mgr, event_name, path_str, decision); + // Some plugins call other audited events, e.g. urlib.request will call socket.connect. So this is so that if urlib.request + // was already approved, socket.connect won't trigger another permission dialog. + const std::vector<std::string> call_site_ids = + PluginAuditDetail::call_site_identities(plugin_descriptor.plugin_root); + if (mgr->has_approved_ancestor(mgr->current_plugin(), call_site_ids)) return 0; + + // A filesystem target that resolves entirely inside a pre-determined allowed root -- the + // plugin system's own data_dir() tree (which holds each plugin's storage folder and the + // installed/system profile cache), the read-only bundled resources folder, or a per-call + // scoped root such as the current G-code folder -- is part of the plugin system's normal + // workflow and does not need a prompt. + if (fs_category && !targets.empty()) { + const bool is_write = event_type != AuditEventCategory::FsRead; + const bool all_inside_allowed_root = + std::all_of(targets.begin(), targets.end(), [&](const std::string& target) { + return mgr->check_path_access(boost::filesystem::path(target), is_write).allowed; + }); + if (all_inside_allowed_root) + return 0; } - // Unknown event — allow by default - return 0; + PluginInstallState state; + const bool have_install_state = PluginManager::instance().get_install_state(mgr->current_plugin(), state); + if (!have_install_state) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Failed to get install state for " << mgr->current_plugin(); + } + + const std::string plugin_name = state.plugin_name.empty() ? mgr->current_plugin() : state.plugin_name; + + std::vector<std::string>* permission_list = PluginAuditDetail::permission_list_for(event_type, state.permissions); + + return PluginAuditDetail::decide_audited_event(*mgr, state, mgr->current_plugin(), plugin_name, event_name, + event_type, targets, permission_list, call_site_ids); } void PluginAuditManager::install_hook() { - if (PySys_AddAuditHook(audit_hook, this) < 0) { - BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to install CPython audit hook"; - return; - } - BOOST_LOG_TRIVIAL(info) << "[AUDIT] CPython audit hook installed successfully"; - - // data_dir() is the only globally-allowed root during enforced plugin execution. - // The executable directory and resources directory are intentionally NOT allowed - // here: plugins must not write outside data_dir() (G-code plugins additionally get - // the temp G-code folder via a scoped root). Reads remain permissive in Loading mode. + // data_dir() is the primary globally-allowed root during plugin execution: read+write. It + // covers the plugin system's own workflow needs -- each plugin's storage folder + // (data_dir()/orca_plugins) and the installed/system profile cache (data_dir()/system) -- + // without a separate, narrower grant for either (G-code plugins additionally get the temp + // G-code folder via a scoped root, see SlicingPipelinePluginCapabilityTrampoline). add_global_allowed_root(data_dir()); + // resources_dir() holds the app's bundled, shared assets (installed system profiles, the + // bundled TLS client cert, web assets). Plugins may read from it -- e.g. inspecting bundled + // profiles -- but must never write into the shared, potentially multi-user app install, so + // it is granted read-only. The bundled cert itself stays unreachable regardless, via the + // "cert" denied-path keyword seeded below. + add_global_allowed_root(resources_dir(), /*allow_write=*/false); + // The user's app config and cloud credentials live directly inside data_dir(), so the // root just granted would otherwise expose them to any plugin. Deny them by name. // @@ -430,6 +974,19 @@ void PluginAuditManager::install_hook() // (see its comment for why all four config names are denied); the tests seed from it too. for (const auto& name : default_denied_filenames()) add_denied_filename(name); + + // Categorical denies on top of the exact-name list above: no path a plugin can reach may + // contain a "secret", "cert"(ificate), or "conf"(ig) path component, regardless of which + // allowed root it happens to sit inside. default_denied_path_keywords() is the single + // source of this list; the tests seed from it too. + for (const auto& keyword : default_denied_path_keywords()) + add_denied_path_keyword(keyword); + + if (PySys_AddAuditHook(audit_hook, this) < 0) { + BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to install CPython audit hook"; + return; + } + BOOST_LOG_TRIVIAL(info) << "[AUDIT] CPython audit hook installed successfully"; } } // namespace Slic3r diff --git a/src/slic3r/plugin/PluginAuditManager.hpp b/src/slic3r/plugin/PluginAuditManager.hpp index 1b24357e47..153f9d6107 100644 --- a/src/slic3r/plugin/PluginAuditManager.hpp +++ b/src/slic3r/plugin/PluginAuditManager.hpp @@ -1,9 +1,13 @@ #ifndef slic3r_PluginAuditManager_hpp_ #define slic3r_PluginAuditManager_hpp_ -#include <Python.h> +// Via pybind11 so this file requests the same python3xx.lib as everything else. +#include <pybind11/conduit/wrap_include_python_h.h> +#include <memory> #include <mutex> #include <string> +#include <unordered_map> +#include <unordered_set> #include <vector> #include <boost/filesystem.hpp> @@ -18,10 +22,31 @@ struct AuditDecision { struct AuditViolation { std::string plugin_key; std::string event_name; - boost::filesystem::path path; std::string reason; }; +// A filesystem root a plugin may access while an audit context is active. allow_write is +// false for a root that only grants reads (e.g. the bundled, shared resources folder) -- +// a write-shaped event never matches such a root, even though a read-shaped one does. +struct AllowedRoot { + boost::filesystem::path path; + bool allow_write = true; +}; + +// The set of CPython audit events PluginAuditManager recognizes, grouped by the kind of +// operation they represent. None means the event isn't one audit_hook() acts on at all. +enum class AuditEventCategory { + None, + FsRead, + FsReadWrite, + FsCreate, + FsDelete, + Http, + Socket, + ProcessCreate, + Threading, +}; + // Returns true if candidate resolves to a path inside allowed_root. // Uses weakly_canonical and component-wise comparison to reject traversal attacks. bool is_inside_allowed_root(const boost::filesystem::path& candidate, @@ -49,12 +74,15 @@ public: void clear_current_capability(); // --- allowed-roots registry --- - void add_global_allowed_root(const boost::filesystem::path& root); - void add_scoped_allowed_root(const boost::filesystem::path& root); + // allow_write = false registers a read-only root: a read-shaped event inside it is allowed, + // but a write/create/delete-shaped event is not, so it falls through to the normal + // prompt-or-deny path instead. + void add_global_allowed_root(const boost::filesystem::path& root, bool allow_write = true); + void add_scoped_allowed_root(const boost::filesystem::path& root, bool allow_write = true); // --- denied-filenames registry --- - // Filenames a plugin may never touch, in any directory, regardless of audit mode or - // enclosing allowed root. A candidate is denied when its filename starts with a + // Filenames a plugin may never touch, in any directory, regardless of the enclosing allowed + // root. A candidate is denied when its filename starts with a // registered name, so .bak/.tmp companions are covered by the same entry. // // The comparison is case-insensitive on every platform, unlike the _WIN32-only iequals @@ -73,33 +101,60 @@ public: // 8.3 short name is out of scope (see the design doc). This blocks direct access only. bool is_denied_filename(const boost::filesystem::path& candidate) const; - // --- enforcement mode --- - enum class AuditMode { - // Import/loading phase: allow reads anywhere, only block writes - // outside allowed roots. Python needs to read stdlib modules - // during import and those are not inside plugin directories. - Loading, + // --- denied-path-keyword registry --- + // Keywords that categorically deny a path if ANY of its components (directory or file + // name), not just the base name, contains one case-insensitively -- e.g. a "secrets" + // subfolder, a "certificates" folder, or a "conf"/"config" file anywhere the plugin can + // otherwise reach, including inside an allowed root. This is intentionally broader and + // fuzzier than the exact-name is_denied_filename registry: it exists to categorically rule + // out whole classes of sensitive paths (secrets, certificates, config) rather than name + // specific known files, at the cost of over-blocking an unrelated name that happens to + // contain the keyword -- the fail-safe direction, same rationale as is_denied_filename. + void add_denied_path_keyword(const std::string& keyword); - // Execution phase: block both reads and writes outside allowed - // roots, plus subprocess/socket/ctypes. - Enforcing, - }; + // The list install_hook() seeds into the keyword registry. Exposed so tests seed the exact + // same set without a live interpreter. + static std::vector<std::string> default_denied_path_keywords(); - void set_audit_mode(AuditMode mode); - AuditMode audit_mode() const; + // True when any component of candidate's (canonicalized) path contains a registered + // keyword, case-insensitively. + bool is_denied_path_keyword(const boost::filesystem::path& candidate) const; + + // is_denied_filename(candidate) || is_denied_path_keyword(candidate). Convenience for + // call sites that only need to know whether a path is categorically off-limits, not which + // specific rule fired. + bool is_denied_path(const boost::filesystem::path& candidate) const; // --- policy checks --- - // Shared core for every audited filesystem event. The deny list is consulted above the - // Loading-mode read exemption and above the allowed roots, so a denied filename is - // blocked even though every scope currently runs in Loading and the files in question - // sit inside data_dir(), which is itself a global allowed root. + // Shared core for every audited filesystem event. The deny checks are consulted above the + // allowed roots, so a denied path is blocked even when it sits inside an allowed root (e.g. + // data_dir(), which is a global allowed root). AuditDecision check_path_access(const boost::filesystem::path& candidate, bool is_write); AuditDecision check_open(const std::string& path, const std::string& mode); + // Ask the user to grant the requested filesystem-read paths. The request may originate on a + // plugin load worker, so the implementation marshals the modal dialog to the wx main thread. + // Returns true only when every missing path was granted and persisted; denial aborts the plugin + // load without adding a permission. + bool request_filesystem_read_permissions(const std::string& plugin_key, + const std::vector<std::string>& paths); + void report_violation(const AuditViolation& violation); + bool audit_denial_pending() const; + void clear_audit_denial(); void clear_last_violation(); bool last_violation(AuditViolation& violation) const; + // --- call-site cascade cache --- + // A single plugin action often fires several nested CPython audit events as it passes + // through stdlib layers (urllib.request calling http.client calling socket, for example). + // Once the user approves one event, every stdlib frame still on the stack for that call + // is recorded here by (filename, function, first line) identity. A later event whose own + // ancestor chain still contains one of those frames is the same logical action seen from + // a deeper layer, so it is auto-approved instead of prompting again. + bool has_approved_ancestor(const std::string& plugin_key, const std::vector<std::string>& call_site_ids) const; + void record_approved_call_sites(const std::string& plugin_key, const std::vector<std::string>& call_site_ids); + bool verbose_events = true; private: @@ -109,17 +164,19 @@ private: static int audit_hook(const char* event, PyObject* args, void* user_data); - static thread_local std::string m_current_plugin_key; - static thread_local std::string m_current_capability_name; - static thread_local AuditMode m_audit_mode; - static thread_local std::vector<boost::filesystem::path> m_scoped_allowed_roots; - static thread_local bool m_has_last_violation; - static thread_local AuditViolation m_last_violation; + static thread_local std::string m_current_plugin_key; + static thread_local std::string m_current_capability_name; + static thread_local std::vector<AllowedRoot> m_scoped_allowed_roots; + static thread_local bool m_audit_denial_pending; + static thread_local bool m_has_last_violation; + static thread_local AuditViolation m_last_violation; - // mutable: is_denied_filename() is a const query that must lock. + // mutable: is_denied_filename() and has_approved_ancestor() are const queries that must lock. mutable std::mutex m_mutex; - std::vector<boost::filesystem::path> m_global_allowed_roots; + std::vector<AllowedRoot> m_global_allowed_roots; std::vector<std::string> m_denied_filenames; + std::vector<std::string> m_denied_path_keywords; + std::unordered_map<std::string, std::unordered_set<std::string>> m_approved_call_sites; // plugin_key -> call-site ids }; // RAII guard that sets the current plugin key and capability name, restoring the previous @@ -130,8 +187,7 @@ class ScopedPluginAuditContext public: explicit ScopedPluginAuditContext( const std::string& plugin_key, - const std::string& capability_name = {}, - PluginAuditManager::AuditMode mode = PluginAuditManager::AuditMode::Loading); + const std::string& capability_name = {}); ~ScopedPluginAuditContext(); @@ -141,8 +197,7 @@ public: private: std::string m_previous_id; std::string m_previous_capability; - PluginAuditManager::AuditMode m_previous_mode; - std::vector<boost::filesystem::path> m_previous_scoped_roots; + std::vector<AllowedRoot> m_previous_scoped_roots; }; } // namespace Slic3r diff --git a/src/slic3r/plugin/PluginFsUtils.cpp b/src/slic3r/plugin/PluginFsUtils.cpp index 8f93f7aaef..4d6aabe3d3 100644 --- a/src/slic3r/plugin/PluginFsUtils.cpp +++ b/src/slic3r/plugin/PluginFsUtils.cpp @@ -631,7 +631,7 @@ void parse_metadata_rfc822(const std::string& content, bool is_ignored_plugin_directory(const boost::filesystem::path& path) { const std::string name = path.filename().string(); - return name.empty() || name[0] == '.' || name.rfind("__", 0) == 0 || name == PLUGIN_SUBSCRIBED_DIR; + return name.empty() || name[0] == '.' || name.rfind("__", 0) == 0 || name == PLUGIN_SUBSCRIBED_DIR || name == PLUGIN_DATA_DIR; } bool is_safe_relative_path(const boost::filesystem::path& path) @@ -784,6 +784,23 @@ bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstall parsed.plugin_name = state["plugin_name"].get<std::string>(); if (state.contains("cloud_uuid") && state["cloud_uuid"].is_string()) parsed.cloud_uuid = state["cloud_uuid"].get<std::string>(); + + if (state.contains("permissions") && state["permissions"].is_object()) { + const auto& permissions = state["permissions"]; + auto read_string_list = [&permissions](const char* key, std::vector<std::string>& out) { + if (!permissions.contains(key) || !permissions[key].is_array()) + return; + for (const auto& entry : permissions[key]) + if (entry.is_string()) + out.push_back(entry.get<std::string>()); + }; + read_string_list("fs_read", parsed.permissions.fs_read); + read_string_list("fs_readwrite", parsed.permissions.fs_readwrite); + read_string_list("network_http", parsed.permissions.network_http); + read_string_list("network_socket", parsed.permissions.network_socket); + read_string_list("process", parsed.permissions.process); + } + if (state.contains("enabled") && state["enabled"].is_boolean()) parsed.enabled = state["enabled"].get<bool>(); @@ -819,6 +836,14 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin if (!state.cloud_uuid.empty()) json["cloud_uuid"] = state.cloud_uuid; + json["permissions"] = { + {"fs_read", state.permissions.fs_read}, + {"fs_readwrite", state.permissions.fs_readwrite}, + {"network_http", state.permissions.network_http}, + {"network_socket", state.permissions.network_socket}, + {"process", state.permissions.process}, + }; + nlohmann::json capabilities = nlohmann::json::array(); for (const auto& [name, enabled] : state.capabilities) capabilities.push_back(nlohmann::json{{name, enabled}}); @@ -836,6 +861,9 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin const std::vector<std::pair<std::string, bool>>& capabilities) { PluginInstallState state; + // Loading a plugin updates its lifecycle/capability state, but must retain permissions granted + // during register_capabilities() or by a previous runtime audit prompt. + read_install_state(plugin_dir, state); state.installed_from = entry.is_cloud_plugin() ? "cloud" : "local"; // Prefer the descriptor's recorded installed_version (the version fetched from the cloud // at install time, preserved across sidecar re-writes) so a stale manifest/PEP723 header @@ -852,10 +880,16 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginDescriptor& entry) { // Install-time writer: the package is not loaded, so its capabilities are not known yet and the - // sidecar is (re)initialized to "auto-load, nothing disabled". PluginManager writes the real - // per-capability flags once the package is loaded, via the (dir, entry, enabled, capabilities) - // overload. - return write_install_state(plugin_dir, entry, true, {}); + // sidecar is (re)initialized to "auto-load, nothing disabled". This intentionally resets + // permissions on a fresh install/reinstall. PluginManager writes the real per-capability flags + // while preserving permissions once the package is loaded, via the overload above. + PluginInstallState state; + state.installed_from = entry.is_cloud_plugin() ? "cloud" : "local"; + state.installed_version = !entry.installed_version.empty() ? entry.installed_version : entry.version; + state.plugin_name = entry.name; + state.cloud_uuid = entry.cloud_uuid(); + state.enabled = true; + return write_install_state(plugin_dir, state); } bool read_python_plugin_metadata(const boost::filesystem::path& py_path, PluginDescriptor& descriptor, std::string& error) diff --git a/src/slic3r/plugin/PluginFsUtils.hpp b/src/slic3r/plugin/PluginFsUtils.hpp index 7922946b1c..5b62e9f7f1 100644 --- a/src/slic3r/plugin/PluginFsUtils.hpp +++ b/src/slic3r/plugin/PluginFsUtils.hpp @@ -12,6 +12,7 @@ #include <vector> #define PLUGIN_SUBSCRIBED_DIR "_subscribed" +#define PLUGIN_DATA_DIR "plugin_data" namespace Slic3r { @@ -81,11 +82,23 @@ inline nlohmann::json py_to_json(const pybind11::handle& o) return py::str(o).cast<std::string>(); // fallback: str() } +struct PluginPermissions +{ + std::vector<std::string> fs_read; + std::vector<std::string> fs_readwrite; + std::vector<std::string> network_http; + std::vector<std::string> network_socket; + std::vector<std::string> process; +}; + struct PluginInstallState { std::string installed_from; // "local" | "cloud" std::string installed_version; std::string plugin_name; std::string cloud_uuid; // empty for local + + PluginPermissions permissions; + bool enabled = true; std::vector<std::pair<std::string, bool>> capabilities; // name -> enabled, ordered }; diff --git a/src/slic3r/plugin/PluginLoader.cpp b/src/slic3r/plugin/PluginLoader.cpp index 92fe92068f..0c45564f1b 100644 --- a/src/slic3r/plugin/PluginLoader.cpp +++ b/src/slic3r/plugin/PluginLoader.cpp @@ -280,7 +280,7 @@ bool load(const PluginDescriptor& descriptor, // (while the active plugin key is set), then instantiates each registered capability and caches // its get_name(). Returns one entry per capability. std::string bridge_error; - auto capabilities_found = bridge.finalize_plugin_capture(descriptor.entry_path, bridge_error); + auto capabilities_found = bridge.finalize_plugin_capture(descriptor.entry_path, descriptor.plugin_key, bridge_error); if (!bridge_error.empty()) { capabilities_found.clear(); error = "Plugin registration failed: " + bridge_error; diff --git a/src/slic3r/plugin/PluginManager.cpp b/src/slic3r/plugin/PluginManager.cpp index 3587c0a824..f9a6dc60ac 100644 --- a/src/slic3r/plugin/PluginManager.cpp +++ b/src/slic3r/plugin/PluginManager.cpp @@ -21,6 +21,7 @@ #include <chrono> #include <mutex> #include <slic3r/plugin/PluginConfig.hpp> +#include <slic3r/plugin/PluginDescriptor.hpp> #include <slic3r/plugin/PluginLoader.hpp> #include <slic3r/plugin/PythonPluginInterface.hpp> #include <slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp> @@ -522,6 +523,38 @@ bool PluginManager::try_get_plugin_descriptor_for_capability(const std::string& return false; } +std::string PluginManager::get_storage_dir(const std::string& plugin_key) const +{ + namespace fs = boost::filesystem; + + PluginDescriptor descriptor; + if (!try_get_plugin_descriptor(plugin_key, descriptor)) + throw std::runtime_error("The current plugin is not registered"); + + const fs::path base_storage_dir = fs::path(get_orca_plugins_dir()) / PLUGIN_DATA_DIR; + + if (!descriptor.is_cloud_plugin()) { + const fs::path local_storage_dir = base_storage_dir / plugin_key; + fs::create_directories(local_storage_dir); + return local_storage_dir.string(); + } + + auto agent = m_cloud_service.get_cloud_agent(); + if (!agent) + throw std::runtime_error("Cloud plugin storage is unavailable before networking is initialized"); + + const std::string user_id = agent->get_user_id(); + if (user_id.empty()) + throw std::runtime_error("Cloud plugin storage is unavailable without a logged-in user"); + + if (!is_valid_plugin_id(plugin_key)) + throw std::runtime_error("The current cloud plugin key is not a valid folder name"); + + const fs::path cloud_storage_dir = base_storage_dir / PLUGIN_SUBSCRIBED_DIR / user_id / plugin_key; + fs::create_directories(cloud_storage_dir); + return cloud_storage_dir.string(); +} + // ── Capability instances ──────────────────────────────────────────────────────────────────── std::vector<std::shared_ptr<PluginCapabilityInterface>> PluginManager::get_plugin_capabilities(const std::string& plugin_key, @@ -590,6 +623,20 @@ std::shared_ptr<PluginCapabilityInterface> PluginManager::get_plugin_capability( return nullptr; } +bool PluginManager::get_install_state(const std::string& plugin_key, PluginInstallState& install_state) +{ + PluginDescriptor descriptor; + if (!try_get_plugin_descriptor(plugin_key, descriptor)) { + return false; + } + + if (!read_install_state(boost::filesystem::path(descriptor.plugin_root), install_state)) { + return false; + } + + return true; +} + // ── Lifecycle ─────────────────────────────────────────────────────────────────────────────── bool PluginManager::is_plugin_loaded(const std::string& plugin_key) const @@ -878,6 +925,8 @@ void PluginManager::load_plugin_impl(const std::string& plugin_key, bool skip_de if (!plugin_loader::load(descriptor, skip_deps, capabilities_to_enable, registry_precheck, plugin, error)) { if (error == LOAD_CANCELLED) return; // cancelled: nothing materialized survives, and no error is recorded + if (error.rfind("Plugin registration failed:", 0) == 0) + mark_plugin_install_state_disabled(plugin_key); fail(std::move(error)); return; } @@ -1103,6 +1152,51 @@ void PluginManager::write_loaded_plugin_install_state(const std::string& plugin_ write_install_state(boost::filesystem::path(descriptor.plugin_root), descriptor, /*enabled=*/true, capabilities); } +void PluginManager::mark_plugin_install_state_disabled(const std::string& plugin_key) +{ + std::lock_guard<std::mutex> state_lock(m_install_state_mutex); + + PluginDescriptor descriptor; + if (!try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty()) + return; + + const boost::filesystem::path root(descriptor.plugin_root); + PluginInstallState state; + if (!read_install_state(root, state)) { + state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local"; + state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version; + state.plugin_name = descriptor.name; + state.cloud_uuid = descriptor.cloud_uuid(); + } + state.enabled = false; + if (!write_install_state(root, state)) + return; + + std::lock_guard<std::mutex> lock(m_mutex); + if (Plugin* plugin = find_plugin_locked(plugin_key)) + plugin->descriptor.enabled = false; +} + +void PluginManager::revoke_plugin_permissions(const std::string& plugin_key) +{ + std::lock_guard<std::mutex> state_lock(m_install_state_mutex); + + PluginDescriptor descriptor; + if (!try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty()) + return; + + const boost::filesystem::path root(descriptor.plugin_root); + PluginInstallState state; + if (!read_install_state(root, state)) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to read install state for " << plugin_key; + return; + } + + state.permissions = {}; + if (!write_install_state(root, state)) + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to revoke permissions for " << plugin_key; +} + // ── Callbacks ─────────────────────────────────────────────────────────────────────────────── void PluginManager::subscribe_on_load_callback(PluginLifecycleCompleteFn fn) @@ -1322,6 +1416,11 @@ bool PluginManager::install_plugin(const boost::filesystem::path& filepath, Plug return false; } + // Every successful install may have replaced executable plugin code. Revoke any permissions + // associated with the previous package so the newly installed version must request them again. + if (!plugin_descriptor.plugin_key.empty()) + revoke_plugin_permissions(plugin_descriptor.plugin_key); + if (!plugin_descriptor.plugin_key.empty()) clear_plugin_error(plugin_descriptor.plugin_key); @@ -1709,6 +1808,7 @@ bool PluginManager::update_cloud_plugin(const std::string& plugin_key, std::stri } clear_plugin_error(plugin_key); + return true; } diff --git a/src/slic3r/plugin/PluginManager.hpp b/src/slic3r/plugin/PluginManager.hpp index ddb0bf9dce..e1fedaff94 100644 --- a/src/slic3r/plugin/PluginManager.hpp +++ b/src/slic3r/plugin/PluginManager.hpp @@ -143,6 +143,10 @@ public: bool try_get_plugin_descriptor_for_capability(const std::string& capability_name, PluginCapabilityType type, PluginDescriptor& out) const; + // Per-plugin storage directory under orca_plugins/plugin_data, created if missing. Throws + // std::runtime_error if the plugin is unregistered, the key is invalid, or (cloud plugins) + // no user is logged in yet. + std::string get_storage_dir(const std::string& plugin_key) const; std::vector<std::shared_ptr<PluginCapabilityInterface>> get_plugin_capabilities( const std::string& plugin_key = "", // "" => all plugins @@ -155,6 +159,8 @@ public: PluginCapabilityType type = PluginCapabilityType::Unknown, bool only_enabled = true) const; + bool get_install_state(const std::string& plugin_key, PluginInstallState& install_state); + void load_plugin(const std::string& plugin_key, bool skip_deps = false, std::vector<std::string> capabilities_to_enable = {}); bool unload_plugin(const std::string& plugin_key); void unload_all_plugins(); @@ -251,6 +257,9 @@ private: // Writes the sidecar for a loaded plugin (enabled=true plus the current per-capability flags). void write_loaded_plugin_install_state(const std::string& plugin_key); + void mark_plugin_install_state_disabled(const std::string& plugin_key); + // Revoke permissions after a package replacement so the new package must request them again. + void revoke_plugin_permissions(const std::string& plugin_key); bool finalize_cloud_plugin_removal(const PluginDescriptor& plugin, bool keep_local, std::string& error); bool delete_installed_plugin_package(const PluginDescriptor& plugin, std::string& error); diff --git a/src/slic3r/plugin/PyPluginTrampoline.hpp b/src/slic3r/plugin/PyPluginTrampoline.hpp index e36c0b375c..ad7fe25f10 100644 --- a/src/slic3r/plugin/PyPluginTrampoline.hpp +++ b/src/slic3r/plugin/PyPluginTrampoline.hpp @@ -26,25 +26,30 @@ try { \ override_call; \ } catch (pybind11::error_already_set & err) { \ + const bool _orca_audit_denial = ::Slic3r::PluginAuditManager::instance().audit_denial_pending(); \ + if (_orca_audit_denial) \ + ::Slic3r::PluginAuditManager::instance().clear_current_plugin(); \ ::Slic3r::log_python_exception_keep(err); \ + if (_orca_audit_denial) \ + ::Slic3r::PluginAuditManager::instance().clear_audit_denial(); \ throw; \ } // Opens the plugin's filesystem audit scope for the duration of a C++ -> Python call, and publishes // the calling capability's cached name so host APIs invoked from Python can tell which capability // they are serving. No-op without an audit plugin key. Declares a local `_orca_audit_scope`. -#define ORCA_PY_AUDIT_SCOPE(mode) \ +#define ORCA_PY_AUDIT_SCOPE() \ std::optional<::Slic3r::ScopedPluginAuditContext> _orca_audit_scope; \ if (const std::string& _orca_audit_key = this->audit_plugin_key(); !_orca_audit_key.empty()) \ - _orca_audit_scope.emplace(_orca_audit_key, this->name(), mode) + _orca_audit_scope.emplace(_orca_audit_key, this->name()) -#define ORCA_PY_OVERRIDE_AUDITED(mode, audit_setup, override_macro, ret, base, name, ...) \ +#define ORCA_PY_OVERRIDE_AUDITED(audit_setup, override_macro, ret, base, name, ...) \ do { \ ::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this); \ ::Slic3r::PythonGILState _orca_python_gil; \ if (!_orca_python_gil) \ throw std::runtime_error("Python interpreter is shutting down"); \ - ORCA_PY_AUDIT_SCOPE(mode); \ + ORCA_PY_AUDIT_SCOPE(); \ if (_orca_audit_scope) \ audit_setup(); \ ORCA_PY_LOGGED_OVERRIDE_BODY(override_macro(ret, base, name, ##__VA_ARGS__)); \ @@ -58,7 +63,7 @@ public: std::string get_name() const override { - ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, std::string, Base, get_name); + ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE_PURE, std::string, Base, get_name); } // Config UI hooks. Available on every capability type, so they live here rather than in @@ -66,7 +71,6 @@ public: bool has_config_ui() const override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, bool, @@ -77,7 +81,6 @@ public: std::string get_config_ui() const override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, std::string, @@ -94,7 +97,7 @@ public: // mistake), both fall back to the base's empty object rather than writing `"cap_config": null`. nlohmann::json get_default_config() const override { - ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading); + ORCA_PY_AUDIT_SCOPE(); try { pybind11::gil_scoped_acquire gil; pybind11::function override = pybind11::get_override(static_cast<const Base*>(this), "get_default_config"); @@ -117,17 +120,17 @@ public: void on_load() override { - ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_load); + ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_load); } void on_unload() override { - ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_unload); + ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_unload); } void on_cancelled() override { - ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled); + ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled); } }; @@ -139,7 +142,7 @@ public: PluginCapabilityType get_type() const override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, PluginCapabilityType, PluginCapabilityInterface, + [] {}, PYBIND11_OVERRIDE, PluginCapabilityType, PluginCapabilityInterface, get_type); } }; diff --git a/src/slic3r/plugin/PythonInterpreter.cpp b/src/slic3r/plugin/PythonInterpreter.cpp index e46ee58f80..3d180c2f2a 100644 --- a/src/slic3r/plugin/PythonInterpreter.cpp +++ b/src/slic3r/plugin/PythonInterpreter.cpp @@ -87,6 +87,12 @@ void log_python_exception_keep(pybind11::error_already_set& err) if (!gil) return; + // Traceback output is host-owned work. In particular, the stderr tee opens the Python log + // file on every write. Keep that open outside the plugin audit context, otherwise an ordinary + // exception raised by a plugin can recursively trigger the filesystem permission dialog while + // its original exception is being reported. + ScopedPluginAuditContext audit_suppression(""); + // Non-destructive: print the traceback to sys.stderr (tee'd to the session log) // WITHOUT consuming err, so the caller can rethrow it intact. For example, downstream C++ // catchers can still read err.what() for the user-facing dialog. We must NOT use @@ -622,10 +628,6 @@ bool PythonInterpreter::initialize() else BOOST_LOG_TRIVIAL(info) << "Bundled uv executable not found"; - // Install the CPython audit hook for plugin policy enforcement. - // This is defense-in-depth: today it only inspects the `open` audit event - // and blocks writes outside the allowed roots; subprocess/socket/ctypes and - // other events are not yet handled. It is NOT a full security sandbox. PluginAuditManager::instance().install_hook(); // Persist Python stderr (plugin tracebacks, including uncaught diff --git a/src/slic3r/plugin/PythonInterpreter.hpp b/src/slic3r/plugin/PythonInterpreter.hpp index ff69ce6846..e15aa91598 100644 --- a/src/slic3r/plugin/PythonInterpreter.hpp +++ b/src/slic3r/plugin/PythonInterpreter.hpp @@ -1,7 +1,8 @@ #ifndef slic3r_PythonInterpreter_hpp_ #define slic3r_PythonInterpreter_hpp_ -#include <Python.h> +// Via pybind11 so this file requests the same python3xx.lib as everything else. +#include <pybind11/conduit/wrap_include_python_h.h> #include <pytypedefs.h> #include <atomic> #include <functional> diff --git a/src/slic3r/plugin/PythonPluginBridge.cpp b/src/slic3r/plugin/PythonPluginBridge.cpp index 328ebbace1..11bb6159cc 100644 --- a/src/slic3r/plugin/PythonPluginBridge.cpp +++ b/src/slic3r/plugin/PythonPluginBridge.cpp @@ -1,5 +1,6 @@ #include "PythonPluginBridge.hpp" +#include <algorithm> #include <boost/log/trivial.hpp> #include <exception> #include <memory> @@ -19,6 +20,7 @@ #include "PyPluginPackage.hpp" #include "PyPluginTrampoline.hpp" #include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp" +#include "pluginTypes/pages/PagesPluginCapability.hpp" #include "pluginTypes/script/ScriptPluginCapability.hpp" #include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp" @@ -39,6 +41,7 @@ thread_local std::string g_active_plugin_key; std::mutex g_registry_mutex; std::unordered_map<std::string, std::vector<py::object>> g_pending_capabilities; std::unordered_map<std::string, py::object> g_pending_package; +std::unordered_map<std::string, std::vector<std::string>> g_pending_fs_read_permissions; struct PluginInstanceHandle { // The C++ plugin interface points into a Python object. Keep both alive through one @@ -76,6 +79,8 @@ void discard_pending_capture_without_python(const std::string& plugin_key) (void) package->second.release(); g_pending_package.erase(package); } + + g_pending_fs_read_permissions.erase(plugin_key); } } // namespace @@ -100,34 +105,38 @@ void PythonPluginBridge::begin_plugin_capture(const std::string& plugin_key) // for this same entry path. g_pending_capabilities.erase(plugin_key); g_pending_package.erase(plugin_key); + g_pending_fs_read_permissions.erase(plugin_key); } // From now until finalize/cancel, @orca.plugin and register_capability() calls made by // Python code on this thread are attributed to this plugin. g_active_plugin_key = plugin_key; } -std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(const std::string& plugin_key, std::string& error) +std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(const std::string& capture_key, + const std::string& plugin_key, + std::string& error) { PythonGILState gil; if (!gil) { error = "Python interpreter is shutting down"; return {}; } - BOOST_LOG_TRIVIAL(info) << "Finalizing Python plugin capture for key " << plugin_key; + BOOST_LOG_TRIVIAL(info) << "Finalizing Python plugin capture for key " << capture_key; // Phase 1: run the package class's register_capabilities() while the active key is // still set. That method is expected to call orca.register_capability() once per // capability class, and register_capability() needs g_active_plugin_key to know which // pending bucket to append to. { - auto clear_active_key = [&plugin_key]() { - if (g_active_plugin_key == plugin_key) + auto clear_active_key = [&capture_key]() { + if (g_active_plugin_key == capture_key) g_active_plugin_key.clear(); }; - auto discard_pending_for_key = [&plugin_key]() { + auto discard_pending_for_key = [&capture_key]() { std::lock_guard<std::mutex> lock(g_registry_mutex); - g_pending_capabilities.erase(plugin_key); - g_pending_package.erase(plugin_key); + g_pending_capabilities.erase(capture_key); + g_pending_package.erase(capture_key); + g_pending_fs_read_permissions.erase(capture_key); }; try { @@ -137,7 +146,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons py::object package_cls; { std::lock_guard<std::mutex> lock(g_registry_mutex); - auto it = g_pending_package.find(plugin_key); + auto it = g_pending_package.find(capture_key); if (it != g_pending_package.end()) { package_cls = it->second; g_pending_package.erase(it); @@ -156,6 +165,33 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons // are kept. py::object package = package_cls(); package.attr("register_capabilities")(); + + // Permission declarations are collected while register_capabilities() runs so the + // plugin can describe its needs without touching wx from the Python load worker. Ask + // only after registration returns, before capability instances are materialized. + std::vector<std::string> fs_read_permissions; + { + std::lock_guard<std::mutex> lock(g_registry_mutex); + auto it = g_pending_fs_read_permissions.find(capture_key); + if (it != g_pending_fs_read_permissions.end()) { + fs_read_permissions = std::move(it->second); + g_pending_fs_read_permissions.erase(it); + } + } + if (!fs_read_permissions.empty()) { + bool granted = false; + { + py::gil_scoped_release release; + granted = PluginAuditManager::instance().request_filesystem_read_permissions(plugin_key, fs_read_permissions); + } + if (!granted) { + error = "Plugin filesystem read permission request denied"; + BOOST_LOG_TRIVIAL(warning) << error << " for key " << plugin_key; + discard_pending_for_key(); + clear_active_key(); + return {}; + } + } } catch (py::error_already_set& err) { log_python_exception_keep(err); error = err.what(); @@ -179,7 +215,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons std::vector<py::object> classes; { std::lock_guard<std::mutex> lock(g_registry_mutex); - auto it = g_pending_capabilities.find(plugin_key); + auto it = g_pending_capabilities.find(capture_key); if (it != g_pending_capabilities.end()) { classes = std::move(it->second); g_pending_capabilities.erase(it); @@ -188,10 +224,10 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons // Registration is complete. Later register_capability() calls should fail instead of // accidentally attaching themselves to this plugin. - if (g_active_plugin_key == plugin_key) + if (g_active_plugin_key == capture_key) g_active_plugin_key.clear(); - BOOST_LOG_TRIVIAL(info) << "Collected " << classes.size() << " registered capability class(es) for key " << plugin_key; + BOOST_LOG_TRIVIAL(info) << "Collected " << classes.size() << " registered capability class(es) for key " << capture_key; std::vector<CapturedCapability> capabilities; capabilities.reserve(classes.size()); @@ -203,7 +239,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons py::object instance = cls(); if (!py::isinstance<PluginCapabilityInterface>(instance)) { error = "Registered capability must inherit from a PluginCapability base"; - BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed type check for key " << plugin_key + BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed type check for key " << capture_key << " error=" << error; return {}; } @@ -211,7 +247,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons auto capability_iface = instance.cast<std::shared_ptr<PluginCapabilityInterface>>(); if (!capability_iface) { error = "Failed to cast Python capability to PluginCapabilityInterface"; - BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed cast for key " << plugin_key + BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed cast for key " << capture_key << " error=" << error; return {}; } @@ -225,7 +261,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons // here is a hard error that rejects the whole plugin capture. if (name.find(';') != std::string::npos) { error = "Capability name must not contain ';': " + name; - BOOST_LOG_TRIVIAL(error) << "Python plugin capture rejected capability for key " << plugin_key + BOOST_LOG_TRIVIAL(error) << "Python plugin capture rejected capability for key " << capture_key << " error=" << error; return {}; } @@ -245,12 +281,12 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons // log the traceback here. GIL is held for the duration of finalize_plugin_capture. log_python_exception_keep(err); error = err.what(); - BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised Python exception for key " << plugin_key + BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised Python exception for key " << capture_key << " error=" << error; return {}; } catch (const std::exception& ex) { error = ex.what(); - BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised exception for key " << plugin_key + BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised exception for key " << capture_key << " error=" << error; return {}; } @@ -278,6 +314,7 @@ void PythonPluginBridge::cancel_plugin_capture(const std::string& plugin_key) // may already have registered under this key. g_pending_capabilities.erase(plugin_key); g_pending_package.erase(plugin_key); + g_pending_fs_read_permissions.erase(plugin_key); } if (g_active_plugin_key == plugin_key) @@ -304,6 +341,7 @@ void PythonPluginBridge::clear_pending_captures() (void) pkg.release(); } g_pending_package.clear(); + g_pending_fs_read_permissions.clear(); g_active_plugin_key.clear(); return; } @@ -312,6 +350,7 @@ void PythonPluginBridge::clear_pending_captures() BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size() << " pending Python plugin capture(s)"; g_pending_capabilities.clear(); g_pending_package.clear(); + g_pending_fs_read_permissions.clear(); g_active_plugin_key.clear(); } @@ -319,17 +358,17 @@ void bind_python_api(pybind11::module_& m) { m.doc() = "OrcaSlicer plugin API"; - auto pluginTypes = py::enum_<PluginCapabilityType>(m, "PluginType", "Available plugin capability groups") - .value("PrinterConnection", PluginCapabilityType::PrinterConnection) - .value("Automation", PluginCapabilityType::Automation) - .value("Analysis", PluginCapabilityType::Analysis) - .value("Importer", PluginCapabilityType::Importer) - .value("Exporter", PluginCapabilityType::Exporter) - .value("Visualization", PluginCapabilityType::Visualization) - .value("Script", PluginCapabilityType::Script) - .value("SlicingPipeline", PluginCapabilityType::SlicingPipeline) - .value("Unknown", PluginCapabilityType::Unknown) - .export_values(); + py::enum_<PluginCapabilityType>(m, "PluginType", "Available plugin capability groups") + .value("PrinterConnection", PluginCapabilityType::PrinterConnection) + .value("Pages", PluginCapabilityType::Pages) + .value("Analysis", PluginCapabilityType::Analysis) + .value("Importer", PluginCapabilityType::Importer) + .value("Exporter", PluginCapabilityType::Exporter) + .value("Visualization", PluginCapabilityType::Visualization) + .value("Script", PluginCapabilityType::Script) + .value("SlicingPipeline", PluginCapabilityType::SlicingPipeline) + .value("Unknown", PluginCapabilityType::Unknown) + .export_values(); py::enum_<PluginResult>(m, "PluginResult", "Execution summary code") .value("Success", PluginResult::Success) @@ -419,9 +458,10 @@ void bind_python_api(pybind11::module_& m) BOOST_LOG_TRIVIAL(debug) << "Registering embedded Python plugin type bindings"; // Make sure you register your bindings here - PrinterAgentPluginCapability::RegisterBindings(m, pluginTypes); - ScriptPluginCapability::RegisterBindings(m, pluginTypes); - SlicingPipelinePluginCapability::RegisterBindings(m, pluginTypes); + PrinterAgentPluginCapability::RegisterBindings(m); + PagesPluginCapability::RegisterBindings(m); + ScriptPluginCapability::RegisterBindings(m); + SlicingPipelinePluginCapability::RegisterBindings(m); PluginHost::RegisterBindings(m); BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings"; @@ -448,6 +488,27 @@ void bind_python_api(pybind11::module_& m) }, R"pbdoc(Register a PluginCapability subclass while OrcaSlicer loads your module.)pbdoc"); + m.def( + "request_permissions", + [](const std::vector<std::string>& fs_read) { + if (g_active_plugin_key.empty()) + throw py::value_error("request_permissions() called outside plugin discovery context"); + + std::lock_guard<std::mutex> lock(g_registry_mutex); + auto& requested = g_pending_fs_read_permissions[g_active_plugin_key]; + for (const std::string& path : fs_read) { + if (path.empty()) + throw py::value_error("request_permissions(fs_read=...) does not accept empty paths"); + if (std::find(requested.begin(), requested.end(), path) == requested.end()) + requested.push_back(path); + } + }, + py::arg("fs_read") = std::vector<std::string>{}, + R"pbdoc(Request filesystem read permissions while OrcaSlicer loads your module. + +The host presents the request to the user after register_capabilities() returns. Denying the +request aborts the plugin load.)pbdoc"); + m.def("plugin", [](py::object cls) { if (g_active_plugin_key.empty()) throw py::value_error("@orca.plugin used outside plugin discovery context"); diff --git a/src/slic3r/plugin/PythonPluginBridge.hpp b/src/slic3r/plugin/PythonPluginBridge.hpp index 6ccadabd00..036c8f1941 100644 --- a/src/slic3r/plugin/PythonPluginBridge.hpp +++ b/src/slic3r/plugin/PythonPluginBridge.hpp @@ -31,7 +31,7 @@ public: // Returns one CapturedCapability per capability, or an empty vector on failure // (error message populated). std::vector<CapturedCapability> finalize_plugin_capture( - const std::string& plugin_key, std::string& error); + const std::string& capture_key, const std::string& plugin_key, std::string& error); // Clear any pending registrations for the key. Safe to call when import fails. void cancel_plugin_capture(const std::string& plugin_key); diff --git a/src/slic3r/plugin/PythonPluginInterface.hpp b/src/slic3r/plugin/PythonPluginInterface.hpp index 4a6df06441..8b7518cf1c 100644 --- a/src/slic3r/plugin/PythonPluginInterface.hpp +++ b/src/slic3r/plugin/PythonPluginInterface.hpp @@ -12,7 +12,7 @@ namespace Slic3r { -enum class PluginCapabilityType { PrinterConnection = 0, Automation, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown }; +enum class PluginCapabilityType { PrinterConnection = 0, Pages, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown }; struct PluginCapabilityId { @@ -39,7 +39,7 @@ inline std::string plugin_capability_type_to_string(PluginCapabilityType type) { switch (type) { case PluginCapabilityType::PrinterConnection: return "printer-connection"; - case PluginCapabilityType::Automation: return "automation"; + case PluginCapabilityType::Pages: return "pages"; case PluginCapabilityType::Analysis: return "analysis"; case PluginCapabilityType::Importer: return "importer"; case PluginCapabilityType::Exporter: return "exporter"; @@ -54,7 +54,7 @@ inline std::string plugin_capability_type_display_name(PluginCapabilityType type { switch (type) { case PluginCapabilityType::PrinterConnection: return "Printer connection"; - case PluginCapabilityType::Automation: return "Automation"; + case PluginCapabilityType::Pages: return "Pages"; case PluginCapabilityType::Analysis: return "Analysis"; case PluginCapabilityType::Importer: return "Importer"; case PluginCapabilityType::Exporter: return "Exporter"; @@ -76,8 +76,8 @@ inline PluginCapabilityType plugin_capability_type_from_string(std::string_view if (lowered == "printer-connection") return PluginCapabilityType::PrinterConnection; - if (lowered == "automation") - return PluginCapabilityType::Automation; + if (lowered == "pages") + return PluginCapabilityType::Pages; if (lowered == "analysis") return PluginCapabilityType::Analysis; if (lowered == "importer") diff --git a/src/slic3r/plugin/host/PluginHost.cpp b/src/slic3r/plugin/host/PluginHost.cpp index 524f07f12c..2830d6f276 100644 --- a/src/slic3r/plugin/host/PluginHost.cpp +++ b/src/slic3r/plugin/host/PluginHost.cpp @@ -1,9 +1,31 @@ #include "PluginHost.hpp" #include "PluginHostBindings.hpp" #include "PluginHostUi.hpp" +#include <slic3r/plugin/PluginAuditManager.hpp> +#include <slic3r/plugin/PluginManager.hpp> + +#include <stdexcept> namespace Slic3r { +namespace host_bindings { +void register_plugin(pybind11::module_& host) +{ + auto plugin_host = host.def_submodule("plugin", "Plugin host API"); + + plugin_host.def( + "storage", + []() -> std::string { + const std::string plugin_key = PluginAuditManager::instance().current_plugin(); + if (plugin_key.empty()) + throw std::runtime_error("plugin.storage() must be called from a plugin callback"); + + return PluginManager::instance().get_storage_dir(plugin_key); + }, + "Return the installed folder of the current plugin."); +} +} // namespace host_bindings + void PluginHost::RegisterBindings(pybind11::module_& module) { auto host = module.def_submodule("host", "Host application API"); @@ -15,6 +37,7 @@ void PluginHost::RegisterBindings(pybind11::module_& module) host_bindings::register_presets(host); host_bindings::register_model(host); host_bindings::register_app(host); + host_bindings::register_plugin(host); // UI: native dialogs and interactive HTML windows for plugins. PluginHostUi::RegisterBindings(host); diff --git a/src/slic3r/plugin/host/PluginHostBindings.hpp b/src/slic3r/plugin/host/PluginHostBindings.hpp index 0f206d5992..94601ad99c 100644 --- a/src/slic3r/plugin/host/PluginHostBindings.hpp +++ b/src/slic3r/plugin/host/PluginHostBindings.hpp @@ -12,5 +12,5 @@ void register_presets(pybind11::module_& host); // PluginHostPresets.cpp void register_model(pybind11::module_& host); // PluginHostModel.cpp void register_app(pybind11::module_& host); // PluginHostApp.cpp void register_slicing(pybind11::module_& host); // PluginHostSlicing.cpp - +void register_plugin(pybind11::module_& host); // PluginHost.cpp } // namespace Slic3r::host_bindings diff --git a/src/slic3r/plugin/host/PluginPages.cpp b/src/slic3r/plugin/host/PluginPages.cpp new file mode 100644 index 0000000000..fed16ee416 --- /dev/null +++ b/src/slic3r/plugin/host/PluginPages.cpp @@ -0,0 +1,469 @@ +#include "PluginPages.hpp" + +#include "libslic3r/AppConfig.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/Notebook.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Widgets/Button.hpp" +#include "slic3r/GUI/Widgets/WebView.hpp" +#include "slic3r/GUI/Widgets/WebViewHostDialog.hpp" +#include "slic3r/GUI/wxExtensions.hpp" +#include "slic3r/plugin/PluginManager.hpp" + +#include <libslic3r/Utils.hpp> + +#include <algorithm> + +#include <boost/filesystem/path.hpp> +#include <boost/log/trivial.hpp> +#include <nlohmann/json.hpp> + +#include <stdexcept> +#include <wx/bookctrl.h> +#include <wx/menu.h> +#include <wx/sizer.h> + +#include <utility> + +namespace Slic3r { +namespace { + +constexpr char PLUGIN_PAGE_BRIDGE_JS[] = R"JS( +(function () { + if (window.top !== window.self) return; + if (window.orca) return; + var handlers = []; + function deliver(payload, attempts) { + try { + if (window.wx && typeof window.wx.postMessage === 'function') { + window.wx.postMessage(payload); + return; + } + } catch (e) { /* retry while the native handler is being registered */ } + if (attempts < 100) + window.setTimeout(function () { deliver(payload, attempts + 1); }, 25); + } + function send(data) { + deliver(JSON.stringify({ + channel: 'orca', kind: 'message', data: (data === undefined ? null : data) + }), 0); + } + window.orca = { + postMessage: function (data) { send(data); }, + onMessage: function (callback) { + if (typeof callback === 'function') handlers.push(callback); + } + }; + window.__orcaDispatch = function (payload) { + var data = payload ? payload.data : null; + for (var i = 0; i < handlers.length; i++) { + try { handlers[i](data); } catch (e) {} + } + }; +})(); +)JS"; + +} // namespace + +PluginPage::PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) + , m_cap(std::move(capability)) + , m_lifetime(std::make_shared<std::atomic<PluginPage*>>(this)) +{ + auto* topsizer = new wxBoxSizer(wxVERTICAL); + SetSizer(topsizer); + + m_browser = WebView::CreateWebView(this, bootstrap_url()); + if (m_browser == nullptr) { + wxLogError("Could not initialize plugin page web view"); + return; + } + + topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1)); + m_browser->Bind(wxEVT_WEBVIEW_LOADED, &PluginPage::on_bootstrap_event, this); + m_browser->Bind(wxEVT_WEBVIEW_ERROR, &PluginPage::on_bootstrap_event, this); + m_browser->Bind(wxEVT_WEBVIEW_NEWWINDOW, &PluginPage::on_new_window, this); + m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &PluginPage::on_script_message, this); + m_browser->AddUserScript(wxString::FromUTF8(GUI::WebViewHostDialog::theme_user_script())); + m_browser->AddUserScript(wxString::FromUTF8(GUI::WebViewHostDialog::plugin_defaults_user_script())); + m_browser->AddUserScript(PLUGIN_PAGE_BRIDGE_JS); + + const std::shared_ptr<std::atomic<PluginPage*>> lifetime = m_lifetime; + m_cap->set_message_sender([lifetime](const std::string& message) { + if (wxTheApp == nullptr) + return; + + GUI::wxGetApp().CallAfter([lifetime, message] { + if (PluginPage* page = lifetime->load(std::memory_order_acquire)) + page->push_message(message); + }); + }); + +} + +PluginPage::~PluginPage() +{ + detach_capability(); + if (m_lifetime) + m_lifetime->store(nullptr, std::memory_order_release); +} + +void PluginPage::detach_capability() +{ + if (m_lifetime) + m_lifetime->store(nullptr, std::memory_order_release); + if (m_cap) + m_cap->clear_message_sender(); + m_cap.reset(); +} + +wxString PluginPage::web_base_url() const +{ + const auto path = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string(); + return wxString("file://") + GUI::from_u8(path) + "/"; +} + +wxString PluginPage::bootstrap_url() const +{ + const auto path = (boost::filesystem::path(resources_dir()) / "web/dialog/PluginWebDialog/blank.html").make_preferred().string(); + return wxString("file://") + GUI::from_u8(path); +} + +void PluginPage::on_bootstrap_event(wxWebViewEvent& event) +{ + load_plugin_content(); + event.Skip(); +} + +void PluginPage::load_plugin_content() +{ + if (m_content_loaded || m_browser == nullptr || m_cap == nullptr) + return; + + m_content_loaded = true; + try { + m_browser->SetPage(wxString::FromUTF8(m_cap->get_ui()), web_base_url()); + } catch (const std::exception& error) { + BOOST_LOG_TRIVIAL(error) << "Failed to load plugin page '" << m_cap->name() << "': " << error.what(); + detach_capability(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << "Failed to load plugin page '" << m_cap->name() << "'"; + detach_capability(); + } +} + +void PluginPage::on_new_window(wxWebViewEvent& event) +{ + const wxString url = event.GetURL(); + if (!url.empty() && m_browser != nullptr) + m_browser->LoadURL(url); + event.Veto(); +} + +void PluginPage::on_script_message(wxWebViewEvent& event) +{ + if (!m_cap) + return; + + const wxString payload = event.GetString(); + nlohmann::json root = nlohmann::json::parse(payload.utf8_string(), nullptr, false); + if (root.is_discarded() || root.value("channel", std::string()) != "orca" || + root.value("kind", std::string()) != "message") + return; + + const auto data = root.find("data"); + try { + m_cap->on_message(data == root.end() + ? "null" + : data->dump(-1, ' ', false, nlohmann::json::error_handler_t::replace)); + } catch (const std::exception& error) { + BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "': " << error.what(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "'"; + } +} + +void PluginPage::push_message(const std::string& message) +{ + if (m_browser == nullptr) + return; + + // PagesPluginCapability::post_message() already dumps JSON, so accept it as-is; only a + // non-JSON payload needs wrapping as a string literal. + const std::string payload = nlohmann::json::accept(message) + ? message + : nlohmann::json(message).dump(-1, ' ', false, nlohmann::json::error_handler_t::replace); + + WebView::RunScript(m_browser, wxString::Format( + "(function dispatch(payload, attempts) {\n" + " if (typeof window.__orcaDispatch === 'function') { window.__orcaDispatch(payload); return; }\n" + " if (attempts < 100) window.setTimeout(function() { dispatch(payload, attempts + 1); }, 25);\n" + "})({data: %s}, 0);", + wxString::FromUTF8(payload))); +} + +PluginPages::~PluginPages() +{ + shutdown(); +} + +void PluginPages::initialize(Notebook* parent) +{ + shutdown(); + m_parent = parent; + if (m_parent == nullptr) + return; + + m_visible_page_count = GUI::wxGetApp().app_config->get_plugin_pages_visible_count(); + + for (const auto& capability : PluginManager::instance().get_plugin_capabilities("", PluginCapabilityType::Pages)) { + if (capability) + create_page(capability->identity()); + } + relayout(); +} + +void PluginPages::shutdown() +{ + while (!m_pages.empty()) + remove_page(m_pages.begin()->first); + m_parent = nullptr; +} + +void PluginPages::set_visible_page_count(int count) +{ + const int clamped = std::clamp(count, PLUGIN_PAGES_VISIBLE_COUNT_MIN, PLUGIN_PAGES_VISIBLE_COUNT_MAX); + if (clamped == m_visible_page_count) + return; + + m_visible_page_count = clamped; + relayout(); +} + +std::shared_ptr<PagesPluginCapability> PluginPages::get_pages_cap(const PluginCapabilityId& id, bool is_enabled) const +{ + auto capability = PluginManager::instance().get_plugin_capability(id, /*only_enabled=*/false); + if (!capability || capability->is_enabled() != is_enabled || capability->type() != PluginCapabilityType::Pages) + return nullptr; + + return std::dynamic_pointer_cast<PagesPluginCapability>(capability); +} + +bool PluginPages::create_page(const PluginCapabilityId& id) +{ + if (m_pages.find(id) != m_pages.end()) + return false; + + auto capability = get_pages_cap(id, true); + if (!capability) + return false; + + std::string icon; + try { + icon = capability->get_icon(); + } catch (const std::exception& error) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to get icon for plugin " << id.plugin_key << ": " << error.what(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to get icon for plugin " << id.plugin_key; + } + + auto* page = new PluginPage(m_parent, std::move(capability)); + if (!page->is_valid()) { + page->Destroy(); + return false; + } + + if (!icon.empty()) { + try { + boost::filesystem::path icon_path(icon); + const std::string extension = icon_path.extension().string(); + if (extension == ".svg" || extension == ".png") + icon_path.replace_extension(); + + page->set_icon(create_scaled_bitmap(icon_path.string(), m_parent, 20)); + } catch (const std::exception& error) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to load icon for plugin " << id.plugin_key << ": " << error.what(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to load icon for plugin " << id.plugin_key; + } + } + + m_pages.emplace(id, page); + m_order.push_back(id); + return true; +} + +void PluginPages::on_cap_register(const PluginCapabilityId& id) +{ + if (m_parent == nullptr) + return; + + if (create_page(id)) + relayout(); +} + +void PluginPages::on_cap_deregister(const PluginCapabilityId& id) +{ + remove_page(id); +} + +void PluginPages::on_plugin_register(const std::string& plugin_key) +{ + for (const auto& capability : PluginManager::instance().get_plugin_capabilities(plugin_key, PluginCapabilityType::Pages)) { + if (capability) + on_cap_register(capability->identity()); + } +} + +void PluginPages::on_plugin_deregister(const std::string& plugin_key) +{ + for (auto it = m_pages.begin(); it != m_pages.end();) { + if (it->first.plugin_key != plugin_key) { + ++it; + continue; + } + + const PluginCapabilityId id = it->first; + ++it; + remove_page(id); + } +} + +void PluginPages::remove_page(const PluginCapabilityId& id) +{ + auto it = m_pages.find(id); + if (it == m_pages.end()) + return; + + PluginPage* page = it->second; + page->detach_capability(); + + m_pages.erase(it); + m_order.erase(std::remove(m_order.begin(), m_order.end(), id), m_order.end()); + + const int idx = m_parent != nullptr ? m_parent->FindPage(page) : wxNOT_FOUND; + if (idx != wxNOT_FOUND) + m_parent->RemovePage(idx); + + relayout(); + page->Destroy(); +} + +wxString PluginPages::page_tab_id(const PluginCapabilityId& id) +{ + return wxString::FromUTF8("plugin." + id.plugin_key + "." + id.name); +} + +void PluginPages::relayout() +{ + if (m_parent == nullptr) + return; + + m_order.erase(std::remove_if(m_order.begin(), m_order.end(), + [this](const PluginCapabilityId& id) { + const bool orphaned = m_pages.find(id) == m_pages.end(); + if (orphaned) + BOOST_LOG_TRIVIAL(error) << "PluginPages::relayout: '" << id.name << "' was in m_order but not m_pages, dropping"; + return orphaned; + }), + m_order.end()); + + const int visible_slots = std::max(1, m_visible_page_count); + const bool need_overflow = static_cast<int>(m_order.size()) > visible_slots; + + // Every visible slot is a normal, individual tab hosting its own page. When there's + // overflow, the last slot's page is swappable via m_overflow_button/show_overflow_menu() + // rather than being a fixed page — m_swapped_in_id tracks which one currently sits there. + std::vector<PluginCapabilityId> tab_ids; + if (!need_overflow) { + tab_ids = m_order; + m_swapped_in_id.reset(); + } else { + const auto overflow_begin = m_order.begin() + (visible_slots - 1); + tab_ids.assign(m_order.begin(), overflow_begin); + + if (!m_swapped_in_id || std::find(overflow_begin, m_order.end(), *m_swapped_in_id) == m_order.end()) + m_swapped_in_id = *overflow_begin; + tab_ids.push_back(*m_swapped_in_id); + } + + // MainFrame::show_device() relayouts on every printer change and most of those change + // nothing, so only touch the notebook when the trailing slots don't already spell out + // tab_ids — a rebuild destroys and recreates every tab button and rasterizes every icon. + const size_t page_count = m_parent->GetPageCount(); + bool up_to_date = page_count >= tab_ids.size(); + for (size_t i = 0; up_to_date && i < tab_ids.size(); ++i) + up_to_date = m_parent->GetPageName(page_count - tab_ids.size() + i) == page_tab_id(tab_ids[i]); + for (const auto& [id, page] : m_pages) { + if (!up_to_date) + break; + const bool wanted = std::find(tab_ids.begin(), tab_ids.end(), id) != tab_ids.end(); + up_to_date = (m_parent->FindPage(page) != wxNOT_FOUND) == wanted; + } + + if (!up_to_date) { + const wxString id_to_reselect = m_parent->GetSelectedPageName(); + + for (const auto& [id, page] : m_pages) { + const int idx = m_parent->FindPage(page); + if (idx != wxNOT_FOUND) + m_parent->RemovePage(idx); + } + + for (const auto& id : tab_ids) { + PluginPage* page = m_pages.at(id); + m_parent->InsertPage(m_parent->GetPageCount(), page_tab_id(id), page, wxString::FromUTF8(id.name), "", + false, page->icon()); + } + + if (!id_to_reselect.empty()) + m_parent->SelectPageByName(id_to_reselect); + } + + if (need_overflow) { + if (m_overflow_button == nullptr) { + auto* btn = new Button(m_parent->GetBtnsListCtrl(), wxString(L"\u25BE"), wxString(), wxNO_BORDER); + btn->SetCornerRadius(0); + const int em = em_unit(m_parent); + btn->SetMinSize({40 * em / 10, 36 * em / 10}); + btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { show_overflow_menu(); }); + GUI::wxGetApp().UpdateDarkUI(btn); + m_overflow_button = btn; + } + m_parent->SetOverflowButton(m_overflow_button); + } else if (m_overflow_button != nullptr) { + m_parent->SetOverflowButton(nullptr); + m_overflow_button->Destroy(); + m_overflow_button = nullptr; + } +} + +void PluginPages::show_overflow_menu() +{ + const int visible_slots = std::max(1, m_visible_page_count); + if (m_overflow_button == nullptr || static_cast<int>(m_order.size()) <= visible_slots) + return; + + const std::vector<PluginCapabilityId> overflow_ids(m_order.begin() + (visible_slots - 1), m_order.end()); + + wxMenu menu; + for (size_t i = 0; i < overflow_ids.size(); ++i) + menu.AppendRadioItem(static_cast<int>(wxID_HIGHEST + 1 + i), wxString::FromUTF8(overflow_ids[i].name)); + if (m_swapped_in_id) { + const auto it = std::find(overflow_ids.begin(), overflow_ids.end(), *m_swapped_in_id); + if (it != overflow_ids.end()) + menu.Check(static_cast<int>(wxID_HIGHEST + 1 + (it - overflow_ids.begin())), true); + } + + menu.Bind(wxEVT_MENU, [this, overflow_ids](wxCommandEvent& evt) { + const size_t index = static_cast<size_t>(evt.GetId() - (wxID_HIGHEST + 1)); + if (index >= overflow_ids.size()) + return; + m_swapped_in_id = overflow_ids[index]; + relayout(); + m_parent->SelectPageByName(page_tab_id(*m_swapped_in_id)); + }); + m_overflow_button->PopupMenu(&menu); +} + +} // namespace Slic3r diff --git a/src/slic3r/plugin/host/PluginPages.hpp b/src/slic3r/plugin/host/PluginPages.hpp new file mode 100644 index 0000000000..4d00de6df9 --- /dev/null +++ b/src/slic3r/plugin/host/PluginPages.hpp @@ -0,0 +1,89 @@ +#pragma once + +#include <slic3r/plugin/PythonPluginInterface.hpp> +#include <slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp> + +#include <atomic> +#include <map> +#include <memory> +#include <optional> +#include <string> +#include <vector> + +#include <wx/bitmap.h> +#include <wx/panel.h> +#include <wx/webview.h> + +class Notebook; + +namespace Slic3r { + +class PluginPage : public wxPanel +{ +public: + PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability); + ~PluginPage() override; + + PluginPage() = delete; + + bool is_valid() const { return m_browser != nullptr && m_cap != nullptr; } + void detach_capability(); + void on_bootstrap_event(wxWebViewEvent& event); + void on_new_window(wxWebViewEvent& event); + void on_script_message(wxWebViewEvent& event); + void push_message(const std::string& message); + void set_icon(const wxBitmap& icon) { m_icon = icon; } + const wxBitmap& icon() const { return m_icon; } + +private: + void load_plugin_content(); + wxString bootstrap_url() const; + wxString web_base_url() const; + + wxWebView* m_browser{nullptr}; + std::shared_ptr<PagesPluginCapability> m_cap; + std::shared_ptr<std::atomic<PluginPage*>> m_lifetime; + bool m_content_loaded{false}; + wxBitmap m_icon; +}; + +class PluginPages +{ +public: + PluginPages() = default; + ~PluginPages(); + + PluginPages(const PluginPages&) = delete; + PluginPages& operator=(const PluginPages&) = delete; + + void initialize(Notebook* parent); + void shutdown(); + + void on_cap_register(const PluginCapabilityId& id); + void on_cap_deregister(const PluginCapabilityId& id); + void on_plugin_register(const std::string& plugin_key); + void on_plugin_deregister(const std::string& plugin_key); + + void set_visible_page_count(int count); + + void relayout(); + +private: + std::shared_ptr<PagesPluginCapability> get_pages_cap(const PluginCapabilityId& id, bool is_enabled) const; + bool create_page(const PluginCapabilityId& id); + void remove_page(const PluginCapabilityId& id); + + void show_overflow_menu(); + static wxString page_tab_id(const PluginCapabilityId& id); + + std::map<PluginCapabilityId, PluginPage*> m_pages; + std::vector<PluginCapabilityId> m_order; + Notebook* m_parent{nullptr}; + + int m_visible_page_count{0}; + + std::optional<PluginCapabilityId> m_swapped_in_id; + wxWindow* m_overflow_button{nullptr}; +}; + +} // namespace Slic3r diff --git a/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.cpp b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.cpp new file mode 100644 index 0000000000..e009f4426f --- /dev/null +++ b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.cpp @@ -0,0 +1,60 @@ +#include "PagesPluginCapability.hpp" +#include "PagesPluginCapabilityTrampoline.hpp" + +#include "../../PluginFsUtils.hpp" + +#include <boost/log/trivial.hpp> +#include <pybind11/pybind11.h> + +#include <utility> + +namespace py = pybind11; + +namespace Slic3r { + +void PagesPluginCapability::RegisterBindings(pybind11::module_& module) +{ + BOOST_LOG_TRIVIAL(debug) << "Registering orca.pages bindings"; + + auto pages = module.def_submodule("pages", "Plugin page API"); + + py::class_<PagesPluginCapability, PluginCapabilityInterface, PyPagesPluginCapabilityTrampoline, + std::shared_ptr<PagesPluginCapability>>(pages, "PagesPluginCapabilityBase") + .def(py::init<>()) + .def("get_type", &PagesPluginCapability::get_type) + .def("get_ui", &PagesPluginCapability::get_ui) + .def("get_icon", &PagesPluginCapability::get_icon) + .def("on_message", &PagesPluginCapability::on_message) + .def( + "post_message", + [](PagesPluginCapability& capability, py::object data) { + capability.post_message(py_to_json(data).dump()); + }, + py::arg("data"), "Send a JSON-compatible value to the page's window.orca.onMessage handlers."); +} + +void PagesPluginCapability::post_message(std::string message) +{ + std::function<void(const std::string&)> sender; + { + std::lock_guard<std::mutex> lock(m_message_mutex); + sender = m_message_sender; + } + + if (sender) + sender(message); +} + +void PagesPluginCapability::set_message_sender(std::function<void(const std::string&)> sender) +{ + std::lock_guard<std::mutex> lock(m_message_mutex); + m_message_sender = std::move(sender); +} + +void PagesPluginCapability::clear_message_sender() +{ + std::lock_guard<std::mutex> lock(m_message_mutex); + m_message_sender = nullptr; +} + +} // namespace Slic3r diff --git a/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp new file mode 100644 index 0000000000..978492006c --- /dev/null +++ b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp @@ -0,0 +1,33 @@ +#ifndef slic3r_PagesPluginCapability_hpp_ +#define slic3r_PagesPluginCapability_hpp_ + +#include "../../PythonPluginInterface.hpp" +#include "pybind11/pybind11.h" + +#include <functional> +#include <mutex> +#include <string> + +namespace Slic3r { +class PagesPluginCapability : public PluginCapabilityInterface +{ +public: + static void RegisterBindings(pybind11::module_& module); + + PluginCapabilityType get_type() const override { return PluginCapabilityType::Pages; } + + virtual std::string get_ui() = 0; + virtual void on_message(std::string message) { (void) message; } + virtual std::string get_icon() { return {}; } + + void post_message(std::string message); + void set_message_sender(std::function<void(const std::string&)> sender); + void clear_message_sender(); + +private: + mutable std::mutex m_message_mutex; + std::function<void(const std::string&)> m_message_sender; +}; +} // namespace Slic3r + +#endif diff --git a/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp new file mode 100644 index 0000000000..f6e13c981b --- /dev/null +++ b/src/slic3r/plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include "PagesPluginCapability.hpp" +#include "../../PluginFsUtils.hpp" +#include "../../PyPluginTrampoline.hpp" + +#include <nlohmann/json.hpp> + +namespace Slic3r { + +class PyPagesPluginCapabilityTrampoline : public PyPluginCommonTrampoline<PagesPluginCapability> +{ +public: + using PyPluginCommonTrampoline<PagesPluginCapability>::PyPluginCommonTrampoline; + + std::string get_icon() override + { + ORCA_PY_OVERRIDE_AUDITED( + [] {}, + PYBIND11_OVERRIDE, + std::string, + PagesPluginCapability, + get_icon); + } + + std::string get_ui() override + { + ORCA_PY_OVERRIDE_AUDITED( + [] {}, + PYBIND11_OVERRIDE_PURE, + std::string, + PagesPluginCapability, + get_ui); + } + + void on_message(std::string message) override + { + PluginCapabilityInterface::RefCounter ref_counter(*this); + PythonGILState gil; + if (!gil) + throw std::runtime_error("Python interpreter is shutting down"); + + ORCA_PY_AUDIT_SCOPE(); + + pybind11::function override = pybind11::get_override(static_cast<PagesPluginCapability*>(this), "on_message"); + if (!override) + return; + + nlohmann::json data = nlohmann::json::parse(message, nullptr, false); + if (data.is_discarded()) + data = message; + + ORCA_PY_LOGGED_OVERRIDE_BODY(override(::Slic3r::json_to_py(data))); + } +}; + +} // namespace Slic3r diff --git a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp index b3d3d5d44c..d428775c12 100644 --- a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp +++ b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp @@ -13,10 +13,8 @@ namespace py = pybind11; namespace Slic3r { -void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes) +void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module) { - (void) pluginTypes; - auto printer_agent_module = module.def_submodule("printer_agent", "Printer Agent API"); py::enum_<FilamentSyncMode>(printer_agent_module, "FilamentSyncMode") diff --git a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp index 33ad211b9c..ede7c6a9b8 100644 --- a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp +++ b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp @@ -19,7 +19,7 @@ namespace Slic3r { class PrinterAgentPluginCapability : public PluginCapabilityInterface, public IPrinterAgent { public: - static void RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes); + static void RegisterBindings(pybind11::module_& module); PluginCapabilityType get_type() const override { return PluginCapabilityType::PrinterConnection; } diff --git a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp index 226ea04650..5491e1cef6 100644 --- a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp +++ b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp @@ -16,206 +16,206 @@ public: AgentInfo get_agent_info() override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, AgentInfo, PrinterAgentPluginCapability, + [] {}, PYBIND11_OVERRIDE_PURE, AgentInfo, PrinterAgentPluginCapability, get_agent_info); } int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, connect_printer, dev_id, + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, connect_printer, dev_id, dev_ip, username, password, use_ssl); } int disconnect_printer() override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, disconnect_printer); + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, disconnect_printer); } int send_message(std::string dev_id, std::string json_str, int qos, int flag) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message, dev_id, + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message, dev_id, json_str, qos, flag); } int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message_to_printer, + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message_to_printer, dev_id, json_str, qos, flag); } bool start_discovery(bool start, bool sending) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, start_discovery, start, + [] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, start_discovery, start, sending); } int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind_detect, dev_ip, + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind_detect, dev_ip, sec_link, detect); } std::string get_user_selected_machine() override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, std::string, PrinterAgentPluginCapability, + [] {}, PYBIND11_OVERRIDE_PURE, std::string, PrinterAgentPluginCapability, get_user_selected_machine); } int set_user_selected_machine(std::string dev_id) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_user_selected_machine, dev_id); } int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_send_gcode_to_sdcard, params, update_fn, cancel_fn, wait_fn); } int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_local_print, + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_local_print, params, update_fn, cancel_fn); } FilamentSyncMode get_filament_sync_mode() const override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, FilamentSyncMode, PrinterAgentPluginCapability, + [] {}, PYBIND11_OVERRIDE_PURE, FilamentSyncMode, PrinterAgentPluginCapability, get_filament_sync_mode); } bool fetch_filament_info(std::string dev_id) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, fetch_filament_info, dev_id); + [] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, fetch_filament_info, dev_id); } int check_cert() override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, check_cert); + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, check_cert); } void install_device_cert(std::string dev_id, bool lan_only) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, void, PrinterAgentPluginCapability, install_device_cert, dev_id, + [] {}, PYBIND11_OVERRIDE_PURE, void, PrinterAgentPluginCapability, install_device_cert, dev_id, lan_only); } int ping_bind(std::string ping_code) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, ping_bind, ping_code); + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, ping_bind, ping_code); } int bind(std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind, dev_ip, dev_id, + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind, dev_ip, dev_id, dev_model, sec_link, timezone, improved, update_fn); } int unbind(std::string dev_id) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, unbind, dev_id); + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, unbind, dev_id); } int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_print, params, + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_print, params, update_fn, cancel_fn, wait_fn); } int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_local_print_with_record, params, update_fn, cancel_fn, wait_fn); } int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_sdcard_print, params, + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_sdcard_print, params, update_fn, cancel_fn); } int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, get_hms_snapshot, dev_id, + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, get_hms_snapshot, dev_id, file_name, callback); } int set_server_callback(OnServerErrFn fn) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_server_callback, fn); + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_server_callback, fn); } int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_ssdp_msg_fn, fn); + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_ssdp_msg_fn, fn); } int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_printer_connected_fn, + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_printer_connected_fn, fn); } int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_subscribe_failure_fn, + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_subscribe_failure_fn, fn); } int set_on_message_fn(OnMessageFn fn) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_message_fn, fn); + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_message_fn, fn); } int set_on_user_message_fn(OnMessageFn fn) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_user_message_fn, fn); + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_user_message_fn, fn); } int set_on_local_connect_fn(OnLocalConnectedFn fn) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_connect_fn, fn); + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_connect_fn, fn); } int set_on_local_message_fn(OnMessageFn fn) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_message_fn, fn); + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_message_fn, fn); } int set_queue_on_main_fn(QueueOnMainFn fn) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_queue_on_main_fn, fn); + [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_queue_on_main_fn, fn); } // request_bind_ticket returns its ticket through a std::string* out-param, which pybind11 @@ -223,7 +223,7 @@ public: // returns a (result, ticket) tuple, which we unpack into the int result and the out-param. int request_bind_ticket(std::string* ticket) override { - ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading); + ORCA_PY_AUDIT_SCOPE(); ::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this); ::Slic3r::PythonGILState gil; if (!gil) diff --git a/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.cpp b/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.cpp index 35a259edf6..712ba9b653 100644 --- a/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.cpp +++ b/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.cpp @@ -9,9 +9,8 @@ namespace py = pybind11; namespace Slic3r { -void ScriptPluginCapability::RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes) +void ScriptPluginCapability::RegisterBindings(pybind11::module_& module) { - (void) pluginTypes; BOOST_LOG_TRIVIAL(debug) << "Registering orca.script bindings"; auto script = module.def_submodule("script", "Script Plugins API"); diff --git a/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp b/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp index cb5bc45c08..fb1319e560 100644 --- a/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp +++ b/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp @@ -11,8 +11,7 @@ public: virtual ExecutionResult execute() = 0; - static void RegisterBindings(pybind11::module_ &module, - pybind11::enum_<PluginCapabilityType> &pluginTypes); + static void RegisterBindings(pybind11::module_ &module); }; } // namespace Slic3r diff --git a/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp b/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp index f77d7ce597..62958a89d9 100644 --- a/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp +++ b/src/slic3r/plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp @@ -13,7 +13,6 @@ public: ExecutionResult execute() override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, ExecutionResult, diff --git a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp index f4569aebba..d2e630242d 100644 --- a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp +++ b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp @@ -8,8 +8,7 @@ namespace Slic3r { bool SlicingPipelineContext::cancelled() const { return print && print->canceled(); } -void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module, py::enum_<PluginCapabilityType>& pluginTypes) { - (void) pluginTypes; // unused: this capability defines its own Step enum (below) rather than extending the shared PluginCapabilityType enum. +void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module) { auto slicing = module.def_submodule("slicing", "Slicing pipeline API (research/experimental)."); py::enum_<SlicingPipelineStepPlugin>(slicing, "Step") diff --git a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp index 639c087371..da0dbcbcbd 100644 --- a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp +++ b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp @@ -37,7 +37,7 @@ public: // Runs on the slicing worker thread. Do not call orca.host.ui.* here: the UI thread can be // blocked waiting on the slicing worker, so a marshaled UI call from this thread can deadlock. virtual ExecutionResult execute(SlicingPipelineContext& ctx) = 0; - static void RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes); + static void RegisterBindings(pybind11::module_& module); }; } // namespace Slic3r diff --git a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapabilityTrampoline.hpp b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapabilityTrampoline.hpp index 8605532c12..5c9d527e42 100644 --- a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapabilityTrampoline.hpp +++ b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapabilityTrampoline.hpp @@ -10,7 +10,6 @@ public: using PyPluginCommonTrampoline<SlicingPipelinePluginCapability>::PyPluginCommonTrampoline; ExecutionResult execute(SlicingPipelineContext& ctx) override { ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [&]{ // At Step.psGCodePostProcess the plugin edits the exported G-code file, which lives // outside data_dir() (a temp/output folder), so writing to it would otherwise be diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3cbdc25f5f..c403152c4e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -30,25 +30,47 @@ if (APPLE) target_link_libraries(test_common INTERFACE "-liconv -framework IOKit" "-framework CoreFoundation" -lc++) endif() -# Copies runtime DLLs next to each test executable. Handles both single-config -# generators (CMAKE_BUILD_TYPE set) and multi-config generators (Ninja -# Multi-Config, Visual Studio) where CMAKE_BUILD_TYPE is empty and DLLs must -# land in every per-config output directory. +# Copies runtime shared libraries next to each test executable. Handles both +# single-config generators (CMAKE_BUILD_TYPE set) and multi-config generators +# (Ninja Multi-Config, Visual Studio) where CMAKE_BUILD_TYPE is empty and DLLs +# must land in every per-config output directory. On Windows the loader finds +# DLLs in the executable's directory; the Linux branch below does the same for +# the deps-built FFmpeg libraries and adds an $ORIGIN rpath, since the ELF +# loader does not search the executable's directory and the CI unit-test runner +# only receives the tests artifact (no deps install). function(orcaslicer_copy_test_dlls) - if (NOT WIN32) - return() - endif() - set(_configs ${CMAKE_CONFIGURATION_TYPES}) - if (NOT _configs) - set(_configs "${CMAKE_BUILD_TYPE}") - endif() - foreach(_cfg IN LISTS _configs) - if (_cfg STREQUAL "Debug") - orcaslicer_copy_dlls(COPY_DLLS "Debug" "d" _unused_dlls) - else() - orcaslicer_copy_dlls(COPY_DLLS "${_cfg}" "" _unused_dlls) + if (WIN32) + set(_configs ${CMAKE_CONFIGURATION_TYPES}) + if (NOT _configs) + set(_configs "${CMAKE_BUILD_TYPE}") endif() - endforeach() + foreach(_cfg IN LISTS _configs) + if (_cfg STREQUAL "Debug") + orcaslicer_copy_dlls(COPY_DLLS "Debug" "d" _unused_dlls) + else() + orcaslicer_copy_dlls(COPY_DLLS "${_cfg}" "" _unused_dlls) + endif() + endforeach() + elseif (UNIX AND NOT APPLE) + # Only test executables that link libslic3r_gui pull in the FFmpeg + # shared libraries (src/slic3r/CMakeLists.txt links PkgConfig::LIBAV + # into it). Copy them next to the executable and give it an $ORIGIN + # rpath so the loader finds them when the tests run on the CI unit-test + # runner, which only receives this build/tests tree. + get_target_property(_linked_libs ${_TEST_NAME}_tests LINK_LIBRARIES) + if (NOT "libslic3r_gui" IN_LIST _linked_libs) + return() + endif() + + set_property(TARGET ${_TEST_NAME}_tests PROPERTY BUILD_RPATH "$ORIGIN") + set(_configs ${CMAKE_CONFIGURATION_TYPES}) + if (NOT _configs) + set(_configs "${CMAKE_BUILD_TYPE}") + endif() + foreach(_cfg IN LISTS _configs) + orcaslicer_copy_sos(${_TEST_NAME}_tests "${_cfg}" "" _unused_sos) + endforeach() + endif() endfunction() # Register Catch2 tags as CTest labels so `ctest -L`/`-LE` can filter by tag. diff --git a/tests/data/utf8_part_names.step b/tests/data/utf8_part_names.step new file mode 100644 index 0000000000..c6d41898ad --- /dev/null +++ b/tests/data/utf8_part_names.step @@ -0,0 +1,1207 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('Open CASCADE Model'),'2;1'); +FILE_NAME('Open CASCADE Shape Model','2026-08-27T11:50:16',('Author'),( + 'Open CASCADE'),'Open CASCADE STEP processor 7.6','Open CASCADE 7.6' + ,'Unknown'); +FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }')); +ENDSEC; +DATA; +#1 = APPLICATION_PROTOCOL_DEFINITION('international standard', + 'automotive_design',2000,#2); +#2 = APPLICATION_CONTEXT( + 'core data for automotive mechanical design processes'); +#3 = SHAPE_DEFINITION_REPRESENTATION(#4,#10); +#4 = PRODUCT_DEFINITION_SHAPE('','',#5); +#5 = PRODUCT_DEFINITION('design','',#6,#9); +#6 = PRODUCT_DEFINITION_FORMATION('','',#7); +#7 = PRODUCT('pièce','pièce','',(#8)); +#8 = PRODUCT_CONTEXT('',#2,'mechanical'); +#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#10 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#15),#345); +#11 = AXIS2_PLACEMENT_3D('',#12,#13,#14); +#12 = CARTESIAN_POINT('',(0.,0.,0.)); +#13 = DIRECTION('',(0.,0.,1.)); +#14 = DIRECTION('',(1.,0.,-0.)); +#15 = MANIFOLD_SOLID_BREP('',#16); +#16 = CLOSED_SHELL('',(#17,#137,#237,#284,#331,#338)); +#17 = ADVANCED_FACE('',(#18),#32,.F.); +#18 = FACE_BOUND('',#19,.F.); +#19 = EDGE_LOOP('',(#20,#55,#83,#111)); +#20 = ORIENTED_EDGE('',*,*,#21,.F.); +#21 = EDGE_CURVE('',#22,#24,#26,.T.); +#22 = VERTEX_POINT('',#23); +#23 = CARTESIAN_POINT('',(0.,0.,0.)); +#24 = VERTEX_POINT('',#25); +#25 = CARTESIAN_POINT('',(0.,0.,6.)); +#26 = SURFACE_CURVE('',#27,(#31,#43),.PCURVE_S1.); +#27 = LINE('',#28,#29); +#28 = CARTESIAN_POINT('',(0.,0.,0.)); +#29 = VECTOR('',#30,1.); +#30 = DIRECTION('',(0.,0.,1.)); +#31 = PCURVE('',#32,#37); +#32 = PLANE('',#33); +#33 = AXIS2_PLACEMENT_3D('',#34,#35,#36); +#34 = CARTESIAN_POINT('',(0.,0.,0.)); +#35 = DIRECTION('',(1.,0.,-0.)); +#36 = DIRECTION('',(0.,0.,1.)); +#37 = DEFINITIONAL_REPRESENTATION('',(#38),#42); +#38 = LINE('',#39,#40); +#39 = CARTESIAN_POINT('',(0.,0.)); +#40 = VECTOR('',#41,1.); +#41 = DIRECTION('',(1.,0.)); +#42 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#43 = PCURVE('',#44,#49); +#44 = PLANE('',#45); +#45 = AXIS2_PLACEMENT_3D('',#46,#47,#48); +#46 = CARTESIAN_POINT('',(0.,0.,0.)); +#47 = DIRECTION('',(-0.,1.,0.)); +#48 = DIRECTION('',(0.,0.,1.)); +#49 = DEFINITIONAL_REPRESENTATION('',(#50),#54); +#50 = LINE('',#51,#52); +#51 = CARTESIAN_POINT('',(0.,0.)); +#52 = VECTOR('',#53,1.); +#53 = DIRECTION('',(1.,0.)); +#54 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#55 = ORIENTED_EDGE('',*,*,#56,.T.); +#56 = EDGE_CURVE('',#22,#57,#59,.T.); +#57 = VERTEX_POINT('',#58); +#58 = CARTESIAN_POINT('',(0.,12.,0.)); +#59 = SURFACE_CURVE('',#60,(#64,#71),.PCURVE_S1.); +#60 = LINE('',#61,#62); +#61 = CARTESIAN_POINT('',(0.,0.,0.)); +#62 = VECTOR('',#63,1.); +#63 = DIRECTION('',(-0.,1.,0.)); +#64 = PCURVE('',#32,#65); +#65 = DEFINITIONAL_REPRESENTATION('',(#66),#70); +#66 = LINE('',#67,#68); +#67 = CARTESIAN_POINT('',(0.,0.)); +#68 = VECTOR('',#69,1.); +#69 = DIRECTION('',(0.,-1.)); +#70 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#71 = PCURVE('',#72,#77); +#72 = PLANE('',#73); +#73 = AXIS2_PLACEMENT_3D('',#74,#75,#76); +#74 = CARTESIAN_POINT('',(0.,0.,0.)); +#75 = DIRECTION('',(0.,0.,1.)); +#76 = DIRECTION('',(1.,0.,-0.)); +#77 = DEFINITIONAL_REPRESENTATION('',(#78),#82); +#78 = LINE('',#79,#80); +#79 = CARTESIAN_POINT('',(0.,0.)); +#80 = VECTOR('',#81,1.); +#81 = DIRECTION('',(0.,1.)); +#82 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#83 = ORIENTED_EDGE('',*,*,#84,.T.); +#84 = EDGE_CURVE('',#57,#85,#87,.T.); +#85 = VERTEX_POINT('',#86); +#86 = CARTESIAN_POINT('',(0.,12.,6.)); +#87 = SURFACE_CURVE('',#88,(#92,#99),.PCURVE_S1.); +#88 = LINE('',#89,#90); +#89 = CARTESIAN_POINT('',(0.,12.,0.)); +#90 = VECTOR('',#91,1.); +#91 = DIRECTION('',(0.,0.,1.)); +#92 = PCURVE('',#32,#93); +#93 = DEFINITIONAL_REPRESENTATION('',(#94),#98); +#94 = LINE('',#95,#96); +#95 = CARTESIAN_POINT('',(0.,-12.)); +#96 = VECTOR('',#97,1.); +#97 = DIRECTION('',(1.,0.)); +#98 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#99 = PCURVE('',#100,#105); +#100 = PLANE('',#101); +#101 = AXIS2_PLACEMENT_3D('',#102,#103,#104); +#102 = CARTESIAN_POINT('',(0.,12.,0.)); +#103 = DIRECTION('',(-0.,1.,0.)); +#104 = DIRECTION('',(0.,0.,1.)); +#105 = DEFINITIONAL_REPRESENTATION('',(#106),#110); +#106 = LINE('',#107,#108); +#107 = CARTESIAN_POINT('',(0.,0.)); +#108 = VECTOR('',#109,1.); +#109 = DIRECTION('',(1.,0.)); +#110 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#111 = ORIENTED_EDGE('',*,*,#112,.F.); +#112 = EDGE_CURVE('',#24,#85,#113,.T.); +#113 = SURFACE_CURVE('',#114,(#118,#125),.PCURVE_S1.); +#114 = LINE('',#115,#116); +#115 = CARTESIAN_POINT('',(0.,0.,6.)); +#116 = VECTOR('',#117,1.); +#117 = DIRECTION('',(-0.,1.,0.)); +#118 = PCURVE('',#32,#119); +#119 = DEFINITIONAL_REPRESENTATION('',(#120),#124); +#120 = LINE('',#121,#122); +#121 = CARTESIAN_POINT('',(6.,0.)); +#122 = VECTOR('',#123,1.); +#123 = DIRECTION('',(0.,-1.)); +#124 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#125 = PCURVE('',#126,#131); +#126 = PLANE('',#127); +#127 = AXIS2_PLACEMENT_3D('',#128,#129,#130); +#128 = CARTESIAN_POINT('',(0.,0.,6.)); +#129 = DIRECTION('',(0.,0.,1.)); +#130 = DIRECTION('',(1.,0.,-0.)); +#131 = DEFINITIONAL_REPRESENTATION('',(#132),#136); +#132 = LINE('',#133,#134); +#133 = CARTESIAN_POINT('',(0.,0.)); +#134 = VECTOR('',#135,1.); +#135 = DIRECTION('',(0.,1.)); +#136 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#137 = ADVANCED_FACE('',(#138),#152,.T.); +#138 = FACE_BOUND('',#139,.T.); +#139 = EDGE_LOOP('',(#140,#170,#193,#216)); +#140 = ORIENTED_EDGE('',*,*,#141,.F.); +#141 = EDGE_CURVE('',#142,#144,#146,.T.); +#142 = VERTEX_POINT('',#143); +#143 = CARTESIAN_POINT('',(20.,0.,0.)); +#144 = VERTEX_POINT('',#145); +#145 = CARTESIAN_POINT('',(20.,0.,6.)); +#146 = SURFACE_CURVE('',#147,(#151,#163),.PCURVE_S1.); +#147 = LINE('',#148,#149); +#148 = CARTESIAN_POINT('',(20.,0.,0.)); +#149 = VECTOR('',#150,1.); +#150 = DIRECTION('',(0.,0.,1.)); +#151 = PCURVE('',#152,#157); +#152 = PLANE('',#153); +#153 = AXIS2_PLACEMENT_3D('',#154,#155,#156); +#154 = CARTESIAN_POINT('',(20.,0.,0.)); +#155 = DIRECTION('',(1.,0.,-0.)); +#156 = DIRECTION('',(0.,0.,1.)); +#157 = DEFINITIONAL_REPRESENTATION('',(#158),#162); +#158 = LINE('',#159,#160); +#159 = CARTESIAN_POINT('',(0.,0.)); +#160 = VECTOR('',#161,1.); +#161 = DIRECTION('',(1.,0.)); +#162 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#163 = PCURVE('',#44,#164); +#164 = DEFINITIONAL_REPRESENTATION('',(#165),#169); +#165 = LINE('',#166,#167); +#166 = CARTESIAN_POINT('',(0.,20.)); +#167 = VECTOR('',#168,1.); +#168 = DIRECTION('',(1.,0.)); +#169 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#170 = ORIENTED_EDGE('',*,*,#171,.T.); +#171 = EDGE_CURVE('',#142,#172,#174,.T.); +#172 = VERTEX_POINT('',#173); +#173 = CARTESIAN_POINT('',(20.,12.,0.)); +#174 = SURFACE_CURVE('',#175,(#179,#186),.PCURVE_S1.); +#175 = LINE('',#176,#177); +#176 = CARTESIAN_POINT('',(20.,0.,0.)); +#177 = VECTOR('',#178,1.); +#178 = DIRECTION('',(-0.,1.,0.)); +#179 = PCURVE('',#152,#180); +#180 = DEFINITIONAL_REPRESENTATION('',(#181),#185); +#181 = LINE('',#182,#183); +#182 = CARTESIAN_POINT('',(0.,0.)); +#183 = VECTOR('',#184,1.); +#184 = DIRECTION('',(0.,-1.)); +#185 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#186 = PCURVE('',#72,#187); +#187 = DEFINITIONAL_REPRESENTATION('',(#188),#192); +#188 = LINE('',#189,#190); +#189 = CARTESIAN_POINT('',(20.,0.)); +#190 = VECTOR('',#191,1.); +#191 = DIRECTION('',(0.,1.)); +#192 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#193 = ORIENTED_EDGE('',*,*,#194,.T.); +#194 = EDGE_CURVE('',#172,#195,#197,.T.); +#195 = VERTEX_POINT('',#196); +#196 = CARTESIAN_POINT('',(20.,12.,6.)); +#197 = SURFACE_CURVE('',#198,(#202,#209),.PCURVE_S1.); +#198 = LINE('',#199,#200); +#199 = CARTESIAN_POINT('',(20.,12.,0.)); +#200 = VECTOR('',#201,1.); +#201 = DIRECTION('',(0.,0.,1.)); +#202 = PCURVE('',#152,#203); +#203 = DEFINITIONAL_REPRESENTATION('',(#204),#208); +#204 = LINE('',#205,#206); +#205 = CARTESIAN_POINT('',(0.,-12.)); +#206 = VECTOR('',#207,1.); +#207 = DIRECTION('',(1.,0.)); +#208 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#209 = PCURVE('',#100,#210); +#210 = DEFINITIONAL_REPRESENTATION('',(#211),#215); +#211 = LINE('',#212,#213); +#212 = CARTESIAN_POINT('',(0.,20.)); +#213 = VECTOR('',#214,1.); +#214 = DIRECTION('',(1.,0.)); +#215 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#216 = ORIENTED_EDGE('',*,*,#217,.F.); +#217 = EDGE_CURVE('',#144,#195,#218,.T.); +#218 = SURFACE_CURVE('',#219,(#223,#230),.PCURVE_S1.); +#219 = LINE('',#220,#221); +#220 = CARTESIAN_POINT('',(20.,0.,6.)); +#221 = VECTOR('',#222,1.); +#222 = DIRECTION('',(-0.,1.,0.)); +#223 = PCURVE('',#152,#224); +#224 = DEFINITIONAL_REPRESENTATION('',(#225),#229); +#225 = LINE('',#226,#227); +#226 = CARTESIAN_POINT('',(6.,0.)); +#227 = VECTOR('',#228,1.); +#228 = DIRECTION('',(0.,-1.)); +#229 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#230 = PCURVE('',#126,#231); +#231 = DEFINITIONAL_REPRESENTATION('',(#232),#236); +#232 = LINE('',#233,#234); +#233 = CARTESIAN_POINT('',(20.,0.)); +#234 = VECTOR('',#235,1.); +#235 = DIRECTION('',(0.,1.)); +#236 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#237 = ADVANCED_FACE('',(#238),#44,.F.); +#238 = FACE_BOUND('',#239,.F.); +#239 = EDGE_LOOP('',(#240,#261,#262,#283)); +#240 = ORIENTED_EDGE('',*,*,#241,.F.); +#241 = EDGE_CURVE('',#22,#142,#242,.T.); +#242 = SURFACE_CURVE('',#243,(#247,#254),.PCURVE_S1.); +#243 = LINE('',#244,#245); +#244 = CARTESIAN_POINT('',(0.,0.,0.)); +#245 = VECTOR('',#246,1.); +#246 = DIRECTION('',(1.,0.,-0.)); +#247 = PCURVE('',#44,#248); +#248 = DEFINITIONAL_REPRESENTATION('',(#249),#253); +#249 = LINE('',#250,#251); +#250 = CARTESIAN_POINT('',(0.,0.)); +#251 = VECTOR('',#252,1.); +#252 = DIRECTION('',(0.,1.)); +#253 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#254 = PCURVE('',#72,#255); +#255 = DEFINITIONAL_REPRESENTATION('',(#256),#260); +#256 = LINE('',#257,#258); +#257 = CARTESIAN_POINT('',(0.,0.)); +#258 = VECTOR('',#259,1.); +#259 = DIRECTION('',(1.,0.)); +#260 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#261 = ORIENTED_EDGE('',*,*,#21,.T.); +#262 = ORIENTED_EDGE('',*,*,#263,.T.); +#263 = EDGE_CURVE('',#24,#144,#264,.T.); +#264 = SURFACE_CURVE('',#265,(#269,#276),.PCURVE_S1.); +#265 = LINE('',#266,#267); +#266 = CARTESIAN_POINT('',(0.,0.,6.)); +#267 = VECTOR('',#268,1.); +#268 = DIRECTION('',(1.,0.,-0.)); +#269 = PCURVE('',#44,#270); +#270 = DEFINITIONAL_REPRESENTATION('',(#271),#275); +#271 = LINE('',#272,#273); +#272 = CARTESIAN_POINT('',(6.,0.)); +#273 = VECTOR('',#274,1.); +#274 = DIRECTION('',(0.,1.)); +#275 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#276 = PCURVE('',#126,#277); +#277 = DEFINITIONAL_REPRESENTATION('',(#278),#282); +#278 = LINE('',#279,#280); +#279 = CARTESIAN_POINT('',(0.,0.)); +#280 = VECTOR('',#281,1.); +#281 = DIRECTION('',(1.,0.)); +#282 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#283 = ORIENTED_EDGE('',*,*,#141,.F.); +#284 = ADVANCED_FACE('',(#285),#100,.T.); +#285 = FACE_BOUND('',#286,.T.); +#286 = EDGE_LOOP('',(#287,#308,#309,#330)); +#287 = ORIENTED_EDGE('',*,*,#288,.F.); +#288 = EDGE_CURVE('',#57,#172,#289,.T.); +#289 = SURFACE_CURVE('',#290,(#294,#301),.PCURVE_S1.); +#290 = LINE('',#291,#292); +#291 = CARTESIAN_POINT('',(0.,12.,0.)); +#292 = VECTOR('',#293,1.); +#293 = DIRECTION('',(1.,0.,-0.)); +#294 = PCURVE('',#100,#295); +#295 = DEFINITIONAL_REPRESENTATION('',(#296),#300); +#296 = LINE('',#297,#298); +#297 = CARTESIAN_POINT('',(0.,0.)); +#298 = VECTOR('',#299,1.); +#299 = DIRECTION('',(0.,1.)); +#300 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#301 = PCURVE('',#72,#302); +#302 = DEFINITIONAL_REPRESENTATION('',(#303),#307); +#303 = LINE('',#304,#305); +#304 = CARTESIAN_POINT('',(0.,12.)); +#305 = VECTOR('',#306,1.); +#306 = DIRECTION('',(1.,0.)); +#307 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#308 = ORIENTED_EDGE('',*,*,#84,.T.); +#309 = ORIENTED_EDGE('',*,*,#310,.T.); +#310 = EDGE_CURVE('',#85,#195,#311,.T.); +#311 = SURFACE_CURVE('',#312,(#316,#323),.PCURVE_S1.); +#312 = LINE('',#313,#314); +#313 = CARTESIAN_POINT('',(0.,12.,6.)); +#314 = VECTOR('',#315,1.); +#315 = DIRECTION('',(1.,0.,-0.)); +#316 = PCURVE('',#100,#317); +#317 = DEFINITIONAL_REPRESENTATION('',(#318),#322); +#318 = LINE('',#319,#320); +#319 = CARTESIAN_POINT('',(6.,0.)); +#320 = VECTOR('',#321,1.); +#321 = DIRECTION('',(0.,1.)); +#322 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#323 = PCURVE('',#126,#324); +#324 = DEFINITIONAL_REPRESENTATION('',(#325),#329); +#325 = LINE('',#326,#327); +#326 = CARTESIAN_POINT('',(0.,12.)); +#327 = VECTOR('',#328,1.); +#328 = DIRECTION('',(1.,0.)); +#329 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#330 = ORIENTED_EDGE('',*,*,#194,.F.); +#331 = ADVANCED_FACE('',(#332),#72,.F.); +#332 = FACE_BOUND('',#333,.F.); +#333 = EDGE_LOOP('',(#334,#335,#336,#337)); +#334 = ORIENTED_EDGE('',*,*,#56,.F.); +#335 = ORIENTED_EDGE('',*,*,#241,.T.); +#336 = ORIENTED_EDGE('',*,*,#171,.T.); +#337 = ORIENTED_EDGE('',*,*,#288,.F.); +#338 = ADVANCED_FACE('',(#339),#126,.T.); +#339 = FACE_BOUND('',#340,.T.); +#340 = EDGE_LOOP('',(#341,#342,#343,#344)); +#341 = ORIENTED_EDGE('',*,*,#112,.F.); +#342 = ORIENTED_EDGE('',*,*,#263,.T.); +#343 = ORIENTED_EDGE('',*,*,#217,.T.); +#344 = ORIENTED_EDGE('',*,*,#310,.F.); +#345 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#349)) GLOBAL_UNIT_ASSIGNED_CONTEXT +((#346,#347,#348)) REPRESENTATION_CONTEXT('Context #1', + '3D Context with UNIT and UNCERTAINTY') ); +#346 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#347 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#348 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#349 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#346, + 'distance_accuracy_value','confusion accuracy'); +#350 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7)); +#351 = SHAPE_DEFINITION_REPRESENTATION(#352,#358); +#352 = PRODUCT_DEFINITION_SHAPE('','',#353); +#353 = PRODUCT_DEFINITION('design','',#354,#357); +#354 = PRODUCT_DEFINITION_FORMATION('','',#355); +#355 = PRODUCT('Gehäuse','Gehäuse','',(#356)); +#356 = PRODUCT_CONTEXT('',#2,'mechanical'); +#357 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#358 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#359),#689); +#359 = MANIFOLD_SOLID_BREP('',#360); +#360 = CLOSED_SHELL('',(#361,#481,#581,#628,#675,#682)); +#361 = ADVANCED_FACE('',(#362),#376,.F.); +#362 = FACE_BOUND('',#363,.F.); +#363 = EDGE_LOOP('',(#364,#399,#427,#455)); +#364 = ORIENTED_EDGE('',*,*,#365,.F.); +#365 = EDGE_CURVE('',#366,#368,#370,.T.); +#366 = VERTEX_POINT('',#367); +#367 = CARTESIAN_POINT('',(25.,0.,0.)); +#368 = VERTEX_POINT('',#369); +#369 = CARTESIAN_POINT('',(25.,0.,6.)); +#370 = SURFACE_CURVE('',#371,(#375,#387),.PCURVE_S1.); +#371 = LINE('',#372,#373); +#372 = CARTESIAN_POINT('',(25.,0.,0.)); +#373 = VECTOR('',#374,1.); +#374 = DIRECTION('',(0.,0.,1.)); +#375 = PCURVE('',#376,#381); +#376 = PLANE('',#377); +#377 = AXIS2_PLACEMENT_3D('',#378,#379,#380); +#378 = CARTESIAN_POINT('',(25.,0.,0.)); +#379 = DIRECTION('',(1.,0.,-0.)); +#380 = DIRECTION('',(0.,0.,1.)); +#381 = DEFINITIONAL_REPRESENTATION('',(#382),#386); +#382 = LINE('',#383,#384); +#383 = CARTESIAN_POINT('',(0.,0.)); +#384 = VECTOR('',#385,1.); +#385 = DIRECTION('',(1.,0.)); +#386 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#387 = PCURVE('',#388,#393); +#388 = PLANE('',#389); +#389 = AXIS2_PLACEMENT_3D('',#390,#391,#392); +#390 = CARTESIAN_POINT('',(25.,0.,0.)); +#391 = DIRECTION('',(-0.,1.,0.)); +#392 = DIRECTION('',(0.,0.,1.)); +#393 = DEFINITIONAL_REPRESENTATION('',(#394),#398); +#394 = LINE('',#395,#396); +#395 = CARTESIAN_POINT('',(0.,0.)); +#396 = VECTOR('',#397,1.); +#397 = DIRECTION('',(1.,0.)); +#398 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#399 = ORIENTED_EDGE('',*,*,#400,.T.); +#400 = EDGE_CURVE('',#366,#401,#403,.T.); +#401 = VERTEX_POINT('',#402); +#402 = CARTESIAN_POINT('',(25.,12.,0.)); +#403 = SURFACE_CURVE('',#404,(#408,#415),.PCURVE_S1.); +#404 = LINE('',#405,#406); +#405 = CARTESIAN_POINT('',(25.,0.,0.)); +#406 = VECTOR('',#407,1.); +#407 = DIRECTION('',(-0.,1.,0.)); +#408 = PCURVE('',#376,#409); +#409 = DEFINITIONAL_REPRESENTATION('',(#410),#414); +#410 = LINE('',#411,#412); +#411 = CARTESIAN_POINT('',(0.,0.)); +#412 = VECTOR('',#413,1.); +#413 = DIRECTION('',(0.,-1.)); +#414 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#415 = PCURVE('',#416,#421); +#416 = PLANE('',#417); +#417 = AXIS2_PLACEMENT_3D('',#418,#419,#420); +#418 = CARTESIAN_POINT('',(25.,0.,0.)); +#419 = DIRECTION('',(0.,0.,1.)); +#420 = DIRECTION('',(1.,0.,-0.)); +#421 = DEFINITIONAL_REPRESENTATION('',(#422),#426); +#422 = LINE('',#423,#424); +#423 = CARTESIAN_POINT('',(0.,0.)); +#424 = VECTOR('',#425,1.); +#425 = DIRECTION('',(0.,1.)); +#426 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#427 = ORIENTED_EDGE('',*,*,#428,.T.); +#428 = EDGE_CURVE('',#401,#429,#431,.T.); +#429 = VERTEX_POINT('',#430); +#430 = CARTESIAN_POINT('',(25.,12.,6.)); +#431 = SURFACE_CURVE('',#432,(#436,#443),.PCURVE_S1.); +#432 = LINE('',#433,#434); +#433 = CARTESIAN_POINT('',(25.,12.,0.)); +#434 = VECTOR('',#435,1.); +#435 = DIRECTION('',(0.,0.,1.)); +#436 = PCURVE('',#376,#437); +#437 = DEFINITIONAL_REPRESENTATION('',(#438),#442); +#438 = LINE('',#439,#440); +#439 = CARTESIAN_POINT('',(0.,-12.)); +#440 = VECTOR('',#441,1.); +#441 = DIRECTION('',(1.,0.)); +#442 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#443 = PCURVE('',#444,#449); +#444 = PLANE('',#445); +#445 = AXIS2_PLACEMENT_3D('',#446,#447,#448); +#446 = CARTESIAN_POINT('',(25.,12.,0.)); +#447 = DIRECTION('',(-0.,1.,0.)); +#448 = DIRECTION('',(0.,0.,1.)); +#449 = DEFINITIONAL_REPRESENTATION('',(#450),#454); +#450 = LINE('',#451,#452); +#451 = CARTESIAN_POINT('',(0.,0.)); +#452 = VECTOR('',#453,1.); +#453 = DIRECTION('',(1.,0.)); +#454 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#455 = ORIENTED_EDGE('',*,*,#456,.F.); +#456 = EDGE_CURVE('',#368,#429,#457,.T.); +#457 = SURFACE_CURVE('',#458,(#462,#469),.PCURVE_S1.); +#458 = LINE('',#459,#460); +#459 = CARTESIAN_POINT('',(25.,0.,6.)); +#460 = VECTOR('',#461,1.); +#461 = DIRECTION('',(-0.,1.,0.)); +#462 = PCURVE('',#376,#463); +#463 = DEFINITIONAL_REPRESENTATION('',(#464),#468); +#464 = LINE('',#465,#466); +#465 = CARTESIAN_POINT('',(6.,0.)); +#466 = VECTOR('',#467,1.); +#467 = DIRECTION('',(0.,-1.)); +#468 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#469 = PCURVE('',#470,#475); +#470 = PLANE('',#471); +#471 = AXIS2_PLACEMENT_3D('',#472,#473,#474); +#472 = CARTESIAN_POINT('',(25.,0.,6.)); +#473 = DIRECTION('',(0.,0.,1.)); +#474 = DIRECTION('',(1.,0.,-0.)); +#475 = DEFINITIONAL_REPRESENTATION('',(#476),#480); +#476 = LINE('',#477,#478); +#477 = CARTESIAN_POINT('',(0.,0.)); +#478 = VECTOR('',#479,1.); +#479 = DIRECTION('',(0.,1.)); +#480 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#481 = ADVANCED_FACE('',(#482),#496,.T.); +#482 = FACE_BOUND('',#483,.T.); +#483 = EDGE_LOOP('',(#484,#514,#537,#560)); +#484 = ORIENTED_EDGE('',*,*,#485,.F.); +#485 = EDGE_CURVE('',#486,#488,#490,.T.); +#486 = VERTEX_POINT('',#487); +#487 = CARTESIAN_POINT('',(41.,0.,0.)); +#488 = VERTEX_POINT('',#489); +#489 = CARTESIAN_POINT('',(41.,0.,6.)); +#490 = SURFACE_CURVE('',#491,(#495,#507),.PCURVE_S1.); +#491 = LINE('',#492,#493); +#492 = CARTESIAN_POINT('',(41.,0.,0.)); +#493 = VECTOR('',#494,1.); +#494 = DIRECTION('',(0.,0.,1.)); +#495 = PCURVE('',#496,#501); +#496 = PLANE('',#497); +#497 = AXIS2_PLACEMENT_3D('',#498,#499,#500); +#498 = CARTESIAN_POINT('',(41.,0.,0.)); +#499 = DIRECTION('',(1.,0.,-0.)); +#500 = DIRECTION('',(0.,0.,1.)); +#501 = DEFINITIONAL_REPRESENTATION('',(#502),#506); +#502 = LINE('',#503,#504); +#503 = CARTESIAN_POINT('',(0.,0.)); +#504 = VECTOR('',#505,1.); +#505 = DIRECTION('',(1.,0.)); +#506 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#507 = PCURVE('',#388,#508); +#508 = DEFINITIONAL_REPRESENTATION('',(#509),#513); +#509 = LINE('',#510,#511); +#510 = CARTESIAN_POINT('',(0.,16.)); +#511 = VECTOR('',#512,1.); +#512 = DIRECTION('',(1.,0.)); +#513 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#514 = ORIENTED_EDGE('',*,*,#515,.T.); +#515 = EDGE_CURVE('',#486,#516,#518,.T.); +#516 = VERTEX_POINT('',#517); +#517 = CARTESIAN_POINT('',(41.,12.,0.)); +#518 = SURFACE_CURVE('',#519,(#523,#530),.PCURVE_S1.); +#519 = LINE('',#520,#521); +#520 = CARTESIAN_POINT('',(41.,0.,0.)); +#521 = VECTOR('',#522,1.); +#522 = DIRECTION('',(-0.,1.,0.)); +#523 = PCURVE('',#496,#524); +#524 = DEFINITIONAL_REPRESENTATION('',(#525),#529); +#525 = LINE('',#526,#527); +#526 = CARTESIAN_POINT('',(0.,0.)); +#527 = VECTOR('',#528,1.); +#528 = DIRECTION('',(0.,-1.)); +#529 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#530 = PCURVE('',#416,#531); +#531 = DEFINITIONAL_REPRESENTATION('',(#532),#536); +#532 = LINE('',#533,#534); +#533 = CARTESIAN_POINT('',(16.,0.)); +#534 = VECTOR('',#535,1.); +#535 = DIRECTION('',(0.,1.)); +#536 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#537 = ORIENTED_EDGE('',*,*,#538,.T.); +#538 = EDGE_CURVE('',#516,#539,#541,.T.); +#539 = VERTEX_POINT('',#540); +#540 = CARTESIAN_POINT('',(41.,12.,6.)); +#541 = SURFACE_CURVE('',#542,(#546,#553),.PCURVE_S1.); +#542 = LINE('',#543,#544); +#543 = CARTESIAN_POINT('',(41.,12.,0.)); +#544 = VECTOR('',#545,1.); +#545 = DIRECTION('',(0.,0.,1.)); +#546 = PCURVE('',#496,#547); +#547 = DEFINITIONAL_REPRESENTATION('',(#548),#552); +#548 = LINE('',#549,#550); +#549 = CARTESIAN_POINT('',(0.,-12.)); +#550 = VECTOR('',#551,1.); +#551 = DIRECTION('',(1.,0.)); +#552 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#553 = PCURVE('',#444,#554); +#554 = DEFINITIONAL_REPRESENTATION('',(#555),#559); +#555 = LINE('',#556,#557); +#556 = CARTESIAN_POINT('',(0.,16.)); +#557 = VECTOR('',#558,1.); +#558 = DIRECTION('',(1.,0.)); +#559 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#560 = ORIENTED_EDGE('',*,*,#561,.F.); +#561 = EDGE_CURVE('',#488,#539,#562,.T.); +#562 = SURFACE_CURVE('',#563,(#567,#574),.PCURVE_S1.); +#563 = LINE('',#564,#565); +#564 = CARTESIAN_POINT('',(41.,0.,6.)); +#565 = VECTOR('',#566,1.); +#566 = DIRECTION('',(-0.,1.,0.)); +#567 = PCURVE('',#496,#568); +#568 = DEFINITIONAL_REPRESENTATION('',(#569),#573); +#569 = LINE('',#570,#571); +#570 = CARTESIAN_POINT('',(6.,0.)); +#571 = VECTOR('',#572,1.); +#572 = DIRECTION('',(0.,-1.)); +#573 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#574 = PCURVE('',#470,#575); +#575 = DEFINITIONAL_REPRESENTATION('',(#576),#580); +#576 = LINE('',#577,#578); +#577 = CARTESIAN_POINT('',(16.,0.)); +#578 = VECTOR('',#579,1.); +#579 = DIRECTION('',(0.,1.)); +#580 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#581 = ADVANCED_FACE('',(#582),#388,.F.); +#582 = FACE_BOUND('',#583,.F.); +#583 = EDGE_LOOP('',(#584,#605,#606,#627)); +#584 = ORIENTED_EDGE('',*,*,#585,.F.); +#585 = EDGE_CURVE('',#366,#486,#586,.T.); +#586 = SURFACE_CURVE('',#587,(#591,#598),.PCURVE_S1.); +#587 = LINE('',#588,#589); +#588 = CARTESIAN_POINT('',(25.,0.,0.)); +#589 = VECTOR('',#590,1.); +#590 = DIRECTION('',(1.,0.,-0.)); +#591 = PCURVE('',#388,#592); +#592 = DEFINITIONAL_REPRESENTATION('',(#593),#597); +#593 = LINE('',#594,#595); +#594 = CARTESIAN_POINT('',(0.,0.)); +#595 = VECTOR('',#596,1.); +#596 = DIRECTION('',(0.,1.)); +#597 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#598 = PCURVE('',#416,#599); +#599 = DEFINITIONAL_REPRESENTATION('',(#600),#604); +#600 = LINE('',#601,#602); +#601 = CARTESIAN_POINT('',(0.,0.)); +#602 = VECTOR('',#603,1.); +#603 = DIRECTION('',(1.,0.)); +#604 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#605 = ORIENTED_EDGE('',*,*,#365,.T.); +#606 = ORIENTED_EDGE('',*,*,#607,.T.); +#607 = EDGE_CURVE('',#368,#488,#608,.T.); +#608 = SURFACE_CURVE('',#609,(#613,#620),.PCURVE_S1.); +#609 = LINE('',#610,#611); +#610 = CARTESIAN_POINT('',(25.,0.,6.)); +#611 = VECTOR('',#612,1.); +#612 = DIRECTION('',(1.,0.,-0.)); +#613 = PCURVE('',#388,#614); +#614 = DEFINITIONAL_REPRESENTATION('',(#615),#619); +#615 = LINE('',#616,#617); +#616 = CARTESIAN_POINT('',(6.,0.)); +#617 = VECTOR('',#618,1.); +#618 = DIRECTION('',(0.,1.)); +#619 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#620 = PCURVE('',#470,#621); +#621 = DEFINITIONAL_REPRESENTATION('',(#622),#626); +#622 = LINE('',#623,#624); +#623 = CARTESIAN_POINT('',(0.,0.)); +#624 = VECTOR('',#625,1.); +#625 = DIRECTION('',(1.,0.)); +#626 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#627 = ORIENTED_EDGE('',*,*,#485,.F.); +#628 = ADVANCED_FACE('',(#629),#444,.T.); +#629 = FACE_BOUND('',#630,.T.); +#630 = EDGE_LOOP('',(#631,#652,#653,#674)); +#631 = ORIENTED_EDGE('',*,*,#632,.F.); +#632 = EDGE_CURVE('',#401,#516,#633,.T.); +#633 = SURFACE_CURVE('',#634,(#638,#645),.PCURVE_S1.); +#634 = LINE('',#635,#636); +#635 = CARTESIAN_POINT('',(25.,12.,0.)); +#636 = VECTOR('',#637,1.); +#637 = DIRECTION('',(1.,0.,-0.)); +#638 = PCURVE('',#444,#639); +#639 = DEFINITIONAL_REPRESENTATION('',(#640),#644); +#640 = LINE('',#641,#642); +#641 = CARTESIAN_POINT('',(0.,0.)); +#642 = VECTOR('',#643,1.); +#643 = DIRECTION('',(0.,1.)); +#644 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#645 = PCURVE('',#416,#646); +#646 = DEFINITIONAL_REPRESENTATION('',(#647),#651); +#647 = LINE('',#648,#649); +#648 = CARTESIAN_POINT('',(0.,12.)); +#649 = VECTOR('',#650,1.); +#650 = DIRECTION('',(1.,0.)); +#651 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#652 = ORIENTED_EDGE('',*,*,#428,.T.); +#653 = ORIENTED_EDGE('',*,*,#654,.T.); +#654 = EDGE_CURVE('',#429,#539,#655,.T.); +#655 = SURFACE_CURVE('',#656,(#660,#667),.PCURVE_S1.); +#656 = LINE('',#657,#658); +#657 = CARTESIAN_POINT('',(25.,12.,6.)); +#658 = VECTOR('',#659,1.); +#659 = DIRECTION('',(1.,0.,-0.)); +#660 = PCURVE('',#444,#661); +#661 = DEFINITIONAL_REPRESENTATION('',(#662),#666); +#662 = LINE('',#663,#664); +#663 = CARTESIAN_POINT('',(6.,0.)); +#664 = VECTOR('',#665,1.); +#665 = DIRECTION('',(0.,1.)); +#666 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#667 = PCURVE('',#470,#668); +#668 = DEFINITIONAL_REPRESENTATION('',(#669),#673); +#669 = LINE('',#670,#671); +#670 = CARTESIAN_POINT('',(0.,12.)); +#671 = VECTOR('',#672,1.); +#672 = DIRECTION('',(1.,0.)); +#673 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#674 = ORIENTED_EDGE('',*,*,#538,.F.); +#675 = ADVANCED_FACE('',(#676),#416,.F.); +#676 = FACE_BOUND('',#677,.F.); +#677 = EDGE_LOOP('',(#678,#679,#680,#681)); +#678 = ORIENTED_EDGE('',*,*,#400,.F.); +#679 = ORIENTED_EDGE('',*,*,#585,.T.); +#680 = ORIENTED_EDGE('',*,*,#515,.T.); +#681 = ORIENTED_EDGE('',*,*,#632,.F.); +#682 = ADVANCED_FACE('',(#683),#470,.T.); +#683 = FACE_BOUND('',#684,.T.); +#684 = EDGE_LOOP('',(#685,#686,#687,#688)); +#685 = ORIENTED_EDGE('',*,*,#456,.F.); +#686 = ORIENTED_EDGE('',*,*,#607,.T.); +#687 = ORIENTED_EDGE('',*,*,#561,.T.); +#688 = ORIENTED_EDGE('',*,*,#654,.F.); +#689 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#693)) GLOBAL_UNIT_ASSIGNED_CONTEXT +((#690,#691,#692)) REPRESENTATION_CONTEXT('Context #1', + '3D Context with UNIT and UNCERTAINTY') ); +#690 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#691 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#692 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#693 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#690, + 'distance_accuracy_value','confusion accuracy'); +#694 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#355)); +#695 = SHAPE_DEFINITION_REPRESENTATION(#696,#702); +#696 = PRODUCT_DEFINITION_SHAPE('','',#697); +#697 = PRODUCT_DEFINITION('design','',#698,#701); +#698 = PRODUCT_DEFINITION_FORMATION('','',#699); +#699 = PRODUCT('bracket','bracket','',(#700)); +#700 = PRODUCT_CONTEXT('',#2,'mechanical'); +#701 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#702 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#703),#1033); +#703 = MANIFOLD_SOLID_BREP('',#704); +#704 = CLOSED_SHELL('',(#705,#825,#925,#972,#1019,#1026)); +#705 = ADVANCED_FACE('',(#706),#720,.F.); +#706 = FACE_BOUND('',#707,.F.); +#707 = EDGE_LOOP('',(#708,#743,#771,#799)); +#708 = ORIENTED_EDGE('',*,*,#709,.F.); +#709 = EDGE_CURVE('',#710,#712,#714,.T.); +#710 = VERTEX_POINT('',#711); +#711 = CARTESIAN_POINT('',(45.,0.,0.)); +#712 = VERTEX_POINT('',#713); +#713 = CARTESIAN_POINT('',(45.,0.,6.)); +#714 = SURFACE_CURVE('',#715,(#719,#731),.PCURVE_S1.); +#715 = LINE('',#716,#717); +#716 = CARTESIAN_POINT('',(45.,0.,0.)); +#717 = VECTOR('',#718,1.); +#718 = DIRECTION('',(0.,0.,1.)); +#719 = PCURVE('',#720,#725); +#720 = PLANE('',#721); +#721 = AXIS2_PLACEMENT_3D('',#722,#723,#724); +#722 = CARTESIAN_POINT('',(45.,0.,0.)); +#723 = DIRECTION('',(1.,0.,-0.)); +#724 = DIRECTION('',(0.,0.,1.)); +#725 = DEFINITIONAL_REPRESENTATION('',(#726),#730); +#726 = LINE('',#727,#728); +#727 = CARTESIAN_POINT('',(0.,0.)); +#728 = VECTOR('',#729,1.); +#729 = DIRECTION('',(1.,0.)); +#730 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#731 = PCURVE('',#732,#737); +#732 = PLANE('',#733); +#733 = AXIS2_PLACEMENT_3D('',#734,#735,#736); +#734 = CARTESIAN_POINT('',(45.,0.,0.)); +#735 = DIRECTION('',(-0.,1.,0.)); +#736 = DIRECTION('',(0.,0.,1.)); +#737 = DEFINITIONAL_REPRESENTATION('',(#738),#742); +#738 = LINE('',#739,#740); +#739 = CARTESIAN_POINT('',(0.,0.)); +#740 = VECTOR('',#741,1.); +#741 = DIRECTION('',(1.,0.)); +#742 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#743 = ORIENTED_EDGE('',*,*,#744,.T.); +#744 = EDGE_CURVE('',#710,#745,#747,.T.); +#745 = VERTEX_POINT('',#746); +#746 = CARTESIAN_POINT('',(45.,12.,0.)); +#747 = SURFACE_CURVE('',#748,(#752,#759),.PCURVE_S1.); +#748 = LINE('',#749,#750); +#749 = CARTESIAN_POINT('',(45.,0.,0.)); +#750 = VECTOR('',#751,1.); +#751 = DIRECTION('',(-0.,1.,0.)); +#752 = PCURVE('',#720,#753); +#753 = DEFINITIONAL_REPRESENTATION('',(#754),#758); +#754 = LINE('',#755,#756); +#755 = CARTESIAN_POINT('',(0.,0.)); +#756 = VECTOR('',#757,1.); +#757 = DIRECTION('',(0.,-1.)); +#758 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#759 = PCURVE('',#760,#765); +#760 = PLANE('',#761); +#761 = AXIS2_PLACEMENT_3D('',#762,#763,#764); +#762 = CARTESIAN_POINT('',(45.,0.,0.)); +#763 = DIRECTION('',(0.,0.,1.)); +#764 = DIRECTION('',(1.,0.,-0.)); +#765 = DEFINITIONAL_REPRESENTATION('',(#766),#770); +#766 = LINE('',#767,#768); +#767 = CARTESIAN_POINT('',(0.,0.)); +#768 = VECTOR('',#769,1.); +#769 = DIRECTION('',(0.,1.)); +#770 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#771 = ORIENTED_EDGE('',*,*,#772,.T.); +#772 = EDGE_CURVE('',#745,#773,#775,.T.); +#773 = VERTEX_POINT('',#774); +#774 = CARTESIAN_POINT('',(45.,12.,6.)); +#775 = SURFACE_CURVE('',#776,(#780,#787),.PCURVE_S1.); +#776 = LINE('',#777,#778); +#777 = CARTESIAN_POINT('',(45.,12.,0.)); +#778 = VECTOR('',#779,1.); +#779 = DIRECTION('',(0.,0.,1.)); +#780 = PCURVE('',#720,#781); +#781 = DEFINITIONAL_REPRESENTATION('',(#782),#786); +#782 = LINE('',#783,#784); +#783 = CARTESIAN_POINT('',(0.,-12.)); +#784 = VECTOR('',#785,1.); +#785 = DIRECTION('',(1.,0.)); +#786 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#787 = PCURVE('',#788,#793); +#788 = PLANE('',#789); +#789 = AXIS2_PLACEMENT_3D('',#790,#791,#792); +#790 = CARTESIAN_POINT('',(45.,12.,0.)); +#791 = DIRECTION('',(-0.,1.,0.)); +#792 = DIRECTION('',(0.,0.,1.)); +#793 = DEFINITIONAL_REPRESENTATION('',(#794),#798); +#794 = LINE('',#795,#796); +#795 = CARTESIAN_POINT('',(0.,0.)); +#796 = VECTOR('',#797,1.); +#797 = DIRECTION('',(1.,0.)); +#798 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#799 = ORIENTED_EDGE('',*,*,#800,.F.); +#800 = EDGE_CURVE('',#712,#773,#801,.T.); +#801 = SURFACE_CURVE('',#802,(#806,#813),.PCURVE_S1.); +#802 = LINE('',#803,#804); +#803 = CARTESIAN_POINT('',(45.,0.,6.)); +#804 = VECTOR('',#805,1.); +#805 = DIRECTION('',(-0.,1.,0.)); +#806 = PCURVE('',#720,#807); +#807 = DEFINITIONAL_REPRESENTATION('',(#808),#812); +#808 = LINE('',#809,#810); +#809 = CARTESIAN_POINT('',(6.,0.)); +#810 = VECTOR('',#811,1.); +#811 = DIRECTION('',(0.,-1.)); +#812 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#813 = PCURVE('',#814,#819); +#814 = PLANE('',#815); +#815 = AXIS2_PLACEMENT_3D('',#816,#817,#818); +#816 = CARTESIAN_POINT('',(45.,0.,6.)); +#817 = DIRECTION('',(0.,0.,1.)); +#818 = DIRECTION('',(1.,0.,-0.)); +#819 = DEFINITIONAL_REPRESENTATION('',(#820),#824); +#820 = LINE('',#821,#822); +#821 = CARTESIAN_POINT('',(0.,0.)); +#822 = VECTOR('',#823,1.); +#823 = DIRECTION('',(0.,1.)); +#824 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#825 = ADVANCED_FACE('',(#826),#840,.T.); +#826 = FACE_BOUND('',#827,.T.); +#827 = EDGE_LOOP('',(#828,#858,#881,#904)); +#828 = ORIENTED_EDGE('',*,*,#829,.F.); +#829 = EDGE_CURVE('',#830,#832,#834,.T.); +#830 = VERTEX_POINT('',#831); +#831 = CARTESIAN_POINT('',(57.,0.,0.)); +#832 = VERTEX_POINT('',#833); +#833 = CARTESIAN_POINT('',(57.,0.,6.)); +#834 = SURFACE_CURVE('',#835,(#839,#851),.PCURVE_S1.); +#835 = LINE('',#836,#837); +#836 = CARTESIAN_POINT('',(57.,0.,0.)); +#837 = VECTOR('',#838,1.); +#838 = DIRECTION('',(0.,0.,1.)); +#839 = PCURVE('',#840,#845); +#840 = PLANE('',#841); +#841 = AXIS2_PLACEMENT_3D('',#842,#843,#844); +#842 = CARTESIAN_POINT('',(57.,0.,0.)); +#843 = DIRECTION('',(1.,0.,-0.)); +#844 = DIRECTION('',(0.,0.,1.)); +#845 = DEFINITIONAL_REPRESENTATION('',(#846),#850); +#846 = LINE('',#847,#848); +#847 = CARTESIAN_POINT('',(0.,0.)); +#848 = VECTOR('',#849,1.); +#849 = DIRECTION('',(1.,0.)); +#850 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#851 = PCURVE('',#732,#852); +#852 = DEFINITIONAL_REPRESENTATION('',(#853),#857); +#853 = LINE('',#854,#855); +#854 = CARTESIAN_POINT('',(0.,12.)); +#855 = VECTOR('',#856,1.); +#856 = DIRECTION('',(1.,0.)); +#857 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#858 = ORIENTED_EDGE('',*,*,#859,.T.); +#859 = EDGE_CURVE('',#830,#860,#862,.T.); +#860 = VERTEX_POINT('',#861); +#861 = CARTESIAN_POINT('',(57.,12.,0.)); +#862 = SURFACE_CURVE('',#863,(#867,#874),.PCURVE_S1.); +#863 = LINE('',#864,#865); +#864 = CARTESIAN_POINT('',(57.,0.,0.)); +#865 = VECTOR('',#866,1.); +#866 = DIRECTION('',(-0.,1.,0.)); +#867 = PCURVE('',#840,#868); +#868 = DEFINITIONAL_REPRESENTATION('',(#869),#873); +#869 = LINE('',#870,#871); +#870 = CARTESIAN_POINT('',(0.,0.)); +#871 = VECTOR('',#872,1.); +#872 = DIRECTION('',(0.,-1.)); +#873 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#874 = PCURVE('',#760,#875); +#875 = DEFINITIONAL_REPRESENTATION('',(#876),#880); +#876 = LINE('',#877,#878); +#877 = CARTESIAN_POINT('',(12.,0.)); +#878 = VECTOR('',#879,1.); +#879 = DIRECTION('',(0.,1.)); +#880 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#881 = ORIENTED_EDGE('',*,*,#882,.T.); +#882 = EDGE_CURVE('',#860,#883,#885,.T.); +#883 = VERTEX_POINT('',#884); +#884 = CARTESIAN_POINT('',(57.,12.,6.)); +#885 = SURFACE_CURVE('',#886,(#890,#897),.PCURVE_S1.); +#886 = LINE('',#887,#888); +#887 = CARTESIAN_POINT('',(57.,12.,0.)); +#888 = VECTOR('',#889,1.); +#889 = DIRECTION('',(0.,0.,1.)); +#890 = PCURVE('',#840,#891); +#891 = DEFINITIONAL_REPRESENTATION('',(#892),#896); +#892 = LINE('',#893,#894); +#893 = CARTESIAN_POINT('',(0.,-12.)); +#894 = VECTOR('',#895,1.); +#895 = DIRECTION('',(1.,0.)); +#896 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#897 = PCURVE('',#788,#898); +#898 = DEFINITIONAL_REPRESENTATION('',(#899),#903); +#899 = LINE('',#900,#901); +#900 = CARTESIAN_POINT('',(0.,12.)); +#901 = VECTOR('',#902,1.); +#902 = DIRECTION('',(1.,0.)); +#903 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#904 = ORIENTED_EDGE('',*,*,#905,.F.); +#905 = EDGE_CURVE('',#832,#883,#906,.T.); +#906 = SURFACE_CURVE('',#907,(#911,#918),.PCURVE_S1.); +#907 = LINE('',#908,#909); +#908 = CARTESIAN_POINT('',(57.,0.,6.)); +#909 = VECTOR('',#910,1.); +#910 = DIRECTION('',(-0.,1.,0.)); +#911 = PCURVE('',#840,#912); +#912 = DEFINITIONAL_REPRESENTATION('',(#913),#917); +#913 = LINE('',#914,#915); +#914 = CARTESIAN_POINT('',(6.,0.)); +#915 = VECTOR('',#916,1.); +#916 = DIRECTION('',(0.,-1.)); +#917 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#918 = PCURVE('',#814,#919); +#919 = DEFINITIONAL_REPRESENTATION('',(#920),#924); +#920 = LINE('',#921,#922); +#921 = CARTESIAN_POINT('',(12.,0.)); +#922 = VECTOR('',#923,1.); +#923 = DIRECTION('',(0.,1.)); +#924 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#925 = ADVANCED_FACE('',(#926),#732,.F.); +#926 = FACE_BOUND('',#927,.F.); +#927 = EDGE_LOOP('',(#928,#949,#950,#971)); +#928 = ORIENTED_EDGE('',*,*,#929,.F.); +#929 = EDGE_CURVE('',#710,#830,#930,.T.); +#930 = SURFACE_CURVE('',#931,(#935,#942),.PCURVE_S1.); +#931 = LINE('',#932,#933); +#932 = CARTESIAN_POINT('',(45.,0.,0.)); +#933 = VECTOR('',#934,1.); +#934 = DIRECTION('',(1.,0.,-0.)); +#935 = PCURVE('',#732,#936); +#936 = DEFINITIONAL_REPRESENTATION('',(#937),#941); +#937 = LINE('',#938,#939); +#938 = CARTESIAN_POINT('',(0.,0.)); +#939 = VECTOR('',#940,1.); +#940 = DIRECTION('',(0.,1.)); +#941 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#942 = PCURVE('',#760,#943); +#943 = DEFINITIONAL_REPRESENTATION('',(#944),#948); +#944 = LINE('',#945,#946); +#945 = CARTESIAN_POINT('',(0.,0.)); +#946 = VECTOR('',#947,1.); +#947 = DIRECTION('',(1.,0.)); +#948 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#949 = ORIENTED_EDGE('',*,*,#709,.T.); +#950 = ORIENTED_EDGE('',*,*,#951,.T.); +#951 = EDGE_CURVE('',#712,#832,#952,.T.); +#952 = SURFACE_CURVE('',#953,(#957,#964),.PCURVE_S1.); +#953 = LINE('',#954,#955); +#954 = CARTESIAN_POINT('',(45.,0.,6.)); +#955 = VECTOR('',#956,1.); +#956 = DIRECTION('',(1.,0.,-0.)); +#957 = PCURVE('',#732,#958); +#958 = DEFINITIONAL_REPRESENTATION('',(#959),#963); +#959 = LINE('',#960,#961); +#960 = CARTESIAN_POINT('',(6.,0.)); +#961 = VECTOR('',#962,1.); +#962 = DIRECTION('',(0.,1.)); +#963 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#964 = PCURVE('',#814,#965); +#965 = DEFINITIONAL_REPRESENTATION('',(#966),#970); +#966 = LINE('',#967,#968); +#967 = CARTESIAN_POINT('',(0.,0.)); +#968 = VECTOR('',#969,1.); +#969 = DIRECTION('',(1.,0.)); +#970 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#971 = ORIENTED_EDGE('',*,*,#829,.F.); +#972 = ADVANCED_FACE('',(#973),#788,.T.); +#973 = FACE_BOUND('',#974,.T.); +#974 = EDGE_LOOP('',(#975,#996,#997,#1018)); +#975 = ORIENTED_EDGE('',*,*,#976,.F.); +#976 = EDGE_CURVE('',#745,#860,#977,.T.); +#977 = SURFACE_CURVE('',#978,(#982,#989),.PCURVE_S1.); +#978 = LINE('',#979,#980); +#979 = CARTESIAN_POINT('',(45.,12.,0.)); +#980 = VECTOR('',#981,1.); +#981 = DIRECTION('',(1.,0.,-0.)); +#982 = PCURVE('',#788,#983); +#983 = DEFINITIONAL_REPRESENTATION('',(#984),#988); +#984 = LINE('',#985,#986); +#985 = CARTESIAN_POINT('',(0.,0.)); +#986 = VECTOR('',#987,1.); +#987 = DIRECTION('',(0.,1.)); +#988 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#989 = PCURVE('',#760,#990); +#990 = DEFINITIONAL_REPRESENTATION('',(#991),#995); +#991 = LINE('',#992,#993); +#992 = CARTESIAN_POINT('',(0.,12.)); +#993 = VECTOR('',#994,1.); +#994 = DIRECTION('',(1.,0.)); +#995 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#996 = ORIENTED_EDGE('',*,*,#772,.T.); +#997 = ORIENTED_EDGE('',*,*,#998,.T.); +#998 = EDGE_CURVE('',#773,#883,#999,.T.); +#999 = SURFACE_CURVE('',#1000,(#1004,#1011),.PCURVE_S1.); +#1000 = LINE('',#1001,#1002); +#1001 = CARTESIAN_POINT('',(45.,12.,6.)); +#1002 = VECTOR('',#1003,1.); +#1003 = DIRECTION('',(1.,0.,-0.)); +#1004 = PCURVE('',#788,#1005); +#1005 = DEFINITIONAL_REPRESENTATION('',(#1006),#1010); +#1006 = LINE('',#1007,#1008); +#1007 = CARTESIAN_POINT('',(6.,0.)); +#1008 = VECTOR('',#1009,1.); +#1009 = DIRECTION('',(0.,1.)); +#1010 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1011 = PCURVE('',#814,#1012); +#1012 = DEFINITIONAL_REPRESENTATION('',(#1013),#1017); +#1013 = LINE('',#1014,#1015); +#1014 = CARTESIAN_POINT('',(0.,12.)); +#1015 = VECTOR('',#1016,1.); +#1016 = DIRECTION('',(1.,0.)); +#1017 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1018 = ORIENTED_EDGE('',*,*,#882,.F.); +#1019 = ADVANCED_FACE('',(#1020),#760,.F.); +#1020 = FACE_BOUND('',#1021,.F.); +#1021 = EDGE_LOOP('',(#1022,#1023,#1024,#1025)); +#1022 = ORIENTED_EDGE('',*,*,#744,.F.); +#1023 = ORIENTED_EDGE('',*,*,#929,.T.); +#1024 = ORIENTED_EDGE('',*,*,#859,.T.); +#1025 = ORIENTED_EDGE('',*,*,#976,.F.); +#1026 = ADVANCED_FACE('',(#1027),#814,.T.); +#1027 = FACE_BOUND('',#1028,.T.); +#1028 = EDGE_LOOP('',(#1029,#1030,#1031,#1032)); +#1029 = ORIENTED_EDGE('',*,*,#800,.F.); +#1030 = ORIENTED_EDGE('',*,*,#951,.T.); +#1031 = ORIENTED_EDGE('',*,*,#905,.T.); +#1032 = ORIENTED_EDGE('',*,*,#998,.F.); +#1033 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#1037)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#1034,#1035,#1036)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#1034 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#1035 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#1036 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#1037 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#1034, + 'distance_accuracy_value','confusion accuracy'); +#1038 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#699)); +ENDSEC; +END-ISO-10303-21; diff --git a/tests/data/wipe_tower_temperature_trace_main.txt b/tests/data/wipe_tower_temperature_trace_main.txt new file mode 100644 index 0000000000..f755008c26 --- /dev/null +++ b/tests/data/wipe_tower_temperature_trace_main.txt @@ -0,0 +1,172 @@ +# Temperature and tool-change commands of a wait_for_temp_on_wipe_tower-off slice, +# captured from the main branch at a10d9e77cf. Regeneration is described +# at the test that reads this file: "Toolchange temperature commands are unchanged +# when the wipe tower wait is off" in tests/fff_print/test_multifilament.cpp. +# +# The "time:" and "lead" values are toolchain-specific -- GCC, Clang and MSVC each produce +# slightly different estimates from an identical toolpath -- so they are compared with a +# tolerance, not exactly. Do not regenerate this file to resolve a mismatch in them: no single +# capture satisfies all three, and recapturing just moves the failure to other platforms. +M104 S215 T0 ; set nozzle temperature +M104 S215 T1 ; set nozzle temperature +; CP PRIMING START +T1 ; change extruder +M109 S215 T1 ; set nozzle temperature and wait for it to be reached +M104 S175 T1 ; set nozzle temperature ;cooldown +T0 ; change extruder +M109 S215 T0 ; set nozzle temperature and wait for it to be reached +; CP PRIMING END +M104 S215 T1 ; preheat T1 time: 30s lead 30.2s +; CP TOOLCHANGE START +M104 S175 T0 ; set nozzle temperature ;cooldown +T1 ; change extruder +M109 S215 T1 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T1 ; set nozzle temperature +M104 S240 T0 ; preheat T0 time: 30s lead 30.0s +; CP TOOLCHANGE START +M104 S200 T1 ; set nozzle temperature ;cooldown +T0 ; change extruder +M109 S240 T0 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T1 ; preheat T1 time: 30s lead 30.4s +; CP TOOLCHANGE START +M104 S200 T0 ; set nozzle temperature ;cooldown +T1 ; change extruder +M109 S240 T1 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T0 ; preheat T0 time: 31s lead 30.9s +; CP TOOLCHANGE START +M104 S200 T1 ; set nozzle temperature ;cooldown +T0 ; change extruder +M109 S240 T0 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T1 ; preheat T1 time: 30s lead 30.2s +; CP TOOLCHANGE START +M104 S200 T0 ; set nozzle temperature ;cooldown +T1 ; change extruder +M109 S240 T1 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T0 ; preheat T0 time: 30s lead 30.2s +; CP TOOLCHANGE START +M104 S200 T1 ; set nozzle temperature ;cooldown +T0 ; change extruder +M109 S240 T0 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T1 ; preheat T1 time: 30s lead 30.2s +; CP TOOLCHANGE START +M104 S200 T0 ; set nozzle temperature ;cooldown +T1 ; change extruder +M109 S240 T1 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T0 ; preheat T0 time: 30s lead 30.2s +; CP TOOLCHANGE START +M104 S200 T1 ; set nozzle temperature ;cooldown +T0 ; change extruder +M109 S240 T0 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T1 ; preheat T1 time: 30s lead 30.2s +; CP TOOLCHANGE START +M104 S200 T0 ; set nozzle temperature ;cooldown +T1 ; change extruder +M109 S240 T1 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T0 ; preheat T0 time: 30s lead 30.2s +; CP TOOLCHANGE START +M104 S200 T1 ; set nozzle temperature ;cooldown +T0 ; change extruder +M109 S240 T0 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T1 ; preheat T1 time: 31s lead 30.7s +; CP TOOLCHANGE START +M104 S200 T0 ; set nozzle temperature ;cooldown +T1 ; change extruder +M109 S240 T1 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T0 ; preheat T0 time: 31s lead 30.6s +; CP TOOLCHANGE START +M104 S200 T1 ; set nozzle temperature ;cooldown +T0 ; change extruder +M109 S240 T0 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T1 ; preheat T1 time: 30s lead 30.3s +; CP TOOLCHANGE START +M104 S200 T0 ; set nozzle temperature ;cooldown +T1 ; change extruder +M109 S240 T1 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T0 ; preheat T0 time: 31s lead 30.6s +; CP TOOLCHANGE START +M104 S200 T1 ; set nozzle temperature ;cooldown +T0 ; change extruder +M109 S240 T0 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T1 ; preheat T1 time: 30s lead 30.0s +; CP TOOLCHANGE START +M104 S200 T0 ; set nozzle temperature ;cooldown +T1 ; change extruder +M109 S240 T1 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T0 ; preheat T0 time: 30s lead 30.2s +; CP TOOLCHANGE START +M104 S200 T1 ; set nozzle temperature ;cooldown +T0 ; change extruder +M109 S240 T0 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T1 ; preheat T1 time: 30s lead 30.2s +; CP TOOLCHANGE START +M104 S200 T0 ; set nozzle temperature ;cooldown +T1 ; change extruder +M109 S240 T1 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T0 ; preheat T0 time: 30s lead 30.2s +; CP TOOLCHANGE START +M104 S200 T1 ; set nozzle temperature ;cooldown +T0 ; change extruder +M109 S240 T0 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T1 ; preheat T1 time: 30s lead 30.2s +; CP TOOLCHANGE START +M104 S200 T0 ; set nozzle temperature ;cooldown +T1 ; change extruder +M109 S240 T1 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T0 ; preheat T0 time: 30s lead 30.2s +; CP TOOLCHANGE START +M104 S200 T1 ; set nozzle temperature ;cooldown +T0 ; change extruder +M109 S240 T0 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T1 ; preheat T1 time: 31s lead 30.7s +; CP TOOLCHANGE START +M104 S200 T0 ; set nozzle temperature ;cooldown +T1 ; change extruder +M109 S240 T1 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T0 ; preheat T0 time: 30s lead 30.4s +; CP TOOLCHANGE START +M104 S200 T1 ; set nozzle temperature ;cooldown +T0 ; change extruder +M109 S240 T0 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T1 ; preheat T1 time: 30s lead 30.4s +; CP TOOLCHANGE START +M104 S200 T0 ; set nozzle temperature ;cooldown +T1 ; change extruder +M109 S240 T1 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T0 ; preheat T0 time: 30s lead 30.0s +; CP TOOLCHANGE START +M104 S200 T1 ; set nozzle temperature ;cooldown +T0 ; change extruder +M109 S240 T0 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +M104 S240 T1 ; preheat T1 time: 30s lead 30.0s +; CP TOOLCHANGE START +M104 S200 T0 ; set nozzle temperature ;cooldown +T1 ; change extruder +M109 S240 T1 ; set nozzle temperature and wait for it to be reached +; CP TOOLCHANGE END +; CP TOOLCHANGE START +; CP TOOLCHANGE END +M104 S0 ; turn off temperature diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt index 70ab639faa..fc46bb8fdc 100644 --- a/tests/fff_print/CMakeLists.txt +++ b/tests/fff_print/CMakeLists.txt @@ -5,6 +5,7 @@ add_executable(${_TEST_NAME}_tests test_helpers.hpp test_cooling.cpp test_extrusion_entity.cpp + test_extrusion_processor.cpp test_fill.cpp test_flow.cpp test_gcode_timing.cpp @@ -14,10 +15,12 @@ add_executable(${_TEST_NAME}_tests test_perimeters.cpp test_print.cpp test_printobject.cpp + test_mixed_filament.cpp test_skirt_brim.cpp test_slicing_pipeline_hook.cpp test_support_material.cpp test_trianglemesh.cpp + test_wipe_tower.cpp ) target_link_libraries(${_TEST_NAME}_tests test_common libslic3r Catch2::Catch2WithMain) set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests") diff --git a/tests/fff_print/test_cooling.cpp b/tests/fff_print/test_cooling.cpp index ec29b5fdc9..a21ff236ce 100644 --- a/tests/fff_print/test_cooling.cpp +++ b/tests/fff_print/test_cooling.cpp @@ -2,7 +2,10 @@ #include "test_helpers.hpp" +#include <algorithm> +#include <sstream> #include <string> +#include <vector> using namespace Slic3r; using namespace Slic3r::Test; @@ -25,3 +28,68 @@ TEST_CASE("Cooling consumes its internal speed markers", "[Cooling]") const std::string gcode = slice({ cube(20) }, { { "layer_height", 0.2 } }); CHECK(gcode.find(";_EXTRUDE_SET_SPEED") == std::string::npos); } + +TEST_CASE("Overhang fan transitions do not depend on overhang speed", "[Cooling][Regression]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "bridge_speed", 2.0 }, + { "enable_arc_fitting", false }, + { "enable_overhang_bridge_fan", true }, + { "enable_overhang_speed", false }, + { "initial_layer_print_height", 0.3 }, + { "inner_wall_speed", 30.0 }, + { "layer_height", 0.3 }, + { "outer_wall_speed", 30.0 }, + { "overhang_1_4_speed", "30" }, + { "overhang_2_4_speed", "29" }, + { "overhang_3_4_speed", "6" }, + { "overhang_4_4_speed", "3" }, + { "slow_down_for_layer_cooling", false }, + { "slowdown_for_curled_perimeters", false }, + }); + config.set_key_value("fan_max_speed", new ConfigOptionFloats{20.0}); + config.set_key_value("fan_min_speed", new ConfigOptionFloats{20.0}); + config.set_key_value("overhang_fan_speed", new ConfigOptionInts{100}); + config.set_key_value("overhang_fan_threshold", new ConfigOptionEnumsGeneric{Overhang_threshold_2_4}); + config.set_key_value("layer_change_gcode", new ConfigOptionString{";TEST_LAYER_Z=[layer_z]"}); + + const auto fan_commands = [](const std::string &gcode) { + std::vector<std::pair<std::string, std::string>> commands; + std::istringstream input(gcode); + std::string layer; + std::string line; + while (std::getline(input, line)) { + if (line.rfind(";TEST_LAYER_Z=", 0) == 0) + layer = line; + else if (!layer.empty() && (line.rfind("M106", 0) == 0 || line.rfind("M107", 0) == 0)) + commands.emplace_back(layer, line); + } + return commands; + }; + const auto feedrates = [](const std::string &gcode) { + std::vector<std::string> values; + std::istringstream input(gcode); + std::string word; + while (input >> word) + if (!word.empty() && word.front() == 'F') + values.push_back(word); + return values; + }; + + constexpr double sphere_radius = 50.0; // 100 mm diameter. + const std::string without_speed_gcode = slice({make_sphere(sphere_radius, PI / 24.0)}, config); + config.set_deserialize_strict({{"enable_overhang_speed", true}}); + const std::string with_speed_gcode = slice({make_sphere(sphere_radius, PI / 24.0)}, config); + + const auto without_speed_fan = fan_commands(without_speed_gcode); + const auto with_speed_fan = fan_commands(with_speed_gcode); + const auto without_speed_feedrates = feedrates(without_speed_gcode); + const auto with_speed_feedrates = feedrates(with_speed_gcode); + + REQUIRE_FALSE(without_speed_fan.empty()); + REQUIRE(std::any_of(without_speed_fan.begin(), without_speed_fan.end(), + [](const auto &command) { return command.second.find("S255") != std::string::npos; })); + REQUIRE(with_speed_feedrates != without_speed_feedrates); + CHECK(with_speed_fan == without_speed_fan); +} diff --git a/tests/fff_print/test_extrusion_processor.cpp b/tests/fff_print/test_extrusion_processor.cpp new file mode 100644 index 0000000000..76e331d66a --- /dev/null +++ b/tests/fff_print/test_extrusion_processor.cpp @@ -0,0 +1,441 @@ +#include <catch2/catch_all.hpp> + +#include "libslic3r/AABBTreeLines.hpp" +#include "libslic3r/GCode/ExtrusionProcessor.hpp" +#include "libslic3r/GCodeReader.hpp" +#include "libslic3r/TriangleMesh.hpp" + +#include "test_helpers.hpp" + +#include <algorithm> +#include <cmath> +#include <string> +#include <vector> + +using namespace Slic3r; +using namespace Slic3r::Test; + +namespace { + +// Print settings the assertions below are derived from. +constexpr double caged_layer_height = 0.2; // mm +constexpr double caged_wall_width = 0.42; // mm, outer wall line width +constexpr double caged_outer_wall_speed = 200.; // mm/s +constexpr double caged_slow_speed = 100.; // mm/s, between every configured overhang speed (<= 50) and the wall speed + +// A wall running 0.2mm out over a previous layer whose edge dishes 0.03mm away from it in the middle, +// standing in for the endpoint readings a caged overhang perimeter takes: enough of a difference to +// print at another speed, but only a fraction of the distance at which slowdown begins. +constexpr double dished_wall_gap = 0.2; // mm, how far the wall runs out past the previous layer's edge +constexpr double dished_layer_depth = 0.03; // mm, how much further out the middle of it reads +constexpr double dished_min_distance = 0.042; // mm, the reading at which the configured speeds begin to slow down +// Every reading here is past that, so the whole wall is slowed and only the amount is in question. +constexpr float dished_end_reading = float(dished_wall_gap + 0.5 * caged_wall_width); +constexpr float dished_mid_reading = float(dished_end_reading + dished_layer_depth); +// The two readings are dished_layer_depth apart, so half of that tells them apart while still allowing +// for the points the passes after sampling add, which read a little further out than the ends do. +constexpr double dished_reading_tolerance = 0.5 * dished_layer_depth; + +// A 40 x 20 x 20 mm box with a 45 degree overhang cut into the y = 0 side. The sloped face spans +// x = 5.086 .. 34.914 only, so the full-height walls of the box cage both ends of every overhang +// perimeter: the endpoints look supported even though the span between them is not. +TriangleMesh caged_overhang_mesh() +{ + return TriangleMesh( + { + {5.0859987f, 10.167065f, 5.711731f}, {34.914257f, 10.167065f, 5.711731f}, + {34.914257f, 0.f, 15.878796f}, {5.0859995f, 0.f, 15.878796f}, + {0.f, 0.f, 0.f}, {0.f, 0.f, 20.f}, + {0.f, 20.f, 20.f}, {0.f, 20.f, 0.f}, + {40.f, 20.f, 20.f}, {40.f, 20.f, 0.f}, + {40.f, 0.f, 20.f}, {40.f, 0.f, 0.f}, + {34.914257f, 0.f, 0.f}, {5.0859995f, 0.f, 0.f}, + {34.914257f, 10.167065f, 0.f}, {5.0859995f, 10.167065f, 0.f}, + }, + { + {0, 1, 2}, {0, 2, 3}, {4, 5, 6}, {4, 6, 7}, {7, 6, 8}, {7, 8, 9}, + {9, 8, 10}, {9, 10, 11}, {12, 11, 10}, {5, 4, 13}, {5, 13, 3}, {2, 12, 10}, + {5, 3, 2}, {10, 5, 2}, {9, 11, 12}, {9, 12, 14}, {13, 4, 7}, {9, 14, 15}, + {15, 13, 7}, {7, 9, 15}, {8, 6, 5}, {8, 5, 10}, {14, 1, 0}, {14, 0, 15}, + {2, 1, 14}, {2, 14, 12}, {15, 0, 3}, {15, 3, 13}, + }); +} + +// Mesh geometry the wall filters below are derived from. +constexpr double caged_box_depth = 20.; // mm, the box spans y = 0 .. 20 +constexpr double caged_slope_face_sum = 15.878796; // mm, y + z of the sloped face, from its corners +// The sloped face spans this x range; outside it the box walls run full height. +constexpr double caged_slope_x_min = 5.0859995; +constexpr double caged_slope_x_max = 34.914257; +constexpr double caged_slope_span = caged_slope_x_max - caged_slope_x_min; // ~29.8 mm +// The z range the sloped face occupies, from the same fixture vertices. +constexpr double caged_slope_z_min = 5.711731; +constexpr double caged_slope_z_max = 15.878796; +// The lowest slope layer still sits on the solid body below the notch, so it is fully supported and +// runs at the outer wall speed by design. The caged span proper begins one layer above it. +constexpr double caged_span_z_min = caged_slope_z_min + caged_layer_height; + +// A layer printed at z is sliced at z - layer_height / 2, and the outer wall centreline sits half a +// line width inside the contour, so the wall on the slope satisfies y + z = 16.189. +constexpr double caged_slope_wall_sum = caged_slope_face_sum + 0.5 * caged_layer_height + 0.5 * caged_wall_width; +// Same inset on the fully supported y = 20 face, vertical over the whole height. +constexpr double caged_back_wall_y = caged_box_depth - 0.5 * caged_wall_width; +// And on the y = 0 face, which runs full height only outside the slope's x range. +constexpr double caged_front_wall_y = 0.5 * caged_wall_width; +// Arachne varies the wall width along a face, and the centreline inset is half that width, so a +// wall sits within about half a line width of where the nominal inset alone would put it. The +// faces being selected are millimetres apart, so this stays far from ambiguous. +constexpr double caged_wall_tolerance = 0.5 * caged_wall_width; + +// Feed rates in mm/min of the long outer wall extrusions `keep_line` selects. +template<typename KeepLine> std::vector<double> outer_wall_feed_rates(const std::string& gcode, KeepLine keep_line) +{ + std::vector<double> feed_rates; + bool outer_wall = false; + GCodeReader parser; + parser.parse_buffer(gcode, [&feed_rates, &outer_wall, &keep_line](GCodeReader& self, const GCodeReader::GCodeLine& line) { + const std::string_view comment = line.comment(); + if (comment.find("FEATURE:") != std::string_view::npos || comment.find("TYPE:") != std::string_view::npos) + outer_wall = comment.find("Outer wall") != std::string_view::npos || + comment.find("External perimeter") != std::string_view::npos; + + if (outer_wall && line.extruding(self) && line.dist_XY(self) > 1.0 && keep_line(self, line)) + feed_rates.push_back(line.new_F(self)); + }); + + return feed_rates; +} + +// The caged 45 degree overhang: outer walls crossing the sloped face for most of its width, on the +// layers where the face genuinely overhangs. +// Both ends are tested against the slope plane rather than requiring a constant Y. Arachne's +// variable-width walls drift slightly in Y along the same slope (Y6.186 -> Y6.189 on one move), so +// a constant-Y filter matches almost nothing under Arachne and silently reduces its coverage. +// The length test excludes the cage walls: they are only as wide as the box is either side of the +// slope, but being vertical their y + z sweeps through the slope plane as z rises, so a couple of +// their fully supported moves would otherwise be counted as part of the span. +std::vector<double> caged_slope_feed_rates(const std::string& gcode) +{ + return outer_wall_feed_rates(gcode, [](const GCodeReader& self, const GCodeReader::GCodeLine& line) { + const double z = line.new_Z(self); + return z > caged_span_z_min && z < caged_slope_z_max && + line.dist_XY(self) > 0.5 * caged_slope_span && + std::abs(self.y() + z - caged_slope_wall_sum) < caged_wall_tolerance && + std::abs(line.new_Y(self) + z - caged_slope_wall_sum) < caged_wall_tolerance; + }); +} + +// The opposite, fully supported face, skipping the initial layer and its own speed settings. +std::vector<double> back_wall_feed_rates(const std::string& gcode) +{ + return outer_wall_feed_rates(gcode, [](const GCodeReader& self, const GCodeReader::GCodeLine& line) { + return line.new_Z(self) > 1.5 * caged_layer_height && + std::abs(self.y() - caged_back_wall_y) < caged_wall_tolerance && + std::abs(line.new_Y(self) - caged_back_wall_y) < caged_wall_tolerance; + }); +} + +// The first layer printed entirely above the slope. Its y = 0 wall runs the full width of the box. +const double caged_layer_above_slope_z = std::ceil(caged_slope_z_max / caged_layer_height) * caged_layer_height; + +// The parts of that wall standing on the cage rather than the slope, so on a contour identical to their own. +// Where the support changes is found by bisection, which stops at spans of 2mm, so the move spanning each end of +// the slope reaches a little way into the cage. Taking only the moves lying wholly outside the slope's x range +// leaves the wall that is unambiguously supported, without asserting how closely the bisection converged. +std::vector<double> cage_shoulder_feed_rates(const std::string& gcode) +{ + return outer_wall_feed_rates(gcode, [](const GCodeReader& self, const GCodeReader::GCodeLine& line) { + return std::abs(line.new_Z(self) - caged_layer_above_slope_z) < 0.5 * caged_layer_height && + std::abs(self.y() - caged_front_wall_y) < caged_wall_tolerance && + std::abs(line.new_Y(self) - caged_front_wall_y) < caged_wall_tolerance && + (std::max(self.x(), line.new_X(self)) <= caged_slope_x_min || + std::min(self.x(), line.new_X(self)) >= caged_slope_x_max); + }); +} + +// The readings a 40mm wall takes over a previous layer whose edge falls away by 0.03mm towards the +// middle: both ends read the same, and the middle reads slightly further out over air. Whether that +// middle reading survives is what decides the speed the wall is printed at. +std::vector<ExtendedPoint<2>> sampled_wall_over_dished_layer(const std::function<float(float)>& distance_to_speed) +{ + const AABBTreeLines::LinesDistancer<Linef> prev_layer(std::vector<Linef>{ + {{0., 0.}, {20., -dished_layer_depth}}, + {{20., -dished_layer_depth}, {40., 0.}}, + {{40., 0.}, {40., -10.}}, + {{40., -10.}, {0., -10.}}, + {{0., -10.}, {0., 0.}}, + }); + const Points wall{Point::new_scale(0., dished_wall_gap), Point::new_scale(40., dished_wall_gap)}; + + return estimate_points_properties<true, true, true, true>(wall, prev_layer, caged_wall_width, -1.f, + dished_min_distance, distance_to_speed); +} + +// A straight, otherwise supported wall over a previous-layer boundary with a 2mm-wide pocket. Moving the +// pocket between x = 10 and x = 20 covers both discovery away from the wall's midpoint and refinement around +// a midpoint that has already been discovered. The current wall is inset half its width from the flat boundary, +// so its supported readings are zero after the estimator applies its boundary offset. +constexpr double narrow_pocket_wall_length = 40.; +constexpr double narrow_pocket_width = 2.; +constexpr double narrow_pocket_depth = 0.3; + +std::vector<ExtendedPoint<2>> sampled_wall_over_narrow_pocket( + double pocket_center, const std::function<float(float)>& distance_to_speed) +{ + const double pocket_left = pocket_center - 0.5 * narrow_pocket_width; + const double pocket_right = pocket_center + 0.5 * narrow_pocket_width; + const AABBTreeLines::LinesDistancer<Linef> prev_layer(std::vector<Linef>{ + {{0., 0.}, {pocket_left, 0.}}, + {{pocket_left, 0.}, {pocket_left, -narrow_pocket_depth}}, + {{pocket_left, -narrow_pocket_depth}, {pocket_right, -narrow_pocket_depth}}, + {{pocket_right, -narrow_pocket_depth}, {pocket_right, 0.}}, + {{pocket_right, 0.}, {narrow_pocket_wall_length, 0.}}, + {{narrow_pocket_wall_length, 0.}, {narrow_pocket_wall_length, -10.}}, + {{narrow_pocket_wall_length, -10.}, {0., -10.}}, + {{0., -10.}, {0., 0.}}, + }); + const double wall_y = -0.5 * caged_wall_width; + const Points wall{Point::new_scale(0., wall_y), Point::new_scale(narrow_pocket_wall_length, wall_y)}; + + return estimate_points_properties<true, true, true, true>(wall, prev_layer, caged_wall_width, -1.f, + dished_min_distance, distance_to_speed); +} + +// A cross section that grows a layer's worth on the two faces meeting at either end of a wall, as any +// 45 degree overhang does. The wall itself stands on a contour identical to its own, but its ends sit +// where the growing faces cut the corners off, and the previous layer's edge there is nearer than the +// half line width the centreline is inset by. Both ends therefore read an overhang while everything +// between them reads supported: the reverse of the caged span, and the case the sampling above must +// leave to the passes after it. +constexpr double stepped_wall_inset = 0.5 * caged_wall_width; // mm, centreline inset from the contour +constexpr double stepped_end_gap = stepped_wall_inset - caged_layer_height; // mm, how far inside the corner ends up +constexpr double stepped_wall_span = 30.; // mm, the length of the wall + +std::vector<ExtendedPoint<2>> sampled_wall_between_growing_corners(const std::function<float(float)>& distance_to_speed) +{ + const AABBTreeLines::LinesDistancer<Linef> prev_layer(std::vector<Linef>{ + {{0., 0.}, {32., 0.}}, + {{32., 0.}, {32., -stepped_wall_span}}, + {{32., -stepped_wall_span}, {0., -stepped_wall_span}}, + {{0., -stepped_wall_span}, {0., 0.}}, + }); + const Points wall{Point::new_scale(stepped_wall_inset, -stepped_end_gap), + Point::new_scale(stepped_wall_inset, stepped_end_gap - stepped_wall_span)}; + + return estimate_points_properties<true, true, true, true>(wall, prev_layer, caged_wall_width, -1.f, + dished_min_distance, distance_to_speed); +} + +// How much of a path is printed below the speed a fully supported reading gives. A segment is printed +// at the lower of the speeds its ends read. +double slowed_length(const std::vector<ExtendedPoint<2>>& points, const std::function<float(float)>& distance_to_speed) +{ + double length = 0.; + for (size_t i = 0; i + 1 < points.size(); ++i) + if (std::min(distance_to_speed(points[i].distance), distance_to_speed(points[i + 1].distance)) < distance_to_speed(0.f)) + length += (points[i + 1].position - points[i].position).norm(); + return length; +} + +float furthest_reading(const std::vector<ExtendedPoint<2>>& points) +{ + return std::max_element(points.begin(), points.end(), [](const ExtendedPoint<2>& l, const ExtendedPoint<2>& r) { + return l.distance < r.distance; + })->distance; +} + +DynamicPrintConfig caged_overhang_config(const char* wall_generator){ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + {"nozzle_diameter", "0.4"}, + {"initial_layer_print_height", caged_layer_height}, + {"layer_height", caged_layer_height}, + {"line_width", caged_wall_width}, + {"outer_wall_line_width", caged_wall_width}, + {"inner_wall_line_width", "0.45"}, + {"wall_loops", "2"}, + {"wall_generator", wall_generator}, + {"wall_sequence", "inner wall/outer wall"}, + {"sparse_infill_density", "15%"}, + {"detect_overhang_wall", "1"}, + {"enable_overhang_speed", "1"}, + {"slowdown_for_curled_perimeters", "0"}, + {"zaa_enabled", "0"}, + {"outer_wall_speed", caged_outer_wall_speed}, + {"inner_wall_speed", "300"}, + {"overhang_1_4_speed", "0"}, + {"overhang_2_4_speed", "50"}, + {"overhang_3_4_speed", "30"}, + {"overhang_4_4_speed", "10"}, + {"bridge_speed", "50"}, + {"filament_max_volumetric_speed", "22"}, + {"slow_down_for_layer_cooling", "0"}, + {"slow_down_layers", "0"}, // Nothing but the overhang settings may lower a wall speed + }); + return config; +} + +std::string caged_overhang_gcode(const char* wall_generator) +{ + Print print; + Model model; + init_print(std::vector<TriangleMesh>{caged_overhang_mesh()}, print, model, caged_overhang_config(wall_generator), nullptr, + false); + return gcode(print); +} + +// Reports the matched move count alongside the extremes, so a filter that selected nothing is +// distinguishable from a span that simply was not slowed. +void info_feed_rates(const char* span, const std::vector<double>& feed_rates) +{ + UNSCOPED_INFO("matched " << feed_rates.size() << " " << span << " moves"); + if (!feed_rates.empty()) { + const auto extremes = std::minmax_element(feed_rates.begin(), feed_rates.end()); + UNSCOPED_INFO("slowest " << *extremes.first / MM_PER_MIN << " mm/s, fastest " << *extremes.second / MM_PER_MIN << " mm/s"); + } +} + +} // namespace + +// Classic reproduces the endpoint-sampling bug: it emits the span as one long move whose endpoints +// both read as supported, so endpoint-only sampling never slows it. Arachne's endpoints already read +// as overhanging, but their placement near the cage makes the inferred support vary by layer. Arachne +// parity is therefore part of this regression's scope: both generators must classify the unsupported +// interior of the same 45-degree span consistently. +TEST_CASE("Caged external overhangs are slowed along their span", "[ExtrusionProcessor][Regression]") +{ + const char* wall_generator = GENERATE("classic", "arachne"); + INFO("wall generator: " << wall_generator); + + const std::vector<double> feed_rates = caged_slope_feed_rates(caged_overhang_gcode(wall_generator)); + info_feed_rates("caged slope", feed_rates); + + REQUIRE_FALSE(feed_rates.empty()); + + // The endpoint bug left Classic at the full wall speed, while Arachne's cage-adjacent endpoint + // samples selected much faster bands on some layers. The whole span must stay in the slowed range + // for both generators, without requiring their different path segmentations to match. + const double fastest = *std::max_element(feed_rates.begin(), feed_rates.end()); + REQUIRE(fastest < caged_slow_speed * MM_PER_MIN); +} + +// The other side of the fix: the midpoint probe fires on every long external perimeter, so a +// regression that over-slows would leave the test above green. A fully supported wall must keep the +// speed it was configured with. +TEST_CASE("Supported vertical walls keep their normal speed", "[ExtrusionProcessor][Regression]") +{ + const char* wall_generator = GENERATE("classic", "arachne"); + INFO("wall generator: " << wall_generator); + + const std::vector<double> feed_rates = back_wall_feed_rates(caged_overhang_gcode(wall_generator)); + info_feed_rates("back wall", feed_rates); + + REQUIRE_FALSE(feed_rates.empty()); + + const double slowest = *std::min_element(feed_rates.begin(), feed_rates.end()); + REQUIRE(slowest >= caged_slow_speed * MM_PER_MIN); +} + +// The slope's top edge falls mid layer, so the first layer above it still stands 0.179mm proud of the layer +// below wherever that layer was still on the slope. That is a real overhang and is slowed, but it ends with the +// slope: outside the slope's x range the box runs full height, so the same wall stands on a contour identical to +// its own. Sampling the interior of that wall at a single point reported one support reading for all of it and +// slowed these fully supported ends along with the rest. +TEST_CASE("Wall sections beside a caged overhang keep their normal speed", "[ExtrusionProcessor][Regression]") +{ + const char* wall_generator = GENERATE("classic", "arachne"); + INFO("wall generator: " << wall_generator); + + const std::vector<double> feed_rates = cage_shoulder_feed_rates(caged_overhang_gcode(wall_generator)); + info_feed_rates("cage shoulder", feed_rates); + + REQUIRE_FALSE(feed_rates.empty()); + + const double slowest = *std::min_element(feed_rates.begin(), feed_rates.end()); + REQUIRE_THAT(slowest / MM_PER_MIN, Catch::Matchers::WithinRel(caged_outer_wall_speed, 0.01)); +} + +// A wall is printed at the lower of the speeds its ends read, so a reading only earns a point in the +// path where it prints at a different speed from the readings around it. Judging that on the readings +// themselves rather than the speeds they produce was too coarse: the configured speeds interpolate +// between their sections, so readings a fraction of the slowdown threshold apart still print more than +// 10% apart, and a real 45 degree overhang had its true reading dropped as if it agreed with its ends. +// The ends then chose the speed on their own, and being next to the walls either side of the overhang +// they read differently from layer to layer, banding an overhang that should have been uniform. +TEST_CASE("An overhang reading is kept whenever it changes the speed", "[ExtrusionProcessor][Regression]") +{ + // A steep speed curve, of the kind the configured overhang speeds interpolate across. + const std::vector<ExtendedPoint<2>> points = + sampled_wall_over_dished_layer([](float distance) { return std::round(200.f - 400.f * distance); }); + + REQUIRE_THAT(furthest_reading(points), Catch::Matchers::WithinAbs(dished_mid_reading, dished_reading_tolerance)); +} + +// The complement, and why the readings alone were tempting: a reading that prints at the same speed as +// its neighbours cannot change the G-code, so sampling must leave the path alone however far out it is. +TEST_CASE("An overhang reading is dropped when the speed is unchanged", "[ExtrusionProcessor]") +{ + // A flat speed curve, of the kind a single configured overhang speed produces. + const std::vector<ExtendedPoint<2>> points = sampled_wall_over_dished_layer([](float) { return 50.f; }); + + REQUIRE_THAT(furthest_reading(points), Catch::Matchers::WithinAbs(dished_end_reading, dished_reading_tolerance)); +} + +TEST_CASE("Coarse probing detects an unsupported pocket away from the wall midpoint", + "[ExtrusionProcessor][Regression]") +{ + const std::function<float(float)> distance_to_speed = [](float distance) { return distance <= 0.2f ? 100.f : 50.f; }; + const std::vector<ExtendedPoint<2>> points = + sampled_wall_over_narrow_pocket(0.25 * narrow_pocket_wall_length, distance_to_speed); + const double slowed = slowed_length(points, distance_to_speed); + + REQUIRE(slowed > 0.); + REQUIRE(slowed < 5.); +} + +TEST_CASE("Coarse probing brackets a narrow slowdown at the wall midpoint", + "[ExtrusionProcessor][Regression]") +{ + // Half of the pocket reading still maps to full speed. A matching probe in either half therefore must not + // prune that half before a supported point has been found close enough to bracket the slow midpoint. + const std::function<float(float)> distance_to_speed = [](float distance) { return distance <= 0.2f ? 100.f : 50.f; }; + const std::vector<ExtendedPoint<2>> points = + sampled_wall_over_narrow_pocket(0.5 * narrow_pocket_wall_length, distance_to_speed); + const double slowed = slowed_length(points, distance_to_speed); + + REQUIRE(slowed > 0.); + REQUIRE(slowed < 5.); +} + +// Sampling probes the interior, so it must not answer for the ends. On a supported wall between two +// corners that read an overhang, the reading that differs is the end's own, and the pass that ends a +// slowdown an end reads places its point from how far out that end is. Sampling took the difference as +// its own to report and put a point at the nearest position bisection had reached instead, which both +// sits further along the wall and leaves too little of it for that pass to run on, so the corner +// slowdown ran millimetres up an otherwise supported wall. Its length grows with the wall, so on a +// model whose cross section keeps growing it reads as a stair stepped band up the corner. +TEST_CASE("A supported wall between overhanging corners is slowed no further than its ends require", + "[ExtrusionProcessor][Regression]") +{ + // A steep speed curve, so the ends and the interior between them print at clearly different speeds. + const std::function<float(float)> distance_to_speed = [](float distance) { + return std::round(float(caged_outer_wall_speed) - 400.f * distance); + }; + + const double sampled = slowed_length(sampled_wall_between_growing_corners(distance_to_speed), distance_to_speed); + // The same wall with sampling switched off: what the endpoint driven passes alone make of the corners. + const double unsampled = slowed_length(sampled_wall_between_growing_corners({}), distance_to_speed); + + // The corners do read an overhang, so there is a slowdown for sampling to have lengthened. + REQUIRE(unsampled > 0.); + REQUIRE(sampled <= unsampled); +} + +TEST_CASE("Benchmark caged overhang interior sampling", "[ExtrusionProcessor][!benchmark]"){ + const char* wall_generator = GENERATE("classic", "arachne"); + + BENCHMARK(wall_generator) + { + return caged_overhang_gcode(wall_generator); + }; +} diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp index 5fbce5a342..d26639c659 100644 --- a/tests/fff_print/test_fill.cpp +++ b/tests/fff_print/test_fill.cpp @@ -698,3 +698,327 @@ TEST_CASE("Solid infill direction offsets every layer when no template is set", CHECK(delta == 30); } } + +TEST_CASE("Honeycomb infill rounds its cell corners with the smooth factor", "[Fill]") +{ + // A cell whose sides are several times the line width, so that the corners have room to be rounded. + const double spacing = 0.45; + const double density = 0.1; + auto fill = [spacing, density](double smooth_factor) { + std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("honeycomb")); + filler->spacing = spacing; + + FillParams params; + params.density = float(density); + params.dont_adjust = true; + // Keep the fragments apart, so that only the turns of the pattern itself are measured. + params.anchor_length_max = 0.f; + params.smooth_factor = smooth_factor; + + Slic3r::ExPolygon square{ Slic3r::Points{ + Point::new_scale(0., 0.), Point::new_scale(50., 0.), Point::new_scale(50., 50.), Point::new_scale(0., 50.) } }; + Slic3r::Surface surface(stInternal, square); + return filler->fill_surface(&surface, params); + }; + + // Cosine of the sharpest turn of any of the paths, 1 meaning none of them turns at all. + auto sharpest_turn_cosine = [](const Slic3r::Polylines &polylines) { + double sharpest = 1.; + for (const Polyline &polyline : polylines) + for (size_t i = 1; i + 1 < polyline.size(); ++i) { + const Vec2d incoming = (polyline[i] - polyline[i - 1]).cast<double>().normalized(); + const Vec2d outgoing = (polyline[i + 1] - polyline[i]).cast<double>().normalized(); + sharpest = std::min(sharpest, incoming.dot(outgoing)); + } + return sharpest; + }; + auto point_count = [](const Slic3r::Polylines &polylines) { + return std::accumulate(polylines.begin(), polylines.end(), size_t(0), + [](size_t count, const Polyline &polyline) { return count + polyline.size(); }); + }; + + const Slic3r::Polylines sharp = fill(0.); + const Slic3r::Polylines smooth = fill(1.); + + REQUIRE(!sharp.empty()); + REQUIRE(smooth.size() == sharp.size()); + REQUIRE(point_count(smooth) > point_count(sharp)); + // The cell corners turn by 60 degrees; smoothing replaces them by gentle curves. + REQUIRE(sharpest_turn_cosine(sharp) < 0.6); + REQUIRE(sharpest_turn_cosine(smooth) > 0.9); +} + +// Point count, number of turns sharper than 25 degrees and length of the sparse infill of a print. +// A rounded corner is a run of much gentler turns, so smoothing shows up as fewer sharp ones. +struct SparseInfillShape { + size_t point_count { 0 }; + size_t sharp_turns { 0 }; + size_t path_count { 0 }; + double length { 0. }; + // Digest of every point in the order it is printed. The counts above all survive the same + // extrusions being joined into different polylines, so only this tells two such fills apart. + uint64_t sequence { 14695981039346656037ull }; +}; + +static SparseInfillShape sparse_infill_shape(const Print &print) +{ + SparseInfillShape shape; + + auto account = [&shape](const ExtrusionPath &path) { + if (!sparse_role(path.role())) + return; + const Points3 &pts = path.polyline.points; + ++shape.path_count; + shape.point_count += pts.size(); + for (const auto &pt : pts) + for (const coord_t coordinate : {pt.x(), pt.y(), pt.z()}) + shape.sequence = (shape.sequence ^ uint64_t(coordinate)) * 1099511628211ull; + for (size_t i = 1; i < pts.size(); ++i) + shape.length += (pts[i] - pts[i - 1]).head<2>().cast<double>().norm(); + for (size_t i = 1; i + 1 < pts.size(); ++i) { + const Vec2d incoming = (pts[i] - pts[i - 1]).head<2>().cast<double>(); + const Vec2d outgoing = (pts[i + 1] - pts[i]).head<2>().cast<double>(); + if (incoming.squaredNorm() > 0. && outgoing.squaredNorm() > 0. && + incoming.normalized().dot(outgoing.normalized()) < 0.9) + ++shape.sharp_turns; + } + }; + + for (const Layer *layer : print.objects().front()->layers()) + for (const LayerRegion *region : layer->regions()) + for (const ExtrusionEntity *entity : region->fills.flatten().entities) { + if (auto *path = dynamic_cast<const ExtrusionPath *>(entity)) + account(*path); + else if (auto *multi = dynamic_cast<const ExtrusionMultiPath *>(entity)) + for (const ExtrusionPath &p : multi->paths) + account(p); + else if (auto *loop = dynamic_cast<const ExtrusionLoop *>(entity)) + for (const ExtrusionPath &p : loop->paths) + account(p); + } + return shape; +} + +TEST_CASE("Lightning infill slices the same model the same way twice", "[Fill][Regression]") +{ + // Slicing twice in one process catches a generator that carries state from one slice to the + // next, or whose result depends on how the parallel layer fill interleaves. + auto shape = [] { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "lightning"}, + {"sparse_infill_density", "50%"}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape first = shape(); + const SparseInfillShape second = shape(); + + REQUIRE(first.path_count > 0); + REQUIRE(second.path_count == first.path_count); + REQUIRE(second.point_count == first.point_count); + REQUIRE(second.sharp_turns == first.sharp_turns); + // No tolerance: the same extrusions in the same order add up to the very same number. + REQUIRE_THAT(second.length, Catch::Matchers::WithinAbs(first.length, 0.)); + // All of the above agree when the same branches are joined into different polylines, so the + // point sequence is what actually decides whether the two slices produced the same infill. + REQUIRE(second.sequence == first.sequence); +} + +TEST_CASE("Lightning infill rounds the turns of its branches with the smooth factor", "[Fill]") +{ + auto shape_for = [](const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "lightning"}, + {"sparse_infill_density", "15%"}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for("0%"); + const SparseInfillShape smooth = shape_for("100%"); + + REQUIRE(sharp.point_count > 0); + // The branch turns are replaced by curves, which cut the corners off and take more points to + // describe. The turns where two branches are joined into one path stay sharp. + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); + REQUIRE(smooth.length < sharp.length); +} + +TEST_CASE("Concentric infill rounds its loops with the smooth factor", "[Fill]") +{ + auto shape_for = [](const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "concentric"}, + {"sparse_infill_density", "20%"}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for("0%"); + const SparseInfillShape smooth = shape_for("100%"); + + REQUIRE(sharp.point_count > 0); + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); + REQUIRE(smooth.length < sharp.length); +} + +TEST_CASE("Cross hatch infill rounds its transition layers with the smooth factor", "[Fill]") +{ + auto shape_for = [](const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "crosshatch"}, + {"sparse_infill_density", "20%"}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for("0%"); + const SparseInfillShape smooth = shape_for("100%"); + + REQUIRE(sharp.point_count > 0); + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); + REQUIRE(smooth.length < sharp.length); +} + +TEST_CASE("Trapezoidal grid infill rounds its corners only with more than one line", "[Fill]") +{ + auto shape_for = [](int multiline, const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "grid"}, + {"sparse_infill_density", "20%"}, + {"fill_multiline", multiline}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for(2, "0%"); + const SparseInfillShape smooth = shape_for(2, "100%"); + + REQUIRE(sharp.point_count > 0); + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); + REQUIRE(smooth.length < sharp.length); + + // A single line per infill wall is the plain crossing line grid, which has no corner of its own. + const SparseInfillShape single_sharp = shape_for(1, "0%"); + const SparseInfillShape single_smooth = shape_for(1, "100%"); + REQUIRE(single_sharp.point_count > 0); + REQUIRE(single_smooth.point_count == single_sharp.point_count); + REQUIRE(single_smooth.length == single_sharp.length); +} + +TEST_CASE("3D honeycomb infill rounds its octahedral waves with the smooth factor", "[Fill]") +{ + auto shape_for = [](const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "3dhoneycomb"}, + {"sparse_infill_density", "20%"}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for("0%"); + const SparseInfillShape smooth = shape_for("100%"); + + REQUIRE(sharp.point_count > 0); + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); + REQUIRE(smooth.length < sharp.length); +} + +TEST_CASE("Smoothed concentric infill stays inside the fill region", "[Fill][Regression]") +{ + // The concentric loops are offsets of the fill region and are never clipped to it, so a corner + // rounded across its boundary ends up in a hole or over a wall. Rounding cuts toward the inside of + // the turn, which leaves the region at every corner of a hole, and in a region thinner than the + // curve even at a corner turning inwards. + const bool thin_region = GENERATE(false, true); + ExPolygon region; + if (thin_region) { + // An L of two 1.2mm wide arms: cutting the corner they meet at crosses both of them. + region = ExPolygon{ Slic3r::Points{ + Point::new_scale(0., 0.), Point::new_scale(20., 0.), Point::new_scale(20., 1.2), + Point::new_scale(1.2, 1.2), Point::new_scale(1.2, 20.), Point::new_scale(0., 20.) } }; + } else { + region = ExPolygon{ Slic3r::Points{ Point::new_scale(0., 0.), Point::new_scale(50., 0.), + Point::new_scale(50., 50.), Point::new_scale(0., 50.) }, + Slic3r::Points{ Point::new_scale(30., 20.), Point::new_scale(30., 30.), + Point::new_scale(20., 30.), Point::new_scale(20., 20.) } }; + } + CAPTURE(thin_region); + + auto fill = [®ion](double smooth_factor) { + std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("concentric")); + filler->spacing = 0.45; + + FillParams params; + params.density = 0.1f; + params.dont_adjust = true; + params.smooth_factor = smooth_factor; + + Slic3r::Surface surface(stInternal, region); + return filler->fill_surface(&surface, params); + }; + auto point_count = [](const Slic3r::Polylines &polylines) { + return std::accumulate(polylines.begin(), polylines.end(), size_t(0), + [](size_t count, const Polyline &polyline) { return count + polyline.size(); }); + }; + + const Slic3r::Polylines sharp = fill(0.); + const Slic3r::Polylines smooth = fill(1.); + REQUIRE(!sharp.empty()); + + // Nothing leaves the fill region, which the unrounded loops already touch from the inside. + const ExPolygons bounds = offset_ex(region, float(SCALED_EPSILON)); + REQUIRE(diff_pl(sharp, bounds).empty()); + REQUIRE(diff_pl(smooth, bounds).empty()); + // The corners that the region has room for are still rounded. + if (!thin_region) + REQUIRE(point_count(smooth) > point_count(sharp)); +} + +TEST_CASE("Smoothing multiline lightning infill keeps its outlines connected", "[Fill][Regression]") +{ + // With more than one line per infill wall, the branches are printed as outlines drawn around them, + // and the outlines of branches that run close to each other merge into one. Rounding the branches + // before those outlines are built moves them apart, which breaks the merged outlines up into + // separate loops - many more of them, each needing its own travel move. + auto shape_for = [](const std::string &smooth_factor) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"sparse_infill_pattern", "lightning"}, + {"sparse_infill_density", "50%"}, + {"fill_multiline", 2}, + {"sparse_infill_smooth_factor", smooth_factor}, + {"layer_height", 0.2}}); + return sparse_infill_shape(print); + }; + + const SparseInfillShape sharp = shape_for("0%"); + const SparseInfillShape smooth = shape_for("100%"); + + REQUIRE(sharp.path_count > 0); + // The loop count varies by a loop or two between platforms and between runs, so this is not an + // exact comparison. Smoothing should leave it about where it was; uncapping the smoothing + // reach, the regression this guards against, adds about 10%. + const size_t allowed_extra = sharp.path_count / 50; // 2% + REQUIRE(smooth.path_count <= sharp.path_count + allowed_extra); + // The outlines are still rounded. + REQUIRE(smooth.point_count > sharp.point_count); + REQUIRE(smooth.sharp_turns < sharp.sharp_turns); +} diff --git a/tests/fff_print/test_gcode_timing.cpp b/tests/fff_print/test_gcode_timing.cpp index 4570f253bb..8c08fc4f03 100644 --- a/tests/fff_print/test_gcode_timing.cpp +++ b/tests/fff_print/test_gcode_timing.cpp @@ -7,9 +7,14 @@ #include "test_utils.hpp" +#include <cmath> #include <fstream> +#include <iomanip> #include <map> #include <memory> +#include <sstream> +#include <string> +#include <vector> using namespace Slic3r; using Catch::Matchers::WithinAbs; @@ -418,3 +423,194 @@ TEST_CASE("Per-slot machine limits follow the active nozzle", "[GCodeTiming][Mul REQUIRE_THAT(times[2], Catch::Matchers::WithinRel(101.0 / 200.0, 0.10)); } } + +// Junction planning decides the speeds the "actual speed" / "actual flow" preview shows. Per-axis +// jerk limits a corner by the largest single-axis component of the velocity change, allowing sqrt(2) +// more speed on a diagonal than on an axis -- a four-lobed ripple around every circle. Klipper and +// Marlin 2 with M205 J plan with junction deviation instead, which sees only the corner angle. +namespace { + +// One acceleration everywhere and axis limits far above it, so only the junction model under test +// can slow a corner down. +FullPrintConfig make_junction_config(GCodeFlavor flavor, double corner_velocity, double junction_deviation) +{ + FullPrintConfig config; + config.gcode_flavor.value = flavor; + config.filament_diameter.values = {1.75}; + config.filament_map.values = {1}; + + const std::vector<double> accel = {1000.0, 1000.0}; + const std::vector<double> axis = {20000.0, 20000.0}; + const std::vector<double> speed = {500.0, 500.0}; + config.machine_max_acceleration_extruding.values = accel; + config.machine_max_acceleration_travel.values = accel; + config.machine_max_acceleration_retracting.values = accel; + config.machine_max_acceleration_x.values = axis; + config.machine_max_acceleration_y.values = axis; + config.machine_max_acceleration_z.values = axis; + config.machine_max_acceleration_e.values = axis; + config.machine_max_speed_x.values = speed; + config.machine_max_speed_y.values = speed; + config.machine_max_speed_z.values = speed; + config.machine_max_speed_e.values = speed; + // Klipper reads this as the square corner velocity, Marlin as classic jerk. + config.machine_max_jerk_x.values = {corner_velocity, corner_velocity}; + config.machine_max_jerk_y.values = {corner_velocity, corner_velocity}; + config.machine_max_jerk_z.values = {corner_velocity, corner_velocity}; + // Kept out of the way so it never binds in the classic-jerk comparisons. + config.machine_max_jerk_e.values = {100.0, 100.0}; + config.machine_max_junction_deviation.values = {junction_deviation, junction_deviation}; + config.machine_min_extruding_rate.values = {0.0, 0.0}; + config.machine_min_travel_rate.values = {0.0, 0.0}; + return config; +} + +constexpr double junction_x = 60.0; +constexpr double junction_y = 60.0; + +// Two 40mm moves meeting at (junction_x, junction_y) with the given turn, rotated by `orientation`. +// 40mm is long enough to reach the commanded 150mm/s and brake back to any corner speed these tests +// produce. `e_per_mm` of zero makes them travels, which keeps the junction vector purely geometric +// as the formulas below assume. +std::string corner_gcode(double turn_deg, double orientation_deg, double e_per_mm = 0.0) +{ + const double len = 40.0; + const double a_in = orientation_deg * M_PI / 180.0; + const double a_out = (orientation_deg + turn_deg) * M_PI / 180.0; + std::ostringstream extrude; + if (e_per_mm > 0.0) + extrude << std::fixed << std::setprecision(4) << " E" << len * e_per_mm; + + std::ostringstream os; + os << std::fixed << std::setprecision(4) + << "M83\n" + << "G1 Z0.2 F1200\n" + << "G1 X" << junction_x - len * std::cos(a_in) << " Y" << junction_y - len * std::sin(a_in) << " F6000\n" + << "G1 X" << junction_x << " Y" << junction_y << extrude.str() << " F9000\n" + << "G1 X" << junction_x + len * std::cos(a_out) << " Y" << junction_y + len * std::sin(a_out) + << extrude.str() << " F9000\n"; + return os.str(); +} + +// Speed allowed through the corner: the vertex ending the incoming move carries that block's exit +// speed, and the vertices the actual-speed pass inserts are all strictly interior. +double corner_speed(const GCodeProcessorResult& r) +{ + for (const auto& mv : r.moves) + if ((mv.type == EMoveType::Travel || mv.type == EMoveType::Extrude) && + std::abs(mv.position.x() - junction_x) < 1e-3 && + std::abs(mv.position.y() - junction_y) < 1e-3) + return mv.actual_feedrate; + return -1.0; +} + +double planned_corner_speed(GCodeFlavor flavor, double corner_velocity, double junction_deviation, + double turn_deg, double orientation_deg = 0.0, double e_per_mm = 0.0) +{ + GCodeProcessor proc; + run_processor(proc, make_junction_config(flavor, corner_velocity, junction_deviation), + corner_gcode(turn_deg, orientation_deg, e_per_mm).c_str()); + return corner_speed(proc.get_result()); +} + +} // namespace + +TEST_CASE("Klipper corners are planned with junction deviation derived from the square corner velocity", + "[GCodeTiming][JunctionDeviation]") +{ + // jd = scv^2 * (sqrt(2) - 1) / max_accel, then v^2 = jd * accel * sin(t/2) / (1 - sin(t/2)). + // The acceleration cancels: the corner speed depends only on the scv and the angle. + const double scv = 5.0; + + SECTION("a right angle is taken at exactly the square corner velocity") { + // sin(t/2) = sqrt(0.5) at 90 degrees, so v == scv -- the definition of the square corner + // velocity, and what makes the mapping above the right one. + REQUIRE_THAT(planned_corner_speed(gcfKlipper, scv, 0.0, 90.0), Catch::Matchers::WithinRel(scv, 0.02)); + } + + SECTION("a shallow corner is taken far faster than the per-axis jerk model allows") { + // 6 degrees: sin(t/2) = cos(3 deg), so v = 5 * sqrt((sqrt(2) - 1) * 728.68) = 86.9mm/s. Per-axis + // jerk ignores the angle and caps the velocity *change* (2v*sin(3 deg)), giving 47.8mm/s. + const double jd_speed = planned_corner_speed(gcfKlipper, scv, 0.0, 6.0); + const double jerk_speed = planned_corner_speed(gcfMarlinLegacy, scv, 0.0, 6.0); + REQUIRE_THAT(jd_speed, Catch::Matchers::WithinRel(86.87, 0.02)); + REQUIRE_THAT(jerk_speed, Catch::Matchers::WithinRel(47.75, 0.02)); + } +} + +TEST_CASE("Junction deviation limits a corner by its angle alone, not by its orientation", + "[GCodeTiming][JunctionDeviation]") +{ + // The four-lobed ripple on circular walls is per-axis jerk being anisotropic: a velocity change + // lying on an axis gets sqrt(2) less headroom than the same change on the diagonal. + const double scv = 5.0; + const double turn = 6.0; + + SECTION("Klipper plans both orientations identically") { + const double on_axis = planned_corner_speed(gcfKlipper, scv, 0.0, turn, 0.0); + const double diagonal = planned_corner_speed(gcfKlipper, scv, 0.0, turn, 45.0); + REQUIRE(on_axis > 0.0); + REQUIRE_THAT(diagonal, Catch::Matchers::WithinRel(on_axis, 0.02)); + } + + SECTION("the classic jerk model keeps its orientation dependence") { + const double on_axis = planned_corner_speed(gcfMarlinLegacy, scv, 0.0, turn, 0.0); + const double diagonal = planned_corner_speed(gcfMarlinLegacy, scv, 0.0, turn, 45.0); + REQUIRE(on_axis > 0.0); + REQUIRE(diagonal / on_axis > 1.2); + } +} + +TEST_CASE("Junction deviation is only used where the firmware actually plans with it", + "[GCodeTiming][JunctionDeviation]") +{ + const double jerk = 5.0; + + SECTION("Marlin 2 with M205 J disabled keeps the classic jerk planning") { + // machine_max_junction_deviation == 0 is how a Marlin 2 printer says it runs classic jerk. + const double classic = planned_corner_speed(gcfMarlinLegacy, jerk, 0.0, 90.0); + REQUIRE(classic > 0.0); + REQUIRE_THAT(planned_corner_speed(gcfMarlinFirmware, jerk, 0.0, 90.0), + Catch::Matchers::WithinRel(classic, 1e-4)); + } + + SECTION("Marlin 2 with M205 J enabled switches to junction deviation") { + // sqrt(1000 * 0.05 * 2.4142136) = 11.0mm/s, independent of the jerk values it no longer reads. + REQUIRE_THAT(planned_corner_speed(gcfMarlinFirmware, jerk, 0.05, 90.0), + Catch::Matchers::WithinRel(10.99, 0.02)); + } + + SECTION("machines without junction deviation are untouched by the jerk values it would ignore") { + // A flavor that never enters the junction deviation path must ignore the setting entirely. + const double without = planned_corner_speed(gcfMarlinLegacy, jerk, 0.0, 90.0); + REQUIRE_THAT(planned_corner_speed(gcfMarlinLegacy, jerk, 0.05, 90.0), + Catch::Matchers::WithinRel(without, 1e-4)); + } +} + +TEST_CASE("How fast a corner is taken does not depend on how much is extruded through it", + "[GCodeTiming][JunctionDeviation]") +{ + // The junction cosine is taken over XYZE, so the direction vectors have to be unit length or the + // E term makes the two paths look more parallel than they are and the corner comes out too fast, + // the more so the higher the flow. Marlin normalizes over XYZE on any extruding move + // (planner.cpp, esteps > 0) and Klipper leaves E out of the cosine altogether + // (toolhead.py::Move.calc_junction); on both, this corner is planned by its geometry alone. + const double scv = 5.0; + const double turn = 6.0; + const double geometric = planned_corner_speed(gcfKlipper, scv, 0.0, turn); + REQUIRE(geometric > 0.0); + + // 0.029mm/mm is an ordinary 0.42 x 0.2 line on 1.75mm filament; 0.1 is a fat large-nozzle one. + // Unnormalized these came out at 94.4 and 150.0mm/s against a geometric 86.9. + for (double e_per_mm : {0.029, 0.1}) + REQUIRE_THAT(planned_corner_speed(gcfKlipper, scv, 0.0, turn, 0.0, e_per_mm), + Catch::Matchers::WithinRel(geometric, 0.02)); + + SECTION("and the same holds on Marlin 2") { + const double marlin = planned_corner_speed(gcfMarlinFirmware, scv, 0.05, turn); + REQUIRE(marlin > 0.0); + REQUIRE_THAT(planned_corner_speed(gcfMarlinFirmware, scv, 0.05, turn, 0.0, 0.029), + Catch::Matchers::WithinRel(marlin, 0.02)); + } +} diff --git a/tests/fff_print/test_mixed_filament.cpp b/tests/fff_print/test_mixed_filament.cpp new file mode 100644 index 0000000000..25b426c210 --- /dev/null +++ b/tests/fff_print/test_mixed_filament.cpp @@ -0,0 +1,324 @@ +#include <catch2/catch_all.hpp> + +#include "libslic3r/GCode/ToolOrdering.hpp" +#include "libslic3r/MultiNozzleUtils.hpp" +#include "libslic3r/Print.hpp" + +#include "test_helpers.hpp" + +using namespace Slic3r; +using namespace Slic3r::Test; + +namespace { + +// Two physical filaments plus one mixed slot (config index 2, 1-based id 3) blending them 60/40. +// The mixed arrays are parallel to filament_colour and must be sized to the filament count. +// Note ConfigOptionBools deserializes on ',' while ConfigOptionStrings uses ';'. +DynamicPrintConfig mixed_config(bool sublayer_on, const char *ratios = "0.6,0.4") +{ + DynamicPrintConfig config = multifilament_config(3); + config.set_deserialize_strict({ + {"filament_is_mixed", "0,0,1"}, + {"filament_mixed_components", ";;1,2"}, + {"filament_mixed_sublayer_ratios", std::string(";;") + ratios}, + {"filament_mixed_gradient", "0,0,0"}, + {"filament_mixed_gradient_range", ";;"}, + {"filament_mixed_gradient_curve", ";;"}, + {"filament_mixed_gradient_per_part","0,0,0"}, + {"enable_mixed_color_sublayer", sublayer_on ? "1" : "0"}, + // Assign every region role to the mixed slot so it actually participates in slicing. + {"outer_wall_filament_id", "3"}, + {"inner_wall_filament_id", "3"}, + {"sparse_infill_filament_id", "3"}, + {"internal_solid_filament_id", "3"}, + {"top_surface_filament_id", "3"}, + {"bottom_surface_filament_id", "3"}, + }); + return config; +} + +// Total sub-layer groups and per-layer mixed-filament resolutions across the whole tool ordering. +void count_mixed(ToolOrdering &to, size_t &groups, size_t &resolutions) +{ + groups = resolutions = 0; + for (const LayerTools < : to.layer_tools()) { + groups += lt.mixed_sub_layer_groups.size(); + resolutions += lt.mixed_filament_resolution.size(); + } +} + +} // namespace + +TEST_CASE("enable_mixed_color_sublayer reaches the Print config", "[MixedFilament]") +{ + Print print; + Model model; + init_print({cube(20)}, print, model, mixed_config(true)); + + // The option lives in PrintConfig; if it did not survive Print::apply the slicer would + // silently fall back to the whole-layer path. + CHECK(print.config().enable_mixed_color_sublayer.value == true); + REQUIRE(print.config().filament_is_mixed.values.size() == 3); + CHECK(print.config().filament_is_mixed.values[2] == true); + REQUIRE(print.config().filament_mixed_components.values.size() == 3); + CHECK(print.config().filament_mixed_components.values[2] == "1,2"); +} + +TEST_CASE("Mixed filament splits layers into sub-layers when the option is on", "[MixedFilament]") +{ + Print print; + Model model; + init_print({cube(20)}, print, model, mixed_config(true)); + print.process(); + + ToolOrdering &to = const_cast<ToolOrdering &>(print.tool_ordering()); + REQUIRE(!to.layer_tools().empty()); + + size_t groups = 0, resolutions = 0; + count_mixed(to, groups, resolutions); + + INFO("layers=" << to.layer_tools().size() << " groups=" << groups); + CHECK(groups > 0); +} + +TEST_CASE("Mixed filament alternates whole layers when the option is off", "[MixedFilament]") +{ + Print print; + Model model; + init_print({cube(20)}, print, model, mixed_config(false)); + print.process(); + + ToolOrdering &to = const_cast<ToolOrdering &>(print.tool_ordering()); + REQUIRE(!to.layer_tools().empty()); + + size_t groups = 0, resolutions = 0; + count_mixed(to, groups, resolutions); + + // With splitting off the slot is realized by the deficit round-robin scheduler instead: + // no sub-layer groups, but a per-layer resolution to one physical component. + INFO("layers=" << to.layer_tools().size() << " resolutions=" << resolutions); + CHECK(groups == 0); + CHECK(resolutions > 0); +} + +TEST_CASE("Sub-layer splitting emits the scaled sub-heights into G-code", "[MixedFilament]") +{ + // layer_height 0.2 split 60/40 gives sub-layers of 0.12 and 0.08. The emitter reports the + // sub-height (not the nominal layer height) in the HEIGHT tag and scales flow to match. + DynamicPrintConfig config = mixed_config(true); + config.set_deserialize_strict({{"layer_height", "0.2"}, {"initial_layer_print_height", "0.2"}}); + + Print print; + Model model; + init_print({cube(20)}, print, model, config); + print.process(); + const std::string gc = Slic3r::Test::gcode(print); + + REQUIRE(!gc.empty()); + INFO("gcode bytes=" << gc.size()); + CHECK(gc.find(";HEIGHT:0.12") != std::string::npos); + CHECK(gc.find(";HEIGHT:0.08") != std::string::npos); +} + +TEST_CASE("Whole-layer mixing emits only the nominal layer height", "[MixedFilament]") +{ + DynamicPrintConfig config = mixed_config(false); + config.set_deserialize_strict({{"layer_height", "0.2"}, {"initial_layer_print_height", "0.2"}}); + + Print print; + Model model; + init_print({cube(20)}, print, model, config); + print.process(); + const std::string gc = Slic3r::Test::gcode(print); + + REQUIRE(!gc.empty()); + // No sub-layer split, so the 60/40 sub-heights must never appear. + CHECK(gc.find(";HEIGHT:0.12") == std::string::npos); + CHECK(gc.find(";HEIGHT:0.08") == std::string::npos); +} + +TEST_CASE("By-object prints without mixed filaments keep their used-filament set", "[MixedFilament]") +{ + // With no mixed slot the by-object bookkeeping stays plain: object 2 prints with filament 2, + // so both filaments are used and no mixed filament is reported. + DynamicPrintConfig config = multifilament_config(2, {{"print_sequence", "by object"}}); + const std::vector<std::vector<ConfigBase::SetDeserializeItem>> overrides{ {}, { {"extruder", "2"} } }; + + Print print; + Model model; + init_print(std::vector<TriangleMesh>{cube(20), cube(20)}, print, model, config, &overrides); + REQUIRE(print.objects().size() == 2); + print.process(); + + CHECK(print.get_slice_used_filaments(false) == std::vector<unsigned int>{0, 1}); + CHECK(print.get_slice_used_filaments(true) == std::vector<unsigned int>{0, 1}); + CHECK(print.get_slice_used_mixed_filaments().empty()); +} + +TEST_CASE("By-layer prints record a mixed slot's components and the slot itself", "[MixedFilament]") +{ + // Control for the by-object case below: the by-layer path publishes the physical + // components (0-based 0 and 1) as used filaments and the mixed slot (config index 2) as + // a used mixed filament. By-object prints must report exactly the same. + Print print; + Model model; + init_print({cube(20)}, print, model, mixed_config(false)); + print.process(); + + CHECK(print.get_slice_used_filaments(false) == std::vector<unsigned int>{0, 1}); + CHECK(print.get_slice_used_mixed_filaments() == std::vector<unsigned int>{2}); +} + +TEST_CASE("By-object prints expand a mixed slot to its components in the slice bookkeeping", "[MixedFilament]") +{ + // Sequential prints build their filament lists from unsorted per-object orderings, which + // still carry the virtual slot (config index 2). The slice-used sets and the published + // grouping result must see the physical components 0 and 1 instead, and the slot itself + // must still be reported as a used mixed filament — exactly what the by-layer path yields. + DynamicPrintConfig config = mixed_config(false); + config.set_deserialize_strict({{"print_sequence", "by object"}}); + + Print print; + Model model; + init_print({cube(20), cube(20)}, print, model, config); + REQUIRE(print.objects().size() == 2); + print.process(); + + const std::vector<unsigned int> components{0, 1}; + CHECK(print.get_slice_used_filaments(false) == components); + CHECK(print.get_slice_used_filaments(true) == components); + CHECK(print.get_slice_used_mixed_filaments() == std::vector<unsigned int>{2}); + + auto group_result = print.get_layered_nozzle_group_result(); + REQUIRE(group_result != nullptr); + CHECK(group_result->get_used_filaments() == components); +} + +TEST_CASE("By-object G-code lists a mixed slot's components in the filament header", "[MixedFilament]") +{ + DynamicPrintConfig config = mixed_config(false); + config.set_deserialize_strict({{"print_sequence", "by object"}}); + + Print print; + Model model; + init_print({cube(20), cube(20)}, print, model, config); + const std::string gc = Slic3r::Test::gcode(print); + + REQUIRE(!gc.empty()); + // The header names the filaments that must be loaded (components 1 and 2, 1-based), + // never the virtual slot 3. + CHECK(gc.find("; filament: 1,2\n") != std::string::npos); + CHECK(gc.find("; filament: 3") == std::string::npos); +} + +TEST_CASE("Print::validate rejects a mixed filament as the wipe tower filament", "[MixedFilament]") +{ + // The validate backstop refuses a mixed (virtual) slot as the wipe tower filament; the GUI hides + // the slot from that option. Two cubes on physical filaments 1 and 2 make the tower real, and the + // region roles mixed_config() points at the slot are reset so only the tower uses it. + DynamicPrintConfig config = mixed_config(false); + config.set_deserialize_strict({ + {"enable_prime_tower", "1"}, + {"wipe_tower_x", "50"}, // inside the 200x200 test bed + {"wipe_tower_y", "50"}, // (the default y, 220, is not) + {"layer_change_gcode", "G92 E0\n"}, // validate() relative-E reset, as in test_print.cpp's build_cubes + {"outer_wall_filament_id", "0"}, + {"inner_wall_filament_id", "0"}, + {"sparse_infill_filament_id", "0"}, + {"internal_solid_filament_id", "0"}, + {"top_surface_filament_id", "0"}, + {"bottom_surface_filament_id", "0"}, + }); + const std::vector<std::vector<ConfigBase::SetDeserializeItem>> overrides{ { {"extruder", "1"} }, { {"extruder", "2"} } }; + + SECTION("a physical wipe tower filament validates") { + config.set_deserialize_strict({{"wipe_tower_filament", "2"}}); + Print print; + Model model; + init_print(std::vector<TriangleMesh>{cube(20), cube(20)}, print, model, config, &overrides); + REQUIRE(print.has_wipe_tower()); + const StringObjectException err = print.validate(); + INFO(err.string); + CHECK(err.string.empty()); + } + + SECTION("the mixed slot is refused") { + config.set_deserialize_strict({{"wipe_tower_filament", "3"}}); + Print print; + Model model; + init_print(std::vector<TriangleMesh>{cube(20), cube(20)}, print, model, config, &overrides); + REQUIRE(print.has_wipe_tower()); + const StringObjectException err = print.validate(); + CHECK_FALSE(err.string.empty()); + CHECK(err.opt_key == "wipe_tower_filament"); + } +} + +TEST_CASE("Print::validate warns when a gradient mixed filament is used without sublayer mixing", "[MixedFilament]") +{ + // A gradient mixed filament only renders its gradient with the process option enabled; without + // it ToolOrdering prints one whole component per layer and the gradient is dropped silently, + // so validate() warns whenever the slot actually takes part in the print. The layer-change + // reset avoids an unrelated relative-extrusion warning, as in the wipe tower test above. + DynamicPrintConfig config = mixed_config(false); + config.set_deserialize_strict({ + {"filament_mixed_gradient", "0,0,1"}, + {"layer_change_gcode", "G92 E0\n"}, + }); + + auto count_opt = [](Print &print, const char *opt_key) { + std::vector<StringObjectException> warnings; + print.validate(&warnings); + return std::count_if(warnings.begin(), warnings.end(), + [&](const StringObjectException &w) { return w.opt_key == opt_key; }); + }; + + SECTION("gradient slot used, sublayer mixing off") { + Print print; + Model model; + init_print({cube(20)}, print, model, config); + std::vector<StringObjectException> warnings; + const StringObjectException err = print.validate(&warnings); + CHECK(err.string.empty()); + const auto it = std::find_if(warnings.begin(), warnings.end(), [](const StringObjectException &w) { + return w.opt_key == "enable_mixed_color_sublayer"; + }); + REQUIRE(it != warnings.end()); + CHECK(it->is_warning); + CHECK(std::count_if(warnings.begin(), warnings.end(), [](const StringObjectException &w) { + return w.opt_key == "enable_mixed_color_sublayer"; + }) == 1); + } + + SECTION("sublayer mixing on") { + config.set_deserialize_strict({{"enable_mixed_color_sublayer", "1"}}); + Print print; + Model model; + init_print({cube(20)}, print, model, config); + CHECK(count_opt(print, "enable_mixed_color_sublayer") == 0); + } + + SECTION("gradient flag off") { + config.set_deserialize_strict({{"filament_mixed_gradient", "0,0,0"}}); + Print print; + Model model; + init_print({cube(20)}, print, model, config); + CHECK(count_opt(print, "enable_mixed_color_sublayer") == 0); + } + + SECTION("mixed slot not used") { + config.set_deserialize_strict({ + {"outer_wall_filament_id", "0"}, + {"inner_wall_filament_id", "0"}, + {"sparse_infill_filament_id", "0"}, + {"internal_solid_filament_id", "0"}, + {"top_surface_filament_id", "0"}, + {"bottom_surface_filament_id", "0"}, + }); + Print print; + Model model; + const std::vector<std::vector<ConfigBase::SetDeserializeItem>> overrides{{{ "extruder", "1" }}}; + init_print(std::vector<TriangleMesh>{cube(20)}, print, model, config, &overrides); + CHECK(count_opt(print, "enable_mixed_color_sublayer") == 0); + } +} diff --git a/tests/fff_print/test_multifilament.cpp b/tests/fff_print/test_multifilament.cpp index f33a8e1e61..484b7fb68a 100644 --- a/tests/fff_print/test_multifilament.cpp +++ b/tests/fff_print/test_multifilament.cpp @@ -1,12 +1,25 @@ #include <catch2/catch_all.hpp> +#include "libslic3r/GCode/GCodeProcessor.hpp" #include "libslic3r/GCodeReader.hpp" #include "test_helpers.hpp" +#include "test_utils.hpp" +#include <algorithm> #include <cctype> +#include <cmath> +#include <cstdio> +#include <cstdlib> +#include <fstream> +#include <limits> +#include <optional> #include <set> +#include <sstream> #include <string> +#include <string_view> +#include <utility> +#include <vector> using namespace Slic3r; using namespace Slic3r::Test; @@ -27,6 +40,210 @@ static std::set<int> tools_for_role(const std::string& gcode, const std::string& return tools; } +// X where the nozzle sits while each tagged _WAIT_FOR_TEMP_ON_WIPE_TOWER M109 blocks: +// the nearest preceding G1 carrying an X (the park travel emitted just before the wait). +static std::vector<double> wait_park_xs(const std::string& gcode) +{ + std::vector<std::string> lines; + std::istringstream stream(gcode); + for (std::string line; std::getline(stream, line);) + lines.emplace_back(std::move(line)); + std::vector<double> xs; + for (size_t i = 0; i < lines.size(); ++i) { + if (lines[i].rfind("M109", 0) != 0 || lines[i].find("_WAIT_FOR_TEMP_ON_WIPE_TOWER") == std::string::npos) + continue; + for (size_t j = i; j-- > 0;) { + if (lines[j].rfind("G1 ", 0) != 0) + continue; + const size_t x_pos = lines[j].find('X'); + if (x_pos == std::string::npos) + continue; + xs.push_back(std::stod(lines[j].substr(x_pos + 1))); + break; + } + } + return xs; +} + +// Estimated print time at each 1-based line of an exported G-code file, from a second +// GCodeProcessor pass over it. MoveVertex::time is the duration of one move and gcode_id is the +// line it came from (already rebased past the M73 insertions), so the running sum before the first +// move of a line is the elapsed time at that line. The file carries its own config footer, so +// process_file configures the processor -- including the shared s_IsBBLPrinter static that other +// tests in this binary mutate -- from the settings the export itself used. +static std::vector<double> elapsed_time_by_line(const std::string& gcode) +{ + ScopedTemporaryFile temp_gcode(".gcode"); + { + std::ofstream os(temp_gcode.string()); + os << gcode; + } + GCodeProcessor processor; + processor.process_file(temp_gcode.string()); + + constexpr size_t NORMAL = size_t(PrintEstimatedStatistics::ETimeMode::Normal); + const size_t n_lines = size_t(std::count(gcode.begin(), gcode.end(), '\n')) + 2; + std::vector<double> elapsed(n_lines, 0.); + double running = 0.; + size_t next = 0; + for (const auto& move : processor.get_result().moves) { + const size_t id = std::min<size_t>(move.gcode_id, n_lines - 1); + while (next <= id) + elapsed[next++] = running; + running += move.time[NORMAL]; + } + while (next < n_lines) + elapsed[next++] = running; + return elapsed; +} + +// The temperature-relevant projection of `gcode`: every M104/M109/Tn line, plus the toolchange and +// priming markers that anchor them, in order. A preheat -- an M104 the GCodeProcessor backtrace +// inserts mid-object, outside any block, naming a tool other than the one currently loaded -- also +// carries "lead <n>s", the estimated time from there to the tool change it heats for, which is the +// property preheat_time controls. No other temperature command gets one: for an M104 retargeting +// the active tool (the first-layer-to-other-layers bump) or one inside a block, the distance to the +// next Tn is a layer time or a handful of moves and says nothing about preheat_time. Everything +// else is dropped, so the trace does not move when travel, tower geometry or line numbering do. +static std::vector<std::string> temperature_trace(const std::string& gcode) +{ + std::vector<std::string> lines; + std::istringstream stream(gcode); + for (std::string line; std::getline(stream, line);) { + line.erase(0, line.find_first_not_of(" \t")); + while (!line.empty() && (line.back() == '\r' || line.back() == ' ' || line.back() == '\t')) + line.pop_back(); + lines.emplace_back(std::move(line)); + } + const std::vector<double> elapsed = elapsed_time_by_line(gcode); + + const auto is_tool = [](const std::string& l) { return l.size() >= 2 && l[0] == 'T' && std::isdigit((unsigned char) l[1]); }; + const auto is_temp = [](const std::string& l) { return l.rfind("M104", 0) == 0 || l.rfind("M109", 0) == 0; }; + const auto marker = [](const std::string& l) -> const char* { + for (const char* m : { "; CP TOOLCHANGE START", "; CP TOOLCHANGE END", "; CP PRIMING START", "; CP PRIMING END" }) + if (l.find(m) != std::string::npos) + return m; + return nullptr; + }; + + // Tool a "T<n>" line, or the "T<n>" argument of an M104, names -- or -1 when it names none. + const auto tool_of = [&is_tool](const std::string& l) -> int { + size_t t = std::string::npos; // index of the 'T' + if (is_tool(l)) + t = 0; + else if (l.rfind("M104", 0) == 0 && l.find(" T") != std::string::npos) + t = l.find(" T") + 1; + if (t == std::string::npos || t + 1 >= l.size() || !std::isdigit((unsigned char) l[t + 1])) + return -1; + return std::stoi(l.substr(t + 1)); + }; + + std::vector<std::string> trace; + bool in_block = false; + int current_tool = -1; + for (size_t i = 0; i < lines.size(); ++i) { + if (const char* m = marker(lines[i])) { + in_block = std::string(m).find("START") != std::string::npos; + trace.emplace_back(m); // the marker alone: some carry a trailing tool id, some do not + } else if (is_tool(lines[i]) || is_temp(lines[i])) { + std::string entry = lines[i]; + const int named = tool_of(lines[i]); + if (!in_block && lines[i].rfind("M104", 0) == 0 && current_tool != -1 && named != -1 && named != current_tool) { + size_t tn = i; + while (tn < lines.size() && !is_tool(lines[tn])) + ++tn; + if (tn < lines.size()) { + char lead[32]; + std::snprintf(lead, sizeof(lead), "\tlead %.1fs", elapsed[tn + 1] - elapsed[i + 1]); + entry += lead; + } + } + if (is_tool(lines[i])) + current_tool = named; + trace.emplace_back(std::move(entry)); + } + } + return trace; +} + +// "M104 S240 T0 ; preheat T0 time: 31s<TAB>lead 30.9s" carries the same quantity twice, and both +// vary by toolchain: the backtrace picks the first line at least preheat_time out, so a sub-tenth +// difference in the estimate selects a neighbouring move and "lead" steps by that move's duration. +// Tolerate "lead", still far below the tens of seconds a displaced preheat would shift it. Check +// "time:" against its own entry's "lead" instead of across runs -- being a rounding of it, that +// still catches a change in how it is derived without tracking the absolute estimate. +static constexpr double TRACE_TIME_TOLERANCE_S = 1.5; +static constexpr double TRACE_ROUNDING_SLACK_S = 0.05; // correct rounding keeps |time - lead| <= 0.5 + +struct TraceEntry +{ + std::string text; // timing values replaced by a placeholder + std::optional<double> time_s; + std::optional<double> lead_s; +}; + +static TraceEntry parse_trace_entry(const std::string& entry) +{ + TraceEntry out; + std::string text = entry; + + // Split off the tail only when it really is a "lead <n>s", so an unexpected one still compares. + const size_t tab = text.find('\t'); + if (tab != std::string::npos) { + const std::string tail = text.substr(tab + 1); // "lead 30.2s" + const size_t sp = tail.find(' '); + if (sp != std::string::npos && sp + 1 < tail.size() + && std::isdigit(static_cast<unsigned char>(tail[sp + 1]))) { + out.lead_s = std::stod(tail.substr(sp + 1)); + text.erase(tab); + } + } + + static constexpr std::string_view k_time = "time: "; + const size_t at = text.find(k_time); + // Require a digit first: a dots-only run would otherwise reach std::stod and throw. + if (at != std::string::npos && at + k_time.size() < text.size() + && std::isdigit(static_cast<unsigned char>(text[at + k_time.size()]))) { + const size_t first = at + k_time.size(); + size_t last = first; + while (last < text.size() && (std::isdigit(static_cast<unsigned char>(text[last])) || text[last] == '.')) + ++last; + out.time_s = std::stod(text.substr(first, last - first)); + text.replace(first, last - first, "<n>"); // surrounding text, incl. the "s", still compared + } + + out.text = std::move(text); + return out; +} + +static bool timings_match(const std::optional<double>& a, const std::optional<double>& b) +{ + if (a.has_value() != b.has_value()) + return false; + return !a.has_value() || std::abs(*a - *b) <= TRACE_TIME_TOLERANCE_S; +} + +// "time:" must be its own entry's "lead" rounded to a whole second. +static bool time_is_rounded_lead(const TraceEntry& e) +{ + if (!e.time_s.has_value() || !e.lead_s.has_value()) + return true; // nothing to cross-check + return std::abs(*e.time_s - *e.lead_s) <= 0.5 + TRACE_ROUNDING_SLACK_S; +} + +// `a` is the slice under test, `b` the recorded golden. +static bool trace_entries_match(const std::string& a, const std::string& b) +{ + const auto x = parse_trace_entry(a); + const auto y = parse_trace_entry(b); + if (x.text != y.text) + return false; + // A field appearing or disappearing is a real change even though the values are tolerated. + if (x.time_s.has_value() != y.time_s.has_value()) + return false; + return timings_match(x.lead_s, y.lead_s) && time_is_rounded_lead(x); +} + // Tool index = filament id - 1; brim and skirt follow the wall filament. TEST_CASE("Each feature prints with its assigned filament", "[MultiFilament]") { @@ -86,6 +303,399 @@ TEST_CASE("Per-object wall filament override is honored", "[MultiFilament]") CHECK(tools_for_role(gcode, "infill") == std::set<int>{ 0 }); // infill not overridden: stays on F1 } +// With wait_for_temp_on_wipe_tower the blocking M109 moves from right after the Tn command to +// a stop point parked beside the wipe tower (heat-up drool falls next to the tower, not onto +// its top): tagged with _WAIT_FOR_TEMP_ON_WIPE_TOWER, after the toolchange and before the +// repositioning move and the first extrusion of the purge. The restore that used to block there +// demotes to a non-blocking M104 and moves ahead of the Tn, so the incoming tool heats up over +// the change itself. Ordering and the off-tower stop are the contract here. +TEST_CASE("Toolchange temperature wait moves to the wipe tower when enabled", "[MultiFilament]") +{ + const bool wait_on_tower = GENERATE(false, true); + DYNAMIC_SECTION("wait_for_temp_on_wipe_tower " << (wait_on_tower ? 1 : 0)) { + const std::string gcode = slice_with_object_overrides( + { cube(20), cube(20) }, + multifilament_config(2, { + { "nozzle_diameter", "0.4,0.4" }, + { "printer_extruder_id", "1,2" }, + { "printer_extruder_variant", "Direct Drive Standard,Direct Drive Standard" }, + { "extruder_printable_height", "0,0" }, + { "single_extruder_multi_material", 0 }, + { "enable_prime_tower", 1 }, + { "prime_tower_width", 35 }, + { "wipe_tower_x", "50" }, + { "wipe_tower_y", "50" }, + { "ooze_prevention", 1 }, + { "standby_temperature_delta", -40 }, + // The post-processor's own preheat pass also inserts an M104 for the incoming + // filament ahead of the Tn; switch it off so the temperature commands under test + // are the only ones in the toolchange block. + { "preheat_time", 0 }, + { "wait_for_temp_on_wipe_tower", wait_on_tower ? 1 : 0 }, + }), + // One filament per object -> a toolchange on every layer. Assigned at the object + // level: the used-filament count that gates the prime tower is derived from + // object/volume configs on the harness's single apply (region filament ids such + // as sparse_infill_filament_id are not counted there and the tower would be + // silently disabled). + { { { "extruder", 1 } }, { { "extruder", 2 } } }); + + // Split into lines and scan the "; CP TOOLCHANGE START".."; CP TOOLCHANGE END" blocks. + std::vector<std::string> lines; + std::istringstream gcode_stream(gcode); + for (std::string line; std::getline(gcode_stream, line);) + lines.emplace_back(std::move(line)); + const auto is_tool_line = [](const std::string& l) { return l.size() >= 2 && l[0] == 'T' && std::isdigit((unsigned char)l[1]); }; + const auto is_m109_line = [](const std::string& l) { return l.rfind("M109", 0) == 0; }; + // A non-blocking set-temperature naming one specific tool, e.g. "M104 S255 T1". + const auto is_m104_for_tool = [](const std::string& l, int tool) { + if (l.rfind("M104", 0) != 0) + return false; + const std::string token = " T" + std::to_string(tool); + const size_t at = l.find(token); + return at != std::string::npos && !std::isdigit((unsigned char)l[at + token.size()]); + }; + const auto is_tagged_wait = [](const std::string& l) { return l.find("_WAIT_FOR_TEMP_ON_WIPE_TOWER") != std::string::npos; }; + const auto is_extruding = [](const std::string& l) { + if (l.rfind("G1 ", 0) != 0) + return false; + const size_t e = l.find(" E"); + return e != std::string::npos && l.find_first_of("XY") != std::string::npos && l[e + 2] != '-'; + }; + + int checked_blocks = 0; + for (size_t i = 0; i < lines.size(); ++i) { + if (lines[i].find("; CP TOOLCHANGE START") == std::string::npos) + continue; + size_t block_end = i; + while (block_end < lines.size() && lines[block_end].find("; CP TOOLCHANGE END") == std::string::npos) + ++block_end; + size_t tool_line = block_end; + for (size_t j = i; j < block_end; ++j) + if (is_tool_line(lines[j])) { tool_line = j; break; } + if (tool_line == block_end) + continue; // final unload block, no toolchange + ++checked_blocks; + + // Where the incoming tool's target temperature is raised, relative to its Tn. + const int new_tool = std::stoi(lines[tool_line].substr(1)); + size_t preheat = tool_line, restore = block_end; + for (size_t j = i; j < tool_line; ++j) + if (is_m104_for_tool(lines[j], new_tool)) { preheat = j; break; } + for (size_t j = tool_line + 1; j < block_end; ++j) + if (is_m104_for_tool(lines[j], new_tool)) { restore = j; break; } + + size_t tagged_wait = block_end, untagged_m109 = block_end, first_extrusion = block_end; + for (size_t j = tool_line + 1; j < block_end; ++j) { + if (is_m109_line(lines[j]) && tagged_wait == block_end && is_tagged_wait(lines[j])) + tagged_wait = j; + if (is_m109_line(lines[j]) && untagged_m109 == block_end && !is_tagged_wait(lines[j])) + untagged_m109 = j; + if (first_extrusion == block_end && is_extruding(lines[j])) + first_extrusion = j; + } + INFO("toolchange block at line " << i + 1); + if (wait_on_tower) { + // The only blocking wait is the tagged one, parked beside the tower before the purge. + REQUIRE(tagged_wait < block_end); + CHECK(untagged_m109 == block_end); + // The target is raised ahead of the toolchange, so the incoming tool heats up + // while it is picked up, and nothing sets it again afterwards. + CHECK(preheat < tool_line); + CHECK(restore == block_end); + REQUIRE(first_extrusion < block_end); + CHECK(tagged_wait < first_extrusion); + // The travel preceding the wait parks outside the tower footprint. The tower + // auto-sizes, so derive its extent from the purge extrusions of this block. + size_t stop_line = block_end; + for (size_t j = tagged_wait; j-- > tool_line;) + if (lines[j].rfind("G1 ", 0) == 0 && lines[j].find('X') != std::string::npos) { stop_line = j; break; } + REQUIRE(stop_line < block_end); + const double stop_x = std::stod(lines[stop_line].substr(lines[stop_line].find('X') + 1)); + double purge_min_x = std::numeric_limits<double>::max(), purge_max_x = std::numeric_limits<double>::lowest(); + for (size_t j = tagged_wait; j < block_end; ++j) { + const size_t x_pos = lines[j].find('X'); + if (!is_extruding(lines[j]) || x_pos == std::string::npos) + continue; + const double x = std::stod(lines[j].substr(x_pos + 1)); + purge_min_x = std::min(purge_min_x, x); + purge_max_x = std::max(purge_max_x, x); + } + REQUIRE(purge_min_x <= purge_max_x); + INFO("stop travel: " << lines[stop_line] << " purge x range: " << purge_min_x << ".." << purge_max_x); + const bool beside_tower = stop_x < purge_min_x - 0.5 || stop_x > purge_max_x + 0.5; + CHECK(beside_tower); + } else { + // Stock behavior: the blocking wait follows the toolchange command directly, and + // nothing raises the incoming tool's target before it. + REQUIRE(untagged_m109 < block_end); + CHECK(tagged_wait == block_end); + CHECK(preheat == tool_line); + if (first_extrusion < block_end) + CHECK(untagged_m109 < first_extrusion); + } + i = block_end; + } + REQUIRE(checked_blocks > 0); + if (!wait_on_tower) + CHECK(gcode.find("_WAIT_FOR_TEMP_ON_WIPE_TOWER") == std::string::npos); + } +} + +// Priming runs before the first layer is set up, so set_extruder sees no layer at all: its +// on_first_layer() test is false and print_z is the initial layer height rather than 0. The +// tower nonetheless blocks on the first layer temperature there, so the pre-heat raised ahead +// of each priming Tn has to name that same temperature — pre-heating to the "other layers" +// value instead leaves the tagged M109 asking the firmware to cool back down before the +// priming lines are extruded. +TEST_CASE("Wipe tower priming pre-heats to the first layer temperature", "[MultiFilament]") +{ + const std::string gcode = slice_with_object_overrides( + { cube(20), cube(20) }, + multifilament_config(2, { + { "nozzle_diameter", "0.4,0.4" }, + { "printer_extruder_id", "1,2" }, + { "printer_extruder_variant", "Direct Drive Standard,Direct Drive Standard" }, + { "extruder_printable_height", "0,0" }, + { "single_extruder_multi_material", 0 }, + { "single_extruder_multi_material_priming", 1 }, + { "enable_prime_tower", 1 }, + { "prime_tower_width", 35 }, + { "wipe_tower_x", "50" }, + { "wipe_tower_y", "50" }, + { "preheat_time", 0 }, // see the wait test above + // Distinct enough that picking the wrong one is unambiguous. + { "nozzle_temperature_initial_layer", "215,215" }, + { "nozzle_temperature", "240,240" }, + { "wait_for_temp_on_wipe_tower", 1 }, + }), + { { { "extruder", 1 } }, { { "extruder", 2 } } }); + + std::vector<std::string> lines; + std::istringstream gcode_stream(gcode); + for (std::string line; std::getline(gcode_stream, line);) + lines.emplace_back(std::move(line)); + // Temperature of an M104/M109, or -1 when the line is neither. + const auto temp_of = [](const std::string& l) { + if (l.rfind("M104", 0) != 0 && l.rfind("M109", 0) != 0) + return -1; + const size_t s = l.find('S'); + return s == std::string::npos ? -1 : std::stoi(l.substr(s + 1)); + }; + + size_t start = lines.size(), end = lines.size(); + for (size_t i = 0; i < lines.size(); ++i) { + if (start == lines.size() && lines[i].find("; CP PRIMING START") != std::string::npos) + start = i; + else if (start < lines.size() && lines[i].find("; CP PRIMING END") != std::string::npos) { + end = i; + break; + } + } + REQUIRE(start < end); + + int checked_waits = 0; + for (size_t i = start; i < end; ++i) { + if (lines[i].find("_WAIT_FOR_TEMP_ON_WIPE_TOWER") == std::string::npos) + continue; + ++checked_waits; + INFO("priming wait at line " << i + 1 << ": " << lines[i]); + CHECK(temp_of(lines[i]) == 215); // the tower waits on the first layer temperature + // The most recent set-temperature before it is the pre-heat, and must agree with it. + int preheat = -1; + for (size_t j = i; j-- > start;) + if ((preheat = temp_of(lines[j])) != -1) + break; + CHECK(preheat == 215); + } + REQUIRE(checked_waits > 0); // the feature under test is active +} + +// The temperature-wait park picks its side of the tower by testing bed containment with the +// tower position at psWipeTower generation time, while WipeTowerIntegration shifts the cached +// moves by the CURRENT position at export. Moving the tower normally invalidates only +// psSkirtBrim (tower gcode is position-independent), but the park makes it bed-relative, so a +// GUI-style move-and-reslice on the same Print must regenerate the tower — otherwise the stale +// park prints outside the bed. Contract: every tagged wait parks inside the printable area. +TEST_CASE("Wipe tower temperature-wait park is regenerated when the tower moves", "[MultiFilament]") +{ + // Two objects, one filament each: a toolchange (and a tagged wait) on every layer, like + // the wait test above — but on a single-extruder machine profile: the synthetic + // dual-extruder keys would drag in the extruder-variant expansion, which is not + // idempotent on the default machine profile and would pollute the re-apply diff below. + // Rectangle wall and no brim keep the tower-local footprint inside [0, 35], so the park + // sits at the generator's 2mm side gap: local -2 or 37. + DynamicPrintConfig config = multifilament_config(2, { + { "single_extruder_multi_material", 0 }, + { "enable_prime_tower", 1 }, + { "prime_tower_width", 35 }, + { "wipe_tower_wall_type", "rectangle" }, // the default rib bulges past the width + { "prime_tower_brim_width", 0 }, // the default 3 widens the first-layer envelope + { "printable_area", "0x0,200x0,200x200,0x200" }, + { "wipe_tower_x", "0" }, + { "wipe_tower_y", "50" }, + { "ooze_prevention", 1 }, + { "standby_temperature_delta", -40 }, + { "wait_for_temp_on_wipe_tower", 1 }, + }); + // init_print force-sets this on its own copy; set it here too so the re-apply below + // diffs in wipe_tower_x ONLY — the exact GUI increment under test. + config.set_key_value("gcode_comments", new ConfigOptionBool(true)); + + Print print; + Model model; + const std::vector<std::vector<ConfigBase::SetDeserializeItem>> overrides{ + { { "extruder", 1 } }, { { "extruder", 2 } } }; // object-level, see the wait test above + init_print(std::vector<TriangleMesh>{ cube(20), cube(20) }, print, model, config, &overrides); + + const std::string at_edge = gcode(print); + const std::vector<double> at_edge_parks = wait_park_xs(at_edge); + REQUIRE(!at_edge_parks.empty()); // the feature under test is active + for (double x : at_edge_parks) { + INFO("wait park X " << x << " with the tower at x=0 on a 200mm bed"); + CHECK(x >= -0.05); + CHECK(x <= 200.05); + } + REQUIRE(print.is_step_done(psWipeTower)); + + // Move the tower to the right bed edge (164 + 35 = 199 keeps the body printable) and + // re-apply on the SAME Print, as the GUI does. Base the re-apply on the print's own + // resolved config so the diff is wipe_tower_x alone — re-applying the caller's config + // would also diff the apply-time extruder normalization write-backs, and those keys + // regenerate the tower for the wrong reason. The cached right-side park would export + // at 164 + 37 = 201, off the bed; regeneration clamps the park against the bed edge. + // Assemble the moved config exactly the way init_print assembled the first one — the + // apply-time normalization is only idempotent when both applies start from the same + // derivation, and any stray diff key would regenerate the tower for the wrong reason. + config.set_deserialize_strict({ { "wipe_tower_x", "164" } }); + DynamicPrintConfig moved_config = DynamicPrintConfig::full_print_config(); + moved_config.apply(config); + moved_config.set_key_value("gcode_comments", new ConfigOptionBool(true)); + print.apply(model, moved_config); + CHECK_FALSE(print.is_step_done(psWipeTower)); // the move must re-generate the tower + + const std::string moved = gcode(print); + const std::vector<double> moved_parks = wait_park_xs(moved); + REQUIRE(!moved_parks.empty()); // the waits must survive the re-slice + for (double x : moved_parks) { + INFO("wait park X " << x << " with the tower at x=164 on a 200mm bed"); + CHECK(x >= -0.05); + CHECK(x <= 200.05); + } +} + +// The flag-off half of the three tests above. Every site wait_for_temp_on_wipe_tower touches is +// guarded -- set_extruder's pre-toolchange preheat block and its post_toolchange skip, +// toolchange_Change's park, the interface-temp guard in WipeTower2::tool_change, and append_tcr2's +// tagged-M109 filter -- so with the option off the feature has to be inert and temperature emission +// has to stay exactly as it was before the option existed. That is pinned against a trace captured +// from main rather than against expectations written from the current code, which would be +// re-derived from the very code they are meant to guard. +// +// Note what main emits here, since it is easy to misread as a missing wait: with preheat_time set, +// the toolchange carries no blocking M109 at all. GCodeProcessor's backtrace moves the heat-up to +// an M104 preheat_time seconds earlier and demotes the in-place command, which is the entire point +// of preheating. The lead times below are what pin that placement. +TEST_CASE("Toolchange temperature commands are unchanged when the wipe tower wait is off", "[MultiFilament][Regression]") +{ + // 20x20x5 cubes at the default 0.2mm layer height are 25 layers, one filament each, so there is + // a toolchange -- and a preheat ahead of it -- on every layer. + const std::string gcode = slice_with_object_overrides( + { make_cube(20., 20., 5.), make_cube(20., 20., 5.) }, + multifilament_config(2, { + { "nozzle_diameter", "0.4,0.4" }, + { "printer_extruder_id", "1,2" }, + { "printer_extruder_variant", "Direct Drive Standard,Direct Drive Standard" }, + { "extruder_printable_height", "0,0" }, + { "single_extruder_multi_material", 0 }, + { "single_extruder_multi_material_priming", 1 }, // reaches toolchange_Change's priming path + { "enable_prime_tower", 1 }, + { "prime_tower_width", 35 }, + { "wipe_tower_x", "50" }, + { "wipe_tower_y", "50" }, + // GCodeProcessor::apply_config enables the preheat backtrace on + // ooze_prevention && preheat_time > 0 && !SEMM && filaments > 1. That is what puts an + // M104 preheat_time seconds ahead of every Tn, and it also gives set_extruder's + // standby/restore pair, which the option demotes and moves when it is on. + { "ooze_prevention", 1 }, + { "standby_temperature_delta", -40 }, + { "preheat_time", 30 }, + { "preheat_steps", 1 }, + // enable_tower_interface_features is deliberately left off: the interface temperature + // is observable only through a change_filament_gcode template that reads + // new_filament_temp, since append_tcr2 strips the tower's own M109 for it, and the + // default template here has none. The option's interface-temp guard is covered by the + // enabled-path tests above instead. + // + // Distinct enough that a wrong pick between the two is unambiguous in the trace. + { "nozzle_temperature_initial_layer", "215,215" }, + { "nozzle_temperature", "240,240" }, + { "wait_for_temp_on_wipe_tower", 0 }, + }), + // Object-level, so the used-filament count that gates the prime tower is derived from it. + { { { "extruder", 1 } }, { { "extruder", 2 } } }); + + const std::vector<std::string> trace = temperature_trace(gcode); + REQUIRE(trace.size() > 1); + CHECK(gcode.find("_WAIT_FOR_TEMP_ON_WIPE_TOWER") == std::string::npos); + + const std::string golden_path = std::string(TEST_DATA_DIR PATH_SEPARATOR "wipe_tower_temperature_trace_main.txt"); + + // Regenerate by appending this test and its helpers to the same file on main (dropping the + // wait_for_temp_on_wipe_tower key, which main's config does not know), rebuilding + // fff_print_tests there, running it with ORCA_UPDATE_WIPE_TOWER_TEMP_TRACE=1, copying the file + // it writes back here, and filling in the commit it was captured from. + if (std::getenv("ORCA_UPDATE_WIPE_TOWER_TEMP_TRACE") != nullptr) { + std::ofstream out(golden_path); + REQUIRE(out.good()); + out << "# Temperature and tool-change commands of a wait_for_temp_on_wipe_tower-off slice,\n" + "# captured from the main branch at <fill in the commit>. Regeneration is described\n" + "# at the test that reads this file: \"Toolchange temperature commands are unchanged\n" + "# when the wipe tower wait is off\" in tests/fff_print/test_multifilament.cpp.\n"; + for (const std::string& entry : trace) + out << entry << "\n"; + WARN("Rewrote " << golden_path << " from this run; it no longer reflects main."); + return; + } + + std::vector<std::string> golden; + { + std::ifstream in(golden_path); + INFO("reading " << golden_path); + REQUIRE(in.good()); + for (std::string line; std::getline(in, line);) { + if (!line.empty() && line.back() == '\r') + line.pop_back(); + if (!line.empty() && line[0] != '#') + golden.push_back(std::move(line)); + } + } + REQUIRE(!golden.empty()); + + // Reported separately from the golden comparison below: it is a different failure. + for (size_t i = 0; i < trace.size(); ++i) { + const auto entry = parse_trace_entry(trace[i]); + if (time_is_rounded_lead(entry)) + continue; + INFO("at trace entry " << i + 1); + INFO(" " << trace[i]); + FAIL("\"time:\" is not its entry's \"lead\" rounded to a whole second"); + } + + const size_t common = std::min(trace.size(), golden.size()); + for (size_t i = 0; i < common; ++i) { + if (trace_entries_match(trace[i], golden[i])) + continue; + // Report the first difference only: past it the two are misaligned and every later entry + // would be reported as a difference too. + INFO("first difference at trace entry " << i + 1); + INFO(" main: " << golden[i]); + INFO(" branch: " << trace[i]); + FAIL("temperature emission differs from main with wait_for_temp_on_wipe_tower off"); + } + CHECK(trace.size() == golden.size()); +} + // max_layer_height can be shorter than the extruder count (normalization sizes it to the // filament count under single_extruder_multi_material). calc_max_layer_height() in ToolOrdering // indexed it per-nozzle and read past the end. Shortened directly here to isolate that read; @@ -104,3 +714,4 @@ TEST_CASE("Multi-extruder slice stays in bounds with a short max_layer_height", init_and_process_print({ cube(20) }, print, config); REQUIRE_FALSE(print.objects().front()->layers().empty()); } + diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 9cb085f78b..6bff945fc3 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -338,6 +338,22 @@ TEST_CASE("G-code lists the resolved extrusion-width settings", "[Print]") CHECK(with_first_layer.find("; first layer extrusion width") != std::string::npos); } +// gcode_skip_config_block suppresses the resolved-settings block while leaving the +// header and executable blocks intact. +TEST_CASE("gcode_skip_config_block omits the resolved-settings comment block", "[Print]") +{ + const std::string gcode = slice({ cube(20) }, { + { "gcode_skip_config_block", true }, + { "gcode_comments", true }, + }); + CHECK(gcode.find("; CONFIG_BLOCK_START") == std::string::npos); + CHECK(gcode.find("; CONFIG_BLOCK_END") == std::string::npos); + CHECK(gcode.find("; layer_height =") == std::string::npos); + CHECK(gcode.find("; fill_density =") == std::string::npos); + CHECK(gcode.find("; HEADER_BLOCK_START") != std::string::npos); + CHECK(gcode.find("; EXECUTABLE_BLOCK_START") != std::string::npos); +} + // Custom G-code templates substitute placeholders during export. TEST_CASE("Custom G-code placeholders are substituted", "[Print]") { diff --git a/tests/fff_print/test_wipe_tower.cpp b/tests/fff_print/test_wipe_tower.cpp new file mode 100644 index 0000000000..2bd7ac4189 --- /dev/null +++ b/tests/fff_print/test_wipe_tower.cpp @@ -0,0 +1,184 @@ +#include <catch2/catch_all.hpp> + +#include <string> +#include <vector> + +#include "libslic3r/BoundingBox.hpp" +#include "libslic3r/ClipperUtils.hpp" +#include "libslic3r/GCode/GCodeProcessor.hpp" +#include "libslic3r/GCode/WipeTower.hpp" +#include "libslic3r/PrintConfig.hpp" + +#include "test_helpers.hpp" + +using namespace Slic3r; +using namespace Slic3r::Test; + +// Taken from the config enum map rather than hand-listed, so a flavor added to GCodeFlavor later +// is covered here without editing this file. +static std::vector<GCodeFlavor> non_klipper_flavors() +{ + std::vector<GCodeFlavor> flavors; + for (const auto &[name, value] : ConfigOptionEnum<GCodeFlavor>::get_enum_values()) + if (GCodeFlavor(value) != gcfKlipper) + flavors.push_back(GCodeFlavor(value)); + return flavors; +} + +static std::string flavor_name(GCodeFlavor flavor) +{ + return ConfigOptionEnum<GCodeFlavor>::get_enum_names()[int(flavor)]; +} + +TEST_CASE("Klipper flushes the wipe tower planner queue with M400", "[WipeTower]") +{ + CHECK(std::string(flush_planner_queue_command(gcfKlipper)) == "M400\n"); +} + +TEST_CASE("Other flavors flush the wipe tower planner queue with a zero dwell", "[WipeTower]") +{ + const GCodeFlavor flavor = GENERATE(from_range(non_klipper_flavors())); + INFO("gcode flavor: " << flavor_name(flavor)); + CHECK(std::string(flush_planner_queue_command(flavor)) == "G4 S0\n"); +} + +// 1.5s is exactly representable as a float, so neither form can drift when rounded. +TEST_CASE("Klipper waits in the wipe tower with a millisecond dwell", "[WipeTower]") +{ + CHECK(wait_command(gcfKlipper, 1.5f) == "G4 P1500\n"); +} + +TEST_CASE("Other flavors wait in the wipe tower with a seconds dwell", "[WipeTower]") +{ + const GCodeFlavor flavor = GENERATE(from_range(non_klipper_flavors())); + INFO("gcode flavor: " << flavor_name(flavor)); + CHECK(wait_command(flavor, 1.5f) == "G4 S1.500\n"); +} + +// The prime tower is validated against the real printable outline, so the placement clamps have to +// agree with it wherever that outline is not a rectangle. A regular hexagon inscribed in a 200mm +// circle stands in for the shipped delta beds. +TEST_CASE("The wipe tower placement clamp follows a non-rectangular bed outline", "[WipeTower]") +{ + const coord_t margin = scaled<coord_t>(1.); + auto square_at = [](double x, double y, double side) { + return BoundingBox(Point::new_scale(x, y), Point::new_scale(x + side, y + side)); + }; + // Does the footprint, padded by pad, sit inside the outline once the returned move is applied? + auto lands_inside = [](BoundingBox box, const Polygons &bed, const Vec2f &move, coord_t pad) { + box.translate(Point::new_scale(move.x(), move.y())); + return diff(Polygons{box.inflated(pad).polygon()}, bed).empty(); + }; + + const Polygons hex_bed{make_circle_num_segments(scaled<double>(100.), 6)}; + const Polygons square_bed{Polygon::new_scale(Pointfs{{0., 0.}, {200., 0.}, {200., 200.}, {0., 200.}})}; + + SECTION("a rectangular bed is left to the bounding box clamp") { + const Vec2f move = WipeTower::move_box_inside_polygon(square_at(50., 50., 30.), square_bed, margin); + CHECK_THAT(move.x(), Catch::Matchers::WithinAbs(0., 1e-6)); + CHECK_THAT(move.y(), Catch::Matchers::WithinAbs(0., 1e-6)); + } + + // Dragging the tower off one edge may not pull it away from the other, or it would jump out from + // under the cursor instead of sliding along the edge. + SECTION("only the violated axis is clamped") { + const Vec2f move = WipeTower::move_box_inside_polygon(square_at(185., 50., 30.), square_bed, margin); + CHECK_THAT(move.x(), Catch::Matchers::WithinAbs(-16., 1e-6)); + CHECK_THAT(move.y(), Catch::Matchers::WithinAbs(0., 1e-6)); + } + + SECTION("a footprint already inside the outline is left alone") { + const Vec2f move = WipeTower::move_box_inside_polygon(square_at(-15., -15., 30.), hex_bed, margin); + CHECK_THAT(move.x(), Catch::Matchers::WithinAbs(0., 1e-6)); + CHECK_THAT(move.y(), Catch::Matchers::WithinAbs(0., 1e-6)); + } + + SECTION("a footprint in the bounding box corner is pulled onto the bed") { + const BoundingBox box = square_at(55., 50., 30.); + REQUIRE_FALSE(lands_inside(box, hex_bed, Vec2f::Zero(), margin)); // in the bbox, off the hexagon + CHECK(lands_inside(box, hex_bed, WipeTower::move_box_inside_polygon(box, hex_bed, margin), margin)); + } + + // An unresolved auto brim width reaches the drag clamp as a negative margin. Padding by it would + // shrink the footprint and hand back a position the slice validation still rejects. + SECTION("a negative margin still lands the footprint inside the outline") { + const BoundingBox box = square_at(55., 50., 30.); + const coord_t brim = scaled<coord_t>(-0.5); + CHECK(lands_inside(box, hex_bed, WipeTower::move_box_inside_polygon(box, hex_bed, brim), 0)); + } + + SECTION("a footprint too large for the bed is left alone") { + const Vec2f move = WipeTower::move_box_inside_polygon(square_at(-200., -200., 400.), hex_bed, margin); + CHECK_THAT(move.x(), Catch::Matchers::WithinAbs(0., 1e-6)); + CHECK_THAT(move.y(), Catch::Matchers::WithinAbs(0., 1e-6)); + } +} + +// The cases above only exercise the helpers in isolation. The one below slices a real +// two-filament print, so it also covers the binding constraint of both changes: that the +// configured `gcode_flavor` reaches the wipe tower writer and lands in the exported G-code. + +// The G-code inside each WIPE_TOWER_START/WIPE_TOWER_END pair, concatenated, so an M400 emitted +// outside the tower (e.g. GCodeProcessor's pre-heat injector) cannot create a false match. +static std::string wipe_tower_regions(const std::string &gcode) +{ + const std::string &start_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Tower_Start); + const std::string &end_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Tower_End); + std::string regions; + size_t pos = 0; + while (true) { + size_t start = gcode.find(start_tag, pos); + if (start == std::string::npos) + break; + size_t end = gcode.find(end_tag, start); + if (end == std::string::npos) + break; + regions.append(gcode, start, end - start); + pos = end + 1; + } + return regions; +} + +// A per-layer toolchange between the wall and infill filaments, same shape as +// test_multifilament.cpp's "Each feature prints with its assigned filament", so the wipe tower +// runs its toolchange path (and so `flush_planner_queue()`) on every layer. +static DynamicPrintConfig wipe_tower_toolchange_config(const std::string &gcode_flavor) +{ + return multifilament_config(2, { + { "sparse_infill_filament_id", 1 }, + { "internal_solid_filament_id", 1 }, + { "top_surface_filament_id", 1 }, + { "bottom_surface_filament_id", 1 }, + { "outer_wall_filament_id", 2 }, + { "inner_wall_filament_id", 2 }, + { "enable_prime_tower", true }, + { "layer_height", 0.3 }, + { "gcode_flavor", gcode_flavor }, + }); +} + +// Slices a 10mm cube under `config`. Not plain Test::slice: a brand-new Print's first `apply()` +// counts one filament in use, and DynamicPrintConfig::normalize_fdm_2's single-filament rule then +// clears `enable_prime_tower`. A second apply, once init_print's regions have settled, sees both +// filaments and the tower survives. +static std::string slice_with_prime_tower(const DynamicPrintConfig &config) +{ + Print print; + Model model; + init_print({ cube(10) }, print, model, config); + print.apply(model, config); + return gcode(print); +} + +TEST_CASE("The wipe tower's toolchange planner flush follows the gcode flavor", "[WipeTower]") +{ + auto [flavor, expected, unexpected] = GENERATE(table<std::string, std::string, std::string>({ + { "klipper", "M400", "G4 S0" }, + { "marlin", "G4 S0", "M400" } })); + DYNAMIC_SECTION(flavor) { + const std::string tower = wipe_tower_regions(slice_with_prime_tower(wipe_tower_toolchange_config(flavor))); + REQUIRE_FALSE(tower.empty()); + CHECK_THAT(tower, Catch::Matchers::ContainsSubstring(expected)); + CHECK_THAT(tower, !Catch::Matchers::ContainsSubstring(unexpected)); + } +} diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index dbc6c99f15..5d3e301ea7 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable(${_TEST_NAME}_tests test_arachne_walls.cpp test_arrange.cpp test_bambu_networking.cpp + test_buildvolume.cpp test_calib.cpp test_clipper_offset.cpp test_clipper_utils.cpp @@ -17,7 +18,10 @@ add_executable(${_TEST_NAME}_tests test_preset_bundle_loading.cpp test_preset_setting_id.cpp test_preset_diff.cpp + test_vendor_cache.cpp test_elephant_foot_compensation.cpp + test_fill_corner_smoothing.cpp + test_filament_mixer.cpp test_fill_plane_path.cpp test_geometry.cpp test_multimaterial_segmentation.cpp @@ -26,9 +30,12 @@ add_executable(${_TEST_NAME}_tests test_mutable_polygon.cpp test_mutable_priority_queue.cpp test_nozzle_volume_type.cpp + test_step.cpp test_stl.cpp + test_triangle_selector.cpp test_meshboolean.cpp test_marchingsquares.cpp + test_model.cpp test_utils.cpp test_timeutils.cpp test_voronoi.cpp diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index 1a082cd8e0..4c0a09cf3f 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -1,5 +1,6 @@ #include "libslic3r/Model.hpp" +#include "libslic3r/TriangleSelector.hpp" #include "libslic3r/Format/3mf.hpp" #include "libslic3r/Format/bbs_3mf.hpp" #include "libslic3r/Format/STL.hpp" @@ -155,10 +156,8 @@ SCENARIO("H2C multi-nozzle .3mf round-trip", "[3mf][MultiNozzle]") { // store_bbs_3mf stages Metadata/project_settings.config through the model's backup path; // point it at a writable temp dir (the default lives under a read-only root in CI). - std::string backup_dir = - (boost::filesystem::temp_directory_path() / boost::filesystem::unique_path("orca_mn_%%%%%%%%")).string(); - boost::filesystem::create_directories(backup_dir); - model.set_backup_path(backup_dir); + ScopedTemporaryDir backup_dir("orca_mn"); + model.set_backup_path(backup_dir.string()); // Global (printer) config: give nozzle_volume_type a non-default value so the slice_info // read-back is a meaningful assertion (High Flow == 1). @@ -180,7 +179,8 @@ SCENARIO("H2C multi-nozzle .3mf round-trip", "[3mf][MultiNozzle]") { plate->config.set_key_value("enable_filament_dynamic_map", new ConfigOptionBool(true)); WHEN("stored to and reloaded from a .3mf") { - std::string test_file = std::string(TEST_DATA_DIR) + "/test_3mf/mn_roundtrip.3mf"; + ScopedTemporaryFile temp(".3mf"); + const std::string test_file = temp.string(); StoreParams store_params; store_params.path = test_file.c_str(); @@ -202,8 +202,6 @@ SCENARIO("H2C multi-nozzle .3mf round-trip", "[3mf][MultiNozzle]") { bool loaded = load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model, &dst_plates, &project_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr, LoadStrategy::LoadModel | LoadStrategy::LoadConfig); - boost::filesystem::remove(test_file); - THEN("every multi-nozzle key round-trips as expected") { REQUIRE(loaded); REQUIRE(dst_plates.size() >= 1); @@ -233,7 +231,6 @@ SCENARIO("H2C multi-nozzle .3mf round-trip", "[3mf][MultiNozzle]") { release_PlateData_list(dst_plates); } delete plate; // store_bbs_3mf does not take ownership of the source plate - boost::filesystem::remove_all(backup_dir); } } @@ -250,10 +247,8 @@ SCENARIO("Non-standard nozzle diameter survives .3mf save on a single-nozzle pri REQUIRE(load_stl(src_file.c_str(), &model)); model.add_default_instances(); - std::string backup_dir = - (boost::filesystem::temp_directory_path() / boost::filesystem::unique_path("orca_nd_%%%%%%%%")).string(); - boost::filesystem::create_directories(backup_dir); - model.set_backup_path(backup_dir); + ScopedTemporaryDir backup_dir("orca_nd"); + model.set_backup_path(backup_dir.string()); // Single extruder with a non-standard 0.5 mm nozzle; extruder_max_nozzle_count stays at its // default (no nozzle cluster), so the writer must emit the exact config diameter. @@ -276,7 +271,8 @@ SCENARIO("Non-standard nozzle diameter survives .3mf save on a single-nozzle pri plate->slice_filaments_info.push_back(fi); WHEN("stored to and reloaded from a .3mf") { - std::string test_file = std::string(TEST_DATA_DIR) + "/test_3mf/nd_roundtrip.3mf"; + ScopedTemporaryFile temp(".3mf"); + const std::string test_file = temp.string(); StoreParams store_params; store_params.path = test_file.c_str(); @@ -296,8 +292,6 @@ SCENARIO("Non-standard nozzle diameter survives .3mf save on a single-nozzle pri bool loaded = load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model, &dst_plates, &project_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr, LoadStrategy::LoadModel | LoadStrategy::LoadConfig); - boost::filesystem::remove(test_file); - THEN("the saved nozzle diameter is the exact 0.5, not the rounded 0.4") { REQUIRE(loaded); REQUIRE(dst_plates.size() >= 1); @@ -315,7 +309,6 @@ SCENARIO("Non-standard nozzle diameter survives .3mf save on a single-nozzle pri release_PlateData_list(dst_plates); } delete plate; // store_bbs_3mf does not take ownership of the source plate - boost::filesystem::remove_all(backup_dir); } } @@ -436,10 +429,8 @@ SCENARIO("Nozzle-group metadata .3mf round-trip", "[3mf][MultiNozzle]") { REQUIRE(load_stl(src_file.c_str(), &model)); model.add_default_instances(); - std::string backup_dir = - (boost::filesystem::temp_directory_path() / boost::filesystem::unique_path("orca_ng_%%%%%%%%")).string(); - boost::filesystem::create_directories(backup_dir); - model.set_backup_path(backup_dir); + ScopedTemporaryDir backup_dir("orca_ng"); + model.set_backup_path(backup_dir.string()); DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); @@ -459,7 +450,8 @@ SCENARIO("Nozzle-group metadata .3mf round-trip", "[3mf][MultiNozzle]") { plate->config.set_key_value("filament_map", new ConfigOptionInts({ 1, 2, 1 })); WHEN("stored to and reloaded from a .3mf") { - std::string test_file = std::string(TEST_DATA_DIR) + "/test_3mf/ng_roundtrip.3mf"; + ScopedTemporaryFile temp(".3mf"); + const std::string test_file = temp.string(); StoreParams store_params; store_params.path = test_file.c_str(); @@ -479,8 +471,6 @@ SCENARIO("Nozzle-group metadata .3mf round-trip", "[3mf][MultiNozzle]") { bool loaded = load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model, &dst_plates, &project_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr, LoadStrategy::LoadModel | LoadStrategy::LoadConfig); - boost::filesystem::remove(test_file); - THEN("the <nozzle> tags round-trip into the loaded plate's nozzles_info") { REQUIRE(loaded); REQUIRE(dst_plates.size() >= 1); @@ -506,67 +496,97 @@ SCENARIO("Nozzle-group metadata .3mf round-trip", "[3mf][MultiNozzle]") { release_PlateData_list(dst_plates); } delete plate; - boost::filesystem::remove_all(backup_dir); } } -SCENARIO("2D convex hull of sinking object", "[3mf][.]") { - GIVEN("model") { - // load a model + +// A mixed-color filament occupies an ordinary filament slot, and painting with it stores an +// ordinary extruder state: a project saved by BambuStudio encodes filament 5 of a 5-slot setup +// as paint state 5, with the mix described by the parallel filament_mixed_* project arrays. +SCENARIO("Mixed-color filament setup and painting round-trip through a .3mf", "[3mf][MixedFilament]") { + GIVEN("a painted model whose project config describes a mixed filament in the last slot") { Model model; std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl"; REQUIRE(load_stl(src_file.c_str(), &model)); model.add_default_instances(); - WHEN("model is rotated, scaled and set as sinking") { - ModelObject* object = model.objects[0]; - object->center_around_origin(false); + // Both the exporter and the importer stage Metadata/project_settings.config through the + // model's backup path; point them at writable temp dirs. + ScopedTemporaryDir backup_dir("orca_mixed_src"); + model.set_backup_path(backup_dir.string()); - // This outputs the same exact data as the Prusaslicer test - write_debug_stl("3mf/orca.ascii", object->volumes[0]->mesh()); + ModelVolume* mv = model.objects.front()->volumes.front(); + { + TriangleSelector selector(mv->mesh()); + selector.set_facet(0, EnforcerBlockerType::Extruder5); // the mixed slot + selector.set_facet(1, EnforcerBlockerType::Extruder2); + REQUIRE(mv->mmu_segmentation_facets.set(selector)); + } - // set instance's attitude so that it is rotated, scaled (and sinking? how is it sinking? the rotation? does it matter if it's sinking?) - ModelInstance* instance = object->instances[0]; - instance->set_rotation(X, -M_PI / 4.0); - instance->set_offset(Vec3d::Zero()); - instance->set_scaling_factor({ 2.0, 2.0, 2.0 }); + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_key_value("filament_colour", new ConfigOptionStrings( + { "#00AE42", "#FFFF00", "#FF0000", "#0000FF", "#FF6A26" })); + config.set_key_value("filament_is_mixed", new ConfigOptionBools( + { false, false, false, false, true })); + config.set_key_value("filament_mixed_components", new ConfigOptionStrings( + { "", "", "", "", "3,2" })); + config.set_key_value("filament_mixed_sublayer_ratios", new ConfigOptionStrings( + { "", "", "", "", "0.4200,0.5800" })); - // calculate 2D convex hull - auto trafo = instance->get_transformation().get_matrix(); + WHEN("stored to and reloaded from a .3mf") { + ScopedTemporaryFile temp(".3mf"); + const std::string test_file = temp.string(); - // This matrix is the same exact matrix as the Prusaslicer test - CAPTURE(trafo); - Polygon hull_2d = object->convex_hull_2d(trafo); + PlateData* plate = new PlateData(); + plate->plate_index = 0; - // But we get different hull_2d.points here (and somehow decimal numbers despite being int64_t values, but that's probabaly printing configuration somewhere -- Prusaslicer's prints out with newlines between the X&Y and not one between coordinates, which is about the worse possible output). - // I think it's something to do with PrusaSlicer ignoring everything under the Z plane, which makes sense from the results. - // See the comments added to ModelObject::convex_hull_2d for more information. + StoreParams store_params; + store_params.path = test_file.c_str(); + store_params.model = &model; + store_params.config = &config; + store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence; + store_params.plate_data_list.push_back(plate); + REQUIRE(store_bbs_3mf(store_params)); - // verify result - Points result = { - { -91501496, -15914144 }, - { 91501496, -15914144 }, - { 91501496, 4243 }, - { 78229680, 4246883 }, - { 56898100, 4246883 }, - { -85501496, 4242641 }, - { -91501496, 4243 } - }; + Model dst_model; + ScopedTemporaryDir dst_backup_dir("orca_mixed_dst"); + dst_model.set_backup_path(dst_backup_dir.string()); + DynamicPrintConfig dst_config; + ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable }; + PlateDataPtrs dst_plates; + std::vector<Preset*> project_presets; + bool is_bbl_3mf = false, is_orca_3mf = false; + Semver file_version; + REQUIRE(load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model, &dst_plates, + &project_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr, + LoadStrategy::LoadModel | LoadStrategy::LoadConfig)); - THEN("2D convex hull should match with reference") { - // Allow 1um error due to floating point rounding. - bool res = hull_2d.points.size() == result.size(); - if (res) { - for (size_t i = 0; i < result.size(); ++ i) { - const Point &p1 = result[i]; - const Point &p2 = hull_2d.points[i]; - CHECK((std::abs(p1.x() - p2.x()) > 1 || std::abs(p1.y() - p2.y()) > 1)); - } - } + THEN("the mixed-filament project keys survive") { + auto* is_mixed = dst_config.option<ConfigOptionBools>("filament_is_mixed"); + REQUIRE(is_mixed != nullptr); + REQUIRE(is_mixed->values == std::vector<unsigned char>({ 0, 0, 0, 0, 1 })); - CAPTURE(hull_2d.points); - REQUIRE(res); + auto* components = dst_config.option<ConfigOptionStrings>("filament_mixed_components"); + REQUIRE(components != nullptr); + REQUIRE(components->values.size() == 5); + REQUIRE(components->values[4] == "3,2"); + + auto* ratios = dst_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios"); + REQUIRE(ratios != nullptr); + REQUIRE(ratios->values.size() == 5); + REQUIRE(ratios->values[4] == "0.4200,0.5800"); } + + THEN("the painted facets survive, including the one painted with the mixed slot") { + REQUIRE(dst_model.objects.size() == 1); + ModelVolume* dst_mv = dst_model.objects.front()->volumes.front(); + REQUIRE_FALSE(dst_mv->mmu_segmentation_facets.empty()); + REQUIRE(dst_mv->mmu_segmentation_facets.has_facets(*dst_mv, EnforcerBlockerType::Extruder2)); + REQUIRE(dst_mv->mmu_segmentation_facets.has_facets(*dst_mv, EnforcerBlockerType::Extruder5)); + } + + release_PlateData_list(dst_plates); + delete plate; // store_bbs_3mf does not take ownership of the source plate } } } diff --git a/tests/libslic3r/test_buildvolume.cpp b/tests/libslic3r/test_buildvolume.cpp new file mode 100644 index 0000000000..b2e8040d68 --- /dev/null +++ b/tests/libslic3r/test_buildvolume.cpp @@ -0,0 +1,40 @@ +#include <catch2/catch_all.hpp> + +#include "libslic3r/BuildVolume.hpp" + +using namespace Slic3r; + +static std::vector<Vec2d> rect_area(double w, double d) +{ + return { { 0., 0. }, { w, 0. }, { w, d }, { 0., d } }; +} + +// extruder_printable_height and extruder_printable_area are independent config options, so a +// profile can leave the heights short. BuildVolume must not index past the end of the heights. +TEST_CASE("BuildVolume falls back to the bed height when extruder_printable_height is short", "[BuildVolume]") +{ + const std::vector<Vec2d> bed = rect_area(200., 200.); + const std::vector<std::vector<Vec2d>> areas = { rect_area(200., 200.), rect_area(100., 200.) }; + const std::vector<double> heights = { 180. }; + + const BuildVolume build_volume(bed, 250., areas, heights); + + REQUIRE(build_volume.get_extruder_area_count() == 2); + // The extruder with a height of its own keeps it, and differs from the bed, so it gets its own volume. + CHECK_THAT(build_volume.get_extruder_area_volume(0).bboxf.max.z(), Catch::Matchers::WithinAbs(180., 1e-6)); + // The extruder without one falls back to the bed's printable_height instead of reading out of range. + CHECK_THAT(build_volume.get_extruder_area_volume(1).bboxf.max.z(), Catch::Matchers::WithinAbs(250., 1e-6)); +} + +TEST_CASE("BuildVolume keeps per-extruder heights when both vectors match", "[BuildVolume]") +{ + const std::vector<Vec2d> bed = rect_area(200., 200.); + const std::vector<std::vector<Vec2d>> areas = { rect_area(120., 200.), rect_area(100., 200.) }; + const std::vector<double> heights = { 180., 200.5 }; + + const BuildVolume build_volume(bed, 250., areas, heights); + + REQUIRE(build_volume.get_extruder_area_count() == 2); + CHECK_THAT(build_volume.get_extruder_area_volume(0).bboxf.max.z(), Catch::Matchers::WithinAbs(180., 1e-6)); + CHECK_THAT(build_volume.get_extruder_area_volume(1).bboxf.max.z(), Catch::Matchers::WithinAbs(200.5, 1e-6)); +} diff --git a/tests/libslic3r/test_config.cpp b/tests/libslic3r/test_config.cpp index f256ae3442..bd147b5881 100644 --- a/tests/libslic3r/test_config.cpp +++ b/tests/libslic3r/test_config.cpp @@ -4,6 +4,8 @@ #include "libslic3r/PrintConfigConstants.hpp" #include "libslic3r/LocalesUtils.hpp" +#include "test_utils.hpp" + #include <cereal/types/polymorphic.hpp> #include <cereal/types/string.hpp> #include <cereal/types/vector.hpp> @@ -233,6 +235,56 @@ SCENARIO("Config ini load/save interface", "[Config]") { } } +TEST_CASE("Flush-volume warning predicate respects used filament transitions", "[Config][Regression]") +{ + const std::vector<double> multipliers = {1.0}; + + SECTION("Single used filament does not trigger warning with zero transition entries") + { + const std::vector<double> matrix = { + 0.0, 0.0, + 0.0, 0.0 + }; + const std::vector<int> used_filaments = {1}; + + REQUIRE_FALSE(has_zero_flush_volume_for_used_filaments(matrix, multipliers, used_filaments)); + } + + SECTION("Two used filaments trigger warning when transition flush entry is zero") + { + const std::vector<double> matrix = { + 0.0, 0.0, + 0.0, 0.0 + }; + const std::vector<int> used_filaments = {1, 2}; + + REQUIRE(has_zero_flush_volume_for_used_filaments(matrix, multipliers, used_filaments)); + } + + SECTION("Two used filaments do not trigger warning when transitions are non-zero") + { + const std::vector<double> matrix = { + 0.0, 280.0, + 280.0, 0.0 + }; + const std::vector<int> used_filaments = {1, 2}; + + REQUIRE_FALSE(has_zero_flush_volume_for_used_filaments(matrix, multipliers, used_filaments)); + } + + SECTION("Zero multiplier still triggers warning when multiple filaments are used") + { + const std::vector<double> matrix = { + 0.0, 280.0, + 280.0, 0.0 + }; + const std::vector<double> zero_multiplier = {0.0}; + const std::vector<int> used_filaments = {1, 2}; + + REQUIRE(has_zero_flush_volume_for_used_filaments(matrix, zero_multiplier, used_filaments)); + } +} + // TODO: https://github.com/SoftFever/OrcaSlicer/issues/11269 - Is this test still relevant? Delete if not. // It was failing so at least "nozzle_type" and "extruder_printable_area" could not be serialized // and an exception was thrown, but "nozzle_type" has been around for at least 3 months now. @@ -407,8 +459,7 @@ SCENARIO("update_diff_values_to_child_config tolerates legacy machine-limit vect // } TEST_CASE("save_to_json round-trips plugin capability references as strings", "[Config][plugins]") { - namespace fs = boost::filesystem; - const fs::path tmp = fs::temp_directory_path() / fs::unique_path("orca_plugins_%%%%-%%%%.json"); + ScopedTemporaryFile tmp(".json"); const std::vector<std::string> refs = { "local_plugin;;inset", "cloud_plugin;550e8400-e29b-41d4-a716-446655440000;inset" @@ -435,8 +486,6 @@ TEST_CASE("save_to_json round-trips plugin capability references as strings", "[ REQUIRE(reloaded.load_from_json(tmp.string(), substitutions, true, key_values, reason) == 0); CHECK(reason.empty()); CHECK(reloaded.option<ConfigOptionStrings>("slicing_pipeline_plugin")->values == refs); - - fs::remove(tmp); } TEST_CASE("plugin capability references survive string-map serialization", "[Config][plugins]") { @@ -779,3 +828,266 @@ SCENARIO("ConfigOptionVector::set_to_index throws on incompatible type", "[Confi } } } + +TEST_CASE("read_cli applies valid values and collects non-option arguments", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--nozzle-temperature", "210,190", "--reduce-crossing-wall=1", "model.3mf"}; + REQUIRE(config.read_cli(5, argv, &extra, &keys)); + REQUIRE(config.opt<ConfigOptionInts>("nozzle_temperature")->values == std::vector<int>{210, 190}); + REQUIRE(config.opt<ConfigOptionBool>("reduce_crossing_wall")->value); + REQUIRE(extra == t_config_option_keys{"model.3mf"}); + REQUIRE(keys == t_config_option_keys{"nozzle_temperature", "reduce_crossing_wall"}); +} + +TEST_CASE("read_cli rejects nil for a non-nullable vector option", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--nozzle-temperature", "nil"}; + REQUIRE_FALSE(config.read_cli(3, argv, &extra, &keys)); +} + +TEST_CASE("read_cli rejects an invalid boolean value", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--reduce-crossing-wall=maybe"}; + REQUIRE_FALSE(config.read_cli(2, argv, &extra, &keys)); +} + +TEST_CASE("read_cli accepts the common spellings of a boolean value", "[Config]") { + const auto [text, expected] = GENERATE(table<const char*, bool>({ + {"--reduce-crossing-wall=1", true}, + {"--reduce-crossing-wall=true", true}, + {"--reduce-crossing-wall=Yes", true}, + {"--reduce-crossing-wall=on", true}, + {"--reduce-crossing-wall=enabled", true}, + {"--reduce-crossing-wall=TRUE", true}, + {"--reduce-crossing-wall=oN", true}, + {"--reduce-crossing-wall=0", false}, + {"--reduce-crossing-wall=false", false}, + {"--reduce-crossing-wall=No", false}, + {"--reduce-crossing-wall=off", false}, + {"--reduce-crossing-wall=disabled", false}, + {"--reduce-crossing-wall=FALSE", false}, + {"--reduce-crossing-wall=DiSaBlEd", false}, + })); + + DYNAMIC_SECTION(text) { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", text}; + REQUIRE(config.read_cli(2, argv, &extra, &keys)); + REQUIRE(config.opt<ConfigOptionBool>("reduce_crossing_wall")->value == expected); + } +} + +TEST_CASE("read_cli accepts the common boolean spellings inside a bools vector", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-soluble=true,no,1"}; + REQUIRE(config.read_cli(2, argv, &extra, &keys)); + REQUIRE(config.opt<ConfigOptionBools>("filament_soluble")->values == std::vector<unsigned char>{1, 0, 1}); +} + +TEST_CASE("read_cli trims whitespace around boolean spellings", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--reduce-crossing-wall= true ", "--filament-soluble= true , no ,1"}; + REQUIRE(config.read_cli(3, argv, &extra, &keys)); + REQUIRE(config.opt<ConfigOptionBool>("reduce_crossing_wall")->value); + REQUIRE(config.opt<ConfigOptionBools>("filament_soluble")->values == std::vector<unsigned char>{1, 0, 1}); +} + +TEST_CASE("read_cli normalizes boolean spellings when a bools vector is repeated", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-soluble=true", "--filament-soluble=off"}; + REQUIRE(config.read_cli(3, argv, &extra, &keys)); + REQUIRE(config.opt<ConfigOptionBools>("filament_soluble")->values == std::vector<unsigned char>{1, 0}); +} + +TEST_CASE("read_cli keeps nil alongside boolean spellings in a nullable bools vector", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--enable-overhang-speed=nil,yes,off"}; + REQUIRE(config.read_cli(2, argv, &extra, &keys)); + auto* opt = config.opt<ConfigOptionBoolsNullable>("enable_overhang_speed"); + REQUIRE(opt != nullptr); + REQUIRE(opt->values.size() == 3); + REQUIRE(opt->is_nil(0)); + REQUIRE(opt->values[1] == 1); + REQUIRE(opt->values[2] == 0); +} + +TEST_CASE("read_cli rejects an empty item inside a bools vector", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-soluble=true,,1"}; + REQUIRE_FALSE(config.read_cli(2, argv, &extra, &keys)); +} + +TEST_CASE("read_cli rejects an unknown spelling next to a valid one in a bools vector", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-soluble=true,affirmative"}; + REQUIRE_FALSE(config.read_cli(2, argv, &extra, &keys)); +} + +// The normalization lives in read_cli's boolean branches, so options of other types keep the +// value verbatim - a path named "on" or a colour named "true" must not turn into "1". +TEST_CASE("read_cli leaves boolean spellings alone for non-boolean options", "[Config]") { + SECTION("string option") { + Slic3r::DynamicPrintAndCLIConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--logfile=true"}; + REQUIRE(config.read_cli(2, argv, &extra, &keys)); + REQUIRE(config.opt<ConfigOptionString>("logfile")->value == "true"); + } + SECTION("strings vector option") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-colour=on;off"}; + REQUIRE(config.read_cli(2, argv, &extra, &keys)); + REQUIRE(config.opt<ConfigOptionStrings>("filament_colour")->values == std::vector<std::string>{"on", "off"}); + } +} + +TEST_CASE("read_cli treats a bare boolean flag as true without consuming the next argument", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--reduce-crossing-wall", "model.3mf"}; + REQUIRE(config.read_cli(3, argv, &extra, &keys)); + REQUIRE(config.opt<ConfigOptionBool>("reduce_crossing_wall")->value); + REQUIRE(extra == t_config_option_keys{"model.3mf"}); +} + +TEST_CASE("read_cli rejects an invalid scalar numeric value", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--top-shell-layers", "several"}; + REQUIRE_FALSE(config.read_cli(3, argv, &extra, &keys)); +} + +TEST_CASE("read_cli appends values when a vector option is repeated", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--nozzle-temperature", "210", "--nozzle-temperature", "190,200"}; + REQUIRE(config.read_cli(5, argv, &extra, &keys)); + REQUIRE(config.opt<ConfigOptionInts>("nozzle_temperature")->values == std::vector<int>{210, 190, 200}); + // the key is recorded once, on first use + REQUIRE(keys == t_config_option_keys{"nozzle_temperature"}); +} + +TEST_CASE("read_cli parses a bools vector given in the --flag=values form", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-soluble=1,0,1"}; + REQUIRE(config.read_cli(2, argv, &extra, &keys)); + REQUIRE(config.opt<ConfigOptionBools>("filament_soluble")->values == std::vector<unsigned char>{1, 0, 1}); +} + +TEST_CASE("read_cli rejects an invalid value inside a bools vector", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-soluble=1,maybe"}; + REQUIRE_FALSE(config.read_cli(2, argv, &extra, &keys)); +} + +TEST_CASE("read_cli appends true for a bare bools vector flag", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-soluble"}; + REQUIRE(config.read_cli(2, argv, &extra, &keys)); + REQUIRE(config.opt<ConfigOptionBools>("filament_soluble")->values == std::vector<unsigned char>{1}); +} + +TEST_CASE("read_cli splits a strings vector on semicolons and unescapes quoted items", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-colour", "#FF0000;\"a\\nb\";#00FF00"}; + REQUIRE(config.read_cli(3, argv, &extra, &keys)); + auto& values = config.opt<ConfigOptionStrings>("filament_colour")->values; + REQUIRE(values == std::vector<std::string>{"#FF0000", "a\nb", "#00FF00"}); +} + +TEST_CASE("read_cli rejects a strings vector with an unterminated quote", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-colour", "\"oops"}; + REQUIRE_FALSE(config.read_cli(3, argv, &extra, &keys)); +} + +TEST_CASE("read_cli parses a points vector in the NxM coordinate form", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--printable-area", "0x0,200x0,200x200,0x200"}; + REQUIRE(config.read_cli(3, argv, &extra, &keys)); + auto& points = config.opt<ConfigOptionPoints>("printable_area")->values; + REQUIRE(points.size() == 4); + REQUIRE_THAT(points[1].x(), Catch::Matchers::WithinAbs(200.0, 1e-9)); + REQUIRE_THAT(points[1].y(), Catch::Matchers::WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(points[3].x(), Catch::Matchers::WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(points[3].y(), Catch::Matchers::WithinAbs(200.0, 1e-9)); +} + +// logfile is a CLI-only option, so it needs the config type whose def pulls in cli_misc_config_def. +TEST_CASE("read_cli stores the log file path as a string", "[Config]") { + Slic3r::DynamicPrintAndCLIConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--logfile", "orca.log"}; + REQUIRE(config.read_cli(3, argv, &extra, &keys)); + REQUIRE(config.opt<ConfigOptionString>("logfile")->value == "orca.log"); +} + +TEST_CASE("read_cli accepts nil entries for a nullable vector option", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-retraction-length", "nil,2.5"}; + REQUIRE(config.read_cli(3, argv, &extra, &keys)); + auto* opt = config.opt<ConfigOptionFloatsNullable>("filament_retraction_length"); + REQUIRE(opt != nullptr); + REQUIRE(opt->values.size() == 2); + REQUIRE(opt->is_nil(0)); + REQUIRE_FALSE(opt->is_nil(1)); + REQUIRE_THAT(opt->values[1], Catch::Matchers::WithinAbs(2.5, 1e-9)); +} + +// get_at() returns values.front() for an out-of-range index, so calling it on an empty vector +// option is UB. filament_id and filament_is_support are unpopulated on a CLI from-scratch slice. +TEST_CASE("get_filament_type treats empty vector options as absent", "[Config][Filament]") +{ + DynamicPrintConfig config; + std::string displayed; + + SECTION("an empty filament_type yields no type at all") + { + config.set_key_value("filament_type", new ConfigOptionStrings()); + REQUIRE(config.get_filament_type(displayed, 0) == ""); + } + + SECTION("an empty filament_is_support falls back to the plain filament type") + { + config.set_key_value("filament_type", new ConfigOptionStrings({"PETG"})); + config.set_key_value("filament_is_support", new ConfigOptionBools()); + REQUIRE(config.get_filament_type(displayed, 0) == "PETG"); + REQUIRE(displayed == "PETG"); + } + + SECTION("a support filament with an empty filament_id resolves from the type alone") + { + config.set_key_value("filament_type", new ConfigOptionStrings({"PLA"})); + config.set_key_value("filament_is_support", new ConfigOptionBools({true})); + config.set_key_value("filament_id", new ConfigOptionStrings()); + REQUIRE(config.get_filament_type(displayed, 0) == "PLA-S"); + REQUIRE(displayed == "Sup.PLA"); + } + + SECTION("a populated filament_id still selects the support type by id") + { + config.set_key_value("filament_type", new ConfigOptionStrings({"PETG"})); + config.set_key_value("filament_is_support", new ConfigOptionBools({true})); + config.set_key_value("filament_id", new ConfigOptionStrings({"GFS00"})); + REQUIRE(config.get_filament_type(displayed, 0) == "PLA-S"); + REQUIRE(displayed == "Sup.PLA"); + } +} diff --git a/tests/libslic3r/test_config_variant_expansion.cpp b/tests/libslic3r/test_config_variant_expansion.cpp index dd7de2af44..28901702cc 100644 --- a/tests/libslic3r/test_config_variant_expansion.cpp +++ b/tests/libslic3r/test_config_variant_expansion.cpp @@ -479,3 +479,84 @@ TEST_CASE("update_values_to_printer_extruders_for_multiple_filaments resolves pe REQUIRE(config.option<ConfigOptionInts>("filament_self_index")->values == std::vector<int>({1, 2})); } } + +// update_values_from_multi_to_multi_2 walks the DESTINATION PRINTER's variant list while writing +// into a row taken from the destination PRINT preset, whose arrays are sized to its own +// print_extruder_variant. Those two widths disagree until the print preset is re-selected for the +// new printer -- Tab::load_current_preset() runs this migration first -- so a project authored on +// a single-variant printer, opened and switched to a wider one, wrote past the end of the row. +TEST_CASE("update_values_from_multi_to_multi_2 sizes the destination row to the variant count", + "[Config][VariantExpansion]") +{ + const std::vector<std::string> src_variants{"Direct Drive Standard"}; + const std::vector<std::string> dst_variants{"Direct Drive Standard", "Direct Drive High Flow", + "Direct Drive Standard", "Direct Drive High Flow"}; + const std::set<std::string> keys{"outer_wall_speed"}; + + // The per-object override as authored on the single-variant printer. + const auto object_override = [] { + DynamicPrintConfig c; + c.option<ConfigOptionFloatsNullable>("outer_wall_speed", true)->values = {42.}; + return c; + }; + + SECTION("a row narrower than the variant list is grown, not overrun") { + DynamicPrintConfig object_config = object_override(); + DynamicPrintConfig dst; + dst.option<ConfigOptionFloatsNullable>("outer_wall_speed", true)->values = {200.}; + + REQUIRE(object_config.update_values_from_multi_to_multi_2(src_variants, dst_variants, dst, keys) == 0); + + const auto& out = object_config.option<ConfigOptionFloatsNullable>("outer_wall_speed")->values; + REQUIRE(out.size() == dst_variants.size()); + // Both "Direct Drive Standard" columns match the source variant, so they take the override. + CHECK(out[0] == Catch::Approx(42.)); + CHECK(out[2] == Catch::Approx(42.)); + // The High Flow columns have no matching source variant: nil, so the destination keeps + // tracking the print preset rather than being pinned to another variant's value. + CHECK(std::isnan(out[1])); + CHECK(std::isnan(out[3])); + } + + // The regression guard: where the row already matches the variant list -- every case that was + // not corrupting the heap -- the resize is a no-op and the output is unchanged. + SECTION("a correctly sized row is untouched") { + DynamicPrintConfig object_config = object_override(); + DynamicPrintConfig dst; + dst.option<ConfigOptionFloatsNullable>("outer_wall_speed", true)->values = {200., 500., 210., 510.}; + + REQUIRE(object_config.update_values_from_multi_to_multi_2(src_variants, dst_variants, dst, keys) == 0); + + const auto& out = object_config.option<ConfigOptionFloatsNullable>("outer_wall_speed")->values; + REQUIRE(out.size() == 4); + CHECK(out[0] == Catch::Approx(42.)); // matched -> override + CHECK(out[1] == Catch::Approx(500.)); // unmatched -> preset value preserved + CHECK(out[2] == Catch::Approx(42.)); + CHECK(out[3] == Catch::Approx(510.)); + } + + // is_nil(idx) indexes values[idx] with no bounds check, so a source shorter than its own + // variant list read out of range before the guard was added. + SECTION("a source shorter than its variant list is read in range") { + DynamicPrintConfig object_config = object_override(); // one value... + DynamicPrintConfig dst; + dst.option<ConfigOptionFloatsNullable>("outer_wall_speed", true)->values = {200., 500.}; + + REQUIRE(object_config.update_values_from_multi_to_multi_2( + {"Direct Drive Standard", "Direct Drive Standard"}, // ...but two source variants + {"Direct Drive Standard", "Direct Drive High Flow"}, dst, keys) == 0); + + const auto& out = object_config.option<ConfigOptionFloatsNullable>("outer_wall_speed")->values; + REQUIRE(out.size() == 2); + CHECK(out[0] == Catch::Approx(42.)); + CHECK(out[1] == Catch::Approx(500.)); + } + + SECTION("an empty destination variant list is refused") { + DynamicPrintConfig object_config = object_override(); + DynamicPrintConfig dst; + dst.option<ConfigOptionFloatsNullable>("outer_wall_speed", true)->values = {200.}; + + CHECK(object_config.update_values_from_multi_to_multi_2(src_variants, {}, dst, keys) == -1); + } +} diff --git a/tests/libslic3r/test_filament_mixer.cpp b/tests/libslic3r/test_filament_mixer.cpp new file mode 100644 index 0000000000..ade0c910dc --- /dev/null +++ b/tests/libslic3r/test_filament_mixer.cpp @@ -0,0 +1,205 @@ +#include <catch2/catch_all.hpp> + +#include "libslic3r/FilamentMixer.hpp" +#include "libslic3r/PrintConfig.hpp" + +using namespace Slic3r; + +TEST_CASE("parse_mixed_components reads 1-based component ids", "[FilamentMixer]") +{ + REQUIRE(parse_mixed_components("1,3") == std::vector<unsigned int>{1, 3}); + REQUIRE(parse_mixed_components("2, 4 ,5") == std::vector<unsigned int>{2, 4, 5}); + + SECTION("Malformed input yields no components") { + REQUIRE(parse_mixed_components("").empty()); + REQUIRE(parse_mixed_components("abc").empty()); + } +} + +TEST_CASE("parse_mixed_ratios normalizes to sum 1.0", "[FilamentMixer]") +{ + auto r = parse_mixed_ratios("0.7,0.3", 2); + REQUIRE(r.size() == 2); + REQUIRE_THAT(r[0], Catch::Matchers::WithinAbs(0.7, 1e-9)); + REQUIRE_THAT(r[1], Catch::Matchers::WithinAbs(0.3, 1e-9)); + + SECTION("Unnormalized input is rescaled") { + auto v = parse_mixed_ratios("2,2", 2); + REQUIRE_THAT(v[0], Catch::Matchers::WithinAbs(0.5, 1e-9)); + REQUIRE_THAT(v[1], Catch::Matchers::WithinAbs(0.5, 1e-9)); + } + + SECTION("Empty or mismatched input falls back to equal shares") { + auto v = parse_mixed_ratios("", 3); + REQUIRE(v.size() == 3); + for (double x : v) + REQUIRE_THAT(x, Catch::Matchers::WithinAbs(1.0 / 3.0, 1e-9)); + } +} + +TEST_CASE("has_any_mixed_filament detects mixed slots", "[FilamentMixer]") +{ + REQUIRE_FALSE(has_any_mixed_filament({})); + REQUIRE_FALSE(has_any_mixed_filament({0, 0, 0})); + REQUIRE(has_any_mixed_filament({0, 1, 0})); +} + +TEST_CASE("expand_mixed_filaments replaces mixed slots with their components", "[FilamentMixer]") +{ + // Slot 2 (0-based) is a mix of physical filaments 1 and 2 (1-based) => 0 and 1 (0-based). + const std::vector<unsigned char> is_mixed = {0, 0, 1}; + const std::vector<std::string> comp_strs = {"", "", "1,2"}; + + REQUIRE(expand_mixed_filaments({2}, is_mixed, comp_strs) == std::vector<unsigned int>{0, 1}); + + SECTION("Non-mixed entries pass through, result is sorted and deduplicated") { + REQUIRE(expand_mixed_filaments({2, 0}, is_mixed, comp_strs) == std::vector<unsigned int>{0, 1}); + } +} + +TEST_CASE("check_mixed_filament_integrity flags dangling component references", "[FilamentMixer]") +{ + const std::vector<unsigned char> is_mixed = {0, 0, 1}; + + SECTION("All components resolve") { + REQUIRE(check_mixed_filament_integrity(is_mixed, {"", "", "1,2"}, 2).empty()); + } + + SECTION("A component past the physical filament count is broken") { + auto broken = check_mixed_filament_integrity(is_mixed, {"", "", "1,9"}, 2); + REQUIRE(broken == std::vector<size_t>{2}); + } +} + +TEST_CASE("remap_mixed_components_on_delete rewrites ids around the deleted slot", "[FilamentMixer]") +{ + const std::vector<unsigned char> is_mixed = {0, 0, 0, 1}; + std::vector<std::string> comps = {"", "", "", "1,3"}; + + SECTION("Deleting a filament below the references shifts them down") { + remap_mixed_components_on_delete(is_mixed, comps, 2); + REQUIRE(comps[3] == "1,2"); + } + + SECTION("Deleting a referenced filament zeroes that component") { + remap_mixed_components_on_delete(is_mixed, comps, 1); + // 1 -> 0 (deleted sentinel), 3 -> 2 + REQUIRE(comps[3] == "0,2"); + } +} + +TEST_CASE("check_mixed_filament_type_consistency flags mismatched component types", "[FilamentMixer]") +{ + const std::vector<unsigned char> is_mixed = {0, 0, 1}; + const std::vector<std::string> comp_strs = {"", "", "1,2"}; + + REQUIRE(check_mixed_filament_type_consistency(is_mixed, comp_strs, {"PLA", "PLA"}).empty()); + + auto bad = check_mixed_filament_type_consistency(is_mixed, comp_strs, {"PLA", "PETG"}); + REQUIRE(bad == std::vector<size_t>{2}); +} + +TEST_CASE("a support-flagged component reads as its own filament type for the consistency check", "[FilamentMixer]") +{ + // The sidebar derives each component's type through DynamicPrintConfig::get_filament_type, + // which folds filament_is_support into the type, so toggling that flag alone flips the + // verdict and the mixed filament list has to be refreshed on filament_is_support too. + DynamicPrintConfig plain_pla; + plain_pla.set_key_value("filament_type", new ConfigOptionStrings({"PLA"})); + plain_pla.set_key_value("filament_is_support", new ConfigOptionBools({false})); + std::string displayed; + REQUIRE(plain_pla.get_filament_type(displayed) == "PLA"); + + DynamicPrintConfig support_pla; + support_pla.set_key_value("filament_type", new ConfigOptionStrings({"PLA"})); + support_pla.set_key_value("filament_is_support", new ConfigOptionBools({true})); + REQUIRE(support_pla.get_filament_type(displayed) == "PLA-S"); + REQUIRE(displayed == "Sup.PLA"); + + const std::vector<unsigned char> is_mixed = {0, 0, 1}; + const std::vector<std::string> comp_strs = {"", "", "1,2"}; + REQUIRE(check_mixed_filament_type_consistency(is_mixed, comp_strs, {"PLA", "PLA-S"}) == std::vector<size_t>{2}); +} + +TEST_CASE("gradient curves round-trip and sample monotonically", "[FilamentMixer]") +{ + SECTION("Empty input yields an empty curve") { + REQUIRE(parse_gradient_curve("").empty()); + REQUIRE(serialize_gradient_curve(GradientCurve{}).empty()); + } + + SECTION("Legacy 2-field anchors survive a parse/serialize round trip") { + GradientCurve c = parse_gradient_curve("0,0.15|0.5,0.5|1,0.85"); + REQUIRE(c.points.size() == 3); + + // Anchors with no tangent override serialize back to the 2-field legacy form + // (canonical fixed-precision, so compare by re-parsing rather than by string). + const std::string round_tripped = serialize_gradient_curve(c); + REQUIRE(round_tripped.find(",nan") == std::string::npos); + + GradientCurve c2 = parse_gradient_curve(round_tripped); + REQUIRE(c2.points.size() == c.points.size()); + for (size_t i = 0; i < c.points.size(); ++i) { + REQUIRE_THAT(c2.points[i].x, Catch::Matchers::WithinAbs(c.points[i].x, 1e-4)); + REQUIRE_THAT(c2.points[i].y, Catch::Matchers::WithinAbs(c.points[i].y, 1e-4)); + } + } + + SECTION("Sampling is clamped at the ends and monotone in between") { + GradientCurve c = parse_gradient_curve("0,0.15|0.5,0.5|1,0.85"); + REQUIRE_THAT(sample_gradient_curve(c, 0.0), Catch::Matchers::WithinAbs(0.15, 1e-9)); + REQUIRE_THAT(sample_gradient_curve(c, 1.0), Catch::Matchers::WithinAbs(0.85, 1e-9)); + // Outside the control point range the end values are held. + REQUIRE_THAT(sample_gradient_curve(c, -1.0), Catch::Matchers::WithinAbs(0.15, 1e-9)); + REQUIRE_THAT(sample_gradient_curve(c, 2.0), Catch::Matchers::WithinAbs(0.85, 1e-9)); + + double prev = sample_gradient_curve(c, 0.0); + for (int i = 1; i <= 20; ++i) { + double v = sample_gradient_curve(c, i / 20.0); + REQUIRE(v >= prev - 1e-9); + prev = v; + } + } + + SECTION("A curve with fewer than two points falls back to 0.5") { + GradientCurve c = parse_gradient_curve("0.5,0.7"); + REQUIRE_THAT(sample_gradient_curve(c, 0.3), Catch::Matchers::WithinAbs(0.5, 1e-9)); + } +} + +TEST_CASE("blend_color mixes two hex colors", "[FilamentMixer]") +{ + // ratio 0 keeps the first color, ratio 1 the second. + REQUIRE(blend_color("#FF0000", "#0000FF", 0.0f) == "#FF0000"); + REQUIRE(blend_color("#FF0000", "#0000FF", 1.0f) == "#0000FF"); + + SECTION("Blue and yellow make green, not grey (pigment mixing)") { + // The polynomial model approximates subtractive pigment behaviour. + std::string mixed = blend_color("#0021D0", "#FCD300", 0.5f); + REQUIRE(mixed.size() == 7); + REQUIRE(mixed[0] == '#'); + auto comp = [&](int i) { return std::stoi(mixed.substr(1 + 2 * i, 2), nullptr, 16); }; + // Green channel should dominate red and blue. + REQUIRE(comp(1) > comp(0)); + REQUIRE(comp(1) > comp(2)); + } +} + +TEST_CASE("blend_color_multi weights components", "[FilamentMixer]") +{ + SECTION("A single component is returned unchanged") { + REQUIRE(blend_color_multi({"#FF0000"}, {1}) == "#FF0000"); + } + + SECTION("Mixing a color with itself stays close to that color") { + // The mixer is a degree-4 polynomial fit of pigment behaviour, so mixing a color with + // itself lands near it rather than exactly on it; allow a small per-channel drift. + std::string mixed = blend_color_multi({"#123456", "#123456"}, {1, 1}); + REQUIRE(mixed.size() == 7); + auto comp = [](const std::string &hex, int i) { + return std::stoi(hex.substr(1 + 2 * i, 2), nullptr, 16); + }; + for (int i = 0; i < 3; ++i) + REQUIRE(std::abs(comp(mixed, i) - comp("#123456", i)) <= 8); + } +} diff --git a/tests/libslic3r/test_fill_corner_smoothing.cpp b/tests/libslic3r/test_fill_corner_smoothing.cpp new file mode 100644 index 0000000000..a9e752f250 --- /dev/null +++ b/tests/libslic3r/test_fill_corner_smoothing.cpp @@ -0,0 +1,194 @@ +#include <catch2/catch_all.hpp> + +#include <algorithm> +#include <cmath> +#include <limits> + +#include "libslic3r/Fill/FillCornerSmoothing.hpp" +#include "libslic3r/Polyline.hpp" +#include "libslic3r/libslic3r.h" + +using namespace Slic3r; + +namespace { + +// A right angle turn, with the outgoing leg ten times longer than the incoming one. +Polyline asymmetric_corner() +{ + return Polyline{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(10., 100.) }; +} + +double max_turn_cosine(const Polyline &polyline) +{ + double sharpest = 1.; + for (size_t i = 1; i + 1 < polyline.size(); ++i) { + const Vec2d incoming = (polyline[i] - polyline[i - 1]).cast<double>().normalized(); + const Vec2d outgoing = (polyline[i + 1] - polyline[i]).cast<double>().normalized(); + sharpest = std::min(sharpest, incoming.dot(outgoing)); + } + return sharpest; +} + +bool contains(const Polyline &polyline, const Point &point) +{ + return std::find(polyline.points.begin(), polyline.points.end(), point) != polyline.points.end(); +} + +const double tolerance = scaled<double>(0.0125); + +} // namespace + +TEST_CASE("Corner smoothing replaces a sharp vertex by a curve", "[FillCornerSmoothing]") +{ + const Polyline sharp = asymmetric_corner(); + Polyline smooth = sharp; + smooth_polyline_corners(smooth, 1., tolerance); + + REQUIRE(smooth.size() > sharp.size()); + REQUIRE(smooth.front() == sharp.front()); + REQUIRE(smooth.back() == sharp.back()); + // The right angle is gone, every remaining turn is a gentle one. + REQUIRE(max_turn_cosine(sharp) < 0.1); + REQUIRE(max_turn_cosine(smooth) > 0.9); + REQUIRE(smooth.length() < sharp.length()); +} + +TEST_CASE("Corner smoothing keeps the path untouched at a zero factor", "[FillCornerSmoothing]") +{ + const Polyline sharp = asymmetric_corner(); + + Polyline none = sharp; + smooth_polyline_corners(none, 0., tolerance); + REQUIRE(none.points == sharp.points); + + Polyline invalid = sharp; + smooth_polyline_corners(invalid, std::numeric_limits<double>::quiet_NaN(), tolerance); + REQUIRE(invalid.points == sharp.points); +} + +TEST_CASE("Corner smoothing consumes at most half of the shorter leg", "[FillCornerSmoothing]") +{ + // The curve must not reach beyond the middle of either adjoining segment, otherwise the curves of + // two adjacent corners would overlap. The shorter leg is 10mm long, so the corner at (10, 0) is + // left 5mm before it and rejoined 5mm past it, even though the other leg is 100mm long. + Polyline smooth = asymmetric_corner(); + smooth_polyline_corners(smooth, 1., tolerance); + + REQUIRE(contains(smooth, Point::new_scale(5., 0.))); + REQUIRE(contains(smooth, Point::new_scale(10., 5.))); + // A Bezier curve stays within the convex hull of its control points, so the rounded path stays + // inside the box spanned by the two legs. + for (const Point &point : smooth.points) { + REQUIRE(point.x() >= 0); + REQUIRE(point.y() >= 0); + REQUIRE(point.x() <= Point::new_scale(10., 0.).x()); + REQUIRE(point.y() <= Point::new_scale(0., 100.).y()); + } +} + +TEST_CASE("Corner smoothing scales the curve with the factor", "[FillCornerSmoothing]") +{ + Polyline half = asymmetric_corner(); + smooth_polyline_corners(half, 0.5, tolerance); + Polyline full = asymmetric_corner(); + smooth_polyline_corners(full, 1., tolerance); + + // Half of the factor leaves the 10mm leg half as far from the corner. + REQUIRE(contains(half, Point::new_scale(7.5, 0.))); + REQUIRE(contains(full, Point::new_scale(5., 0.))); + // A larger factor rounds a wider portion of the legs, cutting more of the corner off. + REQUIRE(full.length() < half.length()); +} + +TEST_CASE("Corner smoothing leaves hairpins sharp", "[FillCornerSmoothing]") +{ + // Both ends of a curve replacing a nearly reversing turn coincide, which would round the hairpin + // into a degenerate loop instead of a tip. + Polyline hairpin{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(0., 0.5) }; + const Polyline sharp = hairpin; + smooth_polyline_corners(hairpin, 1., tolerance); + REQUIRE(hairpin == sharp); +} + +TEST_CASE("Corner smoothing follows the flattening tolerance", "[FillCornerSmoothing]") +{ + Polyline coarse = asymmetric_corner(); + smooth_polyline_corners(coarse, 1., scaled<double>(0.2)); + Polyline fine = asymmetric_corner(); + smooth_polyline_corners(fine, 1., scaled<double>(0.001)); + + REQUIRE(fine.size() > coarse.size()); + REQUIRE(fine.front() == coarse.front()); + REQUIRE(fine.back() == coarse.back()); +} + +TEST_CASE("Corner smoothing emits no zero length segments", "[FillCornerSmoothing]") +{ + // Fully smoothed adjacent corners meet at the midpoint of the segment they share. + Polyline zigzag; + for (int i = 0; i < 8; ++i) + zigzag.points.emplace_back(Point::new_scale(i, i % 2 ? 1. : 0.)); + smooth_polyline_corners(zigzag, 1., tolerance); + + for (size_t i = 1; i < zigzag.size(); ++i) + REQUIRE((zigzag[i] - zigzag[i - 1]).cast<double>().squaredNorm() > 0.); +} + +TEST_CASE("Corner smoothing rounds every vertex of a polygon", "[FillCornerSmoothing]") +{ + // A polygon closes implicitly, so none of its corners may stay sharp, not even the first one. + const Polygon square{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(10., 10.), + Point::new_scale(0., 10.) }; + Polygons smooth{ square }; + smooth_polygons_corners(smooth, 1., tolerance); + const Polyline rounded = smooth.front().split_at_first_point(); + + REQUIRE(smooth.front().size() > square.size()); + REQUIRE(max_turn_cosine(rounded) > 0.9); + // The turn from the closing segment back into the first one must be gentle as well. + const Vec2d incoming = (rounded[rounded.size() - 1] - rounded[rounded.size() - 2]).cast<double>().normalized(); + const Vec2d outgoing = (rounded[1] - rounded[0]).cast<double>().normalized(); + REQUIRE(incoming.dot(outgoing) > 0.9); + // None of the corners is cut by more than half of a 10mm side. + for (const Point &point : smooth.front().points) { + REQUIRE(point.x() >= 0); + REQUIRE(point.y() >= 0); + REQUIRE(point.x() <= Point::new_scale(10., 0.).x()); + REQUIRE(point.y() <= Point::new_scale(0., 10.).y()); + } +} + +TEST_CASE("Corner smoothing keeps the ends of a path that returns to its start", "[FillCornerSmoothing][Regression]") +{ + // A branch of a lightning tree walks out and retraces its way back, ending where it started. Its + // ends are two free ends that happen to coincide, and joining them would close it into a loop. + Polyline retrace{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(10., 10.), + Point::new_scale(5., 10.), Point::new_scale(0., 0.) }; + const Polyline sharp = retrace; + smooth_polyline_corners(retrace, 1., tolerance); + + REQUIRE(retrace.size() > sharp.size()); + REQUIRE(retrace.front() == sharp.front()); + REQUIRE(retrace.back() == sharp.back()); +} + +TEST_CASE("Corner smoothing ignores vertices splitting a straight leg", "[FillCornerSmoothing][Regression]") +{ + // The triangular and grid infills emit a vertex halfway along the straight run joining two of + // their corners. Measuring the legs up to that vertex instead of up to the next corner let the + // rounding reach only half as far there as it did into the very same run elsewhere in the + // pattern, so geometrically identical corners came out rounded to different radii. + const Polyline plain{ Point::new_scale(0., 20.), Point::new_scale(10., 0.), + Point::new_scale(20., 0.), Point::new_scale(30., 20.) }; + Polyline split = plain; + split.points.insert(split.points.begin() + 2, Point::new_scale(15., 0.)); + + Polyline smooth_plain = plain; + smooth_polyline_corners(smooth_plain, 1., tolerance); + Polyline smooth_split = split; + smooth_polyline_corners(smooth_split, 1., tolerance); + + REQUIRE(smooth_split.points == smooth_plain.points); + // Both corners reach the middle of the 10mm run they share, which the extra vertex sat on. + REQUIRE(contains(smooth_plain, Point::new_scale(15., 0.))); +} diff --git a/tests/libslic3r/test_fill_plane_path.cpp b/tests/libslic3r/test_fill_plane_path.cpp index bbb75dce58..7fc7f4a6b0 100644 --- a/tests/libslic3r/test_fill_plane_path.cpp +++ b/tests/libslic3r/test_fill_plane_path.cpp @@ -27,6 +27,31 @@ public: } }; +class TestableOctagramSpiral : public FillOctagramSpiral +{ +public: + Points generate_points(double resolution, double smooth_factor = 0., coord_t max_coordinate = 7) + { + InfillPolylineOutput output(output_scale); + FillParams params; + params.smooth_factor = smooth_factor; + FillOctagramSpiral::generate(-max_coordinate, -max_coordinate, max_coordinate, max_coordinate, resolution, params, output); + return std::move(output.result()); + } +}; + +// Cosine of the sharpest turn of a path, 1 meaning it has no turn at all. +double sharpest_turn_cosine(const Points &points) +{ + double sharpest = 1.; + for (size_t i = 1; i + 1 < points.size(); ++i) { + const Vec2d incoming = (points[i] - points[i - 1]).cast<double>().normalized(); + const Vec2d outgoing = (points[i + 1] - points[i]).cast<double>().normalized(); + sharpest = std::min(sharpest, incoming.dot(outgoing)); + } + return sharpest; +} + double path_length(const Points &points) { double length = 0.; @@ -146,6 +171,35 @@ TEST_CASE("Hilbert smoothing joins straight segments with continuous curvature", REQUIRE(fine_entry_curvature < 0.25 * coarse_entry_curvature); } +TEST_CASE("Octagram spiral smoothing rounds the turns of the spiral", "[FillPlanePath]") +{ + const Points sharp = TestableOctagramSpiral().generate_points(0.005); + const Points smooth = TestableOctagramSpiral().generate_points(0.005, 1.); + + REQUIRE(smooth.size() > sharp.size()); + REQUIRE(smooth.front() == sharp.front()); + REQUIRE(smooth.back() == sharp.back()); + // The spiral alternates between 90 and 135 degree turns; both are rounded into gentle ones. + REQUIRE(sharpest_turn_cosine(sharp) < -0.7); + REQUIRE(sharpest_turn_cosine(smooth) > 0.9); + + for (size_t i = 1; i < smooth.size(); ++i) + REQUIRE((smooth[i] - smooth[i - 1]).cast<double>().squaredNorm() > 0.); +} + +TEST_CASE("Octagram spiral smooth factor controls corner curvature", "[FillPlanePath]") +{ + const Points sharp = TestableOctagramSpiral().generate_points(0.005); + const Points half_smooth = TestableOctagramSpiral().generate_points(0.005, 0.5); + const Points full_smooth = TestableOctagramSpiral().generate_points(0.005, 1.); + const Points invalid_factor = TestableOctagramSpiral().generate_points( + 0.005, std::numeric_limits<double>::quiet_NaN()); + + REQUIRE(path_length(full_smooth) < path_length(half_smooth)); + REQUIRE(path_length(half_smooth) < path_length(sharp)); + REQUIRE(invalid_factor == sharp); +} + TEST_CASE("Hilbert curve smooth factor controls corner curvature", "[FillPlanePath]") { const Points sharp = TestableHilbertCurve().generate_points(0.005); diff --git a/tests/libslic3r/test_geometry.cpp b/tests/libslic3r/test_geometry.cpp index b5f7b7ef98..b4bbe86cc6 100644 --- a/tests/libslic3r/test_geometry.cpp +++ b/tests/libslic3r/test_geometry.cpp @@ -574,11 +574,6 @@ TEST_CASE("Convex polygon intersection on two squares touching one vertex", "[Ge Polygon B = A; B.translate(10 / SCALING_FACTOR, 10 / SCALING_FACTOR); - SVG svg{std::string("one_vertex_touch") + ".svg"}; - svg.draw(A, "blue"); - svg.draw(B, "green"); - svg.Close(); - bool is_inters = Geometry::convex_polygons_intersect(A, B); REQUIRE(is_inters == false); diff --git a/tests/libslic3r/test_model.cpp b/tests/libslic3r/test_model.cpp new file mode 100644 index 0000000000..3a580e3be2 --- /dev/null +++ b/tests/libslic3r/test_model.cpp @@ -0,0 +1,40 @@ +#include <catch2/catch_all.hpp> + +#include "libslic3r/Model.hpp" + +using namespace Slic3r; + +// convex_hull_2d does not clip geometry below the bed, so these cases avoid +// sinking transforms. +TEST_CASE("A part's 2D convex hull is its footprint projected onto the bed", "[Model]") +{ + Model model; + ModelObject* object = model.add_object(); + // Keep the cube's raw coordinates ([0,20] on every axis): the default + // add_volume re-centers the geometry, which would move the footprint. + object->add_volume(make_cube(20, 20, 20), ModelVolumeType::MODEL_PART, false); + + SECTION("identity transform yields the 20 mm square") { + const Polygon hull = object->convex_hull_2d(Geometry::Transformation{}.get_matrix()); + const BoundingBox bb = hull.bounding_box(); + CHECK(hull.size() == 4); + CHECK(bb.min.x() == scaled(0.)); + CHECK(bb.min.y() == scaled(0.)); + CHECK(bb.max.x() == scaled(20.)); + CHECK(bb.max.y() == scaled(20.)); + } + + SECTION("scaling and offset move and grow the footprint") { + Geometry::Transformation t; + t.set_scaling_factor({2, 2, 2}); // cube now spans [0,40] + t.set_offset({10, 5, 0}); // then shift +10 in X, +5 in Y + + const Polygon hull = object->convex_hull_2d(t.get_matrix()); + const BoundingBox bb = hull.bounding_box(); + CHECK(hull.size() == 4); + CHECK(bb.min.x() == scaled(10.)); + CHECK(bb.min.y() == scaled(5.)); + CHECK(bb.max.x() == scaled(50.)); + CHECK(bb.max.y() == scaled(45.)); + } +} diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 351535dd9d..c75a05949e 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -1,32 +1,19 @@ #include <catch2/catch_all.hpp> #include <boost/filesystem.hpp> +#include <fstream> #include "libslic3r/PresetBundle.hpp" #include "libslic3r/AppConfig.hpp" +#include "test_utils.hpp" + using namespace Slic3r; namespace { namespace fs = boost::filesystem; -struct TempPresetDir { - fs::path path; - - TempPresetDir() - { - path = fs::temp_directory_path() / fs::unique_path("orcaslicer-preset-%%%%-%%%%-%%%%"); - fs::create_directories(path); - } - - ~TempPresetDir() - { - boost::system::error_code ec; - fs::remove_all(path, ec); - } -}; - void write_print_preset(const DynamicPrintConfig &default_config, const fs::path &file, const std::string &name, const std::string &inherits = {}) { DynamicPrintConfig config(default_config); @@ -82,17 +69,17 @@ struct RenameTestCollection : public PresetCollection TEST_CASE("Preset identity is canonicalized from load path", "[Preset][Identity]") { - TempPresetDir temp_dir; + ScopedTemporaryDir temp_dir; PresetBundle bundle; PresetsConfigSubstitutions substitutions; - write_print_preset(bundle.prints.default_preset().config, temp_dir.path / PRESET_PRINT_NAME / "User.json", "User"); - write_print_preset(bundle.prints.default_preset().config, temp_dir.path / PRESET_LOCAL_DIR / "bundle-1" / PRESET_PRINT_NAME / "LocalBundle.json", "LocalBundle"); - write_print_preset(bundle.prints.default_preset().config, temp_dir.path / PRESET_SUBSCRIBED_DIR / "remote-1" / PRESET_PRINT_NAME / "Subscribed.json", "Subscribed"); + write_print_preset(bundle.prints.default_preset().config, temp_dir.path() / PRESET_PRINT_NAME / "User.json", "User"); + write_print_preset(bundle.prints.default_preset().config, temp_dir.path() / PRESET_LOCAL_DIR / "bundle-1" / PRESET_PRINT_NAME / "LocalBundle.json", "LocalBundle"); + write_print_preset(bundle.prints.default_preset().config, temp_dir.path() / PRESET_SUBSCRIBED_DIR / "remote-1" / PRESET_PRINT_NAME / "Subscribed.json", "Subscribed"); - bundle.prints.load_presets(temp_dir.path.string(), PRESET_PRINT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable); - bundle.prints.load_presets((temp_dir.path / PRESET_LOCAL_DIR / "bundle-1").string(), PRESET_PRINT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable); - bundle.prints.load_presets((temp_dir.path / PRESET_SUBSCRIBED_DIR / "remote-1").string(), PRESET_PRINT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable); + bundle.prints.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable); + bundle.prints.load_presets((temp_dir.path() / PRESET_LOCAL_DIR / "bundle-1").string(), PRESET_PRINT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable); + bundle.prints.load_presets((temp_dir.path() / PRESET_SUBSCRIBED_DIR / "remote-1").string(), PRESET_PRINT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable); const Preset *root_user = bundle.prints.find_preset("User"); REQUIRE(root_user != nullptr); @@ -112,14 +99,14 @@ TEST_CASE("Preset identity is canonicalized from load path", "[Preset][Identity] TEST_CASE("Legacy bundle import without bundle metadata stays in the user preset directory", "[Preset][Identity]") { - TempPresetDir temp_dir; + ScopedTemporaryDir temp_dir; PresetBundle bundle; PresetsConfigSubstitutions substitutions; std::vector<std::string> result; int overwrite = 0; - std::string file = (temp_dir.path / "legacy-bundle" / "Imported.json").string(); - const fs::path user_root = temp_dir.path / "user"; + std::string file = (temp_dir.path() / "legacy-bundle" / "Imported.json").string(); + const fs::path user_root = temp_dir.path() / "user"; write_print_preset(bundle.prints.default_preset().config, file, "Imported"); fs::create_directories(user_root); @@ -146,7 +133,7 @@ TEST_CASE("Current vendor type tolerates missing printer model", "[Preset][Bundl { PresetBundle bundle; - VendorProfile orca_vendor("ORCA"); + VendorProfile orca_vendor; orca_vendor.id = "ORCA"; VendorProfile::PrinterModel model; model.name = "Orca Test"; orca_vendor.models.emplace_back(model); @@ -157,6 +144,31 @@ TEST_CASE("Current vendor type tolerates missing printer model", "[Preset][Bundl CHECK(bundle.get_current_vendor_type() == VendorType::Unknown); } +TEST_CASE("A malformed entry in a vendor's preset list is counted, not thrown", "[Preset][Bundle]") +{ + ScopedTemporaryDir dir; + + // A bare number where the list wants an object. An array element has no key, + // so reporting one as if it did throws nlohmann's invalid_iterator - which is + // not a parse_error, and escapes the catch around the vendor profile parse. + std::ofstream((dir.path() / "Acme.json").string()) + << R"({"version":"1.0.0","name":"Acme","process_list":[123,)" + << R"({"name":"0.20mm Standard @Acme","sub_path":"process/standard.json"}]})"; + fs::create_directories(dir.path() / "Acme" / "process"); + std::ofstream((dir.path() / "Acme" / "process" / "standard.json").string()) + << R"({"type":"process","name":"0.20mm Standard @Acme","from":"system",)" + << R"("instantiation":"true","layer_height":"0.2"})"; + + PresetBundle bundle; + size_t loaded = 0; + REQUIRE_NOTHROW(loaded = bundle.load_vendor_configs_from_json( + dir.path().string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent).second); + + CHECK(bundle.error_count() > 0); // the malformed element was counted + CHECK(loaded == 1); // the well-formed one beside it still loaded +} + TEST_CASE("Printer extruder count tolerates missing nozzle diameter", "[Preset][Bundle]") { PresetBundle bundle; @@ -172,6 +184,44 @@ TEST_CASE("Printer extruder count tolerates missing nozzle diameter", "[Preset][ CHECK(bundle.get_printer_extruder_count() == 2); } +TEST_CASE("Selected printer uses its default or saved bed type", "[Preset][Bundle]") +{ + PresetBundle bundle; + Preset& printer = add_inmemory_preset(bundle.printers, "Test Printer"); + printer.is_system = true; + printer.config.option<ConfigOptionString>("printer_model")->value = "TEST-MODEL"; + printer.config.option<ConfigOptionString>("printer_variant")->value = "0.4"; + printer.config.option<ConfigOptionString>("default_bed_type")->value = "Engineering Plate"; + + AppConfig app_config; + app_config.set("curr_bed_type", std::to_string(static_cast<int>(btPTE))); + PresetBundle::PresetPreferences preferred_selection; + BedType expected_bed_type; + + SECTION("New printer uses its symbolic default") { + expected_bed_type = btEP; + preferred_selection = {"TEST-MODEL", "0.4"}; + } + SECTION("Re-enabled printer uses its saved selection") { + expected_bed_type = btPC; + preferred_selection = {"TEST-MODEL", "0.4"}; + app_config.set_printer_setting("Test Printer", "curr_bed_type", + std::to_string(static_cast<int>(expected_bed_type))); + } + SECTION("Existing printer keeps its saved selection after presets reload") { + expected_bed_type = btPCT; + app_config.set("presets", PRESET_PRINTER_NAME, "Test Printer"); + app_config.set_printer_setting("Test Printer", "curr_bed_type", + std::to_string(static_cast<int>(expected_bed_type))); + } + + bundle.load_selections(app_config, preferred_selection); + bundle.export_selections(app_config); + + CHECK(bundle.project_config.opt_enum<BedType>("curr_bed_type") == expected_bed_type); + CHECK(app_config.get_printer_setting("Test Printer", "curr_bed_type") == std::to_string(static_cast<int>(expected_bed_type))); +} + TEST_CASE("find_preset resolves a system preset's renamed_from", "[Preset][Rename]") { RenameTestCollection coll; @@ -252,7 +302,7 @@ TEST_CASE("find_preset2 auto-matches removed Generic vendor profiles to the libr TEST_CASE("Renamed parent is normalized into a loaded preset's inherits", "[Preset][Rename]") { - TempPresetDir temp_dir; + ScopedTemporaryDir temp_dir; RenameTestCollection coll; // Current parent, renamed from "Old Process". @@ -262,10 +312,10 @@ TEST_CASE("Renamed parent is normalized into a loaded preset's inherits", "[Pres // A user preset on disk that still inherits the OLD name. write_preset_with_inherits(coll.default_preset().config, - temp_dir.path / PRESET_PRINT_NAME / "Child.json", "Child", "Old Process"); + temp_dir.path() / PRESET_PRINT_NAME / "Child.json", "Child", "Old Process"); PresetsConfigSubstitutions substitutions; - coll.load_presets(temp_dir.path.string(), PRESET_PRINT_NAME, substitutions, + coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable); const Preset *child = coll.find_preset("Child"); @@ -279,17 +329,17 @@ TEST_CASE("Renamed parent is normalized into a loaded preset's inherits", "[Pres TEST_CASE("Removed Generic parent is normalized into a loaded filament's inherits", "[Preset][Rename]") { - TempPresetDir temp_dir; + ScopedTemporaryDir temp_dir; PresetBundle bundle; add_inmemory_preset(bundle.filaments, "Generic PLA @System"); // A user filament that still inherits a removed "<vendor> Generic PLA" profile. write_preset_with_inherits(bundle.filaments.default_preset().config, - temp_dir.path / PRESET_FILAMENT_NAME / "MyPLA.json", "MyPLA", "Voron Generic PLA"); + temp_dir.path() / PRESET_FILAMENT_NAME / "MyPLA.json", "MyPLA", "Voron Generic PLA"); PresetsConfigSubstitutions substitutions; - bundle.filaments.load_presets(temp_dir.path.string(), PRESET_FILAMENT_NAME, substitutions, + bundle.filaments.load_presets(temp_dir.path().string(), PRESET_FILAMENT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable); const Preset *child = bundle.filaments.find_preset("MyPLA"); @@ -488,3 +538,393 @@ TEST_CASE("Plugin capability override keys are scoped per preset type", "[Preset } } +namespace { + +// A standalone filament collection that exposes the protected library masking builder, so the Orca +// Filament Library scenario can be set up without the full system-profile load pipeline. +struct LibraryFilamentTestCollection : public PresetCollection +{ + LibraryFilamentTestCollection() + : PresetCollection(Preset::TYPE_FILAMENT, Preset::filament_options(), + static_cast<const PrintRegionConfig &>(FullPrintConfig::defaults())) + {} + using PresetCollection::update_library_profile_excluded_from; +}; + +} // namespace + +// Orca: a filament in the Orca Filament Library that names its compatible printers has to hide the generic +// library filament sharing its alias, the same way a vendor owned filament does. Otherwise both are compatible +// with that printer and the plater combo box lists the shared alias twice. +TEST_CASE("A printer specific filament supersedes the generic library filament with the same alias", "[Preset][Bundle]") +{ + LibraryFilamentTestCollection filaments; + PresetCollection printers(Preset::TYPE_PRINTER, Preset::printer_options(), + static_cast<const PrintRegionConfig &>(FullPrintConfig::defaults())); + // The masking keys off the vendor name, which VendorProfile's constructor does not derive from the id. + VendorProfile library(PresetBundle::ORCA_FILAMENT_LIBRARY); + VendorProfile vendor("Vendor"); + library.name = PresetBundle::ORCA_FILAMENT_LIBRARY; + vendor.name = "Vendor"; + + auto add_filament = [&filaments](const VendorProfile &owner, const std::string &name, std::vector<std::string> compatible_printers) { + Preset &preset = add_inmemory_preset(filaments, name); + preset.alias = "Generic ABS"; + preset.vendor = &owner; + preset.config.option<ConfigOptionStrings>("compatible_printers", true)->values = std::move(compatible_printers); + }; + + add_filament(library, "Generic ABS @System", {}); + add_filament(library, "Generic ABS @Printer A", { "Printer A" }); + add_filament(vendor, "Generic ABS @Printer B", { "Printer B" }); + + filaments.update_library_profile_excluded_from(); + + const Preset *generic = filaments.find_preset("Generic ABS @System"); + REQUIRE(generic != nullptr); + CHECK(generic->m_excluded_from.count("Printer A") == 1); + CHECK(generic->m_excluded_from.count("Printer B") == 1); + CHECK(generic->m_excluded_from.size() == 2); + + // A printer specific profile names printers, so it is never the one being hidden - not even by itself. + const Preset *specific = filaments.find_preset("Generic ABS @Printer A"); + REQUIRE(specific != nullptr); + CHECK(specific->m_excluded_from.empty()); + + // ...and the generic profile really drops out of the compatible set on the printer it is hidden from. + add_inmemory_preset(printers, "Printer A"); + add_inmemory_preset(printers, "Printer C"); + const Preset *printer_a = printers.find_preset("Printer A"); + const Preset *printer_c = printers.find_preset("Printer C"); + REQUIRE(printer_a != nullptr); + REQUIRE(printer_c != nullptr); + + const PresetWithVendorProfile generic_lib(*generic, &library); + CHECK_FALSE(is_compatible_with_printer(generic_lib, PresetWithVendorProfile(*printer_a, nullptr))); + CHECK(is_compatible_with_printer(generic_lib, PresetWithVendorProfile(*printer_c, nullptr))); +} + + +namespace { + +const char *kMixedKeys[] = { + "filament_is_mixed", + "filament_mixed_components", + "filament_mixed_sublayer_ratios", + "filament_mixed_gradient", + "filament_mixed_gradient_range", + "filament_mixed_gradient_curve", + "filament_mixed_gradient_per_part", +}; + +} // namespace + +// Mixed-color filament metadata lives in project_config as parallel per-filament arrays. +// set_num_filaments() is the single place that grows them alongside filament_colour; if it +// misses them, creating a mixed slot writes past the end of the short arrays. +TEST_CASE("set_num_filaments keeps mixed-color arrays in step with the filament count", "[Preset][Bundle][FilamentMixer]") +{ + auto mixed_array_size = [](const DynamicPrintConfig &cfg, const std::string &key) -> size_t { + if (const auto *b = cfg.option<ConfigOptionBools>(key)) + return b->values.size(); + if (const auto *s = cfg.option<ConfigOptionStrings>(key)) + return s->values.size(); + return size_t(-1); // key missing entirely + }; + + PresetBundle bundle; + + const unsigned int n = GENERATE(2u, 4u, 8u); + bundle.set_num_filaments(n, std::string("#FF0000")); + + REQUIRE(bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values.size() == n); + for (const char *key : kMixedKeys) { + DYNAMIC_SECTION("grown: " << key) { + CHECK(mixed_array_size(bundle.project_config, key) == n); + } + } + + SECTION("shrinking keeps them in step too") { + bundle.set_num_filaments(1, std::string("#00FF00")); + REQUIRE(bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values.size() == 1); + for (const char *key : kMixedKeys) + CHECK(mixed_array_size(bundle.project_config, key) == 1); + } +} + +// A mix is described by 1-based indices into the project's filament list, which Orca rebuilds +// from the selected printer's snapshot (filament_%02u / filament_colors) at startup and on every +// printer selection. Held anywhere but that same per-printer snapshot, the mixed arrays end up +// indexing a filament list they were never saved against. +TEST_CASE("Mixed-color filament metadata is snapshotted per printer, with its filament list", "[Preset][Bundle][FilamentMixer]") +{ + PresetBundle bundle; + // export_selections skips the built-in "Default Printer" placeholder entirely. + add_inmemory_preset(bundle.printers, "Test Printer"); + bundle.printers.select_preset_by_name("Test Printer", true); + bundle.set_num_filaments(2u, std::string("#FF0000")); + bundle.project_config.option<ConfigOptionBools>("filament_is_mixed")->values = { false, true }; + bundle.project_config.option<ConfigOptionStrings>("filament_mixed_components")->values = { "", "1,2" }; + bundle.project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values = { "", "0.5,0.5" }; + + AppConfig app_config; + bundle.export_selections(app_config); + + const std::string printer_name = bundle.printers.get_selected_preset_name(); + for (const char *key : kMixedKeys) { + DYNAMIC_SECTION("per printer, not global: " << key) { + CHECK(app_config.has_printer_setting(printer_name, key)); + CHECK_FALSE(app_config.has("presets", key)); + } + } + + SECTION("with the encoding load_selections reads back") { + CHECK(app_config.get_printer_setting(printer_name, "filament_is_mixed") == "0,1"); + CHECK(app_config.get_printer_setting(printer_name, "filament_mixed_components") == "|1,2"); + CHECK(app_config.get_printer_setting(printer_name, "filament_mixed_sublayer_ratios") == "|0.5,0.5"); + } +} + +// The gradient curve is the one mixed array whose values contain '|' themselves — it separates the +// control points — so it cannot be '|'-joined into the app config like its siblings without a +// multi-point curve being split across filament slots on the way back in. +TEST_CASE("A multi-point gradient curve survives the app-config snapshot", "[Preset][Bundle][FilamentMixer]") +{ + const std::vector<std::string> curves = { "", "", "0,0|0.5,0.3|1,1" }; + + PresetBundle bundle; + add_inmemory_preset(bundle.printers, "Test Printer"); + bundle.printers.select_preset_by_name("Test Printer", true); + bundle.set_num_filaments(3u, std::string("#FF0000")); + bundle.project_config.option<ConfigOptionStrings>("filament_mixed_gradient_curve")->values = curves; + + AppConfig app_config; + bundle.export_selections(app_config); + + // Decoding the stored form returns the three slots intact, curve delimiters and all. A plain + // '|' join would decode as five slots here instead of three. + std::vector<std::string> decoded; + REQUIRE(unescape_strings_cstyle( + app_config.get_printer_setting(bundle.printers.get_selected_preset_name(), "filament_mixed_gradient_curve"), decoded)); + CHECK(decoded == curves); +} + +// A multi-tool printer sizes the filament list from its nozzle count. Mixed-color slots are extra +// virtual filaments at the tail of that list with no nozzle of their own, so the count has to +// allow for them: sizing to the nozzle count alone drops the project's mixes and strips every +// painted facet above the new count. +TEST_CASE("Sizing the filament list to a multi-tool nozzle count keeps mixed slots", "[Preset][Bundle][FilamentMixer]") +{ + // The 5-slot layout of a 4-tool project carrying one mix of filaments 2 and 3. + const size_t nozzle_count = 4; + PresetBundle bundle; + bundle.set_num_filaments(5u, std::string("#FF0000")); + bundle.project_config.option<ConfigOptionBools>("filament_is_mixed")->values = + { false, false, false, false, true }; + bundle.project_config.option<ConfigOptionStrings>("filament_mixed_components")->values = + { "", "", "", "", "2,3" }; + + REQUIRE(bundle.num_mixed_filaments() == 1); + + SECTION("nozzle count plus the mixed slots preserves the mix") { + bundle.set_num_filaments(nozzle_count + bundle.num_mixed_filaments(), std::string("#00FF00")); + + CHECK(bundle.filament_presets.size() == 5); + CHECK(bundle.num_mixed_filaments() == 1); + CHECK(bundle.is_mixed_filament(4)); + CHECK(bundle.project_config.option<ConfigOptionStrings>("filament_mixed_components")->values[4] == "2,3"); + } + + SECTION("the nozzle count alone is what truncated it away") { + bundle.set_num_filaments(nozzle_count, std::string("#00FF00")); + + CHECK(bundle.filament_presets.size() == nozzle_count); + CHECK(bundle.num_mixed_filaments() == 0); + } +} + +// The nozzle-count top-up in update_multi_material_filament_presets() grows filament_presets on +// its own, so a physical count derived from that list reports a slot no per-filament array has +// yet. That is what made the extruder-count handler conclude there was nothing to add and leave +// the new sidebar combo with no colour to draw. +TEST_CASE("The physical filament count is not fooled by a lone filament_presets top-up", "[Preset][Bundle][FilamentMixer]") +{ + PresetBundle bundle; + + SECTION("no mixed slots") { + bundle.set_num_filaments(4u, std::string("#FF0000")); + bundle.printers.get_edited_preset().config.option<ConfigOptionFloats>("nozzle_diameter", true)->values = + { 0.4, 0.4, 0.4, 0.4, 0.4 }; + bundle.update_multi_material_filament_presets(); + + REQUIRE(bundle.filament_presets.size() == 5); // the top-up moved this list on its own + REQUIRE(bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values.size() == 4); + CHECK(bundle.num_physical_filaments() == 4); + } + + SECTION("behind a mixed tail") { + bundle.set_num_filaments(5u, std::string("#FF0000")); + bundle.project_config.option<ConfigOptionBools>("filament_is_mixed")->values = + { false, false, false, false, true }; + bundle.printers.get_edited_preset().config.option<ConfigOptionFloats>("nozzle_diameter", true)->values = + { 0.4, 0.4, 0.4, 0.4, 0.4, 0.4 }; + bundle.update_multi_material_filament_presets(); + + REQUIRE(bundle.filament_presets.size() == 6); + REQUIRE(bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values.size() == 5); + CHECK(bundle.num_physical_filaments() == 4); + CHECK(bundle.num_mixed_filaments() == 1); + } +} + +// Which slots are new is a fact about the per-filament arrays, not about filament_presets, for the +// same reason. Keyed off the wrong one, a freshly opened slot silently keeps filament 1's colour. +TEST_CASE("New filament colours are placed by array position", "[Preset][Bundle][FilamentMixer]") +{ + PresetBundle bundle; + bundle.set_num_filaments(4u, std::string("#FF0000")); + bundle.printers.get_edited_preset().config.option<ConfigOptionFloats>("nozzle_diameter", true)->values = + { 0.4, 0.4, 0.4, 0.4, 0.4 }; + bundle.update_multi_material_filament_presets(); + REQUIRE(bundle.filament_presets.size() == 5); + REQUIRE(bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values.size() == 4); + + // The call Sidebar::add_custom_filament makes once the extruder count opens a slot. + bundle.set_num_filaments(5u, std::string("#00FF00")); + + const auto &colours = bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values; + REQUIRE(colours.size() == 5); + CHECK(colours[4] == "#00FF00"); // not colours[0], which resize() would have padded with +} + +// The mixed-slot flags are written into the app config on exit and read back on the next start. +// If the read side loses them the slots survive as filaments but stop being mixes, so the project +// comes back with the mix showing as an ordinary physical filament. +TEST_CASE("A saved mix is still a mix after an app restart", "[Preset][Bundle][FilamentMixer]") +{ + AppConfig app_config; + + // Last session: a 4-tool project carrying one mix of filaments 2 and 3 at the tail. + { + PresetBundle bundle; + add_inmemory_preset(bundle.printers, "Test Printer"); + bundle.printers.select_preset_by_name("Test Printer", true); + add_inmemory_preset(bundle.filaments, "Test Filament"); + bundle.filaments.select_preset_by_name("Test Filament", true); + bundle.set_num_filaments(5u, std::string("#FF0000")); + bundle.filament_presets.assign(5, "Test Filament"); + bundle.project_config.option<ConfigOptionBools>("filament_is_mixed")->values = + { false, false, false, false, true }; + bundle.project_config.option<ConfigOptionStrings>("filament_mixed_components")->values = + { "", "", "", "", "2,3" }; + bundle.export_selections(app_config); + + REQUIRE(app_config.get_printer_setting("Test Printer", "filament_is_mixed") == "0,0,0,0,1"); + } + + // This session. + PresetBundle bundle; + add_inmemory_preset(bundle.printers, "Test Printer"); + add_inmemory_preset(bundle.filaments, "Test Filament"); + bundle.load_selections(app_config); + + CHECK(bundle.filament_presets.size() == 5); + CHECK(bundle.num_mixed_filaments() == 1); + CHECK(bundle.is_mixed_filament(4)); + CHECK(bundle.project_config.option<ConfigOptionStrings>("filament_mixed_components")->values[4] == "2,3"); +} + +// The same restart, on the printer shape that actually shows the bug: a 4-tool changer whose +// saved filament list is one longer than its nozzle count, because the extra slot is the mix. +TEST_CASE("A saved mix survives a restart on a multi-tool printer", "[Preset][Bundle][FilamentMixer]") +{ + auto make_toolchanger = [](PresetBundle &bundle) -> Preset & { + Preset &p = add_inmemory_preset(bundle.printers, "Tool Changer"); + p.config.option<ConfigOptionFloats>("nozzle_diameter", true)->values = { 0.4, 0.4, 0.4, 0.4 }; + p.config.option<ConfigOptionBool>("single_extruder_multi_material", true)->value = false; + return p; + }; + + AppConfig app_config; + { + PresetBundle bundle; + make_toolchanger(bundle); + bundle.printers.select_preset_by_name("Tool Changer", true); + add_inmemory_preset(bundle.filaments, "Test Filament"); + bundle.filaments.select_preset_by_name("Test Filament", true); + bundle.set_num_filaments(5u, std::string("#FF0000")); + bundle.filament_presets.assign(5, "Test Filament"); + bundle.project_config.option<ConfigOptionBools>("filament_is_mixed")->values = + { false, false, false, false, true }; + bundle.project_config.option<ConfigOptionStrings>("filament_mixed_components")->values = + { "", "", "", "", "1,2" }; + bundle.export_selections(app_config); + REQUIRE(app_config.get_printer_setting("Tool Changer", "filament_is_mixed") == "0,0,0,0,1"); + } + + PresetBundle bundle; + make_toolchanger(bundle); + add_inmemory_preset(bundle.filaments, "Test Filament"); + bundle.load_selections(app_config); + + CHECK(bundle.filament_presets.size() == 5); + CHECK(bundle.num_mixed_filaments() == 1); + CHECK(bundle.is_mixed_filament(4)); + + SECTION("and through the GUI startup calls that follow it") { + // GUI_App::load_current_presets sizes the list for a non-SEMM printer, growing only. + const size_t target = 4u + bundle.num_mixed_filaments(); + if (target > bundle.filament_presets.size()) + bundle.set_num_filaments(target); + CHECK(bundle.num_mixed_filaments() == 1); + + // TabPrinter::extruders_count_changed. + bundle.on_extruders_count_changed(4); + CHECK(bundle.num_mixed_filaments() == 1); + + // Tab::select_preset re-reads the snapshot when remember_printer_config is on. + bundle.update_selections(app_config); + CHECK(bundle.filament_presets.size() == 5); + CHECK(bundle.num_mixed_filaments() == 1); + CHECK(bundle.is_mixed_filament(4)); + } +} + +// The startup sizing in GUI_App::load_current_presets targets the nozzle count plus the mixes. +// That is a floor, never a ceiling: set_num_filaments() trims at the raw tail, which is exactly +// where the mixes live, so applying the target to a longer list deletes them. A list longer than +// the target is reachable - raising the extruder count without saving the printer preset leaves +// the extra physical slot behind on the next start - so the startup sizing must only ever grow. +TEST_CASE("Sizing down to the nozzle count plus mixes is what eats the mixed tail", "[Preset][Bundle][FilamentMixer]") +{ + // 5 physical + 1 mix, on a printer preset still reporting 4 nozzles. + const size_t nozzle_count = 4; + PresetBundle bundle; + bundle.set_num_filaments(6u, std::string("#FF0000")); + bundle.project_config.option<ConfigOptionBools>("filament_is_mixed")->values = + { false, false, false, false, false, true }; + bundle.project_config.option<ConfigOptionStrings>("filament_mixed_components")->values = + { "", "", "", "", "", "1,2" }; + REQUIRE(bundle.num_physical_filaments() == 5); + + const size_t target = nozzle_count + bundle.num_mixed_filaments(); + REQUIRE(target < bundle.filament_presets.size()); + + SECTION("applied as written, the mix is gone and every slot reads physical") { + bundle.set_num_filaments(target); + + CHECK(bundle.filament_presets.size() == target); + CHECK(bundle.num_mixed_filaments() == 0); + CHECK(bundle.num_physical_filaments() == target); + } + + SECTION("applied as a floor, the mix is left alone") { + if (target > bundle.filament_presets.size()) + bundle.set_num_filaments(target); + + CHECK(bundle.filament_presets.size() == 6); + CHECK(bundle.num_mixed_filaments() == 1); + CHECK(bundle.is_mixed_filament(5)); + CHECK(bundle.project_config.option<ConfigOptionStrings>("filament_mixed_components")->values[5] == "1,2"); + } +} diff --git a/tests/libslic3r/test_step.cpp b/tests/libslic3r/test_step.cpp new file mode 100644 index 0000000000..a2aa7218f1 --- /dev/null +++ b/tests/libslic3r/test_step.cpp @@ -0,0 +1,93 @@ +#include <catch2/catch_all.hpp> + +#include <boost/nowide/fstream.hpp> + +#include "libslic3r/Model.hpp" +#include "libslic3r/Format/STEP.hpp" +#include "test_utils.hpp" + +using namespace Slic3r; + +static void write_step_line(const std::string &path, const std::string &line) +{ + boost::nowide::ofstream file(path, std::ios::binary); + file << "ISO-10303-21;\n" << line << "\nEND-ISO-10303-21;\n"; +} + +// preprocess() hands back the input path unless it transcoded into a temporary. +static std::string preprocess_result(const std::string &line) +{ + ScopedSlic3rTemporaryDir scratch; + + ScopedTemporaryFile step(".step"); + write_step_line(step.string(), line); + + std::string output_path; + StepPreProcessor preprocessor; + REQUIRE(preprocessor.preprocess(step.string().c_str(), output_path)); + + return output_path == step.string() ? "untouched" : "transcoded"; +} + +// data/utf8_part_names.step is three boxes written by OCCT's own STEP writer, whose +// PRODUCT names were then patched to raw UTF-8. Most CAD exporters write non-ASCII names +// that way rather than in the \X2\ escape form. The third part is ASCII, as a control. +TEST_CASE("Part names with multi-byte UTF-8 survive import", "[Step]") +{ + // getNamedSolids() replaces a name that isUtf8() rejects with a running number. + const std::string path = TEST_DATA_DIR PATH_SEPARATOR "utf8_part_names.step"; + + Model model; + bool cancel = false; + Step step(path); // no isUtf8Fn, matching how Model::read_from_step builds it + + REQUIRE(step.load() == Step::Step_Status::LOAD_SUCCESS); + REQUIRE(step.mesh(&model, cancel, false) == Step::Step_Status::MESH_SUCCESS); + + REQUIRE(model.objects.size() == 1); + const ModelObject *object = model.objects.front(); + REQUIRE(object->volumes.size() == 3); + // "ce" is split off, or the hex escape would swallow it as further hex digits. + CHECK(object->volumes[0]->name == "pi\xC3\xA8" "ce"); + CHECK(object->volumes[1]->name == "Geh\xC3\xA4use"); + CHECK(object->volumes[2]->name == "bracket"); +} + +TEST_CASE("isUtf8 recognises two, three and four byte sequences", "[Step]") +{ + CHECK(StepPreProcessor::isUtf8("\xC3\xA9")); // U+00E9 + CHECK(StepPreProcessor::isUtf8("\xE4\xB8\xAD")); // U+4E2D + CHECK(StepPreProcessor::isUtf8("\xF0\x9F\x94\xA9")); // U+1F529 + CHECK_FALSE(StepPreProcessor::isUtf8("\x81\x30")); // 0x81 is not a lead byte + CHECK_FALSE(StepPreProcessor::isUtf8("\xC3")); // truncated sequence +} + +// The only caller of isGBK is preprocess(), which nothing calls today. +TEST_CASE("Encoding detection decides whether a step file is transcoded", "[Step]") +{ + SECTION("UTF-8, so left alone") + { + // A two byte sequence also satisfies every GBK range, so misdetecting it as + // not-UTF-8 sends it to be transcoded. + const std::string sequence = GENERATE(std::string("\xC3\xA9"), // U+00E9 + std::string("\xE4\xB8\xAD"), // U+4E2D + std::string("\xF0\x9F\x94\xA9")); // U+1F529 + + CHECK(preprocess_result("NAME('" + sequence + "');") == "untouched"); + } + + SECTION("neither UTF-8 nor GBK, so left alone") + { + // 0x81 is not a UTF-8 lead byte, and 0x30 is below the 0x40 floor for a GBK trail. + CHECK(preprocess_result("NAME('\x81\x30');") == "untouched"); + } + + SECTION("GBK, so transcoded") + { + // U+554A in GBK, whose lead byte is not valid UTF-8. Pins the other direction, + // since a detector that never reports GBK would pass every case above. + CHECK(preprocess_result("NAME('\xB0\xA1');") == "transcoded"); + } + + SECTION("plain ASCII, so left alone") { CHECK(preprocess_result("NAME('bracket');") == "untouched"); } +} diff --git a/tests/libslic3r/test_toolordering_nozzle_group.cpp b/tests/libslic3r/test_toolordering_nozzle_group.cpp index dc54aae80a..26e36c0dbf 100644 --- a/tests/libslic3r/test_toolordering_nozzle_group.cpp +++ b/tests/libslic3r/test_toolordering_nozzle_group.cpp @@ -8,6 +8,8 @@ #include "libslic3r/Print.hpp" #include "libslic3r/TriangleMesh.hpp" +#include "test_utils.hpp" + #include <algorithm> #include <map> #include <set> @@ -708,10 +710,9 @@ TEST_CASE("Sequential selector prints publish a stitched result and cache the pl REQUIRE(print.config().filament_self_index.values.size() >= print.config().filament_map.values.size()); // Export must consume the cached plans and produce g-code without throwing. - boost::filesystem::path gcode_path = boost::filesystem::temp_directory_path() / "orca_seq_dynamic_publish_test.gcode"; - REQUIRE_NOTHROW(print.export_gcode(gcode_path.string(), nullptr, nullptr)); - REQUIRE(boost::filesystem::exists(gcode_path)); - boost::filesystem::remove(gcode_path); + ScopedTemporaryFile gcode(".gcode"); + REQUIRE_NOTHROW(print.export_gcode(gcode.string(), nullptr, nullptr)); + REQUIRE(boost::filesystem::exists(gcode.path())); } TEST_CASE("Per-variant expansion gives migrating filaments one slot per variant", "[PrintConfig][H2C][Dynamic]") diff --git a/tests/libslic3r/test_triangle_selector.cpp b/tests/libslic3r/test_triangle_selector.cpp new file mode 100644 index 0000000000..fd2ab9efa8 --- /dev/null +++ b/tests/libslic3r/test_triangle_selector.cpp @@ -0,0 +1,125 @@ +#include <catch2/catch_all.hpp> + +#include "libslic3r/TriangleSelector.hpp" +#include "libslic3r/TriangleMesh.hpp" + +using namespace Slic3r; + +// A sphere gives well over ExtruderMax original facets, so every extruder state can be assigned +// to a facet of its own without any splitting getting in the way. +static TriangleMesh test_mesh() { return make_sphere(5., 2 * PI / 24); } + +// Read the nibble_idx-th 4-bit group of a serialized bitstream, least significant bit first. +static int nibble_at(const std::vector<bool> &bitstream, size_t nibble_idx) +{ + int n = 0; + for (size_t bit = 0; bit < 4; ++bit) + n |= int(bitstream[nibble_idx * 4 + bit]) << bit; + return n; +} + +TEST_CASE("Every extruder state survives a serialize/deserialize round trip", "[TriangleSelector]") +{ + const TriangleMesh mesh = test_mesh(); + const int max_state = int(EnforcerBlockerType::ExtruderMax); + REQUIRE(int(mesh.its.indices.size()) >= max_state); + + TriangleSelector selector(mesh); + for (int state = 1; state <= max_state; ++state) + selector.set_facet(state - 1, EnforcerBlockerType(state)); + + TriangleSelector restored(mesh); + restored.deserialize(selector.serialize()); + + for (int state = 1; state <= max_state; ++state) { + INFO("Extruder " << state); + REQUIRE(restored.has_facets(EnforcerBlockerType(state))); + REQUIRE(restored.num_facets(EnforcerBlockerType(state)) == 1); + } +} + +TEST_CASE("Serialized data reports the extruder states it uses", "[TriangleSelector]") +{ + const TriangleMesh mesh = test_mesh(); + TriangleSelector selector(mesh); + selector.set_facet(0, EnforcerBlockerType::Extruder16); + selector.set_facet(1, EnforcerBlockerType::Extruder32); + + const TriangleSelector::TriangleSplittingData data = selector.serialize(); + + REQUIRE(data.used_states.size() == size_t(EnforcerBlockerType::ExtruderMax) + 1); + REQUIRE(data.used_states[size_t(EnforcerBlockerType::Extruder16)]); + REQUIRE(data.used_states[size_t(EnforcerBlockerType::Extruder32)]); + REQUIRE_FALSE(data.used_states[size_t(EnforcerBlockerType::Extruder17)]); + + SECTION("used_states recomputed from the bitstream agrees") { + TriangleSelector::TriangleSplittingData recomputed = data; + recomputed.reset_used_states(); + recomputed.update_used_states(0); + REQUIRE(recomputed.used_states == data.used_states); + } + + SECTION("has_facets on the raw data agrees") { + REQUIRE(TriangleSelector::has_facets(data, EnforcerBlockerType::Extruder32)); + REQUIRE_FALSE(TriangleSelector::has_facets(data, EnforcerBlockerType::Extruder17)); + } +} + +// States 3..17 must keep the pre-existing encoding ("11" prefix plus one nibble of state-3) so +// projects written by older builds stay readable and newly written ones stay readable by them. +TEST_CASE("Extruder states up to 17 keep the single-nibble encoding", "[TriangleSelector]") +{ + const int state = GENERATE(3, 8, 16, 17); + + TriangleSelector selector(test_mesh()); + selector.set_facet(0, EnforcerBlockerType(state)); + const std::vector<bool> bitstream = selector.serialize().bitstream; + + INFO("Extruder " << state); + // Two nibbles: the "11"-prefixed leaf code, then the state itself. + REQUIRE(bitstream.size() == 8); + REQUIRE(nibble_at(bitstream, 0) == 0b1100); + REQUIRE(nibble_at(bitstream, 1) == state - 3); +} + +// States 18 and above set the state nibble to 0b1111 and carry (state-18) in one more nibble. +TEST_CASE("Extruder states above 17 are encoded in a second nibble", "[TriangleSelector]") +{ + const int state = GENERATE(18, 25, 32); + + TriangleSelector selector(test_mesh()); + selector.set_facet(0, EnforcerBlockerType(state)); + const std::vector<bool> bitstream = selector.serialize().bitstream; + + INFO("Extruder " << state); + REQUIRE(bitstream.size() == 12); + REQUIRE(nibble_at(bitstream, 0) == 0b1100); + REQUIRE(nibble_at(bitstream, 1) == 0b1111); + REQUIRE(nibble_at(bitstream, 2) == state - 18); +} + +// Model.cpp writes these hex strings into the 3MF for colored mesh imports; the selector must +// decode exactly the states CONST_FILAMENTS assigns to them. +TEST_CASE("Extruder states match the CONST_FILAMENTS hex encoding", "[TriangleSelector]") +{ + struct Case { const char *hex; int state; }; + const auto c = GENERATE(values<Case>({ + {"8", 2}, {"0C", 3}, {"DC", 16}, {"EC", 17}, {"0FC", 18}, {"EFC", 32}, + })); + + // get_triangle_as_string emits the nibbles most significant first, so read the hex backwards. + const std::string hex = c.hex; + std::vector<bool> bitstream; + for (auto it = hex.rbegin(); it != hex.rend(); ++it) { + const int nibble = *it >= 'A' ? (*it - 'A' + 10) : (*it - '0'); + for (int bit = 0; bit < 4; ++bit) + bitstream.push_back((nibble >> bit) & 1); + } + + TriangleSelector::TriangleSplittingData data; + data.triangles_to_split.emplace_back(0, 0); + data.bitstream = bitstream; + + INFO("Hex " << c.hex << " -> extruder " << c.state); + REQUIRE(TriangleSelector::has_facets(data, EnforcerBlockerType(c.state))); +} diff --git a/tests/libslic3r/test_vendor_cache.cpp b/tests/libslic3r/test_vendor_cache.cpp new file mode 100644 index 0000000000..85e100f4d4 --- /dev/null +++ b/tests/libslic3r/test_vendor_cache.cpp @@ -0,0 +1,1620 @@ +#include <catch2/catch_all.hpp> + +#include <boost/filesystem.hpp> +#include <boost/crc.hpp> +#include <cereal/archives/binary.hpp> +#include <cstring> +#include <fstream> +#include <functional> +#include <set> +#include <sstream> + +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/PresetCacheFormat.hpp" +#include "libslic3r/Preset.hpp" +#include "libslic3r/PrintConfig.hpp" +#include "libslic3r/Utils.hpp" + +using namespace Slic3r; +using Catch::Matchers::WithinAbs; +namespace fs = boost::filesystem; + +namespace { + +struct TempDir { + fs::path path; + TempDir() { + path = fs::temp_directory_path() / fs::unique_path("orca-cache-test-%%%%-%%%%"); + fs::create_directories(path); + } + ~TempDir() { boost::system::error_code ec; fs::remove_all(path, ec); } +}; + +std::string write_vendor_json(const fs::path& dir, const std::string& vendor_id, + const std::string& version = "1.0.0") +{ + const fs::path p = dir / (vendor_id + ".json"); + std::ofstream f(p.string()); + f << R"({"version":")" << version << R"(","name":")" << vendor_id << R"("})"; + return p.string(); +} + +// One vendor profile with a single process preset beside it, as an install or an +// update lays it down: <dir>/<vendor>.json plus <dir>/<vendor>/process/standard.json. +void write_vendor_tree(const fs::path& dir, const std::string& vendor, const std::string& version) +{ + fs::create_directories(dir / vendor / "process"); + std::ofstream((dir / (vendor + ".json")).string()) + << R"({"version":")" << version << R"(","name":")" << vendor + << R"(","process_list":[{"name":"0.20mm Standard @)" << vendor << R"(","sub_path":"process/standard.json"}]})"; + std::ofstream((dir / vendor / "process" / "standard.json").string()) + << R"({"type":"process","name":"0.20mm Standard @)" << vendor + << R"(","from":"system","instantiation":"true","layer_height":"0.2"})"; +} + +// A small but complete vendor: one machine model, one process, a non-instantiated +// base filament with an instantiated child inheriting it, a second standalone +// filament carrying explicit metadata, and one machine preset with a rename — so +// the equivalence test below sees every CachedPreset field populated. +void write_full_vendor_tree(const fs::path& dir, const std::string& vendor, const std::string& version) +{ + fs::create_directories(dir / vendor / "process"); + fs::create_directories(dir / vendor / "filament"); + fs::create_directories(dir / vendor / "machine"); + std::ofstream((dir / (vendor + ".json")).string()) + << R"({"version":")" << version << R"(","name":")" << vendor << R"(",)" + << R"("machine_model_list":[{"name":"Test Model","sub_path":"machine/model.json"}],)" + << R"("process_list":[{"name":"0.20mm Standard @)" << vendor << R"(","sub_path":"process/standard.json"}],)" + << R"("filament_list":[)" + << R"({"name":")" << vendor << R"( Base PLA","sub_path":"filament/base.json"},)" + << R"({"name":")" << vendor << R"( PLA @0.4","sub_path":"filament/pla.json"},)" + << R"({"name":")" << vendor << R"( Silk PLA @0.4","sub_path":"filament/silk.json"}],)" + << R"("machine_list":[{"name":")" << vendor << R"( 0.4 nozzle","sub_path":"machine/printer.json"}]})"; + std::ofstream((dir / vendor / "machine" / "model.json").string()) + << R"({"type":"machine_model","name":"Test Model","nozzle_diameter":"0.4"})"; + std::ofstream((dir / vendor / "process" / "standard.json").string()) + << R"({"type":"process","name":"0.20mm Standard @)" << vendor + << R"(","from":"system","instantiation":"true","layer_height":"0.2"})"; + std::ofstream((dir / vendor / "filament" / "base.json").string()) + << R"({"type":"filament","name":")" << vendor + << R"( Base PLA","from":"system","instantiation":"false","filament_id":"GFA_base","filament_cost":"42"})"; + std::ofstream((dir / vendor / "filament" / "pla.json").string()) + << R"({"type":"filament","name":")" << vendor + << R"( PLA @0.4","from":"system","instantiation":"true","filament_id":"GFA00","filament_cost":"20",)" + << R"("setting_id":"GFSA04","description":"Test PLA description"})"; + std::ofstream((dir / vendor / "filament" / "silk.json").string()) + << R"({"type":"filament","name":")" << vendor + << R"( Silk PLA @0.4","from":"system","instantiation":"true","inherits":")" << vendor << R"( Base PLA"})"; + std::ofstream((dir / vendor / "machine" / "printer.json").string()) + << R"({"type":"machine","name":")" << vendor + << R"( 0.4 nozzle","from":"system","instantiation":"true","printer_model":"Test Model","printer_variant":"0.4",)" + << R"("renamed_from":")" << vendor << R"( old 0.4 nozzle"})"; +} + +// The filament library: one non-instantiated base filament other vendors inherit +// from. `cost` lets a test bump the library and watch the change flow through. +void write_lib_tree(const fs::path& dir, const std::string& version, const std::string& cost) +{ + const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); + fs::create_directories(dir / lib / "filament"); + std::ofstream((dir / (lib + ".json")).string()) + << R"({"version":")" << version << R"(","name":")" << lib << R"(",)" + << R"("filament_list":[{"name":"Generic PLA","sub_path":"filament/generic_pla.json"}]})"; + std::ofstream((dir / lib / "filament" / "generic_pla.json").string()) + << R"({"type":"filament","name":"Generic PLA","from":"system","instantiation":"false",)" + << R"("filament_id":"GFL99","filament_cost":")" << cost << R"("})"; +} + +// A vendor whose one filament inherits the library's base and states nothing of +// its own — everything it shows comes from the library it is resolved against. +void write_vendor_with_lib_filament(const fs::path& dir, const std::string& vendor, const std::string& version) +{ + fs::create_directories(dir / vendor / "filament"); + std::ofstream((dir / (vendor + ".json")).string()) + << R"({"version":")" << version << R"(","name":")" << vendor << R"(",)" + << R"("filament_list":[{"name":")" << vendor << R"( PLA @0.4","sub_path":"filament/pla.json"}]})"; + std::ofstream((dir / vendor / "filament" / "pla.json").string()) + << R"({"type":"filament","name":")" << vendor + << R"( PLA @0.4","from":"system","instantiation":"true","inherits":"Generic PLA"})"; +} + +std::string write_versionless_vendor_json(const fs::path& dir, const std::string& vendor_id) +{ + const fs::path p = dir / (vendor_id + ".json"); + std::ofstream f(p.string()); + f << R"({"name":")" << vendor_id << R"("})"; + return p.string(); +} + +// Whole file as bytes, for the byte-identity comparisons below. +std::string slurp(const fs::path& p) +{ + std::string s; + load_string_file(p, s); + return s; +} + +// Flip one byte of the body. The default lands in the stamps at the front, which +// every reader checks; pass an offset past them to corrupt a file that still +// answers VendorCacheFile::peek_version but cannot survive its CRC. +void corrupt_blob_byte(const std::string& path, std::streamoff at = 30) +{ + std::fstream f(path, std::ios::in | std::ios::out | std::ios::binary); + f.seekp(at); + char b = 0; f.read(&b, 1); + f.seekp(at); + b ^= 0xFF; + f.write(&b, 1); +} + +// Overwrite `n` bytes at `payload_off` into the cache's payload (which starts at +// file offset 20, behind the header) and recompute the header CRC, so the file +// stays authentic and only the deserializer can object to its contents. +void patch_payload_bytes(const std::string& path, size_t payload_off, const void* bytes, size_t n) +{ + constexpr size_t header_size = 20; // magic(4) + version(4) + data_size(8) + crc32(4) + std::ifstream in(path, std::ios::binary); + std::vector<char> data(std::istreambuf_iterator<char>(in), {}); + in.close(); + REQUIRE(data.size() >= header_size + payload_off + n); + std::memcpy(&data[header_size + payload_off], bytes, n); + boost::crc_32_type crc; + crc.process_bytes(&data[header_size], data.size() - header_size); + const uint32_t new_crc = crc.checksum(); + std::memcpy(&data[16], &new_crc, 4); + std::ofstream out(path, std::ios::binary | std::ios::trunc); + out.write(data.data(), static_cast<std::streamsize>(data.size())); +} + +// Patch cache_version (the payload's first word) so the file passes the CRC +// check but fails the cache_version check in VendorCacheFile::load. +void patch_cache_version(const std::string& path, uint32_t wrong_version) +{ + patch_payload_bytes(path, 0, &wrong_version, sizeof(wrong_version)); +} + +// Truncates the cache's PAYLOAD (everything after the 20-byte header) by +// `truncate_by` bytes and recomputes data_size/crc32 in the header, exactly +// as the cache writer computes them, so the framing's size and CRC checks +// still pass but cereal runs out of bytes partway through deserializing the +// body — exercising VendorCacheFile::load's catch block instead of its early +// (pre-body) rejection paths. +void truncate_payload_and_fix_header(const std::string& path, size_t truncate_by) +{ + constexpr size_t header_size = 20; // magic(4) + version(4) + data_size(8) + crc32(4) + std::ifstream in(path, std::ios::binary); + std::vector<char> data(std::istreambuf_iterator<char>(in), {}); + in.close(); + REQUIRE(data.size() > header_size + truncate_by); + const size_t new_payload_size = data.size() - header_size - truncate_by; + const uint64_t data_size_field = static_cast<uint64_t>(new_payload_size); + boost::crc_32_type crc; + crc.process_bytes(&data[header_size], new_payload_size); + const uint32_t crc_field = crc.checksum(); + std::memcpy(&data[8], &data_size_field, sizeof(data_size_field)); // data_size offset + std::memcpy(&data[16], &crc_field, sizeof(crc_field)); // crc32 offset + std::ofstream out(path, std::ios::binary | std::ios::trunc); + out.write(data.data(), static_cast<std::streamsize>(header_size + new_payload_size)); +} + +// One vendor as a cache's VendorMap. It carries one printer model ("Test Model", +// variant "0.4") so machine entries can pass install's model/variant validation. +VendorMap one_vendor(const std::string& vendor_id, const std::string& name = "", + Semver ver = Semver(1, 0, 0)) +{ + VendorMap vendors; + VendorProfile vp(vendor_id); + vp.name = name.empty() ? vendor_id + " Corp" : name; + vp.config_version = ver; + VendorProfile::PrinterModel model; + model.id = "Test Model"; + model.variants.emplace_back(VendorProfile::PrinterVariant("0.4")); + vp.models.push_back(model); + vendors.emplace(vendor_id, vp); + return vendors; +} + +// Source-form entries as parse_subfile would emit them. The alias is derived by +// install from the '@' in the name, exactly as it is for the JSON parse. +CachedPreset filament_entry(const std::string& name, const std::string& filament_id = "GFA00", + const std::string& inherits = "") +{ + CachedPreset e; + e.name = name; + e.sub_path = "filament/" + name + ".json"; + e.instantiation = "true"; + e.filament_id = filament_id; + e.inherits = inherits; + return e; +} + +CachedPreset printer_entry(const std::string& name) +{ + CachedPreset e; + e.name = name; + e.sub_path = "machine/" + name + ".json"; + e.instantiation = "true"; + e.config_src.set_key_value("printer_model", new ConfigOptionString("Test Model")); + e.config_src.set_key_value("printer_variant", new ConfigOptionString("0.4")); + return e; +} + +static bool save_one_vendor(const std::string& path, const VendorMap& vendors, + const std::string& vendor, const std::string& vendor_version, + const std::vector<CachedPreset>& filament_entries = {}, + const std::vector<CachedPreset>& machine_entries = {}, + const std::vector<CachedPreset>& process_entries = {}) +{ + VendorCacheData data; + data.vendors = vendors; + data.process_entries = process_entries; + data.filament_entries = filament_entries; + data.machine_entries = machine_entries; + return VendorCacheFile::save(path, vendor, vendor_version, data); +} + +// resources_dir()/data_dir() are process-wide, so restore them however the test +// leaves — including through a failed REQUIRE — to stay green under --order rand. +struct ScopedDirs { + std::string prev_data{data_dir()}, prev_rsrc{resources_dir()}; + ScopedDirs(const fs::path& data, const fs::path& rsrc) + { + set_data_dir(data.string()); + set_resources_dir(rsrc.string()); + } + ~ScopedDirs() { set_data_dir(prev_data); set_resources_dir(prev_rsrc); } +}; + +// A data dir and a resources dir, both pointed at by the process-wide accessors, +// with the two directories a vendor is installed into and shipped from already +// created. What every install- and load-order test needs before it starts. +struct InstallDirs { + TempDir data, rsrc; + fs::path system = data.path / PRESET_SYSTEM_DIR; + fs::path profiles = rsrc.path / "profiles"; + ScopedDirs scoped { data.path, rsrc.path }; + + InstallDirs() + { + fs::create_directories(system); + fs::create_directories(profiles); + } +}; + +// Helper: filter a collection by vendor_id. +std::vector<const Preset*> presets_for(const PresetCollection& coll, const std::string& vendor_id) +{ + std::vector<const Preset*> out; + for (const Preset& p : coll()) + if (p.is_system && p.vendor && p.vendor->id == vendor_id) + out.push_back(&p); + return out; +} + +} // namespace + +namespace Slic3r { +inline bool operator==(const VendorProfile::PrinterVariant& a, const VendorProfile::PrinterVariant& b) { return a.name == b.name; } +inline bool operator==(const VendorProfile::PrinterModel& a, const VendorProfile::PrinterModel& b) +{ + return a.id == b.id && a.name == b.name && a.model_id == b.model_id && a.technology == b.technology + && a.family == b.family && a.variants == b.variants && a.default_materials == b.default_materials + && a.not_support_bed_types == b.not_support_bed_types && a.bed_model == b.bed_model + && a.bed_texture == b.bed_texture && a.image_bed_type == b.image_bed_type + && a.bottom_texture_end_name == b.bottom_texture_end_name + && a.use_double_extruder_default_texture == b.use_double_extruder_default_texture + && a.bottom_texture_rect == b.bottom_texture_rect + && a.bottom_texture_rect_longer == b.bottom_texture_rect_longer + && a.middle_texture_rect == b.middle_texture_rect && a.hotend_model == b.hotend_model; +} +} // namespace Slic3r + +static bool vendor_deep_equal(const VendorProfile& a, const VendorProfile& b) +{ + return a.name == b.name && a.id == b.id && a.config_version == b.config_version + && a.config_update_url == b.config_update_url && a.changelog_url == b.changelog_url + && a.models == b.models && a.default_filaments == b.default_filaments + && a.default_sla_materials == b.default_sla_materials; +} + +static bool preset_deep_equal(const Preset& a, const Preset& b) +{ + return a.type == b.type && a.is_default == b.is_default && a.is_external == b.is_external + && a.is_system == b.is_system && a.is_visible == b.is_visible && a.is_dirty == b.is_dirty + && a.is_compatible == b.is_compatible && a.is_project_embedded == b.is_project_embedded + && a.name == b.name && a.file == b.file && a.loaded == b.loaded + && a.config.equals(b.config) + && a.alias == b.alias && a.renamed_from == b.renamed_from + && a.m_excluded_from == b.m_excluded_from && a.m_from_orca_filament_lib == b.m_from_orca_filament_lib + && a.bundle_id == b.bundle_id && a.version == b.version && a.ini_str == b.ini_str + && a.setting_id == b.setting_id && a.filament_id == b.filament_id && a.user_id == b.user_id + && a.base_id == b.base_id && a.sync_info == b.sync_info && a.description == b.description + && a.updated_time == b.updated_time && a.key_values == b.key_values; +} + +TEST_CASE("a saved cache loads back with names, aliases and filament ids intact", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path cache = tmp.path / "vendor.opc"; + + REQUIRE(save_one_vendor(cache.string(), one_vendor(vid), vid, "1.0.0", + {filament_entry(vid + " PLA @0.4", "GFL_acme_pla")}, + {printer_entry(vid + " Printer 0.4")})); + + PresetBundle out; + REQUIRE(out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); + REQUIRE(out.vendors.count(vid) == 1); + + auto fi = presets_for(out.filaments, vid); + auto pr = presets_for(out.printers, vid); + REQUIRE(fi.size() == 1); + CHECK(fi[0]->name == vid + " PLA @0.4"); + CHECK(fi[0]->alias == "Acme PLA"); + CHECK(fi[0]->filament_id == "GFL_acme_pla"); + REQUIRE(pr.size() == 1); + CHECK(pr[0]->name == vid + " Printer 0.4"); +} + +TEST_CASE("loading a missing cache file returns false", "[VendorCache]") +{ + TempDir tmp; + PresetBundle out; + REQUIRE(!out.load_vendor_cache((tmp.path / "nonexistent.opc").string(), "Acme", Semver("1.0.0"))); +} + +TEST_CASE("a cache with a corrupted byte is rejected by the CRC check", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path cache = tmp.path / "vendor.opc"; + + REQUIRE(save_one_vendor(cache.string(), one_vendor(vid), vid, "1.0.0", + {filament_entry(vid + " PLA")})); + corrupt_blob_byte(cache.string()); + + PresetBundle out; + REQUIRE(!out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); +} + +TEST_CASE("two vendors produce two independent cache files", "[VendorCache]") +{ + TempDir tmp; + const fs::path cacheA = tmp.path / "vendorA.opc"; + const fs::path cacheB = tmp.path / "vendorB.opc"; + + REQUIRE(save_one_vendor(cacheA.string(), one_vendor("VendorA"), "VendorA", "1.0.0", + {filament_entry("VendorA PLA")})); + REQUIRE(save_one_vendor(cacheB.string(), one_vendor("VendorB"), "VendorB", "1.0.0", + {filament_entry("VendorB PLA")})); + + // Corrupt only vendor B's file; vendor A's must be unaffected. + corrupt_blob_byte(cacheB.string()); + + PresetBundle outA; + REQUIRE(outA.load_vendor_cache(cacheA.string(), "VendorA", Semver("1.0.0"))); + REQUIRE(outA.vendors.count("VendorA") == 1); + REQUIRE(presets_for(outA.filaments, "VendorA").size() == 1); + + PresetBundle outB; + REQUIRE(!outB.load_vendor_cache(cacheB.string(), "VendorB", Semver("1.0.0"))); + REQUIRE(outB.vendors.empty()); +} + +TEST_CASE("vendor profile fields survive a cache round-trip", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path cache = tmp.path / "vendor.opc"; + + VendorMap vendors; + VendorProfile vp(vid); + vp.name = "Acme Corporation"; + vp.config_version = Semver(2, 5, 1); + VendorProfile::PrinterModel model; + model.id = "AcmePro"; + model.name = "Acme Pro"; + VendorProfile::PrinterVariant v0_4; v0_4.name = "0.4"; + model.variants.push_back(v0_4); + vp.models.push_back(model); + vendors.emplace(vid, vp); + REQUIRE(save_one_vendor(cache.string(), vendors, vid, "2.5.1")); + + PresetBundle out; + REQUIRE(out.load_vendor_cache(cache.string(), vid, Semver("2.5.1"))); + REQUIRE(out.vendors.count(vid) == 1); + const VendorProfile& gvp = out.vendors.at(vid); + REQUIRE(vendor_deep_equal(gvp, vendors.at(vid))); + // Spot-check the fields the old test asserted directly, so a + // vendor_deep_equal regression still points at what actually broke. + CHECK(gvp.id == vid); + CHECK(gvp.name == "Acme Corporation"); + REQUIRE(gvp.models.size() == 1); + CHECK(gvp.models[0].id == "AcmePro"); + CHECK(gvp.models[0].name == "Acme Pro"); + REQUIRE(gvp.models[0].variants.size() == 1); + CHECK(gvp.models[0].variants[0].name == "0.4"); +} + +TEST_CASE("config option values survive a cache round-trip", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path cache = tmp.path / "vendor.opc"; + + auto entry = filament_entry(vid + " PETG @0.4"); + entry.config_src.set_key_value("filament_type", new ConfigOptionStrings({"PETG"})); + REQUIRE(save_one_vendor(cache.string(), one_vendor(vid), vid, "1.0.0", {entry})); + + PresetBundle out; + REQUIRE(out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); + + auto fi = presets_for(out.filaments, vid); + REQUIRE(fi.size() == 1); + const auto* ft = fi[0]->config.option<ConfigOptionStrings>("filament_type"); + REQUIRE(ft != nullptr); + REQUIRE(ft->values.size() >= 1); + CHECK(ft->values[0] == "PETG"); +} + +TEST_CASE("multiple presets in one collection all round-trip", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path cache = tmp.path / "vendor.opc"; + + const std::vector<std::string> fi_names = {vid + " PLA", vid + " PETG", vid + " ABS"}; + const std::vector<std::string> pr_names = {vid + " Printer 0.4", vid + " Printer 0.6"}; + std::vector<CachedPreset> filament_entries, machine_entries; + for (const auto& n : fi_names) filament_entries.push_back(filament_entry(n)); + for (const auto& n : pr_names) machine_entries.push_back(printer_entry(n)); + + REQUIRE(save_one_vendor(cache.string(), one_vendor(vid), vid, "1.0.0", + filament_entries, machine_entries)); + + PresetBundle out; + REQUIRE(out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); + + auto fi = presets_for(out.filaments, vid); + auto pr = presets_for(out.printers, vid); + REQUIRE(fi.size() == 3); + REQUIRE(pr.size() == 2); + + std::set<std::string> fi_got, pr_got; + for (const auto* p : fi) fi_got.insert(p->name); + for (const auto* p : pr) pr_got.insert(p->name); + for (const auto& n : fi_names) CHECK(fi_got.count(n) == 1); + for (const auto& n : pr_names) CHECK(pr_got.count(n) == 1); +} + +TEST_CASE("a truncated cache file is rejected", "[VendorCache]") +{ + TempDir tmp; + const fs::path cache = tmp.path / "truncated.opc"; + { + std::ofstream f(cache.string(), std::ios::binary); + const char data[] = {0x4F, 0x52, 0x43}; + f.write(data, sizeof(data)); + } + PresetBundle out; + REQUIRE(!out.load_vendor_cache(cache.string(), "Acme", Semver("1.0.0"))); +} + +TEST_CASE("a cache with the wrong magic number is rejected", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path cache = tmp.path / "vendor.opc"; + + REQUIRE(save_one_vendor(cache.string(), one_vendor(vid), vid, "1.0.0", + {filament_entry(vid + " PLA")})); + + { + std::fstream f(cache.string(), std::ios::in | std::ios::out | std::ios::binary); + const uint32_t bad = 0xDEADBEEFu; + f.write(reinterpret_cast<const char*>(&bad), sizeof(bad)); + } + + PresetBundle out; + REQUIRE(!out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); +} + +TEST_CASE("a vendor with no presets saves and loads cleanly", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path cache = tmp.path / "vendor.opc"; + + REQUIRE(save_one_vendor(cache.string(), one_vendor(vid, "Acme Corporation"), vid, "1.0.0")); + + PresetBundle out; + REQUIRE(out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); + REQUIRE(out.vendors.count(vid) == 1); + CHECK(out.vendors.at(vid).id == vid); + CHECK(out.vendors.at(vid).name == "Acme Corporation"); + CHECK(presets_for(out.filaments, vid).empty()); + CHECK(presets_for(out.printers, vid).empty()); + CHECK(presets_for(out.prints, vid).empty()); +} + +TEST_CASE("a cache-loaded vendor is indistinguishable from a JSON-loaded one", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + write_full_vendor_tree(user, "Acme", "1.0.0"); + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + + PresetBundle from_json; + from_json.set_generate_vendor_caches(true); + from_json.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + REQUIRE(fs::exists(user / "Acme.opc")); + + // Take the preset JSONs away: were the cache rejected, the load below would + // have nothing to parse — so its success proves the cache answered. + fs::remove_all(user / "Acme"); + PresetBundle from_cache; + from_cache.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + + // Both paths run the same install code over the same entries, so everything + // observable must come out identical — the vendor profile and every preset, + // field by field. + REQUIRE(from_cache.vendors.count("Acme") == 1); + REQUIRE(vendor_deep_equal(from_cache.vendors.at("Acme"), from_json.vendors.at("Acme"))); + const std::pair<const PresetCollection*, const PresetCollection*> colls[] = { + {&from_json.prints, &from_cache.prints}, + {&from_json.filaments, &from_cache.filaments}, + {&from_json.printers, &from_cache.printers}, + }; + for (const auto& [jc, cc] : colls) { + auto a = presets_for(*jc, "Acme"); + auto b = presets_for(*cc, "Acme"); + REQUIRE(a.size() == b.size()); + REQUIRE(!a.empty()); + for (size_t i = 0; i < a.size(); ++i) { + CHECK(a[i]->name == b[i]->name); + CHECK(preset_deep_equal(*a[i], *b[i])); + } + } + + // Pin the explicit metadata against symmetric loss: dropping a field from + // visit_entry (PresetCacheFormat.cpp) keeps the two bundles equal to each + // other, but not to the fixture. + const Preset* pla = from_cache.filaments.find_preset("Acme PLA @0.4", false); + REQUIRE(pla != nullptr); + CHECK(pla->setting_id == "GFSA04"); + CHECK(pla->description == "Test PLA description"); + const Preset* silk = from_cache.filaments.find_preset("Acme Silk PLA @0.4", false); + REQUIRE(silk != nullptr); + CHECK(silk->filament_id == "GFA_base"); // inherited from the non-instantiated base + const auto* cost = silk->config.option<ConfigOptionFloats>("filament_cost"); + REQUIRE(cost != nullptr); + CHECK_THAT(cost->values.front(), WithinAbs(42., 1e-9)); + const Preset* pr = from_cache.printers.find_preset("Acme 0.4 nozzle", false); + REQUIRE(pr != nullptr); + CHECK(pr->renamed_from == std::vector<std::string>{"Acme old 0.4 nozzle"}); +} + +TEST_CASE("a cache-served vendor reports the errors its parse counted", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + // One process preset without the required "instantiation" key — a parse-phase + // error the load survives, so it must reach the cache's parse_errors stamp. + fs::create_directories(user / "Acme" / "process"); + std::ofstream((user / "Acme.json").string()) + << R"({"version":"1.0.0","name":"Acme","process_list":[{"name":"0.20mm Standard @Acme","sub_path":"process/standard.json"}]})"; + std::ofstream((user / "Acme" / "process" / "standard.json").string()) + << R"({"type":"process","name":"0.20mm Standard @Acme","from":"system","layer_height":"0.2"})"; + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + + PresetBundle from_json; + from_json.set_generate_vendor_caches(true); + from_json.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + REQUIRE(fs::exists(user / "Acme.opc")); + CHECK(from_json.error_count() > 0); + + fs::remove_all(user / "Acme"); + PresetBundle from_cache; + from_cache.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + CHECK(from_cache.error_count() == from_json.error_count()); + CHECK(presets_for(from_cache.prints, "Acme").size() == 1); +} + +TEST_CASE("a non-instantiated base in a regular vendor's cache resolves its children and stays out of the library maps", "[VendorCache]") +{ + TempDir tmp; + const fs::path cache = tmp.path / "vendor.opc"; + + // Entry order is the resolution order: the base must install (into the local + // config maps) before the child that inherits it. + auto base = filament_entry("Acme Base PLA", "GFA_base"); + base.instantiation = "false"; + base.config_src.set_key_value("filament_cost", new ConfigOptionFloats({42.})); + auto child = filament_entry("Acme Silk PLA @0.4", "", "Acme Base PLA"); + REQUIRE(save_one_vendor(cache.string(), one_vendor("Acme"), "Acme", "1.0.0", {base, child})); + + PresetBundle out; + REQUIRE(out.load_vendor_cache(cache.string(), "Acme", Semver("1.0.0"))); + auto fi = presets_for(out.filaments, "Acme"); + REQUIRE(fi.size() == 1); // the base never becomes a preset + CHECK(fi[0]->name == "Acme Silk PLA @0.4"); + CHECK(fi[0]->filament_id == "GFA_base"); + const auto* cost = fi[0]->config.option<ConfigOptionFloats>("filament_cost"); + REQUIRE(cost != nullptr); + CHECK_THAT(cost->values.front(), WithinAbs(42., 1e-9)); + // Only the filament library's bases persist as the cross-vendor inheritance + // maps; a regular vendor's stay local to its own load. + CHECK(out.m_config_maps.empty()); + CHECK(out.m_filament_id_maps.empty()); +} + +TEST_CASE("a cache with the wrong cache version is rejected", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path cache = tmp.path / "vendor.opc"; + + REQUIRE(save_one_vendor(cache.string(), one_vendor(vid), vid, "1.0.0", + {filament_entry(vid + " PLA")})); + patch_cache_version(cache.string(), 0xFFFFFFFFu); + + PresetBundle out; + REQUIRE(!out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); +} + +TEST_CASE("a cache truncated mid-blob is rejected", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path cache = tmp.path / "vendor.opc"; + + REQUIRE(save_one_vendor(cache.string(), one_vendor(vid), vid, "1.0.0", + {filament_entry(vid + " PLA")})); + + { + std::ifstream in(cache.string(), std::ios::binary); + std::vector<char> buf(30); // 20-byte header + 10 bytes of blob + in.read(buf.data(), 30); + in.close(); + std::ofstream out(cache.string(), std::ios::binary | std::ios::trunc); + out.write(buf.data(), 30); + } + + PresetBundle out; + REQUIRE(!out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); +} + +TEST_CASE("printer model bed texture fields survive a cache round-trip", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path cache = tmp.path / "vendor.opc"; + + VendorMap vendors = one_vendor(vid); + VendorProfile::PrinterModel model; + model.id = "N1"; + model.name = "Neat One"; + model.bottom_texture_rect_longer = "5,5,50,10"; + vendors.at(vid).models.push_back(model); + REQUIRE(save_one_vendor(cache.string(), vendors, vid, "1.0.0")); + + PresetBundle out; + REQUIRE(out.load_vendor_cache(cache.string(), vid, Semver("1.0.0"))); + REQUIRE(out.vendors.at(vid).models.size() == 2); + REQUIRE(vendor_deep_equal(out.vendors.at(vid), vendors.at(vid))); + CHECK(out.vendors.at(vid).models[1].bottom_texture_rect_longer == "5,5,50,10"); +} + +TEST_CASE("a cache older than the vendor profile on disk is rejected", "[VendorCache]") +{ + TempDir tmp; + const fs::path cache = tmp.path / "vendor.opc"; + REQUIRE(save_one_vendor(cache.string(), one_vendor("Acme"), "Acme", "1.0.0")); + + PresetBundle out; + REQUIRE(!out.load_vendor_cache(cache.string(), "Acme", Semver("1.0.1"))); +} + +TEST_CASE("a cache newer than the vendor profile on disk is used", "[VendorCache]") +{ + TempDir tmp; + const fs::path cache = tmp.path / "vendor.opc"; + REQUIRE(save_one_vendor(cache.string(), one_vendor("Acme"), "Acme", "1.2.0", + {filament_entry("Acme PLA")})); + + PresetBundle out; + REQUIRE(out.load_vendor_cache(cache.string(), "Acme", Semver("1.0.0"))); + CHECK(presets_for(out.filaments, "Acme").size() == 1); +} + +TEST_CASE("a vendor cache outlives a filament library update and resolves against the new library", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); + write_lib_tree(user, "1.0.0", "20"); + write_vendor_with_lib_filament(user, "Acme", "1.0.0"); + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + + // First launch: the library parses first, then the vendor against it, and + // both caches are written. + PresetBundle base1; + base1.set_generate_vendor_caches(true); + base1.load_vendor_configs_from_json(user.string(), lib, PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + PresetBundle acme1; + acme1.set_generate_vendor_caches(true); + acme1.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent, &base1); + REQUIRE(fs::exists(user / "Acme.opc")); + { + auto fi = presets_for(acme1.filaments, "Acme"); + REQUIRE(fi.size() == 1); + const auto* cost = fi[0]->config.option<ConfigOptionFloats>("filament_cost"); + REQUIRE(cost != nullptr); + CHECK_THAT(cost->values.front(), WithinAbs(20., 1e-9)); + } + + // An update delivers a new library only; the vendor stays as it was. + write_lib_tree(user, "2.0.0", "30"); + PresetBundle base2; + base2.load_vendor_configs_from_json(user.string(), lib, PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + + // Take the vendor's preset JSONs away: were its cache rejected, the load + // below would have nothing to parse — so its success proves the cache + // survived the library bump. + fs::remove_all(user / "Acme"); + PresetBundle acme2; + auto [substitutions, presets_loaded] = acme2.load_vendor_configs_from_json( + user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent, &base2); + CHECK(presets_loaded == 1); + auto fi = presets_for(acme2.filaments, "Acme"); + REQUIRE(fi.size() == 1); + // The cache holds only the vendor's own diff; the library values come from + // the library loaded now, not the one in effect when the cache was written. + const auto* cost = fi[0]->config.option<ConfigOptionFloats>("filament_cost"); + REQUIRE(cost != nullptr); + CHECK_THAT(cost->values.front(), WithinAbs(30., 1e-9)); + CHECK(fi[0]->filament_id == "GFL99"); +} + +TEST_CASE("a vendor installed as its cache alone still loads after a library update", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); + write_lib_tree(user, "1.0.0", "20"); + write_vendor_with_lib_filament(user, "Acme", "1.0.0"); + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + + // Generate the vendor's cache, then strip the vendor to the cache alone — + // the shape of a packaged install, which ships each vendor as its .opc and + // nothing else. + PresetBundle base1; + base1.set_generate_vendor_caches(true); + base1.load_vendor_configs_from_json(user.string(), lib, PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + PresetBundle acme1; + acme1.set_generate_vendor_caches(true); + acme1.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent, &base1); + fs::remove(user / "Acme.json"); + fs::remove_all(user / "Acme"); + + // An OTA update then delivers a new library only. With no JSONs anywhere to + // fall back on, the vendor must keep loading from its cache. + write_lib_tree(user, "2.0.0", "30"); + PresetBundle base2; + base2.load_vendor_configs_from_json(user.string(), lib, PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + PresetBundle acme2; + auto [substitutions, presets_loaded] = acme2.load_vendor_configs_from_json( + user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent, &base2); + CHECK(presets_loaded == 1); + REQUIRE(acme2.vendors.count("Acme") == 1); + auto fi = presets_for(acme2.filaments, "Acme"); + REQUIRE(fi.size() == 1); + const auto* cost = fi[0]->config.option<ConfigOptionFloats>("filament_cost"); + REQUIRE(cost != nullptr); + CHECK_THAT(cost->values.front(), WithinAbs(30., 1e-9)); +} + +TEST_CASE("a cache entry whose parent is missing falls back to the vendor's JSONs", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + write_vendor_tree(user, "Acme", "1.0.0"); + + // A cache claiming the installed version, but whose entry inherits a preset + // no loaded library provides. + REQUIRE(save_one_vendor((user / "Acme.opc").string(), one_vendor("Acme", "Cached Acme"), "Acme", "1.0.0", + {filament_entry("Acme PLA @0.4", "GFA00", "No Such Base")})); + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + + // Directly: the load fails and leaves the bundle clean. + PresetBundle direct; + REQUIRE(!direct.load_vendor_cache((user / "Acme.opc").string(), "Acme", Semver("1.0.0"))); + CHECK(direct.vendors.empty()); + + // Through the vendor load: the JSONs answer instead, as if no cache existed. + PresetBundle out; + auto [substitutions, presets_loaded] = out.load_vendor_configs_from_json( + user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); + CHECK(presets_loaded == 1); + CHECK(out.vendors.at("Acme").name == "Acme"); // the profile's name, not the cache's +} + +TEST_CASE("a profile with no usable version is never served from cache", "[VendorCache]") +{ + TempDir tmp; + const fs::path cache = tmp.path / "vendor.opc"; + REQUIRE(save_one_vendor(cache.string(), one_vendor("Acme"), "Acme", "1.0.0")); + + PresetBundle out; + // An unversioned vendor profile has no version to compare against. + REQUIRE(!out.load_vendor_cache(cache.string(), "Acme", Semver::invalid())); + // And a cache carrying no version of its own cannot cover a profile that has one. + REQUIRE(save_one_vendor(cache.string(), one_vendor("Acme"), "Acme", "")); + REQUIRE(!out.load_vendor_cache(cache.string(), "Acme", Semver("1.0.0"))); + REQUIRE(out.vendors.empty()); +} + +TEST_CASE("a versionless profile beside a cache keeps the cache from being served", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + + REQUIRE(save_one_vendor((user / "Acme.opc").string(), one_vendor("Acme", "Cached Acme"), "Acme", "1.0.0", + {filament_entry("Acme PLA @0.4")})); + // The profile beside the cache parses to no usable version, which can no + // more judge the cache's staleness than it could be cached itself. + write_versionless_vendor_json(user, "Acme"); + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + + PresetBundle out; + auto [substitutions, presets_loaded] = out.load_vendor_configs_from_json( + user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); + // Nothing came from the cache: the versionless profile was parsed instead, + // and it carries no presets. + CHECK(presets_loaded == 0); +} + +TEST_CASE("a vendor's cache is its whole installation", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources"; + const fs::path data = tmp.path / "data"; + fs::create_directories(rsrc / "profiles" / "Acme" / "machine"); + write_vendor_json(rsrc / "profiles", "Acme"); + std::ofstream((rsrc / "profiles" / "Acme" / "machine" / "printer.json").string()) << "{}"; + + REQUIRE(save_one_vendor((rsrc / "profiles" / "Acme.opc").string(), one_vendor("Acme"), "Acme", "1.0.0")); + + ScopedDirs dirs(data, rsrc); + REQUIRE(install_vendor_bundles_from_resources({"Acme"})); + // The cache carries the presets, the vendor profile and the version they were + // built at, so it is installed on its own. + CHECK(fs::exists(data / "system" / "Acme.opc")); + CHECK(!fs::exists(data / "system" / "Acme.json")); + CHECK(!fs::exists(data / "system" / "Acme")); + CHECK(is_vendor_installed("Acme")); + CHECK(installed_vendor_version("Acme") == Semver(1, 0, 0)); + + // A vendor with no cache is installed as its profile and preset JSONs instead, + // parsing them being the only way left to load it — and the cache the previous + // install left behind has to go, or it would shadow the profile just installed. + fs::remove(rsrc / "profiles" / "Acme.opc"); + REQUIRE(install_vendor_bundles_from_resources({"Acme"})); + CHECK(!fs::exists(data / "system" / "Acme.opc")); + CHECK(fs::exists(data / "system" / "Acme" / "machine" / "printer.json")); + CHECK(installed_vendor_version("Acme") == Semver(1, 0, 0)); + + // Installing the cache again takes the profile and its preset JSONs back out. + REQUIRE(save_one_vendor((rsrc / "profiles" / "Acme.opc").string(), one_vendor("Acme"), "Acme", "1.0.0")); + REQUIRE(install_vendor_bundles_from_resources({"Acme"})); + CHECK(fs::exists(data / "system" / "Acme.opc")); + CHECK(!fs::exists(data / "system" / "Acme.json")); + CHECK(!fs::exists(data / "system" / "Acme")); +} + +TEST_CASE("a vendor shipped as a cache alone is installed and loaded from it", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + + // A packaged build: every vendor is its cache, with no profile of any kind + // beside it — not even the filament library's. + const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); + REQUIRE(save_one_vendor((rsrc / (lib + ".opc")).string(), one_vendor(lib, "Shipped Library"), lib, "1.0.0")); + REQUIRE(save_one_vendor((rsrc / "Acme.opc").string(), one_vendor("Acme", "Shipped Acme"), "Acme", "1.0.0")); + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + // The version the build ships the vendor at comes from the cache, there being + // no profile to read it from. + CHECK(resource_vendor_version("Acme") == Semver(1, 0, 0)); + + // Resources reaches the app by being installed, never by being loaded from. + REQUIRE(install_vendor_bundles_from_resources({lib, "Acme"})); + CHECK(fs::exists(user / "Acme.opc")); + CHECK(!fs::exists(user / "Acme.json")); + CHECK(installed_vendor_version("Acme") == Semver(1, 0, 0)); + + PresetBundle after; + after.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + CHECK(after.vendors.at("Acme").name == "Shipped Acme"); +} + +TEST_CASE("a vendor with a profile in the data dir is parsed there and cached there, whatever resources ships", "[VendorCache]") +{ + // The reported regression: a valid resources/profiles/<V>.opc answered + // first, so the JSON in system/ was never parsed and system/<V>.opc was + // never written. Main reads system/ and nothing else. + InstallDirs dirs; + + write_vendor_tree(dirs.system, "Shadow", "1.0.0"); + // A cache in resources at the very same version — under the old two-tier + // lookup this was accepted and the parse skipped. + REQUIRE(save_one_vendor((dirs.profiles / "Shadow.opc").string(), one_vendor("Shadow"), "Shadow", "1.0.0")); + + PresetBundle bundle; + bundle.set_generate_vendor_caches(true); + REQUIRE(bundle.load_vendor_configs_from_json(dirs.system.string(), "Shadow", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent).second > 0); + + // Parsed from system/, and its cache written back beside the profile. + CHECK(fs::exists(dirs.system / "Shadow.opc")); + CHECK(presets_for(bundle.prints, "Shadow").size() == 1); +} + +TEST_CASE("a vendor with nothing installed is not loaded from resources", "[VendorCache]") +{ + // Resources reaches the app by being installed into system/ first. A vendor + // that is not installed is not loaded, however completely resources ships it. + InstallDirs dirs; + + write_vendor_tree(dirs.profiles, "Absent", "1.0.0"); + REQUIRE(save_one_vendor((dirs.profiles / "Absent.opc").string(), one_vendor("Absent"), "Absent", "1.0.0")); + + PresetBundle bundle; + REQUIRE_THROWS(bundle.load_vendor_configs_from_json(dirs.system.string(), "Absent", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent)); + CHECK(presets_for(bundle.prints, "Absent").empty()); +} + +TEST_CASE("a cache installed with no profile beside it is used whatever its version", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + write_vendor_json(rsrc, "Acme"); + + // Installed at an older version than the one now shipped in resources. Nothing + // sits beside it claiming to be newer, so the cache is what the vendor is. + REQUIRE(save_one_vendor((user / "Acme.opc").string(), one_vendor("Acme", "Installed Acme"), "Acme", "0.9.0")); + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + CHECK(VendorCacheFile::peek_version((user / "Acme.opc").string(), "Acme") == "0.9.0"); + CHECK(VendorCacheFile::peek_version((user / "Acme.opc").string(), "Other").empty()); + CHECK(installed_vendor_version("Acme") == Semver(0, 9, 0)); + + // Loading the vendor takes the installed cache, not the newer shipped profile. + PresetBundle out; + out.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + CHECK(out.vendors.at("Acme").name == "Installed Acme"); +} + +TEST_CASE("a vendor whose cache covers it is loaded without parsing any JSON", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + + REQUIRE(save_one_vendor((user / "Acme.opc").string(), one_vendor("Acme", "Cached Acme"), "Acme", "1.0.0", + {filament_entry("Acme PLA @0.4")}, + {printer_entry("Acme Printer 0.4")})); + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + + // The cache is the whole installation — no profile, no preset JSONs — and the + // caller asks for the vendor exactly as it would for a JSON install. + PresetBundle out; + auto [substitutions, presets_loaded] = out.load_vendor_configs_from_json( + user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable); + CHECK(substitutions.empty()); + CHECK(presets_loaded == 2); + CHECK(out.vendors.at("Acme").name == "Cached Acme"); + + // Nothing was written back: the presets never came from a parse. + CHECK(!fs::exists(user / "Acme.json")); +} + +TEST_CASE("a vendor whose cache is stale falls back to parsing its JSONs", "[VendorCache]") +{ + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + + // An update installed the vendor at 2.0.0; the cache next to it was built from + // the profile before that, so it no longer covers what is on disk. + write_vendor_tree(user, "Acme", "2.0.0"); + REQUIRE(save_one_vendor((user / "Acme.opc").string(), one_vendor("Acme", "Cached Acme"), "Acme", "1.0.0")); + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + + PresetBundle out; + auto [substitutions, presets_loaded] = out.load_vendor_configs_from_json( + user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent); + CHECK(presets_loaded == 1); + CHECK(out.vendors.at("Acme").config_version == Semver(2, 0, 0)); + + // A one-off parse like this one leaves the stale cache alone: only a bundle + // told its parses are complete writes one. + CHECK(VendorCacheFile::peek_version((user / "Acme.opc").string(), "Acme") == "1.0.0"); + + PresetBundle caching; + caching.set_generate_vendor_caches(true); + caching.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + CHECK(VendorCacheFile::peek_version((user / "Acme.opc").string(), "Acme") + == get_version_from_json((user / "Acme.json").string()).to_string()); +} + +TEST_CASE("a cache with a mismatched vendor name is rejected", "[VendorCache]") +{ + TempDir tmp; + const fs::path cache = tmp.path / "vendor.opc"; + REQUIRE(save_one_vendor(cache.string(), one_vendor("VendorA"), "VendorA", "1.0.0")); + + PresetBundle out; + REQUIRE(!out.load_vendor_cache(cache.string(), "VendorB", Semver("1.0.0"))); +} + +TEST_CASE("a cache is rejected against an unparsable version", "[VendorCache]") +{ + TempDir tmp; + const fs::path cache = tmp.path / "vendor.opc"; + REQUIRE(save_one_vendor(cache.string(), one_vendor("Acme"), "Acme", "1.0.0")); + PresetBundle out; + // A profile version that does not parse comes out of get_version_from_json + // as zero, which cannot be judged any more than Semver::invalid() can. + REQUIRE(!out.load_vendor_cache(cache.string(), "Acme", Semver())); + REQUIRE(out.vendors.empty()); // rejection happens before the body is touched +} + +TEST_CASE("the filament library's inheritance maps are rebuilt on cache load", "[VendorCache]") +{ + // m_config_maps/m_filament_id_maps are the inheritance base other vendors + // resolve against. The cache no longer stores them: they are rebuilt by + // installing the library's entries — including the non-instantiated bases, + // which exist for exactly this and never become presets. + TempDir tmp; + const fs::path cache = tmp.path / "lib.opc"; + const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY); + + auto base = filament_entry("Generic PLA", "GFL99"); + base.instantiation = "false"; + base.config_src.set_key_value("filament_cost", new ConfigOptionFloats({20.})); + REQUIRE(save_one_vendor(cache.string(), one_vendor(lib), lib, "1.0.0", {base})); + + PresetBundle out; + REQUIRE(out.load_vendor_cache(cache.string(), lib, Semver("1.0.0"))); + REQUIRE(out.m_config_maps.count("Generic PLA") == 1); + const auto* cost = out.m_config_maps.at("Generic PLA").option<ConfigOptionFloats>("filament_cost"); + REQUIRE(cost != nullptr); + CHECK_THAT(cost->values.front(), WithinAbs(20., 1e-9)); + CHECK(out.m_filament_id_maps.at("Generic PLA") == "GFL99"); + CHECK(presets_for(out.filaments, lib).empty()); // not instantiated, not a preset +} + +TEST_CASE("the same fixture parsed twice serializes byte-identically", "[VendorCache]") +{ + // Shipped caches must be reproducible: the same profiles must produce the + // same bytes on every machine that generates them. + TempDir tmp; + const fs::path rsrc = tmp.path / "resources" / "profiles"; + const fs::path user = tmp.path / "data" / PRESET_SYSTEM_DIR; + fs::create_directories(rsrc); + fs::create_directories(user); + write_full_vendor_tree(user, "Acme", "1.0.0"); + + ScopedDirs dirs(tmp.path / "data", tmp.path / "resources"); + + PresetBundle first; + first.set_generate_vendor_caches(true); + first.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + REQUIRE(fs::exists(user / "Acme.opc")); + const std::string bytes1 = slurp(user / "Acme.opc"); + fs::remove(user / "Acme.opc"); + + PresetBundle second; + second.set_generate_vendor_caches(true); + second.load_vendor_configs_from_json(user.string(), "Acme", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + REQUIRE(slurp(user / "Acme.opc") == bytes1); +} + +TEST_CASE("a cache that fails mid-body deserialization is rejected and leaves the bundle clean", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + const fs::path valid_cache = tmp.path / "valid.opc"; + const fs::path corrupt_cache = tmp.path / "corrupt.opc"; + + REQUIRE(save_one_vendor(valid_cache.string(), one_vendor(vid), vid, "1.0.0", + {filament_entry(vid + " PLA @0.4")}, + {printer_entry(vid + " Printer 0.4")})); + // Truncate the tail (machine entries + parse_errors, per VendorCacheFile::save's + // field order) so the header's size/CRC still validate but cereal runs out of + // bytes partway through the body. Grow the cut if a given size ever stops + // throwing (e.g. after an unrelated field-order change to the cache format). + size_t truncate_by = 40; + bool throws = false; + for (; truncate_by <= 200; truncate_by += 8) { + fs::copy_file(valid_cache, corrupt_cache, fs::copy_option::overwrite_if_exists); + truncate_payload_and_fix_header(corrupt_cache.string(), truncate_by); + PresetBundle probe_bundle; + if (!probe_bundle.load_vendor_cache(corrupt_cache.string(), vid, Semver("1.0.0"))) { + throws = true; + break; + } + } + REQUIRE(throws); + + PresetBundle out; + REQUIRE(!out.load_vendor_cache(corrupt_cache.string(), vid, Semver("1.0.0"))); + // The catch block put the bundle back the way a failed parse would leave it. + CHECK(out.vendors.empty()); + CHECK(out.m_config_maps.empty()); + CHECK(presets_for(out.filaments, vid).empty()); + + // The recovery must leave a bundle a caller can still load a good cache into. + REQUIRE(out.load_vendor_cache(valid_cache.string(), vid, Semver("1.0.0"))); + CHECK(out.vendors.count(vid) == 1); + CHECK(presets_for(out.filaments, vid).size() == 1); +} + +TEST_CASE("a cache rejected mid-body leaves the error count where it found it", "[VendorCache]") +{ + InstallDirs dirs; + + // A vendor whose root profile counts a parse error, so the bundle carries a + // non-zero tally into the load below. Without one there is nothing for a + // rejected cache to zero, and nothing to underflow. + std::ofstream((dirs.system / "Noisy.json").string()) + << R"({"version":"1.0.0","name":"Noisy","process_list":"not a list"})"; + + write_vendor_tree(dirs.system, "Counted", "1.0.0"); + // A cache that passes every stamp and then dies in the entries. + REQUIRE(save_one_vendor((dirs.system / "Counted.opc").string(), one_vendor("Counted"), "Counted", "1.0.0", + {filament_entry("Counted PLA @0.4")})); + truncate_payload_and_fix_header((dirs.system / "Counted.opc").string(), 8); + + PresetBundle bundle; + bundle.set_generate_vendor_caches(true); + bundle.load_vendor_configs_from_json(dirs.system.string(), "Noisy", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent); + REQUIRE(bundle.error_count() > 0); + + // The same bundle: the cache is tried, fails mid-body, and the parse that + // follows must be measured against the tally the cache found rather than + // against zero. + REQUIRE(bundle.load_vendor_configs_from_json(dirs.system.string(), "Counted", PresetBundle::LoadSystem, + ForwardCompatibilitySubstitutionRule::EnableSilent).second > 0); + + // The rewritten cache must carry the parse's own error count, not an + // underflowed one. Reload it and check the bundle does not inherit a + // nonsensical tally. + PresetBundle reloaded; + REQUIRE(reloaded.load_vendor_cache((dirs.system / "Counted.opc").string(), "Counted", Semver(1, 0, 0))); + CHECK(reloaded.error_count() == 0); +} + +TEST_CASE("a preset is traced to its vendor in a build that ships caches alone", "[VendorCache]") +{ + InstallDirs dirs; + + std::vector<CachedPreset> filaments { filament_entry("Cached PLA @0.4") }; + std::vector<CachedPreset> printers { printer_entry("Cached 0.4 nozzle") }; + REQUIRE(save_one_vendor((dirs.profiles / "Cached.opc").string(), one_vendor("Cached"), "Cached", "1.0.0", + filaments, printers)); + + CHECK(PresetBundle::find_preset_vendor("Cached PLA @0.4", Preset::TYPE_FILAMENT) == "Cached"); + CHECK(PresetBundle::find_preset_vendor("Cached 0.4 nozzle", Preset::TYPE_PRINTER) == "Cached"); + CHECK(PresetBundle::find_preset_vendor("Nobody's PLA", Preset::TYPE_FILAMENT).empty()); +} + +TEST_CASE("a bundle that cannot be installed does not drop the others", "[VendorCache]") +{ + InstallDirs dirs; + + write_vendor_tree(dirs.profiles, "Good", "1.0.0"); + + // An empty name sorts first out of a std::map, and a name resources does not + // carry can appear anywhere. Neither may cost the batch the vendors it can + // install. + CHECK_FALSE(install_vendor_bundles_from_resources({"", "Absent", "Good"})); + CHECK(fs::exists(dirs.system / "Good.json")); +} + +TEST_CASE("a cache that arrives unusable leaves the profile fallback in place", "[VendorCache]") +{ + InstallDirs dirs; + + write_vendor_tree(dirs.profiles, "Torn", "1.0.0"); + const std::string cache = (dirs.profiles / "Torn.opc").string(); + REQUIRE(save_one_vendor(cache, one_vendor("Torn"), "Torn", "1.0.0")); + // Past the stamps at the front, so the 1 KB peek that chooses the cache form + // still succeeds — only the CRC, which decides whether it can be served, + // catches this. + corrupt_blob_byte(cache, std::streamoff(fs::file_size(cache)) - 4); + REQUIRE(VendorCacheFile::peek_version(cache, "Torn") == "1.0.0"); + + CHECK(install_vendor_bundles_from_resources({"Torn"})); + CHECK(fs::exists(dirs.system / "Torn.json")); + CHECK_FALSE(fs::exists(dirs.system / "Torn.opc")); +} + +TEST_CASE("a vendor installed as an unreadable cache alone counts as not installed", "[VendorCache]") +{ + InstallDirs dirs; + + const std::string cache = (dirs.system / "Broken.opc").string(); + REQUIRE(save_one_vendor(cache, one_vendor("Broken"), "Broken", "1.0.0")); + REQUIRE(is_vendor_installed("Broken")); + + // A cache this build cannot serve is not an installation: there is no + // profile beside it and, since the single-tier load, nowhere else to load + // the vendor from. + corrupt_blob_byte(cache); + CHECK_FALSE(is_vendor_installed("Broken")); + CHECK_FALSE(installed_vendor_version("Broken").valid()); +} + +TEST_CASE("a stale profile beside a newer cache does not hide the cache's version", "[VendorCache]") +{ + InstallDirs dirs; + + write_vendor_json(dirs.system, "Both", "1.0.0"); + REQUIRE(save_one_vendor((dirs.system / "Both.opc").string(), one_vendor("Both"), "Both", "2.0.0")); + + // The cache covers the profile, so the cache is what a load serves — and + // 2.0.0 is the version installed, not the 1.0.0 the profile still claims. + CHECK(installed_vendor_version("Both") == Semver(2, 0, 0)); +} + +TEST_CASE("a profile newer than the cache beside it is the installed version", "[VendorCache]") +{ + InstallDirs dirs; + + write_vendor_json(dirs.system, "Both", "3.0.0"); + REQUIRE(save_one_vendor((dirs.system / "Both.opc").string(), one_vendor("Both"), "Both", "2.0.0")); + + // The cache no longer covers the profile, so the profile is parsed — and + // its version is the one in force. + CHECK(installed_vendor_version("Both") == Semver(3, 0, 0)); +} + +TEST_CASE("a header claiming more body than the file holds is rejected", "[VendorCache]") +{ + TempDir tmp; + const std::string cache = (tmp.path / "Bounded.opc").string(); + REQUIRE(save_one_vendor(cache, one_vendor("Bounded"), "Bounded", "1.0.0")); + + // Claim a body far larger than the file. Nothing may be allocated on the + // strength of that number. + { + std::fstream f(cache, std::ios::in | std::ios::out | std::ios::binary); + const uint64_t huge = 400ull * 1024ull * 1024ull; + f.seekp(8); + f.write(reinterpret_cast<const char*>(&huge), sizeof(huge)); + } + + PresetBundle bundle; + REQUIRE_FALSE(bundle.load_vendor_cache(cache, "Bounded", Semver(1, 0, 0))); +} + +TEST_CASE("a failed write leaves the previous cache in place", "[VendorCache]") +{ + TempDir tmp; + const std::string cache = (tmp.path / "Durable.opc").string(); + REQUIRE(save_one_vendor(cache, one_vendor("Durable"), "Durable", "1.0.0")); + const std::string before = slurp(cache); + + // A directory where the temp file wants to go: the write cannot complete, + // and must not have destroyed what was already there to find that out. + const fs::path blocker = fs::path(cache + "." + std::to_string(get_current_pid()) + ".tmp"); + fs::create_directories(blocker); + + REQUIRE_FALSE(save_one_vendor(cache, one_vendor("Durable"), "Durable", "2.0.0")); + CHECK(slurp(cache) == before); + + fs::remove_all(blocker); +} + +TEST_CASE("a cache written by another build's option ordering still loads", "[VendorCache]") +{ + // The regression the fingerprint used to prevent by refusing the file + // outright: nothing in the payload depends on serialization_key_ordinal, so + // a build that inserted an option ahead of these reads them back correctly. + TempDir tmp; + const std::string cache = (tmp.path / "Ordinal.opc").string(); + + auto e = filament_entry("Ordinal PLA @0.4"); + e.config_src.set_key_value("filament_cost", new ConfigOptionFloats({42.})); + e.config_src.set_key_value("filament_type", new ConfigOptionStrings({"PLA"})); + REQUIRE(save_one_vendor(cache, one_vendor("Ordinal"), "Ordinal", "1.0.0", {e})); + + PresetBundle bundle; + REQUIRE(bundle.load_vendor_cache(cache, "Ordinal", Semver(1, 0, 0))); + const auto filaments = presets_for(bundle.filaments, "Ordinal"); + REQUIRE(filaments.size() == 1); + const auto* cost = filaments.front()->config.option<ConfigOptionFloats>("filament_cost"); + REQUIRE(cost != nullptr); + CHECK_THAT(cost->values.front(), WithinAbs(42., 1e-9)); + CHECK(filaments.front()->config.option<ConfigOptionStrings>("filament_type")->values.front() == "PLA"); +} + +// ---- CacheDictionary and the name-keyed config payload ------------------- + +namespace { + +// Round-trip one config through the dictionary payload, optionally mutating the +// dictionary between write and read to stand in for another build's schema. +DynamicPrintConfig roundtrip_config(const DynamicPrintConfig& in, + const std::function<void(std::string&)>& mutate_blob = {}) +{ + CacheDictionary wdict; + wdict.collect(in); + std::ostringstream os(std::ios::binary); + { + cereal::BinaryOutputArchive ar(os); + wdict.save(ar); + save_config(ar, in, wdict); + } + std::string blob = os.str(); + if (mutate_blob) + mutate_blob(blob); + std::istringstream is(blob, std::ios::binary); + cereal::BinaryInputArchive ar(is); + CacheDictionary rdict; + rdict.load(ar); + DynamicPrintConfig out; + load_config(ar, out, rdict); + return out; +} + +} // namespace + +TEST_CASE("a config round-trips through the cache dictionary", "[VendorCache]") +{ + DynamicPrintConfig in; + in.set_key_value("layer_height", new ConfigOptionFloat(0.28)); + in.set_key_value("printer_model", new ConfigOptionString("Test Model")); + in.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.6})); + in.set_key_value("spiral_mode", new ConfigOptionBool(true)); + + const DynamicPrintConfig out = roundtrip_config(in); + + CHECK_THAT(out.opt_float("layer_height"), WithinAbs(0.28, 1e-9)); + CHECK(out.opt_string("printer_model") == "Test Model"); + REQUIRE(out.option<ConfigOptionFloats>("nozzle_diameter") != nullptr); + CHECK(out.option<ConfigOptionFloats>("nozzle_diameter")->values.size() == 2); + CHECK(out.opt_bool("spiral_mode") == true); +} + +TEST_CASE("an enum option round-trips by name, not by index", "[VendorCache]") +{ + // top_surface_pattern is a coEnum; its stored int is an index into an enum + // whose order is not a wire contract. Assert on the NAME, so a reordering + // of the enum in PrintConfig.cpp cannot make this test pass by accident. + const ConfigOptionDef* def = print_config_def.get("top_surface_pattern"); + REQUIRE(def != nullptr); + REQUIRE(def->type == coEnum); + REQUIRE(def->enum_keys_map != nullptr); + const int monotonic = def->enum_keys_map->at("monotonic"); + + DynamicPrintConfig in; + in.set_key_value("top_surface_pattern", new ConfigOptionEnumGeneric(def->enum_keys_map, monotonic)); + + const DynamicPrintConfig out = roundtrip_config(in); + REQUIRE(out.option("top_surface_pattern") != nullptr); + CHECK(out.opt_enum<InfillPattern>("top_surface_pattern") == InfillPattern(monotonic)); + CHECK(out.option("top_surface_pattern")->serialize() == "monotonic"); +} + +TEST_CASE("a nullable vector enum round-trips by name, nil included", "[VendorCache]") +{ + // coEnums carries a vector of ints and, unlike coEnum, its ConfigOptionType + // does not fit in a byte - a truncated type in the dictionary would make a + // reader take this for a scalar enum and run off the end of the stream. + // nozzle_type is also nullable, and nil is an int no enum_keys_map names, + // so this covers the dictionary's unnamed-value escape hatch too. + const ConfigOptionDef* def = print_config_def.get("nozzle_type"); + REQUIRE(def != nullptr); + REQUIRE(def->type == coEnums); + REQUIRE(def->nullable); + REQUIRE(def->enum_keys_map != nullptr); + const int brass = def->enum_keys_map->at("brass"); + const int nil = ConfigOptionInts::nil_value(); + + DynamicPrintConfig in; + auto* opt = new ConfigOptionEnumsGenericNullable(def->enum_keys_map); + opt->values = { brass, nil }; + in.set_key_value("nozzle_type", opt); + in.set_key_value("printer_model", new ConfigOptionString("Test Model")); + + const DynamicPrintConfig out = roundtrip_config(in); + const auto* got = out.option<ConfigOptionEnumsGenericNullable>("nozzle_type"); + REQUIRE(got != nullptr); + CHECK(got->values == std::vector<int>{brass, nil}); + CHECK(out.opt_string("printer_model") == "Test Model"); +} + +TEST_CASE("an option the build no longer knows is dropped, and the rest still load", "[VendorCache]") +{ + DynamicPrintConfig in; + in.set_key_value("layer_height", new ConfigOptionFloat(0.28)); + in.set_key_value("printer_model", new ConfigOptionString("Test Model")); + + // Rename the key in the dictionary the reader sees: "layer_height" becomes + // "layer_heighX", a key no build defines. Same length, so the blob's + // offsets are untouched - this is exactly what a removed or renamed option + // looks like to a reader. + const DynamicPrintConfig out = roundtrip_config(in, [](std::string& blob) { + const size_t at = blob.find("layer_height"); + REQUIRE(at != std::string::npos); + blob[at + 11] = 'X'; + }); + + CHECK(out.option("layer_height") == nullptr); + CHECK(out.opt_string("printer_model") == "Test Model"); +} + +TEST_CASE("an option whose type changed is dropped, and the rest still load", "[VendorCache]") +{ + // A payload from a build where layer_height was a coString. This one has it + // as a coFloat, so nothing can be done with the value - but the dictionary + // says how it was written, so its bytes are still consumed and printer_model + // behind it still lands. Hand-written rather than round-tripped: only a + // dictionary this build did not produce can disagree with it. + std::ostringstream os(std::ios::binary); + { + cereal::BinaryOutputArchive ar(os); + const std::vector<std::string> keys { "layer_height", "printer_model" }; + const std::vector<uint16_t> types { uint16_t(coString), uint16_t(coString) }; + const std::vector<std::string> enums { std::string() }; // the ENUM_UNNAMED slot + ar(keys, types, enums); + ar(uint32_t(2)); + ar(uint16_t(0)); ar(ConfigOptionString("0.28")); + ar(uint16_t(1)); ar(ConfigOptionString("Test Model")); + } + + std::istringstream is(os.str(), std::ios::binary); + cereal::BinaryInputArchive ar(is); + CacheDictionary rdict; + rdict.load(ar); + DynamicPrintConfig out; + REQUIRE_NOTHROW(load_config(ar, out, rdict)); + CHECK(out.option("layer_height") == nullptr); + CHECK(out.opt_string("printer_model") == "Test Model"); +} + +TEST_CASE("skip_config consumes a config without building one", "[VendorCache]") +{ + DynamicPrintConfig in; + in.set_key_value("layer_height", new ConfigOptionFloat(0.28)); + in.set_key_value("printer_model", new ConfigOptionString("Test Model")); + + CacheDictionary wdict; + wdict.collect(in); + std::ostringstream os(std::ios::binary); + { + cereal::BinaryOutputArchive ar(os); + wdict.save(ar); + save_config(ar, in, wdict); + ar(std::string("sentinel")); // must still be reachable after the skip + } + + std::istringstream is(os.str(), std::ios::binary); + cereal::BinaryInputArchive ar(is); + CacheDictionary rdict; + rdict.load(ar); + skip_config(ar, rdict); + std::string sentinel; + ar(sentinel); + CHECK(sentinel == "sentinel"); +} + +TEST_CASE("a dictionary index past the end of the table is refused", "[VendorCache]") +{ + DynamicPrintConfig in; + in.set_key_value("layer_height", new ConfigOptionFloat(0.28)); + + CacheDictionary wdict; + wdict.collect(in); + std::ostringstream os(std::ios::binary); + { + cereal::BinaryOutputArchive ar(os); + wdict.save(ar); + save_config(ar, in, wdict); + } + std::string blob = os.str(); + // The payload's tail is the option count (uint32), the key index (uint16) + // and the double. Point the key index somewhere the table does not go. + const uint16_t bad = 0xFFFE; + std::memcpy(&blob[blob.size() - sizeof(double) - sizeof(uint16_t)], &bad, sizeof(bad)); + + std::istringstream is(blob, std::ios::binary); + cereal::BinaryInputArchive ar(is); + CacheDictionary rdict; + rdict.load(ar); + DynamicPrintConfig out; + REQUIRE_THROWS(load_config(ar, out, rdict)); +} + +TEST_CASE("a stamp string with an absurd length is rejected, not allocated", "[VendorCache]") +{ + // The stamps are read from whatever <vendor>.opc a directory holds, and a + // string resize to a garbage 64-bit length does not fail as a catchable + // bad_alloc — it takes the app down through the out-of-memory handler. A + // CRC-valid body opening with the right cache version but foreign framing + // where the name's length word sits must be refused before anything is + // allocated. + TempDir tmp; + const std::string cache = (tmp.path / "Evil.opc").string(); + REQUIRE(save_one_vendor(cache, one_vendor("Evil"), "Evil", "1.0.0")); + + // The vendor name's length word sits right behind the payload's version + // word; make it claim a ~9-exabyte name. + const uint64_t huge = 0x7FFFFFFFFFFFFFFFull; + patch_payload_bytes(cache, sizeof(uint32_t), &huge, sizeof(huge)); + + PresetBundle out; + REQUIRE(! out.load_vendor_cache(cache, "Evil", Semver::inf())); + CHECK(out.vendors.empty()); + CHECK(VendorCacheFile::peek_version(cache, "Evil").empty()); +} + diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index c1424064b2..ebbd62b820 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -2,6 +2,7 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp test_dev_mapping.cpp + test_filament_bitmap_utils.cpp test_network_versions.cpp test_action_source.cpp test_plugin_host_api.cpp diff --git a/tests/slic3rutils/plugin_test_utils.hpp b/tests/slic3rutils/plugin_test_utils.hpp index 52e503f6f4..cbdcf1ff95 100644 --- a/tests/slic3rutils/plugin_test_utils.hpp +++ b/tests/slic3rutils/plugin_test_utils.hpp @@ -6,6 +6,8 @@ #include <string> +#include "test_utils.hpp" + namespace Slic3r { // Point data_dir() at a throwaway directory for the lifetime of a test and @@ -13,27 +15,43 @@ namespace Slic3r { // disposable tree and tests don't leak state into each other. struct ScopedDataDir { + ScopedTemporaryDir tmp; // owns the temp dir (create + recursive remove) + boost::filesystem::path dir; // = tmp.path(); kept as a member for callers std::string previous; - boost::filesystem::path dir; explicit ScopedDataDir(const std::string& tag) + : tmp("orca-" + tag), dir(tmp.path()), previous(data_dir()) { - namespace fs = boost::filesystem; - previous = data_dir(); - dir = fs::temp_directory_path() / fs::unique_path("orca-" + tag + "-%%%%-%%%%"); - fs::create_directories(dir); set_data_dir(dir.string()); } - ~ScopedDataDir() - { - set_data_dir(previous); - boost::system::error_code ec; - boost::filesystem::remove_all(dir, ec); - } + ~ScopedDataDir() { set_data_dir(previous); } // tmp removes the directory + + // The plugin manager scans {data_dir}/orca_plugins. + boost::filesystem::path plugins_dir() const { return dir / "orca_plugins"; } ScopedDataDir(const ScopedDataDir&) = delete; ScopedDataDir& operator=(const ScopedDataDir&) = delete; }; +// Point resources_dir() at a throwaway directory for the lifetime of a test and restore the +// previous value afterwards, mirroring ScopedDataDir. +struct ScopedResourcesDir +{ + ScopedTemporaryDir tmp; + boost::filesystem::path dir; + std::string previous; + + explicit ScopedResourcesDir(const std::string& tag) + : tmp("orca-" + tag), dir(tmp.path()), previous(resources_dir()) + { + set_resources_dir(dir.string()); + } + + ~ScopedResourcesDir() { set_resources_dir(previous); } + + ScopedResourcesDir(const ScopedResourcesDir&) = delete; + ScopedResourcesDir& operator=(const ScopedResourcesDir&) = delete; +}; + } // namespace Slic3r diff --git a/tests/slic3rutils/test_filament_bitmap_utils.cpp b/tests/slic3rutils/test_filament_bitmap_utils.cpp new file mode 100644 index 0000000000..997a119521 --- /dev/null +++ b/tests/slic3rutils/test_filament_bitmap_utils.cpp @@ -0,0 +1,256 @@ +// recompute_mixed_slot_colors lives in libslic3r_gui; this is the only suite that links it. +// Same Windows include prologue as test_dev_mapping.cpp (wx pulls in <windows.h>; keep +// WIN32_LEAN_AND_MEAN / NOMINMAX ahead of the Catch2 headers). +#ifdef WIN32 + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #ifndef NOMINMAX + #define NOMINMAX + #endif + #include <Windows.h> +#endif + +#include <catch2/catch_all.hpp> + +#include <cmath> + +#include <wx/colour.h> +#include <wx/string.h> + +#include "libslic3r/FilamentMixer.hpp" +#include "libslic3r/PrintConfig.hpp" +#include "slic3r/GUI/FilamentBitmapUtils.hpp" + +using namespace Slic3r; +using Slic3r::GUI::recompute_mixed_slot_colors; + +namespace { + +// Two physical slots (1 = red, 2 = blue) and mixed slot 3 built from them. +DynamicPrintConfig mixed_config(const std::string& components = "1,2", const std::string& ratios = "0.5,0.5") +{ + DynamicPrintConfig cfg; + cfg.set_key_value("filament_is_mixed", new ConfigOptionBools({false, false, true})); + cfg.set_key_value("filament_mixed_components", new ConfigOptionStrings({"", "", components})); + cfg.set_key_value("filament_mixed_sublayer_ratios", new ConfigOptionStrings({"", "", ratios})); + cfg.set_key_value("filament_mixed_gradient", new ConfigOptionBools({false, false, false})); + cfg.set_key_value("filament_colour", new ConfigOptionStrings({"#FF0000", "#0000FF", "#000000"})); + return cfg; +} + +wxColour expected_blend(const std::vector<std::string>& hex, const std::vector<int>& weights) +{ + return wxColour(wxString(blend_color_multi(hex, weights))); +} + +// Compare channels one at a time so a failure names the channel. +void require_same_rgb(const wxColour& actual, const wxColour& expected) +{ + REQUIRE(int(actual.Red()) == int(expected.Red())); + REQUIRE(int(actual.Green()) == int(expected.Green())); + REQUIRE(int(actual.Blue()) == int(expected.Blue())); +} + +} // namespace + +TEST_CASE("recompute_mixed_slot_colors blends a mixed slot from its components' colours", "[FilamentBitmapUtils]") +{ + std::vector<wxColour> colors{wxColour(255, 0, 0), wxColour(0, 0, 255)}; + recompute_mixed_slot_colors(colors, mixed_config()); + + REQUIRE(colors.size() == 3); + require_same_rgb(colors[2], expected_blend({"#FF0000", "#0000FF"}, {5000, 5000})); + REQUIRE(int(colors[2].Alpha()) == 255); + // Physical slots are left alone. + require_same_rgb(colors[0], wxColour(255, 0, 0)); + require_same_rgb(colors[1], wxColour(0, 0, 255)); +} + +TEST_CASE("recompute_mixed_slot_colors leaves the colours alone without mixed slots", "[FilamentBitmapUtils]") +{ + std::vector<wxColour> colors{wxColour(255, 0, 0), wxColour(0, 0, 255)}; + + SECTION("no mixed keys at all") { + recompute_mixed_slot_colors(colors, DynamicPrintConfig{}); + } + SECTION("mixed flags present but all false") { + DynamicPrintConfig cfg; + cfg.set_key_value("filament_is_mixed", new ConfigOptionBools({false, false})); + cfg.set_key_value("filament_mixed_components", new ConfigOptionStrings({"", ""})); + recompute_mixed_slot_colors(colors, cfg); + } + REQUIRE(colors.size() == 2); + require_same_rgb(colors[0], wxColour(255, 0, 0)); + require_same_rgb(colors[1], wxColour(0, 0, 255)); +} + +TEST_CASE("recompute_mixed_slot_colors falls back to grey for a broken component reference", "[FilamentBitmapUtils]") +{ + const wxColour grey(128, 128, 128, 255); + std::vector<wxColour> colors{wxColour(255, 0, 0), wxColour(0, 0, 255)}; + + SECTION("dangling component id") { + recompute_mixed_slot_colors(colors, mixed_config("1,9")); + } + SECTION("empty component list") { + recompute_mixed_slot_colors(colors, mixed_config("")); + } + REQUIRE(colors.size() == 3); + require_same_rgb(colors[2], grey); +} + +TEST_CASE("recompute_mixed_slot_colors uses the project colour when a slot colour is unset", "[FilamentBitmapUtils]") +{ + // Slot 2 carries no colour in the vector; filament_colour[1] = "#0000FF" is used instead. + std::vector<wxColour> colors{wxColour(255, 0, 0), wxColour()}; + recompute_mixed_slot_colors(colors, mixed_config()); + require_same_rgb(colors[2], expected_blend({"#FF0000", "#0000FF"}, {5000, 5000})); +} + +TEST_CASE("recompute_mixed_slot_colors blends a gradient slot from its end points only", "[FilamentBitmapUtils]") +{ + DynamicPrintConfig cfg; + cfg.set_key_value("filament_is_mixed", new ConfigOptionBools({false, false, false, true})); + cfg.set_key_value("filament_mixed_components", new ConfigOptionStrings({"", "", "", "1,2,3"})); + cfg.set_key_value("filament_mixed_sublayer_ratios", new ConfigOptionStrings({"", "", "", "0.2,0.3,0.5"})); + cfg.set_key_value("filament_mixed_gradient", new ConfigOptionBools({false, false, false, true})); + cfg.set_key_value("filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00", "#0000FF", "#000000"})); + + std::vector<wxColour> colors{wxColour(255, 0, 0), wxColour(0, 255, 0), wxColour(0, 0, 255)}; + recompute_mixed_slot_colors(colors, cfg); + + REQUIRE(colors.size() == 4); + require_same_rgb(colors[3], expected_blend({"#FF0000", "#0000FF"}, {5000, 5000})); +} + +TEST_CASE("recompute_mixed_slot_colors honours the configured ratios and is idempotent", "[FilamentBitmapUtils]") +{ + std::vector<wxColour> colors{wxColour(255, 0, 0), wxColour(0, 0, 255)}; + const DynamicPrintConfig cfg = mixed_config("1,2", "0.7,0.3"); + recompute_mixed_slot_colors(colors, cfg); + const wxColour first = colors[2]; + // The configured 70/30 ratio must reach the blend (it is not the equal-share default). + require_same_rgb(first, expected_blend({"#FF0000", "#0000FF"}, {7000, 3000})); + REQUIRE(first != expected_blend({"#FF0000", "#0000FF"}, {5000, 5000})); + recompute_mixed_slot_colors(colors, cfg); + require_same_rgb(colors[2], first); +} + +// --- mixed_gradient_ramp / sample_gradient_ramp ----------------------------------------- +// +// The ramp is what every mixed filament swatch is drawn from, so these pin the three things +// a plain fade between two endpoint colours cannot express: the reserved ratio band, the +// component order, and the custom curve. + +namespace { + +// Slot 3 (index 2) is a gradient mix of physical slots 1 (red) and 2 (blue). +DynamicPrintConfig gradient_config(const std::string& components = "1,2", + const std::string& range = "0.9,0.1", + const std::string& curve = "") +{ + DynamicPrintConfig cfg; + cfg.set_key_value("filament_is_mixed", new ConfigOptionBools({false, false, true})); + cfg.set_key_value("filament_mixed_components", new ConfigOptionStrings({"", "", components})); + cfg.set_key_value("filament_mixed_gradient", new ConfigOptionBools({false, false, true})); + cfg.set_key_value("filament_mixed_gradient_range", new ConfigOptionStrings({"", "", range})); + cfg.set_key_value("filament_mixed_gradient_curve", new ConfigOptionStrings({"", "", curve})); + cfg.set_key_value("filament_colour", new ConfigOptionStrings({"#FF0000", "#0000FF", "#000000"})); + return cfg; +} + +} // namespace + +TEST_CASE("mixed_gradient_ramp runs bottom to top and never reaches a pure component", "[FilamentBitmapUtils]") +{ + // range "0.9,0.1": component 1 (red) is the majority at the bottom and the minority at the top. + const auto ramp = Slic3r::GUI::mixed_gradient_ramp(gradient_config(), 2, 16); + REQUIRE(ramp.size() == 16); + + // Neither end is the pure component colour - the slicer clamps the blend to + // [kGradientMinRatio, kGradientMaxRatio], which a fade between the pure colours would ignore. + REQUIRE(ramp.front() != wxColour(255, 0, 0)); + REQUIRE(ramp.back() != wxColour(0, 0, 255)); + + // Red falls and blue rises monotonically from bottom to top. + for (size_t i = 1; i < ramp.size(); ++i) { + REQUIRE(int(ramp[i].Red()) <= int(ramp[i - 1].Red())); + REQUIRE(int(ramp[i].Blue()) >= int(ramp[i - 1].Blue())); + } +} + +TEST_CASE("mixed_gradient_ramp follows the range's direction rather than the component order", "[FilamentBitmapUtils]") +{ + const auto rising = Slic3r::GUI::mixed_gradient_ramp(gradient_config("1,2", "0.1,0.9"), 2, 16); + const auto falling = Slic3r::GUI::mixed_gradient_ramp(gradient_config("1,2", "0.9,0.1"), 2, 16); + REQUIRE(rising.size() == 16); + REQUIRE(falling.size() == 16); + + // "0.1,0.9" starts blue-heavy at the bottom; "0.9,0.1" starts red-heavy. Reversing the + // range must reverse the ramp, which endpoint colours ordered by HSV cannot express. + REQUIRE(int(rising.front().Blue()) > int(rising.front().Red())); + REQUIRE(int(falling.front().Red()) > int(falling.front().Blue())); + require_same_rgb(rising.front(), falling.back()); +} + +TEST_CASE("mixed_gradient_ramp bends with a custom curve", "[FilamentBitmapUtils]") +{ + // Component 1 holds near its maximum for the first half, then drops - a shape a straight + // fade between two endpoints cannot draw. + const auto curved = Slic3r::GUI::mixed_gradient_ramp( + gradient_config("1,2", "0.9,0.1", "0,0.9|0.5,0.85|1,0.1"), 2, 16); + const auto linear = Slic3r::GUI::mixed_gradient_ramp(gradient_config("1,2", "0.9,0.1"), 2, 16); + REQUIRE(curved.size() == 16); + + // The curve holds component 1 high through the lower half, so every band up to mid height + // is at least as red as the straight fade and mid height is strictly redder. + for (size_t i = 0; i <= curved.size() / 2; ++i) + REQUIRE(int(curved[i].Red()) >= int(linear[i].Red())); + REQUIRE(int(curved[curved.size() / 2].Red()) > int(linear[linear.size() / 2].Red())); + // It still ends blue-dominant, like the straight fade. + REQUIRE(int(curved.back().Blue()) > int(curved.back().Red())); +} + +TEST_CASE("mixed_gradient_ramp is empty for anything but a two-component gradient slot", "[FilamentBitmapUtils]") +{ + SECTION("slot is not mixed") { + REQUIRE(Slic3r::GUI::mixed_gradient_ramp(gradient_config(), 0, 16).empty()); + } + SECTION("gradient is off") { + DynamicPrintConfig cfg = gradient_config(); + cfg.set_key_value("filament_mixed_gradient", new ConfigOptionBools({false, false, false})); + REQUIRE(Slic3r::GUI::mixed_gradient_ramp(cfg, 2, 16).empty()); + } + SECTION("three components") { + REQUIRE(Slic3r::GUI::mixed_gradient_ramp(gradient_config("1,2,3"), 2, 16).empty()); + } + SECTION("slot out of range") { + REQUIRE(Slic3r::GUI::mixed_gradient_ramp(gradient_config(), 9, 16).empty()); + } + SECTION("no mixed keys at all") { + REQUIRE(Slic3r::GUI::mixed_gradient_ramp(DynamicPrintConfig{}, 0, 16).empty()); + } +} + +TEST_CASE("sample_gradient_ramp blends each step through the shared blender", "[FilamentBitmapUtils]") +{ + // A flat curve makes every step the same 30/70 mix, which must come out as the blend the + // dialog's own swatches are drawn with - not a channel lerp between the two components. + GradientCurve curve; + curve.points = {{0.0, 0.3, NAN, NAN}, {1.0, 0.3, NAN, NAN}}; + const auto ramp = Slic3r::GUI::sample_gradient_ramp(wxColour(255, 0, 0), wxColour(0, 0, 255), curve, 4); + REQUIRE(ramp.size() == 4); + + const wxColour expected = Slic3r::GUI::blend_n_colors({wxColour(255, 0, 0), wxColour(0, 0, 255)}, {0.3, 0.7}); + for (const wxColour& c : ramp) + require_same_rgb(c, expected); +} + +TEST_CASE("sample_gradient_ramp returns nothing without a usable curve or step count", "[FilamentBitmapUtils]") +{ + GradientCurve curve; + REQUIRE(Slic3r::GUI::sample_gradient_ramp(wxColour(255, 0, 0), wxColour(0, 0, 255), curve, 8).empty()); + curve.points = {{0.0, kGradientMaxRatio, NAN, NAN}, {1.0, kGradientMinRatio, NAN, NAN}}; + REQUIRE(Slic3r::GUI::sample_gradient_ramp(wxColour(255, 0, 0), wxColour(0, 0, 255), curve, 0).empty()); +} diff --git a/tests/slic3rutils/test_network_versions.cpp b/tests/slic3rutils/test_network_versions.cpp index efe8b5d831..349082a965 100644 --- a/tests/slic3rutils/test_network_versions.cpp +++ b/tests/slic3rutils/test_network_versions.cpp @@ -6,6 +6,8 @@ #include "libslic3r/Utils.hpp" #include "slic3r/Utils/bambu_networking.hpp" +#include "plugin_test_utils.hpp" + using namespace Slic3r; namespace fs = boost::filesystem; @@ -25,27 +27,16 @@ static const char* PLUGIN_EXT = ".so"; struct PluginFolderFixture { - fs::path root; - std::string previous_data_dir; + ScopedDataDir data{"netver"}; PluginFolderFixture() { - previous_data_dir = data_dir(); - root = fs::temp_directory_path() / fs::unique_path("orca-netver-%%%%%%%%"); - fs::create_directories(root / "plugins"); - set_data_dir(root.string()); - } - - ~PluginFolderFixture() - { - set_data_dir(previous_data_dir); - boost::system::error_code ec; - fs::remove_all(root, ec); + fs::create_directories(data.dir / "plugins"); } void add_plugin(const std::string& version) { - boost::nowide::ofstream f((root / "plugins" / (PLUGIN_PREFIX + version + PLUGIN_EXT)).string()); + boost::nowide::ofstream f((data.dir / "plugins" / (PLUGIN_PREFIX + version + PLUGIN_EXT)).string()); f << "stub"; } }; diff --git a/tests/slic3rutils/test_plugin_audit.cpp b/tests/slic3rutils/test_plugin_audit.cpp index 49eedbd1fb..71ce692671 100644 --- a/tests/slic3rutils/test_plugin_audit.cpp +++ b/tests/slic3rutils/test_plugin_audit.cpp @@ -27,6 +27,16 @@ void seed_denied_names() mgr.add_denied_filename(name); } +// Seed the keyword registry with the same list install_hook() uses. Same rationale as +// seed_denied_names(): a process-singleton registry, seeded from the single shared source so +// production and tests cannot drift apart. +void seed_denied_keywords() +{ + PluginAuditManager& mgr = PluginAuditManager::instance(); + for (const auto& keyword : PluginAuditManager::default_denied_path_keywords()) + mgr.add_denied_path_keyword(keyword); +} + } // namespace TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit]") @@ -81,7 +91,7 @@ TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit } } -TEST_CASE("Plugin audit deny beats allowed roots and the Loading read exemption", "[audit]") +TEST_CASE("Plugin audit deny beats allowed roots", "[audit]") { ScopedDataDir data_dir_guard("plugin-audit-deny"); seed_denied_names(); @@ -91,8 +101,8 @@ TEST_CASE("Plugin audit deny beats allowed roots and the Loading read exemption" // config and the token would otherwise be reachable simply by living inside it. mgr.add_global_allowed_root(data_dir()); - // Enter a plugin context. The deny must hold in Loading mode, which every scope runs in. - ScopedPluginAuditContext ctx("test_plugin", "", PluginAuditManager::AuditMode::Loading); + // Enter a plugin context. The deny must hold even inside a globally allowed root. + ScopedPluginAuditContext ctx("test_plugin", ""); const fs::path conf = fs::path(data_dir()) / (SLIC3R_APP_KEY ".conf"); const fs::path token = fs::path(data_dir()) / secret_constants::USER_SECRET_FILENAME; @@ -103,6 +113,13 @@ TEST_CASE("Plugin audit deny beats allowed roots and the Loading read exemption" CHECK(decision.allowed); } + SECTION("a file outside the allowed root is blocked for reads as well as writes") + { + AuditDecision decision = mgr.check_open((fs::path(data_dir()).parent_path() / "outside.txt").string(), "r"); + CHECK_FALSE(decision.allowed); + CHECK(decision.reason == "outside allowed root"); + } + SECTION("writing the app config is blocked despite data_dir() being allowed") { AuditDecision decision = mgr.check_open(conf.string(), "w"); @@ -110,16 +127,14 @@ TEST_CASE("Plugin audit deny beats allowed roots and the Loading read exemption" CHECK(decision.reason == "denied filename"); } - SECTION("reading the app config is blocked even though Loading exempts reads") + SECTION("reading the app config is blocked despite the allowed root") { - // Without the deny, a read in Loading mode short-circuits to allow. The deny sits above - // that exemption, so this must still be blocked. AuditDecision decision = mgr.check_open(conf.string(), "r"); CHECK_FALSE(decision.allowed); CHECK(decision.reason == "denied filename"); } - SECTION("reading the cloud refresh token is blocked in Loading mode") + SECTION("reading the cloud refresh token is blocked") { AuditDecision decision = mgr.check_open(token.string(), "r"); CHECK_FALSE(decision.allowed); @@ -150,7 +165,7 @@ TEST_CASE("Plugin audit deny beats a plugin's own scoped root", "[audit]") const fs::path plugin_dir = fs::path(data_dir()) / "plugins" / "test_plugin"; fs::create_directories(plugin_dir); - ScopedPluginAuditContext ctx("test_plugin", "", PluginAuditManager::AuditMode::Loading); + ScopedPluginAuditContext ctx("test_plugin", ""); mgr.add_scoped_allowed_root(plugin_dir); SECTION("the plugin's own non-denied file opens for read and write") @@ -185,3 +200,139 @@ TEST_CASE("Plugin audit does not constrain non-plugin code", "[audit]") CHECK(mgr.check_open(conf.string(), "w").allowed); CHECK(mgr.check_open(conf.string(), "r").allowed); } + +TEST_CASE("Plugin audit denies secret/certificate/config-like paths by keyword", "[audit]") +{ + seed_denied_keywords(); + const PluginAuditManager& mgr = PluginAuditManager::instance(); + + SECTION("a 'secrets' directory component is denied") + { + CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/secrets/api_key.json"))); + CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/secret/token.txt"))); + } + + SECTION("a 'certificate(s)' directory component is denied") + { + CHECK(mgr.is_denied_path_keyword(fs::path("/resources/cert/slicer_base64.cer"))); + CHECK(mgr.is_denied_path_keyword(fs::path("/resources/certificates/ca.pem"))); + } + + SECTION("a 'conf'/'config' directory or file component is denied") + { + CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/conf/settings.json"))); + CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/config/settings.json"))); + CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/plugin.conf"))); + } + + SECTION("matching is case-insensitive") + { + CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/SECRETS/token.txt"))); + CHECK(mgr.is_denied_path_keyword(fs::path("/resources/CertBundle/ca.pem"))); + CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/CONFIG.JSON"))); + } + + SECTION("matching is not limited to the base name -- any ancestor component counts") + { + CHECK(mgr.is_denied_path_keyword(fs::path("/data/secrets/nested/deep/file.txt"))); + } + + SECTION("an unrelated path is not denied") + { + CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/plugin/output/model.gcode"))); + CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/plugin/storage/state.json"))); + } + + SECTION("an empty path is not denied") + { + CHECK_FALSE(mgr.is_denied_path_keyword(fs::path())); + } +} + +TEST_CASE("Plugin audit is_denied_path combines the filename and keyword registries", "[audit]") +{ + seed_denied_names(); + seed_denied_keywords(); + const PluginAuditManager& mgr = PluginAuditManager::instance(); + + SECTION("a filename-registry match is denied") + { + CHECK(mgr.is_denied_path(fs::path(SLIC3R_APP_KEY ".conf"))); + } + + SECTION("a keyword-registry match is denied") + { + CHECK(mgr.is_denied_path(fs::path("/plugin/secrets/token.txt"))); + } + + SECTION("a path matching neither registry is not denied") + { + CHECK_FALSE(mgr.is_denied_path(fs::path("/plugin/output/model.gcode"))); + } +} + +TEST_CASE("Plugin audit a read-only allowed root blocks writes but not reads", "[audit]") +{ + ScopedDataDir data_dir_guard("plugin-audit-readonly"); + ScopedResourcesDir resources_dir_guard("plugin-audit-readonly-resources"); + seed_denied_names(); + seed_denied_keywords(); + + PluginAuditManager& mgr = PluginAuditManager::instance(); + mgr.add_global_allowed_root(resources_dir(), /*allow_write=*/false); + + ScopedPluginAuditContext ctx("test_plugin", ""); + + const fs::path readonly_file = fs::path(resources_dir()) / "profiles" / "vendor.json"; + + SECTION("a read inside the read-only root is allowed") + { + CHECK(mgr.check_open(readonly_file.string(), "r").allowed); + } + + SECTION("a write inside the read-only root is blocked") + { + AuditDecision decision = mgr.check_open(readonly_file.string(), "w"); + CHECK_FALSE(decision.allowed); + CHECK(decision.reason == "outside allowed root"); + } + + SECTION("a create inside the read-only root is blocked") + { + AuditDecision decision = mgr.check_path_access(readonly_file, /*is_write=*/true); + CHECK_FALSE(decision.allowed); + } + + SECTION("the bundled cert underneath the read-only root is denied even for reads") + { + const fs::path cert = fs::path(resources_dir()) / "cert" / "slicer_base64.cer"; + AuditDecision decision = mgr.check_open(cert.string(), "r"); + CHECK_FALSE(decision.allowed); + CHECK(decision.reason == "denied path keyword"); + } +} + +TEST_CASE("Plugin audit a scoped root can also be registered read-only", "[audit]") +{ + ScopedDataDir data_dir_guard("plugin-audit-scoped-readonly"); + seed_denied_names(); + seed_denied_keywords(); + + PluginAuditManager& mgr = PluginAuditManager::instance(); + + const fs::path readonly_dir = fs::path(data_dir()) / "readonly_scope"; + fs::create_directories(readonly_dir); + + ScopedPluginAuditContext ctx("test_plugin", ""); + mgr.add_scoped_allowed_root(readonly_dir, /*allow_write=*/false); + + SECTION("a read inside the scoped read-only root is allowed") + { + CHECK(mgr.check_open((readonly_dir / "vendor.json").string(), "r").allowed); + } + + SECTION("a write inside the scoped read-only root is blocked") + { + CHECK_FALSE(mgr.check_open((readonly_dir / "vendor.json").string(), "w").allowed); + } +} diff --git a/tests/slic3rutils/test_plugin_install.cpp b/tests/slic3rutils/test_plugin_install.cpp index fceeaf1f83..ae16703fa6 100644 --- a/tests/slic3rutils/test_plugin_install.cpp +++ b/tests/slic3rutils/test_plugin_install.cpp @@ -112,6 +112,22 @@ TEST_CASE("install-state sidecar is the source of truth for a cloud plugin's ins REQUIRE(read_install_state(plugin_dir, state)); CHECK(state.installed_version == "1.2.0"); + state.permissions.fs_read = {"/path/to/read"}; + state.permissions.fs_readwrite = {"/path/to/readwrite"}; + state.permissions.network_http = {"https://api.example.com"}; + state.permissions.network_socket = {"192.168.45.6:443"}; + state.permissions.process = {"/usr/bin/curl"}; + REQUIRE(write_install_state(plugin_dir, state)); + + // Permission data is persisted in the same sidecar as the installation metadata. + PluginInstallState persisted; + REQUIRE(read_install_state(plugin_dir, persisted)); + CHECK(persisted.permissions.fs_read == state.permissions.fs_read); + CHECK(persisted.permissions.fs_readwrite == state.permissions.fs_readwrite); + CHECK(persisted.permissions.network_http == state.permissions.network_http); + CHECK(persisted.permissions.network_socket == state.permissions.network_socket); + CHECK(persisted.permissions.process == state.permissions.process); + // Reading the sidecar back onto a freshly-scanned descriptor (whose header version is still // 1.0.0) must surface the cloud-installed 1.2.0. This is what lets update_cloud_metadata compare // the cloud's latest version against the installed version instead of the stale header, so an @@ -120,4 +136,4 @@ TEST_CASE("install-state sidecar is the source of truth for a cloud plugin's ins scanned.version = "1.0.0"; // as parsed from the unchanged PEP723 header read_install_state(plugin_dir, scanned); CHECK(scanned.installed_version == "1.2.0"); -} \ No newline at end of file +} diff --git a/tests/slic3rutils/test_plugin_lifecycle.cpp b/tests/slic3rutils/test_plugin_lifecycle.cpp index c49471de28..63a4e6827b 100644 --- a/tests/slic3rutils/test_plugin_lifecycle.cpp +++ b/tests/slic3rutils/test_plugin_lifecycle.cpp @@ -6,6 +6,8 @@ #include <slic3r/plugin/PluginFsUtils.hpp> #include <slic3r/plugin/PythonInterpreter.hpp> +#include "plugin_test_utils.hpp" + #include <boost/filesystem.hpp> #include <algorithm> @@ -25,32 +27,6 @@ namespace fs = boost::filesystem; namespace { -// Point data_dir() at a throwaway directory for the lifetime of a test and restore the previous -// value afterwards, so discovery scans a disposable {data_dir}/orca_plugins tree and tests don't -// leak state into each other. -struct ScopedDataDir -{ - std::string previous; - fs::path dir; - - explicit ScopedDataDir(const std::string& tag) - { - previous = data_dir(); - dir = fs::temp_directory_path() / fs::unique_path("orca-" + tag + "-%%%%-%%%%"); - fs::create_directories(dir); - set_data_dir(dir.string()); - } - - ~ScopedDataDir() - { - set_data_dir(previous); - boost::system::error_code ec; - fs::remove_all(dir, ec); - } - - fs::path plugins_dir() const { return dir / "orca_plugins"; } -}; - // Brings the plugin system up, and tears it down explicitly at the end of the test. // // Shutting the interpreter down here, rather than leaving it to PythonInterpreter's static diff --git a/tests/slic3rutils/test_slicing_pipeline_bindings.cpp b/tests/slic3rutils/test_slicing_pipeline_bindings.cpp index 8f00b6f31b..5e819c09e0 100644 --- a/tests/slic3rutils/test_slicing_pipeline_bindings.cpp +++ b/tests/slic3rutils/test_slicing_pipeline_bindings.cpp @@ -16,6 +16,8 @@ TEST_CASE("SlicingPipeline capability-type string maps round-trip", "[slicing_pi #include "libslic3r/Point.hpp" #include "libslic3r/ExPolygon.hpp" #include "libslic3r/Surface.hpp" + +#include "test_utils.hpp" #include "libslic3r/Layer.hpp" #include "libslic3r/ExtrusionEntity.hpp" #include "libslic3r/ExtrusionEntityCollection.hpp" @@ -142,7 +144,7 @@ TEST_CASE("orca.slicing psGCodePostProcess context: file edit in place + config import_orca_module(); py::gil_scoped_acquire gil; - const fs::path gpath = fs::temp_directory_path() / fs::unique_path("orca_pp_%%%%-%%%%.gcode"); + ScopedTemporaryFile gpath(".gcode"); { boost::nowide::ofstream ofs(gpath.string()); ofs << "; header\nG1 X0 Y0\n"; @@ -196,9 +198,7 @@ _pp_result = Stamp().execute(_pp_ctx) boost::nowide::ifstream ifs(gpath.string()); std::stringstream ss; ss << ifs.rdbuf(); contents = ss.str(); } - CHECK(contents.find("; stamped by File") != std::string::npos); - fs::remove(gpath); -} + CHECK(contents.find("; stamped by File") != std::string::npos);} // --------------------------------------------------------------------------- // Toolpath helpers for the raw-graph tests. diff --git a/tests/test_utils.hpp b/tests/test_utils.hpp index d928f2f41e..e3fbbe8fab 100644 --- a/tests/test_utils.hpp +++ b/tests/test_utils.hpp @@ -4,6 +4,7 @@ #include <libslic3r/TriangleMesh.hpp> #include <libslic3r/Format/OBJ.hpp> #include <libslic3r/SVG.hpp> +#include <libslic3r/Utils.hpp> #include <boost/filesystem.hpp> @@ -27,26 +28,65 @@ inline Slic3r::TriangleMesh load_model(const std::string &obj_filename) return mesh; } -// RAII holder for a unique temporary file path, removed when the guard goes out -// of scope so a failing assertion never leaks it. Uses the system temp dir with -// a unique name (parallel-safe, cross-platform). The file itself is created by -// whoever writes to path()/string(); this only reserves the name and cleans up. -class ScopedTemporaryFile +// --------------------------------------------------------------------------- +// Scoped temporary paths +// --------------------------------------------------------------------------- + +// Owns a unique path under the system temp dir, "<prefix>-<unique>[<extension>]" +// (parallel-safe, cross-platform). Shared base for the RAII temp guards below. +class ScopedTemporaryPath +{ +public: + const boost::filesystem::path &path() const { return m_path; } + std::string string() const { return m_path.string(); } + ScopedTemporaryPath(const ScopedTemporaryPath &) = delete; + ScopedTemporaryPath &operator=(const ScopedTemporaryPath &) = delete; + +protected: + ScopedTemporaryPath(const std::string &prefix, const std::string &extension) + : m_path(boost::filesystem::temp_directory_path() + / boost::filesystem::unique_path(prefix + "-%%%%-%%%%-%%%%" + extension)) + {} + ~ScopedTemporaryPath() = default; // non-virtual: never deleted through a base pointer + + boost::filesystem::path m_path; +}; + +// A temp file the caller creates by writing to path()/string(); the guard only +// reserves the name and removes the file on scope exit. +class ScopedTemporaryFile : public ScopedTemporaryPath { public: explicit ScopedTemporaryFile(const std::string &extension = ".tmp") - : m_path(boost::filesystem::temp_directory_path() - / boost::filesystem::unique_path("orca-%%%%-%%%%-%%%%" + extension)) - {} + : ScopedTemporaryPath("orca", extension) {} ~ScopedTemporaryFile() { boost::system::error_code ec; boost::filesystem::remove(m_path, ec); } - ScopedTemporaryFile(const ScopedTemporaryFile &) = delete; - ScopedTemporaryFile &operator=(const ScopedTemporaryFile &) = delete; +}; - const boost::filesystem::path &path() const { return m_path; } - std::string string() const { return m_path.string(); } +// A temp directory created on construction and removed recursively on scope exit. +class ScopedTemporaryDir : public ScopedTemporaryPath +{ +public: + explicit ScopedTemporaryDir(const std::string &prefix = "orca") + : ScopedTemporaryPath(prefix, "") { boost::filesystem::create_directories(m_path); } + ~ScopedTemporaryDir() { boost::system::error_code ec; boost::filesystem::remove_all(m_path, ec); } +}; + +// A temp directory that is also Slic3r::temporary_dir() for its lifetime. No test +// process sets that global, so code under test which writes there (for example +// StepPreProcessor::preprocess) lands at the filesystem root. Restored on scope exit +// even when an assertion throws, so it cannot leak into later tests. +class ScopedSlic3rTemporaryDir : public ScopedTemporaryDir +{ +public: + explicit ScopedSlic3rTemporaryDir(const std::string &prefix = "orca") + : ScopedTemporaryDir(prefix), m_previous(Slic3r::temporary_dir()) + { Slic3r::set_temporary_dir(string()); } + // Runs before ~ScopedTemporaryDir, so the setting goes back while the directory + // it names still exists. + ~ScopedSlic3rTemporaryDir() { Slic3r::set_temporary_dir(m_previous); } private: - boost::filesystem::path m_path; + const std::string m_previous; }; // --------------------------------------------------------------------------- @@ -66,7 +106,7 @@ inline std::string debug_artifact_path(const std::string &name) boost::filesystem::path dir = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path("orca-test-artifacts-%%%%-%%%%"); boost::filesystem::create_directories(dir); - std::printf("Debug test artifacts will be written to %s\n", dir.string().c_str()); + std::fprintf(stderr, "Debug test artifacts will be written to %s\n", dir.string().c_str()); return dir; }(); boost::filesystem::path full = root / name; @@ -75,57 +115,52 @@ inline std::string debug_artifact_path(const std::string &name) } // Dump a mesh as OBJ. -inline void write_debug_obj(const std::string &name, const Slic3r::TriangleMesh &mesh) +inline void write_debug_obj([[maybe_unused]] const std::string &name, + [[maybe_unused]] const Slic3r::TriangleMesh &mesh) { #ifndef NDEBUG mesh.WriteOBJFile(debug_artifact_path(name).c_str()); -#else - (void) name; (void) mesh; #endif } -inline void write_debug_obj(const std::string &name, const indexed_triangle_set &its) +inline void write_debug_obj([[maybe_unused]] const std::string &name, + [[maybe_unused]] const indexed_triangle_set &its) { #ifndef NDEBUG its_write_obj(its, debug_artifact_path(name).c_str()); -#else - (void) name; (void) its; #endif } // Dump a mesh as ASCII STL. -inline void write_debug_stl(const std::string &name, const Slic3r::TriangleMesh &mesh) +inline void write_debug_stl([[maybe_unused]] const std::string &name, + [[maybe_unused]] const Slic3r::TriangleMesh &mesh) { #ifndef NDEBUG mesh.write_ascii(debug_artifact_path(name).c_str()); -#else - (void) name; (void) mesh; #endif } // Draw an SVG artifact through a callback that receives the open SVG. Second // overload takes a BoundingBox when the drawing needs one. template<class Draw> -inline void write_debug_svg(const std::string &name, Draw &&draw) +inline void write_debug_svg([[maybe_unused]] const std::string &name, [[maybe_unused]] Draw &&draw) { #ifndef NDEBUG Slic3r::SVG svg(debug_artifact_path(name)); draw(svg); svg.Close(); -#else - (void) name; (void) draw; #endif } template<class Draw> -inline void write_debug_svg(const std::string &name, const Slic3r::BoundingBox &bbox, Draw &&draw) +inline void write_debug_svg([[maybe_unused]] const std::string &name, + [[maybe_unused]] const Slic3r::BoundingBox &bbox, + [[maybe_unused]] Draw &&draw) { #ifndef NDEBUG Slic3r::SVG svg(debug_artifact_path(name), bbox); draw(svg); svg.Close(); -#else - (void) name; (void) bbox; (void) draw; #endif } @@ -133,13 +168,11 @@ inline void write_debug_svg(const std::string &name, const Slic3r::BoundingBox & // artifact. operator<< is resolved by ADL at the call site, so this header needn't // include the producer's headers. template<class Produce> -inline void write_debug_stream(const std::string &name, Produce &&produce) +inline void write_debug_stream([[maybe_unused]] const std::string &name, [[maybe_unused]] Produce &&produce) { #ifndef NDEBUG std::ofstream out(debug_artifact_path(name), std::ios::out | std::ios::binary); out << produce(); -#else - (void) name; (void) produce; #endif }