Compare commits

..
Author SHA1 Message Date
Ian Bassi c5cfe40a1f Update translations: fill empty entries in all languages and fix color picker strings (#16652) 2026-10-19 10:19:06 -03:00
SoftFever e5b6cbf091 Let plugins make HTTPS requests on macOS (#16638)
* Let the bundled Python verify HTTPS certificates on macOS

* Restore SSL_CERT_FILE after the bundled Python override test
2026-10-19 20:06:14 +08:00
Alexandre Folle de Menezes 3b9b871ba6 Standardize the spelling of "G-code", "SVG", "GitHub" and "IP" (#16618) 2026-10-19 09:01:39 -03:00
Surfoo cb2260f057 i18n(fr): update translations (#16591) 2026-10-19 09:00:32 -03:00
Alexandre Folle de Menezes 3fcbb2adf9 Translate color names to pt-BR (#16585) 2026-10-19 08:59:35 -03:00
Sergei 252bbbf9f5 Make the exclude-object outline tolerance a user setting (#16503)
* Make the exclude-object outline tolerance configurable

Signed-off-by: Sergei Rozhkov <zmod@zmod.link>

* fix(config): move exclude_object_tolerance to developer mode

Signed-off-by: Sergei Rozhkov <zmod@zmod.link>

* fix(config): move exclude_object_tolerance to expert mode

Signed-off-by: Sergei Rozhkov <zmod@zmod.link>

* feat(ui): hide exclude_object_tolerance for non-Klipper firmwares

mend

Signed-off-by: Sergei Rozhkov <zmod@zmod.link>

* Test that a larger exclude object tolerance writes fewer outline points

Signed-off-by: Sergei Rozhkov <zmod@zmod.link>

---------

Signed-off-by: Sergei Rozhkov <zmod@zmod.link>
2026-10-19 08:17:20 -03:00
Lam Wei Lun 4c2ae6d202 Keep Auto Drop off for projects saved by 2.4.x (#16633)
2.4.0 to 2.4.2 wrote auto_drop on each build item, and 2.4.x objects
carry no auto_drop attribute in model_settings.config. The importer only
read the object attribute and treated a missing one as on, so floating
objects were dropped to the bed on load and the flag was rewritten as on
on the next save.

Record object ids with an item auto_drop="0" and use that as the fallback
when the object has no attribute. The attribute's presence is now tracked
so the fallback applies only when it is absent. Any item set to off turns
auto drop off for the object, since the flag is shared by all instances.
2026-10-19 16:08:59 +08:00
HanifKoh ac022b56ad Wait for the Close Frame in the Moonraker Status Stream Test (#16635)
The fake printer counts the agent's close frame in its read handler, but
Beast answers the close before that read completes, so stop() can return
and the test can read the counter before it is incremented. On a loaded
runner the check saw 0.

Wait for the counter like the other fake-printer counters do, then keep
the exact-count check.
2026-10-19 16:06:41 +08:00
HanifKoh d3699dd9f6 Keep Python Bytecode out of the Bundled Python Home (#16550)
* Keep Python Bytecode out of the Bundled Python Home

The bundled stdlib ships .pyc files that are stale against their sources:
they are compiled when the Python deps are installed, and the copy into the
app does not keep the sources' mtimes. CPython then recompiles every module
it imports and writes the result next to the source. A drag-installed macOS
app rewrites files inside its own bundle on first launch, so its signature
no longer verifies, and read-only installs (AppImage, Flatpak, Program Files)
recompile the same modules on every launch.

Point pycache_prefix at <data_dir>/python/pycache so the interpreter reads
and writes bytecode there. The bundle is never written, the cache survives
between launches, and the data directory is already a plugin read/write root.

* Drop Cached Bytecode Through the Interpreter in the Module Release Test
2026-10-19 13:48:53 +08:00
HanifKoh a315a96c5c Show "No Available Printers" Instead of an Empty Printer Dropdown (#16611)
* Show "No Available Printers" Instead of an Empty Printer Dropdown

A ComboBox with no items opened a one-row-high popup that DropDown never
paints, leaving a blank strip with stale pixels under the box. Don't open
the list when it is empty, and label the printer box in the print and
send-to-printer dialogs when no printer is available.

* Add "No Available Printers" to the Translation Template
2026-10-19 13:30:36 +08:00
HanifKoh 24cf1c6bcc Fix Crash Selecting a User Printer Whose Parent Preset Is Not Loaded (#16579)
A user preset can inherit a system preset from a vendor that is not
installed. get_preset_base() returns null for it, and
get_selected_preset_base() dereferenced that unconditionally, so picking
the printer crashed while building the shared profiles notification URL.
Fall back to the selected preset itself. The create printer and create
filament dialogs had the same null dereference on such presets.
2026-10-19 13:29:26 +08:00
Kris Austin d16ca97380 Snapmaker U1: set the top cover purifier mode and match Snapmaker's start G-code (#16590)
Port the U1 top cover purifier mode and the related start G-code
changes from Snapmaker's Orca (Snapmaker/OrcaSlicer#504 and
Snapmaker/OrcaSlicer#1005, as of 8931178229).

The start G-code sends SET_PURIFIER_MODE for strong cooling, weak
cooling or keep warm, picked from the lowest softening temperature of
the filaments used through min_vitrification_temperature, with
Snapmaker's thresholds and parameters. It also pre-extrudes the first
tool, and turns the part fan off for probing and back on for nozzle
cleaning.

U1 filament presets use Snapmaker's softening temperatures. U1 presets
that have no U1 version in Snapmaker's Orca use the value Snapmaker
gives that material on the U1.
2026-10-18 21:35:32 -05:00
Kris Austin da4d766d46 Fix Moonraker status stream hanging app exit and missing dead printers (#16604)
The status stream read the websocket synchronously. Beast's
expires_after() only applies to async operations, so when a printer
stopped sending, ws.read() blocked forever, and stop_status_stream()
joined that thread when the app closed. On Windows the stuck process
also froze the next launch through the shared WebView2 browser.

Run the whole stream on one io_context owned by the stream thread, and
have stop_status_stream() post an abort to it. A stop on an open
websocket sends a close frame and waits up to 1 s for the answer.
Keep-alive pings detect a dead printer, and the stream reconnects until
stopped, with the backoff capped at 32 s. When a quiet printer still
answers pings, its last status is sent again. The stream starts under
connect_mutex after a staleness check, and a stop from the stream's own
callback no longer joins its own thread.

The Moonraker tests move into test_moonraker_printer_agent.cpp.
2026-10-18 19:33:58 -05:00
Kris Austin 06665d0558 Post a readable diff of changed custom G-code on profile PRs (#16605)
A profile stores each custom G-code setting as one JSON string, so GitHub
shows a G-code edit as one long changed line and reviewers cannot see
what changed.

Add a gcode-diff command to orca_profile_tool.py. For each changed
profile file it decodes, on both sides, every *_gcode and multiline
string option PrintConfig.cpp defines, the options Tab.cpp edits as
code, and any string value that spans several lines. It prints one
collapsed unified diff per setting as markdown. Files with the same diff
for a setting share one section. Trailing spaces are removed, file names
cannot mention GitHub users, and the output is cut to whole sections to
fit a GitHub comment.

Check profiles runs it between the PR's merge commit and its first
parent and adds the result to its artifact, with a line naming the PR
commit. Post profile check comment posts it under its own marker. It
keeps the comment while the diff is unchanged, edits it in place when
only the commit changed, replaces it when the diff changes, and deletes
it when the PR no longer changes any G-code.

The posting job still runs no PR code. It now posts only when the run
checked the head of the PR the artifact names, which covers the
validation comment too.
2026-10-18 13:33:53 -05:00
Kris Austin 0f59cd5d05 Let Global shortcuts run while typing, except keys that edit text (#16558)
#16505 skipped every Global shortcut while ImGui had the keyboard, so
Ctrl+S, Ctrl+R and the others did nothing while the cursor was in the
text tool's box. Skip only the chords that edit text, and only while a
text field (a wx text entry, a spin control, a web view, or an active
ImGui InputText) has the focus. Skip every Global shortcut while an
ImGui modal is open on the focused canvas.

KeyChord::edits_text() defines the text-editing chords. The Global page
of the shortcuts dialog no longer says these shortcuts apply while
typing.
2026-10-18 13:32:41 -05:00
yw4z c1fb801ff3 Linux: Show display backend of app on Troubleshoot dialog (#16615)
* init

* Update TroubleshootDialog.cpp

* fix build
2026-10-18 22:45:30 +08:00
Lam Wei Lun 1ee0b730a1 Linux: use WebKit's shared-memory transport in hypervisor guests
Web views in VirtualBox (and other virtual GPUs) fail on WebKitGTK >= 2.44
because the guest GL stack cannot back the hardware DMABUF renderer. Setting
WEBKIT_DMABUF_RENDERER_FORCE_SHM=1 fixes it and keeps accelerated compositing,
confirmed in VirtualBox on WebKitGTK 2.54.

Detect a hypervisor guest from the cpuinfo hypervisor flag before GTK init and
set the variable non-replacing, so a user-exported value still wins. Bare metal
is unchanged. The skill references are updated to match.
2026-10-18 04:43:45 +08:00
Kiss Lorand 72ba7a2b35 Fix incorrect layer number in G-code conflict warning (#16588)
Fix layer number in G-code conflict warning

Correct the comparison used to look up a layer by Z height.

The reversed comparison could return the total layer count instead of
the layer corresponding to the reported conflict height.
2026-10-17 23:04:50 +03:00
Downtime 8160f64cae Skip the LAN reconnect after a print to the selected printer (#16271)
After a print, `Plater::print_job_finished` selects the target printer again. When that printer was already the selected LAN printer, `set_selected_machine` disconnected, reset and reconnected it for no reason. Skip the call in that case. Selecting the printer by hand still reconnects it, and cloud printers are unchanged.
2026-10-17 13:50:38 -05:00
fd28f6c189 Fix Ctrl+A (select current plate) and Delete All skipping objects that no longer fit or while one of instances not on the bed (#15895)
* Fix Ctrl+A (select current plate) skipping objects that no longer fit the bed

Selection::add_curr_plate() used PartPlate::contain_instance_totally(),
which excludes any instance flagged in instance_outside_set -- an
instance too big for, or dragged outside, the bed. Since that is
exactly the kind of object a user reaches for Ctrl+A to move or
delete, it silently dropped out of "select all" (and out of the
selection entirely when it was the only object on the plate).

Selection::remove_curr_plate(), used for "delete all on this plate",
already does this correctly with the plain membership check,
PartPlate::contain_instance() (no outside-bed exclusion). Match it.

Likely reachable since #12236 changed Ctrl+A's target from "all
plates" to "current plate", switching it onto this pre-existing
function.

Fixes OrcaSlicer/OrcaSlicer#15888

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Trim comment verbosity per review feedback

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix behaviour for instances

* simplify code

* Update Selection.cpp

* Update Selection.cpp

* update

* update

---------

Co-authored-by: Fernando Marino <f.marino@rheagroup.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: yw4z <ywsyildiz@gmail.com>
2026-10-17 02:21:41 +03:00
Kris Austin 9b2fcaa53a fix: "bad allocation" when opening a 2.4 project with a custom filament (#16569) 2026-10-16 20:12:24 -03:00
Ian BassiandRodrigo Faselli 0acf1e6680 Fix wall order on narrow tube walls and under outer brims, and test wall order and direction (#16421)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-10-16 18:56:55 -03:00
Ján Mišenko c6804fbb68 fix: print statistics placeholders fail in custom G-code (#16348)
The G-code editor lists the Print Statistics placeholders for every
custom G-code field, but total_weight, used_filament, total_cost,
print_time, normal_print_time, silent_print_time,
total_wipe_tower_cost and total_wipe_tower_filament were only defined
for filename_format. Using one in custom G-code, including the File
header G-code, stopped slicing with "Not a variable name".

They are known only once the whole G-code is generated, so they use the
reserved-tag mechanism of print_time_sec and used_filament_length: the
placeholder parser gets a tag such as @TOTAL_WEIGHT@ and
GCodeProcessor::run_post_process() replaces it with the same value
filename_format gets, with numbers written to two decimals. The
per-tag replacement loops are folded into one helper. The running
extruded_* totals are unchanged.

Fixes #15320
Fixes #7870
2026-10-16 16:30:05 -05:00
Kiss Lorand 6d40ec93f6 Fix missing tree support paths and misplaced extra walls (#16549) 2026-10-16 18:04:57 -03:00
132 changed files with 22620 additions and 13032 deletions

No files matched your search

+4 -4
View File
@@ -1,6 +1,6 @@
--- ---
name: orca-translations 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 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. 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.
--- ---
# OrcaSlicer translations # 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 `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 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), [localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md),
whose source is `developer_reference/localization_glossary.tsv` in the **OrcaSlicer_WIKI** repository. whose source is the per-language files in `developer_reference/glossary/` (`terms.tsv`, `languages.txt` and one `<lang>.tsv`) in the **OrcaSlicer_WIKI** repository.
A translation pass therefore touches two repositories: the catalogs here, and the glossary there. 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). 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; - anything left empty, and why;
- the glossary rows added or filled. - the glossary rows added or filled.
The catalogs (and `OrcaSlicer.pot`) and the glossary (`localization_glossary.tsv` and `.md`) are The catalogs (and `OrcaSlicer.pot`) and the glossary (`developer_reference/glossary/*.tsv` and `localization_glossary.md`) are
separate changes in separate repositories. The wiki change should land with, or before, the catalog separate changes in separate repositories. The wiki change should land with, or before, the catalog
change. Commit or open PRs only when asked. 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: When this skill and the checkouts disagree, the checkouts win:
- `scripts/run_gettext.sh` / `.bat` for extraction and merging; - `scripts/run_gettext.sh` / `.bat` for extraction and merging;
- `localization/i18n/list.txt` for the scanned sources; - `localization/i18n/list.txt` for the scanned sources;
- the wiki's `localization_guide.md` and `localization_glossary.md` / `.tsv` for the rules and terms; - the wiki's `localization_guide.md` and `localization_glossary.md` and `glossary/*.tsv` for the rules and terms;
- `AGENTS.md` (Localization & translations) for the repository's conventions. - `AGENTS.md` (Localization & translations) for the repository's conventions.
## Editing this skill ## Editing this skill
@@ -28,13 +28,14 @@ import sys
import tempfile import tempfile
import time import time
import unicodedata import unicodedata
import urllib.error
import urllib.request import urllib.request
from pathlib import Path from pathlib import Path
REPO = Path(__file__).resolve().parents[4] REPO = Path(__file__).resolve().parents[4]
I18N = REPO / "localization" / "i18n" I18N = REPO / "localization" / "i18n"
SKIP_LANGS = {"en"} # the English catalog is intentionally empty SKIP_LANGS = {"en"} # the English catalog is intentionally empty
GLOSSARY_TSV = Path("developer_reference") / "localization_glossary.tsv" GLOSSARY_DIR = Path("developer_reference") / "glossary" # terms.tsv, languages.txt and one <lang>.tsv per language
GLOSSARY_MD = Path("developer_reference") / "localization_glossary.md" GLOSSARY_MD = Path("developer_reference") / "localization_glossary.md"
MISSING = {"", "—"} MISSING = {"", "—"}
RAW_URL = "https://raw.githubusercontent.com/OrcaSlicer/OrcaSlicer_WIKI/main/" RAW_URL = "https://raw.githubusercontent.com/OrcaSlicer/OrcaSlicer_WIKI/main/"
@@ -252,30 +253,40 @@ def all_langs():
def find_wiki(arg): def find_wiki(arg):
for cand in [arg, os.environ.get("ORCA_WIKI_DIR"), REPO.parent / "OrcaSlicer_WIKI", REPO.parent / "OrcaSlicer.wiki"]: for cand in [arg, os.environ.get("ORCA_WIKI_DIR"), REPO.parent / "OrcaSlicer_WIKI", REPO.parent / "OrcaSlicer.wiki"]:
if cand and (Path(cand) / GLOSSARY_TSV).exists(): if cand and (Path(cand) / GLOSSARY_DIR / "terms.tsv").exists():
return Path(cand) return Path(cand)
return None return None
def _download(rel): def _download(rel, required=True):
cache = Path(tempfile.gettempdir()) / ("orca_glossary_" + rel.name) cache = Path(tempfile.gettempdir()) / "orca_glossary" / rel
if not cache.exists() or time.time() - cache.stat().st_mtime > 3600: if not cache.exists() or time.time() - cache.stat().st_mtime > 3600:
with urllib.request.urlopen(RAW_URL + rel.as_posix(), timeout=30) as r: try:
cache.write_bytes(r.read()) 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)
return cache return cache
class Glossary: class Glossary:
def __init__(self, wiki_arg=None, quiet=False): """The glossary: rows of [English, Description, <one translation per language loaded>]."""
def __init__(self, wiki_arg=None, quiet=False, langs=None):
self.wiki = find_wiki(wiki_arg) self.wiki = find_wiki(wiki_arg)
self.langs = langs # only these languages' files are read (and downloaded); None means all
if self.wiki: if self.wiki:
self.tsv, self.md = self.wiki / GLOSSARY_TSV, self.wiki / GLOSSARY_MD self.dir, self.md = self.wiki / GLOSSARY_DIR, self.wiki / GLOSSARY_MD
if not quiet: if not quiet:
self.report_clone_state() self.report_clone_state()
else: else:
if not quiet: if not quiet:
print("note: no OrcaSlicer_WIKI clone found; using the published glossary read-only", file=sys.stderr) print("note: no OrcaSlicer_WIKI clone found; using the published glossary read-only", file=sys.stderr)
self.tsv, self.md = _download(GLOSSARY_TSV), _download(GLOSSARY_MD) self.dir, self.md = _download(GLOSSARY_DIR / "terms.tsv").parent, _download(GLOSSARY_MD)
self.load() self.load()
def report_clone_state(self): def report_clone_state(self):
@@ -290,11 +301,23 @@ class Glossary:
if behind and behind != "0": 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) 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): def load(self):
rows = [l.split("\t") for l in self.tsv.read_text(encoding="utf-8-sig").splitlines() if l.strip()] terms = [l.split(" ") for l in self._read("terms.tsv")[1:] if l.strip()]
self.header = rows[0] langs = [l.strip() for l in self._read("languages.txt") if l.strip()]
# "—" marks a language with no established rendering yet langs = [l for l in langs if self.langs is None or l in self.langs]
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.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()
self.matchers = [] self.matchers = []
for row in self.rows: for row in self.rows:
base = re.sub(r"\s*\([^)]*\)\s*$", "", row[0]).strip() base = re.sub(r"\s*\([^)]*\)\s*$", "", row[0]).strip()
@@ -494,7 +517,7 @@ def similar_entries(c, e, pool, cutoff=0.72):
def cmd_pending(a): def cmd_pending(a):
c = Catalog(catalog_path(a.lang)) c = Catalog(catalog_path(a.lang))
g = Glossary(a.wiki) g = Glossary(a.wiki, langs=[a.lang])
todo = [e for e in c.active() if needs_translation(e, c.nplurals, a.include_fuzzy)] 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:] 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 pool = [(x.msgid, x.msgstrs()[0], x.obsolete) for x in c.entries
@@ -533,7 +556,7 @@ def cmd_pending(a):
def cmd_apply(a): def cmd_apply(a):
c = Catalog(catalog_path(a.lang)) c = Catalog(catalog_path(a.lang))
g = None if a.no_glossary else Glossary(a.wiki, quiet=True) g = None if a.no_glossary else Glossary(a.wiki, quiet=True, langs=[a.lang])
data = json.loads(Path(a.input).read_text(encoding="utf-8")) data = json.loads(Path(a.input).read_text(encoding="utf-8"))
items = data["entries"] if isinstance(data, dict) else data items = data["entries"] if isinstance(data, dict) else data
idx = c.index() idx = c.index()
@@ -588,7 +611,7 @@ def find_msgfmt():
def cmd_check(a): def cmd_check(a):
g = None if a.no_glossary else Glossary(a.wiki, quiet=True) g = None if a.no_glossary else Glossary(a.wiki, quiet=True, langs=a.lang or None)
msgfmt = find_msgfmt() msgfmt = find_msgfmt()
status = 0 status = 0
for lang in a.lang or all_langs(): for lang in a.lang or all_langs():
@@ -646,7 +669,7 @@ def cmd_lookup(a):
def cmd_glossary(a): def cmd_glossary(a):
g = Glossary(a.wiki) g = Glossary(a.wiki, langs=[a.lang])
col = g.column(a.lang) col = g.column(a.lang)
if col is None: if col is None:
print(f"'{a.lang}' has no glossary column yet; add one with glossary-set --set {a.lang}=...") print(f"'{a.lang}' has no glossary column yet; add one with glossary-set --set {a.lang}=...")
@@ -733,6 +756,17 @@ 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 ''}") 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): def cmd_glossary_set(a):
wiki = find_wiki(a.wiki) wiki = find_wiki(a.wiki)
if not wiki: if not wiki:
@@ -741,8 +775,21 @@ def cmd_glossary_set(a):
f" {a.term}: " + ", ".join(a.set) + "\n" f" {a.term}: " + ", ".join(a.set) + "\n"
"Clone https://github.com/OrcaSlicer/OrcaSlicer_WIKI next to this repository, or pass --wiki.", file=sys.stderr) "Clone https://github.com/OrcaSlicer/OrcaSlicer_WIKI next to this repository, or pass --wiki.", file=sys.stderr)
return 2 return 2
tsv = wiki / GLOSSARY_TSV gdir = wiki / GLOSSARY_DIR
lock = tsv.with_suffix(".lock") 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"
for _ in range(100): for _ in range(100):
try: try:
fd = os.open(lock, os.O_CREAT | os.O_EXCL) fd = os.open(lock, os.O_CREAT | os.O_EXCL)
@@ -752,50 +799,51 @@ def cmd_glossary_set(a):
else: else:
sys.exit(f"could not lock {lock}; remove it if no other run is active") sys.exit(f"could not lock {lock}; remove it if no other run is active")
try: try:
data = tsv.read_bytes() terms = (gdir / "terms.tsv").read_text(encoding="utf-8-sig").splitlines()
bom = data.startswith(b"\xef\xbb\xbf") new = not any(l.split("\t")[0] == a.term for l in terms[1:])
text = data.decode("utf-8-sig") if new and not a.description:
nl = "\r\n" if "\r\n" in text else "\n" sys.exit("a new term needs --description (one sentence saying what it is)")
lines = text.splitlines() # an established translation is only replaced on request
header = lines[0].split("\t") conflicts = []
values = {} for lang, v in values.items():
for s in a.set: for line in (gdir / f"{lang}.tsv").read_text(encoding="utf-8-sig").splitlines()[1:]:
lang, _, val = s.partition("=") cells = line.split("\t")
if lang not in header: if cells[0] == a.term and len(cells) > 2 and cells[2].strip() not in MISSING and cells[2] != v:
sys.exit(f"no glossary column '{lang}'") conflicts.append(f"{lang}: '{cells[2]}' -> '{v}'")
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: if conflicts and not a.replace:
sys.exit("established translations differ (pass --replace, and update the catalogs to match):\n " + "\n ".join(conflicts)) sys.exit("established translations differ (pass --replace, and update the catalogs to match):\n " + "\n ".join(conflicts))
for i, v in values.items():
row[i] = v def edit_terms(lines):
lines[row_i] = "\t".join(row) for i, l in enumerate(lines):
tsv.write_bytes((b"\xef\xbb\xbf" if bom else b"") + (nl.join(lines) + nl).encode("utf-8")) 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))
finally: finally:
os.close(fd) os.close(fd)
os.unlink(lock) os.unlink(lock)
print(f"glossary: {'added' if new else 'updated'} '{a.term}'") print(f"glossary: {'added' if new else 'updated'} '{a.term}'")
gen = wiki / "generate_glossary.py" r = run_gen("--update")
if gen.exists(): print((r.stdout + r.stderr).strip())
r = subprocess.run([sys.executable, str(gen), "--update"], cwd=wiki, capture_output=True, text=True) return r.returncode
print((r.stdout + r.stderr).strip())
return r.returncode
return 0
def main(): def main():
@@ -574,7 +574,8 @@ 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, 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 `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 `WEBKIT_FORCE_DMABUF_RENDERER=1` (non-replacing): the proprietary driver cannot back WebKit's hardware
DMABUF renderer. On the default path it forces `GDK_BACKEND=x11` when wx lacks EGL and `WAYLAND_DISPLAY` 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`
is set, and sets `WEBKIT_DISABLE_COMPOSITING_MODE=1` (non-replacing) inside that branch when the linked 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 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 that runs through XWayland, which `DISPLAY` and `WAYLAND_DISPLAY` alone do not tell you (a native
@@ -568,13 +568,18 @@ vue-resize workaround script and sets a persistent SQLite cookie store on the vi
## WebKitGTK on Linux sessions ## WebKitGTK on Linux sessions
**Mechanism.** `CLI::run` (`src/OrcaSlicer.cpp`, under `#ifdef __WXGTK__`, before GTK initialises) **Mechanism.** `CLI::run` (`src/OrcaSlicer.cpp`, under `#ifdef __WXGTK__`, before GTK initialises)
sets the NVIDIA DMABUF workaround and then branches on `GDK_BACKEND`: sets the DMABUF workarounds and then branches on `GDK_BACKEND`:
- **Both paths**, before the split: when `/proc/driver/nvidia/version` exists and the linked WebKitGTK - **Both paths**, before the split: when `/proc/driver/nvidia/version` exists and the linked WebKitGTK
honours the switch (>= 2.44, `WebView::SupportsForceSharedMemory()`), it sets honours the switch (>= 2.44, `WebView::SupportsForceSharedMemory()`), it sets
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and `WEBKIT_FORCE_DMABUF_RENDERER=1` non-replacing. The `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 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 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. 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 - **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 `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 `WEBKIT_DISABLE_COMPOSITING_MODE=1` non-replacing when the linked WebKitGTK predates 2.42
@@ -590,9 +595,9 @@ Runtime backend detection elsewhere uses Orca's `is_running_on_wayland()` / `is_
(see `references/platforms.md`), not these environment variables. (see `references/platforms.md`), not these environment variables.
**Pitfall.** **Pitfall.**
- **Rule:** On NVIDIA reach for `WEBKIT_DMABUF_RENDERER_FORCE_SHM` before - **Rule:** On NVIDIA and hypervisor guests reach for `WEBKIT_DMABUF_RENDERER_FORCE_SHM` before
`WEBKIT_DISABLE_COMPOSITING_MODE`. **Why:** the proprietary driver cannot back WebKit's hardware `WEBKIT_DISABLE_COMPOSITING_MODE`. **Why:** the proprietary driver and virtual GPUs cannot back WebKit's
DMABUF renderer, leaving web views blank or slow; FORCE_SHM keeps accelerated compositing and only 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, 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 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 substitute — it is read first, leaves the transport set empty (no compositing from 2.44 on, where the
+25
View File
@@ -232,6 +232,31 @@ jobs:
mkdir -p ${{ runner.temp }}/profile-check-results mkdir -p ${{ runner.temp }}/profile-check-results
echo "${{ github.event.pull_request.number }}" > ${{ runner.temp }}/profile-check-results/pr_number.txt 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 - 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') }} 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: | run: |
+58 -3
View File
@@ -38,6 +38,7 @@ jobs:
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }} GH_REPO: ${{ github.repository }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: | run: |
if [ ! -f pr_number.txt ]; then if [ ! -f pr_number.txt ]; then
echo "No pr_number.txt in artifact, skipping." echo "No pr_number.txt in artifact, skipping."
@@ -49,18 +50,72 @@ jobs:
echo "Invalid PR number: $PR_NUMBER" echo "Invalid PR number: $PR_NUMBER"
exit 1 exit 1
fi 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. # Delete prior comments matching the marker (from check_profiles.yml) or the legacy heading.
OLD_IDS=$(gh api --paginate "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" \ OLD_IDS=$(printf '%s\n' "$COMMENTS" | jq -r 'select((.body | startswith("<!-- profile-validation-comment -->")) or (.body | startswith("## :x: Profile Validation Errors"))) | .id')
--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 for comment_id in $OLD_IDS; do
echo "Deleting outdated profile-validation comment ${comment_id}" echo "Deleting outdated profile-validation comment ${comment_id}"
gh api -X DELETE "repos/${GH_REPO}/issues/comments/${comment_id}" || true gh api -X DELETE "repos/${GH_REPO}/issues/comments/${comment_id}" || true
done done
# Post a new comment only when validation failed (pr_comment.md present). # 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 if [ -f pr_comment.md ]; then
gh pr comment "$PR_NUMBER" --body-file pr_comment.md gh pr comment "$PR_NUMBER" --body-file pr_comment.md || failed=1
else else
echo "Validation succeeded; cleaned up prior comments without posting." echo "Validation succeeded; cleaned up prior comments without posting."
fi 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 ### 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. - 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 (`localization_glossary.tsv`, then regenerate) so they stay in sync. - 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.
- 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. - 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. - Reuse one template per recurring message shape (`Failed to connect to …`, `Are you sure you want to …?`), even where the English wording varies.
+17 -7
View File
@@ -16,6 +16,8 @@ else()
endif() endif()
endif() endif()
set(_openssl_dir ${DESTDIR})
if(WIN32) if(WIN32)
set(_openssl_msvc_env CC=cl CXX=cl RC=rc CL=/FS) set(_openssl_msvc_env CC=cl CXX=cl RC=rc CL=/FS)
# OpenSSL's perl Configure honors the CC environment variable, but the # OpenSSL's perl Configure honors the CC environment variable, but the
@@ -43,6 +45,14 @@ if(WIN32)
else() else()
if(APPLE) if(APPLE)
set(_conf_cmd export MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} && ./Configure -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}) 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() else()
# A static library that is embedded into a shared object must not export # 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 # its symbols. On Linux the running process also loads the system OpenSSL
@@ -81,7 +91,7 @@ ExternalProject_Add(dep_OpenSSL
# URL_HASH SHA256=8c776993154652d0bb393f506d850b811517c8bd8d24b1008aef57fbe55d3f31 # URL_HASH SHA256=8c776993154652d0bb393f506d850b811517c8bd8d24b1008aef57fbe55d3f31
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/OpenSSL DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/OpenSSL
CONFIGURE_COMMAND ${_conf_cmd} ${_cross_arch} CONFIGURE_COMMAND ${_conf_cmd} ${_cross_arch}
"--openssldir=${DESTDIR}" "--openssldir=${_openssl_dir}"
"--prefix=${DESTDIR}" "--prefix=${DESTDIR}"
# OpenSSL's linux-x86_64 target sets multilib=64, so it installs to # OpenSSL's linux-x86_64 target sets multilib=64, so it installs to
# <prefix>/lib64 while every other dep uses <prefix>/lib. CPython's # <prefix>/lib64 while every other dep uses <prefix>/lib. CPython's
@@ -115,12 +125,12 @@ ExternalProject_Add_Step(dep_OpenSSL install_cmake_files
WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}"
) )
if (NOT WIN32 AND NOT APPLE) if (NOT WIN32)
# OpenSSL's object rules do not depend on CFLAGS, so reconfiguring it (for # OpenSSL's object rules do not depend on the configured flags or paths, so
# example to add -fvisibility=hidden) relinks the archives from stale # reconfiguring it (a new compiler flag, a different --openssldir) relinks
# objects instead of recompiling them, and the change silently has no # the archives from stale objects instead of recompiling them, and the
# effect. Drop the objects whenever this recipe changes so the next build # change silently has no effect. Drop the objects whenever this recipe
# actually recompiles them. # changes so the next build actually recompiles them.
ExternalProject_Get_Property(dep_OpenSSL SOURCE_DIR) ExternalProject_Get_Property(dep_OpenSSL SOURCE_DIR)
ExternalProject_Add_Step(dep_OpenSSL clean_objects ExternalProject_Add_Step(dep_OpenSSL clean_objects
DEPENDEES configure DEPENDEES configure
+5 -4
View File
@@ -365,10 +365,11 @@ if(TARGET dep_ZLIB)
add_dependencies(dep_python3 dep_ZLIB) add_dependencies(dep_python3 dep_ZLIB)
endif() endif()
if (NOT WIN32 AND NOT APPLE) if (NOT WIN32)
# CPython's Makefile rules for _ssl and _hashlib depend only on their own # _ssl and _hashlib embed the static OpenSSL built in deps/ (Windows uses
# sources, not on the OpenSSL archives, so a rebuilt OpenSSL does not make # CPython's prebuilt OpenSSL instead). Their Makefile rules depend only on
# them relink and they keep the previous symbols. On an incremental tree, # 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,
# drop the built modules and relink them against the current OpenSSL; a # 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 # 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. # no-op once PGO has run, so sharedmods is invoked explicitly.
-239
View File
@@ -1,239 +0,0 @@
# ChromaStack - High Level Design
## Purpose and scope
ChromaStack prints a colour picture on a single face of a model: a few filaments are printed one on
top of another, the printer swaps filament at fixed layer boundaries, and the *height* of every
pixel decides how many of those layers reach it. Filament is translucent, so a pixel that stops
part-way through a run shows a blend of that run's filament and whatever is under it, which is
where the colours come from - far more of them than there are filaments loaded.
This is that model inside OrcaSlicer, as a mode of a texture displacement layer. The picture and
the filament stack are the input; the output is a grayscale height map and a set of filament swaps.
Nothing about it is a new pipeline: the height map is handed to the layer the user painted, so the
existing projection, preview and bake turn it into real geometry, and the swaps are handed to the
object's height ranges, which is the slicer's own way of printing a Z band in another filament.
It does not import other filament-painting tools' project files, and it does not choose the filaments or
their order - that is the user's artistic decision, and the panel exists to make its result
visible before the print.
## The colour model
`libslic3r/ChromaStack.hpp` holds the whole of it, free of any GUI dependency, which is what lets the
solve run against plain image bytes and be unit tested.
A **stack** (`ChromaStack`) is a list of **bands** (`ChromaBand`), bottom-up: a filament and
how many layers it gets. Alongside them it carries the print's layer height, which the colours
depend on directly, and the **base**: how many layers of solid backing the picture sits on.
The base is not a band. It is a measurement of the model - everything below the painted face, often
a hundred layers or more on a tall part - while a band is a handful of layers the user chooses. It
prints in the bottom band's filament, because that is what is loaded when the print starts, so it
extends that band's height range down to the first layer instead of being a swap of its own. Only
the bands are quantized into the height map, which is why the 255-layer limit
(`CHROMA_MAX_RELIEF_LAYERS`) bounds the picture and not the stack.
Each filament carries its **transmission distance** (TD), the usual measure for how deep
light gets into it: about 1 for an opaque filament, 4 to 7 for a usefully translucent one, 8 and
above for a nearly clear one. TD lives in the filament preset, as
`filament_transmission_distance` - it is a property of the material, so it belongs with the
material and ships with a profile rather than being retyped per project. It has no effect on
slicing.
`chroma_palette()` turns a stack into the colour it shows at every possible column height, which
is all the solver ever needs. The model is Beer-Lambert absorption, layer by layer: a layer of
thickness `h` in a filament of transmission distance `TD` passes
T = exp(-h / (CHROMA_TD_OPAQUE_FRACTION * TD / 3))
of the light from under it and shows its own colour with the rest, so walking up from an opaque
base,
C_k = (1 - T_k) * c_k + T_k * C_(k-1), C_0 = c_1
The constants say what TD means in physical terms: a filament stops showing what is behind it at
about a tenth of its TD number (`CHROMA_TD_OPAQUE_FRACTION`), and three e-folding lengths of
absorption pass 5% of the light, which is where the eye stops seeing through. Those are calibrated
against how the community's TD step prints behave, not derived from first principles - TD is an
empirical scale.
How fast that happens is the single most useful fact about a stack, and `chroma_layers_to_opaque()`
is it: a filament hides what is under it at about a tenth of its TD, so how many *layers* that takes
depends entirely on how thick they are. TD 4 covers in two layers at 0.2 mm and in five at 0.08 mm.
Below about two layers a band has no intermediate colours at all - the picture can only use the
filament or not use it - which is why an otherwise sound stack prints as flat steps at a coarse
layer height. The panel reports the number under the palette ramp, and warns when it falls below
two, because nothing else makes it visible: the filaments look fine, the bands look long enough, and
the result looks like the colour model is wrong when it is the layer height.
How many of a picture's layers actually earn their place is a second question, and
`chroma_distinct_levels()` answers it: a band eventually hides what is under it, and another layer of
it then shows the colour the one below already showed. It is measured against the palette rather than
against TD, because how long a band keeps paying depends on what is under it as much as on the
filament - a light band over a dark one is finished in four or five layers, while a dark one over a
light one goes on moving the colour perceptibly well past the depth at which it is nominally opaque,
since the last few percent of light coming through still shifts a near-black colour. `Trim` shortens
each band to its last layer that still changes anything.
Beer-Lambert is also the only model that stays self-consistent applied per layer: transmittance
multiplies, so a run of n layers of one filament passes `exp(-n h / d)` whether it is composited in
one step or n, and the palette therefore does not depend on how the stack happens to be cut into
bands. A unit test asserts exactly that. Compositing is done on linear light, because the quantity
being attenuated is light rather than an sRGB code.
The base is assumed opaque. That is what makes a column's colour depend only on the layers above it,
and it is why the panel warns when the base is thin.
## The solve
`chroma_solve()` maps each pixel of the picture to the column height whose palette colour is
closest to it, perceptually: both sides go to CIELAB and the match is a CIE76 distance. A 1-D
palette of a few dozen entries makes the extra accuracy of CIEDE2000 hard to see and the cost of it
easy to feel - several hundred million comparisons for a large picture - so the cheaper metric is
the deliberate choice here, unlike the texture colour quantizer, which matches against a
two-dimensional palette of filament mixes and uses a precomputed CIEDE2000 cube.
Optional Floyd-Steinberg error diffusion, serpentine, trades the bands a photograph shows across a
sky or a cheek for fine stipple. Off by default, since stipple is the artifact rather than the cure
for flat-coloured artwork.
Three things come back: the per-pixel layer count, the same thing as an 8-bit grayscale height map
(0 at the base, 255 at the top of the stack), and the palette colours chosen, as an RGB image. The
quantization belongs to the solve rather than to the displacement because the relief's steps have
to land on layer boundaries, and a grey level is the only channel the displacement reads.
The solve also reports the mean and worst colour difference it had to accept. That is the only
honest answer to "will this stack reproduce my picture": under about 5 is a good match, over about
15 means the filaments cannot reach the picture's colours and no number of layers will help.
## How it attaches to a texture displacement layer
A ChromaStack layer is an ordinary `TextureDisplacementLayer` whose height map is computed rather than
imported:
- `chroma_source` keeps the picture the user chose, so the solve can be redone whenever the stack,
the layer height or the picture changes;
- `chroma` is the stack;
- `image_data` - the same field every other layer uses - holds whatever the last solve produced.
So everything downstream needs no changes at all and knows nothing about colour stacks. It also
means the solve writes `depth_mm` (the relief is exactly as tall as the layers above the base) and
pins `invert`, `midlevel` and `smoothing`: blurring a solved height map moves its steps off the
layer boundaries they were solved for.
Colour by filament swap and the layer's own `color_enabled` (one filament per triangle, via
`mmu_segmentation_facets`) are mutually exclusive, and the panel enforces it. They are two
different ways of colouring the same surface, and painting the relief per triangle would hide the
very stack the heights were solved against.
## The filament swaps
`chroma_ranges()` turns the bands into Z bands in the object's own coordinates, merging any two
adjacent bands that share a filament, since nothing is swapped between them. The panel writes them
onto the object as `layer_config_ranges`, each range carrying `extruder` - which
`apply_to_print_region_config()` spreads over every feature filament the range does not otherwise
override, i.e. the whole band prints in that filament - and `layer_height`, both because the solve
is only valid at the height it was run at and because `layer_height_profile_from_ranges()` reads
that option without checking it is there.
Height ranges rather than colour-change custom G-code: they are object-relative, so they survive
the object being moved, they are saved in the project file, and they appear in the object list where
the user can see and edit them. The cost is that an object can only have one set of them, so
applying a stack replaces whatever ranges the object had, with a confirmation when it had any.
`ObjectList::rebuild_layer_ranges()` mirrors them in the object tree without touching the
selection - changing the selection while a gizmo is open tears the gizmo down under the user.
## Geometry the model assumes
A filament swap happens at one Z across the whole plate, and the relief grows along the painted
surface's normal. The two agree only where that surface is horizontal, so:
- the painted patch's height above the bottom of the object is what the base layer count has to
match, and the panel measures it from the patch and offers the number rather than leaving the user
to work it out;
- a patch that is not flat in Z gets a warning, because its colours will only be right where it is
level. The relief itself is still computed, since a gently domed plaque is a legitimate thing to
print and only the user can judge how much error is acceptable.
The model itself is the user's: the stack needs a slab of the base thickness with a face to paint,
and nothing here creates or thickens geometry.
## Seeing the colours
The colours are blends of translucent filaments. No single filament makes them, so they cannot be
expressed as the per-triangle filament assignment the 3D scene paints with, and there is nothing for
the ordinary view to draw. They exist in two places: the sliced preview, which shows the filament
bands as they print, and an overlay the gizmo draws over whatever surface is on screen.
That overlay is a shader, `resources/shaders/*/chromastack.*`, and it reads the colour from each
fragment's own **print Z** rather than from the height map or from anything in the mesh. A filament
swap happens at one Z across the whole plate, so the layer a point sits in is what decides its
colour whatever the picture says. Two things follow. A band edge lands exactly on the layer boundary
however coarse the triangles are - a per-triangle colouring puts it wherever the mesh happens to
have an edge. And it keeps working after a bake, when the relief is real geometry, the paint has
been cleared and the height map is attached to nothing on screen; the overlay is built from the live
preview's displaced mesh before a bake and from the volume's own mesh after one.
The stack's colours reach the shader as a texture of one texel per printed layer, because a base can
be several hundred layers and no uniform array holds that. The mesh carries nothing about the stack,
so editing the stack costs a palette upload rather than a mesh rebuild.
On top of the colour the shader tries to look like printed plastic, because a flat shaded colour
reads as a rendering and the whole point of the overlay is judging a print:
- the terminator is **wrapped** and the grazing angles **glow**, both scaled by the transmission
distance of the filament on top - filament translucent enough to be worth stacking scatters light
rather than reflecting it, and an opaque stack correctly gets neither;
- the walls carry **layer lines**, a groove at every layer boundary that also modulates the
specular, so a wall glints in stripes as a printed one does. Terraces get none: a terrace is one
layer's top surface and has no lines to show.
Both effects are driven by the fragment's height, so they are free of any texture or UV, and both
fade out through `fwidth` once a pixel covers more than about half a layer - lines finer than a
pixel are noise, and a colour read from one layer out of the dozen under a pixel flickers as the
camera moves. The palette lookup slides off the texel centres over the same range so the linear
filter averages the bands a pixel covers instead of picking one of them.
The overlay is drawn pulled forward and without depth writes, the same pass the painted-colour
overlay uses, since it is the same surface as whatever is underneath it.
## What the panel shows
The kind of a layer is the layer's own, chosen when it is added and fixed from then on: **Add layer**
offers *Relief layer* or *ChromaStack picture*, and the card carries its kind as a chip. There is no
panel-wide mode, so one object can hold a picture on its face and a knurl on its rim, and a card
never shows a control that cannot do anything on it.
What the two cards do not share:
| Control | Relief card | ChromaStack card |
| --- | --- | --- |
| Depth | slider | the solve writes it; the card prints the result |
| Tile size | repeats the pattern | **Picture size**, the same number named for what it does |
| Midlevel, Smoothing, Invert, Edge fade | present | absent: each would move the steps off the layer boundaries |
| Colors, Mix filaments, Denoise | one filament per triangle | absent: swaps are how this card colours |
| Tile, tile method | present | absent: one copy, never tiled |
| Blend | four modes | absent: it is the top layer and replaces what is under it |
| Picture, prediction, stack, palette, match, Dither, Solve, Apply swaps | absent | present |
A ChromaStack card has no grip and cannot be reordered: its colours come from swapping filament at a
height, so anything printed above it would bury them. An object may hold only one, refused when the
layer is added rather than when the swaps are applied, because the swaps are the object's own height
ranges and there is one set of those.
The card itself, top to bottom: what it is in one line, the picture and the predicted print side by
side at the same size, the match figures, the base with the measurement from the painted face offered
beside it, a row per band - swatch, filament, layer count, TD - the colours the stack reaches as a
ramp, the totals, dither with the layer height it is solved against, and the two actions. Base and
band counts are typed fields rather than sliders: a base is measured in hundreds of layers and a band
in single digits, and dragging past the end of a slider's range is how a stack silently loses its
backing.
The ramp is computed from the stack alone, before any solve. It is what answers "can these filaments
print this picture" without spending a solve to find out they cannot: a short or flat ramp means the
filaments are too alike, or too opaque, to give the picture anything to work with.
A per-slot flag records that the stack has been edited since the last solve, because a stale height
map looks exactly like a good one.
+7
View File
@@ -53,6 +53,13 @@ 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, `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. 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 ## Which event a chord matches
Letters, digits and special keys match on `wxEVT_KEY_DOWN`. Its key codes do not depend Letters, digits and special keys match on `wxEVT_KEY_DOWN`. Its key codes do not depend
+58 -21
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-15 17:40+0800\n" "POT-Creation-Date: 2026-10-19 09:48-0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -1637,16 +1637,16 @@ msgstr ""
msgid "Stroke of shape (%1%) contains unsupported: %2%." msgid "Stroke of shape (%1%) contains unsupported: %2%."
msgstr "" msgstr ""
msgid "Not valid state please report reproduction steps on github" msgid "Not valid state please report reproduction steps on GitHub"
msgstr "" msgstr ""
msgid "No embossed file" msgid "No embossed file"
msgstr "" msgstr ""
msgid "Missing svg file in embossed shape" msgid "Missing SVG file in embossed shape"
msgstr "" msgstr ""
msgid "Missing data of svg file" msgid "Missing data of SVG file"
msgstr "" msgstr ""
msgid "Face the camera" msgid "Face the camera"
@@ -4579,7 +4579,7 @@ msgstr ""
msgid "Unknown C++ exception." msgid "Unknown C++ exception."
msgstr "" msgstr ""
msgid "Processing G-Code from previous file…" msgid "Processing G-code from previous file…"
msgstr "" msgstr ""
msgid "Slicing complete" msgid "Slicing complete"
@@ -6049,7 +6049,7 @@ msgstr ""
msgid "Size:" msgid "Size:"
msgstr "" msgstr ""
#, possible-boost-format #, possible-c-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)." msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "" msgstr ""
@@ -10657,6 +10657,9 @@ msgstr ""
msgid "Send Failed" msgid "Send Failed"
msgstr "" msgstr ""
msgid "No available printers"
msgstr ""
msgid "The name length exceeds the limit." msgid "The name length exceeds the limit."
msgstr "" msgstr ""
@@ -12315,7 +12318,7 @@ msgstr ""
msgid "Left mouse" msgid "Left mouse"
msgstr "" msgstr ""
msgid "Available anywhere in the window, even while typing in a text field." msgid "Available anywhere in the window."
msgstr "" msgstr ""
msgid "Run favorite 1 to 10" msgid "Run favorite 1 to 10"
@@ -12412,9 +12415,6 @@ msgstr ""
msgid "The default %s is assigned to %s. Reassign it to %s?" msgid "The default %s is assigned to %s. Reassign it to %s?"
msgstr "" msgstr ""
msgid "Global shortcuts also apply while typing."
msgstr ""
msgid "A key that types a character cannot be a global shortcut." msgid "A key that types a character cannot be a global shortcut."
msgstr "" msgstr ""
@@ -15645,6 +15645,12 @@ msgstr ""
msgid "Enable this option to add EXCLUDE OBJECT command in G-code." msgid "Enable this option to add EXCLUDE OBJECT command in G-code."
msgstr "" 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" msgid "Verbose G-code"
msgstr "" msgstr ""
@@ -20540,7 +20546,7 @@ msgid "test %s start..."
msgstr "" msgstr ""
#, possible-c-format, possible-boost-format #, possible-c-format, possible-boost-format
msgid "test %s ip resolved = %s" msgid "test %s IP resolved = %s"
msgstr "" msgstr ""
#, possible-c-format, possible-boost-format #, possible-c-format, possible-boost-format
@@ -26492,17 +26498,18 @@ msgstr ""
msgid "Basic Colors" msgid "Basic Colors"
msgstr "" msgstr ""
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
msgid "Orca Palette" msgid "Orca Palette"
msgstr "" msgstr ""
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor. #. TRN: Keep the standard/brand name "RAL" unchanged.
msgid "RAL Classic" msgid "RAL Classic"
msgstr "" msgstr ""
#. TRN: Keep the standard/brand name "RAL" unchanged.
msgid "Gradients" msgid "Gradients"
msgstr "" msgstr ""
msgctxt "Color picker"
msgid "Solid" msgid "Solid"
msgstr "" msgstr ""
@@ -26877,7 +26884,7 @@ msgstr ""
msgid "Medium violet red" msgid "Medium violet red"
msgstr "" msgstr ""
msgid "PaleV violet red" msgid "Pale violet red"
msgstr "" msgstr ""
msgid "Deep pink" msgid "Deep pink"
@@ -27039,7 +27046,7 @@ msgstr ""
msgid "Shallow sky" msgid "Shallow sky"
msgstr "" msgstr ""
msgid "Electric bel" msgid "Electric blue"
msgstr "" msgstr ""
msgid "Dolphin" msgid "Dolphin"
@@ -27108,7 +27115,7 @@ msgstr ""
msgid "Maize yellow" msgid "Maize yellow"
msgstr "" msgstr ""
msgid "Dalmatian yellow" msgid "Daffodil yellow"
msgstr "" msgstr ""
msgid "Brown beige" msgid "Brown beige"
@@ -27369,7 +27376,7 @@ msgstr ""
msgid "Turquoise blue" msgid "Turquoise blue"
msgstr "" msgstr ""
msgid "Capsanthin violet" msgid "Capri blue"
msgstr "" msgstr ""
msgid "Ocean blue" msgid "Ocean blue"
@@ -27483,7 +27490,7 @@ msgstr ""
msgid "Silver grey" msgid "Silver grey"
msgstr "" msgstr ""
msgid "Olivestone" msgid "Olive grey"
msgstr "" msgstr ""
msgid "Moss grey" msgid "Moss grey"
@@ -28071,10 +28078,19 @@ msgstr ""
msgid "Built-in" msgid "Built-in"
msgstr "" msgstr ""
msgid "My textures"
msgstr ""
msgid "No textures found." msgid "No textures found."
msgstr "" msgstr ""
msgid "My textures" msgid "Texture libraries"
msgstr ""
msgid "Explore more texture libraries"
msgstr ""
msgid "Opens OrcaCloud in the Home page to browse for plugins."
msgstr "" 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." 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."
@@ -30127,6 +30143,9 @@ msgstr ""
msgid "Slicing Pipeline" msgid "Slicing Pipeline"
msgstr "" msgstr ""
msgid "Resource Texture"
msgstr ""
msgid "The plugin's configuration UI was empty. Showing the default editor." msgid "The plugin's configuration UI was empty. Showing the default editor."
msgstr "" msgstr ""
@@ -30242,6 +30261,24 @@ msgstr ""
msgid "Plugin directory does not contain a .py or .whl entry file" msgid "Plugin directory does not contain a .py or .whl entry file"
msgstr "" 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 #, possible-boost-format
msgid "Slicing pipeline plugin '%1%' error: %2%" msgid "Slicing pipeline plugin '%1%' error: %2%"
msgstr "" msgstr ""
@@ -30598,7 +30635,7 @@ msgstr ""
#. TRN %1% is the application name. #. TRN %1% is the application name.
#, possible-boost-format #, possible-boost-format
msgid "Not a gcode file generated by %1%." msgid "Not a G-code file generated by %1%."
msgstr "" msgstr ""
#. TRN %1% is a tag in the G-code file that must not be translated. #. TRN %1% is a tag in the G-code file that must not be translated.
@@ -30686,7 +30723,7 @@ msgstr ""
msgid "Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%" msgid "Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%"
msgstr "" msgstr ""
msgid "Post-processor can't find exported gcode file" msgid "Post-processor can't find exported G-code file"
msgstr "" msgstr ""
#, possible-boost-format #, 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
+58 -21
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-15 17:40+0800\n" "POT-Creation-Date: 2026-10-19 09:48-0300\n"
"PO-Revision-Date: 2026-06-17 15:44-0300\n" "PO-Revision-Date: 2026-06-17 15:44-0300\n"
"Last-Translator: Alexandre Folle de Menezes\n" "Last-Translator: Alexandre Folle de Menezes\n"
"Language-Team: \n" "Language-Team: \n"
@@ -1633,16 +1633,16 @@ msgstr ""
msgid "Stroke of shape (%1%) contains unsupported: %2%." msgid "Stroke of shape (%1%) contains unsupported: %2%."
msgstr "" msgstr ""
msgid "Not valid state please report reproduction steps on github" msgid "Not valid state please report reproduction steps on GitHub"
msgstr "" msgstr ""
msgid "No embossed file" msgid "No embossed file"
msgstr "" msgstr ""
msgid "Missing svg file in embossed shape" msgid "Missing SVG file in embossed shape"
msgstr "" msgstr ""
msgid "Missing data of svg file" msgid "Missing data of SVG file"
msgstr "" msgstr ""
msgid "Face the camera" msgid "Face the camera"
@@ -4575,7 +4575,7 @@ msgstr ""
msgid "Unknown C++ exception." msgid "Unknown C++ exception."
msgstr "" msgstr ""
msgid "Processing G-Code from previous file…" msgid "Processing G-code from previous file…"
msgstr "" msgstr ""
msgid "Slicing complete" msgid "Slicing complete"
@@ -6045,7 +6045,7 @@ msgstr ""
msgid "Size:" msgid "Size:"
msgstr "" msgstr ""
#, boost-format #, c-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)." msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)."
msgstr "" msgstr ""
@@ -10653,6 +10653,9 @@ msgstr ""
msgid "Send Failed" msgid "Send Failed"
msgstr "" msgstr ""
msgid "No available printers"
msgstr ""
msgid "The name length exceeds the limit." msgid "The name length exceeds the limit."
msgstr "" msgstr ""
@@ -12311,7 +12314,7 @@ msgstr ""
msgid "Left mouse" msgid "Left mouse"
msgstr "" msgstr ""
msgid "Available anywhere in the window, even while typing in a text field." msgid "Available anywhere in the window."
msgstr "" msgstr ""
msgid "Run favorite 1 to 10" msgid "Run favorite 1 to 10"
@@ -12408,9 +12411,6 @@ msgstr ""
msgid "The default %s is assigned to %s. Reassign it to %s?" msgid "The default %s is assigned to %s. Reassign it to %s?"
msgstr "" msgstr ""
msgid "Global shortcuts also apply while typing."
msgstr ""
msgid "A key that types a character cannot be a global shortcut." msgid "A key that types a character cannot be a global shortcut."
msgstr "" msgstr ""
@@ -15641,6 +15641,12 @@ msgstr ""
msgid "Enable this option to add EXCLUDE OBJECT command in G-code." msgid "Enable this option to add EXCLUDE OBJECT command in G-code."
msgstr "" 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" msgid "Verbose G-code"
msgstr "" msgstr ""
@@ -20536,7 +20542,7 @@ msgid "test %s start..."
msgstr "" msgstr ""
#, c-format, boost-format #, c-format, boost-format
msgid "test %s ip resolved = %s" msgid "test %s IP resolved = %s"
msgstr "" msgstr ""
#, c-format, boost-format #, c-format, boost-format
@@ -26488,17 +26494,18 @@ msgstr ""
msgid "Basic Colors" msgid "Basic Colors"
msgstr "" msgstr ""
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
msgid "Orca Palette" msgid "Orca Palette"
msgstr "" msgstr ""
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor. #. TRN: Keep the standard/brand name "RAL" unchanged.
msgid "RAL Classic" msgid "RAL Classic"
msgstr "" msgstr ""
#. TRN: Keep the standard/brand name "RAL" unchanged.
msgid "Gradients" msgid "Gradients"
msgstr "" msgstr ""
msgctxt "Color picker"
msgid "Solid" msgid "Solid"
msgstr "" msgstr ""
@@ -26873,7 +26880,7 @@ msgstr ""
msgid "Medium violet red" msgid "Medium violet red"
msgstr "" msgstr ""
msgid "PaleV violet red" msgid "Pale violet red"
msgstr "" msgstr ""
msgid "Deep pink" msgid "Deep pink"
@@ -27035,7 +27042,7 @@ msgstr ""
msgid "Shallow sky" msgid "Shallow sky"
msgstr "" msgstr ""
msgid "Electric bel" msgid "Electric blue"
msgstr "" msgstr ""
msgid "Dolphin" msgid "Dolphin"
@@ -27104,7 +27111,7 @@ msgstr ""
msgid "Maize yellow" msgid "Maize yellow"
msgstr "" msgstr ""
msgid "Dalmatian yellow" msgid "Daffodil yellow"
msgstr "" msgstr ""
msgid "Brown beige" msgid "Brown beige"
@@ -27365,7 +27372,7 @@ msgstr ""
msgid "Turquoise blue" msgid "Turquoise blue"
msgstr "" msgstr ""
msgid "Capsanthin violet" msgid "Capri blue"
msgstr "" msgstr ""
msgid "Ocean blue" msgid "Ocean blue"
@@ -27479,7 +27486,7 @@ msgstr ""
msgid "Silver grey" msgid "Silver grey"
msgstr "" msgstr ""
msgid "Olivestone" msgid "Olive grey"
msgstr "" msgstr ""
msgid "Moss grey" msgid "Moss grey"
@@ -28067,10 +28074,19 @@ msgstr ""
msgid "Built-in" msgid "Built-in"
msgstr "" msgstr ""
msgid "My textures"
msgstr ""
msgid "No textures found." msgid "No textures found."
msgstr "" msgstr ""
msgid "My textures" msgid "Texture libraries"
msgstr ""
msgid "Explore more texture libraries"
msgstr ""
msgid "Opens OrcaCloud in the Home page to browse for plugins."
msgstr "" 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." 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."
@@ -30123,6 +30139,9 @@ msgstr ""
msgid "Slicing Pipeline" msgid "Slicing Pipeline"
msgstr "" msgstr ""
msgid "Resource Texture"
msgstr ""
msgid "The plugin's configuration UI was empty. Showing the default editor." msgid "The plugin's configuration UI was empty. Showing the default editor."
msgstr "" msgstr ""
@@ -30238,6 +30257,24 @@ msgstr ""
msgid "Plugin directory does not contain a .py or .whl entry file" msgid "Plugin directory does not contain a .py or .whl entry file"
msgstr "" 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 #, boost-format
msgid "Slicing pipeline plugin '%1%' error: %2%" msgid "Slicing pipeline plugin '%1%' error: %2%"
msgstr "" msgstr ""
@@ -30594,7 +30631,7 @@ msgstr ""
#. TRN %1% is the application name. #. TRN %1% is the application name.
#, boost-format #, boost-format
msgid "Not a gcode file generated by %1%." msgid "Not a G-code file generated by %1%."
msgstr "" msgstr ""
#. TRN %1% is a tag in the G-code file that must not be translated. #. TRN %1% is a tag in the G-code file that must not be translated.
@@ -30682,7 +30719,7 @@ msgstr ""
msgid "Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%" msgid "Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%"
msgstr "" msgstr ""
msgid "Post-processor can't find exported gcode file" msgid "Post-processor can't find exported G-code file"
msgstr "" msgstr ""
#, boost-format #, 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
+110 -47
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Slic3rPE\n" "Project-Id-Version: Slic3rPE\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-15 17:40+0800\n" "POT-Creation-Date: 2026-10-19 09:48-0300\n"
"PO-Revision-Date: 2026-06-11 12:37-0300\n" "PO-Revision-Date: 2026-06-11 12:37-0300\n"
"Last-Translator: Handle <mail@bysb.net>\n" "Last-Translator: Handle <mail@bysb.net>\n"
"Language-Team: \n" "Language-Team: \n"
@@ -1751,7 +1751,7 @@ msgid "Stroke of shape (%1%) contains unsupported: %2%."
msgstr "形状(%1%)的描边包含不受支持的内容:%2%。" msgstr "形状(%1%)的描边包含不受支持的内容:%2%。"
# AI Translated # AI Translated
msgid "Not valid state please report reproduction steps on github" msgid "Not valid state please report reproduction steps on GitHub"
msgstr "状态无效,请在 GitHub 上报告复现步骤" msgstr "状态无效,请在 GitHub 上报告复现步骤"
# AI Translated # AI Translated
@@ -1759,11 +1759,11 @@ msgid "No embossed file"
msgstr "没有浮雕文件" msgstr "没有浮雕文件"
# AI Translated # AI Translated
msgid "Missing svg file in embossed shape" msgid "Missing SVG file in embossed shape"
msgstr "浮雕形状中缺少 SVG 文件" msgstr "浮雕形状中缺少 SVG 文件"
# AI Translated # AI Translated
msgid "Missing data of svg file" msgid "Missing data of SVG file"
msgstr "缺少 SVG 文件数据" msgstr "缺少 SVG 文件数据"
# AI Translated # AI Translated
@@ -5008,7 +5008,7 @@ msgid "Unknown C++ exception."
msgstr "未知的 C++ 异常。" msgstr "未知的 C++ 异常。"
# AI Translated # AI Translated
msgid "Processing G-Code from previous file…" msgid "Processing G-code from previous file…"
msgstr "正在处理之前文件的 G-code…" msgstr "正在处理之前文件的 G-code…"
msgid "Slicing complete" msgid "Slicing complete"
@@ -6699,7 +6699,7 @@ msgstr "体积:"
msgid "Size:" msgid "Size:"
msgstr "尺寸:" msgstr "尺寸:"
#, boost-format #, c-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)." 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)。" msgstr "发现G-code路径在层%d,高度为%.2lf mm处有冲突。请将有冲突的对象分离得更远(%s <-> %s)。"
@@ -12013,6 +12013,10 @@ msgstr "上传文件到打印机存储失败,请重试。"
msgid "Send Failed" msgid "Send Failed"
msgstr "发送失败" msgstr "发送失败"
# AI Translated
msgid "No available printers"
msgstr "没有可用的打印机"
msgid "The name length exceeds the limit." msgid "The name length exceeds the limit."
msgstr "名称长度超过限制。" msgstr "名称长度超过限制。"
@@ -13922,8 +13926,8 @@ msgid "Left mouse"
msgstr "鼠标左键" msgstr "鼠标左键"
# AI Translated # AI Translated
msgid "Available anywhere in the window, even while typing in a text field." msgid "Available anywhere in the window."
msgstr "在窗口任意位置可用,即使正在文本框中输入。" msgstr "在窗口任意位置可用。"
# AI Translated # AI Translated
msgid "Run favorite 1 to 10" msgid "Run favorite 1 to 10"
@@ -14039,10 +14043,6 @@ msgstr "%s 已分配给 %s。是否将其重新分配给 %s?"
msgid "The default %s is assigned to %s. Reassign it to %s?" msgid "The default %s is assigned to %s. Reassign it to %s?"
msgstr "默认的 %s 已分配给 %s。是否将其重新分配给 %s?" msgstr "默认的 %s 已分配给 %s。是否将其重新分配给 %s?"
# AI Translated
msgid "Global shortcuts also apply while typing."
msgstr "全局快捷键在输入文字时同样生效。"
# AI Translated # AI Translated
msgid "A key that types a character cannot be a global shortcut." msgid "A key that types a character cannot be a global shortcut."
msgstr "会输入字符的按键不能用作全局快捷键。" msgstr "会输入字符的按键不能用作全局快捷键。"
@@ -18086,6 +18086,14 @@ msgstr "对象排除"
msgid "Enable this option to add EXCLUDE OBJECT command in G-code." msgid "Enable this option to add EXCLUDE OBJECT command in G-code."
msgstr "启用此选项以在G-code中添加EXCLUDE OBJECT命令。" 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" msgid "Verbose G-code"
msgstr "注释G-code" msgstr "注释G-code"
@@ -24004,7 +24012,7 @@ msgstr "开始测试 %s..."
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
msgid "test %s ip resolved = %s" msgid "test %s IP resolved = %s"
msgstr "测试 %s:IP 解析结果 = %s" msgstr "测试 %s:IP 解析结果 = %s"
# AI Translated # AI Translated
@@ -31498,20 +31506,21 @@ msgid "Basic Colors"
msgstr "基本颜色" msgstr "基本颜色"
# AI Translated # AI Translated
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
msgid "Orca Palette" msgid "Orca Palette"
msgstr "Orca 调色板" msgstr "Orca 调色板"
# AI Translated # AI Translated
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor. #. TRN: Keep the standard/brand name "RAL" unchanged.
msgid "RAL Classic" msgid "RAL Classic"
msgstr "RAL 经典色" msgstr "RAL 经典色"
# AI Translated # AI Translated
#. TRN: Keep the standard/brand name "RAL" unchanged.
msgid "Gradients" msgid "Gradients"
msgstr "渐变预设" msgstr "渐变预设"
# AI Translated # AI Translated
msgctxt "Color picker"
msgid "Solid" msgid "Solid"
msgstr "纯色" msgstr "纯色"
@@ -32010,7 +32019,7 @@ msgid "Medium violet red"
msgstr "中紫罗兰红" msgstr "中紫罗兰红"
# AI Translated # AI Translated
msgid "PaleV violet red" msgid "Pale violet red"
msgstr "浅紫罗兰红" msgstr "浅紫罗兰红"
# AI Translated # AI Translated
@@ -32226,8 +32235,7 @@ msgid "Shallow sky"
msgstr "浅空" msgstr "浅空"
# AI Translated # 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 blue"
msgid "Electric bel"
msgstr "电光蓝" msgstr "电光蓝"
# AI Translated # AI Translated
@@ -32319,9 +32327,8 @@ msgid "Maize yellow"
msgstr "玉米黄" msgstr "玉米黄"
# AI Translated # 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 "Daffodil yellow"
msgid "Dalmatian yellow" msgstr "水仙黄"
msgstr "达尔马提亚黄"
# AI Translated # AI Translated
msgid "Brown beige" msgid "Brown beige"
@@ -32668,8 +32675,8 @@ msgid "Turquoise blue"
msgstr "绿松石蓝" msgstr "绿松石蓝"
# AI Translated # AI Translated
msgid "Capsanthin violet" msgid "Capri blue"
msgstr "辣椒红素紫" msgstr "卡普里蓝"
# AI Translated # AI Translated
msgid "Ocean blue" msgid "Ocean blue"
@@ -32820,8 +32827,8 @@ msgid "Silver grey"
msgstr "银灰色" msgstr "银灰色"
# AI Translated # AI Translated
msgid "Olivestone" msgid "Olive grey"
msgstr "橄榄石色" msgstr "橄榄灰"
# AI Translated # AI Translated
msgid "Moss grey" msgid "Moss grey"
@@ -33597,13 +33604,25 @@ msgstr "从“我的纹理”中移除"
msgid "Built-in" msgid "Built-in"
msgstr "内置" msgstr "内置"
# AI Translated
msgid "My textures"
msgstr "我的纹理"
# AI Translated # AI Translated
msgid "No textures found." msgid "No textures found."
msgstr "未找到纹理。" msgstr "未找到纹理。"
# AI Translated # AI Translated
msgid "My textures" msgid "Texture libraries"
msgstr "我的纹理" msgstr "纹理库"
# AI Translated
msgid "Explore more texture libraries"
msgstr "探索更多纹理库"
# AI Translated
msgid "Opens OrcaCloud in the Home page to browse for plugins."
msgstr "在主页中打开 OrcaCloud,浏览插件。"
# AI Translated # 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." 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."
@@ -36285,6 +36304,10 @@ msgstr "脚本"
msgid "Slicing Pipeline" msgid "Slicing Pipeline"
msgstr "切片流水线" msgstr "切片流水线"
# AI Translated
msgid "Resource Texture"
msgstr "资源纹理"
# AI Translated # AI Translated
msgid "The plugin's configuration UI was empty. Showing the default editor." msgid "The plugin's configuration UI was empty. Showing the default editor."
msgstr "插件的配置界面为空。正在显示默认编辑器。" msgstr "插件的配置界面为空。正在显示默认编辑器。"
@@ -36433,6 +36456,30 @@ msgstr "插件目录同时包含 .py 和 .whl 入口文件"
msgid "Plugin directory does not contain a .py or .whl entry file" msgid "Plugin directory does not contain a .py or .whl entry file"
msgstr "插件目录不包含 .py 或 .whl 入口文件" 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 # AI Translated
#, boost-format #, boost-format
msgid "Slicing pipeline plugin '%1%' error: %2%" msgid "Slicing pipeline plugin '%1%' error: %2%"
@@ -36891,7 +36938,7 @@ msgstr "加载配置文件“%1%”失败:%2%"
# AI Translated # AI Translated
#. TRN %1% is the application name. #. TRN %1% is the application name.
#, boost-format #, boost-format
msgid "Not a gcode file generated by %1%." msgid "Not a G-code file generated by %1%."
msgstr "不是由 %1% 生成的 G-code 文件。" msgstr "不是由 %1% 生成的 G-code 文件。"
# AI Translated # AI Translated
@@ -37011,7 +37058,7 @@ msgid "Failed making a temporary copy of G-code file %1% before running a post-p
msgstr "运行后处理脚本之前,无法创建 G-code 文件 %1% 的临时副本:%2%" msgstr "运行后处理脚本之前,无法创建 G-code 文件 %1% 的临时副本:%2%"
# AI Translated # AI Translated
msgid "Post-processor can't find exported gcode file" msgid "Post-processor can't find exported G-code file"
msgstr "后处理器找不到导出的 G-code 文件" msgstr "后处理器找不到导出的 G-code 文件"
# AI Translated # AI Translated
@@ -37400,6 +37447,40 @@ msgstr ""
"避免翘曲\n" "避免翘曲\n"
"您知道吗?打印ABS这类易翘曲材料时,适当提高热床温度可以降低翘曲的概率。" "您知道吗?打印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" #~ msgid "Please choose the filament color"
#~ msgstr "请选择耗材丝颜色" #~ msgstr "请选择耗材丝颜色"
@@ -37865,9 +37946,6 @@ msgstr ""
#~ msgid "no configured tool" #~ msgid "no configured tool"
#~ msgstr "未配置的工具头" #~ msgstr "未配置的工具头"
#~ msgid "Add end G-Code when finishing the entire print."
#~ msgstr "所有打印结束时的结尾G-code"
#~ msgid "Maximum X speed" #~ msgid "Maximum X speed"
#~ msgstr "X最大速度" #~ msgstr "X最大速度"
@@ -37884,26 +37962,11 @@ 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." #~ 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 后处理步骤。研究/实验性。" #~ 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 # AI Translated
# Needs human review: "pem-inversion defaults": confirm pem is the physical extruder map and that the default is its inverse. # 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." #~ msgid "Per-plate override mapping physical heads to filament slots (1-based). Empty means use pem-inversion defaults."
#~ msgstr "按盘单独设置物理工具头到耗材丝槽位的映射(从 1 开始编号),覆盖默认映射。留空则使用由物理挤出机映射(pem)反推的默认值。" #~ 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 " #~ msgid " not in range "
#~ msgstr " 不在合理范围内" #~ msgstr " 不在合理范围内"
+111 -48
View File
@@ -9,7 +9,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Orca Slicer\n" "Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-15 17:40+0800\n" "POT-Creation-Date: 2026-10-19 09:48-0300\n"
"PO-Revision-Date: 2025-11-28 13:48-0600\n" "PO-Revision-Date: 2025-11-28 13:48-0600\n"
"Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n" "Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n"
"Language-Team: \n" "Language-Team: \n"
@@ -1826,7 +1826,7 @@ msgid "Stroke of shape (%1%) contains unsupported: %2%."
msgstr "形狀(%1%)的筆劃包含不支援的內容:%2%。" msgstr "形狀(%1%)的筆劃包含不支援的內容:%2%。"
# AI Translated # AI Translated
msgid "Not valid state please report reproduction steps on github" msgid "Not valid state please report reproduction steps on GitHub"
msgstr "狀態無效,請在 GitHub 上回報重現步驟" msgstr "狀態無效,請在 GitHub 上回報重現步驟"
# AI Translated # AI Translated
@@ -1834,11 +1834,11 @@ msgid "No embossed file"
msgstr "沒有浮雕檔案" msgstr "沒有浮雕檔案"
# AI Translated # AI Translated
msgid "Missing svg file in embossed shape" msgid "Missing SVG file in embossed shape"
msgstr "浮雕形狀中缺少 SVG 檔案" msgstr "浮雕形狀中缺少 SVG 檔案"
# AI Translated # AI Translated
msgid "Missing data of svg file" msgid "Missing data of SVG file"
msgstr "缺少 SVG 檔案的資料" msgstr "缺少 SVG 檔案的資料"
msgid "Face the camera" msgid "Face the camera"
@@ -5219,7 +5219,7 @@ msgid "Unknown C++ exception."
msgstr "未知的 C++ 例外。" msgstr "未知的 C++ 例外。"
# AI Translated # AI Translated
msgid "Processing G-Code from previous file…" msgid "Processing G-code from previous file…"
msgstr "正在處理先前檔案的 G-code…" msgstr "正在處理先前檔案的 G-code…"
msgid "Slicing complete" msgid "Slicing complete"
@@ -6912,7 +6912,7 @@ msgid "Size:"
msgstr "尺寸:" msgstr "尺寸:"
# AI Translated # AI Translated
#, boost-format #, c-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)." 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)。" msgstr "在第 %d 層(Z = %.2lf mm)發現 G-code 路徑衝突。請將有衝突的物件分開得更遠(%s <-> %s)。"
@@ -12298,6 +12298,10 @@ msgstr "無法將檔案上傳至列印裝置的儲存空間,請重試。"
msgid "Send Failed" msgid "Send Failed"
msgstr "傳送失敗" msgstr "傳送失敗"
# AI Translated
msgid "No available printers"
msgstr "沒有可用的列印裝置"
msgid "The name length exceeds the limit." msgid "The name length exceeds the limit."
msgstr "名稱長度超過限制。" msgstr "名稱長度超過限制。"
@@ -14251,8 +14255,8 @@ msgid "Left mouse"
msgstr "滑鼠左鍵" msgstr "滑鼠左鍵"
# AI Translated # AI Translated
msgid "Available anywhere in the window, even while typing in a text field." msgid "Available anywhere in the window."
msgstr "在視窗中任何位置皆可使用,即使正在文字欄位中輸入也可以。" msgstr "在視窗中任何位置皆可使用。"
# AI Translated # AI Translated
msgid "Run favorite 1 to 10" msgid "Run favorite 1 to 10"
@@ -14372,10 +14376,6 @@ msgstr "%s 已指派給 %s。要改為指派給 %s 嗎?"
msgid "The default %s is assigned to %s. Reassign it to %s?" msgid "The default %s is assigned to %s. Reassign it to %s?"
msgstr "預設的 %s 已指派給 %s。要改為指派給 %s 嗎?" msgstr "預設的 %s 已指派給 %s。要改為指派給 %s 嗎?"
# AI Translated
msgid "Global shortcuts also apply while typing."
msgstr "全域快速鍵在輸入文字時也會生效。"
# AI Translated # AI Translated
msgid "A key that types a character cannot be a global shortcut." msgid "A key that types a character cannot be a global shortcut."
msgstr "會輸入字元的按鍵不能作為全域快速鍵。" msgstr "會輸入字元的按鍵不能作為全域快速鍵。"
@@ -18513,6 +18513,14 @@ msgstr "物件排除"
msgid "Enable this option to add EXCLUDE OBJECT command in G-code." msgid "Enable this option to add EXCLUDE OBJECT command in G-code."
msgstr "啟用此選項以在 G-code 中加入 EXCLUDE OBJECT 指令。" 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" msgid "Verbose G-code"
msgstr "詳細 G-code" msgstr "詳細 G-code"
@@ -24597,7 +24605,7 @@ msgstr "測試 %s 開始..."
# AI Translated # AI Translated
#, c-format, boost-format #, c-format, boost-format
msgid "test %s ip resolved = %s" msgid "test %s IP resolved = %s"
msgstr "測試 %s IP 解析結果 = %s" msgstr "測試 %s IP 解析結果 = %s"
# AI Translated # AI Translated
@@ -32124,21 +32132,22 @@ msgid "Basic Colors"
msgstr "基本顏色" msgstr "基本顏色"
# AI Translated # AI Translated
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
msgid "Orca Palette" msgid "Orca Palette"
msgstr "Orca 調色盤" msgstr "Orca 調色盤"
# AI Translated # AI Translated
#. TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor. #. TRN: Keep the standard/brand name "RAL" unchanged.
msgid "RAL Classic" msgid "RAL Classic"
msgstr "RAL 經典色" msgstr "RAL 經典色"
# AI Translated # 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. # 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" msgid "Gradients"
msgstr "漸層配色" msgstr "漸層配色"
# AI Translated # AI Translated
msgctxt "Color picker"
msgid "Solid" msgid "Solid"
msgstr "純色" msgstr "純色"
@@ -32637,7 +32646,7 @@ msgid "Medium violet red"
msgstr "中紫羅蘭紅" msgstr "中紫羅蘭紅"
# AI Translated # AI Translated
msgid "PaleV violet red" msgid "Pale violet red"
msgstr "淡紫羅蘭紅" msgstr "淡紫羅蘭紅"
# AI Translated # AI Translated
@@ -32853,8 +32862,7 @@ msgid "Shallow sky"
msgstr "淺色天空" msgstr "淺色天空"
# AI Translated # 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 blue"
msgid "Electric bel"
msgstr "電光藍" msgstr "電光藍"
# AI Translated # AI Translated
@@ -32946,9 +32954,8 @@ msgid "Maize yellow"
msgstr "玉米黃" msgstr "玉米黃"
# AI Translated # 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 "Daffodil yellow"
msgid "Dalmatian yellow" msgstr "水仙黃"
msgstr "達爾馬提亞黃"
# AI Translated # AI Translated
msgid "Brown beige" msgid "Brown beige"
@@ -33296,9 +33303,8 @@ msgid "Turquoise blue"
msgstr "綠松石藍" msgstr "綠松石藍"
# AI Translated # 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 "Capri blue"
msgid "Capsanthin violet" msgstr "卡布里藍"
msgstr "辣椒紅素紫"
# AI Translated # AI Translated
msgid "Ocean blue" msgid "Ocean blue"
@@ -33449,8 +33455,8 @@ msgid "Silver grey"
msgstr "銀灰" msgstr "銀灰"
# AI Translated # AI Translated
msgid "Olivestone" msgid "Olive grey"
msgstr "橄欖石色" msgstr "橄欖灰"
# AI Translated # AI Translated
msgid "Moss grey" msgid "Moss grey"
@@ -34226,13 +34232,25 @@ msgstr "從「我的紋理」移除"
msgid "Built-in" msgid "Built-in"
msgstr "內建" msgstr "內建"
# AI Translated
msgid "My textures"
msgstr "我的紋理"
# AI Translated # AI Translated
msgid "No textures found." msgid "No textures found."
msgstr "找不到紋理。" msgstr "找不到紋理。"
# AI Translated # AI Translated
msgid "My textures" msgid "Texture libraries"
msgstr "我的紋理" msgstr "紋理庫"
# AI Translated
msgid "Explore more texture libraries"
msgstr "探索更多紋理庫"
# AI Translated
msgid "Opens OrcaCloud in the Home page to browse for plugins."
msgstr "在首頁中開啟 OrcaCloud,瀏覽外掛。"
# AI Translated # 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." 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."
@@ -36928,6 +36946,10 @@ msgstr "指令碼"
msgid "Slicing Pipeline" msgid "Slicing Pipeline"
msgstr "切片管線" msgstr "切片管線"
# AI Translated
msgid "Resource Texture"
msgstr "資源紋理"
# AI Translated # AI Translated
msgid "The plugin's configuration UI was empty. Showing the default editor." msgid "The plugin's configuration UI was empty. Showing the default editor."
msgstr "外掛的設定介面為空。將顯示預設編輯器。" msgstr "外掛的設定介面為空。將顯示預設編輯器。"
@@ -37075,6 +37097,30 @@ msgstr "外掛目錄同時包含 .py 和 .whl 進入點檔案"
msgid "Plugin directory does not contain a .py or .whl entry file" msgid "Plugin directory does not contain a .py or .whl entry file"
msgstr "外掛目錄不包含 .py 或 .whl 進入點檔案" 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 # AI Translated
#, boost-format #, boost-format
msgid "Slicing pipeline plugin '%1%' error: %2%" msgid "Slicing pipeline plugin '%1%' error: %2%"
@@ -37533,7 +37579,7 @@ msgstr "載入設定檔「%1%」失敗:%2%"
# AI Translated # AI Translated
#. TRN %1% is the application name. #. TRN %1% is the application name.
#, boost-format #, boost-format
msgid "Not a gcode file generated by %1%." msgid "Not a G-code file generated by %1%."
msgstr "不是由 %1% 產生的 G-code 檔案。" msgstr "不是由 %1% 產生的 G-code 檔案。"
# AI Translated # AI Translated
@@ -37653,7 +37699,7 @@ msgid "Failed making a temporary copy of G-code file %1% before running a post-p
msgstr "執行後處理腳本前,無法建立 G-code 檔案 %1% 的暫存副本:%2%" msgstr "執行後處理腳本前,無法建立 G-code 檔案 %1% 的暫存副本:%2%"
# AI Translated # AI Translated
msgid "Post-processor can't find exported gcode file" msgid "Post-processor can't find exported G-code file"
msgstr "後處理程式找不到匯出的 G-code 檔案" msgstr "後處理程式找不到匯出的 G-code 檔案"
# AI Translated # AI Translated
@@ -38049,6 +38095,41 @@ msgstr ""
"避免翹曲\n" "避免翹曲\n"
"您知道嗎?當列印容易翹曲的材料(如 ABS)時,適當提高熱床溫度可以降低翹曲的機率。" "您知道嗎?當列印容易翹曲的材料(如 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" #~ msgid "Please choose the filament color"
#~ msgstr "請選擇線材顏色" #~ msgstr "請選擇線材顏色"
@@ -38513,9 +38594,6 @@ msgstr ""
#~ msgid "no configured tool" #~ msgid "no configured tool"
#~ msgstr "未設定的工具頭" #~ msgstr "未設定的工具頭"
#~ msgid "Add end G-Code when finishing the entire print."
#~ msgstr "所有列印結束時的結尾 G-code"
#~ msgid "Maximum X speed" #~ msgid "Maximum X speed"
#~ msgstr "X 最大速度" #~ msgstr "X 最大速度"
@@ -38532,25 +38610,10 @@ 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." #~ 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 後處理步驟。研究/實驗性質。" #~ 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 # AI Translated
#~ msgid "Per-plate override mapping physical heads to filament slots (1-based). Empty means use pem-inversion defaults." #~ msgid "Per-plate override mapping physical heads to filament slots (1-based). Empty means use pem-inversion defaults."
#~ msgstr "依列印板覆寫實體工具頭到線材槽位的映射(從 1 開始編號)。留空表示使用由 pem 反推的預設映射。" #~ 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 " #~ msgid " not in range "
#~ msgstr " 不在合理的區間" #~ msgstr " 不在合理的區間"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "Snapmaker", "name": "Snapmaker",
"version": "02.04.00.17", "version": "02.04.00.18",
"force_update": "0", "force_update": "0",
"description": "Snapmaker configurations", "description": "Snapmaker configurations",
"machine_model_list": [ "machine_model_list": [
@@ -49,5 +49,8 @@
], ],
"pressure_advance": [ "pressure_advance": [
"0.02" "0.02"
],
"temperature_vitrification": [
"100"
] ]
} }
@@ -46,5 +46,8 @@
], ],
"fan_min_speed": [ "fan_min_speed": [
"0" "0"
],
"temperature_vitrification": [
"100"
] ]
} }
@@ -45,7 +45,7 @@
"250" "250"
], ],
"temperature_vitrification": [ "temperature_vitrification": [
"178" "70"
], ],
"fan_max_speed": [ "fan_max_speed": [
"20" "20"
@@ -4,5 +4,8 @@
"inherits": "fdm_filament_petg", "inherits": "fdm_filament_petg",
"from": "system", "from": "system",
"filament_id": "OFW8lqnb", "filament_id": "OFW8lqnb",
"instantiation": "false" "instantiation": "false",
"temperature_vitrification": [
"70"
]
} }
@@ -60,7 +60,7 @@
"250" "250"
], ],
"temperature_vitrification": [ "temperature_vitrification": [
"178" "70"
], ],
"fan_max_speed": [ "fan_max_speed": [
"20" "20"
@@ -34,5 +34,8 @@
], ],
"nozzle_temperature": [ "nozzle_temperature": [
"220" "220"
],
"temperature_vitrification": [
"45"
] ]
} }
@@ -34,5 +34,8 @@
], ],
"filament_cooling_final_speed": [ "filament_cooling_final_speed": [
"60" "60"
],
"temperature_vitrification": [
"45"
] ]
} }
@@ -34,5 +34,8 @@
], ],
"nozzle_temperature": [ "nozzle_temperature": [
"220" "220"
],
"temperature_vitrification": [
"45"
] ]
} }
@@ -40,5 +40,8 @@
], ],
"nozzle_temperature": [ "nozzle_temperature": [
"220" "220"
],
"temperature_vitrification": [
"45"
] ]
} }
@@ -31,5 +31,8 @@
], ],
"slow_down_layer_time": [ "slow_down_layer_time": [
"8" "8"
],
"temperature_vitrification": [
"45"
] ]
} }
@@ -50,5 +50,8 @@
"additional_cooling_fan_speed": [ "additional_cooling_fan_speed": [
"70", "70",
"70" "70"
],
"temperature_vitrification": [
"30"
] ]
} }
@@ -137,7 +137,7 @@
"240" "240"
], ],
"temperature_vitrification": [ "temperature_vitrification": [
"60" "30"
], ],
"nozzle_temperature_range_low": [ "nozzle_temperature_range_low": [
"200" "200"
@@ -302,7 +302,7 @@
"-1" "-1"
], ],
"temperature_vitrification": [ "temperature_vitrification": [
"60" "30"
], ],
"textured_cool_plate_temp": [ "textured_cool_plate_temp": [
"40" "40"
@@ -29,5 +29,8 @@
"additional_cooling_fan_speed": [ "additional_cooling_fan_speed": [
"70", "70",
"70" "70"
],
"temperature_vitrification": [
"30"
] ]
} }
@@ -49,5 +49,8 @@
"additional_cooling_fan_speed": [ "additional_cooling_fan_speed": [
"70", "70",
"70" "70"
],
"temperature_vitrification": [
"30"
] ]
} }
@@ -97,7 +97,7 @@
"20", "20",
"12" "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\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_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_tool_change_time": "5", "machine_tool_change_time": "5",
"max_layer_height": [ "max_layer_height": [
"0.14", "0.14",
@@ -97,7 +97,7 @@
"20", "20",
"12" "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\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_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_tool_change_time": "5", "machine_tool_change_time": "5",
"max_layer_height": [ "max_layer_height": [
"0.32", "0.32",
@@ -97,7 +97,7 @@
"20", "20",
"12" "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\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_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_tool_change_time": "5", "machine_tool_change_time": "5",
"max_layer_height": [ "max_layer_height": [
"0.42", "0.42",
@@ -97,7 +97,7 @@
"20", "20",
"12" "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\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_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_tool_change_time": "5", "machine_tool_change_time": "5",
"max_layer_height": [ "max_layer_height": [
"0.56", "0.56",
-92
View File
@@ -1,92 +0,0 @@
#version 110
// What a ChromaStack print looks like, shaded per fragment.
//
// The colour is not a vertex attribute and not a texture on the surface: it is read from the
// fragment's own height. A filament swap happens at one print Z across the whole plate, so the layer
// a point sits in is what decides its colour - which means a band edge lands exactly on the layer
// boundary however coarse the mesh is, and the same shader is right before a bake (on the preview's
// displaced mesh) and after one (on the volume's own).
//
// On top of that it tries to look like printed plastic rather than like flat shaded colour: the
// terminator is wrapped and the grazing angles glow, because filament translucent enough to be worth
// stacking scatters light rather than reflecting it; and the walls carry layer lines, because that is
// the first thing the eye uses to read a print as a print.
#define INTENSITY_CORRECTION 0.6
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SHININESS 20.0
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION)
#define INTENSITY_AMBIENT 0.3
// The stack's colours, one texel per printed layer, bottom to top. A texture rather than a uniform
// array because a base can be several hundred layers and no uniform array survives that.
uniform sampler2D palette;
uniform float palette_size; // texels in `palette`, i.e. the stack's total printed layers
uniform float base_z; // world Z of the bottom of the object
uniform float layer_height; // mm
// How translucent the stack reads, 0 to 1, from the transmission distance of the filament on top.
// It drives the wrap and the rim: an opaque stack should not glow.
uniform float translucency;
uniform float alpha;
varying vec3 clipping_planes_dots;
varying vec3 eye_normal;
varying vec3 eye_position;
varying vec3 world_normal;
varying float world_z;
void main()
{
if (any(lessThan(clipping_planes_dots, vec3(0.0))))
discard;
float z_rel = world_z - base_z;
float layers = max(z_rel / layer_height, 0.0);
// How much of a layer one pixel covers. Everything below is faded out as this grows: layer lines
// finer than a pixel are noise, and a colour read from one layer out of the dozen under a pixel
// flickers as the camera moves.
float px = fwidth(layers);
// The layer this fragment sits in, 1-based. Up close that is an exact texel of the palette; once a
// pixel spans a layer or more it slides to a continuous coordinate so the linear filter averages
// the bands it covers instead of picking one of them at random.
float stepped = clamp(ceil(layers - 1.0e-4), 1.0, palette_size) - 0.5;
float smooth_ = clamp(layers, 0.5, palette_size - 0.5);
float u = mix(stepped, smooth_, clamp(px - 0.5, 0.0, 1.0)) / palette_size;
vec3 albedo = texture2D(palette, vec2(u, 0.5)).rgb;
vec3 N = normalize(eye_normal);
vec3 V = normalize(-eye_position);
// Layer lines, on the walls only: a terrace is one layer's top surface and has none to show. The
// groove runs round the part at every layer boundary, so it is a function of height alone.
float wall = 1.0 - abs(world_normal.z);
float phase = fract(layers);
float groove = smoothstep(0.0, 0.16, phase) * smoothstep(1.0, 0.84, phase);
groove = mix(groove, 0.65, clamp(px * 1.5, 0.0, 1.0)); // fade to flat when too fine to resolve
float lines = mix(1.0, 0.84 + 0.16 * groove, wall * 0.9);
albedo *= lines;
// Diffuse, with a wrapped terminator: light entering translucent filament comes back out a little
// way round the curve, so the shaded side never goes as dark as an opaque surface's would.
float wrap = 0.25 + 0.45 * translucency;
float ndotl = dot(N, LIGHT_TOP_DIR);
float diffuse = max((ndotl + wrap) / (1.0 + wrap), 0.0) * LIGHT_TOP_DIFFUSE;
diffuse += max((dot(N, LIGHT_FRONT_DIR) + wrap) / (1.0 + wrap), 0.0) * LIGHT_FRONT_DIFFUSE;
// Specular, strongest along a layer line, which is what makes a printed wall glint in stripes
// rather than evenly.
float spec = LIGHT_TOP_SPECULAR * pow(max(dot(V, reflect(-LIGHT_TOP_DIR, N)), 0.0), LIGHT_TOP_SHININESS);
spec *= mix(1.0, 0.35 + 0.65 * groove, wall * 0.9);
// Scattering out of the edges: a translucent part lights up where it is seen at a grazing angle
// and where it is thin. Tinted by the filament, because what comes back out has been through it.
float rim = pow(1.0 - clamp(dot(N, V), 0.0, 1.0), 4.0);
vec3 color = albedo * (INTENSITY_AMBIENT + diffuse) + vec3(spec) + albedo * rim * translucency * 0.55;
gl_FragColor = vec4(color, alpha);
}
-40
View File
@@ -1,40 +0,0 @@
#version 110
uniform mat4 view_model_matrix;
uniform mat4 projection_matrix;
uniform mat3 view_normal_matrix;
uniform mat4 volume_world_matrix;
// The volume's normal matrix in world space, so the fragment shader can tell a wall from a terrace -
// which is what decides whether layer lines show at all.
uniform mat3 world_normal_matrix;
// Clipping plane, x = min z, y = max z (the FFF/SLA preview's top/bottom cut).
uniform vec2 z_range;
// Clipping plane, general orientation (the section view).
uniform vec4 clipping_plane;
// GLModel's P3N3 layout, which is what init_from(indexed_triangle_set) produces - this shader needs
// nothing per vertex that the mesh does not already carry, because the colour comes from the
// fragment's own height rather than from anything baked into the vertices.
attribute vec3 v_position;
attribute vec3 v_normal;
varying vec3 clipping_planes_dots;
varying vec3 eye_normal;
varying vec3 eye_position;
varying vec3 world_normal;
varying float world_z;
void main()
{
vec4 model_pos = vec4(v_position, 1.0);
vec4 world_pos = volume_world_matrix * model_pos;
world_z = world_pos.z;
world_normal = normalize(world_normal_matrix * v_normal);
eye_normal = normalize(view_normal_matrix * v_normal);
vec4 eye_pos = view_model_matrix * model_pos;
eye_position = eye_pos.xyz;
gl_Position = projection_matrix * eye_pos;
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
}
-94
View File
@@ -1,94 +0,0 @@
#version 140
// What a ChromaStack print looks like, shaded per fragment.
//
// The colour is not a vertex attribute and not a texture on the surface: it is read from the
// fragment's own height. A filament swap happens at one print Z across the whole plate, so the layer
// a point sits in is what decides its colour - which means a band edge lands exactly on the layer
// boundary however coarse the mesh is, and the same shader is right before a bake (on the preview's
// displaced mesh) and after one (on the volume's own).
//
// On top of that it tries to look like printed plastic rather than like flat shaded colour: the
// terminator is wrapped and the grazing angles glow, because filament translucent enough to be worth
// stacking scatters light rather than reflecting it; and the walls carry layer lines, because that is
// the first thing the eye uses to read a print as a print.
#define INTENSITY_CORRECTION 0.6
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SHININESS 20.0
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION)
#define INTENSITY_AMBIENT 0.3
// The stack's colours, one texel per printed layer, bottom to top. A texture rather than a uniform
// array because a base can be several hundred layers and no uniform array survives that.
uniform sampler2D palette;
uniform float palette_size; // texels in `palette`, i.e. the stack's total printed layers
uniform float base_z; // world Z of the bottom of the object
uniform float layer_height; // mm
// How translucent the stack reads, 0 to 1, from the transmission distance of the filament on top.
// It drives the wrap and the rim: an opaque stack should not glow.
uniform float translucency;
uniform float alpha;
in vec3 clipping_planes_dots;
in vec3 eye_normal;
in vec3 eye_position;
in vec3 world_normal;
in float world_z;
out vec4 out_color;
void main()
{
if (any(lessThan(clipping_planes_dots, vec3(0.0))))
discard;
float z_rel = world_z - base_z;
float layers = max(z_rel / layer_height, 0.0);
// How much of a layer one pixel covers. Everything below is faded out as this grows: layer lines
// finer than a pixel are noise, and a colour read from one layer out of the dozen under a pixel
// flickers as the camera moves.
float px = fwidth(layers);
// The layer this fragment sits in, 1-based. Up close that is an exact texel of the palette; once a
// pixel spans a layer or more it slides to a continuous coordinate so the linear filter averages
// the bands it covers instead of picking one of them at random.
float stepped = clamp(ceil(layers - 1.0e-4), 1.0, palette_size) - 0.5;
float smooth_ = clamp(layers, 0.5, palette_size - 0.5);
float u = mix(stepped, smooth_, clamp(px - 0.5, 0.0, 1.0)) / palette_size;
vec3 albedo = texture(palette, vec2(u, 0.5)).rgb;
vec3 N = normalize(eye_normal);
vec3 V = normalize(-eye_position);
// Layer lines, on the walls only: a terrace is one layer's top surface and has none to show. The
// groove runs round the part at every layer boundary, so it is a function of height alone.
float wall = 1.0 - abs(world_normal.z);
float phase = fract(layers);
float groove = smoothstep(0.0, 0.16, phase) * smoothstep(1.0, 0.84, phase);
groove = mix(groove, 0.65, clamp(px * 1.5, 0.0, 1.0)); // fade to flat when too fine to resolve
float lines = mix(1.0, 0.84 + 0.16 * groove, wall * 0.9);
albedo *= lines;
// Diffuse, with a wrapped terminator: light entering translucent filament comes back out a little
// way round the curve, so the shaded side never goes as dark as an opaque surface's would.
float wrap = 0.25 + 0.45 * translucency;
float ndotl = dot(N, LIGHT_TOP_DIR);
float diffuse = max((ndotl + wrap) / (1.0 + wrap), 0.0) * LIGHT_TOP_DIFFUSE;
diffuse += max((dot(N, LIGHT_FRONT_DIR) + wrap) / (1.0 + wrap), 0.0) * LIGHT_FRONT_DIFFUSE;
// Specular, strongest along a layer line, which is what makes a printed wall glint in stripes
// rather than evenly.
float spec = LIGHT_TOP_SPECULAR * pow(max(dot(V, reflect(-LIGHT_TOP_DIR, N)), 0.0), LIGHT_TOP_SHININESS);
spec *= mix(1.0, 0.35 + 0.65 * groove, wall * 0.9);
// Scattering out of the edges: a translucent part lights up where it is seen at a grazing angle
// and where it is thin. Tinted by the filament, because what comes back out has been through it.
float rim = pow(1.0 - clamp(dot(N, V), 0.0, 1.0), 4.0);
vec3 color = albedo * (INTENSITY_AMBIENT + diffuse) + vec3(spec) + albedo * rim * translucency * 0.55;
out_color = vec4(color, alpha);
}
-40
View File
@@ -1,40 +0,0 @@
#version 140
uniform mat4 view_model_matrix;
uniform mat4 projection_matrix;
uniform mat3 view_normal_matrix;
uniform mat4 volume_world_matrix;
// The volume's normal matrix in world space, so the fragment shader can tell a wall from a terrace -
// which is what decides whether layer lines show at all.
uniform mat3 world_normal_matrix;
// Clipping plane, x = min z, y = max z (the FFF/SLA preview's top/bottom cut).
uniform vec2 z_range;
// Clipping plane, general orientation (the section view).
uniform vec4 clipping_plane;
// GLModel's P3N3 layout, which is what init_from(indexed_triangle_set) produces - this shader needs
// nothing per vertex that the mesh does not already carry, because the colour comes from the
// fragment's own height rather than from anything baked into the vertices.
in vec3 v_position;
in vec3 v_normal;
out vec3 clipping_planes_dots;
out vec3 eye_normal;
out vec3 eye_position;
out vec3 world_normal;
out float world_z;
void main()
{
vec4 model_pos = vec4(v_position, 1.0);
vec4 world_pos = volume_world_matrix * model_pos;
world_z = world_pos.z;
world_normal = normalize(world_normal_matrix * v_normal);
eye_normal = normalize(view_normal_matrix * v_normal);
vec4 eye_pos = view_model_matrix * model_pos;
eye_position = eye_pos.xyz;
gl_Position = projection_matrix * eye_pos;
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
}
@@ -109,7 +109,7 @@ const cssColors = [
{hex: "#800080", name: "Purple"}, {hex: "#800080", name: "Purple"},
{hex: "#663399", name: "Rebecca purple"}, {hex: "#663399", name: "Rebecca purple"},
{hex: "#C71585", name: "Medium violet red"}, {hex: "#C71585", name: "Medium violet red"},
{hex: "#DB7093", name: "PaleV violet red"}, {hex: "#DB7093", name: "Pale violet red"},
{hex: "#FF1493", name: "Deep pink"}, {hex: "#FF1493", name: "Deep pink"},
{hex: "#FF69B4", name: "Hot pink"}, {hex: "#FF69B4", name: "Hot pink"},
{hex: "#FFB6C1", name: "Light pink"}, {hex: "#FFB6C1", name: "Light pink"},
@@ -193,7 +193,7 @@ const palettes = {
{hex: "#40E0D0", name: "Turquoise bay"}, {hex: "#40E0D0", name: "Turquoise bay"},
{hex: "#2EBDEF", name: "Sky over reef"}, {hex: "#2EBDEF", name: "Sky over reef"},
{hex: "#87CEEB", name: "Shallow sky"}, {hex: "#87CEEB", name: "Shallow sky"},
{hex: "#1E90FF", name: "Electric bel"}, {hex: "#1E90FF", name: "Electric blue"},
{hex: "#4169E1", name: "Dolphin"}, {hex: "#4169E1", name: "Dolphin"},
{hex: "#0000CD", name: "Ocean depths"}, {hex: "#0000CD", name: "Ocean depths"},
{hex: "#191970", name: "Whale"}, {hex: "#191970", name: "Whale"},
@@ -219,7 +219,7 @@ const palettes = {
{hex: "#EFBF04", name: "1004 Golden yellow"}, {hex: "#EFBF04", name: "1004 Golden yellow"},
{hex: "#D7A500", name: "1005 Honey yellow"}, {hex: "#D7A500", name: "1005 Honey yellow"},
{hex: "#F2D185", name: "1006 Maize yellow"}, {hex: "#F2D185", name: "1006 Maize yellow"},
{hex: "#FDD185", name: "1007 Dalmatian yellow"}, {hex: "#FDD185", name: "1007 Daffodil yellow"},
{hex: "#8B7D6B", name: "1011 Brown beige"}, {hex: "#8B7D6B", name: "1011 Brown beige"},
{hex: "#F5DB50", name: "1012 Lemon yellow"}, {hex: "#F5DB50", name: "1012 Lemon yellow"},
{hex: "#F3E5AB", name: "1013 Oyster white"}, {hex: "#F3E5AB", name: "1013 Oyster white"},
@@ -318,7 +318,7 @@ const palettes = {
{hex: "#0384A7", name: "5015 Sky blue"}, {hex: "#0384A7", name: "5015 Sky blue"},
{hex: "#006A9E", name: "5017 Traffic blue"}, {hex: "#006A9E", name: "5017 Traffic blue"},
{hex: "#005F87", name: "5018 Turquoise blue"}, {hex: "#005F87", name: "5018 Turquoise blue"},
{hex: "#5A3D5B", name: "5019 Capsanthin violet"}, {hex: "#1B5583", name: "5019 Capri blue"},
{hex: "#04749F", name: "5020 Ocean blue"}, {hex: "#04749F", name: "5020 Ocean blue"},
{hex: "#009AAA", name: "5021 Water blue"}, {hex: "#009AAA", name: "5021 Water blue"},
{hex: "#1A3D5F", name: "5022 Night blue"}, {hex: "#1A3D5F", name: "5022 Night blue"},
@@ -366,7 +366,7 @@ const palettes = {
{hex: "#00A550", name: "6039 Mint green"}, {hex: "#00A550", name: "6039 Mint green"},
{hex: "#7D7D7D", name: "7000 Squirrel grey"}, {hex: "#7D7D7D", name: "7000 Squirrel grey"},
{hex: "#A8A9AD", name: "7001 Silver grey"}, {hex: "#A8A9AD", name: "7001 Silver grey"},
{hex: "#6F7368", name: "7002 Olivestone"}, {hex: "#6F7368", name: "7002 Olive grey"},
{hex: "#8F9080", name: "7003 Moss grey"}, {hex: "#8F9080", name: "7003 Moss grey"},
{hex: "#8E9291", name: "7004 Signal grey"}, {hex: "#8E9291", name: "7004 Signal grey"},
{hex: "#8B8788", name: "7005 Mouse grey"}, {hex: "#8B8788", name: "7005 Mouse grey"},
+251 -4
View File
@@ -12,6 +12,7 @@ commands:
trim delete profile files no <vendor>.json list references trim delete profile files no <vendor>.json list references
update-index regenerate the *_list sections of <vendor>.json update-index regenerate the *_list sections of <vendor>.json
legacy-compat write the printers user presets can be migrated back to 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: options shared by several commands:
--vendor VENDOR act on one vendor bundle only; repeatable, empty means all --vendor VENDOR act on one vendor bundle only; repeatable, empty means all
@@ -94,11 +95,14 @@ parent in the OFL map. filament_vendor / filament_type resolve the same way.
""" """
import argparse import argparse
import difflib
import functools import functools
import html
import json import json
import os import os
import posixpath import posixpath
import re import re
import subprocess
import sys import sys
import uuid import uuid
from collections import Counter, defaultdict from collections import Counter, defaultdict
@@ -192,6 +196,7 @@ VECTOR_KEYS = {
PRINT_CONFIG_CPP = os.path.normpath( PRINT_CONFIG_CPP = os.path.normpath(
os.path.join(SCRIPTS_DIR, "..", "src", "libslic3r", "PrintConfig.cpp")) 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) @functools.lru_cache(maxsize=None)
@@ -313,11 +318,11 @@ REMEDY_HINTS = {
} }
def print_error(msg): def print_error(msg, file=None):
print(f"\033[91m[ERROR]\033[0m {msg}") # Red print(f"\033[91m[ERROR]\033[0m {msg}", file=file) # Red
def print_warning(msg): def print_warning(msg, file=None):
print(f"\033[93m[WARNING]\033[0m {msg}") # Yellow print(f"\033[93m[WARNING]\033[0m {msg}", file=file) # Yellow
def print_info(msg): def print_info(msg):
print(f"\033[94m[INFO]\033[0m {msg}") # Blue print(f"\033[94m[INFO]\033[0m {msg}") # Blue
@@ -3209,6 +3214,223 @@ def run_legacy_compat(base_dir, profiles_dir, output, check):
return 0 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 # CLI
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -3233,6 +3455,9 @@ examples:
orca_profile_tool.py legacy-compat --base /tmp/v2.4.2/resources/profiles 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 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) 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: after adding, renaming or deleting profile files, run in this order:
normalize -> update-index -> generate-id -> check normalize -> update-index -> generate-id -> check
@@ -3387,6 +3612,25 @@ def build_parser():
legacy_cmd.add_argument("--check", action="store_true", legacy_cmd.add_argument("--check", action="store_true",
help="fail if the map differs from what --base implies; write nothing") 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 return parser
@@ -3423,6 +3667,9 @@ def main(argv=None):
if args.command == "legacy-compat": if args.command == "legacy-compat":
return run_legacy_compat(args.base, profiles_dir, args.output, args.check) 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": if args.command == "fix-variant":
_changed, errors = fix_variant_arrays(profiles_dir, vendors, dry_run=args.dry_run, _changed, errors = fix_variant_arrays(profiles_dir, vendors, dry_run=args.dry_run,
strict=args.strict) strict=args.strict)
+309 -3
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Tests for the tree-maintenance half of scripts/orca_profile_tool.py: the """Tests for the tree-maintenance half of scripts/orca_profile_tool.py: the
normalize, trim, update-index, fix-variant and check commands, and the subcommand normalize, trim, update-index, fix-variant, check and gcode-diff commands, and the
dispatch that reaches them (stdlib unittest, no external deps). 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. The id halves are covered by test_filament_id.py and test_setting_id.py.
@@ -14,9 +14,12 @@ import json
import os import os
import re import re
import shutil import shutil
import stat
import subprocess
import sys import sys
import tempfile import tempfile
import unittest import unittest
from unittest import mock
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
@@ -41,7 +44,24 @@ class Tree:
os.makedirs(self.profiles) os.makedirs(self.profiles)
def cleanup(self): def cleanup(self):
shutil.rmtree(self.dir, ignore_errors=True) 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})
def index_path(self, vendor): def index_path(self, vendor):
return os.path.join(self.profiles, vendor + ".json") return os.path.join(self.profiles, vendor + ".json")
@@ -1409,6 +1429,292 @@ class TestDispatch(TreeCase):
self.assertEqual(self.t.read("V", "filament/A.json")["type"], "filament") 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 # the real tree
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+16
View File
@@ -1126,6 +1126,19 @@ 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) int CLI::run(int argc, char **argv)
{ {
// Mark the main thread for the debugger and for runtime checks. // Mark the main thread for the debugger and for runtime checks.
@@ -1176,6 +1189,9 @@ int CLI::run(int argc, char **argv)
::setenv("WEBKIT_FORCE_DMABUF_RENDERER", "1", /* replace */ false); ::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) { if (x11_opt_in) {
// ===== X11 / XWayland (opted in via GDK_BACKEND=x11) ===== // ===== X11 / XWayland (opted in via GDK_BACKEND=x11) =====
BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds."; BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds.";
+3 -7
View File
@@ -812,7 +812,8 @@ bool WallToolPaths::removeEmptyToolPaths(std::vector<VariableWidthLines> &toolpa
* Get the order constraints of the insets when printing walls per region / hole. * 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. * Each returned pair consists of adjacent wall lines where the left has an inset_idx one lower than the right.
* *
* Odd walls should always go after their enclosing wall polygons. * 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.
* *
* \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one. * \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one.
*/ */
@@ -886,12 +887,7 @@ WallToolPaths::ExtrusionLineSet WallToolPaths::getRegionOrder(const std::vector<
continue; // not directly adjacent continue; // not directly adjacent
if (!shorter_then(loc_here - lineloc_nearby.j.p, (lineloc_here.j.w + lineloc_nearby.j.w) / 2 * diagonal_extension)) 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 continue; // points are too far away from each other
if (here->is_odd || nearby->is_odd) { if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) {
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)); order_requirements.emplace(std::make_pair(nearby, here));
} else { } else {
assert((nearby->inset_idx > here->inset_idx) == outer_to_inner); assert((nearby->inset_idx > here->inset_idx) == outer_to_inner);
-2
View File
@@ -327,8 +327,6 @@ set(lisbslic3r_sources
Geometry/VoronoiUtils.cpp Geometry/VoronoiUtils.cpp
Geometry/VoronoiUtils.hpp Geometry/VoronoiUtils.hpp
Geometry/VoronoiVisualUtils.hpp Geometry/VoronoiVisualUtils.hpp
ChromaStack.cpp
ChromaStack.hpp
IMEXArrange.cpp IMEXArrange.cpp
IMEXArrange.hpp IMEXArrange.hpp
IMEXHelpers.cpp IMEXHelpers.cpp
-394
View File
@@ -1,394 +0,0 @@
#include "ChromaStack.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <cstdlib>
#include <numeric>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
#include <miniz.h>
namespace Slic3r {
// How many e-folding lengths of absorption count as "the colour beneath no longer shows". Three
// passes 5% of the light, which is about where the eye stops seeing what is under a layer.
static constexpr float OPAQUE_EFOLDS = 3.f;
// TD is a filament property, so a zero or negative value in a preset is a data error rather than a
// request for an infinitely opaque filament. Clamp instead: below this the filament is opaque at a
// single layer anyway.
static constexpr float TD_MIN_MM = 0.05f;
static constexpr float TD_MAX_MM = 100.f;
static float srgb_to_linear(float c)
{
return c <= 0.04045f ? c / 12.92f : std::pow((c + 0.055f) / 1.055f, 2.4f);
}
static float linear_to_srgb(float c)
{
return c <= 0.0031308f ? c * 12.92f : 1.055f * std::pow(c, 1.f / 2.4f) - 0.055f;
}
// CIELAB under D65, from non-linear sRGB in [0, 1]. Only used for nearest-colour search, so the
// absolute white point matters far less than both sides using the same one.
struct Lab
{
float l = 0.f, a = 0.f, b = 0.f;
};
static Lab srgb_to_lab(float r, float g, float b)
{
const float rl = srgb_to_linear(r), gl = srgb_to_linear(g), bl = srgb_to_linear(b);
const float x = (0.4124f * rl + 0.3576f * gl + 0.1805f * bl) / 0.95047f;
const float y = (0.2126f * rl + 0.7152f * gl + 0.0722f * bl);
const float z = (0.0193f * rl + 0.1192f * gl + 0.9505f * bl) / 1.08883f;
auto f = [](float t) {
return t > 0.008856f ? std::cbrt(t) : (7.787f * t + 16.f / 116.f);
};
const float fx = f(x), fy = f(y), fz = f(z);
return Lab{ 116.f * fy - 16.f, 500.f * (fx - fy), 200.f * (fy - fz) };
}
static float delta_e(const Lab &p, const Lab &q)
{
const float dl = p.l - q.l, da = p.a - q.a, db = p.b - q.b;
return std::sqrt(dl * dl + da * da + db * db);
}
int ChromaStack::relief_layers() const
{
int n = 0;
for (const ChromaBand &band : bands)
n += std::max(0, band.layers);
return n;
}
int ChromaStack::total_layers() const { return std::max(0, base_layers) + relief_layers(); }
float ChromaStack::depth_mm() const { return float(relief_layers()) * layer_height_mm; }
std::vector<ColorRGB> chroma_palette(const ChromaStack &stack)
{
const int total = stack.total_layers();
// No bands means no picture and no filament to print the base in, whatever base_layers says.
if (total <= 0 || stack.bands.empty() || stack.layer_height_mm <= 0.f)
return {};
// The filament each printed layer is in, bottom-up: the base first, in the bottom band's filament,
// then the bands themselves.
std::vector<const ChromaFilament *> per_layer;
per_layer.reserve(size_t(total));
for (int i = 0; i < std::max(0, stack.base_layers); ++i)
per_layer.push_back(&stack.bands.front().filament);
for (const ChromaBand &band : stack.bands)
for (int i = 0; i < band.layers; ++i)
per_layer.push_back(&band.filament);
std::vector<ColorRGB> palette(size_t(total) + 1);
// Linear light, walking up from an opaque base: C_0 is the bottom layer's own colour, so a
// column one layer tall comes out exactly that colour however translucent the filament is.
std::array<float, 3> acc{ srgb_to_linear(per_layer.front()->color.r()), srgb_to_linear(per_layer.front()->color.g()),
srgb_to_linear(per_layer.front()->color.b()) };
auto store = [&palette](size_t k, const std::array<float, 3> &lin) {
palette[k] = ColorRGB(std::clamp(linear_to_srgb(lin[0]), 0.f, 1.f), std::clamp(linear_to_srgb(lin[1]), 0.f, 1.f),
std::clamp(linear_to_srgb(lin[2]), 0.f, 1.f));
};
store(0, acc);
for (int k = 1; k <= total; ++k) {
const ChromaFilament &f = *per_layer[size_t(k - 1)];
const float td = std::clamp(f.td_mm, TD_MIN_MM, TD_MAX_MM);
// The depth at which this filament hides what is behind it, turned into the e-folding length
// of its absorption; see CHROMA_TD_OPAQUE_FRACTION.
const float efold = CHROMA_TD_OPAQUE_FRACTION * td / OPAQUE_EFOLDS;
const float t = std::exp(-stack.layer_height_mm / efold);
const std::array<float, 3> own{ srgb_to_linear(f.color.r()), srgb_to_linear(f.color.g()),
srgb_to_linear(f.color.b()) };
for (int c = 0; c < 3; ++c)
acc[size_t(c)] = (1.f - t) * own[size_t(c)] + t * acc[size_t(c)];
store(size_t(k), acc);
}
return palette;
}
ChromaSolve chroma_solve(const uint8_t *rgb, int width, int height, const ChromaStack &stack)
{
ChromaSolve solve;
const int total = stack.total_layers();
const int base = std::max(0, stack.base_layers);
const int relief = stack.relief_layers();
if (rgb == nullptr || width <= 0 || height <= 0 || relief <= 0 || relief > CHROMA_MAX_RELIEF_LAYERS)
return solve;
const std::vector<ColorRGB> palette = chroma_palette(stack);
if (int(palette.size()) != total + 1)
return solve;
// Only the heights the solver may choose: base (nothing of the relief printed) up to total.
const int choices = relief + 1;
const size_t choice_count = size_t(choices);
std::vector<Lab> choice_lab(choice_count);
std::vector<std::array<uint8_t, 3>> choice_rgb(choice_count);
for (int i = 0; i < choices; ++i) {
const ColorRGB &c = palette[size_t(base + i)];
choice_lab[size_t(i)] = srgb_to_lab(c.r(), c.g(), c.b());
choice_rgb[size_t(i)] = { uint8_t(std::lround(c.r() * 255.f)), uint8_t(std::lround(c.g() * 255.f)),
uint8_t(std::lround(c.b() * 255.f)) };
}
const size_t count = size_t(width) * size_t(height);
solve.width = width;
solve.height = height;
solve.levels.resize(count);
solve.grey.resize(count);
solve.preview.resize(count * 3);
std::vector<float> errors(count, 0.f);
auto pick = [&](float r, float g, float b, float &out_error) {
const Lab target = srgb_to_lab(std::clamp(r, 0.f, 1.f), std::clamp(g, 0.f, 1.f), std::clamp(b, 0.f, 1.f));
int best = 0;
float best_d = std::numeric_limits<float>::max();
for (int i = 0; i < choices; ++i) {
const float d = delta_e(target, choice_lab[size_t(i)]);
if (d < best_d) {
best_d = d;
best = i;
}
}
out_error = best_d;
return best;
};
// The grey level a chosen height is written as. Rounded to the nearest of 256 levels, which for
// any practical layer budget (a few dozen) maps every height to its own level exactly - the
// relief's steps stay on layer boundaries, which is the whole point of quantizing here.
auto grey_of = [relief](int choice) {
return uint8_t(std::lround(255.0 * double(choice) / double(relief)));
};
auto write = [&](size_t px, int choice, float err) {
solve.levels[px] = uint16_t(base + choice);
solve.grey[px] = grey_of(choice);
solve.preview[px * 3] = choice_rgb[size_t(choice)][0];
solve.preview[px * 3 + 1] = choice_rgb[size_t(choice)][1];
solve.preview[px * 3 + 2] = choice_rgb[size_t(choice)][2];
errors[px] = err;
};
if (stack.dither) {
// Floyd-Steinberg, serpentine so the error does not drift consistently to one side. The
// diffusion buffer holds the two rows the kernel reaches, in sRGB - which is where the
// artifact being traded away lives, and what every reference implementation diffuses in.
const size_t row_len = size_t(width);
std::vector<std::array<float, 3>> row(row_len), next(row_len);
for (int x = 0; x < width; ++x) {
const uint8_t *src = rgb + size_t(x) * 3;
row[size_t(x)] = { src[0] / 255.f, src[1] / 255.f, src[2] / 255.f };
}
for (int y = 0; y < height; ++y) {
if (y + 1 < height)
for (int x = 0; x < width; ++x) {
const uint8_t *src = rgb + (size_t(y + 1) * size_t(width) + size_t(x)) * 3;
next[size_t(x)] = { src[0] / 255.f, src[1] / 255.f, src[2] / 255.f };
}
const bool ltr = (y % 2) == 0;
for (int i = 0; i < width; ++i) {
const int x = ltr ? i : width - 1 - i;
const size_t px = size_t(y) * size_t(width) + size_t(x);
float err = 0.f;
const int choice = pick(row[size_t(x)][0], row[size_t(x)][1], row[size_t(x)][2], err);
write(px, choice, err);
std::array<float, 3> residual{};
for (int c = 0; c < 3; ++c)
residual[size_t(c)] = row[size_t(x)][size_t(c)] - choice_rgb[size_t(choice)][size_t(c)] / 255.f;
const int ahead = ltr ? x + 1 : x - 1;
const int back = ltr ? x - 1 : x + 1;
auto spread = [&](std::vector<std::array<float, 3>> &dst, int at, float w) {
if (at < 0 || at >= width)
return;
for (int c = 0; c < 3; ++c)
dst[size_t(at)][size_t(c)] += residual[size_t(c)] * w;
};
spread(row, ahead, 7.f / 16.f);
if (y + 1 < height) {
spread(next, back, 3.f / 16.f);
spread(next, x, 5.f / 16.f);
spread(next, ahead, 1.f / 16.f);
}
}
row.swap(next);
}
} else {
tbb::parallel_for(tbb::blocked_range<int>(0, height), [&](const tbb::blocked_range<int> &range) {
for (int y = range.begin(); y < range.end(); ++y)
for (int x = 0; x < width; ++x) {
const size_t px = size_t(y) * size_t(width) + size_t(x);
const uint8_t *src = rgb + px * 3;
float err = 0.f;
write(px, pick(src[0] / 255.f, src[1] / 255.f, src[2] / 255.f, err), err);
}
});
}
solve.mean_delta_e = count == 0 ? 0. : std::accumulate(errors.begin(), errors.end(), 0.) / double(count);
solve.max_delta_e = errors.empty() ? 0. : double(*std::max_element(errors.begin(), errors.end()));
return solve;
}
float chroma_layers_to_opaque(float td_mm, float layer_height_mm)
{
if (layer_height_mm <= 0.f)
return 0.f;
return CHROMA_TD_OPAQUE_FRACTION * std::clamp(td_mm, TD_MIN_MM, TD_MAX_MM) / layer_height_mm;
}
// Below about one dE two colours are the same colour to the eye. Half of that is the threshold for
// "this layer changed something", so a step has to be comfortably invisible before a layer is called
// wasted.
static constexpr float VISIBLE_DE = 0.5f;
static float step_delta_e(const ColorRGB &a, const ColorRGB &b)
{
return delta_e(srgb_to_lab(a.r(), a.g(), a.b()), srgb_to_lab(b.r(), b.g(), b.b()));
}
int chroma_distinct_levels(const ChromaStack &stack)
{
const std::vector<ColorRGB> palette = chroma_palette(stack);
const int base = std::max(0, stack.base_layers);
if (int(palette.size()) <= base + 1)
return 0;
int n = 0;
for (size_t i = size_t(base) + 1; i < palette.size(); ++i)
if (step_delta_e(palette[i - 1], palette[i]) > VISIBLE_DE)
++n;
return n;
}
void chroma_trim_bands(ChromaStack &stack)
{
// Walked over the stack's own palette rather than over the filaments' TD, because how fast a band
// runs out of anything to say depends on what is under it as much as on the filament. A light band
// over a dark one is finished in four or five layers; a dark one over a light one goes on paying
// for many more, long past the depth at which it is nominally opaque, because the last few percent
// of light coming through still moves a near-black colour perceptibly.
const std::vector<ColorRGB> palette = chroma_palette(stack);
if (palette.empty())
return;
int layer = std::max(0, stack.base_layers); // palette index of the layer under the band being looked at
for (ChromaBand &band : stack.bands) {
int keep = 1;
for (int i = 1; i <= band.layers; ++i) {
const size_t cur = size_t(layer) + size_t(i);
if (cur >= palette.size())
break;
if (step_delta_e(palette[cur - 1], palette[cur]) > VISIBLE_DE)
keep = i; // the last layer of this band that still changes the colour
}
layer += band.layers;
band.layers = std::clamp(keep, 1, band.layers);
}
}
int chroma_level_at(const ChromaStack &stack, double z_mm)
{
const int total = stack.total_layers();
if (total <= 0 || stack.layer_height_mm <= 0.f)
return 0;
// Layer k occupies ((k-1)h, k h], so a point inside it rounds up. The epsilon keeps a surface
// sitting exactly on a layer boundary in the layer that printed it rather than in the one above,
// which is the difference between a terrace showing its own colour and showing the next band's.
const int k = int(std::ceil(z_mm / double(stack.layer_height_mm) - 1e-6));
return std::clamp(k, 1, total);
}
std::vector<unsigned char> chroma_encode_height_png(const ChromaSolve &solve)
{
if (solve.empty())
return {};
size_t size = 0;
void *png = tdefl_write_image_to_png_file_in_memory_ex(solve.grey.data(), solve.width, solve.height, 1, &size,
MZ_DEFAULT_LEVEL, MZ_FALSE);
if (png == nullptr || size == 0)
return {};
const unsigned char *bytes = static_cast<const unsigned char *>(png);
std::vector<unsigned char> out(bytes, bytes + size);
mz_free(png);
return out;
}
std::vector<ChromaRange> chroma_ranges(const ChromaStack &stack, double z_bottom)
{
std::vector<ChromaRange> out;
if (stack.layer_height_mm <= 0.f)
return out;
// The layer height is stored as a float, like every other dimension of a texture displacement
// layer, so multiplying it up leaves noise in the last few digits - 5 layers of 0.1 mm come out as
// 0.5000000074. The slicer derives its own layer boundaries from the print config in double
// precision and lands on 0.5 exactly, and a boundary that misses it by any amount is a boundary a
// layer can fall on the wrong side of. Rounding to the nanometre removes the noise without
// disturbing any layer height a profile can express.
const auto snap = [](double z) { return std::round(z * 1e6) / 1e6; };
// The base is printed in the bottom band's filament - it is what is loaded when the print starts -
// so it extends that band's range down to the first layer instead of being a swap of its own.
int layer = std::max(0, stack.base_layers);
bool first = true;
for (const ChromaBand &band : stack.bands) {
if (band.layers <= 0)
continue;
const double z_min = snap(z_bottom + (first ? 0. : double(layer) * double(stack.layer_height_mm)));
first = false;
layer += band.layers;
const double z_max = snap(z_bottom + double(layer) * double(stack.layer_height_mm));
if (!out.empty() && out.back().filament == band.filament.filament)
out.back().z_max = z_max; // no swap happens here, so it is one range
else
out.push_back(ChromaRange{ z_min, z_max, band.filament.filament });
}
return out;
}
ChromaStack chroma_default_stack(const std::vector<ChromaFilament> &filaments, float layer_height_mm,
int base_layers, int relief_layers)
{
ChromaStack stack;
stack.layer_height_mm = layer_height_mm > 0.f ? layer_height_mm : 0.08f;
stack.base_layers = std::max(0, base_layers);
if (filaments.empty() || relief_layers <= 0)
return stack;
std::vector<ChromaFilament> ordered = filaments;
std::stable_sort(ordered.begin(), ordered.end(), [](const ChromaFilament &a, const ChromaFilament &b) {
// Relative luminance of the linear colour: the perceptual order, so a saturated yellow
// counts as light and a saturated blue as dark, which a plain r+g+b sum gets wrong.
auto lum = [](const ColorRGB &c) {
return 0.2126f * srgb_to_linear(c.r()) + 0.7152f * srgb_to_linear(c.g()) + 0.0722f * srgb_to_linear(c.b());
};
return lum(a.color) > lum(b.color);
});
const int count = int(ordered.size());
int left = relief_layers;
for (int i = 0; i < count; ++i) {
// Spread the remainder over the first bands rather than piling it onto the last one.
const int share = left / (count - i) + ((left % (count - i)) > 0 ? 1 : 0);
ChromaBand band;
band.filament = ordered[size_t(i)];
band.layers = std::max(1, share);
left -= share;
stack.bands.push_back(band);
}
return stack;
}
} // namespace Slic3r
-286
View File
@@ -1,286 +0,0 @@
#ifndef slic3r_ChromaStack_hpp_
#define slic3r_ChromaStack_hpp_
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include <cereal/cereal.hpp>
#include <cereal/types/vector.hpp>
#include "Color.hpp"
namespace Slic3r {
// ChromaStack-style colour printing: a picture is reproduced on a single face by stacking thin layers
// of a few filaments, swapping filament at fixed layer boundaries, and varying the *height* of each
// pixel's column. Where the column is low, the layers above it are missing and the filament below
// shows; where it is high, the topmost filament covers everything beneath it. Because filament is
// translucent, a column that stops part-way through a run shows a blend of that run's filament and
// what is under it, which is what gives far more colours than there are filaments loaded.
//
// So there are only two things to compute, and they are both here:
// - the *palette*: the colour the stack shows at every possible column height (chroma_palette());
// - the *height map*: per pixel, the column height whose palette colour is closest to the picture
// (chroma_solve()).
//
// Everything else is machinery that already exists. The height map is handed to a texture
// displacement layer as its height texture, so the existing projection/preview/bake pipeline turns
// it into real geometry (see TextureDisplacement.hpp), and the filament swaps are handed to the
// object's height ranges (ModelObject::layer_config_ranges), which is the slicer's own way of
// saying "print this Z band with that filament".
// The depth of filament that blocks essentially all the light behind it, as a fraction of the
// filament's transmission distance (TD).
//
// TD's scale is empirical, not physical: its values run from about 1
// for an opaque filament to 10 and above for a nearly clear one, and the community's calibration
// prints (the "seashell" step test) read a TD off the step where the colour beneath stops showing.
// That step is about a tenth of the TD number, which is the only thing this constant encodes - a
// filament of TD 2 stops hiding what is under it at roughly 0.2 mm, i.e. after two or three layers,
// while one of TD 8 needs most of a millimetre. The model below turns that into a per-layer
// transmittance; see chroma_palette().
static constexpr float CHROMA_TD_OPAQUE_FRACTION = 0.1f;
// What a fresh stack gets above the base. Twenty-four layers is about two millimetres at a fine layer
// height: enough steps for a continuous tonal ramp, and still a print that finishes in an evening.
static constexpr int CHROMA_DEFAULT_RELIEF_LAYERS = 24;
// The most picture layers the solve can express. A column's height above the base is stored as one
// byte per pixel in the 8-bit grayscale height map the displacement reads, so beyond this two
// different heights would have to share a grey level and the relief's steps would stop landing on
// layer boundaries - which is the one thing the quantization exists to guarantee.
//
// It bounds the bands, not the stack: the base is whatever the model already is under the painted
// face, often hundreds of layers on a tall part, and nothing about it is quantized into the height
// map. As a limit on the picture it is not one in practice - 255 layers is 20 mm of relief at
// 0.08 mm, far past where a picture gains anything.
static constexpr int CHROMA_MAX_RELIEF_LAYERS = 255;
// The most filament changes one stack may hold. Every band is a swap the printer has to make, so this is
// a limit on patience rather than on the model; the palette itself is bounded by the layer count.
static constexpr size_t CHROMA_MAX_BANDS = 16;
// One filament as the colour model sees it: what colour it is, and how far light gets into it.
struct ChromaFilament
{
// 0-based index into the project's filaments, which is what a height range's `extruder` option
// (1-based) is written from. -1 for a filament that is not loaded, which the GUI refuses to
// solve with rather than silently printing in the wrong colour.
int filament = -1;
ColorRGB color;
// The filament preset's own name, so the panel can say which filament this is. Six similar greys
// are six identical swatches and six identical numbers otherwise. Stored rather than looked up on
// demand because a stack outlives the project's filament list - a preset can be swapped out from
// under a saved stack, and then the name is the only record of what it was solved against.
std::string name;
// Transmission distance, mm, as the filament preset reports it
// (`filament_transmission_distance`). Clamped to a sane range when the palette is built: zero
// would make a layer infinitely opaque and a negative value is meaningless.
float td_mm = 4.f;
template<class Archive> void serialize(Archive &ar)
{
float r = color.r(), g = color.g(), b = color.b();
ar(filament, r, g, b, td_mm, name);
color = ColorRGB(r, g, b);
}
};
// A run of consecutive layers printed in one filament. The stack is a list of these, bottom-up, so
// a band boundary is exactly where the print pauses and the filament is changed.
struct ChromaBand
{
ChromaFilament filament;
int layers = 1;
template<class Archive> void serialize(Archive &ar) { ar(filament, layers); }
};
// The whole recipe: which filaments, in which order, how thick a layer is, and how much of the
// bottom is solid backing rather than picture.
struct ChromaStack
{
std::vector<ChromaBand> bands; // bottom-up; bands.front() is the one the base is printed in
// The print's layer height, mm. Taken from the print profile rather than chosen here - a stack
// solved against 0.08 mm layers prints as the wrong picture at 0.2 mm, so the GUI re-solves when
// the profile's layer height changes.
float layer_height_mm = 0.08f;
// Layers of solid backing under the picture: everything the model already is below the painted
// face. Not part of any band - the bands are the picture - but printed in the bottom band's
// filament, since that is what is loaded when the print starts.
//
// Keeping it out of the bands is what lets it be what it really is: a measurement of the model,
// often a hundred layers or more, while a band is a handful. Folding the two together put a
// three-digit number on a slider meant for single digits and made "how thick is the backing" and
// "how many layers does the first filament get" the same control, which they are not.
//
// The colour model treats the base as opaque. That is what makes a column's colour depend only on
// the layers above it, and why the panel warns about a base thin enough to let the plate show
// through.
int base_layers = 4;
// Whether `base_layers` follows the model or was typed in. It follows by default, and it should:
// the painted face is wherever the geometry puts it, and a base that disagrees with it shifts
// every colour in the picture - the stack ends up anchored at the bottom of the part instead of
// under the face, so the picture's whole range is squeezed into the first few millimetres and
// everything above it clamps to the top band.
//
// It cannot be measured once and left: a layer is created, and its picture chosen, before there
// is any paint to measure, and the face moves again whenever the part is scaled or the layer
// height changes. So the panel re-measures it every frame while this is set.
bool base_auto = true;
// Spread the quantization error into neighbouring pixels (Floyd-Steinberg, serpentine) instead
// of rounding each pixel to its nearest palette colour on its own. A palette of twenty or thirty
// colours has to cover a photograph's whole range, so plain rounding leaves visible bands across
// a sky or a cheek; diffusing the error trades them for fine stipple, which at a tenth of a
// millimetre per pixel reads as the intermediate colour. Off for flat-coloured artwork, where
// stipple is the artifact rather than the cure.
bool dither = false;
// Total printed layers, base included: what the tallest column is.
int total_layers() const;
// Layers the picture is built from, i.e. the bands' own total above the base.
int relief_layers() const;
// The relief's height in mm, which is the `depth_mm` of the texture displacement layer the
// height map is handed to.
float depth_mm() const;
bool empty() const { return bands.empty(); }
template<class Archive> void serialize(Archive &ar) { ar(bands, layer_height_mm, base_layers, dither, base_auto); }
};
// The colour the stack shows at every column height: entry k is a column of exactly k printed
// layers, so the vector has total_layers() + 1 entries.
//
// The model is Beer-Lambert absorption, layer by layer. A layer of thickness h in a filament of
// transmission distance TD passes
//
// T = exp(-h / (CHROMA_TD_OPAQUE_FRACTION * TD / 3))
//
// of the light coming from under it and shows its own colour with the rest, so
//
// C_k = (1 - T_k) * c_k + T_k * C_(k-1)
//
// walking up from an opaque base, C_0 = c_1. The divisor is what makes "opaque at
// CHROMA_TD_OPAQUE_FRACTION * TD" true: three e-folding lengths pass 5% of the light, which is
// where the colour beneath stops being visible to the eye.
//
// Beer-Lambert is not just the physically honest choice here, it is the only one that stays
// consistent when applied per layer: transmittance multiplies, so a run of n layers of one filament
// passes exp(-n h / d) whether it is composited in one step or n, and the palette does not depend on
// how the stack happens to be cut into bands.
//
// Compositing is done on linear light and the entries are converted back to sRGB, because the
// quantity being attenuated is light, not an sRGB code.
std::vector<ColorRGB> chroma_palette(const ChromaStack &stack);
// What chroma_solve() produced.
struct ChromaSolve
{
int width = 0;
int height = 0;
// Per pixel, row-major and top-to-bottom like DecodedHeightTexture: the column's printed layer
// count, between base_layers and total_layers(). Sixteen bits because it counts from the build
// plate, and the base alone can be several hundred layers on a tall part.
std::vector<uint16_t> levels;
// The same thing as the 8-bit grayscale height map the displacement samples: 0 at base_layers,
// 255 at total_layers(). This is what gets encoded as a PNG into the layer's `image_data`, which
// is why the solve has to be the thing that owns the quantization - the relief's steps have to
// land on layer boundaries, and a grey level is the only channel the displacement reads.
std::vector<uint8_t> grey;
// Per pixel RGB: the palette colour the solve chose, i.e. what the print is predicted to look
// like. Shown in the panel next to the source picture, since the 3D view can only show the
// relief - the colours do not exist until the print swaps filament.
std::vector<uint8_t> preview;
// How far the prediction landed from the picture, as a mean and worst-case CIE76 dE over all
// pixels. The panel reports it because it is the only honest answer to "will this stack
// reproduce my image": under about 5 is a good match, over about 15 means the filaments cannot
// reach the picture's colours and no amount of layers will fix it.
double mean_delta_e = 0.;
double max_delta_e = 0.;
bool empty() const { return width <= 0 || height <= 0 || levels.empty(); }
};
// Solves `rgb` (width * height * 3 bytes, row-major, top-to-bottom) against the stack's palette.
// An empty stack, a zero-sized image or a stack with no relief layers gives an empty result.
ChromaSolve chroma_solve(const uint8_t *rgb, int width, int height, const ChromaStack &stack);
// How many layers of this filament it takes to hide what is under it, at this layer height. The
// single most useful number about a stack and the one nothing else makes visible: it is what decides
// whether a band is a gradient or a step.
//
// A filament hides what is behind it at about CHROMA_TD_OPAQUE_FRACTION of its TD (see
// chroma_palette()), so this is that depth in layers. Below two the band has no intermediate colours
// at all - the picture can only use the filament or not use it - which is what a coarse layer height
// does to an otherwise good stack: TD 4 at 0.2 mm covers in two layers, the same filament at 0.08 mm
// in five.
float chroma_layers_to_opaque(float td_mm, float layer_height_mm);
// How many of the picture's layers actually change the colour.
//
// A band eventually stops saying anything: once it hides what is under it, another layer of it shows
// the colour the one below already showed. Those layers still cost print time and still take up
// relief height, and nothing about the stack makes them visible - the band looks like ten layers of
// gradient and is, say, five of gradient and five of flat.
//
// Measured against the palette rather than against TD, because how long a band keeps paying depends
// on what is under it as much as on the filament: a light band over a dark one is finished in four or
// five layers, while a dark one over a light one goes on moving the colour perceptibly well past the
// depth at which it is nominally opaque.
int chroma_distinct_levels(const ChromaStack &stack);
// Shortens every band to its last layer that still changes the colour. Bands already shorter than
// that are left alone: they are still inside their fade, where every layer counts.
void chroma_trim_bands(ChromaStack &stack);
// The printed layer a point `z_mm` above the bottom of the object falls in: 1-based, clamped to the
// stack, 0 for a stack that prints nothing. Pair it with chroma_palette() to get the colour shown
// there.
//
// This, rather than the height map, is what decides the colour a surface shows. A filament swap
// happens at one print Z across the whole plate, so every surface crossing that height changes colour
// whatever the picture says - the terraces of the relief, the walls between them, and the sides of the
// model alike. Colouring a preview this way is therefore not an approximation of the print; it is what
// the print does, and it keeps working after the relief has been baked into the mesh and the height
// map is no longer attached to anything on screen.
int chroma_level_at(const ChromaStack &stack, double z_mm);
// The solve's height map as an 8-bit grayscale PNG, ready to be stored in a
// TextureDisplacementLayer's `image_data`.
std::vector<unsigned char> chroma_encode_height_png(const ChromaSolve &solve);
// One filament swap as an object height range: the Z band, in the object's own coordinates (0 at its
// bottom), that prints in `filament` (0-based).
struct ChromaRange
{
double z_min = 0.;
double z_max = 0.;
int filament = -1;
};
// The stack's bands as height ranges, in object coordinates, starting at `z_bottom` (which is 0 for
// a stack whose base starts at the object's first layer). Bands printed in the same filament as the
// band below are merged, so a stack that repeats a filament does not produce a pointless swap.
std::vector<ChromaRange> chroma_ranges(const ChromaStack &stack, double z_bottom = 0.);
// A starting point for a stack: the given filaments, lightest at the bottom, each with an equal
// share of `relief_layers` on top of `base_layers`.
//
// Light-to-dark is the convention filament-painting guides teach, and it is not arbitrary: a dark
// filament over a light one reads as a midtone at one or two layers and as the dark colour at four,
// so putting the dark filaments last is what buys the long tonal ramp. The reverse order gives a few
// washed-out steps and then nothing.
ChromaStack chroma_default_stack(const std::vector<ChromaFilament> &filaments, float layer_height_mm,
int base_layers, int relief_layers);
} // namespace Slic3r
#endif // slic3r_ChromaStack_hpp_
+1 -1
View File
@@ -1432,7 +1432,7 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo
} }
if (!header_found) { if (!header_found) {
// TRN %1% is the application name. // TRN %1% is the application name.
std::string error_message = format(_u8L("Not a gcode file generated by %1%."), SLIC3R_APP_FULL_NAME); std::string error_message = format(_u8L("Not a G-code file generated by %1%."), SLIC3R_APP_FULL_NAME);
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << error_message; BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << error_message;
throw Slic3r::RuntimeError(error_message.c_str()); throw Slic3r::RuntimeError(error_message.c_str());
} }
+15 -5
View File
@@ -988,7 +988,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
MetadataList metadata; MetadataList metadata;
VolumeMetadataList volumes; VolumeMetadataList volumes;
bool auto_drop { true }; std::optional<bool> auto_drop;
}; };
struct CutObjectInfo struct CutObjectInfo
@@ -1208,6 +1208,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
//IdToGeometryMap m_orig_geometries; // backup & restore //IdToGeometryMap m_orig_geometries; // backup & restore
CurrentConfig m_curr_config; CurrentConfig m_curr_config;
IdToMetadataMap m_objects_metadata; 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; IdToCutObjectInfoMap m_cut_object_infos;
IdToLayerHeightsProfileMap m_layer_heights_profiles; IdToLayerHeightsProfileMap m_layer_heights_profiles;
IdToLayerConfigRangesMap m_layer_config_ranges; IdToLayerConfigRangesMap m_layer_config_ranges;
@@ -1463,6 +1464,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
m_objects.clear(); m_objects.clear();
m_instances.clear(); m_instances.clear();
m_objects_metadata.clear(); m_objects_metadata.clear();
m_items_auto_drop_off.clear();
m_curr_metadata_name.clear(); m_curr_metadata_name.clear();
m_curr_characters.clear(); m_curr_characters.clear();
@@ -1502,6 +1504,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
m_curr_config.object_id = -1; m_curr_config.object_id = -1;
m_curr_config.volume_id = -1; m_curr_config.volume_id = -1;
m_objects_metadata.clear(); m_objects_metadata.clear();
m_items_auto_drop_off.clear();
m_layer_heights_profiles.clear(); m_layer_heights_profiles.clear();
m_layer_config_ranges.clear(); m_layer_config_ranges.clear();
m_brim_ear_points.clear(); m_brim_ear_points.clear();
@@ -2257,11 +2260,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
ObjectMetadata::VolumeMetadataList volumes; ObjectMetadata::VolumeMetadataList volumes;
ObjectMetadata::VolumeMetadataList* volumes_ptr = nullptr; 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); IdToMetadataMap::iterator obj_metadata = m_objects_metadata.find(object.first.second);
if (obj_metadata != m_objects_metadata.end()) { if (obj_metadata != m_objects_metadata.end()) {
// config data has been found, this model was saved using slic3r pe // config data has been found, this model was saved using slic3r pe
model_object->auto_drop = obj_metadata->second.auto_drop; model_object->auto_drop = obj_metadata->second.auto_drop.value_or(legacy_auto_drop);
// apply object's name and config data // apply object's name and config data
for (const Metadata& metadata : obj_metadata->second.metadata) { for (const Metadata& metadata : obj_metadata->second.metadata) {
@@ -2279,6 +2285,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
} }
else { else {
// config data not found, this model was not saved using slic3r pe // 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 // add the entire geometry as the single volume to generate
//volumes.emplace_back(0, (int)obj_geometry->second.triangles.size() - 1); //volumes.emplace_back(0, (int)obj_geometry->second.triangles.size() - 1);
@@ -4121,6 +4128,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
std::string path = bbs_get_attribute_value_string(attributes, num_attributes, PPATH_ATTR); 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)); 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); 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); return !m_load_model || _create_object_instance(path, object_id, transform, printable, 1);
} }
@@ -4489,7 +4498,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
// int instances_count_id = bbs_get_attribute_value_int(attributes, num_attributes, INSTANCESCOUNT_ATTR); // int instances_count_id = bbs_get_attribute_value_int(attributes, num_attributes, INSTANCESCOUNT_ATTR);
ObjectMetadata metadata; ObjectMetadata metadata;
metadata.auto_drop = bbs_get_attribute_value_bool(attributes, num_attributes, AUTO_DROP_ATTR); 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);
m_objects_metadata.insert({ object_id, std::move(metadata) }); m_objects_metadata.insert({ object_id, std::move(metadata) });
m_curr_config.object_id = object_id; m_curr_config.object_id = object_id;
return true; return true;
@@ -10110,7 +10120,7 @@ bool to_xml(std::stringstream &stream, const EmbossShape::SvgFile &svg, const Mo
assert(file_data != nullptr); assert(file_data != nullptr);
if (file_data == nullptr && !svg.path.empty()) file_data = read_from_disk(svg.path); if (file_data == nullptr && !svg.path.empty()) file_data = read_from_disk(svg.path);
if (file_data == nullptr) { 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; return false;
} }
const std::string &file_data_str = *file_data; const std::string &file_data_str = *file_data;
@@ -10126,7 +10136,7 @@ void to_xml(std::stringstream &stream, const EmbossShape &es, const ModelVolume
stream << " <" << SHAPE_TAG << " "; stream << " <" << SHAPE_TAG << " ";
if (es.svg_file.has_value()) if (es.svg_file.has_value())
if (!to_xml(stream, *es.svg_file, volume, archive, export_full_path)) { 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 << "\" "; stream << SHAPE_SCALE_ATTR << "=\"" << es.scale << "\" ";
+10
View File
@@ -3985,6 +3985,16 @@ 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_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("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))); 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 // Sync variant-mapped params into placeholder_parser before processing start gcode
update_placeholder_parser_with_variant_params(); update_placeholder_parser_with_variant_params();
+47 -71
View File
@@ -120,7 +120,13 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags = {
"@PRINT_TIME_HOUR@", "@PRINT_TIME_HOUR@",
"@PRINT_TIME_MINUTE@", "@PRINT_TIME_MINUTE@",
"@PRINT_TIME_SEC@", "@PRINT_TIME_SEC@",
"@USED_FILAMENT_LENGTH@" "@USED_FILAMENT_LENGTH@",
"@NORMAL_PRINT_TIME@",
"@SILENT_PRINT_TIME@",
"@TOTAL_WEIGHT@",
"@TOTAL_COST@",
"@TOTAL_WIPE_TOWER_COST@",
"@TOTAL_WIPE_TOWER_FILAMENT@"
}; };
const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = { const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
@@ -147,7 +153,13 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
"@PRINT_TIME_HOUR@", "@PRINT_TIME_HOUR@",
"@PRINT_TIME_MINUTE@", "@PRINT_TIME_MINUTE@",
"@PRINT_TIME_SEC@", "@PRINT_TIME_SEC@",
"@USED_FILAMENT_LENGTH@" "@USED_FILAMENT_LENGTH@",
"@NORMAL_PRINT_TIME@",
"@SILENT_PRINT_TIME@",
"@TOTAL_WEIGHT@",
"@TOTAL_COST@",
"@TOTAL_WIPE_TOWER_COST@",
"@TOTAL_WIPE_TOWER_FILAMENT@"
}; };
@@ -1264,19 +1276,22 @@ void GCodeProcessor::run_post_process()
return ret; return ret;
}; };
// Process inline placeholders (print_time_total_sec, print_time_day, print_time_hour, print_time_minute, print_time_sec and used_filament_length) // Process inline placeholders (print_time_*, used_filament_length and the print statistics known only after export)
auto process_inline_placeholders = [&](std::string& gcode_line) { auto process_inline_placeholders = [&](std::string& gcode_line) {
bool processed = false; bool processed = false;
// Every inline placeholder contains '@', so a line without one has nothing to replace. // Every inline placeholder contains '@', so a line without one has nothing to replace.
if (gcode_line.find('@') == std::string::npos) if (gcode_line.find('@') == std::string::npos)
return processed; return processed;
const std::string& print_time_total_placeholder = reserved_tag(ETags::Print_Time_Total_Sec_Placeholder); auto replace_tag = [&gcode_line, &processed](ETags tag, const std::string& value) {
const std::string& print_time_day_placeholder = reserved_tag(ETags::Print_Time_Day_Placeholder); const std::string& placeholder = reserved_tag(tag);
const std::string& print_time_hour_placeholder = reserved_tag(ETags::Print_Time_Hour_Placeholder); size_t pos = gcode_line.find(placeholder);
const std::string& print_time_minute_placeholder = reserved_tag(ETags::Print_Time_Minute_Placeholder); while (pos != std::string::npos) {
const std::string& print_time_sec_placeholder = reserved_tag(ETags::Print_Time_Sec_Placeholder); gcode_line.replace(pos, placeholder.length(), value);
const std::string& used_filament_placeholder = reserved_tag(ETags::Used_Filament_Length_Placeholder); processed = true;
pos = gcode_line.find(placeholder, pos + value.length());
}
};
double print_time_total_sec = m_time_processor.machines[static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal)].time; double print_time_total_sec = m_time_processor.machines[static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal)].time;
if (print_time_total_sec < 0.0) if (print_time_total_sec < 0.0)
@@ -1289,70 +1304,31 @@ void GCodeProcessor::run_post_process()
int print_time_minute = (day_remainder_seconds % 3600) / 60; int print_time_minute = (day_remainder_seconds % 3600) / 60;
int print_time_sec = day_remainder_seconds % 60; int print_time_sec = day_remainder_seconds % 60;
// Replace print_time_total_sec double total_filament_mm = 0.0;
size_t pos = gcode_line.find(print_time_total_placeholder); for (const auto& mm : filament_mm) {
while (pos != std::string::npos) { total_filament_mm += mm;
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 print_time_day replace_tag(ETags::Print_Time_Total_Sec_Placeholder, Slic3r::float_to_string_decimal_point(print_time_total_sec, 2));
pos = gcode_line.find(print_time_day_placeholder); replace_tag(ETags::Print_Time_Day_Placeholder, std::to_string(print_time_day));
while (pos != std::string::npos) { replace_tag(ETags::Print_Time_Hour_Placeholder, std::to_string(print_time_hour));
char buf[64]; replace_tag(ETags::Print_Time_Minute_Placeholder, std::to_string(print_time_minute));
sprintf(buf, "%d", print_time_day); replace_tag(ETags::Print_Time_Sec_Placeholder, std::to_string(print_time_sec));
gcode_line.replace(pos, print_time_day_placeholder.length(), buf); replace_tag(ETags::Used_Filament_Length_Placeholder,
processed = true; Slic3r::float_to_string_decimal_point(total_filament_mm / 1000.0, 2)); // Convert mm to m
pos = gcode_line.find(print_time_day_placeholder, pos + strlen(buf)); // 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 print_time_hour replace_tag(ETags::Silent_Print_Time_Placeholder, is_stealth_time_estimator_enabled() ?
pos = gcode_line.find(print_time_hour_placeholder); get_time_dhm(PrintEstimatedStatistics::ETimeMode::Stealth) :
while (pos != std::string::npos) { short_time("N/A"));
char buf[64]; const double total_cost = filament_total_cost + m_print->config().time_cost.value * print_time_total_sec / 3600.0;
sprintf(buf, "%d", print_time_hour); replace_tag(ETags::Total_Weight_Placeholder, Slic3r::float_to_string_decimal_point(filament_total_g, 2));
gcode_line.replace(pos, print_time_hour_placeholder.length(), buf); replace_tag(ETags::Total_Cost_Placeholder, Slic3r::float_to_string_decimal_point(total_cost, 2));
processed = true; replace_tag(ETags::Total_Wipe_Tower_Cost_Placeholder,
pos = gcode_line.find(print_time_hour_placeholder, pos + strlen(buf)); 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_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; return processed;
}; };
+6
View File
@@ -601,6 +601,12 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
Print_Time_Minute_Placeholder, Print_Time_Minute_Placeholder,
Print_Time_Sec_Placeholder, Print_Time_Sec_Placeholder,
Used_Filament_Length_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)]; } 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)]; }
+65 -24
View File
@@ -62,7 +62,12 @@ public:
// Depth in the hierarchy. External perimeter has depth = 0. An external perimeter could be both a contour and a hole. // Depth in the hierarchy. External perimeter has depth = 0. An external perimeter could be both a contour and a hole.
unsigned short depth; unsigned short depth;
// ORCA: an external hole perimeter touching the next outer wall all around, with nothing between them. // 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; 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). // Children contour, may be both CCW and CW oriented (outer contours or holes).
std::vector<PerimeterGeneratorLoop> children; std::vector<PerimeterGeneratorLoop> children;
@@ -572,7 +577,8 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
extrusion_coll.append(std::move(extrusion_loop)); 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. // 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; const bool thin_wall_hole = !pg_extrusion.is_contour && pg_extrusions.size() == 2;
if (thin_wall_hole && perimeter_generator.config->wall_sequence != WallSequence::OuterInner) if (thin_wall_hole && perimeter_generator.config->wall_sequence != WallSequence::OuterInner &&
!perimeter_generator.outer_walls_first_for_brim())
std::reverse(extrusion_coll.entities.begin(), extrusion_coll.entities.end()); std::reverse(extrusion_coll.entities.begin(), extrusion_coll.entities.end());
} }
else { else {
@@ -1690,13 +1696,24 @@ void PerimeterGenerator::process_classic()
last = std::move(offsets); last = std::move(offsets);
// ORCA: a thin wall hole has no room for gap fill or an inner wall anywhere beside its outer wall. // 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.
if (i == 0 && ! holes[0].empty()) { if (i == 0 && ! holes[0].empty()) {
const float room = float(0.5 * (ext_perimeter_spacing2 + gap_fill_min_width)); auto reach = [&last](double room) {
const Polygons reach = to_polygons(offset2_ex(last, -room, room + float(SCALED_EPSILON))); return to_polygons(offset2_ex(last, -float(room), float(room) + float(SCALED_EPSILON)));
for (PerimeterGeneratorLoop &hole : holes[0]) };
hole.is_thin_wall_hole = intersection_pl(Polylines{ hole.polygon.split_at_first_point() }, auto out_of_reach = [](const Polygon &hole, const Polygons &reach) {
ClipperUtils::clip_clipper_polygons_with_subject_bbox(reach, get_extents(hole.polygon).inflated(SCALED_EPSILON))).empty(); 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);
}
} }
//BBS: refer to superslicer //BBS: refer to superslicer
@@ -1835,18 +1852,16 @@ void PerimeterGenerator::process_classic()
// if brim will be printed, reverse the order of perimeters so that // if brim will be printed, reverse the order of perimeters so that
// we continue inwards after having finished the brim // 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; bool is_outer_wall_first = this->config->wall_sequence == WallSequence::OuterInner;
if (is_outer_wall_first || // Orca: a contour with only a narrow wall hole beside it has nothing to sandwich, and prints the hole first,
//BBS: always print outer wall first when there indeed has brim. // then the contour, then the gap fill between them, as Arachne does.
// btLeadingEdgeOnly is an outer brim too (a belt brim at the part's first contact). const PerimeterGeneratorLoops &island_loops = contours.front();
(this->layer_id == 0 && const bool narrow_wall_hole_island = island_loops.size() == 1 && island_loops.front().children.size() == 1 &&
(this->object_config->brim_type == BrimType::btOuterOnly || island_loops.front().children.front().is_narrow_wall_hole;
this->object_config->brim_type == BrimType::btLeadingEdgeOnly) && if (is_outer_wall_first || this->outer_walls_first_for_brim())
this->object_config->brim_width.value > 0))
entities.reverse(); entities.reverse();
// Orca: sandwich mode. Apply after 1st layer. // Orca: sandwich mode. Apply after 1st layer.
else if ((this->config->wall_sequence == WallSequence::InnerOuterInner) && layer_id > 0){ else if ((this->config->wall_sequence == WallSequence::InnerOuterInner) && layer_id > 0 && !narrow_wall_hole_island){
entities.reverse(); // reverse all entities - order them from external to internal 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 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. int position = 0; // index to run the re-ordering for multiple external perimeters in a single island.
@@ -2412,11 +2427,25 @@ std::vector<PerimeterGeneratorArachneExtrusion> reorderPerimetersByProximity(std
// Function to reorder perimeters starting from a given reference index // Function to reorder perimeters starting from a given reference index
auto reorderFromReference = [&](int referenceIndex) { auto reorderFromReference = [&](int referenceIndex) {
std::unordered_set<int> firstLevelIndices; // Find first level touching perimeters. The odd centre line of a wall too narrow for two inner walls is shared
firstLevelIndices.insert(referenceIndex); // 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};
// Find first level touching perimeters std::vector<int> firstLevelTouchingIndices;
std::vector<int> firstLevelTouchingIndices = findAllTouchingPerimeters(entities, firstLevelIndices, threshold_external, threshold_internal, 1); 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; });
// Bring the largest first level perimeter to the front // Bring the largest first level perimeter to the front
// The longest first neighbour is most likely the dominant proximate perimeter // The longest first neighbour is most likely the dominant proximate perimeter
// hence printing it immediately after the external perimeter should speed things up // hence printing it immediately after the external perimeter should speed things up
@@ -2427,8 +2456,10 @@ std::vector<PerimeterGeneratorArachneExtrusion> reorderPerimetersByProximity(std
std::iter_swap(maxIt, firstLevelTouchingIndices.end() - 1); std::iter_swap(maxIt, firstLevelTouchingIndices.end() - 1);
} }
// Insert first level perimeters into reordered list // Insert first level perimeters into reordered list
reordered.push_back(entities[referenceIndex]); for (int idx : outerIndices) {
includedIndices.insert(referenceIndex); reordered.push_back(entities[idx]);
includedIndices.insert(idx);
}
for (int idx : firstLevelTouchingIndices) { for (int idx : firstLevelTouchingIndices) {
if (includedIndices.count(idx) == 0) { if (includedIndices.count(idx) == 0) {
@@ -2757,7 +2788,7 @@ void PerimeterGenerator::process_arachne()
if (layer_id == 0){ // disable inner outer inner algorithm after the first layer if (layer_id == 0){ // disable inner outer inner algorithm after the first layer
is_outer_wall_first = is_outer_wall_first =
this->config->wall_sequence == WallSequence::OuterInner; this->config->wall_sequence == WallSequence::OuterInner || this->outer_walls_first_for_brim();
} }
if (is_outer_wall_first) { if (is_outer_wall_first) {
start_perimeter = 0; start_perimeter = 0;
@@ -3042,6 +3073,16 @@ ExPolygons PerimeterGenerator::printable_slices(const ExPolygons &slices) const
return min_width > EPSILON ? opening_ex(slices, float(scale_(min_width / 2.))) : slices; 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) 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); float nozzle_diameter = print_config->nozzle_diameter.get_at(config->outer_wall_filament_id - 1);
+6
View File
@@ -164,6 +164,8 @@ public:
Polygons lower_slices_polygons() const { return m_lower_slices_polygons; } 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. // 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; 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: private:
std::vector<Polygons> generate_lower_polygons_series(float width); std::vector<Polygons> generate_lower_polygons_series(float width);
@@ -183,6 +185,10 @@ private:
Polygons m_lower_slices_polygons; 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 #endif
+3 -1
View File
@@ -1421,6 +1421,7 @@ static std::vector<std::string> s_Preset_print_options{
"initial_layer_travel_acceleration", "initial_layer_travel_acceleration",
"initial_layer_travel_jerk", "initial_layer_travel_jerk",
"exclude_object", "exclude_object",
"exclude_object_tolerance",
"slow_down_layers", "slow_down_layers",
"infill_anchor", "infill_anchor",
"infill_anchor_max", "infill_anchor_max",
@@ -1489,7 +1490,7 @@ static std::vector<std::string> s_Preset_print_options{
static std::vector<std::string> s_Preset_filament_options {/*"filament_colour", */ "default_filament_colour", "required_nozzle_HRC", "filament_diameter", "pellet_flow_coefficient", "volumetric_speed_coefficients", "filament_type", static std::vector<std::string> s_Preset_filament_options {/*"filament_colour", */ "default_filament_colour", "required_nozzle_HRC", "filament_diameter", "pellet_flow_coefficient", "volumetric_speed_coefficients", "filament_type",
"filament_soluble", "filament_is_support", "filament_printable", "filament_extruder_compatibility", "filament_soluble", "filament_is_support", "filament_printable", "filament_extruder_compatibility",
"filament_max_volumetric_speed", "filament_adaptive_volumetric_speed", "filament_max_volumetric_speed", "filament_adaptive_volumetric_speed",
"filament_flow_ratio", "filament_density", "filament_transmission_distance", "filament_adhesiveness_category", "filament_cost", "filament_minimal_purge_on_wipe_tower", "filament_flow_ratio", "filament_density", "filament_adhesiveness_category", "filament_cost", "filament_minimal_purge_on_wipe_tower",
"filament_tower_interface_pre_extrusion_dist", "filament_tower_interface_pre_extrusion_length", "filament_tower_ironing_area", "filament_tower_interface_purge_volume", "filament_tower_interface_pre_extrusion_dist", "filament_tower_interface_pre_extrusion_length", "filament_tower_ironing_area", "filament_tower_interface_purge_volume",
"filament_tower_interface_print_temp", "filament_tower_interface_print_temp",
"nozzle_temperature", "nozzle_temperature_initial_layer", "nozzle_temperature", "nozzle_temperature_initial_layer",
@@ -2293,6 +2294,7 @@ 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; BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent for config %1%!")%preset->file;
continue; 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.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
//preset->config.apply(std::move(config)); //preset->config.apply(std::move(config));
Preset::normalize(preset->config); Preset::normalize(preset->config);
+8 -1
View File
@@ -756,7 +756,14 @@ public:
Preset& get_edited_preset() { return m_edited_preset; } Preset& get_edited_preset() { return m_edited_preset; }
const Preset& get_edited_preset() const { return m_edited_preset; } const Preset& get_edited_preset() const { return m_edited_preset; }
const Preset& get_selected_preset_base() const { return *get_preset_base(m_presets[m_idx_selected]); } // 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;
}
// Return the last saved preset. // Return the last saved preset.
// const Preset& get_saved_preset() const { return m_saved_preset; } // const Preset& get_saved_preset() const { return m_saved_preset; }
+2 -2
View File
@@ -228,7 +228,6 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
"filament_diameter", "filament_diameter",
"volumetric_speed_coefficients", "volumetric_speed_coefficients",
"filament_density", "filament_density",
"filament_transmission_distance",
"filament_cost", "filament_cost",
"filament_notes", "filament_notes",
"outer_wall_acceleration", "outer_wall_acceleration",
@@ -310,6 +309,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
"gcode_comments", "gcode_comments",
"gcode_label_objects", "gcode_label_objects",
"exclude_object", "exclude_object",
"exclude_object_tolerance",
"support_material_interface_fan_speed", "support_material_interface_fan_speed",
"internal_bridge_fan_speed", // ORCA: Add support for separate internal bridge fan speed control "internal_bridge_fan_speed", // ORCA: Add support for separate internal bridge fan speed control
"ironing_fan_speed", "ironing_fan_speed",
@@ -6930,7 +6930,7 @@ BoundingBoxf3 PrintInstance::get_bounding_box() const {
Polygon PrintInstance::get_convex_hull_2d() { Polygon PrintInstance::get_convex_hull_2d() {
Polygon poly = print_object->model_object()->convex_hull_2d(model_instance->get_matrix()); Polygon poly = print_object->model_object()->convex_hull_2d(model_instance->get_matrix());
poly.douglas_peucker(scale_(0.1)); poly.douglas_peucker(scale_(print_object->print()->config().exclude_object_tolerance.value));
return poly; return poly;
} }
+11 -14
View File
@@ -3440,20 +3440,6 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced; def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloats { 0. }); def->set_default_value(new ConfigOptionFloats { 0. });
def = this->add("filament_transmission_distance", coFloats);
def->label = L("Transmission distance");
def->tooltip = L("How deep light gets into this filament, in millimetres, also called its "
"transmission distance or TD. About 1 for an opaque filament, 4 to 7 for a usefully "
"translucent one, 8 and above for a nearly clear one. Used only to predict the "
"colours of a ChromaStack in the texture displacement tool; it has no effect on "
"slicing. Measure it with a step test print, or take a published value for the "
"filament.");
def->sidetext = L("mm");
def->min = 0;
def->max = 100;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloats { 4. });
def = this->add("filament_type", coStrings); def = this->add("filament_type", coStrings);
def->label = L("Type"); def->label = L("Type");
def->tooltip = L("Filament material type"); def->tooltip = L("Filament material type");
@@ -4643,6 +4629,17 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced; def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false)); 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 = this->add("gcode_comments", coBool);
def->label = L("Verbose G-code"); 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. " def->tooltip = L("Enable this to get a commented G-code file, with each line explained by a descriptive text. "
+1 -3
View File
@@ -1709,9 +1709,6 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionStrings, volumetric_speed_coefficients)) ((ConfigOptionStrings, volumetric_speed_coefficients))
((ConfigOptionInts, filament_adhesiveness_category)) ((ConfigOptionInts, filament_adhesiveness_category))
((ConfigOptionFloats, filament_density)) ((ConfigOptionFloats, filament_density))
// Transmission distance: how deep light gets into this filament. Not used by
// slicing - it is the colour model of a ChromaStack stack (see ChromaStack.hpp).
((ConfigOptionFloats, filament_transmission_distance))
((ConfigOptionStrings, filament_type)) ((ConfigOptionStrings, filament_type))
((ConfigOptionBools, filament_soluble)) ((ConfigOptionBools, filament_soluble))
((ConfigOptionStrings, filament_ids)) ((ConfigOptionStrings, filament_ids))
@@ -2133,6 +2130,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionPercents, filament_shrinkage_compensation_z)) ((ConfigOptionPercents, filament_shrinkage_compensation_z))
((ConfigOptionBool, gcode_label_objects)) ((ConfigOptionBool, gcode_label_objects))
((ConfigOptionBool, exclude_object)) ((ConfigOptionBool, exclude_object))
((ConfigOptionFloat, exclude_object_tolerance))
((ConfigOptionFloats, grab_length)) ((ConfigOptionFloats, grab_length))
((ConfigOptionBool, gcode_comments)) ((ConfigOptionBool, gcode_comments))
((ConfigOptionInt, slow_down_layers)) ((ConfigOptionInt, slow_down_layers))
+8 -9
View File
@@ -17,6 +17,7 @@
#include "I18N.hpp" #include "I18N.hpp"
#include "Layer.hpp" #include "Layer.hpp"
#include "MutablePolygon.hpp" #include "MutablePolygon.hpp"
#include "PerimeterGenerator.hpp"
#include "PrintBase.hpp" #include "PrintBase.hpp"
#include "PrintConfig.hpp" #include "PrintConfig.hpp"
#include "SLA/IndexedMesh.hpp" #include "SLA/IndexedMesh.hpp"
@@ -1464,15 +1465,13 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "top_surface_speed") { || opt_key == "top_surface_speed") {
// Brim is printed below supports, support invalidates brim and skirt. // Brim is printed below supports, support invalidates brim and skirt.
steps.emplace_back(posSupportMaterial); steps.emplace_back(posSupportMaterial);
if (opt_key == "brim_type") { if (opt_key == "brim_type" || opt_key == "brim_width") {
const auto* old_brim_type = old_config.option<ConfigOptionEnum<BrimType>>(opt_key); // Regenerate the walls when the brim starts or stops putting the first layer's outer walls first.
const auto* new_brim_type = new_config.option<ConfigOptionEnum<BrimType>>(opt_key); auto outer_walls_first = [](const ConfigOptionResolver &config) {
//BBS: When switch to manual brim, the object must have brim, then re-generate perimeter return brim_puts_outer_walls_first(config.option<ConfigOptionEnum<BrimType>>("brim_type")->value,
//to make the wall order of first layer to be outer-first config.option<ConfigOptionFloat>("brim_width")->value);
// btLeadingEdgeOnly is printed as an outer brim (Brim.cpp, BeltBrim.cpp), so it };
// takes part in the same first-layer wall order rule. if (outer_walls_first(old_config) != outer_walls_first(new_config))
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); steps.emplace_back(posPerimeters);
} }
} else if ( } else if (
+5 -8
View File
@@ -707,8 +707,10 @@ void tree_supports_generate_paths(
false); false);
} }
if (eec) { if (eec) {
std::reverse(eec->entities.begin(), eec->entities.end()); if (!eec->empty()) {
dst.emplace_back(eec.release()); std::reverse(eec->entities.begin(), eec->entities.end());
dst.emplace_back(eec.release());
}
} }
} }
} }
@@ -1825,12 +1827,7 @@ void generate_support_toolpaths(
no_sort = true; no_sort = true;
} else if (support_params.support_style == SupportMaterialStyle::smsTreeOrganic && } else if (support_params.support_style == SupportMaterialStyle::smsTreeOrganic &&
(config.support_base_pattern == smpNone || config.support_base_pattern == smpDefault)) { (config.support_base_pattern == smpNone || config.support_base_pattern == smpDefault)) {
// Orca: A special case for the hollow Organic supports tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow, support_params);
// 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; done = true;
} }
if (! done) if (! done)
+10 -22
View File
@@ -2149,7 +2149,9 @@ void TreeSupport::draw_circles()
const bool with_lightning_infill = m_support_params.base_fill_pattern == ipLightning; const bool with_lightning_infill = m_support_params.base_fill_pattern == ipLightning;
coordf_t support_extrusion_width = m_support_params.support_extrusion_width; coordf_t support_extrusion_width = m_support_params.support_extrusion_width;
const float tree_brim_width = config.tree_support_brim_width.value; 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 // 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 // tilted, moving belt. So the first-object-layer adhesion features (the tree
// support brim, the hybrid first-layer base expansion) must be suppressed: // support brim, the hybrid first-layer base expansion) must be suppressed:
@@ -2201,7 +2203,7 @@ void TreeSupport::draw_circles()
size_t first_base_roof_area = 0; size_t first_base_roof_area = 0;
bool floor_interface_as_base = false; bool floor_interface_as_base = false;
bool has_circle_node = false; bool has_circle_node = false;
bool need_extra_wall = false; ExPolygons extra_wall_areas;
ExPolygons collision_sharp_tails; ExPolygons collision_sharp_tails;
ExPolygons collision_base; ExPolygons collision_base;
auto get_collision = [&](bool sharp_tail) -> ExPolygons &{ auto get_collision = [&](bool sharp_tail) -> ExPolygons &{
@@ -2279,7 +2281,6 @@ void TreeSupport::draw_circles()
// area = diff_clipped({ ExPolygon(circle) }, get_collision(node.is_sharp_tail && node.distance_to_top <= 0)); // area = diff_clipped({ ExPolygon(circle) }, get_collision(node.is_sharp_tail && node.distance_to_top <= 0));
if (!area.empty()) has_circle_node = true; if (!area.empty()) has_circle_node = true;
if (node.need_extra_wall) need_extra_wall = true;
// merge overhang to get a smoother interface surface // merge overhang to get a smoother interface surface
// Do not merge when buildplate_only is on, because some underneath nodes may have been deleted. // Do not merge when buildplate_only is on, because some underneath nodes may have been deleted.
@@ -2294,6 +2295,10 @@ 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) if (obj_layer_nr>0 && node.distance_to_top < 0)
append(roof_gap_areas, area); append(roof_gap_areas, area);
else if (obj_layer_nr > 0 && node.support_roof_layers_below == 1 && else if (obj_layer_nr > 0 && node.support_roof_layers_below == 1 &&
@@ -2530,7 +2535,8 @@ void TreeSupport::draw_circles()
//if (area(expoly) < SQ(scale_(1))) continue; //if (area(expoly) < SQ(scale_(1))) continue;
area_groups.emplace_back(&expoly, SupportLayer::BaseType, max_layers_above_base); area_groups.emplace_back(&expoly, SupportLayer::BaseType, max_layers_above_base);
area_groups.back().need_infill = overlaps({ expoly }, area_poly); area_groups.back().need_infill = overlaps({ expoly }, area_poly);
area_groups.back().need_extra_wall = need_extra_wall && !area_groups.back().need_infill; area_groups.back().need_extra_wall = !area_groups.back().need_infill &&
!extra_wall_areas.empty() && overlaps({expoly}, extra_wall_areas);
} }
for (size_t roof_idx = 0; roof_idx < ts_layer->roof_areas.size(); ++roof_idx) { for (size_t roof_idx = 0; roof_idx < ts_layer->roof_areas.size(); ++roof_idx) {
auto &expoly = ts_layer->roof_areas[roof_idx]; auto &expoly = ts_layer->roof_areas[roof_idx];
@@ -3433,11 +3439,6 @@ 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++) { for (int layer_nr = 0; layer_nr< contact_nodes.size(); layer_nr++) {
std::vector<SupportNode *> &curr_layer_nodes = contact_nodes[layer_nr]; std::vector<SupportNode *> &curr_layer_nodes = contact_nodes[layer_nr];
if (curr_layer_nodes.empty()) continue; if (curr_layer_nodes.empty()) continue;
@@ -3447,20 +3448,17 @@ void TreeSupport::smooth_nodes()
std::vector<double> radii; std::vector<double> radii;
std::vector<SupportNode *> branch; std::vector<SupportNode *> branch;
SupportNode * p_node = node; SupportNode * p_node = node;
float total_height = 0;
// add a fixed head if it's not a polygon node, see STUDIO-4403 // 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 // 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) { if (node->child && node->child->type!=ePolygon) {
pts.push_back(p_node->child->position); pts.push_back(p_node->child->position);
radii.push_back(p_node->child->radius); radii.push_back(p_node->child->radius);
branch.push_back(p_node->child); branch.push_back(p_node->child);
total_height += p_node->child->height;
} }
do { do {
pts.push_back(p_node->position); pts.push_back(p_node->position);
radii.push_back(p_node->radius); radii.push_back(p_node->radius);
branch.push_back(p_node); branch.push_back(p_node);
total_height += p_node->height;
p_node = p_node->parent; p_node = p_node->parent;
} while (p_node && !p_node->is_processed); } while (p_node && !p_node->is_processed);
if (pts.size() < 3) continue; if (pts.size() < 3) continue;
@@ -3479,9 +3477,6 @@ void TreeSupport::smooth_nodes()
branch[i]->radius = radii1[i]; branch[i]->radius = radii1[i];
branch[i]->movement = (pts[i + 1] - pts[i - 1]) / 2; branch[i]->movement = (pts[i + 1] - pts[i - 1]) / 2;
branch[i]->is_processed = true; 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; BOOST_LOG_TRIVIAL(info) << "smooth_nodes: layer_nr=" << layer_nr << ", i=" << i << ", pt=" << pt << ", movement=" << branch[i]->movement << ", radius=" << branch[i]->radius;
} }
} }
@@ -3489,13 +3484,6 @@ void TreeSupport::smooth_nodes()
std::swap(pts, pts1); std::swap(pts, pts1);
std::swap(radii, radii1); 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,7 +135,6 @@ struct SupportNode
TreeNodeType type = eCircle; TreeNodeType type = eCircle;
bool is_corner = false; bool is_corner = false;
bool is_processed = false; bool is_processed = false;
bool need_extra_wall = false;
bool is_sharp_tail = false; bool is_sharp_tail = false;
bool valid = true; bool valid = true;
ExPolygon overhang; // when type==ePolygon, set this value to get original overhang area ExPolygon overhang; // when type==ePolygon, set this value to get original overhang area
+2 -22
View File
@@ -18,7 +18,6 @@
#include <array> #include <array>
#include "ChromaStack.hpp"
#include "Point.hpp" #include "Point.hpp"
#include "TriangleSelector.hpp" #include "TriangleSelector.hpp"
@@ -354,33 +353,16 @@ struct TextureDisplacementLayer
// for the boundary to be drawn on. // for the boundary to be drawn on.
bool color_enabled = false; bool color_enabled = false;
// ChromaStack (see ChromaStack.hpp): when set, `image_data` is not a texture the user picked but the
// height map chroma_solve() computed, so that the whole displacement pipeline - preview, UV
// editor, bake - goes on reading one grayscale height map and needs to know nothing about colour
// stacks. The picture it was solved from is kept here instead, because the solve has to be redone
// whenever the stack, the print's layer height or the picture itself changes.
//
// A ChromaStack layer gets its colour from filament swaps at fixed Z, which is what `chroma`
// records and what the object's height ranges carry out. That is a different mechanism from
// `color_enabled` (per-triangle filament assignment), and the two are mutually exclusive: the
// panel turns colour off on a ChromaStack layer, since painting the relief in one filament per
// triangle would hide the very stack the heights were solved against.
bool chroma_enabled = false;
ChromaStack chroma;
std::shared_ptr<std::vector<unsigned char>> chroma_source;
bool empty() const { return !image_data || image_data->empty(); } bool empty() const { return !image_data || image_data->empty(); }
template<class Archive> void save(Archive &ar) const template<class Archive> void save(Archive &ar) const
{ {
std::string blob = image_data ? std::string(image_data->begin(), image_data->end()) : std::string(); std::string blob = image_data ? std::string(image_data->begin(), image_data->end()) : std::string();
std::string source = chroma_source ? std::string(chroma_source->begin(), chroma_source->end()) : std::string();
ar(slot, name, path, path_in_3mf, blob, depth_mm, tiling_scale, rotation_deg, offset, invert, tile_enabled, ar(slot, name, path, path_in_3mf, blob, depth_mm, tiling_scale, rotation_deg, offset, invert, tile_enabled,
static_cast<int>(tile_method), static_cast<int>(projection_method), lscm_seam_angle_deg, islands, static_cast<int>(tile_method), static_cast<int>(projection_method), lscm_seam_angle_deg, islands,
static_cast<int>(blend_mode), midlevel, island_padding_mm, lscm_seam_edges, view_project_right, static_cast<int>(blend_mode), midlevel, island_padding_mm, lscm_seam_edges, view_project_right,
view_project_up, smoothing, edge_smoothing, edge_smoothing_amount, auto_connect_islands, island_groups, view_project_up, smoothing, edge_smoothing, edge_smoothing_amount, auto_connect_islands, island_groups,
lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled, chroma_enabled, chroma, lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled);
source);
} }
template<class Archive> void load(Archive &ar) template<class Archive> void load(Archive &ar)
{ {
@@ -388,14 +370,12 @@ struct TextureDisplacementLayer
int tile_method_int = 0; int tile_method_int = 0;
int projection_method_int = 0; int projection_method_int = 0;
int blend_mode_int = 0; int blend_mode_int = 0;
std::string source;
ar(slot, name, path, path_in_3mf, blob, depth_mm, tiling_scale, rotation_deg, offset, invert, tile_enabled, ar(slot, name, path, path_in_3mf, blob, depth_mm, tiling_scale, rotation_deg, offset, invert, tile_enabled,
tile_method_int, projection_method_int, lscm_seam_angle_deg, islands, blend_mode_int, midlevel, tile_method_int, projection_method_int, lscm_seam_angle_deg, islands, blend_mode_int, midlevel,
island_padding_mm, lscm_seam_edges, view_project_right, view_project_up, smoothing, edge_smoothing, island_padding_mm, lscm_seam_edges, view_project_right, view_project_up, smoothing, edge_smoothing,
edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective, edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective,
view_project_matrix, color_enabled, chroma_enabled, chroma, source); view_project_matrix, color_enabled);
image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end()); image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end());
chroma_source = source.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(source.begin(), source.end());
tile_method = static_cast<TextureTileMethod>(tile_method_int); tile_method = static_cast<TextureTileMethod>(tile_method_int);
projection_method = static_cast<TextureProjectionMethod>(projection_method_int); projection_method = static_cast<TextureProjectionMethod>(projection_method_int);
blend_mode = static_cast<TextureBlendMode>(blend_mode_int); blend_mode = static_cast<TextureBlendMode>(blend_mode_int);
+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 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); 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 // BBS: add the logic to process from an existed gcode file
if (m_print->finished()) { if (m_print->finished()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: skip slicing, to process previous gcode file") % __LINE__; 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"))); m_fff_print->set_status(80, _utf8(L("Processing G-code from previous file\u2026" /* \u2026 -> ... */)));
wxCommandEvent evt(m_event_slicing_completed_id); wxCommandEvent evt(m_event_slicing_completed_id);
// Post the Slicing Finished message for the G-code viewer to update. // Post the Slicing Finished message for the G-code viewer to update.
// Passing the timestamp // Passing the timestamp
+10 -8
View File
@@ -303,10 +303,12 @@ nlohmann::json color_picker_ui_strings()
return { return {
{"Color Picker", _u8L("Color Picker")}, {"Color Picker", _u8L("Color Picker")},
{"Basic Colors", _u8L("Basic Colors")}, {"Basic Colors", _u8L("Basic Colors")},
{"Orca Palette", _u8L("Orca Palette")},// TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor. // 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. {"Orca Palette", _u8L("Orca Palette")},
// TRN: Keep the standard/brand name "RAL" unchanged.
{"RAL Classic", _u8L("RAL Classic")},
{"Gradients", _u8L("Gradients")}, {"Gradients", _u8L("Gradients")},
{"Solid", _u8L("Solid")}, {"Solid", _u8L_CONTEXT("Solid", "Color picker")},
{"Gradient", _u8L("Gradient")}, {"Gradient", _u8L("Gradient")},
{"Gradient start", _u8L("Gradient start")}, {"Gradient start", _u8L("Gradient start")},
{"Gradient end", _u8L("Gradient end")}, {"Gradient end", _u8L("Gradient end")},
@@ -440,7 +442,7 @@ nlohmann::json color_picker_ui_strings()
{"Purple", _u8L("Purple")}, {"Purple", _u8L("Purple")},
{"Rebecca purple", _u8L("Rebecca purple")}, {"Rebecca purple", _u8L("Rebecca purple")},
{"Medium violet red", _u8L("Medium violet red")}, {"Medium violet red", _u8L("Medium violet red")},
{"PaleV violet red", _u8L("PaleV violet red")}, {"Pale violet red", _u8L("Pale violet red")},
{"Deep pink", _u8L("Deep pink")}, {"Deep pink", _u8L("Deep pink")},
{"Hot pink", _u8L("Hot pink")}, {"Hot pink", _u8L("Hot pink")},
{"Light pink", _u8L("Light pink")}, {"Light pink", _u8L("Light pink")},
@@ -496,7 +498,7 @@ nlohmann::json color_picker_ui_strings()
{"Turquoise bay", _u8L("Turquoise bay")}, {"Turquoise bay", _u8L("Turquoise bay")},
{"Sky over reef", _u8L("Sky over reef")}, {"Sky over reef", _u8L("Sky over reef")},
{"Shallow sky", _u8L("Shallow sky")}, {"Shallow sky", _u8L("Shallow sky")},
{"Electric bel", _u8L("Electric bel")}, {"Electric blue", _u8L("Electric blue")},
{"Dolphin", _u8L("Dolphin")}, {"Dolphin", _u8L("Dolphin")},
{"Ocean depths", _u8L("Ocean depths")}, {"Ocean depths", _u8L("Ocean depths")},
{"Whale", _u8L("Whale")}, {"Whale", _u8L("Whale")},
@@ -519,7 +521,7 @@ nlohmann::json color_picker_ui_strings()
{"Golden yellow", _u8L("Golden yellow")}, {"Golden yellow", _u8L("Golden yellow")},
{"Honey yellow", _u8L("Honey yellow")}, {"Honey yellow", _u8L("Honey yellow")},
{"Maize yellow", _u8L("Maize yellow")}, {"Maize yellow", _u8L("Maize yellow")},
{"Dalmatian yellow", _u8L("Dalmatian yellow")}, {"Daffodil yellow", _u8L("Daffodil yellow")},
{"Brown beige", _u8L("Brown beige")}, {"Brown beige", _u8L("Brown beige")},
{"Lemon yellow", _u8L("Lemon yellow")}, {"Lemon yellow", _u8L("Lemon yellow")},
{"Oyster white", _u8L("Oyster white")}, {"Oyster white", _u8L("Oyster white")},
@@ -606,7 +608,7 @@ nlohmann::json color_picker_ui_strings()
{"Pigeon blue", _u8L("Pigeon blue")}, {"Pigeon blue", _u8L("Pigeon blue")},
{"Traffic blue", _u8L("Traffic blue")}, {"Traffic blue", _u8L("Traffic blue")},
{"Turquoise blue", _u8L("Turquoise blue")}, {"Turquoise blue", _u8L("Turquoise blue")},
{"Capsanthin violet", _u8L("Capsanthin violet")}, {"Capri blue", _u8L("Capri blue")},
{"Ocean blue", _u8L("Ocean blue")}, {"Ocean blue", _u8L("Ocean blue")},
{"Water blue", _u8L("Water blue")}, {"Water blue", _u8L("Water blue")},
{"Night blue", _u8L("Night blue")}, {"Night blue", _u8L("Night blue")},
@@ -644,7 +646,7 @@ nlohmann::json color_picker_ui_strings()
{"Luminous green", _u8L("Luminous green")}, {"Luminous green", _u8L("Luminous green")},
{"Squirrel grey", _u8L("Squirrel grey")}, {"Squirrel grey", _u8L("Squirrel grey")},
{"Silver grey", _u8L("Silver grey")}, {"Silver grey", _u8L("Silver grey")},
{"Olivestone", _u8L("Olivestone")}, {"Olive grey", _u8L("Olive grey")},
{"Moss grey", _u8L("Moss grey")}, {"Moss grey", _u8L("Moss grey")},
{"Signal grey", _u8L("Signal grey")}, {"Signal grey", _u8L("Signal grey")},
{"Mouse grey", _u8L("Mouse grey")}, {"Mouse grey", _u8L("Mouse grey")},
+2
View File
@@ -1245,6 +1245,8 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
if(gcf_is_klipper) if(gcf_is_klipper)
toggle_field("accel_to_decel_factor", config->opt_bool("accel_to_decel_enable")); 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"); 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_angle", have_make_overhang_printable);
toggle_line("make_overhang_printable_hole_size", 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(); m_printer_name_to_preset.clear();
for (const Preset &printer_preset : printer_presets) { for (const Preset &printer_preset : printer_presets) {
if (!printer_preset.is_visible) continue; if (!printer_preset.is_visible) continue;
if (preset_bundle->printers.get_preset_base(printer_preset)->name != printer_preset.name) continue; if (preset_bundle->printers.get_preset_base(printer_preset) != &printer_preset) continue;
if (auto printer_model = dynamic_cast<ConfigOptionString*>(const_cast<Preset&>(printer_preset).config.option("printer_model", false))) { 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()) { 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)); 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; if (filament_preset.is_default || !filament_preset.is_compatible || filament_preset.is_project_embedded) continue;
ConfigOptionStrings *filament_types; ConfigOptionStrings *filament_types;
const Preset * filament_preset_base = m_preset_bundle->filaments.get_preset_base(filament_preset); const Preset * filament_preset_base = m_preset_bundle->filaments.get_preset_base(filament_preset);
if (filament_preset_base == &filament_preset) { if (filament_preset_base == &filament_preset || filament_preset_base == nullptr) {
filament_types = dynamic_cast<ConfigOptionStrings *>(const_cast<Preset *>(&filament_preset)->config.option("filament_type")); filament_types = dynamic_cast<ConfigOptionStrings *>(const_cast<Preset *>(&filament_preset)->config.option("filament_type"));
} else { } else {
filament_types = dynamic_cast<ConfigOptionStrings *>(const_cast<Preset *>(filament_preset_base)->config.option("filament_type")); filament_types = dynamic_cast<ConfigOptionStrings *>(const_cast<Preset *>(filament_preset_base)->config.option("filament_type"));
-2
View File
@@ -114,8 +114,6 @@ std::pair<bool, std::string> GLShadersManager::init()
valid &= append_shader("mm_gouraud", { prefix + "mm_gouraud.vs", prefix + "mm_gouraud.fs" }); valid &= append_shader("mm_gouraud", { prefix + "mm_gouraud.vs", prefix + "mm_gouraud.fs" });
// Fast shaded preview for the texture displacement gizmo (see libslic3r/TextureDisplacement.hpp). // Fast shaded preview for the texture displacement gizmo (see libslic3r/TextureDisplacement.hpp).
valid &= append_shader("texture_displacement_shaded", { prefix + "texture_displacement_shaded.vs", prefix + "texture_displacement_shaded.fs" }); valid &= append_shader("texture_displacement_shaded", { prefix + "texture_displacement_shaded.vs", prefix + "texture_displacement_shaded.fs" });
// ChromaStack: the printed colours on the model, read per fragment from its own height.
valid &= append_shader("chromastack", { prefix + "chromastack.vs", prefix + "chromastack.fs" });
// UV-check overlay for the same gizmo: a procedural checker or a distortion heatmap over the // UV-check overlay for the same gizmo: a procedural checker or a distortion heatmap over the
// painted patch, to sanity-check the unwrap. // painted patch, to sanity-check the unwrap.
valid &= append_shader("texture_displacement_uvcheck", { prefix + "texture_displacement_uvcheck.vs", prefix + "texture_displacement_uvcheck.fs" }); valid &= append_shader("texture_displacement_uvcheck", { prefix + "texture_displacement_uvcheck.vs", prefix + "texture_displacement_uvcheck.fs" });
-31
View File
@@ -2875,37 +2875,6 @@ void ObjectList::del_layer_from_object(const int obj_idx, const t_layer_height_r
changed_object(obj_idx); changed_object(obj_idx);
} }
void ObjectList::rebuild_layer_ranges(const int obj_idx)
{
if (obj_idx < 0 || obj_idx >= int(m_objects->size()))
return;
const wxDataViewItem obj_item = m_objects_model->GetItemById(obj_idx);
if (!obj_item.IsOk())
return;
const t_layer_config_ranges& ranges = object(obj_idx)->layer_config_ranges;
// Adding and deleting tree items fires selection events on GTK; suppress them as every other
// bulk edit of the tree here does.
const bool prevent_events = m_prevent_list_events;
m_prevent_list_events = true;
wxDataViewItem root = m_objects_model->GetLayerRootItem(obj_item);
if (root.IsOk()) {
m_objects_model->DeleteChildren(root);
if (ranges.empty())
m_objects_model->Delete(root);
else {
for (const auto& range : ranges)
add_layer_item(range.first, root);
Expand(root);
}
} else {
add_layer_root_item(obj_item);
}
m_prevent_list_events = prevent_events;
changed_object(obj_idx);
}
void ObjectList::del_layers_from_object(const int obj_idx) void ObjectList::del_layers_from_object(const int obj_idx)
{ {
object(obj_idx)->layer_config_ranges.clear(); object(obj_idx)->layer_config_ranges.clear();
-5
View File
@@ -321,11 +321,6 @@ public:
void del_settings_from_config(const wxDataViewItem& parent_item); void del_settings_from_config(const wxDataViewItem& parent_item);
void del_instances_from_object(const int obj_idx); void del_instances_from_object(const int obj_idx);
void del_layer_from_object(const int obj_idx, const t_layer_height_range& layer_range); void del_layer_from_object(const int obj_idx, const t_layer_height_range& layer_range);
// Mirrors an object's height ranges in the tree after they have been rewritten from outside the
// list - the texture displacement gizmo writes a whole set at once for a ChromaStack stack's
// filament swaps. Deliberately leaves the selection alone: changing it while a gizmo is open
// tears the gizmo down under the user.
void rebuild_layer_ranges(const int obj_idx);
void del_layers_from_object(const int obj_idx); void del_layers_from_object(const int obj_idx);
bool del_from_cut_object(bool is_connector, bool is_model_part = false, bool is_negative_volume = false); bool del_from_cut_object(bool is_connector, bool is_model_part = false, bool is_negative_volume = false);
bool del_subobject_from_object(const int obj_idx, const int idx, const int type); bool del_subobject_from_object(const int obj_idx, const int idx, const int type);
+3 -3
View File
@@ -1350,7 +1350,7 @@ void GLGizmoSVG::draw_window()
assert(m_volume_id.valid()); assert(m_volume_id.valid());
if (m_volume == nullptr || if (m_volume == nullptr ||
m_volume_id.invalid()) { 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; return;
} }
@@ -1362,13 +1362,13 @@ void GLGizmoSVG::draw_window()
assert(m_volume->emboss_shape->svg_file.has_value()); assert(m_volume->emboss_shape->svg_file.has_value());
if (!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; return;
} }
assert(m_volume->emboss_shape->svg_file->file_data != nullptr); assert(m_volume->emboss_shape->svg_file->file_data != nullptr);
if (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; return;
} }
File diff suppressed because it is too large. Load diff
@@ -264,11 +264,7 @@ private:
ModelVolume* texture_volume(); ModelVolume* texture_volume();
const ModelVolume* texture_volume() const; const ModelVolume* texture_volume() const;
// `chroma` picks the kind: a relief layer, a height map pushed into the surface, or a ChromaStack void add_texture_layer();
// picture, a colour picture made of filament swaps. The kind is a property of the layer and is
// fixed when it is created - there is no panel-wide mode, so a plaque can carry a picture on its
// face and a knurl on its rim at the same time.
void add_texture_layer(bool chroma = false);
void remove_texture_layer(int slot); void remove_texture_layer(int slot);
// Moves the layer in `slot` to position `to_index` of the slot-ordered stack (an insertion index, // Moves the layer in `slot` to position `to_index` of the slot-ordered stack (an insertion index,
// 0..layer count), which is the order blend modes apply in. Layers keep their data; what moves is // 0..layer count), which is the order blend modes apply in. Layers keep their data; what moves is
@@ -439,109 +435,6 @@ private:
void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry); void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry);
void import_custom_texture(TextureDisplacementLayer &layer); void import_custom_texture(TextureDisplacementLayer &layer);
// ---- ChromaStack (see libslic3r/ChromaStack.hpp) ----
//
// A ChromaStack layer is an ordinary texture displacement layer whose height map is computed rather
// than imported: the picture the user picked is kept in `chroma_source`, the stack in
// `chroma`, and `image_data` holds whatever the last solve produced. Everything downstream -
// projection, preview, bake - therefore needs no changes at all, and this is the whole of the
// feature's GUI side: pick a picture, edit the stack, solve, hand the filament swaps to the
// object's height ranges.
// The project's loaded filaments as the colour model sees them: colour from the project, TD from
// each filament's preset. Index is the filament index the swaps are written from.
static std::vector<ChromaFilament> chroma_filaments();
// The layer height the print profile is set to, mm; 0 when there is no print preset. The solve is
// only valid for the layer height it was run at, so the panel compares it against the stack's.
static float print_layer_height();
// Where the active layer's painted patch sits, in the object's own printed coordinates - which is
// what the stack has to be lined up with, because a filament swap happens at a print Z and the
// relief grows upward from this patch.
struct ChromaPatch
{
bool valid = false;
float top_mm = 0.f; // the patch's surface above the bottom of the object
float spread_mm = 0.f; // how far from flat the patch is in Z; 0 is a horizontal face
// The patch's extent in the plane the texture is projected from, in the bake frame - i.e. the
// (x, y) the Z-facing plane of the tri-planar projection samples by. This is what the picture
// has to be placed against: tiling_scale and offset are in that space, not in the patch's.
Vec2f planar_min = Vec2f::Zero();
Vec2f planar_max = Vec2f::Zero();
};
ChromaPatch chroma_patch(const TextureDisplacementLayer &layer) const;
// The painted face's height in layers, which is what the stack's base has to be: -1 when nothing
// is painted yet or there is no layer height to divide by.
int chroma_measured_base(const TextureDisplacementLayer &layer) const;
// Extracting the painted patch is far too expensive to repeat once a frame, which is how often the
// panel wants the number. The paint's own timestamp and the transform the patch was measured in are
// between them everything the answer depends on.
mutable std::array<ChromaPatch, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_patch_cache{};
mutable std::array<uint64_t, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_patch_stamp{};
mutable std::array<Transform3d, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_patch_trafo{};
// The patch is measured in two frames at once - the instance's, for the print height, and the
// bake's, for the placement - so both are part of what the cached answer depends on.
mutable std::array<Transform3d, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_patch_bake{};
// Picks a colour picture for the layer, turns ChromaStack on, and seeds a stack from the loaded
// filaments and the patch if the layer has none yet. Solves straight away, so the relief appears
// without a second click.
void chroma_pick_picture(TextureDisplacementLayer &layer);
// Re-runs the solve and replaces the layer's height map with its result. False (with the reason
// shown to the user) when the layer has no picture, no stack or no relief layers.
bool chroma_solve_layer(TextureDisplacementLayer &layer);
// Writes the stack's filament swaps onto the object as height ranges, replacing whatever ranges it
// had. Takes its own undo snapshot.
void chroma_apply_swaps(const TextureDisplacementLayer &layer);
// The stack's swaps as the object's height ranges already express them: true when applying would
// change nothing, which is what the panel's hint keys off.
bool chroma_swaps_applied(const TextureDisplacementLayer &layer) const;
// The whole of a ChromaStack card between its header and More settings: the picture and the print
// it predicts, the base, the filaments and the colours they reach, and the two actions.
void render_chroma_card(TextureDisplacementLayer &layer, float card_pad, float label_w, float wrap_w);
// The surface drawn in the colours the stack will print, over whatever neutral surface is there:
// the live preview's displaced mesh before a bake, the volume's own mesh after one. Every triangle
// takes the palette colour of the printed layer its centroid falls in (chroma_level_at()), which
// is what the eye will see at that height - so the relief's terraces read as the picture, the walls
// between them show the bands they cut through, and looking straight down gives the blended colours
// the whole feature exists to produce.
//
// Nothing else in the application can show this: the colours are blends of translucent filaments
// that no single filament can make, so they cannot be expressed as the per-triangle filament
// assignment the 3D scene paints with. They exist only here and in the sliced preview.
void rebuild_chroma_colors(const TextureDisplacementLayer &layer);
bool render_chroma_colors(const TextureDisplacementLayer &layer);
GLModel m_chroma_color_model;
// The surface the overlay was built from. The mesh alone: the colours are not in it, so a change
// to the stack costs a palette upload rather than a mesh rebuild.
size_t m_chroma_color_key = 0;
// The stack's colours, one texel per printed layer, and what they were built from. A texture
// because a uniform array cannot hold a base of several hundred layers.
std::unique_ptr<GLTexture> m_chroma_palette_tex;
size_t m_chroma_palette_key = 0;
bool m_show_chroma_colors = true;
// What the last solve of each slot reported, for the panel. Not saved: it is a property of the
// solve, and a project is reopened with whatever height map the solve produced.
struct ChromaReport
{
bool valid = false;
double mean = 0.;
double max = 0.;
};
std::array<ChromaReport, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_report{};
// The predicted print, per slot: the palette colours the solve chose, as a panel thumbnail. The
// 3D view cannot show this - the colours do not exist until the print swaps filament - so it is
// the only place the user sees what the stack will actually produce.
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_preview;
// The source picture's thumbnail, beside the prediction, keyed by the image it was built from.
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_source_thumb;
std::array<const void *, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_source_key{};
// Per slot: the stack has been edited since the height map was solved, so the relief on screen is the
// previous stack's. Nothing else can show this - a stale height map looks like a perfectly good one.
std::array<bool, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_stale{};
// Paint / Erase, from the panel. See get_left_button_state_type(). // Paint / Erase, from the panel. See get_left_button_state_type().
bool m_erase_mode = false; bool m_erase_mode = false;
// Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation. // Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation.
+22
View File
@@ -62,6 +62,7 @@
#include "Search.hpp" #include "Search.hpp"
#include "BitmapCache.hpp" #include "BitmapCache.hpp"
#include "GUI_App.hpp" #include "GUI_App.hpp"
#include "KeyChord.hpp"
#include <nanosvg/nanosvg.h> #include <nanosvg/nanosvg.h>
#include <nanosvg/nanosvgrast.h> #include <nanosvg/nanosvgrast.h>
@@ -2134,6 +2135,27 @@ bool ImGuiWrapper::want_any_input() const
return io.WantCaptureMouse || io.WantCaptureKeyboard || io.WantTextInput; 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) ImFontAtlasCustomRect* ImGuiWrapper::GetTextureCustomRect(const wchar_t& tex_id)
{ {
auto item = m_custom_glyph_rects_ids.find(tex_id); auto item = m_custom_glyph_rects_ids.find(tex_id);
+7
View File
@@ -35,6 +35,7 @@ struct ImRect;
namespace Slic3r { namespace Slic3r {
namespace GUI { namespace GUI {
struct KeyChord;
bool get_data_from_svg(const std::string &filename, unsigned int max_size_px, ThumbnailData &thumbnail_data); bool get_data_from_svg(const std::string &filename, unsigned int max_size_px, ThumbnailData &thumbnail_data);
@@ -219,6 +220,12 @@ public:
bool want_keyboard() const; bool want_keyboard() const;
bool want_text_input() const; bool want_text_input() const;
bool want_any_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 // Optional inputs are used for set up value inside of an optional, with default value
// //
+4 -5
View File
@@ -173,7 +173,7 @@ void KBShortcutsDialog::fill_pages()
using Section = ShortcutSection; using Section = ShortcutSection;
if (wxGetApp().is_editor()) { if (wxGetApp().is_editor()) {
page(_L("Global"), _L("Available anywhere in the window, even while typing in a text field."), ShortcutContext::Global, { page(_L("Global"), _L("Available anywhere in the window."), ShortcutContext::Global, {
fixed(Section::SpeedDial, { alt, "1-9, 0" }, L("Run favorite 1 to 10")), 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")), 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. // wx cycles notebook pages on Ctrl+Tab, which is Cmd+Tab on macOS and never arrives there.
@@ -438,17 +438,16 @@ ShortcutCaptureDialog::ShortcutCaptureDialog(wxWindow* parent, Shortcut shortcut
SetBackgroundColour(*wxWHITE); SetBackgroundColour(*wxWHITE);
wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL);
// A Global shortcut also runs while a text field has the focus, so its hint names the keys it can use. // A Global shortcut runs before the focused control gets the key, so its hint names the keys it can use.
const bool global = (shortcut_info(shortcut).contexts & context_bit(ShortcutContext::Global)) != 0; const bool global = (shortcut_info(shortcut).contexts & context_bit(ShortcutContext::Global)) != 0;
const wxString advice = global_key_advice(); 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."); const wxString rule = _L("A key that types a character cannot be a global shortcut.");
m_hint = global ? typing + "\n" + advice : _L("Esc cancels, Enter confirms."); m_hint = global ? advice : _L("Esc cancels, Enter confirms.");
m_rejection = rule + "\n" + advice; m_rejection = rule + "\n" + advice;
// Wide enough for each sentence on a line of its own where the translation allows, within limits. // Wide enough for each sentence on a line of its own where the translation allows, within limits.
int width = FromDIP(450); int width = FromDIP(450);
for (const wxString& sentence : { typing, rule, advice }) { for (const wxString& sentence : { rule, advice }) {
int extent = 0; int extent = 0;
GetTextExtent(sentence, &extent, nullptr, nullptr, nullptr, &wxGetApp().normal_font()); GetTextExtent(sentence, &extent, nullptr, nullptr, nullptr, &wxGetApp().normal_font());
width = std::max(width, extent); width = std::max(width, extent);
+34
View File
@@ -187,6 +187,40 @@ bool KeyChord::is_menu_accelerator() const
return valid() && ((modifiers & (wxMOD_CONTROL | wxMOD_ALT | wxMOD_RAW_CONTROL)) != 0 || (!is_printable(key) && key != WXK_SPACE)); return valid() && ((modifiers & (wxMOD_CONTROL | wxMOD_ALT | wxMOD_RAW_CONTROL)) != 0 || (!is_printable(key) && key != WXK_SPACE));
} }
bool KeyChord::edits_text() const
{
if (!valid())
return false;
if (!is_menu_accelerator())
return true;
const int mods = modifiers & ~wxMOD_SHIFT;
switch (key) {
case WXK_LEFT: case WXK_RIGHT: case WXK_UP: case WXK_DOWN:
case WXK_HOME: case WXK_END: case WXK_BACK: case WXK_DELETE:
#ifdef __APPLE__
return mods == wxMOD_NONE || mods == wxMOD_CONTROL || mods == wxMOD_ALT; // Option moves by word
#else
return mods == wxMOD_NONE || mods == wxMOD_CONTROL;
#endif
case WXK_INSERT:
return mods == wxMOD_NONE || mods == wxMOD_CONTROL;
case WXK_RETURN: case WXK_TAB: case WXK_ESCAPE: case WXK_PAGEUP: case WXK_PAGEDOWN:
return mods == wxMOD_NONE;
default:
break;
}
if (!is_printable(key))
return false;
#ifdef __APPLE__
if (mods == wxMOD_ALT || mods == wxMOD_RAW_CONTROL)
return true; // Option types a character, Control runs the Cocoa text key bindings
#else
if (mods == (wxMOD_CONTROL | wxMOD_ALT))
return true; // AltGr types a character
#endif
return mods == wxMOD_CONTROL && (key == 'A' || key == 'C' || key == 'V' || key == 'X' || key == 'Y' || key == 'Z');
}
// Only a chord the desktop acts on while still delivering it to the app belongs here. // Only a chord the desktop acts on while still delivering it to the app belongs here.
bool KeyChord::is_system_shortcut() const bool KeyChord::is_system_shortcut() const
{ {
+3
View File
@@ -35,6 +35,9 @@ struct KeyChord
// True when Ctrl or Alt is held or the key is non-printable, the chords a menu can own without // True when Ctrl or Alt is held or the key is non-printable, the chords a menu can own without
// swallowing typing in text fields. // swallowing typing in text fields.
bool is_menu_accelerator() const; bool is_menu_accelerator() const;
// True for a chord a focused text field acts on: typing a character (AltGr and macOS Option
// characters included), moving the caret, selecting, deleting, or the clipboard and undo chords.
bool edits_text() const;
// True for a chord the desktop acts on although the app receives it, so a binding would // True for a chord the desktop acts on although the app receives it, so a binding would
// take it from the system. // take it from the system.
bool is_system_shortcut() const; bool is_system_shortcut() const;
Loaded 100 of 132 files, more files were not shown because too many files have changed in this diff. Show more