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
140 changed files with 10833 additions and 23779 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
View File
@@ -21,6 +21,8 @@ on:
- 'scripts/test_build_win.ps1'
- 'build_release_macos.sh'
- 'scripts/build_preset_cache.*'
- 'signpath/**'
- 'scripts/*.ps1'
- 'scripts/flatpak/**'
- 'scripts/msix/**'
- 'tests/**'
@@ -38,11 +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'
- 'build_release_macos.sh'
- 'scripts/build_preset_cache.*'
- 'scripts/*.ps1'
- 'scripts/flatpak/**'
- 'scripts/msix/**'
- 'tests/**'
+146 -3
View File
@@ -424,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'
@@ -504,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
@@ -530,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
-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 -13
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")
@@ -1368,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)
-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"
+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.
-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
+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 " 不在合理的區間"
+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"},
@@ -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/"
+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);
+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());
}
+5 -15
View File
@@ -988,7 +988,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
MetadataList metadata;
VolumeMetadataList volumes;
std::optional<bool> auto_drop;
bool auto_drop { true };
};
struct CutObjectInfo
@@ -1208,7 +1208,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
//IdToGeometryMap m_orig_geometries; // backup & restore
CurrentConfig m_curr_config;
IdToMetadataMap m_objects_metadata;
std::set<int> m_items_auto_drop_off; // object ids with a build item auto_drop="0" (2.4.x layout)
IdToCutObjectInfoMap m_cut_object_infos;
IdToLayerHeightsProfileMap m_layer_heights_profiles;
IdToLayerConfigRangesMap m_layer_config_ranges;
@@ -1464,7 +1463,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
m_objects.clear();
m_instances.clear();
m_objects_metadata.clear();
m_items_auto_drop_off.clear();
m_curr_metadata_name.clear();
m_curr_characters.clear();
@@ -1504,7 +1502,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
m_curr_config.object_id = -1;
m_curr_config.volume_id = -1;
m_objects_metadata.clear();
m_items_auto_drop_off.clear();
m_layer_heights_profiles.clear();
m_layer_config_ranges.clear();
m_brim_ear_points.clear();
@@ -2260,14 +2257,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
ObjectMetadata::VolumeMetadataList volumes;
ObjectMetadata::VolumeMetadataList* volumes_ptr = nullptr;
// 2.4.x stored auto_drop on build items, shared by all instances of an object; any "off" wins
const bool legacy_auto_drop = !m_items_auto_drop_off.count(object.first.second);
IdToMetadataMap::iterator obj_metadata = m_objects_metadata.find(object.first.second);
if (obj_metadata != m_objects_metadata.end()) {
// config data has been found, this model was saved using slic3r pe
model_object->auto_drop = obj_metadata->second.auto_drop.value_or(legacy_auto_drop);
model_object->auto_drop = obj_metadata->second.auto_drop;
// apply object's name and config data
for (const Metadata& metadata : obj_metadata->second.metadata) {
@@ -2285,7 +2279,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}
else {
// config data not found, this model was not saved using slic3r pe
model_object->auto_drop = legacy_auto_drop;
// add the entire geometry as the single volume to generate
//volumes.emplace_back(0, (int)obj_geometry->second.triangles.size() - 1);
@@ -4128,8 +4121,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
std::string path = bbs_get_attribute_value_string(attributes, num_attributes, PPATH_ATTR);
Transform3d transform = bbs_get_transform_from_3mf_specs_string(bbs_get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR));
int printable = bbs_get_attribute_value_bool(attributes, num_attributes, PRINTABLE_ATTR);
if (const char* auto_drop = bbs_get_attribute_value_charptr(attributes, num_attributes, AUTO_DROP_ATTR); auto_drop != nullptr && !::atoi(auto_drop))
m_items_auto_drop_off.insert(object_id);
return !m_load_model || _create_object_instance(path, object_id, transform, printable, 1);
}
@@ -4498,8 +4489,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
// int instances_count_id = bbs_get_attribute_value_int(attributes, num_attributes, INSTANCESCOUNT_ATTR);
ObjectMetadata metadata;
if (bbs_get_attribute_value_charptr(attributes, num_attributes, AUTO_DROP_ATTR) != nullptr)
metadata.auto_drop = bbs_get_attribute_value_bool(attributes, num_attributes, AUTO_DROP_ATTR);
metadata.auto_drop = bbs_get_attribute_value_bool(attributes, num_attributes, AUTO_DROP_ATTR);
m_objects_metadata.insert({ object_id, std::move(metadata) });
m_curr_config.object_id = object_id;
return true;
@@ -10120,7 +10110,7 @@ bool to_xml(std::stringstream &stream, const EmbossShape::SvgFile &svg, const Mo
assert(file_data != nullptr);
if (file_data == nullptr && !svg.path.empty()) file_data = read_from_disk(svg.path);
if (file_data == nullptr) {
BOOST_LOG_TRIVIAL(warning) << "Can't write SVG file no filedata";
BOOST_LOG_TRIVIAL(warning) << "Can't write svg file no filedata";
return false;
}
const std::string &file_data_str = *file_data;
@@ -10136,7 +10126,7 @@ void to_xml(std::stringstream &stream, const EmbossShape &es, const ModelVolume
stream << " <" << SHAPE_TAG << " ";
if (es.svg_file.has_value())
if (!to_xml(stream, *es.svg_file, volume, archive, export_full_path)) {
BOOST_LOG_TRIVIAL(warning) << "Can't write SVG file defiden embossed shape into 3mf";
BOOST_LOG_TRIVIAL(warning) << "Can't write svg file defiden embossed shape into 3mf";
}
stream << SHAPE_SCALE_ATTR << "=\"" << es.scale << "\" ";
-10
View File
@@ -3985,16 +3985,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
this->placeholder_parser().set("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder)));
this->placeholder_parser().set("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder)));
this->placeholder_parser().set("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
// The final print statistics are known only after the whole G-code is generated, so they are filled in during post-processing too.
// The running extruded_* totals are left alone; profiles use them as numbers.
this->placeholder_parser().set("used_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
this->placeholder_parser().set("print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder)));
this->placeholder_parser().set("normal_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder)));
this->placeholder_parser().set("silent_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Silent_Print_Time_Placeholder)));
this->placeholder_parser().set("total_weight", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Weight_Placeholder)));
this->placeholder_parser().set("total_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Cost_Placeholder)));
this->placeholder_parser().set("total_wipe_tower_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Cost_Placeholder)));
this->placeholder_parser().set("total_wipe_tower_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Filament_Placeholder)));
// Sync variant-mapped params into placeholder_parser before processing start gcode
update_placeholder_parser_with_variant_params();
+71 -47
View File
@@ -120,13 +120,7 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags = {
"@PRINT_TIME_HOUR@",
"@PRINT_TIME_MINUTE@",
"@PRINT_TIME_SEC@",
"@USED_FILAMENT_LENGTH@",
"@NORMAL_PRINT_TIME@",
"@SILENT_PRINT_TIME@",
"@TOTAL_WEIGHT@",
"@TOTAL_COST@",
"@TOTAL_WIPE_TOWER_COST@",
"@TOTAL_WIPE_TOWER_FILAMENT@"
"@USED_FILAMENT_LENGTH@"
};
const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
@@ -153,13 +147,7 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
"@PRINT_TIME_HOUR@",
"@PRINT_TIME_MINUTE@",
"@PRINT_TIME_SEC@",
"@USED_FILAMENT_LENGTH@",
"@NORMAL_PRINT_TIME@",
"@SILENT_PRINT_TIME@",
"@TOTAL_WEIGHT@",
"@TOTAL_COST@",
"@TOTAL_WIPE_TOWER_COST@",
"@TOTAL_WIPE_TOWER_FILAMENT@"
"@USED_FILAMENT_LENGTH@"
};
@@ -1276,22 +1264,19 @@ void GCodeProcessor::run_post_process()
return ret;
};
// Process inline placeholders (print_time_*, used_filament_length and the print statistics known only after export)
// Process inline placeholders (print_time_total_sec, print_time_day, print_time_hour, print_time_minute, print_time_sec and used_filament_length)
auto process_inline_placeholders = [&](std::string& gcode_line) {
bool processed = false;
// Every inline placeholder contains '@', so a line without one has nothing to replace.
if (gcode_line.find('@') == std::string::npos)
return processed;
auto replace_tag = [&gcode_line, &processed](ETags tag, const std::string& value) {
const std::string& placeholder = reserved_tag(tag);
size_t pos = gcode_line.find(placeholder);
while (pos != std::string::npos) {
gcode_line.replace(pos, placeholder.length(), value);
processed = true;
pos = gcode_line.find(placeholder, pos + value.length());
}
};
const std::string& print_time_total_placeholder = reserved_tag(ETags::Print_Time_Total_Sec_Placeholder);
const std::string& print_time_day_placeholder = reserved_tag(ETags::Print_Time_Day_Placeholder);
const std::string& print_time_hour_placeholder = reserved_tag(ETags::Print_Time_Hour_Placeholder);
const std::string& print_time_minute_placeholder = reserved_tag(ETags::Print_Time_Minute_Placeholder);
const std::string& print_time_sec_placeholder = reserved_tag(ETags::Print_Time_Sec_Placeholder);
const std::string& used_filament_placeholder = reserved_tag(ETags::Used_Filament_Length_Placeholder);
double print_time_total_sec = m_time_processor.machines[static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal)].time;
if (print_time_total_sec < 0.0)
@@ -1304,31 +1289,70 @@ void GCodeProcessor::run_post_process()
int print_time_minute = (day_remainder_seconds % 3600) / 60;
int print_time_sec = day_remainder_seconds % 60;
double total_filament_mm = 0.0;
for (const auto& mm : filament_mm) {
total_filament_mm += mm;
// Replace print_time_total_sec
size_t pos = gcode_line.find(print_time_total_placeholder);
while (pos != std::string::npos) {
char buf[64];
sprintf(buf, "%.2f", print_time_total_sec);
gcode_line.replace(pos, print_time_total_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_total_placeholder, pos + strlen(buf));
}
replace_tag(ETags::Print_Time_Total_Sec_Placeholder, Slic3r::float_to_string_decimal_point(print_time_total_sec, 2));
replace_tag(ETags::Print_Time_Day_Placeholder, std::to_string(print_time_day));
replace_tag(ETags::Print_Time_Hour_Placeholder, std::to_string(print_time_hour));
replace_tag(ETags::Print_Time_Minute_Placeholder, std::to_string(print_time_minute));
replace_tag(ETags::Print_Time_Sec_Placeholder, std::to_string(print_time_sec));
replace_tag(ETags::Used_Filament_Length_Placeholder,
Slic3r::float_to_string_decimal_point(total_filament_mm / 1000.0, 2)); // Convert mm to m
// Same values as the print statistics that filename_format gets (PrintStatistics::config(), filled in by
// update_print_estimated_stats() in GCode.cpp), which runs only after this post-processing.
replace_tag(ETags::Normal_Print_Time_Placeholder, get_time_dhm(PrintEstimatedStatistics::ETimeMode::Normal));
replace_tag(ETags::Silent_Print_Time_Placeholder, is_stealth_time_estimator_enabled() ?
get_time_dhm(PrintEstimatedStatistics::ETimeMode::Stealth) :
short_time("N/A"));
const double total_cost = filament_total_cost + m_print->config().time_cost.value * print_time_total_sec / 3600.0;
replace_tag(ETags::Total_Weight_Placeholder, Slic3r::float_to_string_decimal_point(filament_total_g, 2));
replace_tag(ETags::Total_Cost_Placeholder, Slic3r::float_to_string_decimal_point(total_cost, 2));
replace_tag(ETags::Total_Wipe_Tower_Cost_Placeholder,
Slic3r::float_to_string_decimal_point(m_print->print_statistics().total_wipe_tower_cost, 2));
replace_tag(ETags::Total_Wipe_Tower_Filament_Placeholder,
Slic3r::float_to_string_decimal_point(m_print->print_statistics().total_wipe_tower_filament, 2));
// Replace print_time_day
pos = gcode_line.find(print_time_day_placeholder);
while (pos != std::string::npos) {
char buf[64];
sprintf(buf, "%d", print_time_day);
gcode_line.replace(pos, print_time_day_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_day_placeholder, pos + strlen(buf));
}
// Replace print_time_hour
pos = gcode_line.find(print_time_hour_placeholder);
while (pos != std::string::npos) {
char buf[64];
sprintf(buf, "%d", print_time_hour);
gcode_line.replace(pos, print_time_hour_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_hour_placeholder, pos + strlen(buf));
}
// Replace print_time_minute
pos = gcode_line.find(print_time_minute_placeholder);
while (pos != std::string::npos) {
char buf[64];
sprintf(buf, "%d", print_time_minute);
gcode_line.replace(pos, print_time_minute_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_minute_placeholder, pos + strlen(buf));
}
// Replace print_time_sec
pos = gcode_line.find(print_time_sec_placeholder);
while (pos != std::string::npos) {
char buf[64];
sprintf(buf, "%d", print_time_sec);
gcode_line.replace(pos, print_time_sec_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_sec_placeholder, pos + strlen(buf));
}
// Replace used_filament_length
pos = gcode_line.find(used_filament_placeholder);
while (pos != std::string::npos) {
double total_filament_mm = 0.0;
for (const auto& mm : filament_mm) {
total_filament_mm += mm;
}
double used_filament_length = total_filament_mm / 1000.0; // Convert mm to m
char buf[64];
sprintf(buf, "%.2f", used_filament_length);
gcode_line.replace(pos, used_filament_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(used_filament_placeholder, pos + strlen(buf));
}
return processed;
};
-6
View File
@@ -601,12 +601,6 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
Print_Time_Minute_Placeholder,
Print_Time_Sec_Placeholder,
Used_Filament_Length_Placeholder,
Normal_Print_Time_Placeholder,
Silent_Print_Time_Placeholder,
Total_Weight_Placeholder,
Total_Cost_Placeholder,
Total_Wipe_Tower_Cost_Placeholder,
Total_Wipe_Tower_Filament_Placeholder,
};
static const std::string& reserved_tag(ETags tag) { return s_IsBBLPrinter ? Reserved_Tags[static_cast<unsigned char>(tag)] : Reserved_Tags_compatible[static_cast<unsigned char>(tag)]; }
+24 -65
View File
@@ -62,12 +62,7 @@ public:
// Depth in the hierarchy. External perimeter has depth = 0. An external perimeter could be both a contour and a hole.
unsigned short depth;
// ORCA: an external hole perimeter touching the next outer wall all around, with nothing between them.
// E.g. a 1mm tube wall with 0.5mm walls: 2 lines, the contour's outer wall and the hole's.
bool is_thin_wall_hole = false;
// ORCA: an external hole perimeter with no room for an inner wall beside it, at most gap fill. Every thin wall
// hole is one, and so is e.g. a 1.5mm tube wall with 0.5mm walls: 3 lines, the two outer walls and gap fill
// between them. A 2mm tube wall has room for an inner wall a side and is not. Only set for Inner/Outer/Inner.
bool is_narrow_wall_hole = false;
// Children contour, may be both CCW and CW oriented (outer contours or holes).
std::vector<PerimeterGeneratorLoop> children;
@@ -577,8 +572,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
extrusion_coll.append(std::move(extrusion_loop));
// Orca: Reverse the order of paths for thin wall holes. We define thin wall hole as a hole with only one perimeter.
const bool thin_wall_hole = !pg_extrusion.is_contour && pg_extrusions.size() == 2;
if (thin_wall_hole && perimeter_generator.config->wall_sequence != WallSequence::OuterInner &&
!perimeter_generator.outer_walls_first_for_brim())
if (thin_wall_hole && perimeter_generator.config->wall_sequence != WallSequence::OuterInner)
std::reverse(extrusion_coll.entities.begin(), extrusion_coll.entities.end());
}
else {
@@ -1696,24 +1690,13 @@ void PerimeterGenerator::process_classic()
last = std::move(offsets);
// ORCA: a thin wall hole has no room for gap fill or an inner wall anywhere beside its outer wall, a
// narrow wall hole none for an inner wall, the room the next loop is offset by below.
// ORCA: a thin wall hole has no room for gap fill or an inner wall anywhere beside its outer wall.
if (i == 0 && ! holes[0].empty()) {
auto reach = [&last](double room) {
return to_polygons(offset2_ex(last, -float(room), float(room) + float(SCALED_EPSILON)));
};
auto out_of_reach = [](const Polygon &hole, const Polygons &reach) {
return intersection_pl(Polylines{ hole.split_at_first_point() },
ClipperUtils::clip_clipper_polygons_with_subject_bbox(reach, get_extents(hole).inflated(SCALED_EPSILON))).empty();
};
// Only Inner/Outer/Inner orders narrow wall holes apart.
const bool narrow_holes_ordered = this->config->wall_sequence == WallSequence::InnerOuterInner;
const Polygons gap_fill_reach = reach(0.5 * (ext_perimeter_spacing2 + gap_fill_min_width));
const Polygons inner_wall_reach = narrow_holes_ordered ? reach(ext_perimeter_spacing2 + min_spacing / 2.) : Polygons();
for (PerimeterGeneratorLoop &hole : holes[0]) {
hole.is_thin_wall_hole = out_of_reach(hole.polygon, gap_fill_reach);
hole.is_narrow_wall_hole = narrow_holes_ordered && out_of_reach(hole.polygon, inner_wall_reach);
}
const float room = float(0.5 * (ext_perimeter_spacing2 + gap_fill_min_width));
const Polygons reach = to_polygons(offset2_ex(last, -room, room + float(SCALED_EPSILON)));
for (PerimeterGeneratorLoop &hole : holes[0])
hole.is_thin_wall_hole = intersection_pl(Polylines{ hole.polygon.split_at_first_point() },
ClipperUtils::clip_clipper_polygons_with_subject_bbox(reach, get_extents(hole.polygon).inflated(SCALED_EPSILON))).empty();
}
//BBS: refer to superslicer
@@ -1852,16 +1835,18 @@ void PerimeterGenerator::process_classic()
// if brim will be printed, reverse the order of perimeters so that
// we continue inwards after having finished the brim
// TODO: add test for perimeter order
bool is_outer_wall_first = this->config->wall_sequence == WallSequence::OuterInner;
// Orca: a contour with only a narrow wall hole beside it has nothing to sandwich, and prints the hole first,
// then the contour, then the gap fill between them, as Arachne does.
const PerimeterGeneratorLoops &island_loops = contours.front();
const bool narrow_wall_hole_island = island_loops.size() == 1 && island_loops.front().children.size() == 1 &&
island_loops.front().children.front().is_narrow_wall_hole;
if (is_outer_wall_first || this->outer_walls_first_for_brim())
if (is_outer_wall_first ||
//BBS: always print outer wall first when there indeed has brim.
// btLeadingEdgeOnly is an outer brim too (a belt brim at the part's first contact).
(this->layer_id == 0 &&
(this->object_config->brim_type == BrimType::btOuterOnly ||
this->object_config->brim_type == BrimType::btLeadingEdgeOnly) &&
this->object_config->brim_width.value > 0))
entities.reverse();
// Orca: sandwich mode. Apply after 1st layer.
else if ((this->config->wall_sequence == WallSequence::InnerOuterInner) && layer_id > 0 && !narrow_wall_hole_island){
else if ((this->config->wall_sequence == WallSequence::InnerOuterInner) && layer_id > 0){
entities.reverse(); // reverse all entities - order them from external to internal
if(entities.entities.size()>2){ // 3 walls minimum needed to do inner outer inner ordering
int position = 0; // index to run the re-ordering for multiple external perimeters in a single island.
@@ -2427,25 +2412,11 @@ std::vector<PerimeterGeneratorArachneExtrusion> reorderPerimetersByProximity(std
// Function to reorder perimeters starting from a given reference index
auto reorderFromReference = [&](int referenceIndex) {
// Find first level touching perimeters. The odd centre line of a wall too narrow for two inner walls is shared
// by the outer walls on both sides, so those join this island and are printed before it too, holes first.
std::vector<int> outerIndices{referenceIndex};
std::vector<int> firstLevelTouchingIndices;
for (size_t next = 0; next < outerIndices.size(); ++next)
for (int idx : findAllTouchingPerimeters(entities, {outerIndices[next]}, threshold_external, threshold_internal, 1)) {
if (std::find(firstLevelTouchingIndices.begin(), firstLevelTouchingIndices.end(), idx) != firstLevelTouchingIndices.end())
continue;
firstLevelTouchingIndices.push_back(idx);
if (!entities[idx].extrusion->is_odd)
continue;
for (size_t i = 0; i < entities.size(); ++i)
if (entities[i].extrusion->inset_idx == 0 && includedIndices.count(int(i)) == 0 &&
std::find(outerIndices.begin(), outerIndices.end(), int(i)) == outerIndices.end() &&
arachne_lines_touch(*entities[i].extrusion, *entities[idx].extrusion, double(threshold_external)))
outerIndices.push_back(int(i));
}
std::stable_partition(outerIndices.begin(), outerIndices.end(),
[&entities](int idx) { return entities[idx].extrusion->is_closed && !entities[idx].is_contour; });
std::unordered_set<int> firstLevelIndices;
firstLevelIndices.insert(referenceIndex);
// Find first level touching perimeters
std::vector<int> firstLevelTouchingIndices = findAllTouchingPerimeters(entities, firstLevelIndices, threshold_external, threshold_internal, 1);
// Bring the largest first level perimeter to the front
// The longest first neighbour is most likely the dominant proximate perimeter
// hence printing it immediately after the external perimeter should speed things up
@@ -2456,10 +2427,8 @@ std::vector<PerimeterGeneratorArachneExtrusion> reorderPerimetersByProximity(std
std::iter_swap(maxIt, firstLevelTouchingIndices.end() - 1);
}
// Insert first level perimeters into reordered list
for (int idx : outerIndices) {
reordered.push_back(entities[idx]);
includedIndices.insert(idx);
}
reordered.push_back(entities[referenceIndex]);
includedIndices.insert(referenceIndex);
for (int idx : firstLevelTouchingIndices) {
if (includedIndices.count(idx) == 0) {
@@ -2788,7 +2757,7 @@ void PerimeterGenerator::process_arachne()
if (layer_id == 0){ // disable inner outer inner algorithm after the first layer
is_outer_wall_first =
this->config->wall_sequence == WallSequence::OuterInner || this->outer_walls_first_for_brim();
this->config->wall_sequence == WallSequence::OuterInner;
}
if (is_outer_wall_first) {
start_perimeter = 0;
@@ -3073,16 +3042,6 @@ ExPolygons PerimeterGenerator::printable_slices(const ExPolygons &slices) const
return min_width > EPSILON ? opening_ex(slices, float(scale_(min_width / 2.))) : slices;
}
bool PerimeterGenerator::outer_walls_first_for_brim() const
{
return layer_id == 0 && brim_puts_outer_walls_first(object_config->brim_type.value, object_config->brim_width.value);
}
bool brim_puts_outer_walls_first(BrimType brim_type, double brim_width)
{
return brim_width > 0 && (brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btLeadingEdgeOnly);
}
std::vector<Polygons> PerimeterGenerator::generate_lower_polygons_series(float width)
{
float nozzle_diameter = print_config->nozzle_diameter.get_at(config->outer_wall_filament_id - 1);
-6
View File
@@ -164,8 +164,6 @@ public:
Polygons lower_slices_polygons() const { return m_lower_slices_polygons; }
// ORCA: the slices less the slivers the wall generator prints nothing for, so they never count as support.
ExPolygons printable_slices(const ExPolygons &slices) const;
// ORCA: whether this layer prints its outer walls first for the brim, see brim_puts_outer_walls_first().
bool outer_walls_first_for_brim() const;
private:
std::vector<Polygons> generate_lower_polygons_series(float width);
@@ -185,10 +183,6 @@ private:
Polygons m_lower_slices_polygons;
};
// ORCA: a brim against the outer walls makes the first layer print them first, whatever the wall sequence, so the
// brim runs straight on into the outer wall. Auto, ear and painted brims may leave the outer walls bare.
bool brim_puts_outer_walls_first(BrimType brim_type, double brim_width);
}
#endif
-2
View File
@@ -1421,7 +1421,6 @@ static std::vector<std::string> s_Preset_print_options{
"initial_layer_travel_acceleration",
"initial_layer_travel_jerk",
"exclude_object",
"exclude_object_tolerance",
"slow_down_layers",
"infill_anchor",
"infill_anchor_max",
@@ -2294,7 +2293,6 @@ void PresetCollection::load_project_embedded_presets(std::vector<Preset*>& proje
BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent for config %1%!")%preset->file;
continue;
}
extend_default_config_length(config, false, {});
preset->config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
//preset->config.apply(std::move(config));
Preset::normalize(preset->config);
+1 -8
View File
@@ -756,14 +756,7 @@ public:
Preset& get_edited_preset() { return m_edited_preset; }
const Preset& get_edited_preset() const { return m_edited_preset; }
// The root of the selected preset's inherits chain, or the selected preset itself when
// the chain names a preset that is not loaded (e.g. a vendor that is not installed).
const Preset& get_selected_preset_base() const
{
const Preset &selected = m_presets[m_idx_selected];
const Preset *base = get_preset_base(selected);
return base ? *base : selected;
}
const Preset& get_selected_preset_base() const { return *get_preset_base(m_presets[m_idx_selected]); }
// Return the last saved preset.
// const Preset& get_saved_preset() const { return m_saved_preset; }
+1 -2
View File
@@ -309,7 +309,6 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
"gcode_comments",
"gcode_label_objects",
"exclude_object",
"exclude_object_tolerance",
"support_material_interface_fan_speed",
"internal_bridge_fan_speed", // ORCA: Add support for separate internal bridge fan speed control
"ironing_fan_speed",
@@ -6930,7 +6929,7 @@ BoundingBoxf3 PrintInstance::get_bounding_box() const {
Polygon PrintInstance::get_convex_hull_2d() {
Polygon poly = print_object->model_object()->convex_hull_2d(model_instance->get_matrix());
poly.douglas_peucker(scale_(print_object->print()->config().exclude_object_tolerance.value));
poly.douglas_peucker(scale_(0.1));
return poly;
}
-11
View File
@@ -4629,17 +4629,6 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("exclude_object_tolerance", coFloat);
def->label = L("Outline tolerance");
def->tooltip = L("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.");
def->sidetext = L("mm");
def->min = 0;
def->max = 5;
def->mode = comExpert;
def->set_default_value(new ConfigOptionFloat(0.1));
def = this->add("gcode_comments", coBool);
def->label = L("Verbose G-code");
def->tooltip = L("Enable this to get a commented G-code file, with each line explained by a descriptive text. "
-1
View File
@@ -2130,7 +2130,6 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionPercents, filament_shrinkage_compensation_z))
((ConfigOptionBool, gcode_label_objects))
((ConfigOptionBool, exclude_object))
((ConfigOptionFloat, exclude_object_tolerance))
((ConfigOptionFloats, grab_length))
((ConfigOptionBool, gcode_comments))
((ConfigOptionInt, slow_down_layers))
+9 -8
View File
@@ -17,7 +17,6 @@
#include "I18N.hpp"
#include "Layer.hpp"
#include "MutablePolygon.hpp"
#include "PerimeterGenerator.hpp"
#include "PrintBase.hpp"
#include "PrintConfig.hpp"
#include "SLA/IndexedMesh.hpp"
@@ -1465,13 +1464,15 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "top_surface_speed") {
// Brim is printed below supports, support invalidates brim and skirt.
steps.emplace_back(posSupportMaterial);
if (opt_key == "brim_type" || opt_key == "brim_width") {
// Regenerate the walls when the brim starts or stops putting the first layer's outer walls first.
auto outer_walls_first = [](const ConfigOptionResolver &config) {
return brim_puts_outer_walls_first(config.option<ConfigOptionEnum<BrimType>>("brim_type")->value,
config.option<ConfigOptionFloat>("brim_width")->value);
};
if (outer_walls_first(old_config) != outer_walls_first(new_config))
if (opt_key == "brim_type") {
const auto* old_brim_type = old_config.option<ConfigOptionEnum<BrimType>>(opt_key);
const auto* new_brim_type = new_config.option<ConfigOptionEnum<BrimType>>(opt_key);
//BBS: When switch to manual brim, the object must have brim, then re-generate perimeter
//to make the wall order of first layer to be outer-first
// btLeadingEdgeOnly is printed as an outer brim (Brim.cpp, BeltBrim.cpp), so it
// takes part in the same first-layer wall order rule.
if (old_brim_type->value == btOuterOnly || new_brim_type->value == btOuterOnly ||
old_brim_type->value == btLeadingEdgeOnly || new_brim_type->value == btLeadingEdgeOnly)
steps.emplace_back(posPerimeters);
}
} else if (
+8 -5
View File
@@ -707,10 +707,8 @@ void tree_supports_generate_paths(
false);
}
if (eec) {
if (!eec->empty()) {
std::reverse(eec->entities.begin(), eec->entities.end());
dst.emplace_back(eec.release());
}
std::reverse(eec->entities.begin(), eec->entities.end());
dst.emplace_back(eec.release());
}
}
}
@@ -1827,7 +1825,12 @@ void generate_support_toolpaths(
no_sort = true;
} else if (support_params.support_style == SupportMaterialStyle::smsTreeOrganic &&
(config.support_base_pattern == smpNone || config.support_base_pattern == smpDefault)) {
tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow, support_params);
// Orca: A special case for the hollow Organic supports
// Orca: If the tree supports are too tall, use a double wall to make it stronger
SupportParameters support_params2 = support_params;
if (support_layer.print_z > 100.0)
support_params2.tree_branch_diameter_double_wall_area_scaled = 0.1;
tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow, support_params2);
done = true;
}
if (! done)
+22 -10
View File
@@ -2149,9 +2149,7 @@ void TreeSupport::draw_circles()
const bool with_lightning_infill = m_support_params.base_fill_pattern == ipLightning;
coordf_t support_extrusion_width = m_support_params.support_extrusion_width;
const float tree_brim_width = config.tree_support_brim_width.value;
// Reinforce branches moving more than half a support-line width between layers.
// Square the threshold for comparison with the squared XY movement below.
const double max_move_sq = sqr(double(scale_(m_object_config->support_line_width / 2)));
// Belt floor: the first object layer is not on a flat bed — it rests on the
// tilted, moving belt. So the first-object-layer adhesion features (the tree
// support brim, the hybrid first-layer base expansion) must be suppressed:
@@ -2203,7 +2201,7 @@ void TreeSupport::draw_circles()
size_t first_base_roof_area = 0;
bool floor_interface_as_base = false;
bool has_circle_node = false;
ExPolygons extra_wall_areas;
bool need_extra_wall = false;
ExPolygons collision_sharp_tails;
ExPolygons collision_base;
auto get_collision = [&](bool sharp_tail) -> ExPolygons &{
@@ -2281,6 +2279,7 @@ void TreeSupport::draw_circles()
// area = diff_clipped({ ExPolygon(circle) }, get_collision(node.is_sharp_tail && node.distance_to_top <= 0));
if (!area.empty()) has_circle_node = true;
if (node.need_extra_wall) need_extra_wall = true;
// merge overhang to get a smoother interface surface
// Do not merge when buildplate_only is on, because some underneath nodes may have been deleted.
@@ -2295,10 +2294,6 @@ void TreeSupport::draw_circles()
}
}
// Use the Pythagorean theorem to compare movement magnitude (dx² + dy²) without a square root.
if (node.is_processed && node.movement.cast<double>().squaredNorm() > max_move_sq)
append(extra_wall_areas, area);
if (obj_layer_nr>0 && node.distance_to_top < 0)
append(roof_gap_areas, area);
else if (obj_layer_nr > 0 && node.support_roof_layers_below == 1 &&
@@ -2535,8 +2530,7 @@ void TreeSupport::draw_circles()
//if (area(expoly) < SQ(scale_(1))) continue;
area_groups.emplace_back(&expoly, SupportLayer::BaseType, max_layers_above_base);
area_groups.back().need_infill = overlaps({ expoly }, area_poly);
area_groups.back().need_extra_wall = !area_groups.back().need_infill &&
!extra_wall_areas.empty() && overlaps({expoly}, extra_wall_areas);
area_groups.back().need_extra_wall = need_extra_wall && !area_groups.back().need_infill;
}
for (size_t roof_idx = 0; roof_idx < ts_layer->roof_areas.size(); ++roof_idx) {
auto &expoly = ts_layer->roof_areas[roof_idx];
@@ -3439,6 +3433,11 @@ void TreeSupport::smooth_nodes()
}
}
float max_move = scale_(m_object_config->support_line_width / 2);
// if the branch is very tall, the tip also needs extra wall
float thresh_tall_branch = 100;
float thresh_dist_to_top = 30;
for (int layer_nr = 0; layer_nr< contact_nodes.size(); layer_nr++) {
std::vector<SupportNode *> &curr_layer_nodes = contact_nodes[layer_nr];
if (curr_layer_nodes.empty()) continue;
@@ -3448,17 +3447,20 @@ void TreeSupport::smooth_nodes()
std::vector<double> radii;
std::vector<SupportNode *> branch;
SupportNode * p_node = node;
float total_height = 0;
// add a fixed head if it's not a polygon node, see STUDIO-4403
// Polygon node can't be added because the move distance might be huge, making the nodes in between jump and dangling
if (node->child && node->child->type!=ePolygon) {
pts.push_back(p_node->child->position);
radii.push_back(p_node->child->radius);
branch.push_back(p_node->child);
total_height += p_node->child->height;
}
do {
pts.push_back(p_node->position);
radii.push_back(p_node->radius);
branch.push_back(p_node);
total_height += p_node->height;
p_node = p_node->parent;
} while (p_node && !p_node->is_processed);
if (pts.size() < 3) continue;
@@ -3477,6 +3479,9 @@ void TreeSupport::smooth_nodes()
branch[i]->radius = radii1[i];
branch[i]->movement = (pts[i + 1] - pts[i - 1]) / 2;
branch[i]->is_processed = true;
if (branch[i]->parents.size() > 1 || (branch[i]->movement.x() > max_move || branch[i]->movement.y() > max_move) ||
(total_height > thresh_tall_branch && branch[i]->dist_mm_to_top < thresh_dist_to_top))
branch[i]->need_extra_wall = true;
BOOST_LOG_TRIVIAL(info) << "smooth_nodes: layer_nr=" << layer_nr << ", i=" << i << ", pt=" << pt << ", movement=" << branch[i]->movement << ", radius=" << branch[i]->radius;
}
}
@@ -3484,6 +3489,13 @@ void TreeSupport::smooth_nodes()
std::swap(pts, pts1);
std::swap(radii, radii1);
}
else {
// interpolate need_extra_wall in the end
for (size_t i = 1; i < branch.size() - 1; i++) {
if (branch[i - 1]->need_extra_wall && branch[i + 1]->need_extra_wall)
branch[i]->need_extra_wall = true;
}
}
}
}
}
+1
View File
@@ -135,6 +135,7 @@ struct SupportNode
TreeNodeType type = eCircle;
bool is_corner = false;
bool is_processed = false;
bool need_extra_wall = false;
bool is_sharp_tail = false;
bool valid = true;
ExPolygon overhang; // when type==ePolygon, set this value to get original overhang area
+1 -1
View File
@@ -75,7 +75,7 @@ std::vector<float> Layers::get_zs() const
size_t Layers::get_layer_id_at(float z) const
{
auto iter = std::upper_bound(m_items.begin(), m_items.end(), z, [](float z, const Item& item) { return item.z > z; });
auto iter = std::upper_bound(m_items.begin(), m_items.end(), z, [](float z, const Item& item) { return item.z < z; });
return std::distance(m_items.begin(), iter);
}
+1 -1
View File
@@ -225,7 +225,7 @@ void BackgroundSlicingProcess::process_fff()
// BBS: add the logic to process from an existed gcode file
if (m_print->finished()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: skip slicing, to process previous gcode file") % __LINE__;
m_fff_print->set_status(80, _utf8(L("Processing G-code from previous file\u2026" /* \u2026 -> ... */)));
m_fff_print->set_status(80, _utf8(L("Processing G-Code from previous file\u2026")));
wxCommandEvent evt(m_event_slicing_completed_id);
// Post the Slicing Finished message for the G-code viewer to update.
// Passing the timestamp
+8 -10
View File
@@ -303,12 +303,10 @@ nlohmann::json color_picker_ui_strings()
return {
{"Color Picker", _u8L("Color Picker")},
{"Basic Colors", _u8L("Basic Colors")},
// TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
{"Orca Palette", _u8L("Orca Palette")},
// TRN: Keep the standard/brand name "RAL" unchanged.
{"RAL Classic", _u8L("RAL Classic")},
{"Orca Palette", _u8L("Orca Palette")},// TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
{"RAL Classic", _u8L("RAL Classic")}, // TRN: Keep the standard/brand name "RAL" unchanged.
{"Gradients", _u8L("Gradients")},
{"Solid", _u8L_CONTEXT("Solid", "Color picker")},
{"Solid", _u8L("Solid")},
{"Gradient", _u8L("Gradient")},
{"Gradient start", _u8L("Gradient start")},
{"Gradient end", _u8L("Gradient end")},
@@ -442,7 +440,7 @@ nlohmann::json color_picker_ui_strings()
{"Purple", _u8L("Purple")},
{"Rebecca purple", _u8L("Rebecca purple")},
{"Medium violet red", _u8L("Medium violet red")},
{"Pale violet red", _u8L("Pale violet red")},
{"PaleV violet red", _u8L("PaleV violet red")},
{"Deep pink", _u8L("Deep pink")},
{"Hot pink", _u8L("Hot pink")},
{"Light pink", _u8L("Light pink")},
@@ -498,7 +496,7 @@ nlohmann::json color_picker_ui_strings()
{"Turquoise bay", _u8L("Turquoise bay")},
{"Sky over reef", _u8L("Sky over reef")},
{"Shallow sky", _u8L("Shallow sky")},
{"Electric blue", _u8L("Electric blue")},
{"Electric bel", _u8L("Electric bel")},
{"Dolphin", _u8L("Dolphin")},
{"Ocean depths", _u8L("Ocean depths")},
{"Whale", _u8L("Whale")},
@@ -521,7 +519,7 @@ nlohmann::json color_picker_ui_strings()
{"Golden yellow", _u8L("Golden yellow")},
{"Honey yellow", _u8L("Honey yellow")},
{"Maize yellow", _u8L("Maize yellow")},
{"Daffodil yellow", _u8L("Daffodil yellow")},
{"Dalmatian yellow", _u8L("Dalmatian yellow")},
{"Brown beige", _u8L("Brown beige")},
{"Lemon yellow", _u8L("Lemon yellow")},
{"Oyster white", _u8L("Oyster white")},
@@ -608,7 +606,7 @@ nlohmann::json color_picker_ui_strings()
{"Pigeon blue", _u8L("Pigeon blue")},
{"Traffic blue", _u8L("Traffic blue")},
{"Turquoise blue", _u8L("Turquoise blue")},
{"Capri blue", _u8L("Capri blue")},
{"Capsanthin violet", _u8L("Capsanthin violet")},
{"Ocean blue", _u8L("Ocean blue")},
{"Water blue", _u8L("Water blue")},
{"Night blue", _u8L("Night blue")},
@@ -646,7 +644,7 @@ nlohmann::json color_picker_ui_strings()
{"Luminous green", _u8L("Luminous green")},
{"Squirrel grey", _u8L("Squirrel grey")},
{"Silver grey", _u8L("Silver grey")},
{"Olive grey", _u8L("Olive grey")},
{"Olivestone", _u8L("Olivestone")},
{"Moss grey", _u8L("Moss grey")},
{"Signal grey", _u8L("Signal grey")},
{"Mouse grey", _u8L("Mouse grey")},
-2
View File
@@ -1245,8 +1245,6 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
if(gcf_is_klipper)
toggle_field("accel_to_decel_factor", config->opt_bool("accel_to_decel_enable"));
toggle_line("exclude_object_tolerance", gcf_is_klipper && config->opt_bool("exclude_object"));
bool have_make_overhang_printable = config->opt_bool("make_overhang_printable");
toggle_line("make_overhang_printable_angle", have_make_overhang_printable);
toggle_line("make_overhang_printable_hole_size", have_make_overhang_printable);
+2 -2
View File
@@ -3151,7 +3151,7 @@ void CreatePrinterPresetDialog::set_current_visible_printer()
m_printer_name_to_preset.clear();
for (const Preset &printer_preset : printer_presets) {
if (!printer_preset.is_visible) continue;
if (preset_bundle->printers.get_preset_base(printer_preset) != &printer_preset) continue;
if (preset_bundle->printers.get_preset_base(printer_preset)->name != printer_preset.name) continue;
if (auto printer_model = dynamic_cast<ConfigOptionString*>(const_cast<Preset&>(printer_preset).config.option("printer_model", false))) {
if (m_printer_name_to_preset.find(printer_model->value) == m_printer_name_to_preset.end()) {
printer_choice.push_back(from_u8(printer_model->value));
@@ -4962,7 +4962,7 @@ void CreatePresetForPrinterDialog::get_visible_printer_and_compatible_filament_p
if (filament_preset.is_default || !filament_preset.is_compatible || filament_preset.is_project_embedded) continue;
ConfigOptionStrings *filament_types;
const Preset * filament_preset_base = m_preset_bundle->filaments.get_preset_base(filament_preset);
if (filament_preset_base == &filament_preset || filament_preset_base == nullptr) {
if (filament_preset_base == &filament_preset) {
filament_types = dynamic_cast<ConfigOptionStrings *>(const_cast<Preset *>(&filament_preset)->config.option("filament_type"));
} else {
filament_types = dynamic_cast<ConfigOptionStrings *>(const_cast<Preset *>(filament_preset_base)->config.option("filament_type"));
+3 -3
View File
@@ -1350,7 +1350,7 @@ void GLGizmoSVG::draw_window()
assert(m_volume_id.valid());
if (m_volume == nullptr ||
m_volume_id.invalid()) {
ImGui::Text("%s", _u8L("Not valid state please report reproduction steps on GitHub").c_str());
ImGui::Text("%s", _u8L("Not valid state please report reproduction steps on github").c_str());
return;
}
@@ -1362,13 +1362,13 @@ void GLGizmoSVG::draw_window()
assert(m_volume->emboss_shape->svg_file.has_value());
if (!m_volume->emboss_shape->svg_file.has_value()){
ImGui::Text("%s", _u8L("Missing SVG file in embossed shape").c_str());
ImGui::Text("%s", _u8L("Missing svg file in embossed shape").c_str());
return;
}
assert(m_volume->emboss_shape->svg_file->file_data != nullptr);
if (m_volume->emboss_shape->svg_file->file_data == nullptr){
ImGui::Text("%s", _u8L("Missing data of SVG file").c_str());
ImGui::Text("%s", _u8L("Missing data of svg file").c_str());
return;
}
-22
View File
@@ -62,7 +62,6 @@
#include "Search.hpp"
#include "BitmapCache.hpp"
#include "GUI_App.hpp"
#include "KeyChord.hpp"
#include <nanosvg/nanosvg.h>
#include <nanosvg/nanosvgrast.h>
@@ -2135,27 +2134,6 @@ bool ImGuiWrapper::want_any_input() const
return io.WantCaptureMouse || io.WantCaptureKeyboard || io.WantTextInput;
}
bool ImGuiWrapper::text_input_active() const
{
const ImGuiID id = ImGui::GetActiveID();
return id != 0 && ImGui::GetInputTextState(id) != nullptr;
}
bool ImGuiWrapper::modal_popup_open() const
{
// A modal stays on the popup stack while its canvas is hidden, so it counts only if the last finished
// frame drew it, which NewFrame() moves from Active to WasActive.
const ImGuiWindow* modal = ImGui::GetTopMostPopupModal();
if (modal == nullptr)
return false;
return ImGui::GetCurrentContext()->WithinFrameScope ? modal->WasActive : modal->Active;
}
bool ImGuiWrapper::owns_key(const KeyChord& chord) const
{
return modal_popup_open() || (chord.edits_text() && text_input_active());
}
ImFontAtlasCustomRect* ImGuiWrapper::GetTextureCustomRect(const wchar_t& tex_id)
{
auto item = m_custom_glyph_rects_ids.find(tex_id);
-7
View File
@@ -35,7 +35,6 @@ struct ImRect;
namespace Slic3r {
namespace GUI {
struct KeyChord;
bool get_data_from_svg(const std::string &filename, unsigned int max_size_px, ThumbnailData &thumbnail_data);
@@ -220,12 +219,6 @@ public:
bool want_keyboard() const;
bool want_text_input() const;
bool want_any_input() const;
// True while an ImGui InputText is active, without the one-frame lag of want_text_input().
bool text_input_active() const;
// True while an ImGui modal popup is on screen.
bool modal_popup_open() const;
// True for every key while a modal is open, and for text-editing keys while an InputText is active.
bool owns_key(const KeyChord& chord) const;
// Optional inputs are used for set up value inside of an optional, with default value
//
+5 -4
View File
@@ -173,7 +173,7 @@ void KBShortcutsDialog::fill_pages()
using Section = ShortcutSection;
if (wxGetApp().is_editor()) {
page(_L("Global"), _L("Available anywhere in the window."), ShortcutContext::Global, {
page(_L("Global"), _L("Available anywhere in the window, even while typing in a text field."), ShortcutContext::Global, {
fixed(Section::SpeedDial, { alt, "1-9, 0" }, L("Run favorite 1 to 10")),
fixed(Section::SpeedDial, { ctrl, "B" }, L("Pin or unpin the selected action")),
// wx cycles notebook pages on Ctrl+Tab, which is Cmd+Tab on macOS and never arrives there.
@@ -438,16 +438,17 @@ ShortcutCaptureDialog::ShortcutCaptureDialog(wxWindow* parent, Shortcut shortcut
SetBackgroundColour(*wxWHITE);
wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL);
// A Global shortcut runs before the focused control gets the key, so its hint names the keys it can use.
// A Global shortcut also runs while a text field has the focus, so its hint names the keys it can use.
const bool global = (shortcut_info(shortcut).contexts & context_bit(ShortcutContext::Global)) != 0;
const wxString advice = global_key_advice();
const wxString typing = _L("Global shortcuts also apply while typing.");
const wxString rule = _L("A key that types a character cannot be a global shortcut.");
m_hint = global ? advice : _L("Esc cancels, Enter confirms.");
m_hint = global ? typing + "\n" + advice : _L("Esc cancels, Enter confirms.");
m_rejection = rule + "\n" + advice;
// Wide enough for each sentence on a line of its own where the translation allows, within limits.
int width = FromDIP(450);
for (const wxString& sentence : { rule, advice }) {
for (const wxString& sentence : { typing, rule, advice }) {
int extent = 0;
GetTextExtent(sentence, &extent, nullptr, nullptr, nullptr, &wxGetApp().normal_font());
width = std::max(width, extent);
Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff. Show more