Merge branch 'refactor/printer-agent-interface' of https://github.com/OrcaSlicer/OrcaSlicer into refactor/printer-agent-interface

This commit is contained in:
Ian Chua
2026-08-19 17:11:14 +08:00
83 changed files with 3066 additions and 640 deletions
+6
View File
@@ -1,8 +1,12 @@
name: Check profiles name: Check profiles
on: on:
pull_request: 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: branches:
- main - main
- release/*
paths: paths:
- 'resources/profiles/**' - 'resources/profiles/**'
- ".github/workflows/check_profiles.yml" - ".github/workflows/check_profiles.yml"
@@ -20,6 +24,8 @@ permissions:
jobs: jobs:
check_profiles: 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 name: Check profiles
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
+510
View File
@@ -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 = '<!-- pr-merge-bot -->';
// 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<details><summary>Your current grants</summary>\n\n${formatList(grants)}\n\n</details>`;
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/<Vendor>/` **and** `resources/profiles/<Vendor>.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}`);
}
+1
View File
@@ -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. - 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. - 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. - Include targeted tests or documented verification for behavior changes, especially in slicing logic, profiles, formats, and GUI defaults.
- For profile changes (`resources/profiles/<Vendor>/**`), check that `version` in the sibling `resources/profiles/<Vendor>.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. - 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 ## Localization & translations
+1 -1
View File
@@ -152,7 +152,7 @@ echo on
set CMAKE_POLICY_VERSION_MINIMUM=3.5 set CMAKE_POLICY_VERSION_MINIMUM=3.5
if "%USE_NINJA%"=="1" ( if "%USE_NINJA%"=="1" (
cmake .. -G %CMAKE_GENERATOR% -DORCA_TOOLS=ON %SIG_FLAG% -DBUILD_TESTS=%BUILD_TESTS% -DCMAKE_BUILD_TYPE=%build_type% 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 --build . --config %build_type% --target all
) else ( ) 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% -DORCA_TOOLS=ON %SIG_FLAG% -DBUILD_TESTS=%BUILD_TESTS% -DCMAKE_BUILD_TYPE=%build_type%
cmake --build . --config %build_type% --target ALL_BUILD -- -m cmake --build . --config %build_type% --target ALL_BUILD -- -m
+98
View File
@@ -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)
+5 -1
View File
@@ -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) set(_patch_command ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/GNU.cmake ./cmake/compilers/GNU.cmake)
else() else()
set(_patch_command "") set(_patch_command "")
@@ -13,6 +15,8 @@ orcaslicer_add_cmake_project(
-DTBB_BUILD_SHARED=OFF -DTBB_BUILD_SHARED=OFF
-DTBB_BUILD_TESTS=OFF -DTBB_BUILD_TESTS=OFF
-DTBB_TEST=OFF -DTBB_TEST=OFF
-DTBB_ENABLE_IPO=OFF
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF
-DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON
-DCMAKE_DEBUG_POSTFIX=_debug -DCMAKE_DEBUG_POSTFIX=_debug
) )
+28
View File
@@ -0,0 +1,28 @@
---
build/cmake/wxWidgetsConfig.cmake.in | 10 +++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/build/cmake/wxWidgetsConfig.cmake.in b/build/cmake/wxWidgetsConfig.cmake.in
index 1a83f36..70ad8a4 100644
--- a/build/cmake/wxWidgetsConfig.cmake.in
+++ b/build/cmake/wxWidgetsConfig.cmake.in
@@ -58,7 +58,16 @@ if(WIN32_MSVC_NAMING)
endif()
endif()
-include("${CMAKE_CURRENT_LIST_DIR}${wxPLATFORM_LIB_DIR}/@PROJECT_NAME@Targets.cmake")
+if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
+ if (CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64" OR CMAKE_VS_PLATFORM_NAME STREQUAL "ARM64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "^(ARM64|arm64|aarch64)$")
+ set(_wx_clang_msvc_lib_dir "vc_arm64_lib")
+ else()
+ set(_wx_clang_msvc_lib_dir "vc_x64_lib")
+ endif()
+ include("${CMAKE_CURRENT_LIST_DIR}${wxPLATFORM_LIB_DIR}/${_wx_clang_msvc_lib_dir}/@PROJECT_NAME@Targets.cmake")
+else()
+ include("${CMAKE_CURRENT_LIST_DIR}${wxPLATFORM_LIB_DIR}/@PROJECT_NAME@Targets.cmake")
+endif()
macro(wx_inherit_property source dest name)
# property name without _<CONFIG>
--
2.43.0
+1
View File
@@ -28,6 +28,7 @@ orcaslicer_add_cmake_project(
GIT_SHALLOW ON GIT_SHALLOW ON
GIT_SUBMODULES 3rdparty/catch 3rdparty/pcre 3rdparty/libwebp GIT_SUBMODULES 3rdparty/catch 3rdparty/pcre 3rdparty/libwebp
DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} ${JPEG_PKG} DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} ${JPEG_PKG}
PATCH_COMMAND git apply --verbose --ignore-space-change --whitespace=fix ${CMAKE_CURRENT_LIST_DIR}/0001-Clang-CL-fix.patch
CMAKE_ARGS CMAKE_ARGS
-DwxBUILD_PRECOMP=ON -DwxBUILD_PRECOMP=ON
${_wx_toolkit} ${_wx_toolkit}
+5 -1
View File
@@ -37,7 +37,11 @@ target_include_directories(Clipper2
) )
if (WIN32) 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() else()
target_compile_options(Clipper2 PRIVATE -Wall -Wextra -Wpedantic -Werror) target_compile_options(Clipper2 PRIVATE -Wall -Wextra -Wpedantic -Werror)
target_link_libraries(Clipper2 PUBLIC -lm) target_link_libraries(Clipper2 PUBLIC -lm)
+2
View File
@@ -11,6 +11,8 @@ add_library(miniz_static STATIC
if(${CMAKE_C_COMPILER_ID} STREQUAL "GNU") if(${CMAKE_C_COMPILER_ID} STREQUAL "GNU")
target_compile_definitions(miniz_static PRIVATE _GNU_SOURCE) 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() endif()
target_link_libraries(miniz INTERFACE miniz_static) target_link_libraries(miniz INTERFACE miniz_static)
+71 -86
View File
@@ -4,7 +4,7 @@ msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-29 17:40-0300\n" "POT-Creation-Date: 2026-07-29 17:40-0300\n"
"PO-Revision-Date: 2026-08-01 20:32+0300\n" "PO-Revision-Date: 2026-08-04 19:36+0300\n"
"Last-Translator: GlauTech\n" "Last-Translator: GlauTech\n"
"Language-Team: \n" "Language-Team: \n"
"Language: tr\n" "Language: tr\n"
@@ -738,9 +738,8 @@ msgstr "Sabit adım sürükleme"
msgid "Context Menu" msgid "Context Menu"
msgstr "Bağlam Menüsü" msgstr "Bağlam Menüsü"
# AI Translated
msgid "Toggle Auto-Drop" msgid "Toggle Auto-Drop"
msgstr "Otomatik Bırakmayı Aç/Kapat" msgstr "Otomatik düşürmeyi aç / kapat"
msgid "Single sided scaling" msgid "Single sided scaling"
msgstr "Tek taraflı ölçekleme" msgstr "Tek taraflı ölçekleme"
@@ -791,9 +790,8 @@ msgstr "Nesne"
msgid "Part" msgid "Part"
msgstr "Parça" msgstr "Parça"
# AI Translated
msgid "Relative" msgid "Relative"
msgstr "Göreli" msgstr "Göreceli"
# AI Translated # AI Translated
msgid "Coordinate system used for transform actions." msgid "Coordinate system used for transform actions."
@@ -2306,7 +2304,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!" msgstr "dilimleme işlemi sırasında yeni veya açık proje dosyasına izin verilmez!"
msgid "Open Project" 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." 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." 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."
@@ -2734,16 +2732,15 @@ msgstr "Simit"
msgid "Orca Cube" msgid "Orca Cube"
msgstr "Orca Küpü" msgstr "Orca Küpü"
# AI Translated
msgid "OrcaSliced Combo" msgid "OrcaSliced Combo"
msgstr "OrcaSliced Combo" msgstr "Orca Dilimleme Paketi"
# AI Translated # AI Translated
msgid "Orca Badge" msgid "Orca Badge"
msgstr "Orca Rozeti" msgstr "Orca Rozeti"
msgid "Orca Tolerance Test" msgid "Orca Tolerance Test"
msgstr "Orca tolerans testi" msgstr "Orca Tolerans Testi"
msgid "3DBenchy" msgid "3DBenchy"
msgstr "3DBenchy" msgstr "3DBenchy"
@@ -2807,7 +2804,7 @@ msgid "Set as Individual Objects"
msgstr "Bireysel nesneler olarak ayarla" msgstr "Bireysel nesneler olarak ayarla"
msgid "Fill bed with copies" msgid "Fill bed with copies"
msgstr "Yatağı kopyalarla doldurun" msgstr "Tablayı kopyalarla doldur"
msgid "Fill the remaining area of bed with copies of the selected object" 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çilen nesnenin kopyalarıyla doldurun"
@@ -2815,9 +2812,8 @@ msgstr "Yatağın kalan alanını seçilen nesnenin kopyalarıyla doldurun"
msgid "Printable" msgid "Printable"
msgstr "Yazdırılabilir" msgstr "Yazdırılabilir"
# AI Translated
msgid "Auto Drop" msgid "Auto Drop"
msgstr "Otomatik Bırakma" msgstr "Otomatik düşür"
# AI Translated # AI Translated
msgid "Automatically drops the selected object to the build plate." msgid "Automatically drops the selected object to the build plate."
@@ -2967,7 +2963,7 @@ msgid "Add Models"
msgstr "Model ekle" msgstr "Model ekle"
msgid "Show Labels" msgid "Show Labels"
msgstr "Etiketleri Göster" msgstr "Etiketleri göster"
msgid "To Objects" msgid "To Objects"
msgstr "Nesnelere" msgstr "Nesnelere"
@@ -3009,7 +3005,7 @@ msgid "Select all objects on the current plate"
msgstr "Mevcut plakadaki tüm nesneleri seç" msgstr "Mevcut plakadaki tüm nesneleri seç"
msgid "Select All Plates" msgid "Select All Plates"
msgstr "Tüm Plakaları Seç" msgstr "Tüm plakaları seç"
msgid "Select all objects on all plates" msgid "Select all objects on all plates"
msgstr "Tüm plakalardaki tüm nesneleri seç" msgstr "Tüm plakalardaki tüm nesneleri seç"
@@ -3045,13 +3041,13 @@ msgid "Remove the selected plate"
msgstr "Seçilen plakayı kaldır" msgstr "Seçilen plakayı kaldır"
msgid "Add instance" msgid "Add instance"
msgstr "Örnek ekle" msgstr "Kopya ekle"
msgid "Add one more instance of the selected object" msgid "Add one more instance of the selected object"
msgstr "Seçilen nesnenin bir örneğini daha ekle" msgstr "Seçilen nesnenin bir örneğini daha ekle"
msgid "Remove instance" msgid "Remove instance"
msgstr "Örneği kaldır" msgstr "Kopyayı kaldır"
msgid "Remove one instance of the selected object" msgid "Remove one instance of the selected object"
msgstr "Seçilen nesnenin bir örneğini kaldır" msgstr "Seçilen nesnenin bir örneğini kaldır"
@@ -3060,10 +3056,10 @@ msgid "Set number of instances"
msgstr "Örnek sayısını ayarlayın" msgstr "Örnek sayısını ayarlayın"
msgid "Change the number of instances of the selected object" msgid "Change the number of instances of the selected object"
msgstr "Seçilen nesnenin örnek sayısını değiştirme" msgstr "Seçilen nesnenin kopya sayısını değiştirme"
msgid "Fill bed with instances" msgid "Fill bed with instances"
msgstr "Yatağı örneklerle doldurun" msgstr "Tablayı kopyalarla doldur"
msgid "Fill the remaining area of bed with instances of the selected object" 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çilen nesnenin örnekleriyle doldurun"
@@ -3075,7 +3071,7 @@ msgid "Simplify Model"
msgstr "Modeli basitleştir" msgstr "Modeli basitleştir"
msgid "Subdivision mesh" msgid "Subdivision mesh"
msgstr "Alt bölüm ağı" msgstr "Poligon artırma"
msgid "(Lost color)" msgid "(Lost color)"
msgstr "(Renk kaybı)" msgstr "(Renk kaybı)"
@@ -3090,10 +3086,10 @@ msgid "Edit Process Settings"
msgstr "İşlem ayarlarını düzenle" msgstr "İşlem ayarlarını düzenle"
msgid "Copy Process Settings" msgid "Copy Process Settings"
msgstr "İşlem Ayarlarını Kopyala" msgstr "İşlem ayarlarını kopyala"
msgid "Paste Process Settings" 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" msgid "Edit print parameters for a single object"
msgstr "Tek bir nesne için yazdırma parametrelerini düzenleme" msgstr "Tek bir nesne için yazdırma parametrelerini düzenleme"
@@ -3478,7 +3474,7 @@ msgid "More"
msgstr "Daha" msgstr "Daha"
msgid "Open Preferences" msgid "Open Preferences"
msgstr "Tercihleri Aç" msgstr "Tercihleri aç"
msgid "Open next tip" msgid "Open next tip"
msgstr "Sonraki ipucunu aç" msgstr "Sonraki ipucunu aç"
@@ -5450,10 +5446,10 @@ msgid "Acceleration"
msgstr "Hızlanma" msgstr "Hızlanma"
msgid "Jerk" msgid "Jerk"
msgstr "Jerk" msgstr "Sarsıntı"
msgid "Fan Speed" msgid "Fan Speed"
msgstr "Fan hızı" msgstr "Fan Hızı"
msgid "Flow" msgid "Flow"
msgstr "Akış" msgstr "Akış"
@@ -5468,7 +5464,7 @@ msgid "Layer Time"
msgstr "Katman Süresi" msgstr "Katman Süresi"
msgid "Layer Time (log)" msgid "Layer Time (log)"
msgstr "Katman Süresi (günlük)" msgstr "Katman Süresi (log)"
msgid "Pressure Advance" msgid "Pressure Advance"
msgstr "Basınç İlerlemesi" msgstr "Basınç İlerlemesi"
@@ -5477,10 +5473,10 @@ msgid "Noop"
msgstr "Hayır" msgstr "Hayır"
msgid "Retract" msgid "Retract"
msgstr "Geri Çekme" msgstr "Geri çekme"
msgid "Unretract" msgid "Unretract"
msgstr "İleri İtme" msgstr "İleri itme"
msgid "Seam" msgid "Seam"
msgstr "Dikiş" msgstr "Dikiş"
@@ -5578,7 +5574,7 @@ msgid "Acceleration: "
msgstr "İvme: " msgstr "İvme: "
msgid "Jerk: " msgid "Jerk: "
msgstr "Jerk: " msgstr "Sarsıntı: "
msgid "PA: " msgid "PA: "
msgstr "PA: " msgstr "PA: "
@@ -5608,7 +5604,7 @@ msgid "Actual speed profile"
msgstr "Gerçek hız profili" msgstr "Gerçek hız profili"
msgid "Statistics of All Plates" msgid "Statistics of All Plates"
msgstr "Tüm Plakaların İstatistikleri" msgstr "Tüm plakaların istatistikleri"
msgid "Display" msgid "Display"
msgstr "Ekran" msgstr "Ekran"
@@ -5708,7 +5704,7 @@ msgid "Acceleration (mm/s²)"
msgstr "İvme (mm/s²)" msgstr "İvme (mm/s²)"
msgid "Jerk (mm/s)" msgid "Jerk (mm/s)"
msgstr "Jerk (mm/s)" msgstr "Sarsıntı (mm/s)"
msgid "Fan speed (%)" msgid "Fan speed (%)"
msgstr "Fan hızı (%)" msgstr "Fan hızı (%)"
@@ -5759,9 +5755,8 @@ msgstr "Normal mod"
msgid "Total Filament" msgid "Total Filament"
msgstr "Toplam filament" msgstr "Toplam filament"
# AI Translated
msgid "Model Filament" msgid "Model Filament"
msgstr "Model Filamenti" msgstr "Model filamenti"
msgid "Prepare time" msgid "Prepare time"
msgstr "Hazırlık süresi" msgstr "Hazırlık süresi"
@@ -6282,20 +6277,19 @@ msgid "Setup Wizard"
msgstr "Kurulum sihirbazı" msgstr "Kurulum sihirbazı"
msgid "Show Configuration Folder" 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" msgid "Troubleshoot Center"
msgstr "Sorun Giderme Merkezi" msgstr "Sorun giderme merkezi"
msgid "Open Network Test" msgid "Open Network Test"
msgstr "Ağ Testini Aç" msgstr "Ağ testini aç"
msgid "Show Tip of the Day" msgid "Show Tip of the Day"
msgstr "Günün İpucunu Göster" msgstr "Günün ipucunu göster"
msgid "Check for Updates" msgid "Check for Updates"
msgstr "Güncellemeleri Kontrol Et" msgstr "Güncellemeleri kontrol et"
#, c-format, boost-format #, c-format, boost-format
msgid "&About %s" msgid "&About %s"
@@ -6349,7 +6343,7 @@ msgid "Recent files"
msgstr "Son dosyalar" msgstr "Son dosyalar"
msgid "Save Project" msgid "Save Project"
msgstr "Projeyi Kaydet" msgstr "Projeyi kaydet"
msgid "Save current project to file" msgid "Save current project to file"
msgstr "Mevcut projeyi dosyaya kaydet" msgstr "Mevcut projeyi dosyaya kaydet"
@@ -6415,13 +6409,13 @@ msgid "Export toolpaths as OBJ"
msgstr "Takımyollarını OBJ olarak dışa aktar" msgstr "Takımyollarını OBJ olarak dışa aktar"
msgid "Export Preset Bundle" msgid "Export Preset Bundle"
msgstr "Ön Ayar Paketini Dışa Aktar" msgstr "Ön ayar paketini dışa aktar"
msgid "Export current configuration to files" msgid "Export current configuration to files"
msgstr "Geçerli yapılandırmayı dosyalara aktar" msgstr "Geçerli yapılandırmayı dosyalara aktar"
msgid "Export" msgid "Export"
msgstr "Dışa Aktar" msgstr "Dışa aktar"
msgid "Quit" msgid "Quit"
msgstr "Çıkış" msgstr "Çıkış"
@@ -6478,13 +6472,13 @@ msgid "Deselects all objects"
msgstr "Tüm nesnelerin seçimini kaldırır" msgstr "Tüm nesnelerin seçimini kaldırır"
msgid "Use Perspective View" msgid "Use Perspective View"
msgstr "Perspektif Görünüm" msgstr "Perspektif görünüm"
msgid "Use Orthogonal View" msgid "Use Orthogonal View"
msgstr "Ortogonal Görünüm" msgstr "Ortogonal görünüm"
msgid "Auto Perspective" msgid "Auto Perspective"
msgstr "Otomatik Perspektif" msgstr "Otomatik perspektif"
msgid "Automatically switch between orthographic and perspective when changing from top/bottom/side views." 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." msgstr "Üst/Alt/Yan görünümler arasında geçiş yaparken ortografik ve perspektif arasında otomatik olarak geçiş yapın."
@@ -6496,37 +6490,37 @@ msgid "Show G-code window in Preview scene."
msgstr "Previce sahnesinde G-kodu penceresini göster." msgstr "Previce sahnesinde G-kodu penceresini göster."
msgid "Show 3D Navigator" msgid "Show 3D Navigator"
msgstr "3D Gezgini Göster" msgstr "3D gezgini göster"
msgid "Show 3D navigator in Prepare and Preview scene." msgid "Show 3D navigator in Prepare and Preview scene."
msgstr "Hazırlama ve Önizleme sahnesinde 3D gezgini göster." msgstr "Hazırlama ve Önizleme sahnesinde 3D gezgini göster."
msgid "Show Gridlines" msgid "Show Gridlines"
msgstr "Kılavuz Çizgilerini Göster" msgstr "Kılavuz çizgilerini göster"
msgid "Show Gridlines on plate" msgid "Show Gridlines on plate"
msgstr "Kılavuz Çizgilerini plaka üzerinde göster" msgstr "Kılavuz Çizgilerini plaka üzerinde göster"
msgid "Reset Window Layout" msgid "Reset Window Layout"
msgstr "Pencere Düzenini Sıfırla" msgstr "Pencere düzenini sıfırla"
msgid "Reset to default window layout" msgid "Reset to default window layout"
msgstr "Varsayılan pencere düzenine sıfırla" msgstr "Varsayılan pencere düzenine sıfırla"
msgid "Show &Labels" msgid "Show &Labels"
msgstr "Etiketleri Göster" msgstr "Etiketleri göster"
msgid "Show object labels in 3D scene." msgid "Show object labels in 3D scene."
msgstr "3B sahnede nesne etiketlerini göster." msgstr "3B sahnede nesne etiketlerini göster."
msgid "Show &Overhang" msgid "Show &Overhang"
msgstr "Çıkıntıyı Göster" msgstr "Çıkıntıyı göster"
msgid "Show object overhang highlight in 3D scene." msgid "Show object overhang highlight in 3D scene."
msgstr "3B sahnede nesne çıkıntısı vurgusunu göster." msgstr "3B sahnede nesne çıkıntısı vurgusunu göster."
msgid "Show Selected Outline (beta)" 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." msgid "Show outline around selected object in 3D scene."
msgstr "3D sahnede seçilen nesnenin etrafındaki ana hatları göster." msgstr "3D sahnede seçilen nesnenin etrafındaki ana hatları göster."
@@ -6542,13 +6536,11 @@ msgstr "Düzen"
msgid "View" msgid "View"
msgstr "Görünüm" msgstr "Görünüm"
# AI Translated
msgid "Preset Bundle" msgid "Preset Bundle"
msgstr "Ön Ayar Paketi" msgstr "Ön ayar paketi"
# AI Translated
msgid "Sync Presets" msgid "Sync Presets"
msgstr "Ön Ayarları Eşitle" msgstr "Ön ayarları eşitle"
# AI Translated # AI Translated
msgid "Pull and apply the latest presets from OrcaCloud" msgid "Pull and apply the latest presets from OrcaCloud"
@@ -6590,10 +6582,10 @@ msgid "Cornering calibration"
msgstr "Viraj kalibrasyonu" msgstr "Viraj kalibrasyonu"
msgid "Input Shaping Frequency" msgid "Input Shaping Frequency"
msgstr "Input shaping Frekansı" msgstr "Input shaping frekansı"
msgid "Input Shaping Damping/zeta factor" 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" msgid "Input Shaping"
msgstr "Input shaping" msgstr "Input shaping"
@@ -6601,9 +6593,8 @@ msgstr "Input shaping"
msgid "VFA" msgid "VFA"
msgstr "VFA" msgstr "VFA"
# AI Translated
msgid "Calibration Guide" msgid "Calibration Guide"
msgstr "Kalibrasyon Kılavuzu" msgstr "Kalibrasyon kılavuzu"
msgid "&Open G-code" msgid "&Open G-code"
msgstr "&G kodunu aç" msgstr "&G kodunu aç"
@@ -8219,9 +8210,8 @@ msgstr "Bu dosyalar birden fazla parçadan oluşan tek bir nesne olarak mı yük
msgid "An object with multiple parts was detected" msgid "An object with multiple parts was detected"
msgstr "Birden fazla parçaya sahip nesne algılandı" msgstr "Birden fazla parçaya sahip nesne algılandı"
# AI Translated
msgid "Auto-Drop" msgid "Auto-Drop"
msgstr "Otomatik Bırakma" msgstr "Otomatik düşür"
#, c-format, boost-format #, c-format, boost-format
msgid "Connected printer is %s. It must match the project preset for printing.\n" msgid "Connected printer is %s. It must match the project preset for printing.\n"
@@ -8296,9 +8286,8 @@ msgstr "Seçilen nesne bölünemedi."
msgid "Split to Objects" msgid "Split to Objects"
msgstr "Nesnelere Ayır" msgstr "Nesnelere Ayır"
# AI Translated
msgid "Disable Auto-Drop to preserve Z positioning?\n" 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 # AI Translated
msgid "Object with floating parts was detected" msgid "Object with floating parts was detected"
@@ -8433,7 +8422,7 @@ msgid "Creating a new project"
msgstr "Yeni bir proje oluşturma" msgstr "Yeni bir proje oluşturma"
msgid "Load project" msgid "Load project"
msgstr "Projeyi Aç" msgstr "Projeyi aç"
msgid "" msgid ""
"Failed to save the project.\n" "Failed to save the project.\n"
@@ -8869,10 +8858,10 @@ msgid "Current Association: "
msgstr "Mevcut Bağlantı: " msgstr "Mevcut Bağlantı: "
msgid "Current Instance" msgid "Current Instance"
msgstr "Mevcut Örnek" msgstr "Mevcut Kopya"
msgid "Current Instance Path: " msgid "Current Instance Path: "
msgstr "Mevcut Örnek Yolu: " msgstr "Mevcut Kopya Yolu: "
msgid "General" msgid "General"
msgstr "Genel" msgstr "Genel"
@@ -10708,7 +10697,7 @@ msgid "Reserved keywords found"
msgstr "Ayrılmış anahtar kelimeler bulundu" msgstr "Ayrılmış anahtar kelimeler bulundu"
msgid "Setting Overrides" msgid "Setting Overrides"
msgstr "Ayarların Üzerine Yazma" msgstr "Ayarların Üzerine Yaz"
msgid "Basic information" msgid "Basic information"
msgstr "Temel Bilgiler" msgstr "Temel Bilgiler"
@@ -12227,7 +12216,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." msgstr "Elle modda gruplama hatası. Lütfen nozul sayısını denetleyin veya yeniden gruplayın."
msgid "Internal Bridge" msgid "Internal Bridge"
msgstr "İç Köprü" msgstr "İç köprü"
msgid "undefined error" msgid "undefined error"
msgstr "bilinmeyen hata" msgstr "bilinmeyen hata"
@@ -12920,7 +12909,6 @@ msgstr "Çıkıntı bu belirtilen eşiği aştığında, soğutma fanını aşa
msgid "External bridge infill direction" msgid "External bridge infill direction"
msgstr "Dış köprü dolgu yönü" msgstr "Dış köprü dolgu yönü"
# AI Translated
#, no-c-format, no-boost-format #, no-c-format, no-boost-format
msgid "" msgid ""
"External Bridging angle override.\n" "External Bridging angle override.\n"
@@ -12937,14 +12925,13 @@ msgstr ""
"Aksi hâlde verilen açı şuna göre kullanılır:\n" "Aksi hâlde verilen açı şuna göre kullanılır:\n"
" - Mutlak koordinatlar\n" " - Mutlak koordinatlar\n"
" - Mutlak koordinatlar + Model dönüşü: Yönleri modele hizala etkinse\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" "\n"
"Sıfır mutlak açı için 180° kullanın." "Sıfır mutlak açı için 180° kullanın."
msgid "Internal bridge infill direction" msgid "Internal bridge infill direction"
msgstr "İç köprü dolgu yönü" msgstr "İç köprü dolgu yönü"
# AI Translated
msgid "" msgid ""
"Internal Bridging angle override.\n" "Internal Bridging angle override.\n"
"If left to zero, the bridging angle will be calculated automatically for each specific bridge.\n" "If left to zero, the bridging angle will be calculated automatically for each specific bridge.\n"
@@ -12960,13 +12947,12 @@ msgstr ""
"Aksi hâlde verilen açı şuna göre kullanılır:\n" "Aksi hâlde verilen açı şuna göre kullanılır:\n"
" - Mutlak koordinatlar\n" " - Mutlak koordinatlar\n"
" - Mutlak koordinatlar + Model dönüşü: Yönleri modele hizala etkinse\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" "\n"
"Sıfır mutlak açı için 180° kullanın." "Sıfır mutlak açı için 180° kullanın."
# AI Translated
msgid "Relative bridge angle" msgid "Relative bridge angle"
msgstr "Göreli köprü açısı" msgstr "Göreceli köprü açısı"
# AI Translated # AI Translated
msgid "When enabled, the bridge angle values are added to the automatically calculated bridge direction instead of overriding it." msgid "When enabled, the bridge angle values are added to the automatically calculated bridge direction instead of overriding it."
@@ -13413,7 +13399,7 @@ msgstr ""
"Not: Elde edilen değer ilk katman akış oranından etkilenmez." "Not: Elde edilen değer ilk katman akış oranından etkilenmez."
msgid "Brim follows compensated outline" msgid "Brim follows compensated outline"
msgstr "Kenar telafi edilen taslağı takip ediyor" msgstr "Kenar toleranslı dış sınırı takip etsin"
# AI Translated # AI Translated
msgid "" msgid ""
@@ -13724,13 +13710,13 @@ msgid ""
msgstr "" 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" "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" "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" "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" "Hepsi: Tüm uygun eğimli yüzeyler için katı dolgu ekleyin\n"
"Varsayılan değer Tümü'dür." "Varsayılan değer Tümü'dür."
msgid "Critical Only" msgid "Critical Only"
msgstr "Yalnızca kritik" msgstr "Kritik"
msgid "Moderate" msgid "Moderate"
msgstr "Orta" msgstr "Orta"
@@ -14247,7 +14233,7 @@ msgid "By First filament"
msgstr "İlk filamente göre" msgstr "İlk filamente göre"
msgid "By Highest Temp" 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." 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ı, gcode'da ekstrüzyonu hesaplamak için kullanılır; bu nedenle önemlidir ve doğru olmalıdır."
@@ -14685,10 +14671,10 @@ 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)" msgstr "Marlin Firmware Köşe Sapması (geleneksel XY Sarsıntı ayarının yerini alır)"
msgid "Jerk of outer walls." msgid "Jerk of outer walls."
msgstr "Dış duvar JERK değeri." msgstr "Dış duvar sarsıntı değeri."
msgid "Jerk of inner walls." msgid "Jerk of inner walls."
msgstr "İç duvarlar JERK değeri." msgstr "İç duvarlar sarsıntı değeri."
msgid "Jerk for top surface." msgid "Jerk for top surface."
msgstr "Üst yüzey için JERK değeri." msgstr "Üst yüzey için JERK değeri."
@@ -15721,7 +15707,7 @@ msgid ""
"If your Marlin 2 printer uses Classic Jerk set this value to 0.)" "If your Marlin 2 printer uses Classic Jerk set this value to 0.)"
msgstr "" msgstr ""
"Maksimum bağlantı sapması (M205 J, yalnızca Marlin Aygıt Yazılımı için JD > 0 ise geçerlidir)\n" "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" msgid "Minimum speed for extruding"
msgstr "Ekstrüzyon için minimum hız" msgstr "Ekstrüzyon için minimum hız"
@@ -16914,7 +16900,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." msgstr "Model yüzeyinde destek oluşturmayın, yalnızca baskı plakasında."
msgid "Support critical regions only" 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." 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." msgstr "Yalnızca keskin kuyruk, konsol vb. gibi kritik bölgeler için destek oluşturun."
@@ -19288,13 +19274,13 @@ msgid ""
"To test Classic Jerk, set 'Maximum Junction Deviation' in Motion ability to 0." "To test Classic Jerk, set 'Maximum Junction Deviation' in Motion ability to 0."
msgstr "" msgstr ""
"Marlin 2 Kavşak Sapması tespit edildi:\n" "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 "" msgid ""
"Marlin 2 Classic Jerk detected:\n" "Marlin 2 Classic Jerk detected:\n"
"To test Junction Deviation, set 'Maximum Junction Deviation' in Motion ability to a value > 0." "To test Junction Deviation, set 'Maximum Junction Deviation' in Motion ability to a value > 0."
msgstr "" 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." "Kavşak Sapmasını test etmek için Hareket yeteneğinde 'Maksimum Kavşak Sapması'nı > 0 değerine ayarlayın."
msgid "" msgid ""
@@ -19538,7 +19524,7 @@ msgid "Start Test Single-Thread"
msgstr "Tek İş Parçacığı Testini Başlat" msgstr "Tek İş Parçacığı Testini Başlat"
msgid "Export Log" msgid "Export Log"
msgstr "Logu Dışa Aktar" msgstr "Logu dışa aktar"
msgid "OrcaSlicer Version:" msgid "OrcaSlicer Version:"
msgstr "OrcaSlicer Sürümü:" msgstr "OrcaSlicer Sürümü:"
@@ -20281,9 +20267,8 @@ msgstr "Sistem klasörü silinemedi..."
msgid "Failed to determine executable path." msgid "Failed to determine executable path."
msgstr "Yürütülebilir dosya yolu belirlenemedi." msgstr "Yürütülebilir dosya yolu belirlenemedi."
# AI Translated
msgid "Failed to launch a new instance." msgid "Failed to launch a new instance."
msgstr "Yeni bir örnek başlatılamadı." msgstr "Yeni bir kopya başlatılamadı."
# AI Translated # AI Translated
msgid "log(s)" msgid "log(s)"
+21 -8
View File
@@ -256,14 +256,6 @@ if (WIN32)
VERBATIM VERBATIM
) )
endforeach () 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 () else ()
file(TO_NATIVE_PATH "${CMAKE_CURRENT_BINARY_DIR}/resources" WIN_RESOURCES_SYMLINK) file(TO_NATIVE_PATH "${CMAKE_CURRENT_BINARY_DIR}/resources" WIN_RESOURCES_SYMLINK)
add_custom_command(TARGET OrcaSlicer POST_BUILD add_custom_command(TARGET OrcaSlicer POST_BUILD
@@ -279,6 +271,27 @@ if (WIN32)
COMMENT "Copying Python runtime into the build tree" COMMENT "Copying Python runtime into the build tree"
VERBATIM) 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 () else ()
if (APPLE AND NOT CMAKE_MACOSX_BUNDLE) if (APPLE AND NOT CMAKE_MACOSX_BUNDLE)
+1 -1
View File
@@ -69,7 +69,7 @@ void CBaseException::OutputString(LPCTSTR lpszFormat, ...)
//WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), szBuf, _tcslen(szBuf), NULL, NULL); //WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), szBuf, _tcslen(szBuf), NULL, NULL);
//output it to the current directory of binary //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 << output_str;
output_file->flush(); output_file->flush();
} }
+19
View File
@@ -280,6 +280,9 @@ void AppConfig::set_defaults()
set(SETTING_OPENGL_FPS_CAP, std::to_string(fps_cap)); 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()) if (get(SETTING_OPENGL_SHOW_FPS_OVERLAY).empty())
set_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY, false); set_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY, false);
@@ -1630,6 +1633,22 @@ void AppConfig::set_network_plugin_version(const std::string& version)
set(SETTING_NETWORK_PLUGIN_VERSION, 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> AppConfig::get_skipped_network_versions() const
{ {
std::vector<std::string> result; std::vector<std::string> result;
+9
View File
@@ -41,6 +41,11 @@ using namespace nlohmann;
#define SETTING_OPENGL_PHONG_SSAO "opengl_phong_ssao" #define SETTING_OPENGL_PHONG_SSAO "opengl_phong_ssao"
#define SETTING_OPENGL_PHONG_SMOOTH_NORMALS "opengl_phong_smooth_normals" #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) #if defined(_WIN32) || defined(_WIN64)
#define BAMBU_NETWORK_AGENT_VERSION_LEGACY "01.10.01.09" #define BAMBU_NETWORK_AGENT_VERSION_LEGACY "01.10.01.09"
#else #else
@@ -374,6 +379,10 @@ public:
std::string get_network_plugin_version() const; std::string get_network_plugin_version() const;
void set_network_plugin_version(const std::string& version); 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; std::vector<std::string> get_skipped_network_versions() const;
void add_skipped_network_version(const std::string& version); void add_skipped_network_version(const std::string& version);
bool is_network_version_skipped(const std::string& version) const; bool is_network_version_skipped(const std::string& version) const;
+2
View File
@@ -8,6 +8,8 @@
namespace Slic3r { namespace Slic3r {
template BoundingBoxBase<Point, Points>::BoundingBoxBase(const Points &points); 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 BoundingBoxBase<Vec2d>::BoundingBoxBase(const std::vector<Vec2d> &points);
template BoundingBox3Base<Vec3d>::BoundingBox3Base(const std::vector<Vec3d> &points); template BoundingBox3Base<Vec3d>::BoundingBox3Base(const std::vector<Vec3d> &points);
+2
View File
@@ -149,6 +149,8 @@ set(lisbslic3r_sources
Fill/FillConcentric.hpp Fill/FillConcentric.hpp
Fill/FillConcentricInternal.cpp Fill/FillConcentricInternal.cpp
Fill/FillConcentricInternal.hpp Fill/FillConcentricInternal.hpp
Fill/FillCornerSmoothing.cpp
Fill/FillCornerSmoothing.hpp
Fill/Fill.cpp Fill/Fill.cpp
Fill/FillCrossHatch.cpp Fill/FillCrossHatch.cpp
Fill/FillCrossHatch.hpp Fill/FillCrossHatch.hpp
+3 -3
View File
@@ -970,9 +970,9 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
region_config.sparse_infill_rotate_template.value); region_config.sparse_infill_rotate_template.value);
params.fixed_angle = !region_config.sparse_infill_rotate_template.value.empty(); params.fixed_angle = !region_config.sparse_infill_rotate_template.value.empty();
// Orca: special case; apply smoothing factor only for Hilbert Curve sparse infill. // Orca: the smoothing factor only applies to the sparse infill patterns that
// FillHilbertCurve::generate clamps and validates the value itself. // implement it. The fills clamp and validate the value themselves.
if (params.pattern == ipHilbertCurve) if (is_smoothable_infill_pattern(params.pattern, params.multiline))
params.smooth_factor = 0.01 * region_config.sparse_infill_smooth_factor.value; params.smooth_factor = 0.01 * region_config.sparse_infill_smooth_factor.value;
} else { } else {
const bool top_layer_direction_set = surface.is_top() && region_config.top_layer_direction.value >= 0.; const bool top_layer_direction_set = surface.is_top() && region_config.top_layer_direction.value >= 0.;
+4
View File
@@ -2,6 +2,7 @@
#include "../ShortestPath.hpp" #include "../ShortestPath.hpp"
#include "../Surface.hpp" #include "../Surface.hpp"
#include "FillBase.hpp" #include "FillBase.hpp"
#include "FillCornerSmoothing.hpp"
#include "Fill3DHoneycomb.hpp" #include "Fill3DHoneycomb.hpp"
namespace Slic3r { namespace Slic3r {
@@ -271,6 +272,9 @@ void Fill3DHoneycomb::_fill_surface_single(
for (Polyline &pl : polylines){ for (Polyline &pl : polylines){
pl.translate(bb.min); pl.translate(bb.min);
pl.simplify(5 * spacing); // simplify to 5x line width 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 // Apply multiline offset if needed
+22 -1
View File
@@ -5,6 +5,7 @@
#include "Arachne/WallToolPaths.hpp" #include "Arachne/WallToolPaths.hpp"
#include "FillConcentric.hpp" #include "FillConcentric.hpp"
#include "FillCornerSmoothing.hpp"
#include <libslic3r/ShortestPath.hpp> #include <libslic3r/ShortestPath.hpp>
namespace Slic3r { namespace Slic3r {
@@ -32,12 +33,32 @@ void FillConcentric::_fill_surface_single(
Polygons loops = to_polygons(contracted); Polygons loops = to_polygons(contracted);
ExPolygons last { std::move(contracted) }; ExPolygons last { contracted };
while (! last.empty()) { while (! last.empty()) {
last = offset2_ex(last, -(distance + min_spacing/2), +min_spacing/2); last = offset2_ex(last, -(distance + min_spacing/2), +min_spacing/2);
append(loops, to_polygons(last)); 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 &region) { 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 // generate paths from the outermost to the innermost, to avoid
// adhesion problems of the first central tiny loops // adhesion problems of the first central tiny loops
loops = union_pt_chained_outside_in(loops); loops = union_pt_chained_outside_in(loops);
+226
View File
@@ -0,0 +1,226 @@
#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;
}
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
+108
View File
@@ -0,0 +1,108 @@
#pragma once
#include <algorithm>
#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_pending == 0) {
emit(point);
m_previous = point;
} else if (m_pending > 1) {
round_corner(m_previous, m_corner, point);
for (const Vec2d &corner_point : m_corner_points)
emit(corner_point);
m_previous = m_corner;
}
m_corner = point;
m_pending = std::min(m_pending + 1, 2);
}
// Emits the last point of the path and prepares the smoother for a new one.
template<typename Emit> void flush(Emit &emit)
{
if (m_pending > 1)
emit(m_corner);
m_pending = 0;
}
private:
// 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 segment consumed on each side of a corner. Half of a segment
// 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 };
Vec2d m_previous { Vec2d::Zero() };
Vec2d m_corner { Vec2d::Zero() };
// Number of points held back: none, the first point of a path, or a corner candidate.
int m_pending { 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
+4
View File
@@ -3,6 +3,7 @@
#include "../Surface.hpp" #include "../Surface.hpp"
#include <cmath> #include <cmath>
#include "FillBase.hpp" #include "FillBase.hpp"
#include "FillCornerSmoothing.hpp"
#include "FillCrossHatch.hpp" #include "FillCrossHatch.hpp"
namespace Slic3r { namespace Slic3r {
@@ -205,6 +206,9 @@ void FillCrossHatch ::_fill_surface_single(
// shift the pattern to the actual space // shift the pattern to the actual space
for (Polyline &pl : polylines) { pl.translate(bb.min); } 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 // Apply multiline offset if needed
multiline_fill(polylines, params, spacing); multiline_fill(polylines, params, spacing);
+4
View File
@@ -2,6 +2,7 @@
#include "../ShortestPath.hpp" #include "../ShortestPath.hpp"
#include "../Surface.hpp" #include "../Surface.hpp"
#include "FillCornerSmoothing.hpp"
#include "FillHoneycomb.hpp" #include "FillHoneycomb.hpp"
namespace Slic3r { namespace Slic3r {
@@ -70,6 +71,9 @@ void FillHoneycomb::_fill_surface_single(
} }
p.rotate(-direction.first, m.hex_center); p.rotate(-direction.first, m.hex_center);
p.simplify(5 * spacing); // simplify to 5x line width 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); all_polylines.push_back(p);
} }
} }
+14
View File
@@ -2,6 +2,7 @@
#include "../Print.hpp" #include "../Print.hpp"
#include "../ShortestPath.hpp" #include "../ShortestPath.hpp"
#include "FillBase.hpp" #include "FillBase.hpp"
#include "FillCornerSmoothing.hpp"
#include "FillLightning.hpp" #include "FillLightning.hpp"
#include "Lightning/Generator.hpp" #include "Lightning/Generator.hpp"
@@ -17,6 +18,19 @@ void Filler::_fill_surface_single(
const Layer &layer = generator->getTreesForLayer(this->layer_id); 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)); 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 // Apply multiline offset if needed
multiline_fill(fill_lines, params, spacing); multiline_fill(fill_lines, params, spacing);
+59 -147
View File
@@ -2,6 +2,7 @@
#include "../ShortestPath.hpp" #include "../ShortestPath.hpp"
#include "../Surface.hpp" #include "../Surface.hpp"
#include "FillCornerSmoothing.hpp"
#include "FillPlanePath.hpp" #include "FillPlanePath.hpp"
namespace Slic3r { 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>; // Rounds the corners of the generated path on its way to the infill output.
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());
}
template<typename Output> template<typename Output>
static void generate_smooth_hilbert_curve( class SmoothingPolylineOutput
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)
{ {
// A Hilbert curve is defined on a square grid whose side is a power of two. As in the unsmoothed public:
// generator, expand the larger requested dimension to the next valid Hilbert grid size. The output SmoothingPolylineOutput(Output &output, const double smooth_factor, const double tolerance)
// clipper or the later region intersection removes the padded part of the traversal. : m_output(output), m_smoother(smooth_factor, tolerance) {}
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;
const size_t point_count = sz * sz; void reserve(size_t n) { m_output.reserve(n); }
output.reserve(point_count); 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 private:
// if this helper is invoked with an invalid resolution. // The curves of two adjacent corners meet at the midpoint of the segment they share, where they
const double deviation = resolution > 0. && std::isfinite(resolution) ? resolution : EPSILON; // may round to the very same output point. Drop those, they would be zero length extrusions.
// Construct one canonical 90-degree corner from (-corner_distance, 0) to (0, corner_distance). auto emitter()
// 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 return [this](const Vec2d &pt) {
// zero, giving G2 joins to both legs. Every Hilbert turn is an oriented copy of this curve, so flatten const Point snapped = m_output.scaled(pt);
// it only once to the requested chordal-deviation tolerance. if (m_has_last_snapped && snapped == m_last_snapped)
const QuinticBezier corner_curve {{ return;
{-corner_distance, 0.}, {-0.7 * corner_distance, 0.}, {-0.4 * corner_distance, 0.}, m_last_snapped = snapped;
{0., 0.4 * corner_distance}, {0., 0.7 * corner_distance}, {0., corner_distance} m_has_last_snapped = true;
}}; m_output.add_point(pt);
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;
} }
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 &params, 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) 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, void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
const FillParams &params, InfillPolylineOutput &output) const FillParams &params, InfillPolylineOutput &output)
{ {
const double smooth_factor = std::isfinite(params.smooth_factor) ? generate_path(output, params, resolution,
std::clamp(params.smooth_factor, 0., 1.) : 0.; [min_x, min_y, max_x, max_y](auto &out) { generate_hilbert_curve(min_x, min_y, max_x, max_y, out); });
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);
} }
template<typename Output> 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); 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 &params, 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 } // namespace Slic3r
+4 -2
View File
@@ -21,10 +21,10 @@ public:
void add_point(const Vec2d& pt) { m_out.emplace_back(this->scaled(pt)); } void add_point(const Vec2d& pt) { m_out.emplace_back(this->scaled(pt)); }
Points&& result() { return std::move(m_out); } Points&& result() { return std::move(m_out); }
virtual bool clips() const { return false; } virtual bool clips() const { return false; }
// The output grid the generated points are snapped to.
protected:
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)) }; } 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. // Output polyline.
Points m_out; Points m_out;
@@ -93,6 +93,8 @@ public:
protected: protected:
bool centered() const override { return true; } 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, 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 &params, InfillPolylineOutput &output) override;
}; };
} // namespace Slic3r } // namespace Slic3r
+5
View File
@@ -18,6 +18,7 @@
#include "../ShortestPath.hpp" #include "../ShortestPath.hpp"
#include "../VariableWidth.hpp" #include "../VariableWidth.hpp"
#include "FillCornerSmoothing.hpp"
#include "FillRectilinear.hpp" #include "FillRectilinear.hpp"
// #define SLIC3R_DEBUG // #define SLIC3R_DEBUG
@@ -3364,6 +3365,10 @@ bool FillRectilinear::fill_surface_trapezoidal(
for (Polyline &pl : polylines) for (Polyline &pl : polylines)
pl.translate(rotate_vector.second); 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 // Apply multiline fill
multiline_fill(polylines, params, spacing); multiline_fill(polylines, params, spacing);
-1
View File
@@ -49,7 +49,6 @@ static std::vector<std::string> s_project_options {
"filament_multi_colour", "filament_multi_colour",
"wipe_tower_x", "wipe_tower_x",
"wipe_tower_y", "wipe_tower_y",
"wipe_tower_rotation_angle",
"curr_bed_type", "curr_bed_type",
"flush_multiplier", "flush_multiplier",
// Fast-purge mode: project-level purge control, inert at Default. // Fast-purge mode: project-level purge control, inert at Default.
+2 -3
View File
@@ -3469,9 +3469,8 @@ void PrintConfigDef::init_fff_params()
def = this->add("sparse_infill_smooth_factor", coPercent); def = this->add("sparse_infill_smooth_factor", coPercent);
def->label = L("Sparse infill smooth factor"); def->label = L("Sparse infill smooth factor");
def->category = L("Strength"); def->category = L("Strength");
def->tooltip = L("Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, " 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. " "while 100% produces the largest possible curves between adjacent infill lines.");
"Currently applies only to the Hilbert Curve.");
def->sidetext = "%"; def->sidetext = "%";
def->min = 0; def->min = 0;
def->max = 100; def->max = 100;
+23
View File
@@ -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 { enum class IroningType {
NoIroning, NoIroning,
TopSurfaces, TopSurfaces,
+5
View File
@@ -620,6 +620,8 @@ set(SLIC3R_GUI_SOURCES
plugin/host/PluginHostSlicing.cpp plugin/host/PluginHostSlicing.cpp
plugin/host/PluginHostUi.cpp plugin/host/PluginHostUi.cpp
plugin/host/PluginHostUi.hpp plugin/host/PluginHostUi.hpp
plugin/host/PluginPages.cpp
plugin/host/PluginPages.hpp
plugin/CloudPluginService.cpp plugin/CloudPluginService.cpp
plugin/CloudPluginService.hpp plugin/CloudPluginService.hpp
plugin/PluginFsUtils.cpp plugin/PluginFsUtils.cpp
@@ -640,6 +642,9 @@ set(SLIC3R_GUI_SOURCES
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp 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.hpp
plugin/pluginTypes/script/ScriptPluginCapability.cpp plugin/pluginTypes/script/ScriptPluginCapability.cpp
plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp
+5 -5
View File
@@ -869,11 +869,11 @@ void AuxiliaryPanel::init_tabpanel()
m_assembly_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::ASSEMBLY_GUIDE); m_assembly_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::ASSEMBLY_GUIDE);
m_others_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::OTHERS); m_others_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::OTHERS);
m_tabpanel->AddPage(m_designer_panel, _L("Basic Info"), "", true); m_tabpanel->AddPage(m_designer_panel, _L("Basic Info"), true);
m_tabpanel->AddPage(m_pictures_panel, _L("Pictures"), "", false); 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_bill_of_materials_panel, _L("Bill of Materials"), false);
m_tabpanel->AddPage(m_assembly_panel, _L("Assembly Guide"), "", false); m_tabpanel->AddPage(m_assembly_panel, _L("Assembly Guide"), false);
m_tabpanel->AddPage(m_others_panel, _L("Others"), "", false); m_tabpanel->AddPage(m_others_panel, _L("Others"), false);
} }
wxWindow *AuxiliaryPanel::create_side_tools() wxWindow *AuxiliaryPanel::create_side_tools()
-1
View File
@@ -488,7 +488,6 @@ void CalibrationPanel::init_tabpanel() {
selected = true; selected = true;
m_tabpanel->AddPage(m_cali_panels[i], m_tabpanel->AddPage(m_cali_panels[i],
get_calibration_type_name(m_cali_panels[i]->get_calibration_mode()), get_calibration_type_name(m_cali_panels[i]->get_calibration_mode()),
"",
selected); selected);
} }
+1 -1
View File
@@ -752,7 +752,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_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_bottom_shell = config->opt_int("bottom_shell_layers") > 0;
bool has_solid_infill = has_top_shell_layers || has_bottom_shell; 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("top_surface_pattern", has_top_shell);
toggle_field("bottom_surface_pattern", has_bottom_shell); toggle_field("bottom_surface_pattern", has_bottom_shell);
toggle_field("top_surface_density", has_top_shell_layers); toggle_field("top_surface_density", has_top_shell_layers);
+1 -1
View File
@@ -134,7 +134,7 @@ void Downloader::start_download(const std::string& full_url)
Plater* plater = wxGetApp().plater(); Plater* plater = wxGetApp().plater();
mainframe->Freeze(); mainframe->Freeze();
mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor); mainframe->select_tab(TAB_ID_PREPARE);
plater->select_view_3D("3D"); plater->select_view_3D("3D");
plater->select_view("plate"); plater->select_view("plate");
plater->get_current_canvas3D()->zoom_to_bed(); plater->get_current_canvas3D()->zoom_to_bed();
+4 -2
View File
@@ -331,8 +331,10 @@ void Field::PostInitialize()
} }
default: break; default: break;
} }
if (tab_id >= 0) if (tab_id >= 0) {
wxGetApp().mainframe->select_tab(tab_id); 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) if (tab_id > 0)
// tab panel should be focused for correct navigation between tabs // tab panel should be focused for correct navigation between tabs
wxGetApp().tab_panel()->SetFocus(); wxGetApp().tab_panel()->SetFocus();
+2 -2
View File
@@ -2887,7 +2887,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 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 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 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 // BBS
float v = dynamic_cast<const ConfigOptionFloat*>(m_config->option("prime_volume"))->value; float v = dynamic_cast<const ConfigOptionFloat*>(m_config->option("prime_volume"))->value;
Vec3d plate_origin = ppl.get_plate(plate_id)->get_origin(); Vec3d plate_origin = ppl.get_plate(plate_id)->get_origin();
@@ -9196,7 +9196,7 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
view3d_canvas->get_gizmos_manager().reset_all_states(); // close all gizmos view3d_canvas->get_gizmos_manager().reset_all_states(); // close all gizmos
view3d_canvas->reload_scene(true); view3d_canvas->reload_scene(true);
} }
app.mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor); app.mainframe->select_tab(TAB_ID_PREPARE);
} }
} }
}); });
+31 -17
View File
@@ -813,12 +813,12 @@ void GUI_App::post_init()
m_open_method = "url"; m_open_method = "url";
} else { } else {
if (this->init_params->input_gcode) { if (this->init_params->input_gcode) {
mainframe->select_tab(size_t(MainFrame::tp3DEditor)); mainframe->select_tab(TAB_ID_PREPARE);
plater_->select_view_3D("3D"); plater_->select_view_3D("3D");
this->plater()->load_gcode(from_u8(this->init_params->input_files.front())); this->plater()->load_gcode(from_u8(this->init_params->input_files.front()));
m_open_method = "gcode"; m_open_method = "gcode";
} else { } else {
mainframe->select_tab(size_t(MainFrame::tp3DEditor)); mainframe->select_tab(TAB_ID_PREPARE);
plater_->select_view_3D("3D"); plater_->select_view_3D("3D");
wxArrayString input_files; wxArrayString input_files;
for (auto& file : this->init_params->input_files) { for (auto& file : this->init_params->input_files) {
@@ -852,7 +852,7 @@ void GUI_App::post_init()
mainframe->Freeze(); mainframe->Freeze();
#endif #endif
plater_->canvas3D()->enable_render(false); plater_->canvas3D()->enable_render(false);
mainframe->select_tab(size_t(MainFrame::tp3DEditor)); mainframe->select_tab(TAB_ID_PREPARE);
plater_->select_view_3D("3D"); plater_->select_view_3D("3D");
//BBS init the opengl resource here //BBS init the opengl resource here
if (!plater_->canvas3D()->get_wxglcanvas()->IsShownOnScreen() || if (!plater_->canvas3D()->get_wxglcanvas()->IsShownOnScreen() ||
@@ -890,9 +890,9 @@ void GUI_App::post_init()
} }
} }
if (is_editor()) if (is_editor())
mainframe->select_tab(size_t(0)); mainframe->select_tab(TAB_ID_HOME);
if (app_config->get("default_page") == "1") if (app_config->get("default_page") == "1")
mainframe->select_tab(size_t(1)); mainframe->select_tab(TAB_ID_PREPARE);
#ifndef __linux__ #ifndef __linux__
mainframe->Thaw(); mainframe->Thaw();
#endif #endif
@@ -1829,10 +1829,10 @@ bool GUI_App::hot_reload_network_plugin()
wxWindowDisabler disabler; wxWindowDisabler disabler;
if (mainframe) { if (mainframe) {
int current_tab = mainframe->m_tabpanel->GetSelection(); wxString current_tab = mainframe->m_tabpanel->GetSelectedPageName();
if (current_tab == MainFrame::TabPosition::tpMonitor) { if (current_tab == TAB_ID_MONITOR) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": navigating away from Monitor tab before unload"; 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);
} }
} }
@@ -2851,6 +2851,16 @@ void GUI_App::init_plugin_gui_wiring()
plugin_mgr.subscribe_on_unload_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_load_callback(NetworkAgentFactory::register_python_plugin);
plugin_mgr.subscribe_on_unload_callback(NetworkAgentFactory::deregister_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_load_callback(refresh_printer_agent_dropdown_after_load);
plugin_mgr.subscribe_on_unload_callback(switch_printer_agent_after_unload); plugin_mgr.subscribe_on_unload_callback(switch_printer_agent_after_unload);
plugin_mgr.subscribe_on_capability_load_callback( plugin_mgr.subscribe_on_capability_load_callback(
@@ -2866,11 +2876,15 @@ void GUI_App::init_plugin_gui_wiring()
if (Plater* plater = wxGetApp().plater()) if (Plater* plater = wxGetApp().plater())
plater->revalidate_current_plate_if_plugins_missing(); 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( plugin_mgr.subscribe_on_capability_unload_callback(
[refresh_plugins_dialog, switch_printer_agent_after_unload](const PluginCapabilityId& capability) { [refresh_plugins_dialog, switch_printer_agent_after_unload](const PluginCapabilityId& capability) {
if (capability.type == PluginCapabilityType::PrinterConnection) if (capability.type == PluginCapabilityType::PrinterConnection)
NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name); 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(); refresh_plugins_dialog();
switch_printer_agent_after_unload(capability.plugin_key); switch_printer_agent_after_unload(capability.plugin_key);
}); });
@@ -3273,15 +3287,12 @@ bool GUI_App::on_init_inner()
} }
} */ } */
copy_network_if_available();
if (scrn) { if (scrn) {
scrn->SetText(_L("Loading Plugins") + dots, 20); scrn->SetText(_L("Loading Plugins") + dots, 20);
wxYield(); wxYield();
} }
on_init_network();
// Initialize plugins after network then register on_load callbacks so once the plugin loads finish, it gets registered automatically. // 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, // initialize() also installs the libslic3r hooks (capability resolver,
// slicing-pipeline dispatcher) via plugin_hooks::install() -- no // slicing-pipeline dispatcher) via plugin_hooks::install() -- no
@@ -3310,6 +3321,9 @@ bool GUI_App::on_init_inner()
} }
} }
copy_network_if_available();
on_init_network();
if (m_agent) if (m_agent)
plugin_mgr.set_cloud_agent(std::dynamic_pointer_cast<OrcaCloudServiceAgent>(m_agent->get_cloud_agent())); plugin_mgr.set_cloud_agent(std::dynamic_pointer_cast<OrcaCloudServiceAgent>(m_agent->get_cloud_agent()));
@@ -3382,7 +3396,7 @@ bool GUI_App::on_init_inner()
mainframe = new MainFrame(); mainframe = new MainFrame();
// hide settings tabs after first Layout // hide settings tabs after first Layout
if (is_editor()) { if (is_editor()) {
mainframe->select_tab(size_t(0)); mainframe->select_tab(TAB_ID_HOME);
} }
sidebar().obj_list()->init(); sidebar().obj_list()->init();
@@ -4590,7 +4604,7 @@ void GUI_App::recreate_GUI(const wxString &msg_name)
mainframe = new MainFrame(); mainframe = new MainFrame();
if (is_editor()) if (is_editor())
// hide settings tabs after first Layout // 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. // Propagate model objects to object list.
sidebar().obj_list()->init(); sidebar().obj_list()->init();
//sidebar().aux_list()->init_auxiliary(); //sidebar().aux_list()->init_auxiliary();
@@ -9849,7 +9863,7 @@ bool GUI_App::check_url_association(std::wstring url_prefix, std::wstring& reg_b
{ {
reg_bin = L""; reg_bin = L"";
#ifdef WIN32 #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()) { if (!key_full.Exists()) {
return false; return false;
} }
@@ -9875,8 +9889,8 @@ void GUI_App::associate_url(std::wstring url_prefix)
wxString key_string = "\"" + wbinary + "\" \"%1\""; wxString key_string = "\"" + wbinary + "\" \"%1\"";
wxRegKey key_first(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix); wxRegKey key_first(wxRegKey::HKCU, L"Software\\Classes\\" + url_prefix);
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_first.Exists()) { if (!key_first.Exists()) {
key_first.Create(false); key_first.Create(false);
} }
@@ -9896,7 +9910,7 @@ void GUI_App::disassociate_url(std::wstring url_prefix)
#ifdef WIN32 #ifdef WIN32
if (is_running_in_msix()) if (is_running_in_msix())
return; 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()) { if (!key_full.Exists()) {
return; return;
} }
+1
View File
@@ -1,6 +1,7 @@
#include "HMS.hpp" #include "HMS.hpp"
#include "GUI.hpp" #include "GUI.hpp"
#include "GUI_App.hpp"
#include "DeviceManager.hpp" #include "DeviceManager.hpp"
#include "DeviceCore/DevManager.h" #include "DeviceCore/DevManager.h"
#include "DeviceCore/DevUtil.h" #include "DeviceCore/DevUtil.h"
+16 -13
View File
@@ -1,7 +1,6 @@
#ifndef slic3r_HMS_hpp_ #ifndef slic3r_HMS_hpp_
#define slic3r_HMS_hpp_ #define slic3r_HMS_hpp_
#include "GUI_App.hpp"
#include "GUI.hpp" #include "GUI.hpp"
#include "I18N.hpp" #include "I18N.hpp"
#include "Widgets/Label.hpp" #include "Widgets/Label.hpp"
@@ -11,7 +10,11 @@
#include "slic3r/Utils/Http.hpp" #include "slic3r/Utils/Http.hpp"
#include "libslic3r/Thread.hpp" #include "libslic3r/Thread.hpp"
#include "nlohmann/json.hpp" #include "nlohmann/json.hpp"
#include <ctime>
#include <mutex> #include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
namespace Slic3r { namespace Slic3r {
@@ -26,12 +29,12 @@ namespace GUI {
class HMSQuery { class HMSQuery {
protected: protected:
std::unordered_map<string, 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_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_action_jsons;// key-> device id type
std::unordered_map<wxString, wxImage> m_hms_local_images; // key-> image name std::unordered_map<wxString, wxImage> m_hms_local_images; // key-> image name
mutable std::mutex m_hms_mutex; 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: public:
HMSQuery() { } HMSQuery() { }
@@ -61,18 +64,18 @@ private:
// load hms // load hms
void init_hms_info(const std::string& dev_type_id); void init_hms_info(const std::string& dev_type_id);
void copy_from_data_dir_to_local(); 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 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, json* receive_json, std::string& version_info); 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, json save_json); 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 = ""); std::string get_hms_file(std::string hms_type, std::string lang = std::string("en"), std::string dev_id_type = "");
// internal query // internal query
string get_dev_id_type(const MachineObject* obj) const; std::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")); 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")); 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 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 string& dev_id_type, const std::string& long_error_code, std::vector<int>& button_action); 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); int get_hms_info_version(std::string &version);
@@ -85,4 +88,4 @@ std::string get_error_message(int error_code);
} }
#endif #endif
+106 -94
View File
@@ -493,9 +493,8 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
}); });
//BBS //BBS
Bind(EVT_SELECT_TAB, [this](wxCommandEvent&evt) { Bind(EVT_SELECT_TAB, [this](wxCommandEvent& evt) {
TabPosition pos = (TabPosition)evt.GetInt(); m_tabpanel->SelectPageByName(evt.GetString());
m_tabpanel->SetSelection(pos);
}); });
Bind(EVT_SYNC_CLOUD_PRESET, &MainFrame::on_select_default_preset, this); Bind(EVT_SYNC_CLOUD_PRESET, &MainFrame::on_select_default_preset, this);
@@ -702,7 +701,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
} }
return;} return;}
#endif #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') { if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'G') {
m_plater->apply_background_progress(); m_plater->apply_background_progress();
m_print_enable = get_enable_print_status(); m_print_enable = get_enable_print_status();
@@ -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;} 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;} else if (evt.CmdDown() && evt.GetKeyCode() == 'S') { if (can_save()) m_plater->save_project(); return;}
if (evt.CmdDown() && evt.GetKeyCode() == 'F') { 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(); m_plater->sidebar().can_search();
} }
} }
@@ -1007,8 +1006,8 @@ void MainFrame::update_layout()
m_layout = layout; m_layout = layout;
// From the very beginning the Print settings should be selected // From the very beginning the Print settings should be selected
//m_last_selected_tab = m_layout == ESettingsLayout::Dlg ? 0 : 1; //m_last_selected_tab = m_layout == ESettingsLayout::Dlg ? TAB_ID_HOME : TAB_ID_PREPARE;
m_last_selected_tab = 1; m_last_selected_tab = TAB_ID_PREPARE;
// Set new settings // Set new settings
switch (m_layout) switch (m_layout)
@@ -1016,14 +1015,18 @@ void MainFrame::update_layout()
case ESettingsLayout::Old: case ESettingsLayout::Old:
{ {
m_plater->Reparent(m_tabpanel); m_plater->Reparent(m_tabpanel);
m_tabpanel->InsertPage(tp3DEditor, m_plater, _L("Prepare"), std::string("tab_3d_active"), std::string("tab_3d_active"), false); // Right after Home — or first, when there is no Home tab (PositionAfter() would
m_tabpanel->InsertPage(tpPreview, m_plater, _L("Preview"), std::string("tab_preview_active"), std::string("tab_preview_active"), false); // 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_main_sizer->Add(m_tabpanel, 1, wxEXPAND | wxTOP, 0);
m_tabpanel->Bind(wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED, [this](wxCommandEvent& evt) m_tabpanel->Bind(wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED, [this](wxCommandEvent& evt)
{ {
// jump to 3deditor under preview_only mode // 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(); Sidebar& sidebar = GUI::wxGetApp().sidebar();
if (sidebar.need_auto_sync_after_connect_printer()) { if (sidebar.need_auto_sync_after_connect_printer()) {
sidebar.set_need_auto_sync_after_connect_printer(false); sidebar.set_need_auto_sync_after_connect_printer(false);
@@ -1107,6 +1110,9 @@ void MainFrame::update_edge_panels()
void MainFrame::shutdown() void MainFrame::shutdown()
{ {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "MainFrame::shutdown enter"; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "MainFrame::shutdown enter";
if (m_project != nullptr)
m_project->shutdown();
m_plugin_pages.shutdown();
#ifdef __WXGTK__ #ifdef __WXGTK__
// Edge panels are child windows — wxWidgets destroys them automatically. // Edge panels are child windows — wxWidgets destroys them automatically.
m_edge_bottom = nullptr; m_edge_bottom = nullptr;
@@ -1252,15 +1258,14 @@ void MainFrame::init_tabpanel() {
#endif #endif
//BBS //BBS
wxWindow* panel = m_tabpanel->GetCurrentPage(); wxWindow* panel = m_tabpanel->GetCurrentPage();
int sel = m_tabpanel->GetSelection();
//wxString page_text = m_tabpanel->GetPageText(sel); //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 (panel == m_plater) {
if (sel == tp3DEditor) { if (m_last_selected_tab == TAB_ID_PREPARE) {
wxPostEvent(m_plater, SimpleEvent(EVT_GLVIEWTOOLBAR_3D)); wxPostEvent(m_plater, SimpleEvent(EVT_GLVIEWTOOLBAR_3D));
m_param_panel->OnActivate(); m_param_panel->OnActivate();
} }
else if (sel == tpPreview) { else if (m_last_selected_tab == TAB_ID_PREVIEW) {
m_plater->reset_check_status(); m_plater->reset_check_status();
if (!m_plater->check_ams_status(m_slice_select == eSliceAll)) if (!m_plater->check_ams_status(m_slice_select == eSliceAll))
return; return;
@@ -1275,7 +1280,7 @@ void MainFrame::init_tabpanel() {
//monitor //monitor
} }
#ifndef __APPLE__ #ifndef __APPLE__
if (sel == tp3DEditor) { if (m_last_selected_tab == TAB_ID_PREPARE) {
m_topbar->EnableUndoRedoItems(); m_topbar->EnableUndoRedoItems();
} }
else { else {
@@ -1285,34 +1290,16 @@ void MainFrame::init_tabpanel() {
if (panel) if (panel)
panel->SetFocus(); 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()) { if (wxGetApp().is_editor()) {
m_webview = new WebViewPanel(m_tabpanel); m_webview = new WebViewPanel(m_tabpanel);
Bind(EVT_LOAD_URL, [this](wxCommandEvent &evt) { Bind(EVT_LOAD_URL, [this](wxCommandEvent &evt) {
wxString url = evt.GetString(); wxString url = evt.GetString();
select_tab(MainFrame::tpHome); select_tab(TAB_ID_HOME);
m_webview->load_url(url); 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); 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 //BBS add pages
m_monitor = new MonitorPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_monitor = new MonitorPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_monitor->SetBackgroundColour(*wxWHITE); 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); m_printer_view = new PrinterWebView(m_tabpanel);
Bind(EVT_LOAD_PRINTER_URL, [this](LoadPrinterViewEvent &evt) { 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 = new MultiMachinePage(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_multi_machine->SetBackgroundColour(*wxWHITE); m_multi_machine->SetBackgroundColour(*wxWHITE);
// TODO: change the bitmap // 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 = new ProjectPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_project->SetBackgroundColour(*wxWHITE); 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 = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_calibration->SetBackgroundColour(*wxWHITE); 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) { if (m_plater) {
// load initial config // load initial config
@@ -1373,10 +1364,15 @@ void MainFrame::show_device(bool should_use_native) {
const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents");
// The web page is appended when printer agents are enabled. Remove that // The web Device page is the extra tab printer-agents mode shows alongside the native one.
// extra page before switching back to the normal native/Web layout. // Printers that drive the native Bambu device tab have nothing to put in it, so they don't
if (!use_printer_agents) { // get it — otherwise a Bambu user sees two Device tabs, one of them permanently empty.
if ((idx = m_tabpanel->FindPage(m_printer_view)) != wxNOT_FOUND && idx != tpMonitor) { 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_printer_view->Show(false);
m_tabpanel->RemovePage(idx); m_tabpanel->RemovePage(idx);
} }
@@ -1394,8 +1390,8 @@ void MainFrame::show_device(bool should_use_native) {
m_tabpanel->RemovePage(idx); m_tabpanel->RemovePage(idx);
} }
m_monitor->Show(false); m_monitor->Show(false);
m_tabpanel->InsertPage(tpMonitor, m_monitor, _L("Device"), std::string("tab_monitor_active"), m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_monitor,
std::string("tab_monitor_active")); _L("Device"), "tab_monitor_active");
} }
if (m_printer_view == nullptr) { if (m_printer_view == nullptr) {
@@ -1416,28 +1412,31 @@ void MainFrame::show_device(bool should_use_native) {
// TODO: change the bitmap // TODO: change the bitmap
if (m_tabpanel->FindPage(m_multi_machine) == wxNOT_FOUND) { if (m_tabpanel->FindPage(m_multi_machine) == wxNOT_FOUND) {
m_multi_machine->Show(false); m_multi_machine->Show(false);
m_tabpanel->InsertPage(tpMultiDevice, m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"), // Past the web Device tab when it is already there, so enabling multi-machine
std::string("tab_multi_active"), false); // 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) { if (!m_calibration) {
m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_calibration->SetBackgroundColour(*wxWHITE); m_calibration->SetBackgroundColour(*wxWHITE);
} }
// 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.
if (m_tabpanel->FindPage(m_calibration) == wxNOT_FOUND) { if (m_tabpanel->FindPage(m_calibration) == wxNOT_FOUND) {
m_calibration->Show(false); m_calibration->Show(false);
m_tabpanel->AddPage(m_calibration, _L("Calibration"), std::string("tab_calibration_active"), m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PROJECT}), TAB_ID_CALIBRATION, m_calibration,
std::string("tab_calibration_active"), false); _L("Calibration"), "tab_calibration_active");
} }
if ((idx = m_tabpanel->FindPage(m_printer_view)) == wxNOT_FOUND) { if (want_web_device_tab) {
m_printer_view->Show(false); if ((idx = m_tabpanel->FindPage(m_printer_view)) == wxNOT_FOUND) {
m_tabpanel->AddPage(m_printer_view, _L("Device (Web)"), std::string("tab_monitor_active"), m_printer_view->Show(false);
std::string("tab_monitor_active"), false); // Immediately right of the native Device tab, not at the end of the tab bar.
} else { m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR}), TAB_ID_MONITOR_WEB,
m_tabpanel->SetPageText(idx, _L("Device (Web)")); m_printer_view, _L("Device (Web)"), "tab_monitor_active");
} else {
m_tabpanel->SetPageText(idx, _L("Device (Web)"));
}
} }
#ifdef _MSW_DARK_MODE #ifdef _MSW_DARK_MODE
@@ -1445,6 +1444,7 @@ void MainFrame::show_device(bool should_use_native) {
#endif // _MSW_DARK_MODE #endif // _MSW_DARK_MODE
fit_tab_labels(); // ORCA on printer change fit_tab_labels(); // ORCA on printer change
m_plugin_pages.relayout(); // re-sync plugin tabs against the native tabs just mutated above
return; return;
} }
@@ -1466,7 +1466,8 @@ void MainFrame::show_device(bool should_use_native) {
m_monitor->SetBackgroundColour(*wxWHITE); m_monitor->SetBackgroundColour(*wxWHITE);
} }
m_monitor->Show(false); 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 (wxGetApp().is_enable_multi_machine()) {
if (!m_multi_machine) { if (!m_multi_machine) {
@@ -1475,18 +1476,18 @@ void MainFrame::show_device(bool should_use_native) {
} }
// TODO: change the bitmap // TODO: change the bitmap
m_multi_machine->Show(false); m_multi_machine->Show(false);
m_tabpanel->InsertPage(tpMultiDevice, m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"), m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR}), TAB_ID_MULTI_DEVICE, m_multi_machine,
std::string("tab_multi_active"), false); _L("Multi-device"), "tab_multi_active");
} }
if (!m_calibration) { if (!m_calibration) {
m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_calibration->SetBackgroundColour(*wxWHITE); m_calibration->SetBackgroundColour(*wxWHITE);
} }
m_calibration->Show(false); m_calibration->Show(false);
// Calibration is always the last page, so don't use InsertPage here. Otherwise, if multi_machine page is not enabled, // Last of the built-in tabs, but plugin tabs already sit past it — anchor rather than
// the calibration tab won't be properly added as well, due to the TabPosition::tpCalibration no longer matches the real tab position. // append, so its position doesn't depend on the relayout() below running afterwards.
m_tabpanel->AddPage(m_calibration, _L("Calibration"), std::string("tab_calibration_active"), m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PROJECT}), TAB_ID_CALIBRATION, m_calibration,
std::string("tab_calibration_active"), false); _L("Calibration"), "tab_calibration_active");
#ifdef _MSW_DARK_MODE #ifdef _MSW_DARK_MODE
wxGetApp().UpdateDarkUIWin(this); wxGetApp().UpdateDarkUIWin(this);
@@ -1519,10 +1520,17 @@ void MainFrame::show_device(bool should_use_native) {
}); });
} }
m_printer_view->Show(false); m_printer_view->Show(false);
m_tabpanel->InsertPage(tpMonitor, m_printer_view, _L("Device"), std::string("tab_monitor_active"), m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_printer_view,
std::string("tab_monitor_active")); _L("Device"), "tab_monitor_active");
} }
fit_tab_labels(); // ORCA on printer change 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() void MainFrame::fit_tab_labels()
@@ -1554,7 +1562,7 @@ void MainFrame::fit_tab_labels()
bool MainFrame::preview_only_hint() bool MainFrame::preview_only_hint()
{ {
if (m_plater && (m_plater->only_gcode_mode() || (m_plater->using_exported_file()))) { 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")); ConfirmBeforeSendDialog confirm_dlg(this, wxID_ANY, _L("Warning"));
confirm_dlg.Bind(EVT_SECONDARY_CHECK_CONFIRM, [this](wxCommandEvent& e) { confirm_dlg.Bind(EVT_SECONDARY_CHECK_CONFIRM, [this](wxCommandEvent& e) {
@@ -1872,22 +1880,22 @@ bool MainFrame::can_clone() const {
bool MainFrame::can_select() 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 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 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 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 bool MainFrame::can_reslice() const
@@ -1996,7 +2004,7 @@ wxBoxSizer* MainFrame::create_side_tools()
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_ALL)); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_ALL));
else else
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE));
this->m_tabpanel->SetSelection(tpPreview); this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW);
} }
}); });
@@ -3143,7 +3151,7 @@ void MainFrame::init_menubar_as_editor()
wxGetApp().app_config->set_bool("auto_perspective", !wxGetApp().app_config->get_bool("auto_perspective")); wxGetApp().app_config->set_bool("auto_perspective", !wxGetApp().app_config->get_bool("auto_perspective"));
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
}, },
this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; }, this, [this]() { return is_prepare_or_preview_tab(); },
[this]() { return wxGetApp().app_config->get_bool("auto_perspective"); }, this); [this]() { return wxGetApp().app_config->get_bool("auto_perspective"); }, this);
viewMenu->AppendSeparator(); viewMenu->AppendSeparator();
@@ -3152,7 +3160,7 @@ void MainFrame::init_menubar_as_editor()
wxGetApp().toggle_show_gcode_window(); wxGetApp().toggle_show_gcode_window();
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
}, },
this, [this]() { return m_tabpanel->GetSelection() == tpPreview; }, this, [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW; },
[this]() { return wxGetApp().show_gcode_window(); }, this); [this]() { return wxGetApp().show_gcode_window(); }, this);
append_menu_check_item( append_menu_check_item(
@@ -3161,7 +3169,7 @@ void MainFrame::init_menubar_as_editor()
wxGetApp().toggle_show_3d_navigator(); wxGetApp().toggle_show_3d_navigator();
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
}, },
this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; }, this, [this]() { return is_prepare_or_preview_tab(); },
[this]() { return wxGetApp().show_3d_navigator(); }, this); [this]() { return wxGetApp().show_3d_navigator(); }, this);
append_menu_check_item(viewMenu, wxID_ANY, _L("Show Gridlines"), _L("Show Gridlines on plate"), append_menu_check_item(viewMenu, wxID_ANY, _L("Show Gridlines"), _L("Show Gridlines on plate"),
@@ -3169,15 +3177,14 @@ void MainFrame::init_menubar_as_editor()
wxGetApp().toggle_show_plate_gridlines(); wxGetApp().toggle_show_plate_gridlines();
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
}, this, }, this,
[this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; }, [this]() { return is_prepare_or_preview_tab(); },
[this]() { return wxGetApp().show_plate_gridlines(); }, this); [this]() { return wxGetApp().show_plate_gridlines(); }, this);
append_menu_item( append_menu_item(
viewMenu, wxID_ANY, _L("Reset Window Layout"), _L("Reset to default window layout"), viewMenu, wxID_ANY, _L("Reset Window Layout"), _L("Reset to default window layout"),
[this](wxCommandEvent&) { m_plater->reset_window_layout(); }, "", this, [this](wxCommandEvent&) { m_plater->reset_window_layout(); }, "", this,
[this]() { [this]() {
return (m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview) && return is_prepare_or_preview_tab() && m_plater->is_sidebar_enabled();
m_plater->is_sidebar_enabled();
}, },
this); this);
@@ -3199,7 +3206,7 @@ void MainFrame::init_menubar_as_editor()
wxGetApp().toggle_show_outline(); wxGetApp().toggle_show_outline();
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT)); m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
}, },
this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor; }, this, [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE; },
[this]() { return wxGetApp().show_outline(); }, this); [this]() { return wxGetApp().show_outline(); }, this);
/*viewMenu->AppendSeparator(); /*viewMenu->AppendSeparator();
@@ -4000,13 +4007,16 @@ void MainFrame::select_tab(wxPanel* panel)
wxGetApp().params_dialog()->Popup(); wxGetApp().params_dialog()->Popup();
return; 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); 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; return;
//BBS GUI refactor: remove unused layout new/dlg //BBS GUI refactor: remove unused layout new/dlg
/*if (page_idx != wxNOT_FOUND && m_layout == ESettingsLayout::Dlg) /*if (page_idx != wxNOT_FOUND && m_layout == ESettingsLayout::Dlg)
page_idx++;*/ page_idx++;*/
select_tab(size_t(page_idx)); select_tab(page_name);
} }
//BBS //BBS
@@ -4014,7 +4024,7 @@ void MainFrame::jump_to_monitor(std::string dev_id)
{ {
if(!m_monitor) if(!m_monitor)
return; return;
m_tabpanel->SetSelection(tpMonitor); m_tabpanel->SelectPageByName(TAB_ID_MONITOR);
if (!dev_id.empty()) { if (!dev_id.empty()) {
((MonitorPanel*)m_monitor)->select_machine(dev_id); ((MonitorPanel*)m_monitor)->select_machine(dev_id);
} }
@@ -4024,26 +4034,26 @@ void MainFrame::jump_to_multipage()
{ {
if(!m_multi_machine) if(!m_multi_machine)
return; return;
m_tabpanel->SetSelection(tpMultiDevice); m_tabpanel->SelectPageByName(TAB_ID_MULTI_DEVICE);
((MultiMachinePage*)m_multi_machine)->jump_to_send_page(); ((MultiMachinePage*)m_multi_machine)->jump_to_send_page();
} }
//BBS GUI refactor: remove unused layout new/dlg //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; //bool tabpanel_was_hidden = false;
// Controls on page are created on active page of active tab now. // 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 // We should select/activate tab before its showing to avoid an UI-flickering
auto select = [this, tab](bool was_hidden) { auto select = [this, id](bool was_hidden) {
// when tab == -1, it means we should show the last selected tab // when id is empty, it means we should show the last selected tab
//BBS GUI refactor: remove unused layout new/dlg //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 : (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) if (m_tabpanel->GetSelectedPageName() != new_selection)
m_tabpanel->SetSelection(new_selection); m_tabpanel->SelectPageByName(new_selection);
#ifdef _MSW_DARK_MODE #ifdef _MSW_DARK_MODE
/*if (wxGetApp().tabs_as_menu()) { /*if (wxGetApp().tabs_as_menu()) {
if (Tab* cur_tab = dynamic_cast<Tab*>(m_tabpanel->GetPage(new_selection))) if (Tab* cur_tab = dynamic_cast<Tab*>(m_tabpanel->GetPage(new_selection)))
@@ -4052,10 +4062,12 @@ void MainFrame::select_tab(size_t tab/* = size_t(-1)*/)
m_plater->get_current_canvas3D()->render(); m_plater->get_current_canvas3D()->render();
}*/ }*/
#endif #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(); m_plater->canvas3D()->render();
else if (was_hidden) { 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) if (cur_tab)
cur_tab->OnActivate(); cur_tab->OnActivate();
} }
@@ -4064,10 +4076,10 @@ void MainFrame::select_tab(size_t tab/* = size_t(-1)*/)
select(false); select(false);
} }
void MainFrame::request_select_tab(TabPosition pos) void MainFrame::request_select_tab(const wxString& id)
{ {
wxCommandEvent* evt = new wxCommandEvent(EVT_SELECT_TAB); wxCommandEvent* evt = new wxCommandEvent(EVT_SELECT_TAB);
evt->SetInt(pos); evt->SetString(id);
wxQueueEvent(this, evt); wxQueueEvent(this, evt);
} }
@@ -4359,7 +4371,7 @@ void MainFrame::load_printer_url()
} }
} }
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() void MainFrame::refresh_plugin_tips()
+23 -17
View File
@@ -35,6 +35,21 @@
#include "PrinterWebView.hpp" #include "PrinterWebView.hpp"
#include "calib_dlg.hpp" #include "calib_dlg.hpp"
#include "MultiMachinePage.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 #define ENABEL_PRINT_ALL 0
@@ -115,7 +130,7 @@ class MainFrame : public DPIFrame
wxMenuItem* m_menu_item_reslice_now { nullptr }; wxMenuItem* m_menu_item_reslice_now { nullptr };
wxSizer* m_main_sizer{ 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_base_name(const wxString &full_name, const char *extension = nullptr) const;
std::string get_dir_name(const wxString &full_name) const; std::string get_dir_name(const wxString &full_name) const;
@@ -214,19 +229,6 @@ public:
#ifdef __APPLE__ #ifdef __APPLE__
bool get_mac_full_screen() { return m_mac_fullscreen; } bool get_mac_full_screen() { return m_mac_fullscreen; }
#endif #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 //BBS: add slice&&print status update logic
enum SlicePrintEventType enum SlicePrintEventType
@@ -326,8 +328,8 @@ public:
// When tab == -1, will be selected last selected tab // When tab == -1, will be selected last selected tab
//BBS: GUI refactor //BBS: GUI refactor
void select_tab(wxPanel* panel); void select_tab(wxPanel* panel);
void select_tab(size_t tab = size_t(-1)); void select_tab(const wxString& id = wxString());
void request_select_tab(TabPosition pos); void request_select_tab(const wxString& id);
int get_calibration_curr_tab(); int get_calibration_curr_tab();
void select_view(const std::string& direction); void select_view(const std::string& direction);
// Propagate changed configuration from the Tab to the Plater and save changes to the AppConfig // Propagate changed configuration from the Tab to the Plater and save changes to the AppConfig
@@ -360,6 +362,9 @@ public:
//SoftFever //SoftFever
void show_device(bool should_use_native); void show_device(bool should_use_native);
void fit_tab_labels(); // ORCA 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 }; PA_Calibration_Dlg* m_pa_calib_dlg{ nullptr };
FlowRateCalibrationDialog* m_flow_rate_calib_dlg{ nullptr }; FlowRateCalibrationDialog* m_flow_rate_calib_dlg{ nullptr };
@@ -385,7 +390,8 @@ public:
CalibrationPanel* m_calibration{ nullptr }; CalibrationPanel* m_calibration{ nullptr };
WebViewPanel* m_webview { nullptr }; WebViewPanel* m_webview { nullptr };
PrinterWebView* m_printer_view{nullptr}; PrinterWebView* m_printer_view{nullptr};
wxLogWindow* m_log_window { nullptr }; PluginPages m_plugin_pages;
wxLogWindow* m_log_window { nullptr };
// BBS // BBS
//wxBookCtrlBase* m_tabpanel { nullptr }; //wxBookCtrlBase* m_tabpanel { nullptr };
Notebook* m_tabpanel{ nullptr }; Notebook* m_tabpanel{ nullptr };
+9 -6
View File
@@ -186,17 +186,17 @@ void MonitorPanel::init_tabpanel()
//m_status_add_machine_panel = new AddMachinePanel(m_tabpanel); //m_status_add_machine_panel = new AddMachinePanel(m_tabpanel);
m_status_info_panel = new StatusPanel(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_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("Storage"), false);
//m_tabpanel->AddPage(m_media_file_panel, _L("Internal Storage"), "", false); //m_tabpanel->AddPage(m_media_file_panel, _L("Internal Storage"), false);
m_upgrade_panel = new UpgradePanel(m_tabpanel); 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_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(); std::string network_ver = Slic3r::NetworkAgent::get_version();
if (!network_ver.empty()) { if (!network_ver.empty()) {
@@ -413,7 +413,10 @@ void MonitorPanel::update_hms_tag()
bool MonitorPanel::Show(bool show) bool MonitorPanel::Show(bool show)
{ {
#ifdef __APPLE__ #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 #endif
NetworkAgent* m_agent = wxGetApp().getAgent(); NetworkAgent* m_agent = wxGetApp().getAgent();
+3 -3
View File
@@ -86,9 +86,9 @@ void MultiMachinePage::init_tabpanel()
m_cloud_task_manager = new CloudTaskManagerPage(m_tabpanel); m_cloud_task_manager = new CloudTaskManagerPage(m_tabpanel);
m_machine_manager = new MultiMachineManagerPage(m_tabpanel); m_machine_manager = new MultiMachineManagerPage(m_tabpanel);
m_tabpanel->AddPage(m_machine_manager, _L("Device"), "", true); m_tabpanel->AddPage(m_machine_manager, _L("Device"), true);
m_tabpanel->AddPage(m_local_task_manager, _L("Task Sending"), "", false); 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_cloud_task_manager, _L("Task Sent"), false);
} }
void MultiMachinePage::init_timer() void MultiMachinePage::init_timer()
+41 -7
View File
@@ -120,11 +120,11 @@ void ButtonsListCtrl::Rescale()
void ButtonsListCtrl::SetSelection(int sel) void ButtonsListCtrl::SetSelection(int sel)
{ {
if (m_selection == sel) if (m_selection == sel && sel >= 0 && sel < static_cast<int>(m_pageButtons.size()))
return; return;
// BBS: change button color // BBS: change button color
wxColour selected_btn_bg("#009688"); // Gradient #009688 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( StateColor bg_color = StateColor(
std::pair{wxColour(107, 107, 107), (int) StateColor::Hovered}, std::pair{wxColour(107, 107, 107), (int) StateColor::Hovered},
std::pair{wxColour(59, 68, 70), (int) StateColor::Normal}); std::pair{wxColour(59, 68, 70), (int) StateColor::Normal});
@@ -132,9 +132,15 @@ void ButtonsListCtrl::SetSelection(int sel)
StateColor text_color = StateColor( StateColor text_color = StateColor(
std::pair{wxColour(254,254, 254), (int) StateColor::Normal} std::pair{wxColour(254,254, 254), (int) StateColor::Normal}
); );
m_pageButtons[m_selection]->SetSelected(false);
m_pageButtons[m_selection]->SetTextColor(text_color); 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; m_selection = sel;
StateColor bg_color = StateColor( StateColor bg_color = StateColor(
@@ -145,17 +151,19 @@ void ButtonsListCtrl::SetSelection(int sel)
StateColor text_color = StateColor( StateColor text_color = StateColor(
std::pair{wxColour(254, 254, 254), (int) StateColor::Normal} std::pair{wxColour(254, 254, 254), (int) StateColor::Normal}
); );
m_pageButtons[m_selection]->SetSelected(true);
m_pageButtons[m_selection]->SetTextColor(text_color); m_pageButtons[m_selection]->SetTextColor(text_color);
Refresh(); 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); Button * btn = new Button(this, text.empty() ? text : " " + text, bmp_name, wxNO_BORDER);
btn->SetCornerRadius(0); btn->SetCornerRadius(0);
if (bmp_name.empty() && bmp.IsOk())
btn->SetIcon(bmp);
int em = em_unit(this); int em = em_unit(this);
//BBS set size for button //BBS set size for button
btn->SetMinSize({(text.empty() ? 40 : 136) * em / 10, 36 * em / 10}); 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( StateColor text_color = StateColor(
std::pair{wxColour(254,254, 254), (int) StateColor::Normal}); std::pair{wxColour(254,254, 254), (int) StateColor::Normal});
btn->SetTextColor(text_color); btn->SetTextColor(text_color);
btn->SetInactiveIcon(inactive_bmp_name);
btn->SetSelected(false);
btn->Bind(wxEVT_BUTTON, [this, btn](wxCommandEvent& event) { btn->Bind(wxEVT_BUTTON, [this, btn](wxCommandEvent& event) {
if (auto it = std::find(m_pageButtons.begin(), m_pageButtons.end(), btn); it != m_pageButtons.end()) { if (auto it = std::find(m_pageButtons.begin(), m_pageButtons.end(), btn); it != m_pageButtons.end()) {
auto sel = it - m_pageButtons.begin(); 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) 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]; Button* btn = m_pageButtons[n];
m_pageButtons.erase(m_pageButtons.begin() + n); m_pageButtons.erase(m_pageButtons.begin() + n);
m_pageLabels.erase(m_pageLabels.begin() + n); // ORCA m_pageLabels.erase(m_pageLabels.begin() + n); // ORCA
@@ -240,6 +254,24 @@ wxString ButtonsListCtrl::GetPageText(size_t n) const
return btn->GetLabel(); 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 //#endif // _WIN32
void Notebook::Init() void Notebook::Init()
@@ -253,6 +285,8 @@ void Notebook::Init()
m_showTimeout = m_hideTimeout = 0; m_showTimeout = m_hideTimeout = 0;
m_pageNames.clear();
/* On Linux, Gstreamer wxMediaCtrl does not seem to get along well with /* 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 * 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 ... * bug? Is this a Gstreamer bug? No idea, but it is our problem ...
+106 -33
View File
@@ -3,7 +3,11 @@
//#ifdef _WIN32 //#ifdef _WIN32
#include <initializer_list>
#include <string>
#include <vector>
#include <wx/bookctrl.h> #include <wx/bookctrl.h>
#include <wx/bitmap.h>
#include <wx/sizer.h> #include <wx/sizer.h>
class ScalableButton; class ScalableButton;
@@ -23,13 +27,16 @@ public:
void SetSelection(int sel); void SetSelection(int sel);
void UpdateMode(); void UpdateMode();
void Rescale(); 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); void RemovePage(size_t n);
bool SetPageImage(size_t n, const std::string& bmp_name) const; bool SetPageImage(size_t n, const std::string& bmp_name) const;
void SetPageText(size_t n, const wxString& strText); void SetPageText(size_t n, const wxString& strText);
void SetCompact(size_t n, bool compact); // ORCA void SetCompact(size_t n, bool compact); // ORCA
wxString GetPageText(size_t n) const; wxString GetPageText(size_t n) const;
wxFlexGridSizer* GetBtnsSizer(){return m_buttons_sizer;}; // ORCA 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: private:
wxFlexGridSizer* m_buttons_sizer; wxFlexGridSizer* m_buttons_sizer;
@@ -40,9 +47,10 @@ private:
int m_btn_margin; int m_btn_margin;
int m_line_margin; int m_line_margin;
std::vector<wxString> m_pageLabels; // ORCA std::vector<wxString> m_pageLabels; // ORCA
wxWindow* m_overflow_button{nullptr}; // ORCA
}; };
class Notebook: public wxBookCtrlBase class Notebook : public wxBookCtrlBase
{ {
public: public:
Notebook(wxWindow * parent, Notebook(wxWindow * parent,
@@ -103,7 +111,7 @@ public:
// by this control) and show it immediately. // by this control) and show it immediately.
bool ShowNewPage(wxWindow * page) 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. // Implement base class pure virtual methods.
// adds a new page to the control // Page management. Every insertion funnels through the InsertPage() below; `id` is the
bool AddPage(wxWindow* page, // 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 wxString& text,
const std::string& bmp_name, const std::string& bmp_name = "",
const std::string& inactive_bmp_name,
bool bSelect = false) bool bSelect = false)
{ {
DoInvalidateBestSize(); DoInvalidateBestSize();
return InsertPage(GetPageCount(), page, text, bmp_name, inactive_bmp_name, bSelect); return InsertPage(GetPageCount(), id, page, text, bmp_name, bSelect);
} }
// Page management bool AddPage(wxWindow* page, const wxString& text, bool bSelect = false, int imageId = NO_IMAGE) override
virtual bool InsertPage(size_t n,
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; 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)) if (!DoSetSelectionAfterInsertion(n, bSelect))
page->Hide(); page->Hide();
return true; return true;
} }
bool InsertPage(size_t n, virtual bool InsertPage(size_t n,
wxWindow * page, wxWindow * page,
const wxString & text, const wxString & text,
const std::string& bmp_name = "", bool bSelect = false,
const std::string& inactive_bmp_name = "", int WXUNUSED(imageId) = NO_IMAGE) override
bool bSelect = false)
{ {
if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect)) return InsertPage(n, wxString(), page, text, "", bSelect);
return false;
GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name, inactive_bmp_name);
if (bSelect)
SetSelection(n);
return true;
} }
virtual int SetSelection(size_t n) override virtual int SetSelection(size_t n) override
@@ -211,8 +224,8 @@ public:
return DoSetSelection(n); return DoSetSelection(n);
} }
// Neither labels nor images are supported but we still store the labels // Labels are stored by the custom button list; wx's image-list API is unused — tab icons
// just in case the user code attaches some importance to them. // 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 virtual bool SetPageText(size_t n, const wxString & strText) override
{ {
wxCHECK_MSG(n < GetPageCount(), false, wxS("Invalid page")); wxCHECK_MSG(n < GetPageCount(), false, wxS("Invalid page"));
@@ -251,7 +264,64 @@ public:
page->SetFocus(); 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); } 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() void UpdateMode()
{ {
@@ -369,6 +439,7 @@ protected:
wxWindow* const win = wxBookCtrlBase::DoRemovePage(page); wxWindow* const win = wxBookCtrlBase::DoRemovePage(page);
if (win) if (win)
{ {
m_pageNames.erase(m_pageNames.begin() + page);
GetBtnsListCtrl()->RemovePage(page); GetBtnsListCtrl()->RemovePage(page);
DoSetSelectionAfterRemoval(page); DoSetSelectionAfterRemoval(page);
} }
@@ -394,6 +465,8 @@ protected:
private: private:
void Init(); void Init();
std::vector<wxString> m_pageNames; // index-parallel to wxBookCtrlBase::m_pages
wxShowEffect m_showEffect, wxShowEffect m_showEffect,
m_hideEffect; m_hideEffect;
+6 -6
View File
@@ -1918,7 +1918,7 @@ void NotificationManager::push_validate_error_notification(StringObjectException
wxGetApp().sidebar().jump_to_option(opt, Preset::TYPE_PRINT, L""); wxGetApp().sidebar().jump_to_option(opt, Preset::TYPE_PRINT, L"");
} }
else { else {
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
} }
return false; return false;
}; };
@@ -1985,7 +1985,7 @@ void NotificationManager::push_validate_error_notification(StringObjectException
wxGetApp().sidebar().jump_to_option(opt, opt_type, L""); wxGetApp().sidebar().jump_to_option(opt, opt_type, L"");
} }
else { else {
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
} }
return false; 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 (iter != objects.end()) { ovs.push_back({ *iter, nullptr }); }
} }
if (!ovs.empty()) { 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()->select_items(ovs);
} }
return false; return false;
@@ -2046,7 +2046,7 @@ void NotificationManager::push_slicing_warning_notification(const std::string& t
auto& objects = wxGetApp().model().objects; auto& objects = wxGetApp().model().objects;
auto iter = std::find_if(objects.begin(), objects.end(), [id](auto o) { return o->id() == id; }); auto iter = std::find_if(objects.begin(), objects.end(), [id](auto o) { return o->id() == id; });
if (iter != 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()->select_items({ {*iter, nullptr} });
} }
return false; 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 (iter != objects.end()) { ovs.push_back({ *iter, nullptr }); }
} }
if (!ovs.empty()) { 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()->select_items(ovs);
wxGetApp().obj_list()->update_selections_on_canvas(); 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()) { if (!sel_items.empty()) {
obj_list->select_items(sel_items); obj_list->select_items(sel_items);
+43 -32
View File
@@ -5783,6 +5783,8 @@ private:
bool show_warning_dialog { false }; 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_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_3mf(".*3mf", std::regex::icase);
const std::regex Plater::priv::pattern_zip_amf(".*[.]zip[.]amf", std::regex::icase); const std::regex Plater::priv::pattern_zip_amf(".*[.]zip[.]amf", std::regex::icase);
@@ -5797,7 +5799,7 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &fi
#endif // WIN32 #endif // WIN32
m_mainframe.Raise(); m_mainframe.Raise();
m_mainframe.select_tab(size_t(MainFrame::tp3DEditor)); m_mainframe.select_tab(TAB_ID_PREPARE);
if (wxGetApp().is_editor()) if (wxGetApp().is_editor())
m_plater.select_view_3D("3D"); m_plater.select_view_3D("3D");
@@ -6579,9 +6581,9 @@ void Plater::priv::select_next_view_3D()
{ {
if (current_panel == view3D) if (current_panel == view3D)
wxGetApp().mainframe->select_tab(size_t(MainFrame::tpPreview)); wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW);
else if (current_panel == 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) // else if (current_panel == assemble_view)
// set_current_panel(view3D); // set_current_panel(view3D);
} }
@@ -7880,7 +7882,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
q->select_plate(first_plate_index); q->select_plate(first_plate_index);
//set to 3d tab //set to 3d tab
q->select_view_3D("Preview"); q->select_view_3D("Preview");
wxGetApp().mainframe->select_tab(MainFrame::tpPreview); wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW);
} }
else { else {
//set to 3d tab //set to 3d tab
@@ -7899,7 +7901,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
else { else {
//always set to 3D after loading files //always set to 3D after loading files
q->select_view_3D("3D"); q->select_view_3D("3D");
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
} }
if (load_model) { if (load_model) {
@@ -8807,7 +8809,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) { if (inst_idx != -1) {
auto* model = wxGetApp().obj_list()->GetModel(); auto* model = wxGetApp().obj_list()->GetModel();
@@ -8836,7 +8838,7 @@ void Plater::priv::process_validation_warning(StringObjectException const &warni
} else { } else {
auto iter = id.id ? std::find_if(objects.begin(), objects.end(), [id](auto o) { return o->id() == id; }) : objects.end(); auto iter = id.id ? std::find_if(objects.begin(), objects.end(), [id](auto o) { return o->id() == id; }) : objects.end();
if (iter != 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()->select_items({{*iter, nullptr}});
wxGetApp().obj_list()->update_selections_on_canvas(); wxGetApp().obj_list()->update_selections_on_canvas();
} }
@@ -11220,13 +11222,19 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e)
} }
const int new_sel = e.GetSelection(); 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(); update_sidebar();
int old_sel = e.GetOldSelection(); int old_sel = e.GetOldSelection();
const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents");
const bool use_native_device_tab = wxGetApp().preset_bundle && const bool use_native_device_tab = wxGetApp().preset_bundle &&
(wxGetApp().preset_bundle->use_bbl_device_tab() || use_printer_agents); (wxGetApp().preset_bundle->use_bbl_device_tab() || use_printer_agents);
if (use_native_device_tab && new_sel == MainFrame::tpMonitor) { if (use_native_device_tab && new_name == TAB_ID_MONITOR) {
// BBL network module is only required for BBL-vendor printers. // BBL network module is only required for BBL-vendor printers.
// Non-BBL Python plugins (e.g. moonraker) drive the Device tab without it. // Non-BBL Python plugins (e.g. moonraker) drive the Device tab without it.
if (!use_printer_agents && 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()) {
@@ -11238,12 +11246,15 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e)
} }
} }
} else { } else {
// 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 && const bool selecting_web_device_tab = main_frame->m_printer_view &&
main_frame->m_tabpanel->GetPage(new_sel) == main_frame->m_printer_view; main_frame->m_tabpanel->GetPage(new_sel) == main_frame->m_printer_view;
if (selecting_web_device_tab) { if (selecting_web_device_tab) {
// Use the selected discovered machine when the preset has no host. // Use the selected discovered machine when the preset has no host.
main_frame->load_printer_url(); main_frame->load_printer_url();
} else if (new_sel == MainFrame::tpMonitor && wxGetApp().preset_bundle != nullptr) { } else if (new_name == TAB_ID_MONITOR && wxGetApp().preset_bundle != nullptr) {
auto cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; auto cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config;
wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); wxString url = from_u8(PrintHost::get_print_host_webui(&cfg));
if (main_frame->m_printer_view && url.empty()) { if (main_frame->m_printer_view && url.empty()) {
@@ -12106,7 +12117,7 @@ bool Plater::priv::check_ams_status_impl(bool is_slice_all)
wxPostEvent(q, SimpleEvent(EVT_GLTOOLBAR_SLICE_ALL)); wxPostEvent(q, SimpleEvent(EVT_GLTOOLBAR_SLICE_ALL));
else else
wxPostEvent(q, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); 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; return false;
} }
@@ -12674,7 +12685,7 @@ void Plater::priv::take_snapshot(const std::string& snapshot_name, const UndoRed
ModelWipeTower& tower = model.wipe_tower; 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.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(); const GLGizmosManager& gizmos = get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_gizmos_manager() : view3D->get_canvas3d()->get_gizmos_manager();
@@ -12784,7 +12795,7 @@ void Plater::priv::undo_redo_to(std::vector<UndoRedo::Snapshot>::const_iterator
ModelWipeTower& tower = model.wipe_tower; 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.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; const int layer_range_idx = it_snapshot->snapshot_data.layer_range_idx;
@@ -13063,7 +13074,7 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_
get_notification_manager()->clear_all(); get_notification_manager()->clear_all();
if (!silent) if (!silent)
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
//get_partplate_list().reinit(); //get_partplate_list().reinit();
//get_partplate_list().update_slice_context_to_current_plate(p->background_process); //get_partplate_list().update_slice_context_to_current_plate(p->background_process);
@@ -13212,7 +13223,7 @@ void Plater::load_project(wxString const& filename2,
if (!m_exported_file) { if (!m_exported_file) {
p->select_view("topfront"); p->select_view("topfront");
p->camera.requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; 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 { else {
p->partplate_list.select_plate_view(); p->partplate_list.select_plate_view();
@@ -13326,7 +13337,7 @@ void Plater::import_model_id(wxString download_info)
const int max_retries = 3; const int max_retries = 3;
/* jump to 3D eidtor */ /* jump to 3D eidtor */
wxGetApp().mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor); wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
/* prepare progress dialog */ /* prepare progress dialog */
bool cont = true; bool cont = true;
@@ -13635,7 +13646,7 @@ void Plater::calib_pa(const Calib_Params& params)
{ {
const auto calib_pa_name = wxString::Format(L"Pressure Advance Test"); const auto calib_pa_name = wxString::Format(L"Pressure Advance Test");
new_project(false, false, calib_pa_name); 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 print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config;
auto printer_config = &wxGetApp().preset_bundle->printers.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)); print_config->set_key_value("overhang_reverse", new ConfigOptionBool(false));
@@ -14116,7 +14127,7 @@ void Plater::calib_flowrate(bool is_linear, int pass, InfillPattern pattern) {
if (new_project(false, false, calib_name) == wxID_CANCEL) if (new_project(false, false, calib_name) == wxID_CANCEL)
return; return;
wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (is_linear) { if (is_linear) {
if (pass == 1) if (pass == 1)
@@ -14153,7 +14164,7 @@ void Plater::calib_temp(const Calib_Params& params) {
const auto calib_temp_name = wxString::Format(L"Nozzle temperature test"); const auto calib_temp_name = wxString::Format(L"Nozzle temperature test");
new_project(false, false, calib_temp_name); 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 (params.mode != CalibMode::Calib_Temp_Tower) return;
if (!add_model(false, Slic3r::resources_dir() + "/calib/temperature_tower/temperature_tower.drc")) if (!add_model(false, Slic3r::resources_dir() + "/calib/temperature_tower/temperature_tower.drc"))
@@ -14233,7 +14244,7 @@ void Plater::calib_max_vol_speed(const Calib_Params& params)
{ {
const auto calib_vol_speed_name = wxString::Format(L"Max volumetric speed test"); const auto calib_vol_speed_name = wxString::Format(L"Max volumetric speed test");
new_project(false, false, calib_vol_speed_name); 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) if (params.mode != CalibMode::Calib_Vol_speed_Tower)
return; return;
if (!add_model(false, Slic3r::resources_dir() + "/calib/volumetric_speed/SpeedTestStructure.drc")) if (!add_model(false, Slic3r::resources_dir() + "/calib/volumetric_speed/SpeedTestStructure.drc"))
@@ -14312,7 +14323,7 @@ void Plater::calib_retraction(const Calib_Params& params)
{ {
const auto calib_retraction_name = wxString::Format(L"Retraction"); const auto calib_retraction_name = wxString::Format(L"Retraction");
new_project(false, false, calib_retraction_name); 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) if (params.mode != CalibMode::Calib_Retraction_tower)
return; return;
@@ -14372,7 +14383,7 @@ void Plater::calib_VFA(const Calib_Params& params)
{ {
const auto calib_vfa_name = wxString::Format(L"VFA test"); const auto calib_vfa_name = wxString::Format(L"VFA test");
new_project(false, false, calib_vfa_name); 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) if (params.mode != CalibMode::Calib_VFA_Tower)
return; return;
@@ -14455,7 +14466,7 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params)
{ {
const auto calib_input_shaping_name = wxString::Format(L"Input shaping Frequency test"); const auto calib_input_shaping_name = wxString::Format(L"Input shaping Frequency test");
new_project(false, false, calib_input_shaping_name); 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) if (params.mode != CalibMode::Calib_Input_shaping_freq)
return; return;
@@ -14521,7 +14532,7 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params)
{ {
const auto calib_input_shaping_name = wxString::Format(L"Input shaping Damping test"); const auto calib_input_shaping_name = wxString::Format(L"Input shaping Damping test");
new_project(false, false, calib_input_shaping_name); 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) if (params.mode != CalibMode::Calib_Input_shaping_damp)
return; return;
@@ -14586,7 +14597,7 @@ void Plater::Calib_Cornering(const Calib_Params& params)
{ {
const auto Calib_Cornering = wxString::Format(L"Cornering test"); const auto Calib_Cornering = wxString::Format(L"Cornering test");
new_project(false, false, Calib_Cornering); 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) if (params.mode != CalibMode::Calib_Cornering)
return; return;
@@ -14719,7 +14730,7 @@ void Plater::load_gcode(const wxString& filename)
//p->gcode_result.reset(); //p->gcode_result.reset();
//reset_gcode_toolpaths(); //reset_gcode_toolpaths();
p->preview->reload_print(m_only_gcode); 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->set_current_panel(p->preview, true);
p->get_current_canvas3D()->render(); p->get_current_canvas3D()->render();
//p->notification_manager->bbl_show_plateinfo_notification(into_u8(_L("Preview only mode for gcode file."))); //p->notification_manager->bbl_show_plateinfo_notification(into_u8(_L("Preview only mode for gcode file.")));
@@ -15392,7 +15403,7 @@ LoadType determine_load_type(std::string filename, std::string override_setting)
wxGetApp().app_config->set("import_project_action", std::to_string(choice)); wxGetApp().app_config->set("import_project_action", std::to_string(choice));
// BBS: jump to plater panel // BBS: jump to plater panel
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
return load_type; return load_type;
} }
@@ -15621,7 +15632,7 @@ void Plater::reset_with_confirm()
.ShowModal() == wxID_YES) { .ShowModal() == wxID_YES) {
reset(); reset();
// BBS: jump to plater panel // BBS: jump to plater panel
wxGetApp().mainframe->select_tab(size_t(0)); wxGetApp().mainframe->select_tab(TAB_ID_HOME);
} }
} }
@@ -17435,7 +17446,7 @@ int Plater::export_config_3mf(int plate_idx, Export3mfProgressFn proFn)
//BBS //BBS
void Plater::send_calibration_job_finished(wxCommandEvent & evt) 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; auto calibration_panel = p->main_frame->m_calibration;
if (calibration_panel) { if (calibration_panel) {
auto curr_wizard = static_cast<CalibrationWizard*>(calibration_panel->get_tabpanel()->GetPage(evt.GetInt())); auto curr_wizard = static_cast<CalibrationWizard*>(calibration_panel->get_tabpanel()->GetPage(evt.GetInt()));
@@ -17467,7 +17478,7 @@ void Plater::print_job_finished(wxCommandEvent &evt)
if (!dev) return; if (!dev) return;
dev->set_selected_machine(evt.GetString().ToStdString()); 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 //jump to monitor and select device status panel
MonitorPanel* curr_monitor = p->main_frame->m_monitor; MonitorPanel* curr_monitor = p->main_frame->m_monitor;
if(curr_monitor) if(curr_monitor)
@@ -17482,7 +17493,7 @@ void Plater::send_job_finished(wxCommandEvent& evt)
send_gcode_finish(evt.GetString()); send_gcode_finish(evt.GetString());
p->hide_send_to_printer_dlg(); 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 ////jump to monitor and select device status panel
//MonitorPanel* curr_monitor = p->main_frame->m_monitor; //MonitorPanel* curr_monitor = p->main_frame->m_monitor;
//if (curr_monitor) //if (curr_monitor)
@@ -18376,7 +18387,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")); MessageDialog dlg(this, content, title, wxOK | wxFORWARD | wxICON_WARNING, _L("Device Page"));
auto result = dlg.ShowModal(); auto result = dlg.ShowModal();
if (result == wxFORWARD) { if (result == wxFORWARD) {
wxGetApp().mainframe->select_tab(size_t(MainFrame::tpMonitor)); wxGetApp().mainframe->select_tab(TAB_ID_MONITOR);
} }
} }
+2 -2
View File
@@ -290,7 +290,7 @@ public:
Plater(const Plater &) = delete; Plater(const Plater &) = delete;
Plater &operator=(Plater &&) = delete; Plater &operator=(Plater &&) = delete;
Plater &operator=(const Plater &) = delete; Plater &operator=(const Plater &) = delete;
~Plater() = default; ~Plater();
bool Show(bool show = true); bool Show(bool show = true);
@@ -1020,4 +1020,4 @@ wxArrayString get_all_camera_view_type();
} // namespace GUI } // namespace GUI
} // namespace Slic3r } // namespace Slic3r
#endif #endif
+1 -34
View File
@@ -15,39 +15,6 @@ namespace Slic3r { namespace GUI {
namespace { 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 // 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 // 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 // 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() void PluginWebDialog::add_user_scripts()
{ {
if (wxWebView* wv = browser()) { 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); wv->AddUserScript(ORCA_BRIDGE_JS);
} }
} }
+16 -1
View File
@@ -1748,11 +1748,26 @@ void PreferencesDialog::create_items()
g_sizer->Add(item_pop_up_filament_map_dialog); g_sizer->Add(item_pop_up_filament_map_dialog);
#endif #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)); g_sizer->AddSpacer(FromDIP(10));
sizer_page->Add(g_sizer, 0, wxEXPAND); sizer_page->Add(g_sizer, 0, wxEXPAND);
////////////////////////// //////////////////////////
//// CONTROL TAB //// CONTROL TAB
///////////////////////////////////// /////////////////////////////////////
m_pref_tabs->AppendItem(_L("Control")); m_pref_tabs->AppendItem(_L("Control"));
f_sizers.push_back(new wxFlexGridSizer(1, 1, v_gap, 0)); f_sizers.push_back(new wxFlexGridSizer(1, 1, v_gap, 0));
+1 -1
View File
@@ -1042,7 +1042,7 @@ bool PlaterPresetComboBox::switch_to_tab()
//BBS Select NoteBook Tab params //BBS Select NoteBook Tab params
if (tab->GetParent() == wxGetApp().params_panel()) if (tab->GetParent() == wxGetApp().params_panel())
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor); wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
else { else {
wxGetApp().params_dialog()->Popup(); wxGetApp().params_dialog()->Popup();
tab->OnActivate(); tab->OnActivate();
+1 -1
View File
@@ -39,7 +39,7 @@ public:
PresetComboBox(wxWindow* parent, Preset::Type preset_type, const wxSize& size = wxDefaultSize, PresetBundle* preset_bundle = nullptr); PresetComboBox(wxWindow* parent, Preset::Type preset_type, const wxSize& size = wxDefaultSize, PresetBundle* preset_bundle = nullptr);
~PresetComboBox(); ~PresetComboBox();
enum LabelItemType { enum LabelItemType : std::size_t {
LABEL_ITEM_PHYSICAL_PRINTER = 0xffffff01, LABEL_ITEM_PHYSICAL_PRINTER = 0xffffff01,
LABEL_ITEM_PRINTER_MODELS, LABEL_ITEM_PRINTER_MODELS,
LABEL_ITEM_DISABLED, LABEL_ITEM_DISABLED,
+51 -10
View File
@@ -74,7 +74,18 @@ ProjectPanel::ProjectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos,
Fit(); 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> // Helper to convert newlines to <br>
static std::string convert_newlines_to_br(const std::string& text) { 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) 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 update_type;
std::string license; std::string license;
std::string model_name; std::string model_name;
@@ -115,6 +136,9 @@ void ProjectPanel::on_reload(wxCommandEvent& evt)
std::map<std::string, std::vector<json>> files; std::map<std::string, std::vector<json>> files;
if (wxGetApp().plater() == nullptr)
return;
Model model = wxGetApp().plater()->model(); Model model = wxGetApp().plater()->model();
auto model_info = model.model_info; 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()); std::string file_path = encode_path(wxGetApp().plater()->model().get_auxiliary_file_temp_path().c_str());
if (!file_path.empty()) { if (!file_path.empty()) {
files = Reload(file_path); 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 { } else {
clear_model_info(); clear_model_info();
return; return;
@@ -215,15 +246,18 @@ void ProjectPanel::on_reload(wxCommandEvent& evt)
json m_Res = json::object(); json m_Res = json::object();
m_Res["command"] = "show_3mf_info"; 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; m_Res["model"] = j;
wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore)); wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore));
if (m_web_init_completed) { if (m_web_init_completed.load(std::memory_order_acquire) &&
wxGetApp().CallAfter([this, strJS] { !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()); RunScript(strJS.ToStdString());
}); });
} }
}); });
} }
@@ -264,7 +298,7 @@ void ProjectPanel::OnScriptMessage(wxWebViewEvent& evt)
} }
} }
else if (strCmd == "request_3mf_info") { 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") { else if (strCmd == "edit_project_info") {
show_info_editor(true); show_info_editor(true);
@@ -307,13 +341,20 @@ void ProjectPanel::update_model_data()
void ProjectPanel::clear_model_info() 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(); json m_Res = json::object();
m_Res["command"] = "clear_3mf_info"; 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)); 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()); RunScript(strJS.ToStdString());
}); });
} }
+8 -2
View File
@@ -26,9 +26,11 @@
#include "nlohmann/json.hpp" #include "nlohmann/json.hpp"
#include "slic3r/Utils/json_diff.hpp" #include "slic3r/Utils/json_diff.hpp"
#include <atomic>
#include <map> #include <map>
#include <vector> #include <vector>
#include <memory> #include <memory>
#include <boost/thread.hpp>
#include "Event.hpp" #include "Event.hpp"
#include "libslic3r/ProjectTask.hpp" #include "libslic3r/ProjectTask.hpp"
#include "wxExtensions.hpp" #include "wxExtensions.hpp"
@@ -60,14 +62,17 @@ struct project_file{
class ProjectPanel : public wxPanel class ProjectPanel : public wxPanel
{ {
private: private:
bool m_web_init_completed = {false}; std::atomic<bool> m_web_init_completed{false};
bool m_reload_already = {false}; bool m_reload_already = {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}; wxWebView* m_browser = {nullptr};
AuxiliaryPanel* m_auxiliary{nullptr}; AuxiliaryPanel* m_auxiliary{nullptr};
wxString m_project_home_url; wxString m_project_home_url;
wxString m_root_dir; 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); void show_info_editor(bool show);
@@ -75,6 +80,7 @@ private:
public: public:
ProjectPanel(wxWindow *parent, wxWindowID id = wxID_ANY, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize, long style = wxTAB_TRAVERSAL); ProjectPanel(wxWindow *parent, wxWindowID id = wxID_ANY, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize, long style = wxTAB_TRAVERSAL);
~ProjectPanel(); ~ProjectPanel();
void shutdown();
void onWebNavigating(wxWebViewEvent& evt); void onWebNavigating(wxWebViewEvent& evt);
+2 -2
View File
@@ -1088,8 +1088,8 @@ void SelectMachineDialog::sync_ams_mapping_result(std::vector<FilamentInfo> &res
} }
} }
relayout_nozzle_cards(); relayout_nozzle_cards();
auto tab_index = (MainFrame::TabPosition) dynamic_cast<Notebook *>(wxGetApp().tab_panel())->GetSelection(); wxString tab_name = wxGetApp().tab_panel()->GetSelectedPageName();
if (tab_index == MainFrame::TabPosition::tp3DEditor || tab_index == MainFrame::TabPosition::tpPreview) { if (tab_name == TAB_ID_PREPARE || tab_name == TAB_ID_PREVIEW) {
updata_thumbnail_data_after_connected_printer(); updata_thumbnail_data_after_connected_printer();
} }
} }
+2 -2
View File
@@ -1218,8 +1218,8 @@ void SyncAmsInfoDialog::sync_ams_mapping_result(std::vector<FilamentInfo> &resul
iter++; iter++;
} }
} }
auto tab_index = (MainFrame::TabPosition) dynamic_cast<Notebook *>(wxGetApp().tab_panel())->GetSelection(); wxString tab_name = wxGetApp().tab_panel()->GetSelectedPageName();
if (tab_index == MainFrame::TabPosition::tp3DEditor || tab_index == MainFrame::TabPosition::tpPreview) { if (tab_name == TAB_ID_PREPARE || tab_name == TAB_ID_PREVIEW) {
updata_thumbnail_data_after_connected_printer(); updata_thumbnail_data_after_connected_printer();
} }
} }
+8 -4
View File
@@ -2790,7 +2790,7 @@ void TabPrint::build()
optgroup->append_single_option_line("fill_multiline", "strength_settings_infill#fill-multiline"); 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("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("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("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("sparse_infill_rotate_template", "strength_settings_infill_rotation_template_metalanguage");
optgroup->append_single_option_line("skin_infill_density", "strength_settings_patterns#locked-zag"); optgroup->append_single_option_line("skin_infill_density", "strength_settings_patterns#locked-zag");
@@ -6458,7 +6458,7 @@ void Tab::load_current_preset()
std::string bmp_name = tab->type() == Slic3r::Preset::TYPE_FILAMENT ? "spool" : std::string bmp_name = tab->type() == Slic3r::Preset::TYPE_FILAMENT ? "spool" :
tab->type() == Slic3r::Preset::TYPE_SLA_MATERIAL ? "" : "cog"; tab->type() == Slic3r::Preset::TYPE_SLA_MATERIAL ? "" : "cog";
tab->Hide(); // #ys_WORKAROUND : Hide tab before inserting to avoid unwanted rendering of the tab 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 else
#endif #endif
@@ -8537,8 +8537,12 @@ void Page::activate(ConfigOptionMode mode, std::function<void()> throw_if_cancel
#ifdef __WXMSW__ #ifdef __WXMSW__
// BBS: fix field control position // BBS: fix field control position
wxTheApp->CallAfter([this]() { wxTheApp->CallAfter([wp = std::weak_ptr<Page>(shared_from_this())]() {
for (auto group : m_optgroups) { auto page = wp.lock();
if (!page)
return;
for (auto group : page->m_optgroups) {
if (group->custom_ctrl) if (group->custom_ctrl)
group->custom_ctrl->fixup_items_positions(); group->custom_ctrl->fixup_items_positions();
} }
+4 -22
View File
@@ -108,7 +108,7 @@ public:
// by this control) and show it immediately. // by this control) and show it immediately.
bool ShowNewPage(wxWindow * page) bool ShowNewPage(wxWindow * page)
{ {
return AddPage(page, wxString(), ""/*true *//* select it */); return AddPage(page, wxString());
} }
// Set effect to use for showing/hiding pages. // Set effect to use for showing/hiding pages.
@@ -139,14 +139,13 @@ public:
// Implement base class pure virtual methods. // Implement base class pure virtual methods.
// adds a new page to the control
bool AddPage(wxWindow* page, bool AddPage(wxWindow* page,
const wxString& text, const wxString& text,
const std::string& bmp_name, bool bSelect = false,
bool bSelect = false) int imageId = NO_IMAGE) override
{ {
DoInvalidateBestSize(); DoInvalidateBestSize();
return InsertNewPage(GetPageCount(), page, text, bmp_name, bSelect); return InsertPage(GetPageCount(), page, text, bSelect, imageId);
} }
//// Page management //// Page management
@@ -167,23 +166,6 @@ public:
return true; 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)
{ {
if (!wxBookCtrlBase::RemovePage(n)) if (!wxBookCtrlBase::RemovePage(n))
+7 -16
View File
@@ -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()) { this->active_icon = ScalableBitmap();
// BBS set button icon default size to 20 this->active_icon.bmp() = icon;
this->inactive_icon = ScalableBitmap(this, icon.ToStdString(), this->active_icon.px_cnt()); messureSize();
} else {
this->inactive_icon = ScalableBitmap();
}
Refresh(); Refresh();
} }
@@ -257,12 +254,10 @@ void Button::SetStyle(const ButtonStyle style, const ButtonType type)
void Button::Rescale() 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(); this->active_icon.msw_rescale();
if (this->inactive_icon.bmp().IsOk())
this->inactive_icon.msw_rescale();
messureSize(); messureSize();
if(m_has_style) if(m_has_style)
@@ -293,11 +288,7 @@ void Button::render(wxDC& dc)
wxSize szIcon; wxSize szIcon;
wxSize textSize = this->textSize.GetSize(); wxSize textSize = this->textSize.GetSize();
ScalableBitmap icon; const ScalableBitmap& icon = active_icon;
if (m_selected || ((states & (int)StateColor::State::Hovered) != 0))
icon = active_icon;
else
icon = inactive_icon;
wxSize padding = this->paddingSize; wxSize padding = this->paddingSize;
int spacing = 5; int spacing = 5;
// Wrap text // Wrap text
+1 -3
View File
@@ -34,7 +34,6 @@ class Button : public StaticBox
wxSize minSize; // set by outer wxSize minSize; // set by outer
wxSize paddingSize; wxSize paddingSize;
ScalableBitmap active_icon; ScalableBitmap active_icon;
ScalableBitmap inactive_icon;
StateColor text_color; StateColor text_color;
@@ -61,8 +60,7 @@ public:
bool SetFont(const wxFont& font) override; bool SetFont(const wxFont& font) override;
void SetIcon(const wxString& icon); void SetIcon(const wxString& icon);
void SetIcon(const wxBitmap& icon);
void SetInactiveIcon(const wxString& icon);
void SetMinSize(const wxSize& size) override; void SetMinSize(const wxSize& size) override;
void SetMaxSize(const wxSize& size) override; void SetMaxSize(const wxSize& size) override;
+41 -13
View File
@@ -57,18 +57,6 @@ std::string host_theme_vars_css()
return s; 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 // 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 // style's contents and update data-orca-theme. Everything downstream (theme.css
// tokens, plugin element defaults, page layout) re-cascades from these values. // tokens, plugin element defaults, page layout) re-cascades from these values.
@@ -87,6 +75,46 @@ if(document.documentElement)
} // namespace } // 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, std::string WebViewHostDialog::document_start_injector(const std::string& markup,
const char* dom_id, const char* dom_id,
const char* position, const char* position,
@@ -244,7 +272,7 @@ void WebViewHostDialog::register_theme_user_scripts()
// script message handler is registered separately (AddScriptMessageHandler), but on // script message handler is registered separately (AddScriptMessageHandler), but on
// some backends RemoveAllUserScripts() drops it too, which would break // some backends RemoveAllUserScripts() drops it too, which would break
// window.wx.postMessage / HandleStudio. Live re-theme goes through apply_theme_live(). // 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(); add_user_scripts();
} }
@@ -49,6 +49,10 @@ public:
const std::string& prelude = {}, const std::string& prelude = {},
const std::string& on_inject = {}); 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: protected:
wxWebView* browser() const { return m_browser; } wxWebView* browser() const { return m_browser; }
+1 -1
View File
@@ -368,7 +368,7 @@ void PrintHostJobQueue::priv::perform_job(PrintHostJob the_job)
emit_progress(100); emit_progress(100);
if (the_job.switch_to_device_tab) { if (the_job.switch_to_device_tab) {
const auto mainframe = GUI::wxGetApp().mainframe; const auto mainframe = GUI::wxGetApp().mainframe;
mainframe->request_select_tab(MainFrame::TabPosition::tpMonitor); mainframe->request_select_tab(TAB_ID_MONITOR);
} }
} }
} }
+1 -1
View File
@@ -325,7 +325,7 @@ bool SimplyPrint::do_temp_upload(const boost::filesystem::path& file_path,
wxLaunchDefaultBrowser(url); wxLaunchDefaultBrowser(url);
} else { } else {
const auto mainframe = GUI::wxGetApp().mainframe; 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); mainframe->load_printer_url(url);
} }
+16 -14
View File
@@ -31,6 +31,7 @@
#include "PyPluginPackage.hpp" #include "PyPluginPackage.hpp"
#include "PyPluginTrampoline.hpp" #include "PyPluginTrampoline.hpp"
#include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp" #include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp"
#include "pluginTypes/pages/PagesPluginCapability.hpp"
#include "pluginTypes/script/ScriptPluginCapability.hpp" #include "pluginTypes/script/ScriptPluginCapability.hpp"
#include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp" #include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp"
@@ -331,17 +332,17 @@ void bind_python_api(pybind11::module_& m)
{ {
m.doc() = "OrcaSlicer plugin API"; m.doc() = "OrcaSlicer plugin API";
auto pluginTypes = py::enum_<PluginCapabilityType>(m, "PluginType", "Available plugin capability groups") py::enum_<PluginCapabilityType>(m, "PluginType", "Available plugin capability groups")
.value("PrinterConnection", PluginCapabilityType::PrinterConnection) .value("PrinterConnection", PluginCapabilityType::PrinterConnection)
.value("Automation", PluginCapabilityType::Automation) .value("Pages", PluginCapabilityType::Pages)
.value("Analysis", PluginCapabilityType::Analysis) .value("Analysis", PluginCapabilityType::Analysis)
.value("Importer", PluginCapabilityType::Importer) .value("Importer", PluginCapabilityType::Importer)
.value("Exporter", PluginCapabilityType::Exporter) .value("Exporter", PluginCapabilityType::Exporter)
.value("Visualization", PluginCapabilityType::Visualization) .value("Visualization", PluginCapabilityType::Visualization)
.value("Script", PluginCapabilityType::Script) .value("Script", PluginCapabilityType::Script)
.value("SlicingPipeline", PluginCapabilityType::SlicingPipeline) .value("SlicingPipeline", PluginCapabilityType::SlicingPipeline)
.value("Unknown", PluginCapabilityType::Unknown) .value("Unknown", PluginCapabilityType::Unknown)
.export_values(); .export_values();
py::enum_<PluginResult>(m, "PluginResult", "Execution summary code") py::enum_<PluginResult>(m, "PluginResult", "Execution summary code")
.value("Success", PluginResult::Success) .value("Success", PluginResult::Success)
@@ -431,9 +432,10 @@ void bind_python_api(pybind11::module_& m)
BOOST_LOG_TRIVIAL(debug) << "Registering embedded Python plugin type bindings"; BOOST_LOG_TRIVIAL(debug) << "Registering embedded Python plugin type bindings";
// Make sure you register your bindings here // Make sure you register your bindings here
PrinterAgentPluginCapability::RegisterBindings(m, pluginTypes); PrinterAgentPluginCapability::RegisterBindings(m);
ScriptPluginCapability::RegisterBindings(m, pluginTypes); PagesPluginCapability::RegisterBindings(m);
SlicingPipelinePluginCapability::RegisterBindings(m, pluginTypes); ScriptPluginCapability::RegisterBindings(m);
SlicingPipelinePluginCapability::RegisterBindings(m);
PluginHost::RegisterBindings(m); PluginHost::RegisterBindings(m);
BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings"; BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings";
+5 -5
View File
@@ -12,7 +12,7 @@
namespace Slic3r { 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 struct PluginCapabilityId
{ {
@@ -39,7 +39,7 @@ inline std::string plugin_capability_type_to_string(PluginCapabilityType type)
{ {
switch (type) { switch (type) {
case PluginCapabilityType::PrinterConnection: return "printer-connection"; case PluginCapabilityType::PrinterConnection: return "printer-connection";
case PluginCapabilityType::Automation: return "automation"; case PluginCapabilityType::Pages: return "pages";
case PluginCapabilityType::Analysis: return "analysis"; case PluginCapabilityType::Analysis: return "analysis";
case PluginCapabilityType::Importer: return "importer"; case PluginCapabilityType::Importer: return "importer";
case PluginCapabilityType::Exporter: return "exporter"; case PluginCapabilityType::Exporter: return "exporter";
@@ -54,7 +54,7 @@ inline std::string plugin_capability_type_display_name(PluginCapabilityType type
{ {
switch (type) { switch (type) {
case PluginCapabilityType::PrinterConnection: return "Printer connection"; case PluginCapabilityType::PrinterConnection: return "Printer connection";
case PluginCapabilityType::Automation: return "Automation"; case PluginCapabilityType::Pages: return "Pages";
case PluginCapabilityType::Analysis: return "Analysis"; case PluginCapabilityType::Analysis: return "Analysis";
case PluginCapabilityType::Importer: return "Importer"; case PluginCapabilityType::Importer: return "Importer";
case PluginCapabilityType::Exporter: return "Exporter"; case PluginCapabilityType::Exporter: return "Exporter";
@@ -76,8 +76,8 @@ inline PluginCapabilityType plugin_capability_type_from_string(std::string_view
if (lowered == "printer-connection") if (lowered == "printer-connection")
return PluginCapabilityType::PrinterConnection; return PluginCapabilityType::PrinterConnection;
if (lowered == "automation") if (lowered == "pages")
return PluginCapabilityType::Automation; return PluginCapabilityType::Pages;
if (lowered == "analysis") if (lowered == "analysis")
return PluginCapabilityType::Analysis; return PluginCapabilityType::Analysis;
if (lowered == "importer") if (lowered == "importer")
+469
View File
@@ -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
+89
View File
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,59 @@
#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(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {},
PYBIND11_OVERRIDE,
std::string,
PagesPluginCapability,
get_icon);
}
std::string get_ui() override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {},
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(::Slic3r::PluginAuditManager::AuditMode::Loading);
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
@@ -13,10 +13,8 @@ namespace py = pybind11;
namespace Slic3r { 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"); auto printer_agent_module = module.def_submodule("printer_agent", "Printer Agent API");
py::enum_<FilamentSyncMode>(printer_agent_module, "FilamentSyncMode") py::enum_<FilamentSyncMode>(printer_agent_module, "FilamentSyncMode")
@@ -19,7 +19,7 @@ namespace Slic3r {
class PrinterAgentPluginCapability : public PluginCapabilityInterface, public IPrinterAgent class PrinterAgentPluginCapability : public PluginCapabilityInterface, public IPrinterAgent
{ {
public: 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; } PluginCapabilityType get_type() const override { return PluginCapabilityType::PrinterConnection; }
@@ -9,9 +9,8 @@
namespace py = pybind11; namespace py = pybind11;
namespace Slic3r { 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"; BOOST_LOG_TRIVIAL(debug) << "Registering orca.script bindings";
auto script = module.def_submodule("script", "Script Plugins API"); auto script = module.def_submodule("script", "Script Plugins API");
@@ -11,8 +11,7 @@ public:
virtual ExecutionResult execute() = 0; virtual ExecutionResult execute() = 0;
static void RegisterBindings(pybind11::module_ &module, static void RegisterBindings(pybind11::module_ &module);
pybind11::enum_<PluginCapabilityType> &pluginTypes);
}; };
} // namespace Slic3r } // namespace Slic3r
@@ -8,8 +8,7 @@ namespace Slic3r {
bool SlicingPipelineContext::cancelled() const { return print && print->canceled(); } bool SlicingPipelineContext::cancelled() const { return print && print->canceled(); }
void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module, py::enum_<PluginCapabilityType>& pluginTypes) { void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module) {
(void) pluginTypes; // unused: this capability defines its own Step enum (below) rather than extending the shared PluginCapabilityType enum.
auto slicing = module.def_submodule("slicing", "Slicing pipeline API (research/experimental)."); auto slicing = module.def_submodule("slicing", "Slicing pipeline API (research/experimental).");
py::enum_<SlicingPipelineStepPlugin>(slicing, "Step") py::enum_<SlicingPipelineStepPlugin>(slicing, "Step")
@@ -37,7 +37,7 @@ public:
// Runs on the slicing worker thread. Do not call orca.host.ui.* here: the UI thread can be // 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. // blocked waiting on the slicing worker, so a marshaled UI call from this thread can deadlock.
virtual ExecutionResult execute(SlicingPipelineContext& ctx) = 0; virtual ExecutionResult execute(SlicingPipelineContext& ctx) = 0;
static void RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes); static void RegisterBindings(pybind11::module_& module);
}; };
} // namespace Slic3r } // namespace Slic3r
+287
View File
@@ -698,3 +698,290 @@ TEST_CASE("Solid infill direction offsets every layer when no template is set",
CHECK(delta == 30); 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. };
};
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 (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 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 = [&region](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);
REQUIRE(smooth.path_count <= sharp.path_count);
// The outlines are still rounded.
REQUIRE(smooth.point_count > sharp.point_count);
REQUIRE(smooth.sharp_turns < sharp.sharp_turns);
}
+1
View File
@@ -18,6 +18,7 @@ add_executable(${_TEST_NAME}_tests
test_preset_setting_id.cpp test_preset_setting_id.cpp
test_preset_diff.cpp test_preset_diff.cpp
test_elephant_foot_compensation.cpp test_elephant_foot_compensation.cpp
test_fill_corner_smoothing.cpp
test_fill_plane_path.cpp test_fill_plane_path.cpp
test_geometry.cpp test_geometry.cpp
test_multimaterial_segmentation.cpp test_multimaterial_segmentation.cpp
@@ -0,0 +1,173 @@
#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());
}
+54
View File
@@ -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 path_length(const Points &points)
{ {
double length = 0.; 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); 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]") TEST_CASE("Hilbert curve smooth factor controls corner curvature", "[FillPlanePath]")
{ {
const Points sharp = TestableHilbertCurve().generate_points(0.005); const Points sharp = TestableHilbertCurve().generate_points(0.005);