mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-15 11:51:33 +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 });
|
||||||
|
}
|
||||||
@@ -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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Qidi",
|
"name": "Qidi",
|
||||||
"version": "02.04.00.16",
|
"version": "02.04.00.17",
|
||||||
"force_update": "0",
|
"force_update": "0",
|
||||||
"description": "Qidi configurations",
|
"description": "Qidi configurations",
|
||||||
"machine_model_list": [
|
"machine_model_list": [
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
"extruder_clearance_height_to_lid": "120",
|
"extruder_clearance_height_to_lid": "120",
|
||||||
"layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nLOG_Z\nG92 E0\n",
|
"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_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",
|
"thumbnails_format": "PNG",
|
||||||
"default_filament_profile": [
|
"default_filament_profile": [
|
||||||
"Generic PLA @Qidi Q1 Pro 0.4 nozzle"
|
"Generic PLA @Qidi Q1 Pro 0.4 nozzle"
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
"default_filament_profile": [
|
"default_filament_profile": [
|
||||||
"Generic PLA @Qidi"
|
"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",
|
"machine_end_gcode": "M104 S0\nM140 S0\n;Retract the filament\nG92 E0\nG1 E-3 F300\nG28\nM84\n",
|
||||||
"scan_first_layer": "0"
|
"scan_first_layer": "0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"210x210/COLPIC",
|
"210x210/COLPIC",
|
||||||
"110x110/PNG"
|
"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}",
|
"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"
|
"scan_first_layer": "0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,14 +45,6 @@ uniform sampler2D height_tex;
|
|||||||
uniform vec2 height_tex_texel;
|
uniform vec2 height_tex_texel;
|
||||||
uniform float depth_mm;
|
uniform float depth_mm;
|
||||||
uniform float tiling_scale;
|
uniform float tiling_scale;
|
||||||
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - the square tile
|
|
||||||
// every layer had before the two axes could differ - which is also what layer_tile_mm() does on the
|
|
||||||
// CPU side, so an unset uniform behaves exactly as this shader always did.
|
|
||||||
uniform float tiling_scale_v;
|
|
||||||
// Mirror the texture along one axis of the tile. Both default to false, which GLSL guarantees for an
|
|
||||||
// uninitialised bool uniform, so a shader whose caller has not set them behaves exactly as before.
|
|
||||||
uniform bool mirror_u;
|
|
||||||
uniform bool mirror_v;
|
|
||||||
// Height map width / height. Scales the v axis so a non-square image keeps its proportions
|
// Height map width / height. Scales the v axis so a non-square image keeps its proportions
|
||||||
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
|
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
|
||||||
uniform float tex_aspect;
|
uniform float tex_aspect;
|
||||||
@@ -158,29 +150,13 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
|
|
||||||
vec2 tile_mm()
|
|
||||||
{
|
|
||||||
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
|
|
||||||
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
|
|
||||||
}
|
|
||||||
|
|
||||||
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
|
|
||||||
vec2 mirror_sign()
|
|
||||||
{
|
|
||||||
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
vec2 project_uv(vec3 p, vec3 n)
|
vec2 project_uv(vec3 p, vec3 n)
|
||||||
{
|
{
|
||||||
vec2 planar = projection_raw(p, n);
|
vec2 planar = projection_raw(p, n);
|
||||||
|
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||||
float cs = cos(rotation_rad);
|
float cs = cos(rotation_rad);
|
||||||
float sn = sin(rotation_rad);
|
float sn = sin(rotation_rad);
|
||||||
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
||||||
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
|
|
||||||
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
|
|
||||||
// the same division, in the tile's own frame.
|
|
||||||
r *= mirror_sign() / tile_mm();
|
|
||||||
// After the rotation, so the rotation stays a rotation rather than becoming a shear.
|
// After the rotation, so the rotation stays a rotation rather than becoming a shear.
|
||||||
r.y *= tex_aspect;
|
r.y *= tex_aspect;
|
||||||
return r + uv_offset;
|
return r + uv_offset;
|
||||||
@@ -302,7 +278,7 @@ void main()
|
|||||||
// a texel there is no parallax to find and the march would be pure cost - which is the common
|
// a texel there is no parallax to find and the march would be pure cost - which is the common
|
||||||
// case of looking straight down at a surface.
|
// case of looking straight down at a surface.
|
||||||
float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
|
float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
|
||||||
if (v_dot_n > 0.05 && sweep > 0.5 * min(tile_mm().x, tile_mm().y) * height_tex_texel.x) {
|
if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) {
|
||||||
// A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the
|
// A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the
|
||||||
// undisplaced surface. Start at the top of the shell, where the ray is outside the surface
|
// undisplaced surface. Start at the top of the shell, where the ray is outside the surface
|
||||||
// by construction, and step inward; the crossing is what this pixel actually sees.
|
// by construction, and step inward; the crossing is what this pixel actually sees.
|
||||||
@@ -334,17 +310,14 @@ void main()
|
|||||||
float hU = texture2D(height_tex, uv + vec2(0.0, height_tex_texel.y)).r;
|
float hU = texture2D(height_tex, uv + vec2(0.0, height_tex_texel.y)).r;
|
||||||
|
|
||||||
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
|
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
|
||||||
vec2 tile = tile_mm();
|
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0);
|
||||||
|
|
||||||
float cs = cos(rotation_rad);
|
float cs = cos(rotation_rad);
|
||||||
float sn = sin(rotation_rad);
|
float sn = sin(rotation_rad);
|
||||||
// One uv unit is tile.x mm along u but tile.y / tex_aspect mm along v, so each component of
|
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v
|
||||||
// the uv-space gradient carries its own factor before being rotated back into t/b.
|
// component of the gradient carries the extra factor before being rotated back into t/b.
|
||||||
// Each component carries its own extent and its own mirror sign, both of which project_uv()
|
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect);
|
||||||
// applied on the way in; without the sign a mirrored layer would light as though it were not.
|
|
||||||
vec2 ms = mirror_sign();
|
|
||||||
vec2 g = vec2(dh_duv.x * ms.x / tile.x, dh_duv.y * tex_aspect * ms.y / tile.y);
|
|
||||||
// ...and back out of raw-projection units into millimetres along t / b; see the 140 variant.
|
// ...and back out of raw-projection units into millimetres along t / b; see the 140 variant.
|
||||||
vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs) * units_per_mm;
|
vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs) * units_per_mm;
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,6 @@ uniform bool volume_mirrored;
|
|||||||
uniform int mode;
|
uniform int mode;
|
||||||
uniform float checker_freq;
|
uniform float checker_freq;
|
||||||
uniform float tiling_scale;
|
uniform float tiling_scale;
|
||||||
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - see
|
|
||||||
// layer_tile_mm() on the CPU side, so an unset uniform behaves exactly as this shader always did.
|
|
||||||
uniform float tiling_scale_v;
|
|
||||||
// Mirror along one tile axis; false by default, so an unset uniform is the old behaviour.
|
|
||||||
uniform bool mirror_u;
|
|
||||||
uniform bool mirror_v;
|
|
||||||
uniform vec3 tex_anchor; // the volume's origin in world space
|
uniform vec3 tex_anchor; // the volume's origin in world space
|
||||||
uniform float rotation_rad;
|
uniform float rotation_rad;
|
||||||
uniform vec2 uv_offset;
|
uniform vec2 uv_offset;
|
||||||
@@ -73,30 +67,13 @@ vec2 projection_raw(vec3 p, vec3 n)
|
|||||||
return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
|
return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
|
|
||||||
vec2 tile_mm()
|
|
||||||
{
|
|
||||||
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
|
|
||||||
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
|
|
||||||
}
|
|
||||||
|
|
||||||
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
|
|
||||||
vec2 mirror_sign()
|
|
||||||
{
|
|
||||||
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
vec2 project_uv(vec3 p, vec3 n)
|
vec2 project_uv(vec3 p, vec3 n)
|
||||||
{
|
{
|
||||||
vec2 planar = projection_raw(p, n);
|
vec2 planar = projection_raw(p, n);
|
||||||
|
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||||
float cs = cos(rotation_rad);
|
float cs = cos(rotation_rad);
|
||||||
float sn = sin(rotation_rad);
|
float sn = sin(rotation_rad);
|
||||||
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
||||||
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
|
|
||||||
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
|
|
||||||
// the same division, in the tile's own frame.
|
|
||||||
r *= mirror_sign() / tile_mm();
|
|
||||||
r.y *= tex_aspect; // after the rotation, so the rotation stays a rotation rather than a shear
|
r.y *= tex_aspect; // after the rotation, so the rotation stays a rotation rather than a shear
|
||||||
return r + uv_offset;
|
return r + uv_offset;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,14 +104,6 @@ uniform sampler2D height_tex;
|
|||||||
uniform vec2 height_tex_texel; // (1/width, 1/height) of height_tex
|
uniform vec2 height_tex_texel; // (1/width, 1/height) of height_tex
|
||||||
uniform float depth_mm;
|
uniform float depth_mm;
|
||||||
uniform float tiling_scale;
|
uniform float tiling_scale;
|
||||||
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - the square tile
|
|
||||||
// every layer had before the two axes could differ - which is also what layer_tile_mm() does on the
|
|
||||||
// CPU side, so an unset uniform behaves exactly as this shader always did.
|
|
||||||
uniform float tiling_scale_v;
|
|
||||||
// Mirror the texture along one axis of the tile. Both default to false, which GLSL guarantees for an
|
|
||||||
// uninitialised bool uniform, so a shader whose caller has not set them behaves exactly as before.
|
|
||||||
uniform bool mirror_u;
|
|
||||||
uniform bool mirror_v;
|
|
||||||
// Height map width / height. Scales the v axis so a non-square image keeps its proportions
|
// Height map width / height. Scales the v axis so a non-square image keeps its proportions
|
||||||
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
|
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
|
||||||
uniform float tex_aspect;
|
uniform float tex_aspect;
|
||||||
@@ -224,29 +216,13 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
|
|
||||||
vec2 tile_mm()
|
|
||||||
{
|
|
||||||
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
|
|
||||||
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
|
|
||||||
}
|
|
||||||
|
|
||||||
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
|
|
||||||
vec2 mirror_sign()
|
|
||||||
{
|
|
||||||
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
vec2 project_uv(vec3 p, vec3 n)
|
vec2 project_uv(vec3 p, vec3 n)
|
||||||
{
|
{
|
||||||
vec2 planar = projection_raw(p, n);
|
vec2 planar = projection_raw(p, n);
|
||||||
|
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||||
float cs = cos(rotation_rad);
|
float cs = cos(rotation_rad);
|
||||||
float sn = sin(rotation_rad);
|
float sn = sin(rotation_rad);
|
||||||
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
||||||
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
|
|
||||||
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
|
|
||||||
// the same division, in the tile's own frame.
|
|
||||||
r *= mirror_sign() / tile_mm();
|
|
||||||
// After the rotation, so the rotation stays a rotation rather than becoming a shear.
|
// After the rotation, so the rotation stays a rotation rather than becoming a shear.
|
||||||
r.y *= tex_aspect;
|
r.y *= tex_aspect;
|
||||||
return r + uv_offset;
|
return r + uv_offset;
|
||||||
@@ -383,7 +359,7 @@ void main()
|
|||||||
// a texel there is no parallax to find and the march would be pure cost - which is the common
|
// a texel there is no parallax to find and the march would be pure cost - which is the common
|
||||||
// case of looking straight down at a surface.
|
// case of looking straight down at a surface.
|
||||||
float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
|
float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
|
||||||
if (v_dot_n > 0.05 && sweep > 0.5 * min(tile_mm().x, tile_mm().y) * height_tex_texel.x) {
|
if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) {
|
||||||
// A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the
|
// A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the
|
||||||
// undisplaced surface. Start at the top of the shell, where the ray is outside the surface
|
// undisplaced surface. Start at the top of the shell, where the ray is outside the surface
|
||||||
// by construction, and step inward; the crossing is what this pixel actually sees.
|
// by construction, and step inward; the crossing is what this pixel actually sees.
|
||||||
@@ -417,20 +393,17 @@ void main()
|
|||||||
// Central difference, per uv unit (not per texel).
|
// Central difference, per uv unit (not per texel).
|
||||||
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
|
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
|
||||||
|
|
||||||
// uv -> mm is the reciprocal of the tile extents, applied per component below, which is what
|
// uv -> mm is 1/tiling_scale for the triplanar projection, so this turns the uv-space
|
||||||
// turns the uv-space gradient into a real surface slope.
|
// gradient into a real surface slope.
|
||||||
vec2 tile = tile_mm();
|
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.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
|
// uv was rotated by project_uv() while t/b are the unrotated model axes, so rotate the
|
||||||
// gradient back into the axes' frame.
|
// gradient back into the axes' frame.
|
||||||
float cs = cos(rotation_rad);
|
float cs = cos(rotation_rad);
|
||||||
float sn = sin(rotation_rad);
|
float sn = sin(rotation_rad);
|
||||||
// One uv unit is tile.x mm along u but tile.y / tex_aspect mm along v, so each component of
|
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v
|
||||||
// the uv-space gradient carries its own factor before being rotated back into t/b.
|
// component of the gradient carries the extra factor before being rotated back into t/b.
|
||||||
// Each component carries its own extent and its own mirror sign, both of which project_uv()
|
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect);
|
||||||
// applied on the way in; without the sign a mirrored layer would light as though it were not.
|
|
||||||
vec2 ms = mirror_sign();
|
|
||||||
vec2 g = vec2(dh_duv.x * ms.x / tile.x, dh_duv.y * tex_aspect * ms.y / tile.y);
|
|
||||||
// ...and back out of raw-projection units into millimetres of travel along t / b, which is a
|
// ...and back out of raw-projection units into millimetres of travel along t / b, which is a
|
||||||
// no-op except for the spherical longitude (see projection_axes()). After the inverse rotation,
|
// no-op except for the spherical longitude (see projection_axes()). After the inverse rotation,
|
||||||
// because units_per_mm is expressed in the t/b frame rather than in uv.
|
// because units_per_mm is expressed in the t/b frame rather than in uv.
|
||||||
|
|||||||
@@ -25,12 +25,6 @@ uniform bool volume_mirrored;
|
|||||||
uniform int mode; // 0 checker, 1 distortion
|
uniform int mode; // 0 checker, 1 distortion
|
||||||
uniform float checker_freq; // checker squares per uv unit (one uv unit == one texture tile)
|
uniform float checker_freq; // checker squares per uv unit (one uv unit == one texture tile)
|
||||||
uniform float tiling_scale;
|
uniform float tiling_scale;
|
||||||
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - see
|
|
||||||
// layer_tile_mm() on the CPU side, so an unset uniform behaves exactly as this shader always did.
|
|
||||||
uniform float tiling_scale_v;
|
|
||||||
// Mirror along one tile axis; false by default, so an unset uniform is the old behaviour.
|
|
||||||
uniform bool mirror_u;
|
|
||||||
uniform bool mirror_v;
|
|
||||||
uniform vec3 tex_anchor; // the volume's origin in world space
|
uniform vec3 tex_anchor; // the volume's origin in world space
|
||||||
uniform float rotation_rad;
|
uniform float rotation_rad;
|
||||||
uniform vec2 uv_offset;
|
uniform vec2 uv_offset;
|
||||||
@@ -84,30 +78,13 @@ vec2 projection_raw(vec3 p, vec3 n)
|
|||||||
return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
|
return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
|
|
||||||
vec2 tile_mm()
|
|
||||||
{
|
|
||||||
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
|
|
||||||
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
|
|
||||||
}
|
|
||||||
|
|
||||||
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
|
|
||||||
vec2 mirror_sign()
|
|
||||||
{
|
|
||||||
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
vec2 project_uv(vec3 p, vec3 n)
|
vec2 project_uv(vec3 p, vec3 n)
|
||||||
{
|
{
|
||||||
vec2 planar = projection_raw(p, n);
|
vec2 planar = projection_raw(p, n);
|
||||||
|
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||||
float cs = cos(rotation_rad);
|
float cs = cos(rotation_rad);
|
||||||
float sn = sin(rotation_rad);
|
float sn = sin(rotation_rad);
|
||||||
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
||||||
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
|
|
||||||
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
|
|
||||||
// the same division, in the tile's own frame.
|
|
||||||
r *= mirror_sign() / tile_mm();
|
|
||||||
r.y *= tex_aspect; // after the rotation, so the rotation stays a rotation rather than a shear
|
r.y *= tex_aspect; // after the rotation, so the rotation stays a rotation rather than a shear
|
||||||
return r + uv_offset;
|
return r + uv_offset;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1729,8 +1729,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
//plate->pattern_file = it->second->pattern_file;
|
//plate->pattern_file = it->second->pattern_file;
|
||||||
plate->no_light_thumbnail_file = it->second->no_light_thumbnail_file;
|
plate->no_light_thumbnail_file = it->second->no_light_thumbnail_file;
|
||||||
plate->top_file = it->second->top_file;
|
plate->top_file = it->second->top_file;
|
||||||
plate->pick_file = it->second->pick_file.empty();
|
plate->pick_file = it->second->pick_file;
|
||||||
plate->pattern_bbox_file = it->second->pattern_bbox_file.empty();
|
plate->pattern_bbox_file = it->second->pattern_bbox_file;
|
||||||
plate->config = it->second->config;
|
plate->config = it->second->config;
|
||||||
|
|
||||||
if (!plate->thumbnail_file.empty())
|
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_is_role_based_fan_on.fill(false);
|
||||||
m_role_based_fan_marker_layer.fill(-1);
|
m_role_based_fan_marker_layer.fill(-1);
|
||||||
|
|
||||||
m_fan_mover.release();
|
m_fan_mover.reset();
|
||||||
m_ordering_cache.clear();
|
m_ordering_cache.clear();
|
||||||
|
|
||||||
// Belt printer: initialize belt-specific writer via virtual hook.
|
// Belt printer: initialize belt-specific writer via virtual hook.
|
||||||
|
|||||||
@@ -470,10 +470,7 @@ V2Resolution recommend_v2_resolution(const indexed_triangle_set
|
|||||||
const DecodedHeightTexture &tex = decode_height_texture(layer);
|
const DecodedHeightTexture &tex = decode_height_texture(layer);
|
||||||
if (tex.width <= 0)
|
if (tex.width <= 0)
|
||||||
continue;
|
continue;
|
||||||
// Texels are square in both axes (the aspect correction is what keeps them so), so the finer
|
const float texel = layer.tiling_scale / float(tex.width);
|
||||||
// of the two tile extents sets the finest texel the refinement has to resolve.
|
|
||||||
const Vec2f tile = layer_tile_mm(layer);
|
|
||||||
const float texel = std::min(tile.x(), tile.y()) / float(tex.width);
|
|
||||||
if (out.texel_mm <= 0.f || texel < out.texel_mm) {
|
if (out.texel_mm <= 0.f || texel < out.texel_mm) {
|
||||||
out.texel_mm = texel;
|
out.texel_mm = texel;
|
||||||
out.pixels_per_edge = analyze_texture_detail(layer).pixels_per_edge;
|
out.pixels_per_edge = analyze_texture_detail(layer).pixels_per_edge;
|
||||||
@@ -1681,17 +1678,12 @@ namespace {
|
|||||||
// of them was most of the per-sample arithmetic.
|
// of them was most of the per-sample arithmetic.
|
||||||
struct UVTransform
|
struct UVTransform
|
||||||
{
|
{
|
||||||
float inv_u, inv_v, cs, sn, aspect;
|
float scale, cs, sn, aspect;
|
||||||
Vec2f offset;
|
Vec2f offset;
|
||||||
|
|
||||||
UVTransform(const TextureDisplacementLayer &layer, float aspect_)
|
UVTransform(const TextureDisplacementLayer &layer, float aspect_)
|
||||||
{
|
{
|
||||||
const Vec2f tile = layer_tile_mm(layer);
|
scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f;
|
||||||
const Vec2f sign = layer_mirror_sign(layer);
|
|
||||||
// The mirror folds into the reciprocal: it is a negation of the same coordinate, so there is
|
|
||||||
// nothing to do per sample beyond carrying the sign here.
|
|
||||||
inv_u = sign.x() / tile.x();
|
|
||||||
inv_v = sign.y() / tile.y();
|
|
||||||
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
|
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
|
||||||
cs = std::cos(rad);
|
cs = std::cos(rad);
|
||||||
sn = std::sin(rad);
|
sn = std::sin(rad);
|
||||||
@@ -1700,11 +1692,9 @@ struct UVTransform
|
|||||||
}
|
}
|
||||||
Vec2f operator()(const Vec2f &planar) const
|
Vec2f operator()(const Vec2f &planar) const
|
||||||
{
|
{
|
||||||
// See apply_uv_transform() for why both the tile extents and the aspect correction follow
|
const Vec2f scaled = planar * scale;
|
||||||
// the rotation rather than preceding it.
|
Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
|
||||||
Vec2f rotated(planar.x() * cs - planar.y() * sn, planar.x() * sn + planar.y() * cs);
|
// See apply_uv_transform() for why the aspect correction follows the rotation.
|
||||||
rotated.x() *= inv_u;
|
|
||||||
rotated.y() *= inv_v;
|
|
||||||
if (aspect > 0.f && aspect != 1.f)
|
if (aspect > 0.f && aspect != 1.f)
|
||||||
rotated.y() *= aspect;
|
rotated.y() *= aspect;
|
||||||
return rotated + offset;
|
return rotated + offset;
|
||||||
@@ -1724,32 +1714,24 @@ inline Vec3f triplanar_weights(const Vec3f &normal)
|
|||||||
|
|
||||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect)
|
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect)
|
||||||
{
|
{
|
||||||
|
const float scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f;
|
||||||
|
const Vec2f scaled = planar * scale;
|
||||||
|
|
||||||
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
|
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
|
||||||
const float cs = std::cos(rad);
|
const float cs = std::cos(rad);
|
||||||
const float sn = std::sin(rad);
|
const float sn = std::sin(rad);
|
||||||
Vec2f rotated(planar.x() * cs - planar.y() * sn, planar.x() * sn + planar.y() * cs);
|
Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
|
||||||
|
|
||||||
// The tile extents. Dividing here rather than before the rotation is what makes a non-square
|
|
||||||
// tile a rotated *rectangle* on the surface: scaling the two axes of an already-rotated
|
|
||||||
// coordinate by different amounts would be a shear, so the rotation has to come first and the
|
|
||||||
// extents divide in the tile's own frame. With a square tile the two orders agree exactly,
|
|
||||||
// R being linear - which is why this is not a change in behaviour for any existing layer.
|
|
||||||
//
|
|
||||||
// The mirror rides along as a sign on the same division. It is in the tile's own frame too, so
|
|
||||||
// it mirrors along the tile's axes rather than the world's, and because the offset is what
|
|
||||||
// positions the tile, mirroring about the tile's middle is a matter of the offset the gizmo
|
|
||||||
// writes alongside it, not of anything here.
|
|
||||||
const Vec2f tile = layer_tile_mm(layer);
|
|
||||||
const Vec2f sign = layer_mirror_sign(layer);
|
|
||||||
rotated.x() *= sign.x() / tile.x();
|
|
||||||
rotated.y() *= sign.y() / tile.y();
|
|
||||||
|
|
||||||
// Non-square textures. Without this the [0,1] square of uv covers the whole image whatever its
|
// Non-square textures. Without this the [0,1] square of uv covers the whole image whatever its
|
||||||
// proportions, so a 2:1 image is squeezed into one tile and every feature in it comes out half
|
// proportions, so a 2:1 image is squeezed into a square tile and every feature in it comes out
|
||||||
// as wide as it should be. The tile is `layer_tile_mm().y() * height / width` mm along v, which
|
// half as wide as it should be. `tiling_scale` is the tile's size along u; the tile is
|
||||||
// is exactly what keeps texels square - so dividing v by that extent is the same as multiplying
|
// `tiling_scale * height / width` mm along v, which is exactly what keeps texels square - so
|
||||||
// it by width / height. A square texture has aspect 1 and is untouched, which is why this
|
// dividing v by that extent is the same as multiplying it by width / height. A square texture has
|
||||||
// changes nothing for the shipped library.
|
// aspect 1 and is untouched, which is why this changes nothing for the shipped library.
|
||||||
|
//
|
||||||
|
// Applied after the rotation, not before: scaling one axis of an already-rotated coordinate is a
|
||||||
|
// shear, and doing it the other way round would make "Rotation" skew the pattern instead of
|
||||||
|
// turning it.
|
||||||
if (aspect > 0.f && aspect != 1.f)
|
if (aspect > 0.f && aspect != 1.f)
|
||||||
rotated.y() *= aspect;
|
rotated.y() *= aspect;
|
||||||
|
|
||||||
|
|||||||
@@ -177,17 +177,7 @@ struct TextureDisplacementLayer
|
|||||||
std::shared_ptr<std::vector<unsigned char>> image_data;
|
std::shared_ptr<std::vector<unsigned char>> image_data;
|
||||||
|
|
||||||
float depth_mm = 0.4f; // maximum displacement along the surface normal, in mm
|
float depth_mm = 0.4f; // maximum displacement along the surface normal, in mm
|
||||||
float tiling_scale = 12.5f; // size of one texture tile along u, in mm
|
float tiling_scale = 12.5f; // size of one texture tile, in mm
|
||||||
// Size of one tile along v, in mm. Anything <= 0 means "whatever tiling_scale is", which is both
|
|
||||||
// the default and what a layer created before the tile could be non-square meant - so an older
|
|
||||||
// undo snapshot still comes back as the square tile it was.
|
|
||||||
float tiling_scale_v = -1.f;
|
|
||||||
// Mirror the texture along one axis of the tile. Set by dragging a width or height handle back
|
|
||||||
// through the tile's middle, which is how a side handle behaves in an image editor: the extent
|
|
||||||
// cannot go negative, so the sign lives here instead. Both default to off, which is what every
|
|
||||||
// layer written before this meant.
|
|
||||||
bool mirror_u = false;
|
|
||||||
bool mirror_v = false;
|
|
||||||
float rotation_deg = 0.f;
|
float rotation_deg = 0.f;
|
||||||
Vec2f offset = Vec2f::Zero();
|
Vec2f offset = Vec2f::Zero();
|
||||||
bool invert = false;
|
bool invert = false;
|
||||||
@@ -312,8 +302,7 @@ struct TextureDisplacementLayer
|
|||||||
static_cast<int>(tile_method), static_cast<int>(projection_method), lscm_seam_angle_deg, islands,
|
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,
|
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,
|
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, tiling_scale_v,
|
lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled);
|
||||||
mirror_u, mirror_v);
|
|
||||||
}
|
}
|
||||||
template<class Archive> void load(Archive &ar)
|
template<class Archive> void load(Archive &ar)
|
||||||
{
|
{
|
||||||
@@ -325,7 +314,7 @@ struct TextureDisplacementLayer
|
|||||||
tile_method_int, projection_method_int, lscm_seam_angle_deg, islands, blend_mode_int, midlevel,
|
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,
|
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,
|
edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective,
|
||||||
view_project_matrix, color_enabled, tiling_scale_v, mirror_u, mirror_v);
|
view_project_matrix, color_enabled);
|
||||||
image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end());
|
image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end());
|
||||||
tile_method = static_cast<TextureTileMethod>(tile_method_int);
|
tile_method = static_cast<TextureTileMethod>(tile_method_int);
|
||||||
projection_method = static_cast<TextureProjectionMethod>(projection_method_int);
|
projection_method = static_cast<TextureProjectionMethod>(projection_method_int);
|
||||||
@@ -334,25 +323,6 @@ struct TextureDisplacementLayer
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// The layer's tile extents in mm, u first. The v extent falls back to the u one whenever
|
|
||||||
// tiling_scale_v is unset, so "unset means square" is written down once and every consumer - the
|
|
||||||
// bake, the preview shaders, the UV pane and the placement gizmo - agrees on what a layer with no
|
|
||||||
// v extent means.
|
|
||||||
inline Vec2f layer_tile_mm(const TextureDisplacementLayer &layer)
|
|
||||||
{
|
|
||||||
const float su = (layer.tiling_scale > 1e-6f) ? layer.tiling_scale : 1.f;
|
|
||||||
return Vec2f(su, (layer.tiling_scale_v > 1e-6f) ? layer.tiling_scale_v : su);
|
|
||||||
}
|
|
||||||
|
|
||||||
// +1 or -1 per axis, for the layer's mirroring. A separate factor from the extents rather than a
|
|
||||||
// signed extent: the extents are a size the panel shows and the refinement measures texels against,
|
|
||||||
// and letting one of them go negative would mean every one of those places had to remember to take
|
|
||||||
// the magnitude.
|
|
||||||
inline Vec2f layer_mirror_sign(const TextureDisplacementLayer &layer)
|
|
||||||
{
|
|
||||||
return Vec2f(layer.mirror_u ? -1.f : 1.f, layer.mirror_v ? -1.f : 1.f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Settings that apply to the whole layer stack rather than to one layer, held per ModelVolume next
|
// Settings that apply to the whole layer stack rather than to one layer, held per ModelVolume next
|
||||||
// to texture_displacement_layers and consumed by build_texture_displacement().
|
// to texture_displacement_layers and consumed by build_texture_displacement().
|
||||||
struct TextureDisplacementOptions
|
struct TextureDisplacementOptions
|
||||||
@@ -568,13 +538,9 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
|
|||||||
// projection method, without going through project_texture_displacement_uv()'s own dispatch
|
// projection method, without going through project_texture_displacement_uv()'s own dispatch
|
||||||
// (which only knows how to compute the *analytic* methods from a single vertex + normal).
|
// (which only knows how to compute the *analytic* methods from a single vertex + normal).
|
||||||
// `aspect` is the height map's width / height. It scales the v axis so a non-square image is not
|
// `aspect` is the height map's width / height. It scales the v axis so a non-square image is not
|
||||||
// squeezed into a square tile: the tile is `layer_tile_mm().y() * height / width` mm along v, which
|
// squeezed into a square tile: `tiling_scale` is the tile's size along u, and the tile is
|
||||||
// keeps texels square. 1 (the default) is the square case and leaves the coordinate exactly as it
|
// `tiling_scale * height / width` mm along v, which keeps texels square. 1 (the default) is the
|
||||||
// always was.
|
// square case and leaves the coordinate exactly as it always was.
|
||||||
//
|
|
||||||
// The tile extents divide *after* the rotation, not before. With one extent that made no difference
|
|
||||||
// (R is linear, so R(p / s) == R(p) / s), but with two it is what keeps a non-square tile a rotated
|
|
||||||
// rectangle on the surface instead of a sheared parallelogram.
|
|
||||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f);
|
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f);
|
||||||
|
|
||||||
// Applies a row-major 3x4 projective matrix (see TextureDisplacementLayer::view_project_matrix) to a
|
// Applies a row-major 3x4 projective matrix (see TextureDisplacementLayer::view_project_matrix) to a
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include <wx/checklst.h>
|
#include <wx/checklst.h>
|
||||||
#include "slic3r/GUI/GUI_Utils.hpp"
|
#include "slic3r/GUI/GUI_Utils.hpp"
|
||||||
#include "slic3r/GUI/Widgets/Button.hpp"
|
#include "slic3r/GUI/Widgets/Button.hpp"
|
||||||
|
#include "slic3r/GUI/Widgets/DropDown.hpp"
|
||||||
#include "slic3r/GUI/Widgets/Label.hpp"
|
#include "slic3r/GUI/Widgets/Label.hpp"
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include "slic3r/GUI/wxExtensions.hpp"
|
#include "slic3r/GUI/wxExtensions.hpp"
|
||||||
@@ -172,7 +173,26 @@ void AMSMaterialsSetting::create()
|
|||||||
|
|
||||||
Bind(wxEVT_PAINT, &AMSMaterialsSetting::paintEvent, this);
|
Bind(wxEVT_PAINT, &AMSMaterialsSetting::paintEvent, this);
|
||||||
Bind(EVT_SELECTED_COLOR, &AMSMaterialsSetting::on_picker_color, 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);
|
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);
|
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);
|
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);
|
m_sizer_filament->Add(m_comboBox_filament, 1, wxALIGN_CENTER, 0);
|
||||||
|
|
||||||
@@ -420,6 +438,7 @@ void AMSMaterialsSetting::paintEvent(wxPaintEvent &evt)
|
|||||||
AMSMaterialsSetting::~AMSMaterialsSetting()
|
AMSMaterialsSetting::~AMSMaterialsSetting()
|
||||||
{
|
{
|
||||||
m_comboBox_filament->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
|
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);
|
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;
|
std::string selected_ams_id;
|
||||||
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
|
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
|
||||||
if (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++) {
|
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
|
||||||
auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
|
if (selected_info && it->filament_id.compare(selected_info->filament_id) == 0) {
|
||||||
std::string filament_id = filament_item.filament_id;
|
|
||||||
if (it->filament_id.compare(filament_id) == 0) {
|
|
||||||
selected_ams_id = it->filament_id;
|
selected_ams_id = it->filament_id;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -682,12 +700,13 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
|
|||||||
ams_setting_id = "";
|
ams_setting_id = "";
|
||||||
|
|
||||||
// the combobox item
|
// 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)
|
// check filament info (rack-gated per-nozzle blacklist evaluation)
|
||||||
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
|
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
|
||||||
const auto& fila_check_res = sCheckFilamentInfo(preset_bundle, obj, ams_id, slot_id,
|
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()) {
|
if (const auto& prohibit_items = fila_check_res.get_items_by_action("prohibition"); !prohibit_items.empty()) {
|
||||||
wxString info_msg;
|
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();
|
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)
|
void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_min, wxString temp_max, wxString k, wxString n)
|
||||||
{
|
{
|
||||||
if (!obj) return;
|
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_k_val->GetTextCtrl()->SetValue(k);
|
||||||
m_input_n_val->GetTextCtrl()->SetValue(n);
|
m_input_n_val->GetTextCtrl()->SetValue(n);
|
||||||
|
|
||||||
wxArrayString filament_items;
|
|
||||||
wxString bambu_filament_name;
|
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_vendors;// some information for sort
|
||||||
std::unordered_map<wxString, wxString> query_filament_types; //
|
std::unordered_map<wxString, wxString> query_filament_types; //
|
||||||
|
|
||||||
std::set<std::string> filament_id_set;
|
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
|
// 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.
|
// popup (a different printer, a different nozzle) would resolve to that printer's filament_id.
|
||||||
map_filament_items.clear();
|
map_filament_items.clear();
|
||||||
@@ -1035,7 +1180,7 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
|||||||
if (alias.empty())
|
if (alias.empty())
|
||||||
continue;
|
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);
|
_collect_filament_info(alias, *filament_it, query_filament_vendors, query_filament_types);
|
||||||
|
|
||||||
FilamentInfos filament_infos;
|
FilamentInfos filament_infos;
|
||||||
@@ -1044,7 +1189,6 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
|||||||
map_filament_items[alias] = filament_infos;
|
map_filament_items[alias] = filament_infos;
|
||||||
|
|
||||||
if (filament_it->filament_id == ams_filament_id) {
|
if (filament_it->filament_id == ams_filament_id) {
|
||||||
hint_filament_name = from_u8(alias);
|
|
||||||
bambu_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;
|
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
|
rebuild_filament_dropdown_items(ams_filament_id);
|
||||||
int selection_idx = -1;
|
int selection_idx = m_comboBox_filament->GetSelection();
|
||||||
{
|
|
||||||
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);
|
|
||||||
post_select_event(selection_idx);
|
post_select_event(selection_idx);
|
||||||
|
|
||||||
if (selection_idx < 0) {
|
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());
|
int* from_printer = static_cast<int*>(m_comboBox_filament->GetClientData());
|
||||||
|
|
||||||
m_filament_type = "";
|
m_filament_type = "";
|
||||||
|
const FilamentInfos* selected_info = selected_filament_info();
|
||||||
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
|
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
|
||||||
if (preset_bundle) {
|
if (preset_bundle) {
|
||||||
std::ostringstream stream;
|
std::ostringstream stream;
|
||||||
@@ -1309,9 +1441,8 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
|||||||
std::string nozzle_diameter_str = stream.str();
|
std::string nozzle_diameter_str = stream.str();
|
||||||
// Resolve the selection against the same list Popup() built the dropdown from, so the two
|
// 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.
|
// halves of the dialog cannot disagree about which filaments this machine can use.
|
||||||
const std::string selected = m_comboBox_filament->GetValue().ToStdString();
|
if (selected_info) {
|
||||||
if (!selected.empty()) {
|
const std::string& filament_id = selected_info->filament_id;
|
||||||
const std::string filament_id = map_filament_items[selected].filament_id;
|
|
||||||
for (Preset *it : preset_bundle->get_filament_presets_for_machine(
|
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)) {
|
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
|
||||||
if (it->filament_id != filament_id)
|
if (it->filament_id != filament_id)
|
||||||
@@ -1364,7 +1495,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
|||||||
//reset cali
|
//reset cali
|
||||||
int cali_select_idx = -1;
|
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_k_val->Enable(false);
|
||||||
m_input_n_val->Enable(false);
|
m_input_n_val->Enable(false);
|
||||||
m_button_confirm->Disable(); // ORCA No need to change style
|
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_filament_id = "";
|
||||||
ams_setting_id = "";
|
ams_setting_id = "";
|
||||||
|
|
||||||
if (preset_bundle) {
|
if (selected_info) {
|
||||||
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
|
ams_filament_id = selected_info->filament_id;
|
||||||
auto itor = map_filament_items.find(m_comboBox_filament->GetValue().ToStdString());
|
ams_setting_id = selected_info->setting_id;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wxArrayString items;
|
wxArrayString items;
|
||||||
@@ -1529,6 +1648,11 @@ void AMSMaterialsSetting::on_dpi_changed(const wxRect &suggested_rect)
|
|||||||
m_button_reset->Rescale(); // ORCA
|
m_button_reset->Rescale(); // ORCA
|
||||||
m_button_confirm->Rescale(); // ORCA
|
m_button_confirm->Rescale(); // ORCA
|
||||||
m_button_close->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();
|
this->Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
#include "slic3r/GUI/Widgets/StateColor.hpp"
|
#include "slic3r/GUI/Widgets/StateColor.hpp"
|
||||||
#include <wx/panel.h>
|
#include <wx/panel.h>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
#define AMS_MATERIALS_SETTING_DEF_COLOUR wxColour(255, 255, 255)
|
#define AMS_MATERIALS_SETTING_DEF_COLOUR wxColour(255, 255, 255)
|
||||||
#define AMS_MATERIALS_SETTING_GREY900 wxColour(38, 46, 48)
|
#define AMS_MATERIALS_SETTING_GREY900 wxColour(38, 46, 48)
|
||||||
@@ -156,6 +157,7 @@ protected:
|
|||||||
void create_panel_kn(wxWindow* parent);
|
void create_panel_kn(wxWindow* parent);
|
||||||
void on_dpi_changed(const wxRect &suggested_rect) override;
|
void on_dpi_changed(const wxRect &suggested_rect) override;
|
||||||
void on_select_filament(wxCommandEvent& evt);
|
void on_select_filament(wxCommandEvent& evt);
|
||||||
|
void on_toggle_favorite_filament(wxCommandEvent& evt);
|
||||||
void on_select_cali_result(wxCommandEvent &evt);
|
void on_select_cali_result(wxCommandEvent &evt);
|
||||||
void on_select_ok(wxCommandEvent &event);
|
void on_select_ok(wxCommandEvent &event);
|
||||||
void on_select_reset(wxCommandEvent &event);
|
void on_select_reset(wxCommandEvent &event);
|
||||||
@@ -163,6 +165,11 @@ protected:
|
|||||||
void on_clr_picker(wxMouseEvent &event);
|
void on_clr_picker(wxMouseEvent &event);
|
||||||
bool is_virtual_tray();
|
bool is_virtual_tray();
|
||||||
void update_widgets();
|
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);
|
void update_filament_editing(bool is_printing);
|
||||||
// Orca: the variant index of the filament's per-variant options on the nozzle this tray feeds
|
// 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;
|
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;
|
ComboBox *m_comboBox_filament;
|
||||||
#endif
|
|
||||||
ComboBox * m_comboBox_cali_result;
|
ComboBox * m_comboBox_cali_result;
|
||||||
TextInput* m_readonly_filament;
|
TextInput* m_readonly_filament;
|
||||||
|
|
||||||
@@ -215,7 +217,20 @@ protected:
|
|||||||
std::string filament_id;
|
std::string filament_id;
|
||||||
std::string setting_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);
|
wxDECLARE_EVENT(EVT_SELECTED_COLOR, wxCommandEvent);
|
||||||
|
|||||||
@@ -25,8 +25,9 @@ DockPanel::DockPanel(wxWindow* parent,
|
|||||||
const std::string& html,
|
const std::string& html,
|
||||||
MessageHandler on_message,
|
MessageHandler on_message,
|
||||||
CloseHandler on_close,
|
CloseHandler on_close,
|
||||||
CloseHandler on_destroyed)
|
CloseHandler on_destroyed,
|
||||||
: WebPanel(parent, web_hosting::orca_bridge_script())
|
const wxString& profile)
|
||||||
|
: WebPanel(parent, web_hosting::orca_bridge_script(), profile)
|
||||||
, m_html(html)
|
, m_html(html)
|
||||||
, m_on_message(std::move(on_message))
|
, m_on_message(std::move(on_message))
|
||||||
, m_on_close(std::move(on_close))
|
, m_on_close(std::move(on_close))
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include <wx/string.h>
|
||||||
|
|
||||||
class wxWindow;
|
class wxWindow;
|
||||||
|
|
||||||
namespace Slic3r { namespace GUI {
|
namespace Slic3r { namespace GUI {
|
||||||
@@ -27,7 +29,8 @@ public:
|
|||||||
const std::string& html,
|
const std::string& html,
|
||||||
MessageHandler on_message,
|
MessageHandler on_message,
|
||||||
CloseHandler on_close,
|
CloseHandler on_close,
|
||||||
CloseHandler on_destroyed);
|
CloseHandler on_destroyed,
|
||||||
|
const wxString& profile = wxEmptyString);
|
||||||
~DockPanel() override;
|
~DockPanel() override;
|
||||||
|
|
||||||
// Main thread only.
|
// Main thread only.
|
||||||
|
|||||||
@@ -3232,9 +3232,16 @@ bool GUI_App::on_init_inner()
|
|||||||
#ifdef _MSW_DARK_MODE
|
#ifdef _MSW_DARK_MODE
|
||||||
|
|
||||||
#ifndef __WINDOWS__
|
#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();
|
wxSystemAppearance app = wxSystemSettings::GetAppearance();
|
||||||
GUI::wxGetApp().app_config->set("dark_color_mode", app.IsDark() ? "1" : "0");
|
GUI::wxGetApp().app_config->set("dark_color_mode", app.IsDark() ? "1" : "0");
|
||||||
GUI::wxGetApp().app_config->save();
|
GUI::wxGetApp().app_config->save();
|
||||||
|
if (dark_mode() != saved_dark_mode) {
|
||||||
|
init_label_colours();
|
||||||
|
Update_dark_mode_flag();
|
||||||
|
}
|
||||||
#endif // __APPLE__
|
#endif // __APPLE__
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1617,7 +1617,7 @@ void ObjectList::list_manipulation(const wxPoint& mouse_pos, bool evt_context_me
|
|||||||
int obj_idx, vol_idx;
|
int obj_idx, vol_idx;
|
||||||
get_selected_item_indexes(obj_idx, vol_idx, item);
|
get_selected_item_indexes(obj_idx, vol_idx, item);
|
||||||
if (obj_idx != -1) {
|
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();
|
(*m_objects)[obj_idx]->ensure_on_bed();
|
||||||
cnv->reload_scene(true, true);
|
cnv->reload_scene(true, true);
|
||||||
update_info_items(obj_idx);
|
update_info_items(obj_idx);
|
||||||
@@ -2779,12 +2779,13 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type)
|
|||||||
GLCanvas3D* cnv = plater->canvas3D();
|
GLCanvas3D* cnv = plater->canvas3D();
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case InfoItemType::CustomSupports:
|
case InfoItemType::CustomSupports: {
|
||||||
cnv->get_gizmos_manager().reset_all_states();
|
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)
|
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
|
||||||
mv->supported_facets.reset();
|
mv->supported_facets.reset();
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// BBS: remove CustomSeam
|
// BBS: remove CustomSeam
|
||||||
|
|
||||||
@@ -2795,19 +2796,21 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case InfoItemType::MmSegmentation:
|
case InfoItemType::MmSegmentation: {
|
||||||
cnv->get_gizmos_manager().reset_all_states();
|
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)
|
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
|
||||||
mv->mmu_segmentation_facets.reset();
|
mv->mmu_segmentation_facets.reset();
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case InfoItemType::FuzzySkin:
|
case InfoItemType::FuzzySkin: {
|
||||||
cnv->get_gizmos_manager().reset_all_states();
|
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)
|
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
|
||||||
mv->fuzzy_skin_facets.reset();
|
mv->fuzzy_skin_facets.reset();
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// BBS: remove Sinking
|
// BBS: remove Sinking
|
||||||
case InfoItemType::Undef : assert(false); break;
|
case InfoItemType::Undef : assert(false); break;
|
||||||
@@ -6718,7 +6721,7 @@ void ObjectList::OnEditingStarted(wxDataViewEvent &event)
|
|||||||
else if (col == colSinking) {
|
else if (col == colSinking) {
|
||||||
Plater * plater = wxGetApp().plater();
|
Plater * plater = wxGetApp().plater();
|
||||||
GLCanvas3D *cnv = plater->canvas3D();
|
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;
|
int obj_idx, vol_idx;
|
||||||
get_selected_item_indexes(obj_idx, vol_idx, item);
|
get_selected_item_indexes(obj_idx, vol_idx, item);
|
||||||
(*m_objects)[obj_idx]->ensure_on_bed();
|
(*m_objects)[obj_idx]->ensure_on_bed();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -405,99 +405,20 @@ private:
|
|||||||
// Panel state only, not saved with the project.
|
// Panel state only, not saved with the project.
|
||||||
std::array<bool, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_layer_expanded{};
|
std::array<bool, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_layer_expanded{};
|
||||||
|
|
||||||
// "Adjust Texture" mode: instead of painting, the active layer's placement is a control lying
|
// "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active
|
||||||
// on the model. The gizmo is one tile of the texture in the paint patch's own tangent plane:
|
// layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane
|
||||||
// where it sits is the layer's offset, how it is turned is rotation_deg, how big it is is the
|
// (a "pan" - drag anywhere on it for free 2D movement), plus two arrows along the patch's
|
||||||
// tile extents - so each handle is one of the values it edits rather than a stand-in for it.
|
// own U/V axes that constrain the drag to just that one axis for precise nudging. Anchored to
|
||||||
// Anchored to the centroid/average-normal of the active layer's paint patch (see
|
// the centroid/average-normal of the active layer's current paint patch (see
|
||||||
// libslic3r::compute_layer_paint_anchor()), so nothing is drawn until something is painted.
|
// libslic3r::compute_layer_paint_anchor()), so nothing is drawn if it has nothing painted yet.
|
||||||
//
|
//
|
||||||
// Drawn through ImGui's foreground draw list rather than the "flat" shader, which is what gives
|
// NOTE: the drag direction/sign below is this session's best-effort reasoning about which way
|
||||||
// it antialiased sub-pixel strokes, rounded joins, a contrast halo and the numeric readout. The
|
// the texture should appear to move as the handle is dragged - it could not be visually
|
||||||
// cost is that render_adjust_texture_gizmo() has to be called from inside the gizmo's ImGui
|
// confirmed while writing it (no way to render/see pixels in this environment), so it may
|
||||||
// frame (see on_render_input_window()), not from on_render() with the rest of the GL drawing.
|
// need a one-line sign flip once actually tested.
|
||||||
// Move is the whole interior, free in both axes. AxisMove is the four arrows, each holding a
|
|
||||||
// move to one of the tile's axes. EdgeScale is the four edge bars, one tile extent each, and
|
|
||||||
// CornerScale the four corner grips, which take both extents at once.
|
|
||||||
//
|
|
||||||
// The arrows and the bars share the same four rays out of the tile, so they are kept apart by
|
|
||||||
// distance rather than direction: the bar hugs its edge, then a clear gap, then the arrow well
|
|
||||||
// outside it. adjust_pick() leans on that - see the order it tests them in.
|
|
||||||
enum class AdjustHandle { None, Move, AxisMove, EdgeScale, CornerScale, Rotate };
|
|
||||||
|
|
||||||
// The gizmo's geometry for one frame, in screen pixels. Worked out once and shared by the
|
|
||||||
// drawing and the hit test, so the two cannot disagree about where a handle is. Everything
|
|
||||||
// except the frame itself is a fixed number of pixels, which is what keeps the control equally
|
|
||||||
// usable on a 5 mm part and a 500 mm one - the old handle scaled with the volume's bounding box
|
|
||||||
// and so was unusable at both ends.
|
|
||||||
struct AdjustFrame
|
|
||||||
{
|
|
||||||
bool valid = false;
|
|
||||||
Vec2d center = Vec2d::Zero();
|
|
||||||
// +u+v, +u-v, -u-v, -u+v. A scale grip sits on each.
|
|
||||||
std::array<Vec2d, 4> corner{};
|
|
||||||
// +u, -u, +v, -v: the middle of each edge, and the two ends of the bar that sits just
|
|
||||||
// outside it. The bar's ends are kept rather than recomputed so the drawing and the hit test
|
|
||||||
// cannot end up with it in two different places.
|
|
||||||
std::array<Vec2d, 4> edge_mid{};
|
|
||||||
std::array<Vec2d, 4> bar_a{};
|
|
||||||
std::array<Vec2d, 4> bar_b{};
|
|
||||||
// The axis arrow further out on the same ray: where its shaft starts and where its chevron
|
|
||||||
// ends. Kept, like the bar's ends, so the drawing and the hit test cannot disagree.
|
|
||||||
std::array<Vec2d, 4> arm_base{};
|
|
||||||
std::array<Vec2d, 4> arm_tip{};
|
|
||||||
std::array<Vec2d, 4> arm_dir{}; // unit, pointing outward from the centre
|
|
||||||
Vec2d rotate_at = Vec2d::Zero();
|
|
||||||
// The tile projects to less than ADJUST_MIN_TILE_PX across, so the frame was drawn at that
|
|
||||||
// floor instead. It is dashed to say so, and the readout still gives the true millimetres.
|
|
||||||
bool clamped = false;
|
|
||||||
// What the frame was built from, in mesh-local mm: the tile's own (rotated) edge directions
|
|
||||||
// and its extents. The drag arithmetic reuses these rather than recomputing them, so it
|
|
||||||
// cannot end up working against a different tile than the one on screen.
|
|
||||||
Vec3f e_u = Vec3f::UnitX(), e_v = Vec3f::UnitY();
|
|
||||||
Vec2f tile_mm = Vec2f::Ones(); // (u, v) as stored on the layer
|
|
||||||
Vec2f half_planar = Vec2f::Ones(); // half extents in planar mm, v already aspect-corrected
|
|
||||||
Vec2f center_planar = Vec2f::Zero();
|
|
||||||
};
|
|
||||||
|
|
||||||
bool update_adjust_anchor(); // recomputes m_adjust_anchor_pos/normal; false if nothing painted
|
bool update_adjust_anchor(); // recomputes m_adjust_anchor_pos/normal; false if nothing painted
|
||||||
// Drops every scrap of live gizmo state. Called from each of the several ways out of the mode -
|
|
||||||
// the panel's toggle, collapsing the card, switching to seam marking, closing the gizmo - so a
|
|
||||||
// stale hover highlight or a half-finished drag cannot survive into the next time it is opened.
|
|
||||||
void end_adjust_texture_mode();
|
|
||||||
bool on_mouse_adjust_texture(const wxMouseEvent &mouse_event);
|
bool on_mouse_adjust_texture(const wxMouseEvent &mouse_event);
|
||||||
void render_adjust_texture_gizmo();
|
void render_adjust_texture_gizmo();
|
||||||
// This frame's gizmo geometry, or an invalid frame when there is nothing to draw.
|
|
||||||
AdjustFrame adjust_frame() const;
|
|
||||||
// Which handle is under `mouse`, with `index` set to the arm or corner it belongs to. Tested
|
|
||||||
// outermost first; see the hit-area board of the design for the order and the radii.
|
|
||||||
AdjustHandle adjust_pick(const Vec2d &mouse, const AdjustFrame &f, int &index) const;
|
|
||||||
|
|
||||||
// Where the tile's centre sits, in planar mm relative to the anchor, for a layer's placement.
|
|
||||||
// Not the patch centroid: the gizmo *is* the texture's placement, so it has to travel as the
|
|
||||||
// offset changes - pinning it to the centroid is what made the old handle look broken, the
|
|
||||||
// texture sliding while the handle stayed put.
|
|
||||||
static Vec2f adjust_handle_planar(const TextureDisplacementLayer &layer, float aspect);
|
|
||||||
// The exact inverse: the offset that puts the tile's centre at `planar`. Rotating or resizing
|
|
||||||
// the tile changes where a given offset places it, so every drag recomputes the offset from the
|
|
||||||
// centre it wants to keep - which is what makes rotate and scale act *in place* rather than
|
|
||||||
// swinging the tile around the anchor.
|
|
||||||
static Vec2f adjust_offset_for_planar(const Vec2f &planar, float rot_deg, const Vec2f &tile_mm,
|
|
||||||
const Vec2f &mirror_sign, float aspect);
|
|
||||||
// The mesh-local-to-world transform of the selected instance, the (unrotated) tangent basis and
|
|
||||||
// the lifted anchor the whole gizmo is built on. False when there is nothing to build on - which
|
|
||||||
// is the single place that decides the gizmo is not drawable, so the drawing, the hit test and
|
|
||||||
// the drag cannot disagree about it.
|
|
||||||
bool adjust_basis(Transform3d &trafo, Vec3f &u_axis, Vec3f &v_axis, Vec3f &base) const;
|
|
||||||
// Screen-space quad of a tile with the given placement. Used for the pre-drag ghost, which is
|
|
||||||
// drawn from the captured start placement rather than from the layer, the layer having already
|
|
||||||
// been moved by then.
|
|
||||||
bool adjust_quad_for(const Vec2f ¢er_planar, float rot_deg, const Vec2f &tile_mm,
|
|
||||||
std::array<Vec2d, 4> &out) const;
|
|
||||||
// Where the cursor meets the drag plane, in planar mm relative to the anchor. The plane is
|
|
||||||
// deliberately the *fixed* anchor plane rather than the tile's current position: measuring a
|
|
||||||
// drag against a plane the drag itself moves is a feedback loop.
|
|
||||||
bool adjust_planar_under(const Vec2d &mouse, Vec2f &planar) const;
|
|
||||||
// Draws a small '+'/'-' next to the mouse over the 3D view while painting/selecting, so it is
|
// Draws a small '+'/'-' next to the mouse over the 3D view while painting/selecting, so it is
|
||||||
// obvious whether the next stroke adds paint (default) or erases it (Shift). Uses ImGui's
|
// obvious whether the next stroke adds paint (default) or erases it (Shift). Uses ImGui's
|
||||||
// foreground draw list, so it must be called from inside the gizmo's ImGui frame.
|
// foreground draw list, so it must be called from inside the gizmo's ImGui frame.
|
||||||
@@ -1072,35 +993,20 @@ private:
|
|||||||
Vec3f m_adjust_anchor_pos = Vec3f::Zero(); // mesh-local
|
Vec3f m_adjust_anchor_pos = Vec3f::Zero(); // mesh-local
|
||||||
Vec3f m_adjust_anchor_normal = Vec3f::UnitZ(); // mesh-local
|
Vec3f m_adjust_anchor_normal = Vec3f::UnitZ(); // mesh-local
|
||||||
|
|
||||||
// What the cursor is over, and what is being dragged. `index` picks the arm or the corner
|
// Pan: free drag anywhere on the flat panel, moves offset along both axes. AxisU/AxisV: drag
|
||||||
// (0..3, in AdjustFrame's order); it is -1 for Move and Rotate, which have only one of each.
|
// the corresponding arrow, moves offset along only that one axis.
|
||||||
// Only ever one of each is set, so two handles can never both look live.
|
enum class AdjustHandle { None, Pan, AxisU, AxisV };
|
||||||
AdjustHandle m_adjust_hover_kind = AdjustHandle::None;
|
AdjustHandle m_adjust_drag_handle = AdjustHandle::None;
|
||||||
int m_adjust_hover_index = -1;
|
Vec2f m_adjust_drag_start_offset = Vec2f::Zero();
|
||||||
AdjustHandle m_adjust_drag_kind = AdjustHandle::None;
|
// Anchor-relative planar position (see project_planar()) of the point under the mouse at the
|
||||||
int m_adjust_drag_index = -1;
|
// moment the current drag started; every subsequent frame's delta is measured against this,
|
||||||
|
// rather than accumulated frame-to-frame, to avoid drift.
|
||||||
|
Vec2f m_adjust_drag_start_planar = Vec2f::Zero();
|
||||||
|
|
||||||
// Everything the live drag is measured against, captured once on mouse-down. Every frame's
|
// Lazily-built unit quad (the pan panel) and unit line-with-arrowhead (reused, rotated, for
|
||||||
// delta is taken from these rather than accumulated frame to frame, so a long drag cannot
|
// both the U and V axis arrows), transformed into place at render time.
|
||||||
// drift away from the cursor.
|
GLModel m_adjust_panel_glmodel;
|
||||||
Vec2f m_adjust_drag_start_center = Vec2f::Zero(); // tile centre, planar mm
|
GLModel m_adjust_arrow_glmodel;
|
||||||
Vec2f m_adjust_drag_start_grab = Vec2f::Zero(); // the grabbed point, planar mm
|
|
||||||
// How far the grab landed from the handle's own position, in planar mm. A scale drag reads the
|
|
||||||
// extents straight off the cursor, so without subtracting this the tile would jump by however
|
|
||||||
// far inside the generous hit radius the click happened to fall.
|
|
||||||
Vec2f m_adjust_drag_grab_bias = Vec2f::Zero();
|
|
||||||
float m_adjust_drag_start_rot = 0.f;
|
|
||||||
Vec2f m_adjust_drag_start_tile = Vec2f::Ones(); // (u, v) extents as stored
|
|
||||||
float m_adjust_drag_start_angle = 0.f; // planar angle of the grab, radians
|
|
||||||
// The mirroring the layer had when the drag began. A width or height handle dragged back through
|
|
||||||
// the tile's middle toggles its axis against this, rather than against whatever the last frame
|
|
||||||
// left behind, so dragging back and forth across the middle cannot ratchet the flag.
|
|
||||||
bool m_adjust_drag_start_mirror_u = false;
|
|
||||||
bool m_adjust_drag_start_mirror_v = false;
|
|
||||||
|
|
||||||
// The live value to put in the readout pill, built by the drag and drawn by the renderer in the
|
|
||||||
// same frame. Empty when no drag is running, which is also when no pill is drawn.
|
|
||||||
std::string m_adjust_readout;
|
|
||||||
|
|
||||||
std::map<std::string, wxString> m_desc;
|
std::map<std::string, wxString> m_desc;
|
||||||
|
|
||||||
|
|||||||
@@ -296,12 +296,9 @@ void UVEditorCanvas::set_island_fill_colors(std::vector<ColorRGBA> colors)
|
|||||||
Refresh();
|
Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
void UVEditorCanvas::set_uv_transform(float tiling_scale, float tiling_scale_v, const Vec2f &mirror_sign,
|
void UVEditorCanvas::set_uv_transform(float tiling_scale, float rotation_deg, bool tile_enabled, bool tile_mirrored)
|
||||||
float rotation_deg, bool tile_enabled, bool tile_mirrored)
|
|
||||||
{
|
{
|
||||||
m_tiling_scale = (std::abs(tiling_scale) > 1e-6f) ? tiling_scale : 1.f;
|
m_tiling_scale = (std::abs(tiling_scale) > 1e-6f) ? tiling_scale : 1.f;
|
||||||
m_tiling_scale_v = (tiling_scale_v > 1e-6f) ? tiling_scale_v : m_tiling_scale;
|
|
||||||
m_mirror_sign = mirror_sign;
|
|
||||||
m_rotation_deg = rotation_deg;
|
m_rotation_deg = rotation_deg;
|
||||||
m_tile_enabled = tile_enabled;
|
m_tile_enabled = tile_enabled;
|
||||||
m_tile_mirrored = tile_mirrored;
|
m_tile_mirrored = tile_mirrored;
|
||||||
@@ -657,18 +654,11 @@ Vec2f UVEditorCanvas::island_centroid(int island) const
|
|||||||
|
|
||||||
Vec2f UVEditorCanvas::uv_delta_to_unwrap(const Vec2f &delta_uv) const
|
Vec2f UVEditorCanvas::uv_delta_to_unwrap(const Vec2f &delta_uv) const
|
||||||
{
|
{
|
||||||
// apply_uv_transform() maps unwrap -> uv as uv = S^-1 R(unwrap) + offset, with S the tile
|
// apply_uv_transform() maps unwrap -> uv as uv = R(unwrap / tiling) + offset, so the inverse of
|
||||||
// extents, so the inverse of a *delta* (the translation drops out) is unwrap = R^-1(S delta_uv).
|
// a *delta* (the translation drops out) is unwrap = tiling * R^-1(delta_uv).
|
||||||
// The extents therefore multiply *before* the inverse rotation, which is the mirror image of the
|
|
||||||
// forward order; with a square tile the two are interchangeable, with a rectangular one they are
|
|
||||||
// not.
|
|
||||||
const float rad = m_rotation_deg * float(M_PI) / 180.f;
|
const float rad = m_rotation_deg * float(M_PI) / 180.f;
|
||||||
const float cs = std::cos(rad), sn = std::sin(rad);
|
const float cs = std::cos(rad), sn = std::sin(rad);
|
||||||
// The mirror is its own sign on the same axis, and a negation is its own inverse, so it comes
|
return Vec2f(delta_uv.x() * cs + delta_uv.y() * sn, -delta_uv.x() * sn + delta_uv.y() * cs) * m_tiling_scale;
|
||||||
// back out here exactly as it went in.
|
|
||||||
const Vec2f scaled(delta_uv.x() * m_tiling_scale * m_mirror_sign.x(),
|
|
||||||
delta_uv.y() * m_tiling_scale_v * m_mirror_sign.y());
|
|
||||||
return Vec2f(scaled.x() * cs + scaled.y() * sn, -scaled.x() * sn + scaled.y() * cs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Vec2f UVEditorCanvas::snap_correction(int island) const
|
Vec2f UVEditorCanvas::snap_correction(int island) const
|
||||||
@@ -2163,10 +2153,7 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
|
|||||||
void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s)
|
void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s)
|
||||||
{
|
{
|
||||||
const wxString name = s.has_layer ? s.layer_name : _L("No layer mapped with Unwrap");
|
const wxString name = s.has_layer ? s.layer_name : _L("No layer mapped with Unwrap");
|
||||||
// A square tile reads as one number; only a genuinely rectangular one is worth two.
|
const wxString tile = s.has_layer ? wxString::Format(_L("%.1f mm tile"), s.tile_mm) : wxString();
|
||||||
const wxString tile = !s.has_layer ? wxString() :
|
|
||||||
std::abs(s.tile_mm_v - s.tile_mm) < 0.005f ? wxString::Format(_L("%.1f mm tile"), s.tile_mm) :
|
|
||||||
wxString::Format(_L("%.1f x %.1f mm tile"), s.tile_mm, s.tile_mm_v);
|
|
||||||
const bool relayout = m_layer_name->GetLabel() != name || m_tile->GetLabel() != tile;
|
const bool relayout = m_layer_name->GetLabel() != name || m_tile->GetLabel() != tile;
|
||||||
m_layer_name->SetLabel(name);
|
m_layer_name->SetLabel(name);
|
||||||
m_tile->SetLabel(tile);
|
m_tile->SetLabel(tile);
|
||||||
|
|||||||
@@ -91,10 +91,7 @@ public:
|
|||||||
// space, back into the unwrap's mm space - which is where a TextureIsland's offset actually
|
// space, back into the unwrap's mm space - which is where a TextureIsland's offset actually
|
||||||
// lives (see apply_uv_transform()). The tile settings come along because the background has to
|
// lives (see apply_uv_transform()). The tile settings come along because the background has to
|
||||||
// repeat exactly the way the height sampler does, or the pane would stop showing what gets baked.
|
// repeat exactly the way the height sampler does, or the pane would stop showing what gets baked.
|
||||||
// `tiling_scale_v` is the tile's extent along v; pass <= 0 for a square tile, matching
|
void set_uv_transform(float tiling_scale, float rotation_deg, bool tile_enabled, bool tile_mirrored);
|
||||||
// layer_tile_mm().
|
|
||||||
void set_uv_transform(float tiling_scale, float tiling_scale_v, const Vec2f &mirror_sign,
|
|
||||||
float rotation_deg, bool tile_enabled, bool tile_mirrored);
|
|
||||||
|
|
||||||
// Same 8-bit grayscale pixels build_texture_displacement()'s height sampling uses, shown
|
// Same 8-bit grayscale pixels build_texture_displacement()'s height sampling uses, shown
|
||||||
// beneath the wireframe (expanded to RGBA on upload) so the unwrap can be checked against the
|
// beneath the wireframe (expanded to RGBA on upload) so the unwrap can be checked against the
|
||||||
@@ -130,8 +127,7 @@ public:
|
|||||||
{
|
{
|
||||||
bool has_layer = false; // an active layer mapped with Unwrap
|
bool has_layer = false; // an active layer mapped with Unwrap
|
||||||
wxString layer_name;
|
wxString layer_name;
|
||||||
float tile_mm = 0.f; // tile extent along u
|
float tile_mm = 0.f;
|
||||||
float tile_mm_v = 0.f; // along v; equal to tile_mm for a square tile
|
|
||||||
std::vector<unsigned char> thumbnail_rgb; // thumbnail_px square, 3 bytes per pixel; empty for none
|
std::vector<unsigned char> thumbnail_rgb; // thumbnail_px square, 3 bytes per pixel; empty for none
|
||||||
int thumbnail_px = 0;
|
int thumbnail_px = 0;
|
||||||
float seam_angle_deg = 40.f;
|
float seam_angle_deg = 40.f;
|
||||||
@@ -294,10 +290,8 @@ private:
|
|||||||
GLModel m_grid_glmodel;
|
GLModel m_grid_glmodel;
|
||||||
float m_grid_step = 0.f; // the UV step m_grid_glmodel was built for; 0 = not built
|
float m_grid_step = 0.f; // the UV step m_grid_glmodel was built for; 0 = not built
|
||||||
|
|
||||||
float m_tiling_scale = 1.f;
|
float m_tiling_scale = 1.f;
|
||||||
float m_tiling_scale_v = 1.f; // already resolved: never the "unset" sentinel, see set_uv_transform()
|
float m_rotation_deg = 0.f;
|
||||||
Vec2f m_mirror_sign = Vec2f::Ones(); // +1 or -1 per axis, see layer_mirror_sign()
|
|
||||||
float m_rotation_deg = 0.f;
|
|
||||||
bool m_tile_enabled = true;
|
bool m_tile_enabled = true;
|
||||||
bool m_tile_mirrored = false;
|
bool m_tile_mirrored = false;
|
||||||
bool m_snap_enabled = false;
|
bool m_snap_enabled = false;
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ WebDialog::WebDialog(wxWindow* parent,
|
|||||||
SubmitHandler on_submit,
|
SubmitHandler on_submit,
|
||||||
CloseHandler on_close,
|
CloseHandler on_close,
|
||||||
CloseHandler on_destroyed,
|
CloseHandler on_destroyed,
|
||||||
long wx_style)
|
long wx_style,
|
||||||
|
const wxString& profile)
|
||||||
: WebViewHostDialog(parent, wxID_ANY, title, wxDefaultPosition, size, wx_style)
|
: WebViewHostDialog(parent, wxID_ANY, title, wxDefaultPosition, size, wx_style)
|
||||||
, m_html(html)
|
, m_html(html)
|
||||||
, m_on_message(std::move(on_message))
|
, 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
|
// A tiny bundled bootstrap page brings the webview up; the real plugin HTML
|
||||||
// is swapped in via SetPage once the bootstrap finishes loading.
|
// 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
|
// Paint the window/webview in the themed background so there is no white
|
||||||
// flash before the (transparent) bootstrap page and plugin HTML render.
|
// flash before the (transparent) bootstrap page and plugin HTML render.
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ public:
|
|||||||
SubmitHandler on_submit,
|
SubmitHandler on_submit,
|
||||||
CloseHandler on_close,
|
CloseHandler on_close,
|
||||||
CloseHandler on_destroyed,
|
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;
|
~WebDialog() override;
|
||||||
|
|
||||||
static void post_message(WebDialog* dialog, const nlohmann::json& data);
|
static void post_message(WebDialog* dialog, const nlohmann::json& data);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
namespace Slic3r { namespace GUI {
|
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)
|
: wxPanel(parent, wxID_ANY)
|
||||||
{
|
{
|
||||||
SetBackgroundColour(wxGetApp().get_window_default_clr());
|
SetBackgroundColour(wxGetApp().get_window_default_clr());
|
||||||
@@ -25,7 +25,7 @@ WebPanel::WebPanel(wxWindow* parent, const char* bridge_script)
|
|||||||
SetSizer(sizer);
|
SetSizer(sizer);
|
||||||
|
|
||||||
// Never null: WebView::CreateWebView substitutes a placeholder view when no backend is available.
|
// 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->SetBackgroundColour(GetBackgroundColour());
|
||||||
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::theme_user_script()));
|
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::theme_user_script()));
|
||||||
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::element_defaults_user_script()));
|
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::element_defaults_user_script()));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include <wx/event.h>
|
#include <wx/event.h>
|
||||||
#include <wx/panel.h>
|
#include <wx/panel.h>
|
||||||
|
#include <wx/string.h>
|
||||||
#include <wx/webview.h>
|
#include <wx/webview.h>
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
@@ -22,7 +23,7 @@ namespace Slic3r { namespace GUI {
|
|||||||
class WebPanel : public wxPanel
|
class WebPanel : public wxPanel
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
WebPanel(wxWindow* parent, const char* bridge_script);
|
WebPanel(wxWindow* parent, const char* bridge_script, const wxString& profile = wxEmptyString);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
wxWebView* browser() const { return m_browser; }
|
wxWebView* browser() const { return m_browser; }
|
||||||
|
|||||||
@@ -82,6 +82,11 @@ ComboBox::ComboBox(wxWindow *parent,
|
|||||||
e.SetId(GetId());
|
e.SetId(GetId());
|
||||||
GetEventHandler()->ProcessEvent(e);
|
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.Bind(EVT_DISMISS, [this](auto &) {
|
||||||
drop_down = false;
|
drop_down = false;
|
||||||
wxCommandEvent e(wxEVT_COMBOBOX_CLOSEUP);
|
wxCommandEvent e(wxEVT_COMBOBOX_CLOSEUP);
|
||||||
@@ -251,7 +256,7 @@ int ComboBox::Append(const wxString &text,
|
|||||||
int style)
|
int style)
|
||||||
{
|
{
|
||||||
auto valid_bit_map = bitmap.IsOk() ? bitmap : wxNullBitmap;
|
auto valid_bit_map = bitmap.IsOk() ? bitmap : wxNullBitmap;
|
||||||
Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, clientData, group_key, group_label};
|
Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, wxNullBitmap, clientData, group_key, group_label};
|
||||||
item.style = style;
|
item.style = style;
|
||||||
items.push_back(item);
|
items.push_back(item);
|
||||||
SetClientDataType(wxClientData_Void);
|
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(); }
|
unsigned int ComboBox::GetCount() const { return items.size(); }
|
||||||
|
|
||||||
void ComboBox::set_replace_text(wxString text, wxString image_name)
|
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();
|
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,
|
int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items,
|
||||||
unsigned int pos,
|
unsigned int pos,
|
||||||
void ** clientData,
|
void ** clientData,
|
||||||
@@ -357,7 +375,7 @@ int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items,
|
|||||||
{
|
{
|
||||||
if (pos > this->items.size()) return -1;
|
if (pos > this->items.size()) return -1;
|
||||||
for (int i = 0; i < items.GetCount(); ++i) {
|
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);
|
this->items.insert(this->items.begin() + pos, item);
|
||||||
++pos;
|
++pos;
|
||||||
}
|
}
|
||||||
@@ -419,16 +437,18 @@ void ComboBox::keyDown(wxKeyEvent& event)
|
|||||||
case WXK_UP:
|
case WXK_UP:
|
||||||
case WXK_DOWN:
|
case WXK_DOWN:
|
||||||
case WXK_LEFT:
|
case WXK_LEFT:
|
||||||
case WXK_RIGHT:
|
case WXK_RIGHT: {
|
||||||
if ((event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) && GetSelection() > 0) {
|
// Step over disabled rows: the mouse cannot select them either.
|
||||||
SetSelection(GetSelection() - 1);
|
const int step = (event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) ? -1 : 1;
|
||||||
} else if ((event.GetKeyCode() == WXK_DOWN || event.GetKeyCode() == WXK_RIGHT) && GetSelection() + 1 < items.size()) {
|
int n = GetSelection() + step;
|
||||||
SetSelection(GetSelection() + 1);
|
while (n >= 0 && n < int(items.size()) && (items[n].style & DD_ITEM_STYLE_DISABLED))
|
||||||
} else {
|
n += step;
|
||||||
|
if (n < 0 || n >= int(items.size()))
|
||||||
break;
|
break;
|
||||||
}
|
SetSelection(n);
|
||||||
sendComboBoxEvent();
|
sendComboBoxEvent();
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case WXK_TAB:
|
case WXK_TAB:
|
||||||
HandleAsNavigationKey(event);
|
HandleAsNavigationKey(event);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -96,10 +96,12 @@ public:
|
|||||||
|
|
||||||
wxBitmap GetItemBitmap(unsigned int n);
|
wxBitmap GetItemBitmap(unsigned int n);
|
||||||
void SetItemBitmap(unsigned int n, wxBitmap const &bitmap);
|
void SetItemBitmap(unsigned int n, wxBitmap const &bitmap);
|
||||||
|
void SetItemActionBitmap(unsigned int n, wxBitmap const &bitmap);
|
||||||
bool is_drop_down(){return drop_down;}
|
bool is_drop_down(){return drop_down;}
|
||||||
void DeleteOneItem(unsigned int pos) { DoDeleteOneItem(pos); }
|
void DeleteOneItem(unsigned int pos) { DoDeleteOneItem(pos); }
|
||||||
|
|
||||||
void ForceDropdownOpen();
|
void ForceDropdownOpen();
|
||||||
|
void CloseDropdown();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual int DoInsertItems(const wxArrayStringsAdapter &items,
|
virtual int DoInsertItems(const wxArrayStringsAdapter &items,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
#include "slic3r/GUI/wxExtensions.hpp"
|
#include "slic3r/GUI/wxExtensions.hpp"
|
||||||
|
|
||||||
wxDEFINE_EVENT(EVT_DISMISS, wxCommandEvent);
|
wxDEFINE_EVENT(EVT_DISMISS, wxCommandEvent);
|
||||||
|
wxDEFINE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent);
|
||||||
|
|
||||||
BEGIN_EVENT_TABLE(DropDown, PopupWindow)
|
BEGIN_EVENT_TABLE(DropDown, PopupWindow)
|
||||||
|
|
||||||
@@ -242,6 +243,19 @@ static wxSize GetBmpSize(wxBitmap & bmp)
|
|||||||
#endif
|
#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)
|
static void _DrawSplitItem(const wxWindow* w, wxDC& dc, wxString split_text, wxPoint start_pt, int item_width, int item_height)
|
||||||
{
|
{
|
||||||
// save dc
|
// save dc
|
||||||
@@ -371,6 +385,10 @@ void DropDown::render(wxDC &dc)
|
|||||||
rcContent.width -= szBmp.x + 5;
|
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;
|
std::set<wxString> groups;
|
||||||
// draw texts & icons
|
// draw texts & icons
|
||||||
int index = 0;
|
int index = 0;
|
||||||
@@ -434,13 +452,24 @@ void DropDown::render(wxDC &dc)
|
|||||||
auto text = group.IsEmpty()
|
auto text = group.IsEmpty()
|
||||||
? (item.group_key.IsEmpty() ? item.text : item.group_label)
|
? (item.group_key.IsEmpty() ? item.text : item.group_label)
|
||||||
: item.text;
|
: 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()) {
|
if (!text_off && !text.IsEmpty()) {
|
||||||
wxSize tSize = dc.GetMultiLineTextExtent(text);
|
wxSize tSize = dc.GetMultiLineTextExtent(text);
|
||||||
if (pt.x + tSize.x > rcContent.GetRight()) {
|
if (pt.x + tSize.x > rcContent.GetRight()) {
|
||||||
if (is_hover && item.tip.IsEmpty())
|
int max_text_width = rcContent.GetRight() - pt.x;
|
||||||
SetToolTip(text);
|
if (max_text_width <= 0) {
|
||||||
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END,
|
text.clear();
|
||||||
rcContent.GetRight() - pt.x);
|
} 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;
|
pt.y += (rcContent.height - textSize.y) / 2;
|
||||||
dc.SetFont(GetFont());
|
dc.SetFont(GetFont());
|
||||||
@@ -529,6 +558,7 @@ void DropDown::messureSize()
|
|||||||
if (!need_sync) return;
|
if (!need_sync) return;
|
||||||
textSize = wxSize();
|
textSize = wxSize();
|
||||||
iconSize = wxSize();
|
iconSize = wxSize();
|
||||||
|
actionIconWidth = 0;
|
||||||
count = 0;
|
count = 0;
|
||||||
wxClientDC dc(GetParent() ? GetParent() : this);
|
wxClientDC dc(GetParent() ? GetParent() : this);
|
||||||
dc.SetFont(GetFont());
|
dc.SetFont(GetFont());
|
||||||
@@ -565,8 +595,13 @@ void DropDown::messureSize()
|
|||||||
size1.x += size2.x + (text_off ? 0 : 5);
|
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;
|
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;
|
if (!align_icon) iconSize.x = 0;
|
||||||
wxSize szContent = textSize;
|
wxSize szContent = textSize;
|
||||||
if (szContent.x < FromDIP(120) && !use_content_width)
|
if (szContent.x < FromDIP(120) && !use_content_width)
|
||||||
@@ -636,6 +671,11 @@ void DropDown::messureSize()
|
|||||||
e.SetId(GetId());
|
e.SetId(GetId());
|
||||||
GetEventHandler()->ProcessEvent(e);
|
GetEventHandler()->ProcessEvent(e);
|
||||||
});
|
});
|
||||||
|
subDropDown->Bind(EVT_DROPDOWN_ITEM_ACTION, [this](wxCommandEvent &e) {
|
||||||
|
e.SetEventObject(this);
|
||||||
|
e.SetId(GetId());
|
||||||
|
GetEventHandler()->ProcessEvent(e);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
need_sync = false;
|
need_sync = false;
|
||||||
}
|
}
|
||||||
@@ -714,6 +754,15 @@ void DropDown::mouseReleased(wxMouseEvent& event)
|
|||||||
if (hover_item < 0)
|
if (hover_item < 0)
|
||||||
return;
|
return;
|
||||||
if (hover_item >= 0 && (subDropDown == nullptr || subDropDown->group.empty())) { // not moved
|
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();
|
sendDropDownEvent();
|
||||||
if (mainDropDown)
|
if (mainDropDown)
|
||||||
mainDropDown->hover_item = -1; // To Dismiss mainDropDown
|
mainDropDown->hover_item = -1; // To Dismiss mainDropDown
|
||||||
@@ -830,6 +879,17 @@ void DropDown::sendDropDownEvent()
|
|||||||
GetEventHandler()->ProcessEvent(event);
|
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()
|
void DropDown::Dismiss()
|
||||||
{
|
{
|
||||||
if (subDropDown && subDropDown->IsShown())
|
if (subDropDown && subDropDown->IsShown())
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <wx/event.h>
|
#include <wx/event.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <utility>
|
||||||
#include <wx/gdicmn.h>
|
#include <wx/gdicmn.h>
|
||||||
#include "slic3r/GUI/Widgets/StateColor.hpp"
|
#include "slic3r/GUI/Widgets/StateColor.hpp"
|
||||||
#include <boost/date_time/posix_time/ptime.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
|
#define DD_ITEM_STYLE_DIMMED 0x0004 // gray text, but still selectable
|
||||||
|
|
||||||
wxDECLARE_EVENT(EVT_DISMISS, wxCommandEvent);
|
wxDECLARE_EVENT(EVT_DISMISS, wxCommandEvent);
|
||||||
|
wxDECLARE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent);
|
||||||
|
|
||||||
class DropDown : public PopupWindow
|
class DropDown : public PopupWindow
|
||||||
{
|
{
|
||||||
@@ -37,6 +39,7 @@ public:
|
|||||||
wxString text_static_tips;// display static tips for TextInput.eg.PrinterInfoBox
|
wxString text_static_tips;// display static tips for TextInput.eg.PrinterInfoBox
|
||||||
wxBitmap icon;
|
wxBitmap icon;
|
||||||
wxBitmap icon_textctrl;// display icon for TextInput.eg.PrinterInfoBox
|
wxBitmap icon_textctrl;// display icon for TextInput.eg.PrinterInfoBox
|
||||||
|
wxBitmap action_icon;
|
||||||
void * data{nullptr};
|
void * data{nullptr};
|
||||||
wxString group_key{};
|
wxString group_key{};
|
||||||
wxString group_label{};
|
wxString group_label{};
|
||||||
@@ -65,6 +68,7 @@ private:
|
|||||||
|
|
||||||
wxSize textSize;
|
wxSize textSize;
|
||||||
wxSize iconSize;
|
wxSize iconSize;
|
||||||
|
int actionIconWidth = 0;
|
||||||
wxSize rowSize;
|
wxSize rowSize;
|
||||||
|
|
||||||
StateHandler state_handler;
|
StateHandler state_handler;
|
||||||
@@ -135,6 +139,7 @@ private:
|
|||||||
int hoverIndex();
|
int hoverIndex();
|
||||||
|
|
||||||
int selectedItem();
|
int selectedItem();
|
||||||
|
std::pair<int, int> actionColumn() const;
|
||||||
|
|
||||||
friend class ComboBox;
|
friend class ComboBox;
|
||||||
void messureSize();
|
void messureSize();
|
||||||
@@ -148,6 +153,7 @@ private:
|
|||||||
void mouseWheelMoved(wxMouseEvent &event);
|
void mouseWheelMoved(wxMouseEvent &event);
|
||||||
|
|
||||||
void sendDropDownEvent();
|
void sendDropDownEvent();
|
||||||
|
void sendDropDownActionEvent(int index);
|
||||||
|
|
||||||
|
|
||||||
DECLARE_EVENT_TABLE()
|
DECLARE_EVENT_TABLE()
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
#include "slic3r/GUI/GUI_App.hpp"
|
#include "slic3r/GUI/GUI_App.hpp"
|
||||||
#include "slic3r/Utils/MacDarkMode.hpp"
|
#include "slic3r/Utils/MacDarkMode.hpp"
|
||||||
|
|
||||||
|
#include <libslic3r/Utils.hpp>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
|
|
||||||
@@ -12,6 +14,7 @@
|
|||||||
#include <exception>
|
#include <exception>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
|
#include <wx/chartype.h>
|
||||||
#include <wx/setup.h>
|
#include <wx/setup.h>
|
||||||
#include <wx/webview.h>
|
#include <wx/webview.h>
|
||||||
#include <wx/string.h>
|
#include <wx/string.h>
|
||||||
@@ -72,6 +75,26 @@ webkit_javascript_result_unref (WebKitJavascriptResult *js_result);
|
|||||||
}
|
}
|
||||||
#endif
|
#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__
|
#ifdef __WIN32__
|
||||||
// Run Download and Install in another thread so we don't block the UI thread
|
// Run Download and Install in another thread so we don't block the UI thread
|
||||||
DWORD DownloadAndInstallWV2RT() {
|
DWORD DownloadAndInstallWV2RT() {
|
||||||
@@ -135,6 +158,9 @@ DWORD DownloadAndInstallWV2RT() {
|
|||||||
class WebViewEdge : public wxWebViewEdge
|
class WebViewEdge : public wxWebViewEdge
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
WebViewEdge() = default;
|
||||||
|
explicit WebViewEdge(const wxWebViewConfiguration& config) : wxWebViewEdge(config) {}
|
||||||
|
|
||||||
bool SetUserAgent(const wxString &userAgent) override
|
bool SetUserAgent(const wxString &userAgent) override
|
||||||
{
|
{
|
||||||
bool dark = userAgent.Contains("dark");
|
bool dark = userAgent.Contains("dark");
|
||||||
@@ -213,6 +239,11 @@ public:
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
explicit WebViewWebKit(const wxWebViewConfiguration& config)
|
||||||
|
: wxWebViewWebKit(config)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
~WebViewWebKit() override
|
~WebViewWebKit() override
|
||||||
{
|
{
|
||||||
RemoveScriptMessageHandler("wx");
|
RemoveScriptMessageHandler("wx");
|
||||||
@@ -284,7 +315,7 @@ static WebViewRef *webview_ref(wxWebView *webView)
|
|||||||
return webView ? static_cast<WebViewRef *>(webView->GetRefData()) : nullptr;
|
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
|
#if wxUSE_WEBVIEW_EDGE
|
||||||
// Check if a fixed version of edge is present in
|
// 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(); }
|
if (!url2.empty()) { url2 = wxURI(url2).BuildURI(); }
|
||||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << url2.ToUTF8();
|
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << url2.ToUTF8();
|
||||||
|
|
||||||
|
const bool isolated = !profile.IsEmpty();
|
||||||
|
|
||||||
#ifdef __WIN32__
|
#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__)
|
#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
|
#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
|
#endif
|
||||||
if (webView) {
|
if (webView) {
|
||||||
webView->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
webView->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ wxDECLARE_EVENT(EVT_WEBVIEW_RECREATED, wxCommandEvent);
|
|||||||
class WebView
|
class WebView
|
||||||
{
|
{
|
||||||
public:
|
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
|
#if wxUSE_WEBVIEW_EDGE
|
||||||
static bool CheckWebViewRuntime();
|
static bool CheckWebViewRuntime();
|
||||||
static bool DownloadAndInstallWebViewRuntime();
|
static bool DownloadAndInstallWebViewRuntime();
|
||||||
|
|||||||
@@ -172,7 +172,8 @@ WebViewHostDialog::WebViewHostDialog(wxWindow* parent,
|
|||||||
bool WebViewHostDialog::create_webview(const std::string& resource_path,
|
bool WebViewHostDialog::create_webview(const std::string& resource_path,
|
||||||
const wxString& title,
|
const wxString& title,
|
||||||
const wxSize& dialog_size,
|
const wxSize& dialog_size,
|
||||||
const wxSize& min_size)
|
const wxSize& min_size,
|
||||||
|
const wxString& profile)
|
||||||
{
|
{
|
||||||
SetTitle(title);
|
SetTitle(title);
|
||||||
SetMinSize(FromDIP(min_size));
|
SetMinSize(FromDIP(min_size));
|
||||||
@@ -181,7 +182,7 @@ bool WebViewHostDialog::create_webview(const std::string& resource_path,
|
|||||||
wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL);
|
wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
|
||||||
// Created empty and loaded below, after the user scripts are added, so the first load already has them.
|
// 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) {
|
if (m_browser == nullptr) {
|
||||||
wxLogError("Could not init m_browser");
|
wxLogError("Could not init m_browser");
|
||||||
delete topsizer;
|
delete topsizer;
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ public:
|
|||||||
bool create_webview(const std::string& resource_path,
|
bool create_webview(const std::string& resource_path,
|
||||||
const wxString& title,
|
const wxString& title,
|
||||||
const wxSize& dialog_size = wxSize(820, 660),
|
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);
|
void load_url(const wxString& url);
|
||||||
bool run_script(const wxString& script);
|
bool run_script(const wxString& script);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ extern double mac_max_scaling_factor();
|
|||||||
extern void set_miniaturizable(void * window);
|
extern void set_miniaturizable(void * window);
|
||||||
void WKWebView_evaluateJavaScript(void * web, wxString const & script, void (*callback)(wxString const &));
|
void WKWebView_evaluateJavaScript(void * web, wxString const & script, void (*callback)(wxString const &));
|
||||||
void WKWebView_setTransparentBackground(void * web);
|
void WKWebView_setTransparentBackground(void * web);
|
||||||
|
void WKWebViewConfiguration_setNonPersistentDataStore(void * configuration);
|
||||||
void WKWebView_force_display(void * web);
|
void WKWebView_force_display(void * web);
|
||||||
void set_tag_when_enter_full_screen(bool isfullscreen);
|
void set_tag_when_enter_full_screen(bool isfullscreen);
|
||||||
void set_title_colour_after_set_title(void * window);
|
void set_title_colour_after_set_title(void * window);
|
||||||
|
|||||||
@@ -102,6 +102,15 @@ void WKWebView_setTransparentBackground(void * web)
|
|||||||
[webView registerForDraggedTypes: @[NSFilenamesPboardType]];
|
[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
|
// 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
|
// 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
|
// 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,
|
auto* dlg = new GUI::WebDialog(ui_parent(), wxString::FromUTF8(title), html,
|
||||||
wxSize(w, h), std::move(msg_adapter), std::move(submit_adapter),
|
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);
|
UiRegistry::instance().bind(new_id, dlg, plugin_key);
|
||||||
if (modal) {
|
if (modal) {
|
||||||
dlg->ShowModal();
|
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 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),
|
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);
|
UiRegistry::instance().bind(new_id, panel, plugin_key);
|
||||||
plater->add_dock_pane(panel, GUI::plugin_pane_name(plugin_key, title), wxString::FromUTF8(title), dock,
|
plater->add_dock_pane(panel, GUI::plugin_pane_name(plugin_key, title), wxString::FromUTF8(title), dock,
|
||||||
wxSize(w, h), [panel]() { panel->fire_close(); });
|
wxSize(w, h), [panel]() { panel->fire_close(); });
|
||||||
|
|||||||
@@ -77,7 +77,8 @@ constexpr char PLUGIN_PAGE_BRIDGE_JS[] = R"JS(
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
PluginPage::PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability)
|
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_cap(std::move(capability))
|
||||||
, m_lifetime(std::make_shared<std::atomic<PluginPage*>>(this))
|
, 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));
|
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