mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-12 18:31:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1abd6649c7 | ||
|
|
56b559d36a |
@@ -1,65 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,476 +0,0 @@
|
|||||||
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 });
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 876 B |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Qidi",
|
"name": "Qidi",
|
||||||
"version": "02.04.00.17",
|
"version": "02.04.00.16",
|
||||||
"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=[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",
|
"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",
|
||||||
"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[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_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_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[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_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_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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,17 +72,21 @@ varying float weight;
|
|||||||
varying float island_active;
|
varying float island_active;
|
||||||
varying vec2 vertex_uv;
|
varying vec2 vertex_uv;
|
||||||
|
|
||||||
// The cylinder's own frame, built exactly as libslic3r's project_cylindrical() builds it - including
|
// The cylinder's own frame, built exactly as libslic3r's project_cylindrical() builds it - right
|
||||||
// the handedness, which comes out left-handed for an axis of +Z. Copied rather than "corrected", so
|
// handed, so the angle taken from it grows anticlockwise about the axis, which seen from outside the
|
||||||
// the preview wraps the texture the same way round as the bake.
|
// cylinder is to the viewer's right rather than mirrored.
|
||||||
void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd)
|
void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd)
|
||||||
{
|
{
|
||||||
up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0);
|
up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0);
|
||||||
vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
|
vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
|
||||||
right = normalize(cross(up, arbitrary));
|
right = normalize(cross(up, arbitrary));
|
||||||
fwd = normalize(cross(right, up));
|
fwd = normalize(cross(up, right));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Which side of a world plane a face is on, with zero counting as the positive side. Not GLSL's own
|
||||||
|
// sign(), which returns 0 there and would collapse the coordinate onto a line.
|
||||||
|
float box_face_sign(float n) { return n >= 0.0 ? 1.0 : -1.0; }
|
||||||
|
|
||||||
// The raw, millimetre-valued projection of `p` (a position in the texture frame), before the layer's
|
// The raw, millimetre-valued projection of `p` (a position in the texture frame), before the layer's
|
||||||
// tiling/rotation/aspect/offset - a term-for-term transcription of libslic3r's project_planar(),
|
// tiling/rotation/aspect/offset - a term-for-term transcription of libslic3r's project_planar(),
|
||||||
// project_cylindrical() and project_spherical(). `n` is read by the planar mode only.
|
// project_cylindrical() and project_spherical(). `n` is read by the planar mode only.
|
||||||
@@ -94,7 +98,7 @@ vec2 projection_raw(vec3 p, vec3 n)
|
|||||||
vec3 rel = p - patch_center;
|
vec3 rel = p - patch_center;
|
||||||
float x = dot(rel, right);
|
float x = dot(rel, right);
|
||||||
float y = dot(rel, fwd);
|
float y = dot(rel, fwd);
|
||||||
return vec2(atan(y, x) * sqrt(x * x + y * y), dot(rel, up));
|
return vec2(atan(y, x) * sqrt(x * x + y * y), -dot(rel, up));
|
||||||
}
|
}
|
||||||
if (projection_mode == 2) { // Spherical: (longitude, latitude) * radius
|
if (projection_mode == 2) { // Spherical: (longitude, latitude) * radius
|
||||||
vec3 rel = p - patch_center;
|
vec3 rel = p - patch_center;
|
||||||
@@ -102,10 +106,18 @@ vec2 projection_raw(vec3 p, vec3 n)
|
|||||||
if (radius < 1e-8)
|
if (radius < 1e-8)
|
||||||
return vec2(0.0);
|
return vec2(0.0);
|
||||||
vec3 dir = rel / radius;
|
vec3 dir = rel / radius;
|
||||||
return vec2(atan(dir.y, dir.x), asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
return vec2(atan(dir.y, dir.x), -asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
||||||
}
|
}
|
||||||
vec3 an = abs(n); // Triplanar: drop the dominant normal axis
|
// Triplanar: drop the dominant normal axis. v is negated so the image's first pixel row (v = 0)
|
||||||
return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
|
// sits at the top of the model instead of upside down, and u is flipped on the far side of each
|
||||||
|
// plane so the two faces sharing it are not mirror images. libslic3r's box_plane_yz() and its two
|
||||||
|
// siblings, term for term.
|
||||||
|
vec3 an = abs(n);
|
||||||
|
if (an.x >= an.y && an.x >= an.z)
|
||||||
|
return vec2(box_face_sign(n.x) * p.y, -p.z);
|
||||||
|
if (an.y >= an.x && an.y >= an.z)
|
||||||
|
return vec2(-box_face_sign(n.y) * p.x, -p.z);
|
||||||
|
return vec2(box_face_sign(n.z) * p.x, -p.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The two surface directions projection_raw()'s u and v run along at `p`, plus how many raw units one
|
// The two surface directions projection_raw()'s u and v run along at `p`, plus how many raw units one
|
||||||
@@ -124,7 +136,7 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
|
|||||||
vec2 xy = vec2(dot(rel, right), dot(rel, fwd));
|
vec2 xy = vec2(dot(rel, right), dot(rel, fwd));
|
||||||
float r = length(xy);
|
float r = length(xy);
|
||||||
t = (r > 1e-6) ? (fwd * xy.x - right * xy.y) / r : right; // circumferential: u runs along it
|
t = (r > 1e-6) ? (fwd * xy.x - right * xy.y) / r : right; // circumferential: u runs along it
|
||||||
b = up; // v is the distance along the axis
|
b = -up; // v is the distance *against* the axis
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (projection_mode == 2) {
|
if (projection_mode == 2) {
|
||||||
@@ -133,20 +145,20 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
|
|||||||
vec3 dir = (r > 1e-8) ? rel / r : vec3(0.0, 0.0, 1.0);
|
vec3 dir = (r > 1e-8) ? rel / r : vec3(0.0, 0.0, 1.0);
|
||||||
float c = length(dir.xy); // cos(latitude)
|
float c = length(dir.xy); // cos(latitude)
|
||||||
t = (c > 1e-6) ? vec3(-dir.y, dir.x, 0.0) / c : vec3(1.0, 0.0, 0.0);
|
t = (c > 1e-6) ? vec3(-dir.y, dir.x, 0.0) / c : vec3(1.0, 0.0, 0.0);
|
||||||
b = cross(dir, t); // increasing latitude, unit length
|
b = -cross(dir, t); // v grows away from the pole, unit length
|
||||||
units_per_mm = vec2(1.0 / max(c, 1e-3), 1.0);
|
units_per_mm = vec2(1.0 / max(c, 1e-3), 1.0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
vec3 an = abs(n);
|
vec3 an = abs(n); // the same signed basis projection_raw() uses
|
||||||
if (an.x >= an.y && an.x >= an.z) { // planar = p.yz
|
if (an.x >= an.y && an.x >= an.z) { // planar = (+-y, -z)
|
||||||
t = vec3(0.0, 1.0, 0.0);
|
t = vec3(0.0, box_face_sign(n.x), 0.0);
|
||||||
b = vec3(0.0, 0.0, 1.0);
|
b = vec3(0.0, 0.0, -1.0);
|
||||||
} else if (an.y >= an.x && an.y >= an.z) { // planar = p.xz
|
} else if (an.y >= an.x && an.y >= an.z) { // planar = (+-x, -z)
|
||||||
t = vec3(1.0, 0.0, 0.0);
|
t = vec3(-box_face_sign(n.y), 0.0, 0.0);
|
||||||
b = vec3(0.0, 0.0, 1.0);
|
b = vec3(0.0, 0.0, -1.0);
|
||||||
} else { // planar = p.xy
|
} else { // planar = (+-x, -y)
|
||||||
t = vec3(1.0, 0.0, 0.0);
|
t = vec3(box_face_sign(n.z), 0.0, 0.0);
|
||||||
b = vec3(0.0, 1.0, 0.0);
|
b = vec3(0.0, -1.0, 0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,14 +36,20 @@ varying vec4 world_pos;
|
|||||||
varying float distortion;
|
varying float distortion;
|
||||||
varying vec2 vertex_uv;
|
varying vec2 vertex_uv;
|
||||||
|
|
||||||
|
// Right handed (up x right), like libslic3r's project_cylindrical(), so the angle taken from it grows
|
||||||
|
// anticlockwise about the axis - to the viewer's right when seen from outside the cylinder.
|
||||||
void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd)
|
void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd)
|
||||||
{
|
{
|
||||||
up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0);
|
up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0);
|
||||||
vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
|
vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
|
||||||
right = normalize(cross(up, arbitrary));
|
right = normalize(cross(up, arbitrary));
|
||||||
fwd = normalize(cross(right, up));
|
fwd = normalize(cross(up, right));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Which side of a world plane a face is on, with zero counting as the positive side. Not GLSL's own
|
||||||
|
// sign(), which returns 0 there and would collapse the coordinate onto a line.
|
||||||
|
float box_face_sign(float n) { return n >= 0.0 ? 1.0 : -1.0; }
|
||||||
|
|
||||||
// Term for term libslic3r's project_planar() / project_cylindrical() / project_spherical(), in mm.
|
// Term for term libslic3r's project_planar() / project_cylindrical() / project_spherical(), in mm.
|
||||||
vec2 projection_raw(vec3 p, vec3 n)
|
vec2 projection_raw(vec3 p, vec3 n)
|
||||||
{
|
{
|
||||||
@@ -53,7 +59,7 @@ vec2 projection_raw(vec3 p, vec3 n)
|
|||||||
vec3 rel = p - patch_center;
|
vec3 rel = p - patch_center;
|
||||||
float x = dot(rel, right);
|
float x = dot(rel, right);
|
||||||
float y = dot(rel, fwd);
|
float y = dot(rel, fwd);
|
||||||
return vec2(atan(y, x) * sqrt(x * x + y * y), dot(rel, up));
|
return vec2(atan(y, x) * sqrt(x * x + y * y), -dot(rel, up));
|
||||||
}
|
}
|
||||||
if (projection_mode == 2) {
|
if (projection_mode == 2) {
|
||||||
vec3 rel = p - patch_center;
|
vec3 rel = p - patch_center;
|
||||||
@@ -61,10 +67,16 @@ vec2 projection_raw(vec3 p, vec3 n)
|
|||||||
if (radius < 1e-8)
|
if (radius < 1e-8)
|
||||||
return vec2(0.0);
|
return vec2(0.0);
|
||||||
vec3 dir = rel / radius;
|
vec3 dir = rel / radius;
|
||||||
return vec2(atan(dir.y, dir.x), asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
return vec2(atan(dir.y, dir.x), -asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
||||||
}
|
}
|
||||||
|
// v negated so the image's first pixel row sits at the top of the model, u flipped on the far side
|
||||||
|
// of each plane so the two faces sharing it are not mirror images - see libslic3r's box_plane_yz().
|
||||||
vec3 an = abs(n);
|
vec3 an = abs(n);
|
||||||
return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
|
if (an.x >= an.y && an.x >= an.z)
|
||||||
|
return vec2(box_face_sign(n.x) * p.y, -p.z);
|
||||||
|
if (an.y >= an.x && an.y >= an.z)
|
||||||
|
return vec2(-box_face_sign(n.y) * p.x, -p.z);
|
||||||
|
return vec2(box_face_sign(n.z) * p.x, -p.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
vec2 project_uv(vec3 p, vec3 n)
|
vec2 project_uv(vec3 p, vec3 n)
|
||||||
|
|||||||
@@ -138,17 +138,21 @@ in vec2 vertex_uv;
|
|||||||
|
|
||||||
out vec4 out_color;
|
out vec4 out_color;
|
||||||
|
|
||||||
// The cylinder's own frame, built exactly as libslic3r's project_cylindrical() builds it - including
|
// The cylinder's own frame, built exactly as libslic3r's project_cylindrical() builds it - right
|
||||||
// the handedness, which comes out left-handed for an axis of +Z. Copied rather than "corrected", so
|
// handed, so the angle taken from it grows anticlockwise about the axis, which seen from outside the
|
||||||
// the preview wraps the texture the same way round as the bake.
|
// cylinder is to the viewer's right rather than mirrored.
|
||||||
void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd)
|
void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd)
|
||||||
{
|
{
|
||||||
up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0);
|
up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0);
|
||||||
vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
|
vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
|
||||||
right = normalize(cross(up, arbitrary));
|
right = normalize(cross(up, arbitrary));
|
||||||
fwd = normalize(cross(right, up));
|
fwd = normalize(cross(up, right));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Which side of a world plane a face is on, with zero counting as the positive side. Not GLSL's own
|
||||||
|
// sign(), which returns 0 there and would collapse the coordinate onto a line.
|
||||||
|
float box_face_sign(float n) { return n >= 0.0 ? 1.0 : -1.0; }
|
||||||
|
|
||||||
// The raw, millimetre-valued projection of `p` (a position in the texture frame), before the layer's
|
// The raw, millimetre-valued projection of `p` (a position in the texture frame), before the layer's
|
||||||
// tiling/rotation/aspect/offset - a term-for-term transcription of libslic3r's project_planar(),
|
// tiling/rotation/aspect/offset - a term-for-term transcription of libslic3r's project_planar(),
|
||||||
// project_cylindrical() and project_spherical(). `n` is read by the planar mode only.
|
// project_cylindrical() and project_spherical(). `n` is read by the planar mode only.
|
||||||
@@ -160,7 +164,7 @@ vec2 projection_raw(vec3 p, vec3 n)
|
|||||||
vec3 rel = p - patch_center;
|
vec3 rel = p - patch_center;
|
||||||
float x = dot(rel, right);
|
float x = dot(rel, right);
|
||||||
float y = dot(rel, fwd);
|
float y = dot(rel, fwd);
|
||||||
return vec2(atan(y, x) * sqrt(x * x + y * y), dot(rel, up));
|
return vec2(atan(y, x) * sqrt(x * x + y * y), -dot(rel, up));
|
||||||
}
|
}
|
||||||
if (projection_mode == 2) { // Spherical: (longitude, latitude) * radius
|
if (projection_mode == 2) { // Spherical: (longitude, latitude) * radius
|
||||||
vec3 rel = p - patch_center;
|
vec3 rel = p - patch_center;
|
||||||
@@ -168,10 +172,18 @@ vec2 projection_raw(vec3 p, vec3 n)
|
|||||||
if (radius < 1e-8)
|
if (radius < 1e-8)
|
||||||
return vec2(0.0);
|
return vec2(0.0);
|
||||||
vec3 dir = rel / radius;
|
vec3 dir = rel / radius;
|
||||||
return vec2(atan(dir.y, dir.x), asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
return vec2(atan(dir.y, dir.x), -asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
||||||
}
|
}
|
||||||
vec3 an = abs(n); // Triplanar: drop the dominant normal axis
|
// Triplanar: drop the dominant normal axis. v is negated so the image's first pixel row (v = 0)
|
||||||
return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
|
// sits at the top of the model instead of upside down, and u is flipped on the far side of each
|
||||||
|
// plane so the two faces sharing it are not mirror images. libslic3r's box_plane_yz() and its two
|
||||||
|
// siblings, term for term.
|
||||||
|
vec3 an = abs(n);
|
||||||
|
if (an.x >= an.y && an.x >= an.z)
|
||||||
|
return vec2(box_face_sign(n.x) * p.y, -p.z);
|
||||||
|
if (an.y >= an.x && an.y >= an.z)
|
||||||
|
return vec2(-box_face_sign(n.y) * p.x, -p.z);
|
||||||
|
return vec2(box_face_sign(n.z) * p.x, -p.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The two surface directions projection_raw()'s u and v run along at `p`, plus how many raw units one
|
// The two surface directions projection_raw()'s u and v run along at `p`, plus how many raw units one
|
||||||
@@ -190,7 +202,7 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
|
|||||||
vec2 xy = vec2(dot(rel, right), dot(rel, fwd));
|
vec2 xy = vec2(dot(rel, right), dot(rel, fwd));
|
||||||
float r = length(xy);
|
float r = length(xy);
|
||||||
t = (r > 1e-6) ? (fwd * xy.x - right * xy.y) / r : right; // circumferential: u runs along it
|
t = (r > 1e-6) ? (fwd * xy.x - right * xy.y) / r : right; // circumferential: u runs along it
|
||||||
b = up; // v is the distance along the axis
|
b = -up; // v is the distance *against* the axis
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (projection_mode == 2) {
|
if (projection_mode == 2) {
|
||||||
@@ -199,20 +211,20 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
|
|||||||
vec3 dir = (r > 1e-8) ? rel / r : vec3(0.0, 0.0, 1.0);
|
vec3 dir = (r > 1e-8) ? rel / r : vec3(0.0, 0.0, 1.0);
|
||||||
float c = length(dir.xy); // cos(latitude)
|
float c = length(dir.xy); // cos(latitude)
|
||||||
t = (c > 1e-6) ? vec3(-dir.y, dir.x, 0.0) / c : vec3(1.0, 0.0, 0.0);
|
t = (c > 1e-6) ? vec3(-dir.y, dir.x, 0.0) / c : vec3(1.0, 0.0, 0.0);
|
||||||
b = cross(dir, t); // increasing latitude, unit length
|
b = -cross(dir, t); // v grows away from the pole, unit length
|
||||||
units_per_mm = vec2(1.0 / max(c, 1e-3), 1.0);
|
units_per_mm = vec2(1.0 / max(c, 1e-3), 1.0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
vec3 an = abs(n);
|
vec3 an = abs(n); // the same signed basis projection_raw() uses
|
||||||
if (an.x >= an.y && an.x >= an.z) { // planar = p.yz
|
if (an.x >= an.y && an.x >= an.z) { // planar = (+-y, -z)
|
||||||
t = vec3(0.0, 1.0, 0.0);
|
t = vec3(0.0, box_face_sign(n.x), 0.0);
|
||||||
b = vec3(0.0, 0.0, 1.0);
|
b = vec3(0.0, 0.0, -1.0);
|
||||||
} else if (an.y >= an.x && an.y >= an.z) { // planar = p.xz
|
} else if (an.y >= an.x && an.y >= an.z) { // planar = (+-x, -z)
|
||||||
t = vec3(1.0, 0.0, 0.0);
|
t = vec3(-box_face_sign(n.y), 0.0, 0.0);
|
||||||
b = vec3(0.0, 0.0, 1.0);
|
b = vec3(0.0, 0.0, -1.0);
|
||||||
} else { // planar = p.xy
|
} else { // planar = (+-x, -y)
|
||||||
t = vec3(1.0, 0.0, 0.0);
|
t = vec3(box_face_sign(n.z), 0.0, 0.0);
|
||||||
b = vec3(0.0, 1.0, 0.0);
|
b = vec3(0.0, -1.0, 0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,14 +47,20 @@ in vec2 vertex_uv;
|
|||||||
|
|
||||||
out vec4 out_color;
|
out vec4 out_color;
|
||||||
|
|
||||||
|
// Right handed (up x right), like libslic3r's project_cylindrical(), so the angle taken from it grows
|
||||||
|
// anticlockwise about the axis - to the viewer's right when seen from outside the cylinder.
|
||||||
void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd)
|
void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd)
|
||||||
{
|
{
|
||||||
up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0);
|
up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0);
|
||||||
vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
|
vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
|
||||||
right = normalize(cross(up, arbitrary));
|
right = normalize(cross(up, arbitrary));
|
||||||
fwd = normalize(cross(right, up));
|
fwd = normalize(cross(up, right));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Which side of a world plane a face is on, with zero counting as the positive side. Not GLSL's own
|
||||||
|
// sign(), which returns 0 there and would collapse the coordinate onto a line.
|
||||||
|
float box_face_sign(float n) { return n >= 0.0 ? 1.0 : -1.0; }
|
||||||
|
|
||||||
// Term for term libslic3r's project_planar() / project_cylindrical() / project_spherical(), in mm.
|
// Term for term libslic3r's project_planar() / project_cylindrical() / project_spherical(), in mm.
|
||||||
vec2 projection_raw(vec3 p, vec3 n)
|
vec2 projection_raw(vec3 p, vec3 n)
|
||||||
{
|
{
|
||||||
@@ -64,7 +70,7 @@ vec2 projection_raw(vec3 p, vec3 n)
|
|||||||
vec3 rel = p - patch_center;
|
vec3 rel = p - patch_center;
|
||||||
float x = dot(rel, right);
|
float x = dot(rel, right);
|
||||||
float y = dot(rel, fwd);
|
float y = dot(rel, fwd);
|
||||||
return vec2(atan(y, x) * sqrt(x * x + y * y), dot(rel, up));
|
return vec2(atan(y, x) * sqrt(x * x + y * y), -dot(rel, up));
|
||||||
}
|
}
|
||||||
if (projection_mode == 2) {
|
if (projection_mode == 2) {
|
||||||
vec3 rel = p - patch_center;
|
vec3 rel = p - patch_center;
|
||||||
@@ -72,10 +78,16 @@ vec2 projection_raw(vec3 p, vec3 n)
|
|||||||
if (radius < 1e-8)
|
if (radius < 1e-8)
|
||||||
return vec2(0.0);
|
return vec2(0.0);
|
||||||
vec3 dir = rel / radius;
|
vec3 dir = rel / radius;
|
||||||
return vec2(atan(dir.y, dir.x), asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
return vec2(atan(dir.y, dir.x), -asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
||||||
}
|
}
|
||||||
|
// v negated so the image's first pixel row sits at the top of the model, u flipped on the far side
|
||||||
|
// of each plane so the two faces sharing it are not mirror images - see libslic3r's box_plane_yz().
|
||||||
vec3 an = abs(n);
|
vec3 an = abs(n);
|
||||||
return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
|
if (an.x >= an.y && an.x >= an.z)
|
||||||
|
return vec2(box_face_sign(n.x) * p.y, -p.z);
|
||||||
|
if (an.y >= an.x && an.y >= an.z)
|
||||||
|
return vec2(-box_face_sign(n.y) * p.x, -p.z);
|
||||||
|
return vec2(box_face_sign(n.z) * p.x, -p.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
vec2 project_uv(vec3 p, vec3 n)
|
vec2 project_uv(vec3 p, vec3 n)
|
||||||
|
|||||||
@@ -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;
|
plate->pick_file = it->second->pick_file.empty();
|
||||||
plate->pattern_bbox_file = it->second->pattern_bbox_file;
|
plate->pattern_bbox_file = it->second->pattern_bbox_file.empty();
|
||||||
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.reset();
|
m_fan_mover.release();
|
||||||
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.
|
||||||
|
|||||||
@@ -491,6 +491,57 @@ V2Resolution recommend_v2_resolution(const indexed_triangle_set
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Which side of a world plane a face is on, with an exactly zero component counted as the positive
|
||||||
|
// side. The tie never shows: a normal component of zero carries zero tri-planar blend weight (see
|
||||||
|
// triplanar_weights()), so the plane it orients contributes nothing at that point.
|
||||||
|
inline float box_face_sign(float n) { return n >= 0.f ? 1.f : -1.f; }
|
||||||
|
|
||||||
|
// The box projection's 2D basis for one of the three world planes, oriented the way a viewer
|
||||||
|
// standing outside the model sees that face.
|
||||||
|
//
|
||||||
|
// v runs *down* the image, because that is what v means to the sampler: v = 0 is the texture's first
|
||||||
|
// pixel row, i.e. its top (DecodedHeightTexture::sample(); the UV editor negates Y on screen for the
|
||||||
|
// same reason, see UVEditorCanvas::render()). So v = -z on the four upright planes and v = -y on the
|
||||||
|
// horizontal one, which is what puts the top of an image at the top of the model rather than
|
||||||
|
// upside down.
|
||||||
|
//
|
||||||
|
// u then has to depend on which *side* of the plane the face is on, or one of the two faces sharing
|
||||||
|
// a plane comes out left-right mirrored: with a single u per plane, a portrait image reads upright on
|
||||||
|
// +X and 180 degrees round on -X. Fixing that is the sign below, chosen so that u x v == -normal,
|
||||||
|
// i.e. (right, down, into the surface) is right-handed for a viewer outside:
|
||||||
|
//
|
||||||
|
// face u v face u v face u v
|
||||||
|
// +X +y -z +Y -x -z +Z +x -y
|
||||||
|
// -X -y -z -Y +x -z -Z -x -y
|
||||||
|
//
|
||||||
|
// The sign flips where its own normal component crosses zero, and the blend weight of the plane it
|
||||||
|
// belongs to is exactly zero there, so the blended tri-planar sum stays continuous across the flip -
|
||||||
|
// it is not a return of the hard axis switch described on TextureProjectionMethod::Triplanar.
|
||||||
|
inline Vec2f box_plane_yz(const Vec3f &p, float nx) { return Vec2f(box_face_sign(nx) * p.y(), -p.z()); }
|
||||||
|
inline Vec2f box_plane_xz(const Vec3f &p, float ny) { return Vec2f(-box_face_sign(ny) * p.x(), -p.z()); }
|
||||||
|
inline Vec2f box_plane_xy(const Vec3f &p, float nz) { return Vec2f(box_face_sign(nz) * p.x(), -p.y()); }
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void box_projection_axes(const Vec3f &normal, Vec3f &u_axis, Vec3f &v_axis)
|
||||||
|
{
|
||||||
|
// The same table as box_plane_yz()/box_plane_xz()/box_plane_xy(), handed out as a pair of world
|
||||||
|
// directions for the plane the normal is closest to. Two shapes of one table, because the bake's
|
||||||
|
// hot path wants the closed form and the GUI wants the axes; the test suite checks them against
|
||||||
|
// each other so they cannot drift apart.
|
||||||
|
const Vec3f n = normal.cwiseAbs();
|
||||||
|
if (n.x() >= n.y() && n.x() >= n.z()) {
|
||||||
|
u_axis = box_face_sign(normal.x()) * Vec3f::UnitY();
|
||||||
|
v_axis = -Vec3f::UnitZ();
|
||||||
|
} else if (n.y() >= n.x() && n.y() >= n.z()) {
|
||||||
|
u_axis = -box_face_sign(normal.y()) * Vec3f::UnitX();
|
||||||
|
v_axis = -Vec3f::UnitZ();
|
||||||
|
} else {
|
||||||
|
u_axis = box_face_sign(normal.z()) * Vec3f::UnitX();
|
||||||
|
v_axis = -Vec3f::UnitY();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Vec2f project_planar(const Vec3f &position, const Vec3f &normal)
|
Vec2f project_planar(const Vec3f &position, const Vec3f &normal)
|
||||||
{
|
{
|
||||||
// Planar-project onto the two axes orthogonal to the dominant component of `normal`. Called
|
// Planar-project onto the two axes orthogonal to the dominant component of `normal`. Called
|
||||||
@@ -501,23 +552,26 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal)
|
|||||||
// other face in the same patch - exactly the bug an earlier version of this feature had).
|
// other face in the same patch - exactly the bug an earlier version of this feature had).
|
||||||
const Vec3f n = normal.cwiseAbs();
|
const Vec3f n = normal.cwiseAbs();
|
||||||
if (n.x() >= n.y() && n.x() >= n.z())
|
if (n.x() >= n.y() && n.x() >= n.z())
|
||||||
return Vec2f(position.y(), position.z());
|
return box_plane_yz(position, normal.x());
|
||||||
if (n.y() >= n.x() && n.y() >= n.z())
|
if (n.y() >= n.x() && n.y() >= n.z())
|
||||||
return Vec2f(position.x(), position.z());
|
return box_plane_xz(position, normal.y());
|
||||||
return Vec2f(position.x(), position.y());
|
return box_plane_xy(position, normal.z());
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// Wrapped around patch_axis, centered at patch_center. u is the arc length (mm) around the axis at
|
// Wrapped around patch_axis, centered at patch_center. u is the arc length (mm) around the axis at
|
||||||
// this point's own radius, v is the signed distance along the axis - a reasonable approximation
|
// this point's own radius, v is the distance along the axis, measured *against* it - a reasonable
|
||||||
// for roughly cylindrical selections, not an exact fit for arbitrary geometry.
|
// approximation for roughly cylindrical selections, not an exact fit for arbitrary geometry.
|
||||||
Vec2f project_cylindrical(const Vec3f &position, const Vec3f &patch_center, const Vec3f &patch_axis)
|
Vec2f project_cylindrical(const Vec3f &position, const Vec3f &patch_center, const Vec3f &patch_axis)
|
||||||
{
|
{
|
||||||
Vec3f up = patch_axis;
|
Vec3f up = patch_axis;
|
||||||
up = (up.norm() > 1e-8f) ? Vec3f(up.normalized()) : Vec3f::UnitZ();
|
up = (up.norm() > 1e-8f) ? Vec3f(up.normalized()) : Vec3f::UnitZ();
|
||||||
const Vec3f arbitrary = (std::abs(up.dot(Vec3f::UnitZ())) < 0.9f) ? Vec3f::UnitZ() : Vec3f::UnitX();
|
const Vec3f arbitrary = (std::abs(up.dot(Vec3f::UnitZ())) < 0.9f) ? Vec3f::UnitZ() : Vec3f::UnitX();
|
||||||
const Vec3f right = Vec3f(up.cross(arbitrary).normalized());
|
const Vec3f right = Vec3f(up.cross(arbitrary).normalized());
|
||||||
const Vec3f fwd = Vec3f(right.cross(up).normalized());
|
// up x right, not right x up: that makes (right, fwd, up) right-handed, so the angle below grows
|
||||||
|
// anticlockwise about the axis - which, seen from outside the cylinder, is to the viewer's right.
|
||||||
|
// The other way round the texture wraps on mirrored, as if seen through the model from behind.
|
||||||
|
const Vec3f fwd = Vec3f(up.cross(right).normalized());
|
||||||
|
|
||||||
const Vec3f rel = position - patch_center;
|
const Vec3f rel = position - patch_center;
|
||||||
const float along_axis = rel.dot(up);
|
const float along_axis = rel.dot(up);
|
||||||
@@ -526,7 +580,9 @@ Vec2f project_cylindrical(const Vec3f &position, const Vec3f &patch_center, cons
|
|||||||
const float radius = std::sqrt(x * x + y * y);
|
const float radius = std::sqrt(x * x + y * y);
|
||||||
const float angle = std::atan2(y, x);
|
const float angle = std::atan2(y, x);
|
||||||
|
|
||||||
return Vec2f(angle * radius, along_axis);
|
// Negated, like every other projection's v: v = 0 is the texture's top row, so v has to grow
|
||||||
|
// *down* the axis for the top of the image to sit at the top of the model.
|
||||||
|
return Vec2f(angle * radius, -along_axis);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Longitude/latitude around patch_center. u/v are scaled by this point's own distance from the
|
// Longitude/latitude around patch_center. u/v are scaled by this point's own distance from the
|
||||||
@@ -542,7 +598,9 @@ Vec2f project_spherical(const Vec3f &position, const Vec3f &patch_center)
|
|||||||
const Vec3f dir = rel / radius;
|
const Vec3f dir = rel / radius;
|
||||||
const float longitude = std::atan2(dir.y(), dir.x());
|
const float longitude = std::atan2(dir.y(), dir.x());
|
||||||
const float latitude = std::asin(std::clamp(dir.z(), -1.f, 1.f));
|
const float latitude = std::asin(std::clamp(dir.z(), -1.f, 1.f));
|
||||||
return Vec2f(longitude, latitude) * radius;
|
// Longitude already grows to the right as seen from outside; latitude grows toward the north
|
||||||
|
// pole, so it is negated to put the image's top row (v = 0) at the top of the model.
|
||||||
|
return Vec2f(longitude, -latitude) * radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CGAL's LSCM parameterizer expects a clean mesh with no isolated (unreferenced) vertices - but
|
// CGAL's LSCM parameterizer expects a clean mesh with no isolated (unreferenced) vertices - but
|
||||||
@@ -837,9 +895,26 @@ std::vector<int> segment_into_charts(const indexed_triangle_set &mesh, const std
|
|||||||
// solve at all, and why this also serves as the fallback for a chart LSCM cannot handle.
|
// solve at all, and why this also serves as the fallback for a chart LSCM cannot handle.
|
||||||
std::vector<Vec2f> project_to_tangent_plane(const indexed_triangle_set &chart, const Vec3f &normal)
|
std::vector<Vec2f> project_to_tangent_plane(const indexed_triangle_set &chart, const Vec3f &normal)
|
||||||
{
|
{
|
||||||
const Vec3f seed = (std::abs(normal.z()) < 0.9f) ? Vec3f::UnitZ() : Vec3f::UnitX();
|
// Oriented like the box projection (see box_projection_axes()), so a chart reads the right way up
|
||||||
const Vec3f u = Vec3f(seed.cross(normal).normalized());
|
// and unmirrored instead of being flattened to whichever basis a cross product happened to give.
|
||||||
const Vec3f v = Vec3f(normal.cross(u).normalized());
|
// For a chart whose normal *is* a world axis - a cube face, which is the common case once the
|
||||||
|
// seam angle has cut the patch along its sharp edges - this is exactly the box basis, so such a
|
||||||
|
// face unwraps to the same orientation Triplanar would have given it.
|
||||||
|
Vec3f u, v;
|
||||||
|
box_projection_axes(normal, u, v);
|
||||||
|
// The box axes lie in the tangent plane already when the normal is a world axis; for any other
|
||||||
|
// normal, u is squared up against it and v follows. u x v == -normal keeps the chart the way
|
||||||
|
// round a viewer outside the model sees it (a plain normal x u would mirror it).
|
||||||
|
u = u - normal * normal.dot(u);
|
||||||
|
if (u.norm() <= 1e-6f) {
|
||||||
|
// Out of reach for a unit normal - the box basis puts u perpendicular to the normal's own
|
||||||
|
// dominant axis, so the two can never be within 45 degrees of each other - but a caller
|
||||||
|
// handing over an unnormalized normal should still get a usable basis rather than a NaN.
|
||||||
|
const Vec3f seed = (std::abs(normal.z()) < 0.9f) ? Vec3f::UnitZ() : Vec3f::UnitX();
|
||||||
|
u = seed.cross(normal);
|
||||||
|
}
|
||||||
|
u = Vec3f(u.normalized());
|
||||||
|
v = Vec3f(u.cross(normal).normalized());
|
||||||
|
|
||||||
std::vector<Vec2f> uvs(chart.vertices.size());
|
std::vector<Vec2f> uvs(chart.vertices.size());
|
||||||
for (size_t i = 0; i < chart.vertices.size(); ++i)
|
for (size_t i = 0; i < chart.vertices.size(); ++i)
|
||||||
@@ -1814,8 +1889,10 @@ float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisp
|
|||||||
return texture.sample(uv, layer.tile_enabled, layer.tile_method);
|
return texture.sample(uv, layer.tile_enabled, layer.tile_method);
|
||||||
}
|
}
|
||||||
// Flat projection onto the captured projector plane. Single-valued per point, so unlike
|
// Flat projection onto the captured projector plane. Single-valued per point, so unlike
|
||||||
// blended triplanar it is one sample, and it is what "project from view" places.
|
// blended triplanar it is one sample, and it is what "project from view" places. The
|
||||||
return sample_at(Vec2f(position.dot(layer.view_project_right), position.dot(layer.view_project_up)));
|
// projector's up axis is negated because v runs *down* the image (v = 0 is its first pixel
|
||||||
|
// row), the same reason the projective branch above measures window y downward.
|
||||||
|
return sample_at(Vec2f(position.dot(layer.view_project_right), -position.dot(layer.view_project_up)));
|
||||||
case TextureProjectionMethod::LSCM: // no usable unwrap for this patch - fall back to Triplanar
|
case TextureProjectionMethod::LSCM: // no usable unwrap for this patch - fall back to Triplanar
|
||||||
case TextureProjectionMethod::Triplanar:
|
case TextureProjectionMethod::Triplanar:
|
||||||
default: break;
|
default: break;
|
||||||
@@ -1830,14 +1907,15 @@ float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisp
|
|||||||
const float w_sum = w.x() + w.y() + w.z();
|
const float w_sum = w.x() + w.y() + w.z();
|
||||||
if (w_sum < 1e-8f)
|
if (w_sum < 1e-8f)
|
||||||
// Degenerate normal: no axis is meaningfully dominant, so no blend is meaningful either.
|
// Degenerate normal: no axis is meaningfully dominant, so no blend is meaningful either.
|
||||||
return sample_at(Vec2f(position.x(), position.y()));
|
return sample_at(box_plane_xy(position, normal.z()));
|
||||||
w /= w_sum;
|
w /= w_sum;
|
||||||
|
|
||||||
// Each plane drops the axis it is named for, matching project_planar()'s own convention (which
|
// Each plane drops the axis it is named for and is oriented for the side of that plane the normal
|
||||||
// the GUI's on-canvas placement gizmo also relies on).
|
// points to, matching project_planar()'s own convention (which the GUI's on-canvas placement
|
||||||
return w.x() * sample_at(Vec2f(position.y(), position.z())) +
|
// gizmo also relies on). See box_plane_yz() for why the orientation is not simply (y, z).
|
||||||
w.y() * sample_at(Vec2f(position.x(), position.z())) +
|
return w.x() * sample_at(box_plane_yz(position, normal.x())) +
|
||||||
w.z() * sample_at(Vec2f(position.x(), position.y()));
|
w.y() * sample_at(box_plane_xz(position, normal.y())) +
|
||||||
|
w.z() * sample_at(box_plane_xy(position, normal.z()));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDisplacementLayer &layer,
|
bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDisplacementLayer &layer,
|
||||||
@@ -1899,7 +1977,8 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
|
|||||||
out = texture.sample_color(uv, layer.tile_enabled, layer.tile_method);
|
out = texture.sample_color(uv, layer.tile_enabled, layer.tile_method);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
const Vec2f p(position.dot(layer.view_project_right), position.dot(layer.view_project_up));
|
// Negated up, exactly as sample_layer_height() does it.
|
||||||
|
const Vec2f p(position.dot(layer.view_project_right), -position.dot(layer.view_project_up));
|
||||||
if (!covered(p))
|
if (!covered(p))
|
||||||
return false;
|
return false;
|
||||||
out = sample_at(p);
|
out = sample_at(p);
|
||||||
@@ -1915,7 +1994,7 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
|
|||||||
Vec3f w = triplanar_weights(normal);
|
Vec3f w = triplanar_weights(normal);
|
||||||
const float w_sum = w.x() + w.y() + w.z();
|
const float w_sum = w.x() + w.y() + w.z();
|
||||||
if (w_sum < 1e-8f) {
|
if (w_sum < 1e-8f) {
|
||||||
const Vec2f p(position.x(), position.y());
|
const Vec2f p = box_plane_xy(position, normal.z());
|
||||||
if (!covered(p))
|
if (!covered(p))
|
||||||
return false;
|
return false;
|
||||||
out = sample_at(p);
|
out = sample_at(p);
|
||||||
@@ -1926,8 +2005,8 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
|
|||||||
// A blend of three planes is only "not covered" where *every* contributing plane is outside the
|
// A blend of three planes is only "not covered" where *every* contributing plane is outside the
|
||||||
// placement; where some are, the covered ones are renormalised so the colour does not fade toward
|
// placement; where some are, the covered ones are renormalised so the colour does not fade toward
|
||||||
// black at the edge of an untiled tri-planar layer.
|
// black at the edge of an untiled tri-planar layer.
|
||||||
const std::array<Vec2f, 3> planes = { Vec2f(position.y(), position.z()), Vec2f(position.x(), position.z()),
|
const std::array<Vec2f, 3> planes = { box_plane_yz(position, normal.x()), box_plane_xz(position, normal.y()),
|
||||||
Vec2f(position.x(), position.y()) };
|
box_plane_xy(position, normal.z()) };
|
||||||
Vec3f acc = Vec3f::Zero();
|
Vec3f acc = Vec3f::Zero();
|
||||||
float acc_w = 0.f;
|
float acc_w = 0.f;
|
||||||
for (int i = 0; i < 3; ++i)
|
for (int i = 0; i < 3; ++i)
|
||||||
|
|||||||
@@ -524,11 +524,24 @@ struct TextureColorSettings
|
|||||||
bool empty() const { return palette.empty(); }
|
bool empty() const { return palette.empty(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The two world directions the box projection takes its u and v along on a face whose outward
|
||||||
|
// normal is `normal`: the plane is the one that normal is closest to, and the pair is oriented as a
|
||||||
|
// viewer standing outside the model sees it - u to their right, v *down* the image, which is the
|
||||||
|
// direction v runs in the texture (v = 0 is the image's first pixel row, see
|
||||||
|
// DecodedHeightTexture::sample()). So an image comes out the way it looks in a file manager rather
|
||||||
|
// than upside down, and it is not mirrored on the three faces facing the other way.
|
||||||
|
//
|
||||||
|
// Exposed because the GUI needs the same basis for the on-canvas "adjust texture placement" gizmo
|
||||||
|
// (its ring's zero angle and its offset handle's plane), and it must not be a second, drifting copy
|
||||||
|
// of the table.
|
||||||
|
void box_projection_axes(const Vec3f &normal, Vec3f &u_axis, Vec3f &v_axis);
|
||||||
|
|
||||||
// Raw dominant-axis planar projection of `position` (in mm, not yet scaled/rotated/offset by any
|
// Raw dominant-axis planar projection of `position` (in mm, not yet scaled/rotated/offset by any
|
||||||
// layer), dropping the axis position that best aligns with `normal`. Exposed on its own (rather
|
// layer), dropping the axis position that best aligns with `normal`. Equivalent to projecting onto
|
||||||
// than only inline inside project_texture_displacement_uv()) so GUI code - the on-canvas
|
// box_projection_axes()' pair. Exposed on its own (rather than only inline inside
|
||||||
// "adjust texture placement" gizmo - can map a dragged 3D point into the exact same 2D space
|
// project_texture_displacement_uv()) so GUI code - the on-canvas "adjust texture placement" gizmo -
|
||||||
// tiling_scale/rotation_deg/offset operate in, without duplicating the axis-selection logic.
|
// can map a dragged 3D point into the exact same 2D space tiling_scale/rotation_deg/offset operate
|
||||||
|
// in, without duplicating the axis-selection logic.
|
||||||
Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
|
Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
|
||||||
|
|
||||||
// Applies a layer's tiling_scale/rotation_deg/offset to an already-projected planar coordinate
|
// Applies a layer's tiling_scale/rotation_deg/offset to an already-projected planar coordinate
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
#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"
|
||||||
@@ -173,26 +172,7 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -211,9 +191,11 @@ 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);
|
||||||
m_star_filled = ScalableBitmap(this, "score_star_light", 16);
|
#endif
|
||||||
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);
|
||||||
|
|
||||||
@@ -438,7 +420,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,9 +531,10 @@ 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++) {
|
||||||
if (selected_info && it->filament_id.compare(selected_info->filament_id) == 0) {
|
auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
|
||||||
|
std::string filament_id = filament_item.filament_id;
|
||||||
|
if (it->filament_id.compare(filament_id) == 0) {
|
||||||
selected_ams_id = it->filament_id;
|
selected_ams_id = it->filament_id;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -700,13 +682,12 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
|
|||||||
ams_setting_id = "";
|
ams_setting_id = "";
|
||||||
|
|
||||||
// the combobox item
|
// the combobox item
|
||||||
const FilamentInfos* selected_info = selected_filament_info();
|
auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
|
||||||
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_id, ams_filament_id, ams_setting_id);
|
filament_item.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;
|
||||||
@@ -1005,133 +986,6 @@ 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;
|
||||||
@@ -1145,12 +999,13 @@ 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();
|
||||||
@@ -1180,7 +1035,7 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
|||||||
if (alias.empty())
|
if (alias.empty())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
m_sorted_filament_items.push_back(alias);
|
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;
|
||||||
@@ -1189,6 +1044,7 @@ 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);
|
||||||
|
|
||||||
|
|
||||||
@@ -1359,11 +1215,24 @@ 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(m_sorted_filament_items.begin(), m_sorted_filament_items.end(), _filament_sorter);
|
std::sort(filament_items.begin(), filament_items.end(), _filament_sorter);
|
||||||
}
|
}
|
||||||
|
|
||||||
rebuild_filament_dropdown_items(ams_filament_id);
|
// traverse the hint selection idx
|
||||||
int selection_idx = m_comboBox_filament->GetSelection();
|
int selection_idx = -1;
|
||||||
|
{
|
||||||
|
for(int i = 0; i < filament_items.size(); i++)
|
||||||
|
{
|
||||||
|
if (hint_filament_name == filament_items[i])
|
||||||
|
{
|
||||||
|
selection_idx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_comboBox_filament->Set(filament_items);
|
||||||
|
m_comboBox_filament->SetSelection(selection_idx);
|
||||||
post_select_event(selection_idx);
|
post_select_event(selection_idx);
|
||||||
|
|
||||||
if (selection_idx < 0) {
|
if (selection_idx < 0) {
|
||||||
@@ -1420,7 +1289,6 @@ 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;
|
||||||
@@ -1441,8 +1309,9 @@ 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.
|
||||||
if (selected_info) {
|
const std::string selected = m_comboBox_filament->GetValue().ToStdString();
|
||||||
const std::string& filament_id = selected_info->filament_id;
|
if (!selected.empty()) {
|
||||||
|
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)
|
||||||
@@ -1495,7 +1364,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 || !selected_info) {
|
if ( !this->obj || m_filament_selection < 0) {
|
||||||
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
|
||||||
@@ -1514,9 +1383,21 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
|||||||
ams_filament_id = "";
|
ams_filament_id = "";
|
||||||
ams_setting_id = "";
|
ams_setting_id = "";
|
||||||
|
|
||||||
if (selected_info) {
|
if (preset_bundle) {
|
||||||
ams_filament_id = selected_info->filament_id;
|
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
|
||||||
ams_setting_id = selected_info->setting_id;
|
auto itor = map_filament_items.find(m_comboBox_filament->GetValue().ToStdString());
|
||||||
|
if ( itor != map_filament_items.end()) {
|
||||||
|
ams_filament_id = itor->second.filament_id;
|
||||||
|
ams_setting_id = itor->second.setting_id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it->alias.compare(m_comboBox_filament->GetValue().ToStdString()) == 0) {
|
||||||
|
ams_filament_id = it->filament_id;
|
||||||
|
ams_setting_id = it->setting_id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wxArrayString items;
|
wxArrayString items;
|
||||||
@@ -1648,11 +1529,6 @@ 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,7 +31,6 @@
|
|||||||
#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)
|
||||||
@@ -157,7 +156,6 @@ 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);
|
||||||
@@ -165,11 +163,6 @@ 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
|
||||||
@@ -209,7 +202,12 @@ 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;
|
||||||
|
|
||||||
@@ -217,20 +215,7 @@ protected:
|
|||||||
std::string filament_id;
|
std::string filament_id;
|
||||||
std::string setting_id;
|
std::string setting_id;
|
||||||
};
|
};
|
||||||
struct FilamentListItem {
|
std::map<std::string, FilamentInfos> map_filament_items;
|
||||||
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,9 +25,8 @@ 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)
|
||||||
const wxString& profile)
|
: WebPanel(parent, web_hosting::orca_bridge_script())
|
||||||
: 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,8 +6,6 @@
|
|||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <wx/string.h>
|
|
||||||
|
|
||||||
class wxWindow;
|
class wxWindow;
|
||||||
|
|
||||||
namespace Slic3r { namespace GUI {
|
namespace Slic3r { namespace GUI {
|
||||||
@@ -29,8 +27,7 @@ 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,16 +3232,9 @@ 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 snapshot(plater, _u8L("Shift objects to bed"));
|
Plater::TakeSnapshot(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,13 +2779,12 @@ 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 snapshot(plater, "Remove support painting");
|
Plater::TakeSnapshot(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
|
||||||
|
|
||||||
@@ -2796,21 +2795,19 @@ 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 snapshot(plater, "Remove color painting");
|
Plater::TakeSnapshot(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 snapshot(plater, _u8L("Remove paint-on fuzzy skin"));
|
Plater::TakeSnapshot(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;
|
||||||
@@ -6721,7 +6718,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 snapshot(plater, _u8L("Shift objects to bed"));
|
Plater::TakeSnapshot(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();
|
||||||
|
|||||||
@@ -1252,8 +1252,9 @@ std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_vertex_uvs(const in
|
|||||||
uv[vi] = Vec2f(-1e6f, -1e6f);
|
uv[vi] = Vec2f(-1e6f, -1e6f);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Negated up, as sample_layer_height()'s affine branch has it: v runs down the image.
|
||||||
const Vec2f planar(patch.vertices[vi].dot(layer.view_project_right),
|
const Vec2f planar(patch.vertices[vi].dot(layer.view_project_right),
|
||||||
patch.vertices[vi].dot(layer.view_project_up));
|
-patch.vertices[vi].dot(layer.view_project_up));
|
||||||
uv[vi] = apply_uv_transform(planar, layer, aspect);
|
uv[vi] = apply_uv_transform(planar, layer, aspect);
|
||||||
}
|
}
|
||||||
return uv;
|
return uv;
|
||||||
@@ -3292,19 +3293,10 @@ Vec3f GLGizmoTextureDisplacement::adjust_handle_center(const TextureDisplacement
|
|||||||
|
|
||||||
void GLGizmoTextureDisplacement::adjust_tangent_basis(Vec3f &u_axis, Vec3f &v_axis) const
|
void GLGizmoTextureDisplacement::adjust_tangent_basis(Vec3f &u_axis, Vec3f &v_axis) const
|
||||||
{
|
{
|
||||||
// Mirrors project_planar()'s own dominant-axis choice exactly, so the ring's "0 degrees" and
|
// libslic3r's own basis, asked for rather than restated here, so the ring's "0 degrees" and the
|
||||||
// the offset handle's plane always agree with what project_texture_displacement_uv() does.
|
// offset handle's plane always agree with what project_texture_displacement_uv() does - including
|
||||||
const Vec3f n = m_adjust_anchor_normal.cwiseAbs();
|
// v pointing *down* the texture and u flipping on the far side of each plane.
|
||||||
if (n.x() >= n.y() && n.x() >= n.z()) {
|
box_projection_axes(m_adjust_anchor_normal, u_axis, v_axis);
|
||||||
u_axis = Vec3f::UnitY();
|
|
||||||
v_axis = Vec3f::UnitZ();
|
|
||||||
} else if (n.y() >= n.x() && n.y() >= n.z()) {
|
|
||||||
u_axis = Vec3f::UnitX();
|
|
||||||
v_axis = Vec3f::UnitZ();
|
|
||||||
} else {
|
|
||||||
u_axis = Vec3f::UnitX();
|
|
||||||
v_axis = Vec3f::UnitY();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLGizmoTextureDisplacement::render_adjust_texture_gizmo()
|
void GLGizmoTextureDisplacement::render_adjust_texture_gizmo()
|
||||||
|
|||||||
@@ -423,8 +423,8 @@ private:
|
|||||||
// 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.
|
||||||
void render_paint_cursor_hint();
|
void render_paint_cursor_hint();
|
||||||
// Mesh-local tangent-plane basis at m_adjust_anchor_normal, matching project_planar()'s
|
// Mesh-local tangent-plane basis at m_adjust_anchor_normal: box_projection_axes(), so dragging
|
||||||
// dominant-axis convention so dragging on-canvas maps consistently onto offset.
|
// on-canvas maps consistently onto offset whichever face the anchor is on.
|
||||||
void adjust_tangent_basis(Vec3f &u_axis, Vec3f &v_axis) const;
|
void adjust_tangent_basis(Vec3f &u_axis, Vec3f &v_axis) const;
|
||||||
|
|
||||||
// The plane a drag is measured against: the paint patch's anchor, lifted clear of the surface.
|
// The plane a drag is measured against: the paint patch's anchor, lifted clear of the surface.
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ 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))
|
||||||
@@ -38,7 +37,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), profile);
|
create_webview(web_hosting::BOOTSTRAP_PAGE, title, size, wxSize(320, 240));
|
||||||
|
|
||||||
// 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,8 +49,7 @@ 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, const wxString& profile)
|
WebPanel::WebPanel(wxWindow* parent, const char* bridge_script)
|
||||||
: 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, const wxString&
|
|||||||
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(), profile);
|
m_browser = WebView::CreateWebView(this, web_hosting::bootstrap_url());
|
||||||
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,7 +4,6 @@
|
|||||||
|
|
||||||
#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>
|
||||||
@@ -23,7 +22,7 @@ namespace Slic3r { namespace GUI {
|
|||||||
class WebPanel : public wxPanel
|
class WebPanel : public wxPanel
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
WebPanel(wxWindow* parent, const char* bridge_script, const wxString& profile = wxEmptyString);
|
WebPanel(wxWindow* parent, const char* bridge_script);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
wxWebView* browser() const { return m_browser; }
|
wxWebView* browser() const { return m_browser; }
|
||||||
|
|||||||
@@ -82,11 +82,6 @@ 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);
|
||||||
@@ -256,7 +251,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, wxNullBitmap, clientData, group_key, group_label};
|
Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, 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);
|
||||||
@@ -302,12 +297,6 @@ 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)
|
||||||
@@ -361,13 +350,6 @@ 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,
|
||||||
@@ -375,7 +357,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, wxNullBitmap, clientData ? clientData[i] : NULL };
|
Item item { items[i], wxEmptyString, wxNullBitmap, wxNullBitmap, clientData ? clientData[i] : NULL };
|
||||||
this->items.insert(this->items.begin() + pos, item);
|
this->items.insert(this->items.begin() + pos, item);
|
||||||
++pos;
|
++pos;
|
||||||
}
|
}
|
||||||
@@ -437,18 +419,16 @@ 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:
|
||||||
// Step over disabled rows: the mouse cannot select them either.
|
if ((event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) && GetSelection() > 0) {
|
||||||
const int step = (event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) ? -1 : 1;
|
SetSelection(GetSelection() - 1);
|
||||||
int n = GetSelection() + step;
|
} else if ((event.GetKeyCode() == WXK_DOWN || event.GetKeyCode() == WXK_RIGHT) && GetSelection() + 1 < items.size()) {
|
||||||
while (n >= 0 && n < int(items.size()) && (items[n].style & DD_ITEM_STYLE_DISABLED))
|
SetSelection(GetSelection() + 1);
|
||||||
n += step;
|
} else {
|
||||||
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,12 +96,10 @@ 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,7 +30,6 @@
|
|||||||
#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)
|
||||||
|
|
||||||
@@ -243,19 +242,6 @@ 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
|
||||||
@@ -385,10 +371,6 @@ 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;
|
||||||
@@ -452,24 +434,13 @@ 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()) {
|
||||||
int max_text_width = rcContent.GetRight() - pt.x;
|
if (is_hover && item.tip.IsEmpty())
|
||||||
if (max_text_width <= 0) {
|
SetToolTip(text);
|
||||||
text.clear();
|
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END,
|
||||||
} else {
|
rcContent.GetRight() - pt.x);
|
||||||
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());
|
||||||
@@ -558,7 +529,6 @@ 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());
|
||||||
@@ -595,13 +565,8 @@ 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)
|
||||||
@@ -671,11 +636,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -754,15 +714,6 @@ 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
|
||||||
@@ -879,17 +830,6 @@ 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,7 +5,6 @@
|
|||||||
#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>
|
||||||
@@ -28,7 +27,6 @@ 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
|
||||||
{
|
{
|
||||||
@@ -39,7 +37,6 @@ 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{};
|
||||||
@@ -68,7 +65,6 @@ private:
|
|||||||
|
|
||||||
wxSize textSize;
|
wxSize textSize;
|
||||||
wxSize iconSize;
|
wxSize iconSize;
|
||||||
int actionIconWidth = 0;
|
|
||||||
wxSize rowSize;
|
wxSize rowSize;
|
||||||
|
|
||||||
StateHandler state_handler;
|
StateHandler state_handler;
|
||||||
@@ -139,7 +135,6 @@ 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();
|
||||||
@@ -153,7 +148,6 @@ private:
|
|||||||
void mouseWheelMoved(wxMouseEvent &event);
|
void mouseWheelMoved(wxMouseEvent &event);
|
||||||
|
|
||||||
void sendDropDownEvent();
|
void sendDropDownEvent();
|
||||||
void sendDropDownActionEvent(int index);
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE_EVENT_TABLE()
|
DECLARE_EVENT_TABLE()
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
#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>
|
||||||
|
|
||||||
@@ -14,7 +12,6 @@
|
|||||||
#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>
|
||||||
@@ -75,26 +72,6 @@ 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() {
|
||||||
@@ -158,9 +135,6 @@ 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");
|
||||||
@@ -239,11 +213,6 @@ public:
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
explicit WebViewWebKit(const wxWebViewConfiguration& config)
|
|
||||||
: wxWebViewWebKit(config)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
~WebViewWebKit() override
|
~WebViewWebKit() override
|
||||||
{
|
{
|
||||||
RemoveScriptMessageHandler("wx");
|
RemoveScriptMessageHandler("wx");
|
||||||
@@ -315,7 +284,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, wxString const & profile)
|
wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url)
|
||||||
{
|
{
|
||||||
#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
|
||||||
@@ -335,36 +304,12 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url, wxStr
|
|||||||
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 = nullptr;
|
wxWebView* webView = new WebViewEdge;
|
||||||
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 = nullptr;
|
wxWebView* webView = new WebViewWebKit;
|
||||||
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
|
||||||
wxWebView* webView = nullptr;
|
auto webView = wxWebView::New();
|
||||||
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,11 +14,7 @@ wxDECLARE_EVENT(EVT_WEBVIEW_RECREATED, wxCommandEvent);
|
|||||||
class WebView
|
class WebView
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
// `profile` empty -> the shared default backend store used by all trusted UI.
|
static wxWebView *CreateWebView(wxWindow *parent, wxString const &url);
|
||||||
// `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,8 +172,7 @@ 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));
|
||||||
@@ -182,7 +181,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, profile);
|
m_browser = WebView::CreateWebView(this, wxEmptyString);
|
||||||
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,8 +38,7 @@ 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,7 +12,6 @@ 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,15 +102,6 @@ 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,8 +390,7 @@ 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();
|
||||||
@@ -488,7 +487,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), wxString::FromUTF8(plugin_key));
|
std::move(on_destroyed));
|
||||||
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,8 +77,7 @@ 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,40 +2043,3 @@ 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);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1652,6 +1652,321 @@ TEST_CASE("Per-corner LSCM UVs give each triangle its own island's placement", "
|
|||||||
CHECK(differs);
|
CHECK(differs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
// UV orientation: which way up a texture lands on the model.
|
||||||
|
//
|
||||||
|
// v = 0 is the height map's *first* pixel row, i.e. its top (DecodedHeightTexture::sample(), and
|
||||||
|
// the UV editor negates Y on screen for the same reason). So "the image is the right way up" is the
|
||||||
|
// statement that the row a point samples gets *lower* as the point moves up the face, and "it is
|
||||||
|
// not left-right mirrored" is that the column gets higher as the point moves to the right of a
|
||||||
|
// viewer standing outside the model. Both are measured below by sampling a ramp whose value says
|
||||||
|
// which row (or column) it came from, which settles the question without having to reason about the
|
||||||
|
// uv transform at all.
|
||||||
|
|
||||||
|
static constexpr int RAMP_N = 64; // the ramp texture's size, in texels
|
||||||
|
static constexpr float RAMP_TILING = 40.f; // mm per tile
|
||||||
|
static constexpr float CUBE_HALF = 5.f; // half extent of the cube the probes walk over
|
||||||
|
// How far a probe steps off a face centre. Short on purpose: the cylindrical and spherical u is an
|
||||||
|
// arc length (angle * radius), so a long step also changes the radius, and that term swamps the
|
||||||
|
// angular one. It is symmetric about the face centre, so at a short step it cancels between the two
|
||||||
|
// readings a check compares.
|
||||||
|
static constexpr float RAMP_STEP = 1.f;
|
||||||
|
// What one step's worth of travel is in ramp rows: 2 * RAMP_STEP mm of separation, RAMP_N texels per
|
||||||
|
// RAMP_TILING mm. Halved as the margin a check demands, so "flat" cannot pass as either direction.
|
||||||
|
static constexpr double RAMP_MARGIN = RAMP_N * RAMP_STEP / RAMP_TILING;
|
||||||
|
|
||||||
|
// A height map whose rows (or columns) carry a linear ramp: row r holds 3 + 4r, so the sampled
|
||||||
|
// height identifies the row. Built directly rather than through a PNG because these tests are about
|
||||||
|
// the projection, not the decoder - decode_height_texture() has its own tests.
|
||||||
|
static DecodedHeightTexture ramp_texture(bool by_row)
|
||||||
|
{
|
||||||
|
DecodedHeightTexture t;
|
||||||
|
t.width = t.height = RAMP_N;
|
||||||
|
t.pixels.resize(size_t(RAMP_N) * size_t(RAMP_N));
|
||||||
|
for (int y = 0; y < RAMP_N; ++y)
|
||||||
|
for (int x = 0; x < RAMP_N; ++x)
|
||||||
|
t.pixels[size_t(y) * size_t(RAMP_N) + size_t(x)] = uint8_t(3 + 4 * (by_row ? y : x));
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// uv 0.5 at the cube's centre, and a tile wide enough that every point the probes visit stays well
|
||||||
|
// inside the one tile - so no reading is a wrapped-around neighbour of the one it is compared with.
|
||||||
|
static TextureDisplacementLayer ramp_layer(TextureProjectionMethod method)
|
||||||
|
{
|
||||||
|
TextureDisplacementLayer layer;
|
||||||
|
layer.projection_method = method;
|
||||||
|
layer.tiling_scale = RAMP_TILING;
|
||||||
|
layer.offset = Vec2f(0.5f, 0.5f);
|
||||||
|
layer.depth_mm = 1.f;
|
||||||
|
return layer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Which row (or column) of `ramp` the layer reads at `p`. Fractional, because a point between two
|
||||||
|
// texels reads the ramp between them; only the direction it moves in matters here.
|
||||||
|
static double ramp_index(const DecodedHeightTexture &ramp, const TextureDisplacementLayer &layer, const Vec3f &p,
|
||||||
|
const Vec3f &n, const Vec3f ¢er = Vec3f(Vec3f::Zero()),
|
||||||
|
const Vec3f &axis = Vec3f(Vec3f::UnitZ()))
|
||||||
|
{
|
||||||
|
return (double(sample_layer_height(ramp, layer, p, n, center, axis, nullptr)) * 255.0 - 3.0) / 4.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// How a viewer standing outside the model sees a face: their right, and their up. Up is world +Z on
|
||||||
|
// the four upright faces; on the top and bottom, where +Z is edge on, it is world +Y.
|
||||||
|
static void outside_viewer_frame(const Vec3f &n, Vec3f &right, Vec3f &up)
|
||||||
|
{
|
||||||
|
up = (std::abs(n.z()) < 0.9f) ? Vec3f(0.f, 0.f, 1.f) : Vec3f(0.f, 1.f, 0.f);
|
||||||
|
right = Vec3f((-n).cross(up).normalized());
|
||||||
|
}
|
||||||
|
|
||||||
|
static const std::array<std::pair<const char *, Vec3f>, 6> CUBE_FACES = { {
|
||||||
|
{ "+X", Vec3f(1.f, 0.f, 0.f) },
|
||||||
|
{ "-X", Vec3f(-1.f, 0.f, 0.f) },
|
||||||
|
{ "+Y", Vec3f(0.f, 1.f, 0.f) },
|
||||||
|
{ "-Y", Vec3f(0.f, -1.f, 0.f) },
|
||||||
|
{ "+Z", Vec3f(0.f, 0.f, 1.f) },
|
||||||
|
{ "-Z", Vec3f(0.f, 0.f, -1.f) },
|
||||||
|
} };
|
||||||
|
|
||||||
|
TEST_CASE("the box projection lands a texture upright and unmirrored on all six faces of a cube",
|
||||||
|
"[TextureDisplacement]")
|
||||||
|
{
|
||||||
|
const DecodedHeightTexture rows = ramp_texture(true);
|
||||||
|
const DecodedHeightTexture cols = ramp_texture(false);
|
||||||
|
const TextureDisplacementLayer layer = ramp_layer(TextureProjectionMethod::Triplanar);
|
||||||
|
|
||||||
|
for (const auto &[name, n] : CUBE_FACES) {
|
||||||
|
DYNAMIC_SECTION("face " << name)
|
||||||
|
{
|
||||||
|
Vec3f right, up;
|
||||||
|
outside_viewer_frame(n, right, up);
|
||||||
|
const Vec3f centre = n * CUBE_HALF;
|
||||||
|
|
||||||
|
// Upright: the row read near the top of the face is nearer the image's first row.
|
||||||
|
const double row_up = ramp_index(rows, layer, centre + up * RAMP_STEP, n);
|
||||||
|
const double row_down = ramp_index(rows, layer, centre - up * RAMP_STEP, n);
|
||||||
|
INFO("row at the top of the face " << row_up << ", at the bottom " << row_down);
|
||||||
|
CHECK(row_down - row_up > RAMP_MARGIN);
|
||||||
|
|
||||||
|
// Unmirrored: the column read to the viewer's right is further into the image. Without a
|
||||||
|
// per-side sign this fails on exactly the three faces facing away - -X, +Y and -Z - which
|
||||||
|
// is the mirroring a bare v flip leaves behind.
|
||||||
|
const double col_left = ramp_index(cols, layer, centre - right * RAMP_STEP, n);
|
||||||
|
const double col_right = ramp_index(cols, layer, centre + right * RAMP_STEP, n);
|
||||||
|
INFO("column at the left of the face " << col_left << ", at the right " << col_right);
|
||||||
|
CHECK(col_right - col_left > RAMP_MARGIN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("every projection method lands a texture upright on an upright face", "[TextureDisplacement]")
|
||||||
|
{
|
||||||
|
const DecodedHeightTexture rows = ramp_texture(true);
|
||||||
|
// ViewProjected is one flat projection from the captured camera rather than a per-face map, so its
|
||||||
|
// "up" is the projector's own (view_project_up), which the default capture puts at +Y.
|
||||||
|
const auto method = GENERATE(TextureProjectionMethod::Triplanar, TextureProjectionMethod::Cylindrical,
|
||||||
|
TextureProjectionMethod::Spherical, TextureProjectionMethod::ViewProjected);
|
||||||
|
const TextureDisplacementLayer layer = ramp_layer(method);
|
||||||
|
|
||||||
|
for (const auto &[name, n] : CUBE_FACES) {
|
||||||
|
// Only the four upright faces: on the top and bottom a cylinder's v is constant (they are its
|
||||||
|
// caps) and a sphere's is at an extremum (they are its poles), so neither says anything about
|
||||||
|
// which way up the image is.
|
||||||
|
if (std::abs(n.z()) > 0.9f)
|
||||||
|
continue;
|
||||||
|
DYNAMIC_SECTION("method " << int(method) << ", face " << name)
|
||||||
|
{
|
||||||
|
const Vec3f up = (method == TextureProjectionMethod::ViewProjected) ? layer.view_project_up :
|
||||||
|
Vec3f(0.f, 0.f, 1.f);
|
||||||
|
const Vec3f centre = n * CUBE_HALF;
|
||||||
|
const double row_up = ramp_index(rows, layer, centre + up * RAMP_STEP, n);
|
||||||
|
const double row_down = ramp_index(rows, layer, centre - up * RAMP_STEP, n);
|
||||||
|
INFO("row at the top " << row_up << ", at the bottom " << row_down);
|
||||||
|
CHECK(row_down - row_up > RAMP_MARGIN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("the cylindrical and spherical wraps run left to right as seen from outside", "[TextureDisplacement]")
|
||||||
|
{
|
||||||
|
const DecodedHeightTexture cols = ramp_texture(false);
|
||||||
|
const auto method = GENERATE(TextureProjectionMethod::Cylindrical, TextureProjectionMethod::Spherical);
|
||||||
|
const TextureDisplacementLayer layer = ramp_layer(method);
|
||||||
|
|
||||||
|
for (const auto &[name, n] : CUBE_FACES) {
|
||||||
|
// The caps and poles are out, as above - u is undefined on the axis. So is whichever face the
|
||||||
|
// atan2 branch cut runs through, where u jumps from +pi to -pi: that is a real seam in both
|
||||||
|
// projections, and a reading taken across it compares two opposite sides of the model.
|
||||||
|
if (std::abs(n.z()) > 0.9f)
|
||||||
|
continue;
|
||||||
|
const bool on_the_cut = (method == TextureProjectionMethod::Cylindrical) ? (n.y() < -0.9f) : (n.x() < -0.9f);
|
||||||
|
if (on_the_cut)
|
||||||
|
continue;
|
||||||
|
DYNAMIC_SECTION("method " << int(method) << ", face " << name)
|
||||||
|
{
|
||||||
|
Vec3f right, up;
|
||||||
|
outside_viewer_frame(n, right, up);
|
||||||
|
const Vec3f centre = n * CUBE_HALF;
|
||||||
|
const double col_left = ramp_index(cols, layer, centre - right * RAMP_STEP, n);
|
||||||
|
const double col_right = ramp_index(cols, layer, centre + right * RAMP_STEP, n);
|
||||||
|
INFO("column at the left " << col_left << ", at the right " << col_right);
|
||||||
|
CHECK(col_right - col_left > RAMP_MARGIN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("an unwrapped cube's flat charts are laid out the right way up", "[TextureDisplacement]")
|
||||||
|
{
|
||||||
|
indexed_triangle_set cube = its_make_cube(2. * CUBE_HALF, 2. * CUBE_HALF, 2. * CUBE_HALF);
|
||||||
|
for (Vec3f &v : cube.vertices)
|
||||||
|
v -= Vec3f(CUBE_HALF, CUBE_HALF, CUBE_HALF);
|
||||||
|
|
||||||
|
const PatchUnwrap unwrap = compute_patch_unwrap(cube, LSCM_DEFAULT_SEAM_ANGLE_DEG);
|
||||||
|
REQUIRE_FALSE(unwrap.empty());
|
||||||
|
REQUIRE(unwrap.chart_count == 6); // one flat chart per face, cut at the 90-degree edges
|
||||||
|
|
||||||
|
// Each chart carries one face. The four upright ones span world Z, and their v has to grow
|
||||||
|
// downwards like every other projection's - whatever the packing then did with the island.
|
||||||
|
int upright_charts = 0;
|
||||||
|
for (int c = 0; c < unwrap.chart_count; ++c) {
|
||||||
|
int top = -1, bottom = -1;
|
||||||
|
for (size_t i = 0; i < unwrap.uvs.size(); ++i) {
|
||||||
|
if (unwrap.vertex_chart[i] != c)
|
||||||
|
continue;
|
||||||
|
const float z = cube.vertices[size_t(unwrap.source_vertex[i])].z();
|
||||||
|
if (top < 0 || z > cube.vertices[size_t(unwrap.source_vertex[size_t(top)])].z())
|
||||||
|
top = int(i);
|
||||||
|
if (bottom < 0 || z < cube.vertices[size_t(unwrap.source_vertex[size_t(bottom)])].z())
|
||||||
|
bottom = int(i);
|
||||||
|
}
|
||||||
|
REQUIRE(top >= 0);
|
||||||
|
REQUIRE(bottom >= 0);
|
||||||
|
const float v_top = unwrap.uvs[size_t(top)].y(), v_bottom = unwrap.uvs[size_t(bottom)].y();
|
||||||
|
if (std::abs(v_top - v_bottom) < 1e-3f)
|
||||||
|
continue; // a cap: the whole chart is at one height, so it has no up
|
||||||
|
++upright_charts;
|
||||||
|
DYNAMIC_SECTION("chart " << c)
|
||||||
|
{
|
||||||
|
INFO("v at the chart's topmost vertex " << v_top << ", at its bottommost " << v_bottom);
|
||||||
|
CHECK(v_bottom - v_top > 1.f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CHECK(upright_charts == 4); // the four sides; the top and bottom faces are the caps skipped above
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("project_planar agrees with the box projection's own axes", "[TextureDisplacement]")
|
||||||
|
{
|
||||||
|
// The two express the same table in different shapes - one a pair of world axes for the GUI, the
|
||||||
|
// other the closed form the bake's hot path uses - so they are checked against each other here
|
||||||
|
// rather than left to drift apart.
|
||||||
|
const Vec3f p(2.f, -3.f, 7.f);
|
||||||
|
for (const auto &[name, n] : CUBE_FACES) {
|
||||||
|
DYNAMIC_SECTION("face " << name)
|
||||||
|
{
|
||||||
|
Vec3f u_axis, v_axis;
|
||||||
|
box_projection_axes(n, u_axis, v_axis);
|
||||||
|
const Vec2f planar = project_planar(p, n);
|
||||||
|
CHECK_THAT(planar.x(), WithinAbs(p.dot(u_axis), 1e-5f));
|
||||||
|
CHECK_THAT(planar.y(), WithinAbs(p.dot(v_axis), 1e-5f));
|
||||||
|
// Right-handed for a viewer outside the model: u to their right, v down, so u x v points
|
||||||
|
// into the surface. Getting this wrong is exactly what mirrors a face.
|
||||||
|
CHECK_THAT(u_axis.cross(v_axis).dot(n), WithinAbs(-1.f, 1e-5f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An image with a different value in each quadrant, so one bake says both which way up it landed
|
||||||
|
// and whether it came out mirrored. Distinct enough that a wrong quadrant cannot be mistaken for the
|
||||||
|
// right one: top-left 255, top-right 170, bottom-left 85, bottom-right 0.
|
||||||
|
static std::shared_ptr<std::vector<unsigned char>> make_quadrant_marker_png(size_t w = 32, size_t h = 32)
|
||||||
|
{
|
||||||
|
std::vector<uint8_t> pixels(w * h);
|
||||||
|
for (size_t y = 0; y < h; ++y)
|
||||||
|
for (size_t x = 0; x < w; ++x)
|
||||||
|
pixels[y * w + x] = (y < h / 2) ? ((x < w / 2) ? 255 : 170) : ((x < w / 2) ? 85 : 0);
|
||||||
|
const boost::filesystem::path tmp_path = boost::filesystem::temp_directory_path()
|
||||||
|
/ boost::filesystem::unique_path("texdisp_test_%%%%%%%%.png");
|
||||||
|
REQUIRE(Slic3r::png::write_gray_to_file(tmp_path.string(), w, h, pixels));
|
||||||
|
|
||||||
|
std::vector<unsigned char> bytes;
|
||||||
|
{
|
||||||
|
std::ifstream ifs(tmp_path.string(), std::ios::binary);
|
||||||
|
bytes.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
|
||||||
|
}
|
||||||
|
boost::system::error_code ec;
|
||||||
|
boost::filesystem::remove(tmp_path, ec);
|
||||||
|
|
||||||
|
REQUIRE_FALSE(bytes.empty());
|
||||||
|
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("baking an asymmetric image puts the top of the image at the top of the model", "[TextureDisplacement]")
|
||||||
|
{
|
||||||
|
// An upright wall in the plane y = 0, facing -Y, tessellated so that each of the image's four
|
||||||
|
// quadrants gets a vertex at its centre. A flat sheet rather than a cube because every vertex
|
||||||
|
// normal is then exactly the face normal, which is what makes the expected depth below an exact
|
||||||
|
// number instead of a tri-planar blend of three of them.
|
||||||
|
constexpr int STEPS = 4; // cells per side, so 5x5 vertices
|
||||||
|
constexpr float SPAN = 20.f; // mm, the wall's width and height
|
||||||
|
indexed_triangle_set wall;
|
||||||
|
for (int j = 0; j <= STEPS; ++j)
|
||||||
|
for (int i = 0; i <= STEPS; ++i)
|
||||||
|
wall.vertices.emplace_back(-SPAN / 2.f + SPAN * float(i) / float(STEPS), 0.f,
|
||||||
|
SPAN * float(j) / float(STEPS));
|
||||||
|
const auto at = [](int i, int j) { return j * (STEPS + 1) + i; };
|
||||||
|
for (int j = 0; j < STEPS; ++j)
|
||||||
|
for (int i = 0; i < STEPS; ++i) {
|
||||||
|
// Wound so that (+x) x (+z) = -y: the wall faces -Y, away from the origin.
|
||||||
|
wall.indices.emplace_back(at(i, j), at(i + 1, j), at(i + 1, j + 1));
|
||||||
|
wall.indices.emplace_back(at(i, j), at(i + 1, j + 1), at(i, j + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
TextureDisplacementFacetsData facets{};
|
||||||
|
facets[0] = paint_whole_mesh(wall);
|
||||||
|
|
||||||
|
TextureDisplacementLayer layer;
|
||||||
|
layer.slot = 0;
|
||||||
|
layer.projection_method = TextureProjectionMethod::Triplanar;
|
||||||
|
layer.image_data = make_quadrant_marker_png();
|
||||||
|
layer.depth_mm = 2.f;
|
||||||
|
// The wall is 20 mm across and one tile is 40 mm, so it covers the middle half of the image in
|
||||||
|
// both axes: the quadrant boundaries land on x = 0 and z = 10, and the probed vertices sit a
|
||||||
|
// whole number of texels away from both.
|
||||||
|
layer.tiling_scale = 40.f;
|
||||||
|
layer.offset = Vec2f(0.5f, 0.75f);
|
||||||
|
// A single decal rather than a repeat, so that getting the orientation wrong cannot be masked by
|
||||||
|
// the wrap bringing the flipped coordinate back into the image at a plausible-looking row. With
|
||||||
|
// tiling off, uv stays inside [0.25, 0.75] in both axes only while v runs the right way.
|
||||||
|
layer.tile_enabled = false;
|
||||||
|
|
||||||
|
const indexed_triangle_set result = build_texture_displacement(wall, { layer }, facets, classic_options());
|
||||||
|
REQUIRE(result.vertices.size() == wall.vertices.size());
|
||||||
|
|
||||||
|
// Seen from outside this face the viewer's right is +X and their up is +Z, so the image's
|
||||||
|
// top-left quadrant has to land at low x and high z. Each entry: the vertex, and the image value
|
||||||
|
// whose quadrant it must have read.
|
||||||
|
const std::array<std::tuple<const char *, int, int, int>, 4> expected = { {
|
||||||
|
{ "top-left (255)", 1, 3, 255 },
|
||||||
|
{ "top-right (170)", 3, 3, 170 },
|
||||||
|
{ "bottom-left (85)", 1, 1, 85 },
|
||||||
|
{ "bottom-right (0)", 3, 1, 0 },
|
||||||
|
} };
|
||||||
|
|
||||||
|
for (const auto &[name, i, j, value] : expected) {
|
||||||
|
DYNAMIC_SECTION("quadrant " << name)
|
||||||
|
{
|
||||||
|
const size_t vi = size_t(at(i, j));
|
||||||
|
// The whole displacement is along -Y, so this is the relief's height at that vertex.
|
||||||
|
const float raised = wall.vertices[vi].y() - result.vertices[vi].y();
|
||||||
|
INFO("vertex at " << wall.vertices[vi].x() << ", " << wall.vertices[vi].z());
|
||||||
|
CHECK_THAT(raised, WithinAbs(layer.depth_mm * float(value) / 255.f, 1e-3f));
|
||||||
|
// Nothing moved sideways or vertically: a mirrored or rotated basis would still have
|
||||||
|
// displaced along the normal, so this is here to catch the relief leaning instead.
|
||||||
|
CHECK_THAT(result.vertices[vi].x(), WithinAbs(wall.vertices[vi].x(), 1e-4f));
|
||||||
|
CHECK_THAT(result.vertices[vi].z(), WithinAbs(wall.vertices[vi].z(), 1e-4f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("The Cylindrical/Spherical patch frame is the bake's own, so a preview can share it", "[TextureDisplacement]")
|
TEST_CASE("The Cylindrical/Spherical patch frame is the bake's own, so a preview can share it", "[TextureDisplacement]")
|
||||||
{
|
{
|
||||||
// The fast preview reconstructs these two projections in the fragment shader and needs the very
|
// The fast preview reconstructs these two projections in the fragment shader and needs the very
|
||||||
|
|||||||
Reference in New Issue
Block a user