mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-12 18:31:18 +00:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b29f06a0ab | ||
|
|
9ec0d39592 | ||
|
|
c55f9c2251 | ||
|
|
d2675f15d1 | ||
|
|
5371c85362 | ||
|
|
0b9b05ed85 | ||
|
|
a243941b72 | ||
|
|
6a1a5d51e9 | ||
|
|
e625845b34 | ||
|
|
01b340dee1 | ||
|
|
4c2a63e11e | ||
|
|
f8cf34c55b | ||
|
|
00d11a7ba0 | ||
|
|
546b9c621d | ||
|
|
a13089d357 | ||
|
|
fb6c93963d | ||
|
|
7688f7a16c |
@@ -0,0 +1,65 @@
|
||||
name: clang-tidy fix
|
||||
|
||||
# Makes the missing-include fix that /bot fix-tidy (pr-tidy-bot.yml) posts on a
|
||||
# pull request: scripts/run_clang_tidy.sh --fix on the merge commit the
|
||||
# clang-tidy check runs on, uploaded as the clang-tidy-fix artifact.
|
||||
#
|
||||
# Every push to a PR starts this workflow, and its job skips on that first
|
||||
# attempt. The bot re-runs it on request. A re-run keeps the pull_request
|
||||
# context, so the PR's code runs with a read-only token, no secrets and the
|
||||
# PR's own cache, never main's, whoever asked for it.
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
fix:
|
||||
if: github.run_attempt > 1
|
||||
# A hosted runner, so nothing the PR's code leaves behind outlives the job.
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
lfs: 'false'
|
||||
# The PR merge commit plus its first parent, the base it is diffed against.
|
||||
fetch-depth: 2
|
||||
persist-credentials: false
|
||||
|
||||
# Parsing needs the dependency headers, not a build of this PR's deps/, so
|
||||
# a PR that changes deps/ falls back to the newest cache main has.
|
||||
- name: Restore cached deps
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
|
||||
key: linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }}
|
||||
restore-keys: linux-clang-cache-orcaslicer_deps-build-
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Apt-Install Dependencies
|
||||
uses: ./.github/actions/apt-install-deps
|
||||
|
||||
# The status says whether findings remain that --fix cannot fix.
|
||||
- name: Fix the changed lines
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/clang-tidy-fix"
|
||||
status=0
|
||||
scripts/run_clang_tidy.sh --base HEAD^1 --yes --fix || status=$?
|
||||
echo "$status" > "$RUNNER_TEMP/clang-tidy-fix/status"
|
||||
git -c core.quotePath=false diff --no-color --no-ext-diff --src-prefix=a/ --dst-prefix=b/ \
|
||||
> "$RUNNER_TEMP/clang-tidy-fix/fix.patch"
|
||||
|
||||
- name: Upload the fix
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: clang-tidy-fix
|
||||
path: ${{ runner.temp }}/clang-tidy-fix/
|
||||
retention-days: 7
|
||||
# A later re-run of the same run replaces the earlier fix.
|
||||
overwrite: true
|
||||
@@ -0,0 +1,476 @@
|
||||
name: PR clang-tidy Bot
|
||||
|
||||
# Helps a pull request past the clang-tidy check.
|
||||
#
|
||||
# When the check fails on a PR's changes, a comment asks the author to fix it,
|
||||
# either with scripts/run_clang_tidy.sh --fix or by commenting:
|
||||
# /bot fix-tidy
|
||||
#
|
||||
# That command, from the PR's author or an account with write access, re-runs
|
||||
# the PR's clang-tidy fix run (clang_tidy_fix.yml), which makes the fix in the
|
||||
# PR's own context. When it finishes, the fix is posted as one review: each
|
||||
# include whose place falls on a line the PR's diff shows is a suggestion GitHub
|
||||
# can commit in one click, and the review's body holds the whole patch with a
|
||||
# git apply command. Findings --fix cannot fix, such as a using in a header,
|
||||
# are left for the check to report.
|
||||
#
|
||||
# No code from the PR runs here. Nothing is pushed, and the patch is only
|
||||
# posted when every line it adds is an #include in a C++ file the PR changes.
|
||||
#
|
||||
# workflow_run matches the workflows by their name: keep "clang-tidy" and
|
||||
# "clang-tidy fix" in step with clang_tidy.yml and clang_tidy_fix.yml, and
|
||||
# TIDY_STEP below with the clang-tidy step's name in clang_tidy.yml.
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types:
|
||||
- created
|
||||
workflow_run:
|
||||
workflows:
|
||||
- clang-tidy
|
||||
- clang-tidy fix
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
request:
|
||||
# Skips the job unless a PR comment mentions the command.
|
||||
if: >-
|
||||
github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||
&& github.event_name == 'issue_comment'
|
||||
&& github.event.issue.pull_request != null
|
||||
&& contains(github.event.comment.body, '/bot fix-tidy')
|
||||
permissions:
|
||||
actions: write # re-run the fix run
|
||||
pull-requests: write # react to and answer the comment, which is the PR's
|
||||
issues: write # feedback comment + reactions
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Re-run the PR's clang-tidy fix
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const MARKER = '<!-- pr-tidy-bot -->';
|
||||
const FIX_WORKFLOW = 'clang_tidy_fix.yml';
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const issue = context.payload.issue;
|
||||
const comment = context.payload.comment;
|
||||
|
||||
// Ignores a comment whose sender is not its author.
|
||||
if (context.payload.sender.login !== comment.user.login || comment.user.type !== 'User') {
|
||||
core.info('Ignoring a comment not written by its sender, or by a bot.');
|
||||
return;
|
||||
}
|
||||
|
||||
const commandLine = (comment.body || '')
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.find((line) => /^\/bot\s+fix-tidy\b/i.test(line));
|
||||
if (!commandLine) {
|
||||
core.info('No /bot fix-tidy command found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const commenter = comment.user.login;
|
||||
// Answers only the PR's author and maintainers, so the command cannot be used to spam.
|
||||
if (commenter !== issue.user.login) {
|
||||
const { data: access } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username: commenter });
|
||||
if (!['admin', 'write'].includes(access.permission)) {
|
||||
core.info(`Ignoring /bot fix-tidy from @${commenter}: not the PR's author and no write access.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const react = (content) =>
|
||||
github.rest.reactions.createForIssueComment({ owner, repo, comment_id: comment.id, content });
|
||||
async function refuse(reason) {
|
||||
await react('-1');
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body: `${MARKER}\n@${commenter} ${reason}` });
|
||||
}
|
||||
|
||||
await react('eyes');
|
||||
|
||||
if (!/^\/bot\s+fix-tidy$/i.test(commandLine)) {
|
||||
return refuse('`/bot fix-tidy` takes no arguments.');
|
||||
}
|
||||
|
||||
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: issue.number });
|
||||
if (pr.state !== 'open') {
|
||||
return refuse('the PR is not open.');
|
||||
}
|
||||
|
||||
const sha = pr.head.sha.slice(0, 10);
|
||||
const { data: { workflow_runs: runs } } = await github.rest.actions.listWorkflowRuns({
|
||||
owner, repo, workflow_id: FIX_WORKFLOW, event: 'pull_request', head_sha: pr.head.sha, per_page: 1
|
||||
});
|
||||
const run = runs[0];
|
||||
// A PR last pushed before clang_tidy_fix.yml existed has no run to re-run.
|
||||
if (!run) {
|
||||
return refuse(`there is no clang-tidy fix run for ${sha}, the PR's latest commit. If you just pushed it, ` +
|
||||
'try again in a minute. Otherwise, push a commit or merge the base branch to start one, then comment `/bot fix-tidy` again.');
|
||||
}
|
||||
if (run.status !== 'completed') {
|
||||
// The first attempt is the one every push starts, and it makes no fix.
|
||||
return refuse(run.run_attempt > 1
|
||||
? `a clang-tidy fix for ${sha} is already [running](${run.html_url}). I'll post it when it finishes.`
|
||||
: `the PR's checks on ${sha} are still starting. Comment \`/bot fix-tidy\` again in a minute.`);
|
||||
}
|
||||
// GitHub refuses to re-run a run created over 30 days ago.
|
||||
try {
|
||||
await github.rest.actions.reRunWorkflow({ owner, repo, run_id: run.id });
|
||||
} catch (error) {
|
||||
core.warning(`Re-running ${run.html_url} failed: ${error.message}`);
|
||||
return refuse(`GitHub won't re-run the [clang-tidy fix run](${run.html_url}) for ${sha} (${error.message}). ` +
|
||||
'Push a commit or merge the base branch to start a new one, then comment `/bot fix-tidy` again.');
|
||||
}
|
||||
core.info(`Re-ran ${run.html_url}`);
|
||||
|
||||
suggest:
|
||||
# The fix run's re-runs only. Its first attempt makes no fix.
|
||||
if: >-
|
||||
github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||
&& github.event_name == 'workflow_run'
|
||||
&& github.event.workflow_run.name == 'clang-tidy fix'
|
||||
&& github.event.workflow_run.event == 'pull_request'
|
||||
&& github.event.workflow_run.run_attempt > 1
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-suggest-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: false
|
||||
permissions:
|
||||
actions: read # download the fix
|
||||
contents: read # check out the PR head
|
||||
pull-requests: write # the review
|
||||
issues: write # feedback comment + reactions
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Find the PR and who asked
|
||||
id: find
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const MARKER = '<!-- pr-tidy-bot -->';
|
||||
const CPP = /^(src|tests)\/.+\.(cpp|cc|cxx|hpp|h|hxx)$/;
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const run = context.payload.workflow_run;
|
||||
|
||||
// workflow_run lists no PR for a fork's branch, so the PR is found by its branch.
|
||||
const { data: prs } = await github.rest.pulls.list({
|
||||
owner, repo, state: 'open', head: `${run.head_repository.owner.login}:${run.head_branch}`
|
||||
});
|
||||
const pr = prs[0];
|
||||
if (!pr) {
|
||||
core.info('No open PR for this run.');
|
||||
return;
|
||||
}
|
||||
|
||||
// The latest /bot fix-tidy comment is the one this run answers.
|
||||
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr.number, per_page: 100 });
|
||||
const request = comments.reverse().find((c) => c.user.type === 'User' && /^\s*\/bot\s+fix-tidy\s*$/im.test(c.body || ''));
|
||||
const mention = request ? `@${request.user.login} ` : '';
|
||||
async function refuse(reason) {
|
||||
if (request) {
|
||||
await github.rest.reactions.createForIssueComment({ owner, repo, comment_id: request.id, content: '-1' });
|
||||
}
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body: `${MARKER}\n${mention}${reason}` });
|
||||
}
|
||||
|
||||
if (pr.head.sha !== run.head_sha) {
|
||||
return refuse('the PR changed while I was making its clang-tidy fix. Comment `/bot fix-tidy` again.');
|
||||
}
|
||||
if (run.conclusion !== 'success') {
|
||||
return refuse(`I couldn't make the clang-tidy fix. See the [run log](${run.html_url}).`);
|
||||
}
|
||||
|
||||
// The right-side lines of each changed C++ file's diff: the lines a
|
||||
// review comment can be placed on. A file too large for GitHub to
|
||||
// show its diff has none, and gets no suggestions.
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number: pr.number, per_page: 100 });
|
||||
const ranges = {};
|
||||
for (const file of files) {
|
||||
if (file.status === 'removed' || !CPP.test(file.filename)) {
|
||||
continue;
|
||||
}
|
||||
ranges[file.filename] = [...(file.patch || '').matchAll(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/gm)]
|
||||
.map((m) => [Number(m[1]), Number(m[1]) + Number(m[2] ?? 1) - 1])
|
||||
.filter(([first, last]) => last >= first);
|
||||
}
|
||||
|
||||
core.setOutput('pr', pr.number);
|
||||
core.setOutput('head-sha', pr.head.sha);
|
||||
core.setOutput('head-repo', run.head_repository.full_name);
|
||||
core.setOutput('ranges', JSON.stringify(ranges));
|
||||
core.setOutput('mention', mention);
|
||||
core.setOutput('request-id', request ? request.id : '');
|
||||
|
||||
- name: Download the fix
|
||||
if: steps.find.outputs.pr != ''
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: clang-tidy-fix
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ github.token }}
|
||||
path: ${{ runner.temp }}/clang-tidy-fix
|
||||
|
||||
# The PR's own files, only to apply the patch to. Nothing in them is run.
|
||||
- name: Checkout the PR head
|
||||
if: steps.find.outputs.pr != ''
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ steps.find.outputs.head-repo }}
|
||||
ref: ${{ steps.find.outputs.head-sha }}
|
||||
path: head
|
||||
fetch-depth: 1
|
||||
lfs: 'false'
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
src
|
||||
tests
|
||||
|
||||
# The fix was made on the PR's merge commit, so it is applied to the head
|
||||
# and diffed again, giving line numbers in the PR's own files.
|
||||
- name: Check the fix and place its suggestions
|
||||
id: place
|
||||
if: steps.find.outputs.pr != ''
|
||||
env:
|
||||
RANGES: ${{ steps.find.outputs.ranges }}
|
||||
run: |
|
||||
python3 -I - "$RUNNER_TEMP/clang-tidy-fix/fix.patch" head "$RUNNER_TEMP" <<'EOF'
|
||||
import json, os, re, subprocess, sys
|
||||
|
||||
patch_path, head, out = sys.argv[1:]
|
||||
ranges = json.loads(os.environ["RANGES"])
|
||||
|
||||
def output(**values):
|
||||
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f:
|
||||
for key, value in values.items():
|
||||
f.write(f"{key}={value}\n")
|
||||
|
||||
patch = open(patch_path, encoding="utf-8").read()
|
||||
if not patch.strip():
|
||||
output(result="empty")
|
||||
sys.exit()
|
||||
|
||||
# Every line the patch adds must be an #include or blank, in a C++ file the
|
||||
# PR changes, and it may not remove, create, rename or change the mode of
|
||||
# anything.
|
||||
include = re.compile(r'\s*#\s*include\s*(<[\w./+-]+>|"[\w./+-]+")\s*')
|
||||
header = re.compile(r"(index [0-9a-f]+\.\.[0-9a-f]+( \d+)?|@@ -\d+(,\d+)? \+\d+(,\d+)? @@.*)")
|
||||
problems = []
|
||||
path = None
|
||||
for line in patch.splitlines():
|
||||
if line.startswith("diff --git "):
|
||||
match = re.fullmatch(r"diff --git a/(.+) b/\1", line)
|
||||
path = match.group(1) if match else None
|
||||
if path not in ranges:
|
||||
problems.append(f"{line}: not a C++ file the PR changes")
|
||||
# git appends a tab to the --- and +++ lines of a path that contains a space.
|
||||
elif line.removesuffix("\t") in (f"--- a/{path}", f"+++ b/{path}") or header.fullmatch(line):
|
||||
pass
|
||||
elif line.startswith(" ") or line == "\\ No newline at end of file":
|
||||
pass
|
||||
elif line.startswith("+") and (include.fullmatch(line[1:]) or not line[1:].strip()):
|
||||
pass
|
||||
else:
|
||||
problems.append(f"{path}: {line}")
|
||||
if problems:
|
||||
for problem in problems:
|
||||
print(f"::error::Refused patch line {problem}")
|
||||
output(result="rejected")
|
||||
sys.exit()
|
||||
|
||||
if subprocess.run(["git", "-C", head, "apply", "--index", os.path.abspath(patch_path)]).returncode != 0:
|
||||
output(result="apply-failed")
|
||||
sys.exit()
|
||||
|
||||
git = ["git", "-C", head, "-c", "core.quotePath=false", "diff", "--cached", "--no-color", "--no-ext-diff",
|
||||
"--src-prefix=a/", "--dst-prefix=b/"]
|
||||
with open(os.path.join(out, "head.patch"), "w", encoding="utf-8") as f:
|
||||
f.write(subprocess.run(git, check=True, capture_output=True, encoding="utf-8").stdout)
|
||||
|
||||
# Each run of added lines becomes one suggestion on the line it goes after
|
||||
# (before, at the top of a file): that line, replaced by itself and the
|
||||
# includes. It can only sit on a line the PR's diff shows.
|
||||
hunks = []
|
||||
path = None
|
||||
for line in subprocess.run(git + ["-U0"], check=True, capture_output=True, encoding="utf-8").stdout.splitlines():
|
||||
if line.startswith("diff --git "):
|
||||
path = re.fullmatch(r"diff --git a/(.+) b/\1", line).group(1)
|
||||
elif line.startswith("@@ "):
|
||||
hunks.append((path, int(re.match(r"@@ -(\d+)", line).group(1)), []))
|
||||
elif hunks and line.startswith("+") and not line.startswith("+++ "):
|
||||
hunks[-1][2].append(line[1:])
|
||||
|
||||
suggestions = []
|
||||
total = suggested = 0
|
||||
for path, after, added in hunks:
|
||||
count = sum(bool(include.fullmatch(line)) for line in added)
|
||||
total += count
|
||||
original = subprocess.run(["git", "-C", head, "show", f"HEAD:{path}"], check=True, capture_output=True,
|
||||
encoding="utf-8").stdout.splitlines()
|
||||
line = max(after, 1)
|
||||
if line > len(original) or not any(first <= line <= last for first, last in ranges[path]):
|
||||
continue
|
||||
body = "\n".join([original[line - 1], *added] if after else [*added, original[0]])
|
||||
# A fence longer than any run of backticks in the line it repeats.
|
||||
fence = "`" * max([3] + [len(run) + 1 for run in re.findall(r"`+", body)])
|
||||
suggestions.append({"path": path, "line": line, "side": "RIGHT",
|
||||
"body": f"{fence}suggestion\n{body}\n{fence}"})
|
||||
suggested += count
|
||||
with open(os.path.join(out, "suggestions.json"), "w", encoding="utf-8") as f:
|
||||
json.dump(suggestions, f)
|
||||
output(result="ok", includes=total, suggested=suggested)
|
||||
EOF
|
||||
|
||||
- name: Post the fix
|
||||
if: always() && steps.find.outputs.pr != ''
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
PR: ${{ steps.find.outputs.pr }}
|
||||
MENTION: ${{ steps.find.outputs.mention }}
|
||||
REQUEST_ID: ${{ steps.find.outputs.request-id }}
|
||||
HEAD_SHA: ${{ steps.find.outputs.head-sha }}
|
||||
RESULT: ${{ steps.place.outputs.result }}
|
||||
INCLUDES: ${{ steps.place.outputs.includes }}
|
||||
SUGGESTED: ${{ steps.place.outputs.suggested }}
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const MARKER = '<!-- pr-tidy-bot -->';
|
||||
// GitHub's limit on a review body is 65536 characters.
|
||||
const MAX_PATCH = 60000;
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const { PR, MENTION, REQUEST_ID, HEAD_SHA, RESULT, INCLUDES, SUGGESTED, RUNNER_TEMP } = process.env;
|
||||
const issue_number = Number(PR);
|
||||
const sha = HEAD_SHA.slice(0, 10);
|
||||
const fixUrl = context.payload.workflow_run.html_url;
|
||||
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
|
||||
|
||||
const react = (content) => REQUEST_ID
|
||||
? github.rest.reactions.createForIssueComment({ owner, repo, comment_id: Number(REQUEST_ID), content })
|
||||
: Promise.resolve();
|
||||
async function reply(body, content) {
|
||||
await react(content);
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number, body: `${MARKER}\n${MENTION}${body}` });
|
||||
}
|
||||
|
||||
const read = (...parts) => fs.readFileSync(path.join(RUNNER_TEMP, ...parts), 'utf8');
|
||||
const passes = RESULT && read('clang-tidy-fix', 'status').trim() === '0';
|
||||
|
||||
if (RESULT === 'empty') {
|
||||
return passes
|
||||
? reply(`clang-tidy passes on ${sha}. There is nothing to fix.`, '+1')
|
||||
: reply(`clang-tidy has no automatic fix for what it reports on ${sha}. The findings, in the [fix run's log](${fixUrl}), need a manual edit.`, '-1');
|
||||
}
|
||||
if (RESULT === 'rejected') {
|
||||
return reply(`the clang-tidy fix changes more than includes, so I didn't post it. See the [run log](${runUrl}).`, '-1');
|
||||
}
|
||||
if (RESULT === 'apply-failed') {
|
||||
return reply(`the clang-tidy fix doesn't apply to ${sha}. Merge the base branch into the PR, then comment \`/bot fix-tidy\` again.`, '-1');
|
||||
}
|
||||
if (RESULT !== 'ok') {
|
||||
return reply(`something went wrong. See the [run log](${runUrl}).`, '-1');
|
||||
}
|
||||
|
||||
const patch = read('head.patch');
|
||||
const suggestions = JSON.parse(read('suggestions.json'));
|
||||
|
||||
const one = INCLUDES === '1';
|
||||
const includes = one ? '1 missing include' : `${INCLUDES} missing includes`;
|
||||
const inline = SUGGESTED === '0'
|
||||
? (one ? 'It goes on a line this PR does not change, so I can\'t suggest it inline.'
|
||||
: 'They go on lines this PR does not change, so I can\'t suggest them inline.')
|
||||
: SUGGESTED === INCLUDES
|
||||
? (one ? 'It is suggested inline below.' : 'Each is suggested inline below.')
|
||||
: `${SUGGESTED} of them are suggested inline below; the rest go on lines this PR does not change.`;
|
||||
const remaining = passes
|
||||
? `With ${one ? 'it' : 'them'}, clang-tidy passes.`
|
||||
: `Some findings remain that need a manual edit; see the [fix run's log](${fixUrl}).`;
|
||||
// A fence longer than any run of backticks in the patch's context lines.
|
||||
const fence = '`'.repeat(Math.max(3, ...[...patch.matchAll(/`+/g)].map((m) => m[0].length + 1)));
|
||||
const apply = patch.length > MAX_PATCH
|
||||
? `The full patch is too large to show here. Download \`fix.patch\` from the [fix run](${fixUrl}) and run \`git apply fix.patch\`.`
|
||||
: `To apply ${one ? 'it' : 'all of them'}, run this in your checkout of the PR branch:\n\n${fence}sh\ngit apply <<'EOF'\n${patch}EOF\n${fence}`;
|
||||
const body = (withSuggestions) => `${MARKER}\n${MENTION}clang-tidy's fix for ${sha} adds ${includes}. ` +
|
||||
`${withSuggestions ? `${inline} ` : ''}${remaining}\n\n${apply}`;
|
||||
|
||||
const review = { owner, repo, pull_number: issue_number, commit_id: HEAD_SHA, event: 'COMMENT' };
|
||||
try {
|
||||
await github.rest.pulls.createReview({ ...review, body: body(true), comments: suggestions });
|
||||
} catch (error) {
|
||||
// A suggestion GitHub will not place still leaves the patch.
|
||||
core.warning(`Posting the suggestions failed (${error.message}); posting the patch alone.`);
|
||||
await github.rest.pulls.createReview({ ...review, body: body(false) });
|
||||
}
|
||||
await react('+1');
|
||||
|
||||
nudge:
|
||||
if: >-
|
||||
github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||
&& github.event_name == 'workflow_run'
|
||||
&& github.event.workflow_run.name == 'clang-tidy'
|
||||
&& github.event.workflow_run.event == 'pull_request'
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-nudge-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: false
|
||||
permissions:
|
||||
actions: read # the check's steps
|
||||
pull-requests: write # comment on the PR
|
||||
issues: write # the comment
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Ask for a fix when the check fails
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const MARKER = '<!-- pr-tidy-bot-nudge -->';
|
||||
// The clang-tidy step's name in clang_tidy.yml. A failure in any other
|
||||
// step is the CI's, not the PR's.
|
||||
const TIDY_STEP = 'Run clang-tidy on the changed lines';
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const run = context.payload.workflow_run;
|
||||
|
||||
// workflow_run lists no PR for a fork's branch, so the PR is found by its branch.
|
||||
const { data: prs } = await github.rest.pulls.list({
|
||||
owner, repo, state: 'open', head: `${run.head_repository.owner.login}:${run.head_branch}`
|
||||
});
|
||||
const pr = prs.find((p) => p.head.sha === run.head_sha);
|
||||
if (!pr) {
|
||||
core.info('No open PR at this run\'s commit; a newer run will answer.');
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: { jobs } } = await github.rest.actions.listJobsForWorkflowRun({ owner, repo, run_id: run.id });
|
||||
const failed = jobs.some((job) => (job.steps || []).some((step) => step.name === TIDY_STEP && step.conclusion === 'failure'));
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr.number, per_page: 100 });
|
||||
const previous = comments.find((c) => c.user.type === 'Bot' && (c.body || '').startsWith(MARKER));
|
||||
|
||||
if (!failed) {
|
||||
// Removes the request once the check passes.
|
||||
if (previous && run.conclusion === 'success') {
|
||||
await github.rest.issues.deleteComment({ owner, repo, comment_id: previous.id });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const body = `${MARKER}\n@${pr.user.login} the clang-tidy check found problems in this PR's changes ` +
|
||||
`at ${run.head_sha.slice(0, 10)} ([log](${run.html_url})). Missing includes can be fixed either way:\n\n` +
|
||||
'- run `scripts/run_clang_tidy.sh --fix` (`scripts\\run_clang_tidy.ps1 -Fix` on Windows) and push the result, or\n' +
|
||||
'- comment `/bot fix-tidy`, and I\'ll suggest the fix here for you to apply.\n\n' +
|
||||
'Other findings, such as a `using` in a header, need a manual edit.';
|
||||
// Edits the earlier request instead of posting another, so each push does not notify again.
|
||||
if (previous) {
|
||||
await github.rest.issues.updateComment({ owner, repo, comment_id: previous.id, body });
|
||||
} else {
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body });
|
||||
}
|
||||
@@ -124,9 +124,7 @@ Control on the Global page, and from the `?` key on the page of the view that re
|
||||
the dialog should list it; the row's section heading is the `section_table` entry
|
||||
above it, so a new section needs an entry there too. Pick a default that does not
|
||||
collide inside its contexts; the `[Shortcuts]` tests check every default against the
|
||||
others. A default `KeyChord{}` leaves the action unbound: it is listed in the dialog
|
||||
for the user to bind and matches nothing until they do, which is the honest answer
|
||||
when every key its contexts can reach is already spoken for.
|
||||
others.
|
||||
2. Handle it in the dispatcher of its context: `MainFrame::handle_global_shortcut`,
|
||||
`GLCanvas3D::handle_shortcut`, `ObjectList::dispatch_shortcut`, or a gizmo's
|
||||
`on_tool_shortcut`. A gizmo that opens on a key sets `m_shortcut` in its constructor.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="28" height="27" viewBox="0 0 28 27" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path transform="matrix(0.9 0 0 0.9 1.4 1.35)" d="M11.9888 2.01601C12.8984 0.494662 15.1024 0.494662 16.012 2.01601L18.8677 6.79237C19.1958 7.34113 19.7339 7.73201 20.3572 7.87447L25.7821 9.11446C27.5101 9.50943 28.1913 11.6056 27.0254 12.9408L23.3652 17.1327C22.9449 17.6143 22.7394 18.2469 22.7964 18.8836L23.2936 24.4263C23.4519 26.1918 21.6687 27.4873 20.0386 26.7911L14.9209 24.6056C14.3329 24.3545 13.6679 24.3545 13.0799 24.6056L7.96209 26.7911C6.33196 27.4873 4.54887 26.1918 4.70722 24.4263L5.20434 18.8836C5.26145 18.2469 5.05595 17.6143 4.63544 17.1327L0.97533 12.9408C-0.19048 11.6056 0.490599 9.50943 2.21859 9.11446L7.64364 7.87447C8.2669 7.73201 8.80494 7.34113 9.13302 6.79237L11.9888 2.01601Z" stroke="#949494" stroke-width="2.4" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 876 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g transform="rotate(-45 8 8)"><path d="M8 14.4 6.5 11.2 9.5 11.2 Z" style="fill:#009688"/><path d="M6.5 7.3 6.5 11.2 8 14.4 9.5 11.2 9.5 7.3" style="fill:none;stroke:#949494;stroke-width:0.4;stroke-linejoin:round;stroke-linecap:round"/><rect x="5.5" y="3.8" width="5" height="3.5" rx="0.9" style="fill:none;stroke:#949494;stroke-width:0.4;stroke-linejoin:round"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 458 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><circle cx="5.9" cy="6.3" r="1.25" style="fill:#009688"/><path d="M3.4 11.9 6.4 7.7 8.8 10.7 10.4 8.6 12.6 11.9 Z" style="fill:#009688"/><rect x="2.375" y="2.375" width="11.25" height="11.25" rx="1.25" style="fill:none;stroke:#949494;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round"/></svg>
|
||||
|
Before Width: | Height: | Size: 386 B |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Qidi",
|
||||
"version": "02.04.00.16",
|
||||
"version": "02.04.00.17",
|
||||
"force_update": "0",
|
||||
"description": "Qidi configurations",
|
||||
"machine_model_list": [
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
"extruder_clearance_height_to_lid": "120",
|
||||
"layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nLOG_Z\nG92 E0\n",
|
||||
"machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}",
|
||||
"machine_start_gcode": "PRINT_START BED=[hot_plate_temp_initial_layer] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature]\nM83\nM140 S[hot_plate_temp_initial_layer]\nM104 S[nozzle_temperature_initial_layer]\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 3} Z0\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 6}\nG1 Z1 F600\n",
|
||||
"machine_start_gcode": "PRINT_START BED=[bed_temperature_initial_layer_single] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature]\nM83\nM140 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 3} Z0\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 6}\nG1 Z1 F600\n",
|
||||
"thumbnails_format": "PNG",
|
||||
"default_filament_profile": [
|
||||
"Generic PLA @Qidi Q1 Pro 0.4 nozzle"
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
"default_filament_profile": [
|
||||
"Generic PLA @Qidi"
|
||||
],
|
||||
"machine_start_gcode": "G28\nM140 S[hot_plate_temp_initial_layer]\nM190 S[hot_plate_temp_initial_layer]\nM109 S[nozzle_temperature_initial_layer]\nG92 E-19\nG0 Y5 Z0.3 F3600\nG1 X5 E0 F2400\n",
|
||||
"machine_start_gcode": "G28\nM140 S[bed_temperature_initial_layer_single]\nM190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nG92 E-19\nG0 Y5 Z0.3 F3600\nG1 X5 E0 F2400\n",
|
||||
"machine_end_gcode": "M104 S0\nM140 S0\n;Retract the filament\nG92 E0\nG1 E-3 F300\nG28\nM84\n",
|
||||
"scan_first_layer": "0"
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"210x210/COLPIC",
|
||||
"110x110/PNG"
|
||||
],
|
||||
"machine_start_gcode": "PRINT_START\nG28\nM141 S0\nG0 Z50 F600\nM190 S[hot_plate_temp_initial_layer]\nG28 Z\nG29; mesh bed leveling ,comment this code to close it\nG0 X0 Y0 Z50 F6000\nM141 S{overall_chamber_temperature}\nM109 S[nozzle_temperature_initial_layer]\nM106 P3 S255\nM83\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 12} E{-10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 E{10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\n",
|
||||
"machine_start_gcode": "PRINT_START\nG28\nM141 S0\nG0 Z50 F600\nM190 S[bed_temperature_initial_layer_single]\nG28 Z\nG29; mesh bed leveling ,comment this code to close it\nG0 X0 Y0 Z50 F6000\nM141 S{overall_chamber_temperature}\nM109 S[nozzle_temperature_initial_layer]\nM106 P3 S255\nM83\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 12} E{-10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 E{10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\n",
|
||||
"machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}",
|
||||
"scan_first_layer": "0"
|
||||
}
|
||||
|
||||
@@ -51,11 +51,6 @@ uniform float tex_aspect;
|
||||
uniform float rotation_rad;
|
||||
uniform vec2 uv_offset;
|
||||
uniform bool invert;
|
||||
// False means the layer places a single copy rather than repeating: outside that copy there is
|
||||
// simply no texture. DecodedHeightTexture::sample() has always returned 0 there, so the bake got
|
||||
// this right while the preview wrapped the image and showed a tiled pattern that was never going
|
||||
// to be printed.
|
||||
uniform bool tile_enabled;
|
||||
uniform float midlevel; // the height that means "don't move"; needed by the parallax step
|
||||
uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor)
|
||||
uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin
|
||||
@@ -223,14 +218,6 @@ int nearest_palette_entry(vec3 rgb)
|
||||
return best;
|
||||
}
|
||||
|
||||
// True where this uv has texture on it: everywhere when the layer repeats, and only inside the
|
||||
// single placed copy when it does not. The bake's own sampler draws the same line (see
|
||||
// DecodedHeightTexture::sample), so the preview now stops at the same edge it does.
|
||||
bool in_tile(vec2 uv)
|
||||
{
|
||||
return tile_enabled || (uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0);
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
if (any(lessThan(clipping_planes_dots, ZERO)))
|
||||
@@ -256,9 +243,9 @@ void main()
|
||||
? vec2(dot(island_delta_lin.xy, vertex_uv), dot(island_delta_lin.zw, vertex_uv)) + island_delta_tr
|
||||
: vertex_uv;
|
||||
color_uv = uv;
|
||||
have_uv = in_tile(uv);
|
||||
have_uv = true;
|
||||
float h = texture2D(height_tex, uv).r;
|
||||
float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0) * (have_uv ? 1.0 : 0.0);
|
||||
float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
||||
vec3 sigmaS = dFdx(world_pos.xyz);
|
||||
vec3 sigmaT = dFdy(world_pos.xyz);
|
||||
vec3 R1 = cross(sigmaT, triangle_normal);
|
||||
@@ -275,12 +262,10 @@ void main()
|
||||
|
||||
// Parallax occlusion mapping: march the view ray through the height shell and shade at the
|
||||
// first point where it drops below the displaced surface (see header).
|
||||
// uv first: outside a single placed copy there is no shell to march through.
|
||||
vec2 uv = project_uv(tex_pos, triangle_normal);
|
||||
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0) *
|
||||
(in_tile(uv) ? 1.0 : 0.0);
|
||||
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
||||
vec3 view_dir = normalize(eye_model_pos - tex_pos);
|
||||
float v_dot_n = dot(view_dir, triangle_normal);
|
||||
vec2 uv = project_uv(tex_pos, triangle_normal);
|
||||
|
||||
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from
|
||||
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
|
||||
@@ -317,7 +302,7 @@ void main()
|
||||
}
|
||||
|
||||
color_uv = uv; // after the parallax march, so colour and relief stay registered
|
||||
have_uv = in_tile(uv);
|
||||
have_uv = true;
|
||||
|
||||
float hL = texture2D(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r;
|
||||
float hR = texture2D(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r;
|
||||
@@ -326,8 +311,7 @@ void main()
|
||||
|
||||
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
|
||||
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0) *
|
||||
(have_uv ? 1.0 : 0.0);
|
||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0);
|
||||
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
@@ -357,12 +341,7 @@ void main()
|
||||
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
|
||||
// the same material under the same light, and the relief this preview exists to show is unaffected.
|
||||
vec3 albedo = uniform_color.rgb;
|
||||
// The fourth channel is coverage: where the picture is not there, the surface keeps its own
|
||||
// colour rather than being painted the black that a transparent texel's rgb reads as.
|
||||
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0) {
|
||||
vec4 tex_rgba = texture2D(color_tex, color_uv);
|
||||
if (tex_rgba.a >= 0.5)
|
||||
albedo = palette_rgb[nearest_palette_entry(tex_rgba.rgb)];
|
||||
}
|
||||
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
|
||||
albedo = palette_rgb[nearest_palette_entry(texture2D(color_tex, color_uv).rgb)];
|
||||
gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
|
||||
}
|
||||
|
||||
@@ -110,11 +110,6 @@ uniform float tex_aspect;
|
||||
uniform float rotation_rad;
|
||||
uniform vec2 uv_offset;
|
||||
uniform bool invert;
|
||||
// False means the layer places a single copy rather than repeating: outside that copy there is
|
||||
// simply no texture. DecodedHeightTexture::sample() has always returned 0 there, so the bake got
|
||||
// this right while the preview wrapped the image and showed a tiled pattern that was never going
|
||||
// to be printed.
|
||||
uniform bool tile_enabled;
|
||||
uniform float midlevel; // the height that means "don't move"; needed by the parallax step
|
||||
uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor)
|
||||
uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin
|
||||
@@ -289,14 +284,6 @@ int nearest_palette_entry(vec3 rgb)
|
||||
return best;
|
||||
}
|
||||
|
||||
// True where this uv has texture on it: everywhere when the layer repeats, and only inside the
|
||||
// single placed copy when it does not. The bake's own sampler draws the same line (see
|
||||
// DecodedHeightTexture::sample), so the preview now stops at the same edge it does.
|
||||
bool in_tile(vec2 uv)
|
||||
{
|
||||
return tile_enabled || (uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0);
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
if (any(lessThan(clipping_planes_dots, ZERO)))
|
||||
@@ -334,9 +321,9 @@ void main()
|
||||
? vec2(dot(island_delta_lin.xy, vertex_uv), dot(island_delta_lin.zw, vertex_uv)) + island_delta_tr
|
||||
: vertex_uv;
|
||||
color_uv = uv;
|
||||
have_uv = in_tile(uv);
|
||||
have_uv = true;
|
||||
float h = texture(height_tex, uv).r;
|
||||
float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0) * (have_uv ? 1.0 : 0.0);
|
||||
float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
||||
vec3 sigmaS = dFdx(world_pos.xyz);
|
||||
vec3 sigmaT = dFdy(world_pos.xyz);
|
||||
vec3 R1 = cross(sigmaT, triangle_normal);
|
||||
@@ -356,12 +343,10 @@ void main()
|
||||
|
||||
// Parallax occlusion mapping: march the view ray through the height shell and shade at the
|
||||
// first point where it drops below the displaced surface (see header).
|
||||
// uv first: outside a single placed copy there is no shell to march through.
|
||||
vec2 uv = project_uv(tex_pos, triangle_normal);
|
||||
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0) *
|
||||
(in_tile(uv) ? 1.0 : 0.0);
|
||||
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
||||
vec3 view_dir = normalize(eye_model_pos - tex_pos);
|
||||
float v_dot_n = dot(view_dir, triangle_normal);
|
||||
vec2 uv = project_uv(tex_pos, triangle_normal);
|
||||
|
||||
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from
|
||||
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
|
||||
@@ -398,7 +383,7 @@ void main()
|
||||
}
|
||||
|
||||
color_uv = uv; // after the parallax march, so colour and relief stay registered
|
||||
have_uv = in_tile(uv);
|
||||
have_uv = true;
|
||||
|
||||
float hL = texture(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r;
|
||||
float hR = texture(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r;
|
||||
@@ -411,8 +396,7 @@ void main()
|
||||
// uv -> mm is 1/tiling_scale for the triplanar projection, so this turns the uv-space
|
||||
// gradient into a real surface slope.
|
||||
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0) *
|
||||
(have_uv ? 1.0 : 0.0);
|
||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0);
|
||||
// uv was rotated by project_uv() while t/b are the unrotated model axes, so rotate the
|
||||
// gradient back into the axes' frame.
|
||||
float cs = cos(rotation_rad);
|
||||
@@ -445,12 +429,7 @@ void main()
|
||||
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
|
||||
// the same material under the same light, and the relief this preview exists to show is unaffected.
|
||||
vec3 albedo = uniform_color.rgb;
|
||||
// The fourth channel is coverage: where the picture is not there, the surface keeps its own
|
||||
// colour rather than being painted the black that a transparent texel's rgb reads as.
|
||||
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0) {
|
||||
vec4 tex_rgba = texture(color_tex, color_uv);
|
||||
if (tex_rgba.a >= 0.5)
|
||||
albedo = palette_rgb[nearest_palette_entry(tex_rgba.rgb)];
|
||||
}
|
||||
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
|
||||
albedo = palette_rgb[nearest_palette_entry(texture(color_tex, color_uv).rgb)];
|
||||
out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
|
||||
}
|
||||
|
||||
@@ -1729,8 +1729,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
//plate->pattern_file = it->second->pattern_file;
|
||||
plate->no_light_thumbnail_file = it->second->no_light_thumbnail_file;
|
||||
plate->top_file = it->second->top_file;
|
||||
plate->pick_file = it->second->pick_file.empty();
|
||||
plate->pattern_bbox_file = it->second->pattern_bbox_file.empty();
|
||||
plate->pick_file = it->second->pick_file;
|
||||
plate->pattern_bbox_file = it->second->pattern_bbox_file;
|
||||
plate->config = it->second->config;
|
||||
|
||||
if (!plate->thumbnail_file.empty())
|
||||
|
||||
@@ -3295,7 +3295,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
m_is_role_based_fan_on.fill(false);
|
||||
m_role_based_fan_marker_layer.fill(-1);
|
||||
|
||||
m_fan_mover.release();
|
||||
m_fan_mover.reset();
|
||||
m_ordering_cache.clear();
|
||||
|
||||
// Belt printer: initialize belt-specific writer via virtual hook.
|
||||
|
||||
@@ -115,20 +115,6 @@ float DecodedHeightTexture::sample(const Vec2f &uv, bool tile_enabled, TextureTi
|
||||
return top * (1.f - ty) + bottom * ty;
|
||||
}
|
||||
|
||||
float DecodedHeightTexture::sample_alpha(const Vec2f &uv, bool tile_enabled, TextureTileMethod tile_method) const
|
||||
{
|
||||
TexelTap tap;
|
||||
if (!has_alpha())
|
||||
return 1.f; // no coverage plane means the picture is everywhere, which is the usual case
|
||||
if (!texel_tap(uv, tile_enabled, tile_method, tap))
|
||||
return 0.f;
|
||||
|
||||
auto at = [this](int x, int y) { return float(alpha[size_t(y) * size_t(width) + size_t(x)]) / 255.f; };
|
||||
const float top = at(tap.x0, tap.y0) * (1.f - tap.tx) + at(tap.x1, tap.y0) * tap.tx;
|
||||
const float bottom = at(tap.x0, tap.y1) * (1.f - tap.tx) + at(tap.x1, tap.y1) * tap.tx;
|
||||
return top * (1.f - tap.ty) + bottom * tap.ty;
|
||||
}
|
||||
|
||||
Vec3f DecodedHeightTexture::sample_color(const Vec2f &uv, bool tile_enabled, TextureTileMethod tile_method) const
|
||||
{
|
||||
TexelTap tap;
|
||||
@@ -275,272 +261,6 @@ bool height_texture_has_color(const TextureDisplacementLayer &layer)
|
||||
return decode_height_texture(layer).has_color();
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Unsharp mask over the colour: blur a copy and push each texel away from it. The blur is the same
|
||||
// sliding-window helper the Smoothing slider uses, run per channel, so the cost is the image size
|
||||
// rather than the image size times the radius.
|
||||
//
|
||||
// Worth having here for a reason it would not be in a photo editor: the picture is about to be
|
||||
// reduced to a handful of flat colours, and a soft edge between two of them quantizes into a ragged
|
||||
// band of both. Sharpening first makes that boundary land in one place.
|
||||
void sharpen_rgb(DecodedHeightTexture &tex, float amount)
|
||||
{
|
||||
if (!tex.has_color() || amount <= 0.f)
|
||||
return;
|
||||
const size_t n = size_t(tex.width) * size_t(tex.height);
|
||||
// A small radius, scaled with the image so a 4k picture is not sharpened a tenth as hard as a
|
||||
// 512 one. Deliberately not tied to `amount`: that is how much, this is how wide.
|
||||
const int radius = std::max(1, int(std::lround(double(std::min(tex.width, tex.height)) / 512.0)));
|
||||
const float k = std::clamp(amount, 0.f, 1.f) * 1.5f;
|
||||
|
||||
std::vector<uint8_t> plane(n);
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
plane[i] = tex.rgb[i * 3 + size_t(c)];
|
||||
smooth_height_pixels_box(plane, tex.width, tex.height, radius);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const float sharp = float(tex.rgb[i * 3 + size_t(c)]) + k * (float(tex.rgb[i * 3 + size_t(c)]) - float(plane[i]));
|
||||
tex.rgb[i * 3 + size_t(c)] = uint8_t(std::lround(std::clamp(sharp, 0.f, 255.f)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reduces the picture to at most `count` colours, by k-means in RGB.
|
||||
//
|
||||
// "At most": two of the chosen colours can still match the same filament, in which case the print
|
||||
// uses fewer. The guarantee runs the right way round, which is the one that matters - asking for
|
||||
// three colours cannot produce four.
|
||||
//
|
||||
// k-means rather than a fixed palette or an even split of the luma range, because the useful
|
||||
// question is which colours this picture is actually made of. A tartan asks for its own handful of
|
||||
// greens and reds; posterizing the range would hand it eight shades of mud instead. The clustering
|
||||
// runs over a coarse histogram, not over the pixels: at 1024 x 1024 that is 4096 weighted points
|
||||
// instead of a million, which is what makes it cheap enough to sit on a slider.
|
||||
void reduce_colors(DecodedHeightTexture &tex, int count)
|
||||
{
|
||||
if (!tex.has_color() || count < 2 || count > 64)
|
||||
return;
|
||||
const size_t n = size_t(tex.width) * size_t(tex.height);
|
||||
|
||||
// 16 levels per channel, each bin keeping the mean of what fell in it, so a colour is not
|
||||
// snapped to a bin corner before the clustering has had a say.
|
||||
struct Bin { double r = 0, g = 0, b = 0; uint32_t n = 0; };
|
||||
std::vector<Bin> bins(size_t(16 * 16 * 16));
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const uint8_t r = tex.rgb[i * 3], g = tex.rgb[i * 3 + 1], b = tex.rgb[i * 3 + 2];
|
||||
Bin &bin = bins[(size_t(r >> 4) << 8) | (size_t(g >> 4) << 4) | size_t(b >> 4)];
|
||||
bin.r += r; bin.g += g; bin.b += b; ++bin.n;
|
||||
}
|
||||
struct Point { float c[3]; double w; };
|
||||
std::vector<Point> pts;
|
||||
pts.reserve(bins.size());
|
||||
for (const Bin &bin : bins)
|
||||
if (bin.n > 0)
|
||||
pts.push_back({ { float(bin.r / bin.n), float(bin.g / bin.n), float(bin.b / bin.n) }, double(bin.n) });
|
||||
if (pts.size() <= size_t(count))
|
||||
return; // already no more colours than asked for
|
||||
|
||||
// Seeded the k-means++ way, deterministically: the heaviest point first, then each next one is
|
||||
// whichever point is furthest from everything chosen so far, weighted by how much of the image
|
||||
// it is. Picking the N fullest bins instead would put every centre inside the one dominant
|
||||
// colour of a picture that has one.
|
||||
std::vector<std::array<float, 3>> centre;
|
||||
{
|
||||
size_t best = 0;
|
||||
for (size_t i = 1; i < pts.size(); ++i)
|
||||
if (pts[i].w > pts[best].w)
|
||||
best = i;
|
||||
centre.push_back({ pts[best].c[0], pts[best].c[1], pts[best].c[2] });
|
||||
std::vector<double> d2(pts.size(), std::numeric_limits<double>::max());
|
||||
while (int(centre.size()) < count) {
|
||||
const auto &last = centre.back();
|
||||
double top = -1.0;
|
||||
size_t pick = 0;
|
||||
for (size_t i = 0; i < pts.size(); ++i) {
|
||||
const double dr = pts[i].c[0] - last[0], dg = pts[i].c[1] - last[1], db = pts[i].c[2] - last[2];
|
||||
d2[i] = std::min(d2[i], dr * dr + dg * dg + db * db);
|
||||
if (const double score = d2[i] * pts[i].w; score > top) {
|
||||
top = score;
|
||||
pick = i;
|
||||
}
|
||||
}
|
||||
if (top <= 0.0)
|
||||
break; // fewer distinct colours than asked for
|
||||
centre.push_back({ pts[pick].c[0], pts[pick].c[1], pts[pick].c[2] });
|
||||
}
|
||||
}
|
||||
|
||||
// Lloyd's algorithm over the weighted bins. Twelve passes: on a histogram this coarse it has
|
||||
// stopped moving well before that, and a fixed count keeps the slider's cost predictable.
|
||||
const int k = int(centre.size());
|
||||
for (int iter = 0; iter < 12; ++iter) {
|
||||
std::vector<std::array<double, 3>> sum(size_t(k), { 0.0, 0.0, 0.0 });
|
||||
std::vector<double> wsum(size_t(k), 0.0);
|
||||
for (const Point &p : pts) {
|
||||
int best = 0;
|
||||
double bd = std::numeric_limits<double>::max();
|
||||
for (int c = 0; c < k; ++c) {
|
||||
const double dr = p.c[0] - centre[size_t(c)][0], dg = p.c[1] - centre[size_t(c)][1],
|
||||
db = p.c[2] - centre[size_t(c)][2];
|
||||
if (const double d = dr * dr + dg * dg + db * db; d < bd) { bd = d; best = c; }
|
||||
}
|
||||
for (int ch = 0; ch < 3; ++ch)
|
||||
sum[size_t(best)][size_t(ch)] += double(p.c[ch]) * p.w;
|
||||
wsum[size_t(best)] += p.w;
|
||||
}
|
||||
for (int c = 0; c < k; ++c)
|
||||
if (wsum[size_t(c)] > 0.0)
|
||||
for (int ch = 0; ch < 3; ++ch)
|
||||
centre[size_t(c)][size_t(ch)] = float(sum[size_t(c)][size_t(ch)] / wsum[size_t(c)]);
|
||||
}
|
||||
|
||||
// Every texel takes its nearest centre. Parallel by rows, as the tonal pass is.
|
||||
const size_t rows = size_t(tex.height), cols = size_t(tex.width);
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, rows), [&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t y = range.begin(); y < range.end(); ++y)
|
||||
for (size_t x = 0; x < cols; ++x) {
|
||||
uint8_t *px = tex.rgb.data() + (y * cols + x) * 3;
|
||||
int best = 0;
|
||||
float bd = std::numeric_limits<float>::max();
|
||||
for (int c = 0; c < k; ++c) {
|
||||
const float dr = float(px[0]) - centre[size_t(c)][0], dg = float(px[1]) - centre[size_t(c)][1],
|
||||
db = float(px[2]) - centre[size_t(c)][2];
|
||||
if (const float d = dr * dr + dg * dg + db * db; d < bd) { bd = d; best = c; }
|
||||
}
|
||||
for (int ch = 0; ch < 3; ++ch)
|
||||
px[size_t(ch)] = uint8_t(std::lround(std::clamp(centre[size_t(best)][size_t(ch)], 0.f, 255.f)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Keys one colour out, so a logo on a white card prints as the logo and the object's own colour
|
||||
// shows through the rest.
|
||||
//
|
||||
// The corners are the default because a background that does not reach all four of them is not a
|
||||
// background, and that makes the common case one click; the median of the four is taken so a single
|
||||
// stray corner cannot decide it. The panel's dropper and swatch override it when the picture needs
|
||||
// something else keyed out.
|
||||
void remove_background(DecodedHeightTexture &tex, const TextureDisplacementLayer &layer)
|
||||
{
|
||||
if (!tex.has_color() || !layer.bg_remove || tex.width < 2 || tex.height < 2)
|
||||
return;
|
||||
const size_t n = size_t(tex.width) * size_t(tex.height);
|
||||
float key[3];
|
||||
if (layer.bg_from_corners) {
|
||||
const size_t corner[4] = { 0, size_t(tex.width) - 1, (size_t(tex.height) - 1) * size_t(tex.width),
|
||||
n - 1 };
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
std::array<float, 4> v{};
|
||||
for (int i = 0; i < 4; ++i)
|
||||
v[size_t(i)] = float(tex.rgb[corner[i] * 3 + size_t(c)]);
|
||||
std::sort(v.begin(), v.end());
|
||||
key[c] = 0.5f * (v[1] + v[2]);
|
||||
}
|
||||
} else {
|
||||
for (int c = 0; c < 3; ++c)
|
||||
key[c] = std::clamp(layer.bg_color[c], 0.f, 1.f) * 255.f;
|
||||
}
|
||||
// Euclidean in 0..255 RGB, against a tolerance scaled to the diagonal of that cube, so the
|
||||
// slider reads as "how different from the background still counts as background".
|
||||
const float limit = std::clamp(layer.bg_tolerance, 0.f, 1.f) * 441.67f;
|
||||
if (!tex.has_alpha())
|
||||
tex.alpha.assign(n, 255);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const float dr = float(tex.rgb[i * 3]) - key[0];
|
||||
const float dg = float(tex.rgb[i * 3 + 1]) - key[1];
|
||||
const float db = float(tex.rgb[i * 3 + 2]) - key[2];
|
||||
if (std::sqrt(dr * dr + dg * dg + db * db) <= limit)
|
||||
tex.alpha[i] = 0;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool has_color_adjustments(const TextureDisplacementLayer &layer)
|
||||
{
|
||||
return layer.brightness != 0.f || layer.contrast != 0.f || layer.saturation != 1.f ||
|
||||
layer.gamma != 1.f || layer.color_invert || layer.hue != 0.f || layer.sharpness > 0.f ||
|
||||
layer.color_count >= 2 || layer.bg_remove;
|
||||
}
|
||||
|
||||
void apply_color_adjustments(DecodedHeightTexture &tex, const TextureDisplacementLayer &layer)
|
||||
{
|
||||
if (!tex.has_color() || !has_color_adjustments(layer))
|
||||
return;
|
||||
|
||||
// A contrast of -1 collapses the image to mid grey and +1 doubles the slope about it, which is
|
||||
// the range a single slider can offer without a scale nobody can read off the handle.
|
||||
const float slope = 1.f + std::clamp(layer.contrast, -1.f, 1.f);
|
||||
const float bright = std::clamp(layer.brightness, -1.f, 1.f);
|
||||
const float sat = std::max(0.f, layer.saturation);
|
||||
const float inv_g = 1.f / std::max(0.01f, layer.gamma);
|
||||
const bool negate = layer.color_invert;
|
||||
// Hue is a rotation of the colour about the grey axis. Rodrigues' formula about (1,1,1)/sqrt(3),
|
||||
// then the luma is put back exactly (the rotation preserves R+G+B, not the Rec. 709 weighting),
|
||||
// so turning the wheel changes which filament an area matches without also changing how light
|
||||
// it is.
|
||||
const float hue_rad = layer.hue * float(M_PI) / 180.f;
|
||||
const float hue_cos = std::cos(hue_rad), hue_sin = std::sin(hue_rad);
|
||||
const bool rotate = layer.hue != 0.f;
|
||||
constexpr float LW[3] = { 0.2126f, 0.7152f, 0.0722f }; // Rec. 709 luma
|
||||
|
||||
// Each output byte depends only on its own pixel, so the rows are independent. At 1024 x 1024
|
||||
// this runs on every preview rebuild and every bake, which is what makes it worth splitting.
|
||||
const size_t rows = size_t(tex.height);
|
||||
const size_t cols = size_t(tex.width);
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, rows), [&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t y = range.begin(); y < range.end(); ++y) {
|
||||
uint8_t *row = tex.rgb.data() + y * cols * 3;
|
||||
for (size_t x = 0; x < cols; ++x) {
|
||||
uint8_t *px = row + x * 3;
|
||||
float c[3];
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
float v = float(px[k]) * (1.f / 255.f);
|
||||
v = std::pow(v, inv_g);
|
||||
v = (v - 0.5f) * slope + 0.5f;
|
||||
v += bright;
|
||||
c[k] = v;
|
||||
}
|
||||
if (rotate) {
|
||||
const float luma = LW[0] * c[0] + LW[1] * c[1] + LW[2] * c[2];
|
||||
// The component of the colour across the grey axis, which is what a hue is.
|
||||
const float mean = (c[0] + c[1] + c[2]) / 3.f;
|
||||
const float p[3] = { c[0] - mean, c[1] - mean, c[2] - mean };
|
||||
// k x p for the unit grey axis k = (1,1,1)/sqrt(3).
|
||||
constexpr float INV_SQRT3 = 0.57735027f;
|
||||
const float x[3] = { INV_SQRT3 * (p[1] - p[2]), INV_SQRT3 * (p[2] - p[0]),
|
||||
INV_SQRT3 * (p[0] - p[1]) };
|
||||
for (int k = 0; k < 3; ++k)
|
||||
c[k] = mean + p[k] * hue_cos + x[k] * hue_sin;
|
||||
const float luma_now = LW[0] * c[0] + LW[1] * c[1] + LW[2] * c[2];
|
||||
for (float &v : c)
|
||||
v += luma - luma_now;
|
||||
}
|
||||
if (sat != 1.f) {
|
||||
// Rec. 709 luma, the same weights the rest of the colour path uses to decide
|
||||
// what counts as light and dark.
|
||||
const float luma = LW[0] * c[0] + LW[1] * c[1] + LW[2] * c[2];
|
||||
for (float &v : c)
|
||||
v = luma + (v - luma) * sat;
|
||||
}
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
float v = std::clamp(c[k], 0.f, 1.f);
|
||||
if (negate)
|
||||
v = 1.f - v;
|
||||
px[k] = uint8_t(std::lround(v * 255.f));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Sharpen, threshold and the background key are all passes over the whole image rather than
|
||||
// per-texel arithmetic, so they come after the loop above and in this order: sharpening a
|
||||
// thresholded image would only ring along the one edge it has left, and the key has to read the
|
||||
// colours the user is actually looking at.
|
||||
sharpen_rgb(tex, layer.sharpness);
|
||||
reduce_colors(tex, layer.color_count);
|
||||
remove_background(tex, layer);
|
||||
}
|
||||
|
||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer)
|
||||
{
|
||||
DecodedHeightTexture result;
|
||||
@@ -593,10 +313,6 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
|
||||
result.height = h;
|
||||
result.pixels.resize(size_t(w) * size_t(h));
|
||||
result.rgb.resize(size_t(w) * size_t(h) * 3);
|
||||
// A fourth channel is coverage, and it is the only thing that can say "the picture is
|
||||
// not here" as opposed to "the picture is black here". Kept only when the source has it.
|
||||
if (bpp >= 4)
|
||||
result.alpha.resize(size_t(w) * size_t(h));
|
||||
// decode_colored_png() hands its buffer back bottom-up - it is shared with the CLI's
|
||||
// plate-thumbnail loader, which expects that - while this type, and decode_png()'s
|
||||
// grayscale path above, are top-to-bottom. Reverse the rows on the way in so a colour
|
||||
@@ -610,8 +326,6 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
|
||||
result.rgb[dst * 3 + 1] = g;
|
||||
result.rgb[dst * 3 + 2] = b;
|
||||
result.pixels[dst] = uint8_t(std::lround(0.299 * r + 0.587 * g + 0.114 * b));
|
||||
if (bpp >= 4)
|
||||
result.alpha[dst] = src[3];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2145,14 +1859,10 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
|
||||
// Outside a non-tiled placement there is no texture at all - the same hard edge sample() gives the
|
||||
// height. Checked explicitly because sample_color() reports it as black, which is a real colour.
|
||||
auto covered = [&](const Vec2f &planar) {
|
||||
if (layer.tile_enabled)
|
||||
return true;
|
||||
const Vec2f uv = xf(planar);
|
||||
if (!layer.tile_enabled && !(uv.x() >= 0.f && uv.x() < 1.f && uv.y() >= 0.f && uv.y() < 1.f))
|
||||
return false;
|
||||
// Where the picture is transparent there is no colour to print, which is the same answer as
|
||||
// being outside the placement - not black, which is a colour. Half coverage counts as
|
||||
// covered: a triangle prints in one filament either way, so the only useful question is
|
||||
// which side of the edge it falls on.
|
||||
return texture.sample_alpha(uv, layer.tile_enabled, layer.tile_method) >= 0.5f;
|
||||
return uv.x() >= 0.f && uv.x() < 1.f && uv.y() >= 0.f && uv.y() < 1.f;
|
||||
};
|
||||
|
||||
if (lscm_uv != nullptr) {
|
||||
@@ -3531,13 +3241,9 @@ std::shared_ptr<std::vector<PreparedLayer>> prepare_sampleable_layers(
|
||||
const TriangleSelector::TriangleSplittingData &data = facets_data[size_t(layer->slot)];
|
||||
if (data.triangles_to_split.empty())
|
||||
continue;
|
||||
DecodedHeightTexture tex = decode_height_texture(*layer);
|
||||
const DecodedHeightTexture tex = decode_height_texture(*layer);
|
||||
if (tex.empty() || (need_color && !tex.has_color()))
|
||||
continue;
|
||||
// The one place the bake and every preview job read a layer's colour from, so correcting it
|
||||
// here is what keeps what the panel shows and what the bake prints the same image.
|
||||
if (need_color)
|
||||
apply_color_adjustments(tex, *layer);
|
||||
|
||||
TriangleSelector selector(selector_mesh);
|
||||
selector.deserialize(data, false);
|
||||
|
||||
@@ -293,32 +293,6 @@ struct TextureDisplacementLayer
|
||||
// for the boundary to be drawn on.
|
||||
bool color_enabled = false;
|
||||
|
||||
// Colour correction, applied to the image's RGB before it is matched against the filaments and
|
||||
// before the preview quantizes it. Never to the height: the relief is what the grayscale says,
|
||||
// and a brightness slider has no business moving geometry. The defaults below are the identity,
|
||||
// so a layer that never meets the Image tool behaves exactly as it did.
|
||||
float brightness = 0.f; // -1 .. 1, added last, after contrast
|
||||
float contrast = 0.f; // -1 .. 1, pivoted on mid grey; -1 flattens to grey, 1 doubles the slope
|
||||
float hue = 0.f; // -180 .. 180 degrees, rotated about the grey axis at constant luma
|
||||
float sharpness = 0.f; // 0 .. 1 unsharp mask amount, 0 is off
|
||||
// How many colours the picture is allowed to use, 0 for as many as it likes. Reduced by k-means
|
||||
// in RGB before the filaments are matched, so the result uses at most this many of them: 2 for a
|
||||
// logo that should be two flat colours, 4 or 5 for a poster, off for a photograph.
|
||||
int color_count = 0; // 0, or 2 .. 12
|
||||
// Leaves the background of a picture unprinted, so the object's own colour shows through instead
|
||||
// of a slab of white behind a logo. Texels the source image marked transparent always count;
|
||||
// `bg_remove` additionally keys out one colour.
|
||||
bool bg_remove = false;
|
||||
float bg_tolerance = 0.15f; // 0 .. 1, distance from the keyed colour that still counts as background
|
||||
// The colour keyed out. Taken from the median of the picture's four corners while
|
||||
// `bg_from_corners` is set, which is right for nearly every logo on a flat card; the panel's
|
||||
// dropper and swatch clear it and put a colour of the user's own here.
|
||||
bool bg_from_corners = true;
|
||||
float bg_color[3] = { 1.f, 1.f, 1.f };
|
||||
float saturation = 1.f; // 0 .. 2, 1 leaves it alone, 0 is grayscale
|
||||
float gamma = 1.f; // 0.2 .. 5, 1 leaves it alone; above 1 lifts the midtones
|
||||
bool color_invert = false; // negative of the colour, as opposed to `invert` which flips relief
|
||||
|
||||
bool empty() const { return !image_data || image_data->empty(); }
|
||||
|
||||
template<class Archive> void save(Archive &ar) const
|
||||
@@ -328,9 +302,7 @@ struct TextureDisplacementLayer
|
||||
static_cast<int>(tile_method), static_cast<int>(projection_method), lscm_seam_angle_deg, islands,
|
||||
static_cast<int>(blend_mode), midlevel, island_padding_mm, lscm_seam_edges, view_project_right,
|
||||
view_project_up, smoothing, edge_smoothing, edge_smoothing_amount, auto_connect_islands, island_groups,
|
||||
lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled, brightness,
|
||||
contrast, saturation, gamma, color_invert, hue, sharpness, color_count, bg_remove,
|
||||
bg_tolerance, bg_from_corners, bg_color[0], bg_color[1], bg_color[2]);
|
||||
lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled);
|
||||
}
|
||||
template<class Archive> void load(Archive &ar)
|
||||
{
|
||||
@@ -342,8 +314,7 @@ struct TextureDisplacementLayer
|
||||
tile_method_int, projection_method_int, lscm_seam_angle_deg, islands, blend_mode_int, midlevel,
|
||||
island_padding_mm, lscm_seam_edges, view_project_right, view_project_up, smoothing, edge_smoothing,
|
||||
edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective,
|
||||
view_project_matrix, color_enabled, brightness, contrast, saturation, gamma, color_invert, hue, sharpness,
|
||||
color_count, bg_remove, bg_tolerance, bg_from_corners, bg_color[0], bg_color[1], bg_color[2]);
|
||||
view_project_matrix, color_enabled);
|
||||
image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end());
|
||||
tile_method = static_cast<TextureTileMethod>(tile_method_int);
|
||||
projection_method = static_cast<TextureProjectionMethod>(projection_method_int);
|
||||
@@ -477,19 +448,11 @@ struct DecodedHeightTexture
|
||||
// has_color() is what the whole colour feature keys off: a layer set to colour a model with a
|
||||
// grayscale texture on it simply colours nothing.
|
||||
std::vector<uint8_t> rgb;
|
||||
// Coverage: 0 where the picture is not there at all, 255 where it is fully there. Empty when the
|
||||
// source image had no alpha and nothing has keyed a background out, which is the common case and
|
||||
// means "opaque everywhere". Distinct from a black texel, which is a colour.
|
||||
std::vector<uint8_t> alpha;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
bool empty() const { return width <= 0 || height <= 0 || pixels.empty(); }
|
||||
bool has_color() const { return !empty() && rgb.size() == size_t(width) * size_t(height) * 3; }
|
||||
bool has_alpha() const { return !empty() && alpha.size() == size_t(width) * size_t(height); }
|
||||
// Coverage at a uv, bilinear, in [0, 1]. 1 everywhere when there is no alpha plane.
|
||||
float sample_alpha(const Vec2f &uv, bool tile_enabled = true,
|
||||
TextureTileMethod tile_method = TextureTileMethod::Repeat) const;
|
||||
// Bilinearly sampled height in [0, 1] at a normalized uv coordinate. When tile_enabled is false,
|
||||
// a uv outside [0, 1) samples as 0 - the texture simply is not there, rather than its border
|
||||
// row/column being smeared outward forever (which is what clamping the coordinate would do, and
|
||||
@@ -526,19 +489,6 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
|
||||
// texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read.
|
||||
bool height_texture_has_color(const TextureDisplacementLayer &layer);
|
||||
|
||||
// True when `layer`'s colour correction is anything other than the identity, so callers can skip
|
||||
// both the work and the cache invalidation it would otherwise force.
|
||||
bool has_color_adjustments(const TextureDisplacementLayer &layer);
|
||||
|
||||
// Applies that correction to an already decoded image, in place, to `rgb` only. Order is gamma,
|
||||
// contrast, brightness, saturation, invert, each clamped back into range - the order a photo editor
|
||||
// applies them in, and the one that keeps contrast pivoted on mid grey rather than on a brightness
|
||||
// the slider has already moved.
|
||||
//
|
||||
// Deliberately not folded into decode_height_texture(): that result is cached per image, shared by
|
||||
// every layer using the same file, and keyed on the image bytes alone.
|
||||
void apply_color_adjustments(DecodedHeightTexture &tex, const TextureDisplacementLayer &layer);
|
||||
|
||||
// Maps a linear RGB colour in [0, 1] to an index into the caller's palette, or -1 for "no colour".
|
||||
//
|
||||
// Deliberately a callback rather than a function here: matching a colour to a filament is a
|
||||
|
||||
@@ -186,8 +186,6 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/Gizmos/GLGizmoFlatten.hpp
|
||||
GUI/Gizmos/GLGizmoFuzzySkin.cpp
|
||||
GUI/Gizmos/GLGizmoFuzzySkin.hpp
|
||||
GUI/Gizmos/GLGizmoImage.cpp
|
||||
GUI/Gizmos/GLGizmoImage.hpp
|
||||
GUI/Gizmos/GLGizmoMeasure.cpp
|
||||
GUI/Gizmos/GLGizmoMeasure.hpp
|
||||
GUI/Gizmos/GLGizmoMeshBoolean.cpp
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <wx/checklst.h>
|
||||
#include "slic3r/GUI/GUI_Utils.hpp"
|
||||
#include "slic3r/GUI/Widgets/Button.hpp"
|
||||
#include "slic3r/GUI/Widgets/DropDown.hpp"
|
||||
#include "slic3r/GUI/Widgets/Label.hpp"
|
||||
#include <utility>
|
||||
#include "slic3r/GUI/wxExtensions.hpp"
|
||||
@@ -172,7 +173,26 @@ void AMSMaterialsSetting::create()
|
||||
|
||||
Bind(wxEVT_PAINT, &AMSMaterialsSetting::paintEvent, this);
|
||||
Bind(EVT_SELECTED_COLOR, &AMSMaterialsSetting::on_picker_color, this);
|
||||
m_comboBox_filament->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
|
||||
m_comboBox_filament->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
|
||||
m_comboBox_filament->Bind(EVT_DROPDOWN_ITEM_ACTION, &AMSMaterialsSetting::on_toggle_favorite_filament, this);
|
||||
|
||||
// Favorites toggled in the open list are regrouped just before it opens again, so rows never move
|
||||
// under the cursor. These handlers run ahead of ComboBox's own, which then measure and show the
|
||||
// rebuilt rows; wxEVT_COMBOBOX_DROPDOWN would be too late, as ComboBox sends it after showing the list.
|
||||
auto regroup_favorites = [this]() {
|
||||
if (!m_favorites_regroup || m_comboBox_filament->is_drop_down())
|
||||
return;
|
||||
const FilamentInfos* info = selected_filament_info();
|
||||
const std::string filament_id = info ? info->filament_id : std::string();
|
||||
rebuild_filament_dropdown_items(filament_id);
|
||||
};
|
||||
m_comboBox_filament->Bind(wxEVT_LEFT_DOWN, [regroup_favorites](wxMouseEvent& e) { regroup_favorites(); e.Skip(); });
|
||||
m_comboBox_filament->Bind(wxEVT_LEFT_DCLICK, [regroup_favorites](wxMouseEvent& e) { regroup_favorites(); e.Skip(); });
|
||||
m_comboBox_filament->Bind(wxEVT_KEY_DOWN, [regroup_favorites](wxKeyEvent& e) {
|
||||
if (e.GetKeyCode() == WXK_RETURN || e.GetKeyCode() == WXK_SPACE) // the keys that open the list
|
||||
regroup_favorites();
|
||||
e.Skip();
|
||||
});
|
||||
|
||||
m_comboBox_cali_result->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_cali_result), NULL, this);
|
||||
}
|
||||
@@ -191,11 +211,9 @@ void AMSMaterialsSetting::create_panel_normal(wxWindow* parent)
|
||||
|
||||
m_sizer_filament->Add(0, 0, 0, wxEXPAND, 0);
|
||||
|
||||
#ifdef __APPLE__
|
||||
m_comboBox_filament = new wxComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY);
|
||||
#else
|
||||
m_comboBox_filament = new ::ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY);
|
||||
#endif
|
||||
m_star_filled = ScalableBitmap(this, "score_star_light", 16);
|
||||
m_star_empty = ScalableBitmap(this, "score_star_outline", 16);
|
||||
|
||||
m_sizer_filament->Add(m_comboBox_filament, 1, wxALIGN_CENTER, 0);
|
||||
|
||||
@@ -420,6 +438,7 @@ void AMSMaterialsSetting::paintEvent(wxPaintEvent &evt)
|
||||
AMSMaterialsSetting::~AMSMaterialsSetting()
|
||||
{
|
||||
m_comboBox_filament->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
|
||||
m_comboBox_filament->Unbind(EVT_DROPDOWN_ITEM_ACTION, &AMSMaterialsSetting::on_toggle_favorite_filament, this);
|
||||
m_comboBox_cali_result->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_cali_result), NULL, this);
|
||||
}
|
||||
|
||||
@@ -531,10 +550,9 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) {
|
||||
std::string selected_ams_id;
|
||||
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
|
||||
if (preset_bundle) {
|
||||
const FilamentInfos* selected_info = selected_filament_info();
|
||||
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
|
||||
auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
|
||||
std::string filament_id = filament_item.filament_id;
|
||||
if (it->filament_id.compare(filament_id) == 0) {
|
||||
if (selected_info && it->filament_id.compare(selected_info->filament_id) == 0) {
|
||||
selected_ams_id = it->filament_id;
|
||||
break;
|
||||
}
|
||||
@@ -682,12 +700,13 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
|
||||
ams_setting_id = "";
|
||||
|
||||
// the combobox item
|
||||
auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
|
||||
const FilamentInfos* selected_info = selected_filament_info();
|
||||
const std::string filament_id = selected_info ? selected_info->filament_id : std::string();
|
||||
|
||||
// check filament info (rack-gated per-nozzle blacklist evaluation)
|
||||
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
|
||||
const auto& fila_check_res = sCheckFilamentInfo(preset_bundle, obj, ams_id, slot_id,
|
||||
filament_item.filament_id, ams_filament_id, ams_setting_id);
|
||||
filament_id, ams_filament_id, ams_setting_id);
|
||||
|
||||
if (const auto& prohibit_items = fila_check_res.get_items_by_action("prohibition"); !prohibit_items.empty()) {
|
||||
wxString info_msg;
|
||||
@@ -986,6 +1005,133 @@ static void _collect_filament_info(const wxString& shown_name,
|
||||
query_filament_types[shown_name] = filament.config.get_filament_type();
|
||||
}
|
||||
|
||||
static const char* AMS_FAVORITE_FILAMENT_IDS_KEY = "ams_favorite_filament_ids";
|
||||
|
||||
const AMSMaterialsSetting::FilamentInfos* AMSMaterialsSetting::selected_filament_info() const
|
||||
{
|
||||
const int selection = m_comboBox_filament->GetSelection();
|
||||
if (selection < 0 || selection >= int(m_filament_list_items.size()) || !m_filament_list_items[selection].is_filament)
|
||||
return nullptr;
|
||||
return &m_filament_list_items[selection].filament;
|
||||
}
|
||||
|
||||
std::set<std::string> AMSMaterialsSetting::load_favorite_filament_ids() const
|
||||
{
|
||||
std::set<std::string> favorite_ids;
|
||||
std::vector<std::string> values;
|
||||
const std::string serialized = wxGetApp().app_config->get(AMS_FAVORITE_FILAMENT_IDS_KEY);
|
||||
if (!serialized.empty() && Slic3r::unescape_strings_cstyle(serialized, values)) {
|
||||
favorite_ids.insert(values.begin(), values.end());
|
||||
}
|
||||
return favorite_ids;
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::save_favorite_filament_ids(const std::set<std::string>& favorite_ids)
|
||||
{
|
||||
std::vector<std::string> values(favorite_ids.begin(), favorite_ids.end());
|
||||
wxGetApp().app_config->set(AMS_FAVORITE_FILAMENT_IDS_KEY, Slic3r::escape_strings_cstyle(values));
|
||||
wxGetApp().app_config->save();
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::rebuild_filament_dropdown_items(const std::string& selected_filament_id)
|
||||
{
|
||||
std::vector<DropDown::Item> items;
|
||||
m_filament_list_items.clear();
|
||||
|
||||
auto append_header = [&items, this](const wxString& label) {
|
||||
DropDown::Item item;
|
||||
item.text = label;
|
||||
item.style = DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED;
|
||||
items.push_back(item);
|
||||
m_filament_list_items.push_back(FilamentListItem{});
|
||||
};
|
||||
|
||||
auto append_filament = [&items, this](const wxString& name) {
|
||||
auto info_it = map_filament_items.find(name);
|
||||
if (info_it == map_filament_items.end())
|
||||
return;
|
||||
|
||||
DropDown::Item item;
|
||||
item.text = name;
|
||||
items.push_back(item);
|
||||
m_filament_list_items.push_back(FilamentListItem{info_it->second, true});
|
||||
};
|
||||
|
||||
// Favorites are listed again at the top under their own header. Without any, the list is the
|
||||
// plain filament list.
|
||||
const std::set<std::string> favorite_ids = load_favorite_filament_ids();
|
||||
std::vector<wxString> favorites;
|
||||
for (const wxString& name : m_sorted_filament_items) {
|
||||
auto info_it = map_filament_items.find(name);
|
||||
if (info_it != map_filament_items.end() && favorite_ids.count(info_it->second.filament_id) > 0)
|
||||
favorites.push_back(name);
|
||||
}
|
||||
if (!favorites.empty()) {
|
||||
append_header(_L("Favorite filaments"));
|
||||
for (const wxString& name : favorites)
|
||||
append_filament(name);
|
||||
append_header(_L("All filaments"));
|
||||
}
|
||||
for (const wxString& name : m_sorted_filament_items)
|
||||
append_filament(name);
|
||||
|
||||
int selection_idx = -1;
|
||||
if (!selected_filament_id.empty()) {
|
||||
for (int i = 0; i < int(m_filament_list_items.size()); ++i) {
|
||||
if (m_filament_list_items[i].is_filament && m_filament_list_items[i].filament.filament_id == selected_filament_id) {
|
||||
selection_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear() first: SetItems() keeps the old selection, and SetSelection() ignores an index equal to it,
|
||||
// which would leave the previous filament's name on the combo.
|
||||
m_comboBox_filament->Clear();
|
||||
m_comboBox_filament->SetItems(items);
|
||||
update_favorite_stars();
|
||||
m_comboBox_filament->SetSelection(selection_idx);
|
||||
m_favorites_regroup = false;
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::update_favorite_stars()
|
||||
{
|
||||
const std::set<std::string> favorite_ids = load_favorite_filament_ids();
|
||||
for (size_t i = 0; i < m_filament_list_items.size(); ++i) {
|
||||
const FilamentListItem& row = m_filament_list_items[i];
|
||||
if (row.is_filament) {
|
||||
const bool favorite = favorite_ids.count(row.filament.filament_id) > 0;
|
||||
m_comboBox_filament->SetItemActionBitmap(i, favorite ? m_star_filled.bmp() : m_star_empty.bmp());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::toggle_favorite_filament(const std::string& filament_id)
|
||||
{
|
||||
if (filament_id.empty())
|
||||
return;
|
||||
|
||||
std::set<std::string> favorite_ids = load_favorite_filament_ids();
|
||||
auto iter = favorite_ids.find(filament_id);
|
||||
if (iter == favorite_ids.end())
|
||||
favorite_ids.insert(filament_id);
|
||||
else
|
||||
favorite_ids.erase(iter);
|
||||
|
||||
save_favorite_filament_ids(favorite_ids);
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::on_toggle_favorite_filament(wxCommandEvent& evt)
|
||||
{
|
||||
const int index = evt.GetInt();
|
||||
if (index < 0 || index >= int(m_filament_list_items.size()) || !m_filament_list_items[index].is_filament)
|
||||
return;
|
||||
|
||||
toggle_favorite_filament(m_filament_list_items[index].filament.filament_id);
|
||||
update_favorite_stars();
|
||||
m_favorites_regroup = true;
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_min, wxString temp_max, wxString k, wxString n)
|
||||
{
|
||||
if (!obj) return;
|
||||
@@ -999,13 +1145,12 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
m_input_k_val->GetTextCtrl()->SetValue(k);
|
||||
m_input_n_val->GetTextCtrl()->SetValue(n);
|
||||
|
||||
wxArrayString filament_items;
|
||||
wxString bambu_filament_name;
|
||||
wxString hint_filament_name; // the hint type to be selected
|
||||
std::unordered_map<wxString, wxString> query_filament_vendors;// some information for sort
|
||||
std::unordered_map<wxString, wxString> query_filament_types; //
|
||||
|
||||
std::set<std::string> filament_id_set;
|
||||
m_sorted_filament_items.clear();
|
||||
// The alias keyed map has to start empty: it is a member, so a stale alias left by an earlier
|
||||
// popup (a different printer, a different nozzle) would resolve to that printer's filament_id.
|
||||
map_filament_items.clear();
|
||||
@@ -1035,7 +1180,7 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
if (alias.empty())
|
||||
continue;
|
||||
|
||||
filament_items.push_back(alias);
|
||||
m_sorted_filament_items.push_back(alias);
|
||||
_collect_filament_info(alias, *filament_it, query_filament_vendors, query_filament_types);
|
||||
|
||||
FilamentInfos filament_infos;
|
||||
@@ -1044,7 +1189,6 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
map_filament_items[alias] = filament_infos;
|
||||
|
||||
if (filament_it->filament_id == ams_filament_id) {
|
||||
hint_filament_name = from_u8(alias);
|
||||
bambu_filament_name = from_u8(alias);
|
||||
|
||||
|
||||
@@ -1215,24 +1359,11 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
return name_compare != 0 ? name_compare < 0 : left < right;
|
||||
};
|
||||
|
||||
std::sort(filament_items.begin(), filament_items.end(), _filament_sorter);
|
||||
std::sort(m_sorted_filament_items.begin(), m_sorted_filament_items.end(), _filament_sorter);
|
||||
}
|
||||
|
||||
// traverse the hint selection idx
|
||||
int selection_idx = -1;
|
||||
{
|
||||
for(int i = 0; i < filament_items.size(); i++)
|
||||
{
|
||||
if (hint_filament_name == filament_items[i])
|
||||
{
|
||||
selection_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_comboBox_filament->Set(filament_items);
|
||||
m_comboBox_filament->SetSelection(selection_idx);
|
||||
rebuild_filament_dropdown_items(ams_filament_id);
|
||||
int selection_idx = m_comboBox_filament->GetSelection();
|
||||
post_select_event(selection_idx);
|
||||
|
||||
if (selection_idx < 0) {
|
||||
@@ -1289,6 +1420,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
int* from_printer = static_cast<int*>(m_comboBox_filament->GetClientData());
|
||||
|
||||
m_filament_type = "";
|
||||
const FilamentInfos* selected_info = selected_filament_info();
|
||||
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
|
||||
if (preset_bundle) {
|
||||
std::ostringstream stream;
|
||||
@@ -1309,9 +1441,8 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
std::string nozzle_diameter_str = stream.str();
|
||||
// Resolve the selection against the same list Popup() built the dropdown from, so the two
|
||||
// halves of the dialog cannot disagree about which filaments this machine can use.
|
||||
const std::string selected = m_comboBox_filament->GetValue().ToStdString();
|
||||
if (!selected.empty()) {
|
||||
const std::string filament_id = map_filament_items[selected].filament_id;
|
||||
if (selected_info) {
|
||||
const std::string& filament_id = selected_info->filament_id;
|
||||
for (Preset *it : preset_bundle->get_filament_presets_for_machine(
|
||||
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
|
||||
if (it->filament_id != filament_id)
|
||||
@@ -1364,7 +1495,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
//reset cali
|
||||
int cali_select_idx = -1;
|
||||
|
||||
if ( !this->obj || m_filament_selection < 0) {
|
||||
if ( !this->obj || m_filament_selection < 0 || !selected_info) {
|
||||
m_input_k_val->Enable(false);
|
||||
m_input_n_val->Enable(false);
|
||||
m_button_confirm->Disable(); // ORCA No need to change style
|
||||
@@ -1383,21 +1514,9 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
ams_filament_id = "";
|
||||
ams_setting_id = "";
|
||||
|
||||
if (preset_bundle) {
|
||||
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
|
||||
auto itor = map_filament_items.find(m_comboBox_filament->GetValue().ToStdString());
|
||||
if ( itor != map_filament_items.end()) {
|
||||
ams_filament_id = itor->second.filament_id;
|
||||
ams_setting_id = itor->second.setting_id;
|
||||
break;
|
||||
}
|
||||
|
||||
if (it->alias.compare(m_comboBox_filament->GetValue().ToStdString()) == 0) {
|
||||
ams_filament_id = it->filament_id;
|
||||
ams_setting_id = it->setting_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (selected_info) {
|
||||
ams_filament_id = selected_info->filament_id;
|
||||
ams_setting_id = selected_info->setting_id;
|
||||
}
|
||||
|
||||
wxArrayString items;
|
||||
@@ -1529,6 +1648,11 @@ void AMSMaterialsSetting::on_dpi_changed(const wxRect &suggested_rect)
|
||||
m_button_reset->Rescale(); // ORCA
|
||||
m_button_confirm->Rescale(); // ORCA
|
||||
m_button_close->Rescale(); // ORCA
|
||||
// An open list would keep the measurements of the old stars, so close it before replacing them.
|
||||
m_comboBox_filament->CloseDropdown();
|
||||
m_star_filled.msw_rescale();
|
||||
m_star_empty.msw_rescale();
|
||||
update_favorite_stars();
|
||||
this->Refresh();
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "slic3r/GUI/Widgets/StateColor.hpp"
|
||||
#include <wx/panel.h>
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
#define AMS_MATERIALS_SETTING_DEF_COLOUR wxColour(255, 255, 255)
|
||||
#define AMS_MATERIALS_SETTING_GREY900 wxColour(38, 46, 48)
|
||||
@@ -156,6 +157,7 @@ protected:
|
||||
void create_panel_kn(wxWindow* parent);
|
||||
void on_dpi_changed(const wxRect &suggested_rect) override;
|
||||
void on_select_filament(wxCommandEvent& evt);
|
||||
void on_toggle_favorite_filament(wxCommandEvent& evt);
|
||||
void on_select_cali_result(wxCommandEvent &evt);
|
||||
void on_select_ok(wxCommandEvent &event);
|
||||
void on_select_reset(wxCommandEvent &event);
|
||||
@@ -163,6 +165,11 @@ protected:
|
||||
void on_clr_picker(wxMouseEvent &event);
|
||||
bool is_virtual_tray();
|
||||
void update_widgets();
|
||||
void rebuild_filament_dropdown_items(const std::string& selected_filament_id);
|
||||
void update_favorite_stars();
|
||||
std::set<std::string> load_favorite_filament_ids() const;
|
||||
void save_favorite_filament_ids(const std::set<std::string>& favorite_ids);
|
||||
void toggle_favorite_filament(const std::string& filament_id);
|
||||
|
||||
void update_filament_editing(bool is_printing);
|
||||
// Orca: the variant index of the filament's per-variant options on the nozzle this tray feeds
|
||||
@@ -202,12 +209,7 @@ protected:
|
||||
|
||||
int m_pa_cali_select_id = 0;
|
||||
|
||||
// Orca: native wxComboBox on macOS (the custom ComboBox mis-renders the filament dropdown there)
|
||||
#ifdef __APPLE__
|
||||
wxComboBox *m_comboBox_filament;
|
||||
#else
|
||||
ComboBox *m_comboBox_filament;
|
||||
#endif
|
||||
ComboBox * m_comboBox_cali_result;
|
||||
TextInput* m_readonly_filament;
|
||||
|
||||
@@ -215,7 +217,20 @@ protected:
|
||||
std::string filament_id;
|
||||
std::string setting_id;
|
||||
};
|
||||
std::map<std::string, FilamentInfos> map_filament_items;
|
||||
struct FilamentListItem {
|
||||
FilamentInfos filament;
|
||||
bool is_filament{false};
|
||||
};
|
||||
const FilamentInfos* selected_filament_info() const;
|
||||
|
||||
// Keyed by the same wxString the dropdown shows, so a row's name never has to be converted back.
|
||||
std::map<wxString, FilamentInfos> map_filament_items;
|
||||
std::vector<wxString> m_sorted_filament_items;
|
||||
std::vector<FilamentListItem> m_filament_list_items;
|
||||
ScalableBitmap m_star_filled;
|
||||
ScalableBitmap m_star_empty;
|
||||
// Favorites changed while the list was open; their rows are regrouped before it opens again.
|
||||
bool m_favorites_regroup{false};
|
||||
};
|
||||
|
||||
wxDECLARE_EVENT(EVT_SELECTED_COLOR, wxCommandEvent);
|
||||
|
||||
@@ -25,8 +25,9 @@ DockPanel::DockPanel(wxWindow* parent,
|
||||
const std::string& html,
|
||||
MessageHandler on_message,
|
||||
CloseHandler on_close,
|
||||
CloseHandler on_destroyed)
|
||||
: WebPanel(parent, web_hosting::orca_bridge_script())
|
||||
CloseHandler on_destroyed,
|
||||
const wxString& profile)
|
||||
: WebPanel(parent, web_hosting::orca_bridge_script(), profile)
|
||||
, m_html(html)
|
||||
, m_on_message(std::move(on_message))
|
||||
, m_on_close(std::move(on_close))
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include <wx/string.h>
|
||||
|
||||
class wxWindow;
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
@@ -27,7 +29,8 @@ public:
|
||||
const std::string& html,
|
||||
MessageHandler on_message,
|
||||
CloseHandler on_close,
|
||||
CloseHandler on_destroyed);
|
||||
CloseHandler on_destroyed,
|
||||
const wxString& profile = wxEmptyString);
|
||||
~DockPanel() override;
|
||||
|
||||
// Main thread only.
|
||||
|
||||
@@ -3232,9 +3232,16 @@ bool GUI_App::on_init_inner()
|
||||
#ifdef _MSW_DARK_MODE
|
||||
|
||||
#ifndef __WINDOWS__
|
||||
// The label colours and the dark mode flag were set above from the saved mode. When the system theme
|
||||
// differs from it, refresh them, or dialogs that map their colours stay in the old mode for this session.
|
||||
const bool saved_dark_mode = dark_mode();
|
||||
wxSystemAppearance app = wxSystemSettings::GetAppearance();
|
||||
GUI::wxGetApp().app_config->set("dark_color_mode", app.IsDark() ? "1" : "0");
|
||||
GUI::wxGetApp().app_config->save();
|
||||
if (dark_mode() != saved_dark_mode) {
|
||||
init_label_colours();
|
||||
Update_dark_mode_flag();
|
||||
}
|
||||
#endif // __APPLE__
|
||||
|
||||
|
||||
|
||||
@@ -1617,7 +1617,7 @@ void ObjectList::list_manipulation(const wxPoint& mouse_pos, bool evt_context_me
|
||||
int obj_idx, vol_idx;
|
||||
get_selected_item_indexes(obj_idx, vol_idx, item);
|
||||
if (obj_idx != -1) {
|
||||
Plater::TakeSnapshot(plater, _u8L("Shift objects to bed"));
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
|
||||
(*m_objects)[obj_idx]->ensure_on_bed();
|
||||
cnv->reload_scene(true, true);
|
||||
update_info_items(obj_idx);
|
||||
@@ -2779,12 +2779,13 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type)
|
||||
GLCanvas3D* cnv = plater->canvas3D();
|
||||
|
||||
switch (type) {
|
||||
case InfoItemType::CustomSupports:
|
||||
case InfoItemType::CustomSupports: {
|
||||
cnv->get_gizmos_manager().reset_all_states();
|
||||
Plater::TakeSnapshot(plater, "Remove support painting");
|
||||
Plater::TakeSnapshot snapshot(plater, "Remove support painting");
|
||||
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
|
||||
mv->supported_facets.reset();
|
||||
break;
|
||||
}
|
||||
|
||||
// BBS: remove CustomSeam
|
||||
|
||||
@@ -2795,19 +2796,21 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type)
|
||||
}
|
||||
break;
|
||||
|
||||
case InfoItemType::MmSegmentation:
|
||||
case InfoItemType::MmSegmentation: {
|
||||
cnv->get_gizmos_manager().reset_all_states();
|
||||
Plater::TakeSnapshot(plater, "Remove color painting");
|
||||
Plater::TakeSnapshot snapshot(plater, "Remove color painting");
|
||||
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
|
||||
mv->mmu_segmentation_facets.reset();
|
||||
break;
|
||||
}
|
||||
|
||||
case InfoItemType::FuzzySkin:
|
||||
case InfoItemType::FuzzySkin: {
|
||||
cnv->get_gizmos_manager().reset_all_states();
|
||||
Plater::TakeSnapshot(plater, _u8L("Remove paint-on fuzzy skin"));
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Remove paint-on fuzzy skin"));
|
||||
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
|
||||
mv->fuzzy_skin_facets.reset();
|
||||
break;
|
||||
}
|
||||
|
||||
// BBS: remove Sinking
|
||||
case InfoItemType::Undef : assert(false); break;
|
||||
@@ -6718,7 +6721,7 @@ void ObjectList::OnEditingStarted(wxDataViewEvent &event)
|
||||
else if (col == colSinking) {
|
||||
Plater * plater = wxGetApp().plater();
|
||||
GLCanvas3D *cnv = plater->canvas3D();
|
||||
Plater::TakeSnapshot(plater, _u8L("Shift objects to bed"));
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
|
||||
int obj_idx, vol_idx;
|
||||
get_selected_item_indexes(obj_idx, vol_idx, item);
|
||||
(*m_objects)[obj_idx]->ensure_on_bed();
|
||||
|
||||
@@ -1,891 +0,0 @@
|
||||
#include "GLGizmoImage.hpp"
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "libslic3r/format.hpp"
|
||||
|
||||
#include "slic3r/GUI/Camera.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/GLTexture.hpp"
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/IconManager.hpp"
|
||||
#include "slic3r/GUI/ImGuiWrapper.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/TextureLibrary.hpp"
|
||||
#include "slic3r/GUI/Shortcuts.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoUtils.hpp"
|
||||
#include "slic3r/GUI/Jobs/Worker.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <imgui.h>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <wx/string.h>
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
GLGizmoImage::GLGizmoImage(GLCanvas3D &parent, const std::string &icon_filename, unsigned int sprite_id)
|
||||
: GLGizmoTextureDisplacement(parent, icon_filename, sprite_id)
|
||||
{
|
||||
}
|
||||
|
||||
bool GLGizmoImage::on_init()
|
||||
{
|
||||
if (!GLGizmoTextureDisplacement::on_init())
|
||||
return false;
|
||||
// Everything the base set up stays - it is the same painting machinery - except the key that
|
||||
// opens it, which is this tool's own.
|
||||
m_shortcut = Shortcut::GizmoImage;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string GLGizmoImage::on_get_name() const
|
||||
{
|
||||
return _u8L("Image");
|
||||
}
|
||||
|
||||
wxString GLGizmoImage::handle_snapshot_action_name(bool shift_down, GLGizmoPainterBase::Button button_down) const
|
||||
{
|
||||
// Same paint, same masks; only the undo step's wording follows the tool the user is in.
|
||||
const bool erasing = shift_down || (m_erase_mode != (button_down == GLGizmoPainterBase::Button::Right));
|
||||
return erasing ? _L("Remove image from the area") : _L("Place image");
|
||||
}
|
||||
|
||||
bool GLGizmoTextureDisplacement::pick_background_at(const Vec2d &mouse_pos)
|
||||
{
|
||||
ModelVolume *mv = texture_volume();
|
||||
ModelObject *mo = m_c->selection_info()->model_object();
|
||||
TextureDisplacementLayer *layer = mv != nullptr ? active_layer() : nullptr;
|
||||
if (mv == nullptr || mo == nullptr || layer == nullptr || layer->empty())
|
||||
return false;
|
||||
|
||||
const int idx = texture_volume_raycaster_index();
|
||||
if (idx < 0)
|
||||
return false;
|
||||
const Selection &selection = m_parent.get_selection();
|
||||
const Transform3d trafo = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix();
|
||||
const Camera &camera = wxGetApp().plater()->get_camera();
|
||||
|
||||
Vec3f hit = Vec3f::Zero(), normal = Vec3f::Zero();
|
||||
if (!m_c->raycaster()->raycasters()[size_t(idx)]->unproject_on_mesh(
|
||||
mouse_pos, trafo, camera, hit, normal,
|
||||
m_c->object_clipper()->get_clipping_plane()))
|
||||
return false;
|
||||
|
||||
// The picture's own colour at that point, read through the projection the bake uses, rather
|
||||
// than the pixel on screen: the screen pixel carries the lighting and the filament the colour
|
||||
// was already quantized to, so keying on it would remove the wrong thing.
|
||||
//
|
||||
// Uncorrected, deliberately. The key is applied at the end of the correction chain, against
|
||||
// colours the earlier passes have already moved, so it has to be stated in those terms - the
|
||||
// same reason the panel's dropper reads the corrected preview.
|
||||
const indexed_triangle_set &base = mv->mesh().its;
|
||||
const indexed_triangle_set patch =
|
||||
extract_painted_patch(base, mv->texture_displacement_facet(layer->slot).get_data());
|
||||
Vec3f center = Vec3f::Zero(), axis = Vec3f::UnitZ(), average_normal = Vec3f::UnitZ();
|
||||
if (!patch.indices.empty())
|
||||
texture_displacement_patch_frame(patch, texture_displacement_vertex_normals(base), center, axis,
|
||||
average_normal);
|
||||
|
||||
DecodedHeightTexture tex = decode_height_texture(*layer);
|
||||
apply_color_adjustments(tex, *layer);
|
||||
Vec3f rgb;
|
||||
if (!sample_layer_color(tex, *layer, hit, normal, rgb, center, axis))
|
||||
return false;
|
||||
|
||||
for (int c = 0; c < 3; ++c)
|
||||
layer->bg_color[c] = std::clamp(rgb[c], 0.f, 1.f);
|
||||
layer->bg_from_corners = false;
|
||||
layer->bg_remove = true;
|
||||
m_image_bg_picking = false;
|
||||
m_preview_params_dirty = true;
|
||||
m_parent.set_as_dirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::set_file_drag_over(bool over)
|
||||
{
|
||||
if (!image_mode() || m_file_drag_over == over)
|
||||
return;
|
||||
m_file_drag_over = over;
|
||||
// A drag is not a mouse move as far as the canvas is concerned, so nothing would repaint and the
|
||||
// zone would appear only once something else happened to trigger a frame.
|
||||
m_parent.set_as_dirty();
|
||||
m_parent.request_extra_frame();
|
||||
}
|
||||
|
||||
// Drawn over the whole canvas on the foreground list, from inside the panel's ImGui frame, which is
|
||||
// what puts it above the 3D view and above the panel itself.
|
||||
void GLGizmoTextureDisplacement::render_image_drop_hint()
|
||||
{
|
||||
if (!m_file_drag_over)
|
||||
return;
|
||||
|
||||
const ImVec2 size = ImGui::GetIO().DisplaySize;
|
||||
if (size.x < 1.f || size.y < 1.f)
|
||||
return;
|
||||
ImDrawList *dl = ImGui::GetForegroundDrawList();
|
||||
const ImU32 teal = ImGui::GetColorU32(ImGuiWrapper::COL_ORCA);
|
||||
|
||||
// Dim the view so the zone reads as a state the canvas is in, not as decoration on top of it.
|
||||
dl->AddRectFilled(ImVec2(0.f, 0.f), size, IM_COL32(0, 0, 0, 70));
|
||||
|
||||
const float inset = std::round(ImGui::GetFontSize() * 1.2f);
|
||||
const ImVec2 a(inset, inset), b(size.x - inset, size.y - inset);
|
||||
if (b.x <= a.x || b.y <= a.y)
|
||||
return;
|
||||
|
||||
// A dashed border, drawn as runs along the four edges: ImGui 1.83 has no dashed stroke.
|
||||
const float dash = std::round(ImGui::GetFontSize() * 0.75f);
|
||||
const float gap = dash;
|
||||
const float th = std::max(2.f, std::round(ImGui::GetFontSize() * 0.12f));
|
||||
for (float x = a.x; x < b.x; x += dash + gap) {
|
||||
const float x1 = std::min(x + dash, b.x);
|
||||
dl->AddLine(ImVec2(x, a.y), ImVec2(x1, a.y), teal, th);
|
||||
dl->AddLine(ImVec2(x, b.y), ImVec2(x1, b.y), teal, th);
|
||||
}
|
||||
for (float y = a.y; y < b.y; y += dash + gap) {
|
||||
const float y1 = std::min(y + dash, b.y);
|
||||
dl->AddLine(ImVec2(a.x, y), ImVec2(a.x, y1), teal, th);
|
||||
dl->AddLine(ImVec2(b.x, y), ImVec2(b.x, y1), teal, th);
|
||||
}
|
||||
|
||||
// What dropping here will do. Worded as an invitation rather than a promise: anything that is
|
||||
// not a picture falls through to the model loader, exactly as it did before this tool existed.
|
||||
const std::string text = _u8L("Drop a picture to put it on the model");
|
||||
const ImVec2 ts = ImGui::CalcTextSize(text.c_str());
|
||||
const float pad = std::round(ImGui::GetFontSize() * 0.8f);
|
||||
const ImVec2 c((a.x + b.x) * 0.5f, (a.y + b.y) * 0.5f);
|
||||
const ImVec2 r0(c.x - ts.x * 0.5f - pad, c.y - ts.y * 0.5f - pad * 0.6f);
|
||||
const ImVec2 r1(c.x + ts.x * 0.5f + pad, c.y + ts.y * 0.5f + pad * 0.6f);
|
||||
dl->AddRectFilled(r0, r1, ImGui::GetColorU32(ImGuiCol_WindowBg, 0.92f), ImGui::GetStyle().FrameRounding);
|
||||
dl->AddRect(r0, r1, teal, ImGui::GetStyle().FrameRounding, 0, th * 0.5f);
|
||||
dl->AddText(ImVec2(c.x - ts.x * 0.5f, c.y - ts.y * 0.5f), ImGui::GetColorU32(ImGuiCol_Text), text.c_str());
|
||||
}
|
||||
|
||||
bool GLGizmoTextureDisplacement::drop_image_file(const std::string &path)
|
||||
{
|
||||
if (!image_mode() || texture_volume() == nullptr)
|
||||
return false;
|
||||
|
||||
// Imported rather than just read: the same thing the panel's Import tile does, so a dropped
|
||||
// picture lands in "My images" and is there the next time without hunting for the file again.
|
||||
std::string error;
|
||||
const std::optional<TextureLibraryEntry> entry = import_texture_to_library(path, error);
|
||||
if (!entry) {
|
||||
show_error(nullptr, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// The tool starts with no layer at all, so a drop before anything is loaded has to make one.
|
||||
if (active_layer() == nullptr)
|
||||
add_texture_layer();
|
||||
TextureDisplacementLayer *layer = active_layer();
|
||||
if (layer == nullptr)
|
||||
return false;
|
||||
set_layer_texture(*layer, *entry);
|
||||
m_parent.set_as_dirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// The Image tool's window.
|
||||
//
|
||||
// A member of GLGizmoTextureDisplacement rather than of GLGizmoImage, and defined here rather than
|
||||
// in that class's own file: it needs the same private machinery the displacement panel uses (the
|
||||
// layer, the texture picker, the previews, the bake), and keeping it here leaves that 1700 line
|
||||
// panel untouched instead of threading it with tests for which tool is drawing.
|
||||
//
|
||||
// What it deliberately leaves out, all of which only means something once the surface moves or once
|
||||
// there is more than one picture: brushes and the erase toggle, the layer stack, blend modes, depth,
|
||||
// midlevel, edge fade, tiling mode, the unwrap projection and its UV editor, seams, the wireframe
|
||||
// and the mesh-preparation sections.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, float bottom_limit)
|
||||
{
|
||||
ModelObject *mo = m_c->selection_info()->model_object();
|
||||
if (mo == nullptr)
|
||||
return;
|
||||
ModelVolume *mv = texture_volume();
|
||||
|
||||
// The palette follows project state nobody tells this gizmo about (the filaments in the
|
||||
// sidebar), so it is checked once a frame and a change re-runs the preview.
|
||||
cached_palette();
|
||||
if (m_palette_changed)
|
||||
m_preview_params_dirty = true;
|
||||
|
||||
// Everything this tool fixes, re-pinned each frame so what the preview shows is what Apply will
|
||||
// produce: Standard's recipe, zero depth with colour on, a click that floods the connected
|
||||
// surface rather than a brush, and the fast preview, which with no relief to build is both
|
||||
// instant and exactly the final colour.
|
||||
// Two statements, not one `||`: short-circuiting would skip the image presets on any frame the
|
||||
// standard ones changed something.
|
||||
const bool std_changed = apply_standard_mode_presets(mv);
|
||||
const bool image_changed = apply_image_mode_presets(mv);
|
||||
if (std_changed || image_changed)
|
||||
m_preview_params_dirty = true;
|
||||
m_tool_type = ToolType::SMART_FILL;
|
||||
m_cursor_type = TriangleSelector::CursorType::POINTER;
|
||||
m_erase_mode = false;
|
||||
m_auto_update = true;
|
||||
if (!m_use_shaded_preview)
|
||||
apply_view_mode(1);
|
||||
|
||||
float scale = m_parent.get_scale();
|
||||
#ifdef WIN32
|
||||
const int dpi = get_dpi_for_window(wxGetApp().GetTopWindow());
|
||||
scale *= (float) dpi / (float) DPI_DEFAULT;
|
||||
#endif // WIN32
|
||||
|
||||
const float approx_height = m_imgui->scaled(22.f);
|
||||
y = std::min(y, bottom_limit - approx_height);
|
||||
|
||||
// Docked (the default) it is pinned to the right edge of the 3D canvas and cannot be moved, the
|
||||
// same place and the same rule as the displacement panel, so the two tools do not put their
|
||||
// controls in different corners. Undocked it becomes an ordinary floating window with a title
|
||||
// bar to drag it by; the position is deliberately not seeded on undock, so it stays exactly
|
||||
// where it was and the user simply gains the ability to move it.
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse;
|
||||
if (!m_undocked) {
|
||||
flags |= ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar;
|
||||
// Right-aligned (pivot 1), so the width the window auto-resized to last frame does not need
|
||||
// to be known here.
|
||||
float right = float(m_parent.get_canvas_size().get_width()) - m_imgui->scaled(0.5f);
|
||||
GizmoImguiSetNextWIndowPos(right, y, 0.f, 0.f, ImGuiCond_Always, 1.0f, 0.0f);
|
||||
}
|
||||
|
||||
ImGuiWrapper::push_toolbar_style(m_parent.get_scale());
|
||||
GizmoImguiBegin(get_name(), flags);
|
||||
ensure_panel_icons();
|
||||
const float previous_body_h = m_panel_body_h;
|
||||
const float previous_footer_h = m_panel_footer_h;
|
||||
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
const bool busy = m_bake_in_progress || m_prepare_in_progress;
|
||||
const float panel_w = m_imgui->scaled(16.f);
|
||||
const float frame_h = ImGui::GetFrameHeight();
|
||||
const float icon_sm = std::round(frame_h * 1.05f);
|
||||
const float gap_s = std::round(m_imgui->scaled(0.4f));
|
||||
const float label_w = m_imgui->scaled(4.6f);
|
||||
const float wrap_w = m_imgui->scaled(15.f);
|
||||
const ImVec4 orca = ImGuiWrapper::COL_ORCA;
|
||||
const ImVec4 col_frame = ImGui::GetStyleColorVec4(ImGuiCol_Separator);
|
||||
|
||||
const auto hover_tip = [&](const auto &text) {
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(text, wrap_w);
|
||||
};
|
||||
// Label column plus a slider filling the rest of the row, the same shape the displacement panel
|
||||
// uses, so the two tools read as one family.
|
||||
const auto float_row = [&](const char *id, const wxString &label, float *v, float v_min, float v_max,
|
||||
const char *format, bool log) -> bool {
|
||||
const float x0 = ImGui::GetCursorPosX();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
m_imgui->text(label);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
|
||||
ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX());
|
||||
return ImGui::SliderFloat(id, v, v_min, v_max, format,
|
||||
ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0));
|
||||
};
|
||||
// Borderless icon button, for the dock toggle: always the grey monochrome variant.
|
||||
const auto icon_button = [&](int uid, const std::string &iconfile, const wxString &label,
|
||||
const wxString &tip_text) -> bool {
|
||||
const auto it = m_panel_icon_map.find(iconfile);
|
||||
const float pad = std::max(1.f, std::round(icon_sm * 0.14f));
|
||||
bool clicked = false;
|
||||
ImGui::PushID(uid);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.f);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.f, 0.f, 0.f, 0.f));
|
||||
if (it != m_panel_icon_map.end() && !it->second.empty() && it->second[0]->is_valid()) {
|
||||
const IconManager::Icon &ic = *it->second[0];
|
||||
clicked = m_imgui->image_button((ImTextureID) (intptr_t) ic.tex_id,
|
||||
ImVec2(icon_sm - 2.f * pad, icon_sm - 2.f * pad), ic.tl, ic.br, int(pad));
|
||||
} else {
|
||||
clicked = ImGui::Button(label.ToUTF8().data(), ImVec2(0.f, icon_sm));
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopID();
|
||||
hover_tip(tip_text);
|
||||
return clicked;
|
||||
};
|
||||
// A section header that folds its contents away. ImGui's own CollapsingHeader draws a filled bar
|
||||
// that is louder than anything else in this window, so this is the same triangle-and-text the
|
||||
// displacement panel's "More settings" link uses, at heading weight.
|
||||
const auto section = [&](const char *id, const wxString &label, bool &open) {
|
||||
const float arrow = std::round(ImGui::GetFontSize() * 0.55f);
|
||||
const ImVec2 ts = ImGui::CalcTextSize(label.ToUTF8().data());
|
||||
const ImVec2 p = ImGui::GetCursorScreenPos();
|
||||
ImGui::PushID(id);
|
||||
if (ImGui::InvisibleButton("##sec", ImVec2(panel_w, ts.y + gap_s)))
|
||||
open = !open;
|
||||
const bool hot = ImGui::IsItemHovered();
|
||||
ImDrawList *dl = ImGui::GetWindowDrawList();
|
||||
const ImU32 col = ImGui::GetColorU32(hot ? ImGuiCol_Text : ImGuiCol_TextDisabled);
|
||||
const float cx = p.x + 0.5f * arrow, cy = p.y + 0.5f * ts.y, hs = 0.32f * arrow;
|
||||
if (open)
|
||||
dl->AddTriangleFilled(ImVec2(cx - hs, cy - 0.5f * hs), ImVec2(cx + hs, cy - 0.5f * hs),
|
||||
ImVec2(cx, cy + 0.5f * hs), col);
|
||||
else
|
||||
dl->AddTriangleFilled(ImVec2(cx - 0.5f * hs, cy - hs), ImVec2(cx - 0.5f * hs, cy + hs),
|
||||
ImVec2(cx + 0.5f * hs, cy), col);
|
||||
dl->AddText(ImVec2(p.x + arrow + gap_s, p.y), col, label.ToUTF8().data());
|
||||
ImGui::PopID();
|
||||
return open;
|
||||
};
|
||||
|
||||
// The integer twin of float_row, for the colour count.
|
||||
const auto int_row = [&](const char *id, const wxString &label, int *v, int v_min, int v_max,
|
||||
const char *format) -> bool {
|
||||
const float x0 = ImGui::GetCursorPosX();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
m_imgui->text(label);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
|
||||
ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX());
|
||||
return ImGui::SliderInt(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp);
|
||||
};
|
||||
// Framed icon toggle, the displacement panel's one cut down to what this window needs.
|
||||
const auto icon_toggle = [&](int uid, const std::string &iconfile, bool active, const wxString &label,
|
||||
const wxString &tip_text) -> bool {
|
||||
const auto it = m_panel_icon_map.find(iconfile);
|
||||
const float pad = std::max(1.f, std::round(icon_sm * 0.14f));
|
||||
bool clicked = false;
|
||||
ImGui::PushID(uid);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.f);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, active ? ImVec4(orca.x, orca.y, orca.z, 0.22f) : ImVec4(0.f, 0.f, 0.f, 0.f));
|
||||
if (it != m_panel_icon_map.end() && int(it->second.size()) > (active ? 1 : 0) &&
|
||||
it->second[active ? 1 : 0]->is_valid()) {
|
||||
const IconManager::Icon &ic = *it->second[active ? 1 : 0];
|
||||
clicked = m_imgui->image_button((ImTextureID) (intptr_t) ic.tex_id,
|
||||
ImVec2(icon_sm - 2.f * pad, icon_sm - 2.f * pad), ic.tl, ic.br, int(pad));
|
||||
} else {
|
||||
clicked = ImGui::Button(label.ToUTF8().data(), ImVec2(0.f, icon_sm));
|
||||
}
|
||||
ImGui::GetWindowDrawList()->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(),
|
||||
ImGui::GetColorU32(active ? orca : col_frame), style.FrameRounding);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopID();
|
||||
hover_tip(tip_text);
|
||||
return clicked;
|
||||
};
|
||||
|
||||
TextureDisplacementLayer *layer = mv != nullptr ? active_layer() : nullptr;
|
||||
|
||||
{
|
||||
const float x0 = ImGui::GetCursorPosX();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
m_imgui->push_bold_font();
|
||||
m_imgui->text(wxString::FromUTF8(get_name(false)));
|
||||
m_imgui->pop_bold_font();
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + panel_w - icon_sm));
|
||||
if (icon_button(920, "canvas_drag.svg", m_undocked ? _L("Dock panel") : _L("Undock panel"),
|
||||
_L("Detach this panel so it can be dragged anywhere over the 3D view, or dock it back "
|
||||
"against the right edge.")))
|
||||
m_undocked = !m_undocked;
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
// Everything between the title and the footer scrolls as one, so Apply and Close stay on screen
|
||||
// on a short canvas instead of being the first thing pushed off the bottom. Sized from last
|
||||
// frame's content and footer heights, as the displacement panel's body is.
|
||||
const float bottom = m_undocked ? ImGui::GetIO().DisplaySize.y : bottom_limit;
|
||||
const float max_body_h = std::max(m_imgui->scaled(8.f), bottom - ImGui::GetCursorScreenPos().y -
|
||||
m_panel_footer_h - style.WindowPadding.y -
|
||||
style.ItemSpacing.y);
|
||||
const float body_h = m_panel_body_h > 0.f ? std::min(m_panel_body_h + 1.f, max_body_h) : max_body_h;
|
||||
const float scrollbar_w = m_imgui->scaled(0.5f);
|
||||
const ImVec4 grab = wxGetApp().dark_mode() ? ImVec4(1.f, 1.f, 1.f, 0.26f) : ImVec4(0.f, 0.f, 0.f, 0.26f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, scrollbar_w);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 0.5f * scrollbar_w);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, grab);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(grab.x, grab.y, grab.z, 0.45f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(grab.x, grab.y, grab.z, 0.65f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.f, 0.f, 0.f, 0.f));
|
||||
ImGui::BeginChild("##image_body", ImVec2(panel_w, body_h), false);
|
||||
|
||||
// The picture, shown whole and the full width of the window: it is the thing being placed, so a
|
||||
// row-sized thumbnail beside a file name is the wrong size for it. Clicking it opens the library,
|
||||
// which is also where Import lives, and before anything is loaded the same box is an empty frame
|
||||
// with a plus in it - one target, in the same place, whichever state the tool is in.
|
||||
const auto open_library = [&](int slot) {
|
||||
m_picker_slot = slot;
|
||||
m_picker_open_request = true;
|
||||
};
|
||||
// A layer with no picture on it is the starting state, not a broken one: the tool creates the
|
||||
// layer empty and the panel stays on the frame below until a picture has been chosen.
|
||||
const bool has_picture = layer != nullptr && !layer->empty();
|
||||
const bool placed = mv != nullptr && mv->is_texture_displacement_painted();
|
||||
|
||||
// The handle comes up by itself the first moment there is something to anchor it to, so the
|
||||
// picture can be nudged straight after the click that placed it without hunting for a toggle.
|
||||
// Armed once: turning it back off has to stay off, not be undone on the next frame.
|
||||
if (has_picture && placed && !m_image_handle_armed) {
|
||||
m_image_handle_armed = true;
|
||||
if (update_adjust_anchor())
|
||||
m_adjust_texture_mode = true;
|
||||
}
|
||||
if (!has_picture) {
|
||||
const float box_h = std::round(panel_w * 0.6f);
|
||||
const ImVec2 p0 = ImGui::GetCursorScreenPos();
|
||||
m_imgui->disabled_begin(busy || mv == nullptr);
|
||||
const bool clicked = ImGui::InvisibleButton("##load", ImVec2(panel_w, box_h));
|
||||
m_imgui->disabled_end();
|
||||
const bool hot = ImGui::IsItemHovered();
|
||||
ImDrawList *dl = ImGui::GetWindowDrawList();
|
||||
const ImU32 col = ImGui::GetColorU32(hot ? orca : col_frame);
|
||||
dl->AddRect(p0, ImVec2(p0.x + panel_w, p0.y + box_h), col, style.FrameRounding);
|
||||
const float cx = p0.x + 0.5f * panel_w, cy = p0.y + 0.5f * box_h;
|
||||
const float arm = std::round(box_h * 0.18f), thick = std::max(1.f, std::round(box_h * 0.035f));
|
||||
dl->AddLine(ImVec2(cx - arm, cy), ImVec2(cx + arm, cy), col, thick);
|
||||
dl->AddLine(ImVec2(cx, cy - arm), ImVec2(cx, cy + arm), col, thick);
|
||||
hover_tip(_u8L("Choose a picture to put on the model."));
|
||||
if (clicked) {
|
||||
if (layer == nullptr)
|
||||
add_texture_layer(); // created with no picture on it, on purpose
|
||||
open_library(layer != nullptr ? layer->slot : m_active_layer_slot);
|
||||
}
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + panel_w);
|
||||
ImGui::TextDisabled("%s", _u8L("Choose a picture, then click the model to put it there.").c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
} else {
|
||||
// The colour texture is the corrected image at full resolution, which is exactly what should
|
||||
// be previewed - move a correction slider and the box follows. A grayscale picture has no
|
||||
// colour texture, so it falls back to the height thumbnail.
|
||||
GLTexture *shown = get_layer_color_texture(*layer);
|
||||
if (shown == nullptr)
|
||||
shown = get_layer_thumbnail(*layer);
|
||||
const float aspect = std::max(0.05f, layer_texture_aspect(*layer));
|
||||
const float box_h = std::clamp(std::round(panel_w / aspect), std::round(panel_w * 0.35f),
|
||||
std::round(panel_w * 1.1f));
|
||||
const ImVec2 box_min = ImGui::GetCursorScreenPos();
|
||||
bool box_hit = false;
|
||||
if (shown != nullptr) {
|
||||
box_hit = ImGui::ImageButton((ImTextureID) (intptr_t) shown->get_id(), ImVec2(panel_w, box_h),
|
||||
ImVec2(0.f, 0.f), ImVec2(1.f, 1.f), 0);
|
||||
} else {
|
||||
box_hit = ImGui::Button("?##thumb", ImVec2(panel_w, box_h));
|
||||
}
|
||||
hover_tip(m_image_bg_picking ?
|
||||
_u8L("Click the background, here or on the model.") :
|
||||
_u8L("Choose a different picture. Where you placed it and how it is corrected stay as "
|
||||
"they are."));
|
||||
if (box_hit) {
|
||||
if (!m_image_bg_picking) {
|
||||
open_library(layer->slot);
|
||||
} else {
|
||||
// Read straight out of the decoded image rather than off the screen: the preview is
|
||||
// scaled, and it already has the corrections applied, so a screen read would key the
|
||||
// corrected colour against itself.
|
||||
const ImVec2 rel((ImGui::GetIO().MousePos.x - box_min.x) / panel_w,
|
||||
(ImGui::GetIO().MousePos.y - box_min.y) / box_h);
|
||||
const DecodedHeightTexture &tex = decode_height_texture(*layer);
|
||||
if (tex.has_color()) {
|
||||
const int px = std::clamp(int(rel.x * float(tex.width)), 0, tex.width - 1);
|
||||
const int py = std::clamp(int(rel.y * float(tex.height)), 0, tex.height - 1);
|
||||
const size_t i = (size_t(py) * size_t(tex.width) + size_t(px)) * 3;
|
||||
for (int c = 0; c < 3; ++c)
|
||||
layer->bg_color[c] = float(tex.rgb[i + size_t(c)]) / 255.f;
|
||||
layer->bg_from_corners = false;
|
||||
layer->bg_remove = true;
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
m_image_bg_picking = false;
|
||||
}
|
||||
}
|
||||
const std::string name = layer->name.empty() ? _u8L("Image") : layer->name;
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + panel_w);
|
||||
ImGui::TextDisabled("%s", name.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
|
||||
if (!height_texture_has_color(*layer)) {
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + panel_w);
|
||||
m_imgui->warning_text(_L("This image is grayscale, so it has no colors to print. Choose a color picture."));
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
|
||||
// Placing it: one click floods the surface it lands on, out to the first fold sharper than
|
||||
// the angle below. No brush, no erase toggle - on a flat side or a cube face the click alone
|
||||
// is the whole gesture, and the slider is there for the cases where it is not.
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + panel_w);
|
||||
ImGui::TextDisabled("%s", placed ? _u8L("Click the model to move the picture somewhere else.").c_str() :
|
||||
_u8L("Click the model to put the picture there.").c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
float fill_angle = m_smart_fill_angle;
|
||||
if (float_row("##fill_angle", _L("Spread"), &fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°", false))
|
||||
m_smart_fill_angle = fill_angle;
|
||||
hover_tip(_u8L("How far a click spreads from the spot you hit: it keeps going while the surface stays "
|
||||
"flatter than this, and stops at a sharper fold. Small values keep it to one face."));
|
||||
|
||||
ImGui::Separator();
|
||||
if (section("place", _L("Placement"), m_image_sec_place)) {
|
||||
|
||||
// Projection, minus Unwrap: that one is an editor's worth of UI (seams, islands, the UV pane)
|
||||
// for a tool whose whole point is a click and a few sliders.
|
||||
{
|
||||
struct Mapping { const char *file; wxString label; wxString tip; TextureProjectionMethod method; };
|
||||
const Mapping mappings[] = {
|
||||
{ "menu_obj_cube.svg", _L("Flat"),
|
||||
_L("Flat - lays the picture straight onto the surface from whichever axis it faces, and blends "
|
||||
"where it wraps around an edge. The right choice for a face or a flat side."),
|
||||
TextureProjectionMethod::Triplanar },
|
||||
{ "menu_obj_cylinder.svg", _L("Around"),
|
||||
_L("Around - wraps the picture around the area, for a round part like a mug or a tube."),
|
||||
TextureProjectionMethod::Cylindrical },
|
||||
{ "menu_obj_sphere.svg", _L("Ball"),
|
||||
_L("Ball - wraps the picture around the area in both directions, for a dome or a sphere."),
|
||||
TextureProjectionMethod::Spherical },
|
||||
{ "texture_displacement_map_view.svg", _L("From view"),
|
||||
_L("From view - throws the picture onto the area from where you are looking now, like a slide "
|
||||
"projector. Line the view up with the surface first, because faces turned away stretch."),
|
||||
TextureProjectionMethod::ViewProjected },
|
||||
};
|
||||
const float x0 = ImGui::GetCursorPosX();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
// Plain text, like every other label in this window: the displacement panel greys its
|
||||
// headings because it has many, and here there are five rows in total.
|
||||
m_imgui->text(_L_CONTEXT("Mapping", "Texture Displacement"));
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
|
||||
for (int mi = 0; mi < int(IM_ARRAYSIZE(mappings)); ++mi) {
|
||||
if (mi > 0)
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
const bool on = layer->projection_method == mappings[mi].method;
|
||||
if (icon_toggle(900 + mi, mappings[mi].file, on, mappings[mi].label, mappings[mi].tip) && !on) {
|
||||
// Capture the current view the moment "From view" is chosen, so it does something
|
||||
// sensible immediately rather than projecting from a stale direction.
|
||||
if (mappings[mi].method == TextureProjectionMethod::ViewProjected)
|
||||
capture_view_projection(*layer);
|
||||
layer->projection_method = mappings[mi].method;
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (layer->projection_method == TextureProjectionMethod::ViewProjected) {
|
||||
if (m_imgui->button(_u8L("Capture current view"))) {
|
||||
capture_view_projection(*layer);
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
hover_tip(_u8L("Throws the picture on again from where you are looking now. Orbit the model, press "
|
||||
"this, and it is re-laid from the new angle."));
|
||||
}
|
||||
|
||||
m_preview_params_dirty |= float_row("##size", _L("Size"), &layer->tiling_scale, 1.f, 300.f, "%.1f mm", true);
|
||||
hover_tip(_u8L("How wide the picture is on the model, in millimetres."));
|
||||
m_preview_params_dirty |= float_row("##rotation", _L("Rotation"), &layer->rotation_deg, 0.f, 360.f, "%.0f°", false);
|
||||
hover_tip(_u8L("Turns the picture on the surface, for lining it up with an edge of the model."));
|
||||
|
||||
// Sliding it into place, and whether it repeats. Both belong on one row: the handle is the
|
||||
// coarse control and Tile decides what it is sliding - one copy, or a pattern.
|
||||
{
|
||||
const float x0 = ImGui::GetCursorPosX();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
m_imgui->text(_L("Move"));
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
|
||||
if (icon_toggle(910, "texture_displacement_adjust.svg", m_adjust_texture_mode, _L("Move on the model"),
|
||||
_L("Move on the model - drag the handle that appears on the surface to slide the "
|
||||
"picture around, or one of its two arrows to slide it along just that axis."))) {
|
||||
if (!m_adjust_texture_mode) {
|
||||
update_model_object(); // flush any pending placement before anchoring to it
|
||||
if (update_adjust_anchor())
|
||||
m_adjust_texture_mode = true;
|
||||
else
|
||||
show_error(nullptr, _u8L("Click the model to put the picture somewhere first."));
|
||||
} else {
|
||||
m_adjust_texture_mode = false;
|
||||
m_adjust_drag_handle = AdjustHandle::None;
|
||||
}
|
||||
}
|
||||
ImGui::SameLine(0.f, gap_s * 2.f);
|
||||
if (m_imgui->bbl_checkbox(_L("Tile"), layer->tile_enabled))
|
||||
m_preview_params_dirty = true;
|
||||
hover_tip(_u8L("Repeats the picture over the area instead of placing a single copy. Off by "
|
||||
"default, because a photo or a logo goes on once; Size then sets how big that "
|
||||
"one copy is."));
|
||||
}
|
||||
|
||||
// The one number that decides how sharp the printed picture is. Each triangle prints in a
|
||||
// single filament, so the mesh under the paint has to be finer than the detail wanted out of
|
||||
// the image - and the budget above is what stops that running away.
|
||||
{
|
||||
TextureDisplacementOptions &opts = mv->texture_displacement_options;
|
||||
float edge = opts.v2_refine_mm;
|
||||
m_imgui->disabled_begin(busy);
|
||||
if (float_row("##resolution", _L("Detail"), &edge, 0.02f, 2.f, "%.2f mm", true)) {
|
||||
opts.v2_refine_mm = edge;
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
m_imgui->disabled_end();
|
||||
hover_tip(_u8L("How fine the mesh is made under the picture, in millimetres. Each triangle prints "
|
||||
"in one filament, so this is how sharp an edge between two colors can come out. "
|
||||
"Smaller costs triangles fast: halving it needs four times as many."));
|
||||
|
||||
// Refining past the budget is not an error - the bake simplifies back down to it - but the
|
||||
// result then carries less of the picture than this slider asks for, and without saying so
|
||||
// the only sign is a print that came out softer than expected.
|
||||
const size_t budget = size_t(std::max(0, opts.v2_max_triangles_k)) * 1000;
|
||||
const size_t needed = estimated_refined_triangles(*mv, edge);
|
||||
if (budget > 0 && needed > budget * 5 / 4) {
|
||||
const auto count = [](size_t n) {
|
||||
return n >= 1000000 ? Slic3r::format("%1$.1f M", double(n) / 1000000.) :
|
||||
Slic3r::format("%1% k", (n + 500) / 1000);
|
||||
};
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + panel_w);
|
||||
m_imgui->warning_text(Slic3r::format(_u8L("Needs about %1% triangles, budget %2% - it will be "
|
||||
"simplified back down and come out softer."),
|
||||
count(needed), count(budget)));
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
}
|
||||
|
||||
} // Placement
|
||||
|
||||
ImGui::Separator();
|
||||
if (section("color", _L("Color"), m_image_sec_color)) {
|
||||
|
||||
// Colour correction. Every one of these changes which filament each area lands in, which is
|
||||
// why they are here rather than left to whatever the file happened to contain: a photo that
|
||||
// prints as mud usually only needs contrast.
|
||||
m_preview_params_dirty |= float_row("##brightness", _L("Brightness"), &layer->brightness, -1.f, 1.f, "%.2f", false);
|
||||
hover_tip(_u8L("Lightens or darkens the whole picture before it is matched to your filaments."));
|
||||
m_preview_params_dirty |= float_row("##contrast", _L("Contrast"), &layer->contrast, -1.f, 1.f, "%.2f", false);
|
||||
hover_tip(_u8L("Pushes light and dark apart. Raise it when a photo prints as mud; lower it when "
|
||||
"everything collapses into the lightest and darkest filament."));
|
||||
m_preview_params_dirty |= float_row("##saturation", _L("Saturation"), &layer->saturation, 0.f, 2.f, "%.2f", false);
|
||||
hover_tip(_u8L("How strong the colors are. At 0 the picture is gray, so it prints in your grays and "
|
||||
"blacks; raising it pushes each area towards a purer filament."));
|
||||
m_preview_params_dirty |= float_row("##hue", _L("Hue"), &layer->hue, -180.f, 180.f, "%.0f°", false);
|
||||
hover_tip(_u8L("Turns every color of the picture around the color wheel, without changing how light "
|
||||
"it is. Your filaments are a fixed, arbitrary set, so this is what lets the picture "
|
||||
"meet them: if you have orange loaded but no red, a small turn puts the reds on it."));
|
||||
m_preview_params_dirty |= float_row("##gamma", _L("Gamma"), &layer->gamma, 0.2f, 5.f, "%.2f", true);
|
||||
hover_tip(_u8L("Lifts or drops the midtones without touching the brightest and darkest parts. Use it "
|
||||
"when the middle of the picture is the part coming out wrong."));
|
||||
m_preview_params_dirty |= float_row("##sharpen", _L("Sharpen"), &layer->sharpness, 0.f, 1.f, "%.2f", false);
|
||||
hover_tip(_u8L("Crisps up the edges before the picture is matched to your filaments. Worth more here "
|
||||
"than in a photo: a soft edge between two colors quantizes into a ragged band of "
|
||||
"both, and sharpening first makes it land in one place."));
|
||||
|
||||
// How many colours the picture may use. 2 is a logo in two flat colours, 4 or 5 a poster,
|
||||
// off a photograph. This is not the same as the displacement tool's "Mixed colors", which
|
||||
// caps how many mixed filament slots a bake may create rather than what the picture uses.
|
||||
{
|
||||
int count = layer->color_count;
|
||||
if (int_row("##colors", _L("Colors"), &count, 0, 12, count == 0 ? _u8L("Off").c_str() : "%d")) {
|
||||
// 1 is not a picture, so the slider steps over it on the way out of Off.
|
||||
layer->color_count = count == 1 ? 2 : count;
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
hover_tip(_u8L("Reduces the picture to this many colors before your filaments are matched to "
|
||||
"it, so it prints in at most that many. The colors are the ones the picture is "
|
||||
"actually made of, not an even split of the range. Off leaves it alone."));
|
||||
}
|
||||
|
||||
if (m_imgui->bbl_checkbox(_L("Negative"), layer->color_invert))
|
||||
m_preview_params_dirty = true;
|
||||
hover_tip(_u8L("Prints the picture inverted, light for dark and each color for its opposite."));
|
||||
|
||||
} // Color
|
||||
|
||||
ImGui::Separator();
|
||||
if (!section("bg", _L("Background"), m_image_sec_bg)) {
|
||||
// Folded away, but still worth saying whether it is doing anything.
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("%s", layer->bg_remove ? _u8L("removed").c_str() : _u8L("kept").c_str());
|
||||
} else {
|
||||
if (m_imgui->bbl_checkbox(_L("Remove background"), layer->bg_remove))
|
||||
m_preview_params_dirty = true;
|
||||
hover_tip(_u8L("Leaves the background unprinted, so the object's own color shows through instead of "
|
||||
"a slab of white behind a logo. A picture that is already transparent needs none of "
|
||||
"this: those parts are left out either way."));
|
||||
if (layer->bg_remove) {
|
||||
// Which colour is being removed, shown rather than described: the swatch is also a
|
||||
// picker, and the dropper beside it takes the colour from the preview above.
|
||||
const float x0 = ImGui::GetCursorPosX();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
m_imgui->text(_L("Color"));
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
|
||||
float shown_bg[3] = { layer->bg_color[0], layer->bg_color[1], layer->bg_color[2] };
|
||||
if (layer->bg_from_corners) {
|
||||
// Nothing has been picked, so show what the corners actually gave: the same median
|
||||
// the bake will key on, not a placeholder white.
|
||||
const DecodedHeightTexture &tex = decode_height_texture(*layer);
|
||||
if (tex.has_color() && tex.width > 1 && tex.height > 1) {
|
||||
const size_t n = size_t(tex.width) * size_t(tex.height);
|
||||
const size_t corner[4] = { 0, size_t(tex.width) - 1,
|
||||
(size_t(tex.height) - 1) * size_t(tex.width), n - 1 };
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
std::array<float, 4> v{};
|
||||
for (int i = 0; i < 4; ++i)
|
||||
v[size_t(i)] = float(tex.rgb[corner[i] * 3 + size_t(c)]);
|
||||
std::sort(v.begin(), v.end());
|
||||
shown_bg[c] = 0.5f * (v[1] + v[2]) / 255.f;
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX() - icon_sm - gap_s);
|
||||
if (ImGui::ColorEdit3("##bg_color", shown_bg,
|
||||
ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel)) {
|
||||
for (int c = 0; c < 3; ++c)
|
||||
layer->bg_color[c] = shown_bg[c];
|
||||
layer->bg_from_corners = false;
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
hover_tip(layer->bg_from_corners ?
|
||||
_u8L("Taken from the four corners of the picture, which is right for a logo on a "
|
||||
"plain card. Click to choose a color yourself instead.") :
|
||||
_u8L("The color being removed. Click to change it."));
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(930, "texture_displacement_pipette.svg", m_image_bg_picking, _L("Pick the color"),
|
||||
_L("Pick the color - then click the background, either in the preview above or on "
|
||||
"the model itself.")))
|
||||
m_image_bg_picking = !m_image_bg_picking;
|
||||
if (!layer->bg_from_corners) {
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (m_imgui->button(_u8L("Corners"))) {
|
||||
layer->bg_from_corners = true;
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
hover_tip(_u8L("Back to taking the color from the picture's four corners."));
|
||||
}
|
||||
|
||||
m_preview_params_dirty |= float_row("##bg_tol", _L("Tolerance"), &layer->bg_tolerance, 0.01f, 0.6f, "%.2f", false);
|
||||
hover_tip(_u8L("How different from that color still counts as background. Raise it when a "
|
||||
"shadow or a gradient is left behind; lower it when it starts eating the picture."));
|
||||
}
|
||||
|
||||
} // Background
|
||||
|
||||
// Outside both sections, because it clears the colour corrections and the background key
|
||||
// alike, and a reset that lives inside one of them would look like it only reached that far.
|
||||
ImGui::Separator();
|
||||
if (m_imgui->button(_u8L("Reset all"))) {
|
||||
layer->brightness = 0.f;
|
||||
layer->contrast = 0.f;
|
||||
layer->saturation = 1.f;
|
||||
layer->hue = 0.f;
|
||||
layer->gamma = 1.f;
|
||||
layer->sharpness = 0.f;
|
||||
layer->color_count = 0;
|
||||
layer->bg_remove = false;
|
||||
layer->bg_tolerance = 0.15f;
|
||||
layer->bg_from_corners = true;
|
||||
layer->bg_color[0] = layer->bg_color[1] = layer->bg_color[2] = 1.f;
|
||||
m_image_bg_picking = false;
|
||||
layer->color_invert = false;
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
hover_tip(_u8L("Puts every correction and the background back to where they started, leaving the "
|
||||
"picture as it was loaded. Where you placed it and how big it is are not touched."));
|
||||
}
|
||||
|
||||
// The library popup lives in this window's scope, where its open request and the popup itself
|
||||
// share one ID stack.
|
||||
{
|
||||
const ImVec2 win_min = ImGui::GetWindowPos();
|
||||
const ImVec2 win_sz = ImGui::GetWindowSize();
|
||||
render_texture_library_popup(win_min, ImVec2(win_min.x + win_sz.x, win_min.y + win_sz.y));
|
||||
}
|
||||
|
||||
// ImGui sliders report "changed" on every frame of a drag, and a preview rebuild is real work,
|
||||
// so with the mouse still down it waits for the release.
|
||||
if (m_preview_params_dirty && !ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||
rebuild_preview();
|
||||
m_preview_params_dirty = false;
|
||||
}
|
||||
|
||||
m_panel_body_h = ImGui::GetCursorPosY() - style.ItemSpacing.y;
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleColor(5);
|
||||
ImGui::PopStyleVar(2);
|
||||
|
||||
const float footer_top = ImGui::GetCursorScreenPos().y;
|
||||
ImGui::Separator();
|
||||
{
|
||||
const bool painted = placed;
|
||||
const bool no_colors = mv != nullptr && !any_layer_colors(*mv);
|
||||
// Colour is the only thing this bake produces, so with a grayscale picture there would be
|
||||
// nothing to show for it - and it is not a cheap no-op: the bake still refines the painted
|
||||
// area and replaces the mesh with the result.
|
||||
const bool can_apply = !busy && mv != nullptr && painted && !no_colors;
|
||||
|
||||
const float button_h = std::round(frame_h * 1.25f);
|
||||
const float button_w = std::max({ ImGui::CalcTextSize(_u8L("Apply").c_str()).x,
|
||||
ImGui::CalcTextSize(_u8L("Close").c_str()).x,
|
||||
ImGui::CalcTextSize(_u8L("Stop").c_str()).x,
|
||||
ImGui::CalcTextSize(_u8L("Working...").c_str()).x }) +
|
||||
m_imgui->scaled(0.5f);
|
||||
const float row_y = ImGui::GetCursorPosY();
|
||||
const float icon_h = 21.f * scale;
|
||||
const float row_h = std::max(icon_h, button_h);
|
||||
|
||||
ImGui::SetCursorPosY(row_y + (row_h - icon_h) * .5f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(int(m_imgui->scaled(.5f)), style.ItemSpacing.y));
|
||||
GLGizmoUtils::render_wiki_guide_button(m_parent, scale,
|
||||
"https://www.orcaslicer.com/wiki/print_prepare/prepare_texture_displacement");
|
||||
ImGui::PopStyleVar(1);
|
||||
|
||||
ImGui::SameLine(panel_w - button_w * 2 - style.ItemSpacing.x);
|
||||
GLGizmoUtils::push_orca_button_style();
|
||||
m_imgui->push_bold_font();
|
||||
m_imgui->disabled_begin(!can_apply);
|
||||
ImGui::SetCursorPosY(row_y + (row_h - button_h) * .5f);
|
||||
const std::string apply_label = busy ? _u8L("Working...") : _u8L("Apply");
|
||||
if (ImGui::Button((apply_label + "##apply").c_str(), ImVec2(button_w, button_h)))
|
||||
bake_standard();
|
||||
m_imgui->disabled_end();
|
||||
m_imgui->pop_bold_font();
|
||||
GLGizmoUtils::pop_orca_button_style();
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
m_imgui->tooltip(!has_picture ? _u8L("Choose a picture first.") :
|
||||
!painted ? _u8L("Click the model to place the picture first.") :
|
||||
no_colors ? _u8L("This picture is grayscale, so there is nothing to print. "
|
||||
"Choose a color picture.") :
|
||||
_u8L("Prints the picture where you placed it, in the nearest of your "
|
||||
"loaded filaments. The surface itself is left exactly where it "
|
||||
"is: this adds color, not relief."),
|
||||
wrap_w);
|
||||
|
||||
ImGui::SameLine();
|
||||
if (busy) {
|
||||
if (ImGui::Button((_u8L("Stop") + "##stop").c_str(), ImVec2(button_w, button_h)))
|
||||
wxGetApp().plater()->get_ui_job_worker().cancel_all();
|
||||
hover_tip(_u8L("Stops it. Whatever it had already finished stays on the model, and can be undone."));
|
||||
} else {
|
||||
if (ImGui::Button((_u8L("Close") + "##close").c_str(), ImVec2(button_w, button_h)))
|
||||
m_parent.reset_all_gizmos();
|
||||
hover_tip(_u8L("Closes the tool. Your picture and where you put it stay with the model."));
|
||||
}
|
||||
}
|
||||
|
||||
m_panel_footer_h = ImGui::GetCursorScreenPos().y - footer_top;
|
||||
// Both heights feed back into next frame's layout, so the window needs one more frame whenever
|
||||
// either of them moved.
|
||||
if (std::abs(m_panel_body_h - previous_body_h) > 0.5f || std::abs(m_panel_footer_h - previous_footer_h) > 0.5f)
|
||||
m_parent.request_extra_frame();
|
||||
|
||||
// Last, on the foreground list, so it covers the panel as well as the view.
|
||||
render_image_drop_hint();
|
||||
|
||||
GizmoImguiEnd();
|
||||
ImGuiWrapper::pop_toolbar_style();
|
||||
}
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
@@ -1,46 +0,0 @@
|
||||
#ifndef slic3r_GLGizmoImage_hpp_
|
||||
#define slic3r_GLGizmoImage_hpp_
|
||||
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <wx/string.h>
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
class GLCanvas3D;
|
||||
|
||||
// "Put this picture on the model": the texture-displacement gizmo with the relief taken out of it.
|
||||
//
|
||||
// There is no second engine here and no copy of one. It is GLGizmoTextureDisplacement with
|
||||
// image_mode() on, which pins every layer to zero depth and colour on (see
|
||||
// apply_image_mode_presets()) and leaves out the controls that only mean something once the surface
|
||||
// moves. Painting, the layer stack, the texture library, the previews and the bake are the very same
|
||||
// code, reached the very same way - a zero-depth bake refines the painted area and writes the
|
||||
// colours exactly as a relief bake does, it just displaces by nothing.
|
||||
//
|
||||
// Its own toolbar button because the two are different jobs, not two settings of one: a user who
|
||||
// wants a photo on a part should not have to know what a height map is, or find the one slider that
|
||||
// has to be zero.
|
||||
class GLGizmoImage : public GLGizmoTextureDisplacement
|
||||
{
|
||||
public:
|
||||
GLGizmoImage(GLCanvas3D &parent, const std::string &icon_filename, unsigned int sprite_id);
|
||||
|
||||
protected:
|
||||
bool image_mode() const override { return true; }
|
||||
|
||||
bool on_init() override;
|
||||
std::string on_get_name() const override;
|
||||
|
||||
wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override;
|
||||
|
||||
std::string get_gizmo_entering_text() const override { return _u8L("Entering Image painting"); }
|
||||
std::string get_gizmo_leaving_text() const override { return _u8L("Leaving Image painting"); }
|
||||
std::string get_action_snapshot_name() const override { return _u8L("Image editing"); }
|
||||
};
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_GLGizmoImage_hpp_
|
||||
@@ -409,18 +409,10 @@ std::unique_ptr<GLTexture> upload_color_texture(const DecodedHeightTexture &deco
|
||||
for (int c = 0; c < 3; ++c)
|
||||
sum[c] += decoded.rgb[si + size_t(c)];
|
||||
}
|
||||
// Coverage rides in the fourth channel, box-filtered with the colour, so the shader can
|
||||
// leave the surface alone where the picture is not there (see the colour path in
|
||||
// texture_displacement_shaded.fs).
|
||||
unsigned int sum_a = 0;
|
||||
if (decoded.has_alpha())
|
||||
for (int sy = y0; sy < y1 && sy < decoded.height; ++sy)
|
||||
for (int sx = x0; sx < x1 && sx < decoded.width; ++sx)
|
||||
sum_a += decoded.alpha[size_t(sy) * size_t(decoded.width) + size_t(sx)];
|
||||
const size_t di = (size_t(y) * size_t(w) + size_t(x)) * 4;
|
||||
for (int c = 0; c < 3; ++c)
|
||||
rgba[di + size_t(c)] = (n > 0) ? static_cast<unsigned char>(sum[size_t(c)] / n) : 0;
|
||||
rgba[di + 3] = (decoded.has_alpha() && n > 0) ? static_cast<unsigned char>(sum_a / n) : 255;
|
||||
rgba[di + 3] = 255;
|
||||
}
|
||||
|
||||
auto texture = std::make_unique<GLTexture>();
|
||||
@@ -539,7 +531,6 @@ void GLGizmoTextureDisplacement::on_shutdown()
|
||||
m_palette_pure_quantizer = nullptr;
|
||||
m_palette_filaments.clear();
|
||||
m_palette_images.clear();
|
||||
m_palette_adjusts.clear();
|
||||
m_mix_ranking.reset();
|
||||
m_palette_slots.clear();
|
||||
m_palette_changed = false;
|
||||
@@ -565,8 +556,6 @@ void GLGizmoTextureDisplacement::on_shutdown()
|
||||
m_seam_edit_mode = false;
|
||||
m_adjust_drag_handle = AdjustHandle::None;
|
||||
m_adjust_anchor_valid = false;
|
||||
m_image_handle_armed = false;
|
||||
m_file_drag_over = false;
|
||||
// The pane is request-only: closing the gizmo drops the request, so reopening it later doesn't
|
||||
// silently reopen the pane too.
|
||||
m_show_uv_editor = false;
|
||||
@@ -771,21 +760,8 @@ bool GLGizmoTextureDisplacement::on_mouse(const wxMouseEvent &mouse_event)
|
||||
{
|
||||
if (m_seam_edit_mode)
|
||||
return on_mouse_seam(mouse_event);
|
||||
// The background dropper takes the next click wherever it lands, the model included, so it comes
|
||||
// before both the handle and the paint.
|
||||
if (m_image_bg_picking && mouse_event.LeftDown() &&
|
||||
pick_background_at(Vec2d(mouse_event.GetX(), mouse_event.GetY())))
|
||||
return true;
|
||||
if (m_adjust_texture_mode) {
|
||||
if (on_mouse_adjust_texture(mouse_event))
|
||||
return true;
|
||||
// The displacement tool turns this mode on deliberately, so there a click off the handle
|
||||
// doing nothing is the point. The Image tool has it on from the start, so the same click has
|
||||
// to fall through and place the picture, or turning it on would disable the tool's one
|
||||
// gesture.
|
||||
if (!image_mode())
|
||||
return false;
|
||||
}
|
||||
if (m_adjust_texture_mode)
|
||||
return on_mouse_adjust_texture(mouse_event);
|
||||
|
||||
const bool handled = GLGizmoPainterBase::on_mouse(mouse_event);
|
||||
// A consumed drag/click is a paint (or erase) event: the base class has already updated the live
|
||||
@@ -1587,9 +1563,6 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh()
|
||||
shader->set_uniform("height_tex", 0);
|
||||
shader->set_uniform("height_tex_texel", Vec2f(1.f / float(tex->get_width()), 1.f / float(tex->get_height())));
|
||||
shader->set_uniform("depth_mm", layer->depth_mm);
|
||||
// Without this the preview wrapped the image and showed a repeating pattern where the bake
|
||||
// places a single copy.
|
||||
shader->set_uniform("tile_enabled", layer->tile_enabled);
|
||||
shader->set_uniform("tiling_scale", layer->tiling_scale);
|
||||
// Read off the uploaded texture rather than the decoded one: they are the same image, and this is
|
||||
// the aspect the sampler will actually see.
|
||||
@@ -3613,11 +3586,6 @@ void GLGizmoTextureDisplacement::update_model_object()
|
||||
wxGetApp().obj_list()->update_info_items(std::find(mos.begin(), mos.end(), mo) - mos.begin());
|
||||
m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS));
|
||||
rebuild_preview();
|
||||
// The on-canvas handle is anchored to the paint patch, so a stroke that moved the paint has
|
||||
// to move it with them. Matters most in the Image tool, where the handle is on from the
|
||||
// start and a click moves the picture to a different part of the model.
|
||||
if (m_adjust_texture_mode)
|
||||
update_adjust_anchor();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3700,7 +3668,6 @@ void GLGizmoTextureDisplacement::ensure_panel_icons()
|
||||
// a monochrome ("normal", grey in the current theme) and an original-colour variant.
|
||||
static const std::vector<std::string> names = {
|
||||
"texture_displacement_brush.svg", "texture_displacement_face.svg", "texture_displacement_connected_area.svg",
|
||||
"texture_displacement_pipette.svg", // the Image panel's background dropper
|
||||
"texture_displacement_real_preview.svg", "texture_displacement_fast_preview.svg",
|
||||
"texture_displacement_checker.svg", "texture_displacement_distortion.svg",
|
||||
"texture_displacement_wireframe.svg", "texture_displacement_cross.svg",
|
||||
@@ -3759,24 +3726,13 @@ void GLGizmoTextureDisplacement::add_texture_layer()
|
||||
// Start the layer off on the first library texture rather than on nothing at all: a textureless
|
||||
// layer looks broken (painting on it appears to do nothing, because there is no height map to
|
||||
// displace by). The user swaps it for another from the layer's own picker.
|
||||
//
|
||||
// Not in the Image tool: there the layer deliberately starts with no picture at all, so the panel
|
||||
// stays on its empty frame until the user has chosen one. Picking a shipped height map for them
|
||||
// would be choosing the wrong thing twice over - it is not their picture, and most of that
|
||||
// library is grayscale, which an image bake cannot print.
|
||||
if (image_mode()) {
|
||||
// One copy, not a repeating pattern: a photo or a logo is placed once. The panel's Tile
|
||||
// checkbox is there for the cases that do want it, so this is a default and not a pin.
|
||||
layer.tile_enabled = false;
|
||||
} else {
|
||||
const std::vector<TextureLibraryEntry> &library = texture_library();
|
||||
if (!library.empty())
|
||||
if (const LibraryTexture *tex = get_library_texture(library.front().path)) {
|
||||
layer.name = library.front().name;
|
||||
layer.path = library.front().path;
|
||||
layer.image_data = tex->image_data;
|
||||
}
|
||||
}
|
||||
const std::vector<TextureLibraryEntry> &library = texture_library();
|
||||
if (!library.empty())
|
||||
if (const LibraryTexture *tex = get_library_texture(library.front().path)) {
|
||||
layer.name = library.front().name;
|
||||
layer.path = library.front().path;
|
||||
layer.image_data = tex->image_data;
|
||||
}
|
||||
mv->texture_displacement_layers.push_back(std::move(layer));
|
||||
// A new layer opens with every setting showing; there is nothing on it yet to hide settings from.
|
||||
m_layer_expanded[size_t(free_slot)] = true;
|
||||
@@ -3971,19 +3927,15 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
||||
return n;
|
||||
};
|
||||
|
||||
// The shipped library is grayscale height maps, which is what the displacement tool wants and the
|
||||
// opposite of what an image bake can use: it prints colour, and a height map has none. So the
|
||||
// Image tool offers only the user's own pictures, and the section is left out entirely rather
|
||||
// than shown full of things that would all warn when picked.
|
||||
if (!image_mode()) {
|
||||
ImGui::TextDisabled("%s", _u8L("Built-in").c_str());
|
||||
ImGui::TextDisabled("%s", _u8L("Built-in").c_str());
|
||||
{
|
||||
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
||||
const int count = grid(origin, false);
|
||||
end_grid(origin, count);
|
||||
if (count == 0)
|
||||
ImGui::TextDisabled("%s", _u8L("No textures found.").c_str());
|
||||
}
|
||||
ImGui::TextDisabled("%s", image_mode() ? _u8L("My images").c_str() : _u8L("My textures").c_str());
|
||||
ImGui::TextDisabled("%s", _u8L("My textures").c_str());
|
||||
{
|
||||
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
||||
const int count = grid(origin, true);
|
||||
@@ -3991,20 +3943,15 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
||||
if (ImGui::Button("+##import_texture", ImVec2(tile, tile)))
|
||||
import_clicked = true;
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(image_mode() ?
|
||||
_u8L("Import a picture from your computer. Its colors are kept, and it is saved to "
|
||||
"your personal folder so it is here next time.") :
|
||||
_u8L("Import your own image as a height map. It is converted to the format the "
|
||||
"slicer bakes from and saved to your personal texture folder, kept separate "
|
||||
"from the textures shipped with OrcaSlicer."),
|
||||
m_imgui->tooltip(_u8L("Import your own image as a height map. It is converted to the format the slicer "
|
||||
"bakes from and saved to your personal texture folder, kept separate from the "
|
||||
"textures shipped with OrcaSlicer."),
|
||||
m_imgui->scaled(20.f));
|
||||
end_grid(origin, count + 1);
|
||||
}
|
||||
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + grid_w);
|
||||
ImGui::TextDisabled("%s", image_mode() ?
|
||||
_u8L("PNG, JPG or BMP. Hover one to remove it.").c_str() :
|
||||
_u8L("PNG, JPG or BMP - converted to a height map on import. Hover one of yours to remove it.").c_str());
|
||||
ImGui::TextDisabled("%s", _u8L("PNG, JPG or BMP - converted to a height map on import. Hover one of yours to remove it.").c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
|
||||
if (chosen || import_clicked)
|
||||
@@ -4584,22 +4531,15 @@ const std::vector<GLGizmoTextureDisplacement::PaletteEntry> &GLGizmoTextureDispl
|
||||
// The images themselves rather than their addresses, so a freed and reallocated image can never
|
||||
// pass for the old one. Whether one has colour, and whether its colours are flat, follows from it.
|
||||
std::vector<std::shared_ptr<std::vector<unsigned char>>> images;
|
||||
// ... and what each one is corrected by, which changes the colours the mixes are ranked against
|
||||
// without changing the image it came from.
|
||||
std::vector<ColorAdjustKey> adjusts;
|
||||
if (mv != nullptr)
|
||||
for (const TextureDisplacementLayer &layer : mv->texture_displacement_layers)
|
||||
if (layer.color_enabled && !layer.empty()) {
|
||||
if (layer.color_enabled && !layer.empty())
|
||||
images.push_back(layer.image_data);
|
||||
adjusts.push_back(ColorAdjustKey::of(layer));
|
||||
}
|
||||
|
||||
const bool ranking_stale = filaments != m_palette_filaments || images != m_palette_images ||
|
||||
adjusts != m_palette_adjusts;
|
||||
const bool ranking_stale = filaments != m_palette_filaments || images != m_palette_images;
|
||||
if (ranking_stale) {
|
||||
m_palette_filaments = std::move(filaments);
|
||||
m_palette_images = std::move(images);
|
||||
m_palette_adjusts = std::move(adjusts);
|
||||
m_mix_ranking.reset();
|
||||
}
|
||||
if (mixing && !m_mix_ranking)
|
||||
@@ -4676,12 +4616,9 @@ std::vector<GLGizmoTextureDisplacement::MixTarget> GLGizmoTextureDisplacement::m
|
||||
continue;
|
||||
TextureDisplacementLayer raw = layer;
|
||||
raw.smoothing = 0.f;
|
||||
DecodedHeightTexture tex = decode_height_texture(raw);
|
||||
const DecodedHeightTexture tex = decode_height_texture(raw);
|
||||
if (!tex.has_color())
|
||||
continue;
|
||||
// Corrected first: the mixes are chosen to cover the colours the bake will actually ask for,
|
||||
// and a darkened or desaturated image asks for different ones.
|
||||
apply_color_adjustments(tex, layer);
|
||||
|
||||
// 16 levels per channel, each bin keeping the mean of the colours that fell in it: a coarse grid
|
||||
// to gather on, without snapping every colour to a bin corner.
|
||||
@@ -5387,16 +5324,10 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_color_texture(const TextureDisp
|
||||
{
|
||||
if (layer.empty() || !layer.color_enabled)
|
||||
return nullptr;
|
||||
const ColorAdjustKey adjust = ColorAdjustKey::of(layer);
|
||||
if (m_color_tex && m_color_tex_source == layer.image_data.get() && m_color_tex_smoothing == layer.smoothing &&
|
||||
m_color_tex_adjust == adjust)
|
||||
if (m_color_tex && m_color_tex_source == layer.image_data.get() && m_color_tex_smoothing == layer.smoothing)
|
||||
return m_color_tex.get();
|
||||
|
||||
// Corrected here rather than inside the decode, exactly as prepare_sampleable_layers() does for
|
||||
// the bake, so the fast preview shades the image the bake will print.
|
||||
DecodedHeightTexture tex = decode_height_texture(layer);
|
||||
apply_color_adjustments(tex, layer);
|
||||
std::unique_ptr<GLTexture> texture = upload_color_texture(tex, HEIGHT_TEX_MAX_PX);
|
||||
std::unique_ptr<GLTexture> texture = upload_color_texture(decode_height_texture(layer), HEIGHT_TEX_MAX_PX);
|
||||
if (!texture) {
|
||||
m_color_tex.reset();
|
||||
m_color_tex_source = nullptr;
|
||||
@@ -5405,7 +5336,6 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_color_texture(const TextureDisp
|
||||
m_color_tex = std::move(texture);
|
||||
m_color_tex_source = layer.image_data.get();
|
||||
m_color_tex_smoothing = layer.smoothing;
|
||||
m_color_tex_adjust = adjust;
|
||||
return m_color_tex.get();
|
||||
}
|
||||
|
||||
@@ -5482,8 +5412,6 @@ static constexpr float STD_SUBDIV_BORDER_MM = 0.4f;
|
||||
// what the eye actually lands on - a stepped outline around a printed decal reads as a defect in a
|
||||
// way a slightly coarse relief transition does not - and it costs triangles along an outline only.
|
||||
static constexpr float STD_SUBDIV_COLOR_MM = 0.25f;
|
||||
// The Image tool's triangle budget, in thousands. See apply_image_mode_presets().
|
||||
static constexpr int IMAGE_BUDGET_K = 1000;
|
||||
|
||||
bool GLGizmoTextureDisplacement::apply_standard_mode_presets(ModelVolume *mv)
|
||||
{
|
||||
@@ -5514,61 +5442,6 @@ bool GLGizmoTextureDisplacement::apply_standard_mode_presets(ModelVolume *mv)
|
||||
return changed;
|
||||
}
|
||||
|
||||
// Pinned into the volume rather than applied on the way to the bake, exactly as Standard mode pins
|
||||
// displace_border and smooth_enabled above: the preview, the bake, the triangle estimate and the
|
||||
// palette all read the layers straight off the volume, and a value that only some of them saw would
|
||||
// put the preview and the bake out of step.
|
||||
//
|
||||
// The two tools share one layer stack - that is what "not a fork" means here - so opening the Image
|
||||
// tool on a volume whose layers carry relief flattens them, and they stay flat when the displacement
|
||||
// tool is reopened. Only that one number changes: the paint, the image, the placement and any relief
|
||||
// already baked into the mesh are untouched.
|
||||
bool GLGizmoTextureDisplacement::apply_image_mode_presets(ModelVolume *mv)
|
||||
{
|
||||
if (mv == nullptr)
|
||||
return false;
|
||||
bool changed = false;
|
||||
TextureDisplacementOptions &opts = mv->texture_displacement_options;
|
||||
// A million triangles for the picture. The auto budget is 750 k, chosen for relief, where the
|
||||
// triangles buy shape; here they buy nothing but the sharpness of the edges between colours, and
|
||||
// that is the whole output, so it is worth spending more of them on it.
|
||||
if (opts.v2_max_triangles_k != IMAGE_BUDGET_K) {
|
||||
opts.v2_max_triangles_k = IMAGE_BUDGET_K;
|
||||
changed = true;
|
||||
}
|
||||
// Resolution is a slider in this panel rather than an Auto checkbox, so it needs a real value to
|
||||
// start from: the same recommendation Auto would have produced for this model.
|
||||
if (opts.v2_refine_mm <= 0.f) {
|
||||
const float edge = v2_recommendation(*mv).edge_mm;
|
||||
opts.v2_refine_mm = edge > 0.f ? edge : 0.3f;
|
||||
changed = true;
|
||||
}
|
||||
for (TextureDisplacementLayer &layer : mv->texture_displacement_layers) {
|
||||
// Zero depth is the whole of "no relief": the bake still refines the painted area and still
|
||||
// writes the colours, every layer just contributes a displacement of 0 (see
|
||||
// make_combined_displacement_sampler()). Nothing special-cases it.
|
||||
if (layer.depth_mm != 0.f) {
|
||||
layer.depth_mm = 0.f;
|
||||
changed = true;
|
||||
}
|
||||
// Colour is the only thing an image bake produces, so it is on wherever there is colour to
|
||||
// use rather than being a checkbox the user could leave off and wonder at.
|
||||
const bool wanted = height_texture_has_color(layer);
|
||||
if (layer.color_enabled != wanted) {
|
||||
layer.color_enabled = wanted;
|
||||
changed = true;
|
||||
}
|
||||
// Unwrap is the one projection the Image tool does not offer: it is an editor's worth of UI
|
||||
// (seams, islands, the UV pane) for a case that wants a click and a slider. A layer arriving
|
||||
// from the displacement tool set to it falls back to the triplanar default.
|
||||
if (layer.projection_method == TextureProjectionMethod::LSCM) {
|
||||
layer.projection_method = TextureProjectionMethod::Triplanar;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::bake_standard()
|
||||
{
|
||||
ModelVolume *mv = texture_volume();
|
||||
@@ -5877,14 +5750,6 @@ void GLGizmoTextureDisplacement::render_paint_cursor_hint()
|
||||
|
||||
void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float bottom_limit)
|
||||
{
|
||||
// The Image tool gets its own, much smaller window (see GLGizmoImage.cpp): everything below this
|
||||
// point is the displacement panel, and it is left exactly as it was rather than being threaded
|
||||
// with tests for which of the two tools is drawing.
|
||||
if (image_mode()) {
|
||||
render_image_input_window(x, y, bottom_limit);
|
||||
return;
|
||||
}
|
||||
|
||||
ModelObject *mo = m_c->selection_info()->model_object();
|
||||
if (!mo)
|
||||
return;
|
||||
|
||||
@@ -154,33 +154,6 @@ public:
|
||||
// What the cache was built from.
|
||||
std::vector<ColorRGBA> m_palette_filaments;
|
||||
std::vector<std::shared_ptr<std::vector<unsigned char>>> m_palette_images; // the colouring layers' images
|
||||
// One layer's colour correction, as a value that can be compared and cached on. It belongs to
|
||||
// every cache keyed on "what the image looks like", because a correction slider changes the
|
||||
// pixels while leaving the image_data pointer alone.
|
||||
struct ColorAdjustKey
|
||||
{
|
||||
float brightness = 0.f, contrast = 0.f, saturation = 1.f, gamma = 1.f, hue = 0.f;
|
||||
float sharpness = 0.f, bg_tolerance = 0.15f;
|
||||
float bg_r = 1.f, bg_g = 1.f, bg_b = 1.f;
|
||||
int color_count = 0;
|
||||
bool invert = false, bg_remove = false, bg_from_corners = true;
|
||||
bool operator==(const ColorAdjustKey &o) const
|
||||
{
|
||||
return brightness == o.brightness && contrast == o.contrast && saturation == o.saturation &&
|
||||
gamma == o.gamma && hue == o.hue && sharpness == o.sharpness &&
|
||||
bg_tolerance == o.bg_tolerance && bg_r == o.bg_r && bg_g == o.bg_g && bg_b == o.bg_b &&
|
||||
color_count == o.color_count && invert == o.invert && bg_remove == o.bg_remove &&
|
||||
bg_from_corners == o.bg_from_corners;
|
||||
}
|
||||
bool operator!=(const ColorAdjustKey &o) const { return !(*this == o); }
|
||||
static ColorAdjustKey of(const TextureDisplacementLayer &l)
|
||||
{
|
||||
return { l.brightness, l.contrast, l.saturation, l.gamma, l.hue,
|
||||
l.sharpness, l.bg_tolerance, l.bg_color[0], l.bg_color[1], l.bg_color[2],
|
||||
l.color_count, l.color_invert, l.bg_remove, l.bg_from_corners };
|
||||
}
|
||||
};
|
||||
std::vector<ColorAdjustKey> m_palette_adjusts; // alongside m_palette_images, one per entry
|
||||
std::optional<std::vector<PaletteEntry>> m_mix_ranking; // rank_mixes(), computed on demand
|
||||
bool m_palette_mixing = false;
|
||||
int m_palette_mix_count = 0;
|
||||
@@ -230,25 +203,7 @@ public:
|
||||
// rotation handles instead of painting); otherwise forwards to the normal painting handling.
|
||||
bool on_mouse(const wxMouseEvent &mouse_event) override;
|
||||
|
||||
// Loads `path` as the Image tool's picture, importing it into the user's library on the way so
|
||||
// it is there next time. For a drop onto the 3D view; see PlaterDropTarget::OnDropFiles(). False
|
||||
// when this is not the Image tool, or the file could not be read.
|
||||
bool drop_image_file(const std::string &path);
|
||||
|
||||
// Called while a file is being dragged over the 3D view, so the Image tool can show that
|
||||
// dropping it there will do something. Without it the drag has no feedback at all and the only
|
||||
// way to find out is to let go.
|
||||
void set_file_drag_over(bool over);
|
||||
|
||||
protected:
|
||||
// True in the Image tool (see GLGizmoImage): this same gizmo restricted to the one case where
|
||||
// the surface does not move at all - a colour picture laid on the model. Everything behind the
|
||||
// panel is shared and unchanged: the same layers, the same paint masks, the same bake. This flag
|
||||
// only tells the panel which of the two tools it is drawing, and makes it pin the values the
|
||||
// restricted case fixes (see apply_image_mode_presets()).
|
||||
virtual bool image_mode() const { return false; }
|
||||
|
||||
bool on_init() override;
|
||||
void on_render_input_window(float x, float y, float bottom_limit) override;
|
||||
std::string on_get_name() const override;
|
||||
// Never in the assemble view: its toolbar does not offer this gizmo, and every preview here is drawn
|
||||
@@ -266,11 +221,8 @@ protected:
|
||||
EnforcerBlockerType get_left_button_state_type() const override { return m_erase_mode ? EnforcerBlockerType::NONE : EnforcerBlockerType::ENFORCER; }
|
||||
EnforcerBlockerType get_right_button_state_type() const override { return m_erase_mode ? EnforcerBlockerType::ENFORCER : EnforcerBlockerType::NONE; }
|
||||
|
||||
// Paint / Erase, from the panel. See get_left_button_state_type(). Protected because a subclass
|
||||
// overriding handle_snapshot_action_name() has to read the same flag to name the undo step.
|
||||
bool m_erase_mode = false;
|
||||
|
||||
private:
|
||||
bool on_init() override;
|
||||
void update_model_object() override;
|
||||
void update_from_model_object(bool first_update) override;
|
||||
void on_opening() override {}
|
||||
@@ -316,36 +268,6 @@ private:
|
||||
// the paint if it were not carried across (see prepare_mesh()).
|
||||
void bake_standard();
|
||||
|
||||
// Image mode's fixed values, on top of Standard's and pinned the same way (every frame, so the
|
||||
// preview cannot disagree with the bake): zero depth on every layer, and colour on wherever the
|
||||
// picture has any. Those two are what turn the displacement bake into "put this picture on the
|
||||
// model" without a second code path. Returns true if it actually changed something.
|
||||
bool apply_image_mode_presets(ModelVolume *mv);
|
||||
// The Image tool's whole window: load a picture, click the model to place it, correct it, Apply.
|
||||
// Defined in GLGizmoImage.cpp, beside the class it exists for, rather than here - it is a member
|
||||
// only so that it can reach the same private machinery the displacement panel uses, and keeping
|
||||
// it out of this file leaves that panel's 1700 lines alone.
|
||||
void render_image_input_window(float x, float y, float bottom_limit);
|
||||
// Set once the Image tool has switched its placement handle on for the current picture, so that
|
||||
// turning the handle back off stays off instead of being re-armed on the next frame.
|
||||
bool m_image_handle_armed = false;
|
||||
// Set between a drag entering the 3D view and leaving or dropping on it; drives the drop zone
|
||||
// render_image_input_window() draws over the canvas.
|
||||
bool m_file_drag_over = false;
|
||||
void render_image_drop_hint();
|
||||
// Set while the panel's background dropper is armed: the next click on the picture preview takes
|
||||
// the colour under it instead of opening the library.
|
||||
bool m_image_bg_picking = false;
|
||||
// Takes the background colour from the point of the model under `mouse_pos`: the picture's own
|
||||
// colour there, read through the same projection the bake uses, not the lit pixel on screen.
|
||||
// False when the ray misses or the layer does not colour that point, leaving the dropper armed.
|
||||
bool pick_background_at(const Vec2d &mouse_pos);
|
||||
// Which of the Image panel's sections are unfolded. Panel state for the session, deliberately
|
||||
// not saved with the project.
|
||||
bool m_image_sec_place = true;
|
||||
bool m_image_sec_color = true;
|
||||
bool m_image_sec_bg = true;
|
||||
|
||||
// Queues one prepare_mesh() run on the job worker and commits its result when it lands. Every
|
||||
// mesh-preparation button goes through here - Pro's Remesh, Pro's adaptive Subdivide and Standard's
|
||||
// Bake differ only in which stages `params` enables and in what happens afterwards:
|
||||
@@ -477,6 +399,8 @@ private:
|
||||
void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry);
|
||||
void import_custom_texture(TextureDisplacementLayer &layer);
|
||||
|
||||
// Paint / Erase, from the panel. See get_left_button_state_type().
|
||||
bool m_erase_mode = false;
|
||||
// Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation.
|
||||
// Panel state only, not saved with the project.
|
||||
std::array<bool, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_layer_expanded{};
|
||||
@@ -1010,7 +934,6 @@ private:
|
||||
std::unique_ptr<GLTexture> m_color_tex;
|
||||
const void *m_color_tex_source = nullptr;
|
||||
float m_color_tex_smoothing = -1.f;
|
||||
ColorAdjustKey m_color_tex_adjust;
|
||||
|
||||
// Library textures the picker has shown at least once, keyed by file path (see LibraryTexture).
|
||||
std::map<std::string, LibraryTexture> m_library_textures;
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
//#include "slic3r/GUI/Gizmos/GLGizmoHollow.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoSeam.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoImage.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoSimplify.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoEmboss.hpp"
|
||||
@@ -202,11 +201,6 @@ void GLGizmosManager::switch_gizmos_icon_filename()
|
||||
// light/dark switch.
|
||||
gizmo->set_icon_filename("toolbar_texture_displacement.svg");
|
||||
break;
|
||||
case(EType::Image):
|
||||
// Drawn flat in Orca's menu-icon palette, which reads on either background, so one file
|
||||
// serves both themes here too.
|
||||
gizmo->set_icon_filename("toolbar_image.svg");
|
||||
break;
|
||||
case(EType::MeshBoolean):
|
||||
gizmo->set_icon_filename(m_is_dark ? "toolbar_meshboolean_dark.svg" : "toolbar_meshboolean.svg");
|
||||
break;
|
||||
@@ -258,8 +252,6 @@ bool GLGizmosManager::init()
|
||||
m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, m_is_dark ? "mmu_segmentation_dark.svg" : "mmu_segmentation.svg", EType::MmSegmentation));
|
||||
// One shared icon (no dedicated dark variant yet); it recolours acceptably in both themes.
|
||||
m_gizmos.emplace_back(new GLGizmoTextureDisplacement(m_parent, "toolbar_texture_displacement.svg", EType::TextureDisplacement));
|
||||
// The same gizmo restricted to the zero-displacement case; see GLGizmoImage.
|
||||
m_gizmos.emplace_back(new GLGizmoImage(m_parent, "toolbar_image.svg", EType::Image));
|
||||
m_gizmos.emplace_back(new GLGizmoEmboss(m_parent, m_is_dark ? "toolbar_text_dark.svg" : "toolbar_text.svg", EType::Emboss));
|
||||
m_gizmos.emplace_back(new GLGizmoSVG(m_parent));
|
||||
m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, m_is_dark ? "toolbar_measure_dark.svg" : "toolbar_measure.svg", EType::Measure));
|
||||
@@ -593,8 +585,6 @@ bool GLGizmosManager::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_p
|
||||
return dynamic_cast<GLGizmoFuzzySkin*>(m_gizmos[FuzzySkin].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
|
||||
else if (m_current == TextureDisplacement)
|
||||
return dynamic_cast<GLGizmoTextureDisplacement*>(m_gizmos[TextureDisplacement].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
|
||||
else if (m_current == Image)
|
||||
return dynamic_cast<GLGizmoImage*>(m_gizmos[Image].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
|
||||
else if (m_current == MeshBoolean)
|
||||
return dynamic_cast<GLGizmoMeshBoolean*>(m_gizmos[MeshBoolean].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
|
||||
else if (m_current == BrimEars)
|
||||
@@ -609,7 +599,6 @@ bool GLGizmosManager::is_paint_gizmo()
|
||||
m_current == EType::MmSegmentation ||
|
||||
m_current == EType::FuzzySkin ||
|
||||
m_current == EType::TextureDisplacement ||
|
||||
m_current == EType::Image ||
|
||||
m_current == EType::Seam;
|
||||
}
|
||||
|
||||
@@ -1561,8 +1550,6 @@ std::string get_name_from_gizmo_etype(GLGizmosManager::EType type)
|
||||
return "Fuzzy Skin Painting";
|
||||
case GLGizmosManager::EType::TextureDisplacement:
|
||||
return "Texture Displacement";
|
||||
case GLGizmosManager::EType::Image:
|
||||
return "Image";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -96,7 +96,6 @@ public:
|
||||
FuzzySkin,
|
||||
MmSegmentation,
|
||||
TextureDisplacement,
|
||||
Image,
|
||||
Emboss,
|
||||
Svg,
|
||||
Measure,
|
||||
|
||||
@@ -67,7 +67,6 @@
|
||||
#include "slic3r/GUI/Field.hpp"
|
||||
#include "slic3r/GUI/Widgets/PopupWindow.hpp"
|
||||
#include <set>
|
||||
#include <wx/filename.h>
|
||||
#include "slic3r/GUI/Widgets/StaticLine.hpp"
|
||||
#include "slic3r/GUI/GUI_ObjectSettings.hpp"
|
||||
#include "slic3r/GUI/ColorDecomposeDialog.hpp"
|
||||
@@ -247,7 +246,6 @@
|
||||
#include "DeviceCore/DevNozzleSystem.h" // DevNozzle, GetExtNozzles / GetRackNozzles
|
||||
#include "ProjectDirtyStateManager.hpp"
|
||||
#include "Gizmos/GLGizmoSimplify.hpp" // create suggestion notification
|
||||
#include "Gizmos/GLGizmoImage.hpp" // Drop an image onto the Image tool
|
||||
#include "Gizmos/GLGizmoSVG.hpp" // Drop SVG file
|
||||
#include "Gizmos/GizmoObjectManipulation.hpp"
|
||||
|
||||
@@ -7112,9 +7110,6 @@ void Sidebar::can_search()
|
||||
p->can_search();
|
||||
}
|
||||
|
||||
// Defined with the other drop helpers below; forward declared because the drop target uses it.
|
||||
void set_image_drag_over(Plater &plater, bool over);
|
||||
|
||||
class PlaterDropTarget : public wxFileDropTarget
|
||||
{
|
||||
public:
|
||||
@@ -7124,53 +7119,12 @@ public:
|
||||
|
||||
virtual bool OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames);
|
||||
|
||||
// A drag has no feedback of its own, so the Image tool is told when one is over the view and
|
||||
// draws a drop zone for as long as it is. wxWidgets cannot hand us the file names before the
|
||||
// drop on GTK, so this says nothing about what is being dragged: the zone invites a picture,
|
||||
// and anything else still falls through to the model loader.
|
||||
virtual wxDragResult OnEnter(wxCoord x, wxCoord y, wxDragResult def)
|
||||
{
|
||||
set_image_drag_over(m_plater, true);
|
||||
return wxFileDropTarget::OnEnter(x, y, def);
|
||||
}
|
||||
virtual void OnLeave()
|
||||
{
|
||||
set_image_drag_over(m_plater, false);
|
||||
wxFileDropTarget::OnLeave();
|
||||
}
|
||||
|
||||
private:
|
||||
MainFrame& m_mainframe;
|
||||
Plater& m_plater;
|
||||
};
|
||||
|
||||
namespace {
|
||||
// The Image gizmo, but only while it is the open one. Null otherwise, which is what keeps a drag or
|
||||
// a drop from meaning anything different with the tool closed.
|
||||
GLGizmoImage *open_image_gizmo(Plater &plater)
|
||||
{
|
||||
GLCanvas3D *canvas = plater.canvas3D();
|
||||
if (canvas == nullptr)
|
||||
return nullptr;
|
||||
GLGizmosManager &gizmos = canvas->get_gizmos_manager();
|
||||
if (gizmos.get_current_type() != GLGizmosManager::Image)
|
||||
return nullptr;
|
||||
return dynamic_cast<GLGizmoImage *>(gizmos.get_gizmo(GLGizmosManager::Image));
|
||||
}
|
||||
|
||||
// True when `file` is a picture and the Image gizmo is the one currently open, in which case it has
|
||||
// taken the file. Mirrors emboss_svg() below, which does the same for an SVG and the text tool.
|
||||
bool drop_image_on_gizmo(Plater &plater, const wxString &file)
|
||||
{
|
||||
static const std::set<wxString> image_ext = { "png", "jpg", "jpeg", "bmp" };
|
||||
const wxString ext = wxFileName(file).GetExt().Lower();
|
||||
if (image_ext.count(ext) == 0)
|
||||
return false;
|
||||
|
||||
GLGizmoImage *image = open_image_gizmo(plater);
|
||||
return image != nullptr && image->drop_image_file(into_u8(file));
|
||||
}
|
||||
|
||||
bool emboss_svg(Plater& plater, const wxString &svg_file, const Vec2d& mouse_drop_position)
|
||||
{
|
||||
std::string svg_file_str = into_u8(svg_file);
|
||||
@@ -7829,12 +7783,6 @@ const std::regex Plater::priv::pattern_zip_amf(".*[.]zip[.]amf", std::regex::ica
|
||||
const std::regex Plater::priv::pattern_any_amf(".*[.](amf|amf[.]xml|zip[.]amf)", std::regex::icase);
|
||||
const std::regex Plater::priv::pattern_prusa(".*bbl", std::regex::icase);
|
||||
|
||||
void set_image_drag_over(Plater &plater, bool over)
|
||||
{
|
||||
if (GLGizmoImage *image = open_image_gizmo(plater))
|
||||
image->set_file_drag_over(over);
|
||||
}
|
||||
|
||||
bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames)
|
||||
{
|
||||
#ifdef WIN32
|
||||
@@ -7842,18 +7790,11 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &fi
|
||||
this->MSWUpdateDragImageOnLeave();
|
||||
#endif // WIN32
|
||||
|
||||
set_image_drag_over(m_plater, false); // the drag is over, whoever ends up taking the file
|
||||
|
||||
m_mainframe.Raise();
|
||||
m_mainframe.select_tab(TAB_ID_PREPARE);
|
||||
if (wxGetApp().is_editor())
|
||||
m_plater.select_view_3D("3D");
|
||||
|
||||
// A picture dropped while the Image tool is open is that tool's input, not a model to load. Only
|
||||
// then: with the tool closed a dropped PNG goes on doing whatever it did before.
|
||||
if (filenames.size() == 1 && drop_image_on_gizmo(m_plater, filenames.Last()))
|
||||
return true;
|
||||
|
||||
// When only one .svg file is dropped on scene
|
||||
if (filenames.size() == 1) {
|
||||
const wxString &filename = filenames.Last();
|
||||
|
||||
@@ -114,9 +114,6 @@ constexpr std::array<ShortcutInfo, size_t(Shortcut::Count)> shortcut_table = {{
|
||||
SHORTCUT(GizmoAssembly, "gizmo_assembly", L("Gizmo assemble"), PLATER, { 'Y' }),
|
||||
SHORTCUT(GizmoBrimEars, "gizmo_brim_ears", L("Gizmo brim ears"), PLATER, { 'E' }),
|
||||
SHORTCUT(GizmoDisplacement, "gizmo_displacement", L("Gizmo texture displacement painting"), PLATER, { 'D' }),
|
||||
// Unbound by default: every single letter this context can reach is already taken, and a default
|
||||
// that shadows one of them is worse than none. The shortcuts dialog lists it for binding.
|
||||
SHORTCUT(GizmoImage, "gizmo_image", L("Gizmo image painting"), PLATER, KeyChord{}),
|
||||
|
||||
// Sliders
|
||||
SHORTCUT(GoToLayer, "go_to_layer", L("Jump to layer"), PREVIEW, { 'G', SHIFT }),
|
||||
|
||||
@@ -38,7 +38,7 @@ enum class Shortcut : uint8_t {
|
||||
MoveSelectionLeft, MoveSelectionRight, MoveSelectionUp, MoveSelectionDown, RotateSelectionLeft, RotateSelectionRight,
|
||||
// Gizmos
|
||||
GizmoMove, GizmoRotate, GizmoScale, GizmoFlatten, GizmoCut, GizmoMeshBoolean, GizmoFdmSupports, GizmoSeam, GizmoFuzzySkin,
|
||||
GizmoMmuSegmentation, GizmoEmboss, GizmoMeasure, GizmoAssembly, GizmoBrimEars, GizmoDisplacement, GizmoImage,
|
||||
GizmoMmuSegmentation, GizmoEmboss, GizmoMeasure, GizmoAssembly, GizmoBrimEars, GizmoDisplacement,
|
||||
// Sliders
|
||||
GoToLayer, LayerSliderUp, LayerSliderDown, MovesSliderLeft, MovesSliderRight, MovesSliderStart, MovesSliderEnd,
|
||||
// Painting tools
|
||||
|
||||
@@ -27,7 +27,8 @@ WebDialog::WebDialog(wxWindow* parent,
|
||||
SubmitHandler on_submit,
|
||||
CloseHandler on_close,
|
||||
CloseHandler on_destroyed,
|
||||
long wx_style)
|
||||
long wx_style,
|
||||
const wxString& profile)
|
||||
: WebViewHostDialog(parent, wxID_ANY, title, wxDefaultPosition, size, wx_style)
|
||||
, m_html(html)
|
||||
, m_on_message(std::move(on_message))
|
||||
@@ -37,7 +38,7 @@ WebDialog::WebDialog(wxWindow* parent,
|
||||
{
|
||||
// A tiny bundled bootstrap page brings the webview up; the real plugin HTML
|
||||
// is swapped in via SetPage once the bootstrap finishes loading.
|
||||
create_webview(web_hosting::BOOTSTRAP_PAGE, title, size, wxSize(320, 240));
|
||||
create_webview(web_hosting::BOOTSTRAP_PAGE, title, size, wxSize(320, 240), profile);
|
||||
|
||||
// Paint the window/webview in the themed background so there is no white
|
||||
// flash before the (transparent) bootstrap page and plugin HTML render.
|
||||
|
||||
@@ -49,7 +49,8 @@ public:
|
||||
SubmitHandler on_submit,
|
||||
CloseHandler on_close,
|
||||
CloseHandler on_destroyed,
|
||||
long wx_style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER);
|
||||
long wx_style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER,
|
||||
const wxString& profile = wxEmptyString);
|
||||
~WebDialog() override;
|
||||
|
||||
static void post_message(WebDialog* dialog, const nlohmann::json& data);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
WebPanel::WebPanel(wxWindow* parent, const char* bridge_script)
|
||||
WebPanel::WebPanel(wxWindow* parent, const char* bridge_script, const wxString& profile)
|
||||
: wxPanel(parent, wxID_ANY)
|
||||
{
|
||||
SetBackgroundColour(wxGetApp().get_window_default_clr());
|
||||
@@ -25,7 +25,7 @@ WebPanel::WebPanel(wxWindow* parent, const char* bridge_script)
|
||||
SetSizer(sizer);
|
||||
|
||||
// Never null: WebView::CreateWebView substitutes a placeholder view when no backend is available.
|
||||
m_browser = WebView::CreateWebView(this, web_hosting::bootstrap_url());
|
||||
m_browser = WebView::CreateWebView(this, web_hosting::bootstrap_url(), profile);
|
||||
m_browser->SetBackgroundColour(GetBackgroundColour());
|
||||
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::theme_user_script()));
|
||||
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::element_defaults_user_script()));
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <wx/event.h>
|
||||
#include <wx/panel.h>
|
||||
#include <wx/string.h>
|
||||
#include <wx/webview.h>
|
||||
|
||||
#include <optional>
|
||||
@@ -22,7 +23,7 @@ namespace Slic3r { namespace GUI {
|
||||
class WebPanel : public wxPanel
|
||||
{
|
||||
public:
|
||||
WebPanel(wxWindow* parent, const char* bridge_script);
|
||||
WebPanel(wxWindow* parent, const char* bridge_script, const wxString& profile = wxEmptyString);
|
||||
|
||||
protected:
|
||||
wxWebView* browser() const { return m_browser; }
|
||||
|
||||
@@ -82,6 +82,11 @@ ComboBox::ComboBox(wxWindow *parent,
|
||||
e.SetId(GetId());
|
||||
GetEventHandler()->ProcessEvent(e);
|
||||
});
|
||||
drop.Bind(EVT_DROPDOWN_ITEM_ACTION, [this](wxCommandEvent &e) {
|
||||
e.SetEventObject(this);
|
||||
e.SetId(GetId());
|
||||
GetEventHandler()->ProcessEvent(e);
|
||||
});
|
||||
drop.Bind(EVT_DISMISS, [this](auto &) {
|
||||
drop_down = false;
|
||||
wxCommandEvent e(wxEVT_COMBOBOX_CLOSEUP);
|
||||
@@ -251,7 +256,7 @@ int ComboBox::Append(const wxString &text,
|
||||
int style)
|
||||
{
|
||||
auto valid_bit_map = bitmap.IsOk() ? bitmap : wxNullBitmap;
|
||||
Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, clientData, group_key, group_label};
|
||||
Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, wxNullBitmap, clientData, group_key, group_label};
|
||||
item.style = style;
|
||||
items.push_back(item);
|
||||
SetClientDataType(wxClientData_Void);
|
||||
@@ -297,6 +302,12 @@ void ComboBox::ForceDropdownOpen()
|
||||
}
|
||||
}
|
||||
|
||||
void ComboBox::CloseDropdown()
|
||||
{
|
||||
if (drop.IsShown())
|
||||
drop.DismissAndNotify();
|
||||
}
|
||||
|
||||
unsigned int ComboBox::GetCount() const { return items.size(); }
|
||||
|
||||
void ComboBox::set_replace_text(wxString text, wxString image_name)
|
||||
@@ -350,6 +361,13 @@ void ComboBox::SetItemBitmap(unsigned int n, wxBitmap const &bitmap)
|
||||
drop.Invalidate();
|
||||
}
|
||||
|
||||
void ComboBox::SetItemActionBitmap(unsigned int n, wxBitmap const &bitmap)
|
||||
{
|
||||
if (n >= items.size()) return;
|
||||
items[n].action_icon = bitmap.IsOk() ? bitmap : wxNullBitmap;
|
||||
drop.Invalidate();
|
||||
}
|
||||
|
||||
int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items,
|
||||
unsigned int pos,
|
||||
void ** clientData,
|
||||
@@ -357,7 +375,7 @@ int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items,
|
||||
{
|
||||
if (pos > this->items.size()) return -1;
|
||||
for (int i = 0; i < items.GetCount(); ++i) {
|
||||
Item item { items[i], wxEmptyString, wxNullBitmap, wxNullBitmap, clientData ? clientData[i] : NULL };
|
||||
Item item { items[i], wxEmptyString, wxNullBitmap, wxNullBitmap, wxNullBitmap, clientData ? clientData[i] : NULL };
|
||||
this->items.insert(this->items.begin() + pos, item);
|
||||
++pos;
|
||||
}
|
||||
@@ -419,16 +437,18 @@ void ComboBox::keyDown(wxKeyEvent& event)
|
||||
case WXK_UP:
|
||||
case WXK_DOWN:
|
||||
case WXK_LEFT:
|
||||
case WXK_RIGHT:
|
||||
if ((event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) && GetSelection() > 0) {
|
||||
SetSelection(GetSelection() - 1);
|
||||
} else if ((event.GetKeyCode() == WXK_DOWN || event.GetKeyCode() == WXK_RIGHT) && GetSelection() + 1 < items.size()) {
|
||||
SetSelection(GetSelection() + 1);
|
||||
} else {
|
||||
case WXK_RIGHT: {
|
||||
// Step over disabled rows: the mouse cannot select them either.
|
||||
const int step = (event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) ? -1 : 1;
|
||||
int n = GetSelection() + step;
|
||||
while (n >= 0 && n < int(items.size()) && (items[n].style & DD_ITEM_STYLE_DISABLED))
|
||||
n += step;
|
||||
if (n < 0 || n >= int(items.size()))
|
||||
break;
|
||||
}
|
||||
SetSelection(n);
|
||||
sendComboBoxEvent();
|
||||
break;
|
||||
}
|
||||
case WXK_TAB:
|
||||
HandleAsNavigationKey(event);
|
||||
break;
|
||||
|
||||
@@ -96,10 +96,12 @@ public:
|
||||
|
||||
wxBitmap GetItemBitmap(unsigned int n);
|
||||
void SetItemBitmap(unsigned int n, wxBitmap const &bitmap);
|
||||
void SetItemActionBitmap(unsigned int n, wxBitmap const &bitmap);
|
||||
bool is_drop_down(){return drop_down;}
|
||||
void DeleteOneItem(unsigned int pos) { DoDeleteOneItem(pos); }
|
||||
|
||||
void ForceDropdownOpen();
|
||||
void CloseDropdown();
|
||||
|
||||
protected:
|
||||
virtual int DoInsertItems(const wxArrayStringsAdapter &items,
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "slic3r/GUI/wxExtensions.hpp"
|
||||
|
||||
wxDEFINE_EVENT(EVT_DISMISS, wxCommandEvent);
|
||||
wxDEFINE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent);
|
||||
|
||||
BEGIN_EVENT_TABLE(DropDown, PopupWindow)
|
||||
|
||||
@@ -242,6 +243,19 @@ static wxSize GetBmpSize(wxBitmap & bmp)
|
||||
#endif
|
||||
}
|
||||
|
||||
// The x-range [first, second) of the column at the right of the rows that holds the items' action
|
||||
// icons, empty when no item has one. render() draws the icons in it and mouseReleased() hit-tests
|
||||
// clicks against it, so the drawn icon and its clickable area always match.
|
||||
std::pair<int, int> DropDown::actionColumn() const
|
||||
{
|
||||
if (actionIconWidth <= 0)
|
||||
return {0, 0};
|
||||
int right = rowSize.x;
|
||||
if (rowSize.y * count > GetSize().y) // position bar
|
||||
right -= 6;
|
||||
return {right - actionIconWidth - FromDIP(16), right};
|
||||
}
|
||||
|
||||
static void _DrawSplitItem(const wxWindow* w, wxDC& dc, wxString split_text, wxPoint start_pt, int item_width, int item_height)
|
||||
{
|
||||
// save dc
|
||||
@@ -371,6 +385,10 @@ void DropDown::render(wxDC &dc)
|
||||
rcContent.width -= szBmp.x + 5;
|
||||
}
|
||||
|
||||
const auto action_column = actionColumn();
|
||||
if (action_column.second > action_column.first)
|
||||
rcContent.width = action_column.first - rcContent.x;
|
||||
|
||||
std::set<wxString> groups;
|
||||
// draw texts & icons
|
||||
int index = 0;
|
||||
@@ -434,13 +452,24 @@ void DropDown::render(wxDC &dc)
|
||||
auto text = group.IsEmpty()
|
||||
? (item.group_key.IsEmpty() ? item.text : item.group_label)
|
||||
: item.text;
|
||||
if (item.action_icon.IsOk()) {
|
||||
auto action_size = GetBmpSize(item.action_icon);
|
||||
wxPoint action_pt(action_column.second - action_size.x - FromDIP(8),
|
||||
rcContent.y + (rcContent.height - action_size.y) / 2);
|
||||
dc.DrawBitmap(item.action_icon, action_pt);
|
||||
}
|
||||
|
||||
if (!text_off && !text.IsEmpty()) {
|
||||
wxSize tSize = dc.GetMultiLineTextExtent(text);
|
||||
if (pt.x + tSize.x > rcContent.GetRight()) {
|
||||
if (is_hover && item.tip.IsEmpty())
|
||||
SetToolTip(text);
|
||||
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END,
|
||||
rcContent.GetRight() - pt.x);
|
||||
int max_text_width = rcContent.GetRight() - pt.x;
|
||||
if (max_text_width <= 0) {
|
||||
text.clear();
|
||||
} else {
|
||||
if (is_hover && item.tip.IsEmpty())
|
||||
SetToolTip(text);
|
||||
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END, max_text_width);
|
||||
}
|
||||
}
|
||||
pt.y += (rcContent.height - textSize.y) / 2;
|
||||
dc.SetFont(GetFont());
|
||||
@@ -529,6 +558,7 @@ void DropDown::messureSize()
|
||||
if (!need_sync) return;
|
||||
textSize = wxSize();
|
||||
iconSize = wxSize();
|
||||
actionIconWidth = 0;
|
||||
count = 0;
|
||||
wxClientDC dc(GetParent() ? GetParent() : this);
|
||||
dc.SetFont(GetFont());
|
||||
@@ -565,8 +595,13 @@ void DropDown::messureSize()
|
||||
size1.x += size2.x + (text_off ? 0 : 5);
|
||||
}
|
||||
}
|
||||
if (item.action_icon.IsOk())
|
||||
actionIconWidth = std::max(actionIconWidth, GetBmpSize(item.action_icon).x);
|
||||
if (size1.x > textSize.x) textSize = size1;
|
||||
}
|
||||
// Every row leaves room for the action column, as render() and actionColumn() lay it out.
|
||||
if (actionIconWidth > 0)
|
||||
textSize.x += actionIconWidth + FromDIP(16);
|
||||
if (!align_icon) iconSize.x = 0;
|
||||
wxSize szContent = textSize;
|
||||
if (szContent.x < FromDIP(120) && !use_content_width)
|
||||
@@ -636,6 +671,11 @@ void DropDown::messureSize()
|
||||
e.SetId(GetId());
|
||||
GetEventHandler()->ProcessEvent(e);
|
||||
});
|
||||
subDropDown->Bind(EVT_DROPDOWN_ITEM_ACTION, [this](wxCommandEvent &e) {
|
||||
e.SetEventObject(this);
|
||||
e.SetId(GetId());
|
||||
GetEventHandler()->ProcessEvent(e);
|
||||
});
|
||||
}
|
||||
need_sync = false;
|
||||
}
|
||||
@@ -714,6 +754,15 @@ void DropDown::mouseReleased(wxMouseEvent& event)
|
||||
if (hover_item < 0)
|
||||
return;
|
||||
if (hover_item >= 0 && (subDropDown == nullptr || subDropDown->group.empty())) { // not moved
|
||||
int index = hoverIndex();
|
||||
if (index >= 0 && items[index].action_icon.IsOk()) {
|
||||
const auto action_column = actionColumn();
|
||||
if (event.GetX() >= action_column.first && event.GetX() < action_column.second) {
|
||||
sendDropDownActionEvent(index);
|
||||
paintNow();
|
||||
return;
|
||||
}
|
||||
}
|
||||
sendDropDownEvent();
|
||||
if (mainDropDown)
|
||||
mainDropDown->hover_item = -1; // To Dismiss mainDropDown
|
||||
@@ -830,6 +879,17 @@ void DropDown::sendDropDownEvent()
|
||||
GetEventHandler()->ProcessEvent(event);
|
||||
}
|
||||
|
||||
void DropDown::sendDropDownActionEvent(int index)
|
||||
{
|
||||
if (index < 0 || index >= int(items.size()) || (items[index].style & DD_ITEM_STYLE_DISABLED))
|
||||
return;
|
||||
wxCommandEvent event(EVT_DROPDOWN_ITEM_ACTION, GetId());
|
||||
event.SetEventObject(this);
|
||||
event.SetInt(index);
|
||||
event.SetString(items[index].text);
|
||||
GetEventHandler()->ProcessEvent(event);
|
||||
}
|
||||
|
||||
void DropDown::Dismiss()
|
||||
{
|
||||
if (subDropDown && subDropDown->IsShown())
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <wx/event.h>
|
||||
#include <vector>
|
||||
#include <cstddef>
|
||||
#include <utility>
|
||||
#include <wx/gdicmn.h>
|
||||
#include "slic3r/GUI/Widgets/StateColor.hpp"
|
||||
#include <boost/date_time/posix_time/ptime.hpp>
|
||||
@@ -27,6 +28,7 @@ class wxWindow;
|
||||
#define DD_ITEM_STYLE_DIMMED 0x0004 // gray text, but still selectable
|
||||
|
||||
wxDECLARE_EVENT(EVT_DISMISS, wxCommandEvent);
|
||||
wxDECLARE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent);
|
||||
|
||||
class DropDown : public PopupWindow
|
||||
{
|
||||
@@ -37,6 +39,7 @@ public:
|
||||
wxString text_static_tips;// display static tips for TextInput.eg.PrinterInfoBox
|
||||
wxBitmap icon;
|
||||
wxBitmap icon_textctrl;// display icon for TextInput.eg.PrinterInfoBox
|
||||
wxBitmap action_icon;
|
||||
void * data{nullptr};
|
||||
wxString group_key{};
|
||||
wxString group_label{};
|
||||
@@ -65,6 +68,7 @@ private:
|
||||
|
||||
wxSize textSize;
|
||||
wxSize iconSize;
|
||||
int actionIconWidth = 0;
|
||||
wxSize rowSize;
|
||||
|
||||
StateHandler state_handler;
|
||||
@@ -135,6 +139,7 @@ private:
|
||||
int hoverIndex();
|
||||
|
||||
int selectedItem();
|
||||
std::pair<int, int> actionColumn() const;
|
||||
|
||||
friend class ComboBox;
|
||||
void messureSize();
|
||||
@@ -148,6 +153,7 @@ private:
|
||||
void mouseWheelMoved(wxMouseEvent &event);
|
||||
|
||||
void sendDropDownEvent();
|
||||
void sendDropDownActionEvent(int index);
|
||||
|
||||
|
||||
DECLARE_EVENT_TABLE()
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/Utils/MacDarkMode.hpp"
|
||||
|
||||
#include <libslic3r/Utils.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
@@ -12,6 +14,7 @@
|
||||
#include <exception>
|
||||
#include <thread>
|
||||
|
||||
#include <wx/chartype.h>
|
||||
#include <wx/setup.h>
|
||||
#include <wx/webview.h>
|
||||
#include <wx/string.h>
|
||||
@@ -72,6 +75,26 @@ webkit_javascript_result_unref (WebKitJavascriptResult *js_result);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Private storage root for an isolated (plugin) profile. Plugin pages must not reach the shared
|
||||
// default store's cookies/session (e.g. the OrcaCloud login), so each profile gets its own dir.
|
||||
// macOS isolates with an in-memory store instead (see CreateWebView), so this is unused there.
|
||||
#if !defined(__WXOSX__)
|
||||
static wxString isolated_profile_path(const wxString& profile)
|
||||
{
|
||||
wxString safe;
|
||||
safe.reserve(profile.size());
|
||||
for (wxString::const_iterator it = profile.begin(); it != profile.end(); ++it) {
|
||||
const wxChar c = *it;
|
||||
const bool keep = (c >= wxT('a') && c <= wxT('z')) || (c >= wxT('A') && c <= wxT('Z')) ||
|
||||
(c >= wxT('0') && c <= wxT('9')) || c == wxT('.') || c == wxT('-') || c == wxT('_');
|
||||
safe += keep ? c : wxT('_');
|
||||
}
|
||||
if (safe.empty())
|
||||
safe = wxT("default");
|
||||
return wxString::FromUTF8((boost::filesystem::path(Slic3r::data_dir()) / "web_profiles" / safe.ToStdString()).string());
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef __WIN32__
|
||||
// Run Download and Install in another thread so we don't block the UI thread
|
||||
DWORD DownloadAndInstallWV2RT() {
|
||||
@@ -135,6 +158,9 @@ DWORD DownloadAndInstallWV2RT() {
|
||||
class WebViewEdge : public wxWebViewEdge
|
||||
{
|
||||
public:
|
||||
WebViewEdge() = default;
|
||||
explicit WebViewEdge(const wxWebViewConfiguration& config) : wxWebViewEdge(config) {}
|
||||
|
||||
bool SetUserAgent(const wxString &userAgent) override
|
||||
{
|
||||
bool dark = userAgent.Contains("dark");
|
||||
@@ -213,6 +239,11 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
explicit WebViewWebKit(const wxWebViewConfiguration& config)
|
||||
: wxWebViewWebKit(config)
|
||||
{
|
||||
}
|
||||
|
||||
~WebViewWebKit() override
|
||||
{
|
||||
RemoveScriptMessageHandler("wx");
|
||||
@@ -284,7 +315,7 @@ static WebViewRef *webview_ref(wxWebView *webView)
|
||||
return webView ? static_cast<WebViewRef *>(webView->GetRefData()) : nullptr;
|
||||
}
|
||||
|
||||
wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url)
|
||||
wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url, wxString const & profile)
|
||||
{
|
||||
#if wxUSE_WEBVIEW_EDGE
|
||||
// Check if a fixed version of edge is present in
|
||||
@@ -304,12 +335,36 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url)
|
||||
if (!url2.empty()) { url2 = wxURI(url2).BuildURI(); }
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << url2.ToUTF8();
|
||||
|
||||
const bool isolated = !profile.IsEmpty();
|
||||
|
||||
#ifdef __WIN32__
|
||||
wxWebView* webView = new WebViewEdge;
|
||||
wxWebView* webView = nullptr;
|
||||
if (isolated) {
|
||||
wxWebViewConfiguration config = wxWebView::NewConfiguration(wxWebViewBackendEdge);
|
||||
config.SetDataPath(isolated_profile_path(profile));
|
||||
webView = new WebViewEdge(config);
|
||||
} else {
|
||||
webView = new WebViewEdge;
|
||||
}
|
||||
#elif defined(__WXOSX__)
|
||||
wxWebView* webView = new WebViewWebKit;
|
||||
wxWebView* webView = nullptr;
|
||||
if (isolated) {
|
||||
// wx has no SetDataPath on macOS; give the view its own in-memory store instead.
|
||||
wxWebViewConfiguration config = wxWebView::NewConfiguration(wxWebViewBackendWebKit);
|
||||
Slic3r::GUI::WKWebViewConfiguration_setNonPersistentDataStore(config.GetNativeConfiguration());
|
||||
webView = new WebViewWebKit(config);
|
||||
} else {
|
||||
webView = new WebViewWebKit;
|
||||
}
|
||||
#else
|
||||
auto webView = wxWebView::New();
|
||||
wxWebView* webView = nullptr;
|
||||
if (isolated) {
|
||||
wxWebViewConfiguration config = wxWebView::NewConfiguration(wxWebViewBackendWebKit);
|
||||
config.SetDataPath(isolated_profile_path(profile));
|
||||
webView = wxWebView::New(config);
|
||||
} else {
|
||||
webView = wxWebView::New();
|
||||
}
|
||||
#endif
|
||||
if (webView) {
|
||||
webView->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
||||
|
||||
@@ -14,7 +14,11 @@ wxDECLARE_EVENT(EVT_WEBVIEW_RECREATED, wxCommandEvent);
|
||||
class WebView
|
||||
{
|
||||
public:
|
||||
static wxWebView *CreateWebView(wxWindow *parent, wxString const &url);
|
||||
// `profile` empty -> the shared default backend store used by all trusted UI.
|
||||
// `profile` non-empty -> a private store keyed by `profile`, so this view shares no
|
||||
// cookies/storage/session with the shared store or any other profile. Used for
|
||||
// plugin-supplied (untrusted) content.
|
||||
static wxWebView *CreateWebView(wxWindow *parent, wxString const &url, wxString const &profile = wxEmptyString);
|
||||
#if wxUSE_WEBVIEW_EDGE
|
||||
static bool CheckWebViewRuntime();
|
||||
static bool DownloadAndInstallWebViewRuntime();
|
||||
|
||||
@@ -172,7 +172,8 @@ WebViewHostDialog::WebViewHostDialog(wxWindow* parent,
|
||||
bool WebViewHostDialog::create_webview(const std::string& resource_path,
|
||||
const wxString& title,
|
||||
const wxSize& dialog_size,
|
||||
const wxSize& min_size)
|
||||
const wxSize& min_size,
|
||||
const wxString& profile)
|
||||
{
|
||||
SetTitle(title);
|
||||
SetMinSize(FromDIP(min_size));
|
||||
@@ -181,7 +182,7 @@ bool WebViewHostDialog::create_webview(const std::string& resource_path,
|
||||
wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
// Created empty and loaded below, after the user scripts are added, so the first load already has them.
|
||||
m_browser = WebView::CreateWebView(this, wxEmptyString);
|
||||
m_browser = WebView::CreateWebView(this, wxEmptyString, profile);
|
||||
if (m_browser == nullptr) {
|
||||
wxLogError("Could not init m_browser");
|
||||
delete topsizer;
|
||||
|
||||
@@ -38,7 +38,8 @@ public:
|
||||
bool create_webview(const std::string& resource_path,
|
||||
const wxString& title,
|
||||
const wxSize& dialog_size = wxSize(820, 660),
|
||||
const wxSize& min_size = wxSize(640, 640));
|
||||
const wxSize& min_size = wxSize(640, 640),
|
||||
const wxString& profile = wxEmptyString);
|
||||
|
||||
void load_url(const wxString& url);
|
||||
bool run_script(const wxString& script);
|
||||
|
||||
@@ -12,6 +12,7 @@ extern double mac_max_scaling_factor();
|
||||
extern void set_miniaturizable(void * window);
|
||||
void WKWebView_evaluateJavaScript(void * web, wxString const & script, void (*callback)(wxString const &));
|
||||
void WKWebView_setTransparentBackground(void * web);
|
||||
void WKWebViewConfiguration_setNonPersistentDataStore(void * configuration);
|
||||
void WKWebView_force_display(void * web);
|
||||
void set_tag_when_enter_full_screen(bool isfullscreen);
|
||||
void set_title_colour_after_set_title(void * window);
|
||||
|
||||
@@ -102,6 +102,15 @@ void WKWebView_setTransparentBackground(void * web)
|
||||
[webView registerForDraggedTypes: @[NSFilenamesPboardType]];
|
||||
}
|
||||
|
||||
// Give a WKWebViewConfiguration its own in-memory website data store, so the view's cookies and
|
||||
// storage neither come from nor leak into the shared default store. Used to isolate plugin web views.
|
||||
void WKWebViewConfiguration_setNonPersistentDataStore(void * configuration)
|
||||
{
|
||||
WKWebViewConfiguration * config = (WKWebViewConfiguration*)configuration;
|
||||
if (config)
|
||||
config.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore];
|
||||
}
|
||||
|
||||
// Force a WKWebView to re-lay-out and repaint. Needed for chrome-less popups: the window is
|
||||
// transparent, so a WKWebView whose layer has no pending frame leaves the whole window invisible
|
||||
// until the user generates input (scroll/arrow). setNeedsDisplay alone does not always reach the
|
||||
|
||||
@@ -390,7 +390,8 @@ py::object ui_create_window(const std::string& html, const std::string& title, i
|
||||
|
||||
auto* dlg = new GUI::WebDialog(ui_parent(), wxString::FromUTF8(title), html,
|
||||
wxSize(w, h), std::move(msg_adapter), std::move(submit_adapter),
|
||||
std::move(on_close), std::move(on_destroyed), PLUGIN_WX_STYLE);
|
||||
std::move(on_close), std::move(on_destroyed), PLUGIN_WX_STYLE,
|
||||
wxString::FromUTF8(plugin_key));
|
||||
UiRegistry::instance().bind(new_id, dlg, plugin_key);
|
||||
if (modal) {
|
||||
dlg->ShowModal();
|
||||
@@ -487,7 +488,7 @@ py::object ui_create_dock_panel(const std::string& html, const std::string& titl
|
||||
|
||||
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
|
||||
auto* panel = new GUI::DockPanel(plater, html, std::move(msg_adapter), make_close_adapter(close_holder),
|
||||
std::move(on_destroyed));
|
||||
std::move(on_destroyed), wxString::FromUTF8(plugin_key));
|
||||
UiRegistry::instance().bind(new_id, panel, plugin_key);
|
||||
plater->add_dock_pane(panel, GUI::plugin_pane_name(plugin_key, title), wxString::FromUTF8(title), dock,
|
||||
wxSize(w, h), [panel]() { panel->fire_close(); });
|
||||
|
||||
@@ -77,7 +77,8 @@ constexpr char PLUGIN_PAGE_BRIDGE_JS[] = R"JS(
|
||||
} // namespace
|
||||
|
||||
PluginPage::PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability)
|
||||
: GUI::WebPanel(parent, PLUGIN_PAGE_BRIDGE_JS)
|
||||
: GUI::WebPanel(parent, PLUGIN_PAGE_BRIDGE_JS,
|
||||
wxString::FromUTF8(capability ? capability->identity().plugin_key : std::string()))
|
||||
, m_cap(std::move(capability))
|
||||
, m_lifetime(std::make_shared<std::atomic<PluginPage*>>(this))
|
||||
{
|
||||
|
||||
@@ -2043,3 +2043,40 @@ TEST_CASE("3MF XML entries declaring more than an int can hold fail to load", "[
|
||||
CHECK_FALSE(parser.check_3mf_from_prusa(path));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Loading a gcode.3mf from a stream keeps the plate's pick and pattern file paths", "[3mf][Regression]")
|
||||
{
|
||||
Model model;
|
||||
REQUIRE(load_stl((std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl").c_str(), &model));
|
||||
model.add_default_instances();
|
||||
ScopedTemporaryDir backup_dir("orca_pick");
|
||||
model.set_backup_path(backup_dir.string());
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
|
||||
auto plate = std::make_unique<PlateData>();
|
||||
plate->plate_index = 0;
|
||||
plate->pick_file = (backup_dir.path() / "pick.png").string();
|
||||
plate->pattern_bbox_file = (backup_dir.path() / "plate.json").string();
|
||||
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
const std::string test_file = temp.string();
|
||||
StoreParams store_params;
|
||||
store_params.path = test_file.c_str();
|
||||
store_params.model = &model;
|
||||
store_params.config = &config;
|
||||
store_params.plate_data_list.push_back(plate.get());
|
||||
store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
|
||||
REQUIRE(store_bbs_3mf(store_params));
|
||||
|
||||
boost::nowide::ifstream in(test_file, std::ios::binary);
|
||||
Model dst_model;
|
||||
DynamicPrintConfig dst_config;
|
||||
PlateDataPtrs dst_plates;
|
||||
Semver file_version;
|
||||
dst_model.set_backup_path(backup_dir.string());
|
||||
REQUIRE(load_gcode_3mf_from_stream(in, &dst_config, &dst_model, &dst_plates, &file_version));
|
||||
REQUIRE(dst_plates.size() == 1);
|
||||
CHECK(dst_plates.front()->pick_file == "Metadata/pick_1.png");
|
||||
CHECK(dst_plates.front()->pattern_bbox_file == "Metadata/plate_1.json");
|
||||
release_PlateData_list(dst_plates);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user