Compare commits

..
Author SHA1 Message Date
SoftFever 4c74ab393b Sign the uninstaller in Windows release installers 2026-10-19 13:07:53 +08:00
SoftFever 948e6637fb Sign Windows release builds with the SignPath release certificate
Release-branch builds sign every unsigned executable, DLL and Python module in the x64 and arm64 portable bundles and installers, after approval in SignPath. The binaries and installer now carry OrcaSlicer version information.
2026-10-19 00:32:44 +08:00
SoftFever 40761ec6e2 Sign the Windows installer and the binaries it installs 2026-10-18 23:35:16 +08:00
SoftFever 186cd8e65f ci(windows): detect untrusted root via structured signature status
The previous text match on signtool output was flaky: signtool interleaves
stdout (chain details) and stderr (error), so the wrapped "untrusted root"
phrase stayed contiguous for one file but was split for another, causing the
verify step to pass orca-slicer.exe but throw on OrcaSlicer.dll.

Use Get-AuthenticodeSignature instead, whose StatusMessage is a single clean
string, to decide the untrusted-root exception. signtool remains the strict
primary check; this only governs the -AllowUntrustedRoot test-cert path.
2026-10-18 23:35:16 +08:00
SoftFever 8bc0eec4aa ci(windows): reset exit code after accepting untrusted test-cert root
The verify script accepted both signed binaries and printed "verification
passed", but the step still failed: signtool exits 1 on the untrusted test
root, and GitHub's pwsh wrapper exits with that lingering $LASTEXITCODE.
Add an explicit `exit 0` on the success path so the step passes.
2026-10-18 23:35:16 +08:00
SoftFever ec30dad1cf ci(windows): tolerate untrusted root in signature verification for test cert
signtool verify /pa exits non-zero when the certificate chain terminates in
an untrusted root, which is always the case for the SignPath self-signed test
certificate. Add an -AllowUntrustedRoot switch to verify-authenticode.ps1 that
accepts a signed-but-untrusted-root result (while still failing on unsigned or
otherwise invalid files), and pass it from the workflow during test-signing.

Remove the switch once signing-policy-slug moves to release-signing with a
production CA-issued certificate, so release builds enforce a fully trusted chain.
2026-10-18 23:35:16 +08:00
SoftFever f1428b72b8 ci(windows): wire SignPath test-signing for portable bundle
Add SignPath artifact configuration (windows-portable-v1) signing only the
first-party binaries orca-slicer.exe and OrcaSlicer.dll, plus verification
and inventory scripts. The Windows build job uploads the unsigned portable
bundle, submits it to SignPath (test-signing), verifies the returned
signatures, and rebuilds the installer/zip from the signed binaries.

project-slug matches the SignPath project (OrcaSlicer).
2026-10-18 23:35:16 +08:00
SoftFever 3880f2c58e Bump version to 2.5.0-alpha 2026-10-16 18:52:11 +08:00
213 changed files with 11446 additions and 28006 deletions

No files matched your search

+4 -4
View File
@@ -1,6 +1,6 @@
---
name: orca-translations
description: Use when updating, filling, reviewing or fixing OrcaSlicer translations in localization/i18n — running scripts/run_gettext --full, translating empty or new msgids in the .po catalogs, AI translation passes and their "# AI Translated" / "# Needs human review" comments, plural forms, msgctxt homonyms, and keeping catalogs consistent with the localization glossary (OrcaSlicer_WIKI developer_reference/glossary), including adding new glossary terms. Also use for msgfmt or run_gettext failures, placeholder or line-break mismatches, and reviewing a translation PR against the glossary.
description: Use when updating, filling, reviewing or fixing OrcaSlicer translations in localization/i18n — running scripts/run_gettext --full, translating empty or new msgids in the .po catalogs, AI translation passes and their "# AI Translated" / "# Needs human review" comments, plural forms, msgctxt homonyms, and keeping catalogs consistent with the localization glossary (OrcaSlicer_WIKI localization_glossary.tsv), including adding new glossary terms. Also use for msgfmt or run_gettext failures, placeholder or line-break mismatches, and reviewing a translation PR against the glossary.
---
# OrcaSlicer translations
@@ -9,7 +9,7 @@ The UI strings live in gettext catalogs: `localization/i18n/OrcaSlicer.pot` is t
`localization/i18n/<lang>/OrcaSlicer_<lang>.po` holds each language. The `en` catalog is empty on
purpose, so skip it. Recurring terms are fixed by the
[localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md),
whose source is the per-language files in `developer_reference/glossary/` (`terms.tsv`, `languages.txt` and one `<lang>.tsv`) in the **OrcaSlicer_WIKI** repository.
whose source is `developer_reference/localization_glossary.tsv` in the **OrcaSlicer_WIKI** repository.
A translation pass therefore touches two repositories: the catalogs here, and the glossary there.
All catalog editing goes through [`scripts/po_tool.py`](scripts/po_tool.py) (standard library only).
@@ -153,7 +153,7 @@ Report:
- anything left empty, and why;
- the glossary rows added or filled.
The catalogs (and `OrcaSlicer.pot`) and the glossary (`developer_reference/glossary/*.tsv` and `localization_glossary.md`) are
The catalogs (and `OrcaSlicer.pot`) and the glossary (`localization_glossary.tsv` and `.md`) are
separate changes in separate repositories. The wiki change should land with, or before, the catalog
change. Commit or open PRs only when asked.
@@ -210,7 +210,7 @@ with the translators, followed by the catalogs.
When this skill and the checkouts disagree, the checkouts win:
- `scripts/run_gettext.sh` / `.bat` for extraction and merging;
- `localization/i18n/list.txt` for the scanned sources;
- the wiki's `localization_guide.md` and `localization_glossary.md` and `glossary/*.tsv` for the rules and terms;
- the wiki's `localization_guide.md` and `localization_glossary.md` / `.tsv` for the rules and terms;
- `AGENTS.md` (Localization & translations) for the repository's conventions.
## Editing this skill
@@ -28,14 +28,13 @@ import sys
import tempfile
import time
import unicodedata
import urllib.error
import urllib.request
from pathlib import Path
REPO = Path(__file__).resolve().parents[4]
I18N = REPO / "localization" / "i18n"
SKIP_LANGS = {"en"} # the English catalog is intentionally empty
GLOSSARY_DIR = Path("developer_reference") / "glossary" # terms.tsv, languages.txt and one <lang>.tsv per language
GLOSSARY_TSV = Path("developer_reference") / "localization_glossary.tsv"
GLOSSARY_MD = Path("developer_reference") / "localization_glossary.md"
MISSING = {"", "—"}
RAW_URL = "https://raw.githubusercontent.com/OrcaSlicer/OrcaSlicer_WIKI/main/"
@@ -253,40 +252,30 @@ def all_langs():
def find_wiki(arg):
for cand in [arg, os.environ.get("ORCA_WIKI_DIR"), REPO.parent / "OrcaSlicer_WIKI", REPO.parent / "OrcaSlicer.wiki"]:
if cand and (Path(cand) / GLOSSARY_DIR / "terms.tsv").exists():
if cand and (Path(cand) / GLOSSARY_TSV).exists():
return Path(cand)
return None
def _download(rel, required=True):
cache = Path(tempfile.gettempdir()) / "orca_glossary" / rel
def _download(rel):
cache = Path(tempfile.gettempdir()) / ("orca_glossary_" + rel.name)
if not cache.exists() or time.time() - cache.stat().st_mtime > 3600:
try:
with urllib.request.urlopen(RAW_URL + rel.as_posix(), timeout=30) as r:
data = r.read()
except urllib.error.HTTPError:
if required:
raise
return None
cache.parent.mkdir(parents=True, exist_ok=True)
cache.write_bytes(data)
with urllib.request.urlopen(RAW_URL + rel.as_posix(), timeout=30) as r:
cache.write_bytes(r.read())
return cache
class Glossary:
"""The glossary: rows of [English, Description, <one translation per language loaded>]."""
def __init__(self, wiki_arg=None, quiet=False, langs=None):
def __init__(self, wiki_arg=None, quiet=False):
self.wiki = find_wiki(wiki_arg)
self.langs = langs # only these languages' files are read (and downloaded); None means all
if self.wiki:
self.dir, self.md = self.wiki / GLOSSARY_DIR, self.wiki / GLOSSARY_MD
self.tsv, self.md = self.wiki / GLOSSARY_TSV, self.wiki / GLOSSARY_MD
if not quiet:
self.report_clone_state()
else:
if not quiet:
print("note: no OrcaSlicer_WIKI clone found; using the published glossary read-only", file=sys.stderr)
self.dir, self.md = _download(GLOSSARY_DIR / "terms.tsv").parent, _download(GLOSSARY_MD)
self.tsv, self.md = _download(GLOSSARY_TSV), _download(GLOSSARY_MD)
self.load()
def report_clone_state(self):
@@ -301,23 +290,11 @@ class Glossary:
if behind and behind != "0":
print(f"note: the wiki clone is {behind} commits behind origin/main (as of its last fetch)", file=sys.stderr)
def _read(self, name, required=True):
path = self.dir / name if self.wiki else _download(GLOSSARY_DIR / name, required)
return path.read_text(encoding="utf-8-sig").splitlines() if path and path.exists() else None
def load(self):
terms = [l.split(" ") for l in self._read("terms.tsv")[1:] if l.strip()]
langs = [l.strip() for l in self._read("languages.txt") if l.strip()]
langs = [l for l in langs if self.langs is None or l in self.langs]
self.header = ["English", "Description"] + langs
self.rows = [[r[0], r[1] if len(r) > 1 else ""] + [""] * len(langs) for r in terms]
index = {r[0]: r for r in self.rows}
for col, lang in enumerate(langs, start=2):
for line in (self._read(f"{lang}.tsv", required=False) or [])[1:]:
cells = line.split(" ")
# "—" marks a language with no established rendering yet
if len(cells) > 2 and cells[0] in index and cells[2].strip() not in MISSING:
index[cells[0]][col] = cells[2].strip()
rows = [l.split("\t") for l in self.tsv.read_text(encoding="utf-8-sig").splitlines() if l.strip()]
self.header = rows[0]
# "—" marks a language with no established rendering yet
self.rows = [[c if c.strip() not in MISSING else "" for c in r] + [""] * (len(rows[0]) - len(r)) for r in rows[1:]]
self.matchers = []
for row in self.rows:
base = re.sub(r"\s*\([^)]*\)\s*$", "", row[0]).strip()
@@ -517,7 +494,7 @@ def similar_entries(c, e, pool, cutoff=0.72):
def cmd_pending(a):
c = Catalog(catalog_path(a.lang))
g = Glossary(a.wiki, langs=[a.lang])
g = Glossary(a.wiki)
todo = [e for e in c.active() if needs_translation(e, c.nplurals, a.include_fuzzy)]
batch = todo[a.offset:a.offset + a.limit] if a.limit else todo[a.offset:]
pool = [(x.msgid, x.msgstrs()[0], x.obsolete) for x in c.entries
@@ -556,7 +533,7 @@ def cmd_pending(a):
def cmd_apply(a):
c = Catalog(catalog_path(a.lang))
g = None if a.no_glossary else Glossary(a.wiki, quiet=True, langs=[a.lang])
g = None if a.no_glossary else Glossary(a.wiki, quiet=True)
data = json.loads(Path(a.input).read_text(encoding="utf-8"))
items = data["entries"] if isinstance(data, dict) else data
idx = c.index()
@@ -611,7 +588,7 @@ def find_msgfmt():
def cmd_check(a):
g = None if a.no_glossary else Glossary(a.wiki, quiet=True, langs=a.lang or None)
g = None if a.no_glossary else Glossary(a.wiki, quiet=True)
msgfmt = find_msgfmt()
status = 0
for lang in a.lang or all_langs():
@@ -669,7 +646,7 @@ def cmd_lookup(a):
def cmd_glossary(a):
g = Glossary(a.wiki, langs=[a.lang])
g = Glossary(a.wiki)
col = g.column(a.lang)
if col is None:
print(f"'{a.lang}' has no glossary column yet; add one with glossary-set --set {a.lang}=...")
@@ -756,17 +733,6 @@ def cmd_candidates(a):
print(f"{cnt:7} {langs:6} {text}{' [label]' if k in labels else ''} e.g. {ex[:70]}{'…' if len(ex) > 70 else ''}")
def _edit_tsv(path, edit):
"""Apply edit(lines) to a glossary TSV, keeping its BOM and line endings."""
data = path.read_bytes()
bom = data.startswith(b"\xef\xbb\xbf")
text = data.decode("utf-8-sig")
nl = "\r\n" if "\r\n" in text else "\n"
lines = text.splitlines()
edit(lines)
path.write_bytes((b"\xef\xbb\xbf" if bom else b"") + (nl.join(lines) + nl).encode("utf-8"))
def cmd_glossary_set(a):
wiki = find_wiki(a.wiki)
if not wiki:
@@ -775,21 +741,8 @@ def cmd_glossary_set(a):
f" {a.term}: " + ", ".join(a.set) + "\n"
"Clone https://github.com/OrcaSlicer/OrcaSlicer_WIKI next to this repository, or pass --wiki.", file=sys.stderr)
return 2
gdir = wiki / GLOSSARY_DIR
gen = wiki / "generate_glossary.py"
if not gen.exists():
sys.exit(f"{gen} not found")
run_gen = lambda *args: subprocess.run([sys.executable, str(gen), *args], cwd=wiki, capture_output=True, text=True)
langs = [l.strip() for l in (gdir / "languages.txt").read_text(encoding="utf-8-sig").splitlines() if l.strip()]
values = {}
for s in a.set:
lang, _, val = s.partition("=")
if lang not in langs:
sys.exit(f"no glossary language '{lang}' (add it to {GLOSSARY_DIR.as_posix()}/languages.txt)")
if "\t" in val or "\n" in val:
sys.exit("values cannot contain tabs or line breaks")
values[lang] = val.strip()
lock = gdir / ".lock"
tsv = wiki / GLOSSARY_TSV
lock = tsv.with_suffix(".lock")
for _ in range(100):
try:
fd = os.open(lock, os.O_CREAT | os.O_EXCL)
@@ -799,51 +752,50 @@ def cmd_glossary_set(a):
else:
sys.exit(f"could not lock {lock}; remove it if no other run is active")
try:
terms = (gdir / "terms.tsv").read_text(encoding="utf-8-sig").splitlines()
new = not any(l.split("\t")[0] == a.term for l in terms[1:])
if new and not a.description:
sys.exit("a new term needs --description (one sentence saying what it is)")
# an established translation is only replaced on request
conflicts = []
for lang, v in values.items():
for line in (gdir / f"{lang}.tsv").read_text(encoding="utf-8-sig").splitlines()[1:]:
cells = line.split("\t")
if cells[0] == a.term and len(cells) > 2 and cells[2].strip() not in MISSING and cells[2] != v:
conflicts.append(f"{lang}: '{cells[2]}' -> '{v}'")
data = tsv.read_bytes()
bom = data.startswith(b"\xef\xbb\xbf")
text = data.decode("utf-8-sig")
nl = "\r\n" if "\r\n" in text else "\n"
lines = text.splitlines()
header = lines[0].split("\t")
values = {}
for s in a.set:
lang, _, val = s.partition("=")
if lang not in header:
sys.exit(f"no glossary column '{lang}'")
if "\t" in val or "\n" in val:
sys.exit("values cannot contain tabs or line breaks")
values[header.index(lang)] = val.strip()
row_i = next((i for i, l in enumerate(lines[1:], 1) if l.split("\t")[0] == a.term), None)
new = row_i is None
if new:
if not a.description:
sys.exit("a new term needs --description (one sentence saying what it is)")
row = [a.term, a.description] + [""] * (len(header) - 2)
lines.append("")
row_i = len(lines) - 1
else:
row = lines[row_i].split("\t")
row += [""] * (len(header) - len(row))
if a.description:
row[1] = a.description
conflicts = [f"{header[i]}: '{row[i]}' -> '{v}'" for i, v in values.items() if row[i].strip() not in MISSING and row[i] != v]
if conflicts and not a.replace:
sys.exit("established translations differ (pass --replace, and update the catalogs to match):\n " + "\n ".join(conflicts))
def edit_terms(lines):
for i, l in enumerate(lines):
if i and l.split("\t")[0] == a.term:
if a.description:
lines[i] = f"{a.term}\t{a.description}"
return
lines.append(f"{a.term}\t{a.description}")
if new or a.description:
_edit_tsv(gdir / "terms.tsv", edit_terms)
r = run_gen("--sync") # copies the term and description into every language file
if r.returncode:
sys.exit((r.stdout + r.stderr).strip())
def setter(lang, v):
def edit(lines):
for i, l in enumerate(lines):
cells = l.split("\t")
if i and cells[0] == a.term:
lines[i] = "\t".join(cells[:2] + [v])
return
sys.exit(f"{lang}.tsv has no row for '{a.term}'")
return edit
for lang, v in values.items():
_edit_tsv(gdir / f"{lang}.tsv", setter(lang, v))
for i, v in values.items():
row[i] = v
lines[row_i] = "\t".join(row)
tsv.write_bytes((b"\xef\xbb\xbf" if bom else b"") + (nl.join(lines) + nl).encode("utf-8"))
finally:
os.close(fd)
os.unlink(lock)
print(f"glossary: {'added' if new else 'updated'} '{a.term}'")
r = run_gen("--update")
print((r.stdout + r.stderr).strip())
return r.returncode
gen = wiki / "generate_glossary.py"
if gen.exists():
r = subprocess.run([sys.executable, str(gen), "--update"], cwd=wiki, capture_output=True, text=True)
print((r.stdout + r.stderr).strip())
return r.returncode
return 0
def main():
@@ -574,8 +574,7 @@ DMABUF workaround and then branches on `GDK_BACKEND` (an `x11` prefix means the
paths, when `/proc/driver/nvidia/version` exists and the linked WebKitGTK honours the switch (>= 2.44,
`WebView::SupportsForceSharedMemory()`), it sets `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and
`WEBKIT_FORCE_DMABUF_RENDERER=1` (non-replacing): the proprietary driver cannot back WebKit's hardware
DMABUF renderer. On a hypervisor guest (`/proc/cpuinfo` `hypervisor` flag) and WebKitGTK >= 2.44 it sets
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` alone, since virtual GPUs cannot back the hardware path either. On the default path it forces `GDK_BACKEND=x11` when wx lacks EGL and `WAYLAND_DISPLAY`
DMABUF renderer. On the default path it forces `GDK_BACKEND=x11` when wx lacks EGL and `WAYLAND_DISPLAY`
is set, and sets `WEBKIT_DISABLE_COMPOSITING_MODE=1` (non-replacing) inside that branch when the linked
WebKitGTK is older than 2.42: a forced X11 backend in a Wayland session is the only default-path case
that runs through XWayland, which `DISPLAY` and `WAYLAND_DISPLAY` alone do not tell you (a native
@@ -568,18 +568,13 @@ vue-resize workaround script and sets a persistent SQLite cookie store on the vi
## WebKitGTK on Linux sessions
**Mechanism.** `CLI::run` (`src/OrcaSlicer.cpp`, under `#ifdef __WXGTK__`, before GTK initialises)
sets the DMABUF workarounds and then branches on `GDK_BACKEND`:
sets the NVIDIA DMABUF workaround and then branches on `GDK_BACKEND`:
- **Both paths**, before the split: when `/proc/driver/nvidia/version` exists and the linked WebKitGTK
honours the switch (>= 2.44, `WebView::SupportsForceSharedMemory()`), it sets
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and `WEBKIT_FORCE_DMABUF_RENDERER=1` non-replacing. The
proprietary driver cannot back WebKit's hardware DMABUF renderer, so the shared-memory transport keeps
accelerated compositing instead of dropping it; `WEBKIT_FORCE_DMABUF_RENDERER` clears the
Debian/Ubuntu/openSUSE `isNVIDIA()` early return, which otherwise returns before the FORCE_SHM check.
- **Hypervisor guests**, same position: when `is_hypervisor_guest()` (the `hypervisor` flag in
`/proc/cpuinfo`) and WebKitGTK >= 2.44, it sets `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` non-replacing.
Virtual GPUs (VirtualBox VMSVGA, VMware, QEMU) cannot back the hardware DMABUF path either, and
FORCE_SHM alone was confirmed to fix web views in VirtualBox; the `WEBKIT_FORCE_DMABUF_RENDERER` pair
is not needed without the Debian `isNVIDIA()` patch. The flag is x86-only, so ARM guests are not covered.
- **Default path** (`GDK_BACKEND` unset or not starting with `x11`): if wx lacks EGL (`!wxHAS_EGL`) and
`WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`, and in that same branch it sets
`WEBKIT_DISABLE_COMPOSITING_MODE=1` non-replacing when the linked WebKitGTK predates 2.42
@@ -595,9 +590,9 @@ Runtime backend detection elsewhere uses Orca's `is_running_on_wayland()` / `is_
(see `references/platforms.md`), not these environment variables.
**Pitfall.**
- **Rule:** On NVIDIA and hypervisor guests reach for `WEBKIT_DMABUF_RENDERER_FORCE_SHM` before
`WEBKIT_DISABLE_COMPOSITING_MODE`. **Why:** the proprietary driver and virtual GPUs cannot back WebKit's
hardware DMABUF renderer, leaving web views blank or slow; FORCE_SHM keeps accelerated compositing and only
- **Rule:** On NVIDIA reach for `WEBKIT_DMABUF_RENDERER_FORCE_SHM` before
`WEBKIT_DISABLE_COMPOSITING_MODE`. **Why:** the proprietary driver cannot back WebKit's hardware
DMABUF renderer, leaving web views blank or slow; FORCE_SHM keeps accelerated compositing and only
skips the hardware path, while disabling compositing drops it. Threaded scrolling is a separate,
release-dependent default (off in 2.44/2.45, on from 2.46). `WEBKIT_DISABLE_DMABUF_RENDERER` is not a
substitute — it is read first, leaves the transport set empty (no compositing from 2.44 on, where the
+4 -8
View File
@@ -19,10 +19,10 @@ on:
- 'build_linux.sh'
- 'build_win.bat'
- 'scripts/test_build_win.ps1'
- 'scripts/check_exception_throw_info.ps1'
- 'scripts/test_check_exception_throw_info.ps1'
- 'build_release_macos.sh'
- 'scripts/build_preset_cache.*'
- 'signpath/**'
- 'scripts/*.ps1'
- 'scripts/flatpak/**'
- 'scripts/msix/**'
- 'tests/**'
@@ -40,13 +40,13 @@ on:
- 'version.inc'
- ".github/workflows/build_*.yml"
- ".github/workflows/unit_tests*.yml"
- 'signpath/**'
- 'build_linux.sh'
- 'build_win.bat'
- 'scripts/test_build_win.ps1'
- 'scripts/check_exception_throw_info.ps1'
- 'scripts/test_check_exception_throw_info.ps1'
- 'build_release_macos.sh'
- 'scripts/build_preset_cache.*'
- 'scripts/*.ps1'
- 'scripts/flatpak/**'
- 'scripts/msix/**'
- 'tests/**'
@@ -88,10 +88,6 @@ jobs:
shell: powershell
run: .\scripts\test_build_win.ps1
- name: Run the exception throw info check tests
shell: powershell
run: .\scripts\test_check_exception_throw_info.ps1
build_linux:
strategy:
fail-fast: false
+155 -10
View File
@@ -65,6 +65,15 @@ jobs:
shell: bash
run: |
leg="${{ runner.os }}-${{ inputs.arch || 'amd64' }}${{ runner.os == 'Windows' && format('-{0}', inputs.compiler) || '' }}"
# clang-cl refuses a precompiled header from another cl.exe build and ccache
# does not hash that build, so each one gets its own cache. The build number
# is read from cl.exe itself; the toolset directory keeps its name across patches.
if [ "${{ runner.os }}" = Windows ]; then
vswhere='/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe'
toolset=$(tr -d '\r\n' < "$("$vswhere" -latest -products '*' -find 'VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt' | tr -d '\r')")
cl=$("$vswhere" -latest -products '*' -find 'VC\Tools\MSVC\'"$toolset"'\**\cl.exe' | tr -d '\r' | head -1)
leg="$leg-vc$("$cl" 2>&1 | grep -o -E 'Version [0-9.]+' | cut -d' ' -f2)"
fi
echo "CCACHE_LEG=$leg" >> "$GITHUB_ENV"
echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV"
@@ -415,6 +424,10 @@ jobs:
"BUILD_DIR=build" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
"ARCH_SUFFIX=_x64" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
}
# Release branches ship binaries and installers signed through SignPath with the
# release certificate. Every signing request waits for a manual approval in
# SignPath, so main and pull request builds stay unsigned.
"SIGN_WINDOWS=${{ github.repository == 'OrcaSlicer/OrcaSlicer' && startsWith(github.ref, 'refs/heads/release/') && !vars.SELF_HOSTED }}" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
- name: setup MSVC
if: runner.os == 'Windows'
@@ -495,19 +508,157 @@ jobs:
retention-days: 5
if-no-files-found: error
# makensis generates the uninstaller while it compiles the installer, so a first
# installer build exports it to be signed with the binaries, and "Create installer
# Win" embeds the signed copy. See src/dev-utils/platform/msw/uninstaller-signing.nsh.
- name: Export the Windows uninstaller for SignPath
if: env.SIGN_WINDOWS == 'true'
working-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}
shell: pwsh
run: |
$bundle = "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer".Replace('\', '/')
# Forward slashes: cpack drops the backslashes from makensis arguments.
$uninstaller = "${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/Uninstall.exe".Replace('\', '/')
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $uninstaller) | Out-Null
cpack -G NSIS -D CPACK_INSTALL_CMAKE_PROJECTS= -D "CPACK_INSTALLED_DIRECTORIES=$bundle;." -D "CPACK_NSIS_EXECUTABLE_PRE_ARGUMENTS=/DORCA_EXPORT_UNINSTALLER=$uninstaller"
# Signs OrcaSlicer's own binaries, the uninstaller and every bundled executable, DLL
# and Python extension module that ships without a signature; files their vendor
# signed, such as the Microsoft runtime, keep that signature. See signpath/README.md.
- name: Stage Windows binaries for SignPath
id: stage-windows-binaries
if: env.SIGN_WINDOWS == 'true'
shell: pwsh
run: |
$version = "${{ env.ver }}" -replace '^V', ''
./scripts/stage-for-signpath.ps1 -Bundle "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer" -Path "${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/Uninstall.exe" -Destination "${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/unsigned-binaries" -ProductVersion $version
"version=$version" | Out-File -Append -FilePath $env:GITHUB_OUTPUT -Encoding utf8
- name: Upload unsigned Windows binaries for SignPath
id: upload-windows-binaries
if: env.SIGN_WINDOWS == 'true'
uses: actions/upload-artifact@v7
with:
name: signpath_windows_binaries${{ env.ARCH_SUFFIX }}_unsigned
path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/unsigned-binaries
retention-days: 1
if-no-files-found: error
# Waits until the request is approved in SignPath.
- name: Sign Windows binaries with SignPath
if: env.SIGN_WINDOWS == 'true'
uses: signpath/github-action-submit-signing-request@v2
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
project-slug: OrcaSlicer
signing-policy-slug: release-signing
artifact-configuration-slug: windows-binaries-v1
github-artifact-id: ${{ steps.upload-windows-binaries.outputs.artifact-id }}
parameters: |
version: "${{ steps.stage-windows-binaries.outputs.version }}"
wait-for-completion: true
wait-for-completion-timeout-in-seconds: 10800
output-artifact-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-binaries
- name: Verify Windows binary signatures
if: env.SIGN_WINDOWS == 'true'
shell: pwsh
run: ./scripts/verify-authenticode.ps1 -ArtifactDirectory '${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-binaries'
- name: Replace Windows binaries with signed output
if: env.SIGN_WINDOWS == 'true'
shell: pwsh
run: ./scripts/restore-from-signpath.ps1 -SignedDirectory "${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-binaries" -Bundle "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer" -Exclude Uninstall.exe
# NSIS is x86-only; it runs (and the installer it emits runs) under ARM64's
# x86 emulation, packaging the native arm64 payload from build-arm64.
# The installer packs the installed OrcaSlicer/ tree, which the portable zip and the
# MSIX are made from too, instead of re-running the install rules: those take
# CPython and uv from the dependency tree and the download cache, which keep the
# unsigned copies.
- name: Create installer Win
if: runner.os == 'Windows' && !vars.SELF_HOSTED
working-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}
run: |
cpack -G NSIS
$bundle = "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer".Replace('\', '/')
$signedUninstaller = @()
if ($env:SIGN_WINDOWS -eq 'true') {
$uninstaller = "${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-binaries/Uninstall.exe".Replace('\', '/')
$signedUninstaller = "-D", "CPACK_NSIS_EXECUTABLE_PRE_ARGUMENTS=/DORCA_SIGNED_UNINSTALLER=$uninstaller"
}
cpack -G NSIS -D CPACK_INSTALL_CMAKE_PROJECTS= -D "CPACK_INSTALLED_DIRECTORIES=$bundle;." @signedUninstaller
# Installs into a scratch directory and checks that every executable and DLL an
# installation puts on disk, the uninstaller included, carries a valid signature,
# ours or its vendor's. The hosted runner is discarded afterwards, so the
# installation is left in place.
- name: Verify every binary the installer installs is signed
if: env.SIGN_WINDOWS == 'true'
shell: pwsh
run: |
$installDir = Join-Path "${{ runner.temp }}" "orca-install-check"
$installer = "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe"
# NSIS requires /D= to come last.
$setup = Start-Process -FilePath $installer -ArgumentList "/S", "/D=$installDir" -Wait -PassThru
if ($setup.ExitCode -ne 0) { throw "The installer exited with code $($setup.ExitCode)." }
if (-not (Test-Path -LiteralPath (Join-Path $installDir "Uninstall.exe") -PathType Leaf)) {
throw "The installation in '$installDir' has no Uninstall.exe."
}
./scripts/verify-authenticode.ps1 -ArtifactDirectory $installDir
- name: Stage Windows installer for SignPath
if: env.SIGN_WINDOWS == 'true'
shell: pwsh
run: ./scripts/stage-for-signpath.ps1 -Path "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe" -Destination "${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/unsigned-installer" -ProductVersion "${{ steps.stage-windows-binaries.outputs.version }}"
- name: Upload unsigned Windows installer for SignPath
id: upload-windows-installer
if: env.SIGN_WINDOWS == 'true'
uses: actions/upload-artifact@v7
with:
name: signpath_windows_installer${{ env.ARCH_SUFFIX }}_unsigned
path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/unsigned-installer
retention-days: 1
if-no-files-found: error
- name: Sign Windows installer with SignPath
if: env.SIGN_WINDOWS == 'true'
uses: signpath/github-action-submit-signing-request@v2
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
project-slug: OrcaSlicer
signing-policy-slug: release-signing
artifact-configuration-slug: windows-installer-v1
github-artifact-id: ${{ steps.upload-windows-installer.outputs.artifact-id }}
parameters: |
version: "${{ steps.stage-windows-binaries.outputs.version }}"
wait-for-completion: true
wait-for-completion-timeout-in-seconds: 3600
output-artifact-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-installer
- name: Verify Windows installer signature
if: env.SIGN_WINDOWS == 'true'
shell: pwsh
run: ./scripts/verify-authenticode.ps1 -ArtifactDirectory '${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-installer' -Files 'OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe'
- name: Replace Windows installer with signed output
if: env.SIGN_WINDOWS == 'true'
shell: pwsh
run: Copy-Item -LiteralPath "${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-installer/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe" -Destination "${{ github.workspace }}/${{ env.BUILD_DIR }}" -Force
- name: Pack app
if: runner.os == 'Windows'
working-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}
shell: cmd
run: '"C:/Program Files/7-Zip/7z.exe" a -tzip OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable.zip ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer'
shell: pwsh
run: |
$zipPath = "OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable.zip"
Remove-Item -LiteralPath $zipPath -Force -ErrorAction SilentlyContinue
& "C:/Program Files/7-Zip/7z.exe" a -tzip $zipPath "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer"
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
- name: Pack PDB
if: runner.os == 'Windows' && inputs.arch != 'arm64' && !vars.SELF_HOSTED
@@ -521,6 +672,7 @@ jobs:
with:
name: OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable
path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer
if-no-files-found: error
- name: Upload artifacts Win installer
if: runner.os == 'Windows' && !vars.SELF_HOSTED
@@ -543,13 +695,6 @@ jobs:
name: OrcaSlicer_profile_validator_Windows_${{ env.ver }}
path: ${{ github.workspace }}/build/src/Release/OrcaSlicer_profile_validator.exe
- name: Check exception throw info Win
if: runner.os == 'Windows' && inputs.compiler == 'clang'
working-directory: ${{ github.workspace }}
# After the uploads, so a failure still leaves the build and its PDB to inspect, and before any deploy.
shell: pwsh
run: scripts\check_exception_throw_info.ps1 -BuildDir $env:BUILD_DIR
- name: Deploy Windows release portable
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
uses: ./.github/actions/deploy-nightly
-25
View File
@@ -232,31 +232,6 @@ jobs:
mkdir -p ${{ runner.temp }}/profile-check-results
echo "${{ github.event.pull_request.number }}" > ${{ runner.temp }}/profile-check-results/pr_number.txt
# Builds a readable diff of the custom G-code the PR changes, which GitHub shows as
# one long line per setting, for check_profiles_comment.yml to post.
- name: Diff custom G-code
if: ${{ always() && github.event_name == 'pull_request' }}
continue-on-error: true
env:
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
git fetch --no-tags --depth=2 origin "$(git rev-parse HEAD)"
results=${{ runner.temp }}/profile-check-results
diff=${{ runner.temp }}/gcode_diff.md
# HEAD is the PR's merge commit, so HEAD^1 is the base it merges onto.
python3 ./scripts/orca_profile_tool.py gcode-diff --base HEAD^1 --head HEAD > "$diff"
if [ -s "$diff" ]; then
{
echo "<!-- profile-gcode-diff-comment -->"
cat "$diff"
echo ""
# The comment workflow finds this line as the only one starting with <sub>.
echo "<sub>For commit ${PR_HEAD_SHA} of this PR.</sub>"
} > "$results/gcode_diff.md"
fi
# Tells the comment workflow that a missing gcode_diff.md means no G-code changed.
touch "$results/gcode_diff_done.txt"
- name: Prepare comment artifact
if: ${{ always() && github.event_name == 'pull_request' && (steps.profile_tool.outcome == 'failure' || steps.legacy_compat.outcome == 'failure' || steps.validate_system.outcome == 'failure' || steps.validate_slice.outcome == 'failure' || steps.validate_filament_subtypes.outcome == 'failure' || steps.validate_custom.outcome == 'failure') }}
run: |
+3 -58
View File
@@ -38,7 +38,6 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
if [ ! -f pr_number.txt ]; then
echo "No pr_number.txt in artifact, skipping."
@@ -50,72 +49,18 @@ jobs:
echo "Invalid PR number: $PR_NUMBER"
exit 1
fi
# PR code wrote the artifact, so only a run for the named PR's current head may post.
if [ -z "$HEAD_SHA" ]; then
echo "The triggering run names no head commit."
exit 1
fi
if ! PR_HEAD=$(gh api "repos/${GH_REPO}/pulls/${PR_NUMBER}" --jq .head.sha); then
echo "Cannot look up PR #${PR_NUMBER}."
exit 1
fi
if [ "$HEAD_SHA" != "$PR_HEAD" ]; then
echo "${HEAD_SHA} is not the head of PR #${PR_NUMBER}; skipping."
exit 0
fi
# Lists every bot comment on the PR, one JSON object per line.
COMMENTS=$(gh api --paginate "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" \
--jq '.[] | select(.user.login == "github-actions[bot]") | {id, body} | @json')
# Delete prior comments matching the marker (from check_profiles.yml) or the legacy heading.
OLD_IDS=$(printf '%s\n' "$COMMENTS" | jq -r 'select((.body | startswith("<!-- profile-validation-comment -->")) or (.body | startswith("## :x: Profile Validation Errors"))) | .id')
OLD_IDS=$(gh api --paginate "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" \
--jq '.[] | select(.user.login == "github-actions[bot]") | select((.body | startswith("<!-- profile-validation-comment -->")) or (.body | startswith("## :x: Profile Validation Errors"))) | .id')
for comment_id in $OLD_IDS; do
echo "Deleting outdated profile-validation comment ${comment_id}"
gh api -X DELETE "repos/${GH_REPO}/issues/comments/${comment_id}" || true
done
# Post a new comment only when validation failed (pr_comment.md present).
# A failed post fails the job only after the G-code diff is handled.
failed=0
if [ -f pr_comment.md ]; then
gh pr comment "$PR_NUMBER" --body-file pr_comment.md || failed=1
gh pr comment "$PR_NUMBER" --body-file pr_comment.md
else
echo "Validation succeeded; cleaned up prior comments without posting."
fi
# Makes the G-code diff comment match gcode_diff.md, or deletes it if there is none.
update_gcode_diff_comment() {
if [ ! -f gcode_diff_done.txt ]; then
echo "The G-code diff step did not finish; leaving any G-code diff comment as it is."
return 0
fi
GCODE_IDS=$(printf '%s\n' "$COMMENTS" | jq -r 'select(.body | startswith("<!-- profile-gcode-diff-comment -->")) | .id')
if [ -f gcode_diff.md ] && [ "$(wc -c < gcode_diff.md)" -gt 65536 ]; then
echo "gcode_diff.md is over GitHub's comment limit; not posting it."
rm gcode_diff.md
fi
if [ -f gcode_diff.md ] && [ "$(echo "$GCODE_IDS" | wc -w)" -eq 1 ]; then
OLD_BODY=$(printf '%s\n' "$COMMENTS" | jq -r --argjson id "$GCODE_IDS" 'select(.id == $id) | .body' | tr -d '\r')
NEW_BODY=$(tr -d '\r' < gcode_diff.md)
if [ "$OLD_BODY" = "$NEW_BODY" ]; then
echo "G-code diff unchanged; keeping comment ${GCODE_IDS}."
return 0
fi
# Edits in place when only the <sub> line differs, since edits notify nobody.
if [ "$(grep -v '^<sub>' <<< "$OLD_BODY")" = "$(grep -v '^<sub>' <<< "$NEW_BODY")" ]; then
echo "G-code diff unchanged at a new commit; updating comment ${GCODE_IDS}."
gh api -X PATCH "repos/${GH_REPO}/issues/comments/${GCODE_IDS}" -F body=@gcode_diff.md > /dev/null
return 0
fi
fi
for comment_id in $GCODE_IDS; do
echo "Deleting outdated G-code diff comment ${comment_id}"
gh api -X DELETE "repos/${GH_REPO}/issues/comments/${comment_id}" || true
done
if [ -f gcode_diff.md ]; then
gh pr comment "$PR_NUMBER" --body-file gcode_diff.md
fi
}
update_gcode_diff_comment
exit "$failed"
+1 -1
View File
@@ -91,7 +91,7 @@ See `.claude/skills/orca-translations/references/source-audit.md` for the full c
### Terminology
- Use the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md) as the source of truth for recurring terms, so the same English term is always rendered the same way within a language, and terms that must stay in English (brand/product names, acronyms, materials, file formats, G-code tokens, macros/variables/identifiers) are not translated.
- If a term's established translation changes, update both the affected `.po` files and the glossary (`developer_reference/glossary/<lang>.tsv` in the wiki, then regenerate) so they stay in sync.
- If a term's established translation changes, update both the affected `.po` files and the glossary (`localization_glossary.tsv`, then regenerate) so they stay in sync.
- Translate the *meaning*, not the words. Check what the string actually controls before translating it — English reuses one word for different things. `Flow ratio` (multiplier), `Flow Rate` (throughput) and `Flow Dynamics` (pressure compensation) are three different terms; `extruder` may mean the toolhead, the feeder motor, or the nozzle depending on the string.
- Reuse one template per recurring message shape (`Failed to connect to …`, `Are you sure you want to …?`), even where the English wording varies.
+17 -21
View File
@@ -288,19 +288,6 @@ if (APPLE)
# Type"); Ninja/-Wall does not, and under -Werror it fails Xcode builds on code CI accepts.
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_64_TO_32_BIT_CONVERSION "NO")
# The Xcode generator ignores CMAKE_<LANG>_COMPILER_LAUNCHER, so hand the launcher to
# Xcode's own Compiler Launcher setting, which wraps every clang compile (C, C++,
# Objective-C++ and the precompiled header) with one program: the C++ launcher wins when
# both are set. The setting takes a single program path, not a CMake launcher list.
# Xcode versions without it ignore it and build uncached.
if (CMAKE_GENERATOR STREQUAL "Xcode")
if (CMAKE_CXX_COMPILER_LAUNCHER)
set(CMAKE_XCODE_ATTRIBUTE_C_COMPILER_LAUNCHER "${CMAKE_CXX_COMPILER_LAUNCHER}")
elseif (CMAKE_C_COMPILER_LAUNCHER)
set(CMAKE_XCODE_ATTRIBUTE_C_COMPILER_LAUNCHER "${CMAKE_C_COMPILER_LAUNCHER}")
endif ()
endif ()
message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}")
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(CMAKE_INSTALL_RPATH "$ORIGIN")
@@ -321,14 +308,6 @@ if (IS_CROSS_COMPILE)
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
endif ()
# With a precompiled header, clang-cl can omit an exception type's copy constructor from its throw info,
# so copies of the exception share and double-free their data: https://github.com/llvm/llvm-project/issues/53486
if (MSVC AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
message(STATUS "clang-cl: building without the precompiled header (llvm/llvm-project#53486)")
set_property(CACHE SLIC3R_PCH PROPERTY HELPSTRING "Use precompiled headers (ignored with clang-cl, llvm/llvm-project#53486)")
set(SLIC3R_PCH OFF)
endif ()
# Print out the SLIC3R_* cache options
get_cmake_property(_cache_vars CACHE_VARIABLES)
list (SORT _cache_vars)
@@ -1376,6 +1355,23 @@ set (CPACK_NSIS_EXECUTABLES_DIRECTORY ".")
set(CPACK_PACKAGE_EXECUTABLES "orca-slicer;OrcaSlicer")
set(CPACK_CREATE_DESKTOP_LINKS "orca-slicer")
set (CPACK_RESOURCE_FILE_LICENSE ${CMAKE_SOURCE_DIR}/LICENSE.txt) # must also include in install command
# Version information of the installer and of the uninstaller it writes, the same as the
# binaries it installs carry (src/dev-utils/platform/msw/OrcaSlicer-version.rc.in):
# SignPath only signs them when ProductName and ProductVersion match the artifact
# configurations in signpath/. The include lets the release workflow embed a signed
# uninstaller. The NSIS strings are single-quoted and the paths use forward slashes
# because CPack writes this value into CPackConfig.cmake unescaped; !include only accepts
# a forward-slash path through !addincludedir.
string (CONCAT CPACK_NSIS_DEFINES
"VIProductVersion '${ORCA_VERSION_MAJOR}.${ORCA_VERSION_MINOR}.${ORCA_VERSION_PATCH}.0'\n"
"VIFileVersion '${ORCA_VERSION_MAJOR}.${ORCA_VERSION_MINOR}.${ORCA_VERSION_PATCH}.0'\n"
"VIAddVersionKey /LANG=1033 'CompanyName' 'SoftFever'\n"
"VIAddVersionKey /LANG=1033 'FileDescription' '${SLIC3R_APP_NAME} Installer'\n"
"VIAddVersionKey /LANG=1033 'FileVersion' '${SoftFever_VERSION}'\n"
"VIAddVersionKey /LANG=1033 'ProductName' '${SLIC3R_APP_NAME}'\n"
"VIAddVersionKey /LANG=1033 'ProductVersion' '${SoftFever_VERSION}'\n"
"!addincludedir '${CMAKE_SOURCE_DIR}/src/dev-utils/platform/msw'\n"
"!include 'uninstaller-signing.nsh'\n")
set(CPACK_WIX_UPGRADE_GUID "058245e8-20e0-4a95-9ab7-1acfe17ad511")
set(CPACK_GENERATOR NSIS)
+2 -2
View File
@@ -473,8 +473,8 @@ fi
# cmake 4.x compatibility workaround
export CMAKE_POLICY_VERSION_MINIMUM=3.5
DISTRIBUTION=$(awk -F= '/^ID=/ {print $2}' /etc/os-release | tr -d "'\"")
DISTRIBUTION_LIKE=$(awk -F= '/^ID_LIKE=/ {print $2}' /etc/os-release | tr -d "'\"")
DISTRIBUTION=$(awk -F= '/^ID=/ {print $2}' /etc/os-release | tr -d '"')
DISTRIBUTION_LIKE=$(awk -F= '/^ID_LIKE=/ {print $2}' /etc/os-release | tr -d '"')
# Check for direct distribution match to Ubuntu/Debian
if [ "${DISTRIBUTION}" == "ubuntu" ] || [ "${DISTRIBUTION}" == "linuxmint" ] ; then
DISTRIBUTION="debian"
-50
View File
@@ -111,54 +111,6 @@ else
export CMAKE_POLICY_COMPAT=""
fi
# Compiler cache, as in build_linux.sh. The program comes from CMAKE_CCACHE, else from a
# launcher already set in the environment (the CI build sets ccache there), else sccache or
# ccache from PATH; one program serves both languages. Each run applies its own choice to the
# build directories, replacing a launcher an earlier run or a hand-run configure recorded.
CMAKE_CCACHE_ARGS=()
CCACHE_VAR=""
for var in CMAKE_CCACHE CMAKE_CXX_COMPILER_LAUNCHER CMAKE_C_COMPILER_LAUNCHER; do
if [ -n "${!var:-}" ]; then
CCACHE_VAR=$var
break
fi
done
if [ -n "$CCACHE_VAR" ]; then
CCACHE_PROGRAM=${!CCACHE_VAR}
echo "Checking ${CCACHE_VAR} environment variable for compiler cache program..."
CMAKE_CCACHE=$(command -v "${CCACHE_PROGRAM}") || {
echo "${CCACHE_VAR} environment variable is set to '${CCACHE_PROGRAM}' but it was not found in PATH."
CMAKE_CCACHE=""
}
elif command -v sccache >/dev/null 2>&1 ; then
CMAKE_CCACHE=$(command -v sccache)
elif command -v ccache >/dev/null 2>&1 ; then
CMAKE_CCACHE=$(command -v ccache)
fi
if [ -n "${CMAKE_CCACHE}" ] ; then
echo "${CMAKE_CCACHE} found, enabling compiler caching..."
CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER="${CMAKE_CCACHE}" -DCMAKE_CXX_COMPILER_LAUNCHER="${CMAKE_CCACHE}")
# The same ccache settings as the CI build. Without this sloppiness ccache refuses every
# compile that uses the precompiled header, which is most of OrcaSlicer, and skips headers
# a checkout has just written. It is added to the sloppiness already configured, and
# CCACHE_SLOPPINESS or CCACHE_(NO)DEPEND set in the environment win.
if [[ "$(basename "${CMAKE_CCACHE}")" == ccache ]] ; then
if [[ -z "${CCACHE_SLOPPINESS+x}" ]] ; then
CCACHE_SLOPPINESS=$("${CMAKE_CCACHE}" --get-config sloppiness 2>/dev/null || true)
export CCACHE_SLOPPINESS="${CCACHE_SLOPPINESS:+${CCACHE_SLOPPINESS},}pch_defines,time_macros,include_file_mtime,include_file_ctime"
fi
# Hash the includes the compiler reports instead of preprocessing every miss.
if [[ -z "${CCACHE_DEPEND+x}${CCACHE_NODEPEND+x}" ]] ; then
export CCACHE_DEPEND=1
fi
fi
else
echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds."
# Clear a launcher recorded by an earlier configure. The entries must be set empty rather
# than removed: CMake fills a missing launcher entry from the environment.
CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER:STRING= -DCMAKE_CXX_COMPILER_LAUNCHER:STRING=)
fi
echo "Build params:"
echo " - ARCH: $ARCH"
echo " - BUILD_CONFIG: $BUILD_CONFIG"
@@ -210,7 +162,6 @@ function build_deps() {
-DCMAKE_OSX_ARCHITECTURES:STRING="${_ARCH}" \
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
${CMAKE_POLICY_COMPAT}
fi
cmake --build . --config "$BUILD_CONFIG" --target deps
@@ -313,7 +264,6 @@ function build_slicer() {
-DCMAKE_OSX_ARCHITECTURES="${_ARCH}" \
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
${CMAKE_POLICY_COMPAT}
fi
cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET"
+4 -7
View File
@@ -53,7 +53,7 @@ call :add_section "Build configuration"
call :add_arg config string "" config "release, debug, relwithdebinfo or minsizerel (default: release)"
call :add_arg target_arch string "" arch "x64 or arm64 (default: the host architecture)"
call :add_arg slicer_asan bool a asan "Build the slicer with ASAN enabled"
call :add_arg no_pch bool "" no-pch "Build without the precompiled header, implied by -l and --cache"
call :add_arg no_pch bool "" no-pch "Build without the precompiled header, implied by --cache"
call :add_section "Toolchain"
call :add_arg use_clang_cl bool l clang-cl "Use clang-cl as the compiler"
@@ -388,9 +388,6 @@ if "%use_ninja%" == "ON" if "%use_msbuild%" == "ON" (
exit /b 1
)
REM CMakeLists turns the precompiled header off for clang-cl (llvm/llvm-project#53486).
if "%use_clang_cl%" == "ON" set "no_pch=ON"
set "generator=!vs_gen_%vs_version%!"
if "%use_ninja%" == "ON" (
set "generator=Ninja Multi-Config"
@@ -546,7 +543,7 @@ REM real path rather than one glued onto the repository root.
for %%p in ("!build_dir!") do set "build_full=%%~fp"
echo Configuration: %build_type%, %arch%
if not "%clang_exe%" == "" echo Compiler: %clang_exe%
if "%build_slicer%" == "ON" if "%no_pch%" == "ON" echo Precompiled header: off
if "%no_pch%" == "ON" echo Precompiled header: off
if not "%cache_args%" == "" echo Compiler cache: %cache_exe%
set "SIG_FLAG="
@@ -719,8 +716,8 @@ if "%build_slicer%" == "ON" (
set "slicer_args=!slicer_args! -DSLIC3R_ASAN=ON"
)
REM Under cl, a later -DSLIC3R_PCH=ON in ORCA_SLICER_CMAKE_ARGS still wins,
REM because CMake takes the last definition on the command line.
REM A later -DSLIC3R_PCH=ON in ORCA_SLICER_CMAKE_ARGS still wins, because
REM CMake takes the last definition on the command line.
if "%no_pch%" == "ON" (
set "slicer_args=!slicer_args! -DSLIC3R_PCH=OFF"
)
+2 -5
View File
@@ -26,7 +26,6 @@ orcaslicer_add_cmake_project(OpenCV
URL https://github.com/opencv/opencv/archive/refs/tags/4.6.0.tar.gz
URL_HASH SHA256=1ec1cba65f9f20fe5a41fda1586e01c70ea0c9a6d7b67c9e13edf0cfe2239277
PATCH_COMMAND git apply ${OpenCV_DIRECTORY_FLAG} --verbose --ignore-space-change --whitespace=fix ${CMAKE_CURRENT_LIST_DIR}/0001-vs.patch ${CMAKE_CURRENT_LIST_DIR}/0002-clang19-macos.patch
DEPENDS ${JPEG_PKG}
CMAKE_ARGS
-DBUILD_SHARED_LIBS=0
-DBUILD_PERE_TESTS=OFF
@@ -35,8 +34,7 @@ orcaslicer_add_cmake_project(OpenCV
-DBUILD_EXAMPLES=OFF
-DBUILD_JASPER=OFF
-DBUILD_JAVA=OFF
# Share Orca's libjpeg to avoid conflicting codec symbols in the application.
-DBUILD_JPEG=OFF
-DBUILD_JPEG=ON
-DBUILD_APPS_LIST=version
-DBUILD_opencv_apps=OFF
-DBUILD_opencv_java=OFF
@@ -69,8 +67,7 @@ orcaslicer_add_cmake_project(OpenCV
-DWITH_OPENJPEG=OFF
-DWITH_QUIRC=OFF
-DWITH_VTK=OFF
# Textured GLB/OBJ imports decode embedded and external JPEGs with cv::imdecode.
-DWITH_JPEG=ON
-DWITH_JPEG=OFF
-DWITH_WEBP=OFF
-DWITH_TIFF=OFF
-DBUILD_TIFF=OFF
+7 -17
View File
@@ -16,8 +16,6 @@ else()
endif()
endif()
set(_openssl_dir ${DESTDIR})
if(WIN32)
set(_openssl_msvc_env CC=cl CXX=cl RC=rc CL=/FS)
# OpenSSL's perl Configure honors the CC environment variable, but the
@@ -45,14 +43,6 @@ if(WIN32)
else()
if(APPLE)
set(_conf_cmd export MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} && ./Configure -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET})
# OPENSSLDIR is only where OpenSSL looks at runtime for cert.pem, certs/
# and openssl.cnf; install_sw installs nothing there. Point it at the
# system CA bundle, the one libcurl already uses here, so the bundled
# Python's ssl module can verify HTTPS. Elsewhere it stays the prefix:
# Windows Python reads the system certificate store, and on Linux, where
# the bundle's location differs by distro, CurlGlobalInit points
# SSL_CERT_FILE at it at startup.
set(_openssl_dir /etc/ssl)
else()
# A static library that is embedded into a shared object must not export
# its symbols. On Linux the running process also loads the system OpenSSL
@@ -91,7 +81,7 @@ ExternalProject_Add(dep_OpenSSL
# URL_HASH SHA256=8c776993154652d0bb393f506d850b811517c8bd8d24b1008aef57fbe55d3f31
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/OpenSSL
CONFIGURE_COMMAND ${_conf_cmd} ${_cross_arch}
"--openssldir=${_openssl_dir}"
"--openssldir=${DESTDIR}"
"--prefix=${DESTDIR}"
# OpenSSL's linux-x86_64 target sets multilib=64, so it installs to
# <prefix>/lib64 while every other dep uses <prefix>/lib. CPython's
@@ -125,12 +115,12 @@ ExternalProject_Add_Step(dep_OpenSSL install_cmake_files
WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}"
)
if (NOT WIN32)
# OpenSSL's object rules do not depend on the configured flags or paths, so
# reconfiguring it (a new compiler flag, a different --openssldir) relinks
# the archives from stale objects instead of recompiling them, and the
# change silently has no effect. Drop the objects whenever this recipe
# changes so the next build actually recompiles them.
if (NOT WIN32 AND NOT APPLE)
# OpenSSL's object rules do not depend on CFLAGS, so reconfiguring it (for
# example to add -fvisibility=hidden) relinks the archives from stale
# objects instead of recompiling them, and the change silently has no
# effect. Drop the objects whenever this recipe changes so the next build
# actually recompiles them.
ExternalProject_Get_Property(dep_OpenSSL SOURCE_DIR)
ExternalProject_Add_Step(dep_OpenSSL clean_objects
DEPENDEES configure
+4 -5
View File
@@ -365,11 +365,10 @@ if(TARGET dep_ZLIB)
add_dependencies(dep_python3 dep_ZLIB)
endif()
if (NOT WIN32)
# _ssl and _hashlib embed the static OpenSSL built in deps/ (Windows uses
# CPython's prebuilt OpenSSL instead). Their Makefile rules depend only on
# their own sources, not on the OpenSSL archives, so a rebuilt OpenSSL does
# not make them relink and they keep the previous one. On an incremental tree,
if (NOT WIN32 AND NOT APPLE)
# CPython's Makefile rules for _ssl and _hashlib depend only on their own
# sources, not on the OpenSSL archives, so a rebuilt OpenSSL does not make
# them relink and they keep the previous symbols. On an incremental tree,
# drop the built modules and relink them against the current OpenSSL; a
# fresh build is left alone (its PGO target builds them). "make" alone is a
# no-op once PGO has run, so sharedmods is invoked explicitly.
+1 -9
View File
@@ -21,14 +21,6 @@ else ()
set(_wx_edge "-DwxUSE_WEBVIEW_EDGE=OFF")
endif ()
# With a precompiled header, clang-cl can omit an exception type's copy constructor from its throw info,
# so copies of the exception share and double-free their data: https://github.com/llvm/llvm-project/issues/53486
if (MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
set(_wx_precomp OFF)
else ()
set(_wx_precomp ON)
endif ()
set(_wx_patch_command "")
if (APPLE)
set(_wx_patch_command
@@ -47,7 +39,7 @@ orcaslicer_add_cmake_project(
PATCH_COMMAND ${_wx_patch_command}
DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} ${JPEG_PKG}
CMAKE_ARGS
-DwxBUILD_PRECOMP=${_wx_precomp}
-DwxBUILD_PRECOMP=ON
${_wx_toolkit}
"-DCMAKE_DEBUG_POSTFIX:STRING=${_wx_debug_postfix}"
-DwxBUILD_DEBUG_LEVEL=0
@@ -1,8 +1,8 @@
# Adaptive infill — High Level Design
# Adaptive TPMS infill — High Level Design
## Purpose and scope
`adaptive_infill_mode` grades the sparse infill of the Gyroid, TPMS-D and TPMS-FK
`tpms_adaptive` grades the sparse infill of the Gyroid, TPMS-D and TPMS-FK
patterns inside the object: the cells grow continuously from the surface
towards the center of the object. `distance_warp`, `smooth_blend` and
`stepped_shells` follow the distance to the nearest surface, including the top
@@ -10,8 +10,8 @@ and bottom, like concentric shells; `lobes` follows the whole 3D shape towards
the center of each lobe of the object; `normal_z`, `normal_y` and `normal_x`
follow each section of the object normal to that axis, so the grading does not
change along the axis, as suits a profile extruded along it.
`sparse_infill_density` is the density at the surface, `adaptive_infill_interior_density`
the density at the center, and `adaptive_infill_gradient` picks how the density
`sparse_infill_density` is the density at the surface, `tpms_interior_density`
the density at the center, and `tpms_adaptive_gradient` picks how the density
goes from one to the other. Only internal sparse infill is graded; the Gyroid
Z-buckling optimization does not apply to it.
@@ -22,9 +22,9 @@ split into shells or blended between densities.
## Radial field
`AdaptiveInfillField` gives every point of an object the center of its lobe and a
`TpmsRadialField` gives every point of an object the center of its lobe and a
radial coordinate: 0 at the center, 1 at the surface along the ray from the
center. `PrintObject::prepare_adaptive_infill_fields()` builds it in
center. `PrintObject::prepare_tpms_radial_fields()` builds it in
`bridge_over_infill()`, next to the adaptive cubic octree, because the anchoring
infill generated there has to match the printed infill. A field is built for
every mode a region uses, and is shared by the regions using that mode: the
@@ -202,7 +202,7 @@ built for them.
## Constraints
- With `adaptive_infill_mode` disabled, or for other patterns, the fill parameters are reset
- With `tpms_adaptive` disabled, or for other patterns, the fill parameters are reset
to their defaults, so they neither change the infill nor split fill batches.
- `Layer::get_sparse_infill_max_void_area()` uses the sparser of the two
densities, as the voids at the center are that large.
+2 -5
View File
@@ -350,11 +350,8 @@ identity default and translates nothing. `NetworkAgent` forwards both to the liv
comparison sites below reach them through `wxGetApp().getAgent()` and leave an id untranslated
while no agent is live. Whole documents are Bambu's business alone:
`BBLPrinterAgent::to_orca_payload` and `from_orca_payload` rewrite every string under
`tray_info_idx`, `filament_id`, `filamentId`, `cate` or `fila_id` at any depth; text that does not
parse, or carries none of those keys, comes back unchanged. The last two are the H2C nozzle rack's:
`cate` in the auto nozzle mapping request, `fila_id` in the rack status. A new Bambu message that
carries a filament id under another key needs that key added to `filament_id_keys`, or the id
crosses the boundary untranslated. The map is loaded once, lazily; a missing or malformed
`tray_info_idx`, `filament_id` or `filamentId` at any depth; text that does not parse, or carries
none of those keys, comes back unchanged. The map is loaded once, lazily; a missing or malformed
file degrades to identity with a log line rather than failing.
| Boundary | Where it translates |
-7
View File
@@ -53,13 +53,6 @@ Space counts as typing. The speed dial's default is the one bare Space, and
`MainFrame` leaves it to a focused control that uses Space itself (text fields, buttons,
combo boxes), so it opens the dial from the canvases and the tab strip only.
`MainFrame::handle_global_shortcut` skips a chord that `KeyChord::edits_text()` reports
while a text field has the focus (a wx text entry, spin control, web view or ImGui
InputText), and skips every chord while an ImGui modal is open, as a wx modal dialog
would. `ImGuiWrapper::owns_key()` reads the active InputText and the popup stack
directly, since `WantCaptureKeyboard` lags a frame, and is called only when the focus is on
a visible canvas from `OpenGLManager::create_wxglcanvas()`.
## Which event a chord matches
Letters, digits and special keys match on `wxEVT_KEY_DOWN`. Its key codes do not depend
-198
View File
@@ -1,198 +0,0 @@
# Travel optimization — High Level Design
## Purpose and scope
Inside an island, the infill paths of each region are chained into one print
order by a greedy pass that joins the closest free path ends first. That pass
leaves its longest joins for last, and they often cross the whole island. Two
things shorten the travel between infill paths:
- the fill generators join their lines into as few paths as they can, since
every separate path needs a travel to it;
- the G-code export refines the greedy chain with a local search that shortens
the straight-line travel between the paths.
The refinement covers every chain that `chain_and_reorder_extrusion_entities()`
builds from a start point: the infill and ironing of `GCode::extrude_infill()`
and the support of `GCode::extrude_support()`, as well as the order of objects
and islands under `print_order` Default. It changes only the order and the
direction of the paths, not the paths or the walls. Neither part has a setting.
## The chain
`ObjectByExtruder::Island::Region::append()` flattens every sortable fill
collection into its paths, so the paths of all the fills of a region in one
island are chained together. A collection that must keep its order (`no_sort`:
monotonic, concentric, a forced fill order, the flow calibration) stays a single
entry that cannot be reversed. Paths that must keep their direction cannot be
reversed either: Grid's, since it alternates their direction between layers for
strength, and those of a fill with a forced order. A loop starts and ends at the
same point, so reversing it changes nothing.
`extrude_infill()` chains each region from the current nozzle position with
`chain_and_reorder_extrusion_entities()`, and chains the paths inside a nested
sortable collection the same way; `extrude_support()` chains the support paths
of a layer so too, unless they keep their order. The overload without a start
point, used to chain collections while slicing, keeps the greedy chain. The
order the fill generators give their paths does not reach the G-code for a
sortable fill, which is chained again.
## Connecting the lines
Most patterns cut their lines at the infill boundary, and `Fill::connect_infill()`
joins pairs of line ends that are neighbors on a boundary contour by the arc of
wall between them, shortest arcs first, as long as neither end is joined yet and
the join closes no loop. An arc is joined when it is shorter than
`infill_anchor_max`, or than two anchors and a line width if that is longer:
two ends further apart each get an anchor toward the other, and where those
would meet, the join prints about as much wall as one path instead of two.
Regular patterns leave many arcs of the same length, and the order among them
decides the pairing. In an arbitrary order the pairs along one wall can fall out
of step, such as lines 1–2 and 3–4, then 6–7 and 8–9, which leaves the end of
line 5 loose, and every loose end starts or ends a separate path. So the arcs
are also sorted in steps of half the line spacing and, within a step, along each
contour forwards and backwards, which pairs the ends of a wall in one phase.
Each order is replayed on the flags and a union-find of the lines, without
building any path, and the one that joins the most lines is used; a tie keeps
the shortest-first order.
The replay includes the last pass, which joins any two open ends that are
neighbors on a contour. With multiline infill the main pass leaves the copies of
one line to that pass, so that each copy reaches a wall of its own. Two
neighboring groups cut at both walls can then be joined at either wall by arcs
of the same length: at one, their open ends stay next to their own copies, which
the last pass joins, so each line prints out and back; at the other, the copies
end up in separate paths printed apart.
A line end left unpaired gets an anchor, a piece of the wall at most
`infill_anchor` long. It runs toward the nearest open end of another path,
looking up to 64 ends along the contour, since the next path usually starts
there; without one it takes the side with more free wall. With multiline infill
an anchor toward a copy of the same line stops short of the copy rather than
printing over it. A copy is a neighboring end less than the group width
(`multiline` spacings) across the line, at any wall angle, while the next group
lies a whole line spacing away. Meeting the wall at an angle θ, the copy covers
half a width over sin θ of the wall on each side of its end, so the anchor stops
that far and half its own width before it, or is left out; the end still counts
as anchored, so the last pass pairs the other ends as before.
Last, `Fill::fill_surface_extrusion()` joins the sparse infill lines whose ends
are at most two line spacings apart with a straight piece. With the anchors off
(`infill_anchor_max` 0) nothing above runs and this is the only joining; with
them on, it takes what the steps above leave next to each other, such as two
anchors that stop just short of each other or a short piece cut off by a corner.
A gap that short leaves no room to print anything in between, and as a travel it
is the one hop a reordering can trade for a longer travel that retracts.
Multiline copies leave the most such ends: one spacing apart across the line,
their ends where it meets a wall at an angle θ are one spacing over sin θ apart
along it, so two spacings reach walls met at 30° and steeper.
Ends that meet are joined first, then the gaps between the leading ends of their
lines, along each line's overall direction, then the rest, shortest first within
each, without closing a loop or crossing a line or another join. Taking the
leading ends first joins every pair of copies at the same side, which leaves
their open ends next to each other; by length alone, neighboring pairs were
joined at different walls and the travel between them crossed the island. Lines
that cannot reverse, such as Grid's, only join the end of one to the start of the
next, and fills that keep their order are left alone.
## The search
A chain of up to eight paths is found exactly, by dynamic programming over the
subsets of paths: a state is the paths visited so far, the last one and its
direction, about four thousand states for eight paths. The shared greedy
chaining, `chain_segments_greedy_constrained_reversals_()`, does the same for two
to four segments, which also orders up to four islands of a layer and the loops
of a perimeter collection exactly. Either way the first path is no longer
always the one nearest the start; with a free end, a path near the start can be
worth leaving for last.
Objects (`print_order` Default) and the tour of islands across objects are
visited in the same order on every layer, so `chain_points_with_postprocessing()`
orders them as a closed tour rather than an open chain. Up to ten points the
shortest tour is found exactly by the same dynamic programming with the first
point fixed; with a start point the tour then begins at the point nearest it.
Larger sets take the greedy chain and alternate 2-opt, or-opt and crossing
removal; or-opt moves runs of up to three points next to one of the eight points
nearest an end of the run, found on the same grid as the chain's candidates.
For longer chains, the search refines the greedy chain. On an island cut into many short pieces,
such as infill without anchors, that chain can sweep away from the nozzle and
come back for the rest with one long jump, which no local move below removes.
So a nearest-next chain, going from the nozzle to the nearest free path end each
time, is built too; when it is already shorter than the refined greedy chain, it
is refined and used instead. Elsewhere it costs one grid query per path.
`improve_chain_by_local_search()` sees the chain as a sequence of segments with
an entry and an exit point each. Its cost is the straight distance from the
nozzle to the first entry plus the distance from every exit to the next entry;
the end of the chain is free. It applies three kinds of moves:
- 2-opt reverses a run of segments, which flips each of them and replaces the
two links at the ends of the run.
- Or-opt moves a run of up to three segments between two others, flipped or
not.
- A run of any length that starts or ends next to the nozzle moves to the front.
Local search can settle on a chain that starts with a block of paths far from
the nozzle while the paths next to it sit in the middle, and moving that block
takes more than three segments at once.
A move that would flip a segment that cannot be reversed is skipped, so such
segments are only reordered.
A move is tried only if every link it creates joins an end point to one of the
32 end points nearest to it, which covers every move on a chain of up to 17
segments, as most islands are; with 8 candidates the search missed most of the
gain on grid-like patterns such as Triangles. The candidates come from a uniform
grid of about two end points per cell, searched in rings of cells around the
query. `KDTreeIndirect` visits its subtrees in a fixed order rather than the
nearer one first, so a query for 32 points walks most of the tree, and on long
chains finding the candidates would cost more than the search itself.
The candidates of an end point are tried nearest first, and only while they are
closer to it than the link the move would replace there. A 2-opt move that pays
off creates a link shorter than the one it replaces at one of its two ends, so
this misses no 2-opt move. An or-opt move is tried only while its new link is
shorter than what taking the run out saves, which skips a few or-opt moves that
would still pay off but cuts the search on long chains to a fraction.
The search works through a queue of segments and applies the first move that
shortens the chain around the segment it examines. After a move, only the
segments next to a changed link are queued again. A new link can also make room
for a run elsewhere that no segment next to it would try, so when the queue
empties after any move, every segment is queued once more, and the search ends
when such a sweep finds no move.
Every move shortens the chain by more than `SCALED_EPSILON`, so the result is
never longer than the greedy or the nearest-next chain and the search ends; a
cap of 50 moves per segment only bounds its time. The search has no randomness
and visits the segments in a fixed order, so the same input always gives the
same G-code.
## Cost
Finding the candidates of an end point visits a few cells, and the nearest-next
chain makes one such query per path, which only grows where the paths around the
nozzle are used up and the chain jumps. Examining a segment tries at most a few
hundred candidate links, usually only the nearest few, plus a scan of the chain
for the move to the front when it is the first segment, and a move rewrites only
the range of the chain it changes, so the search stays close to linear in the
number of segments. Most islands hold a few to a few dozen paths; the cost shows
on infill cut into thousands of pieces per island, such as dense infill with the
anchors off. The straight joins only look at the line segments near a candidate
join for crossings.
## Limitations
The search minimizes straight-line distance. It does not know which travels
leave the infill area and retract under `reduce_infill_retraction`, nor the
detours of `reduce_crossing_wall`, and a travel's time is not proportional to
its length: a short travel is bound by acceleration, so its time grows about
with the square root of its length. A chain that is shorter in total but trades
very short travels for longer ones, or for ones past `retraction_minimum_travel`,
can occasionally take a little longer to print.
The end of the chain is free: it is not steered toward the next island or the
start of the next layer. The regions of an island are chained one after the
other, in region order, each from where the previous one ended.
+21 -58
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-19 09:48-0300\n"
"POT-Creation-Date: 2026-10-15 17:40+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -1637,16 +1637,16 @@ msgstr ""
msgid "Stroke of shape (%1%) contains unsupported: %2%."
msgstr ""
msgid "Not valid state please report reproduction steps on GitHub"
msgid "Not valid state please report reproduction steps on github"
msgstr ""
msgid "No embossed file"
msgstr ""
msgid "Missing SVG file in embossed shape"
msgid "Missing svg file in embossed shape"
msgstr ""
msgid "Missing data of SVG file"
msgid "Missing data of svg file"
msgstr ""
msgid "Face the camera"
@@ -4579,7 +4579,7 @@ msgstr ""
msgid "Unknown C++ exception."
msgstr ""
msgid "Processing G-code from previous file…"
msgid "Processing G-Code from previous file…"
msgstr ""
msgid "Slicing complete"
@@ -6049,7 +6049,7 @@ msgstr ""
msgid "Size:"
msgstr ""
#, possible-c-format, possible-boost-format
#, possible-boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr ""
@@ -10657,9 +10657,6 @@ msgstr ""
msgid "Send Failed"
msgstr ""
msgid "No available printers"
msgstr ""
msgid "The name length exceeds the limit."
msgstr ""
@@ -12318,7 +12315,7 @@ msgstr ""
msgid "Left mouse"
msgstr ""
msgid "Available anywhere in the window."
msgid "Available anywhere in the window, even while typing in a text field."
msgstr ""
msgid "Run favorite 1 to 10"
@@ -12415,6 +12412,9 @@ msgstr ""
msgid "The default %s is assigned to %s. Reassign it to %s?"
msgstr ""
msgid "Global shortcuts also apply while typing."
msgstr ""
msgid "A key that types a character cannot be a global shortcut."
msgstr ""
@@ -15645,12 +15645,6 @@ msgstr ""
msgid "Enable this option to add EXCLUDE OBJECT command in G-code."
msgstr ""
msgid "Outline tolerance"
msgstr ""
msgid "Maximum deviation of the object outline written to EXCLUDE_OBJECT_DEFINE from the original outline. Larger values give fewer points, which helps firmware that stalls while parsing G-code with many objects. 0 keeps every point."
msgstr ""
msgid "Verbose G-code"
msgstr ""
@@ -20546,7 +20540,7 @@ msgid "test %s start..."
msgstr ""
#, possible-c-format, possible-boost-format
msgid "test %s IP resolved = %s"
msgid "test %s ip resolved = %s"
msgstr ""
#, possible-c-format, possible-boost-format
@@ -26498,18 +26492,17 @@ msgstr ""
msgid "Basic Colors"
msgstr ""
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
msgid "Orca Palette"
msgstr ""
#. TRN: Keep the standard/brand name "RAL" unchanged.
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
msgid "RAL Classic"
msgstr ""
#. TRN: Keep the standard/brand name "RAL" unchanged.
msgid "Gradients"
msgstr ""
msgctxt "Color picker"
msgid "Solid"
msgstr ""
@@ -26884,7 +26877,7 @@ msgstr ""
msgid "Medium violet red"
msgstr ""
msgid "Pale violet red"
msgid "PaleV violet red"
msgstr ""
msgid "Deep pink"
@@ -27046,7 +27039,7 @@ msgstr ""
msgid "Shallow sky"
msgstr ""
msgid "Electric blue"
msgid "Electric bel"
msgstr ""
msgid "Dolphin"
@@ -27115,7 +27108,7 @@ msgstr ""
msgid "Maize yellow"
msgstr ""
msgid "Daffodil yellow"
msgid "Dalmatian yellow"
msgstr ""
msgid "Brown beige"
@@ -27376,7 +27369,7 @@ msgstr ""
msgid "Turquoise blue"
msgstr ""
msgid "Capri blue"
msgid "Capsanthin violet"
msgstr ""
msgid "Ocean blue"
@@ -27490,7 +27483,7 @@ msgstr ""
msgid "Silver grey"
msgstr ""
msgid "Olive grey"
msgid "Olivestone"
msgstr ""
msgid "Moss grey"
@@ -28078,19 +28071,10 @@ msgstr ""
msgid "Built-in"
msgstr ""
msgid "My textures"
msgstr ""
msgid "No textures found."
msgstr ""
msgid "Texture libraries"
msgstr ""
msgid "Explore more texture libraries"
msgstr ""
msgid "Opens OrcaCloud in the Home page to browse for plugins."
msgid "My textures"
msgstr ""
msgid "Import your own image as a height map. It is converted to the format the slicer bakes from and saved to your personal texture folder, kept separate from the textures shipped with OrcaSlicer."
@@ -30143,9 +30127,6 @@ msgstr ""
msgid "Slicing Pipeline"
msgstr ""
msgid "Resource Texture"
msgstr ""
msgid "The plugin's configuration UI was empty. Showing the default editor."
msgstr ""
@@ -30261,24 +30242,6 @@ msgstr ""
msgid "Plugin directory does not contain a .py or .whl entry file"
msgstr ""
msgid "Resource texture directory is not set"
msgstr ""
msgid "Resource texture directory could not be resolved"
msgstr ""
msgid "Resource texture path is not a directory"
msgstr ""
msgid "Plugin folder could not be resolved"
msgstr ""
msgid "Resource texture directory is outside the plugin folder"
msgstr ""
msgid "Resource texture directory could not be read"
msgstr ""
#, possible-boost-format
msgid "Slicing pipeline plugin '%1%' error: %2%"
msgstr ""
@@ -30635,7 +30598,7 @@ msgstr ""
#. TRN %1% is the application name.
#, possible-boost-format
msgid "Not a G-code file generated by %1%."
msgid "Not a gcode file generated by %1%."
msgstr ""
#. TRN %1% is a tag in the G-code file that must not be translated.
@@ -30723,7 +30686,7 @@ msgstr ""
msgid "Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%"
msgstr ""
msgid "Post-processor can't find exported G-code file"
msgid "Post-processor can't find exported gcode file"
msgstr ""
#, possible-boost-format
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+21 -58
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-19 09:48-0300\n"
"POT-Creation-Date: 2026-10-15 17:40+0800\n"
"PO-Revision-Date: 2026-06-17 15:44-0300\n"
"Last-Translator: Alexandre Folle de Menezes\n"
"Language-Team: \n"
@@ -1633,16 +1633,16 @@ msgstr ""
msgid "Stroke of shape (%1%) contains unsupported: %2%."
msgstr ""
msgid "Not valid state please report reproduction steps on GitHub"
msgid "Not valid state please report reproduction steps on github"
msgstr ""
msgid "No embossed file"
msgstr ""
msgid "Missing SVG file in embossed shape"
msgid "Missing svg file in embossed shape"
msgstr ""
msgid "Missing data of SVG file"
msgid "Missing data of svg file"
msgstr ""
msgid "Face the camera"
@@ -4575,7 +4575,7 @@ msgstr ""
msgid "Unknown C++ exception."
msgstr ""
msgid "Processing G-code from previous file…"
msgid "Processing G-Code from previous file…"
msgstr ""
msgid "Slicing complete"
@@ -6045,7 +6045,7 @@ msgstr ""
msgid "Size:"
msgstr ""
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr ""
@@ -10653,9 +10653,6 @@ msgstr ""
msgid "Send Failed"
msgstr ""
msgid "No available printers"
msgstr ""
msgid "The name length exceeds the limit."
msgstr ""
@@ -12314,7 +12311,7 @@ msgstr ""
msgid "Left mouse"
msgstr ""
msgid "Available anywhere in the window."
msgid "Available anywhere in the window, even while typing in a text field."
msgstr ""
msgid "Run favorite 1 to 10"
@@ -12411,6 +12408,9 @@ msgstr ""
msgid "The default %s is assigned to %s. Reassign it to %s?"
msgstr ""
msgid "Global shortcuts also apply while typing."
msgstr ""
msgid "A key that types a character cannot be a global shortcut."
msgstr ""
@@ -15641,12 +15641,6 @@ msgstr ""
msgid "Enable this option to add EXCLUDE OBJECT command in G-code."
msgstr ""
msgid "Outline tolerance"
msgstr ""
msgid "Maximum deviation of the object outline written to EXCLUDE_OBJECT_DEFINE from the original outline. Larger values give fewer points, which helps firmware that stalls while parsing G-code with many objects. 0 keeps every point."
msgstr ""
msgid "Verbose G-code"
msgstr ""
@@ -20542,7 +20536,7 @@ msgid "test %s start..."
msgstr ""
#, c-format, boost-format
msgid "test %s IP resolved = %s"
msgid "test %s ip resolved = %s"
msgstr ""
#, c-format, boost-format
@@ -26494,18 +26488,17 @@ msgstr ""
msgid "Basic Colors"
msgstr ""
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
msgid "Orca Palette"
msgstr ""
#. TRN: Keep the standard/brand name "RAL" unchanged.
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
msgid "RAL Classic"
msgstr ""
#. TRN: Keep the standard/brand name "RAL" unchanged.
msgid "Gradients"
msgstr ""
msgctxt "Color picker"
msgid "Solid"
msgstr ""
@@ -26880,7 +26873,7 @@ msgstr ""
msgid "Medium violet red"
msgstr ""
msgid "Pale violet red"
msgid "PaleV violet red"
msgstr ""
msgid "Deep pink"
@@ -27042,7 +27035,7 @@ msgstr ""
msgid "Shallow sky"
msgstr ""
msgid "Electric blue"
msgid "Electric bel"
msgstr ""
msgid "Dolphin"
@@ -27111,7 +27104,7 @@ msgstr ""
msgid "Maize yellow"
msgstr ""
msgid "Daffodil yellow"
msgid "Dalmatian yellow"
msgstr ""
msgid "Brown beige"
@@ -27372,7 +27365,7 @@ msgstr ""
msgid "Turquoise blue"
msgstr ""
msgid "Capri blue"
msgid "Capsanthin violet"
msgstr ""
msgid "Ocean blue"
@@ -27486,7 +27479,7 @@ msgstr ""
msgid "Silver grey"
msgstr ""
msgid "Olive grey"
msgid "Olivestone"
msgstr ""
msgid "Moss grey"
@@ -28074,19 +28067,10 @@ msgstr ""
msgid "Built-in"
msgstr ""
msgid "My textures"
msgstr ""
msgid "No textures found."
msgstr ""
msgid "Texture libraries"
msgstr ""
msgid "Explore more texture libraries"
msgstr ""
msgid "Opens OrcaCloud in the Home page to browse for plugins."
msgid "My textures"
msgstr ""
msgid "Import your own image as a height map. It is converted to the format the slicer bakes from and saved to your personal texture folder, kept separate from the textures shipped with OrcaSlicer."
@@ -30139,9 +30123,6 @@ msgstr ""
msgid "Slicing Pipeline"
msgstr ""
msgid "Resource Texture"
msgstr ""
msgid "The plugin's configuration UI was empty. Showing the default editor."
msgstr ""
@@ -30257,24 +30238,6 @@ msgstr ""
msgid "Plugin directory does not contain a .py or .whl entry file"
msgstr ""
msgid "Resource texture directory is not set"
msgstr ""
msgid "Resource texture directory could not be resolved"
msgstr ""
msgid "Resource texture path is not a directory"
msgstr ""
msgid "Plugin folder could not be resolved"
msgstr ""
msgid "Resource texture directory is outside the plugin folder"
msgstr ""
msgid "Resource texture directory could not be read"
msgstr ""
#, boost-format
msgid "Slicing pipeline plugin '%1%' error: %2%"
msgstr ""
@@ -30631,7 +30594,7 @@ msgstr ""
#. TRN %1% is the application name.
#, boost-format
msgid "Not a G-code file generated by %1%."
msgid "Not a gcode file generated by %1%."
msgstr ""
#. TRN %1% is a tag in the G-code file that must not be translated.
@@ -30719,7 +30682,7 @@ msgstr ""
msgid "Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%"
msgstr ""
msgid "Post-processor can't find exported G-code file"
msgid "Post-processor can't find exported gcode file"
msgstr ""
#, boost-format
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+47 -110
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Slic3rPE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-19 09:48-0300\n"
"POT-Creation-Date: 2026-10-15 17:40+0800\n"
"PO-Revision-Date: 2026-06-11 12:37-0300\n"
"Last-Translator: Handle <mail@bysb.net>\n"
"Language-Team: \n"
@@ -1751,7 +1751,7 @@ msgid "Stroke of shape (%1%) contains unsupported: %2%."
msgstr "形状(%1%)的描边包含不受支持的内容:%2%。"
# AI Translated
msgid "Not valid state please report reproduction steps on GitHub"
msgid "Not valid state please report reproduction steps on github"
msgstr "状态无效,请在 GitHub 上报告复现步骤"
# AI Translated
@@ -1759,11 +1759,11 @@ msgid "No embossed file"
msgstr "没有浮雕文件"
# AI Translated
msgid "Missing SVG file in embossed shape"
msgid "Missing svg file in embossed shape"
msgstr "浮雕形状中缺少 SVG 文件"
# AI Translated
msgid "Missing data of SVG file"
msgid "Missing data of svg file"
msgstr "缺少 SVG 文件数据"
# AI Translated
@@ -5008,7 +5008,7 @@ msgid "Unknown C++ exception."
msgstr "未知的 C++ 异常。"
# AI Translated
msgid "Processing G-code from previous file…"
msgid "Processing G-Code from previous file…"
msgstr "正在处理之前文件的 G-code…"
msgid "Slicing complete"
@@ -6699,7 +6699,7 @@ msgstr "体积:"
msgid "Size:"
msgstr "尺寸:"
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "发现G-code路径在层%d,高度为%.2lf mm处有冲突。请将有冲突的对象分离得更远(%s <-> %s)。"
@@ -12013,10 +12013,6 @@ msgstr "上传文件到打印机存储失败,请重试。"
msgid "Send Failed"
msgstr "发送失败"
# AI Translated
msgid "No available printers"
msgstr "没有可用的打印机"
msgid "The name length exceeds the limit."
msgstr "名称长度超过限制。"
@@ -13926,8 +13922,8 @@ msgid "Left mouse"
msgstr "鼠标左键"
# AI Translated
msgid "Available anywhere in the window."
msgstr "在窗口任意位置可用。"
msgid "Available anywhere in the window, even while typing in a text field."
msgstr "在窗口任意位置可用,即使正在文本框中输入。"
# AI Translated
msgid "Run favorite 1 to 10"
@@ -14043,6 +14039,10 @@ msgstr "%s 已分配给 %s。是否将其重新分配给 %s?"
msgid "The default %s is assigned to %s. Reassign it to %s?"
msgstr "默认的 %s 已分配给 %s。是否将其重新分配给 %s?"
# AI Translated
msgid "Global shortcuts also apply while typing."
msgstr "全局快捷键在输入文字时同样生效。"
# AI Translated
msgid "A key that types a character cannot be a global shortcut."
msgstr "会输入字符的按键不能用作全局快捷键。"
@@ -18086,14 +18086,6 @@ msgstr "对象排除"
msgid "Enable this option to add EXCLUDE OBJECT command in G-code."
msgstr "启用此选项以在G-code中添加EXCLUDE OBJECT命令。"
# AI Translated
msgid "Outline tolerance"
msgstr "轮廓线公差"
# AI Translated
msgid "Maximum deviation of the object outline written to EXCLUDE_OBJECT_DEFINE from the original outline. Larger values give fewer points, which helps firmware that stalls while parsing G-code with many objects. 0 keeps every point."
msgstr "写入 EXCLUDE_OBJECT_DEFINE 的对象轮廓线与原始轮廓线之间的最大偏差。数值越大,点数越少,有助于避免固件在解析包含大量对象的 G-code 时卡顿。0 表示保留所有点。"
msgid "Verbose G-code"
msgstr "注释G-code"
@@ -24012,7 +24004,7 @@ msgstr "开始测试 %s..."
# AI Translated
#, c-format, boost-format
msgid "test %s IP resolved = %s"
msgid "test %s ip resolved = %s"
msgstr "测试 %s:IP 解析结果 = %s"
# AI Translated
@@ -31506,21 +31498,20 @@ msgid "Basic Colors"
msgstr "基本颜色"
# AI Translated
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
msgid "Orca Palette"
msgstr "Orca 调色板"
# AI Translated
#. TRN: Keep the standard/brand name "RAL" unchanged.
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
msgid "RAL Classic"
msgstr "RAL 经典色"
# AI Translated
#. TRN: Keep the standard/brand name "RAL" unchanged.
msgid "Gradients"
msgstr "渐变预设"
# AI Translated
msgctxt "Color picker"
msgid "Solid"
msgstr "纯色"
@@ -32019,7 +32010,7 @@ msgid "Medium violet red"
msgstr "中紫罗兰红"
# AI Translated
msgid "Pale violet red"
msgid "PaleV violet red"
msgstr "浅紫罗兰红"
# AI Translated
@@ -32235,7 +32226,8 @@ msgid "Shallow sky"
msgstr "浅空"
# AI Translated
msgid "Electric blue"
# Needs human review: The English “Electric bel” looks like a typo (Electric blue? Electric eel?); the color is #1E90FF, so 电光蓝 was chosen. Recheck once the source name is fixed.
msgid "Electric bel"
msgstr "电光蓝"
# AI Translated
@@ -32327,8 +32319,9 @@ msgid "Maize yellow"
msgstr "玉米黄"
# AI Translated
msgid "Daffodil yellow"
msgstr "水仙黄"
# Needs human review: RAL 1007 is “Daffodil yellow” (水仙黄); the English name in the palette looks wrong. Recheck once the source name is fixed.
msgid "Dalmatian yellow"
msgstr "达尔马提亚黄"
# AI Translated
msgid "Brown beige"
@@ -32675,8 +32668,8 @@ msgid "Turquoise blue"
msgstr "绿松石蓝"
# AI Translated
msgid "Capri blue"
msgstr "卡普里蓝"
msgid "Capsanthin violet"
msgstr "辣椒红素紫"
# AI Translated
msgid "Ocean blue"
@@ -32827,8 +32820,8 @@ msgid "Silver grey"
msgstr "银灰色"
# AI Translated
msgid "Olive grey"
msgstr "橄榄灰"
msgid "Olivestone"
msgstr "橄榄石色"
# AI Translated
msgid "Moss grey"
@@ -33604,25 +33597,13 @@ msgstr "从“我的纹理”中移除"
msgid "Built-in"
msgstr "内置"
# AI Translated
msgid "My textures"
msgstr "我的纹理"
# AI Translated
msgid "No textures found."
msgstr "未找到纹理。"
# AI Translated
msgid "Texture libraries"
msgstr "纹理库"
# AI Translated
msgid "Explore more texture libraries"
msgstr "探索更多纹理库"
# AI Translated
msgid "Opens OrcaCloud in the Home page to browse for plugins."
msgstr "在主页中打开 OrcaCloud,浏览插件。"
msgid "My textures"
msgstr "我的纹理"
# AI Translated
msgid "Import your own image as a height map. It is converted to the format the slicer bakes from and saved to your personal texture folder, kept separate from the textures shipped with OrcaSlicer."
@@ -36304,10 +36285,6 @@ msgstr "脚本"
msgid "Slicing Pipeline"
msgstr "切片流水线"
# AI Translated
msgid "Resource Texture"
msgstr "资源纹理"
# AI Translated
msgid "The plugin's configuration UI was empty. Showing the default editor."
msgstr "插件的配置界面为空。正在显示默认编辑器。"
@@ -36456,30 +36433,6 @@ msgstr "插件目录同时包含 .py 和 .whl 入口文件"
msgid "Plugin directory does not contain a .py or .whl entry file"
msgstr "插件目录不包含 .py 或 .whl 入口文件"
# AI Translated
msgid "Resource texture directory is not set"
msgstr "未设置资源纹理目录"
# AI Translated
msgid "Resource texture directory could not be resolved"
msgstr "无法解析资源纹理目录"
# AI Translated
msgid "Resource texture path is not a directory"
msgstr "资源纹理路径不是目录"
# AI Translated
msgid "Plugin folder could not be resolved"
msgstr "无法解析插件文件夹"
# AI Translated
msgid "Resource texture directory is outside the plugin folder"
msgstr "资源纹理目录位于插件文件夹之外"
# AI Translated
msgid "Resource texture directory could not be read"
msgstr "无法读取资源纹理目录"
# AI Translated
#, boost-format
msgid "Slicing pipeline plugin '%1%' error: %2%"
@@ -36938,7 +36891,7 @@ msgstr "加载配置文件“%1%”失败:%2%"
# AI Translated
#. TRN %1% is the application name.
#, boost-format
msgid "Not a G-code file generated by %1%."
msgid "Not a gcode file generated by %1%."
msgstr "不是由 %1% 生成的 G-code 文件。"
# AI Translated
@@ -37058,7 +37011,7 @@ msgid "Failed making a temporary copy of G-code file %1% before running a post-p
msgstr "运行后处理脚本之前,无法创建 G-code 文件 %1% 的临时副本:%2%"
# AI Translated
msgid "Post-processor can't find exported G-code file"
msgid "Post-processor can't find exported gcode file"
msgstr "后处理器找不到导出的 G-code 文件"
# AI Translated
@@ -37447,40 +37400,6 @@ msgstr ""
"避免翘曲\n"
"您知道吗?打印ABS这类易翘曲材料时,适当提高热床温度可以降低翘曲的概率。"
# AI Translated
# Needs human review: RAL 1007 is “Daffodil yellow” (水仙黄); the English name in the palette looks wrong. Recheck once the source name is fixed.
#~ msgid "Dalmatian yellow"
#~ msgstr "达尔马提亚黄"
# AI Translated
#~ msgid "Capsanthin violet"
#~ msgstr "辣椒红素紫"
# AI Translated
#~ msgid "Olivestone"
#~ msgstr "橄榄石色"
# AI Translated
#~ msgid "Available anywhere in the window, even while typing in a text field."
#~ msgstr "在窗口任意位置可用,即使正在文本框中输入。"
# AI Translated
#~ msgid "Global shortcuts also apply while typing."
#~ msgstr "全局快捷键在输入文字时同样生效。"
# AI Translated
#~ msgid "Solid"
#~ msgstr "纯色"
# AI Translated
#~ msgid "PaleV violet red"
#~ msgstr "浅紫罗兰红"
# AI Translated
# Needs human review: The English “Electric bel” looks like a typo (Electric blue? Electric eel?); the color is #1E90FF, so 电光蓝 was chosen. Recheck once the source name is fixed.
#~ msgid "Electric bel"
#~ msgstr "电光蓝"
#~ msgid "Please choose the filament color"
#~ msgstr "请选择耗材丝颜色"
@@ -37946,6 +37865,9 @@ msgstr ""
#~ msgid "no configured tool"
#~ msgstr "未配置的工具头"
#~ msgid "Add end G-Code when finishing the entire print."
#~ msgstr "所有打印结束时的结尾G-code"
#~ msgid "Maximum X speed"
#~ msgstr "X最大速度"
@@ -37962,11 +37884,26 @@ msgstr ""
#~ msgid "Python plugin(s) invoked at each slicing pipeline step to read and modify intermediate slicing data, including a final G-code post-processing step. Research/experimental."
#~ msgstr "在每个切片流水线步骤中调用的 Python 插件,用于读取和修改中间切片数据,包括最终的 G-code 后处理步骤。研究/实验性。"
# AI Translated
#~ msgid "When enabled, the slicer emits g-code with the primary zone's contents centered on bed origin (0,0). The printer firmware is responsible for placing copies/mirrors at each physical zone position. Enable only on printers whose firmware applies its own zone offsets in copy/mirror mode (e.g. RepRapFirmware IDEX duplication mode). Has no effect in Primary mode."
#~ msgstr "启用后,切片软件输出的 G-code 会以热床原点 (0,0) 为中心放置主区域的内容,由打印机固件负责把复制件/镜像件放到各个物理区域。仅当打印机固件在复制/镜像模式下会自行应用区域偏移时才启用(例如 RepRapFirmware 的 IDEX 复制模式)。主模式下无效。"
# AI Translated
# Needs human review: "pem-inversion defaults": confirm pem is the physical extruder map and that the default is its inverse.
#~ msgid "Per-plate override mapping physical heads to filament slots (1-based). Empty means use pem-inversion defaults."
#~ msgstr "按盘单独设置物理工具头到耗材丝槽位的映射(从 1 开始编号),覆盖默认映射。留空则使用由物理挤出机映射(pem)反推的默认值。"
# AI Translated
#~ msgid "Axis the mesh is rotated about before slicing. This is the belt printer's tilt: an isometric (no distortion) rotation that also drives bed rendering and support gravity tilt, and that the g-code back-transform inverts before the machine-frame shear/scale and remap. X is the typical gantry tilt (belt travels along Y)."
#~ msgstr "切片前模型网格的旋转轴。这一旋转即传送带打印机的倾斜:它是保距(无畸变)旋转,同时用于热床渲染和支撑的重力倾斜;生成 G-code 时,逆变换会先还原这一旋转,再进行机器坐标系剪切/缩放和重映射。X 是典型的龙门倾斜轴(传送带沿 Y 方向移动)。"
# AI Translated
#~ msgid "Expert override: set the machine-frame (g-code shear/scale) tilt angle independently of the pre-slice rotation angle. When disabled, the machine-frame transform is derived from the belt tilt angle, so a single angle drives both stages. Enable only to compensate for a machine whose physical gantry tilt differs from the slicing rotation."
#~ msgstr "专家选项:单独设置机器坐标系(G-code 剪切/缩放)的倾斜角度,与切片前的旋转角度互不影响。禁用时,机器坐标系变换由传送带倾斜角度推导,两个阶段使用同一个角度。仅当机器实际的龙门倾角与切片旋转角度不一致时,才需启用此项进行补偿。"
#~ msgid "The maximum deviation allowed when reducing the resolution for the 'Maximum wall resolution' setting. If you increase this, the print will be less accurate, but the G-Code will be smaller. 'Maximum wall deviation' limits 'Maximum wall resolution', so if the two conflict, 'Maximum wall deviation' takes precedence."
#~ msgstr "在为“最大墙体分辨率”设置降低分辨率时所允许的最大偏差。如果增大此值,打印精度会降低,但 G-code 会更小。“最大墙体偏差”会限制“最大墙体分辨率”,因此当两者冲突时,以“最大墙体偏差”为准。"
#~ msgid " not in range "
#~ msgstr " 不在合理范围内"
+48 -111
View File
@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-19 09:48-0300\n"
"POT-Creation-Date: 2026-10-15 17:40+0800\n"
"PO-Revision-Date: 2025-11-28 13:48-0600\n"
"Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n"
"Language-Team: \n"
@@ -1826,7 +1826,7 @@ msgid "Stroke of shape (%1%) contains unsupported: %2%."
msgstr "形狀(%1%)的筆劃包含不支援的內容:%2%。"
# AI Translated
msgid "Not valid state please report reproduction steps on GitHub"
msgid "Not valid state please report reproduction steps on github"
msgstr "狀態無效,請在 GitHub 上回報重現步驟"
# AI Translated
@@ -1834,11 +1834,11 @@ msgid "No embossed file"
msgstr "沒有浮雕檔案"
# AI Translated
msgid "Missing SVG file in embossed shape"
msgid "Missing svg file in embossed shape"
msgstr "浮雕形狀中缺少 SVG 檔案"
# AI Translated
msgid "Missing data of SVG file"
msgid "Missing data of svg file"
msgstr "缺少 SVG 檔案的資料"
msgid "Face the camera"
@@ -5219,7 +5219,7 @@ msgid "Unknown C++ exception."
msgstr "未知的 C++ 例外。"
# AI Translated
msgid "Processing G-code from previous file…"
msgid "Processing G-Code from previous file…"
msgstr "正在處理先前檔案的 G-code…"
msgid "Slicing complete"
@@ -6912,7 +6912,7 @@ msgid "Size:"
msgstr "尺寸:"
# AI Translated
#, c-format, boost-format
#, boost-format
msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "在第 %d 層(Z = %.2lf mm)發現 G-code 路徑衝突。請將有衝突的物件分開得更遠(%s <-> %s)。"
@@ -12298,10 +12298,6 @@ msgstr "無法將檔案上傳至列印裝置的儲存空間,請重試。"
msgid "Send Failed"
msgstr "傳送失敗"
# AI Translated
msgid "No available printers"
msgstr "沒有可用的列印裝置"
msgid "The name length exceeds the limit."
msgstr "名稱長度超過限制。"
@@ -14255,8 +14251,8 @@ msgid "Left mouse"
msgstr "滑鼠左鍵"
# AI Translated
msgid "Available anywhere in the window."
msgstr "在視窗中任何位置皆可使用。"
msgid "Available anywhere in the window, even while typing in a text field."
msgstr "在視窗中任何位置皆可使用,即使正在文字欄位中輸入也可以。"
# AI Translated
msgid "Run favorite 1 to 10"
@@ -14376,6 +14372,10 @@ msgstr "%s 已指派給 %s。要改為指派給 %s 嗎?"
msgid "The default %s is assigned to %s. Reassign it to %s?"
msgstr "預設的 %s 已指派給 %s。要改為指派給 %s 嗎?"
# AI Translated
msgid "Global shortcuts also apply while typing."
msgstr "全域快速鍵在輸入文字時也會生效。"
# AI Translated
msgid "A key that types a character cannot be a global shortcut."
msgstr "會輸入字元的按鍵不能作為全域快速鍵。"
@@ -18513,14 +18513,6 @@ msgstr "物件排除"
msgid "Enable this option to add EXCLUDE OBJECT command in G-code."
msgstr "啟用此選項以在 G-code 中加入 EXCLUDE OBJECT 指令。"
# AI Translated
msgid "Outline tolerance"
msgstr "輪廓線公差"
# AI Translated
msgid "Maximum deviation of the object outline written to EXCLUDE_OBJECT_DEFINE from the original outline. Larger values give fewer points, which helps firmware that stalls while parsing G-code with many objects. 0 keeps every point."
msgstr "寫入 EXCLUDE_OBJECT_DEFINE 的物件輪廓線與原始輪廓線之間的最大偏差。數值越大,點數越少,有助於避免韌體在解析包含大量物件的 G-code 時卡住。0 表示保留所有點。"
msgid "Verbose G-code"
msgstr "詳細 G-code"
@@ -24605,7 +24597,7 @@ msgstr "測試 %s 開始..."
# AI Translated
#, c-format, boost-format
msgid "test %s IP resolved = %s"
msgid "test %s ip resolved = %s"
msgstr "測試 %s IP 解析結果 = %s"
# AI Translated
@@ -32132,22 +32124,21 @@ msgid "Basic Colors"
msgstr "基本顏色"
# AI Translated
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
msgid "Orca Palette"
msgstr "Orca 調色盤"
# AI Translated
#. TRN: Keep the standard/brand name "RAL" unchanged.
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
msgid "RAL Classic"
msgstr "RAL 經典色"
# AI Translated
# Needs human review: Palette dropdown entry for the gradient presets (next to 基本顏色 / Orca 調色盤 / RAL 經典色); 漸層配色 keeps it apart from the Solid/Gradient switch label 漸層. Check it reads right in the dropdown.
#. TRN: Keep the standard/brand name "RAL" unchanged.
msgid "Gradients"
msgstr "漸層配色"
# AI Translated
msgctxt "Color picker"
msgid "Solid"
msgstr "純色"
@@ -32646,7 +32637,7 @@ msgid "Medium violet red"
msgstr "中紫羅蘭紅"
# AI Translated
msgid "Pale violet red"
msgid "PaleV violet red"
msgstr "淡紫羅蘭紅"
# AI Translated
@@ -32862,7 +32853,8 @@ msgid "Shallow sky"
msgstr "淺色天空"
# AI Translated
msgid "Electric blue"
# Needs human review: Orca palette: the English is a typo for "Electric blue" (hex #1E90FF); translated as 電光藍. Revisit if the source keeps a different name.
msgid "Electric bel"
msgstr "電光藍"
# AI Translated
@@ -32954,8 +32946,9 @@ msgid "Maize yellow"
msgstr "玉米黃"
# AI Translated
msgid "Daffodil yellow"
msgstr "水仙黃"
# Needs human review: RAL 1007 in the RAL Classic list: the English name looks wrong (1007 is Daffodil yellow, 水仙黃); translated literally. Revisit once the source name is fixed.
msgid "Dalmatian yellow"
msgstr "達爾馬提亞黃"
# AI Translated
msgid "Brown beige"
@@ -33303,8 +33296,9 @@ msgid "Turquoise blue"
msgstr "綠松石藍"
# AI Translated
msgid "Capri blue"
msgstr "卡布里藍"
# Needs human review: RAL 5019 in the RAL Classic list: the English name is not a RAL Classic name (5019 is Capri blue); translated literally. Revisit once the source name is fixed.
msgid "Capsanthin violet"
msgstr "辣椒紅素紫"
# AI Translated
msgid "Ocean blue"
@@ -33455,8 +33449,8 @@ msgid "Silver grey"
msgstr "銀灰"
# AI Translated
msgid "Olive grey"
msgstr "橄欖灰"
msgid "Olivestone"
msgstr "橄欖石色"
# AI Translated
msgid "Moss grey"
@@ -34232,25 +34226,13 @@ msgstr "從「我的紋理」移除"
msgid "Built-in"
msgstr "內建"
# AI Translated
msgid "My textures"
msgstr "我的紋理"
# AI Translated
msgid "No textures found."
msgstr "找不到紋理。"
# AI Translated
msgid "Texture libraries"
msgstr "紋理庫"
# AI Translated
msgid "Explore more texture libraries"
msgstr "探索更多紋理庫"
# AI Translated
msgid "Opens OrcaCloud in the Home page to browse for plugins."
msgstr "在首頁中開啟 OrcaCloud,瀏覽外掛。"
msgid "My textures"
msgstr "我的紋理"
# AI Translated
msgid "Import your own image as a height map. It is converted to the format the slicer bakes from and saved to your personal texture folder, kept separate from the textures shipped with OrcaSlicer."
@@ -36946,10 +36928,6 @@ msgstr "指令碼"
msgid "Slicing Pipeline"
msgstr "切片管線"
# AI Translated
msgid "Resource Texture"
msgstr "資源紋理"
# AI Translated
msgid "The plugin's configuration UI was empty. Showing the default editor."
msgstr "外掛的設定介面為空。將顯示預設編輯器。"
@@ -37097,30 +37075,6 @@ msgstr "外掛目錄同時包含 .py 和 .whl 進入點檔案"
msgid "Plugin directory does not contain a .py or .whl entry file"
msgstr "外掛目錄不包含 .py 或 .whl 進入點檔案"
# AI Translated
msgid "Resource texture directory is not set"
msgstr "未設定資源紋理目錄"
# AI Translated
msgid "Resource texture directory could not be resolved"
msgstr "無法解析資源紋理目錄"
# AI Translated
msgid "Resource texture path is not a directory"
msgstr "資源紋理路徑不是目錄"
# AI Translated
msgid "Plugin folder could not be resolved"
msgstr "無法解析外掛資料夾"
# AI Translated
msgid "Resource texture directory is outside the plugin folder"
msgstr "資源紋理目錄位於外掛資料夾之外"
# AI Translated
msgid "Resource texture directory could not be read"
msgstr "無法讀取資源紋理目錄"
# AI Translated
#, boost-format
msgid "Slicing pipeline plugin '%1%' error: %2%"
@@ -37579,7 +37533,7 @@ msgstr "載入設定檔「%1%」失敗:%2%"
# AI Translated
#. TRN %1% is the application name.
#, boost-format
msgid "Not a G-code file generated by %1%."
msgid "Not a gcode file generated by %1%."
msgstr "不是由 %1% 產生的 G-code 檔案。"
# AI Translated
@@ -37699,7 +37653,7 @@ msgid "Failed making a temporary copy of G-code file %1% before running a post-p
msgstr "執行後處理腳本前,無法建立 G-code 檔案 %1% 的暫存副本:%2%"
# AI Translated
msgid "Post-processor can't find exported G-code file"
msgid "Post-processor can't find exported gcode file"
msgstr "後處理程式找不到匯出的 G-code 檔案"
# AI Translated
@@ -38095,41 +38049,6 @@ msgstr ""
"避免翹曲\n"
"您知道嗎?當列印容易翹曲的材料(如 ABS)時,適當提高熱床溫度可以降低翹曲的機率。"
# AI Translated
# Needs human review: RAL 1007 in the RAL Classic list: the English name looks wrong (1007 is Daffodil yellow, 水仙黃); translated literally. Revisit once the source name is fixed.
#~ msgid "Dalmatian yellow"
#~ msgstr "達爾馬提亞黃"
# AI Translated
# Needs human review: RAL 5019 in the RAL Classic list: the English name is not a RAL Classic name (5019 is Capri blue); translated literally. Revisit once the source name is fixed.
#~ msgid "Capsanthin violet"
#~ msgstr "辣椒紅素紫"
# AI Translated
#~ msgid "Olivestone"
#~ msgstr "橄欖石色"
# AI Translated
#~ msgid "Available anywhere in the window, even while typing in a text field."
#~ msgstr "在視窗中任何位置皆可使用,即使正在文字欄位中輸入也可以。"
# AI Translated
#~ msgid "Global shortcuts also apply while typing."
#~ msgstr "全域快速鍵在輸入文字時也會生效。"
# AI Translated
#~ msgid "Solid"
#~ msgstr "純色"
# AI Translated
#~ msgid "PaleV violet red"
#~ msgstr "淡紫羅蘭紅"
# AI Translated
# Needs human review: Orca palette: the English is a typo for "Electric blue" (hex #1E90FF); translated as 電光藍. Revisit if the source keeps a different name.
#~ msgid "Electric bel"
#~ msgstr "電光藍"
#~ msgid "Please choose the filament color"
#~ msgstr "請選擇線材顏色"
@@ -38594,6 +38513,9 @@ msgstr ""
#~ msgid "no configured tool"
#~ msgstr "未設定的工具頭"
#~ msgid "Add end G-Code when finishing the entire print."
#~ msgstr "所有列印結束時的結尾 G-code"
#~ msgid "Maximum X speed"
#~ msgstr "X 最大速度"
@@ -38610,10 +38532,25 @@ msgstr ""
#~ msgid "Python plugin(s) invoked at each slicing pipeline step to read and modify intermediate slicing data, including a final G-code post-processing step. Research/experimental."
#~ msgstr "在每個切片管線步驟呼叫的 Python 外掛,用於讀取與修改中介切片資料,包括最終的 G-code 後處理步驟。研究/實驗性質。"
# AI Translated
#~ msgid "When enabled, the slicer emits g-code with the primary zone's contents centered on bed origin (0,0). The printer firmware is responsible for placing copies/mirrors at each physical zone position. Enable only on printers whose firmware applies its own zone offsets in copy/mirror mode (e.g. RepRapFirmware IDEX duplication mode). Has no effect in Primary mode."
#~ msgstr "啟用後,切片軟體輸出的 G-code 會將主區域的內容以列印板原點 (0,0) 為中心放置,並由列印裝置韌體負責將複製件/鏡像件放到各個實體區域位置。僅在韌體會於複製/鏡像模式下自行套用區域偏移的列印裝置上啟用(例如 RepRapFirmware 的 IDEX 複製模式)。在主模式下無效。"
# AI Translated
#~ msgid "Per-plate override mapping physical heads to filament slots (1-based). Empty means use pem-inversion defaults."
#~ msgstr "依列印板覆寫實體工具頭到線材槽位的映射(從 1 開始編號)。留空表示使用由 pem 反推的預設映射。"
# AI Translated
#~ msgid "Axis the mesh is rotated about before slicing. This is the belt printer's tilt: an isometric (no distortion) rotation that also drives bed rendering and support gravity tilt, and that the g-code back-transform inverts before the machine-frame shear/scale and remap. X is the typical gantry tilt (belt travels along Y)."
#~ msgstr "切片前網格旋轉所繞的軸。這就是輸送帶列印裝置的傾斜:一種等距(無變形)旋轉,同時用於列印板渲染與支撐的重力傾斜;G-code 反向轉換會先還原此旋轉,再進行機器座標系的剪切/縮放與重新映射。X 是典型的龍門傾斜軸(輸送帶沿 Y 方向移動)。"
# AI Translated
#~ msgid "Expert override: set the machine-frame (g-code shear/scale) tilt angle independently of the pre-slice rotation angle. When disabled, the machine-frame transform is derived from the belt tilt angle, so a single angle drives both stages. Enable only to compensate for a machine whose physical gantry tilt differs from the slicing rotation."
#~ msgstr "專家覆寫選項:獨立於切片前的旋轉角度,單獨設定機器座標系(G-code 剪切/縮放)的傾斜角度。停用時,機器座標系變換由輸送帶傾斜角度推導,兩個階段使用同一個角度。僅在機器實際的龍門傾角與切片旋轉角度不同時,才啟用此選項進行補償。"
#~ msgid "The maximum deviation allowed when reducing the resolution for the 'Maximum wall resolution' setting. If you increase this, the print will be less accurate, but the G-Code will be smaller. 'Maximum wall deviation' limits 'Maximum wall resolution', so if the two conflict, 'Maximum wall deviation' takes precedence."
#~ msgstr "「最大牆解析度」設定在降低解析度時所允許的最大偏差。若增加此值,列印會較不精確,但 G-Code 會更小。「最大牆偏差」會限制「最大牆解析度」,因此若兩者衝突,將以「最大牆偏差」為準。"
#~ msgid " not in range "
#~ msgstr " 不在合理的區間"
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Snapmaker",
"version": "02.04.00.18",
"version": "02.04.00.17",
"force_update": "0",
"description": "Snapmaker configurations",
"machine_model_list": [
@@ -49,8 +49,5 @@
],
"pressure_advance": [
"0.02"
],
"temperature_vitrification": [
"100"
]
}
@@ -46,8 +46,5 @@
],
"fan_min_speed": [
"0"
],
"temperature_vitrification": [
"100"
]
}
@@ -45,7 +45,7 @@
"250"
],
"temperature_vitrification": [
"70"
"178"
],
"fan_max_speed": [
"20"
@@ -4,8 +4,5 @@
"inherits": "fdm_filament_petg",
"from": "system",
"filament_id": "OFW8lqnb",
"instantiation": "false",
"temperature_vitrification": [
"70"
]
"instantiation": "false"
}
@@ -60,7 +60,7 @@
"250"
],
"temperature_vitrification": [
"70"
"178"
],
"fan_max_speed": [
"20"
@@ -34,8 +34,5 @@
],
"nozzle_temperature": [
"220"
],
"temperature_vitrification": [
"45"
]
}
@@ -34,8 +34,5 @@
],
"filament_cooling_final_speed": [
"60"
],
"temperature_vitrification": [
"45"
]
}
@@ -34,8 +34,5 @@
],
"nozzle_temperature": [
"220"
],
"temperature_vitrification": [
"45"
]
}
@@ -40,8 +40,5 @@
],
"nozzle_temperature": [
"220"
],
"temperature_vitrification": [
"45"
]
}
@@ -31,8 +31,5 @@
],
"slow_down_layer_time": [
"8"
],
"temperature_vitrification": [
"45"
]
}
@@ -50,8 +50,5 @@
"additional_cooling_fan_speed": [
"70",
"70"
],
"temperature_vitrification": [
"30"
]
}
@@ -137,7 +137,7 @@
"240"
],
"temperature_vitrification": [
"30"
"60"
],
"nozzle_temperature_range_low": [
"200"
@@ -302,7 +302,7 @@
"-1"
],
"temperature_vitrification": [
"30"
"60"
],
"textured_cool_plate_temp": [
"40"
@@ -29,8 +29,5 @@
"additional_cooling_fan_speed": [
"70",
"70"
],
"temperature_vitrification": [
"30"
]
}
@@ -49,8 +49,5 @@
"additional_cooling_fan_speed": [
"70",
"70"
],
"temperature_vitrification": [
"30"
]
}
@@ -97,7 +97,7 @@
"20",
"12"
],
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\n;===== top cover purifier =====\n{if min_vitrification_temperature <= 50}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=48 FAN_SPEED=0.6 DELAY_OFF=0 ; strong cooling\n{elsif min_vitrification_temperature <= 70}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=45 ALARM_TEMP=51 FAN_SPEED=0.6 DELAY_OFF=0 ; weak cooling\n{else}\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=600 ; keep warm\n{endif}\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\n\n;==== 预挤出耗材 =====\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nT{initial_extruder}\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nSM_PRINT_PREEXTRUDE_FILAMENT INDEX={initial_extruder}\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nM107\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nM107\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nM106 S255\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\nM107\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\nM107\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; AlwLine truncated
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0\n{endif} \n\n\n;===== 画起始线 =================\nG90\nG1 Z1.5\nG0 X85 Y1 Z2 F18000\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\nG1 Z0.2\nM83\nG1 X185 E15 F360\nG1 Z1.5\n\nG90\nM106 S0",
"machine_tool_change_time": "5",
"max_layer_height": [
"0.14",
@@ -97,7 +97,7 @@
"20",
"12"
],
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\n;===== top cover purifier =====\n{if min_vitrification_temperature <= 50}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=48 FAN_SPEED=0.6 DELAY_OFF=0 ; strong cooling\n{elsif min_vitrification_temperature <= 70}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=45 ALARM_TEMP=51 FAN_SPEED=0.6 DELAY_OFF=0 ; weak cooling\n{else}\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=600 ; keep warm\n{endif}\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\n\n;==== 预挤出耗材 =====\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nT{initial_extruder}\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nSM_PRINT_PREEXTRUDE_FILAMENT INDEX={initial_extruder}\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nM107\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nM107\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nM106 S255\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\nM107\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\nM107\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; AlwLine truncated
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0\n{endif} \n\n\n;===== 画起始线 =================\nG90\nG1 Z1.5\nG0 X85 Y1 Z2 F18000\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\nG1 Z0.2\nM83\nG1 X185 E15 F360\nG1 Z1.5\n\nG90\nM106 S0",
"machine_tool_change_time": "5",
"max_layer_height": [
"0.32",
@@ -97,7 +97,7 @@
"20",
"12"
],
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\n;===== top cover purifier =====\n{if min_vitrification_temperature <= 50}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=48 FAN_SPEED=0.6 DELAY_OFF=0 ; strong cooling\n{elsif min_vitrification_temperature <= 70}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=45 ALARM_TEMP=51 FAN_SPEED=0.6 DELAY_OFF=0 ; weak cooling\n{else}\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=600 ; keep warm\n{endif}\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\n\n;==== 预挤出耗材 =====\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nT{initial_extruder}\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nSM_PRINT_PREEXTRUDE_FILAMENT INDEX={initial_extruder}\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nM107\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nM107\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nM106 S255\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\nM107\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\nM107\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; AlwLine truncated
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0\n{endif} \n\n\n;===== 画起始线 =================\nG90\nG1 Z1.5\nG0 X85 Y1 Z2 F18000\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\nG1 Z0.2\nM83\nG1 X185 E15 F360\nG1 Z1.5\n\nG90\nM106 S0",
"machine_tool_change_time": "5",
"max_layer_height": [
"0.42",
@@ -97,7 +97,7 @@
"20",
"12"
],
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\n;===== top cover purifier =====\n{if min_vitrification_temperature <= 50}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=42 ALARM_TEMP=48 FAN_SPEED=0.6 DELAY_OFF=0 ; strong cooling\n{elsif min_vitrification_temperature <= 70}\nSET_PURIFIER_MODE MODE=1 DESIRE_TEMP=45 ALARM_TEMP=51 FAN_SPEED=0.6 DELAY_OFF=0 ; weak cooling\n{else}\nSET_PURIFIER_MODE MODE=3 DESIRE_TEMP=45 FAN_SPEED=0.6 DELAY_OFF=600 ; keep warm\n{endif}\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\n\n;==== 预挤出耗材 =====\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nT{initial_extruder}\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder]}\nSM_PRINT_PREEXTRUDE_FILAMENT INDEX={initial_extruder}\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nM107\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nM107\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nM106 S255\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\nM107\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\nM107\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; AlwLine truncated
"machine_start_gcode": "SET_PRINT_AUTO_BED_LEVELING ENABLE=1\nSET_TIME_LAPSE_CAMERA ENABLE=1\n;===== date: 20260128 =====================\n\nPRINT_START\nDEFECT_DETECTION_START\nSET_PRINT_STATS_INFO TOTAL_LAYER={total_layer_count} CURRENT_LAYER=0\nTIMELAPSE_START\nM140 S{bed_temperature_initial_layer_single}\nM104 T{initial_extruder} S140\nM204 S10000\nG28 X Y\nDEFECT_DETECT_NOODLE_FIRST\n;===== 床面异物检测 ========\nT{initial_extruder}\nG90\nDEFECT_DETECTION_DETECT_BED\n;===== 取放头检测 =================\nSM_PRINT_CHECK_SWITCH_EXTRUDER\n\n;===== 自动进料 & 挤出流量 & 预挤出 ======================\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=1 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=0\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=0\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=2 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=1\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=1\nSM_PRINT_EXTRUDER_PREHEAT EXTRUDER=3 TEMP=140\nSM_PRINT_AUTO_FEED EXTRUDER=2\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=2\nSM_PRINT_AUTO_FEED EXTRUDER=3\nSM_PRINT_FLOW_CALIBRATE EXTRUDER=3\nM104 S0 T0 A0\nM104 S0 T1 A0\nM104 S0 T2 A0\nM104 S0 T3 A0\nM104 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\n\n;===== 粗回零 =================\nT{initial_extruder}\nM106 S255\nM106 P2 S0\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 T{initial_extruder} S{nozzle_temperature[initial_extruder] - 90}\nROUGHLY_CLEAN_NOZZLE_WITH_DISCARD\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nG28 Z I140 J140\n\n;===== 检测钢板 =================\nDETECT_BED_PLATE\n\n;===== 深度清洁喷嘴 =================\nG90\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nM109 S{nozzle_temperature[initial_extruder] - 50}\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_1\nM104 S{nozzle_temperature[initial_extruder] - 90}\nG0 Z5 F10000\nMOVE_TO_DISCARD_FILAMENT_POSITION\nROUGHLY_CLEAN_NOZZLE\nMOVE_TO_XY_IDLE_POSITION_EXTRUDER\nFINELY_CLEAN_NOZZLE_STAGE_2\n\n;===== 精回零 =================\nM106 S255\nM109 S{nozzle_temperature[initial_extruder] - 90}\nM190 S{bed_temperature_initial_layer_single}\nM107 P2\nG90\nG0 Z5 F10000\nWAIT_CHAMBER_TEMP TIMEOUT=180\n{if curr_bed_type==\"High Temp Plate\"} \nG28 Z Z_OFFSET -0.07 \n{else} \nG28 Z \n{endif} \n\n\n;===== 热床调平 =================\n{if curr_bed_type==\"High Temp Plate\"} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0 Z_OFFSET=-0.07\n{else} \n; Always pass `ADAPTIVE_MARGIN=0` because Orca has already handled `adaptive_bed_mesh_margin` internally\n; Make sure to set ADAPTIVE to 0 otherwise Klipper will use it's own adaptive bed mesh logic\nBED_MESH_CALIBRATE mesh_min={adaptive_bed_mesh_min[0]},{adaptive_bed_mesh_min[1]} mesh_max={adaptive_bed_mesh_max[0]},{adaptive_bed_mesh_max[1]} ALGORITHM=[bed_mesh_algo] PROBE_COUNT={bed_mesh_probe_count[0]},{bed_mesh_probe_count[1]} ADAPTIVE=0 ADAPTIVE_MARGIN=0\n{endif} \n\n\n;===== 画起始线 =================\nG90\nG1 Z1.5\nG0 X85 Y1 Z2 F18000\nM109 S{nozzle_temperature_initial_layer[initial_extruder]}\nG1 Z0.2\nM83\nG1 X185 E15 F360\nG1 Z1.5\n\nG90\nM106 S0",
"machine_tool_change_time": "5",
"max_layer_height": [
"0.56",
@@ -109,7 +109,7 @@ const cssColors = [
{hex: "#800080", name: "Purple"},
{hex: "#663399", name: "Rebecca purple"},
{hex: "#C71585", name: "Medium violet red"},
{hex: "#DB7093", name: "Pale violet red"},
{hex: "#DB7093", name: "PaleV violet red"},
{hex: "#FF1493", name: "Deep pink"},
{hex: "#FF69B4", name: "Hot pink"},
{hex: "#FFB6C1", name: "Light pink"},
@@ -193,7 +193,7 @@ const palettes = {
{hex: "#40E0D0", name: "Turquoise bay"},
{hex: "#2EBDEF", name: "Sky over reef"},
{hex: "#87CEEB", name: "Shallow sky"},
{hex: "#1E90FF", name: "Electric blue"},
{hex: "#1E90FF", name: "Electric bel"},
{hex: "#4169E1", name: "Dolphin"},
{hex: "#0000CD", name: "Ocean depths"},
{hex: "#191970", name: "Whale"},
@@ -219,7 +219,7 @@ const palettes = {
{hex: "#EFBF04", name: "1004 Golden yellow"},
{hex: "#D7A500", name: "1005 Honey yellow"},
{hex: "#F2D185", name: "1006 Maize yellow"},
{hex: "#FDD185", name: "1007 Daffodil yellow"},
{hex: "#FDD185", name: "1007 Dalmatian yellow"},
{hex: "#8B7D6B", name: "1011 Brown beige"},
{hex: "#F5DB50", name: "1012 Lemon yellow"},
{hex: "#F3E5AB", name: "1013 Oyster white"},
@@ -318,7 +318,7 @@ const palettes = {
{hex: "#0384A7", name: "5015 Sky blue"},
{hex: "#006A9E", name: "5017 Traffic blue"},
{hex: "#005F87", name: "5018 Turquoise blue"},
{hex: "#1B5583", name: "5019 Capri blue"},
{hex: "#5A3D5B", name: "5019 Capsanthin violet"},
{hex: "#04749F", name: "5020 Ocean blue"},
{hex: "#009AAA", name: "5021 Water blue"},
{hex: "#1A3D5F", name: "5022 Night blue"},
@@ -366,7 +366,7 @@ const palettes = {
{hex: "#00A550", name: "6039 Mint green"},
{hex: "#7D7D7D", name: "7000 Squirrel grey"},
{hex: "#A8A9AD", name: "7001 Silver grey"},
{hex: "#6F7368", name: "7002 Olive grey"},
{hex: "#6F7368", name: "7002 Olivestone"},
{hex: "#8F9080", name: "7003 Moss grey"},
{hex: "#8E9291", name: "7004 Signal grey"},
{hex: "#8B8788", name: "7005 Mouse grey"},
-126
View File
@@ -1,126 +0,0 @@
<#
.SYNOPSIS
Fails when a Windows build has an exception class with broken throw info.
.DESCRIPTION
Clang on the MSVC ABI can leave the copy constructor out of the CatchableType it emits for
an exception class that comes through a precompiled header
(https://github.com/llvm/llvm-project/issues/53486). The linker keeps one throw info per
class, so when it keeps such a record, a copy of that exception (std::exception_ptr, a catch
by value) shares its data with the original and the data is freed twice.
A class is either trivially copyable or not, so a binary should never have both a CatchableType
without a copy constructor (_CT??_R0?AV<class>@@@8<size>) and one with it
(_CT??_R0?AV<class>@@@8??0<class>...). The script lists each PDB's public symbols with the
llvm-pdbutil next to the build's compiler and reports every class that has both.
scripts\test_check_exception_throw_info.ps1 tests it.
.PARAMETER BuildDir
The slicer build directory. Its CMakeCache.txt names the compiler, and its src\<Config>
holds the PDBs to check, which must include OrcaSlicer.pdb.
.PARAMETER Config
The configuration whose PDBs to check (default: Release).
.PARAMETER Pdb
PDBs to check instead of the ones in <BuildDir>\src\<Config>, as a list or comma-separated.
.PARAMETER Dump
Saved `llvm-pdbutil dump --publics` output to check instead of PDBs. Needs no build.
.EXAMPLE
scripts\check_exception_throw_info.ps1 -BuildDir build
#>
param(
[string] $BuildDir,
[string] $Config = 'Release',
[string[]] $Pdb,
[string[]] $Dump
)
$ErrorActionPreference = 'Stop'
# powershell -File passes "a.pdb,b.pdb" as one string. A path that exists as given stays whole.
function Split-PathList([string[]] $values) {
@($values | Where-Object { $_ } | ForEach-Object {
if ((Test-Path -LiteralPath $_) -or $_ -notmatch ',') { $_ } else { $_ -split ',' | Where-Object { $_ } }
})
}
$Dump = Split-PathList $Dump
if (-not $Dump) {
if (-not $BuildDir) {
throw 'Name the slicer build directory with -BuildDir.'
}
$cache = Join-Path $BuildDir 'CMakeCache.txt'
if (-not (Test-Path $cache)) {
throw "No CMakeCache.txt in $BuildDir. Name the slicer build directory."
}
$entry = Select-String -Path $cache -Pattern '^CMAKE_CXX_COMPILER:\w+=(.+)$' | Select-Object -First 1
if (-not $entry) {
throw "No CMAKE_CXX_COMPILER in $cache."
}
$compiler = $entry.Matches[0].Groups[1].Value
# A tree configured with a bare compiler name found it on PATH.
if (-not (Split-Path $compiler)) {
$found = Get-Command $compiler -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $found) {
throw "$compiler, the compiler in $cache, is not on PATH."
}
$compiler = $found.Source
}
$pdbutil = Join-Path (Split-Path $compiler) 'llvm-pdbutil.exe'
if (-not (Test-Path $pdbutil)) {
throw "No llvm-pdbutil.exe next to $compiler. This check needs a build made with Clang."
}
$Pdb = Split-PathList $Pdb
if (-not $Pdb) {
# OrcaSlicer.dll holds the slicer itself, so a check without it proves nothing.
$pdbDir = Join-Path $BuildDir "src\$Config"
if (-not (Test-Path (Join-Path $pdbDir 'OrcaSlicer.pdb'))) {
throw "No OrcaSlicer.pdb in $pdbDir."
}
$Pdb = @(Get-ChildItem $pdbDir -Filter *.pdb | ForEach-Object FullName)
}
}
$catchable = [regex]'`_CT\?\?_R0\?A[UV](.+?)@@@8([^`]*)`'
$tmp = [IO.Path]::GetTempFileName()
$failed = $false
$classes = 0
try {
foreach ($file in $(if ($Dump) { $Dump } else { $Pdb })) {
$text = $file
if (-not $Dump) {
$run = Start-Process -FilePath $pdbutil -ArgumentList 'dump', '--publics', "`"$file`"" -NoNewWindow -Wait -PassThru -RedirectStandardOutput $tmp
if ($run.ExitCode -ne 0) {
throw "llvm-pdbutil failed on $file."
}
$text = $tmp
}
$kinds = @{}
foreach ($match in $catchable.Matches([IO.File]::ReadAllText((Resolve-Path -LiteralPath $text).Path))) {
$class = $match.Groups[1].Value
if (-not $kinds.ContainsKey($class)) { $kinds[$class] = @{} }
$kinds[$class][$(if ($match.Groups[2].Value.StartsWith('??0')) { 'copy' } else { 'bitwise' })] = $true
}
$broken = @($kinds.Keys | Where-Object { $kinds[$_].Count -eq 2 } | Sort-Object)
"$(Split-Path -Leaf $file): $($kinds.Count) exception classes, $($broken.Count) with broken throw info"
foreach ($class in $broken) { " $class" }
if ($broken.Count -gt 0) { $failed = $true }
$classes += $kinds.Count
}
} finally {
Remove-Item $tmp -ErrorAction SilentlyContinue
}
# The slicer's PDBs always contain exception throw info, so finding none means the dump format or the PDBs changed.
if ($classes -eq 0) {
'No exception classes found. Check that llvm-pdbutil still prints public symbols in backticks.'
$failed = $true
}
exit [int]$failed
@@ -45,10 +45,28 @@
<color type="primary" scheme_preference="dark">#00695C</color>
</branding>
<releases>
<release version="2.5.0-dev" date="2026-06-22" type="development">
<url type="details">https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds</url>
<release version="2.5.0-alpha" date="2026-10-14" type="development">
<url type="details">https://www.orcaslicer.com/wiki/releases/release_2_5_0_alpha</url>
<description>
<p>See the release page for detailed changelog.</p>
<p>See the release page for detailed changelog of OrcaSlicer 2.5.0-alpha release.</p>
</description>
</release>
<release version="2.4.2" date="2026-07-26">
<url type="details">https://www.orcaslicer.com/wiki/releases/release_2_4_2</url>
<description>
<p>See the release page for detailed changelog of OrcaSlicer 2.4.2 release.</p>
</description>
</release>
<release version="2.4.1" date="2026-06-26">
<url type="details">https://www.orcaslicer.com/wiki/releases/release_2_4_1</url>
<description>
<p>See the release page for detailed changelog of OrcaSlicer 2.4.1 release.</p>
</description>
</release>
<release version="2.4.0" date="2026-06-20">
<url type="details">https://www.orcaslicer.com/wiki/releases/release_2_4_0</url>
<description>
<p>See the release page for detailed changelog of OrcaSlicer 2.4.0 release.</p>
</description>
</release>
<release version="2.3.2" date="2025-03-23">
+48
View File
@@ -0,0 +1,48 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
[string]$ArtifactDirectory
)
$ErrorActionPreference = "Stop"
function Get-RelativePath {
param(
[Parameter(Mandatory = $true)]
[string]$Root,
[Parameter(Mandatory = $true)]
[string]$Path
)
$rootWithSeparator = $Root.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
$rootUri = [Uri]$rootWithSeparator
$pathUri = [Uri]$Path
[Uri]::UnescapeDataString($rootUri.MakeRelativeUri($pathUri).ToString()).Replace("/", [IO.Path]::DirectorySeparatorChar)
}
$artifactRoot = (Resolve-Path -LiteralPath $ArtifactDirectory).Path
$binaries = Get-ChildItem -LiteralPath $artifactRoot -Recurse -File |
Where-Object { $_.Extension -in @(".exe", ".dll") } |
Sort-Object -Property FullName
if (-not $binaries) {
Write-Warning "No .exe or .dll files found under '$artifactRoot'."
exit 0
}
$binaries | ForEach-Object {
$signature = Get-AuthenticodeSignature -LiteralPath $_.FullName
[pscustomobject]@{
RelativePath = Get-RelativePath -Root $artifactRoot -Path $_.FullName
Status = $signature.Status
SignatureType = $signature.SignatureType
Signer = if ($signature.SignerCertificate) { $signature.SignerCertificate.Subject } else { "" }
SignerThumbprint = if ($signature.SignerCertificate) { $signature.SignerCertificate.Thumbprint } else { "" }
Timestamped = [bool]$signature.TimeStamperCertificate
TimeStamper = if ($signature.TimeStamperCertificate) { $signature.TimeStamperCertificate.Subject } else { "" }
}
} | Format-Table -AutoSize
+4 -251
View File
@@ -12,7 +12,6 @@ commands:
trim delete profile files no <vendor>.json list references
update-index regenerate the *_list sections of <vendor>.json
legacy-compat write the printers user presets can be migrated back to
gcode-diff print a readable markdown diff of changed custom G-code
options shared by several commands:
--vendor VENDOR act on one vendor bundle only; repeatable, empty means all
@@ -95,14 +94,11 @@ parent in the OFL map. filament_vendor / filament_type resolve the same way.
"""
import argparse
import difflib
import functools
import html
import json
import os
import posixpath
import re
import subprocess
import sys
import uuid
from collections import Counter, defaultdict
@@ -196,7 +192,6 @@ VECTOR_KEYS = {
PRINT_CONFIG_CPP = os.path.normpath(
os.path.join(SCRIPTS_DIR, "..", "src", "libslic3r", "PrintConfig.cpp"))
TAB_CPP = os.path.normpath(os.path.join(SCRIPTS_DIR, "..", "src", "slic3r", "GUI", "Tab.cpp"))
@functools.lru_cache(maxsize=None)
@@ -318,11 +313,11 @@ REMEDY_HINTS = {
}
def print_error(msg, file=None):
print(f"\033[91m[ERROR]\033[0m {msg}", file=file) # Red
def print_error(msg):
print(f"\033[91m[ERROR]\033[0m {msg}") # Red
def print_warning(msg, file=None):
print(f"\033[93m[WARNING]\033[0m {msg}", file=file) # Yellow
def print_warning(msg):
print(f"\033[93m[WARNING]\033[0m {msg}") # Yellow
def print_info(msg):
print(f"\033[94m[INFO]\033[0m {msg}") # Blue
@@ -3214,223 +3209,6 @@ def run_legacy_compat(base_dir, profiles_dir, output, check):
return 0
# ---------------------------------------------------------------------------
# gcode-diff
# ---------------------------------------------------------------------------
# UTF-8 bytes, kept well under GitHub's 65,536-character comment limit.
GCODE_DIFF_BUDGET = 60000
# The file names a section's summary shows before "and N more".
GCODE_DIFF_SUMMARY_FILES = 3
GCODE_DIFF_HEADER = (
"## Custom G-code changes\n"
"\n"
"GitHub shows custom G-code in a profile as one long line, so below is a line by line "
"diff of each changed G-code or other text setting.\n")
@functools.lru_cache(maxsize=None)
def _text_option_keys():
"""The string options PrintConfig.cpp defines as G-code or other multi-line text
(every *_gcode option, and every one it marks multiline, such as notes), plus the
ones Tab.cpp edits as code, such as filename_format."""
with open(PRINT_CONFIG_CPP, encoding="utf-8") as f:
source = f.read()
keys = set()
for body in re.split(r"\bdef\s*=\s*this->add(?:_nullable)?\(", source)[1:]:
match = re.match(r'"(\w+)",\s*coStrings?\b', body)
if match and (match.group(1).endswith("_gcode")
or re.search(r"def->multiline\s*=\s*true", body)):
keys.add(match.group(1))
if "machine_start_gcode" not in keys:
raise RuntimeError(f"{PRINT_CONFIG_CPP} no longer defines machine_start_gcode")
# Tab.cpp only adds keys, so a missing file is not an error.
try:
with open(TAB_CPP, encoding="utf-8") as f:
gui = f.read()
except OSError:
gui = ""
keys.update(re.findall(r'get_option\("(\w+)"\);(?:(?!get_option\().)*?\bis_code\s*=\s*true',
gui, re.DOTALL))
return frozenset(keys)
def _git(cwd, *args, stdin=None):
return subprocess.run(["git", "-C", cwd, *args], input=stdin, check=True,
capture_output=True).stdout
def _blobs_at(profiles_dir, specs):
"""{(ref, path): bytes, or None if not a file there} for (ref, path relative to
profiles_dir) pairs, all read by one git cat-file process."""
prefix = _git(profiles_dir, "rev-parse", "--show-prefix").decode("utf-8").strip()
out = _git(profiles_dir, "cat-file", "--batch",
stdin="".join(f"{ref}:{prefix}{rel}\n" for ref, rel in specs).encode("utf-8"))
blobs, pos = {}, 0
for spec in specs:
end = out.index(b"\n", pos)
header = out[pos:end]
pos = end + 1
# "<name> missing" and "<name> ambiguous" have no content; "<oid> <type> <size>" does.
if header.endswith((b" missing", b" ambiguous")):
blobs[spec] = None
continue
_oid, kind, size = header.split()
blobs[spec] = out[pos:pos + int(size)] if kind == b"blob" else None
pos += int(size) + 1
return blobs
def _parse_profile(raw, rel, where):
"""The JSON in raw, or None with a warning when it is absent or not JSON."""
if raw is None:
print_warning(f"{rel}: skipped, not found at {where}", file=sys.stderr)
return None
try:
return json.loads(raw.decode("utf-8-sig"))
except ValueError as e:
print_warning(f"{rel}: skipped, cannot read it at {where}: {e}", file=sys.stderr)
return None
def _changed_profiles(profiles_dir, base, head):
"""(old path, new path) of each JSON file under profiles_dir that differs between
base and head (the working tree, untracked files included, when None); the absent
side of an added or deleted file is None, and so is a side that is not JSON. Renames
are paired."""
out = _git(profiles_dir, "diff", "--name-status", "-z", "-M", "--relative", base,
*([head] if head else []), "--", ".")
fields = out.decode("utf-8").split("\0")
pairs, i = [], 0
while i + 1 < len(fields):
status = fields[i]
old = new = fields[i + 1]
if status[0] in "RC":
new = fields[i + 2]
i += 3
else:
i += 2
old = None if status[0] == "A" or not old.endswith(".json") else old
new = None if status[0] == "D" or not new.endswith(".json") else new
if old or new:
pairs.append((old, new))
if head is None:
out = _git(profiles_dir, "ls-files", "-z", "--others", "--exclude-standard", "--", ".")
pairs += [(None, rel) for rel in out.decode("utf-8").split("\0")
if rel.endswith(".json") and rel.split("/")[0] != USER_DIR]
return pairs
def _text_lines(value):
return [line.rstrip() for line in value.split("\n")] if value else []
def _text_changes(old, new):
"""(setting, unified diff) for every G-code or other text value that differs
between two profiles; a vector option with several values gives one per value."""
changes = []
for key in list(new) + [k for k in old if k not in new]:
a, b = _as_list(old.get(key, [])), _as_list(new.get(key, []))
if a == b or not all(isinstance(v, str) for v in a + b):
continue
if key not in _text_option_keys() and not any("\n" in v for v in a + b):
continue
width = max(len(a), len(b))
for i in range(width):
diff = list(difflib.unified_diff(
_text_lines(a[i] if i < len(a) else ""), _text_lines(b[i] if i < len(b) else ""),
lineterm=""))[2:] # drops the ---/+++ file header
if diff:
changes.append((key if width == 1 else f"{key}[{i}]", "\n".join(diff)))
return changes
def _gcode_diff_section(files, setting, diff):
# A fence longer than any backtick run in the G-code cannot be closed by it.
fence = "`" * max([3] + [len(run) + 1 for run in re.findall(r"`+", diff)])
# In <code>, the "@BBL" of a filament file name does not mention a GitHub user.
def code(text):
return f"<code>{html.escape(text, quote=False)}</code>"
names, listing = ", ".join(map(code, files)), ""
if len(files) > GCODE_DIFF_SUMMARY_FILES + 1: # never "and 1 more"
listing = f"<p>{names}</p>\n\n" # an HTML block, so markdown in names stays literal
names = (", ".join(map(code, files[:GCODE_DIFF_SUMMARY_FILES]))
+ f" and {len(files) - GCODE_DIFF_SUMMARY_FILES} more")
return (f"\n<details>\n<summary><b>{names} - {code(setting)} (click to expand)</b>"
f"</summary>\n\n{listing}{fence}diff\n{diff}\n{fence}\n\n</details>\n")
def gcode_diff(profiles_dir, base, head=None, vendors=None):
"""Markdown with one collapsed diff per changed G-code setting between base and
head, or "" when none changed. Files whose diff of a setting is identical share
one section. Cut to whole sections under GCODE_DIFF_BUDGET."""
pairs = [(old, new) for old, new in _changed_profiles(profiles_dir, base, head)
if not vendors or any(p and p.split("/")[0] in vendors for p in (old, new))]
specs = ([(base, old) for old, _ in pairs if old]
+ [(head, new) for _, new in pairs if new and head])
blobs = _blobs_at(profiles_dir, specs) if specs else {}
def load(ref, rel):
if rel is None:
return {}
if ref:
return _parse_profile(blobs[(ref, rel)], rel, ref)
try:
return _parse_profile(Path(profiles_dir, rel).read_bytes(), rel, "the working tree")
except OSError as e:
print_warning(f"{rel}: skipped, cannot read it: {e}", file=sys.stderr)
return None
groups = {} # (setting, diff) -> files, in the order git lists them
for old_rel, new_rel in pairs:
old, new = load(base, old_rel), load(head, new_rel)
if not isinstance(old, dict) or not isinstance(new, dict):
continue
for setting, diff in _text_changes(old, new):
groups.setdefault((setting, diff), []).append(new_rel or old_rel)
if not groups:
return ""
sections = [_gcode_diff_section(files, setting, diff)
for (setting, diff), files in groups.items()]
budget = GCODE_DIFF_BUDGET - len(GCODE_DIFF_HEADER.encode("utf-8"))
shown = 0
for section in sections:
budget -= len(section.encode("utf-8"))
if budget < 0:
break
shown += 1
note, hidden = "", len(sections) - shown
if hidden:
note = (f"\n{hidden} of {len(sections)} sections {'is' if hidden == 1 else 'are'} not "
"shown because the comment would be too long for GitHub. See the Files "
"changed tab for the rest.\n")
return GCODE_DIFF_HEADER + "".join(sections[:shown]) + note
def _default_gcode_diff_base(profiles_dir, head):
for main in ("upstream/main", "origin/main"):
try:
return _git(profiles_dir, "merge-base", head or "HEAD", main).decode().strip()
except subprocess.CalledProcessError:
continue
raise RuntimeError("no --base given, and neither upstream/main nor origin/main exists")
def run_gcode_diff(profiles_dir, vendors, base, head):
try:
markdown = gcode_diff(profiles_dir, base or _default_gcode_diff_base(profiles_dir, head),
head, vendors)
# UnicodeDecodeError means git listed a file name that is not UTF-8.
except (OSError, RuntimeError, UnicodeDecodeError, subprocess.CalledProcessError) as e:
stderr = getattr(e, "stderr", None)
print_error(stderr.decode(errors="replace").strip() if stderr else str(e), file=sys.stderr)
return 1
sys.stdout.write(markdown)
return 0
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
@@ -3455,9 +3233,6 @@ examples:
orca_profile_tool.py legacy-compat --base /tmp/v2.4.2/resources/profiles
rewrite resources/info/legacy_compatible_printers.json against 2.4.2's
profiles (git archive v2.4.2 resources/profiles | tar -x -C /tmp/v2.4.2)
orca_profile_tool.py gcode-diff > gcode.md
the changed custom G-code of the working tree since it left upstream/main,
as markdown with one collapsed diff per setting
after adding, renaming or deleting profile files, run in this order:
normalize -> update-index -> generate-id -> check
@@ -3612,25 +3387,6 @@ def build_parser():
legacy_cmd.add_argument("--check", action="store_true",
help="fail if the map differs from what --base implies; write nothing")
gcode_cmd = add(
"gcode-diff", [vendor_opt, profiles_opt],
"print a readable markdown diff of changed custom G-code",
"Print, as markdown, a unified diff of every G-code or other multi-line\n"
"setting that differs between --base and --head in each changed profile\n"
"file, one collapsed section per setting. Files whose diff of a setting is\n"
"identical share a section. Trailing spaces are removed from every line, and\n"
"the output is cut to whole sections to fit a GitHub comment. Prints nothing\n"
"when no such setting changed. CI posts it on profile PRs.\n"
"\n"
"The settings are the *_gcode and multiline string options PrintConfig.cpp\n"
"defines, the options Tab.cpp edits as code (such as filename_format), and any\n"
"string value that spans several lines.")
gcode_cmd.add_argument("--base", metavar="REF",
help="compare against this git ref (default: the merge base "
"with upstream/main, else origin/main)")
gcode_cmd.add_argument("--head", metavar="REF",
help="compare this git ref (default: the working tree)")
return parser
@@ -3667,9 +3423,6 @@ def main(argv=None):
if args.command == "legacy-compat":
return run_legacy_compat(args.base, profiles_dir, args.output, args.check)
if args.command == "gcode-diff":
return run_gcode_diff(profiles_dir, vendors, args.base, args.head)
if args.command == "fix-variant":
_changed, errors = fix_variant_arrays(profiles_dir, vendors, dry_run=args.dry_run,
strict=args.strict)
+44
View File
@@ -0,0 +1,44 @@
# Copies the files SignPath signed back over the originals in the portable bundle they
# were staged from with stage-for-signpath.ps1 -Bundle: the root of the signed artifact
# holds OrcaSlicer's own binaries, third-party/ the other files under their paths in the
# bundle. Root files named in -Exclude came from outside the bundle, such as the exported
# uninstaller, and stay in -SignedDirectory.
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
[string]$SignedDirectory,
[Parameter(Mandatory = $true)]
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
[string]$Bundle,
[string[]]$Exclude = @()
)
$ErrorActionPreference = "Stop"
$signedRoot = (Resolve-Path -LiteralPath $SignedDirectory).Path.TrimEnd('\', '/')
$thirdPartyPrefix = "third-party" + [IO.Path]::DirectorySeparatorChar
$files = Get-ChildItem -LiteralPath $signedRoot -Recurse -File |
Where-Object { $_.DirectoryName -ne $signedRoot -or $_.Name -notin $Exclude }
if (-not $files) {
throw "No signed files found in '$signedRoot'."
}
foreach ($file in $files) {
$relative = $file.FullName.Substring($signedRoot.Length + 1)
if ($relative.StartsWith($thirdPartyPrefix, [StringComparison]::OrdinalIgnoreCase)) {
$relative = $relative.Substring($thirdPartyPrefix.Length)
}
# Every signed file replaces one that was staged, so a file without an original means
# the artifact does not have the layout stage-for-signpath.ps1 produces.
$target = Join-Path $Bundle $relative
if (-not (Test-Path -LiteralPath $target -PathType Leaf)) {
throw "Signed file '$relative' has no original in '$Bundle'."
}
Copy-Item -LiteralPath $file.FullName -Destination $target -Force
}
Write-Host "Restored $(@($files).Count) signed files into '$Bundle'."
+82
View File
@@ -0,0 +1,82 @@
# Copies the files a SignPath signing request covers into the directory that request
# uploads, after checking their version information. SignPath refuses to sign a file
# whose ProductName or ProductVersion differs from its artifact configuration (see
# signpath/), and reports it only once the request has been approved, so the check runs
# before anything is submitted.
#
# -Path stages the given files at the root of -Destination.
#
# -Bundle stages a portable OrcaSlicer bundle for signpath/windows-binaries-v1.xml:
# OrcaSlicer's own binaries (-OwnBinaries, plus any -Path files from outside the bundle,
# such as the exported uninstaller) at the root, where their version information is
# checked, and every other executable, DLL and Python extension module that carries no
# signature under third-party/, at its path in the bundle. Files signed by their vendor,
# such as the Microsoft runtime, keep that signature. restore-from-signpath.ps1 copies the
# signed files back.
[CmdletBinding(DefaultParameterSetName = "Path")]
param(
[Parameter(Mandatory = $true, ParameterSetName = "Path")]
[Parameter(ParameterSetName = "Bundle")]
[string[]]$Path,
[Parameter(Mandatory = $true, ParameterSetName = "Bundle")]
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
[string]$Bundle,
[Parameter(ParameterSetName = "Bundle")]
[string[]]$OwnBinaries = @("orca-slicer.exe", "OrcaSlicer.dll"),
[Parameter(Mandatory = $true)]
[string]$Destination,
[Parameter(Mandatory = $true)]
[string]$ProductVersion,
[string]$ProductName = "OrcaSlicer"
)
$ErrorActionPreference = "Stop"
function Test-VersionInfo {
param([string]$File)
if (-not (Test-Path -LiteralPath $File -PathType Leaf)) {
throw "File to sign was not found: $File"
}
$info = [Diagnostics.FileVersionInfo]::GetVersionInfo((Resolve-Path -LiteralPath $File).Path)
if ($info.ProductName -ne $ProductName -or $info.ProductVersion -ne $ProductVersion) {
throw "'$File' has ProductName '$($info.ProductName)' and ProductVersion '$($info.ProductVersion)'; SignPath expects '$ProductName' and '$ProductVersion'."
}
}
New-Item -ItemType Directory -Force -Path $Destination | Out-Null
$ownFiles = @($Path | Where-Object { $_ })
if ($PSCmdlet.ParameterSetName -eq "Bundle") {
$bundleRoot = (Resolve-Path -LiteralPath $Bundle).Path.TrimEnd('\', '/')
$ownFiles = @($OwnBinaries | ForEach-Object { Join-Path $bundleRoot $_ }) + $ownFiles
}
foreach ($file in $ownFiles) {
Test-VersionInfo $file
Copy-Item -LiteralPath $file -Destination $Destination -Force
Write-Host "Staged $(Split-Path -Leaf $file) ($ProductName $ProductVersion)"
}
if ($PSCmdlet.ParameterSetName -eq "Path") {
exit 0
}
$thirdParty = Get-ChildItem -LiteralPath $bundleRoot -Recurse -File |
Where-Object { $_.Extension -in ".exe", ".dll", ".pyd" } |
Where-Object { $_.DirectoryName -ne $bundleRoot -or $_.Name -notin $OwnBinaries } |
Where-Object { (Get-AuthenticodeSignature -LiteralPath $_.FullName).Status -eq "NotSigned" }
foreach ($file in $thirdParty) {
$relative = $file.FullName.Substring($bundleRoot.Length + 1)
$target = Join-Path (Join-Path $Destination "third-party") $relative
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $target) | Out-Null
Copy-Item -LiteralPath $file.FullName -Destination $target -Force
}
Write-Host "Staged $(@($thirdParty).Count) unsigned third-party files under third-party/"
-4
View File
@@ -301,10 +301,6 @@ $cases = @(
Contains = @('Precompiled header: off') }
@{ Name = 'the precompiled header is on unless asked'; Args = @('-s')
NotContains = @('SLIC3R_PCH') }
@{ Name = '-l turns the precompiled header off'; Args = @('-s', '-l')
Contains = @('-DSLIC3R_PCH=OFF', 'Precompiled header: off') }
@{ Name = 'a deps-only build says nothing about the precompiled header'; Args = @('-d', '-l')
NotContains = @('Precompiled header') }
@{ Name = '--no-pch works without a cache'; Args = @('-s', '--no-pch')
NotContains = @('COMPILER_LAUNCHER') }
@{ Name = 'the slicer build runs gettext'; Args = @('-s')
@@ -1,93 +0,0 @@
<#
.SYNOPSIS
Tests scripts/check_exception_throw_info.ps1 on canned llvm-pdbutil dumps.
.DESCRIPTION
Each case runs the check in its own powershell -File process, as CI does, and asserts on the
exit code and output. Dumps are written to a temporary folder, so no build is needed.
.EXAMPLE
powershell -File scripts/test_check_exception_throw_info.ps1
#>
$ErrorActionPreference = 'Stop'
$check = Join-Path $PSScriptRoot 'check_exception_throw_info.ps1'
$work = Join-Path ([IO.Path]::GetTempPath()) 'check_exception_throw_info_tests'
Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $work | Out-Null
function Write-Dump([string] $path, [string[]] $symbols) {
New-Item -ItemType Directory -Force -Path (Split-Path $path) | Out-Null
$i = 0
$lines = @(' Public Symbols', '============================================================')
foreach ($symbol in $symbols) {
$i++
$lines += " $i | S_PUB32 [size = 48] ``$symbol``", ' flags = none, addr = 0007:0000'
}
Set-Content -Path $path -Encoding ascii -Value $lines
}
$bitwise = '_CT??_R0?AVfuture_error@std@@@840'
$copy = '_CT??_R0?AVfuture_error@std@@@8??0future_error@std@@QEAA@AEBV01@@Z40'
$trivial = '_CT??_R0?AUtag@@@81'
Write-Dump "$work\broken.txt" @($bitwise, $copy, $trivial)
Write-Dump "$work\clean.txt" @($copy, $trivial)
Write-Dump "$work\empty.txt" @()
Write-Dump "$work\a dir, with a comma\clean.txt" @($copy)
# A build directory whose compiler has an llvm-pdbutil beside it but no OrcaSlicer.pdb.
# Nothing runs the stand-in; the check stops before it gets there.
$build = "$work\build"
New-Item -ItemType Directory -Force -Path "$build\bin", "$build\src\Release" | Out-Null
Copy-Item "$env:SystemRoot\System32\where.exe" "$build\bin\llvm-pdbutil.exe"
Set-Content -Path "$build\CMakeCache.txt" -Encoding ascii -Value "CMAKE_CXX_COMPILER:FILEPATH=$build\bin\clang-cl.exe"
Set-Content -Path "$build\src\Release\orca-slicer.pdb" -Value ''
$bare = "$work\bare"
New-Item -ItemType Directory -Force -Path $bare | Out-Null
Set-Content -Path "$bare\CMakeCache.txt" -Encoding ascii -Value 'CMAKE_CXX_COMPILER:STRING=no-such-clang-cl.exe'
$cases = @(
@{ Name = 'a class with and without a copy constructor fails'; Args = @('-Dump', "$work\broken.txt")
Exit = 1; Contains = @('1 with broken throw info', 'future_error@std') }
@{ Name = 'copy constructors and trivially copyable classes pass'; Args = @('-Dump', "$work\clean.txt")
Exit = 0; Contains = @('2 exception classes, 0 with broken throw info') }
@{ Name = 'no exception classes at all fails'; Args = @('-Dump', "$work\empty.txt")
Exit = 1; Contains = @('No exception classes found') }
@{ Name = 'a comma-separated list checks each dump'; Args = @('-Dump', "$work\broken.txt,$work\clean.txt")
Exit = 1; Contains = @('broken.txt: 2 exception classes', 'clean.txt: 2 exception classes') }
@{ Name = 'a path with a comma stays whole'; Args = @('-Dump', "$work\a dir, with a comma\clean.txt")
Exit = 0; Contains = @('clean.txt: 1 exception classes') }
@{ Name = 'a build without OrcaSlicer.pdb fails'; Args = @('-BuildDir', $build)
Exit = 1; Contains = @('No OrcaSlicer.pdb in') }
@{ Name = 'a compiler that is not on PATH is named'; Args = @('-BuildDir', $bare)
Exit = 1; Contains = @('no-such-clang-cl.exe, the compiler in') }
@{ Name = 'a missing build directory is named'; Args = @('-BuildDir', "$work\nowhere")
Exit = 1; Contains = @('No CMakeCache.txt in') }
@{ Name = 'no build directory and no dump fails'; Args = @()
Exit = 1; Contains = @('Name the slicer build directory') }
)
$failed = 0
foreach ($case in $cases) {
$ErrorActionPreference = 'Continue'
$output = (& powershell -NoProfile -File $check @($case.Args) 2>&1 | Out-String)
$code = $LASTEXITCODE
$ErrorActionPreference = 'Stop'
$problems = @()
if ($code -ne $case.Exit) { $problems += "exit $code, expected $($case.Exit)" }
foreach ($text in $case.Contains) {
if (-not $output.Contains($text)) { $problems += "no '$text' in the output" }
}
if ($problems) {
$failed++
" FAIL $($case.Name)"
$problems | ForEach-Object { " $_" }
$output.TrimEnd() -split "`r?`n" | ForEach-Object { " | $_" }
} else {
" ok $($case.Name)"
}
}
Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue
"$($cases.Count - $failed) passed, $failed failed"
exit [int]($failed -gt 0)
+3 -309
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Tests for the tree-maintenance half of scripts/orca_profile_tool.py: the
normalize, trim, update-index, fix-variant, check and gcode-diff commands, and the
subcommand dispatch that reaches them (stdlib unittest, no external deps).
normalize, trim, update-index, fix-variant and check commands, and the subcommand
dispatch that reaches them (stdlib unittest, no external deps).
The id halves are covered by test_filament_id.py and test_setting_id.py.
@@ -14,12 +14,9 @@ import json
import os
import re
import shutil
import stat
import subprocess
import sys
import tempfile
import unittest
from unittest import mock
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
@@ -44,24 +41,7 @@ class Tree:
os.makedirs(self.profiles)
def cleanup(self):
root = os.path.abspath(self.dir)
def writable_retry(func, path, _exc):
# Windows refuses to delete read-only files such as git's objects, and POSIX
# refuses to delete the entries of a read-only directory.
if not os.path.lexists(path): # already removed, as on a second cleanup
return
parent = os.path.dirname(os.path.abspath(path))
try:
if os.path.commonpath([parent, root]) == root: # never the temp dir itself
os.chmod(parent, stat.S_IRWXU)
if not os.path.islink(path): # chmod would follow it out of the tree
os.chmod(path, stat.S_IRWXU)
func(path)
except (OSError, ValueError): # commonpath raises ValueError across drives
pass
hook = "onexc" if sys.version_info >= (3, 12) else "onerror"
shutil.rmtree(self.dir, **{hook: writable_retry})
shutil.rmtree(self.dir, ignore_errors=True)
def index_path(self, vendor):
return os.path.join(self.profiles, vendor + ".json")
@@ -1429,292 +1409,6 @@ class TestDispatch(TreeCase):
self.assertEqual(self.t.read("V", "filament/A.json")["type"], "filament")
# ---------------------------------------------------------------------------
# gcode-diff
# ---------------------------------------------------------------------------
@unittest.skipUnless(shutil.which("git"), "git not installed")
class TestGcodeDiff(TreeCase):
START = "G28\nM104 S200\nG1 Z5"
def git(self, *args):
subprocess.run(["git", "-C", self.t.dir, "-c", "user.name=t", "-c", "user.email=t@t",
"-c", "commit.gpgsign=false", *args], check=True, capture_output=True)
def printer(self, name, **extra):
self.t.write("V", f"machine/{name}.json", {"type": "machine", "name": name, **extra})
def commit(self):
self.git("add", "-A")
self.git("commit", "-q", "--allow-empty", "-m", "x")
@classmethod
def setUpClass(cls):
# git's list of the variables that tie a process to a repository, asked with no
# GIT_* set since a stray GIT_DIR fails the query.
clean = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
cls.repo_env_vars = set(subprocess.run(
["git", "rev-parse", "--local-env-vars"], env=clean, check=True,
capture_output=True, text=True).stdout.split())
def setUp(self):
super().setUp()
# A git hook's GIT_DIR or GIT_INDEX_FILE would point every git call here, the
# tool's included, at the real repository instead of the temp tree.
env = {k: v for k, v in os.environ.items() if k not in self.repo_env_vars}
patcher = mock.patch.dict(os.environ, env, clear=True)
patcher.start()
self.addCleanup(patcher.stop)
self.git("init", "-q")
for name in ("A", "B", "C"):
self.printer(name, machine_start_gcode=self.START, nozzle_diameter=["0.4"])
self.printer("Gone", machine_end_gcode="M84")
self.commit()
def diff(self):
rc, out = self.run_command("gcode-diff", "--base", "HEAD")
self.assertEqual(rc, 0, out)
return out
def test_identical_edits_share_one_section(self):
for name in ("A", "B"):
self.printer(name, machine_start_gcode="G28\nM104 S210 \nG1 Z5")
self.printer("C", machine_start_gcode="G28\nM104 S220\nG1 Z5")
out = self.diff()
self.assertEqual(out.count("<details>"), 2)
self.assertIn("<code>V/machine/A.json</code>, <code>V/machine/B.json</code> - "
"<code>machine_start_gcode</code>", out)
self.assertIn("<code>V/machine/C.json</code> - <code>machine_start_gcode</code>", out)
# Trailing spaces are removed.
self.assertIn("\n-M104 S200\n+M104 S210\n", out)
self.assertIn("\n\n```diff\n@@ ", out)
self.assertIn("\n```\n\n</details>", out)
def test_nothing_is_printed_without_a_text_change(self):
self.printer("A", machine_start_gcode=self.START, nozzle_diameter=["0.6"])
self.assertEqual(self.diff(), "")
def test_added_and_deleted_files_show_the_whole_value(self):
# An untracked file counts as added; the validator's user dir is not a profile.
self.printer("New", machine_start_gcode="G28\nG29")
self.t.write("user", "default/machine/U.json", {"machine_start_gcode": "G28"})
os.remove(self.t.path("V", "machine/Gone.json"))
out = self.diff()
self.assertIn("@@ -0,0 +1,2 @@\n+G28\n+G29\n", out)
self.assertIn("<code>V/machine/Gone.json</code> - <code>machine_end_gcode</code>", out)
self.assertIn("@@ -1 +0,0 @@\n-M84\n", out)
self.assertNotIn("U.json", out)
def test_a_multi_line_value_counts_whatever_its_key(self):
self.printer("A", machine_start_gcode=self.START, custom_note="one\ntwo")
self.assertIn("<code>V/machine/A.json</code> - <code>custom_note</code>", self.diff())
def test_a_file_name_cannot_mention_a_user(self):
self.t.write("V", "filament/PLA @BBL A1.json", {"filament_start_gcode": ["M106"]})
self.assertIn("<code>V/filament/PLA @BBL A1.json</code>", self.diff())
def test_head_compares_two_commits(self):
self.printer("A", machine_start_gcode="G28")
self.commit()
rc, out = self.run_command("gcode-diff", "--base", "HEAD~1", "--head", "HEAD")
self.assertEqual(rc, 0, out)
self.assertIn("<code>V/machine/A.json</code> - <code>machine_start_gcode</code>", out)
def test_a_renamed_file_diffs_against_its_old_self(self):
os.rename(self.t.path("V", "machine/A.json"), self.t.path("V", "machine/A2.json"))
self.printer("A2", machine_start_gcode="G28\nM104 S210\nG1 Z5", nozzle_diameter=["0.4"])
self.commit()
rc, out = self.run_command("gcode-diff", "--base", "HEAD~1", "--head", "HEAD")
self.assertEqual(rc, 0, out)
self.assertIn("<code>V/machine/A2.json</code> - <code>machine_start_gcode</code>", out)
self.assertIn("\n G28\n-M104 S200\n+M104 S210\n G1 Z5\n", out)
def test_a_file_renamed_off_json_loses_its_values(self):
os.rename(self.t.path("V", "machine/Gone.json"), self.t.path("V", "machine/Gone.json.bak"))
self.commit()
rc, out = self.run_command("gcode-diff", "--base", "HEAD~1", "--head", "HEAD")
self.assertEqual(rc, 0, out)
self.assertIn("<code>V/machine/Gone.json</code> - <code>machine_end_gcode</code>", out)
self.assertIn("@@ -1 +0,0 @@\n-M84\n", out)
def test_a_file_renamed_onto_json_is_new(self):
with open(self.t.path("V", "machine/Draft.txt"), "w", encoding="utf-8") as f:
json.dump({"type": "machine", "machine_start_gcode": self.START}, f)
self.commit()
os.rename(self.t.path("V", "machine/Draft.txt"), self.t.path("V", "machine/Draft.json"))
self.commit()
rc, out = self.run_command("gcode-diff", "--base", "HEAD~1", "--head", "HEAD")
self.assertEqual(rc, 0, out)
self.assertIn("@@ -0,0 +1,3 @@\n+G28\n+M104 S200\n+G1 Z5\n", out)
def test_a_batch_read_survives_paths_that_are_not_files(self):
blobs = apt._blobs_at(self.t.profiles, [("HEAD", "V/machine"), ("HEAD", "V/no such.json"),
("HEAD", "V/machine/Gone.json")])
self.assertIsNone(blobs[("HEAD", "V/machine")])
self.assertIsNone(blobs[("HEAD", "V/no such.json")])
self.assertEqual(json.loads(blobs[("HEAD", "V/machine/Gone.json")])["machine_end_gcode"],
"M84")
def test_each_value_of_a_vector_option_is_its_own_setting(self):
self.t.write("V", "filament/F.json", {"filament_start_gcode": ["M106", "M107"]})
self.commit()
self.t.write("V", "filament/F.json", {"filament_start_gcode": ["M106", "M106 S0"]})
out = self.diff()
self.assertEqual(out.count("<details>"), 1)
self.assertIn("<code>filament_start_gcode[1]</code>", out)
def test_vendor_limits_the_files(self):
self.printer("A", machine_start_gcode="G28")
self.t.write("W", "machine/X.json", {"machine_start_gcode": "G28"})
rc, out = self.run_command("gcode-diff", "--base", "HEAD", "--vendor", "V")
self.assertEqual(rc, 0, out)
self.assertIn("V/machine/A.json", out)
self.assertNotIn("W/machine/X.json", out)
def test_vendor_includes_files_moved_out_of_it(self):
self.t.add_vendor("W")
os.rename(self.t.path("V", "machine/A.json"), self.t.path("W", "machine/A.json"))
data = self.t.read("W", "machine/A.json")
self.t.write("W", "machine/A.json", {**data, "machine_start_gcode": "G28\nM104 S210\nG1 Z5"})
self.commit()
rc, out = self.run_command("gcode-diff", "--base", "HEAD~1", "--head", "HEAD",
"--vendor", "V")
self.assertEqual(rc, 0, out)
self.assertIn("<code>W/machine/A.json</code> - <code>machine_start_gcode</code>", out)
def same_edit(self, names):
"""The diff after one start G-code edit to each of names; returns (summary, output)."""
for name in names:
self.printer(name, machine_start_gcode=self.START, nozzle_diameter=["0.4"])
self.commit()
for name in names:
self.printer(name, machine_start_gcode="G28")
out = self.diff()
return re.search(r"<summary>(.*)</summary>", out).group(1), out
def test_a_long_file_list_moves_into_the_section(self):
summary, out = self.same_edit("ABCDE")
self.assertIn("<code>V/machine/C.json</code> and 2 more - ", summary)
self.assertNotIn("D.json", summary)
self.assertIn("<code>V/machine/D.json</code>, <code>V/machine/E.json</code></p>\n\n```diff",
out)
def test_four_files_are_all_named_in_the_summary(self):
summary, _out = self.same_edit("ABCD")
self.assertIn("<code>V/machine/D.json</code> - ", summary)
self.assertNotIn("more", summary)
def test_an_error_goes_to_stderr_only(self):
err = io.StringIO()
with contextlib.redirect_stderr(err):
rc, out = self.run_command("gcode-diff", "--base", "no-such-ref")
self.assertEqual(rc, 1)
self.assertEqual(out, "")
self.assertIn("no-such-ref", err.getvalue())
def test_a_file_name_that_is_not_utf8_is_an_error(self):
bad = UnicodeDecodeError("utf-8", b"\xe9.json", 0, 1, "invalid continuation byte")
err = io.StringIO()
with mock.patch.object(apt, "_changed_profiles", side_effect=bad), \
contextlib.redirect_stderr(err):
rc, out = self.run_command("gcode-diff", "--base", "HEAD")
self.assertEqual((rc, out), (1, ""))
self.assertIn("invalid continuation byte", err.getvalue())
def test_output_is_cut_to_whole_sections(self):
for name in ("A", "B", "C"):
self.printer(name, machine_start_gcode=name * 300)
size = len(apt.gcode_diff(self.t.profiles, "HEAD").encode("utf-8"))
with mock.patch.object(apt, "GCODE_DIFF_BUDGET", size - 1):
out = self.diff()
self.assertEqual(out.count("<details>"), 2)
self.assertEqual(out.count("</details>"), 2)
self.assertIn("\n1 of 3 sections is not shown", out)
with mock.patch.object(apt, "GCODE_DIFF_BUDGET", 1):
self.assertIn("\n3 of 3 sections are not shown", self.diff())
def test_cleanup_removes_the_repo(self):
# setUp's commit leaves read-only git objects, and a read-only directory blocks
# deletion on POSIX.
os.chmod(self.t.path("V", "machine"), stat.S_IRUSR | stat.S_IXUSR)
self.t.cleanup()
self.assertFalse(os.path.exists(self.t.dir))
with mock.patch.object(os, "chmod") as chmod:
self.t.cleanup() # a second cleanup, as addCleanup runs, must not touch the temp dir
chmod.assert_not_called()
def test_only_repository_variables_are_stripped(self):
self.assertTrue({"GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE"} <= self.repo_env_vars)
self.assertFalse({"GIT_SSH", "GIT_EXEC_PATH", "GIT_EDITOR"} & self.repo_env_vars)
def test_cleanup_leaves_symlink_targets_alone(self):
outside = tempfile.mkdtemp(prefix="profile_tool_outside_")
self.addCleanup(shutil.rmtree, outside, True)
target = os.path.join(outside, "f")
open(target, "w").close()
os.chmod(target, stat.S_IRUSR)
self.addCleanup(os.chmod, target, stat.S_IRWXU) # runs before the rmtree above
machine = self.t.path("V", "machine")
try:
os.symlink(target, os.path.join(machine, "link"))
except (OSError, NotImplementedError):
self.skipTest("cannot create symlinks here")
os.chmod(machine, stat.S_IRUSR | stat.S_IXUSR) # so removing the link fails at first
self.t.cleanup()
self.assertFalse(os.path.exists(self.t.dir))
self.assertEqual(stat.S_IMODE(os.stat(target).st_mode) & stat.S_IWUSR, 0)
def test_a_git_hook_environment_is_ignored(self):
scripts = os.path.join(REPO_ROOT, "scripts")
result = subprocess.run(
[sys.executable, "-m", "unittest",
"tests.test_profile_tool.TestGcodeDiff.test_head_compares_two_commits"],
cwd=scripts, capture_output=True, text=True,
env={**os.environ, "GIT_DIR": os.path.join(self.t.dir, "nowhere")})
self.assertEqual(result.returncode, 0, result.stderr)
def test_the_options_are_read_from_the_engine(self):
keys = apt._text_option_keys()
self.assertTrue({"machine_start_gcode", "filament_start_gcode", "printing_by_object_gcode",
"notes", "post_process"} <= keys)
self.assertNotIn("emit_machine_limits_to_gcode", keys) # a coBool
START_DEF = 'def = this->add("machine_start_gcode", coString);\n'
def keys_from(self, print_config, tab=None):
"""_text_option_keys read from these sources; no tab means no Tab.cpp."""
paths = [os.path.join(self.t.dir, "PrintConfig.cpp"), os.path.join(self.t.dir, "Tab.cpp")]
for path, text in zip(paths, (print_config, tab)):
if text is not None:
with open(path, "w", encoding="utf-8") as f:
f.write(text)
apt._text_option_keys.cache_clear()
self.addCleanup(apt._text_option_keys.cache_clear)
with mock.patch.object(apt, "PRINT_CONFIG_CPP", paths[0]), \
mock.patch.object(apt, "TAB_CPP", paths[1]):
return apt._text_option_keys()
def test_multiline_is_read_whatever_its_spacing(self):
keys = self.keys_from(self.START_DEF + 'def = this->add("printer_notes", coString);\n'
' def->multiline = true;\n')
self.assertIn("printer_notes", keys)
def test_the_code_options_of_tab_cpp_are_read(self):
keys = self.keys_from(self.START_DEF, 'option = optgroup->get_option("filename_format");\n'
'option.opt.is_code = true;\n'
'option = optgroup->get_option("layer_height");\n')
self.assertIn("filename_format", keys)
self.assertNotIn("layer_height", keys)
def test_a_missing_tab_cpp_is_not_an_error(self):
self.assertIn("machine_start_gcode", self.keys_from(self.START_DEF))
def test_markdown_in_a_file_name_stays_literal(self):
section = apt._gcode_diff_section(["V/a *b*.json", "V/c.json", "V/d.json", "V/e.json",
"V/f.json"], "machine_start_gcode", "@@ -1 +1 @@")
self.assertIn("\n<p><code>V/a *b*.json</code>, ", section)
# ---------------------------------------------------------------------------
# the real tree
# ---------------------------------------------------------------------------
+121
View File
@@ -0,0 +1,121 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
[string]$ArtifactDirectory,
# Paths relative to -ArtifactDirectory. Without them every executable, DLL and Python
# extension module under it is verified.
[string[]]$Files,
[string]$SignToolPath,
# Accept signatures whose certificate chain terminates in an untrusted root.
# Required for the SignPath test certificate (self-signed). Do NOT pass this
# with the release certificate, so release builds enforce a fully trusted chain.
[switch]$AllowUntrustedRoot
)
$ErrorActionPreference = "Stop"
function Resolve-SignToolPath {
param(
[string]$ExplicitPath
)
if ($ExplicitPath) {
if (Test-Path -LiteralPath $ExplicitPath -PathType Leaf) {
return (Resolve-Path -LiteralPath $ExplicitPath).Path
}
throw "SignTool was not found at '$ExplicitPath'."
}
$fromPath = Get-Command -Name "signtool.exe" -ErrorAction SilentlyContinue
if ($fromPath) {
return $fromPath.Source
}
$candidateRoots = @(
"${env:ProgramFiles(x86)}\Windows Kits\10\bin",
"${env:ProgramFiles}\Windows Kits\10\bin"
) | Where-Object { $_ -and (Test-Path -LiteralPath $_ -PathType Container) }
foreach ($root in $candidateRoots) {
$candidate = Get-ChildItem -LiteralPath $root -Recurse -Filter "signtool.exe" -File -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match "\\(x64|arm64)\\signtool\.exe$" } |
Sort-Object -Property FullName -Descending |
Select-Object -First 1
if ($candidate) {
return $candidate.FullName
}
}
throw "signtool.exe was not found. Install the Windows SDK or pass -SignToolPath."
}
$artifactRoot = (Resolve-Path -LiteralPath $ArtifactDirectory).Path.TrimEnd('\', '/')
$signtool = Resolve-SignToolPath -ExplicitPath $SignToolPath
if (-not $Files) {
$Files = Get-ChildItem -LiteralPath $artifactRoot -Recurse -File |
Where-Object { $_.Extension -in ".exe", ".dll", ".pyd" } |
ForEach-Object { $_.FullName.Substring($artifactRoot.Length + 1) }
if (-not $Files) {
throw "No executables or DLLs found in '$artifactRoot'."
}
}
Write-Host "Using SignTool: $signtool"
Write-Host "Verifying Authenticode signatures of $(@($Files).Count) files in: $artifactRoot"
foreach ($relativePath in $Files) {
$filePath = Join-Path $artifactRoot $relativePath
if (-not (Test-Path -LiteralPath $filePath -PathType Leaf)) {
throw "Expected signed file was not found: $filePath"
}
# Capture signtool output without letting native stderr (redirected via 2>&1)
# raise a terminating NativeCommandError under $ErrorActionPreference = 'Stop'.
$previousErrorActionPreference = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try {
$output = & $signtool verify /pa /all /tw /v $filePath 2>&1
$exitCode = $LASTEXITCODE
}
finally {
$ErrorActionPreference = $previousErrorActionPreference
}
$signature = Get-AuthenticodeSignature -LiteralPath $filePath
if ($exitCode -eq 0) {
Write-Host "Verified $relativePath ($($signature.SignerCertificate.Subject))"
continue
}
if ($AllowUntrustedRoot) {
# signtool interleaves its stdout (chain details) and stderr (the error)
# unpredictably, so the error text cannot be matched reliably. Use the
# structured Get-AuthenticodeSignature result instead: accept only when the
# file is genuinely signed and the sole problem is that the chain terminates
# in an untrusted root (i.e. the self-signed SignPath test certificate).
$isSigned = ($signature.SignatureType -eq "Authenticode") -and ($null -ne $signature.SignerCertificate)
$untrustedRootOnly = $signature.StatusMessage -match "terminated in a root certificate which is not trusted"
if ($isSigned -and $untrustedRootOnly) {
Write-Host "Accepted $relativePath ($($signature.SignerCertificate.Subject)): signed, but its certificate chains to an untrusted root (expected for the SignPath test certificate)."
continue
}
}
# The full chain and error details only for a file that did not verify.
$output | ForEach-Object { Write-Host " $_" }
throw "SignTool verification failed for '$relativePath' with exit code $exitCode."
}
Write-Host "Authenticode verification passed."
# signtool exits non-zero for the untrusted test-cert root even when we accept
# it above, leaving $LASTEXITCODE = 1. The GitHub Actions pwsh wrapper exits
# with that lingering code, so reset it to report success explicitly.
exit 0
+23
View File
@@ -0,0 +1,23 @@
# SignPath configurations
Windows builds of `release/*` branches in `OrcaSlicer/OrcaSlicer` are code signed through [SignPath](https://about.signpath.io/), with the certificate the SignPath Foundation issues to open source projects. The XML files here are copies of the project's artifact configurations in SignPath, which the workflow refers to by slug only; a change to one has to be made in SignPath too, under the same slug.
Every signing request uses the `release-signing` policy and waits in SignPath until it is approved, so each Windows build of a release branch needs two approvals per architecture: the binaries, then the installer. Builds of `main` and pull requests are not signed.
## What gets signed
Every executable, DLL and Python extension module in the bundle ends up with a valid signature, so Windows features that check each loaded binary, such as Smart App Control, find none unsigned. Files their vendor already signed keep that signature: the Microsoft runtime DLLs and `WebView2Loader.dll` (Microsoft) and CPython's bundled OpenSSL and libffi (Python Software Foundation). Everything else is signed with the project's certificate: OrcaSlicer's own `orca-slicer.exe` and `OrcaSlicer.dll`, the libraries built from upstream sources (OCCT, FFmpeg, FreeType, GMP, MPFR, CPython), the downloaded `uv.exe`, and the NSIS installer with the `Uninstall.exe` it installs.
The SignPath Foundation's conditions ask projects to sign only binaries built from their own source and to leave upstream projects' binaries unsigned, which they allow inside a signed installer. Signing the upstream libraries departs from that, by the maintainers' choice. Narrowing the signed set back to OrcaSlicer's own files means dropping the `third-party` file set from `windows-binaries-v1` and the third-party staging from `scripts/stage-for-signpath.ps1`.
The Foundation also requires metadata restrictions on signed files. OrcaSlicer's own binaries, the installer and the uninstaller must have the ProductName `OrcaSlicer` and the ProductVersion passed in with the request, which the workflow takes from `SoftFever_VERSION` in `version.inc`. The binaries get that version information from `src/dev-utils/platform/msw/OrcaSlicer-version.rc.in`, the installer and its uninstaller from `CPACK_NSIS_DEFINES` in `CMakeLists.txt`. The upstream files carry their own projects' metadata, so their file set has no restriction. `scripts/stage-for-signpath.ps1` checks the version information before a request is submitted, since SignPath only rejects a mismatch after the request has been approved.
## `windows-binaries-v1`
Signs the binaries of the installed bundle, `${{ env.BUILD_DIR }}/OrcaSlicer`, and the installer's uninstaller. `scripts/stage-for-signpath.ps1 -Bundle` stages them for upload: `orca-slicer.exe`, `OrcaSlicer.dll` and `Uninstall.exe` at the root of the artifact ZIP, where the metadata restrictions apply, and every other unsigned `.exe`, `.dll` and `.pyd` of the bundle under `third-party/`, at its path in the bundle. `scripts/restore-from-signpath.ps1` copies the signed files back over the originals. The portable ZIP, the MSIX package and the installer are all packed from that bundle; `cpack` is pointed at it with `CPACK_INSTALLED_DIRECTORIES` rather than re-running the install rules, which would take CPython and `uv.exe` from their unsigned sources again.
makensis generates the uninstaller while it compiles the installer, so it cannot be signed after the fact like the other files. Before the binaries are submitted, the workflow builds the installer once to export its uninstaller, and the final `cpack` embeds the signed copy in its place, through the hooks in `src/dev-utils/platform/msw/uninstaller-signing.nsh`. Both builds pack the same files with the same script, so the uninstaller is the same one the final installer would have generated. After the final `cpack`, the workflow installs the installer silently into a scratch directory on the runner and verifies that every binary the installation puts on disk, the uninstaller included, carries a valid signature.
## `windows-installer-v1`
Signs the NSIS installer `cpack` produces, `OrcaSlicer_Windows_Installer_<version>_<arch>.exe`, uploaded as the only file of its artifact.
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<artifact-configuration xmlns="http://signpath.io/artifact-configuration/v1">
<parameters>
<parameter name="version" required="true" />
</parameters>
<zip-file>
<pe-file-set product-name="OrcaSlicer" product-version="${version}">
<include path="orca-slicer.exe" />
<include path="OrcaSlicer.dll" />
<include path="Uninstall.exe" />
<for-each>
<authenticode-sign />
</for-each>
</pe-file-set>
<directory path="third-party">
<pe-file-set>
<include path="**/*.exe" min-matches="0" max-matches="unbounded" />
<include path="**/*.dll" min-matches="0" max-matches="unbounded" />
<include path="**/*.pyd" min-matches="0" max-matches="unbounded" />
<for-each>
<authenticode-sign />
</for-each>
</pe-file-set>
</directory>
</zip-file>
</artifact-configuration>
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<artifact-configuration xmlns="http://signpath.io/artifact-configuration/v1">
<parameters>
<parameter name="version" required="true" />
</parameters>
<zip-file>
<pe-file path="OrcaSlicer_Windows_Installer_*.exe" product-name="OrcaSlicer" product-version="${version}">
<authenticode-sign />
</pe-file>
</zip-file>
</artifact-configuration>
+6 -2
View File
@@ -140,7 +140,11 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/dev-utils/platform/msw/OrcaSlicer.rc.
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/dev-utils/platform/msw/OrcaSlicer.manifest.in ${CMAKE_CURRENT_BINARY_DIR}/OrcaSlicer.manifest @ONLY)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/dev-utils/platform/osx/Info.plist.in ${CMAKE_CURRENT_BINARY_DIR}/Info.plist @ONLY)
if (WIN32)
add_library(OrcaSlicer SHARED OrcaSlicer.cpp OrcaSlicer.hpp)
foreach (_orca_rc_binary IN ITEMS orca-slicer.exe OrcaSlicer.dll)
set(ORCA_RC_ORIGINAL_FILENAME ${_orca_rc_binary})
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/dev-utils/platform/msw/OrcaSlicer-version.rc.in ${CMAKE_CURRENT_BINARY_DIR}/${_orca_rc_binary}.version.rc @ONLY)
endforeach ()
add_library(OrcaSlicer SHARED OrcaSlicer.cpp OrcaSlicer.hpp ${CMAKE_CURRENT_BINARY_DIR}/OrcaSlicer.dll.version.rc)
else ()
add_executable(OrcaSlicer OrcaSlicer.cpp OrcaSlicer.hpp)
endif ()
@@ -216,7 +220,7 @@ if (WIN32)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -municode")
endif()
add_executable(OrcaSlicer_app_gui WIN32 OrcaSlicer_app_msvc.cpp ${CMAKE_CURRENT_BINARY_DIR}/OrcaSlicer.rc)
add_executable(OrcaSlicer_app_gui WIN32 OrcaSlicer_app_msvc.cpp ${CMAKE_CURRENT_BINARY_DIR}/OrcaSlicer.rc ${CMAKE_CURRENT_BINARY_DIR}/orca-slicer.exe.version.rc)
# Generate debug symbols even in release mode.
if(MSVC)
target_link_options(OrcaSlicer_app_gui PUBLIC "$<$<CONFIG:RELEASE>:/DEBUG>")
-16
View File
@@ -1126,19 +1126,6 @@ static void load_downward_settings_list_from_config(std::string config_file, std
}
}
#ifdef __WXGTK__
// Virtual GPUs (VirtualBox, VMware, QEMU) cannot back WebKit's hardware DMABUF renderer.
static bool is_hypervisor_guest()
{
std::ifstream cpuinfo("/proc/cpuinfo");
std::string line;
while (std::getline(cpuinfo, line))
if (boost::starts_with(line, "flags"))
return line.find(" hypervisor") != std::string::npos;
return false;
}
#endif
int CLI::run(int argc, char **argv)
{
// Mark the main thread for the debugger and for runtime checks.
@@ -1189,9 +1176,6 @@ int CLI::run(int argc, char **argv)
::setenv("WEBKIT_FORCE_DMABUF_RENDERER", "1", /* replace */ false);
}
if (is_hypervisor_guest() && WebView::SupportsForceSharedMemory())
::setenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM", "1", /* replace */ false);
if (x11_opt_in) {
// ===== X11 / XWayland (opted in via GDK_BACKEND=x11) =====
BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds.";
@@ -0,0 +1,26 @@
// Version information of orca-slicer.exe and OrcaSlicer.dll, configured once per binary.
// SignPath only signs a binary whose ProductName and ProductVersion match its artifact
// configuration (signpath/windows-binaries-v1.xml), which the workflow passes the
// SoftFever_VERSION from version.inc.
1 VERSIONINFO
FILEVERSION @ORCA_VERSION_MAJOR@,@ORCA_VERSION_MINOR@,@ORCA_VERSION_PATCH@,0
PRODUCTVERSION @ORCA_VERSION_MAJOR@,@ORCA_VERSION_MINOR@,@ORCA_VERSION_PATCH@,0
{
BLOCK "StringFileInfo"
{
BLOCK "040904E4"
{
VALUE "CompanyName", "SoftFever"
VALUE "FileDescription", "@SLIC3R_APP_NAME@"
VALUE "FileVersion", "@SoftFever_VERSION@"
VALUE "ProductName", "@SLIC3R_APP_NAME@"
VALUE "ProductVersion", "@SoftFever_VERSION@"
VALUE "InternalName", "@SLIC3R_APP_NAME@"
VALUE "OriginalFilename", "@ORCA_RC_ORIGINAL_FILENAME@"
}
}
BLOCK "VarFileInfo"
{
VALUE "Translation", 0x409, 1252
}
}
+1 -24
View File
@@ -1,25 +1,2 @@
1 VERSIONINFO
FILEVERSION @SLIC3R_VERSION@
PRODUCTVERSION @SLIC3R_VERSION@
{
BLOCK "StringFileInfo"
{
BLOCK "040904E4"
{
VALUE "CompanyName", "SoftFever"
VALUE "FileDescription", "@SLIC3R_APP_NAME@"
VALUE "FileVersion", "@SLIC3R_BUILD_ID@"
VALUE "ProductName", "@SLIC3R_APP_NAME@"
VALUE "ProductVersion", "@SLIC3R_BUILD_ID@"
VALUE "InternalName", "@SLIC3R_APP_NAME@"
VALUE "LegalCopyright", ""
VALUE "OriginalFilename", "orca-slicer.exe"
}
}
BLOCK "VarFileInfo"
{
VALUE "Translation", 0x409, 1252
}
}
2 ICON "@SLIC3R_RESOURCES_DIR@/images/OrcaSlicer.ico"
1 24 "OrcaSlicer.manifest"
1 24 "OrcaSlicer.manifest"
@@ -0,0 +1,19 @@
; Lets the Windows release workflow ship a signed uninstaller (see signpath/README.md).
; makensis generates the uninstaller while it compiles the installer, so it cannot be
; signed afterwards like the installed binaries. Instead a first compile exports it
; (/DORCA_EXPORT_UNINSTALLER=<path>), the export is signed together with the binaries, and
; the final compile embeds the signed copy in place of the uninstaller it generates
; (/DORCA_SIGNED_UNINSTALLER=<path>). Both compiles run the same script over the same
; installed files, so they generate the same uninstaller. A failed copy aborts the
; compile rather than leave the uninstaller unsigned.
;
; cpack passes the paths with forward slashes, which cmd's copy does not accept for its
; source, so they are converted first.
!ifdef ORCA_EXPORT_UNINSTALLER
!searchreplace ORCA_EXPORT_UNINSTALLER_PATH "${ORCA_EXPORT_UNINSTALLER}" "/" "\"
!uninstfinalize 'cmd /c copy /y "%1" "${ORCA_EXPORT_UNINSTALLER_PATH}"' = 0
!endif
!ifdef ORCA_SIGNED_UNINSTALLER
!searchreplace ORCA_SIGNED_UNINSTALLER_PATH "${ORCA_SIGNED_UNINSTALLER}" "/" "\"
!uninstfinalize 'cmd /c copy /y "${ORCA_SIGNED_UNINSTALLER_PATH}" "%1"' = 0
!endif
+7 -3
View File
@@ -812,8 +812,7 @@ bool WallToolPaths::removeEmptyToolPaths(std::vector<VariableWidthLines> &toolpa
* Get the order constraints of the insets when printing walls per region / hole.
* Each returned pair consists of adjacent wall lines where the left has an inset_idx one lower than the right.
*
* Odd walls follow the same order as even ones, so that the centre line of a narrow wall is an inner wall
* like any other when printing inner to outer.
* Odd walls should always go after their enclosing wall polygons.
*
* \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one.
*/
@@ -887,7 +886,12 @@ WallToolPaths::ExtrusionLineSet WallToolPaths::getRegionOrder(const std::vector<
continue; // not directly adjacent
if (!shorter_then(loc_here - lineloc_nearby.j.p, (lineloc_here.j.w + lineloc_nearby.j.w) / 2 * diagonal_extension))
continue; // points are too far away from each other
if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) {
if (here->is_odd || nearby->is_odd) {
if (here->is_odd && !nearby->is_odd && nearby->inset_idx < here->inset_idx)
order_requirements.emplace(std::make_pair(nearby, here));
if (nearby->is_odd && !here->is_odd && here->inset_idx < nearby->inset_idx)
order_requirements.emplace(std::make_pair(here, nearby));
} else if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) {
order_requirements.emplace(std::make_pair(nearby, here));
} else {
assert((nearby->inset_idx > here->inset_idx) == outer_to_inner);
+9 -25
View File
@@ -122,8 +122,8 @@ void update_arrange_params(ArrangeParams& params, const DynamicPrintConfig* prin
void update_selected_items_inflation(ArrangePolygons& selected, const DynamicPrintConfig* print_cfg, ArrangeParams& params) {
// do not inflate brim_width. Objects are allowed to have overlapped brim.
// A brim reaching the printable area's edge is fine, so measure against the full bed.
BoundingBox bedbb = Polygon(get_bed_shape(*print_cfg)).bounding_box();
Points bedpts = get_shrink_bedpts(print_cfg, params);
BoundingBox bedbb = Polygon(bedpts).bounding_box();
// set obj distance for auto seq_print
if (params.is_seq_print) {
bool all_objects_are_short = std::all_of(selected.begin(), selected.end(), [&](ArrangePolygon& ap) { return ap.height < params.nozzle_height; });
@@ -144,30 +144,14 @@ void update_selected_items_inflation(ArrangePolygons& selected, const DynamicPri
// 3. otherwise, use each object's own brim width
ap.inflation = params.min_obj_distance != 0 ? params.min_obj_distance / 2 :
plate_has_tree_support ? scaled(brim_max / 2) : scaled(ap.brim_width);
if (params.min_obj_distance != 0)
return; // an explicit object distance is the user's call, not ours to shrink
// An item cannot be larger than the printable area, inflation included. Inflating
// an almost bed filling item would leave it too big to ever be placed.
BoundingBox apbb = ap.poly.contour.bounding_box();
auto diffx = bedbb.size().x() - apbb.size().x();
auto diffy = bedbb.size().y() - apbb.size().y();
ap.inflation = std::max<coord_t>(0, std::min({ ap.inflation, diffx / 2, diffy / 2 }));
});
// Give the bed margin, which the arrange bed keeps from the printable area, back
// where the largest item needs it. The skirt keeps its room: pushing a skirt off
// the bed is worse than saying the item does not fit.
if (!selected.empty()) {
coord_t max_width = 0, max_height = 0;
for (const ArrangePolygon &ap : selected) {
BoundingBox apbb = ap.poly.contour.bounding_box();
max_width = std::max(max_width, apbb.size().x() + 2 * ap.inflation);
max_height = std::max(max_height, apbb.size().y() + 2 * ap.inflation);
BoundingBox apbb = ap.poly.contour.bounding_box();
auto diffx = bedbb.size().x() - apbb.size().x() - 5;
auto diffy = bedbb.size().y() - apbb.size().y() - 5;
if (diffx > 0 && diffy > 0) {
auto min_diff = std::min(diffx, diffy);
ap.inflation = std::min(min_diff / 2, ap.inflation);
}
auto margin = [](coord_t bed, coord_t item) { return std::max(0., unscale<double>(bed - item) / 2.); };
params.bed_shrink_x = std::min<double>(params.bed_shrink_x, std::max<double>(params.brim_skirt_distance, margin(bedbb.size().x(), max_width)));
params.bed_shrink_y = std::min<double>(params.bed_shrink_y, std::max<double>(params.brim_skirt_distance, margin(bedbb.size().y(), max_height)));
}
});
}
void update_unselected_items_inflation(ArrangePolygons& unselected, const DynamicPrintConfig* print_cfg, const ArrangeParams& params)
+2 -5
View File
@@ -182,8 +182,8 @@ set(lisbslic3r_sources
Fill/FillPlanePath.hpp
Fill/FillRectilinear.cpp
Fill/FillRectilinear.hpp
Fill/AdaptiveInfillField.cpp
Fill/AdaptiveInfillField.hpp
Fill/FillTpmsAdaptive.cpp
Fill/FillTpmsAdaptive.hpp
Fill/FillTpmsD.cpp
Fill/FillTpmsD.hpp
Fill/FillTpmsFK.cpp
@@ -666,9 +666,6 @@ endif ()
encoding_check(libslic3r)
target_compile_definitions(libslic3r PUBLIC -DUSE_TBB -DTBB_USE_CAPTURED_EXCEPTION=0)
# CGAL changes Kd_tree's member layout when TBB is enabled. Match libslic3r_cgal
# in every consumer so inline constructors/destructors cannot mix the two layouts.
target_compile_definitions(libslic3r PUBLIC CGAL_LINKED_WITH_TBB)
if (USE_SLIC3R_CONSOLE_LOG)
target_compile_definitions(libslic3r PRIVATE $<$<CONFIG:RelWithDebInfo>:SLIC3R_CONSOLE_LOG>)
endif()
+1 -1
View File
@@ -1432,7 +1432,7 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo
}
if (!header_found) {
// TRN %1% is the application name.
std::string error_message = format(_u8L("Not a G-code file generated by %1%."), SLIC3R_APP_FULL_NAME);
std::string error_message = format(_u8L("Not a gcode file generated by %1%."), SLIC3R_APP_FULL_NAME);
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << error_message;
throw Slic3r::RuntimeError(error_message.c_str());
}
+16 -41
View File
@@ -377,8 +377,7 @@ public:
virtual void set(const ConfigOption* rhs, size_t start, size_t len) = 0;
virtual void set_with_restore(const ConfigOptionVectorBase* rhs, std::vector<int>& restore_index, int stride) = 0;
virtual void set_with_restore_2(const ConfigOptionVectorBase* rhs, std::vector<int>& restore_index, int start, int len, bool skip_error = false) = 0;
// keep_nil: copy a nil column from rhs as well, for options whose nil means "disabled" rather than "inherit".
virtual void set_only_diff(const ConfigOptionVectorBase* rhs, std::vector<int>& diff_index, int stride, bool keep_nil = false) = 0;
virtual void set_only_diff(const ConfigOptionVectorBase* rhs, std::vector<int>& diff_index, int stride) = 0;
virtual void set_to_index(const ConfigOptionVectorBase* rhs, std::vector<int>& dest_index, int stride) = 0;
virtual void set_with_nil(const ConfigOptionVectorBase* rhs, const ConfigOptionVectorBase* inherits, int stride) = 0;
// Resize the vector of values, copy the newly added values from opt_default if provided.
@@ -581,7 +580,7 @@ public:
//set a item related with extruder variants when loading user config, only set the different value of some extruder
//rhs: item from user config
//diff_index: which index in this vector need to be set
virtual void set_only_diff(const ConfigOptionVectorBase* rhs, std::vector<int>& diff_index, int stride, bool keep_nil) override
virtual void set_only_diff(const ConfigOptionVectorBase* rhs, std::vector<int>& diff_index, int stride) override
{
if (rhs->type() == this->type()) {
// Assign the first value of the rhs vector.
@@ -593,7 +592,7 @@ public:
for (size_t i = 0; i < diff_index.size(); i++) {
if (diff_index[i] != -1) {
for (size_t j = 0; j < stride; j++) {
if (keep_nil || !other->is_nil(diff_index[i] * stride))
if (!other->is_nil(diff_index[i] * stride))
this->values[i * stride + j] = other->values[diff_index[i] * stride + j];
}
}
@@ -842,12 +841,8 @@ public:
{
UNUSED(append);
std::istringstream iss(str);
double value;
iss >> value;
if (iss.fail())
return false;
this->value = value;
return true;
iss >> this->value;
return !iss.fail();
}
bool operator==(const ConfigOption &rhs) const override
@@ -1038,12 +1033,8 @@ public:
{
UNUSED(append);
std::istringstream iss(str);
int value;
iss >> value;
if (iss.fail())
return false;
this->value = value;
return true;
iss >> this->value;
return !iss.fail();
}
ConfigOptionInt& operator=(const ConfigOption *opt)
@@ -1261,12 +1252,8 @@ public:
UNUSED(append);
// don't try to parse the trailing % since it's optional
std::istringstream iss(str);
double value;
iss >> value;
if (iss.fail())
return false;
this->value = value;
return true;
iss >> this->value;
return !iss.fail();
}
private:
@@ -1378,14 +1365,10 @@ public:
bool deserialize(const std::string &str, bool append = false) override
{
UNUSED(append);
std::istringstream iss(str);
double value;
iss >> value;
if (iss.fail())
return false;
this->value = value;
this->percent = str.find_first_of("%") != std::string::npos;
return true;
std::istringstream iss(str);
iss >> this->value;
return !iss.fail();
}
private:
@@ -1555,12 +1538,8 @@ public:
{
UNUSED(append);
char dummy;
Vec2d value;
if (sscanf(str.data(), " %lf , %lf %c", &value(0), &value(1), &dummy) != 2 &&
sscanf(str.data(), " %lf x %lf %c", &value(0), &value(1), &dummy) != 2)
return false;
this->value = value;
return true;
return sscanf(str.data(), " %lf , %lf %c", &this->value(0), &this->value(1), &dummy) == 2 ||
sscanf(str.data(), " %lf x %lf %c", &this->value(0), &this->value(1), &dummy) == 2;
}
private:
@@ -1679,12 +1658,8 @@ public:
{
UNUSED(append);
char dummy;
Vec3d value;
if (sscanf(str.data(), " %lf , %lf , %lf %c", &value(0), &value(1), &value(2), &dummy) != 3 &&
sscanf(str.data(), " %lf x %lf x %lf %c", &value(0), &value(1), &value(2), &dummy) != 3)
return false;
this->value = value;
return true;
return sscanf(str.data(), " %lf , %lf , %lf %c", &this->value(0), &this->value(1), &this->value(2), &dummy) == 3 ||
sscanf(str.data(), " %lf x %lf x %lf %c", &this->value(0), &this->value(1), &this->value(2), &dummy) == 3;
}
private:
+1 -1
View File
@@ -616,7 +616,7 @@ struct PropagateDanielssonSingleStep {
if ((signs[addr] & 2) == 0) {
float *v = &L[addr << 1];
float l = v[0] * v[0] + v[1] * v[1];
float *v2s = v + 2 * addr_delta;
float *v2s = v + (addr_delta << 1);
float v2[2] = {
v2s[0] + INCX * resolution,
v2s[1] + INCY * resolution
+23 -23
View File
@@ -312,9 +312,9 @@ struct SurfaceFillParams
bool gyroid_optimized = false;
// For TPMS: grade the density from the surface to the interior of the object.
AdaptiveInfillMode adaptive_infill_mode = AdaptiveInfillMode::Disabled;
float adaptive_infill_interior_density = 0.f;
AdaptiveInfillGradient adaptive_infill_gradient = AdaptiveInfillGradient::Linear;
TpmsAdaptiveMode tpms_adaptive = TpmsAdaptiveMode::Disabled;
float tpms_interior_density = 0.f;
TpmsAdaptiveGradient tpms_adaptive_gradient = TpmsAdaptiveGradient::Linear;
// Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. };
@@ -358,9 +358,9 @@ struct SurfaceFillParams
RETURN_COMPARE_NON_EQUAL(skin_infill_depth);
RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
RETURN_COMPARE_NON_EQUAL(gyroid_optimized);
RETURN_COMPARE_NON_EQUAL(adaptive_infill_mode);
RETURN_COMPARE_NON_EQUAL(adaptive_infill_interior_density);
RETURN_COMPARE_NON_EQUAL(adaptive_infill_gradient);
RETURN_COMPARE_NON_EQUAL(tpms_adaptive);
RETURN_COMPARE_NON_EQUAL(tpms_interior_density);
RETURN_COMPARE_NON_EQUAL(tpms_adaptive_gradient);
RETURN_COMPARE_NON_EQUAL(smooth_factor);
RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern);
RETURN_COMPARE_NON_EQUAL(separated_infills);
@@ -394,9 +394,9 @@ struct SurfaceFillParams
this->center_of_surface_pattern == rhs.center_of_surface_pattern &&
this->separated_infills == rhs.separated_infills &&
this->gyroid_optimized == rhs.gyroid_optimized &&
this->adaptive_infill_mode == rhs.adaptive_infill_mode &&
this->adaptive_infill_interior_density == rhs.adaptive_infill_interior_density &&
this->adaptive_infill_gradient == rhs.adaptive_infill_gradient &&
this->tpms_adaptive == rhs.tpms_adaptive &&
this->tpms_interior_density == rhs.tpms_interior_density &&
this->tpms_adaptive_gradient == rhs.tpms_adaptive_gradient &&
this->smooth_factor == rhs.smooth_factor &&
this->fill_order == rhs.fill_order;
}
@@ -1010,17 +1010,17 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
params.extrusion_role != erTopSolidInfill && params.extrusion_role != erBottomSurface;
// Orca: only the TPMS sparse infill is graded; reset otherwise, as params is reused.
params.adaptive_infill_mode = is_adaptive_infill_pattern(params.pattern) && params.extrusion_role == erInternalInfill ?
region_config.adaptive_infill_mode.value : AdaptiveInfillMode::Disabled;
const bool adaptive_infill_mode = params.adaptive_infill_mode != AdaptiveInfillMode::Disabled;
params.adaptive_infill_interior_density = adaptive_infill_mode ? std::max(1.f, float(region_config.adaptive_infill_interior_density)) : 0.f;
params.adaptive_infill_gradient = adaptive_infill_mode ? region_config.adaptive_infill_gradient.value : AdaptiveInfillGradient::Linear;
params.tpms_adaptive = is_tpms_adaptive_pattern(params.pattern) && params.extrusion_role == erInternalInfill ?
region_config.tpms_adaptive.value : TpmsAdaptiveMode::Disabled;
const bool tpms_adaptive = params.tpms_adaptive != TpmsAdaptiveMode::Disabled;
params.tpms_interior_density = tpms_adaptive ? std::max(1.f, float(region_config.tpms_interior_density)) : 0.f;
params.tpms_adaptive_gradient = tpms_adaptive ? region_config.tpms_adaptive_gradient.value : TpmsAdaptiveGradient::Linear;
// Pass through gyroid_optimized only when the effective pattern is Gyroid,
// so non-Gyroid fills do not differ in SurfaceFillParams by an irrelevant flag
// (which would unnecessarily split fill batching). Adaptive density replaces it.
// Stored on SurfaceFillParams; copied to FillParams during conversion.
params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized && !adaptive_infill_mode;
params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized && !tpms_adaptive;
if (params.extrusion_role == erInternalInfill) {
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
@@ -1373,7 +1373,7 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
f->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle;
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
f->adaptive_infill_field = this->object()->adaptive_infill_field(surface_fill.params.adaptive_infill_mode);
f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive);
f->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config();
if (surface_fill.params.pattern == ipConcentricInternal) {
@@ -1423,9 +1423,9 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
params.adaptive_infill_mode = surface_fill.params.adaptive_infill_mode;
params.adaptive_infill_interior_density = float(0.01 * surface_fill.params.adaptive_infill_interior_density);
params.adaptive_infill_gradient = surface_fill.params.adaptive_infill_gradient;
params.tpms_adaptive = surface_fill.params.tpms_adaptive;
params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density);
params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient;
params.smooth_factor = surface_fill.params.smooth_factor;
// BBS
@@ -1598,7 +1598,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
f->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle;
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
f->adaptive_infill_field = this->object()->adaptive_infill_field(surface_fill.params.adaptive_infill_mode);
f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive);
f->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config();
@@ -1638,9 +1638,9 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.multiline = surface_fill.params.multiline;
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
params.adaptive_infill_mode = surface_fill.params.adaptive_infill_mode;
params.adaptive_infill_interior_density = float(0.01 * surface_fill.params.adaptive_infill_interior_density);
params.adaptive_infill_gradient = surface_fill.params.adaptive_infill_gradient;
params.tpms_adaptive = surface_fill.params.tpms_adaptive;
params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density);
params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient;
params.smooth_factor = surface_fill.params.smooth_factor;
// Orca: Match make_fills() when choosing the origin of plane-path patterns.
// Without the sparse extrusion role, the filler uses each surface's bounds
+13 -256
View File
@@ -11,10 +11,6 @@
#include <string>
#include <vector>
#include <utility>
#include <unordered_map>
#include <unordered_set>
#include <tuple>
#include "../AABBTreeLines.hpp"
#include "../ClipperUtils.hpp"
#include "../EdgeGrid.hpp"
#include "../Geometry.hpp"
@@ -210,9 +206,6 @@ void Fill::fill_surface_extrusion(const Surface* surface, const FillParams& para
thick_polylines.clear();
}
else {
// Orca: sparse infill lines whose ends are next to each other print as one path, without a travel between them.
if (params.extrusion_role == erInternalInfill && ! eec->no_sort)
join_close_ends(polylines, scale_(2. * this->spacing), params.can_reverse);
extrusion_entities_append_paths(
eec->entities, std::move(polylines),
params.extrusion_role,
@@ -1651,8 +1644,6 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
};
const double line_half_width = 0.5 * scale_(spacing);
// Two ends whose anchors would meet are joined instead, with about as much wall.
const double join_length = anchor_length > SCALED_EPSILON ? std::max(anchor_length_max, 2. * (anchor_length + line_half_width)) : anchor_length_max;
#if 0
// Connection from end of one infill line to the start of another infill line.
@@ -1729,10 +1720,6 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
}
#endif
// Line ends not yet joined to another line along the contour.
std::vector<char> open_end(graph.map_infill_end_point_to_boundary.size(), 1);
auto end_index = [&graph](const ContourIntersectionPoint *cp) { return size_t(cp - graph.map_infill_end_point_to_boundary.data()); };
struct Arc {
ContourIntersectionPoint *intersection;
double arc_length;
@@ -1744,81 +1731,6 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
if (cp.contour_idx != boundary_idx_unconnected && cp.next_on_contour != &cp && cp.could_connect_next())
arches.push_back({ &cp, path_length_along_contour_ccw(&cp, cp.next_on_contour, graph.boundary_params[cp.contour_idx].back()) });
std::sort(arches.begin(), arches.end(), [](const auto& l, const auto& r) { return l.arc_length < r.arc_length; });
// Lines the arcs would join in this order.
auto count_joins = [&](const std::vector<Arc> &order) {
std::vector<char> consumed(graph.map_infill_end_point_to_boundary.size());
for (size_t i = 0; i < consumed.size(); ++ i)
consumed[i] = graph.map_infill_end_point_to_boundary[i].consumed;
std::vector<size_t> parent(infill_ordered.size());
std::iota(parent.begin(), parent.end(), 0);
auto find = [&parent](size_t i) {
while (parent[i] != i)
i = parent[i] = parent[parent[i]];
return i;
};
size_t joins = 0;
for (const Arc &arc : order) {
const size_t end1 = end_index(arc.intersection), end2 = end_index(arc.intersection->next_on_contour);
if (consumed[end1] || consumed[end2] || (params.multiline > 1 && arc.arc_length < scale_(spacing) * params.multiline))
continue;
const size_t polyline1 = find(end1 / 2), polyline2 = find(end2 / 2);
if (polyline1 == polyline2)
continue;
if (arc.arc_length < join_length) {
parent[polyline2] = polyline1;
++ joins;
} else if (anchor_length <= SCALED_EPSILON)
continue;
consumed[end1] = consumed[end2] = 1;
}
// The last pass joins the open ends next to each other, also those of a multiline group.
for (size_t end = 0; end < consumed.size(); ++ end) {
const ContourIntersectionPoint &cp = graph.map_infill_end_point_to_boundary[end];
if (consumed[end] || cp.contour_idx == boundary_idx_unconnected)
continue;
const double length = graph.boundary_params[cp.contour_idx].back();
auto open = [&](const ContourIntersectionPoint *other, bool trimmed) {
return other != &cp && ! consumed[end_index(other)] && ! trimmed;
};
const double lprev = open(cp.prev_on_contour, cp.prev_trimmed || cp.prev_on_contour->next_trimmed) ?
path_length_along_contour_ccw(cp.prev_on_contour, &cp, length) : std::numeric_limits<double>::max();
const double lnext = open(cp.next_on_contour, cp.next_trimmed || cp.next_on_contour->prev_trimmed) ?
path_length_along_contour_ccw(&cp, cp.next_on_contour, length) : std::numeric_limits<double>::max();
consumed[end] = anchor_length > SCALED_EPSILON;
for (const double l : { std::min(lprev, lnext), std::max(lprev, lnext) }) {
if (l > join_length)
break;
const size_t other = end_index(l == lprev ? cp.prev_on_contour : cp.next_on_contour);
const size_t polyline1 = find(end / 2), polyline2 = find(other / 2);
if (polyline1 == polyline2)
continue;
parent[polyline2] = polyline1;
consumed[end] = consumed[other] = 1;
++ joins;
break;
}
}
return joins;
};
// Equal arcs taken along the contour pair a wall in one phase; keep the order that joins the most lines.
size_t best_joins = count_joins(arches);
const double bucket = 0.5 * scale_(spacing);
for (const bool backwards : { false, true }) {
std::vector<Arc> along_contour = arches;
std::sort(along_contour.begin(), along_contour.end(), [bucket, backwards](const Arc &l, const Arc &r) {
const long long bucket_l = std::llround(l.arc_length / bucket), bucket_r = std::llround(r.arc_length / bucket);
if (bucket_l != bucket_r)
return bucket_l < bucket_r;
if (l.intersection->contour_idx != r.intersection->contour_idx)
return l.intersection->contour_idx < r.intersection->contour_idx;
return backwards ? l.intersection->param > r.intersection->param : l.intersection->param < r.intersection->param;
});
if (const size_t joins = count_joins(along_contour); joins > best_joins) {
best_joins = joins;
arches = std::move(along_contour);
}
}
}
//FIXME improve the Traveling Salesman problem with 2-opt and 3-opt local optimization.
@@ -1840,7 +1752,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
if (polyline_idx1 != polyline_idx2) {
Polyline &polyline1 = infill_ordered[polyline_idx1];
Polyline &polyline2 = infill_ordered[polyline_idx2];
if (arc.arc_length < join_length) {
if (arc.arc_length < anchor_length_max) {
// Not closing a loop, connecting the lines.
assert(contour[cp1->point_idx] == polyline1.points.front() || contour[cp1->point_idx] == polyline1.points.back());
if (contour[cp1->point_idx] == polyline1.points.front())
@@ -1849,7 +1761,6 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
if (contour[cp2->point_idx] == polyline2.points.back())
polyline2.reverse();
take(polyline1, polyline2, contour, cp1, cp2, false);
open_end[end_index(cp1)] = open_end[end_index(cp2)] = 0;
// Mark the second polygon as merged with the first one.
if (polyline_idx2 < polyline_idx1) {
polyline2 = std::move(polyline1);
@@ -1867,23 +1778,6 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
}
}
// Distance along the contour to the nearest open end of another polyline, within 64 ends.
auto distance_to_open_end = [&](const ContourIntersectionPoint &from, size_t polyline_idx, bool backwards) {
const double length = graph.boundary_params[from.contour_idx].back();
double distance = 0.;
const ContourIntersectionPoint *cp = &from;
for (size_t step = 0; step < 64; ++ step) {
const ContourIntersectionPoint *other = backwards ? cp->prev_on_contour : cp->next_on_contour;
if (other == nullptr || other == &from)
break;
distance += backwards ? closed_contour_distance_cw(cp->param, other->param, length) : closed_contour_distance_ccw(cp->param, other->param, length);
if (open_end[end_index(other)] && get_and_update_merged_with(end_index(other) / 2) != polyline_idx)
return distance;
cp = other;
}
return std::numeric_limits<double>::max();
};
// Connect the remaining open infill lines to the perimeter lines if possible.
for (ContourIntersectionPoint &contour_point : graph.map_infill_end_point_to_boundary)
if (! contour_point.consumed && contour_point.contour_idx != boundary_idx_unconnected) {
@@ -1902,7 +1796,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
assert(contour[contour_point.point_idx] == polyline.points.front() || contour[contour_point.point_idx] == polyline.points.back());
bool connected = false;
for (double l : { std::min(lprev, lnext), std::max(lprev, lnext) }) {
if (l == std::numeric_limits<double>::max() || l > join_length)
if (l == std::numeric_limits<double>::max() || l > anchor_length_max)
break;
// Take the complete contour.
bool reversed = l == lprev;
@@ -1921,7 +1815,6 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
if (contour[cp2->point_idx] == polyline2.points.back())
polyline2.reverse();
take(polyline, polyline2, contour, &contour_point, cp2, reversed);
open_end[end_index(&contour_point)] = open_end[end_index(cp2)] = 0;
if (polyline_idx < polyline_idx2) {
// Mark the second polyline as merged with the first one.
merged_with[polyline_idx2] = polyline_idx;
@@ -1936,35 +1829,18 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
break;
}
if (! connected && anchor_length > SCALED_EPSILON) {
// Hook toward the nearest open end of another polyline, where the next path likely starts.
const double free_prev = contour_point.contour_not_taken_length_prev;
const double free_next = contour_point.contour_not_taken_length_next;
const double to_prev = distance_to_open_end(contour_point, polyline_idx, true);
const double to_next = distance_to_open_end(contour_point, polyline_idx, false);
bool backwards = free_prev > free_next;
if (to_prev < to_next && free_prev > SCALED_EPSILON)
backwards = true;
else if (to_next < to_prev && free_next > SCALED_EPSILON)
backwards = false;
// Orca: with multiline, a hook toward a copy stops short of it; the copy takes half a width over the sine of its wall angle.
double length = anchor_length;
if (const ContourIntersectionPoint *other = backwards ? contour_point.prev_on_contour : contour_point.next_on_contour;
params.multiline > 1 && other != &contour_point) {
const Point &end = contour[contour_point.point_idx];
Vec2d dir = Vec2d::Zero();
for (size_t i = 1; i < polyline.size() && dir.squaredNorm() < sqr(2. * scale_(spacing) * params.multiline); ++ i)
dir = (end - polyline.points[polyline.points.front() == end ? i : polyline.size() - 1 - i]).cast<double>();
const Vec2d to_copy = (contour[other->point_idx] - end).cast<double>();
const double across = dir.squaredNorm() > 0. ? std::abs(cross2(dir.normalized(), to_copy)) : 0.;
if (across > SCALED_EPSILON && across < scale_(spacing) * params.multiline) {
const double gap = backwards ? path_length_along_contour_ccw(other, &contour_point, contour_params.back()) :
path_length_along_contour_ccw(&contour_point, other, contour_params.back());
length = std::min(length, gap - line_half_width * (1. + to_copy.norm() / across));
}
// Which to take? One could optimize for:
// 1) Shortest path
// 2) Hook length
// ...
// Let's take the longer now, as this improves the chance of another hook to be placed on the other side of this contour point.
double l = std::max(contour_point.contour_not_taken_length_prev, contour_point.contour_not_taken_length_next);
if (l > SCALED_EPSILON) {
if (contour_point.contour_not_taken_length_prev > contour_point.contour_not_taken_length_next)
take_limited(polyline, contour, contour_params, &contour_point, contour_point.prev_on_contour, true, anchor_length, line_half_width);
else
take_limited(polyline, contour, contour_params, &contour_point, contour_point.next_on_contour, false, anchor_length, line_half_width);
}
if ((backwards ? free_prev : free_next) > SCALED_EPSILON)
take_limited(polyline, contour, contour_params, &contour_point, backwards ? contour_point.prev_on_contour : contour_point.next_on_contour,
backwards, std::max(0., length), line_half_width);
}
}
@@ -2922,123 +2798,4 @@ void multiline_fill(Polylines& polylines, const FillParams& params, float spacin
polylines = std::move(all_polylines);
}
void join_close_ends(Polylines &polylines, double max_gap, bool can_reverse)
{
const size_t n = polylines.size();
if (n < 2 || max_gap <= 0.)
return;
static constexpr size_t npos = size_t(-1);
// End 2 * i is the first point of polyline i, 2 * i + 1 its last.
auto end_point = [&polylines](size_t e) -> const Point & { return (e & 1) ? polylines[e >> 1].points.back() : polylines[e >> 1].points.front(); };
// Ends in cells of max_gap, so the ends within max_gap of one are in its cell or the eight around it.
auto cell_of = [max_gap](const Point &p) { return Vec2i64(int64_t(std::floor(p.x() / max_gap)), int64_t(std::floor(p.y() / max_gap))); };
auto key = [](const Vec2i64 &c) { return (uint64_t(c.x()) << 32) ^ uint64_t(uint32_t(c.y())); };
std::unordered_map<uint64_t, std::vector<size_t>> grid;
for (size_t e = 0; e < 2 * n; ++ e)
if (polylines[e >> 1].size() >= 2)
grid[key(cell_of(end_point(e)))].push_back(e);
// The end of a polyline that comes last along its overall direction, taken along the dominant axis.
std::vector<char> backwards(n, 0);
for (size_t i = 0; i < n; ++ i)
if (polylines[i].size() >= 2) {
const Vec2d dir = (polylines[i].points.back() - polylines[i].points.front()).cast<double>();
backwards[i] = std::abs(dir.x()) >= std::abs(dir.y()) ? dir.x() < 0. : dir.y() < 0.;
}
auto trailing = [&backwards](size_t e) { return bool(e & 1) != bool(backwards[e >> 1]); };
// Ends that meet first, then shortest gaps between leading ends, then the others: the copies of neighboring multiline
// groups are all joined at the same side, which leaves their open ends next to each other rather than on alternating walls.
std::vector<std::tuple<bool, bool, double, size_t, size_t>> gaps;
for (const auto &[cell_key, ends] : grid) {
const Vec2i64 c = cell_of(end_point(ends.front()));
for (int64_t dx = -1; dx <= 1; ++ dx)
for (int64_t dy = -1; dy <= 1; ++ dy)
if (auto it = grid.find(key(c + Vec2i64(dx, dy))); it != grid.end())
for (size_t a : ends)
for (size_t b : it->second)
if (a < b && (a >> 1) != (b >> 1) && (can_reverse || (a & 1) != (b & 1)))
if (const double d = (end_point(a) - end_point(b)).cast<double>().norm(); d <= max_gap)
gaps.emplace_back(d > SCALED_EPSILON, d > SCALED_EPSILON && (trailing(a) || trailing(b)), d, a, b);
}
if (gaps.empty())
return;
std::sort(gaps.begin(), gaps.end());
std::vector<size_t> partner(2 * n, npos), parent(n);
std::iota(parent.begin(), parent.end(), 0);
auto find = [&parent](size_t i) {
while (parent[i] != i)
i = parent[i] = parent[parent[i]];
return i;
};
// A join may not cross a line or another join; touching them at its own ends is fine, as where a line ends on another.
// Only the segments through a cell next to a candidate join can cross one.
std::unordered_set<uint64_t> near_gaps;
for (const auto &[apart, trail, d, a, b] : gaps)
for (size_t e : { a, b })
for (int64_t dx = -1; dx <= 1; ++ dx)
for (int64_t dy = -1; dy <= 1; ++ dy)
near_gaps.insert(key(cell_of(end_point(e)) + Vec2i64(dx, dy)));
Lines near_lines;
for (const Polyline &pl : polylines)
for (size_t i = 1; i < pl.size(); ++ i) {
const Vec2i64 c0 = cell_of(pl.points[i - 1]), c1 = cell_of(pl.points[i]);
const Vec2i64 lo = c0.cwiseMin(c1), hi = c0.cwiseMax(c1);
bool near = (hi - lo).maxCoeff() > 3;
for (int64_t x = lo.x(); x <= hi.x() && ! near; ++ x)
for (int64_t y = lo.y(); y <= hi.y() && ! near; ++ y)
near = near_gaps.count(key(Vec2i64(x, y))) > 0;
if (near)
near_lines.emplace_back(pl.points[i - 1], pl.points[i]);
}
const AABBTreeLines::LinesDistancer<Line> lines(std::move(near_lines));
auto crosses = [&](size_t a, size_t b) {
const Vec2d p = end_point(a).cast<double>(), q = end_point(b).cast<double>();
const double length = (q - p).norm();
if (length <= 2. * SCALED_EPSILON)
return false;
const Vec2d dir = (q - p) / length;
const Line inner(Point((p + SCALED_EPSILON * dir).cast<coord_t>()), Point((q - SCALED_EPSILON * dir).cast<coord_t>()));
if (! lines.get_lines().empty() && ! lines.intersections_with_line<false>(inner).empty())
return true;
const Vec2i64 c = cell_of(end_point(a));
for (int64_t dx = -3; dx <= 3; ++ dx)
for (int64_t dy = -3; dy <= 3; ++ dy)
if (auto it = grid.find(key(c + Vec2i64(dx, dy))); it != grid.end())
for (size_t e : it->second)
if (partner[e] != npos && Geometry::segments_intersect(inner.a, inner.b, end_point(e), end_point(partner[e])))
return true;
return false;
};
for (const auto &[apart, trail, d, a, b] : gaps)
if (partner[a] == npos && partner[b] == npos && find(a >> 1) != find(b >> 1) && ! crosses(a, b)) {
partner[a] = b;
partner[b] = a;
parent[find(a >> 1)] = find(b >> 1);
}
// Walk each group from a polyline with a free end, entering the next polyline at the end joined to the last one.
Polylines out;
std::vector<char> done(n, 0);
auto walk = [&](size_t first_end) {
Polyline joined;
for (size_t e = first_end; e != npos;) {
Polyline &pl = polylines[e >> 1];
done[e >> 1] = 1;
if (e & 1)
pl.reverse();
joined.points.insert(joined.points.end(), pl.points.begin() + (! joined.empty() && joined.points.back() == pl.points.front()), pl.points.end());
e = partner[e ^ 1];
}
out.emplace_back(std::move(joined));
};
for (size_t i = 0; i < n; ++ i)
if (! done[i] && partner[2 * i] == npos)
walk(2 * i);
for (size_t i = 0; i < n; ++ i)
if (! done[i] && partner[2 * i + 1] == npos)
walk(2 * i + 1);
polylines = std::move(out);
}
} // namespace Slic3r
+6 -9
View File
@@ -33,7 +33,7 @@ namespace Slic3r { class ExtrusionEntityCollection; }
namespace Slic3r {
class Surface;
class AdaptiveInfillField;
class TpmsRadialField;
enum InfillPattern : int;
namespace FillAdaptive {
@@ -93,9 +93,9 @@ struct FillParams
bool gyroid_optimized { false };
// For TPMS: grade the density from the surface to the interior of the object. Density fraction.
AdaptiveInfillMode adaptive_infill_mode { AdaptiveInfillMode::Disabled };
float adaptive_infill_interior_density { 0.f };
AdaptiveInfillGradient adaptive_infill_gradient { AdaptiveInfillGradient::Linear };
TpmsAdaptiveMode tpms_adaptive { TpmsAdaptiveMode::Disabled };
float tpms_interior_density { 0.f };
TpmsAdaptiveGradient tpms_adaptive_gradient { TpmsAdaptiveGradient::Linear };
// Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. };
@@ -162,7 +162,7 @@ public:
FillAdaptive::Octree* adapt_fill_octree = nullptr;
// Radial coordinate inside the object for the adaptive TPMS infill
const AdaptiveInfillField* adaptive_infill_field = nullptr;
const TpmsRadialField* tpms_radial_field = nullptr;
// PrintConfig and PrintObjectConfig are used by infills that use Arachne (Concentric and FillEnsuring).
// Orca: also used by gap fill function.
@@ -258,11 +258,8 @@ public:
static coord_t _adjust_solid_spacing(const coord_t width, const coord_t distance);
};
//Fill Multiline
//Fill Multiline
void multiline_fill(Polylines& polylines, const FillParams& params, float spacing);
// Joins polylines whose ends are at most max_gap apart, shortest gaps first and without closing a loop, so each such
// group prints as one path. Without can_reverse only the end of one polyline joins the start of another.
void join_close_ends(Polylines &polylines, double max_gap, bool can_reverse);
} // namespace Slic3r
#endif // slic3r_FillBase_hpp_
+6 -6
View File
@@ -18,7 +18,7 @@
#include "FillGyroid.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "AdaptiveInfillField.hpp"
#include "FillTpmsAdaptive.hpp"
namespace Slic3r {
@@ -315,8 +315,8 @@ void FillGyroid::_fill_surface_single(
ExPolygon expolygon,
Polylines &polylines_out)
{
if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
[&](const FillParams &shell_params, const ExPolygon &shell) {
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
});
@@ -342,11 +342,11 @@ void FillGyroid::_fill_surface_single(
// generate pattern
Polylines polylines;
if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
// Radians per mm of the regular pattern at a density.
auto frequency = [&params, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.adaptive_infill_interior_density), params.adaptive_infill_gradient},
*this->adaptive_infill_field, bb, this->z, params.layer_height, this->spacing, infill_angle);
polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient},
*this->tpms_radial_field, bb, this->z, params.layer_height, this->spacing, infill_angle);
} else if (params.gyroid_optimized) {
// Marching-squares path on the gyroid implicit field. Base period matches
// the standard parametric path's wavelength: 2*pi * spacing / density_adj.
@@ -1,4 +1,4 @@
#include "AdaptiveInfillField.hpp"
#include "FillTpmsAdaptive.hpp"
#include <algorithm>
#include <array>
@@ -146,11 +146,11 @@ void rasterize(const ExPolygons &expolygons, const Vec2d &origin, double cell, i
}
// Scale of the pattern relative to the surface at a depth from 0 at the surface to 1 at the deepest point.
double target_scale(double ratio, AdaptiveInfillGradient gradient, double depth)
double target_scale(double ratio, TpmsAdaptiveGradient gradient, double depth)
{
switch (gradient) {
case AdaptiveInfillGradient::Quadratic: return 1. + (ratio - 1.) * depth * depth;
case AdaptiveInfillGradient::Exponential: return std::pow(ratio, depth);
case TpmsAdaptiveGradient::Quadratic: return 1. + (ratio - 1.) * depth * depth;
case TpmsAdaptiveGradient::Exponential: return std::pow(ratio, depth);
default: return 1. + (ratio - 1.) * depth;
}
}
@@ -159,7 +159,7 @@ double target_scale(double ratio, AdaptiveInfillGradient gradient, double depth)
// continuous level of the target of a depth.
struct DensityLevels
{
DensityLevels(double ratio, double step, AdaptiveInfillGradient gradient)
DensityLevels(double ratio, double step, TpmsAdaptiveGradient gradient)
: ratio(ratio), gradient(gradient), count(int(std::ceil(std::abs(std::log(ratio)) / std::log(step) - EPSILON)))
{}
@@ -170,7 +170,7 @@ struct DensityLevels
}
double ratio;
AdaptiveInfillGradient gradient;
TpmsAdaptiveGradient gradient;
int count;
};
@@ -212,12 +212,12 @@ private:
} // namespace
AdaptiveInfillField::AdaptiveInfillField(const std::vector<Slice> &slices, const BoundingBox &bbox, AdaptiveInfillMode mode,
TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode,
const std::function<void()> &throw_if_canceled)
: m_mode(mode)
, m_axis(mode == AdaptiveInfillMode::NormalX ? 0 : mode == AdaptiveInfillMode::NormalY ? 1 : mode == AdaptiveInfillMode::NormalZ ? 2 : -1)
, m_axis(mode == TpmsAdaptiveMode::NormalX ? 0 : mode == TpmsAdaptiveMode::NormalY ? 1 : mode == TpmsAdaptiveMode::NormalZ ? 2 : -1)
{
assert(!slices.empty() && mode != AdaptiveInfillMode::Disabled);
assert(!slices.empty() && mode != TpmsAdaptiveMode::Disabled);
const Vec3d min(unscaled(bbox.min.x()), unscaled(bbox.min.y()), slices.front().bottom_z);
const Vec3d extent = Vec3d(unscaled(bbox.max.x()), unscaled(bbox.max.y()), slices.back().top_z) - min;
@@ -269,7 +269,7 @@ AdaptiveInfillField::AdaptiveInfillField(const std::vector<Slice> &slices, const
// Bodies are the connected inside nodes, none of which is on the border. The deepest nodes of a body are the
// centers of its lobes, unless the depth between them stays above NeckRatio; where the depth ties, the center
// is the node nearest to the middle of the tied nodes.
const bool lobes = m_mode != AdaptiveInfillMode::SteppedShells && m_mode != AdaptiveInfillMode::SmoothBlend;
const bool lobes = m_mode != TpmsAdaptiveMode::SteppedShells && m_mode != TpmsAdaptiveMode::SmoothBlend;
m_body.assign(depth.size(), -1);
std::vector<size_t> body_nodes;
for (size_t seed = 0; seed < depth.size(); ++seed) {
@@ -341,7 +341,7 @@ AdaptiveInfillField::AdaptiveInfillField(const std::vector<Slice> &slices, const
}
throw_if_canceled();
if (m_mode == AdaptiveInfillMode::SteppedShells || m_mode == AdaptiveInfillMode::SmoothBlend || m_mode == AdaptiveInfillMode::DistanceWarp) {
if (m_mode == TpmsAdaptiveMode::SteppedShells || m_mode == TpmsAdaptiveMode::SmoothBlend || m_mode == TpmsAdaptiveMode::DistanceWarp) {
m_depth.assign(depth.size(), 0.f);
for (size_t i = 0; i < depth.size(); ++i)
if (m_body[i] >= 0)
@@ -421,7 +421,7 @@ AdaptiveInfillField::AdaptiveInfillField(const std::vector<Slice> &slices, const
lobe.reach.resize(log_reach.size());
std::transform(log_reach.begin(), log_reach.end(), lobe.reach.begin(), [](double v) { return float(std::exp(v)); });
if (m_mode == AdaptiveInfillMode::DistanceWarp) {
if (m_mode == TpmsAdaptiveMode::DistanceWarp) {
std::vector<double> mean(log_reach.size() * (Samples + 1));
for (size_t d = 0; d < log_reach.size(); ++d) {
const Vec3d dir = direction(int(d) / Azimuth, int(d) % Azimuth);
@@ -488,7 +488,7 @@ AdaptiveInfillField::AdaptiveInfillField(const std::vector<Slice> &slices, const
}
}
double AdaptiveInfillField::depth(const Vec3d &pt) const
double TpmsRadialField::depth(const Vec3d &pt) const
{
assert(!m_depth.empty());
const Vec3d f = (pt - m_origin) / m_cell;
@@ -508,7 +508,7 @@ double AdaptiveInfillField::depth(const Vec3d &pt) const
return value;
}
Vec3d AdaptiveInfillField::offset(const Vec3d &pt, const Vec3d &center) const
Vec3d TpmsRadialField::offset(const Vec3d &pt, const Vec3d &center) const
{
Vec3d d = pt - center;
if (m_axis >= 0)
@@ -516,7 +516,7 @@ Vec3d AdaptiveInfillField::offset(const Vec3d &pt, const Vec3d &center) const
return d;
}
double AdaptiveInfillField::radial(const Lobe &lobe, const Vec3d &pt) const
double TpmsRadialField::radial(const Lobe &lobe, const Vec3d &pt) const
{
const Vec3d d = this->offset(pt, lobe.center);
const double r = d.norm();
@@ -562,7 +562,7 @@ double AdaptiveInfillField::radial(const Lobe &lobe, const Vec3d &pt) const
return warp(mean);
}
size_t AdaptiveInfillField::radial(const Vec3d &pt, Radials &out) const
size_t TpmsRadialField::radial(const Vec3d &pt, Radials &out) const
{
assert(!this->empty());
Vec3i32 idx;
@@ -588,7 +588,7 @@ size_t AdaptiveInfillField::radial(const Vec3d &pt, Radials &out) const
return count;
}
size_t AdaptiveInfillField::body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const
size_t TpmsRadialField::body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const
{
const Body &body = m_bodies[m_body[node]];
if (body.lobes == 1) {
@@ -641,7 +641,7 @@ struct AdaptiveTpmsField
const long gsize = std::lround(gsizef / rsizef);
const AdaptiveTpms &tpms;
const AdaptiveInfillField &radial_field;
const TpmsRadialField &radial_field;
RadialScale scale;
DensityLevels levels;
Point size;
@@ -650,7 +650,7 @@ struct AdaptiveTpmsField
double cos_angle;
double sin_angle;
AdaptiveTpmsField(const AdaptiveTpms &tpms, const AdaptiveInfillField &radial_field, const BoundingBox &bbox, coordf_t z, float angle)
AdaptiveTpmsField(const AdaptiveTpms &tpms, const TpmsRadialField &radial_field, const BoundingBox &bbox, coordf_t z, float angle)
: tpms(tpms), radial_field(radial_field), scale(tpms, radial_field.axis() < 0 ? 3 : 2)
, levels(std::max(tpms.interior_frequency / tpms.surface_frequency, 1e-3), BlendRatio, tpms.gradient)
, size(bbox.size()), offs(bbox.min), z(z)
@@ -666,7 +666,7 @@ struct AdaptiveTpmsField
const double x = unscaled(pt.x());
const double y = unscaled(pt.y());
const Vec3d obj(cos_angle * x - sin_angle * y, sin_angle * x + cos_angle * y, z);
if (radial_field.mode() == AdaptiveInfillMode::SmoothBlend) {
if (radial_field.mode() == TpmsAdaptiveMode::SmoothBlend) {
// The regular patterns of the two levels around the target of the depth, blended by a smoothstep.
auto lattice = [this, x, y](int level) {
const double frequency = tpms.surface_frequency * levels.scale(level);
@@ -681,7 +681,7 @@ struct AdaptiveTpmsField
return float((w < 1. ? (1. - w) * lattice(k) : 0.) + (w > 0. ? w * lattice(k + 1) : 0.));
}
const int axis = radial_field.axis();
AdaptiveInfillField::Radials radials;
TpmsRadialField::Radials radials;
const size_t count = radial_field.radial(obj, radials);
float value = 0.f;
for (size_t i = 0; i < count; ++i) {
@@ -708,20 +708,20 @@ template<> struct _RasterTraits<AdaptiveTpmsField>
};
// Continuous density level of the depth over a layer, in the object frame.
struct AdaptiveLevelField
struct TpmsLevelField
{
static constexpr float gsizef = 0.5f;
static constexpr float rsizef = 0.05f;
const coord_t rsize = scaled(rsizef);
const long gsize = std::lround(gsizef / rsizef);
const AdaptiveInfillField &field;
const TpmsRadialField &field;
const DensityLevels &levels;
Point size;
Point offs;
double z;
AdaptiveLevelField(const AdaptiveInfillField &field, const DensityLevels &levels, const BoundingBox &bbox, coordf_t z)
TpmsLevelField(const TpmsRadialField &field, const DensityLevels &levels, const BoundingBox &bbox, coordf_t z)
: field(field), levels(levels), size(bbox.size()), offs(bbox.min), z(z)
{}
@@ -736,19 +736,19 @@ struct AdaptiveLevelField
inline Point to_Point(const Coord &p) const { return Point(to_coord(p.c) + offs.x(), to_coord(p.r) + offs.y()); }
};
template<> struct _RasterTraits<AdaptiveLevelField>
template<> struct _RasterTraits<TpmsLevelField>
{
using ValueType = float;
static float get(const AdaptiveLevelField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); }
static size_t rows(const AdaptiveLevelField &sf) { return sf.to_coordr(sf.size.y()); }
static size_t cols(const AdaptiveLevelField &sf) { return sf.to_coordr(sf.size.x()); }
static float get(const TpmsLevelField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); }
static size_t rows(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.y()); }
static size_t cols(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.x()); }
};
} // namespace marchsq
namespace Slic3r {
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const AdaptiveInfillField &field, BoundingBox bbox,
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox,
coordf_t z, coordf_t layer_height, coordf_t spacing, float angle)
{
// A cell of margin for the rings closed along the raster border, and a fixed sampling grid for every region.
@@ -775,21 +775,21 @@ Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const AdaptiveInfillField
return polylines;
}
std::vector<AdaptiveInfillShell> make_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z,
float surface_density, float interior_density, AdaptiveInfillGradient gradient)
std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z,
float surface_density, float interior_density, TpmsAdaptiveGradient gradient)
{
const DensityLevels levels(interior_density / surface_density, ShellRatio, gradient);
if (levels.count == 0)
return {{surface_density, {expolygon}}};
// A fixed sampling grid, so that every region of a layer gets the same shells.
const coord_t cell = scaled(marchsq::AdaptiveLevelField::gsizef);
const coord_t cell = scaled(marchsq::TpmsLevelField::gsizef);
BoundingBox bbox = get_extents(expolygon);
bbox.offset(cell);
bbox.merge(align_to_grid(bbox.min, Point(cell, cell)));
const marchsq::AdaptiveLevelField raster(field, levels, bbox, z);
const marchsq::TpmsLevelField raster(field, levels, bbox, z);
// Each level takes the part deeper than the middle between it and the previous one.
std::vector<AdaptiveInfillShell> shells;
std::vector<TpmsShell> shells;
ExPolygons remaining{expolygon};
for (int level = 0; level < levels.count && !remaining.empty(); ++level) {
Polygons deeper;
@@ -808,12 +808,12 @@ std::vector<AdaptiveInfillShell> make_adaptive_infill_shells(const AdaptiveInfil
return shells;
}
void fill_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing,
void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing,
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell)
{
FillParams shell_params = params;
shell_params.adaptive_infill_mode = AdaptiveInfillMode::Disabled;
for (const AdaptiveInfillShell &shell : make_adaptive_infill_shells(field, expolygon, z, params.density, params.adaptive_infill_interior_density, params.adaptive_infill_gradient)) {
shell_params.tpms_adaptive = TpmsAdaptiveMode::Disabled;
for (const TpmsShell &shell : make_tpms_shells(field, expolygon, z, params.density, params.tpms_interior_density, params.tpms_adaptive_gradient)) {
shell_params.density = shell.density;
for (const ExPolygon &part : offset_ex(shell.expolygons, -float(scale_(0.5 * spacing))))
fill_shell(shell_params, part);
@@ -21,7 +21,7 @@ namespace Slic3r {
// lobe, 1 at its surface. Lobes are parts of a body separated by a neck, like two spheres united. In the 2D modes,
// every section normal to the axis has its own bodies and lobes, and distances are measured within the section. The
// modes following the distance to the surface use the depth of every point instead; Distance warp also the lobes.
class AdaptiveInfillField
class TpmsRadialField
{
public:
struct Slice
@@ -39,7 +39,7 @@ public:
};
// Slices sorted by z, in the XY coordinates of the fill and the print Z.
AdaptiveInfillField(const std::vector<Slice> &slices, const BoundingBox &bbox, AdaptiveInfillMode mode,
TpmsRadialField(const std::vector<Slice> &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode,
const std::function<void()> &throw_if_canceled);
// Lobes blended near the sides between them, from a body or from each of the two sections around a point.
@@ -56,7 +56,7 @@ public:
// Axis normal to the sections in the 2D modes, -1 in the modes graded in 3D.
int axis() const { return m_axis; }
AdaptiveInfillMode mode() const { return m_mode; }
TpmsAdaptiveMode mode() const { return m_mode; }
// In the modes following the distance to the surface: depth relative to the deepest point of the body, from 0 at
// the surface to 1.
@@ -91,7 +91,7 @@ private:
static constexpr int Polar = 24;
static constexpr int Azimuth = 48;
AdaptiveInfillMode m_mode;
TpmsAdaptiveMode m_mode;
int m_axis;
Vec3d m_origin;
double m_cell;
@@ -106,9 +106,9 @@ private:
std::vector<float> m_depth;
};
using AdaptiveInfillFieldPtr = std::unique_ptr<AdaptiveInfillField>;
using TpmsRadialFieldPtr = std::unique_ptr<TpmsRadialField>;
// A field for every adaptive mode in use, indexed by the mode.
using AdaptiveInfillFields = std::array<AdaptiveInfillFieldPtr, size_t(AdaptiveInfillMode::Count)>;
using TpmsRadialFields = std::array<TpmsRadialFieldPtr, size_t(TpmsAdaptiveMode::Count)>;
struct AdaptiveTpms
{
@@ -117,14 +117,14 @@ struct AdaptiveTpms
// Pattern frequencies at the surface and at the center, in radians per mm.
double surface_frequency;
double interior_frequency;
AdaptiveInfillGradient gradient;
TpmsAdaptiveGradient gradient;
};
// Infill lines in the fill frame, the object frame rotated by -angle; z is the print_z of the layer.
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const AdaptiveInfillField &field, BoundingBox bbox,
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox,
coordf_t z, coordf_t layer_height, coordf_t spacing, float angle);
struct AdaptiveInfillShell
struct TpmsShell
{
float density;
ExPolygons expolygons;
@@ -132,12 +132,12 @@ struct AdaptiveInfillShell
// Stepped shells: the parts of an expolygon in the object frame at each density, from the surface inwards; z is the
// middle of the layer.
std::vector<AdaptiveInfillShell> make_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z,
float surface_density, float interior_density, AdaptiveInfillGradient gradient);
std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z,
float surface_density, float interior_density, TpmsAdaptiveGradient gradient);
// Stepped shells: fills every shell with fill_shell at its density, each shrunk by half a line like a filled region,
// so the lines connected along the boundaries of two shells don't overlap.
void fill_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing,
void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing,
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell);
} // namespace Slic3r
+6 -6
View File
@@ -17,7 +17,7 @@
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h"
#include "FillTpmsD.hpp"
#include "AdaptiveInfillField.hpp"
#include "FillTpmsAdaptive.hpp"
namespace Slic3r {
@@ -117,8 +117,8 @@ void FillTpmsD::_fill_surface_single(
ExPolygon expolygon,
Polylines &polylines_out)
{
if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
[&](const FillParams &shell_params, const ExPolygon &shell) {
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
});
@@ -130,13 +130,13 @@ void FillTpmsD::_fill_surface_single(
expolygon.rotate(-infill_angle);
Polylines polylines;
if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
// Radians per mm of the regular pattern at a density.
auto frequency = [&params, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
BoundingBox bbox = expolygon.contour.bounding_box();
bbox.offset(scale_((params.multiline + 1) * this->spacing));
polylines = make_adaptive_tpms({schwarz_d, frequency(params.density), frequency(params.adaptive_infill_interior_density), params.adaptive_infill_gradient},
*this->adaptive_infill_field, bbox, this->z, params.layer_height, this->spacing, infill_angle);
polylines = make_adaptive_tpms({schwarz_d, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient},
*this->tpms_radial_field, bbox, this->z, params.layer_height, this->spacing, infill_angle);
} else {
BoundingBox bb = expolygon.contour.bounding_box();
// Density adjusted to have a good %of weight.
+7 -7
View File
@@ -8,7 +8,7 @@
#include "libslic3r/Fill/FillBase.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "FillTpmsFK.hpp"
#include "AdaptiveInfillField.hpp"
#include "FillTpmsAdaptive.hpp"
#include <cmath>
#include <algorithm>
#include <cstddef>
@@ -132,8 +132,8 @@ void FillTpmsFK::_fill_surface_single(const FillParams& params,
ExPolygon expolygon,
Polylines& polylines_out)
{
if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
[&](const FillParams &shell_params, const ExPolygon &shell) {
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
});
@@ -151,10 +151,10 @@ void FillTpmsFK::_fill_surface_single(const FillParams& params,
// Enlarge the bounding box by the multi-line width to avoid artifacts at the edges.
bbox.offset(scale_((params.multiline + 1) * spacing));
Polylines polylines;
if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
polylines = make_adaptive_tpms({fischer_koch, 2. * PI / period(params.density), 2. * PI / period(params.adaptive_infill_interior_density),
params.adaptive_infill_gradient},
*this->adaptive_infill_field, bbox, this->z, params.layer_height, spacing, infill_angle);
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
polylines = make_adaptive_tpms({fischer_koch, 2. * PI / period(params.density), 2. * PI / period(params.tpms_interior_density),
params.tpms_adaptive_gradient},
*this->tpms_radial_field, bbox, this->z, params.layer_height, spacing, infill_angle);
} else {
marchsq::ScalarField sf = marchsq::ScalarField(bbox, this->z, period(params.density));
// Get simplified lines using coarse tolerance of 0.1mm (this is infill).
Loaded 100 of 213 files, more files were not shown because too many files have changed in this diff. Show more